Former/vectorwrap_stencil.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
}