Composer: hole positions from production CAD + SVG error diagnostics

(1) Mounting holes were rendering at 6.5mm inset — a guess. The actual
    Mineracks production DXF (Name-Plate-85x85-316L 2B.DXF, the SH-02
    plate) places 4 circles at (3, 3), (3, 82), (82, 3), (82, 82) with
    radius 1.5mm. Centres sit on the outerMargin line; the hole edge
    falls 1.5mm shy of the plate edge.

    holeInsetMM:        6.5  → 3.0
    holeDiameterMM:     3.5  → 3.0  (true M3 clearance from CAD)
    holeDangerDiameterMM: unchanged at 7mm (covers the M3 nut/head
        footprint per the spec)

    Hole fill now #fff so the eye reads "metal removed here" instead
    of a printed dot. Same 3mm inset is applied to SH-01 and SH-03 —
    same physical drill jig, just different plate aspect.

(2) SVG-mode error message when no drawable shapes are found now
    enumerates what was actually in the file (e.g. "found: 12 <g>,
    4 <text>, 2 <tspan>") and gives a targeted hint:
      - if <text>/<tspan>: "text logos need 'Path → Object to Path'
        in Inkscape first"
      - if <image>: "raster images can't be engraved; re-trace as
        vector outlines"
      - if <use>/<symbol>: "unresolved references; expand in your
        editor first"
    Helps users figure out what to fix in their .svg without having
    to guess.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
mineracks 2026-05-28 20:38:14 +10:00
parent 3ab72cbfdd
commit 0203be2e8f
2 changed files with 44 additions and 15 deletions

View File

@ -328,21 +328,23 @@ func exportQRSVG(this js.Value, args []js.Value) any {
})
}
// holeInsetMM is how far the centre of each M3 mounting hole sits from
// the nearest plate edge. Sits roughly mid-way between outerMargin (3mm)
// and innerMargin (10mm) — the holes are physically drilled in that band
// on real SeedHammer SH-01/02/03 plates so engraving must avoid it.
const holeInsetMM = 6.5
// holeInsetMM is the distance from each plate edge to the centre of each
// M3 mounting hole. Verified against the Mineracks SH-02 production DXF
// (Name-Plate-85x85-316L 2B.DXF): 4 circles at (3,3), (3,82), (82,3),
// (82,82), radius 1.5mm. The centres sit exactly on the outerMargin
// line. Same value used for SH-01 and SH-03 — same physical drill jig.
const holeInsetMM = 3.0
// holeDiameterMM is the visual diameter of each mounting hole. The actual
// M3 clearance hole is 3.2mm but 3.5mm gives the preview better contrast
// against the plate fill.
const holeDiameterMM = 3.5
// holeDiameterMM matches the M3 clearance hole in the CAD: 3.0mm
// diameter (radius 1.5mm). The hole renders identically to the
// production part — no visual fudging.
const holeDiameterMM = 3.0
// holeDangerDiameterMM is the radius of the dashed "no-engrave" exclusion
// ring drawn around each hole. Mirrors the practical margin a user should
// leave so the engrave head doesn't catch on the screw head or punch
// directly onto stainless that's resting on a nut.
// holeDangerDiameterMM is the diameter of the dashed "no-engrave"
// exclusion ring drawn around each hole. Picked so the ring's outer
// edge sits at ~6mm from plate edge — gives the user a clear "don't
// engrave inside the M3 nut footprint" margin (M3 nut across-flats is
// 5.5mm, head/socket 5-6mm).
const holeDangerDiameterMM = 7.0
// holePositions returns the mounting-hole centres for a given plate.
@ -392,12 +394,15 @@ func writePlateChrome(sb *strings.Builder, dims plateDims) {
// rendered with a contrasting fill so it reads as "metal removed
// here, leave clear" at a glance.
for _, h := range holePositions(dims) {
// no-engrave exclusion ring (dashed red)
fmt.Fprintf(sb,
`<circle cx="%g" cy="%g" r="%g" fill="none" stroke="#c92a2a" stroke-width="0.15" stroke-dasharray="0.5,0.5" opacity="0.7"/>`,
h[0], h[1], holeDangerDiameterMM/2,
)
// the actual hole — fill matches the page bg so the eye reads
// "metal removed here" rather than a printed dot
fmt.Fprintf(sb,
`<circle cx="%g" cy="%g" r="%g" fill="#777" stroke="#222" stroke-width="0.2"/>`,
`<circle cx="%g" cy="%g" r="%g" fill="#fff" stroke="#444" stroke-width="0.25"/>`,
h[0], h[1], holeDiameterMM/2,
)
}

View File

@ -240,7 +240,31 @@ async function onSVGFile(ev) {
.map((p) => p.getAttribute("d"))
.filter(Boolean);
if (ds.length === 0) {
throw new Error("file contains no drawable shapes (no paths, rects, circles, lines, polygons)");
// Diagnostic: what IS in the file? Surface element counts so the
// user knows what to convert. Common offenders: <text> logos
// (need to be converted to outlines first via Path → Object to
// Path in Inkscape), <image> rasters (need to be re-vectorised),
// <use>/<symbol> instances (need to be resolved).
const counts = {};
for (const el of root.querySelectorAll("*")) {
const tag = el.tagName.toLowerCase();
counts[tag] = (counts[tag] || 0) + 1;
}
const summary = Object.entries(counts)
.filter(([t]) => t !== "svg")
.sort((a, b) => b[1] - a[1])
.slice(0, 6)
.map(([t, n]) => `${n} <${t}>`)
.join(", ") || "(empty)";
let hint = "";
if (counts.text || counts.tspan) {
hint = " — text logos need 'Path → Object to Path' in Inkscape first";
} else if (counts.image) {
hint = " — raster images can't be engraved; re-trace the logo as vector outlines";
} else if (counts.use || counts.symbol) {
hint = " — unresolved <use>/<symbol> references; expand in your editor first";
}
throw new Error(`file has no drawable shapes${hint}\nfound: ${summary}`);
}
svgPaths = ds;
els.svgSummary.textContent = `${f.name}${ds.length} path${ds.length === 1 ? "" : "s"}, ${ds.reduce((n, d) => n + d.length, 0)} chars total`;