From d5304783328afb1d5d8aa2e0b92cd7a35a3b724b Mon Sep 17 00:00:00 2001 From: zhao0335 <3797723137@qq.com> Date: Sat, 12 Sep 2026 04:24:28 +0800 Subject: [PATCH] fix(editor): respect export scope when collecting floorplan schedules collectFloorplanSchedules walked the whole level subtree and ignored FloorplanExportScope, so a structure-only PDF still emitted zone/room schedule pages whose geometry the same export excluded. Thread scope into the collector and gate schedule contributors with isFloorplanNodeInExportScope, matching geometry collection. Fixes #631 --- .../lib/floorplan/floorplan-export.test.ts | 76 +++++++++++++++++++ .../src/lib/floorplan/floorplan-export.tsx | 6 +- 2 files changed, 81 insertions(+), 1 deletion(-) diff --git a/packages/editor/src/lib/floorplan/floorplan-export.test.ts b/packages/editor/src/lib/floorplan/floorplan-export.test.ts index 69c2db5fd..adde3b017 100644 --- a/packages/editor/src/lib/floorplan/floorplan-export.test.ts +++ b/packages/editor/src/lib/floorplan/floorplan-export.test.ts @@ -15,6 +15,7 @@ import { splitFloorplanOverlay } from '../../components/editor-2d/renderers/floo import { DEFAULT_FLOORPLAN_ANNOTATION_VISIBILITY } from './annotation-visibility' import { collectFloorplanGeometry, + collectFloorplanSchedules, filterFloorplanExportOverlay, fitPlanToBox, isFloorplanExportAnnotationGeometry, @@ -424,6 +425,81 @@ describe('isFloorplanNodeInExportScope', () => { }) }) +describe('collectFloorplanSchedules', () => { + test('omits non-structure schedule contributors under structure scope', () => { + const restoreRegistry = nodeRegistry._snapshot() + const structureKind = 'test:structure-schedule' + const siteKind = 'test:site-schedule' + const levelId = 'level_schedules' as AnyNode['id'] + const structureNodeId = 'structure_scheduled' as AnyNode['id'] + const siteNodeId = 'site_scheduled' as AnyNode['id'] + + const scheduleFor = (title: string) => ({ + id: title.toLowerCase(), + title, + columns: [{ key: 'id', label: 'ID' }], + rows: [{ id: 'row', cells: { id: '1' } }], + }) + + try { + nodeRegistry._reset() + registerNode({ + kind: structureKind, + schemaVersion: 1, + schema: z.object({ type: z.literal(structureKind) }) as never, + category: 'structure', + defaults: () => ({}) as never, + capabilities: {}, + extensions: { + 'pascal:editor/floorplan': { + schedule: () => scheduleFor('Doors'), + }, + }, + } as AnyNodeDefinition) + registerNode({ + kind: siteKind, + schemaVersion: 1, + schema: z.object({ type: z.literal(siteKind) }) as never, + category: 'site', + defaults: () => ({}) as never, + capabilities: {}, + extensions: { + 'pascal:editor/floorplan': { + schedule: () => scheduleFor('Rooms'), + }, + }, + } as AnyNodeDefinition) + + const nodes = { + [levelId]: { + id: levelId, + type: 'level', + visible: true, + children: [structureNodeId, siteNodeId], + }, + [structureNodeId]: { + id: structureNodeId, + type: structureKind, + visible: true, + }, + [siteNodeId]: { + id: siteNodeId, + type: siteKind, + visible: true, + }, + } as unknown as Record + + const full = collectFloorplanSchedules(nodes, levelId, 'metric', 'full') + expect(full.map((schedule) => schedule.title).sort()).toEqual(['Doors', 'Rooms']) + + const structure = collectFloorplanSchedules(nodes, levelId, 'metric', 'structure') + expect(structure.map((schedule) => schedule.title)).toEqual(['Doors']) + } finally { + restoreRegistry() + } + }) +}) + describe('collectFloorplanGeometry', () => { test('collects the active Site once below architecture with semantic context and projection', async () => { const restoreRegistry = nodeRegistry._snapshot() diff --git a/packages/editor/src/lib/floorplan/floorplan-export.tsx b/packages/editor/src/lib/floorplan/floorplan-export.tsx index f10c0af70..cf0b26bbf 100644 --- a/packages/editor/src/lib/floorplan/floorplan-export.tsx +++ b/packages/editor/src/lib/floorplan/floorplan-export.tsx @@ -219,7 +219,7 @@ export async function exportFloorplanPdf(scope: FloorplanExportScope): Promise, levelId: AnyNodeId, unit: 'metric' | 'imperial', + scope: FloorplanExportScope = 'full', ): FloorplanSchedule[] { const siblingsByType = new Map() const visit = (id: AnyNodeId) => { @@ -324,6 +325,9 @@ export function collectFloorplanSchedules( for (const [kind, definition] of nodeRegistry.entries()) { const scheduleContribution = getFloorplanNodeExtension(definition)?.schedule if (!scheduleContribution) continue + // Same scope gate as geometry: a structure-only PDF must not list rooms + // or other site-category contributors whose plan geometry was excluded. + if (!isFloorplanNodeInExportScope(definition, scope)) continue const siblings = siblingsByType.get(kind) ?? [] const schedule = scheduleContribution({ siblings, nodes, levelId, unit }) if (schedule && schedule.rows.length > 0) schedules.push(schedule)