@pascal-app/editor 1.0.0-beta.4 → 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 +15 -8
- package/scripts/generate-print-golden-house.ts +89 -0
- package/src/components/editor/alignment-3d-guide-layer.tsx +12 -3
- 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 +17 -5
- package/src/components/editor/elevation-3d-guide-layer.tsx +3 -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 +182 -57
- package/src/components/editor/floating-action-menu.tsx +55 -55
- package/src/components/editor/floorplan-background-selection.test.ts +31 -4
- package/src/components/editor/floorplan-background-selection.ts +27 -7
- package/src/components/editor/floorplan-panel.tsx +81 -57
- package/src/components/editor/grid.tsx +12 -7
- package/src/components/editor/group-actions.ts +34 -50
- package/src/components/editor/group-floating-action-menu.tsx +30 -15
- 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 +252 -46
- package/src/components/editor/measurement-pill.tsx +12 -12
- package/src/components/editor/node-action-menu.tsx +42 -1
- package/src/components/editor/node-arrow-handles.tsx +47 -58
- package/src/components/editor/opening-guides-3d-layer.tsx +13 -4
- package/src/components/editor/selection-manager.tsx +132 -47
- package/src/components/editor/site-edge-labels.tsx +2 -1
- package/src/components/editor/snapshot-capture-overlay.tsx +309 -41
- package/src/components/editor/snapshot-capture.test.ts +422 -0
- package/src/components/editor/snapshot-capture.ts +145 -0
- package/src/components/editor/thumbnail-generator.tsx +295 -232
- package/src/components/editor/use-floorplan-background-placement.ts +53 -5
- package/src/components/editor/wall-measurement-label.tsx +3 -2
- package/src/components/editor/wall-move-side-handles.tsx +299 -7
- package/src/components/editor-2d/floorplan-group-action-menu.tsx +23 -16
- 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 +204 -114
- 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 +494 -0
- package/src/components/tools/item/placement-strategies.ts +192 -24
- 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 +447 -180
- 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 +63 -8
- 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-brush-cursor.tsx +2 -1
- 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 +527 -4
- package/src/components/tools/wall/wall-drafting.ts +55 -462
- 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 +12 -13
- package/src/components/ui/command-palette/index.tsx +7 -2
- package/src/components/ui/controls/material-paint-panel.tsx +6 -4
- package/src/components/ui/controls/material-picker.tsx +4 -4
- package/src/components/ui/controls/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 +23 -6
- 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 +78 -26
- 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/panel-wrapper.tsx +228 -12
- 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/items-panel/function-tree-panel.tsx +7 -2
- package/src/components/ui/sidebar/panels/settings-panel/index.tsx +390 -36
- package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +31 -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 +128 -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 +22 -7
- package/src/components/ui/sidebar/tab-bar.tsx +9 -1
- package/src/components/ui/sidebar/use-plugin-panels.tsx +17 -2
- package/src/components/viewer/floorplan-compass-button.tsx +50 -0
- package/src/components/viewer/floorplan-preview-geometry.test.ts +107 -0
- package/src/components/viewer/floorplan-preview-geometry.ts +281 -0
- package/src/components/viewer/floorplan-preview-navigation.test.ts +43 -0
- package/src/components/viewer/floorplan-preview-navigation.ts +62 -0
- package/src/components/viewer/floorplan-preview.tsx +1069 -0
- package/src/components/viewer/use-viewer-camera-navigation-sync.ts +140 -0
- package/src/components/viewer/viewer-scene-header.tsx +2 -1
- package/src/components/viewer/viewer-stage-modes.test.ts +38 -0
- package/src/components/viewer/viewer-stage-modes.ts +49 -0
- package/src/components/viewer/viewer-stage-switcher.tsx +95 -0
- package/src/components/viewer/viewer-stage.test.tsx +47 -0
- package/src/components/viewer/viewer-stage.tsx +221 -0
- package/src/components/viewer-overlay.tsx +13 -30
- 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 +122 -1
- package/src/hooks/use-auto-save.ts +119 -13
- 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 +164 -63
- package/src/hooks/use-registered-tool-enabled.ts +20 -0
- package/src/hooks/use-save-shortcut.ts +33 -0
- package/src/index.tsx +109 -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/contextual-help.test.ts +8 -0
- package/src/lib/contextual-help.ts +8 -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.test.ts +47 -0
- package/src/lib/floorplan/floorplan-readonly.ts +85 -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 +523 -77
- package/src/lib/history.ts +133 -14
- package/src/lib/host-tree-children.test.ts +27 -0
- package/src/lib/host-tree-children.ts +79 -0
- package/src/lib/inspector-card-mode.test.ts +92 -0
- package/src/lib/inspector-card-mode.ts +60 -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/measurement-parser.ts +3 -0
- package/src/lib/measurements.test.ts +4 -0
- package/src/lib/model-export.ts +29 -0
- package/src/lib/paint-preview-owner.ts +71 -0
- package/src/lib/paint-scope.test.ts +58 -2
- package/src/lib/paint-scope.ts +8 -2
- 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 +125 -6
- package/src/lib/selection-routing.ts +34 -1
- package/src/lib/session-groups.test.ts +88 -0
- package/src/lib/session-groups.ts +201 -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/structured-clone-fallback.test.ts +45 -0
- package/src/lib/structured-clone-fallback.ts +24 -0
- 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/store/use-session-groups.ts +103 -0
- package/src/components/tools/roof/roof-tool.tsx +0 -656
- package/src/components/ui/helpers/roof-helper.tsx +0 -14
|
@@ -1,9 +1,21 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
|
-
import {
|
|
4
|
-
|
|
3
|
+
import {
|
|
4
|
+
emitter,
|
|
5
|
+
type FenceNode,
|
|
6
|
+
isCurvedWall,
|
|
7
|
+
nodeRegistry,
|
|
8
|
+
type WallNode,
|
|
9
|
+
} from '@pascal-app/core'
|
|
10
|
+
import {
|
|
11
|
+
type MouseEvent as ReactMouseEvent,
|
|
12
|
+
useCallback,
|
|
13
|
+
useEffect,
|
|
14
|
+
useSyncExternalStore,
|
|
15
|
+
} from 'react'
|
|
5
16
|
import { resolveCeilingPlanPointSnap } from '../../lib/ceiling-plan-snap'
|
|
6
17
|
import { alignFloorplanDraftPoint, getPlanPointDistance } from '../../lib/floorplan'
|
|
18
|
+
import { resolveGenericFloorplanGridEventPoint } from '../../lib/floorplan-grid-event-point'
|
|
7
19
|
import { resolveSlabPlanPointSnap } from '../../lib/slab-plan-snap'
|
|
8
20
|
import useAlignmentGuides from '../../store/use-alignment-guides'
|
|
9
21
|
import useEditor, { isAngleSnapActive, isMagneticSnapActive } from '../../store/use-editor'
|
|
@@ -12,6 +24,9 @@ import useSegmentDraftChain from '../../store/use-segment-draft-chain'
|
|
|
12
24
|
import { snapFenceDraftPoint } from '../tools/fence/fence-drafting'
|
|
13
25
|
import { getSegmentGridStep, type WallPlanPoint } from '../tools/wall/wall-drafting'
|
|
14
26
|
|
|
27
|
+
const NOOP_SUBSCRIBE = () => () => {}
|
|
28
|
+
const DEFAULT_ROOF_FOOTPRINT_CHOICE = () => 'draw'
|
|
29
|
+
|
|
15
30
|
type UseFloorplanBackgroundPlacementArgs = {
|
|
16
31
|
activePolygonDraftPoints: WallPlanPoint[]
|
|
17
32
|
ceilingDraftPoints: WallPlanPoint[]
|
|
@@ -55,6 +70,7 @@ type UseFloorplanBackgroundPlacementArgs = {
|
|
|
55
70
|
isWallBuildActive: boolean
|
|
56
71
|
isZoneBuildActive: boolean
|
|
57
72
|
levelId: string | null
|
|
73
|
+
registryToolOwnsSnapping: boolean
|
|
58
74
|
roofDraftStart: WallPlanPoint | null
|
|
59
75
|
setCursorPoint: React.Dispatch<React.SetStateAction<WallPlanPoint | null>>
|
|
60
76
|
setFenceDraftEnd: React.Dispatch<React.SetStateAction<WallPlanPoint | null>>
|
|
@@ -113,6 +129,7 @@ export function useFloorplanBackgroundPlacement({
|
|
|
113
129
|
isWallBuildActive,
|
|
114
130
|
isZoneBuildActive,
|
|
115
131
|
levelId,
|
|
132
|
+
registryToolOwnsSnapping,
|
|
116
133
|
roofDraftStart,
|
|
117
134
|
setCursorPoint,
|
|
118
135
|
setFenceDraftEnd,
|
|
@@ -125,6 +142,26 @@ export function useFloorplanBackgroundPlacement({
|
|
|
125
142
|
walls,
|
|
126
143
|
worldGridSnap,
|
|
127
144
|
}: UseFloorplanBackgroundPlacementArgs) {
|
|
145
|
+
// Read the roof's footprint-source option through the registry, not
|
|
146
|
+
// `@pascal-app/nodes`: this file lands in the nodes package's program via
|
|
147
|
+
// its editor imports, so a direct nodes import would cycle onto nodes' own
|
|
148
|
+
// dist output.
|
|
149
|
+
const roofFootprintOption = nodeRegistry
|
|
150
|
+
.get('roof')
|
|
151
|
+
?.toolOptions?.find((option) => option.id === 'footprintSource')
|
|
152
|
+
const roofFootprintChoice = useSyncExternalStore(
|
|
153
|
+
roofFootprintOption?.subscribe ?? NOOP_SUBSCRIBE,
|
|
154
|
+
roofFootprintOption?.value ?? DEFAULT_ROOF_FOOTPRINT_CHOICE,
|
|
155
|
+
roofFootprintOption?.value ?? DEFAULT_ROOF_FOOTPRINT_CHOICE,
|
|
156
|
+
)
|
|
157
|
+
// Conical always builds from a curved wall pick, regardless of the choice.
|
|
158
|
+
const roofIsConical = useEditor((state) => state.toolDefaults.roof?.roofType === 'conical')
|
|
159
|
+
const roofFootprintSource = roofIsConical ? 'walls' : roofFootprintChoice
|
|
160
|
+
|
|
161
|
+
useEffect(() => {
|
|
162
|
+
if (isRoofBuildActive && roofFootprintSource !== 'draw') clearRoofPlacementDraft()
|
|
163
|
+
}, [clearRoofPlacementDraft, isRoofBuildActive, roofFootprintSource])
|
|
164
|
+
|
|
128
165
|
const handleBackgroundPlacementClick = useCallback(
|
|
129
166
|
(
|
|
130
167
|
planPoint: WallPlanPoint,
|
|
@@ -188,6 +225,11 @@ export function useFloorplanBackgroundPlacement({
|
|
|
188
225
|
emitFloorplanGridEvent('click', snappedPoint, event)
|
|
189
226
|
setCursorPoint(snappedPoint)
|
|
190
227
|
|
|
228
|
+
if (roofFootprintSource !== 'draw') {
|
|
229
|
+
clearRoofPlacementDraft()
|
|
230
|
+
return true
|
|
231
|
+
}
|
|
232
|
+
|
|
191
233
|
if (roofDraftStart) {
|
|
192
234
|
clearRoofPlacementDraft()
|
|
193
235
|
} else {
|
|
@@ -357,9 +399,13 @@ export function useFloorplanBackgroundPlacement({
|
|
|
357
399
|
// local floor-plan draft handler (column / spawn / shelf / etc.).
|
|
358
400
|
// The tool's `grid:click` subscriber owns the placement.
|
|
359
401
|
if (isFloorplanGridInteractionActive) {
|
|
360
|
-
const
|
|
361
|
-
|
|
362
|
-
|
|
402
|
+
const eventPoint = resolveGenericFloorplanGridEventPoint({
|
|
403
|
+
point: planPoint,
|
|
404
|
+
registryToolOwnsSnapping,
|
|
405
|
+
snap: getSnappedFloorplanPoint,
|
|
406
|
+
})
|
|
407
|
+
emitFloorplanGridEvent('click', eventPoint, event)
|
|
408
|
+
setCursorPoint(eventPoint)
|
|
363
409
|
return true
|
|
364
410
|
}
|
|
365
411
|
|
|
@@ -392,6 +438,8 @@ export function useFloorplanBackgroundPlacement({
|
|
|
392
438
|
isZoneBuildActive,
|
|
393
439
|
levelId,
|
|
394
440
|
roofDraftStart,
|
|
441
|
+
registryToolOwnsSnapping,
|
|
442
|
+
roofFootprintSource,
|
|
395
443
|
setCursorPoint,
|
|
396
444
|
setFenceDraftEnd,
|
|
397
445
|
setFenceDraftStart,
|
|
@@ -510,6 +510,7 @@ function SelectedMeasurementAnnotation({ node }: { node: WallNode | ItemNode })
|
|
|
510
510
|
function WallMeasurementAnnotation({ wall }: { wall: WallNode }) {
|
|
511
511
|
const nodes = useScene((state) => state.nodes)
|
|
512
512
|
const unit = useViewer((state) => state.unit)
|
|
513
|
+
const metricNotation = useViewer((state) => state.metricNotation)
|
|
513
514
|
const isNight = useViewer((state) => getSceneTheme(state.sceneTheme).appearance === 'dark')
|
|
514
515
|
const color = isNight ? '#ffffff' : '#111111'
|
|
515
516
|
const shadowColor = isNight ? '#111111' : '#ffffff'
|
|
@@ -528,9 +529,9 @@ function WallMeasurementAnnotation({ wall }: { wall: WallNode }) {
|
|
|
528
529
|
}
|
|
529
530
|
return total
|
|
530
531
|
}, [guide, wall])
|
|
531
|
-
const label = formatLinearMeasurement(length, unit)
|
|
532
|
+
const label = formatLinearMeasurement(length, unit, metricNotation)
|
|
532
533
|
const height = useMemo(() => getWallEffectiveHeightForNodes(wall, nodes), [nodes, wall])
|
|
533
|
-
const heightLabel = `H ${formatLinearMeasurement(height, unit)}`
|
|
534
|
+
const heightLabel = `H ${formatLinearMeasurement(height, unit, metricNotation)}`
|
|
534
535
|
|
|
535
536
|
if (!(guide && Number.isFinite(length) && length >= 0.01)) return null
|
|
536
537
|
|
|
@@ -4,8 +4,11 @@ import {
|
|
|
4
4
|
type AnyNode,
|
|
5
5
|
type AnyNodeId,
|
|
6
6
|
type FenceNode,
|
|
7
|
+
getFenceCenterlineFrameAt,
|
|
8
|
+
getFenceCenterlineLength,
|
|
7
9
|
getWallBaseElevationForNodes,
|
|
8
10
|
getWallCurveFrameAt,
|
|
11
|
+
getWallCurveLength,
|
|
9
12
|
getWallEffectiveHeightForNodes,
|
|
10
13
|
getWallThickness,
|
|
11
14
|
isCurvedWall,
|
|
@@ -16,6 +19,7 @@ import {
|
|
|
16
19
|
type WallNode,
|
|
17
20
|
} from '@pascal-app/core'
|
|
18
21
|
import { useViewer } from '@pascal-app/viewer'
|
|
22
|
+
import { Html } from '@react-three/drei'
|
|
19
23
|
import { createPortal, type ThreeEvent, useFrame, useThree } from '@react-three/fiber'
|
|
20
24
|
import { useEffect, useMemo, useRef, useState } from 'react'
|
|
21
25
|
import {
|
|
@@ -29,6 +33,7 @@ import {
|
|
|
29
33
|
OrthographicCamera,
|
|
30
34
|
Plane,
|
|
31
35
|
Quaternion,
|
|
36
|
+
Ray,
|
|
32
37
|
Shape,
|
|
33
38
|
Vector2,
|
|
34
39
|
Vector3,
|
|
@@ -37,6 +42,7 @@ import { mergeGeometries } from 'three/examples/jsm/utils/BufferGeometryUtils.js
|
|
|
37
42
|
import { MeshBasicNodeMaterial } from 'three/webgpu'
|
|
38
43
|
import {
|
|
39
44
|
clearStructuralElevationGuide,
|
|
45
|
+
getFenceBaseElevationForNodes,
|
|
40
46
|
publishStructuralElevationGuide,
|
|
41
47
|
resolveStructuralElevationSnap,
|
|
42
48
|
} from '../../lib/elevation-guides'
|
|
@@ -52,6 +58,7 @@ import useInteractionScope, {
|
|
|
52
58
|
import { suppressBoxSelectForPointer } from '../tools/select/box-select-state'
|
|
53
59
|
import { resolveResizeSnapValue } from './handles/resize-snap'
|
|
54
60
|
import { type HandleDragControls, useHandleDrag } from './handles/use-handle-drag'
|
|
61
|
+
import { MeasurementPill } from './measurement-pill'
|
|
55
62
|
import {
|
|
56
63
|
createArrowHitAreaGeometry,
|
|
57
64
|
createEndpointHitAreaGeometry,
|
|
@@ -76,6 +83,8 @@ const CORNER_DASH_SIZE = 0.1
|
|
|
76
83
|
const CORNER_GAP_SIZE = 0.07
|
|
77
84
|
const CORNER_DASH_THICKNESS = 0.006
|
|
78
85
|
const CORNER_FLOOR_OFFSET = 0.01
|
|
86
|
+
const THICKNESS_HANDLE_RADIUS = 0.075
|
|
87
|
+
const MIN_WALL_THICKNESS = 0.05
|
|
79
88
|
|
|
80
89
|
type WallMoveHandle = {
|
|
81
90
|
key: string
|
|
@@ -140,14 +149,11 @@ export function WallMoveSideHandles() {
|
|
|
140
149
|
const isCurveReshape = useIsCurveReshape()
|
|
141
150
|
|
|
142
151
|
const selectedId = selectedIds.length === 1 ? selectedIds[0] : null
|
|
143
|
-
//
|
|
144
|
-
//
|
|
145
|
-
// Only walls still need the legacy renderer here — the registry path
|
|
146
|
-
// didn't render correctly for walls specifically and was reverted in
|
|
147
|
-
// commit 0e207a7f; revisit once that's diagnosed.
|
|
152
|
+
// Walls still use this legacy handle renderer. Fences retain their registry
|
|
153
|
+
// handles and mount only the matching thickness dots from this component.
|
|
148
154
|
const selectedNode = useScene((state) => {
|
|
149
155
|
const node = selectedId ? state.nodes[selectedId as AnyNodeId] : null
|
|
150
|
-
return node?.type === 'wall' ? node : null
|
|
156
|
+
return node?.type === 'wall' || node?.type === 'fence' ? node : null
|
|
151
157
|
})
|
|
152
158
|
|
|
153
159
|
const shouldRender =
|
|
@@ -160,7 +166,11 @@ export function WallMoveSideHandles() {
|
|
|
160
166
|
|
|
161
167
|
if (!shouldRender || !selectedNode) return null
|
|
162
168
|
|
|
163
|
-
return
|
|
169
|
+
return selectedNode.type === 'wall' ? (
|
|
170
|
+
<WallMoveSideHandlesForWall wall={selectedNode} />
|
|
171
|
+
) : (
|
|
172
|
+
<FenceThicknessHandles fence={selectedNode} />
|
|
173
|
+
)
|
|
164
174
|
}
|
|
165
175
|
|
|
166
176
|
function WallMoveSideHandlesForWall({ wall }: { wall: WallNode }) {
|
|
@@ -220,6 +230,18 @@ function WallMoveSideHandlesForWall({ wall }: { wall: WallNode }) {
|
|
|
220
230
|
<WallMoveArrowHandle handle={handle} key={handle.key} wall={effectiveWall} />
|
|
221
231
|
))}
|
|
222
232
|
<WallHeightArrowHandle wall={effectiveWall} />
|
|
233
|
+
<StructureThicknessHandle
|
|
234
|
+
baseElevation={baseElevation}
|
|
235
|
+
levelObject={levelObject}
|
|
236
|
+
node={effectiveWall}
|
|
237
|
+
side={1}
|
|
238
|
+
/>
|
|
239
|
+
<StructureThicknessHandle
|
|
240
|
+
baseElevation={baseElevation}
|
|
241
|
+
levelObject={levelObject}
|
|
242
|
+
node={effectiveWall}
|
|
243
|
+
side={-1}
|
|
244
|
+
/>
|
|
223
245
|
<WallCornerLeaderHandle endpoint="start" wall={effectiveWall} />
|
|
224
246
|
<WallCornerLeaderHandle endpoint="end" wall={effectiveWall} />
|
|
225
247
|
</group>
|
|
@@ -233,6 +255,276 @@ function WallMoveSideHandlesForWall({ wall }: { wall: WallNode }) {
|
|
|
233
255
|
)
|
|
234
256
|
}
|
|
235
257
|
|
|
258
|
+
function closestAxisParameterToRay(axisOrigin: Vector3, axisDirection: Vector3, ray: Ray) {
|
|
259
|
+
const originToRay = new Vector3().subVectors(axisOrigin, ray.origin)
|
|
260
|
+
const directionDot = axisDirection.dot(ray.direction)
|
|
261
|
+
const axisDot = axisDirection.dot(originToRay)
|
|
262
|
+
const rayDot = ray.direction.dot(originToRay)
|
|
263
|
+
const denominator = 1 - directionDot * directionDot
|
|
264
|
+
if (Math.abs(denominator) < 1e-6) return -axisDot
|
|
265
|
+
|
|
266
|
+
const axisParameter = (directionDot * rayDot - axisDot) / denominator
|
|
267
|
+
const rayParameter = rayDot + directionDot * axisParameter
|
|
268
|
+
return rayParameter < 0 ? -axisDot : axisParameter
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
function StructureThicknessHandle({
|
|
272
|
+
node,
|
|
273
|
+
side,
|
|
274
|
+
baseElevation,
|
|
275
|
+
levelObject,
|
|
276
|
+
}: {
|
|
277
|
+
node: WallNode | FenceNode
|
|
278
|
+
side: 1 | -1
|
|
279
|
+
baseElevation: number
|
|
280
|
+
levelObject: Object3D
|
|
281
|
+
}) {
|
|
282
|
+
const [isHovered, setIsHovered] = useState(false)
|
|
283
|
+
const [isDragging, setIsDragging] = useState(false)
|
|
284
|
+
const { camera } = useThree()
|
|
285
|
+
const unit = useViewer((state) => state.unit)
|
|
286
|
+
const zoom = camera instanceof OrthographicCamera ? 1 / camera.zoom : 1
|
|
287
|
+
const frame =
|
|
288
|
+
node.type === 'fence' ? getFenceCenterlineFrameAt(node, 0.5) : getWallCurveFrameAt(node, 0.5)
|
|
289
|
+
const thickness = node.type === 'fence' ? (node.thickness ?? 0.08) : getWallThickness(node)
|
|
290
|
+
const structureHeight =
|
|
291
|
+
node.type === 'fence'
|
|
292
|
+
? (node.height ?? 1.8)
|
|
293
|
+
: getWallEffectiveHeightForNodes(node, useScene.getState().nodes)
|
|
294
|
+
const outward = new Vector2(frame.normal.x * side, frame.normal.y * side)
|
|
295
|
+
const faceOffset = thickness / 2 + 0.006
|
|
296
|
+
const position: [number, number, number] = [
|
|
297
|
+
frame.point.x + outward.x * faceOffset,
|
|
298
|
+
structureHeight / 2,
|
|
299
|
+
frame.point.y + outward.y * faceOffset,
|
|
300
|
+
]
|
|
301
|
+
const rotationY = Math.atan2(outward.x, outward.y)
|
|
302
|
+
const isActive = isHovered || isDragging
|
|
303
|
+
const scale = zoom * (isActive ? 1.18 : 1)
|
|
304
|
+
const hitGeometry = useMemo(() => createEndpointHitAreaGeometry(0.13), [])
|
|
305
|
+
const hitMaterial = useInvisibleHitAreaMaterial()
|
|
306
|
+
const dotMaterial = useMemo(
|
|
307
|
+
() =>
|
|
308
|
+
new MeshBasicNodeMaterial({
|
|
309
|
+
color: new Color(ARROW_COLOR),
|
|
310
|
+
side: DoubleSide,
|
|
311
|
+
depthTest: false,
|
|
312
|
+
depthWrite: false,
|
|
313
|
+
}),
|
|
314
|
+
[],
|
|
315
|
+
)
|
|
316
|
+
const ringMaterial = useMemo(
|
|
317
|
+
() =>
|
|
318
|
+
new MeshBasicNodeMaterial({
|
|
319
|
+
color: new Color(ARROW_HOVER_COLOR),
|
|
320
|
+
side: DoubleSide,
|
|
321
|
+
depthTest: false,
|
|
322
|
+
depthWrite: false,
|
|
323
|
+
}),
|
|
324
|
+
[],
|
|
325
|
+
)
|
|
326
|
+
const dragControls = useMemo<HandleDragControls>(
|
|
327
|
+
() => ({
|
|
328
|
+
onStart: () => {},
|
|
329
|
+
onEnd: () => {},
|
|
330
|
+
}),
|
|
331
|
+
[],
|
|
332
|
+
)
|
|
333
|
+
|
|
334
|
+
useEffect(() => {
|
|
335
|
+
dotMaterial.color.set(isActive ? ARROW_HOVER_COLOR : ARROW_COLOR)
|
|
336
|
+
}, [dotMaterial, isActive])
|
|
337
|
+
useEffect(() => () => hitGeometry.dispose(), [hitGeometry])
|
|
338
|
+
useEffect(() => () => dotMaterial.dispose(), [dotMaterial])
|
|
339
|
+
useEffect(() => () => ringMaterial.dispose(), [ringMaterial])
|
|
340
|
+
useEffect(
|
|
341
|
+
() => () => {
|
|
342
|
+
if (document.body.style.cursor === 'ew-resize') document.body.style.cursor = ''
|
|
343
|
+
},
|
|
344
|
+
[],
|
|
345
|
+
)
|
|
346
|
+
|
|
347
|
+
const activateThicknessResize = useHandleDrag({
|
|
348
|
+
kind: 'drag',
|
|
349
|
+
cursor: 'ew-resize',
|
|
350
|
+
dragControls,
|
|
351
|
+
handleIndex: side > 0 ? 0 : 1,
|
|
352
|
+
node,
|
|
353
|
+
rideObject: levelObject,
|
|
354
|
+
setIsDragging,
|
|
355
|
+
onStart: ({ event, getPointerRay, initialNode, nodeId, rideObject }) => {
|
|
356
|
+
if (initialNode.type !== 'wall' && initialNode.type !== 'fence') return null
|
|
357
|
+
|
|
358
|
+
rideObject.updateWorldMatrix(true, false)
|
|
359
|
+
const initialFrame =
|
|
360
|
+
initialNode.type === 'fence'
|
|
361
|
+
? getFenceCenterlineFrameAt(initialNode, 0.5)
|
|
362
|
+
: getWallCurveFrameAt(initialNode, 0.5)
|
|
363
|
+
const localAxis = new Vector3(initialFrame.normal.x * side, 0, initialFrame.normal.y * side)
|
|
364
|
+
const initialStructureHeight =
|
|
365
|
+
initialNode.type === 'fence'
|
|
366
|
+
? (initialNode.height ?? 1.8)
|
|
367
|
+
: getWallEffectiveHeightForNodes(initialNode, useScene.getState().nodes)
|
|
368
|
+
const initialStructureThickness =
|
|
369
|
+
initialNode.type === 'fence'
|
|
370
|
+
? (initialNode.thickness ?? 0.08)
|
|
371
|
+
: getWallThickness(initialNode)
|
|
372
|
+
const minimumThickness = initialNode.type === 'fence' ? 0.03 : MIN_WALL_THICKNESS
|
|
373
|
+
const localOrigin = new Vector3(
|
|
374
|
+
initialFrame.point.x + localAxis.x * (initialStructureThickness / 2 + 0.006),
|
|
375
|
+
baseElevation + initialStructureHeight / 2,
|
|
376
|
+
initialFrame.point.y + localAxis.z * (initialStructureThickness / 2 + 0.006),
|
|
377
|
+
)
|
|
378
|
+
const worldOrigin = localOrigin.clone().applyMatrix4(rideObject.matrixWorld)
|
|
379
|
+
const worldAxisEnd = localOrigin.clone().add(localAxis).applyMatrix4(rideObject.matrixWorld)
|
|
380
|
+
const worldAxis = worldAxisEnd.sub(worldOrigin)
|
|
381
|
+
const axisScale = worldAxis.length()
|
|
382
|
+
if (axisScale < 1e-6) return null
|
|
383
|
+
worldAxis.normalize()
|
|
384
|
+
|
|
385
|
+
const pointerRay = new Ray()
|
|
386
|
+
const initialPointer =
|
|
387
|
+
closestAxisParameterToRay(
|
|
388
|
+
worldOrigin,
|
|
389
|
+
worldAxis,
|
|
390
|
+
getPointerRay(event.nativeEvent.clientX, event.nativeEvent.clientY, pointerRay),
|
|
391
|
+
) / axisScale
|
|
392
|
+
let lastThickness = initialStructureThickness
|
|
393
|
+
|
|
394
|
+
return {
|
|
395
|
+
onBegin: () => {
|
|
396
|
+
useInteractionScope.getState().begin({
|
|
397
|
+
kind: 'handle-drag',
|
|
398
|
+
nodeId,
|
|
399
|
+
handle: 'thickness',
|
|
400
|
+
})
|
|
401
|
+
},
|
|
402
|
+
onEnd: () => {
|
|
403
|
+
useInteractionScope.getState().endIf((scope) => scope.kind === 'handle-drag')
|
|
404
|
+
},
|
|
405
|
+
move: ({ event: moveEvent, getPointerRay: getMovePointerRay }) => {
|
|
406
|
+
const currentPointer =
|
|
407
|
+
closestAxisParameterToRay(
|
|
408
|
+
worldOrigin,
|
|
409
|
+
worldAxis,
|
|
410
|
+
getMovePointerRay(moveEvent.clientX, moveEvent.clientY, pointerRay),
|
|
411
|
+
) / axisScale
|
|
412
|
+
const rawThickness = initialStructureThickness + (currentPointer - initialPointer) * 2
|
|
413
|
+
const nextThickness = Math.max(
|
|
414
|
+
minimumThickness,
|
|
415
|
+
resolveResizeSnapValue({
|
|
416
|
+
rawValue: rawThickness,
|
|
417
|
+
fallbackValue: lastThickness,
|
|
418
|
+
gridSnapEnabled: true,
|
|
419
|
+
gridSnapActive: isGridSnapActive(),
|
|
420
|
+
gridSnapStep: useEditor.getState().gridSnapStep,
|
|
421
|
+
magneticSnapActive: false,
|
|
422
|
+
}),
|
|
423
|
+
)
|
|
424
|
+
if (nextThickness !== lastThickness) sfxEmitter.emit('sfx:resize')
|
|
425
|
+
lastThickness = nextThickness
|
|
426
|
+
return { thickness: nextThickness }
|
|
427
|
+
},
|
|
428
|
+
}
|
|
429
|
+
},
|
|
430
|
+
})
|
|
431
|
+
|
|
432
|
+
return (
|
|
433
|
+
<>
|
|
434
|
+
<group position={position} rotation={[0, rotationY, 0]} scale={scale}>
|
|
435
|
+
<InvisibleHandleHitArea
|
|
436
|
+
geometry={hitGeometry}
|
|
437
|
+
material={hitMaterial}
|
|
438
|
+
onPointerDown={activateThicknessResize}
|
|
439
|
+
onPointerEnter={(event) => {
|
|
440
|
+
event.stopPropagation()
|
|
441
|
+
setIsHovered(true)
|
|
442
|
+
document.body.style.cursor = 'ew-resize'
|
|
443
|
+
}}
|
|
444
|
+
onPointerLeave={(event) => {
|
|
445
|
+
event.stopPropagation()
|
|
446
|
+
setIsHovered(false)
|
|
447
|
+
if (!isDragging && document.body.style.cursor === 'ew-resize') {
|
|
448
|
+
document.body.style.cursor = ''
|
|
449
|
+
}
|
|
450
|
+
}}
|
|
451
|
+
scale={1}
|
|
452
|
+
/>
|
|
453
|
+
<mesh material={dotMaterial} raycast={NO_RAYCAST} renderOrder={1003}>
|
|
454
|
+
<circleGeometry args={[THICKNESS_HANDLE_RADIUS, 32]} />
|
|
455
|
+
</mesh>
|
|
456
|
+
<mesh material={ringMaterial} raycast={NO_RAYCAST} renderOrder={1002}>
|
|
457
|
+
<ringGeometry args={[THICKNESS_HANDLE_RADIUS, THICKNESS_HANDLE_RADIUS * 1.18, 32]} />
|
|
458
|
+
</mesh>
|
|
459
|
+
</group>
|
|
460
|
+
{isDragging ? (
|
|
461
|
+
<Html
|
|
462
|
+
center
|
|
463
|
+
position={[position[0], position[1] + 0.22, position[2]]}
|
|
464
|
+
style={{ pointerEvents: 'none', userSelect: 'none' }}
|
|
465
|
+
zIndexRange={[25, 0]}
|
|
466
|
+
>
|
|
467
|
+
<MeasurementPill
|
|
468
|
+
height={structureHeight}
|
|
469
|
+
length={
|
|
470
|
+
node.type === 'fence' ? getFenceCenterlineLength(node) : getWallCurveLength(node)
|
|
471
|
+
}
|
|
472
|
+
primary="thickness"
|
|
473
|
+
thickness={thickness}
|
|
474
|
+
unit={unit}
|
|
475
|
+
/>
|
|
476
|
+
</Html>
|
|
477
|
+
) : null}
|
|
478
|
+
</>
|
|
479
|
+
)
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
function FenceThicknessHandles({ fence }: { fence: FenceNode }) {
|
|
483
|
+
const nodes = useScene((state) => state.nodes)
|
|
484
|
+
const liveOverride = useLiveNodeOverrides((state) => state.overrides.get(fence.id))
|
|
485
|
+
const effectiveFence = useMemo(
|
|
486
|
+
() => (liveOverride ? ({ ...fence, ...liveOverride } as FenceNode) : fence),
|
|
487
|
+
[fence, liveOverride],
|
|
488
|
+
)
|
|
489
|
+
const [levelObject, setLevelObject] = useState<Object3D | null>(() =>
|
|
490
|
+
fence.parentId ? (sceneRegistry.nodes.get(fence.parentId) ?? null) : null,
|
|
491
|
+
)
|
|
492
|
+
|
|
493
|
+
useEffect(() => {
|
|
494
|
+
let frameId = 0
|
|
495
|
+
const resolveLevelObject = () => {
|
|
496
|
+
const next = fence.parentId ? (sceneRegistry.nodes.get(fence.parentId) ?? null) : null
|
|
497
|
+
setLevelObject(next)
|
|
498
|
+
if (!next) frameId = window.requestAnimationFrame(resolveLevelObject)
|
|
499
|
+
}
|
|
500
|
+
resolveLevelObject()
|
|
501
|
+
return () => {
|
|
502
|
+
if (frameId) window.cancelAnimationFrame(frameId)
|
|
503
|
+
}
|
|
504
|
+
}, [fence.parentId])
|
|
505
|
+
|
|
506
|
+
if (!levelObject) return null
|
|
507
|
+
|
|
508
|
+
const baseElevation = getFenceBaseElevationForNodes(effectiveFence, nodes)
|
|
509
|
+
return createPortal(
|
|
510
|
+
<group position={[0, baseElevation, 0]}>
|
|
511
|
+
<StructureThicknessHandle
|
|
512
|
+
baseElevation={baseElevation}
|
|
513
|
+
levelObject={levelObject}
|
|
514
|
+
node={effectiveFence}
|
|
515
|
+
side={1}
|
|
516
|
+
/>
|
|
517
|
+
<StructureThicknessHandle
|
|
518
|
+
baseElevation={baseElevation}
|
|
519
|
+
levelObject={levelObject}
|
|
520
|
+
node={effectiveFence}
|
|
521
|
+
side={-1}
|
|
522
|
+
/>
|
|
523
|
+
</group>,
|
|
524
|
+
levelObject,
|
|
525
|
+
)
|
|
526
|
+
}
|
|
527
|
+
|
|
236
528
|
function buildDashedVerticalGeometry(height: number) {
|
|
237
529
|
if (!(Number.isFinite(height) && height > 0)) return new BufferGeometry()
|
|
238
530
|
|
|
@@ -1,11 +1,17 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
|
+
import { useScene } from '@pascal-app/core'
|
|
3
4
|
import { useViewer } from '@pascal-app/viewer'
|
|
4
|
-
import { useEffect, useState } from 'react'
|
|
5
|
+
import { useEffect, useMemo, useState } from 'react'
|
|
5
6
|
import { createPortal } from 'react-dom'
|
|
6
7
|
import { isActive } from '../../lib/interaction/scope'
|
|
8
|
+
import { canCreateSessionGroup, selectionIntersectsSessionGroup } from '../../lib/session-groups'
|
|
7
9
|
import useEditor from '../../store/use-editor'
|
|
8
10
|
import useInteractionScope, { useMovingNode } from '../../store/use-interaction-scope'
|
|
11
|
+
import useSessionGroups, {
|
|
12
|
+
groupCurrentSelection,
|
|
13
|
+
ungroupCurrentSelection,
|
|
14
|
+
} from '../../store/use-session-groups'
|
|
9
15
|
import {
|
|
10
16
|
deleteSelection,
|
|
11
17
|
duplicateSelectionAndPickUp,
|
|
@@ -14,25 +20,27 @@ import {
|
|
|
14
20
|
import { NodeActionMenu } from '../editor/node-action-menu'
|
|
15
21
|
|
|
16
22
|
/**
|
|
17
|
-
* Floating
|
|
18
|
-
* floor plan — the group sibling of `FloorplanRegistryActionMenu` (which is
|
|
19
|
-
* sole-selection only). Anchored above the dashed group selection box; every
|
|
20
|
-
* action targets the whole selection: Move picks the group up (it rides the
|
|
21
|
-
* cursor until a click places it), Duplicate clones the selection and picks
|
|
22
|
-
* the clones up, Delete removes everything selected.
|
|
23
|
-
*
|
|
24
|
-
* Gated on floorplan hover so it never coexists with the 3D group menu in
|
|
25
|
-
* split view (that one hides while the floor plan is hovered), and hidden
|
|
26
|
-
* during any active interaction so it never competes with a live drag.
|
|
23
|
+
* Floating multi-select pill on the floor plan: Move, Group, Ungroup, Duplicate, Delete.
|
|
27
24
|
*/
|
|
28
25
|
export function FloorplanGroupActionMenu() {
|
|
29
|
-
const
|
|
26
|
+
const selectedIds = useViewer((s) => s.selection.selectedIds)
|
|
27
|
+
const isMultiSelect = selectedIds.length > 1
|
|
30
28
|
const movingNode = useMovingNode()
|
|
31
29
|
const isFloorplanHovered = useEditor((s) => s.isFloorplanHovered)
|
|
32
30
|
const scopeActive = useInteractionScope((s) => isActive(s.scope))
|
|
31
|
+
const sessionGroups = useSessionGroups((s) => s.groups)
|
|
32
|
+
const sceneNodes = useScene((s) => s.nodes)
|
|
33
|
+
const liveIds = useMemo(() => new Set(Object.keys(sceneNodes)), [sceneNodes])
|
|
34
|
+
const showGroup = useMemo(
|
|
35
|
+
() => canCreateSessionGroup(sessionGroups, selectedIds, liveIds),
|
|
36
|
+
[sessionGroups, selectedIds, liveIds],
|
|
37
|
+
)
|
|
38
|
+
const showUngroup = useMemo(
|
|
39
|
+
() => selectionIntersectsSessionGroup(sessionGroups, selectedIds, liveIds),
|
|
40
|
+
[sessionGroups, selectedIds, liveIds],
|
|
41
|
+
)
|
|
33
42
|
|
|
34
43
|
const [position, setPosition] = useState<{ left: number; top: number } | null>(null)
|
|
35
|
-
|
|
36
44
|
const isVisible = isMultiSelect && !movingNode && isFloorplanHovered && !scopeActive
|
|
37
45
|
|
|
38
46
|
useEffect(() => {
|
|
@@ -43,9 +51,6 @@ export function FloorplanGroupActionMenu() {
|
|
|
43
51
|
let raf = 0
|
|
44
52
|
const tick = () => {
|
|
45
53
|
raf = requestAnimationFrame(tick)
|
|
46
|
-
// The dashed group box exists exactly while the multi-selection has
|
|
47
|
-
// transformable participants — anchor to its top edge. Only publish
|
|
48
|
-
// actual changes so the idle poll doesn't re-render every frame.
|
|
49
54
|
const box = document.querySelector('[data-group-selection-box]') as SVGGElement | null
|
|
50
55
|
if (!box) {
|
|
51
56
|
setPosition((prev) => (prev === null ? prev : null))
|
|
@@ -77,9 +82,11 @@ export function FloorplanGroupActionMenu() {
|
|
|
77
82
|
<NodeActionMenu
|
|
78
83
|
onDelete={() => deleteSelection()}
|
|
79
84
|
onDuplicate={() => duplicateSelectionAndPickUp()}
|
|
85
|
+
onGroup={showGroup ? () => groupCurrentSelection() : undefined}
|
|
80
86
|
onMove={() => startGroupPickUp()}
|
|
81
87
|
onPointerDown={(event) => event.stopPropagation()}
|
|
82
88
|
onPointerUp={(event) => event.stopPropagation()}
|
|
89
|
+
onUngroup={showUngroup ? () => ungroupCurrentSelection() : undefined}
|
|
83
90
|
/>
|
|
84
91
|
</div>,
|
|
85
92
|
document.body,
|