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 {
|
type ProjectInfoJS struct {
|
||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
|
Type string `json:"type"`
|
||||||
Path string `json:"path"`
|
Path string `json:"path"`
|
||||||
CreatedAt string `json:"createdAt"`
|
CreatedAt string `json:"createdAt"`
|
||||||
HasStencil bool `json:"hasStencil"`
|
HasStencil bool `json:"hasStencil"`
|
||||||
|
|
@ -76,12 +77,25 @@ type ProjectInfoJS struct {
|
||||||
HasVectorWrap bool `json:"hasVectorWrap"`
|
HasVectorWrap bool `json:"hasVectorWrap"`
|
||||||
HasStructural bool `json:"hasStructural"`
|
HasStructural bool `json:"hasStructural"`
|
||||||
HasScanHelper bool `json:"hasScanHelper"`
|
HasScanHelper bool `json:"hasScanHelper"`
|
||||||
|
HasObject bool `json:"hasObject"`
|
||||||
BoardW float64 `json:"boardW"`
|
BoardW float64 `json:"boardW"`
|
||||||
BoardH float64 `json:"boardH"`
|
BoardH float64 `json:"boardH"`
|
||||||
ShowGrid bool `json:"showGrid"`
|
ShowGrid bool `json:"showGrid"`
|
||||||
TraditionalControls bool `json:"traditionalControls"`
|
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 {
|
type SessionInfoJS struct {
|
||||||
ProjectName string `json:"projectName"`
|
ProjectName string `json:"projectName"`
|
||||||
BoardW float64 `json:"boardW"`
|
BoardW float64 `json:"boardW"`
|
||||||
|
|
@ -184,9 +198,14 @@ func (a *App) GetRecentProjects() []ProjectInfoJS {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
pType := proj.Type
|
||||||
|
if pType == "" {
|
||||||
|
pType = "pcb"
|
||||||
|
}
|
||||||
info := ProjectInfoJS{
|
info := ProjectInfoJS{
|
||||||
ID: proj.ID,
|
ID: proj.ID,
|
||||||
Name: proj.Name,
|
Name: proj.Name,
|
||||||
|
Type: pType,
|
||||||
Path: e.Path,
|
Path: e.Path,
|
||||||
CreatedAt: e.LastOpened.Format(time.RFC3339),
|
CreatedAt: e.LastOpened.Format(time.RFC3339),
|
||||||
ShowGrid: proj.Settings.ShowGrid,
|
ShowGrid: proj.Settings.ShowGrid,
|
||||||
|
|
@ -209,6 +228,9 @@ func (a *App) GetRecentProjects() []ProjectInfoJS {
|
||||||
if proj.ScanHelper != nil {
|
if proj.ScanHelper != nil {
|
||||||
info.HasScanHelper = true
|
info.HasScanHelper = true
|
||||||
}
|
}
|
||||||
|
if proj.Object != nil {
|
||||||
|
info.HasObject = true
|
||||||
|
}
|
||||||
result = append(result, info)
|
result = append(result, info)
|
||||||
}
|
}
|
||||||
return result
|
return result
|
||||||
|
|
@ -747,10 +769,13 @@ func (a *App) GenerateEnclosureOutputs() (*GenerateResultJS, error) {
|
||||||
|
|
||||||
// ======== Project Lifecycle ========
|
// ======== Project Lifecycle ========
|
||||||
|
|
||||||
func (a *App) CreateNewProject(name string) (string, error) {
|
func (a *App) CreateNewProject(name, projectType string) (string, error) {
|
||||||
if name == "" {
|
if name == "" {
|
||||||
name = "Untitled"
|
name = "Untitled"
|
||||||
}
|
}
|
||||||
|
if projectType == "" {
|
||||||
|
projectType = "pcb"
|
||||||
|
}
|
||||||
safeName := sanitizeDirName(name)
|
safeName := sanitizeDirName(name)
|
||||||
if safeName == "" {
|
if safeName == "" {
|
||||||
safeName = "untitled"
|
safeName = "untitled"
|
||||||
|
|
@ -767,6 +792,7 @@ func (a *App) CreateNewProject(name string) (string, error) {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
proj.Name = name
|
proj.Name = name
|
||||||
|
proj.Type = projectType
|
||||||
SaveProject(path, proj)
|
SaveProject(path, proj)
|
||||||
|
|
||||||
a.mu.Lock()
|
a.mu.Lock()
|
||||||
|
|
@ -878,9 +904,14 @@ func (a *App) GetProjectInfo() *ProjectInfoJS {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
p := a.project
|
p := a.project
|
||||||
|
projType := p.Type
|
||||||
|
if projType == "" {
|
||||||
|
projType = "pcb"
|
||||||
|
}
|
||||||
info := &ProjectInfoJS{
|
info := &ProjectInfoJS{
|
||||||
ID: p.ID,
|
ID: p.ID,
|
||||||
Name: p.Name,
|
Name: p.Name,
|
||||||
|
Type: projType,
|
||||||
Path: a.projectPath,
|
Path: a.projectPath,
|
||||||
CreatedAt: p.CreatedAt.Format(time.RFC3339),
|
CreatedAt: p.CreatedAt.Format(time.RFC3339),
|
||||||
ShowGrid: p.Settings.ShowGrid,
|
ShowGrid: p.Settings.ShowGrid,
|
||||||
|
|
@ -903,6 +934,9 @@ func (a *App) GetProjectInfo() *ProjectInfoJS {
|
||||||
if p.ScanHelper != nil {
|
if p.ScanHelper != nil {
|
||||||
info.HasScanHelper = true
|
info.HasScanHelper = true
|
||||||
}
|
}
|
||||||
|
if p.Object != nil {
|
||||||
|
info.HasObject = true
|
||||||
|
}
|
||||||
return info
|
return info
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1251,6 +1285,77 @@ func (a *App) GetActiveMode() string {
|
||||||
return ""
|
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 ========
|
// ======== Vector Wrap ========
|
||||||
|
|
||||||
type SVGValidationResultJS struct {
|
type SVGValidationResultJS struct {
|
||||||
|
|
@ -1623,16 +1728,27 @@ func (a *App) GetUnwrapLayout() (*UnwrapLayout, error) {
|
||||||
return layout, nil
|
return layout, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *App) ImportUnwrapArtwork(svgContent string) error {
|
func (a *App) SaveUnwrapSVG() (string, error) {
|
||||||
a.mu.Lock()
|
svg, err := a.GetUnwrapSVG()
|
||||||
defer a.mu.Unlock()
|
if err != nil {
|
||||||
|
return "", err
|
||||||
if a.vectorWrapSession == nil {
|
|
||||||
a.vectorWrapSession = &VectorWrapSession{}
|
|
||||||
}
|
}
|
||||||
// Store the artwork SVG for use with surface wrapping
|
|
||||||
a.vectorWrapSession.EnclosureSCAD = svgContent
|
outDir, err := a.GetOutputDir()
|
||||||
return nil
|
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 ========
|
// ======== Structural Procedures ========
|
||||||
|
|
|
||||||
|
|
@ -48,6 +48,7 @@
|
||||||
<section id="page-structural" class="page"></section>
|
<section id="page-structural" class="page"></section>
|
||||||
<section id="page-scanhelper" class="page"></section>
|
<section id="page-scanhelper" class="page"></section>
|
||||||
<section id="page-unwrap" 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>
|
<section id="page-former" class="page page-former"></section>
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -96,6 +96,7 @@ function navigate(page) {
|
||||||
case 'structural': initStructural(); break;
|
case 'structural': initStructural(); break;
|
||||||
case 'scanhelper': initScanHelper(); break;
|
case 'scanhelper': initScanHelper(); break;
|
||||||
case 'unwrap': initUnwrap(); 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="section-title">Recent Projects</div>
|
||||||
<div class="project-list">
|
<div class="project-list">
|
||||||
${projects.map(p => {
|
${projects.map(p => {
|
||||||
|
const typeLabel = p.type === 'surfacewrap' ? 'Surface Wrap' : 'PCB';
|
||||||
const modes = [];
|
const modes = [];
|
||||||
if (p.hasEnclosure) modes.push('Enclosure');
|
if (p.hasEnclosure) modes.push('Enclosure');
|
||||||
if (p.hasStencil) modes.push('Stencil');
|
if (p.hasStencil) modes.push('Stencil');
|
||||||
if (p.hasVectorWrap) modes.push('Vector Wrap');
|
if (p.hasVectorWrap) modes.push('Vector Wrap');
|
||||||
if (p.hasStructural) modes.push('Structural');
|
if (p.hasStructural) modes.push('Structural');
|
||||||
if (p.hasScanHelper) modes.push('Scan Helper');
|
if (p.hasScanHelper) modes.push('Scan Helper');
|
||||||
|
if (p.hasObject) modes.push('3D Object');
|
||||||
const modeStr = modes.length > 0 ? modes.join(', ') : 'No modes configured';
|
const modeStr = modes.length > 0 ? modes.join(', ') : 'No modes configured';
|
||||||
return `
|
return `
|
||||||
<div class="project-item" onclick="openProject('${p.path.replace(/'/g, "\\'")}')">
|
<div class="project-item" onclick="openProject('${p.path.replace(/'/g, "\\'")}')">
|
||||||
<span class="project-name">${esc(p.name)}</span>
|
<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>
|
<span class="project-meta">${esc(modeStr)}</span>
|
||||||
</div>`;
|
</div>`;
|
||||||
}).join('')}
|
}).join('')}
|
||||||
|
|
@ -158,30 +162,57 @@ async function initDashboard() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const modeCards = [
|
const isSW = info.type === 'surfacewrap';
|
||||||
{ id: 'stencil', label: 'Stencil', desc: 'Solder paste stencils from gerber files', active: info.hasStencil },
|
const hasObj = info.hasObject;
|
||||||
{ 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 },
|
let modeCards;
|
||||||
{ id: 'structural', label: 'Structural Fill', desc: 'Pattern-infilled 3D forms from outlines', active: info.hasStructural },
|
if (isSW) {
|
||||||
{ id: 'scanhelper', label: 'Scan Helper', desc: 'Calibration grids for scanning objects', active: info.hasScanHelper },
|
modeCards = [
|
||||||
{ id: 'unwrap', label: 'Unwrap', desc: 'Flat SVG template of the unfolded enclosure surface', active: info.hasEnclosure },
|
{ 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 = `
|
page.innerHTML = `
|
||||||
<div class="page-header">
|
<div class="page-header">
|
||||||
<h2>${esc(info.name)}</h2>
|
<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>
|
<div class="spacer" style="flex:1"></div>
|
||||||
<button class="btn btn-sm" onclick="openOutputFolder()">Open Folder</button>
|
<button class="btn btn-sm" onclick="openOutputFolder()">Open Folder</button>
|
||||||
<button class="btn btn-sm" onclick="closeProject()">Close Project</button>
|
<button class="btn btn-sm" onclick="closeProject()">Close Project</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="dashboard-grid">
|
<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}')">
|
<div class="action-card" onclick="navigate('${m.id}')">
|
||||||
<h3>${m.label}</h3>
|
<h3>${m.label}</h3>
|
||||||
<p>${m.desc}</p>
|
<p>${m.desc}</p>
|
||||||
${m.active ? '<span class="badge" style="margin-top:8px">Configured</span>' : ''}
|
${badge}
|
||||||
</div>
|
</div>`;
|
||||||
`).join('')}
|
}).join('')}
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
@ -190,12 +221,14 @@ async function initDashboard() {
|
||||||
async function createNewProject() {
|
async function createNewProject() {
|
||||||
const name = await showInputDialog('New Project', 'Project name:', 'My Project');
|
const name = await showInputDialog('New Project', 'Project name:', 'My Project');
|
||||||
if (name === null) return;
|
if (name === null) return;
|
||||||
|
const projectType = await showTypeDialog();
|
||||||
|
if (projectType === null) return;
|
||||||
try {
|
try {
|
||||||
const path = await wails()?.CreateNewProject(name || 'Untitled');
|
const path = await wails()?.CreateNewProject(name || 'Untitled', projectType);
|
||||||
if (!path) return;
|
if (!path) return;
|
||||||
const info = await wails()?.GetProjectInfo();
|
const info = await wails()?.GetProjectInfo();
|
||||||
if (info) {
|
if (info) {
|
||||||
state.project = { name: info.name, path: info.path };
|
state.project = { name: info.name, path: info.path, type: info.type };
|
||||||
navigate('dashboard');
|
navigate('dashboard');
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} 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() {
|
async function openProjectDialog() {
|
||||||
try {
|
try {
|
||||||
const path = await wails()?.OpenProjectDialog();
|
const path = await wails()?.OpenProjectDialog();
|
||||||
if (!path) return;
|
if (!path) return;
|
||||||
const info = await wails()?.GetProjectInfo();
|
const info = await wails()?.GetProjectInfo();
|
||||||
if (info) {
|
if (info) {
|
||||||
state.project = { name: info.name, path: info.path };
|
state.project = { name: info.name, path: info.path, type: info.type };
|
||||||
navigate('dashboard');
|
navigate('dashboard');
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|
@ -367,19 +433,51 @@ function showStencilResult(files) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// ===== Enclosure Page =====
|
// ===== Enclosure Page =====
|
||||||
function initEnclosure() {
|
async function initEnclosure(prefill) {
|
||||||
const page = $('#page-enclosure');
|
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: '' };
|
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 = `
|
page.innerHTML = `
|
||||||
<div class="page-header">
|
<div class="page-header">
|
||||||
<button class="btn" onclick="navigate(state.project ? 'dashboard' : 'landing')">← Back</button>
|
<button class="btn" onclick="navigate(state.project ? 'dashboard' : 'landing')">← Back</button>
|
||||||
<h2>Enclosure Generator</h2>
|
<h2>Enclosure Generator${prefill ? ' — Reconfigure' : ''}</h2>
|
||||||
</div>
|
</div>
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="card-title">Gerber Job File (.gbrjob)</div>
|
<div class="card-title">Gerber Job File (.gbrjob)</div>
|
||||||
<div class="file-row">
|
<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>
|
<button class="btn btn-sm" onclick="selectGbrjob()">Select File...</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -387,7 +485,7 @@ function initEnclosure() {
|
||||||
<div class="card-title">Gerber Files</div>
|
<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>
|
<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">
|
<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>
|
||||||
<div style="display:flex;gap:6px;flex-wrap:wrap">
|
<div style="display:flex;gap:6px;flex-wrap:wrap">
|
||||||
<button class="btn btn-sm" onclick="selectGerberFolder()">Select Folder...</button>
|
<button class="btn btn-sm" onclick="selectGerberFolder()">Select Folder...</button>
|
||||||
|
|
@ -399,7 +497,7 @@ function initEnclosure() {
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="card-title">PTH Drill File (Optional)</div>
|
<div class="card-title">PTH Drill File (Optional)</div>
|
||||||
<div class="file-row">
|
<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="selectDrill('pth')">Select...</button>
|
||||||
<button class="btn btn-sm" onclick="clearDrill('pth')">Clear</button>
|
<button class="btn btn-sm" onclick="clearDrill('pth')">Clear</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -407,7 +505,7 @@ function initEnclosure() {
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="card-title">NPTH Drill File (Optional)</div>
|
<div class="card-title">NPTH Drill File (Optional)</div>
|
||||||
<div class="file-row">
|
<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="selectDrill('npth')">Select...</button>
|
||||||
<button class="btn btn-sm" onclick="clearDrill('npth')">Clear</button>
|
<button class="btn btn-sm" onclick="clearDrill('npth')">Clear</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -417,19 +515,19 @@ function initEnclosure() {
|
||||||
<div class="card-title">Parameters</div>
|
<div class="card-title">Parameters</div>
|
||||||
<div class="form-row">
|
<div class="form-row">
|
||||||
<span class="form-label">Wall Thickness (mm)</span>
|
<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>
|
||||||
<div class="form-row">
|
<div class="form-row">
|
||||||
<span class="form-label">Wall Height (mm)</span>
|
<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>
|
||||||
<div class="form-row">
|
<div class="form-row">
|
||||||
<span class="form-label">Clearance (mm)</span>
|
<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>
|
||||||
<div class="form-row">
|
<div class="form-row">
|
||||||
<span class="form-label">DPI</span>
|
<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>
|
</div>
|
||||||
<div class="card">
|
<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 =====
|
// ===== Enclosure Preview Page =====
|
||||||
async function initPreview() {
|
async function initPreview() {
|
||||||
const page = $('#page-preview');
|
const page = $('#page-preview');
|
||||||
|
|
@ -593,8 +712,10 @@ async function initPreview() {
|
||||||
|
|
||||||
page.innerHTML = `
|
page.innerHTML = `
|
||||||
<div class="page-header">
|
<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>
|
<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>
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="card-title">Board Preview — ${info.boardW.toFixed(1)} × ${info.boardH.toFixed(1)} mm | ${info.sides?.length || 0} sides</div>
|
<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();
|
hideLoading();
|
||||||
const info = await wails()?.GetProjectInfo();
|
const info = await wails()?.GetProjectInfo();
|
||||||
if (info) {
|
if (info) {
|
||||||
state.project = { name: info.name, path: info.path };
|
state.project = { name: info.name, path: info.path, type: info.type };
|
||||||
navigate('dashboard');
|
navigate('dashboard');
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|
@ -793,6 +914,28 @@ async function openProject(path) {
|
||||||
async function initVectorWrap() {
|
async function initVectorWrap() {
|
||||||
const page = $('#page-vectorwrap');
|
const page = $('#page-vectorwrap');
|
||||||
state.vectorwrap = { svgPath: '', modelPath: '', modelType: '' };
|
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 = `
|
page.innerHTML = `
|
||||||
<div class="page-header">
|
<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-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 id="vw-svg-warnings" style="display:none;margin-top:6px"></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="card" id="vw-model-card">
|
${modelCardHTML}
|
||||||
<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>
|
|
||||||
<div class="action-bar">
|
<div class="action-bar">
|
||||||
<div class="spacer"></div>
|
<div class="spacer"></div>
|
||||||
<button class="btn btn-primary" onclick="openVectorWrapFormer()" id="vw-open-btn" disabled>Open in Former</button>
|
<button class="btn btn-primary" onclick="openVectorWrapFormer()" id="vw-open-btn" disabled>Open in Former</button>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
try {
|
// PCB mode: auto-load project enclosure
|
||||||
const vwCheck = await wails()?.GetVectorWrapInfo();
|
if (!isSW) {
|
||||||
if (vwCheck?.hasProjectEnclosure) {
|
try {
|
||||||
const label = $('#vw-use-project-label');
|
const vwCheck = await wails()?.GetVectorWrapInfo();
|
||||||
if (label) label.style.display = 'block';
|
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() {
|
async function selectVectorWrapSVG() {
|
||||||
|
|
@ -995,6 +1154,58 @@ function toggleVWAssembly() {
|
||||||
if (btn) btn.textContent = assembled ? 'Take Off Lid' : 'Put Lid On';
|
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 =====
|
// ===== Unwrap Page =====
|
||||||
let _unwrapSVG = '';
|
let _unwrapSVG = '';
|
||||||
|
|
||||||
|
|
@ -1018,15 +1229,20 @@ async function initUnwrap() {
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let _unwrapSavedPath = '';
|
||||||
|
|
||||||
async function generateUnwrap() {
|
async function generateUnwrap() {
|
||||||
try {
|
try {
|
||||||
showLoading('Generating unwrap template...');
|
showLoading('Generating unwrap template...');
|
||||||
const svg = await wails()?.GetUnwrapSVG();
|
const path = await wails()?.SaveUnwrapSVG();
|
||||||
hideLoading();
|
hideLoading();
|
||||||
if (!svg) {
|
if (!path) {
|
||||||
alert('No enclosure session active. Set up an enclosure first.');
|
alert('No enclosure session active. Set up an enclosure first.');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
_unwrapSavedPath = path;
|
||||||
|
|
||||||
|
const svg = await wails()?.GetUnwrapSVG();
|
||||||
_unwrapSVG = svg;
|
_unwrapSVG = svg;
|
||||||
const preview = $('#unwrap-preview');
|
const preview = $('#unwrap-preview');
|
||||||
if (preview) {
|
if (preview) {
|
||||||
|
|
@ -1038,35 +1254,36 @@ async function generateUnwrap() {
|
||||||
svgEl.style.maxHeight = '100%';
|
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) {
|
} catch (e) {
|
||||||
hideLoading();
|
hideLoading();
|
||||||
alert('Failed to generate unwrap: ' + e);
|
alert('Failed to generate unwrap: ' + e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function exportUnwrapSVG() {
|
async function openUnwrapExternal() {
|
||||||
if (!_unwrapSVG) return;
|
if (!_unwrapSavedPath) return;
|
||||||
const blob = new Blob([_unwrapSVG], { type: 'image/svg+xml' });
|
try {
|
||||||
const url = URL.createObjectURL(blob);
|
await wails()?.OpenFileExternal(_unwrapSavedPath);
|
||||||
const a = document.createElement('a');
|
} catch (e) {
|
||||||
a.href = url;
|
alert('Failed to open file: ' + e);
|
||||||
a.download = 'enclosure-unwrap.svg';
|
}
|
||||||
a.click();
|
|
||||||
URL.revokeObjectURL(url);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function importUnwrapArtwork() {
|
async function openUnwrapFolder() {
|
||||||
try {
|
try {
|
||||||
const path = await wails()?.SelectFile('Select Artwork SVG', '*.svg');
|
await wails()?.OpenOutputFolder();
|
||||||
if (!path) return;
|
|
||||||
showLoading('Importing artwork...');
|
|
||||||
await wails()?.ImportUnwrapArtwork(path);
|
|
||||||
hideLoading();
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
hideLoading();
|
alert('Failed to open folder: ' + e);
|
||||||
alert('Import failed: ' + e);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -2773,9 +2990,10 @@ Object.assign(window, {
|
||||||
setVWOverlayOpacity, setVWOverlayZ, applyVWTransform, applyVWGridRes, toggleVWGrid, resetVWGrid,
|
setVWOverlayOpacity, setVWOverlayZ, applyVWTransform, applyVWGridRes, toggleVWGrid, resetVWGrid,
|
||||||
selectStructuralSVG, generateStructural, updateStructuralLive, renderStructuralPreview,
|
selectStructuralSVG, generateStructural, updateStructuralLive, renderStructuralPreview,
|
||||||
applyScanPagePreset, generateScanSheet, showScanResult,
|
applyScanPagePreset, generateScanSheet, showScanResult,
|
||||||
generateUnwrap, exportUnwrapSVG, importUnwrapArtwork,
|
generateUnwrap, openUnwrapExternal, openUnwrapFolder,
|
||||||
toggleUnwrapFolds, toggleUnwrapLabels, toggleUnwrapCutouts,
|
toggleUnwrapFolds, toggleUnwrapLabels, toggleUnwrapCutouts,
|
||||||
toggleVWCameraLock,
|
toggleVWCameraLock,
|
||||||
|
selectObjectModel, configureObject, reconfigureEnclosure,
|
||||||
});
|
});
|
||||||
|
|
||||||
// ===== Init =====
|
// ===== Init =====
|
||||||
|
|
|
||||||
|
|
@ -266,6 +266,38 @@ body {
|
||||||
letter-spacing: 0.5px;
|
letter-spacing: 0.5px;
|
||||||
font-weight: 600;
|
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 */
|
||||||
.dashboard-grid {
|
.dashboard-grid {
|
||||||
|
|
|
||||||
|
|
@ -3,8 +3,9 @@ export function buildUnwrapSidebar() {
|
||||||
<div class="unwrap-sidebar">
|
<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>
|
<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="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="openUnwrapExternal()" id="unwrap-open-btn" style="width:100%;margin-bottom:6px" disabled>Open in Default App</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="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>
|
<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">
|
<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 {
|
type ProjectData struct {
|
||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
|
Type string `json:"type,omitempty"` // "pcb" or "surfacewrap"; empty = legacy pcb
|
||||||
CreatedAt time.Time `json:"createdAt"`
|
CreatedAt time.Time `json:"createdAt"`
|
||||||
Version int `json:"version"`
|
Version int `json:"version"`
|
||||||
|
|
||||||
|
|
@ -14,10 +15,18 @@ type ProjectData struct {
|
||||||
VectorWrap *VectorWrapData `json:"vectorWrap,omitempty"`
|
VectorWrap *VectorWrapData `json:"vectorWrap,omitempty"`
|
||||||
Structural *StructuralData `json:"structural,omitempty"`
|
Structural *StructuralData `json:"structural,omitempty"`
|
||||||
ScanHelper *ScanHelperData `json:"scanHelper,omitempty"`
|
ScanHelper *ScanHelperData `json:"scanHelper,omitempty"`
|
||||||
|
Object *ObjectData `json:"object,omitempty"`
|
||||||
|
|
||||||
Settings ProjectSettings `json:"settings"`
|
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 {
|
type ProjectSettings struct {
|
||||||
ShowGrid bool `json:"showGrid"`
|
ShowGrid bool `json:"showGrid"`
|
||||||
TraditionalControls bool `json:"traditionalControls"`
|
TraditionalControls bool `json:"traditionalControls"`
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue