Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions apps/editor/lib/floorplan-export-surface.test.ts
Original file line number Diff line number Diff line change
@@ -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')
})
})
Original file line number Diff line number Diff line change
@@ -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
4 changes: 4 additions & 0 deletions packages/editor/src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
45 changes: 44 additions & 1 deletion packages/editor/src/lib/floorplan/floorplan-export.test.ts
Original file line number Diff line number Diff line change
@@ -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,
Expand Down Expand Up @@ -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)
})
})
31 changes: 23 additions & 8 deletions packages/editor/src/lib/floorplan/floorplan-export.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
type FloorplanPalette,
type FloorplanPoint,
type LiveNodeOverrides,
type NodeCategory,
nodeRegistry,
resolveBuildingForLevel,
useScene,
Expand Down Expand Up @@ -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. */
Expand Down Expand Up @@ -747,7 +765,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 })
Expand All @@ -762,10 +780,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 })
Expand Down