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
17 changes: 17 additions & 0 deletions apps/editor/lib/floorplan-export-surface.test.ts
Original file line number Diff line number Diff line change
@@ -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')
})
})
4 changes: 4 additions & 0 deletions packages/editor/src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
39 changes: 38 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,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)
})
})
22 changes: 17 additions & 5 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 @@ -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'
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Routing export scope never added

Medium Severity

FloorplanExportScope and isFloorplanNodeInExportScope still only distinguish full and structure. Hosts cannot request the advertised routing sheet that keeps structure and utility nodes (ducts, pipes, HVAC) while dropping furniture.

Additional Locations (2)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit f730e4f. Configure here.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is expected, not a regression: the 'routing' scope was intentionally dropped from this PR per @Aymericr's review (comment above) — it had no UI trigger anywhere in the tree (#632) and its name would have collided with PrintContentScope from #701. It's deferred to a follow-up PR with a UI trigger and a non-colliding name ('services'/'mep' suggested). See the updated PR description for the full context.


const SVG_NS = 'http://www.w3.org/2000/svg'
/** Minimum and proportional margin around the structural drawing bounds. */
const MIN_PLAN_PADDING_M = 1
Expand Down Expand Up @@ -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 })
Expand All @@ -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 })
Expand Down