224 lines
6.2 KiB
Go
224 lines
6.2 KiB
Go
package main
|
|
|
|
import (
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
)
|
|
|
|
type VWStencilConfigJS struct {
|
|
Clearance float64 `json:"clearance"`
|
|
WallThickness float64 `json:"wallThickness"`
|
|
TranslateX float64 `json:"translateX"`
|
|
TranslateY float64 `json:"translateY"`
|
|
Rotation float64 `json:"rotation"`
|
|
ScaleX float64 `json:"scaleX"`
|
|
ScaleY float64 `json:"scaleY"`
|
|
}
|
|
|
|
type VWStencilResultJS struct {
|
|
SCADSource string `json:"scadSource"`
|
|
SCADPath string `json:"scadPath"`
|
|
}
|
|
|
|
// GenerateVectorWrapStencil creates a SCAD file: thin shell conforming to the
|
|
// object surface with the SVG vector art cut through as stencil openings.
|
|
func (a *App) GenerateVectorWrapStencil(cfg VWStencilConfigJS) (*VWStencilResultJS, error) {
|
|
a.mu.RLock()
|
|
vws := a.vectorWrapSession
|
|
projPath := a.projectPath
|
|
a.mu.RUnlock()
|
|
|
|
if vws == nil {
|
|
return nil, fmt.Errorf("no vector wrap session active")
|
|
}
|
|
if vws.SVGPath == "" {
|
|
return nil, fmt.Errorf("no SVG loaded for vector wrap")
|
|
}
|
|
|
|
debugLog("GenerateVectorWrapStencil: svg=%s model=%s (%s)", vws.SVGPath, vws.ModelPath, vws.ModelType)
|
|
|
|
if cfg.Clearance <= 0 {
|
|
cfg.Clearance = 0.2
|
|
}
|
|
if cfg.WallThickness <= 0 {
|
|
cfg.WallThickness = 0.8
|
|
}
|
|
if cfg.ScaleX == 0 {
|
|
cfg.ScaleX = 1
|
|
}
|
|
if cfg.ScaleY == 0 {
|
|
cfg.ScaleY = 1
|
|
}
|
|
|
|
outDir := formerTempDir()
|
|
if projPath != "" {
|
|
outDir = filepath.Join(projPath, "output")
|
|
}
|
|
os.MkdirAll(outDir, 0755)
|
|
|
|
// Copy SVG to output directory so the SCAD can reference it
|
|
svgDest := filepath.Join(outDir, "stencil_art.svg")
|
|
if err := copyFile(vws.SVGPath, svgDest); err != nil {
|
|
return nil, fmt.Errorf("copy SVG: %w", err)
|
|
}
|
|
|
|
// Determine object source
|
|
objectSCAD, sourceType, err := a.resolveObjectSource(vws)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// If STL, copy it to output dir too
|
|
stlRef := ""
|
|
if sourceType == "stl" {
|
|
stlDest := filepath.Join(outDir, "stencil_model.stl")
|
|
if err := os.WriteFile(stlDest, vws.ModelSTL, 0644); err != nil {
|
|
return nil, fmt.Errorf("write STL: %w", err)
|
|
}
|
|
stlRef = "stencil_model.stl"
|
|
}
|
|
|
|
// Get SVG dimensions
|
|
svgW, svgH := 100.0, 100.0
|
|
if vws.SVGDoc != nil {
|
|
svgW = vws.SVGDoc.Width
|
|
svgH = vws.SVGDoc.Height
|
|
}
|
|
|
|
scad := buildVWStencilSCAD(objectSCAD, sourceType, stlRef, svgW, svgH, cfg)
|
|
|
|
scadPath := filepath.Join(outDir, "wrap_stencil.scad")
|
|
if err := os.WriteFile(scadPath, []byte(scad), 0644); err != nil {
|
|
return nil, fmt.Errorf("write SCAD: %w", err)
|
|
}
|
|
|
|
debugLog("GenerateVectorWrapStencil: wrote %s (%d bytes)", scadPath, len(scad))
|
|
|
|
return &VWStencilResultJS{
|
|
SCADSource: scad,
|
|
SCADPath: scadPath,
|
|
}, nil
|
|
}
|
|
|
|
func (a *App) resolveObjectSource(vws *VectorWrapSession) (string, string, error) {
|
|
// SCAD source
|
|
if vws.ModelType == "scad" && vws.ModelPath != "" {
|
|
data, err := os.ReadFile(vws.ModelPath)
|
|
if err != nil {
|
|
return "", "", fmt.Errorf("read SCAD model: %w", err)
|
|
}
|
|
return string(data), "scad", nil
|
|
}
|
|
|
|
// STL
|
|
if vws.ModelType == "stl" && len(vws.ModelSTL) > 0 {
|
|
return "", "stl", nil
|
|
}
|
|
|
|
// Project object — check for reconstructed.scad
|
|
a.mu.RLock()
|
|
projPath := a.projectPath
|
|
a.mu.RUnlock()
|
|
if projPath != "" {
|
|
scadPath := filepath.Join(projPath, "output", "reconstructed.scad")
|
|
if data, err := os.ReadFile(scadPath); err == nil && len(data) > 10 {
|
|
return string(data), "scad", nil
|
|
}
|
|
}
|
|
|
|
// Reconstructed STL in vectorwrap dir
|
|
if vws.ModelPath != "" && len(vws.ModelSTL) > 0 {
|
|
return "", "stl", nil
|
|
}
|
|
|
|
return "", "", fmt.Errorf("no object source found")
|
|
}
|
|
|
|
func buildVWStencilSCAD(objectSCAD, sourceType, stlRef string, svgW, svgH float64, cfg VWStencilConfigJS) string {
|
|
cl := cfg.Clearance
|
|
wt := cfg.WallThickness
|
|
|
|
var buf strings.Builder
|
|
buf.WriteString("// Vector Wrap Stencil — fits around the object for painting/marking\n")
|
|
buf.WriteString(fmt.Sprintf("// Shell: clearance=%.2fmm, wall=%.2fmm\n\n", cl, wt))
|
|
buf.WriteString("$fn = 32;\n\n")
|
|
|
|
// Target object module
|
|
buf.WriteString("module target() {\n")
|
|
if sourceType == "stl" {
|
|
buf.WriteString(fmt.Sprintf(" import(%q);\n", stlRef))
|
|
} else {
|
|
for _, line := range strings.Split(objectSCAD, "\n") {
|
|
buf.WriteString(" " + line + "\n")
|
|
}
|
|
}
|
|
buf.WriteString("}\n\n")
|
|
|
|
// Shell module
|
|
buf.WriteString("module shell() {\n")
|
|
buf.WriteString(" difference() {\n")
|
|
buf.WriteString(fmt.Sprintf(" minkowski() { target(); sphere(r=%.4f); }\n", cl+wt))
|
|
buf.WriteString(fmt.Sprintf(" minkowski() { target(); sphere(r=%.4f); }\n", cl))
|
|
buf.WriteString(" }\n")
|
|
buf.WriteString("}\n\n")
|
|
|
|
// SVG art module with user transforms
|
|
buf.WriteString("module art() {\n")
|
|
buf.WriteString(fmt.Sprintf(" translate([%.4f, %.4f, 0])\n", cfg.TranslateX, cfg.TranslateY))
|
|
buf.WriteString(fmt.Sprintf(" rotate([0, 0, %.4f])\n", cfg.Rotation))
|
|
buf.WriteString(fmt.Sprintf(" scale([%.4f, %.4f, 1])\n", cfg.ScaleX, cfg.ScaleY))
|
|
// Center the SVG around origin
|
|
buf.WriteString(fmt.Sprintf(" translate([%.4f, %.4f, 0])\n", -svgW/2, -svgH/2))
|
|
buf.WriteString(" import(\"stencil_art.svg\");\n")
|
|
buf.WriteString("}\n\n")
|
|
|
|
// Stencil: shell minus SVG projected from all 6 cardinal directions
|
|
buf.WriteString("// Stencil: shell with SVG art cut through from all directions\n")
|
|
buf.WriteString("difference() {\n")
|
|
buf.WriteString(" shell();\n\n")
|
|
|
|
// Project from Z+ (top-down)
|
|
buf.WriteString(" // Top-down projection\n")
|
|
buf.WriteString(" linear_extrude(height=500, center=true)\n")
|
|
buf.WriteString(" art();\n\n")
|
|
|
|
// Project from Z- (bottom-up) — same as top-down for extrude center=true
|
|
// Already covered by center=true
|
|
|
|
// Project from X+ (right side)
|
|
buf.WriteString(" // Right side projection\n")
|
|
buf.WriteString(" rotate([90, 0, 90])\n")
|
|
buf.WriteString(" linear_extrude(height=500, center=true)\n")
|
|
buf.WriteString(" art();\n\n")
|
|
|
|
// Project from Y+ (front)
|
|
buf.WriteString(" // Front projection\n")
|
|
buf.WriteString(" rotate([90, 0, 0])\n")
|
|
buf.WriteString(" linear_extrude(height=500, center=true)\n")
|
|
buf.WriteString(" art();\n")
|
|
|
|
buf.WriteString("}\n")
|
|
|
|
return buf.String()
|
|
}
|
|
|
|
func copyFile(src, dst string) error {
|
|
in, err := os.Open(src)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer in.Close()
|
|
|
|
out, err := os.Create(dst)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer out.Close()
|
|
|
|
_, err = io.Copy(out, in)
|
|
return err
|
|
}
|