@pascal-app/editor 1.0.0-beta.4 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/bunfig.toml +4 -0
- package/package.json +15 -8
- package/scripts/generate-print-golden-house.ts +89 -0
- package/src/components/editor/alignment-3d-guide-layer.tsx +12 -3
- package/src/components/editor/bake-exporter.tsx +1 -0
- package/src/components/editor/camera-dragging-lifecycle.test.ts +41 -0
- package/src/components/editor/camera-dragging-lifecycle.ts +10 -2
- package/src/components/editor/capture-camera-rig.tsx +59 -0
- package/src/components/editor/custom-camera-controls.tsx +20 -11
- package/src/components/editor/editor-layout-mobile.tsx +6 -6
- package/src/components/editor/editor-layout-v2.tsx +17 -5
- package/src/components/editor/elevation-3d-guide-layer.tsx +3 -2
- package/src/components/editor/export-manager.tsx +175 -33
- package/src/components/editor/first-person/build-collider-world.test.ts +30 -0
- package/src/components/editor/first-person-controls.tsx +182 -57
- package/src/components/editor/floating-action-menu.tsx +55 -55
- package/src/components/editor/floorplan-background-selection.test.ts +31 -4
- package/src/components/editor/floorplan-background-selection.ts +27 -7
- package/src/components/editor/floorplan-panel.tsx +81 -57
- package/src/components/editor/grid.tsx +12 -7
- package/src/components/editor/group-actions.ts +34 -50
- package/src/components/editor/group-floating-action-menu.tsx +30 -15
- package/src/components/editor/group-move-3d.ts +51 -21
- package/src/components/editor/group-transform-shared.test.ts +78 -0
- package/src/components/editor/group-transform-shared.ts +74 -7
- package/src/components/editor/handles/handle-arrow-raycast.test.ts +86 -0
- package/src/components/editor/handles/handle-arrow.tsx +109 -48
- package/src/components/editor/handles/linear-resize-drag.ts +67 -0
- package/src/components/editor/handles/resize-snap.test.ts +46 -0
- package/src/components/editor/handles/resize-snap.ts +10 -1
- package/src/components/editor/handles/use-handle-drag.ts +18 -1
- package/src/components/editor/index.tsx +252 -46
- package/src/components/editor/measurement-pill.tsx +12 -12
- package/src/components/editor/node-action-menu.tsx +42 -1
- package/src/components/editor/node-arrow-handles.tsx +47 -58
- package/src/components/editor/opening-guides-3d-layer.tsx +13 -4
- package/src/components/editor/selection-manager.tsx +132 -47
- package/src/components/editor/site-edge-labels.tsx +2 -1
- package/src/components/editor/snapshot-capture-overlay.tsx +309 -41
- package/src/components/editor/snapshot-capture.test.ts +422 -0
- package/src/components/editor/snapshot-capture.ts +145 -0
- package/src/components/editor/thumbnail-generator.tsx +295 -232
- package/src/components/editor/use-floorplan-background-placement.ts +53 -5
- package/src/components/editor/wall-measurement-label.tsx +3 -2
- package/src/components/editor/wall-move-side-handles.tsx +299 -7
- package/src/components/editor-2d/floorplan-group-action-menu.tsx +23 -16
- package/src/components/editor-2d/floorplan-group-move.tsx +67 -33
- package/src/components/editor-2d/floorplan-registered-tool-layer.tsx +3 -1
- package/src/components/editor-2d/floorplan-registry-move-overlay.tsx +129 -38
- package/src/components/editor-2d/renderers/floorplan-dimension-renderer.tsx +14 -1
- package/src/components/editor-2d/renderers/floorplan-geometry-renderer.test.tsx +16 -0
- package/src/components/editor-2d/renderers/floorplan-geometry-renderer.tsx +2 -0
- package/src/components/editor-2d/renderers/floorplan-placement-preview-layer.tsx +52 -3
- package/src/components/editor-2d/renderers/floorplan-registry-layer.test.ts +196 -1
- package/src/components/editor-2d/renderers/floorplan-registry-layer.tsx +204 -114
- package/src/components/systems/ceiling/ceiling-bracket-batch.test.ts +402 -0
- package/src/components/systems/ceiling/ceiling-bracket-batch.ts +371 -0
- package/src/components/systems/ceiling/ceiling-selection-affordance-system.test.ts +347 -0
- package/src/components/systems/ceiling/ceiling-selection-affordance-system.tsx +281 -305
- package/src/components/systems/roof/roof-edit-system.test.tsx +120 -0
- package/src/components/systems/roof/roof-edit-system.tsx +10 -19
- package/src/components/systems/selection-affordance-manager.tsx +59 -9
- package/src/components/systems/selection-affordance-services.ts +19 -0
- package/src/components/systems/stair/stair-edit-system.tsx +5 -2
- package/src/components/tools/fence/fence-drafting.test.ts +100 -0
- package/src/components/tools/fence/fence-drafting.ts +6 -25
- package/src/components/tools/item/draft-creation.test.ts +12 -0
- package/src/components/tools/item/draft-creation.ts +10 -0
- package/src/components/tools/item/face-host-commit.test.ts +65 -0
- package/src/components/tools/item/face-host-commit.ts +56 -0
- package/src/components/tools/item/face-host-preview.test.ts +77 -0
- package/src/components/tools/item/face-host-preview.ts +82 -0
- package/src/components/tools/item/move-tool.tsx +4 -3
- package/src/components/tools/item/placement-math.test.ts +11 -1
- package/src/components/tools/item/placement-math.ts +0 -20
- package/src/components/tools/item/placement-strategies.test.ts +494 -0
- package/src/components/tools/item/placement-strategies.ts +192 -24
- package/src/components/tools/item/placement-surface.test.ts +61 -0
- package/src/components/tools/item/placement-surface.ts +24 -0
- package/src/components/tools/item/placement-types.ts +6 -0
- package/src/components/tools/item/test-face-host.ts +121 -0
- package/src/components/tools/item/use-draft-node.test.tsx +336 -0
- package/src/components/tools/item/use-draft-node.ts +26 -4
- package/src/components/tools/item/use-placement-coordinator.tsx +447 -180
- package/src/components/tools/registered-tool-install-lifecycle.test.tsx +256 -0
- package/src/components/tools/registry/move-registry-node-tool.test.ts +10 -0
- package/src/components/tools/registry/move-registry-node-tool.tsx +306 -110
- package/src/components/tools/registry-tool-context.tsx +30 -0
- package/src/components/tools/select/box-select-tool.tsx +3 -1
- package/src/components/tools/select/marquee-footprint.test.ts +53 -0
- package/src/components/tools/select/marquee-footprint.ts +29 -0
- package/src/components/tools/shared/placement-box.tsx +63 -8
- package/src/components/tools/shared/placement-dimension-guides.tsx +121 -0
- package/src/components/tools/shared/pointer-support-cap.test.ts +319 -17
- package/src/components/tools/shared/pointer-support-cap.ts +45 -8
- package/src/components/tools/site/terrain-brush-cursor.tsx +2 -1
- package/src/components/tools/site/terrain-sculpt-grid.tsx +9 -5
- package/src/components/tools/site/terrain-sculpt-tool.tsx +1 -1
- package/src/components/tools/stair/stair-tool.tsx +127 -74
- package/src/components/tools/tool-manager.tsx +25 -8
- package/src/components/tools/wall/wall-drafting.test.ts +527 -4
- package/src/components/tools/wall/wall-drafting.ts +55 -462
- package/src/components/ui/action-menu/control-modes.tsx +9 -6
- package/src/components/ui/action-menu/index.tsx +11 -1
- package/src/components/ui/command-palette/editor-commands.tsx +12 -13
- package/src/components/ui/command-palette/index.tsx +7 -2
- package/src/components/ui/controls/material-paint-panel.tsx +6 -4
- package/src/components/ui/controls/material-picker.tsx +4 -4
- package/src/components/ui/controls/scene-material-list.tsx +6 -5
- package/src/components/ui/controls/segmented-control.tsx +8 -1
- package/src/components/ui/controls/slider-control.tsx +19 -3
- package/src/components/ui/controls/toggle-control.tsx +18 -5
- package/src/components/ui/controls/tool-options-panel.tsx +126 -0
- package/src/components/ui/floating-level-selector.tsx +23 -6
- package/src/components/ui/helpers/contextual-helper-panel.tsx +9 -0
- package/src/components/ui/helpers/helper-manager.tsx +47 -11
- package/src/components/ui/helpers/item-helper.tsx +1 -1
- package/src/components/ui/helpers/registered-tool-helper.tsx +21 -0
- package/src/components/ui/panels/homogeneous-selection.test.ts +80 -0
- package/src/components/ui/panels/homogeneous-selection.ts +47 -0
- package/src/components/ui/panels/mobile-selection-bar.tsx +11 -5
- package/src/components/ui/panels/multi-field-value.test.ts +209 -0
- package/src/components/ui/panels/multi-field-value.ts +163 -0
- package/src/components/ui/panels/multi-height-mode.tsx +185 -0
- package/src/components/ui/panels/multi-parametric-inspector.tsx +263 -0
- package/src/components/ui/panels/multi-selection-panel.tsx +78 -26
- package/src/components/ui/panels/node-display.ts +4 -0
- package/src/components/ui/panels/panel-manager.tsx +77 -3
- package/src/components/ui/panels/panel-wrapper.tsx +228 -12
- package/src/components/ui/panels/parametric-field-control.tsx +173 -0
- package/src/components/ui/panels/parametric-field-utils.ts +18 -0
- package/src/components/ui/panels/parametric-inspector.tsx +29 -162
- package/src/components/ui/panels/reference-panel.tsx +31 -1
- package/src/components/ui/primitives/shortcut-token.tsx +23 -1
- package/src/components/ui/scene-loader.tsx +35 -0
- package/src/components/ui/sidebar/panels/items-panel/function-tree-panel.tsx +7 -2
- package/src/components/ui/sidebar/panels/settings-panel/index.tsx +390 -36
- package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +31 -23
- package/src/components/ui/sidebar/panels/settings-panel/print-export-button.test.ts +140 -0
- package/src/components/ui/sidebar/panels/settings-panel/print-export-button.tsx +109 -0
- package/src/components/ui/sidebar/panels/site-panel/dormer-tree-node.tsx +19 -5
- package/src/components/ui/sidebar/panels/site-panel/index.tsx +128 -46
- package/src/components/ui/sidebar/panels/site-panel/level-tree-node.tsx +5 -2
- package/src/components/ui/sidebar/panels/site-panel/registry-tree-node.tsx +28 -11
- package/src/components/ui/sidebar/panels/site-panel/tree-node.test.ts +8 -0
- package/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +22 -7
- package/src/components/ui/sidebar/tab-bar.tsx +9 -1
- package/src/components/ui/sidebar/use-plugin-panels.tsx +17 -2
- package/src/components/viewer/floorplan-compass-button.tsx +50 -0
- package/src/components/viewer/floorplan-preview-geometry.test.ts +107 -0
- package/src/components/viewer/floorplan-preview-geometry.ts +281 -0
- package/src/components/viewer/floorplan-preview-navigation.test.ts +43 -0
- package/src/components/viewer/floorplan-preview-navigation.ts +62 -0
- package/src/components/viewer/floorplan-preview.tsx +1069 -0
- package/src/components/viewer/use-viewer-camera-navigation-sync.ts +140 -0
- package/src/components/viewer/viewer-scene-header.tsx +2 -1
- package/src/components/viewer/viewer-stage-modes.test.ts +38 -0
- package/src/components/viewer/viewer-stage-modes.ts +49 -0
- package/src/components/viewer/viewer-stage-switcher.tsx +95 -0
- package/src/components/viewer/viewer-stage.test.tsx +47 -0
- package/src/components/viewer/viewer-stage.tsx +221 -0
- package/src/components/viewer-overlay.tsx +13 -30
- package/src/components/viewer-zone-system.tsx +6 -2
- package/src/components/walkthrough-hud.tsx +19 -10
- package/src/hooks/use-auto-save.test.ts +122 -1
- package/src/hooks/use-auto-save.ts +119 -13
- package/src/hooks/use-ceiling-events.test.ts +122 -0
- package/src/hooks/use-drag-action.ts +17 -3
- package/src/hooks/use-grid-events.test.ts +240 -0
- package/src/hooks/use-grid-events.ts +190 -11
- package/src/hooks/use-keyboard.test.ts +244 -0
- package/src/hooks/use-keyboard.ts +164 -63
- package/src/hooks/use-registered-tool-enabled.ts +20 -0
- package/src/hooks/use-save-shortcut.ts +33 -0
- package/src/index.tsx +109 -4
- package/src/lib/active-placement-surface.test.ts +16 -0
- package/src/lib/active-placement-surface.ts +14 -0
- package/src/lib/ceiling-surface-raycast.test.ts +64 -0
- package/src/lib/ceiling-surface-raycast.ts +12 -0
- package/src/lib/contextual-help-extension.ts +17 -0
- package/src/lib/contextual-help.test.ts +8 -0
- package/src/lib/contextual-help.ts +8 -0
- package/src/lib/continuation.test.ts +11 -0
- package/src/lib/continuation.ts +8 -1
- package/src/lib/direct-manipulation.test.ts +132 -0
- package/src/lib/direct-manipulation.ts +38 -1
- package/src/lib/editor-api.ts +6 -3
- package/src/lib/elevation-guides.test.ts +28 -1
- package/src/lib/elevation-guides.ts +22 -7
- package/src/lib/floating-menu-scale.test.ts +29 -0
- package/src/lib/floating-menu-scale.ts +14 -0
- package/src/lib/floorplan/apply-alignment.test.ts +47 -2
- package/src/lib/floorplan/apply-alignment.ts +10 -1
- package/src/lib/floorplan/floorplan-export.test.ts +397 -1
- package/src/lib/floorplan/floorplan-export.tsx +111 -16
- package/src/lib/floorplan/floorplan-pdfkit-renderer.test.ts +26 -0
- package/src/lib/floorplan/floorplan-pdfkit-renderer.ts +32 -24
- package/src/lib/floorplan/floorplan-readonly.test.ts +47 -0
- package/src/lib/floorplan/floorplan-readonly.ts +85 -0
- package/src/lib/floorplan-grid-event-point.test.ts +29 -0
- package/src/lib/floorplan-grid-event-point.ts +13 -0
- package/src/lib/glb-export.test.ts +1169 -21
- package/src/lib/glb-export.ts +629 -101
- package/src/lib/grid-event-presentation.ts +16 -0
- package/src/lib/history.test.ts +523 -77
- package/src/lib/history.ts +133 -14
- package/src/lib/host-tree-children.test.ts +27 -0
- package/src/lib/host-tree-children.ts +79 -0
- package/src/lib/inspector-card-mode.test.ts +92 -0
- package/src/lib/inspector-card-mode.ts +60 -0
- package/src/lib/interaction/hot-set.test.ts +10 -1
- package/src/lib/interaction/hot-set.ts +3 -1
- package/src/lib/interaction/overlay-policy.test.ts +33 -1
- package/src/lib/interaction/overlay-policy.ts +16 -0
- package/src/lib/interaction/registered-drafting.ts +36 -0
- package/src/lib/interaction/scope.ts +31 -0
- package/src/lib/level-print-export.test.ts +484 -0
- package/src/lib/level-print-export.ts +526 -0
- package/src/lib/local-project-presentation-persistence.test.ts +302 -0
- package/src/lib/local-project-presentation-persistence.ts +335 -0
- package/src/lib/material-paint.ts +2 -0
- package/src/lib/measurement-parser.ts +3 -0
- package/src/lib/measurements.test.ts +4 -0
- package/src/lib/model-export.ts +29 -0
- package/src/lib/paint-preview-owner.ts +71 -0
- package/src/lib/paint-scope.test.ts +58 -2
- package/src/lib/paint-scope.ts +8 -2
- package/src/lib/planar-cursor-placement.test.ts +178 -1
- package/src/lib/planar-cursor-placement.ts +61 -5
- package/src/lib/plugin-panels.test.ts +92 -1
- package/src/lib/plugin-panels.ts +48 -0
- package/src/lib/portable-export.test.ts +366 -0
- package/src/lib/portable-export.ts +969 -0
- package/src/lib/print-3mf.test.ts +86 -0
- package/src/lib/print-3mf.ts +161 -0
- package/src/lib/print-content-scope.test.ts +107 -0
- package/src/lib/print-content-scope.ts +90 -0
- package/src/lib/print-export.test.ts +294 -0
- package/src/lib/print-export.ts +735 -0
- package/src/lib/print-feature-thickness.test.ts +147 -0
- package/src/lib/print-feature-thickness.ts +180 -0
- package/src/lib/print-golden-house.test-fixture.ts +352 -0
- package/src/lib/print-golden-house.test.ts +249 -0
- package/src/lib/print-roof-solids.test.ts +133 -0
- package/src/lib/print-roof-solids.ts +630 -0
- package/src/lib/print-shell-compiler-baseline.test.ts +419 -0
- package/src/lib/print-shell-compiler-baseline.ts +225 -0
- package/src/lib/print-shell-compiler-manifold-core.ts +330 -0
- package/src/lib/print-shell-compiler-manifold-worker.ts +187 -0
- package/src/lib/print-shell-compiler-manifold.worker.ts +20 -0
- package/src/lib/print-shell-compiler-mesh-data.ts +32 -0
- package/src/lib/print-shell-compiler-protocol.ts +30 -0
- package/src/lib/print-shell-compiler.ts +273 -0
- package/src/lib/print-wall-solids.test.ts +194 -0
- package/src/lib/print-wall-solids.ts +328 -0
- package/src/lib/rigid-plan-svg-transform.test.ts +35 -0
- package/src/lib/rigid-plan-svg-transform.ts +24 -0
- package/src/lib/scene.ts +22 -8
- package/src/lib/selection-routing.test.ts +125 -6
- package/src/lib/selection-routing.ts +34 -1
- package/src/lib/session-groups.test.ts +88 -0
- package/src/lib/session-groups.ts +201 -0
- package/src/lib/sfx-bus.ts +4 -0
- package/src/lib/sfx-player.ts +8 -0
- package/src/lib/snapping-mode.test.ts +32 -4
- package/src/lib/snapping-mode.ts +11 -7
- package/src/lib/structured-clone-fallback.test.ts +45 -0
- package/src/lib/structured-clone-fallback.ts +24 -0
- package/src/lib/usdz-export.ts +38 -0
- package/src/lib/walkthrough-pointer-lock.ts +47 -0
- package/src/store/live-draft-preview-stores.test.ts +3 -0
- package/src/store/terrain-sculpt-mode.test.ts +3 -6
- package/src/store/tool-mode.test.ts +87 -0
- package/src/store/use-camera-hint-focus.ts +48 -0
- package/src/store/use-editor.tsx +299 -188
- package/src/store/use-floorplan-draft-preview.ts +9 -0
- package/src/store/use-interaction-scope.test.ts +23 -1
- package/src/store/use-interaction-scope.ts +63 -2
- package/src/store/use-placement-preview.ts +56 -3
- package/src/store/use-session-groups.ts +103 -0
- package/src/components/tools/roof/roof-tool.tsx +0 -656
- package/src/components/ui/helpers/roof-helper.tsx +0 -14
|
@@ -1,11 +1,24 @@
|
|
|
1
1
|
import { describe, expect, test } from 'bun:test'
|
|
2
|
-
import
|
|
2
|
+
import {
|
|
3
|
+
type AnyNode,
|
|
4
|
+
type AnyNodeDefinition,
|
|
5
|
+
type FloorplanGeometry,
|
|
6
|
+
type GeometryContext,
|
|
7
|
+
loadPlugin,
|
|
8
|
+
type NodeCategory,
|
|
9
|
+
nodeRegistry,
|
|
10
|
+
registerNode,
|
|
11
|
+
} from '@pascal-app/core'
|
|
12
|
+
import PDFDocument from 'pdfkit'
|
|
13
|
+
import { z } from 'zod'
|
|
3
14
|
import { splitFloorplanOverlay } from '../../components/editor-2d/renderers/floorplan-registry-layer'
|
|
4
15
|
import { DEFAULT_FLOORPLAN_ANNOTATION_VISIBILITY } from './annotation-visibility'
|
|
5
16
|
import {
|
|
17
|
+
collectFloorplanGeometry,
|
|
6
18
|
filterFloorplanExportOverlay,
|
|
7
19
|
fitPlanToBox,
|
|
8
20
|
isFloorplanExportAnnotationGeometry,
|
|
21
|
+
isFloorplanNodeInExportScope,
|
|
9
22
|
partitionFloorplanExportOverlay,
|
|
10
23
|
resolveFloorplanExportAnnotationVisibility,
|
|
11
24
|
resolveFloorplanExportNodeGeometry,
|
|
@@ -19,6 +32,63 @@ import {
|
|
|
19
32
|
rotateFloorplanExportBounds,
|
|
20
33
|
} from './floorplan-export'
|
|
21
34
|
import { floorplanGeometryMetadata } from './floorplan-extension'
|
|
35
|
+
import { FloorplanPdfDocument } from './floorplan-pdfkit-document'
|
|
36
|
+
import { renderFloorplanGeometryToPdfKit } from './floorplan-pdfkit-renderer'
|
|
37
|
+
|
|
38
|
+
type GroupGeometry = Extract<FloorplanGeometry, { kind: 'group' }>
|
|
39
|
+
type GroupTransform = NonNullable<GroupGeometry['transform']>
|
|
40
|
+
|
|
41
|
+
function flattenGeometry(geometry: FloorplanGeometry | null): FloorplanGeometry[] {
|
|
42
|
+
if (!geometry) return []
|
|
43
|
+
if (geometry.kind !== 'group') return [geometry]
|
|
44
|
+
return [geometry, ...geometry.children.flatMap(flattenGeometry)]
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function applyGeometryTransforms(
|
|
48
|
+
point: readonly [number, number],
|
|
49
|
+
transforms: readonly GroupTransform[],
|
|
50
|
+
): [number, number] {
|
|
51
|
+
let x = point[0]
|
|
52
|
+
let y = point[1]
|
|
53
|
+
for (let index = transforms.length - 1; index >= 0; index -= 1) {
|
|
54
|
+
const transform = transforms[index]!
|
|
55
|
+
if (transform.rotate !== undefined) {
|
|
56
|
+
const cos = Math.cos(transform.rotate)
|
|
57
|
+
const sin = Math.sin(transform.rotate)
|
|
58
|
+
const rotatedX = x * cos - y * sin
|
|
59
|
+
y = x * sin + y * cos
|
|
60
|
+
x = rotatedX
|
|
61
|
+
}
|
|
62
|
+
if (transform.translate) {
|
|
63
|
+
x += transform.translate[0]
|
|
64
|
+
y += transform.translate[1]
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return [x, y]
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function projectedGeometryPoint(
|
|
71
|
+
geometry: FloorplanGeometry,
|
|
72
|
+
target: 'circle' | 'image',
|
|
73
|
+
transforms: readonly GroupTransform[] = [],
|
|
74
|
+
): [number, number] | null {
|
|
75
|
+
if (geometry.kind === target) {
|
|
76
|
+
const point =
|
|
77
|
+
geometry.kind === 'circle'
|
|
78
|
+
? ([geometry.cx, geometry.cy] as const)
|
|
79
|
+
: geometry.kind === 'image'
|
|
80
|
+
? geometry.center
|
|
81
|
+
: null
|
|
82
|
+
return point ? applyGeometryTransforms(point, transforms) : null
|
|
83
|
+
}
|
|
84
|
+
if (geometry.kind !== 'group') return null
|
|
85
|
+
const nestedTransforms = geometry.transform ? [...transforms, geometry.transform] : transforms
|
|
86
|
+
for (const child of geometry.children) {
|
|
87
|
+
const point = projectedGeometryPoint(child, target, nestedTransforms)
|
|
88
|
+
if (point) return point
|
|
89
|
+
}
|
|
90
|
+
return null
|
|
91
|
+
}
|
|
22
92
|
|
|
23
93
|
describe('filterFloorplanExportOverlay', () => {
|
|
24
94
|
test('preserves annotation metadata while splitting geometry passes', () => {
|
|
@@ -317,3 +387,329 @@ describe('resolveFloorplanPageLayout', () => {
|
|
|
317
387
|
})
|
|
318
388
|
})
|
|
319
389
|
})
|
|
390
|
+
|
|
391
|
+
describe('isFloorplanNodeInExportScope', () => {
|
|
392
|
+
const definition = (category?: NodeCategory) => ({ category })
|
|
393
|
+
|
|
394
|
+
test('includes structure-category nodes under structure and full', () => {
|
|
395
|
+
expect(isFloorplanNodeInExportScope(definition('structure'), 'structure')).toBe(true)
|
|
396
|
+
expect(isFloorplanNodeInExportScope(definition('structure'), 'full')).toBe(true)
|
|
397
|
+
})
|
|
398
|
+
|
|
399
|
+
test('excludes utility-category nodes under structure', () => {
|
|
400
|
+
expect(isFloorplanNodeInExportScope(definition('utility'), 'full')).toBe(true)
|
|
401
|
+
expect(isFloorplanNodeInExportScope(definition('utility'), 'structure')).toBe(false)
|
|
402
|
+
})
|
|
403
|
+
|
|
404
|
+
test('includes furnish-category nodes only under full', () => {
|
|
405
|
+
expect(isFloorplanNodeInExportScope(definition('furnish'), 'full')).toBe(true)
|
|
406
|
+
expect(isFloorplanNodeInExportScope(definition('furnish'), 'structure')).toBe(false)
|
|
407
|
+
})
|
|
408
|
+
|
|
409
|
+
test('includes analysis and site-category nodes only under full', () => {
|
|
410
|
+
for (const category of ['analysis', 'site'] as const) {
|
|
411
|
+
expect(isFloorplanNodeInExportScope(definition(category), 'full')).toBe(true)
|
|
412
|
+
expect(isFloorplanNodeInExportScope(definition(category), 'structure')).toBe(false)
|
|
413
|
+
}
|
|
414
|
+
})
|
|
415
|
+
|
|
416
|
+
test('excludes nodes with no category except under full', () => {
|
|
417
|
+
expect(isFloorplanNodeInExportScope(definition(undefined), 'full')).toBe(true)
|
|
418
|
+
expect(isFloorplanNodeInExportScope(definition(undefined), 'structure')).toBe(false)
|
|
419
|
+
})
|
|
420
|
+
|
|
421
|
+
test('handles an undefined definition like a no-category node', () => {
|
|
422
|
+
expect(isFloorplanNodeInExportScope(undefined, 'full')).toBe(true)
|
|
423
|
+
expect(isFloorplanNodeInExportScope(undefined, 'structure')).toBe(false)
|
|
424
|
+
})
|
|
425
|
+
})
|
|
426
|
+
|
|
427
|
+
describe('collectFloorplanGeometry', () => {
|
|
428
|
+
test('collects the active Site once below architecture with semantic context and projection', async () => {
|
|
429
|
+
const restoreRegistry = nodeRegistry._snapshot()
|
|
430
|
+
const activeSiteId = 'site_active'
|
|
431
|
+
const otherSiteId = 'site_other'
|
|
432
|
+
const enabledPluginId = 'test:site-pdf-enabled'
|
|
433
|
+
const disabledPluginId = 'test:site-pdf-disabled'
|
|
434
|
+
const enabledKind = 'test:site-pdf-overlay'
|
|
435
|
+
const disabledKind = 'test:site-pdf-disabled-overlay'
|
|
436
|
+
const architectureKind = 'test:level-architecture'
|
|
437
|
+
const semanticChildId = 'site_overlay_detail'
|
|
438
|
+
const referencedNodeId = 'site_reference'
|
|
439
|
+
const inlinePng =
|
|
440
|
+
'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+A8AAQUBAScY42YAAAAASUVORK5CYII='
|
|
441
|
+
|
|
442
|
+
const siteDefinition = (
|
|
443
|
+
kind: string,
|
|
444
|
+
floorplan: (node: Record<string, unknown>, context: GeometryContext) => FloorplanGeometry,
|
|
445
|
+
): AnyNodeDefinition =>
|
|
446
|
+
({
|
|
447
|
+
kind,
|
|
448
|
+
schemaVersion: 1,
|
|
449
|
+
schema: z.object({ type: z.literal(kind) }) as never,
|
|
450
|
+
category: 'utility',
|
|
451
|
+
defaults: () => ({}) as never,
|
|
452
|
+
capabilities: {},
|
|
453
|
+
floorplanScope: 'site',
|
|
454
|
+
floorplan,
|
|
455
|
+
}) as AnyNodeDefinition
|
|
456
|
+
|
|
457
|
+
const enabledDefinition = siteDefinition(enabledKind, (siteOverlay, context) => ({
|
|
458
|
+
kind: 'group',
|
|
459
|
+
children: [
|
|
460
|
+
{
|
|
461
|
+
kind: 'rect',
|
|
462
|
+
x: 10,
|
|
463
|
+
y: 3,
|
|
464
|
+
width: context.children[0]?.id === semanticChildId ? 3 : -3,
|
|
465
|
+
height: context.siblings.some(({ id }) => id === 'site_overlay_hidden') ? 4 : -4,
|
|
466
|
+
fill:
|
|
467
|
+
siteOverlay.id === 'site_overlay' &&
|
|
468
|
+
context.parent?.id === activeSiteId &&
|
|
469
|
+
context.resolve(referencedNodeId)?.id === referencedNodeId
|
|
470
|
+
? '#102030'
|
|
471
|
+
: '#ff0000',
|
|
472
|
+
},
|
|
473
|
+
{
|
|
474
|
+
kind: 'path',
|
|
475
|
+
d: 'M0,0H4V4H0ZM1,1H3V3H1Z',
|
|
476
|
+
fill: '#3f6b2f',
|
|
477
|
+
fillRule: 'evenodd',
|
|
478
|
+
},
|
|
479
|
+
{
|
|
480
|
+
kind: 'image',
|
|
481
|
+
url: inlinePng,
|
|
482
|
+
center: [10, 3],
|
|
483
|
+
width: 1,
|
|
484
|
+
height: 1,
|
|
485
|
+
},
|
|
486
|
+
{ kind: 'circle', cx: 12, cy: 5, r: 0.5 },
|
|
487
|
+
],
|
|
488
|
+
}))
|
|
489
|
+
|
|
490
|
+
try {
|
|
491
|
+
nodeRegistry._reset()
|
|
492
|
+
registerNode({
|
|
493
|
+
kind: architectureKind,
|
|
494
|
+
schemaVersion: 1,
|
|
495
|
+
schema: z.object({ type: z.literal(architectureKind) }) as never,
|
|
496
|
+
category: 'structure',
|
|
497
|
+
defaults: () => ({}) as never,
|
|
498
|
+
capabilities: {},
|
|
499
|
+
floorplan: () => ({
|
|
500
|
+
kind: 'polygon',
|
|
501
|
+
points: [
|
|
502
|
+
[0, 0],
|
|
503
|
+
[4, 0],
|
|
504
|
+
[4, 4],
|
|
505
|
+
],
|
|
506
|
+
}),
|
|
507
|
+
} as AnyNodeDefinition)
|
|
508
|
+
await loadPlugin({
|
|
509
|
+
id: enabledPluginId,
|
|
510
|
+
apiVersion: 1,
|
|
511
|
+
nodes: [enabledDefinition],
|
|
512
|
+
})
|
|
513
|
+
await loadPlugin({
|
|
514
|
+
id: disabledPluginId,
|
|
515
|
+
apiVersion: 1,
|
|
516
|
+
nodes: [siteDefinition(disabledKind, () => ({ kind: 'circle', cx: 0, cy: 0, r: 1 }))],
|
|
517
|
+
})
|
|
518
|
+
|
|
519
|
+
const activeSite = {
|
|
520
|
+
id: activeSiteId,
|
|
521
|
+
type: 'site',
|
|
522
|
+
parentId: null,
|
|
523
|
+
visible: true,
|
|
524
|
+
children: [
|
|
525
|
+
'building_active',
|
|
526
|
+
'site_overlay',
|
|
527
|
+
'site_overlay_hidden',
|
|
528
|
+
'site_overlay_disabled',
|
|
529
|
+
],
|
|
530
|
+
} as unknown as AnyNode
|
|
531
|
+
const activeBuilding = {
|
|
532
|
+
id: 'building_active',
|
|
533
|
+
type: 'building',
|
|
534
|
+
parentId: activeSiteId,
|
|
535
|
+
children: ['level_active', 'level_upper'],
|
|
536
|
+
position: [10, 0, 5],
|
|
537
|
+
rotation: [0, Math.PI / 2, 0],
|
|
538
|
+
} as unknown as AnyNode
|
|
539
|
+
const activeLevel = {
|
|
540
|
+
id: 'level_active',
|
|
541
|
+
type: 'level',
|
|
542
|
+
parentId: activeBuilding.id,
|
|
543
|
+
children: ['level_architecture'],
|
|
544
|
+
} as unknown as AnyNode
|
|
545
|
+
const upperLevel = {
|
|
546
|
+
id: 'level_upper',
|
|
547
|
+
type: 'level',
|
|
548
|
+
parentId: activeBuilding.id,
|
|
549
|
+
children: ['level_upper_architecture'],
|
|
550
|
+
} as unknown as AnyNode
|
|
551
|
+
const nodes = {
|
|
552
|
+
[activeSite.id]: activeSite,
|
|
553
|
+
[activeBuilding.id]: activeBuilding,
|
|
554
|
+
[activeLevel.id]: activeLevel,
|
|
555
|
+
[upperLevel.id]: upperLevel,
|
|
556
|
+
level_upper_architecture: {
|
|
557
|
+
id: 'level_upper_architecture',
|
|
558
|
+
type: architectureKind,
|
|
559
|
+
parentId: upperLevel.id,
|
|
560
|
+
visible: true,
|
|
561
|
+
} as unknown as AnyNode,
|
|
562
|
+
level_architecture: {
|
|
563
|
+
id: 'level_architecture',
|
|
564
|
+
type: architectureKind,
|
|
565
|
+
parentId: activeLevel.id,
|
|
566
|
+
visible: true,
|
|
567
|
+
} as unknown as AnyNode,
|
|
568
|
+
site_overlay: {
|
|
569
|
+
id: 'site_overlay',
|
|
570
|
+
type: enabledKind,
|
|
571
|
+
parentId: null,
|
|
572
|
+
children: [semanticChildId],
|
|
573
|
+
visible: true,
|
|
574
|
+
} as unknown as AnyNode,
|
|
575
|
+
[semanticChildId]: {
|
|
576
|
+
id: semanticChildId,
|
|
577
|
+
type: 'test:site-detail',
|
|
578
|
+
parentId: 'site_overlay',
|
|
579
|
+
} as unknown as AnyNode,
|
|
580
|
+
[referencedNodeId]: {
|
|
581
|
+
id: referencedNodeId,
|
|
582
|
+
type: 'test:site-reference',
|
|
583
|
+
parentId: activeSiteId,
|
|
584
|
+
} as unknown as AnyNode,
|
|
585
|
+
site_overlay_hidden: {
|
|
586
|
+
id: 'site_overlay_hidden',
|
|
587
|
+
type: enabledKind,
|
|
588
|
+
parentId: activeSiteId,
|
|
589
|
+
visible: false,
|
|
590
|
+
} as unknown as AnyNode,
|
|
591
|
+
site_overlay_disabled: {
|
|
592
|
+
id: 'site_overlay_disabled',
|
|
593
|
+
type: disabledKind,
|
|
594
|
+
parentId: activeSiteId,
|
|
595
|
+
visible: true,
|
|
596
|
+
} as unknown as AnyNode,
|
|
597
|
+
[otherSiteId]: {
|
|
598
|
+
id: otherSiteId,
|
|
599
|
+
type: 'site',
|
|
600
|
+
parentId: null,
|
|
601
|
+
children: ['site_overlay_other'],
|
|
602
|
+
} as unknown as AnyNode,
|
|
603
|
+
site_overlay_other: {
|
|
604
|
+
id: 'site_overlay_other',
|
|
605
|
+
type: enabledKind,
|
|
606
|
+
parentId: otherSiteId,
|
|
607
|
+
visible: true,
|
|
608
|
+
} as unknown as AnyNode,
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
const full = collectFloorplanGeometry(
|
|
612
|
+
nodes,
|
|
613
|
+
activeLevel.id,
|
|
614
|
+
'full',
|
|
615
|
+
'metric',
|
|
616
|
+
'meters',
|
|
617
|
+
DEFAULT_FLOORPLAN_ANNOTATION_VISIBILITY,
|
|
618
|
+
'floor-plan',
|
|
619
|
+
'finished-faces',
|
|
620
|
+
[enabledPluginId],
|
|
621
|
+
)
|
|
622
|
+
|
|
623
|
+
expect(full.map(({ id }) => id)).toEqual(['site_overlay', 'level_architecture'])
|
|
624
|
+
const siteModel = full[0]?.model
|
|
625
|
+
if (!siteModel) throw new Error('Expected Site geometry')
|
|
626
|
+
const siteParts = flattenGeometry(siteModel)
|
|
627
|
+
expect(siteParts).toEqual(
|
|
628
|
+
expect.arrayContaining([
|
|
629
|
+
expect.objectContaining({
|
|
630
|
+
kind: 'rect',
|
|
631
|
+
width: 3,
|
|
632
|
+
height: 4,
|
|
633
|
+
fill: '#102030',
|
|
634
|
+
}),
|
|
635
|
+
expect.objectContaining({ kind: 'path', fillRule: 'evenodd' }),
|
|
636
|
+
expect.objectContaining({ kind: 'image', url: inlinePng }),
|
|
637
|
+
]),
|
|
638
|
+
)
|
|
639
|
+
expect(projectedGeometryPoint(siteModel, 'image')).toEqual([
|
|
640
|
+
expect.closeTo(2),
|
|
641
|
+
expect.closeTo(0),
|
|
642
|
+
])
|
|
643
|
+
expect(projectedGeometryPoint(siteModel, 'circle')).toEqual([
|
|
644
|
+
expect.closeTo(0),
|
|
645
|
+
expect.closeTo(2),
|
|
646
|
+
])
|
|
647
|
+
|
|
648
|
+
const rawPdf = new PDFDocument({ autoFirstPage: false, compress: false })
|
|
649
|
+
const chunks: Buffer[] = []
|
|
650
|
+
rawPdf.on('data', (chunk: Buffer) => chunks.push(chunk))
|
|
651
|
+
const completedPdf = Promise.withResolvers<string>()
|
|
652
|
+
rawPdf.on('end', () => completedPdf.resolve(Buffer.concat(chunks).toString('latin1')))
|
|
653
|
+
const pdf = new FloorplanPdfDocument(rawPdf, [200, 200])
|
|
654
|
+
pdf.addPage()
|
|
655
|
+
for (const { model } of full) {
|
|
656
|
+
if (!model) continue
|
|
657
|
+
await renderFloorplanGeometryToPdfKit(pdf, model, {
|
|
658
|
+
annotationLayer: false,
|
|
659
|
+
placement: { x: 20, y: 20, width: 100, height: 100 },
|
|
660
|
+
rotationDeg: 0,
|
|
661
|
+
viewport: { x: -4, y: -4, width: 20, height: 20 },
|
|
662
|
+
})
|
|
663
|
+
}
|
|
664
|
+
rawPdf.end()
|
|
665
|
+
const renderedPdf = await completedPdf.promise
|
|
666
|
+
expect(renderedPdf).toMatch(/f\*/)
|
|
667
|
+
expect(renderedPdf).toContain('/Subtype /Image')
|
|
668
|
+
|
|
669
|
+
const upper = collectFloorplanGeometry(
|
|
670
|
+
nodes,
|
|
671
|
+
upperLevel.id,
|
|
672
|
+
'full',
|
|
673
|
+
'metric',
|
|
674
|
+
'meters',
|
|
675
|
+
DEFAULT_FLOORPLAN_ANNOTATION_VISIBILITY,
|
|
676
|
+
'floor-plan',
|
|
677
|
+
'finished-faces',
|
|
678
|
+
[enabledPluginId],
|
|
679
|
+
)
|
|
680
|
+
expect(upper.map(({ id }) => id)).toEqual(['site_overlay', 'level_upper_architecture'])
|
|
681
|
+
|
|
682
|
+
const hiddenSiteNodes = {
|
|
683
|
+
...nodes,
|
|
684
|
+
[activeSite.id]: { ...activeSite, visible: false } as AnyNode,
|
|
685
|
+
}
|
|
686
|
+
const hiddenSite = collectFloorplanGeometry(
|
|
687
|
+
hiddenSiteNodes,
|
|
688
|
+
activeLevel.id,
|
|
689
|
+
'full',
|
|
690
|
+
'metric',
|
|
691
|
+
'meters',
|
|
692
|
+
DEFAULT_FLOORPLAN_ANNOTATION_VISIBILITY,
|
|
693
|
+
'floor-plan',
|
|
694
|
+
'finished-faces',
|
|
695
|
+
[enabledPluginId],
|
|
696
|
+
)
|
|
697
|
+
expect(hiddenSite.map(({ id }) => id)).toEqual(['level_architecture'])
|
|
698
|
+
|
|
699
|
+
const structure = collectFloorplanGeometry(
|
|
700
|
+
nodes,
|
|
701
|
+
activeLevel.id,
|
|
702
|
+
'structure',
|
|
703
|
+
'metric',
|
|
704
|
+
'meters',
|
|
705
|
+
DEFAULT_FLOORPLAN_ANNOTATION_VISIBILITY,
|
|
706
|
+
'floor-plan',
|
|
707
|
+
'finished-faces',
|
|
708
|
+
[enabledPluginId],
|
|
709
|
+
)
|
|
710
|
+
expect(structure.map(({ id }) => id)).toEqual(['level_architecture'])
|
|
711
|
+
} finally {
|
|
712
|
+
restoreRegistry()
|
|
713
|
+
}
|
|
714
|
+
})
|
|
715
|
+
})
|
|
@@ -7,7 +7,9 @@ import {
|
|
|
7
7
|
type FloorplanGeometry,
|
|
8
8
|
type FloorplanPalette,
|
|
9
9
|
type FloorplanPoint,
|
|
10
|
+
isNodeKindEnabled,
|
|
10
11
|
type LiveNodeOverrides,
|
|
12
|
+
type NodeCategory,
|
|
11
13
|
nodeRegistry,
|
|
12
14
|
resolveBuildingForLevel,
|
|
13
15
|
useScene,
|
|
@@ -20,10 +22,13 @@ import { resolveSvgAnnotationCollisions } from '../../components/editor-2d/rende
|
|
|
20
22
|
import { FloorplanGeometryRenderer } from '../../components/editor-2d/renderers/floorplan-geometry-renderer'
|
|
21
23
|
import {
|
|
22
24
|
buildContext,
|
|
25
|
+
collectDirectFloorplanScopeNodes,
|
|
23
26
|
collectFloorplanLinkedLevelNodes,
|
|
24
27
|
floorplanLayerRank,
|
|
25
28
|
getFloorplanLevelData,
|
|
29
|
+
isFloorplanHierarchyVisible,
|
|
26
30
|
isFloorplanNodeVisible,
|
|
31
|
+
siteToFloorplanTransform,
|
|
27
32
|
splitFloorplanOverlay,
|
|
28
33
|
} from '../../components/editor-2d/renderers/floorplan-registry-layer'
|
|
29
34
|
import useDrawingView, { DRAWING_TYPE_OPTIONS } from '../../store/use-drawing-view'
|
|
@@ -69,6 +74,20 @@ import { FLOORPLAN_VIEW_ROTATION_DEG } from './geometry'
|
|
|
69
74
|
*/
|
|
70
75
|
export type FloorplanExportScope = 'full' | 'structure'
|
|
71
76
|
|
|
77
|
+
/**
|
|
78
|
+
* Whether a node belongs in the given export scope. `'full'` short-circuits
|
|
79
|
+
* and admits every node; `'structure'` admits only `structure`-category
|
|
80
|
+
* nodes. An `undefined` definition (unregistered node type) behaves like a
|
|
81
|
+
* node with no category.
|
|
82
|
+
*/
|
|
83
|
+
export function isFloorplanNodeInExportScope(
|
|
84
|
+
definition: { category?: NodeCategory } | undefined,
|
|
85
|
+
scope: FloorplanExportScope,
|
|
86
|
+
): boolean {
|
|
87
|
+
if (scope === 'full') return true
|
|
88
|
+
return definition?.category === 'structure'
|
|
89
|
+
}
|
|
90
|
+
|
|
72
91
|
const SVG_NS = 'http://www.w3.org/2000/svg'
|
|
73
92
|
/** Minimum and proportional margin around the structural drawing bounds. */
|
|
74
93
|
const MIN_PLAN_PADDING_M = 1
|
|
@@ -131,12 +150,27 @@ type ExportGeometry = {
|
|
|
131
150
|
annotations: FloorplanGeometry | null
|
|
132
151
|
}
|
|
133
152
|
|
|
153
|
+
type ExportGeometryContext = {
|
|
154
|
+
children: AnyNode[]
|
|
155
|
+
siblings: AnyNode[]
|
|
156
|
+
parent: AnyNode
|
|
157
|
+
outputTransform: { translate: FloorplanPoint; rotate: number }
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
type ExportGeometryEntry = {
|
|
161
|
+
id: AnyNodeId
|
|
162
|
+
node: AnyNode
|
|
163
|
+
parentOverride?: AnyNode
|
|
164
|
+
context?: ExportGeometryContext
|
|
165
|
+
scopeRank?: number
|
|
166
|
+
}
|
|
167
|
+
|
|
134
168
|
export type FloorplanPageLayout = {
|
|
135
169
|
planBox: { x: number; y: number; width: number; height: number }
|
|
136
170
|
}
|
|
137
171
|
|
|
138
172
|
export async function exportFloorplanPdf(scope: FloorplanExportScope): Promise<void> {
|
|
139
|
-
const nodes = useScene.getState()
|
|
173
|
+
const { nodes, installedPlugins } = useScene.getState()
|
|
140
174
|
const viewer = useViewer.getState()
|
|
141
175
|
const unit = viewer.unit
|
|
142
176
|
const metricNotation = viewer.metricNotation
|
|
@@ -183,6 +217,7 @@ export async function exportFloorplanPdf(scope: FloorplanExportScope): Promise<v
|
|
|
183
217
|
annotationVisibility,
|
|
184
218
|
drawingType,
|
|
185
219
|
wallDimensionReference,
|
|
220
|
+
installedPlugins,
|
|
186
221
|
)
|
|
187
222
|
const schedules = collectFloorplanSchedules(nodes, level.id, unit)
|
|
188
223
|
if (geometries.length === 0 && schedules.length === 0) continue
|
|
@@ -721,7 +756,7 @@ function applyFloorplanViewport(
|
|
|
721
756
|
mounted.svg.insertBefore(background, mounted.svg.firstChild)
|
|
722
757
|
}
|
|
723
758
|
|
|
724
|
-
function collectFloorplanGeometry(
|
|
759
|
+
export function collectFloorplanGeometry(
|
|
725
760
|
nodes: Record<string, AnyNode>,
|
|
726
761
|
levelId: AnyNodeId,
|
|
727
762
|
scope: FloorplanExportScope,
|
|
@@ -730,10 +765,11 @@ function collectFloorplanGeometry(
|
|
|
730
765
|
annotationVisibility: FloorplanAnnotationVisibility,
|
|
731
766
|
drawingType: ConstructionDrawingType,
|
|
732
767
|
wallDimensionReference: FloorplanWallDimensionReference,
|
|
768
|
+
installedPlugins: readonly string[],
|
|
733
769
|
): ExportGeometry[] {
|
|
734
770
|
const noLiveOverrides = new Map<string, LiveNodeOverrides>()
|
|
735
771
|
const levelNodeIdsByType = new Map<string, AnyNodeId[]>()
|
|
736
|
-
const entries:
|
|
772
|
+
const entries: ExportGeometryEntry[] = []
|
|
737
773
|
|
|
738
774
|
const visit = (id: AnyNodeId) => {
|
|
739
775
|
const node = nodes[id]
|
|
@@ -747,7 +783,7 @@ function collectFloorplanGeometry(
|
|
|
747
783
|
if (
|
|
748
784
|
def?.floorplan &&
|
|
749
785
|
isFloorplanNodeVisible(node) &&
|
|
750
|
-
(
|
|
786
|
+
isFloorplanNodeInExportScope(def, scope)
|
|
751
787
|
) {
|
|
752
788
|
const drawingNode = resolveNodeForDrawingType(node, nodes, drawingType)
|
|
753
789
|
if (drawingNode) entries.push({ id, node: drawingNode })
|
|
@@ -758,31 +794,74 @@ function collectFloorplanGeometry(
|
|
|
758
794
|
visit(levelId)
|
|
759
795
|
|
|
760
796
|
const activeLevelNode = nodes[levelId]
|
|
797
|
+
const collectedIds = new Set(entries.map((entry) => entry.id))
|
|
761
798
|
if (activeLevelNode) {
|
|
762
|
-
const collectedIds = new Set(entries.map((entry) => entry.id))
|
|
763
799
|
for (const linked of collectFloorplanLinkedLevelNodes(nodes, levelId, collectedIds)) {
|
|
764
800
|
const definition = nodeRegistry.get(linked.node.type)
|
|
765
|
-
if (
|
|
766
|
-
isFloorplanNodeVisible(linked.node) &&
|
|
767
|
-
(scope === 'full' || definition?.category === 'structure')
|
|
768
|
-
) {
|
|
801
|
+
if (isFloorplanNodeVisible(linked.node) && isFloorplanNodeInExportScope(definition, scope)) {
|
|
769
802
|
const drawingNode = resolveNodeForDrawingType(linked.node, nodes, drawingType)
|
|
770
803
|
if (drawingNode) {
|
|
771
804
|
entries.push({ id: linked.id, node: drawingNode, parentOverride: activeLevelNode })
|
|
805
|
+
collectedIds.add(linked.id)
|
|
772
806
|
}
|
|
773
807
|
}
|
|
774
808
|
}
|
|
775
809
|
}
|
|
776
810
|
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
811
|
+
const buildingId = resolveBuildingForLevel(levelId, nodes as Record<AnyNodeId, AnyNode>)
|
|
812
|
+
const buildingNode = buildingId ? nodes[buildingId] : undefined
|
|
813
|
+
const siteNode =
|
|
814
|
+
buildingNode?.type === 'building' && buildingNode.parentId
|
|
815
|
+
? nodes[buildingNode.parentId as AnyNodeId]
|
|
816
|
+
: undefined
|
|
817
|
+
if (siteNode?.type === 'site' && buildingNode?.type === 'building') {
|
|
818
|
+
const siteScopedNodes = collectDirectFloorplanScopeNodes(nodes, siteNode, 'site')
|
|
819
|
+
const outputTransform = siteToFloorplanTransform(
|
|
820
|
+
buildingNode.position,
|
|
821
|
+
buildingNode.rotation[1],
|
|
822
|
+
)
|
|
823
|
+
for (const node of siteScopedNodes) {
|
|
824
|
+
if (collectedIds.has(node.id) || !isNodeKindEnabled(node.type, installedPlugins)) continue
|
|
825
|
+
const drawingNode = resolveNodeForDrawingType(node, nodes, drawingType)
|
|
826
|
+
if (!drawingNode) continue
|
|
827
|
+
const definition = nodeRegistry.get(drawingNode.type)
|
|
828
|
+
if (
|
|
829
|
+
!definition?.floorplan ||
|
|
830
|
+
!isFloorplanNodeInExportScope(definition, scope) ||
|
|
831
|
+
!isFloorplanHierarchyVisible(drawingNode, nodes, noLiveOverrides, siteNode.id)
|
|
832
|
+
) {
|
|
833
|
+
continue
|
|
834
|
+
}
|
|
835
|
+
const childIds = 'children' in node ? node.children : undefined
|
|
836
|
+
const children = Array.isArray(childIds)
|
|
837
|
+
? childIds.map((childId) => nodes[childId]).filter((child): child is AnyNode => !!child)
|
|
838
|
+
: []
|
|
839
|
+
const siblings = siteScopedNodes.filter(
|
|
840
|
+
(candidate) => candidate.id !== node.id && candidate.type === node.type,
|
|
841
|
+
)
|
|
842
|
+
entries.push({
|
|
843
|
+
id: node.id,
|
|
844
|
+
node: drawingNode,
|
|
845
|
+
context: { children, siblings, parent: siteNode, outputTransform },
|
|
846
|
+
scopeRank: -1,
|
|
847
|
+
})
|
|
848
|
+
collectedIds.add(node.id)
|
|
849
|
+
}
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
// Document order is paint order. Site context sits below level
|
|
853
|
+
// architecture, then each scope retains the live layer's z-order.
|
|
854
|
+
entries.sort(
|
|
855
|
+
(a, b) =>
|
|
856
|
+
(a.scopeRank ?? 0) - (b.scopeRank ?? 0) ||
|
|
857
|
+
floorplanLayerRank(a.node.type) - floorplanLayerRank(b.node.type),
|
|
858
|
+
)
|
|
780
859
|
|
|
781
860
|
// One-shot per-type cache for `computeFloorplanLevelData`; value type is
|
|
782
861
|
// module-private to the registry layer, so let it infer.
|
|
783
862
|
const levelDataCache = new Map()
|
|
784
863
|
const out: ExportGeometry[] = []
|
|
785
|
-
for (const { id, node, parentOverride } of entries) {
|
|
864
|
+
for (const { id, node, parentOverride, context } of entries) {
|
|
786
865
|
const builder = nodeRegistry.get(node.type)?.floorplan
|
|
787
866
|
if (!builder) continue
|
|
788
867
|
const levelData = getFloorplanLevelData(
|
|
@@ -803,9 +882,25 @@ function collectFloorplanGeometry(
|
|
|
803
882
|
),
|
|
804
883
|
levelData,
|
|
805
884
|
)
|
|
806
|
-
const ctx =
|
|
807
|
-
|
|
808
|
-
|
|
885
|
+
const ctx = context
|
|
886
|
+
? {
|
|
887
|
+
...baseContext,
|
|
888
|
+
children: context.children,
|
|
889
|
+
siblings: context.siblings,
|
|
890
|
+
parent: context.parent,
|
|
891
|
+
}
|
|
892
|
+
: parentOverride
|
|
893
|
+
? { ...baseContext, parent: parentOverride }
|
|
894
|
+
: baseContext
|
|
895
|
+
const builtGeometry = builder(node, ctx)
|
|
896
|
+
if (!builtGeometry) continue
|
|
897
|
+
const geometry = context
|
|
898
|
+
? {
|
|
899
|
+
kind: 'group' as const,
|
|
900
|
+
children: [builtGeometry],
|
|
901
|
+
transform: context.outputTransform,
|
|
902
|
+
}
|
|
903
|
+
: builtGeometry
|
|
809
904
|
const visibleGeometry = filterFloorplanAnnotationGeometry(geometry, annotationVisibility)
|
|
810
905
|
if (!visibleGeometry) continue
|
|
811
906
|
const { base, overlay } = splitFloorplanOverlay(visibleGeometry)
|
|
@@ -85,6 +85,32 @@ describe('renderFloorplanGeometryToPdfKit', () => {
|
|
|
85
85
|
expect([...new Set(baseFonts)]).toEqual(['Courier'])
|
|
86
86
|
expect([...new Set(fontSizes)]).toEqual(['1.6'])
|
|
87
87
|
})
|
|
88
|
+
test('uses even-odd fill for compound plugin paths', async () => {
|
|
89
|
+
const geometry = {
|
|
90
|
+
kind: 'path',
|
|
91
|
+
d: 'M0,0H4V4H0ZM1,1H3V3H1Z',
|
|
92
|
+
fill: '#3f6b2f',
|
|
93
|
+
fillRule: 'evenodd',
|
|
94
|
+
} satisfies FloorplanGeometry
|
|
95
|
+
|
|
96
|
+
const pdf = await renderTestPdf(geometry)
|
|
97
|
+
|
|
98
|
+
expect(pdf).toMatch(/f\*/)
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
test('writes a data-url PNG image without resolving it as an asset', async () => {
|
|
102
|
+
const geometry = {
|
|
103
|
+
kind: 'image',
|
|
104
|
+
url: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+A8AAQUBAScY42YAAAAASUVORK5CYII=',
|
|
105
|
+
center: [2, 2],
|
|
106
|
+
width: 1,
|
|
107
|
+
height: 1,
|
|
108
|
+
} satisfies FloorplanGeometry
|
|
109
|
+
|
|
110
|
+
const pdf = await renderTestPdf(geometry)
|
|
111
|
+
|
|
112
|
+
expect(pdf).toContain('/Subtype /Image')
|
|
113
|
+
})
|
|
88
114
|
})
|
|
89
115
|
|
|
90
116
|
async function renderTestPdf(geometry: FloorplanGeometry, rotationDeg = 0): Promise<string> {
|