feat(editor): floorplan export (multi-page PDF, per level)#449
Conversation
Add a Floorplan group to the settings Export panel alongside the 3D model exports, with "Full floorplan" and "Structure only" buttons. Export re-runs the live registry-driven floorplan pipeline (def.floorplan -> FloorplanGeometryRenderer) headlessly with a neutral viewState, fits each level to its own page, and titles each page with the level label. Every level of the active building becomes a page in one landscape A4 PDF. "Structure only" keeps category === 'structure' nodes; "Full" keeps every visible node with a floorplan builder. - new lib/floorplan/floorplan-export.tsx; jsPDF + svg2pdf dynamically imported so they only load on export - export five pure helpers from floorplan-registry-layer for reuse (buildContext, getFloorplanLevelData, floorplanLayerRank, splitFloorplanOverlay, isFloorplanNodeVisible) — no behaviour change - bake vector-effect:non-scaling-stroke widths into real units before svg2pdf (which ignores the hint and would otherwise draw door/window/ stair linework as metre-wide strokes) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using high effort and found 5 potential issues.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit b87b1f9. Configure here.
| const childIds = (node as { children?: AnyNodeId[] }).children | ||
| if (Array.isArray(childIds)) for (const cid of childIds) visit(cid) | ||
| } | ||
| visit(levelId) |
There was a problem hiding this comment.
Building elevators omitted from PDF
High Severity
The collectFloorplanGeometry function's node collection logic only traverses nodes under a specific level. This means building-scoped nodes, such as elevators, which are typically parented to the building itself, are not included in the floorplan export. As a result, exported PDFs may be missing these elements, leading to incomplete floorplans or blank pages if a level only contains such nodes. The live floorplan renderer correctly includes these.
Reviewed by Cursor Bugbot for commit b87b1f9. Configure here.
| isFloorplanNodeVisible(node) && | ||
| (scope === 'full' || def.category === 'structure') | ||
| ) { | ||
| entries.push({ id, node }) |
There was a problem hiding this comment.
Hidden parent visibility not honored
Medium Severity
The floorplan export uses isFloorplanNodeVisible to determine node inclusion, unlike the live editor's isFloorplanHierarchyVisible which considers ancestor visibility. This can lead to nodes appearing in the PDF that are hidden in the live editor due to a hidden parent.
Reviewed by Cursor Bugbot for commit b87b1f9. Configure here.
| if (!svg || !bbox || bbox.width === 0 || bbox.height === 0) { | ||
| cleanup() | ||
| return null | ||
| } |
There was a problem hiding this comment.
Item icons may miss PDF
Medium Severity
In mountFloorplanSvg, the SVG bounds are measured after a fixed two-frame delay. This can be insufficient for asynchronously loaded assets (like item icons from FloorplanImage) to render, potentially causing those icons to be missing from the PDF and the resulting viewBox to be incorrectly sized.
Reviewed by Cursor Bugbot for commit b87b1f9. Configure here.
| const geometry = builder(node, ctx) | ||
| if (!geometry) continue | ||
| const { base } = splitFloorplanOverlay(geometry) | ||
| if (base) out.push({ id, base }) |
There was a problem hiding this comment.
Zone labels stripped from export
Medium Severity
After building geometry, export keeps only the base half of splitFloorplanOverlay. Zone names are emitted as kind: 'text' in buildZoneFloorplan, which OVERLAY_KINDS routes to the overlay tree. The live floorplan draws overlay in a second pass, so full PDFs show colored zones without centered name labels.
Reviewed by Cursor Bugbot for commit b87b1f9. Configure here.
| for (const node of Object.values(nodes)) { | ||
| if (node.type === 'level') return node.id as AnyNodeId | ||
| } | ||
| return null |
There was a problem hiding this comment.
Non-deterministic building fallback
Low Severity
When selection.levelId is missing or invalid, resolveExportLevels uses firstLevelId, which scans Object.values(nodes) and returns the first level encountered. That order is not stable, so multi-building scenes can export the wrong building.
Reviewed by Cursor Bugbot for commit b87b1f9. Configure here.
|
Preview deployment for your docs. Learn more about Mintlify Previews.
💡 Tip: Enable Workflows to automatically generate PRs for you. |


What does this PR do?
Adds a Floorplan export to the settings Export panel, next to the 3D model exports. Two buttons:
category === 'structure'nodes (walls, slabs, ceilings, doors, windows, stairs, columns, roofs…)Both produce a landscape A4 PDF with one page per level of the active building, each page titled with the level label (
node.name, falling back toLevel <n>).How it works
def.floorplan(node, ctx)→FloorplanGeometryRenderer) headlessly, with a neutralviewStateso geometry renders in its clean, unselected form. The plan is measured withgetBBox()and fit to the page (independent of live pan/zoom).jsPDF+svg2pdf.jsare dynamically imported, so they're code-split and only load when an export actually runs.exported fromfloorplan-registry-layerfor reuse (buildContext,getFloorplanLevelData,floorplanLayerRank,splitFloorplanOverlay,isFloorplanNodeVisible) — no behaviour change.Notable fix
svg2pdfignoresvector-effect: non-scaling-stroke(used by door/window/stair/fence builders with pixel-sized stroke widths). Left as-is it drew those as metre-wide strokes (huge grey blobs). Before handing each page to svg2pdf we now bake those widths into the real-unit value that lands at the intended point weight once the plan is scaled onto the page.How to test
bun dev, open a project with a building that has one or more levels (walls, doors, windows, furniture).Screenshots / screen recording
Checklist
bun devbun checkto verify)mainbranch🤖 Generated with Claude Code
Note
Low Risk
Client-side export only; no auth or server changes. Main risk is bundle size from new PDF deps (mitigated by dynamic import) and edge cases in headless SVG/PDF rendering.
Overview
Adds floorplan PDF export from Settings → Export: Full floorplan (all visible floorplan nodes) and Structure only (
category === 'structure'). Each run downloads a landscape A4 PDF with one page per level of the active building, titled from the level name, with the plan fit to the page (not the live 2D viewport).Export reuses the same registry floorplan pipeline headlessly (
FloorplanGeometryRenderer+ neutral view state), with dynamicjspdf/svg2pdf.jsloads. Several helpers are exported fromfloorplan-registry-layerfor reuse only.Before
svg2pdf,vector-effect: non-scaling-strokewidths are converted to real units so door/window/stair lines don’t render as metre-wide blobs in the PDF.Reviewed by Cursor Bugbot for commit b87b1f9. Bugbot is set up for automated code reviews on this repo. Configure here.