fix(export): produce a file the print shop can actually use - #20
Merged
Conversation
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>
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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, inlib/domain/export.ts) derives the scale from the surface's owndpi_target— already the SSOT, set on the Surface step — and bounds it by the browser canvas ceiling.Stage._toKonvaCanvasskips layers whereisVisible()is false, so the artwork exports alone.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 buildgreen with CI's placeholder env; standalone output produced.🤖 Generated with Claude Code