diff --git a/README.md b/README.md index 1f0aaab..5c5f38b 100644 --- a/README.md +++ b/README.md @@ -30,13 +30,39 @@ A desktop application for generating 3D-printable solder paste stencils and snap ### Requirements -- [Go](https://go.dev/dl/) 1.21+ -- [Wails CLI](https://wails.io/docs/gettingstarted/installation): `go install github.com/wailsapp/wails/v2/cmd/wails@latest` -- [Node.js](https://nodejs.org/) 18+ (for frontend build) -- macOS: Xcode Command Line Tools (`xcode-select --install`) +#### macOS + +1. Install [Homebrew](https://brew.sh/) (if not already installed) +2. Install dependencies: + ```bash + brew install go node + xcode-select --install # Xcode Command Line Tools + go install github.com/wailsapp/wails/v2/cmd/wails@latest + ``` + +#### Windows + +1. Install dependencies (using [winget](https://learn.microsoft.com/en-us/windows/package-manager/winget/), built into Windows 10/11): + ```powershell + winget install GoLang.Go + winget install OpenJS.NodeJS.LTS + go install github.com/wailsapp/wails/v2/cmd/wails@latest + ``` + Or download manually: [Go](https://go.dev/dl/) 1.21+, [Node.js](https://nodejs.org/) 18+ +2. **WebView2 Runtime** — required by Wails. Pre-installed on most Windows 10/11 machines with Edge. If missing, download from [Microsoft](https://developer.microsoft.com/en-us/microsoft-edge/webview2/). + +#### Version Summary + +| Tool | Minimum | Install docs | +|------|---------|--------------| +| Go | 1.21+ | https://go.dev/dl/ | +| Node.js | 18+ | https://nodejs.org/ | +| Wails CLI | v2 | https://wails.io/docs/gettingstarted/installation | ### Build +The build scripts check for missing dependencies and print install instructions if anything is missing. + **macOS:** ```bash ./build.sh diff --git a/app.go b/app.go index b15fd0b..162f6ca 100644 --- a/app.go +++ b/app.go @@ -143,6 +143,7 @@ func (a *App) autosaveCutouts() { } func (a *App) startup(ctx context.Context) { + debugLog("app.startup() called") a.ctx = ctx // Render and cache the logo (PNG for favicon, SVG for background art) @@ -153,15 +154,19 @@ func (a *App) startup(ctx context.Context) { a.imageServer.Store("/api/logo.png", buf.Bytes()) } a.imageServer.Store("/api/logo.svg", formerLogoSVG) + debugLog("app.startup() done, logo stored (svg=%d bytes)", len(formerLogoSVG)) } // ======== Landing Page ======== func (a *App) GetRecentProjects() []ProjectInfoJS { + debugLog("GetRecentProjects() called") entries, err := ListProjects(20) if err != nil { + debugLog("GetRecentProjects() error: %v", err) return nil } + debugLog("GetRecentProjects() found %d projects", len(entries)) var result []ProjectInfoJS for _, e := range entries { name := e.Data.ProjectName @@ -194,6 +199,13 @@ func (a *App) GetLogoDataURL() string { return "data:image/png;base64," + base64.StdEncoding.EncodeToString(buf.Bytes()) } +func (a *App) GetLogoSVGDataURL() string { + if len(formerLogoSVG) == 0 { + return "" + } + return "data:image/svg+xml;base64," + base64.StdEncoding.EncodeToString(formerLogoSVG) +} + // ======== File Dialogs ======== func (a *App) SelectFile(title string, patterns string) (string, error) { @@ -300,10 +312,15 @@ func (a *App) DiscoverGerberFiles(folderPath string) ([]string, error) { } func (a *App) BuildEnclosureSession(gbrjobPath string, gerberPaths []string, drillPath, npthPath string, wallThickness, wallHeight, clearance, dpi float64, exports []string) error { + debugLog("BuildEnclosureSession() called: gbrjob=%s gerbers=%d drill=%q npth=%q", gbrjobPath, len(gerberPaths), drillPath, npthPath) + debugLog(" params: wallThick=%.2f wallH=%.2f clearance=%.2f dpi=%.0f exports=%v", wallThickness, wallHeight, clearance, dpi, exports) + if gbrjobPath == "" { + debugLog(" ERROR: no gerber job file selected") return fmt.Errorf("no gerber job file selected") } if len(gerberPaths) == 0 { + debugLog(" ERROR: no gerber files selected") return fmt.Errorf("no gerber files selected") } @@ -329,12 +346,21 @@ func (a *App) BuildEnclosureSession(gbrjobPath string, gerberPaths []string, dri exports = []string{"stl", "scad"} } - os.MkdirAll("temp", 0755) + tempDir := formerTempDir() + cwd, _ := os.Getwd() + debugLog(" CWD=%s, tempDir=%s", cwd, tempDir) + if err := os.MkdirAll(tempDir, 0755); err != nil { + debugLog(" ERROR creating temp dir: %v", err) + return fmt.Errorf("failed to create temp dir: %v", err) + } uuid := randomID() + debugLog(" session uuid=%s", uuid) // Copy gbrjob - gbrjobDst := filepath.Join("temp", uuid+"_"+filepath.Base(gbrjobPath)) + gbrjobDst := filepath.Join(tempDir, uuid+"_"+filepath.Base(gbrjobPath)) + debugLog(" copying gbrjob %s -> %s", gbrjobPath, gbrjobDst) if err := CopyFile(gbrjobPath, gbrjobDst); err != nil { + debugLog(" ERROR copy gbrjob: %v", err) return fmt.Errorf("failed to copy gbrjob: %v", err) } @@ -343,8 +369,10 @@ func (a *App) BuildEnclosureSession(gbrjobPath string, gerberPaths []string, dri var sourceDir string for _, src := range gerberPaths { baseName := filepath.Base(src) - dst := filepath.Join("temp", uuid+"_"+baseName) + dst := filepath.Join(tempDir, uuid+"_"+baseName) + debugLog(" copying gerber %s -> %s", src, dst) if err := CopyFile(src, dst); err != nil { + debugLog(" ERROR copy gerber %s: %v", baseName, err) return fmt.Errorf("failed to copy %s: %v", baseName, err) } savedGerbers[baseName] = dst @@ -356,36 +384,46 @@ func (a *App) BuildEnclosureSession(gbrjobPath string, gerberPaths []string, dri // Copy drill files var drillDst, npthDst string if drillPath != "" { - drillDst = filepath.Join("temp", uuid+"_drill"+filepath.Ext(drillPath)) + drillDst = filepath.Join(tempDir, uuid+"_drill"+filepath.Ext(drillPath)) + debugLog(" copying drill %s -> %s", drillPath, drillDst) if err := CopyFile(drillPath, drillDst); err != nil { + debugLog(" ERROR copy drill: %v", err) return fmt.Errorf("failed to copy PTH drill: %v", err) } } if npthPath != "" { - npthDst = filepath.Join("temp", uuid+"_npth"+filepath.Ext(npthPath)) + npthDst = filepath.Join(tempDir, uuid+"_npth"+filepath.Ext(npthPath)) + debugLog(" copying npth %s -> %s", npthPath, npthDst) if err := CopyFile(npthPath, npthDst); err != nil { + debugLog(" ERROR copy npth: %v", err) return fmt.Errorf("failed to copy NPTH drill: %v", err) } } + debugLog(" calling core BuildEnclosureSession...") _, session, err := BuildEnclosureSession(gbrjobDst, savedGerbers, drillDst, npthDst, ecfg, exports) if err != nil { + debugLog(" ERROR session build: %v", err) return fmt.Errorf("session build failed: %v", err) } + debugLog(" session built OK: project=%s board=%.1fx%.1fmm", session.ProjectName, session.BoardW, session.BoardH) session.SourceDir = sourceDir a.mu.Lock() a.enclosureSession = session a.cutouts = nil a.mu.Unlock() + debugLog(" session stored in app state") // Render board preview if session.OutlineImg != nil { var buf bytes.Buffer png.Encode(&buf, session.OutlineImg) a.imageServer.Store("/api/board-preview.png", buf.Bytes()) + debugLog(" board preview rendered (%d bytes)", buf.Len()) } + debugLog("BuildEnclosureSession() returning nil (success)") return nil } @@ -605,6 +643,7 @@ func (a *App) ClearLidCutouts() { } func (a *App) GenerateEnclosureOutputs() (*GenerateResultJS, error) { + debugLog("GenerateEnclosureOutputs() called") a.mu.RLock() session := a.enclosureSession allCutouts := make([]Cutout, len(a.cutouts)) @@ -612,18 +651,22 @@ func (a *App) GenerateEnclosureOutputs() (*GenerateResultJS, error) { a.mu.RUnlock() if session == nil { + debugLog(" ERROR: no enclosure session active") return nil, fmt.Errorf("no enclosure session active") } outputDir := session.SourceDir if outputDir == "" { - outputDir = filepath.Join(".", "temp") + outputDir = formerTempDir() } + debugLog(" outputDir=%s cutouts=%d", outputDir, len(allCutouts)) files, err := GenerateEnclosureOutputs(session, allCutouts, outputDir) if err != nil { + debugLog(" ERROR generate: %v", err) return nil, err } + debugLog(" generated %d files", len(files)) // Auto-save session inst := InstanceData{ @@ -643,7 +686,7 @@ func (a *App) GenerateEnclosureOutputs() (*GenerateResultJS, error) { ProjectName: session.ProjectName, Cutouts: allCutouts, } - if savedDir, saveErr := SaveSession(inst, filepath.Join(".", "temp"), session.OutlineImg); saveErr != nil { + if savedDir, saveErr := SaveSession(inst, formerTempDir(), session.OutlineImg); saveErr != nil { log.Printf("Warning: could not save session: %v", saveErr) } else { a.mu.Lock() @@ -697,7 +740,7 @@ func (a *App) SaveEnclosureProfile(name string) error { } sourceDir := session.SourceDir if sourceDir == "" { - sourceDir = filepath.Join(".", "temp") + sourceDir = formerTempDir() } savedDir, err := SaveProfile(inst, name, sourceDir, session.OutlineImg) if err == nil && savedDir != "" { @@ -961,7 +1004,7 @@ func (a *App) GetOutputDir() (string, error) { outputDir := session.SourceDir if outputDir == "" { - outputDir = filepath.Join(".", "temp") + outputDir = formerTempDir() } absDir, err := filepath.Abs(outputDir) diff --git a/build-windows.bat b/build-windows.bat index 64abb57..a3998e5 100644 --- a/build-windows.bat +++ b/build-windows.bat @@ -1,7 +1,56 @@ @echo off -REM Build Former for Windows — run this on a Windows machine with Go and Wails installed -REM Prerequisites: Go 1.21+, Wails CLI (go install github.com/wailsapp/wails/v2/cmd/wails@latest) +REM Build Former for Windows +setlocal +REM --- Dependency checks --- +set "missing=" + +where go >nul 2>&1 +if %ERRORLEVEL% NEQ 0 ( + set "missing=1" + echo ERROR: Go not found. + echo Install: winget install GoLang.Go + echo Or download from https://go.dev/dl/ + echo. +) + +where node >nul 2>&1 +if %ERRORLEVEL% NEQ 0 ( + set "missing=1" + echo ERROR: Node.js not found. + echo Install: winget install OpenJS.NodeJS.LTS + echo Or download from https://nodejs.org/ + echo. +) + +where wails >nul 2>&1 +if %ERRORLEVEL% NEQ 0 ( + set "missing=1" + echo ERROR: Wails CLI not found. + echo Install: go install github.com/wailsapp/wails/v2/cmd/wails@latest + echo Docs: https://wails.io/docs/gettingstarted/installation + echo. +) + +REM Check for WebView2 runtime (required by Wails on Windows) +reg query "HKLM\SOFTWARE\WOW6432Node\Microsoft\EdgeUpdate\Clients\{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}" >nul 2>&1 +if %ERRORLEVEL% NEQ 0 ( + reg query "HKLM\SOFTWARE\Microsoft\EdgeUpdate\Clients\{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}" >nul 2>&1 + if %ERRORLEVEL% NEQ 0 ( + set "missing=1" + echo ERROR: WebView2 runtime not found. + echo Usually pre-installed on Windows 10/11 with Edge. + echo Download: https://developer.microsoft.com/en-us/microsoft-edge/webview2/ + echo. + ) +) + +if defined missing ( + echo Install the tools listed above and re-run this script. + exit /b 1 +) + +REM --- Build --- echo Generating app icon... go run ./cmd/genicon 2>nul && echo Icon generated. || echo Icon generation skipped. @@ -14,3 +63,4 @@ if %ERRORLEVEL% NEQ 0 ( echo. echo Done: build\bin\Former.exe +endlocal diff --git a/build-windows.sh b/build-windows.sh index 496ef0d..134f36b 100755 --- a/build-windows.sh +++ b/build-windows.sh @@ -2,6 +2,49 @@ # Cross-compile Former for Windows (amd64) from macOS/Linux set -e +# --- Dependency checks --- +missing=() + +if ! command -v go &>/dev/null; then + missing+=("go") + echo "ERROR: Go not found." + if [[ "$OSTYPE" == darwin* ]]; then + echo " Install: brew install go" + else + echo " Install: sudo apt install golang (or your distro's package manager)" + fi + echo " Or download from https://go.dev/dl/" + echo "" +fi + +if ! command -v node &>/dev/null; then + missing+=("node") + echo "ERROR: Node.js not found." + if [[ "$OSTYPE" == darwin* ]]; then + echo " Install: brew install node" + else + echo " Install: sudo apt install nodejs npm (or your distro's package manager)" + fi + echo " Or download from https://nodejs.org/" + echo "" +fi + +if ! command -v wails &>/dev/null && [ ! -x "$HOME/go/bin/wails" ]; then + missing+=("wails") + echo "ERROR: Wails CLI not found." + echo " Install: go install github.com/wailsapp/wails/v2/cmd/wails@latest" + echo " Docs: https://wails.io/docs/gettingstarted/installation" + echo "" +fi + +if [ ${#missing[@]} -gt 0 ]; then + echo "Missing dependencies: ${missing[*]}" + echo "Install the tools listed above and re-run this script." + exit 1 +fi + +# --- Build --- + # Generate app icon (needs CGO on macOS for native SVG rendering) echo "Generating app icon..." if [[ "$OSTYPE" == darwin* ]]; then diff --git a/build.sh b/build.sh index 674add4..4456d32 100755 --- a/build.sh +++ b/build.sh @@ -1,6 +1,48 @@ #!/bin/bash # Build Former for macOS set -e + +# --- Dependency checks --- +missing=() + +if ! command -v go &>/dev/null; then + missing+=("go") + echo "ERROR: Go not found." + echo " Install: brew install go" + echo " Or download from https://go.dev/dl/" + echo "" +fi + +if ! command -v node &>/dev/null; then + missing+=("node") + echo "ERROR: Node.js not found." + echo " Install: brew install node" + echo " Or download from https://nodejs.org/" + echo "" +fi + +if ! command -v wails &>/dev/null && [ ! -x "$HOME/go/bin/wails" ]; then + missing+=("wails") + echo "ERROR: Wails CLI not found." + echo " Install: go install github.com/wailsapp/wails/v2/cmd/wails@latest" + echo " Docs: https://wails.io/docs/gettingstarted/installation" + echo "" +fi + +if ! xcode-select -p &>/dev/null; then + missing+=("xcode-cli") + echo "ERROR: Xcode Command Line Tools not found." + echo " Install: xcode-select --install" + echo "" +fi + +if [ ${#missing[@]} -gt 0 ]; then + echo "Missing dependencies: ${missing[*]}" + echo "Install the tools listed above and re-run this script." + exit 1 +fi + +# --- Build --- pkill -f "Former.app" || true sleep 0.5 diff --git a/debug.sh b/debug.sh new file mode 100755 index 0000000..44550d6 --- /dev/null +++ b/debug.sh @@ -0,0 +1,29 @@ +#!/bin/bash +# Build Former for macOS — DEBUG mode (logs to ~/former/debug.log) +set -e +pkill -f "Former.app" || true +sleep 0.5 + +export SDKROOT=$(xcrun --show-sdk-path) +export CC=/usr/bin/clang +export CGO_ENABLED=1 + +WAILS=$(command -v wails || echo "$HOME/go/bin/wails") + +# Generate app icon from Former.svg +echo "Generating app icon..." +go run ./cmd/genicon 2>/dev/null && echo "Icon generated." || echo "Icon generation skipped." + +echo "Building Former (DEBUG)..." +"$WAILS" build -skipbindings -tags debug + +echo "" +echo "Debug build complete. Logs will be written to ~/former/debug.log" +echo "Launching..." +open build/bin/Former.app + +# Tail the log so you can see it live +sleep 1 +echo "" +echo "=== Tailing ~/former/debug.log (Ctrl-C to stop) ===" +tail -f ~/former/debug.log 2>/dev/null || echo "Waiting for log file..." diff --git a/debug_off.go b/debug_off.go new file mode 100644 index 0000000..caa6310 --- /dev/null +++ b/debug_off.go @@ -0,0 +1,5 @@ +//go:build !debug + +package main + +func debugLog(format string, args ...interface{}) {} diff --git a/debug_on.go b/debug_on.go new file mode 100644 index 0000000..1e273df --- /dev/null +++ b/debug_on.go @@ -0,0 +1,39 @@ +//go:build debug + +package main + +import ( + "fmt" + "log" + "os" + "path/filepath" + "time" +) + +var debugLogger *log.Logger + +func init() { + home, _ := os.UserHomeDir() + logDir := filepath.Join(home, "former") + os.MkdirAll(logDir, 0755) + logPath := filepath.Join(logDir, "debug.log") + + f, err := os.OpenFile(logPath, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0644) + if err != nil { + // Fall back to stderr + debugLogger = log.New(os.Stderr, "[DEBUG] ", log.Ltime|log.Lshortfile) + debugLogger.Printf("Could not open %s: %v — logging to stderr", logPath, err) + return + } + + debugLogger = log.New(f, "[DEBUG] ", log.Ltime|log.Lshortfile) + debugLogger.Printf("=== Former debug session started %s ===", time.Now().Format(time.RFC3339)) + debugLogger.Printf("Log file: %s", logPath) + fmt.Fprintf(os.Stderr, "[Former] Debug logging to %s\n", logPath) +} + +func debugLog(format string, args ...interface{}) { + if debugLogger != nil { + debugLogger.Printf(format, args...) + } +} diff --git a/frontend/src/main.js b/frontend/src/main.js index 6b0d3b1..7932802 100644 --- a/frontend/src/main.js +++ b/frontend/src/main.js @@ -53,8 +53,7 @@ function navigate(page) { async function initLanding() { const page = $('#page-landing'); const projects = await wails()?.GetRecentProjects() || []; - - let logoSrc = '/api/logo.png'; + const logoSrc = await wails()?.GetLogoSVGDataURL() || ''; let projectsHTML = ''; if (projects.length > 0) { @@ -73,13 +72,11 @@ async function initLanding() { } page.innerHTML = ` -
- -

Former

PCB Stencil & Enclosure Generator

+ ${logoSrc ? `
` : ''}

New Stencil

@@ -286,19 +283,19 @@ function initEnclosure() {
Parameters
Wall Thickness (mm) - +
Wall Height (mm) - +
Clearance (mm) - +
DPI - +
@@ -415,8 +412,16 @@ async function buildEnclosure() { if ($('#enc-svg')?.checked) exports.push('svg'); if ($('#enc-png')?.checked) exports.push('png'); + console.log('[Former] buildEnclosure: gbrjob=' + state.enclosure.gbrjobPath + + ' gerbers=' + state.enclosure.gerberPaths.length + + ' drill=' + state.enclosure.drillPath + + ' npth=' + state.enclosure.npthPath + + ' exports=' + exports.join(',')); + console.log('[Former] wails() =', wails()); + showLoading('Building enclosure session...'); try { + console.log('[Former] calling BuildEnclosureSession...'); await wails().BuildEnclosureSession( state.enclosure.gbrjobPath, state.enclosure.gerberPaths, @@ -428,9 +433,11 @@ async function buildEnclosure() { parseFloat($('#enc-dpi')?.value) || 0, exports, ); + console.log('[Former] BuildEnclosureSession succeeded, navigating to preview'); hideLoading(); navigate('preview'); } catch (e) { + console.error('[Former] BuildEnclosureSession FAILED:', e); hideLoading(); alert('Session build failed: ' + e); } diff --git a/frontend/src/style.css b/frontend/src/style.css index ede9b93..a77a2b6 100644 --- a/frontend/src/style.css +++ b/frontend/src/style.css @@ -134,8 +134,6 @@ body { width: 100%; display: flex; justify-content: center; - margin-top: -8px; - margin-bottom: -60px; pointer-events: none; z-index: 0; -webkit-mask-image: linear-gradient(to bottom, rgba(0,0,0,1) 20%, rgba(0,0,0,0.5) 100%); diff --git a/main.go b/main.go index cb4083d..eea1197 100644 --- a/main.go +++ b/main.go @@ -45,8 +45,11 @@ func main() { } // Ensure working directories exist - os.MkdirAll("temp", 0755) + cwd, _ := os.Getwd() + debugLog("CWD: %s", cwd) + debugLog("Ensuring ~/former dirs (includes temp)...") ensureFormerDirs() + debugLog("Startup complete, launching GUI") runGUI() } diff --git a/storage.go b/storage.go index 63c3901..aa962a8 100644 --- a/storage.go +++ b/storage.go @@ -17,6 +17,10 @@ func formerBaseDir() string { return filepath.Join(home, "former") } +func formerTempDir() string { + return filepath.Join(formerBaseDir(), "temp") +} + func formerSessionsDir() string { return filepath.Join(formerBaseDir(), "sessions") } @@ -26,8 +30,19 @@ func formerProfilesDir() string { } func ensureFormerDirs() { - os.MkdirAll(formerSessionsDir(), 0755) - os.MkdirAll(formerProfilesDir(), 0755) + td := formerTempDir() + sd := formerSessionsDir() + pd := formerProfilesDir() + debugLog("ensureFormerDirs: temp=%s sessions=%s profiles=%s", td, sd, pd) + if err := os.MkdirAll(td, 0755); err != nil { + debugLog(" ERROR creating temp dir: %v", err) + } + if err := os.MkdirAll(sd, 0755); err != nil { + debugLog(" ERROR creating sessions dir: %v", err) + } + if err := os.MkdirAll(pd, 0755); err != nil { + debugLog(" ERROR creating profiles dir: %v", err) + } } // ProjectEntry represents a saved project on disk