@pascal-app/editor 0.9.2 → 1.0.0-beta.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/package.json +9 -7
- package/src/components/editor/bake-exporter.tsx +3 -1
- package/src/components/editor/bake-thumbnail.tsx +95 -0
- package/src/components/editor/camera-dragging-lifecycle.test.ts +49 -0
- package/src/components/editor/camera-dragging-lifecycle.ts +41 -0
- package/src/components/editor/custom-camera-controls.tsx +307 -212
- package/src/components/editor/delete-confirmation-dialog.tsx +54 -0
- package/src/components/editor/elevation-3d-guide-layer.tsx +120 -0
- package/src/components/editor/first-person/build-collider-world.test.ts +66 -2
- package/src/components/editor/first-person/build-collider-world.ts +20 -0
- package/src/components/editor/first-person/terrain-collider.test.ts +132 -0
- package/src/components/editor/first-person/terrain-collider.ts +212 -0
- package/src/components/editor/first-person-controls.tsx +295 -132
- package/src/components/editor/floating-action-menu.tsx +59 -65
- package/src/components/editor/floorplan-background-selection.test.ts +16 -2
- package/src/components/editor/floorplan-background-selection.ts +4 -18
- package/src/components/editor/floorplan-camera-sync.test.ts +249 -0
- package/src/components/editor/floorplan-camera-sync.ts +213 -0
- package/src/components/editor/floorplan-mode-coordinator.tsx +82 -0
- package/src/components/editor/floorplan-navigation-presentation.test.ts +142 -0
- package/src/components/editor/floorplan-navigation-presentation.ts +156 -0
- package/src/components/editor/floorplan-panel.tsx +931 -861
- package/src/components/editor/group-actions.ts +155 -15
- package/src/components/editor/group-floating-action-menu.tsx +1 -1
- package/src/components/editor/group-rotate-handle.tsx +1 -1
- package/src/components/editor/group-selection-box-3d.tsx +1 -1
- package/src/components/editor/handles/handle-drag-history.test.ts +96 -0
- package/src/components/editor/handles/handle-drag-history.ts +14 -0
- package/src/components/editor/handles/preview-overrides.test.ts +26 -0
- package/src/components/editor/handles/preview-overrides.ts +13 -0
- package/src/components/editor/handles/resize-snap.test.ts +52 -0
- package/src/components/editor/handles/resize-snap.ts +23 -0
- package/src/components/editor/handles/use-handle-drag.ts +17 -7
- package/src/components/editor/index.tsx +42 -9
- package/src/components/editor/measurement-pill.tsx +6 -1
- package/src/components/editor/node-arrow-handles.tsx +51 -15
- package/src/components/editor/quick-measurement-card.tsx +9 -3
- package/src/components/editor/quick-measurement-hud.tsx +7 -1
- package/src/components/editor/selection-manager.tsx +3 -1
- package/src/components/editor/site-edge-labels.tsx +24 -2
- package/src/components/editor/snapshot-capture-overlay.tsx +4 -1
- package/src/components/editor/thumbnail-generator.tsx +66 -294
- package/src/components/editor/use-floorplan-hit-testing.test.ts +82 -0
- package/src/components/editor/use-floorplan-hit-testing.ts +144 -161
- package/src/components/editor/use-mesh-settle-epoch.ts +1 -0
- package/src/components/editor/wall-measurement-label.tsx +8 -16
- package/src/components/editor/wall-move-side-handles.tsx +193 -18
- package/src/components/editor/wall-opening-highlights.tsx +63 -32
- package/src/components/editor/wall-snap-beacon-layer.tsx +6 -4
- package/src/components/editor-2d/floorplan-alignment-guide-layer.tsx +2 -1
- package/src/components/editor-2d/floorplan-cursor-indicator-overlay.tsx +39 -8
- package/src/components/editor-2d/floorplan-cursor-indicator-position.test.ts +31 -0
- package/src/components/editor-2d/floorplan-cursor-indicator-position.ts +39 -0
- package/src/components/editor-2d/floorplan-group-move.tsx +47 -4
- package/src/components/editor-2d/floorplan-measurement-tool-layer.tsx +21 -5
- package/src/components/editor-2d/floorplan-registered-tool-layer.tsx +72 -0
- package/src/components/editor-2d/floorplan-registry-action-menu.tsx +93 -59
- package/src/components/editor-2d/floorplan-registry-move-overlay.tsx +27 -0
- package/src/components/editor-2d/floorplan-render-context.test.ts +27 -0
- package/src/components/editor-2d/floorplan-render-context.tsx +113 -8
- package/src/components/editor-2d/renderers/floorplan-annotation-layout.test.ts +285 -0
- package/src/components/editor-2d/renderers/floorplan-annotation-layout.ts +516 -0
- package/src/components/editor-2d/renderers/floorplan-dimension-renderer.test.tsx +236 -0
- package/src/components/editor-2d/renderers/floorplan-dimension-renderer.tsx +618 -0
- package/src/components/editor-2d/renderers/floorplan-geometry-renderer.test.tsx +369 -0
- package/src/components/editor-2d/renderers/floorplan-geometry-renderer.tsx +431 -32
- package/src/components/editor-2d/renderers/floorplan-label-angle.test.ts +75 -1
- package/src/components/editor-2d/renderers/floorplan-label-angle.ts +92 -0
- package/src/components/editor-2d/renderers/floorplan-placement-preview-layer.tsx +93 -28
- package/src/components/editor-2d/renderers/floorplan-registry-layer.test.ts +279 -0
- package/src/components/editor-2d/renderers/floorplan-registry-layer.tsx +621 -281
- package/src/components/editor-2d/renderers/floorplan-stair-layer.tsx +20 -10
- package/src/components/systems/ceiling/ceiling-selection-affordance-system.tsx +12 -7
- package/src/components/tools/fence/fence-drafting.ts +27 -0
- package/src/components/tools/item/use-draft-node.ts +131 -72
- package/src/components/tools/item/use-placement-coordinator.tsx +52 -4
- package/src/components/tools/registry/move-registry-node-tool.tsx +75 -4
- package/src/components/tools/roof/roof-tool.tsx +16 -0
- package/src/components/tools/shared/floor-stack-preview.ts +4 -0
- package/src/components/tools/shared/horizontal-construction-plane.ts +123 -0
- package/src/components/tools/shared/pointer-support-cap.test.ts +98 -0
- package/src/components/tools/shared/pointer-support-cap.ts +273 -0
- package/src/components/tools/site/site-boundary-editor.tsx +13 -6
- package/src/components/tools/site/stroke-pointer-ownership.test.ts +91 -0
- package/src/components/tools/site/stroke-pointer-ownership.ts +57 -0
- package/src/components/tools/site/terrain-brush-cursor.tsx +247 -0
- package/src/components/tools/site/terrain-sculpt-grid.tsx +192 -0
- package/src/components/tools/site/terrain-sculpt-tool.tsx +356 -0
- package/src/components/tools/stair/stair-click-guard.test.ts +58 -0
- package/src/components/tools/stair/stair-click-guard.ts +72 -0
- package/src/components/tools/stair/stair-tool.tsx +98 -47
- package/src/components/tools/tool-manager.tsx +67 -9
- package/src/components/tools/wall/wall-drafting.test.ts +118 -1
- package/src/components/tools/wall/wall-drafting.ts +128 -9
- package/src/components/tools/wall/wall-snap-geometry.test.ts +8 -0
- package/src/components/tools/wall/wall-snap-geometry.ts +70 -3
- package/src/components/tools/zone/zone-tool.tsx +16 -0
- package/src/components/ui/action-menu/control-modes.tsx +4 -1
- package/src/components/ui/action-menu/measurement-control.tsx +107 -28
- package/src/components/ui/command-palette/editor-commands.tsx +14 -1
- package/src/components/ui/controls/material-paint-panel.tsx +7 -1
- package/src/components/ui/controls/material-picker.tsx +86 -7
- package/src/components/ui/controls/terrain-sculpt-panel.tsx +193 -0
- package/src/components/ui/floating-level-selector.tsx +73 -19
- package/src/components/ui/helpers/helper-manager.tsx +46 -0
- package/src/components/ui/panels/multi-selection-panel.tsx +57 -0
- package/src/components/ui/panels/panel-manager.tsx +14 -1
- package/src/components/ui/panels/parametric-inspector.tsx +4 -2
- package/src/components/ui/panels/selection-breakdown.test.ts +24 -0
- package/src/components/ui/panels/selection-breakdown.ts +20 -0
- package/src/components/ui/sidebar/panels/settings-panel/index.tsx +14 -4
- package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +16 -0
- package/src/components/ui/sidebar/panels/site-panel/building-tree-node.tsx +2 -1
- package/src/components/ui/sidebar/panels/site-panel/index.tsx +2 -0
- package/src/components/viewer/viewer-controls-bar.tsx +466 -0
- package/src/components/viewer/viewer-scene-header.tsx +235 -0
- package/src/components/viewer-overlay.tsx +20 -655
- package/src/components/walkthrough-hud.tsx +111 -0
- package/src/hooks/use-grid-events.ts +24 -0
- package/src/hooks/use-keyboard.ts +53 -30
- package/src/index.tsx +137 -4
- package/src/lib/active-placement-surface.test.ts +24 -0
- package/src/lib/active-placement-surface.ts +8 -1
- package/src/lib/camera-pose.test.ts +237 -0
- package/src/lib/camera-pose.ts +172 -0
- package/src/lib/direct-manipulation.test.ts +83 -0
- package/src/lib/direct-manipulation.ts +9 -0
- package/src/lib/door-interaction.ts +1 -1
- package/src/lib/elevation-guides.test.ts +107 -0
- package/src/lib/elevation-guides.ts +236 -0
- package/src/lib/floorplan/annotation-visibility.test.ts +327 -0
- package/src/lib/floorplan/annotation-visibility.ts +133 -0
- package/src/lib/floorplan/drawing-coordination.test.ts +46 -0
- package/src/lib/floorplan/drawing-coordination.ts +13 -0
- package/src/lib/floorplan/floorplan-export.test.ts +319 -0
- package/src/lib/floorplan/floorplan-export.tsx +745 -131
- package/src/lib/floorplan/floorplan-extension.test.ts +43 -0
- package/src/lib/floorplan/floorplan-extension.ts +181 -0
- package/src/lib/floorplan/floorplan-mode.test.ts +120 -0
- package/src/lib/floorplan/floorplan-mode.ts +73 -0
- package/src/lib/floorplan/floorplan-pdfkit-document.ts +194 -0
- package/src/lib/floorplan/floorplan-pdfkit-renderer.test.ts +107 -0
- package/src/lib/floorplan/floorplan-pdfkit-renderer.ts +680 -0
- package/src/lib/floorplan/geometry.ts +2 -3
- package/src/lib/fresh-planar-placement.test.ts +44 -1
- package/src/lib/fresh-planar-placement.ts +21 -4
- package/src/lib/glb-export.test.ts +116 -1
- package/src/lib/glb-export.ts +204 -7
- package/src/lib/ground-surface.ts +95 -0
- package/src/lib/history.test.ts +96 -0
- package/src/lib/history.ts +22 -0
- package/src/lib/interaction/overlay-policy.test.ts +2 -1
- package/src/lib/interaction/scope.ts +40 -8
- package/src/lib/level-duplication.test.ts +2 -1
- package/src/lib/measurements.test.ts +5 -0
- package/src/lib/measurements.ts +12 -1
- package/src/lib/quick-action-feedback.ts +33 -0
- package/src/lib/quick-action-nodes.test.ts +94 -0
- package/src/lib/quick-action-nodes.ts +47 -0
- package/src/lib/scene-clipboard.test.ts +130 -1
- package/src/lib/scene-clipboard.ts +237 -13
- package/src/lib/selection-routing.test.ts +16 -1
- package/src/lib/selection-routing.ts +5 -0
- package/src/lib/sfx-bus.ts +33 -2
- package/src/lib/sfx-player.test.ts +148 -6
- package/src/lib/sfx-player.ts +156 -8
- package/src/lib/site-boundary.test.ts +53 -0
- package/src/lib/site-boundary.ts +27 -0
- package/src/lib/snapping-mode.test.ts +29 -2
- package/src/lib/snapping-mode.ts +15 -3
- package/src/lib/stair-duplication.test.ts +103 -0
- package/src/lib/stair-duplication.ts +57 -85
- package/src/lib/stair-levels.ts +2 -0
- package/src/lib/terrain-sculpt.test.ts +429 -0
- package/src/lib/terrain-sculpt.ts +301 -0
- package/src/lib/terrain-verb-color.test.ts +51 -0
- package/src/lib/terrain-verb-color.ts +58 -0
- package/src/lib/touch-gesture-priority.test.ts +45 -0
- package/src/lib/touch-gesture-priority.ts +30 -0
- package/src/lib/window-interaction.ts +1 -1
- package/src/store/camera-pose-store.test.ts +29 -0
- package/src/store/camera-pose-store.ts +27 -0
- package/src/store/live-draft-preview-stores.test.ts +136 -0
- package/src/store/navigation-sync-pose-store.test.ts +34 -0
- package/src/store/navigation-sync-pose-store.ts +27 -0
- package/src/store/terrain-sculpt-mode.test.ts +223 -0
- package/src/store/use-delete-confirmation.ts +27 -0
- package/src/store/use-drawing-view.test.ts +23 -0
- package/src/store/use-drawing-view.ts +84 -0
- package/src/store/use-editor.tsx +184 -15
- package/src/store/use-elevation-guides.ts +40 -0
- package/src/store/use-fence-curve-draft.ts +38 -8
- package/src/store/use-first-person-hud.ts +40 -0
- package/src/store/use-floorplan-annotation-visibility.ts +60 -0
- package/src/store/use-floorplan-draft-preview.ts +54 -4
- package/src/store/use-floorplan-mode.ts +110 -0
- package/src/store/use-interaction-scope.test.ts +23 -3
- package/src/store/use-interaction-scope.ts +8 -0
- package/src/store/use-measurement-draft.test.ts +29 -0
- package/src/store/use-measurement-draft.ts +33 -6
- package/src/store/use-path-draft-preview.ts +90 -0
- package/src/store/use-stair-build-preview.ts +12 -2
- package/src/lib/floorplan/selection-tool.ts +0 -271
|
@@ -23,6 +23,7 @@ import {
|
|
|
23
23
|
resolveAlignment,
|
|
24
24
|
resolveConnectivityUpdates,
|
|
25
25
|
resolveFacingIndicator,
|
|
26
|
+
resolveSupportSlabPatch,
|
|
26
27
|
sceneRegistry,
|
|
27
28
|
spatialGridManager,
|
|
28
29
|
useLiveNodeOverrides,
|
|
@@ -30,6 +31,7 @@ import {
|
|
|
30
31
|
useScene,
|
|
31
32
|
} from '@pascal-app/core'
|
|
32
33
|
import { useViewer } from '@pascal-app/viewer'
|
|
34
|
+
import { useThree } from '@react-three/fiber'
|
|
33
35
|
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
|
34
36
|
import { markToolCancelConsumed } from '../../../hooks/use-keyboard'
|
|
35
37
|
import { commitFreshPlacementSubtree } from '../../../lib/fresh-planar-placement'
|
|
@@ -54,6 +56,7 @@ import { DragBoundingBox } from '../shared/drag-bounding-box'
|
|
|
54
56
|
import { getFloorStackPreviewPosition } from '../shared/floor-stack-preview'
|
|
55
57
|
import { useFreshPlacementVisibility } from '../shared/fresh-placement-visibility'
|
|
56
58
|
import { PlacementBox } from '../shared/placement-box'
|
|
59
|
+
import { resolvePointerSupportSurface } from '../shared/pointer-support-cap'
|
|
57
60
|
|
|
58
61
|
/** Snap a world-plan coordinate to the editor's active grid step (0.5 / 0.25
|
|
59
62
|
* / 0.1 / 0.05), read live so changing the step mid-drag takes effect. */
|
|
@@ -231,6 +234,15 @@ const CLICK_TRIGGER_KINDS = [
|
|
|
231
234
|
] as const
|
|
232
235
|
|
|
233
236
|
export function MoveRegistryNodeTool({ node }: { node: AnyNode }) {
|
|
237
|
+
// Live camera ref — the pointer-surface cap reconstructs the cursor world
|
|
238
|
+
// ray (camera → grid hit) to find which walking surface is aimed at.
|
|
239
|
+
const camera = useThree((s) => s.camera)
|
|
240
|
+
const cameraRef = useRef(camera)
|
|
241
|
+
cameraRef.current = camera
|
|
242
|
+
// Level-local elevation of the surface the pointer ray points at,
|
|
243
|
+
// refreshed per grid move. Caps the floor-support election so a deck
|
|
244
|
+
// hanging above the aimed-at floor never lifts the dragged node.
|
|
245
|
+
const supportCapRef = useRef<number | null>(null)
|
|
234
246
|
// Kinds whose `position` lives in a host parent's local frame declare
|
|
235
247
|
// `movable.parentFrame` (cabinet module ↔ its run). The tool converts the
|
|
236
248
|
// plan-frame cursor through the capability's hooks and previews via
|
|
@@ -356,6 +368,7 @@ export function MoveRegistryNodeTool({ node }: { node: AnyNode }) {
|
|
|
356
368
|
? [(r[0] as number) ?? 0, rotationY, (r[2] as number) ?? 0]
|
|
357
369
|
: rotationY
|
|
358
370
|
})(),
|
|
371
|
+
maxElevation: supportCapRef.current,
|
|
359
372
|
})
|
|
360
373
|
},
|
|
361
374
|
[parentFrame, frameParent, node],
|
|
@@ -404,6 +417,9 @@ export function MoveRegistryNodeTool({ node }: { node: AnyNode }) {
|
|
|
404
417
|
rotationRef.current = originalRotationY
|
|
405
418
|
altRef.current = false
|
|
406
419
|
validRef.current = true
|
|
420
|
+
// No pointer surface known yet — uncapped election (the node keeps its
|
|
421
|
+
// persisted host / committed elevation until the first grid move).
|
|
422
|
+
supportCapRef.current = null
|
|
407
423
|
// Re-sync the box transform to the (possibly new) node. `node` changes
|
|
408
424
|
// without this component remounting whenever a positioned preset re-arms a
|
|
409
425
|
// fresh clone after a drop, or the user picks a different catalog tile —
|
|
@@ -591,8 +607,21 @@ export function MoveRegistryNodeTool({ node }: { node: AnyNode }) {
|
|
|
591
607
|
}
|
|
592
608
|
|
|
593
609
|
const onGridMove = (event: GridEvent) => {
|
|
594
|
-
|
|
595
|
-
|
|
610
|
+
// The pointer decides the target surface AND the cursor plan point,
|
|
611
|
+
// both resolved from the true camera ray in one place. The event's
|
|
612
|
+
// own hit can't be used directly: its plane rides at the ghost's
|
|
613
|
+
// last height, so whenever that plane sits on a different storey
|
|
614
|
+
// than the aimed-at surface the hit XZ is perspective-skewed along
|
|
615
|
+
// the ray — electing at that skewed point is what made a drag over
|
|
616
|
+
// a deck-above-a-floor hop between the two surfaces (each hop moved
|
|
617
|
+
// the plane, which re-skewed the next hit, which flipped the
|
|
618
|
+
// election back). Cap and XZ from the same ray ∩ pointed-surface
|
|
619
|
+
// test are plane-height independent, so the elected surface is a
|
|
620
|
+
// single fixed point per pointer ray.
|
|
621
|
+
const pointed = resolvePointerSupportSurface(cameraRef.current, event.position)
|
|
622
|
+
supportCapRef.current = pointed?.elevation ?? null
|
|
623
|
+
const rawX = pointed?.localPoint?.[0] ?? event.localPosition[0]
|
|
624
|
+
const rawZ = pointed?.localPoint?.[2] ?? event.localPosition[2]
|
|
596
625
|
revealFreshPlacement()
|
|
597
626
|
|
|
598
627
|
const resolved = resolvePlanarCursorPosition({
|
|
@@ -721,9 +750,12 @@ export function MoveRegistryNodeTool({ node }: { node: AnyNode }) {
|
|
|
721
750
|
// the absolute world plan position here. Polygon-based kinds
|
|
722
751
|
// (slab / ceiling / fence) follow a different delta contract —
|
|
723
752
|
// their floor-plan move-targets handle the override themselves.
|
|
753
|
+
// The pointer surface cap rides along so FloorElevationSystem's
|
|
754
|
+
// per-frame Y agrees with this tool's preview.
|
|
724
755
|
useLiveTransforms.getState().set(node.id, {
|
|
725
756
|
position,
|
|
726
757
|
rotation: rotationRef.current,
|
|
758
|
+
supportElevationCap: supportCapRef.current ?? undefined,
|
|
727
759
|
})
|
|
728
760
|
syncParentFramePreview(position)
|
|
729
761
|
markMovedNodeDirty()
|
|
@@ -781,9 +813,25 @@ export function MoveRegistryNodeTool({ node }: { node: AnyNode }) {
|
|
|
781
813
|
let committedId = node.id as AnyNodeId
|
|
782
814
|
|
|
783
815
|
if (useScene.getState().nodes[node.id]) {
|
|
816
|
+
const effectiveNode = {
|
|
817
|
+
...(node as Record<string, unknown>),
|
|
818
|
+
position,
|
|
819
|
+
rotation,
|
|
820
|
+
} as AnyNode
|
|
784
821
|
const data = {
|
|
785
822
|
position,
|
|
786
823
|
rotation,
|
|
824
|
+
// The pointer cap makes the persisted host reproduce the capped
|
|
825
|
+
// election — a drop under a deck stores the aimed-at lower slab
|
|
826
|
+
// (or the ground), not the deck hanging above.
|
|
827
|
+
...resolveSupportSlabPatch(
|
|
828
|
+
effectiveNode,
|
|
829
|
+
{
|
|
830
|
+
...useScene.getState().nodes,
|
|
831
|
+
[node.id]: effectiveNode,
|
|
832
|
+
},
|
|
833
|
+
{ maxElevation: supportCapRef.current },
|
|
834
|
+
),
|
|
787
835
|
...(isNew
|
|
788
836
|
? {
|
|
789
837
|
metadata: stripPlacementMetadataFlags(node.metadata),
|
|
@@ -819,6 +867,16 @@ export function MoveRegistryNodeTool({ node }: { node: AnyNode }) {
|
|
|
819
867
|
const liveParent = useScene.getState().nodes[frameParent.id as AnyNodeId]
|
|
820
868
|
if (liveNode && liveParent) {
|
|
821
869
|
parentFrame.onCommit(liveNode, liveParent, createSceneApi(useScene))
|
|
870
|
+
const committedNodes = useScene.getState().nodes
|
|
871
|
+
const committedParent = committedNodes[frameParent.id as AnyNodeId]
|
|
872
|
+
if (committedParent) {
|
|
873
|
+
useScene
|
|
874
|
+
.getState()
|
|
875
|
+
.updateNode(
|
|
876
|
+
committedParent.id,
|
|
877
|
+
resolveSupportSlabPatch(committedParent, committedNodes),
|
|
878
|
+
)
|
|
879
|
+
}
|
|
822
880
|
}
|
|
823
881
|
}
|
|
824
882
|
useScene.temporal.getState().pause()
|
|
@@ -834,9 +892,21 @@ export function MoveRegistryNodeTool({ node }: { node: AnyNode }) {
|
|
|
834
892
|
metadata: {},
|
|
835
893
|
position,
|
|
836
894
|
rotation,
|
|
837
|
-
})
|
|
895
|
+
}) as AnyNode
|
|
896
|
+
const committedNode = def.schema.parse({
|
|
897
|
+
...reparsed,
|
|
898
|
+
parentId: node.parentId,
|
|
899
|
+
...resolveSupportSlabPatch(
|
|
900
|
+
{ ...reparsed, parentId: node.parentId } as AnyNode,
|
|
901
|
+
{
|
|
902
|
+
...useScene.getState().nodes,
|
|
903
|
+
[reparsed.id]: reparsed,
|
|
904
|
+
},
|
|
905
|
+
{ maxElevation: supportCapRef.current },
|
|
906
|
+
),
|
|
907
|
+
}) as AnyNode
|
|
838
908
|
useScene.temporal.getState().resume()
|
|
839
|
-
useScene.getState().createNode(
|
|
909
|
+
useScene.getState().createNode(committedNode, node.parentId as AnyNodeId)
|
|
840
910
|
useScene.temporal.getState().pause()
|
|
841
911
|
committed = true
|
|
842
912
|
}
|
|
@@ -901,6 +971,7 @@ export function MoveRegistryNodeTool({ node }: { node: AnyNode }) {
|
|
|
901
971
|
useLiveTransforms.getState().set(node.id, {
|
|
902
972
|
position,
|
|
903
973
|
rotation: rotationRef.current,
|
|
974
|
+
supportElevationCap: supportCapRef.current ?? undefined,
|
|
904
975
|
})
|
|
905
976
|
syncParentFramePreview(position)
|
|
906
977
|
markMovedNodeDirty()
|
|
@@ -31,6 +31,7 @@ import { EDITOR_LAYER } from '../../../lib/constants'
|
|
|
31
31
|
import { sfxEmitter } from '../../../lib/sfx-bus'
|
|
32
32
|
import { snapWorldXZForActiveBuilding } from '../../../lib/world-grid-snap'
|
|
33
33
|
import useEditor, { isGridSnapActive, isMagneticSnapActive } from '../../../store/use-editor'
|
|
34
|
+
import { useFloorplanDraftPreview } from '../../../store/use-floorplan-draft-preview'
|
|
34
35
|
import { CursorSphere } from '../shared/cursor-sphere'
|
|
35
36
|
|
|
36
37
|
const DEFAULT_WALL_HEIGHT = 0.5
|
|
@@ -476,6 +477,9 @@ export const RoofTool: React.FC = () => {
|
|
|
476
477
|
})
|
|
477
478
|
|
|
478
479
|
if (corner1Ref.current) {
|
|
480
|
+
const draftPreview = useFloorplanDraftPreview.getState()
|
|
481
|
+
draftPreview.setRoofDraftStart([corner1Ref.current[0], corner1Ref.current[2]])
|
|
482
|
+
draftPreview.setRoofDraftEnd([gridX, gridZ])
|
|
479
483
|
updateOutline(corner1Ref.current, cursorPosition)
|
|
480
484
|
}
|
|
481
485
|
}
|
|
@@ -497,11 +501,17 @@ export const RoofTool: React.FC = () => {
|
|
|
497
501
|
setSelection({ selectedIds: [roofId as AnyNode['id']] })
|
|
498
502
|
|
|
499
503
|
corner1Ref.current = null
|
|
504
|
+
const draftPreview = useFloorplanDraftPreview.getState()
|
|
505
|
+
draftPreview.setRoofDraftStart(null)
|
|
506
|
+
draftPreview.setRoofDraftEnd(null)
|
|
500
507
|
outlineRef.current.visible = false
|
|
501
508
|
alignmentCandidates = collectRoofAlignmentAnchors(useScene.getState().nodes, currentLevelId)
|
|
502
509
|
clearSurfacePlanSnapFeedback()
|
|
503
510
|
} else {
|
|
504
511
|
corner1Ref.current = [gridX, y, gridZ]
|
|
512
|
+
const draftPreview = useFloorplanDraftPreview.getState()
|
|
513
|
+
draftPreview.setRoofDraftStart([gridX, gridZ])
|
|
514
|
+
draftPreview.setRoofDraftEnd([gridX, gridZ])
|
|
505
515
|
sfxEmitter.emit('sfx:structure-build-start')
|
|
506
516
|
setPreview((prev) => ({
|
|
507
517
|
...prev,
|
|
@@ -514,6 +524,9 @@ export const RoofTool: React.FC = () => {
|
|
|
514
524
|
if (corner1Ref.current) {
|
|
515
525
|
markToolCancelConsumed()
|
|
516
526
|
corner1Ref.current = null
|
|
527
|
+
const draftPreview = useFloorplanDraftPreview.getState()
|
|
528
|
+
draftPreview.setRoofDraftStart(null)
|
|
529
|
+
draftPreview.setRoofDraftEnd(null)
|
|
517
530
|
outlineRef.current.visible = false
|
|
518
531
|
setPreview((prev) => ({ ...prev, corner1: null }))
|
|
519
532
|
}
|
|
@@ -531,6 +544,9 @@ export const RoofTool: React.FC = () => {
|
|
|
531
544
|
clearSurfacePlanSnapFeedback()
|
|
532
545
|
|
|
533
546
|
corner1Ref.current = null
|
|
547
|
+
const draftPreview = useFloorplanDraftPreview.getState()
|
|
548
|
+
draftPreview.setRoofDraftStart(null)
|
|
549
|
+
draftPreview.setRoofDraftEnd(null)
|
|
534
550
|
}
|
|
535
551
|
}, [currentLevelId, setSelection])
|
|
536
552
|
|
|
@@ -6,6 +6,8 @@ type FloorStackPreviewArgs = {
|
|
|
6
6
|
rotation?: unknown
|
|
7
7
|
levelId?: string | null
|
|
8
8
|
nodes?: Record<AnyNodeId, AnyNode>
|
|
9
|
+
/** Pointer-decided support cap — see `FloorPlacedElevationArgs.maxElevation`. */
|
|
10
|
+
maxElevation?: number | null
|
|
9
11
|
}
|
|
10
12
|
|
|
11
13
|
export function getFloorStackPreviewPosition({
|
|
@@ -14,6 +16,7 @@ export function getFloorStackPreviewPosition({
|
|
|
14
16
|
rotation,
|
|
15
17
|
levelId,
|
|
16
18
|
nodes,
|
|
19
|
+
maxElevation,
|
|
17
20
|
}: FloorStackPreviewArgs): [number, number, number] {
|
|
18
21
|
return getFloorStackedPosition({
|
|
19
22
|
node,
|
|
@@ -21,5 +24,6 @@ export function getFloorStackPreviewPosition({
|
|
|
21
24
|
position,
|
|
22
25
|
rotation,
|
|
23
26
|
levelId,
|
|
27
|
+
maxElevation,
|
|
24
28
|
})
|
|
25
29
|
}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type AnyNodeId,
|
|
3
|
+
GROUND_SUPPORT_ID,
|
|
4
|
+
type GridEvent,
|
|
5
|
+
sceneRegistry,
|
|
6
|
+
terrainSupportLift,
|
|
7
|
+
useScene,
|
|
8
|
+
} from '@pascal-app/core'
|
|
9
|
+
import { useViewer } from '@pascal-app/viewer'
|
|
10
|
+
import { Vector3 } from 'three'
|
|
11
|
+
import { publishPlacementSurface } from '../../../lib/active-placement-surface'
|
|
12
|
+
import type { PointerSupportSurface } from './pointer-support-cap'
|
|
13
|
+
|
|
14
|
+
const SURFACE_UP = new Vector3(0, 1, 0)
|
|
15
|
+
const worldScratch = new Vector3()
|
|
16
|
+
const localScratch = new Vector3()
|
|
17
|
+
const surfacePointScratch = new Vector3()
|
|
18
|
+
|
|
19
|
+
export type HorizontalConstructionPlane = {
|
|
20
|
+
/** Building-local Y used by the cursor and draft preview. */
|
|
21
|
+
localY: number
|
|
22
|
+
/** World Y used by the shared placement grid. */
|
|
23
|
+
worldY: number
|
|
24
|
+
/** Level-local base elevation used when the node is committed. */
|
|
25
|
+
elevation: number | null
|
|
26
|
+
/** Support source selected at the first click. */
|
|
27
|
+
supportSlabId: string | null
|
|
28
|
+
/** Optional scene node from which this frozen plane was derived. */
|
|
29
|
+
sourceNodeId: AnyNodeId | null
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function resolveEventConstructionPlane(
|
|
33
|
+
event: GridEvent,
|
|
34
|
+
pointed: PointerSupportSurface | null,
|
|
35
|
+
): HorizontalConstructionPlane {
|
|
36
|
+
if (pointed) {
|
|
37
|
+
return {
|
|
38
|
+
localY: pointed.localPoint?.[1] ?? event.localPosition[1],
|
|
39
|
+
worldY: pointed.worldY,
|
|
40
|
+
elevation: pointed.elevation,
|
|
41
|
+
supportSlabId: pointed.supportSlabId,
|
|
42
|
+
sourceNodeId: pointed.sourceNodeId,
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
let elevation: number | null = null
|
|
47
|
+
const levelId = useViewer.getState().selection.levelId
|
|
48
|
+
const levelMesh = levelId ? sceneRegistry.nodes.get(levelId as AnyNodeId) : undefined
|
|
49
|
+
if (levelMesh) {
|
|
50
|
+
localScratch.set(event.position[0], event.position[1], event.position[2])
|
|
51
|
+
levelMesh.worldToLocal(localScratch)
|
|
52
|
+
elevation = localScratch.y
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return {
|
|
56
|
+
localY: event.localPosition[1],
|
|
57
|
+
worldY: event.position[1],
|
|
58
|
+
elevation,
|
|
59
|
+
supportSlabId: null,
|
|
60
|
+
sourceNodeId: null,
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function resolveLevelConstructionPlane(): HorizontalConstructionPlane | null {
|
|
65
|
+
const { buildingId, levelId } = useViewer.getState().selection
|
|
66
|
+
if (!levelId) return null
|
|
67
|
+
|
|
68
|
+
const levelMesh = sceneRegistry.nodes.get(levelId as AnyNodeId)
|
|
69
|
+
worldScratch.set(0, 0, 0)
|
|
70
|
+
if (levelMesh) levelMesh.localToWorld(worldScratch)
|
|
71
|
+
|
|
72
|
+
const buildingMesh = buildingId ? sceneRegistry.nodes.get(buildingId as AnyNodeId) : undefined
|
|
73
|
+
localScratch.copy(worldScratch)
|
|
74
|
+
if (buildingMesh) buildingMesh.worldToLocal(localScratch)
|
|
75
|
+
|
|
76
|
+
return {
|
|
77
|
+
localY: localScratch.y,
|
|
78
|
+
worldY: worldScratch.y,
|
|
79
|
+
elevation: 0,
|
|
80
|
+
supportSlabId: null,
|
|
81
|
+
sourceNodeId: null,
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function resampleTerrainConstructionPlane(
|
|
86
|
+
plane: HorizontalConstructionPlane,
|
|
87
|
+
point: readonly [number, number],
|
|
88
|
+
): HorizontalConstructionPlane {
|
|
89
|
+
if (plane.supportSlabId !== GROUND_SUPPORT_ID || plane.sourceNodeId) return plane
|
|
90
|
+
|
|
91
|
+
const levelId = useViewer.getState().selection.levelId
|
|
92
|
+
if (!levelId) return plane
|
|
93
|
+
const elevation = terrainSupportLift(useScene.getState().nodes, levelId, point[0], point[1])
|
|
94
|
+
if (elevation == null) return plane
|
|
95
|
+
|
|
96
|
+
const levelMesh = sceneRegistry.nodes.get(levelId as AnyNodeId)
|
|
97
|
+
worldScratch.set(point[0], elevation, point[1])
|
|
98
|
+
if (levelMesh) levelMesh.localToWorld(worldScratch)
|
|
99
|
+
|
|
100
|
+
const buildingId = useViewer.getState().selection.buildingId
|
|
101
|
+
const buildingMesh = buildingId ? sceneRegistry.nodes.get(buildingId as AnyNodeId) : undefined
|
|
102
|
+
localScratch.copy(worldScratch)
|
|
103
|
+
if (buildingMesh) buildingMesh.worldToLocal(localScratch)
|
|
104
|
+
|
|
105
|
+
return {
|
|
106
|
+
localY: localScratch.y,
|
|
107
|
+
worldY: worldScratch.y,
|
|
108
|
+
elevation,
|
|
109
|
+
supportSlabId: GROUND_SUPPORT_ID,
|
|
110
|
+
sourceNodeId: null,
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export function publishHorizontalConstructionPlane(
|
|
115
|
+
event: GridEvent,
|
|
116
|
+
plane: HorizontalConstructionPlane,
|
|
117
|
+
): void {
|
|
118
|
+
publishPlacementSurface(
|
|
119
|
+
surfacePointScratch.set(event.position[0], plane.worldY, event.position[2]),
|
|
120
|
+
SURFACE_UP,
|
|
121
|
+
'fixed-plane',
|
|
122
|
+
)
|
|
123
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, test } from 'bun:test'
|
|
2
|
+
import {
|
|
3
|
+
type AnyNode,
|
|
4
|
+
type AnyNodeId,
|
|
5
|
+
sceneRegistry,
|
|
6
|
+
spatialGridManager,
|
|
7
|
+
useScene,
|
|
8
|
+
} from '@pascal-app/core'
|
|
9
|
+
import { useViewer } from '@pascal-app/viewer'
|
|
10
|
+
import { BoxGeometry, Mesh, MeshBasicMaterial, PerspectiveCamera } from 'three'
|
|
11
|
+
import { resolvePointerSupportSurface } from './pointer-support-cap'
|
|
12
|
+
|
|
13
|
+
const LEVEL_ID = 'level_test' as AnyNodeId
|
|
14
|
+
const WALL_ID = 'wall_test' as AnyNodeId
|
|
15
|
+
|
|
16
|
+
describe('resolvePointerSupportSurface node tops', () => {
|
|
17
|
+
beforeEach(() => {
|
|
18
|
+
spatialGridManager.clear()
|
|
19
|
+
sceneRegistry.clear()
|
|
20
|
+
useViewer.setState({
|
|
21
|
+
selection: {
|
|
22
|
+
buildingId: null,
|
|
23
|
+
levelId: LEVEL_ID,
|
|
24
|
+
zoneId: null,
|
|
25
|
+
selectedIds: [],
|
|
26
|
+
},
|
|
27
|
+
} as never)
|
|
28
|
+
useScene.setState({
|
|
29
|
+
nodes: {
|
|
30
|
+
[LEVEL_ID]: {
|
|
31
|
+
id: LEVEL_ID,
|
|
32
|
+
type: 'level',
|
|
33
|
+
object: 'node',
|
|
34
|
+
parentId: null,
|
|
35
|
+
visible: true,
|
|
36
|
+
metadata: {},
|
|
37
|
+
children: [WALL_ID],
|
|
38
|
+
level: 0,
|
|
39
|
+
} as AnyNode,
|
|
40
|
+
[WALL_ID]: {
|
|
41
|
+
id: WALL_ID,
|
|
42
|
+
type: 'wall',
|
|
43
|
+
object: 'node',
|
|
44
|
+
parentId: LEVEL_ID,
|
|
45
|
+
visible: true,
|
|
46
|
+
metadata: {},
|
|
47
|
+
children: [],
|
|
48
|
+
assemblyLayers: [],
|
|
49
|
+
start: [-1, 0],
|
|
50
|
+
end: [1, 0],
|
|
51
|
+
frontSide: 'unknown',
|
|
52
|
+
backSide: 'unknown',
|
|
53
|
+
} as AnyNode,
|
|
54
|
+
},
|
|
55
|
+
})
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
afterEach(() => {
|
|
59
|
+
sceneRegistry.clear()
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
test('prefers the nearest upward-facing registered node surface over the ground', () => {
|
|
63
|
+
const wallMesh = new Mesh(new BoxGeometry(2, 2, 0.2), new MeshBasicMaterial())
|
|
64
|
+
wallMesh.position.y = 1
|
|
65
|
+
wallMesh.updateMatrixWorld(true)
|
|
66
|
+
sceneRegistry.nodes.set(WALL_ID, wallMesh)
|
|
67
|
+
sceneRegistry.byType.wall!.add(WALL_ID)
|
|
68
|
+
|
|
69
|
+
const camera = new PerspectiveCamera()
|
|
70
|
+
camera.position.set(0, 5, 0)
|
|
71
|
+
camera.updateMatrixWorld(true)
|
|
72
|
+
|
|
73
|
+
const support = resolvePointerSupportSurface(camera, [0, 0, 0], {
|
|
74
|
+
includeNodeTopSurfaces: true,
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
expect(support?.sourceNodeId).toBe(WALL_ID)
|
|
78
|
+
expect(support?.elevation).toBeCloseTo(2)
|
|
79
|
+
expect(support?.worldPoint).toEqual([0, 2, 0])
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
test('keeps the ground result when node-top surfaces are not requested', () => {
|
|
83
|
+
const wallMesh = new Mesh(new BoxGeometry(2, 2, 0.2), new MeshBasicMaterial())
|
|
84
|
+
wallMesh.position.y = 1
|
|
85
|
+
wallMesh.updateMatrixWorld(true)
|
|
86
|
+
sceneRegistry.nodes.set(WALL_ID, wallMesh)
|
|
87
|
+
sceneRegistry.byType.wall!.add(WALL_ID)
|
|
88
|
+
|
|
89
|
+
const camera = new PerspectiveCamera()
|
|
90
|
+
camera.position.set(0, 5, 0)
|
|
91
|
+
camera.updateMatrixWorld(true)
|
|
92
|
+
|
|
93
|
+
const support = resolvePointerSupportSurface(camera, [0, 0, 0])
|
|
94
|
+
|
|
95
|
+
expect(support?.sourceNodeId).toBeNull()
|
|
96
|
+
expect(support?.elevation).toBe(0)
|
|
97
|
+
})
|
|
98
|
+
})
|