@pascal-app/editor 1.0.0-beta.5 → 1.0.1
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/bake-exporter.tsx +5 -0
- package/src/components/editor/bake-thumbnail.tsx +4 -1
- 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 +55 -51
- package/src/components/editor/floorplan-panel.tsx +99 -15
- 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/handle-placement.test.ts +111 -0
- package/src/components/editor/handles/handle-placement.ts +69 -0
- package/src/components/editor/handles/linear-resize-drag.ts +78 -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 +23 -3
- package/src/components/editor/index.tsx +151 -24
- package/src/components/editor/node-action-menu.tsx +14 -1
- package/src/components/editor/node-arrow-handles.tsx +67 -119
- package/src/components/editor/selection-manager.tsx +153 -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 +295 -239
- 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-action-menu.tsx +12 -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 +226 -35
- package/src/components/procedural-items/inspector.tsx +223 -0
- 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/ceiling/ceiling-system.tsx +3 -1
- 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/item-surface-placement.test.ts +175 -0
- package/src/components/tools/item/item-surface-strategy-resolver.test.ts +210 -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 +304 -171
- 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 +10 -0
- package/src/components/tools/item/shelf-surface-strategy-resolver.test.ts +257 -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 +116 -44
- package/src/components/tools/item/use-placement-coordinator.tsx +672 -256
- package/src/components/tools/registered-tool-install-lifecycle.test.tsx +256 -0
- package/src/components/tools/registry/item-surface-child-frame.test.ts +90 -0
- package/src/components/tools/registry/item-surface-move.test.ts +501 -0
- package/src/components/tools/registry/item-surface-move.ts +441 -0
- package/src/components/tools/registry/item-surface-resolver.test.ts +257 -0
- package/src/components/tools/registry/move-registry-node-tool.test.ts +10 -0
- package/src/components/tools/registry/move-registry-node-tool.tsx +505 -164
- 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 +56 -17
- package/src/components/tools/shared/shelf-stickiness.ts +54 -0
- package/src/components/tools/shared/surface-hit.test.ts +56 -0
- package/src/components/tools/shared/surface-hit.ts +23 -0
- package/src/components/tools/shared/surface-rejection.test.tsx +106 -0
- package/src/components/tools/shared/surface-rejection.tsx +29 -0
- 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/tools/zone/zone-tool.tsx +13 -4
- 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 +63 -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/metric-control.tsx +29 -13
- 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 +44 -21
- 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 +103 -10
- 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 +266 -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 +35 -164
- package/src/components/ui/panels/reference-panel.tsx +34 -4
- package/src/components/ui/primitives/checkbox.tsx +34 -0
- 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 +502 -31
- 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 +167 -0
- package/src/components/ui/sidebar/panels/settings-panel/print-export-button.tsx +128 -0
- package/src/components/ui/sidebar/panels/site-panel/building-tree-node.tsx +8 -1
- package/src/components/ui/sidebar/panels/site-panel/dormer-tree-node.tsx +19 -5
- package/src/components/ui/sidebar/panels/site-panel/index.tsx +222 -50
- package/src/components/ui/sidebar/panels/site-panel/item-tree-node.tsx +2 -7
- 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/shelf-tree-node.tsx +3 -8
- 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 +20 -7
- package/src/components/ui/sidebar/panels/site-panel/unit-tree-node.tsx +224 -0
- package/src/components/ui/sidebar/panels/site-panel/zone-membership-checkbox.tsx +22 -0
- package/src/components/ui/sidebar/panels/site-panel/zone-tree-node.tsx +24 -7
- package/src/components/ui/sidebar/tab-bar.tsx +2 -0
- package/src/components/ui/sidebar/use-plugin-panels.tsx +17 -2
- package/src/components/ui/snap-target-badge.tsx +2 -0
- package/src/components/viewer/viewer-scene-header.tsx +5 -1
- package/src/components/viewer/viewer-stage.tsx +4 -1
- package/src/components/viewer/viewer-units-panel.tsx +116 -0
- 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 +144 -0
- package/src/hooks/use-ceiling-events.ts +5 -1
- 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 +132 -63
- package/src/hooks/use-registered-tool-enabled.ts +20 -0
- package/src/hooks/use-save-shortcut.ts +33 -0
- package/src/index.tsx +87 -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/export-texture-utils.test.ts +167 -0
- package/src/lib/export-texture-utils.ts +121 -0
- package/src/lib/export-user-data.test.ts +48 -0
- package/src/lib/export-user-data.ts +69 -0
- 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 +536 -2
- package/src/lib/floorplan/floorplan-export.tsx +120 -19
- 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 +5 -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/fresh-planar-placement.ts +13 -5
- package/src/lib/glb-export-serialize.test.ts +280 -0
- package/src/lib/glb-export.test.ts +1375 -21
- package/src/lib/glb-export.ts +781 -106
- 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-duplication.test.ts +73 -0
- package/src/lib/level-duplication.ts +32 -2
- package/src/lib/level-print-export.test.ts +484 -0
- package/src/lib/level-print-export.ts +526 -0
- package/src/lib/linear-display.test.ts +59 -0
- package/src/lib/linear-display.ts +39 -0
- package/src/lib/local-guide-image.test.ts +24 -0
- package/src/lib/local-guide-image.ts +34 -1
- 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/node-action-movement.ts +21 -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 +455 -0
- package/src/lib/portable-export.ts +1042 -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 +379 -0
- package/src/lib/print-shell-compiler-manifold-worker.ts +200 -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 +38 -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-clipboard.ts +1 -1
- 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/send-to-app.test.ts +108 -0
- package/src/lib/send-to-app.ts +121 -0
- package/src/lib/sfx/movement-tick.ts +37 -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/surface-attachment.ts +77 -0
- package/src/lib/units.ts +254 -0
- package/src/lib/usdz-export.ts +65 -0
- package/src/lib/use-linear-display.ts +8 -32
- 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-handle-group.ts +4 -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-send-to-app.ts +21 -0
- 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,8 +217,9 @@ export async function exportFloorplanPdf(scope: FloorplanExportScope): Promise<v
|
|
|
183
217
|
annotationVisibility,
|
|
184
218
|
drawingType,
|
|
185
219
|
wallDimensionReference,
|
|
220
|
+
installedPlugins,
|
|
186
221
|
)
|
|
187
|
-
const schedules = collectFloorplanSchedules(nodes, level.id, unit)
|
|
222
|
+
const schedules = collectFloorplanSchedules(nodes, level.id, unit, scope)
|
|
188
223
|
if (geometries.length === 0 && schedules.length === 0) continue
|
|
189
224
|
const layout = resolveFloorplanPageLayout(A4_LANDSCAPE_WIDTH_PT, A4_LANDSCAPE_HEIGHT_PT)
|
|
190
225
|
|
|
@@ -270,6 +305,7 @@ export function collectFloorplanSchedules(
|
|
|
270
305
|
nodes: Record<string, AnyNode>,
|
|
271
306
|
levelId: AnyNodeId,
|
|
272
307
|
unit: 'metric' | 'imperial',
|
|
308
|
+
scope: FloorplanExportScope = 'full',
|
|
273
309
|
): FloorplanSchedule[] {
|
|
274
310
|
const siblingsByType = new Map<string, AnyNode[]>()
|
|
275
311
|
const visit = (id: AnyNodeId) => {
|
|
@@ -289,6 +325,9 @@ export function collectFloorplanSchedules(
|
|
|
289
325
|
for (const [kind, definition] of nodeRegistry.entries()) {
|
|
290
326
|
const scheduleContribution = getFloorplanNodeExtension(definition)?.schedule
|
|
291
327
|
if (!scheduleContribution) continue
|
|
328
|
+
// Same scope gate as geometry: a structure-only PDF must not list rooms
|
|
329
|
+
// or other site-category contributors whose plan geometry was excluded.
|
|
330
|
+
if (!isFloorplanNodeInExportScope(definition, scope)) continue
|
|
292
331
|
const siblings = siblingsByType.get(kind) ?? []
|
|
293
332
|
const schedule = scheduleContribution({ siblings, nodes, levelId, unit })
|
|
294
333
|
if (schedule && schedule.rows.length > 0) schedules.push(schedule)
|
|
@@ -721,7 +760,7 @@ function applyFloorplanViewport(
|
|
|
721
760
|
mounted.svg.insertBefore(background, mounted.svg.firstChild)
|
|
722
761
|
}
|
|
723
762
|
|
|
724
|
-
function collectFloorplanGeometry(
|
|
763
|
+
export function collectFloorplanGeometry(
|
|
725
764
|
nodes: Record<string, AnyNode>,
|
|
726
765
|
levelId: AnyNodeId,
|
|
727
766
|
scope: FloorplanExportScope,
|
|
@@ -730,10 +769,11 @@ function collectFloorplanGeometry(
|
|
|
730
769
|
annotationVisibility: FloorplanAnnotationVisibility,
|
|
731
770
|
drawingType: ConstructionDrawingType,
|
|
732
771
|
wallDimensionReference: FloorplanWallDimensionReference,
|
|
772
|
+
installedPlugins: readonly string[],
|
|
733
773
|
): ExportGeometry[] {
|
|
734
774
|
const noLiveOverrides = new Map<string, LiveNodeOverrides>()
|
|
735
775
|
const levelNodeIdsByType = new Map<string, AnyNodeId[]>()
|
|
736
|
-
const entries:
|
|
776
|
+
const entries: ExportGeometryEntry[] = []
|
|
737
777
|
|
|
738
778
|
const visit = (id: AnyNodeId) => {
|
|
739
779
|
const node = nodes[id]
|
|
@@ -747,7 +787,7 @@ function collectFloorplanGeometry(
|
|
|
747
787
|
if (
|
|
748
788
|
def?.floorplan &&
|
|
749
789
|
isFloorplanNodeVisible(node) &&
|
|
750
|
-
(
|
|
790
|
+
isFloorplanNodeInExportScope(def, scope)
|
|
751
791
|
) {
|
|
752
792
|
const drawingNode = resolveNodeForDrawingType(node, nodes, drawingType)
|
|
753
793
|
if (drawingNode) entries.push({ id, node: drawingNode })
|
|
@@ -758,31 +798,74 @@ function collectFloorplanGeometry(
|
|
|
758
798
|
visit(levelId)
|
|
759
799
|
|
|
760
800
|
const activeLevelNode = nodes[levelId]
|
|
801
|
+
const collectedIds = new Set(entries.map((entry) => entry.id))
|
|
761
802
|
if (activeLevelNode) {
|
|
762
|
-
const collectedIds = new Set(entries.map((entry) => entry.id))
|
|
763
803
|
for (const linked of collectFloorplanLinkedLevelNodes(nodes, levelId, collectedIds)) {
|
|
764
804
|
const definition = nodeRegistry.get(linked.node.type)
|
|
765
|
-
if (
|
|
766
|
-
isFloorplanNodeVisible(linked.node) &&
|
|
767
|
-
(scope === 'full' || definition?.category === 'structure')
|
|
768
|
-
) {
|
|
805
|
+
if (isFloorplanNodeVisible(linked.node) && isFloorplanNodeInExportScope(definition, scope)) {
|
|
769
806
|
const drawingNode = resolveNodeForDrawingType(linked.node, nodes, drawingType)
|
|
770
807
|
if (drawingNode) {
|
|
771
808
|
entries.push({ id: linked.id, node: drawingNode, parentOverride: activeLevelNode })
|
|
809
|
+
collectedIds.add(linked.id)
|
|
772
810
|
}
|
|
773
811
|
}
|
|
774
812
|
}
|
|
775
813
|
}
|
|
776
814
|
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
815
|
+
const buildingId = resolveBuildingForLevel(levelId, nodes as Record<AnyNodeId, AnyNode>)
|
|
816
|
+
const buildingNode = buildingId ? nodes[buildingId] : undefined
|
|
817
|
+
const siteNode =
|
|
818
|
+
buildingNode?.type === 'building' && buildingNode.parentId
|
|
819
|
+
? nodes[buildingNode.parentId as AnyNodeId]
|
|
820
|
+
: undefined
|
|
821
|
+
if (siteNode?.type === 'site' && buildingNode?.type === 'building') {
|
|
822
|
+
const siteScopedNodes = collectDirectFloorplanScopeNodes(nodes, siteNode, 'site')
|
|
823
|
+
const outputTransform = siteToFloorplanTransform(
|
|
824
|
+
buildingNode.position,
|
|
825
|
+
buildingNode.rotation[1],
|
|
826
|
+
)
|
|
827
|
+
for (const node of siteScopedNodes) {
|
|
828
|
+
if (collectedIds.has(node.id) || !isNodeKindEnabled(node.type, installedPlugins)) continue
|
|
829
|
+
const drawingNode = resolveNodeForDrawingType(node, nodes, drawingType)
|
|
830
|
+
if (!drawingNode) continue
|
|
831
|
+
const definition = nodeRegistry.get(drawingNode.type)
|
|
832
|
+
if (
|
|
833
|
+
!definition?.floorplan ||
|
|
834
|
+
!isFloorplanNodeInExportScope(definition, scope) ||
|
|
835
|
+
!isFloorplanHierarchyVisible(drawingNode, nodes, noLiveOverrides, siteNode.id)
|
|
836
|
+
) {
|
|
837
|
+
continue
|
|
838
|
+
}
|
|
839
|
+
const childIds = 'children' in node ? node.children : undefined
|
|
840
|
+
const children = Array.isArray(childIds)
|
|
841
|
+
? childIds.map((childId) => nodes[childId]).filter((child): child is AnyNode => !!child)
|
|
842
|
+
: []
|
|
843
|
+
const siblings = siteScopedNodes.filter(
|
|
844
|
+
(candidate) => candidate.id !== node.id && candidate.type === node.type,
|
|
845
|
+
)
|
|
846
|
+
entries.push({
|
|
847
|
+
id: node.id,
|
|
848
|
+
node: drawingNode,
|
|
849
|
+
context: { children, siblings, parent: siteNode, outputTransform },
|
|
850
|
+
scopeRank: -1,
|
|
851
|
+
})
|
|
852
|
+
collectedIds.add(node.id)
|
|
853
|
+
}
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
// Document order is paint order. Site context sits below level
|
|
857
|
+
// architecture, then each scope retains the live layer's z-order.
|
|
858
|
+
entries.sort(
|
|
859
|
+
(a, b) =>
|
|
860
|
+
(a.scopeRank ?? 0) - (b.scopeRank ?? 0) ||
|
|
861
|
+
floorplanLayerRank(a.node.type) - floorplanLayerRank(b.node.type),
|
|
862
|
+
)
|
|
780
863
|
|
|
781
864
|
// One-shot per-type cache for `computeFloorplanLevelData`; value type is
|
|
782
865
|
// module-private to the registry layer, so let it infer.
|
|
783
866
|
const levelDataCache = new Map()
|
|
784
867
|
const out: ExportGeometry[] = []
|
|
785
|
-
for (const { id, node, parentOverride } of entries) {
|
|
868
|
+
for (const { id, node, parentOverride, context } of entries) {
|
|
786
869
|
const builder = nodeRegistry.get(node.type)?.floorplan
|
|
787
870
|
if (!builder) continue
|
|
788
871
|
const levelData = getFloorplanLevelData(
|
|
@@ -803,9 +886,25 @@ function collectFloorplanGeometry(
|
|
|
803
886
|
),
|
|
804
887
|
levelData,
|
|
805
888
|
)
|
|
806
|
-
const ctx =
|
|
807
|
-
|
|
808
|
-
|
|
889
|
+
const ctx = context
|
|
890
|
+
? {
|
|
891
|
+
...baseContext,
|
|
892
|
+
children: context.children,
|
|
893
|
+
siblings: context.siblings,
|
|
894
|
+
parent: context.parent,
|
|
895
|
+
}
|
|
896
|
+
: parentOverride
|
|
897
|
+
? { ...baseContext, parent: parentOverride }
|
|
898
|
+
: baseContext
|
|
899
|
+
const builtGeometry = builder(node, ctx)
|
|
900
|
+
if (!builtGeometry) continue
|
|
901
|
+
const geometry = context
|
|
902
|
+
? {
|
|
903
|
+
kind: 'group' as const,
|
|
904
|
+
children: [builtGeometry],
|
|
905
|
+
transform: context.outputTransform,
|
|
906
|
+
}
|
|
907
|
+
: builtGeometry
|
|
809
908
|
const visibleGeometry = filterFloorplanAnnotationGeometry(geometry, annotationVisibility)
|
|
810
909
|
if (!visibleGeometry) continue
|
|
811
910
|
const { base, overlay } = splitFloorplanOverlay(visibleGeometry)
|
|
@@ -926,9 +1025,10 @@ function combineGeometryList(
|
|
|
926
1025
|
* Levels to export, ordered bottom-to-top. The active building (the building
|
|
927
1026
|
* owning the selected level, or the first one found) contributes all of its
|
|
928
1027
|
* level children; if there is no building wrapper we fall back to the single
|
|
929
|
-
* resolved level.
|
|
1028
|
+
* resolved level. Roof support levels are excluded: they are not occupied
|
|
1029
|
+
* stories, so they do not get a floor-plan page.
|
|
930
1030
|
*/
|
|
931
|
-
function resolveExportLevels(nodes: Record<string, AnyNode>): ExportLevel[] {
|
|
1031
|
+
export function resolveExportLevels(nodes: Record<string, AnyNode>): ExportLevel[] {
|
|
932
1032
|
const selected = useViewer.getState().selection.levelId as AnyNodeId | null | undefined
|
|
933
1033
|
const activeLevelId = selected && nodes[selected] ? selected : firstLevelId(nodes)
|
|
934
1034
|
if (!activeLevelId) return []
|
|
@@ -943,6 +1043,7 @@ function resolveExportLevels(nodes: Record<string, AnyNode>): ExportLevel[] {
|
|
|
943
1043
|
levelNodes = node ? [node] : []
|
|
944
1044
|
}
|
|
945
1045
|
|
|
1046
|
+
levelNodes = levelNodes.filter((n) => n.metadata.role !== 'roof')
|
|
946
1047
|
levelNodes.sort((a, b) => levelIndexOf(a) - levelIndexOf(b))
|
|
947
1048
|
return levelNodes.map((n) => ({ id: n.id as AnyNodeId, label: levelLabelOf(n) }))
|
|
948
1049
|
}
|
|
@@ -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> {
|
|
@@ -14,6 +14,8 @@ export type FloorplanViewState = {
|
|
|
14
14
|
highlighted: boolean
|
|
15
15
|
hovered: boolean
|
|
16
16
|
moving: boolean
|
|
17
|
+
focusedUnitId?: string
|
|
18
|
+
focusedUnitMemberIds?: readonly string[]
|
|
17
19
|
palette: FloorplanPalette | undefined
|
|
18
20
|
}
|
|
19
21
|
|
|
@@ -52,6 +54,7 @@ export function buildFloorplanContext(
|
|
|
52
54
|
siblings,
|
|
53
55
|
parent,
|
|
54
56
|
levelData,
|
|
57
|
+
sceneNodes: nodes,
|
|
55
58
|
extensions: createFloorplanContextExtensions({
|
|
56
59
|
automaticDimensions: viewState.automaticDimensions,
|
|
57
60
|
metricNotation: viewState.metricNotation ?? 'meters',
|
|
@@ -65,6 +68,8 @@ export function buildFloorplanContext(
|
|
|
65
68
|
highlighted: viewState.highlighted,
|
|
66
69
|
hovered: viewState.hovered,
|
|
67
70
|
moving: viewState.moving,
|
|
71
|
+
focusedUnitId: viewState.focusedUnitId,
|
|
72
|
+
focusedUnitMemberIds: viewState.focusedUnitMemberIds,
|
|
68
73
|
palette: viewState.palette,
|
|
69
74
|
}
|
|
70
75
|
: undefined,
|
|
@@ -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
|
+
}
|
|
@@ -8,6 +8,11 @@ import {
|
|
|
8
8
|
useScene,
|
|
9
9
|
} from '@pascal-app/core'
|
|
10
10
|
import { getPlacementMetadataRecord, stripPlacementMetadataFlags } from './placement-metadata'
|
|
11
|
+
import {
|
|
12
|
+
surfaceAttachmentId,
|
|
13
|
+
surfaceAttachmentUpdates,
|
|
14
|
+
updateSurfaceNode,
|
|
15
|
+
} from './surface-attachment'
|
|
11
16
|
|
|
12
17
|
function cleanPlacementMetadata<N extends AnyNode>(node: N): N {
|
|
13
18
|
return {
|
|
@@ -131,16 +136,19 @@ export function commitFreshPlacementSubtree(
|
|
|
131
136
|
parentId,
|
|
132
137
|
})
|
|
133
138
|
|
|
139
|
+
const surfaceId = surfaceAttachmentId(subtree.root)
|
|
134
140
|
const temporal = useScene.temporal.getState()
|
|
135
141
|
const wasTracking = (temporal as { isTracking?: boolean }).isTracking !== false
|
|
136
142
|
if (wasTracking) temporal.pause()
|
|
143
|
+
updateSurfaceNode(rootId, {}, null)
|
|
137
144
|
useScene.getState().deleteNode(rootId)
|
|
138
145
|
temporal.resume()
|
|
139
|
-
useScene
|
|
140
|
-
.
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
)
|
|
146
|
+
useScene.getState().applyNodeChanges({
|
|
147
|
+
create: cloned.nodes.map((node, index) =>
|
|
148
|
+
index === 0 && parentId ? { node, parentId } : { node },
|
|
149
|
+
),
|
|
150
|
+
update: surfaceAttachmentUpdates(cloned.rootId, parentId, surfaceId),
|
|
151
|
+
})
|
|
144
152
|
if (!wasTracking) temporal.pause()
|
|
145
153
|
|
|
146
154
|
return cloned.rootId
|