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 000000000..e424f94d7 --- /dev/null +++ b/apps/editor/lib/floorplan-export-surface.test.ts @@ -0,0 +1,17 @@ +import { describe, expect, test } from 'bun:test' +import { exportFloorplanPdf, type FloorplanExportScope } from '@pascal-app/editor' + +// Runtime smoke assertion for the package-entry re-export (plan U2 / issue +// #619): this test imports the whole @pascal-app/editor barrel, so if the +// entry stops re-exporting `exportFloorplanPdf` or the `FloorplanExportScope` +// type, the import fails at test-run time (and `check-types`) 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', () => { + const scopes: FloorplanExportScope[] = ['full', 'structure'] + expect(scopes).toEqual(['full', 'structure']) + expect(typeof exportFloorplanPdf).toBe('function') + }) +}) diff --git a/packages/editor/src/index.tsx b/packages/editor/src/index.tsx index 83f27b8b1..a4c9ee020 100644 --- a/packages/editor/src/index.tsx +++ b/packages/editor/src/index.tsx @@ -397,6 +397,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, diff --git a/packages/editor/src/lib/floorplan/floorplan-export.test.ts b/packages/editor/src/lib/floorplan/floorplan-export.test.ts index 2ad2cbd70..f8fb37237 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,39 @@ describe('resolveFloorplanPageLayout', () => { }) }) }) + +describe('isFloorplanNodeInExportScope', () => { + const definition = (category?: NodeCategory) => ({ category }) + + test('includes structure-category nodes under structure and full', () => { + expect(isFloorplanNodeInExportScope(definition('structure'), 'structure')).toBe(true) + expect(isFloorplanNodeInExportScope(definition('structure'), 'full')).toBe(true) + }) + + test('excludes utility-category nodes under structure', () => { + expect(isFloorplanNodeInExportScope(definition('utility'), 'full')).toBe(true) + expect(isFloorplanNodeInExportScope(definition('utility'), 'structure')).toBe(false) + }) + + test('includes furnish-category nodes only under full', () => { + expect(isFloorplanNodeInExportScope(definition('furnish'), 'full')).toBe(true) + expect(isFloorplanNodeInExportScope(definition('furnish'), 'structure')).toBe(false) + }) + + test('includes analysis and site-category nodes only under full', () => { + for (const category of ['analysis', 'site'] as const) { + expect(isFloorplanNodeInExportScope(definition(category), 'full')).toBe(true) + expect(isFloorplanNodeInExportScope(definition(category), 'structure')).toBe(false) + } + }) + + test('excludes nodes with no category except under full', () => { + expect(isFloorplanNodeInExportScope(definition(undefined), 'full')).toBe(true) + expect(isFloorplanNodeInExportScope(definition(undefined), 'structure')).toBe(false) + }) + + test('handles an undefined definition like a no-category node', () => { + expect(isFloorplanNodeInExportScope(undefined, 'full')).toBe(true) + expect(isFloorplanNodeInExportScope(undefined, 'structure')).toBe(false) + }) +}) diff --git a/packages/editor/src/lib/floorplan/floorplan-export.tsx b/packages/editor/src/lib/floorplan/floorplan-export.tsx index f71481f73..2dde3775b 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, @@ -69,6 +70,20 @@ import { FLOORPLAN_VIEW_ROTATION_DEG } from './geometry' */ export type FloorplanExportScope = 'full' | 'structure' +/** + * Whether a node belongs in the given export scope. `'full'` short-circuits + * and admits every node; `'structure'` admits only `structure`-category + * nodes. An `undefined` definition (unregistered node type) behaves like a + * node with no category. + */ +export function isFloorplanNodeInExportScope( + definition: { category?: NodeCategory } | undefined, + scope: FloorplanExportScope, +): boolean { + if (scope === 'full') return true + return definition?.category === 'structure' +} + const SVG_NS = 'http://www.w3.org/2000/svg' /** Minimum and proportional margin around the structural drawing bounds. */ const MIN_PLAN_PADDING_M = 1 @@ -747,7 +762,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 +777,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 })