From 1c1e9feff42c9b5425260348b85af3164a09b514 Mon Sep 17 00:00:00 2001 From: Som Samantray Date: Tue, 11 Aug 2026 20:48:04 +0530 Subject: [PATCH 1/4] feat(editor): add structure+utility 'routing' floorplan export scope Add a third FloorplanExportScope value that includes structure and utility nodes (ducts, pipes, HVAC) without furniture, shared through a pure isFloorplanNodeInExportScope predicate used at both collection filter sites. Existing 'full' and 'structure' behavior is unchanged. Co-authored-by: CommandCodeBot --- .../lib/floorplan/floorplan-export.test.ts | 45 ++++++++++++++++++- .../src/lib/floorplan/floorplan-export.tsx | 36 +++++++++++---- 2 files changed, 72 insertions(+), 9 deletions(-) diff --git a/packages/editor/src/lib/floorplan/floorplan-export.test.ts b/packages/editor/src/lib/floorplan/floorplan-export.test.ts index 2ad2cbd70a..502ba63078 100644 --- a/packages/editor/src/lib/floorplan/floorplan-export.test.ts +++ b/packages/editor/src/lib/floorplan/floorplan-export.test.ts @@ -1,11 +1,12 @@ import { describe, expect, test } from 'bun:test' -import type { FloorplanGeometry } from '@pascal-app/core' +import type { FloorplanGeometry, NodeCategory } from '@pascal-app/core' import { splitFloorplanOverlay } from '../../components/editor-2d/renderers/floorplan-registry-layer' import { DEFAULT_FLOORPLAN_ANNOTATION_VISIBILITY } from './annotation-visibility' import { filterFloorplanExportOverlay, fitPlanToBox, isFloorplanExportAnnotationGeometry, + isFloorplanNodeInExportScope, partitionFloorplanExportOverlay, resolveFloorplanExportAnnotationVisibility, resolveFloorplanExportNodeGeometry, @@ -317,3 +318,45 @@ describe('resolveFloorplanPageLayout', () => { }) }) }) + +describe('isFloorplanNodeInExportScope', () => { + const node = (category?: NodeCategory) => ({ category }) + + test('includes structure-category nodes under structure, routing, and full', () => { + expect(isFloorplanNodeInExportScope(node('structure'), 'structure')).toBe(true) + expect(isFloorplanNodeInExportScope(node('structure'), 'routing')).toBe(true) + expect(isFloorplanNodeInExportScope(node('structure'), 'full')).toBe(true) + }) + + test('includes utility-category nodes under routing and full, not structure', () => { + expect(isFloorplanNodeInExportScope(node('utility'), 'routing')).toBe(true) + expect(isFloorplanNodeInExportScope(node('utility'), 'full')).toBe(true) + expect(isFloorplanNodeInExportScope(node('utility'), 'structure')).toBe(false) + }) + + test('includes furnish-category nodes only under full', () => { + expect(isFloorplanNodeInExportScope(node('furnish'), 'full')).toBe(true) + expect(isFloorplanNodeInExportScope(node('furnish'), 'structure')).toBe(false) + expect(isFloorplanNodeInExportScope(node('furnish'), 'routing')).toBe(false) + }) + + test('includes analysis and site-category nodes only under full', () => { + for (const category of ['analysis', 'site'] as const) { + expect(isFloorplanNodeInExportScope(node(category), 'full')).toBe(true) + expect(isFloorplanNodeInExportScope(node(category), 'structure')).toBe(false) + expect(isFloorplanNodeInExportScope(node(category), 'routing')).toBe(false) + } + }) + + test('excludes nodes with no category except under full', () => { + expect(isFloorplanNodeInExportScope(node(undefined), 'full')).toBe(true) + expect(isFloorplanNodeInExportScope(node(undefined), 'structure')).toBe(false) + expect(isFloorplanNodeInExportScope(node(undefined), 'routing')).toBe(false) + }) + + test('handles an undefined definition like a no-category node', () => { + expect(isFloorplanNodeInExportScope(undefined, 'full')).toBe(true) + expect(isFloorplanNodeInExportScope(undefined, 'structure')).toBe(false) + expect(isFloorplanNodeInExportScope(undefined, 'routing')).toBe(false) + }) +}) diff --git a/packages/editor/src/lib/floorplan/floorplan-export.tsx b/packages/editor/src/lib/floorplan/floorplan-export.tsx index f71481f73b..6a30eefa71 100644 --- a/packages/editor/src/lib/floorplan/floorplan-export.tsx +++ b/packages/editor/src/lib/floorplan/floorplan-export.tsx @@ -8,6 +8,7 @@ import { type FloorplanPalette, type FloorplanPoint, type LiveNodeOverrides, + type NodeCategory, nodeRegistry, resolveBuildingForLevel, useScene, @@ -64,10 +65,27 @@ import { FLOORPLAN_VIEW_ROTATION_DEG } from './geometry' * text instead of being reinterpreted from browser SVG. * * `scope: 'structure'` keeps only `category === 'structure'` nodes (walls, - * slabs, ceilings, doors, windows, stairs, columns, roofs…); `'full'` keeps - * every node that has a floorplan builder and is visible. + * slabs, ceilings, doors, windows, stairs, columns, roofs…); `'routing'` + * keeps structure **and** utility (`category === 'utility'` — ducts, pipes, + * HVAC equipment…) but no furniture; `'full'` keeps every node that has a + * floorplan builder and is visible. */ -export type FloorplanExportScope = 'full' | 'structure' +export type FloorplanExportScope = 'full' | 'structure' | 'routing' + +/** + * Whether a node belongs in the given export scope. `'full'` short-circuits + * and admits every node; `'structure'` admits only `structure`-category + * nodes; `'routing'` admits `structure` and `utility`. An `undefined` + * definition (unregistered node type) behaves like a node with no category. + */ +export function isFloorplanNodeInExportScope( + node: { category?: NodeCategory } | undefined, + scope: FloorplanExportScope, +): boolean { + if (scope === 'full') return true + if (node?.category === 'structure') return true + return scope === 'routing' && node?.category === 'utility' +} const SVG_NS = 'http://www.w3.org/2000/svg' /** Minimum and proportional margin around the structural drawing bounds. */ @@ -721,6 +739,11 @@ function applyFloorplanViewport( mounted.svg.insertBefore(background, mounted.svg.firstChild) } +/** + * Collects the floorplan geometry for one level under the given export + * scope (`'full'`, `'structure'`, or `'routing'`), walking the level's + * subtree and any linked-level nodes through the same scope predicate. + */ function collectFloorplanGeometry( nodes: Record, levelId: AnyNodeId, @@ -747,7 +770,7 @@ function collectFloorplanGeometry( if ( def?.floorplan && isFloorplanNodeVisible(node) && - (scope === 'full' || def.category === 'structure') + isFloorplanNodeInExportScope(def, scope) ) { const drawingNode = resolveNodeForDrawingType(node, nodes, drawingType) if (drawingNode) entries.push({ id, node: drawingNode }) @@ -762,10 +785,7 @@ function collectFloorplanGeometry( const collectedIds = new Set(entries.map((entry) => entry.id)) for (const linked of collectFloorplanLinkedLevelNodes(nodes, levelId, collectedIds)) { const definition = nodeRegistry.get(linked.node.type) - if ( - isFloorplanNodeVisible(linked.node) && - (scope === 'full' || definition?.category === 'structure') - ) { + if (isFloorplanNodeVisible(linked.node) && isFloorplanNodeInExportScope(definition, scope)) { const drawingNode = resolveNodeForDrawingType(linked.node, nodes, drawingType) if (drawingNode) { entries.push({ id: linked.id, node: drawingNode, parentOverride: activeLevelNode }) From 70ac0a79aa8500915036483a1872f4d2a6102b43 Mon Sep 17 00:00:00 2001 From: Som Samantray Date: Tue, 11 Aug 2026 20:49:15 +0530 Subject: [PATCH 2/4] feat(editor): export exportFloorplanPdf from the package entry Re-export exportFloorplanPdf and the FloorplanExportScope type from @pascal-app/editor so hosts with their own export UI can trigger a floorplan PDF export without reaching into the settings panel. Co-authored-by: CommandCodeBot --- packages/editor/src/index.tsx | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/editor/src/index.tsx b/packages/editor/src/index.tsx index 44030432aa..80c924da0d 100644 --- a/packages/editor/src/index.tsx +++ b/packages/editor/src/index.tsx @@ -359,6 +359,10 @@ export type { FloorplanAnnotationCategory, FloorplanAnnotationVisibility, } from './lib/floorplan/annotation-visibility' +export { + exportFloorplanPdf, + type FloorplanExportScope, +} from './lib/floorplan/floorplan-export' export { createFloorplanContextExtensions, FLOORPLAN_CONTEXT_EXTENSION_KEY, From f2d3051bfb7eef2bef8ab0f4ef8d6f0a0ddadbd9 Mon Sep 17 00:00:00 2001 From: Som Samantray Date: Tue, 11 Aug 2026 21:20:28 +0530 Subject: [PATCH 3/4] fix(review): add package-entry re-export smoke test and drop redundant comment Per code review, add a consumer-side compile-time assertion in apps/editor that imports exportFloorplanPdf and FloorplanExportScope from the @pascal-app/editor package entry, so a broken re-export fails check-types instead of passing silently. Also drop the WHAT-narration doc comment on collectFloorplanGeometry (behavior-preserving). Co-authored-by: CommandCodeBot --- apps/editor/lib/floorplan-export-surface.test.ts | 16 ++++++++++++++++ .../src/lib/floorplan/floorplan-export.tsx | 5 ----- 2 files changed, 16 insertions(+), 5 deletions(-) create mode 100644 apps/editor/lib/floorplan-export-surface.test.ts diff --git a/apps/editor/lib/floorplan-export-surface.test.ts b/apps/editor/lib/floorplan-export-surface.test.ts new file mode 100644 index 0000000000..a27f9642f3 --- /dev/null +++ b/apps/editor/lib/floorplan-export-surface.test.ts @@ -0,0 +1,16 @@ +import { describe, expect, test } from 'bun:test' +import { exportFloorplanPdf, type FloorplanExportScope } from '@pascal-app/editor' + +// Compile-time smoke assertion for the package-entry re-export (plan U2 / +// issue #619): if the entry stops re-exporting `exportFloorplanPdf` or the +// `FloorplanExportScope` type, this import fails `check-types` and the test +// fails, instead of the regression passing silently. Runtime coverage of the +// export pipeline itself lives in @pascal-app/editor's floorplan tests; here +// we only pin the public surface. +describe('package entry floorplan export surface', () => { + test('exportFloorplanPdf accepts every scope member including routing', () => { + const scopes: FloorplanExportScope[] = ['full', 'structure', 'routing'] + expect(scopes).toContain('routing') + expect(typeof exportFloorplanPdf).toBe('function') + }) +}) diff --git a/packages/editor/src/lib/floorplan/floorplan-export.tsx b/packages/editor/src/lib/floorplan/floorplan-export.tsx index 6a30eefa71..990b0c7539 100644 --- a/packages/editor/src/lib/floorplan/floorplan-export.tsx +++ b/packages/editor/src/lib/floorplan/floorplan-export.tsx @@ -739,11 +739,6 @@ function applyFloorplanViewport( mounted.svg.insertBefore(background, mounted.svg.firstChild) } -/** - * Collects the floorplan geometry for one level under the given export - * scope (`'full'`, `'structure'`, or `'routing'`), walking the level's - * subtree and any linked-level nodes through the same scope predicate. - */ function collectFloorplanGeometry( nodes: Record, levelId: AnyNodeId, From 40a154373f9423a557ebc42bdb9a4f541c3d134a Mon Sep 17 00:00:00 2001 From: Som Samantray Date: Tue, 11 Aug 2026 21:36:04 +0530 Subject: [PATCH 4/4] docs(review): record residual review findings Co-authored-by: CommandCodeBot --- .../feat-floorplan-export-routing-scope.md | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 docs/residual-review-findings/feat-floorplan-export-routing-scope.md diff --git a/docs/residual-review-findings/feat-floorplan-export-routing-scope.md b/docs/residual-review-findings/feat-floorplan-export-routing-scope.md new file mode 100644 index 0000000000..b1a64220f4 --- /dev/null +++ b/docs/residual-review-findings/feat-floorplan-export-routing-scope.md @@ -0,0 +1,19 @@ +## Residual Review Findings + +Recorded from the ce-code-review run (run-id `20260811-210518-012d2372`) on branch `feat/floorplan-export-routing-scope`, head `f2d3051`. All residuals were filed to GitHub Issues (repo `pascalorg/editor`); no `no_sink` or `failed` items. + +- **P2** — `packages/editor/src/lib/floorplan/floorplan-export.tsx:768` — Add direct tests for floorplan export scope filtering at both collection sites — [tracker #635](https://github.com/pascalorg/editor/issues/635) +- **P3** — `packages/editor/src/lib/floorplan/floorplan-export.tsx:206` — Floorplan export schedules ignore scope: excluded categories still emit schedule pages — [tracker #631](https://github.com/pascalorg/editor/issues/631) +- **P3** — `packages/editor/src/lib/floorplan/floorplan-export.tsx:73` — Floorplan 'routing' export scope has no in-tree UI trigger — [tracker #632](https://github.com/pascalorg/editor/issues/632) +- **P3** — `packages/editor/src/index.tsx:362` — exportFloorplanPdf host API can't deliver its own contract: drawingType pinned — [tracker #633](https://github.com/pascalorg/editor/issues/633) +- **P3** — `packages/editor/src/lib/floorplan/floorplan-export.tsx:521` — Routing export: extreme-aspect utility runs can overflow fixed-pad PDF viewport — [tracker #634](https://github.com/pascalorg/editor/issues/634) + +### Applied (not residual) + +- **P2 conf 100** — plan's U2 compile-time smoke assertion for the `@pascal-app/editor` package-entry re-export — **applied** in commit `f2d3051` (`apps/editor/lib/floorplan-export-surface.test.ts`); verified it fails loudly (`TS2724`) when the re-export is broken. + +### Source run context + +- Plan: `docs/plans/2026-08-11-001-feat-floorplan-export-scope-plan.md` (origin: pascalorg/editor#619) +- Review artifact: `/tmp/compound-engineering-501/ce-code-review/20260811-210518-012d2372/` +- Reviewers: correctness (clean), project-standards (advisory only), testing, adversarial, api-contract