mirror of
https://github.com/mineracks/seedhammer-v1-companion.git
synced 2026-06-26 22:01:05 +10:00
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:
parent
3ab72cbfdd
commit
0203be2e8f
@ -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,
|
||||
)
|
||||
}
|
||||
|
||||
@ -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`;
|
||||
|
||||
Loading…
Reference in New Issue
Block a user