Skip to content

Serve generated SVG drawings in the app - #142

Open
kamilpajak wants to merge 23 commits into
masterfrom
feature/svg-catalog-app-integration
Open

kamilpajak wants to merge 23 commits into
masterfrom
feature/svg-catalog-app-integration

Conversation

@kamilpajak

Copy link
Copy Markdown
Owner

Feature: the label generator now renders the generated vector drawings instead of the legacy raster images for every standard covered by the catalog.

How it works today / problem

Goal

Show the SVG drawings in the app now: crisper labels at print DPI, and first-time illustrations for standards that never had a raster image.

How

  • catalog/integrate.py copies all manifest SVGs into static/images/standards/ and repoints data/image-mappings.json (228 repointed, 19 new entries; new entries get hardwareType derived from the manifest family).
  • New scripts/catalog-repoint-standards.mjs does the surgical standards-generated.ts update the catalog README prescribes: it edits only the image field for shipped standards that are manifest keys. Result: 109 shipped standards point at SVGs (104 repointed, 5 first-time: ISO 4026-4029, ISO 14579).
  • resolveImageWithSvgPriority now loads .svg standard images instead of returning image: null. Without this, the exporter and preview skip the aspect-ratio measurement and the layout solver mis-sizes the image slot.
  • The six stale washer-pilot SVGs in static/ are refreshed (their hashes had drifted from catalog/out/).

Test plan

  • Unit tests (741) pass, including new resolver cases for .svg load and failure
  • Catalog pytest suite (519 + 2 new integrate.py tests) passes in the pinned container
  • Playwright e2e passes on chromium and webkit locally (firefox binary not installed locally; CI runs the full matrix)
  • pnpm check, pnpm lint, pnpm validate-images clean
  • Manual: label previews screenshot-verified for DIN 912/934/125/604/607, ISO 4026, ISO 7379 - correct drawings, correct aspect ratio
  • Not covered: visual review of all 109 drawings in the app (spot checks only; the drawings themselves were reviewed on contact sheets in the generator PRs)

Out of scope

  • PNG-basename bridging for 18 mapping keys whose id is not a manifest key (only din562 is shipped); each needs a per-key visual-equivalence check
  • Removing the now-redundant png-to-svg upgrade path (AVAILABLE_SVGS)

catalog/integrate.py now sets hardwareType (derived from the manifest
family) when it creates a brand-new image-mapping entry, and rejects
families it cannot classify. scripts/catalog-repoint-standards.mjs
performs the surgical standards-generated.ts update the README
prescribes: it repoints or inserts only the image field for shipped
standards covered by the manifest, since pnpm standards:build cannot be
used without the local dinmedia cache.
resolveImageWithSvgPriority returned image: null for svg sources, so
label-exporter and label-preview skipped the aspect-ratio calculation
and the constraint solver sized the image slot from a default ratio.
Svg standard images are now loaded and cached like the png path, which
becomes load-bearing once standards point directly at generated svgs.
Integrate all 247 manifest keys: copy the 125 generated svgs into
static/images/standards/ (refreshing the six stale washer pilots),
repoint data/image-mappings.json, and update the image field for the
109 shipped standards covered by the manifest. Five standards gain an
illustration for the first time: iso4026-iso4029 (din913-din916 set
screws) and iso14579. The remaining 52 shipped png images have no
generated drawing yet.
Review follow-up: the codemod depends on the generated file's exact
formatting, so it now fails loudly when the header or entry pattern
changes instead of silently editing nothing. Also documents that
FAMILY_TO_HARDWARE_TYPE is a deliberate whitelist.
The white tile behind dropdown thumbnails exists for the legacy rasters
(black drawings that would vanish on the dark theme). Vector drawings
have a transparent background and black strokes, so they render on the
dark list directly with a css invert filter for light strokes; png
thumbnails keep the white tile.
The label printer prints pure black only and drops very thin lines: the
drawing lands in an ~8mm slot at 360dpi, where the previous 0.4mm
outline was 1-2 dots and the gray dashed hidden lines dithered away.
All layers now print black with weights 0.8/0.6/0.4mm, keeping hidden
edges and centerlines distinguishable by dash pattern and weight alone.
All 125 drawings regenerated.
Eight shipped standards without a manifest key of their own (iso8678,
din562, iso7090, iso12474, iso14583, iso4162, iso15071, iso15072)
inherited another standard's png through a designation cross-reference,
so they kept rendering rasters after the svg integration. The repoint
script now swaps such a png for the same standard's svg in a second
pass - a like-for-like upgrade, no new equivalence claim. 117 shipped
standards render vector drawings.
integrate.py repoints data/image-mappings.json at the generated svgs, so
/dev/asset-compare was reading the same svg into both panes. The legacy
column now reads data/legacy-image-mappings.json, a frozen snapshot of
the mapping as it shipped before vectorization, and each card carries a
live badge when the app already serves that drawing.
…imetres

Every drawing is scaled into the same small image slot on the label, but
the drawings span 20mm to 133mm in their own coordinates, so a constant
width in drawing units reached the paper anywhere between 0.8 and 4.1
dots at 360dpi - 59 of 125 below the 2-dot floor where a thermal head
starts dropping lines. Drawing standards treat line width as an absolute
width on the finished output chosen for the format, the way CAD plots
lineweights in paper space regardless of viewport scale.

Weights are now derived per drawing from the target printed width (3
dots for the outline, 2 for hidden edges and centerlines) through the
drawing's own extent, so all 125 measure exactly 3.00 and 2.00 dots. The
dash patterns move to dots as well: the exporter's ISO pattern is built
for a full-size sheet and leaves one or two marks per edge at label
size, and it offers no hook for a custom pattern, so the two dashed
layers are rewritten in the written file.
…size

The 3-dot outline printed reliably but swallowed the drawing: at the ~128
dots a drawing gets on the label, a socket head or a washer chamfer
merged into a blob. Both layers now sit on 2 dots, the practical minimum
for a solid line on a maintained thermal head, and the layers stay apart
by dash pattern rather than weight. Centerlines are kept: they are what
makes the drawing read as an engineering drawing rather than an icon.
A chain line crossing the whole drawing is the densest layer at label
size and was competing with the outline for attention. Centerlines carry
no geometry, only the symmetry reading, so they drop to 1.5 dots while
the outline and hidden edges stay on the 2-dot print floor.
The whole box is scaled into a fixed slot on the label, so the 2mm
margin came straight out of the drawing's own size: geometry filled only
72-88% of the box, against 99-100% for the legacy rasters it replaces.
Spacing around the image belongs to whoever places it, and the label's
constraint solver already keeps the image clear of the text. Geometry
now fills 86-93%, the rest being the centerline overhang, and the
exporter still pads by half a line width so the outer stroke is not
clipped.
Back to the absolute 0.4/0.3/0.2mm the families were drawn and reviewed
with, dropping the printed-dot normalisation and the shortened dash
patterns: the uniform 2-dot line was reliable on paper but cost visible
detail at the size a drawing gets on a label. This is the starting point
for print testing; the research and the measurements behind the
alternative are kept in catalog/README.md so the next adjustment starts
from data rather than theory.

Two changes from the original are kept deliberately: all layers stay
pure black, since the label printer cannot reproduce the gray the hidden
layer used, and the view box still hugs the drawing.
ISO 128 wants a chain line to begin and end with a long dash, and a
stroke-dasharray cannot promise that: the pattern runs until the line
stops, wherever that falls. Some arms ended in a gap, so the axis faded
out short of its tip and the image carried an empty band up to 1.3mm
wide, the view box following the geometry that does reach the tip.

The chain is now emitted as geometry. Each arm keeps its exact length,
so the overhang past the outline stays consistent, and the pattern is
stretched by up to half a period to land on it - what a drafter does
with the linetype scale, and imperceptible over these lengths. An arm
too short for one long dash becomes a single stroke. The exporter's
CENTER proportions are kept, so the drawings look unchanged; the empty
band is down to a 0.1mm median with all 125 under 0.3mm.
First tuning step from print testing: outline 0.5mm, hidden edges 0.4mm,
centerlines 0.3mm.

The chain-line rhythm is decoupled from the pen in the same change. The
exporter derives dash lengths from the line width, which is right for a
full-size sheet where the width is chosen once per drawing group, but
here the width is a printer setting being tuned: at 0.3mm the long dash
became 9.5mm, longer than a small drawing's whole arm, and 69 of 125
axes collapsed into a solid line that reads as an edge. The pattern is
now fixed in millimetres at what the 0.2mm drawings had, so only the pen
changes - 8 of 125 axes are a single stroke, exactly as before.
Widths were fixed in drawing units while every drawing is scaled into
the same small slot on the label, so the same 0.5mm reached the paper
anywhere between 0.5 and 3.1 dots depending on how large the part is
drawn. The widths are now a target on the paper - 1.5 dots outline, 1.2
hidden, 0.9 centerline - converted back into drawing units per drawing
through its own extent. They keep the 5:4:3 ratio of the 0.5/0.4/0.3mm
they replace and sit on what that scheme printed at the median drawing,
so the average weight is unchanged and only the spread is gone.

Dash patterns follow. Hidden lines get it for free, the exporter
deriving the dash from the now-proportional weight, landing on a
constant 14.4 dots. Centerlines carry their pattern in dots as well:
tying it to the pen instead had collapsed 69 of 125 axes into a solid
line when the weight was last raised.
Every projected edge was sampled at 72 segments regardless of shape,
because the sampler cannot tell a line from an arc. A hex outline
therefore stored each of its six straight sides as 73 points: 904 of the
942 paths in din472.svg were perfectly straight, and the 125 drawings
totalled 14.47MB against 0.77MB for the 181 rasters they replace. The
standard picker renders one img per standard with no lazy loading, so
opening it fetched all of them at once.

Sampled points are now simplified back down with Ramer-Douglas-Peucker
at a tolerance of 1/2000 of the drawing extent - about a twelfth of a
printer dot at label size - and coordinates are written to 3 decimals
instead of 4. The drawings total 1.33MB (0.24MB gzipped), the largest
falling from 1.15MB to 70KB. Rasterised at label size and at preview
size the change moves 1% of ink pixels, all of them single boundary
pixels; the thumbnails also load lazily now.
The widths were solved against LABEL_SLOT_MM = 9.15, a number measured
off the ink of a rendered drawing rather than off the slot the app
actually gives it. calculateOptimalImageSize() hands a drawing 11.4 x
10mm on the default 12x35mm label, so every line printed about 25% over
target. The slot is also not square and the app fits contain-style,
while the weights were solved from the drawing's longer side alone, so a
portrait and a landscape drawing differed by half again on the same
label: the measured spread across the catalogue was 1.31 to 2.09 dots.

Weights now solve against min(slotW/w, slotH/h) with the reference slot
spelled out from the solver's own formula, and every drawing prints 1.50
dots on that label. Longer labels widen the slot, so drawings reproduced
there are larger and their lines proportionally thicker - uniformity is
a promise across drawings at one label size, not across label sizes.

_chain_dashes only collapses an arm to a solid stroke when it is shorter
than one long dash. Rounding the repeat count without a floor collapsed
arms up to 1.8 long dashes too, drawing 338 of 750 axes as a continuous
line, which reads as an edge rather than an axis; 8 remain solid.

The tests now state the reference slot from the solver's formula instead
of re-deriving the renderer's own model, cover a portrait and a
landscape drawing that bind on opposite axes, and bound the longest
centerline segment by the fitter's worst-case stretch rather than by a
tolerance wide enough to hide a collapsed arm.
…r nothing

The repoint script bridged by filename: any legacy png whose name
collided with a manifest key was swapped for that drawing, so two
approximations were adopted without anyone deciding to - iso14583 draws
a hexalobular drive as a cross recess, din562 draws a thin square nut at
full height. Both were already what the raster showed, so neither is a
new claim, but they are now an explicit allowlist with the compromise
written next to each entry instead of a rule that catches whatever it
happens to catch.

The script also writes the bridges into image-mappings.json, which
integrate.py cannot do because those ids are not manifest keys. Left
alone the two files disagreed for exactly those two standards, and
regenerating the dataset from the mappings would have silently undone
them; the script now fails if any standard disagrees between the two.

integrate.py resolves every standard before touching the filesystem and
replaces the mappings through a temporary file. Checking inside the copy
loop meant a manifest with an unmapped family left the drawings before
it already copied while the mappings were never written.
…yout change

The live badge read data/image-mappings.json, which integrate.py
repoints for every manifest key the moment the SVG is copied, so all 247
items carried it and it said nothing. More than half of those keys are
not ids the app knows either - the DIN and ISO forms of one fastener are
a single shipped standard, so din931 rides along inside iso4014. The
badge now comes from the shipped dataset and marks 98 of the 125
drawings; the 27 without it are shipped but never reach a user.

The changelog also records that 25 standards changed label layout: the
new drawings are less wide than the pictures they replace, so they fall
under the aspect ratio that puts the image in a full-width band and now
sit beside the text instead.
The Unreleased entry duplicated what shipped as 3.1.0, so the Fixed
section is dropped here and only the drawing changes stay unreleased.
Recounted while resolving: 21 standards gain a first illustration, not 5,
and 26 labels lose the full-width band layout rather than gaining it.
The line widths were seeded from what the old fixed-millimetre scheme
printed at the median drawing, with a note to tune them from real output.
A calibration strip carrying din125, din127, din316, din7991 and din936 at
1.5, 2.0, 2.5 and 3.0 dots settled it: the first two are too thin to hold
every line on a 360dpi label printer, both 2.5 and 3.0 print clean.

2.5 wins the tie because ink costs detail. Rasterised into the 11.4x10mm
slot, going from 2.5 to 3.0 dots closes 5.9% of the white enclosed by the
outlines (24582 to 23121 px across the five drawings) and adds 17% ink,
for nothing the printer could not already reproduce.

The three weights move together, so the 5:4:3 ratio between visible,
hidden and centre lines is unchanged. Every drawing still prints at the
same width as every other: verified at 2.50 dots across the catalog.

catalog/qa/ gains the two scripts that build the strip, so the next
calibration does not start from scratch.
@sonarqubecloud

Copy link
Copy Markdown

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant