@pascal-app/editor 1.0.0-beta.5 → 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 +13 -7
- package/scripts/generate-print-golden-house.ts +89 -0
- 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 +9 -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 +180 -12
- package/src/components/editor/floating-action-menu.tsx +42 -51
- package/src/components/editor/floorplan-panel.tsx +76 -12
- package/src/components/editor/grid.tsx +12 -7
- package/src/components/editor/group-actions.ts +34 -50
- 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 +140 -21
- package/src/components/editor/node-action-menu.tsx +14 -1
- package/src/components/editor/node-arrow-handles.tsx +47 -58
- package/src/components/editor/selection-manager.tsx +104 -42
- package/src/components/editor/snapshot-capture-overlay.tsx +298 -39
- 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 +284 -237
- package/src/components/editor/use-floorplan-background-placement.ts +53 -5
- package/src/components/editor/wall-move-side-handles.tsx +299 -7
- 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 +180 -32
- 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 +331 -4
- package/src/components/tools/item/placement-strategies.ts +135 -2
- 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 +428 -153
- 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 +57 -7
- 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-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 +320 -2
- package/src/components/tools/wall/wall-drafting.ts +23 -114
- 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 +9 -12
- 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 +22 -5
- 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 +48 -37
- 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/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/settings-panel/index.tsx +390 -36
- package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +20 -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 +116 -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 +14 -6
- package/src/components/ui/sidebar/tab-bar.tsx +2 -0
- package/src/components/ui/sidebar/use-plugin-panels.tsx +17 -2
- package/src/components/viewer/viewer-scene-header.tsx +2 -1
- package/src/components/viewer/viewer-stage.tsx +4 -1
- package/src/components/viewer-overlay.tsx +1 -22
- 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 +73 -1
- package/src/hooks/use-auto-save.ts +76 -5
- 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 +129 -62
- package/src/hooks/use-registered-tool-enabled.ts +20 -0
- package/src/hooks/use-save-shortcut.ts +33 -0
- package/src/index.tsx +82 -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/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.ts +1 -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 +528 -147
- package/src/lib/history.ts +81 -7
- package/src/lib/host-tree-children.test.ts +27 -0
- package/src/lib/host-tree-children.ts +79 -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/model-export.ts +29 -0
- package/src/lib/paint-preview-owner.ts +71 -0
- 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 +105 -1
- package/src/lib/selection-routing.ts +19 -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/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/components/tools/roof/roof-tool.tsx +0 -656
- package/src/components/ui/helpers/roof-helper.tsx +0 -14
|
@@ -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> {
|
|
@@ -191,6 +191,7 @@ function paintStyledGeometry(
|
|
|
191
191
|
const opacity = geometry.opacity ?? 1
|
|
192
192
|
const fillOpacity = (geometry.fillOpacity ?? 1) * opacity
|
|
193
193
|
const strokeOpacity = (geometry.strokeOpacity ?? 1) * opacity
|
|
194
|
+
const fillRule = geometry.fillRule === 'evenodd' ? 'even-odd' : 'non-zero'
|
|
194
195
|
|
|
195
196
|
if (fill) raw.fillColor(fill).fillOpacity(fillOpacity)
|
|
196
197
|
if (stroke) {
|
|
@@ -206,8 +207,8 @@ function paintStyledGeometry(
|
|
|
206
207
|
)
|
|
207
208
|
}
|
|
208
209
|
|
|
209
|
-
if (fill && stroke) raw.fillAndStroke(fill, stroke)
|
|
210
|
-
else if (fill) raw.fill(fill)
|
|
210
|
+
if (fill && stroke) raw.fillAndStroke(fill, stroke, fillRule)
|
|
211
|
+
else if (fill) raw.fill(fill, fillRule)
|
|
211
212
|
else if (stroke) raw.stroke(stroke)
|
|
212
213
|
}
|
|
213
214
|
|
|
@@ -620,29 +621,36 @@ async function drawImage(
|
|
|
620
621
|
doc: FloorplanPdfDocument,
|
|
621
622
|
geometry: Extract<FloorplanGeometry, { kind: 'image' }>,
|
|
622
623
|
): Promise<void> {
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
if (
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
fit: [geometry.width, geometry.height] as [number, number],
|
|
638
|
-
align: 'center' as const,
|
|
639
|
-
valign: 'center' as const,
|
|
640
|
-
}
|
|
641
|
-
raw.image(dataUrl, -geometry.width / 2, -geometry.height / 2, options)
|
|
642
|
-
raw.restore()
|
|
643
|
-
} catch {
|
|
644
|
-
return
|
|
624
|
+
const source = geometry.url.startsWith('data:') ? geometry.url : await loadAssetUrl(geometry.url)
|
|
625
|
+
if (!source) {
|
|
626
|
+
throw new Error('[floorplan-export] Could not resolve a floorplan image')
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
let dataUrl = source
|
|
630
|
+
if (!source.startsWith('data:')) {
|
|
631
|
+
const response = await fetch(source)
|
|
632
|
+
if (!response.ok) {
|
|
633
|
+
throw new Error(
|
|
634
|
+
`[floorplan-export] Could not load a floorplan image (HTTP ${response.status})`,
|
|
635
|
+
)
|
|
636
|
+
}
|
|
637
|
+
dataUrl = await blobToDataUrl(await response.blob())
|
|
645
638
|
}
|
|
639
|
+
|
|
640
|
+
const raw = doc.raw
|
|
641
|
+
raw.save().translate(geometry.center[0], geometry.center[1])
|
|
642
|
+
if (geometry.rotation) raw.rotate((geometry.rotation * 180) / Math.PI)
|
|
643
|
+
raw.opacity(geometry.opacity ?? 1)
|
|
644
|
+
const options =
|
|
645
|
+
geometry.preserveAspectRatio === 'none'
|
|
646
|
+
? { width: geometry.width, height: geometry.height }
|
|
647
|
+
: {
|
|
648
|
+
fit: [geometry.width, geometry.height] as [number, number],
|
|
649
|
+
align: 'center' as const,
|
|
650
|
+
valign: 'center' as const,
|
|
651
|
+
}
|
|
652
|
+
raw.image(dataUrl, -geometry.width / 2, -geometry.height / 2, options)
|
|
653
|
+
raw.restore()
|
|
646
654
|
}
|
|
647
655
|
|
|
648
656
|
function blobToDataUrl(blob: Blob): Promise<string> {
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test'
|
|
2
|
+
import { resolveGenericFloorplanGridEventPoint } from './floorplan-grid-event-point'
|
|
3
|
+
|
|
4
|
+
const snapHalf = ([x, z]: [number, number]): [number, number] => [
|
|
5
|
+
Math.round(x / 0.5) * 0.5,
|
|
6
|
+
Math.round(z / 0.5) * 0.5,
|
|
7
|
+
]
|
|
8
|
+
|
|
9
|
+
describe('resolveGenericFloorplanGridEventPoint', () => {
|
|
10
|
+
test('passes the raw pointer to registry tools so attachment can win before grid', () => {
|
|
11
|
+
expect(
|
|
12
|
+
resolveGenericFloorplanGridEventPoint({
|
|
13
|
+
point: [0.73, 0.32],
|
|
14
|
+
registryToolOwnsSnapping: true,
|
|
15
|
+
snap: snapHalf,
|
|
16
|
+
}),
|
|
17
|
+
).toEqual([0.73, 0.32])
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
test('keeps the floorplan snap for interactions without a registry-owned resolver', () => {
|
|
21
|
+
expect(
|
|
22
|
+
resolveGenericFloorplanGridEventPoint({
|
|
23
|
+
point: [0.73, 0.32],
|
|
24
|
+
registryToolOwnsSnapping: false,
|
|
25
|
+
snap: snapHalf,
|
|
26
|
+
}),
|
|
27
|
+
).toEqual([0.5, 0.5])
|
|
28
|
+
})
|
|
29
|
+
})
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export type FloorplanGridEventPoint = [number, number]
|
|
2
|
+
|
|
3
|
+
export function resolveGenericFloorplanGridEventPoint({
|
|
4
|
+
point,
|
|
5
|
+
registryToolOwnsSnapping,
|
|
6
|
+
snap,
|
|
7
|
+
}: {
|
|
8
|
+
point: FloorplanGridEventPoint
|
|
9
|
+
registryToolOwnsSnapping: boolean
|
|
10
|
+
snap: (point: FloorplanGridEventPoint) => FloorplanGridEventPoint
|
|
11
|
+
}): FloorplanGridEventPoint {
|
|
12
|
+
return registryToolOwnsSnapping ? point : snap(point)
|
|
13
|
+
}
|