@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,22 +1,53 @@
|
|
|
1
|
-
import { afterEach, beforeEach, describe, expect, test } from 'bun:test'
|
|
1
|
+
import { afterEach, beforeAll, beforeEach, describe, expect, test } from 'bun:test'
|
|
2
2
|
import {
|
|
3
3
|
type AnyNode,
|
|
4
|
+
type AnyNodeDefinition,
|
|
4
5
|
type AnyNodeId,
|
|
6
|
+
getWallBaseElevationForNodes,
|
|
7
|
+
ItemNode,
|
|
8
|
+
nodeRegistry,
|
|
9
|
+
registerNode,
|
|
10
|
+
type SlabNode,
|
|
5
11
|
sceneRegistry,
|
|
6
12
|
spatialGridManager,
|
|
7
13
|
useScene,
|
|
8
14
|
} from '@pascal-app/core'
|
|
9
|
-
import { useViewer } from '@pascal-app/viewer'
|
|
10
|
-
import {
|
|
15
|
+
import { hideFromScene, showInScene, useViewer } from '@pascal-app/viewer'
|
|
16
|
+
import {
|
|
17
|
+
BoxGeometry,
|
|
18
|
+
Mesh,
|
|
19
|
+
MeshBasicMaterial,
|
|
20
|
+
OrthographicCamera,
|
|
21
|
+
PerspectiveCamera,
|
|
22
|
+
Vector3,
|
|
23
|
+
} from 'three'
|
|
24
|
+
import { z } from 'zod'
|
|
25
|
+
import useInteractionScope from '../../../store/use-interaction-scope'
|
|
26
|
+
import { createWallOnCurrentLevel } from '../wall/wall-drafting'
|
|
11
27
|
import { resolvePointerSupportSurface } from './pointer-support-cap'
|
|
12
28
|
|
|
13
29
|
const LEVEL_ID = 'level_test' as AnyNodeId
|
|
14
30
|
const WALL_ID = 'wall_test' as AnyNodeId
|
|
31
|
+
const PLATFORM_ID = 'plugin-platform_test' as AnyNodeId
|
|
32
|
+
const PLATFORM_KIND = 'plugin-platform'
|
|
15
33
|
|
|
16
34
|
describe('resolvePointerSupportSurface node tops', () => {
|
|
35
|
+
beforeAll(() => {
|
|
36
|
+
if (nodeRegistry.has(PLATFORM_KIND)) return
|
|
37
|
+
registerNode({
|
|
38
|
+
kind: PLATFORM_KIND,
|
|
39
|
+
schemaVersion: 1,
|
|
40
|
+
schema: z.object({}),
|
|
41
|
+
category: 'structure',
|
|
42
|
+
defaults: () => ({}),
|
|
43
|
+
capabilities: { surfaces: { top: { height: 2 } } },
|
|
44
|
+
} as unknown as AnyNodeDefinition)
|
|
45
|
+
})
|
|
46
|
+
|
|
17
47
|
beforeEach(() => {
|
|
18
48
|
spatialGridManager.clear()
|
|
19
49
|
sceneRegistry.clear()
|
|
50
|
+
useInteractionScope.getState().end()
|
|
20
51
|
useViewer.setState({
|
|
21
52
|
selection: {
|
|
22
53
|
buildingId: null,
|
|
@@ -59,12 +90,63 @@ describe('resolvePointerSupportSurface node tops', () => {
|
|
|
59
90
|
sceneRegistry.clear()
|
|
60
91
|
})
|
|
61
92
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
93
|
+
const addPluginPlatform = (z = 0, size: [number, number, number] = [4, 2, 4]) => {
|
|
94
|
+
useScene.setState((state) => ({
|
|
95
|
+
nodes: {
|
|
96
|
+
...state.nodes,
|
|
97
|
+
[PLATFORM_ID]: {
|
|
98
|
+
id: PLATFORM_ID,
|
|
99
|
+
type: PLATFORM_KIND,
|
|
100
|
+
object: 'node',
|
|
101
|
+
parentId: LEVEL_ID,
|
|
102
|
+
visible: true,
|
|
103
|
+
metadata: {},
|
|
104
|
+
} as unknown as AnyNode,
|
|
105
|
+
},
|
|
106
|
+
}))
|
|
107
|
+
const platformMesh = new Mesh(new BoxGeometry(...size), new MeshBasicMaterial())
|
|
108
|
+
platformMesh.position.set(0, 1, z)
|
|
109
|
+
platformMesh.updateMatrixWorld(true)
|
|
110
|
+
sceneRegistry.nodes.set(PLATFORM_ID, platformMesh)
|
|
111
|
+
sceneRegistry.byType[PLATFORM_KIND]!.add(PLATFORM_ID)
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
test('keeps an off-center orthographic ray on the cursor line', () => {
|
|
115
|
+
const camera = new OrthographicCamera(-20, 20, 20, -20, -1000, 1000)
|
|
116
|
+
camera.position.set(10, 10, 10)
|
|
117
|
+
camera.lookAt(0, 0, 0)
|
|
118
|
+
camera.updateMatrixWorld(true)
|
|
119
|
+
|
|
120
|
+
const direction = camera.getWorldDirection(new Vector3())
|
|
121
|
+
const right = new Vector3(1, 0, 0).applyQuaternion(camera.quaternion)
|
|
122
|
+
const up = new Vector3(0, 1, 0).applyQuaternion(camera.quaternion)
|
|
123
|
+
const rayOrigin = camera.position.clone().addScaledVector(right, 3).addScaledVector(up, 2)
|
|
124
|
+
const groundDistance = -rayOrigin.y / direction.y
|
|
125
|
+
const worldHit = rayOrigin.clone().addScaledVector(direction, groundDistance)
|
|
126
|
+
const topDistance = (2 - rayOrigin.y) / direction.y
|
|
127
|
+
const expectedTop = rayOrigin.clone().addScaledVector(direction, topDistance)
|
|
128
|
+
|
|
129
|
+
addPluginPlatform(expectedTop.z, [0.5, 2, 0.5])
|
|
130
|
+
const platformMesh = sceneRegistry.nodes.get(PLATFORM_ID)!
|
|
131
|
+
platformMesh.position.set(expectedTop.x, 1, expectedTop.z)
|
|
132
|
+
platformMesh.updateMatrixWorld(true)
|
|
133
|
+
|
|
134
|
+
const support = resolvePointerSupportSurface(
|
|
135
|
+
camera,
|
|
136
|
+
worldHit.toArray() as [number, number, number],
|
|
137
|
+
{
|
|
138
|
+
includeNodeTopSurfaces: true,
|
|
139
|
+
},
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
expect(support?.sourceNodeId).toBe(PLATFORM_ID)
|
|
143
|
+
expect(support?.worldPoint?.[0]).toBeCloseTo(expectedTop.x)
|
|
144
|
+
expect(support?.worldPoint?.[1]).toBeCloseTo(expectedTop.y)
|
|
145
|
+
expect(support?.worldPoint?.[2]).toBeCloseTo(expectedTop.z)
|
|
146
|
+
})
|
|
147
|
+
|
|
148
|
+
test('discovers a plugin-declared top surface without a kind-name list', () => {
|
|
149
|
+
addPluginPlatform()
|
|
68
150
|
|
|
69
151
|
const camera = new PerspectiveCamera()
|
|
70
152
|
camera.position.set(0, 5, 0)
|
|
@@ -74,25 +156,245 @@ describe('resolvePointerSupportSurface node tops', () => {
|
|
|
74
156
|
includeNodeTopSurfaces: true,
|
|
75
157
|
})
|
|
76
158
|
|
|
77
|
-
expect(support?.sourceNodeId).toBe(
|
|
159
|
+
expect(support?.sourceNodeId).toBe(PLATFORM_ID)
|
|
78
160
|
expect(support?.elevation).toBeCloseTo(2)
|
|
79
161
|
expect(support?.worldPoint).toEqual([0, 2, 0])
|
|
80
162
|
})
|
|
81
163
|
|
|
82
|
-
test('keeps
|
|
83
|
-
const
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
164
|
+
test('keeps an unhovered item top above a batched slab across batch transitions', () => {
|
|
165
|
+
const restoreRegistry = nodeRegistry._snapshot()
|
|
166
|
+
const item = ItemNode.parse({
|
|
167
|
+
id: 'item_support_batch',
|
|
168
|
+
parentId: LEVEL_ID,
|
|
169
|
+
asset: {
|
|
170
|
+
id: 'table',
|
|
171
|
+
name: 'Table',
|
|
172
|
+
src: '/table.glb',
|
|
173
|
+
category: 'furniture',
|
|
174
|
+
thumbnail: '/table.png',
|
|
175
|
+
dimensions: [4, 2, 4],
|
|
176
|
+
},
|
|
177
|
+
})
|
|
178
|
+
const slab = {
|
|
179
|
+
id: 'slab_support_batch',
|
|
180
|
+
type: 'slab',
|
|
181
|
+
parentId: LEVEL_ID,
|
|
182
|
+
polygon: [
|
|
183
|
+
[-3, -3],
|
|
184
|
+
[3, -3],
|
|
185
|
+
[3, 3],
|
|
186
|
+
[-3, 3],
|
|
187
|
+
],
|
|
188
|
+
elevation: 0.25,
|
|
189
|
+
thickness: 0.25,
|
|
190
|
+
holes: [],
|
|
191
|
+
visible: true,
|
|
192
|
+
} as unknown as SlabNode
|
|
193
|
+
const itemMesh = new Mesh(new BoxGeometry(4, 2, 4), new MeshBasicMaterial())
|
|
194
|
+
itemMesh.position.y = 1.25
|
|
195
|
+
itemMesh.updateMatrixWorld(true)
|
|
196
|
+
const slabMesh = new Mesh(new BoxGeometry(6, 0.25, 6), new MeshBasicMaterial())
|
|
197
|
+
slabMesh.position.y = 0.125
|
|
198
|
+
slabMesh.updateMatrixWorld(true)
|
|
199
|
+
try {
|
|
200
|
+
registerNode({
|
|
201
|
+
kind: 'item',
|
|
202
|
+
schema: ItemNode,
|
|
203
|
+
schemaVersion: 1,
|
|
204
|
+
category: 'furnish',
|
|
205
|
+
defaults: () => ({}),
|
|
206
|
+
capabilities: { surfaces: { top: { height: 2 } } },
|
|
207
|
+
} as unknown as AnyNodeDefinition)
|
|
208
|
+
useScene.setState((state) => ({
|
|
209
|
+
nodes: { ...state.nodes, [item.id]: item, [slab.id]: slab },
|
|
210
|
+
}))
|
|
211
|
+
sceneRegistry.nodes.set(item.id, itemMesh)
|
|
212
|
+
sceneRegistry.byType.item!.add(item.id)
|
|
213
|
+
sceneRegistry.nodes.set(slab.id, slabMesh)
|
|
214
|
+
sceneRegistry.byType.slab!.add(slab.id)
|
|
215
|
+
spatialGridManager.handleNodeCreated(slab as AnyNode, LEVEL_ID)
|
|
216
|
+
useViewer.setState({ hoveredId: null })
|
|
217
|
+
const camera = new PerspectiveCamera()
|
|
218
|
+
camera.position.set(0, 5, 0)
|
|
219
|
+
camera.updateMatrixWorld(true)
|
|
220
|
+
for (const batched of [false, true, false]) {
|
|
221
|
+
for (const mesh of [itemMesh, slabMesh]) {
|
|
222
|
+
if (batched) hideFromScene(mesh, 'batched')
|
|
223
|
+
else showInScene(mesh, 'batched')
|
|
224
|
+
}
|
|
225
|
+
const top = resolvePointerSupportSurface(camera, [0, 0, 0], {
|
|
226
|
+
includeNodeTopSurfaces: true,
|
|
227
|
+
})
|
|
228
|
+
expect(top?.sourceNodeId).toBe(item.id)
|
|
229
|
+
expect(top?.worldPoint).toEqual([0, 2.25, 0])
|
|
230
|
+
const floor = resolvePointerSupportSurface(camera, [0, 0, 0])
|
|
231
|
+
expect(floor?.supportSlabId).toBe(slab.id)
|
|
232
|
+
expect(floor?.worldPoint).toEqual([0, 0.25, 0])
|
|
233
|
+
}
|
|
234
|
+
} finally {
|
|
235
|
+
restoreRegistry()
|
|
236
|
+
for (const mesh of [itemMesh, slabMesh]) {
|
|
237
|
+
mesh.geometry.dispose()
|
|
238
|
+
mesh.material.dispose()
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
})
|
|
242
|
+
|
|
243
|
+
test('keeps the ground result unless node-top surfaces are asked for', () => {
|
|
244
|
+
addPluginPlatform()
|
|
245
|
+
|
|
246
|
+
const camera = new PerspectiveCamera()
|
|
247
|
+
camera.position.set(0, 5, 0)
|
|
248
|
+
camera.updateMatrixWorld(true)
|
|
249
|
+
|
|
250
|
+
// The default. A floor placement aims THROUGH whatever upward-facing
|
|
251
|
+
// geometry sits between the camera and the floor — a room's ceiling, the
|
|
252
|
+
// top of the wall the ray passes over — so only the tools that build on a
|
|
253
|
+
// surface opt in.
|
|
254
|
+
for (const options of [undefined, { includeNodeTopSurfaces: false }]) {
|
|
255
|
+
const support = resolvePointerSupportSurface(camera, [0, 0, 0], options)
|
|
256
|
+
expect(support?.sourceNodeId).toBeNull()
|
|
257
|
+
expect(support?.elevation).toBe(0)
|
|
258
|
+
}
|
|
259
|
+
})
|
|
260
|
+
|
|
261
|
+
test('never elects the node the active interaction is placing or moving', () => {
|
|
262
|
+
addPluginPlatform()
|
|
263
|
+
useInteractionScope.getState().begin({
|
|
264
|
+
kind: 'placing',
|
|
265
|
+
node: useScene.getState().nodes[PLATFORM_ID]!,
|
|
266
|
+
nodeId: PLATFORM_ID,
|
|
267
|
+
nodeType: PLATFORM_KIND,
|
|
268
|
+
view: '3d',
|
|
269
|
+
pressDrag: false,
|
|
270
|
+
driver: 'registry-tool',
|
|
271
|
+
})
|
|
88
272
|
|
|
89
273
|
const camera = new PerspectiveCamera()
|
|
90
274
|
camera.position.set(0, 5, 0)
|
|
91
275
|
camera.updateMatrixWorld(true)
|
|
92
276
|
|
|
93
|
-
const support = resolvePointerSupportSurface(camera, [0, 0, 0]
|
|
277
|
+
const support = resolvePointerSupportSurface(camera, [0, 0, 0], {
|
|
278
|
+
includeNodeTopSurfaces: true,
|
|
279
|
+
})
|
|
94
280
|
|
|
281
|
+
// Its own top would raise it by its own height on every pointer move.
|
|
95
282
|
expect(support?.sourceNodeId).toBeNull()
|
|
96
283
|
expect(support?.elevation).toBe(0)
|
|
97
284
|
})
|
|
285
|
+
|
|
286
|
+
test('uses a plugin-declared top as the wall construction surface', () => {
|
|
287
|
+
const lowSlab = {
|
|
288
|
+
id: 'slab_low',
|
|
289
|
+
type: 'slab',
|
|
290
|
+
object: 'node',
|
|
291
|
+
parentId: LEVEL_ID,
|
|
292
|
+
visible: true,
|
|
293
|
+
metadata: {},
|
|
294
|
+
children: [],
|
|
295
|
+
polygon: [
|
|
296
|
+
[-2, 1],
|
|
297
|
+
[2, 1],
|
|
298
|
+
[2, 3],
|
|
299
|
+
[-2, 3],
|
|
300
|
+
],
|
|
301
|
+
holes: [],
|
|
302
|
+
holeMetadata: [],
|
|
303
|
+
elevation: 0.25,
|
|
304
|
+
thickness: 0.25,
|
|
305
|
+
recessed: false,
|
|
306
|
+
autoFromWalls: false,
|
|
307
|
+
} as SlabNode
|
|
308
|
+
const highSlab = {
|
|
309
|
+
...lowSlab,
|
|
310
|
+
id: 'slab_high',
|
|
311
|
+
polygon: [
|
|
312
|
+
[-1, 1],
|
|
313
|
+
[0, 1],
|
|
314
|
+
[0, 3],
|
|
315
|
+
[-1, 3],
|
|
316
|
+
],
|
|
317
|
+
elevation: 1,
|
|
318
|
+
} as SlabNode
|
|
319
|
+
useScene.setState((state) => ({
|
|
320
|
+
nodes: {
|
|
321
|
+
...state.nodes,
|
|
322
|
+
[lowSlab.id]: lowSlab,
|
|
323
|
+
[highSlab.id]: highSlab,
|
|
324
|
+
},
|
|
325
|
+
}))
|
|
326
|
+
spatialGridManager.handleNodeCreated(lowSlab as AnyNode, LEVEL_ID)
|
|
327
|
+
spatialGridManager.handleNodeCreated(highSlab as AnyNode, LEVEL_ID)
|
|
328
|
+
addPluginPlatform(2)
|
|
329
|
+
|
|
330
|
+
const camera = new PerspectiveCamera()
|
|
331
|
+
camera.position.set(0, 5, 2)
|
|
332
|
+
camera.updateMatrixWorld(true)
|
|
333
|
+
|
|
334
|
+
const support = resolvePointerSupportSurface(camera, [0, 0, 2], {
|
|
335
|
+
includeNodeTopSurfaces: true,
|
|
336
|
+
})
|
|
337
|
+
|
|
338
|
+
expect(support?.sourceNodeId).toBe(PLATFORM_ID)
|
|
339
|
+
expect(support?.elevation).toBeCloseTo(2)
|
|
340
|
+
expect(support?.worldPoint).toEqual([0, 2, 2])
|
|
341
|
+
|
|
342
|
+
const wall = createWallOnCurrentLevel([-0.75, 2], [0.75, 2], {
|
|
343
|
+
supportCap: support?.elevation,
|
|
344
|
+
preferredSupportSlabId: support?.supportSlabId,
|
|
345
|
+
constructionElevation: support?.elevation,
|
|
346
|
+
constructionHeight: 2.5,
|
|
347
|
+
flatConstructionBase: support?.sourceNodeId != null,
|
|
348
|
+
})
|
|
349
|
+
expect(wall).not.toBeNull()
|
|
350
|
+
expect(getWallBaseElevationForNodes(wall!, useScene.getState().nodes)).toBeCloseTo(2)
|
|
351
|
+
const wallSupport = spatialGridManager.getSlabSupportForWall(
|
|
352
|
+
LEVEL_ID,
|
|
353
|
+
wall!.start,
|
|
354
|
+
wall!.end,
|
|
355
|
+
wall!.curveOffset,
|
|
356
|
+
wall!.thickness,
|
|
357
|
+
wall!.supportSlabId,
|
|
358
|
+
undefined,
|
|
359
|
+
wall!.supportOffset,
|
|
360
|
+
)
|
|
361
|
+
expect(
|
|
362
|
+
wallSupport.baseSegments.every((segment) => Math.abs(segment.elevation - 2) < 1e-6),
|
|
363
|
+
).toBe(true)
|
|
364
|
+
})
|
|
365
|
+
|
|
366
|
+
test('tolerates a registered definition without capabilities', () => {
|
|
367
|
+
// Gates the pollution class behind the night-8 CI flake (run
|
|
368
|
+
// 32580694134): `capabilities` is typed required, but a minimal plugin
|
|
369
|
+
// definition (or a leaked test fixture) can ship without it at runtime.
|
|
370
|
+
// The resolver enumerates every registered kind, so one capabilities-less
|
|
371
|
+
// entry must read as "no top surface" — not crash the election.
|
|
372
|
+
const restoreRegistry = nodeRegistry._snapshot()
|
|
373
|
+
try {
|
|
374
|
+
registerNode({
|
|
375
|
+
kind: 'plugin-minimal-capless',
|
|
376
|
+
schemaVersion: 1,
|
|
377
|
+
schema: z.object({}),
|
|
378
|
+
category: 'structure',
|
|
379
|
+
defaults: () => ({}),
|
|
380
|
+
// No `capabilities` — deliberately.
|
|
381
|
+
} as unknown as AnyNodeDefinition)
|
|
382
|
+
addPluginPlatform()
|
|
383
|
+
|
|
384
|
+
const camera = new PerspectiveCamera()
|
|
385
|
+
camera.position.set(0, 5, 0)
|
|
386
|
+
camera.updateMatrixWorld(true)
|
|
387
|
+
|
|
388
|
+
const support = resolvePointerSupportSurface(camera, [0, 0, 0], {
|
|
389
|
+
includeNodeTopSurfaces: true,
|
|
390
|
+
})
|
|
391
|
+
|
|
392
|
+
// No throw, and the election still works: the capabilities-less kind is
|
|
393
|
+
// skipped while the platform's declared top is found as usual.
|
|
394
|
+
expect(support?.sourceNodeId).toBe(PLATFORM_ID)
|
|
395
|
+
expect(support?.elevation).toBeCloseTo(2)
|
|
396
|
+
} finally {
|
|
397
|
+
restoreRegistry()
|
|
398
|
+
}
|
|
399
|
+
})
|
|
98
400
|
})
|
|
@@ -4,13 +4,16 @@ import {
|
|
|
4
4
|
GROUND_SUPPORT_ID,
|
|
5
5
|
type ItemNode,
|
|
6
6
|
isLowProfileItemSurface,
|
|
7
|
+
nodeRegistry,
|
|
7
8
|
sceneRegistry,
|
|
8
9
|
spatialGridManager,
|
|
9
10
|
useScene,
|
|
10
11
|
} from '@pascal-app/core'
|
|
11
|
-
import { useViewer } from '@pascal-app/viewer'
|
|
12
|
+
import { setSurfaceRaycastLayers, useViewer } from '@pascal-app/viewer'
|
|
12
13
|
import { type Camera, Matrix3, type Object3D, Raycaster, Vector3 } from 'three'
|
|
13
14
|
import { resolveTerrainGroundHit } from '../../../lib/ground-surface'
|
|
15
|
+
import { scopeNodeId } from '../../../lib/interaction/scope'
|
|
16
|
+
import useInteractionScope from '../../../store/use-interaction-scope'
|
|
14
17
|
|
|
15
18
|
const originScratch = new Vector3()
|
|
16
19
|
const hitScratch = new Vector3()
|
|
@@ -19,11 +22,10 @@ const pointScratch = new Vector3()
|
|
|
19
22
|
const worldRayOrigin = new Vector3()
|
|
20
23
|
const worldRayDirection = new Vector3()
|
|
21
24
|
const nodeTopRaycaster = new Raycaster()
|
|
25
|
+
setSurfaceRaycastLayers(nodeTopRaycaster.layers)
|
|
22
26
|
const nodeTopNormal = new Vector3()
|
|
23
27
|
const nodeTopNormalMatrix = new Matrix3()
|
|
24
28
|
|
|
25
|
-
const NODE_TOP_SURFACE_KINDS = ['wall', 'item', 'column'] as const
|
|
26
|
-
|
|
27
29
|
export type PointerSupportSurface = {
|
|
28
30
|
/** Level-local elevation of the pointed surface — the election cap. */
|
|
29
31
|
elevation: number
|
|
@@ -85,7 +87,19 @@ export function resolvePointerSupportSurface(
|
|
|
85
87
|
// The world ray, kept before the level conversion below: the terrain field is
|
|
86
88
|
// world-space (site geometry, not level-local), so the march needs this frame.
|
|
87
89
|
camera.getWorldPosition(worldRayOrigin)
|
|
88
|
-
|
|
90
|
+
const cameraToHit = hitScratch.set(worldHit[0], worldHit[1], worldHit[2]).sub(worldRayOrigin)
|
|
91
|
+
if ((camera as Camera & { isOrthographicCamera?: boolean }).isOrthographicCamera) {
|
|
92
|
+
// For an orthographic camera every screen pixel has the same direction. The
|
|
93
|
+
// hit point is offset from the camera along the view plane, so using
|
|
94
|
+
// `camera.position -> hit` tilts the ray toward the screen centre and makes
|
|
95
|
+
// support surfaces drift away from the cursor off-axis.
|
|
96
|
+
camera.getWorldDirection(worldRayDirection).normalize()
|
|
97
|
+
worldRayOrigin
|
|
98
|
+
.set(worldHit[0], worldHit[1], worldHit[2])
|
|
99
|
+
.addScaledVector(worldRayDirection, -cameraToHit.dot(worldRayDirection))
|
|
100
|
+
} else {
|
|
101
|
+
worldRayDirection.copy(cameraToHit).normalize()
|
|
102
|
+
}
|
|
89
103
|
|
|
90
104
|
originScratch.copy(worldRayOrigin)
|
|
91
105
|
hitScratch.set(worldHit[0], worldHit[1], worldHit[2])
|
|
@@ -151,16 +165,39 @@ export function resolvePointerSupportSurface(
|
|
|
151
165
|
localPoint = [pointScratch.x, pointScratch.y, pointScratch.z]
|
|
152
166
|
}
|
|
153
167
|
|
|
154
|
-
|
|
168
|
+
// Node tops are opt-in. A ray aimed at a floor crosses every upward-facing
|
|
169
|
+
// face above that floor first — a room's ceiling, the top of the wall it
|
|
170
|
+
// passes over — so electing "the nearest node top along the ray" silently
|
|
171
|
+
// lifts anything placed inside a finished room. Only the tools that build ON
|
|
172
|
+
// a surface (wall / column / fence / stair / block) mean that, and they say
|
|
173
|
+
// so. Everything else places against the floor the pointer indicates.
|
|
174
|
+
//
|
|
175
|
+
// `capabilities` is typed required on NodeDefinition, but this enumerates
|
|
176
|
+
// EVERY registered kind — including plugin bundles that bypass the type at
|
|
177
|
+
// runtime. A minimal definition without `capabilities` must read as "no top
|
|
178
|
+
// surface", not crash the resolver (night-8 CI, run 32580694134).
|
|
179
|
+
const nodeTopSurfaceKinds = options?.includeNodeTopSurfaces
|
|
180
|
+
? Array.from(nodeRegistry.entries())
|
|
181
|
+
.filter(([, definition]) => definition.capabilities?.surfaces?.top !== undefined)
|
|
182
|
+
.map(([kind]) => kind)
|
|
183
|
+
: []
|
|
184
|
+
if (nodeTopSurfaceKinds.some((kind) => (sceneRegistry.byType[kind]?.size ?? 0) > 0)) {
|
|
155
185
|
nodeTopRaycaster.set(worldRayOrigin, worldRayDirection.clone().normalize())
|
|
156
186
|
const nodes = useScene.getState().nodes
|
|
157
187
|
const registeredOwners = new Map(
|
|
158
188
|
[...sceneRegistry.nodes.entries()].map(([nodeId, object]) => [object, nodeId as AnyNodeId]),
|
|
159
189
|
)
|
|
160
|
-
|
|
190
|
+
// The node the active interaction is placing/moving cannot be a surface for
|
|
191
|
+
// itself: its mesh rides the cursor, so electing its own top would raise it
|
|
192
|
+
// by its own height on every pointer move. Tools neuter the dragged mesh's
|
|
193
|
+
// `raycast` for their own pointer routing, but that is each tool's private
|
|
194
|
+
// convention — the election owns the invariant.
|
|
195
|
+
const interactingNodeId = scopeNodeId(useInteractionScope.getState().scope)
|
|
196
|
+
const isEligibleCandidate = (nodeId: AnyNodeId) => {
|
|
161
197
|
let current = nodes[nodeId]
|
|
162
198
|
const visited = new Set<AnyNodeId>()
|
|
163
199
|
while (current && !visited.has(current.id)) {
|
|
200
|
+
if (current.id === interactingNodeId) return false
|
|
164
201
|
if (current.id === levelId) return true
|
|
165
202
|
visited.add(current.id)
|
|
166
203
|
current = current.parentId ? nodes[current.parentId as AnyNodeId] : undefined
|
|
@@ -179,12 +216,12 @@ export function resolvePointerSupportSurface(
|
|
|
179
216
|
}
|
|
180
217
|
| undefined
|
|
181
218
|
|
|
182
|
-
for (const kind of
|
|
219
|
+
for (const kind of nodeTopSurfaceKinds) {
|
|
183
220
|
for (const rawId of sceneRegistry.byType[kind] ?? []) {
|
|
184
221
|
const nodeId = rawId as AnyNodeId
|
|
185
222
|
const node = nodes[nodeId]
|
|
186
223
|
const object = sceneRegistry.nodes.get(nodeId)
|
|
187
|
-
if (!(node?.visible && object?.visible &&
|
|
224
|
+
if (!(node?.visible && object?.visible && isEligibleCandidate(nodeId))) continue
|
|
188
225
|
if (
|
|
189
226
|
node.type === 'item' &&
|
|
190
227
|
(!canHostOnTop(node) || isLowProfileItemSurface(node as ItemNode))
|
|
@@ -39,10 +39,15 @@ function writeGridHeights(
|
|
|
39
39
|
}
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
+
let first = (row0 * field.cols + col0) * 3
|
|
43
|
+
let end = ((row1 - 1) * field.cols + col1) * 3
|
|
44
|
+
// Several dabs can precede a render; none may discard an earlier pending upload.
|
|
45
|
+
for (const pending of attribute.updateRanges) {
|
|
46
|
+
first = Math.min(first, pending.start)
|
|
47
|
+
end = Math.max(end, pending.start + pending.count)
|
|
48
|
+
}
|
|
42
49
|
attribute.clearUpdateRanges()
|
|
43
|
-
|
|
44
|
-
const last = ((row1 - 1) * field.cols + (col1 - 1)) * 3 + 2
|
|
45
|
-
attribute.addUpdateRange(first, last - first + 1)
|
|
50
|
+
attribute.addUpdateRange(first, end - first)
|
|
46
51
|
attribute.needsUpdate = true
|
|
47
52
|
}
|
|
48
53
|
|
|
@@ -162,7 +167,7 @@ export function TerrainSculptGrid({
|
|
|
162
167
|
if (stroke?.lastPatch && stroke.lastPatch !== lastPatch) {
|
|
163
168
|
lastPatch = stroke.lastPatch
|
|
164
169
|
writeGridHeights(attribute, stroke.field, stroke.lastPatch)
|
|
165
|
-
|
|
170
|
+
// This grid is neither frustum-culled nor raycast, so no per-dab bounds scan.
|
|
166
171
|
return
|
|
167
172
|
}
|
|
168
173
|
|
|
@@ -173,7 +178,6 @@ export function TerrainSculptGrid({
|
|
|
173
178
|
const restored = terrainFieldOf(current) ?? sculptFieldForSite(current)
|
|
174
179
|
if (restored.cols !== field.cols || restored.rows !== field.rows) return
|
|
175
180
|
writeGridHeights(attribute, restored, null)
|
|
176
|
-
target.geometry.computeBoundingSphere()
|
|
177
181
|
}
|
|
178
182
|
})
|
|
179
183
|
}, [field.cols, field.rows, site.id])
|
|
@@ -214,7 +214,7 @@ export const TerrainSculptTool: React.FC = () => {
|
|
|
214
214
|
detachStrokeAnchor(active.stroke)
|
|
215
215
|
return
|
|
216
216
|
}
|
|
217
|
-
const point = groundPoint(event, active.
|
|
217
|
+
const point = groundPoint(event, active.stroke.snapshot)
|
|
218
218
|
if (!point) return
|
|
219
219
|
const brushPatch = advanceStroke(active.stroke, point[0], point[1])
|
|
220
220
|
if (!brushPatch) return
|