@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
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
import { describe, expect, test } from 'bun:test'
|
|
1
|
+
import { afterEach, describe, expect, test } from 'bun:test'
|
|
2
2
|
import { type AnyNode, CeilingNode, LevelNode, SlabNode } from '@pascal-app/core'
|
|
3
3
|
import useElevationGuides from '../store/use-elevation-guides'
|
|
4
4
|
import {
|
|
5
5
|
clearStructuralElevationGuide,
|
|
6
6
|
collectElevationSnapTargets,
|
|
7
|
+
publishResolvedElevationGuide,
|
|
7
8
|
publishStructuralElevationGuide,
|
|
8
9
|
resolveElevationSnapMatch,
|
|
9
10
|
resolveStructuralElevationSnap,
|
|
@@ -42,6 +43,10 @@ function structuralScene() {
|
|
|
42
43
|
}
|
|
43
44
|
|
|
44
45
|
describe('elevation guides', () => {
|
|
46
|
+
afterEach(() => {
|
|
47
|
+
useElevationGuides.setState({ guide: null })
|
|
48
|
+
})
|
|
49
|
+
|
|
45
50
|
test('collects the level, slab faces, and ceiling plane on the source level', () => {
|
|
46
51
|
const { level, nodes, slab } = structuralScene()
|
|
47
52
|
const targets = collectElevationSnapTargets(
|
|
@@ -104,4 +109,26 @@ describe('elevation guides', () => {
|
|
|
104
109
|
publishStructuralElevationGuide(source, 0.7, nodes)
|
|
105
110
|
expect(useElevationGuides.getState().guide).toBeNull()
|
|
106
111
|
})
|
|
112
|
+
|
|
113
|
+
test('publishes an explicitly resolved neighboring datum', () => {
|
|
114
|
+
const { level } = structuralScene()
|
|
115
|
+
useElevationGuides.setState({ guide: null })
|
|
116
|
+
|
|
117
|
+
publishResolvedElevationGuide(
|
|
118
|
+
{ nodeId: 'leanto_moving', levelId: level.id, anchor: [2, 1] },
|
|
119
|
+
{
|
|
120
|
+
id: 'leanto_neighbor:high-edge',
|
|
121
|
+
elevation: 3.4,
|
|
122
|
+
anchor: [5, 1],
|
|
123
|
+
label: 'Neighbor shed edge',
|
|
124
|
+
},
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
expect(useElevationGuides.getState().guide).toMatchObject({
|
|
128
|
+
ownerId: 'leanto_moving',
|
|
129
|
+
elevation: 3.4,
|
|
130
|
+
direction: [1, 0],
|
|
131
|
+
label: 'Neighbor shed edge',
|
|
132
|
+
})
|
|
133
|
+
})
|
|
107
134
|
})
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import {
|
|
2
2
|
type AnyNode,
|
|
3
3
|
type AnyNodeId,
|
|
4
|
+
type FenceNode,
|
|
4
5
|
findLevelAncestorId,
|
|
5
6
|
getWallBaseElevationForNodes,
|
|
6
7
|
getWallEffectiveHeightForNodes,
|
|
@@ -53,8 +54,10 @@ function segmentCenter(
|
|
|
53
54
|
// A stale host resolves to the level base — the sculpted ground under the
|
|
54
55
|
// fence's start point, sampled where the builder samples it, so the guide line
|
|
55
56
|
// lands on the rail it claims to describe.
|
|
56
|
-
function
|
|
57
|
-
|
|
57
|
+
export function getFenceBaseElevationForNodes(
|
|
58
|
+
node: FenceNode,
|
|
59
|
+
nodes: Record<string, AnyNode>,
|
|
60
|
+
): number {
|
|
58
61
|
const host = node.supportSlabId ? nodes[node.supportSlabId as AnyNodeId] : undefined
|
|
59
62
|
const hosted = host?.type === 'slab' && (host.parentId ?? null) === (node.parentId ?? null)
|
|
60
63
|
const levelId = findLevelAncestorId(node.id as AnyNodeId, nodes)
|
|
@@ -161,7 +164,7 @@ export function collectElevationSnapTargets(
|
|
|
161
164
|
}
|
|
162
165
|
|
|
163
166
|
if (node.type === 'fence') {
|
|
164
|
-
const base =
|
|
167
|
+
const base = getFenceBaseElevationForNodes(node, nodes)
|
|
165
168
|
const center = segmentCenter(node.start, node.end)
|
|
166
169
|
targets.push({
|
|
167
170
|
id: `${node.id}:base`,
|
|
@@ -245,8 +248,20 @@ export function publishStructuralElevationGuide(
|
|
|
245
248
|
return
|
|
246
249
|
}
|
|
247
250
|
|
|
248
|
-
|
|
249
|
-
|
|
251
|
+
publishResolvedElevationGuide(source, match.target)
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
export function publishResolvedElevationGuide(
|
|
255
|
+
source: ElevationGuideSource,
|
|
256
|
+
target: ElevationSnapTarget,
|
|
257
|
+
): void {
|
|
258
|
+
if (!source.levelId) {
|
|
259
|
+
clearStructuralElevationGuide(source.nodeId)
|
|
260
|
+
return
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
const dx = target.anchor[0] - source.anchor[0]
|
|
264
|
+
const dz = target.anchor[1] - source.anchor[1]
|
|
250
265
|
const length = Math.hypot(dx, dz)
|
|
251
266
|
const direction: [number, number] = length > 1e-6 ? [dx / length, dz / length] : [1, 0]
|
|
252
267
|
|
|
@@ -255,8 +270,8 @@ export function publishStructuralElevationGuide(
|
|
|
255
270
|
levelId: source.levelId,
|
|
256
271
|
center: source.anchor,
|
|
257
272
|
direction,
|
|
258
|
-
elevation:
|
|
259
|
-
label:
|
|
273
|
+
elevation: target.elevation,
|
|
274
|
+
label: target.label,
|
|
260
275
|
})
|
|
261
276
|
}
|
|
262
277
|
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test'
|
|
2
|
+
import { OrthographicCamera, PerspectiveCamera, Vector3 } from 'three'
|
|
3
|
+
import { getFloatingMenuScale } from './floating-menu-scale'
|
|
4
|
+
|
|
5
|
+
describe('getFloatingMenuScale', () => {
|
|
6
|
+
test('uses the standard orthographic zoom scale and clamps its range', () => {
|
|
7
|
+
const camera = new OrthographicCamera()
|
|
8
|
+
const anchor = new Vector3()
|
|
9
|
+
|
|
10
|
+
camera.zoom = 15
|
|
11
|
+
expect(getFloatingMenuScale(camera, anchor)).toBe(0.75)
|
|
12
|
+
camera.zoom = 2
|
|
13
|
+
expect(getFloatingMenuScale(camera, anchor)).toBe(0.5)
|
|
14
|
+
camera.zoom = 40
|
|
15
|
+
expect(getFloatingMenuScale(camera, anchor)).toBe(1)
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
test('uses inverse perspective-camera distance and clamps its range', () => {
|
|
19
|
+
const camera = new PerspectiveCamera()
|
|
20
|
+
const anchor = new Vector3()
|
|
21
|
+
|
|
22
|
+
camera.position.set(0, 0, 16)
|
|
23
|
+
expect(getFloatingMenuScale(camera, anchor)).toBe(0.75)
|
|
24
|
+
camera.position.set(0, 0, 48)
|
|
25
|
+
expect(getFloatingMenuScale(camera, anchor)).toBe(0.5)
|
|
26
|
+
camera.position.set(0, 0, 6)
|
|
27
|
+
expect(getFloatingMenuScale(camera, anchor)).toBe(1)
|
|
28
|
+
})
|
|
29
|
+
})
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { type Camera, OrthographicCamera, type Vector3 } from 'three'
|
|
2
|
+
|
|
3
|
+
const MIN_MENU_SCALE = 0.5
|
|
4
|
+
const MAX_MENU_SCALE = 1
|
|
5
|
+
const REF_ORTHO_ZOOM = 20
|
|
6
|
+
const REF_CAMERA_DISTANCE = 12
|
|
7
|
+
|
|
8
|
+
export function getFloatingMenuScale(camera: Camera, anchor: Vector3): number {
|
|
9
|
+
const raw =
|
|
10
|
+
camera instanceof OrthographicCamera
|
|
11
|
+
? camera.zoom / REF_ORTHO_ZOOM
|
|
12
|
+
: REF_CAMERA_DISTANCE / Math.max(camera.position.distanceTo(anchor), 0.001)
|
|
13
|
+
return Math.min(MAX_MENU_SCALE, Math.max(MIN_MENU_SCALE, raw))
|
|
14
|
+
}
|
|
@@ -1,8 +1,23 @@
|
|
|
1
|
-
import { afterEach, describe, expect, test } from 'bun:test'
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, test } from 'bun:test'
|
|
2
|
+
import { LevelNode, useScene, WallNode } from '@pascal-app/core'
|
|
3
|
+
import { useViewer } from '@pascal-app/viewer'
|
|
2
4
|
import useAlignmentGuides from '../../store/use-alignment-guides'
|
|
3
|
-
import { applyFloorplanAlignment } from './apply-alignment'
|
|
5
|
+
import { alignFloorplanDraftPoint, applyFloorplanAlignment } from './apply-alignment'
|
|
4
6
|
|
|
5
7
|
describe('applyFloorplanAlignment', () => {
|
|
8
|
+
beforeEach(() => {
|
|
9
|
+
// These tests assume no active building (alignment runs on world axes).
|
|
10
|
+
// The scene/viewer stores are process-wide singletons, so an earlier test
|
|
11
|
+
// FILE can leak a selected building fixture into them — under bun's
|
|
12
|
+
// platform-dependent file order that turned into an order-dependent
|
|
13
|
+
// failure (getActiveBuildingPose reading a fixture building without a
|
|
14
|
+
// rotation array). Pin the empty-scene context explicitly.
|
|
15
|
+
useScene.setState({ nodes: {} } as never)
|
|
16
|
+
useViewer.setState({
|
|
17
|
+
selection: { buildingId: null, levelId: null, zoneId: null, selectedIds: [] },
|
|
18
|
+
} as never)
|
|
19
|
+
})
|
|
20
|
+
|
|
6
21
|
afterEach(() => {
|
|
7
22
|
useAlignmentGuides.getState().clear()
|
|
8
23
|
})
|
|
@@ -22,4 +37,34 @@ describe('applyFloorplanAlignment', () => {
|
|
|
22
37
|
expect(result.guides).toHaveLength(1)
|
|
23
38
|
expect(useAlignmentGuides.getState().guides).toHaveLength(1)
|
|
24
39
|
})
|
|
40
|
+
|
|
41
|
+
test('can restrict candidates to the active level', () => {
|
|
42
|
+
const lowerLevel = LevelNode.parse({ id: 'level_lower', level: 0 })
|
|
43
|
+
const upperLevel = LevelNode.parse({ id: 'level_upper', level: 1 })
|
|
44
|
+
const lowerWall = WallNode.parse({
|
|
45
|
+
id: 'wall_lower',
|
|
46
|
+
parentId: lowerLevel.id,
|
|
47
|
+
start: [10, 10],
|
|
48
|
+
end: [14, 10],
|
|
49
|
+
})
|
|
50
|
+
const upperWall = WallNode.parse({
|
|
51
|
+
id: 'wall_upper',
|
|
52
|
+
parentId: upperLevel.id,
|
|
53
|
+
start: [0, 0],
|
|
54
|
+
end: [4, 0],
|
|
55
|
+
})
|
|
56
|
+
useScene.setState({
|
|
57
|
+
nodes: Object.fromEntries(
|
|
58
|
+
[lowerLevel, upperLevel, lowerWall, upperWall].map((node) => [node.id, node]),
|
|
59
|
+
),
|
|
60
|
+
} as never)
|
|
61
|
+
|
|
62
|
+
expect(
|
|
63
|
+
alignFloorplanDraftPoint([0.04, 0], {
|
|
64
|
+
applySnap: true,
|
|
65
|
+
levelId: lowerLevel.id,
|
|
66
|
+
}),
|
|
67
|
+
).toEqual([0.04, 0])
|
|
68
|
+
expect(useAlignmentGuides.getState().guides).toHaveLength(0)
|
|
69
|
+
})
|
|
25
70
|
})
|
|
@@ -2,6 +2,7 @@ import {
|
|
|
2
2
|
type AlignmentAnchor,
|
|
3
3
|
type AlignmentGuide,
|
|
4
4
|
collectAlignmentAnchors,
|
|
5
|
+
resolveLevelId,
|
|
5
6
|
useScene,
|
|
6
7
|
} from '@pascal-app/core'
|
|
7
8
|
import useAlignmentGuides from '../../store/use-alignment-guides'
|
|
@@ -96,13 +97,21 @@ export function alignFloorplanDraftPoint(
|
|
|
96
97
|
bypass?: boolean
|
|
97
98
|
threshold?: number
|
|
98
99
|
excludeIds?: readonly string[]
|
|
100
|
+
levelId?: string | null
|
|
99
101
|
},
|
|
100
102
|
): [number, number] {
|
|
101
103
|
if (opts?.bypass) {
|
|
102
104
|
useAlignmentGuides.getState().clear()
|
|
103
105
|
return [point[0], point[1]]
|
|
104
106
|
}
|
|
105
|
-
|
|
107
|
+
const nodes = useScene.getState().nodes
|
|
108
|
+
let candidates = collectAlignmentAnchors(nodes, FLOORPLAN_DRAFT_ALIGN_ID)
|
|
109
|
+
if (opts && 'levelId' in opts) {
|
|
110
|
+
candidates = candidates.filter((anchor) => {
|
|
111
|
+
const candidate = nodes[anchor.nodeId as keyof typeof nodes]
|
|
112
|
+
return candidate ? resolveLevelId(candidate, nodes) === opts.levelId : false
|
|
113
|
+
})
|
|
114
|
+
}
|
|
106
115
|
if (opts?.excludeIds?.length) {
|
|
107
116
|
const excluded = new Set(opts.excludeIds)
|
|
108
117
|
candidates = candidates.filter((anchor) => !excluded.has(anchor.nodeId))
|
|
@@ -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
|
+
})
|