diff --git a/app.go b/app.go
index d416002..a373c01 100644
--- a/app.go
+++ b/app.go
@@ -69,6 +69,7 @@ func (s *ImageServer) ServeHTTP(w http.ResponseWriter, r *http.Request) {
type ProjectInfoJS struct {
ID string `json:"id"`
Name string `json:"name"`
+ Type string `json:"type"`
Path string `json:"path"`
CreatedAt string `json:"createdAt"`
HasStencil bool `json:"hasStencil"`
@@ -76,12 +77,25 @@ type ProjectInfoJS struct {
HasVectorWrap bool `json:"hasVectorWrap"`
HasStructural bool `json:"hasStructural"`
HasScanHelper bool `json:"hasScanHelper"`
+ HasObject bool `json:"hasObject"`
BoardW float64 `json:"boardW"`
BoardH float64 `json:"boardH"`
ShowGrid bool `json:"showGrid"`
TraditionalControls bool `json:"traditionalControls"`
}
+type EnclosureSetupJS struct {
+ GbrjobFile string `json:"gbrjobFile"`
+ GerberFiles []string `json:"gerberFiles"`
+ DrillPath string `json:"drillPath"`
+ NPTHPath string `json:"npthPath"`
+ WallThick float64 `json:"wallThick"`
+ WallHeight float64 `json:"wallHeight"`
+ Clearance float64 `json:"clearance"`
+ DPI float64 `json:"dpi"`
+ SourceDir string `json:"sourceDir"`
+}
+
type SessionInfoJS struct {
ProjectName string `json:"projectName"`
BoardW float64 `json:"boardW"`
@@ -184,9 +198,14 @@ func (a *App) GetRecentProjects() []ProjectInfoJS {
if err != nil {
continue
}
+ pType := proj.Type
+ if pType == "" {
+ pType = "pcb"
+ }
info := ProjectInfoJS{
ID: proj.ID,
Name: proj.Name,
+ Type: pType,
Path: e.Path,
CreatedAt: e.LastOpened.Format(time.RFC3339),
ShowGrid: proj.Settings.ShowGrid,
@@ -209,6 +228,9 @@ func (a *App) GetRecentProjects() []ProjectInfoJS {
if proj.ScanHelper != nil {
info.HasScanHelper = true
}
+ if proj.Object != nil {
+ info.HasObject = true
+ }
result = append(result, info)
}
return result
@@ -747,10 +769,13 @@ func (a *App) GenerateEnclosureOutputs() (*GenerateResultJS, error) {
// ======== Project Lifecycle ========
-func (a *App) CreateNewProject(name string) (string, error) {
+func (a *App) CreateNewProject(name, projectType string) (string, error) {
if name == "" {
name = "Untitled"
}
+ if projectType == "" {
+ projectType = "pcb"
+ }
safeName := sanitizeDirName(name)
if safeName == "" {
safeName = "untitled"
@@ -767,6 +792,7 @@ func (a *App) CreateNewProject(name string) (string, error) {
return "", err
}
proj.Name = name
+ proj.Type = projectType
SaveProject(path, proj)
a.mu.Lock()
@@ -878,9 +904,14 @@ func (a *App) GetProjectInfo() *ProjectInfoJS {
return nil
}
p := a.project
+ projType := p.Type
+ if projType == "" {
+ projType = "pcb"
+ }
info := &ProjectInfoJS{
ID: p.ID,
Name: p.Name,
+ Type: projType,
Path: a.projectPath,
CreatedAt: p.CreatedAt.Format(time.RFC3339),
ShowGrid: p.Settings.ShowGrid,
@@ -903,6 +934,9 @@ func (a *App) GetProjectInfo() *ProjectInfoJS {
if p.ScanHelper != nil {
info.HasScanHelper = true
}
+ if p.Object != nil {
+ info.HasObject = true
+ }
return info
}
@@ -1251,6 +1285,77 @@ func (a *App) GetActiveMode() string {
return ""
}
+// ======== Enclosure Reconfigure ========
+
+func (a *App) GetEnclosureSetupData() *EnclosureSetupJS {
+ a.mu.RLock()
+ defer a.mu.RUnlock()
+
+ if a.project == nil || a.project.Enclosure == nil {
+ return nil
+ }
+ enc := a.project.Enclosure
+ var gerberFiles []string
+ for name := range enc.GerberFiles {
+ gerberFiles = append(gerberFiles, name)
+ }
+ // Reconstruct gbrjob file from gerber files map
+ var gbrjob string
+ for name := range enc.GerberFiles {
+ if strings.HasSuffix(strings.ToLower(name), ".gbrjob") {
+ gbrjob = name
+ break
+ }
+ }
+ return &EnclosureSetupJS{
+ GbrjobFile: gbrjob,
+ GerberFiles: gerberFiles,
+ DrillPath: enc.DrillPath,
+ NPTHPath: enc.NPTHPath,
+ WallThick: enc.Config.WallThickness,
+ WallHeight: enc.Config.WallHeight,
+ Clearance: enc.Config.Clearance,
+ DPI: enc.Config.DPI,
+ }
+}
+
+// ======== 3D Object Setup (Surface Wrap projects) ========
+
+func (a *App) ConfigureProjectObject(modelPath string) error {
+ if modelPath == "" {
+ return fmt.Errorf("no model file selected")
+ }
+ a.mu.Lock()
+ if a.project == nil {
+ a.mu.Unlock()
+ return fmt.Errorf("no project open")
+ }
+ projPath := a.projectPath
+ a.mu.Unlock()
+
+ baseName := filepath.Base(modelPath)
+ dstDir := filepath.Join(projPath, "vectorwrap")
+ os.MkdirAll(dstDir, 0755)
+ dstPath := filepath.Join(dstDir, baseName)
+ if err := CopyFile(modelPath, dstPath); err != nil {
+ return fmt.Errorf("copy model: %v", err)
+ }
+
+ modelType := "stl"
+ if strings.HasSuffix(strings.ToLower(modelPath), ".scad") {
+ modelType = "scad"
+ }
+
+ a.mu.Lock()
+ a.project.Object = &ObjectData{
+ ModelFile: baseName,
+ ModelType: modelType,
+ }
+ a.mu.Unlock()
+ a.autosaveProject()
+ return nil
+}
+
// ======== Vector Wrap ========
type SVGValidationResultJS struct {
@@ -1623,16 +1728,27 @@ func (a *App) GetUnwrapLayout() (*UnwrapLayout, error) {
return layout, nil
}
-func (a *App) ImportUnwrapArtwork(svgContent string) error {
- a.mu.Lock()
- defer a.mu.Unlock()
-
- if a.vectorWrapSession == nil {
- a.vectorWrapSession = &VectorWrapSession{}
+func (a *App) SaveUnwrapSVG() (string, error) {
+ svg, err := a.GetUnwrapSVG()
+ if err != nil {
+ return "", err
}
- // Store the artwork SVG for use with surface wrapping
- a.vectorWrapSession.EnclosureSCAD = svgContent
- return nil
+
+ outDir, err := a.GetOutputDir()
+ if err != nil {
+ return "", err
+ }
+ os.MkdirAll(outDir, 0755)
+
+ outPath := filepath.Join(outDir, "enclosure-unwrap.svg")
+ if err := os.WriteFile(outPath, []byte(svg), 0644); err != nil {
+ return "", fmt.Errorf("write SVG: %v", err)
+ }
+ return outPath, nil
+}
+
+func (a *App) OpenFileExternal(path string) error {
+ return exec.Command("open", path).Start()
}
// ======== Structural Procedures ========
diff --git a/frontend/index.html b/frontend/index.html
index 9c538ac..2e8c6c4 100644
--- a/frontend/index.html
+++ b/frontend/index.html
@@ -48,6 +48,7 @@
+
diff --git a/frontend/src/main.js b/frontend/src/main.js
index 549f668..2ecfa45 100644
--- a/frontend/src/main.js
+++ b/frontend/src/main.js
@@ -96,6 +96,7 @@ function navigate(page) {
case 'structural': initStructural(); break;
case 'scanhelper': initScanHelper(); break;
case 'unwrap': initUnwrap(); break;
+ case 'objectsetup': initObjectSetup(); break;
}
}
@@ -112,16 +113,19 @@ async function initLanding() {
Recent Projects
${projects.map(p => {
+ const typeLabel = p.type === 'surfacewrap' ? 'Surface Wrap' : 'PCB';
const modes = [];
if (p.hasEnclosure) modes.push('Enclosure');
if (p.hasStencil) modes.push('Stencil');
if (p.hasVectorWrap) modes.push('Vector Wrap');
if (p.hasStructural) modes.push('Structural');
if (p.hasScanHelper) modes.push('Scan Helper');
+ if (p.hasObject) modes.push('3D Object');
const modeStr = modes.length > 0 ? modes.join(', ') : 'No modes configured';
return `
${esc(p.name)}
+ ${typeLabel}
${esc(modeStr)}
`;
}).join('')}
@@ -158,30 +162,57 @@ async function initDashboard() {
return;
}
- const modeCards = [
- { id: 'stencil', label: 'Stencil', desc: 'Solder paste stencils from gerber files', active: info.hasStencil },
- { id: 'enclosure', label: 'Enclosure', desc: 'PCB enclosures from KiCad projects', active: info.hasEnclosure },
- { id: 'vectorwrap', label: 'Vector Wrap', desc: 'Wrap 2D vector art onto 3D surfaces', active: info.hasVectorWrap },
- { id: 'structural', label: 'Structural Fill', desc: 'Pattern-infilled 3D forms from outlines', active: info.hasStructural },
- { id: 'scanhelper', label: 'Scan Helper', desc: 'Calibration grids for scanning objects', active: info.hasScanHelper },
- { id: 'unwrap', label: 'Unwrap', desc: 'Flat SVG template of the unfolded enclosure surface', active: info.hasEnclosure },
- ];
+ const isSW = info.type === 'surfacewrap';
+ const hasObj = info.hasObject;
+
+ let modeCards;
+ if (isSW) {
+ modeCards = [
+ { id: 'objectsetup', label: '3D Object', desc: 'Provide STL/SCAD target mesh', active: hasObj, required: !hasObj },
+ { id: 'vectorwrap', label: 'Vector Wrap', desc: 'Wrap 2D vector art onto 3D surfaces', active: info.hasVectorWrap, disabled: !hasObj },
+ { id: 'unwrap', label: 'Unwrap', desc: 'Flat SVG template of the unfolded surface', active: false, disabled: !hasObj },
+ { id: 'structural', label: 'Structural Fill', desc: 'Pattern-infilled 3D forms from outlines', active: info.hasStructural },
+ { id: 'scanhelper', label: 'Scan Helper', desc: 'Calibration grids for scanning objects', active: info.hasScanHelper },
+ ];
+ } else {
+ modeCards = [
+ { id: 'stencil', label: 'Stencil', desc: 'Solder paste stencils from gerber files', active: info.hasStencil },
+ { id: 'enclosure', label: 'Enclosure', desc: 'PCB enclosures from KiCad projects', active: info.hasEnclosure },
+ { id: 'vectorwrap', label: 'Vector Wrap', desc: 'Wrap 2D vector art onto 3D surfaces', active: info.hasVectorWrap },
+ { id: 'structural', label: 'Structural Fill', desc: 'Pattern-infilled 3D forms from outlines', active: info.hasStructural },
+ { id: 'scanhelper', label: 'Scan Helper', desc: 'Calibration grids for scanning objects', active: info.hasScanHelper },
+ { id: 'unwrap', label: 'Unwrap', desc: 'Flat SVG template of the unfolded enclosure surface', active: info.hasEnclosure },
+ ];
+ }
page.innerHTML = `
- ${modeCards.map(m => `
+ ${modeCards.map(m => {
+ if (m.disabled) {
+ return `
+
+
${m.label}
+
${m.desc}
+
Requires 3D Object
+
`;
+ }
+ let badge = '';
+ if (m.required) badge = '
Required';
+ else if (m.active) badge = '
Configured';
+ return `
${m.label}
${m.desc}
- ${m.active ? '
Configured' : ''}
-
- `).join('')}
+ ${badge}
+
`;
+ }).join('')}
`;
}
@@ -190,12 +221,14 @@ async function initDashboard() {
async function createNewProject() {
const name = await showInputDialog('New Project', 'Project name:', 'My Project');
if (name === null) return;
+ const projectType = await showTypeDialog();
+ if (projectType === null) return;
try {
- const path = await wails()?.CreateNewProject(name || 'Untitled');
+ const path = await wails()?.CreateNewProject(name || 'Untitled', projectType);
if (!path) return;
const info = await wails()?.GetProjectInfo();
if (info) {
- state.project = { name: info.name, path: info.path };
+ state.project = { name: info.name, path: info.path, type: info.type };
navigate('dashboard');
}
} catch (e) {
@@ -203,13 +236,46 @@ async function createNewProject() {
}
}
+function showTypeDialog() {
+ return new Promise(resolve => {
+ const overlay = document.createElement('div');
+ overlay.style.cssText = 'position:fixed;inset:0;background:rgba(0,0,0,0.6);z-index:10000;display:flex;align-items:center;justify-content:center;';
+ const box = document.createElement('div');
+ box.style.cssText = 'background:var(--bg-surface);border:1px solid var(--border);border-radius:var(--radius);padding:24px;min-width:400px;text-align:center;box-shadow:0 8px 24px rgba(0,0,0,0.4);';
+ box.innerHTML = `
+ Project Type
+
+
+
PCB / KiCad
+
Enclosures and stencils from gerber files
+
+
+
Surface Wrap
+
Wrap 2D art onto 3D objects
+
+
+
+
+
+ `;
+ overlay.appendChild(box);
+ document.body.appendChild(overlay);
+ const done = (val) => { overlay.remove(); resolve(val); };
+ box.querySelectorAll('.type-option').forEach(card => {
+ card.onclick = () => done(card.dataset.type);
+ });
+ box.querySelector('#type-cancel').onclick = () => done(null);
+ overlay.onclick = e => { if (e.target === overlay) done(null); };
+ });
+}
+
async function openProjectDialog() {
try {
const path = await wails()?.OpenProjectDialog();
if (!path) return;
const info = await wails()?.GetProjectInfo();
if (info) {
- state.project = { name: info.name, path: info.path };
+ state.project = { name: info.name, path: info.path, type: info.type };
navigate('dashboard');
}
} catch (e) {
@@ -367,19 +433,51 @@ function showStencilResult(files) {
}
// ===== Enclosure Page =====
-function initEnclosure() {
+async function initEnclosure(prefill) {
const page = $('#page-enclosure');
+
+ // If session exists and no explicit reconfigure request, go to preview
+ if (!prefill) {
+ const sessionCheck = await wails()?.GetSessionInfo();
+ if (sessionCheck?.hasSession) {
+ navigate('preview');
+ return;
+ }
+ }
+
state.enclosure = { gbrjobPath: '', gerberPaths: [], drillPath: '', npthPath: '', sourceDir: '' };
+ // Pre-fill from existing config when reconfiguring
+ if (prefill) {
+ state.enclosure.gbrjobPath = prefill.gbrjobFile || '';
+ state.enclosure.gerberPaths = prefill.gerberFiles || [];
+ state.enclosure.drillPath = prefill.drillPath || '';
+ state.enclosure.npthPath = prefill.npthPath || '';
+ state.enclosure.sourceDir = prefill.sourceDir || '';
+ }
+
+ const gbrjobLabel = state.enclosure.gbrjobPath ? state.enclosure.gbrjobPath.split('/').pop() : 'No file selected';
+ const gbrjobClass = state.enclosure.gbrjobPath ? ' has-file' : '';
+ const gerberLabel = state.enclosure.gerberPaths.length > 0 ? `${state.enclosure.gerberPaths.length} files` : 'No files selected';
+ const gerberClass = state.enclosure.gerberPaths.length > 0 ? ' has-file' : '';
+ const drillLabel = state.enclosure.drillPath ? state.enclosure.drillPath.split('/').pop() : 'No file selected';
+ const drillClass = state.enclosure.drillPath ? ' has-file' : '';
+ const npthLabel = state.enclosure.npthPath ? state.enclosure.npthPath.split('/').pop() : 'No file selected';
+ const npthClass = state.enclosure.npthPath ? ' has-file' : '';
+ const wt = prefill?.wallThick || 1.5;
+ const wh = prefill?.wallHeight || 10.0;
+ const cl = prefill?.clearance || 0.3;
+ const dp = prefill?.dpi || 600;
+
page.innerHTML = `
Gerber Job File (.gbrjob)
- No file selected
+ ${esc(gbrjobLabel)}
@@ -387,7 +485,7 @@ function initEnclosure() {
Gerber Files
Select a folder to auto-discover, or add files individually.
- No files selected
+ ${esc(gerberLabel)}
@@ -399,7 +497,7 @@ function initEnclosure() {
PTH Drill File (Optional)
- No file selected
+ ${esc(drillLabel)}
@@ -407,7 +505,7 @@ function initEnclosure() {
@@ -577,6 +675,27 @@ async function buildEnclosure() {
}
}
+async function reconfigureEnclosure() {
+ const setup = await wails()?.GetEnclosureSetupData();
+ if (!setup) {
+ navigate('enclosure');
+ return;
+ }
+ // Navigate to enclosure page with pre-fill data — bypasses the session check
+ $$('.page').forEach(p => hide(p));
+ const el = $('#page-enclosure');
+ if (el) show(el);
+ const modeTabs = $('#nav-mode-tabs');
+ const projectName = $('#nav-project-name');
+ const openOutput = $('#nav-open-output');
+ if (state.project) {
+ if (modeTabs) modeTabs.style.display = '';
+ if (projectName) { projectName.style.display = ''; projectName.textContent = state.project.name; }
+ if (openOutput) openOutput.style.display = '';
+ }
+ initEnclosure(setup);
+}
+
// ===== Enclosure Preview Page =====
async function initPreview() {
const page = $('#page-preview');
@@ -593,8 +712,10 @@ async function initPreview() {
page.innerHTML = `
Board Preview — ${info.boardW.toFixed(1)} × ${info.boardH.toFixed(1)} mm | ${info.sides?.length || 0} sides
@@ -780,7 +901,7 @@ async function openProject(path) {
hideLoading();
const info = await wails()?.GetProjectInfo();
if (info) {
- state.project = { name: info.name, path: info.path };
+ state.project = { name: info.name, path: info.path, type: info.type };
navigate('dashboard');
}
} catch (e) {
@@ -793,6 +914,28 @@ async function openProject(path) {
async function initVectorWrap() {
const page = $('#page-vectorwrap');
state.vectorwrap = { svgPath: '', modelPath: '', modelType: '' };
+ const isSW = state.project?.type === 'surfacewrap';
+
+ let modelCardHTML;
+ if (isSW) {
+ // Surface Wrap: model comes from 3D Object setup, just show its name
+ const info = await wails()?.GetProjectInfo();
+ const objName = info?.hasObject ? 'Configured via 3D Object' : 'Not configured';
+ modelCardHTML = `
+
+
3D Model
+
${esc(objName)}
+
`;
+ } else {
+ // PCB: use project enclosure directly, no external model option
+ modelCardHTML = `
+
+
3D Model
+
+
Checking project enclosure...
+
+
`;
+ }
page.innerHTML = `
-
-
3D Model
-
-
-
Select an STL or OpenSCAD file for the target surface.
-
- No file selected
-
-
-
-
+ ${modelCardHTML}
`;
- try {
- const vwCheck = await wails()?.GetVectorWrapInfo();
- if (vwCheck?.hasProjectEnclosure) {
- const label = $('#vw-use-project-label');
- if (label) label.style.display = 'block';
+ // PCB mode: auto-load project enclosure
+ if (!isSW) {
+ try {
+ const vwCheck = await wails()?.GetVectorWrapInfo();
+ const statusEl = $('#vw-enc-status');
+ if (vwCheck?.hasProjectEnclosure) {
+ showLoading('Loading project enclosure...');
+ await wails().UseProjectEnclosureForVectorWrap();
+ hideLoading();
+ state.vectorwrap.modelPath = 'project-enclosure';
+ state.vectorwrap.modelType = 'project-enclosure';
+ if (statusEl) {
+ statusEl.textContent = 'Using project enclosure';
+ statusEl.style.color = 'var(--success)';
+ }
+ } else if (statusEl) {
+ statusEl.textContent = 'No enclosure configured — set up Enclosure first';
+ statusEl.style.color = 'var(--warning)';
+ }
+ } catch (e) {
+ hideLoading();
+ const statusEl = $('#vw-enc-status');
+ if (statusEl) {
+ statusEl.textContent = 'Failed to load enclosure: ' + e;
+ statusEl.style.color = 'var(--error)';
+ }
}
- } catch (_) {}
+ } else {
+ // Surface Wrap: auto-set model state if object is configured
+ const info = await wails()?.GetProjectInfo();
+ if (info?.hasObject) {
+ state.vectorwrap.modelPath = 'project-object';
+ state.vectorwrap.modelType = 'project-object';
+ }
+ }
+ updateVWOpenBtn();
}
async function selectVectorWrapSVG() {
@@ -995,6 +1154,58 @@ function toggleVWAssembly() {
if (btn) btn.textContent = assembled ? 'Take Off Lid' : 'Put Lid On';
}
+// ===== 3D Object Setup Page (Surface Wrap projects) =====
+let _objectModelPath = '';
+
+function initObjectSetup() {
+ const page = $('#page-objectsetup');
+ _objectModelPath = '';
+
+ page.innerHTML = `
+
+
+
STL / SCAD Model File
+
Select the 3D model to use as the target surface.
+
+ No file selected
+
+
+
+
+ `;
+}
+
+async function selectObjectModel() {
+ try {
+ const path = await wails().SelectFile('Select 3D Model', '*.stl;*.scad');
+ if (!path) return;
+ _objectModelPath = path;
+ $('#obj-model-name').textContent = path.split('/').pop();
+ $('#obj-model-name').classList.add('has-file');
+ const btn = $('#obj-configure-btn');
+ if (btn) btn.disabled = false;
+ } catch (e) { console.error(e); }
+}
+
+async function configureObject() {
+ if (!_objectModelPath) return;
+ showLoading('Configuring 3D object...');
+ try {
+ await wails().ConfigureProjectObject(_objectModelPath);
+ hideLoading();
+ navigate('dashboard');
+ } catch (e) {
+ hideLoading();
+ alert('Failed to configure object: ' + e);
+ }
+}
+
// ===== Unwrap Page =====
let _unwrapSVG = '';
@@ -1018,15 +1229,20 @@ async function initUnwrap() {
`;
}
+let _unwrapSavedPath = '';
+
async function generateUnwrap() {
try {
showLoading('Generating unwrap template...');
- const svg = await wails()?.GetUnwrapSVG();
+ const path = await wails()?.SaveUnwrapSVG();
hideLoading();
- if (!svg) {
+ if (!path) {
alert('No enclosure session active. Set up an enclosure first.');
return;
}
+ _unwrapSavedPath = path;
+
+ const svg = await wails()?.GetUnwrapSVG();
_unwrapSVG = svg;
const preview = $('#unwrap-preview');
if (preview) {
@@ -1038,35 +1254,36 @@ async function generateUnwrap() {
svgEl.style.maxHeight = '100%';
}
}
- const exportBtn = $('#unwrap-export-btn');
- if (exportBtn) exportBtn.disabled = false;
+
+ const status = $('#unwrap-status');
+ if (status) {
+ status.style.display = 'block';
+ status.textContent = 'Saved: ' + path.split('/').pop();
+ }
+ const openBtn = $('#unwrap-open-btn');
+ if (openBtn) openBtn.disabled = false;
+ const folderBtn = $('#unwrap-folder-btn');
+ if (folderBtn) folderBtn.disabled = false;
} catch (e) {
hideLoading();
alert('Failed to generate unwrap: ' + e);
}
}
-function exportUnwrapSVG() {
- if (!_unwrapSVG) return;
- const blob = new Blob([_unwrapSVG], { type: 'image/svg+xml' });
- const url = URL.createObjectURL(blob);
- const a = document.createElement('a');
- a.href = url;
- a.download = 'enclosure-unwrap.svg';
- a.click();
- URL.revokeObjectURL(url);
+async function openUnwrapExternal() {
+ if (!_unwrapSavedPath) return;
+ try {
+ await wails()?.OpenFileExternal(_unwrapSavedPath);
+ } catch (e) {
+ alert('Failed to open file: ' + e);
+ }
}
-async function importUnwrapArtwork() {
+async function openUnwrapFolder() {
try {
- const path = await wails()?.SelectFile('Select Artwork SVG', '*.svg');
- if (!path) return;
- showLoading('Importing artwork...');
- await wails()?.ImportUnwrapArtwork(path);
- hideLoading();
+ await wails()?.OpenOutputFolder();
} catch (e) {
- hideLoading();
- alert('Import failed: ' + e);
+ alert('Failed to open folder: ' + e);
}
}
@@ -2773,9 +2990,10 @@ Object.assign(window, {
setVWOverlayOpacity, setVWOverlayZ, applyVWTransform, applyVWGridRes, toggleVWGrid, resetVWGrid,
selectStructuralSVG, generateStructural, updateStructuralLive, renderStructuralPreview,
applyScanPagePreset, generateScanSheet, showScanResult,
- generateUnwrap, exportUnwrapSVG, importUnwrapArtwork,
+ generateUnwrap, openUnwrapExternal, openUnwrapFolder,
toggleUnwrapFolds, toggleUnwrapLabels, toggleUnwrapCutouts,
toggleVWCameraLock,
+ selectObjectModel, configureObject, reconfigureEnclosure,
});
// ===== Init =====
diff --git a/frontend/src/style.css b/frontend/src/style.css
index 72b7516..6b9b6ce 100644
--- a/frontend/src/style.css
+++ b/frontend/src/style.css
@@ -266,6 +266,38 @@ body {
letter-spacing: 0.5px;
font-weight: 600;
}
+.badge-warn {
+ color: var(--warning, #f9e2af);
+ border-color: var(--warning, #f9e2af);
+}
+.badge-muted {
+ color: var(--text-subtle);
+ border-color: var(--border);
+ opacity: 0.7;
+}
+
+.card-disabled {
+ pointer-events: none;
+ opacity: 0.35;
+}
+
+.project-type-badge {
+ font-size: 9px;
+ padding: 2px 6px;
+ border-radius: 4px;
+ text-transform: uppercase;
+ letter-spacing: 0.3px;
+ font-weight: 600;
+ vertical-align: middle;
+}
+.badge-pcb {
+ background: color-mix(in srgb, var(--accent) 15%, transparent);
+ color: var(--accent);
+}
+.badge-sw {
+ background: color-mix(in srgb, var(--success, #a6e3a1) 15%, transparent);
+ color: var(--success, #a6e3a1);
+}
/* Dashboard grid */
.dashboard-grid {
diff --git a/frontend/src/ui/unwrap-ui.js b/frontend/src/ui/unwrap-ui.js
index 1a814cc..caaad51 100644
--- a/frontend/src/ui/unwrap-ui.js
+++ b/frontend/src/ui/unwrap-ui.js
@@ -3,8 +3,9 @@ export function buildUnwrapSidebar() {