There's been a lot of changes, still in heavy early development so forgive me for not being great with the commit messages yet. I'm more apologizing in advance than for the present meaningless commit message - they'll get worse for a while.
This commit is contained in:
parent
6bf857e58c
commit
79c32fd583
136
app.go
136
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 ========
|
||||
|
|
|
|||
|
|
@ -48,6 +48,7 @@
|
|||
<section id="page-structural" class="page"></section>
|
||||
<section id="page-scanhelper" class="page"></section>
|
||||
<section id="page-unwrap" class="page"></section>
|
||||
<section id="page-objectsetup" class="page"></section>
|
||||
<section id="page-former" class="page page-former"></section>
|
||||
</main>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -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() {
|
|||
<div class="section-title">Recent Projects</div>
|
||||
<div class="project-list">
|
||||
${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 `
|
||||
<div class="project-item" onclick="openProject('${p.path.replace(/'/g, "\\'")}')">
|
||||
<span class="project-name">${esc(p.name)}</span>
|
||||
<span class="project-type-badge ${p.type === 'surfacewrap' ? 'badge-sw' : 'badge-pcb'}">${typeLabel}</span>
|
||||
<span class="project-meta">${esc(modeStr)}</span>
|
||||
</div>`;
|
||||
}).join('')}
|
||||
|
|
@ -158,7 +162,20 @@ async function initDashboard() {
|
|||
return;
|
||||
}
|
||||
|
||||
const modeCards = [
|
||||
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 },
|
||||
|
|
@ -166,22 +183,36 @@ async function initDashboard() {
|
|||
{ 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 = `
|
||||
<div class="page-header">
|
||||
<h2>${esc(info.name)}</h2>
|
||||
<span class="project-type-badge ${isSW ? 'badge-sw' : 'badge-pcb'}" style="margin-left:8px">${isSW ? 'Surface Wrap' : 'PCB'}</span>
|
||||
<div class="spacer" style="flex:1"></div>
|
||||
<button class="btn btn-sm" onclick="openOutputFolder()">Open Folder</button>
|
||||
<button class="btn btn-sm" onclick="closeProject()">Close Project</button>
|
||||
</div>
|
||||
<div class="dashboard-grid">
|
||||
${modeCards.map(m => `
|
||||
${modeCards.map(m => {
|
||||
if (m.disabled) {
|
||||
return `
|
||||
<div class="action-card card-disabled">
|
||||
<h3>${m.label}</h3>
|
||||
<p>${m.desc}</p>
|
||||
<span class="badge badge-muted" style="margin-top:8px">Requires 3D Object</span>
|
||||
</div>`;
|
||||
}
|
||||
let badge = '';
|
||||
if (m.required) badge = '<span class="badge badge-warn" style="margin-top:8px">Required</span>';
|
||||
else if (m.active) badge = '<span class="badge" style="margin-top:8px">Configured</span>';
|
||||
return `
|
||||
<div class="action-card" onclick="navigate('${m.id}')">
|
||||
<h3>${m.label}</h3>
|
||||
<p>${m.desc}</p>
|
||||
${m.active ? '<span class="badge" style="margin-top:8px">Configured</span>' : ''}
|
||||
</div>
|
||||
`).join('')}
|
||||
${badge}
|
||||
</div>`;
|
||||
}).join('')}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
|
@ -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 = `
|
||||
<div style="font-size:14px;font-weight:500;margin-bottom:16px;color:var(--text-primary)">Project Type</div>
|
||||
<div style="display:grid;grid-template-columns:1fr 1fr;gap:12px">
|
||||
<div class="action-card type-option" data-type="pcb" style="cursor:pointer;padding:16px;text-align:center">
|
||||
<h3 style="margin:0 0 6px;font-size:13px">PCB / KiCad</h3>
|
||||
<p style="margin:0;font-size:11px;color:var(--text-subtle)">Enclosures and stencils from gerber files</p>
|
||||
</div>
|
||||
<div class="action-card type-option" data-type="surfacewrap" style="cursor:pointer;padding:16px;text-align:center">
|
||||
<h3 style="margin:0 0 6px;font-size:13px">Surface Wrap</h3>
|
||||
<p style="margin:0;font-size:11px;color:var(--text-subtle)">Wrap 2D art onto 3D objects</p>
|
||||
</div>
|
||||
</div>
|
||||
<div style="margin-top:12px">
|
||||
<button class="btn btn-sm" id="type-cancel">Cancel</button>
|
||||
</div>
|
||||
`;
|
||||
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 = `
|
||||
<div class="page-header">
|
||||
<button class="btn" onclick="navigate(state.project ? 'dashboard' : 'landing')">← Back</button>
|
||||
<h2>Enclosure Generator</h2>
|
||||
<h2>Enclosure Generator${prefill ? ' — Reconfigure' : ''}</h2>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="card-title">Gerber Job File (.gbrjob)</div>
|
||||
<div class="file-row">
|
||||
<span class="file-name" id="enc-gbrjob-name">No file selected</span>
|
||||
<span class="file-name${gbrjobClass}" id="enc-gbrjob-name">${esc(gbrjobLabel)}</span>
|
||||
<button class="btn btn-sm" onclick="selectGbrjob()">Select File...</button>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -387,7 +485,7 @@ function initEnclosure() {
|
|||
<div class="card-title">Gerber Files</div>
|
||||
<p style="font-size:12px;color:var(--text-subtle);margin-bottom:8px">Select a folder to auto-discover, or add files individually.</p>
|
||||
<div class="file-row" style="margin-bottom:8px">
|
||||
<span class="file-name" id="enc-gerber-count">No files selected</span>
|
||||
<span class="file-name${gerberClass}" id="enc-gerber-count">${esc(gerberLabel)}</span>
|
||||
</div>
|
||||
<div style="display:flex;gap:6px;flex-wrap:wrap">
|
||||
<button class="btn btn-sm" onclick="selectGerberFolder()">Select Folder...</button>
|
||||
|
|
@ -399,7 +497,7 @@ function initEnclosure() {
|
|||
<div class="card">
|
||||
<div class="card-title">PTH Drill File (Optional)</div>
|
||||
<div class="file-row">
|
||||
<span class="file-name" id="enc-drill-name">No file selected</span>
|
||||
<span class="file-name${drillClass}" id="enc-drill-name">${esc(drillLabel)}</span>
|
||||
<button class="btn btn-sm" onclick="selectDrill('pth')">Select...</button>
|
||||
<button class="btn btn-sm" onclick="clearDrill('pth')">Clear</button>
|
||||
</div>
|
||||
|
|
@ -407,7 +505,7 @@ function initEnclosure() {
|
|||
<div class="card">
|
||||
<div class="card-title">NPTH Drill File (Optional)</div>
|
||||
<div class="file-row">
|
||||
<span class="file-name" id="enc-npth-name">No file selected</span>
|
||||
<span class="file-name${npthClass}" id="enc-npth-name">${esc(npthLabel)}</span>
|
||||
<button class="btn btn-sm" onclick="selectDrill('npth')">Select...</button>
|
||||
<button class="btn btn-sm" onclick="clearDrill('npth')">Clear</button>
|
||||
</div>
|
||||
|
|
@ -417,19 +515,19 @@ function initEnclosure() {
|
|||
<div class="card-title">Parameters</div>
|
||||
<div class="form-row">
|
||||
<span class="form-label">Wall Thickness (mm)</span>
|
||||
<input class="form-input" id="enc-wall-thick" type="number" step="0.1" value="1.5">
|
||||
<input class="form-input" id="enc-wall-thick" type="number" step="0.1" value="${wt}">
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<span class="form-label">Wall Height (mm)</span>
|
||||
<input class="form-input" id="enc-wall-height" type="number" step="0.1" value="10.0">
|
||||
<input class="form-input" id="enc-wall-height" type="number" step="0.1" value="${wh}">
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<span class="form-label">Clearance (mm)</span>
|
||||
<input class="form-input" id="enc-clearance" type="number" step="0.1" value="0.3">
|
||||
<input class="form-input" id="enc-clearance" type="number" step="0.1" value="${cl}">
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<span class="form-label">DPI</span>
|
||||
<input class="form-input" id="enc-dpi" type="number" step="1" value="600">
|
||||
<input class="form-input" id="enc-dpi" type="number" step="1" value="${dp}">
|
||||
</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
|
|
@ -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 = `
|
||||
<div class="page-header">
|
||||
<button class="btn" onclick="navigate('enclosure')">← Back</button>
|
||||
<button class="btn" onclick="navigate('dashboard')">← Back</button>
|
||||
<h2>Enclosure Preview — ${esc(info.projectName)}</h2>
|
||||
<div class="spacer" style="flex:1"></div>
|
||||
<button class="btn btn-sm" onclick="reconfigureEnclosure()">Reconfigure</button>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="card-title">Board Preview — ${info.boardW.toFixed(1)} × ${info.boardH.toFixed(1)} mm | ${info.sides?.length || 0} sides</div>
|
||||
|
|
@ -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 = `
|
||||
<div class="card" id="vw-model-card">
|
||||
<div class="card-title">3D Model</div>
|
||||
<div style="font-size:12px;color:var(--success)">${esc(objName)}</div>
|
||||
</div>`;
|
||||
} else {
|
||||
// PCB: use project enclosure directly, no external model option
|
||||
modelCardHTML = `
|
||||
<div class="card" id="vw-model-card">
|
||||
<div class="card-title">3D Model</div>
|
||||
<div id="vw-model-body">
|
||||
<div id="vw-enc-status" style="font-size:12px;color:var(--text-subtle)">Checking project enclosure...</div>
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
page.innerHTML = `
|
||||
<div class="page-header">
|
||||
|
|
@ -809,33 +952,49 @@ async function initVectorWrap() {
|
|||
<div id="vw-svg-info" style="display:none;margin-top:8px;font-size:12px;color:var(--text-secondary)"></div>
|
||||
<div id="vw-svg-warnings" style="display:none;margin-top:6px"></div>
|
||||
</div>
|
||||
<div class="card" id="vw-model-card">
|
||||
<div class="card-title">3D Model</div>
|
||||
<label id="vw-use-project-label" style="display:none;margin-bottom:10px;cursor:pointer;font-size:12px;color:var(--text-secondary);user-select:none">
|
||||
<input type="checkbox" id="vw-use-project-cb" onchange="toggleUseProjectEnclosure(this.checked)" style="margin-right:6px;vertical-align:middle">
|
||||
Use project's enclosure
|
||||
</label>
|
||||
<div id="vw-model-body">
|
||||
<p style="font-size:12px;color:var(--text-subtle);margin-bottom:8px">Select an STL or OpenSCAD file for the target surface.</p>
|
||||
<div class="file-row">
|
||||
<span class="file-name" id="vw-model-name">No file selected</span>
|
||||
<button class="btn btn-sm" onclick="selectVectorWrapModel()">Select Model...</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
${modelCardHTML}
|
||||
<div class="action-bar">
|
||||
<div class="spacer"></div>
|
||||
<button class="btn btn-primary" onclick="openVectorWrapFormer()" id="vw-open-btn" disabled>Open in Former</button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// PCB mode: auto-load project enclosure
|
||||
if (!isSW) {
|
||||
try {
|
||||
const vwCheck = await wails()?.GetVectorWrapInfo();
|
||||
const statusEl = $('#vw-enc-status');
|
||||
if (vwCheck?.hasProjectEnclosure) {
|
||||
const label = $('#vw-use-project-label');
|
||||
if (label) label.style.display = 'block';
|
||||
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)';
|
||||
}
|
||||
} catch (_) {}
|
||||
} 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)';
|
||||
}
|
||||
}
|
||||
} 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 = `
|
||||
<div class="page-header">
|
||||
<button class="btn" onclick="navigate('dashboard')">← Back</button>
|
||||
<h2>3D Object Setup</h2>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="card-title">STL / SCAD Model File</div>
|
||||
<p style="font-size:12px;color:var(--text-subtle);margin-bottom:8px">Select the 3D model to use as the target surface.</p>
|
||||
<div class="file-row">
|
||||
<span class="file-name" id="obj-model-name">No file selected</span>
|
||||
<button class="btn btn-sm" onclick="selectObjectModel()">Select File...</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="action-bar">
|
||||
<div class="spacer"></div>
|
||||
<button class="btn btn-primary" onclick="configureObject()" id="obj-configure-btn" disabled>Configure Object →</button>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
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 =====
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -3,8 +3,9 @@ export function buildUnwrapSidebar() {
|
|||
<div class="unwrap-sidebar">
|
||||
<div style="font-size:10px;color:var(--text-subtle);text-transform:uppercase;margin:0 0 8px;letter-spacing:0.5px">Unwrap Template</div>
|
||||
<button class="btn btn-sm" onclick="generateUnwrap()" id="unwrap-gen-btn" style="width:100%;margin-bottom:6px">Generate Template</button>
|
||||
<button class="btn btn-sm" onclick="exportUnwrapSVG()" id="unwrap-export-btn" style="width:100%;margin-bottom:6px" disabled>Export SVG</button>
|
||||
<button class="btn btn-sm" onclick="importUnwrapArtwork()" id="unwrap-import-btn" style="width:100%;margin-bottom:12px">Import Artwork</button>
|
||||
<button class="btn btn-sm" onclick="openUnwrapExternal()" id="unwrap-open-btn" style="width:100%;margin-bottom:6px" disabled>Open in Default App</button>
|
||||
<button class="btn btn-sm" onclick="openUnwrapFolder()" id="unwrap-folder-btn" style="width:100%;margin-bottom:12px" disabled>Open Containing Folder</button>
|
||||
<div id="unwrap-status" style="display:none;font-size:11px;color:var(--success);margin-bottom:8px"></div>
|
||||
|
||||
<div style="font-size:10px;color:var(--text-subtle);text-transform:uppercase;margin:8px 0 4px;letter-spacing:0.5px">Display</div>
|
||||
<label style="display:flex;align-items:center;gap:6px;font-size:11px;color:var(--text-secondary);cursor:pointer;margin:4px 0">
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import "time"
|
|||
type ProjectData struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Type string `json:"type,omitempty"` // "pcb" or "surfacewrap"; empty = legacy pcb
|
||||
CreatedAt time.Time `json:"createdAt"`
|
||||
Version int `json:"version"`
|
||||
|
||||
|
|
@ -14,10 +15,18 @@ type ProjectData struct {
|
|||
VectorWrap *VectorWrapData `json:"vectorWrap,omitempty"`
|
||||
Structural *StructuralData `json:"structural,omitempty"`
|
||||
ScanHelper *ScanHelperData `json:"scanHelper,omitempty"`
|
||||
Object *ObjectData `json:"object,omitempty"`
|
||||
|
||||
Settings ProjectSettings `json:"settings"`
|
||||
}
|
||||
|
||||
// ObjectData stores the 3D object reference for Surface Wrap projects.
|
||||
type ObjectData struct {
|
||||
ModelFile string `json:"modelFile,omitempty"`
|
||||
ModelType string `json:"modelType,omitempty"` // "stl" or "scad"
|
||||
WallThickness float64 `json:"wallThickness"`
|
||||
}
|
||||
|
||||
type ProjectSettings struct {
|
||||
ShowGrid bool `json:"showGrid"`
|
||||
TraditionalControls bool `json:"traditionalControls"`
|
||||
|
|
|
|||
Loading…
Reference in New Issue