Skip to content

fix(export): produce a file the print shop can actually use - #20

Merged
github-actions[bot] merged 1 commit into
mainfrom
fix/export-at-print-spec
Aug 24, 2026
Merged

fix(export): produce a file the print shop can actually use#20
github-actions[bot] merged 1 commit into
mainfrom
fix/export-at-print-spec

Conversation

@catomean

Copy link
Copy Markdown
Collaborator

The Export step is where PrintCraft's premise cashes out — the artifact is the point — and it did neither of the two things that make a file printable.

What was broken

1. Editor guides were baked into the exported PNG. Seam lines, dead-zone boxes and buffer tints share the Konva stage with the artwork, and nothing excluded them from toDataURL. Every export carried red dashed lines across the picture; send that to a print shop and the guides come back on the glass.

2. Resolution was a magic pixelRatio: 4, unrelated to the surface. For Roli's Duschwand (197 × 190 cm at 200 DPI, ~15559 px wide) a 4× scale of the ~900 px canvas is roughly 60 DPI — about a third of spec — and the toast said "PNG exported". The failure path silently retried at half resolution and still reported success.

3. The Export page had a second DPI selector that persisted nowhere and reached no exporter. Two sources of truth for one number, and the one with the buttons was the one that did nothing.

What changed

  • resolveExportScale() (pure, in lib/domain/export.ts) derives the scale from the surface's own dpi_target — already the SSOT, set on the Surface step — and bounds it by the browser canvas ceiling.
  • The guide layer is hidden for the render. Verified against Konva's source: Stage._toKonvaCanvas skips layers where isVisible() is false, so the artwork exports alone.
  • When the browser refuses an allocation that large, the export steps down and reports the DPI it actually reached rather than claiming the target. The Duschwand fits under the ceiling and exports at the full 200 DPI — there's a test pinning exactly that.
  • Export page shows the surface's target and links to the Surface step to change it.
  • Drops getPixelRatio, dead since it was written. Corrects the CI comment still claiming this repo has no test suite.

Panel splitting remains manual and is still called out as such in the UI — the honest state, not a promise.

Verification

npm run verify — lint 0 errors (10 warnings, one fewer than before), tsc clean, 41 tests pass (10 new). npm run build green with CI's placeholder env; standalone output produced.

🤖 Generated with Claude Code

The Export step was the one place the product's premise cashes out — "we
don't export images, we export artifacts" — and it did neither of the two
things that make a file printable.

1. The exported PNG had the editor's guides baked into it. The seam lines,
   dead-zone boxes and buffer tints live in the same Konva stage as the
   artwork, so every export carried red dashed lines across the picture.
   Send that to a print shop and the guides come back on the glass.

2. Resolution was a magic `pixelRatio: 4` with no relationship to the
   surface. For Roli's Duschwand (197 x 190 cm at 200 DPI) that is roughly
   60 DPI — about a third of spec — and the toast said "PNG exported".
   The failure path was worse: it silently retried at half resolution and
   still reported success.

Export now resolves the scale from the surface's own `dpi_target` (already
the SSOT, set on the Surface step) and hides the guide layer for the
render — Konva's stage export skips layers that are not visible, so the
artwork leaves alone. When the browser refuses an allocation that large it
steps down and reports the DPI it actually reached instead of claiming the
target. The Duschwand fits under the canvas ceiling, so it exports at the
full 200 DPI; there is a test pinning exactly that.

The Export page had its own DPI selector that persisted nowhere and reached
no exporter — two sources of truth for one number, and the losing one was
the one with the buttons. It now shows the surface's target and links to
the Surface step to change it.

Also drops `getPixelRatio`, dead since it was written, and corrects the CI
comment that still claimed this repo has no test suite.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@github-actions
github-actions Bot merged commit fd686ce into main Aug 24, 2026
1 check passed
@github-actions
github-actions Bot deleted the fix/export-at-print-spec branch August 24, 2026 04:10
catomean added a commit that referenced this pull request Aug 24, 2026
Phase 1 asks for "export at target DPI + panel split". The DPI half landed
in #20; this is the split. A two-panel Duschwand now downloads two files —
each the panel's own trim size plus bleed, both at 200 DPI — instead of one
image of the whole wall that somebody has to cut up by hand.

Cropping per panel also retires the browser canvas ceiling as a practical
limit. The whole Duschwand is ~233M pixels and sat just under the bound;
its largest panel is ~92M and is not close to it. Bigger murals that could
never have exported as one file now export at full spec.

Bleed is the part worth being careful about. At a seam, a panel's bleed is
real content from the panel next to it — the crop simply reaches across —
so the scene stays continuous when the two sheets are mounted. At the outer
edges there is no artwork behind the bleed, so the crop comes back
transparent there and the outermost row and column are stretched over it.
A print shop that receives a transparent trim margin prints white on it.

Two things in the browser half only showed up under a real canvas, and are
the reason `lib/export/stage-export.ts` is separated from the pure geometry
in `lib/domain/export.ts`:

- Konva leaves its own transform on the exported context (scaled by
  pixelRatio, translated to the crop origin). Edge strips are measured in
  device pixels, so the fill has to reset it first, or it draws nowhere
  near the margin.
- The artwork's boundary pixel is antialiased. Stretching that column
  printed a translucent strip along the trim edge, so the fill copies the
  first fully-interior row/column instead and paints over the boundary.

Verified in a browser against a two-panel stage: crop geometry, hidden
guide layer, seam continuity in both directions, and opaque outer bleed.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
github-actions Bot pushed a commit that referenced this pull request Aug 28, 2026
…sh (#25)

The background is the scene — the place the figures stand in. The editor
collected it into a URL.createObjectURL blob in React state and stopped
there: it was never uploaded, and compositions.background_url was never
written, even though the column, the storage folder and an authorized,
versioned /api/compositions route all already existed.

So the background survived exactly one page view. Reopen the project the
next day and the canvas is bare black — and the export takes the stage as
it finds it, renders the panels from it and reports "print-ready" for a
file with no scene in it at all. Same failure shape as the baked-in guides
in #20: a confident success message on an artifact nobody re-checks before
it goes to the print shop.

The upload now goes to storage and the path to the composition row, so the
background reloads with the project. Export refuses to run while it is
still in flight rather than printing the bare canvas.

Only background_url is written. Figure placement already lives on the
figures rows, and copying it into layout would give it a second home.

An orphaned endpoint is what made this invisible, so wired.test.ts fails
if any route family has no caller in the app — verified by removing the
new hook, which fails it on /api/compositions.

verify: lint 0 errors (8 pre-existing warnings) · tsc clean · 78 tests pass


Claude-Session: https://claude.ai/code/session_014opKWKa65PXxn2MiWSKwwh

Co-authored-by: Mao Nakamoto <41178744+maonakamoto@users.noreply.github.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
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