@pascal-app/editor 0.9.1 → 0.9.2
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 +12 -9
- package/src/components/editor/alignment-3d-guide-layer.tsx +10 -2
- package/src/components/editor/bake-exporter.tsx +47 -0
- package/src/components/editor/custom-camera-controls.tsx +63 -25
- package/src/components/editor/editor-layout-mobile.tsx +5 -0
- package/src/components/editor/editor-layout-v2.tsx +18 -0
- package/src/components/editor/export-manager.tsx +71 -63
- package/src/components/editor/fence-tangent-lines-3d.tsx +87 -0
- package/src/components/editor/first-person/build-collider-world.test.ts +36 -3
- package/src/components/editor/first-person/build-collider-world.ts +32 -5
- package/src/components/editor/first-person-controls.tsx +118 -7
- package/src/components/editor/floating-action-menu.tsx +410 -33
- package/src/components/editor/floating-building-action-menu.tsx +1 -1
- package/src/components/editor/floorplan-background-selection.test.ts +58 -0
- package/src/components/editor/floorplan-background-selection.ts +3 -2
- package/src/components/editor/floorplan-panel.tsx +1804 -849
- package/src/components/editor/grid.tsx +166 -39
- package/src/components/editor/group-actions.ts +457 -0
- package/src/components/editor/group-floating-action-menu.tsx +130 -0
- package/src/components/editor/group-move-3d.ts +397 -0
- package/src/components/editor/group-rotate-handle.tsx +102 -64
- package/src/components/editor/group-selection-box-3d.tsx +173 -0
- package/src/components/editor/group-transform-shared.test.ts +192 -1
- package/src/components/editor/group-transform-shared.ts +240 -7
- package/src/components/editor/handles/handle-arrow.tsx +81 -23
- package/src/components/editor/handles/use-handle-drag.ts +37 -4
- package/src/components/editor/index.tsx +248 -27
- package/src/components/editor/measurement-pill.tsx +62 -22
- package/src/components/editor/node-action-menu.tsx +14 -1
- package/src/components/editor/node-arrow-handles.tsx +366 -173
- package/src/components/editor/opening-guides-3d-layer.tsx +144 -0
- package/src/components/editor/quick-measurement-card.tsx +70 -0
- package/src/components/editor/quick-measurement-hud.tsx +28 -0
- package/src/components/editor/riser-diagram-panel.tsx +137 -0
- package/src/components/editor/selection-manager.tsx +812 -466
- package/src/components/editor/site-edge-labels.tsx +4 -4
- package/src/components/editor/slab-hole-highlights.tsx +13 -5
- package/src/components/editor/snapshot-capture-overlay.tsx +255 -115
- package/src/components/editor/three-context-bridge.ts +22 -0
- package/src/components/editor/thumbnail-generator.tsx +136 -43
- package/src/components/editor/use-floorplan-background-placement.ts +113 -45
- package/src/components/editor/use-floorplan-scene-data.ts +5 -5
- package/src/components/editor/use-mesh-settle-epoch.ts +26 -0
- package/src/components/editor/wall-measurement-label.tsx +3 -13
- package/src/components/editor/wall-move-side-handles.tsx +36 -21
- package/src/components/editor/wall-snap-beacon-layer.tsx +160 -6
- package/src/components/editor-2d/floorplan-action-menu-layer.tsx +9 -5
- package/src/components/editor-2d/floorplan-cursor-indicator-overlay.tsx +1 -1
- package/src/components/editor-2d/floorplan-group-action-menu.tsx +87 -0
- package/src/components/editor-2d/floorplan-group-move.tsx +701 -0
- package/src/components/editor-2d/floorplan-measurement-tool-layer.test.ts +113 -0
- package/src/components/editor-2d/floorplan-measurement-tool-layer.tsx +1656 -0
- package/src/components/editor-2d/floorplan-quick-measure-layer.tsx +212 -0
- package/src/components/editor-2d/floorplan-registry-action-menu.tsx +169 -4
- package/src/components/editor-2d/floorplan-registry-move-overlay.tsx +300 -59
- package/src/components/editor-2d/floorplan-snap-beacon-layer.tsx +5 -3
- package/src/components/editor-2d/renderers/floorplan-label-angle.test.ts +15 -0
- package/src/components/editor-2d/renderers/floorplan-label-angle.ts +14 -0
- package/src/components/editor-2d/renderers/floorplan-placement-preview-layer.tsx +4 -1
- package/src/components/editor-2d/renderers/floorplan-registry-layer.test.ts +308 -0
- package/src/components/editor-2d/renderers/floorplan-registry-layer.tsx +1704 -342
- package/src/components/editor-2d/renderers/floorplan-voronoi-layer.tsx +128 -0
- package/src/components/systems/ceiling/ceiling-selection-affordance-system.tsx +419 -60
- package/src/components/systems/ceiling/ceiling-system.tsx +97 -4
- package/src/components/systems/roof/roof-edit-system.tsx +1768 -13
- package/src/components/systems/selection-affordance-manager.tsx +38 -0
- package/src/components/systems/zone/zone-label-editor-system.tsx +14 -1
- package/src/components/systems/zone/zone-system.tsx +42 -13
- package/src/components/tools/elevator/elevator-tool.tsx +34 -8
- package/src/components/tools/elevator/move-elevator-tool.tsx +13 -2
- package/src/components/tools/fence/fence-drafting.ts +80 -8
- package/src/components/tools/item/move-tool.tsx +17 -13
- package/src/components/tools/item/placement-math.test.ts +13 -1
- package/src/components/tools/item/placement-math.ts +28 -2
- package/src/components/tools/item/placement-strategies.ts +246 -5
- package/src/components/tools/item/placement-types.ts +13 -1
- package/src/components/tools/item/use-draft-node.ts +31 -1
- package/src/components/tools/item/use-placement-coordinator.tsx +761 -121
- package/src/components/tools/registry/move-registry-node-tool.tsx +561 -99
- package/src/components/tools/roof/roof-tool.tsx +320 -94
- package/src/components/tools/select/box-select-state.ts +18 -7
- package/src/components/tools/select/box-select-tool.tsx +176 -40
- package/src/components/tools/select/marquee-geometry.test.ts +161 -0
- package/src/components/tools/select/marquee-geometry.ts +155 -0
- package/src/components/tools/select/plane-box-select-tool.tsx +1 -8
- package/src/components/tools/select/select-candidates.ts +1 -1
- package/src/components/tools/shared/cursor-sphere.tsx +62 -26
- package/src/components/tools/shared/drag-bounding-box.tsx +28 -4
- package/src/components/tools/shared/facing-indicator.tsx +87 -0
- package/src/components/tools/shared/facing-pose-indicator.tsx +54 -0
- package/src/components/tools/shared/placement-box.tsx +183 -1
- package/src/components/tools/shared/polygon-editor.tsx +276 -35
- package/src/components/tools/site/site-boundary-editor.tsx +88 -36
- package/src/components/tools/stair/stair-defaults.ts +1 -0
- package/src/components/tools/stair/stair-tool.tsx +82 -11
- package/src/components/tools/tool-manager.tsx +86 -25
- package/src/components/tools/wall/wall-drafting.test.ts +330 -0
- package/src/components/tools/wall/wall-drafting.ts +180 -103
- package/src/components/tools/wall/wall-snap-geometry.test.ts +44 -0
- package/src/components/tools/wall/wall-snap-geometry.ts +67 -6
- package/src/components/tools/zone/zone-boundary-editor.tsx +5 -1
- package/src/components/tools/zone/zone-tool.tsx +82 -88
- package/src/components/ui/action-menu/camera-actions.tsx +24 -17
- package/src/components/ui/action-menu/control-modes.tsx +40 -93
- package/src/components/ui/action-menu/furnish-tools.tsx +5 -5
- package/src/components/ui/action-menu/index.tsx +2 -4
- package/src/components/ui/action-menu/measurement-control.tsx +188 -0
- package/src/components/ui/action-menu/structure-tools.tsx +22 -13
- package/src/components/ui/action-menu/view-toggles.tsx +42 -90
- package/src/components/ui/command-palette/editor-commands.tsx +2 -2
- package/src/components/ui/command-palette/index.tsx +4 -3
- package/src/components/ui/controls/material-paint-panel.tsx +86 -17
- package/src/components/ui/controls/material-picker.tsx +83 -152
- package/src/components/ui/controls/material-properties-editor.tsx +108 -0
- package/src/components/ui/controls/metric-control.tsx +118 -44
- package/src/components/ui/controls/scene-material-list.tsx +247 -0
- package/src/components/ui/controls/slider-control.tsx +88 -38
- package/src/components/ui/floating-level-selector.tsx +1 -1
- package/src/components/ui/helpers/building-helper.tsx +10 -23
- package/src/components/ui/helpers/contextual-helper-panel.tsx +428 -0
- package/src/components/ui/helpers/helper-manager.tsx +227 -14
- package/src/components/ui/helpers/item-helper.tsx +28 -32
- package/src/components/ui/helpers/registered-tool-helper.tsx +45 -11
- package/src/components/ui/helpers/roof-helper.tsx +10 -12
- package/src/components/ui/icon-ref.tsx +47 -0
- package/src/components/ui/item-catalog/catalog-items.tsx +39 -1
- package/src/components/ui/item-catalog/item-catalog.tsx +13 -36
- package/src/components/ui/level-duplicate-dialog.tsx +1 -1
- package/src/components/ui/panels/node-display.ts +17 -17
- package/src/components/ui/panels/panel-manager.tsx +22 -13
- package/src/components/ui/panels/panel-wrapper.tsx +24 -3
- package/src/components/ui/panels/parametric-inspector.tsx +15 -2
- package/src/components/ui/panels/reference-panel.tsx +54 -19
- package/src/components/ui/primitives/shortcut-token.tsx +39 -3
- package/src/components/ui/primitives/sidebar.tsx +1 -0
- package/src/components/ui/sidebar/app-sidebar.tsx +5 -1
- package/src/components/ui/sidebar/icon-rail.tsx +50 -31
- package/src/components/ui/sidebar/panels/plugins-panel.tsx +218 -0
- package/src/components/ui/sidebar/panels/settings-panel/index.tsx +71 -40
- package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +60 -6
- package/src/components/ui/sidebar/panels/settings-panel/load-build-dialog.tsx +12 -4
- package/src/components/ui/sidebar/panels/site-panel/ceiling-tree-node.tsx +4 -3
- package/src/components/ui/sidebar/panels/site-panel/chimney-tree-node.tsx +10 -7
- package/src/components/ui/sidebar/panels/site-panel/column-tree-node.tsx +1 -1
- package/src/components/ui/sidebar/panels/site-panel/door-tree-node.tsx +7 -2
- package/src/components/ui/sidebar/panels/site-panel/dormer-tree-node.tsx +10 -7
- package/src/components/ui/sidebar/panels/site-panel/elevator-tree-node.tsx +1 -1
- package/src/components/ui/sidebar/panels/site-panel/fence-tree-node.tsx +1 -1
- package/src/components/ui/sidebar/panels/site-panel/gutter-tree-node.tsx +10 -7
- package/src/components/ui/sidebar/panels/site-panel/index.tsx +40 -17
- package/src/components/ui/sidebar/panels/site-panel/item-tree-node.tsx +31 -17
- package/src/components/ui/sidebar/panels/site-panel/level-tree-node.tsx +1 -1
- package/src/components/ui/sidebar/panels/site-panel/registry-tree-node.tsx +72 -25
- package/src/components/ui/sidebar/panels/site-panel/roof-tree-node.tsx +7 -3
- package/src/components/ui/sidebar/panels/site-panel/shelf-tree-node.tsx +12 -8
- package/src/components/ui/sidebar/panels/site-panel/slab-tree-node.tsx +4 -3
- package/src/components/ui/sidebar/panels/site-panel/solar-panel-tree-node.tsx +10 -7
- package/src/components/ui/sidebar/panels/site-panel/spawn-tree-node.tsx +1 -1
- package/src/components/ui/sidebar/panels/site-panel/stair-tree-node.tsx +2 -2
- package/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +44 -3
- package/src/components/ui/sidebar/panels/site-panel/tree-structure.ts +36 -0
- package/src/components/ui/sidebar/panels/site-panel/wall-tree-node.tsx +1 -1
- package/src/components/ui/sidebar/panels/site-panel/window-tree-node.tsx +7 -2
- package/src/components/ui/sidebar/panels/site-panel/zone-tree-node.tsx +3 -3
- package/src/components/ui/sidebar/tab-bar.tsx +42 -28
- package/src/components/ui/sidebar/use-plugin-panels.tsx +132 -0
- package/src/components/ui/snap-target-badge.test.tsx +40 -0
- package/src/components/ui/snap-target-badge.tsx +88 -0
- package/src/components/viewer-overlay.tsx +85 -10
- package/src/components/viewer-zone-system.tsx +6 -1
- package/src/hooks/use-auto-save.test.ts +14 -0
- package/src/hooks/use-auto-save.ts +54 -7
- package/src/hooks/use-ceiling-events.ts +3 -2
- package/src/hooks/use-drag-action.ts +4 -1
- package/src/hooks/use-keyboard.ts +385 -40
- package/src/index.tsx +219 -6
- package/src/lib/active-placement-surface.ts +35 -0
- package/src/lib/ceiling-plan-snap.ts +24 -0
- package/src/lib/contextual-help.test.ts +112 -0
- package/src/lib/contextual-help.ts +144 -0
- package/src/lib/continuation.ts +64 -0
- package/src/lib/direct-manipulation.test.ts +192 -0
- package/src/lib/direct-manipulation.ts +109 -0
- package/src/lib/editor-api.ts +23 -35
- package/src/lib/floorplan/apply-alignment.test.ts +25 -0
- package/src/lib/floorplan/apply-alignment.ts +18 -12
- package/src/lib/floorplan/floorplan-export.tsx +364 -0
- package/src/lib/floorplan/geometry.ts +53 -0
- package/src/lib/floorplan/index.ts +3 -0
- package/src/lib/floorplan/items.ts +5 -2
- package/src/lib/floorplan/plan-coords.ts +21 -0
- package/src/lib/fresh-planar-placement.test.ts +240 -3
- package/src/lib/fresh-planar-placement.ts +68 -1
- package/src/lib/glb-export.test.ts +441 -0
- package/src/lib/glb-export.ts +874 -0
- package/src/lib/history.ts +9 -0
- package/src/lib/interaction/hot-set.test.ts +133 -0
- package/src/lib/interaction/hot-set.ts +67 -0
- package/src/lib/interaction/overlay-policy.test.ts +51 -0
- package/src/lib/interaction/overlay-policy.ts +59 -0
- package/src/lib/interaction/scope.ts +211 -0
- package/src/lib/level-selection.ts +2 -2
- package/src/lib/material-paint.ts +36 -49
- package/src/lib/measurement-kind.test.ts +30 -0
- package/src/lib/measurement-kind.ts +19 -0
- package/src/lib/measurement-label.test.ts +47 -0
- package/src/lib/measurement-label.ts +68 -0
- package/src/lib/measurement-parser.ts +116 -0
- package/src/lib/measurements.test.ts +183 -0
- package/src/lib/measurements.ts +199 -0
- package/src/lib/paint-scope.test.ts +454 -0
- package/src/lib/paint-scope.ts +461 -0
- package/src/lib/placement-drag-release.ts +23 -0
- package/src/lib/planar-cursor-placement.test.ts +57 -0
- package/src/lib/plugin-panels.test.ts +19 -0
- package/src/lib/plugin-panels.ts +91 -0
- package/src/lib/quick-measurement.test.ts +77 -0
- package/src/lib/quick-measurement.ts +109 -0
- package/src/lib/roof-duplication.ts +6 -6
- package/src/lib/roof-hover-outline-proxy.ts +22 -0
- package/src/lib/roof-wall-hit.ts +164 -0
- package/src/lib/scene-clipboard.test.ts +196 -0
- package/src/lib/scene-clipboard.ts +73 -8
- package/src/lib/scene.ts +28 -5
- package/src/lib/selection-routing.test.ts +298 -0
- package/src/lib/selection-routing.ts +175 -0
- package/src/lib/sfx/index.ts +1 -0
- package/src/lib/sfx/movement-tick.test.ts +65 -0
- package/src/lib/sfx/movement-tick.ts +18 -0
- package/src/lib/sfx-bus.ts +50 -13
- package/src/lib/sfx-player.test.ts +124 -0
- package/src/lib/sfx-player.ts +111 -66
- package/src/lib/slab-plan-snap.test.ts +93 -0
- package/src/lib/slab-plan-snap.ts +108 -0
- package/src/lib/snapping-mode.test.ts +138 -0
- package/src/lib/snapping-mode.ts +179 -0
- package/src/lib/stair-duplication.ts +4 -4
- package/src/lib/surface-plan-snap.test.ts +71 -0
- package/src/lib/surface-plan-snap.ts +256 -0
- package/src/lib/use-linear-display.ts +40 -0
- package/src/lib/world-grid-snap.ts +37 -5
- package/src/store/use-direct-manipulation-feedback.ts +20 -0
- package/src/store/use-editor.tsx +484 -114
- package/src/store/use-facing-pose.ts +44 -0
- package/src/store/use-fence-curve-draft.ts +21 -0
- package/src/store/use-floorplan-draft-preview.ts +101 -0
- package/src/store/use-floorplan-marquee.ts +50 -0
- package/src/store/use-interaction-scope.test.ts +186 -0
- package/src/store/use-interaction-scope.ts +132 -0
- package/src/store/use-measurement-draft.test.ts +530 -0
- package/src/store/use-measurement-draft.ts +543 -0
- package/src/store/use-opening-guides.ts +41 -0
- package/src/store/use-placement-preview.ts +11 -3
- package/src/store/use-quick-measurement-hud.test.ts +53 -0
- package/src/store/use-quick-measurement-hud.ts +77 -0
- package/src/store/use-segment-draft-chain.ts +28 -0
- package/src/store/use-stair-build-preview.ts +43 -0
- package/src/store/use-wall-snap-indicator.ts +2 -0
- package/src/components/editor/first-person/bvh-ecctrl.tsx +0 -860
- package/src/components/editor/group-move-handle.tsx +0 -316
- package/src/components/ui/panels/paint-panel.tsx +0 -163
- package/src/lib/level-name.ts +0 -11
|
@@ -1,51 +1,27 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
DEFAULT_ANGLE_STEP,
|
|
3
|
+
emitter,
|
|
4
|
+
type GridEvent,
|
|
5
|
+
type LevelNode,
|
|
6
|
+
snapPointAlongAngleRay,
|
|
7
|
+
useScene,
|
|
8
|
+
ZoneNode,
|
|
9
|
+
} from '@pascal-app/core'
|
|
2
10
|
import { useViewer } from '@pascal-app/viewer'
|
|
3
11
|
import { useEffect, useMemo, useRef, useState } from 'react'
|
|
4
12
|
import { BufferGeometry, DoubleSide, type Group, type Line, Shape, Vector3 } from 'three'
|
|
5
13
|
import { EDITOR_LAYER } from './../../../lib/constants'
|
|
6
14
|
import { sfxEmitter } from './../../../lib/sfx-bus'
|
|
15
|
+
import {
|
|
16
|
+
clearSurfacePlanSnapFeedback,
|
|
17
|
+
resolveSurfacePlanPointSnap,
|
|
18
|
+
} from './../../../lib/surface-plan-snap'
|
|
7
19
|
import { snapWorldXZForActiveBuilding } from './../../../lib/world-grid-snap'
|
|
8
|
-
import useEditor from './../../../store/use-editor'
|
|
20
|
+
import useEditor, { isAngleSnapActive, isGridSnapActive } from './../../../store/use-editor'
|
|
9
21
|
import { CursorSphere } from '../shared/cursor-sphere'
|
|
10
22
|
|
|
11
23
|
const Y_OFFSET = 0.02
|
|
12
24
|
|
|
13
|
-
/**
|
|
14
|
-
* Snaps a point to the nearest axis-aligned or 45-degree diagonal from the last point
|
|
15
|
-
*/
|
|
16
|
-
const calculateSnapPoint = (
|
|
17
|
-
lastPoint: [number, number],
|
|
18
|
-
currentPoint: [number, number],
|
|
19
|
-
): [number, number] => {
|
|
20
|
-
const [x1, y1] = lastPoint
|
|
21
|
-
const [x, y] = currentPoint
|
|
22
|
-
|
|
23
|
-
const dx = x - x1
|
|
24
|
-
const dy = y - y1
|
|
25
|
-
const absDx = Math.abs(dx)
|
|
26
|
-
const absDy = Math.abs(dy)
|
|
27
|
-
|
|
28
|
-
// Calculate distances to horizontal, vertical, and diagonal lines
|
|
29
|
-
const horizontalDist = absDy
|
|
30
|
-
const verticalDist = absDx
|
|
31
|
-
const diagonalDist = Math.abs(absDx - absDy)
|
|
32
|
-
|
|
33
|
-
// Find the minimum distance to determine which axis to snap to
|
|
34
|
-
const minDist = Math.min(horizontalDist, verticalDist, diagonalDist)
|
|
35
|
-
|
|
36
|
-
if (minDist === diagonalDist) {
|
|
37
|
-
// Snap to 45° diagonal
|
|
38
|
-
const diagonalLength = Math.min(absDx, absDy)
|
|
39
|
-
return [x1 + Math.sign(dx) * diagonalLength, y1 + Math.sign(dy) * diagonalLength]
|
|
40
|
-
}
|
|
41
|
-
if (minDist === horizontalDist) {
|
|
42
|
-
// Snap to horizontal
|
|
43
|
-
return [x, y1]
|
|
44
|
-
}
|
|
45
|
-
// Snap to vertical
|
|
46
|
-
return [x1, y]
|
|
47
|
-
}
|
|
48
|
-
|
|
49
25
|
/**
|
|
50
26
|
* Creates a zone with the given polygon points
|
|
51
27
|
*/
|
|
@@ -107,11 +83,35 @@ export const ZoneTool: React.FC = () => {
|
|
|
107
83
|
if (!currentLevelId) return
|
|
108
84
|
|
|
109
85
|
let cursorPosition: [number, number] = [0, 0]
|
|
86
|
+
let snappedCursorPosition: [number, number] | null = null
|
|
110
87
|
|
|
111
88
|
// Initialize line geometries
|
|
112
89
|
mainLineRef.current.geometry = new BufferGeometry()
|
|
113
90
|
closingLineRef.current.geometry = new BufferGeometry()
|
|
114
91
|
|
|
92
|
+
// Snapping follows the active mode: `angles` locks the ray to 15° from the
|
|
93
|
+
// last vertex (grid quantizes the distance along it), otherwise `grid`
|
|
94
|
+
// quantizes to the world-aligned grid; the shared surface snap then layers
|
|
95
|
+
// wall-corner/midpoint/crossing magnetics and alignment guides on top —
|
|
96
|
+
// the same pipeline slab/ceiling drafting uses. No held-Shift bypass —
|
|
97
|
+
// Shift cycles the mode (see interaction-scope.md).
|
|
98
|
+
const snapDraftPoint = (
|
|
99
|
+
lastPoint: [number, number] | undefined,
|
|
100
|
+
gridPoint: [number, number],
|
|
101
|
+
rawPoint: [number, number],
|
|
102
|
+
): [number, number] => {
|
|
103
|
+
const gridStep = isGridSnapActive() ? useEditor.getState().gridSnapStep : 0
|
|
104
|
+
const orthoPoint: [number, number] =
|
|
105
|
+
isAngleSnapActive() && lastPoint
|
|
106
|
+
? [...snapPointAlongAngleRay(lastPoint, rawPoint, DEFAULT_ANGLE_STEP, gridStep)]
|
|
107
|
+
: gridPoint
|
|
108
|
+
return resolveSurfacePlanPointSnap({
|
|
109
|
+
rawPoint,
|
|
110
|
+
fallbackPoint: orthoPoint,
|
|
111
|
+
levelId: currentLevelId,
|
|
112
|
+
}).point
|
|
113
|
+
}
|
|
114
|
+
|
|
115
115
|
const updateLines = () => {
|
|
116
116
|
const points = pointsRef.current
|
|
117
117
|
const y = levelYRef.current + Y_OFFSET
|
|
@@ -127,11 +127,9 @@ export const ZoneTool: React.FC = () => {
|
|
|
127
127
|
|
|
128
128
|
// Add cursor point
|
|
129
129
|
const lastPoint = points[points.length - 1]
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
linePoints.push(new Vector3(snapped[0], y, snapped[1]))
|
|
134
|
-
}
|
|
130
|
+
const snapped = snappedCursorPosition ?? cursorPosition
|
|
131
|
+
if (lastPoint && isValidPoint(snapped)) {
|
|
132
|
+
linePoints.push(new Vector3(snapped[0], y, snapped[1]))
|
|
135
133
|
}
|
|
136
134
|
|
|
137
135
|
// Update main line geometry
|
|
@@ -145,17 +143,14 @@ export const ZoneTool: React.FC = () => {
|
|
|
145
143
|
|
|
146
144
|
// Update closing line (from cursor back to first point)
|
|
147
145
|
const firstPoint = points[0]
|
|
148
|
-
if (points.length >= 2 && lastPoint && isValidPoint(firstPoint)) {
|
|
149
|
-
const
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
closingLineRef.current.geometry = new BufferGeometry().setFromPoints(closingPoints)
|
|
157
|
-
closingLineRef.current.visible = true
|
|
158
|
-
}
|
|
146
|
+
if (points.length >= 2 && lastPoint && isValidPoint(firstPoint) && isValidPoint(snapped)) {
|
|
147
|
+
const closingPoints = [
|
|
148
|
+
new Vector3(snapped[0], y, snapped[1]),
|
|
149
|
+
new Vector3(firstPoint[0], y, firstPoint[1]),
|
|
150
|
+
]
|
|
151
|
+
closingLineRef.current.geometry.dispose()
|
|
152
|
+
closingLineRef.current.geometry = new BufferGeometry().setFromPoints(closingPoints)
|
|
153
|
+
closingLineRef.current.visible = true
|
|
159
154
|
} else {
|
|
160
155
|
closingLineRef.current.visible = false
|
|
161
156
|
}
|
|
@@ -163,40 +158,45 @@ export const ZoneTool: React.FC = () => {
|
|
|
163
158
|
|
|
164
159
|
const updatePreview = () => {
|
|
165
160
|
const points = pointsRef.current
|
|
166
|
-
const
|
|
161
|
+
const cursorPt = snappedCursorPosition ?? cursorPosition
|
|
167
162
|
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
cursorPt
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
setPreview({ points: [...points], cursorPoint: cursorPt, levelY: levelYRef.current })
|
|
163
|
+
setPreview({
|
|
164
|
+
points: [...points],
|
|
165
|
+
cursorPoint: isValidPoint(cursorPt) ? cursorPt : null,
|
|
166
|
+
levelY: levelYRef.current,
|
|
167
|
+
})
|
|
176
168
|
updateLines()
|
|
177
169
|
}
|
|
178
170
|
|
|
171
|
+
// World-grid snap projected into building-local; rotated buildings
|
|
172
|
+
// used to pull the snap off the visible grid lines. Grid quantize only
|
|
173
|
+
// in grid mode; off / lines / angles leave the raw cursor.
|
|
174
|
+
const gridPointOf = (event: GridEvent): [number, number] =>
|
|
175
|
+
isGridSnapActive()
|
|
176
|
+
? snapWorldXZForActiveBuilding(
|
|
177
|
+
event.position[0],
|
|
178
|
+
event.position[2],
|
|
179
|
+
useEditor.getState().gridSnapStep,
|
|
180
|
+
).local
|
|
181
|
+
: [event.localPosition[0], event.localPosition[2]]
|
|
182
|
+
|
|
179
183
|
const onGridMove = (event: GridEvent) => {
|
|
180
184
|
if (!cursorRef.current) return
|
|
181
185
|
|
|
182
|
-
|
|
183
|
-
// used to pull the snap off the visible grid lines.
|
|
184
|
-
const [gridX, gridZ] = snapWorldXZForActiveBuilding(
|
|
185
|
-
event.position[0],
|
|
186
|
-
event.position[2],
|
|
187
|
-
0.5,
|
|
188
|
-
).local
|
|
189
|
-
cursorPosition = [gridX, gridZ]
|
|
186
|
+
cursorPosition = gridPointOf(event)
|
|
190
187
|
levelYRef.current = event.localPosition[1]
|
|
191
188
|
|
|
192
|
-
// If we have points, snap to axis from last point
|
|
193
189
|
const lastPoint = pointsRef.current[pointsRef.current.length - 1]
|
|
194
|
-
const displayPoint = lastPoint
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
190
|
+
const displayPoint = snapDraftPoint(lastPoint, cursorPosition, [
|
|
191
|
+
event.localPosition[0],
|
|
192
|
+
event.localPosition[2],
|
|
193
|
+
])
|
|
194
|
+
snappedCursorPosition = displayPoint
|
|
195
|
+
|
|
196
|
+
// Play snap sound when the snapped position changes during drawing — only
|
|
197
|
+
// when a quantizing mode is active (off / lines move continuously).
|
|
199
198
|
if (
|
|
199
|
+
(isGridSnapActive() || isAngleSnapActive()) &&
|
|
200
200
|
pointsRef.current.length > 0 &&
|
|
201
201
|
previousSnappedPointRef.current &&
|
|
202
202
|
(displayPoint[0] !== previousSnappedPointRef.current[0] ||
|
|
@@ -214,18 +214,11 @@ export const ZoneTool: React.FC = () => {
|
|
|
214
214
|
const onGridClick = (event: GridEvent) => {
|
|
215
215
|
if (!currentLevelId) return
|
|
216
216
|
|
|
217
|
-
const [gridX, gridZ] = snapWorldXZForActiveBuilding(
|
|
218
|
-
event.position[0],
|
|
219
|
-
event.position[2],
|
|
220
|
-
0.5,
|
|
221
|
-
).local
|
|
222
|
-
let clickPoint: [number, number] = [gridX, gridZ]
|
|
223
|
-
|
|
224
|
-
// Snap to axis from last point
|
|
225
217
|
const lastPoint = pointsRef.current[pointsRef.current.length - 1]
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
218
|
+
const clickPoint = snapDraftPoint(lastPoint, gridPointOf(event), [
|
|
219
|
+
event.localPosition[0],
|
|
220
|
+
event.localPosition[2],
|
|
221
|
+
])
|
|
229
222
|
|
|
230
223
|
// Check if clicking on the first point to close the shape
|
|
231
224
|
const firstPoint = pointsRef.current[0]
|
|
@@ -279,6 +272,7 @@ export const ZoneTool: React.FC = () => {
|
|
|
279
272
|
|
|
280
273
|
// Reset state on unmount
|
|
281
274
|
pointsRef.current = []
|
|
275
|
+
clearSurfacePlanSnapFeedback()
|
|
282
276
|
}
|
|
283
277
|
}, [currentLevelId])
|
|
284
278
|
|
|
@@ -2,9 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
import { emitter } from '@pascal-app/core'
|
|
4
4
|
import Image from 'next/image'
|
|
5
|
+
import useEditor from '../../../store/use-editor'
|
|
5
6
|
import { ActionButton } from './action-button'
|
|
6
7
|
|
|
7
8
|
export function CameraActions({ hideOrbit = false }: { hideOrbit?: boolean }) {
|
|
9
|
+
// Orbit stays useful in 2D-only (it spins the synced floorplan view), but
|
|
10
|
+
// top view only tilts the hidden 3D camera — pointless without the canvas.
|
|
11
|
+
const is2dOnly = useEditor((s) => s.viewMode === '2d')
|
|
12
|
+
|
|
8
13
|
const goToTopView = () => {
|
|
9
14
|
emitter.emit('camera-controls:top-view')
|
|
10
15
|
}
|
|
@@ -33,7 +38,7 @@ export function CameraActions({ hideOrbit = false }: { hideOrbit?: boolean }) {
|
|
|
33
38
|
alt="Orbit Left"
|
|
34
39
|
className="h-[28px] w-[28px] -scale-x-100 object-contain opacity-70 transition-opacity group-hover:opacity-100"
|
|
35
40
|
height={28}
|
|
36
|
-
src="/icons/rotate.
|
|
41
|
+
src="/icons/rotate.webp"
|
|
37
42
|
width={28}
|
|
38
43
|
/>
|
|
39
44
|
</ActionButton>
|
|
@@ -50,7 +55,7 @@ export function CameraActions({ hideOrbit = false }: { hideOrbit?: boolean }) {
|
|
|
50
55
|
alt="Orbit Right"
|
|
51
56
|
className="h-[28px] w-[28px] object-contain opacity-70 transition-opacity group-hover:opacity-100"
|
|
52
57
|
height={28}
|
|
53
|
-
src="/icons/rotate.
|
|
58
|
+
src="/icons/rotate.webp"
|
|
54
59
|
width={28}
|
|
55
60
|
/>
|
|
56
61
|
</ActionButton>
|
|
@@ -58,21 +63,23 @@ export function CameraActions({ hideOrbit = false }: { hideOrbit?: boolean }) {
|
|
|
58
63
|
)}
|
|
59
64
|
|
|
60
65
|
{/* Top View */}
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
66
|
+
{!is2dOnly && (
|
|
67
|
+
<ActionButton
|
|
68
|
+
className="group hover:bg-white/5"
|
|
69
|
+
label="Top View"
|
|
70
|
+
onClick={goToTopView}
|
|
71
|
+
size="icon"
|
|
72
|
+
variant="ghost"
|
|
73
|
+
>
|
|
74
|
+
<Image
|
|
75
|
+
alt="Top View"
|
|
76
|
+
className="h-[28px] w-[28px] object-contain opacity-70 transition-opacity group-hover:opacity-100"
|
|
77
|
+
height={28}
|
|
78
|
+
src="/icons/topview.webp"
|
|
79
|
+
width={28}
|
|
80
|
+
/>
|
|
81
|
+
</ActionButton>
|
|
82
|
+
)}
|
|
76
83
|
</div>
|
|
77
84
|
)
|
|
78
85
|
}
|
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
3
|
import { Icon } from '@iconify/react'
|
|
4
|
-
import { type LevelNode, useScene } from '@pascal-app/core'
|
|
5
|
-
import { useViewer } from '@pascal-app/viewer'
|
|
6
4
|
import { type LucideIcon, Trash2 } from 'lucide-react'
|
|
7
5
|
import Image from 'next/image'
|
|
6
|
+
import { Fragment } from 'react'
|
|
8
7
|
import { cn } from './../../../lib/utils'
|
|
9
|
-
import useEditor
|
|
8
|
+
import useEditor from './../../../store/use-editor'
|
|
10
9
|
import { ActionButton } from './action-button'
|
|
10
|
+
import { MeasurementControl } from './measurement-control'
|
|
11
11
|
|
|
12
|
-
type ControlId = 'select' | 'box-select' | '
|
|
12
|
+
type ControlId = 'select' | 'box-select' | 'zone' | 'delete'
|
|
13
13
|
|
|
14
14
|
type ControlConfig = {
|
|
15
15
|
id: ControlId
|
|
@@ -26,22 +26,15 @@ type ControlConfig = {
|
|
|
26
26
|
const controls: ControlConfig[] = [
|
|
27
27
|
{
|
|
28
28
|
id: 'select',
|
|
29
|
-
imageSrc: '/icons/select.
|
|
29
|
+
imageSrc: '/icons/select.webp',
|
|
30
30
|
label: 'Select',
|
|
31
31
|
shortcut: 'V',
|
|
32
32
|
color: 'hover:bg-blue-500/20 hover:text-blue-400',
|
|
33
33
|
activeColor: 'bg-blue-500/20 text-blue-400',
|
|
34
34
|
},
|
|
35
|
-
{
|
|
36
|
-
id: 'site-edit',
|
|
37
|
-
imageSrc: '/icons/site-flag.png',
|
|
38
|
-
label: 'Edit site',
|
|
39
|
-
color: 'hover:bg-white/5',
|
|
40
|
-
activeColor: 'bg-white/10 hover:bg-white/10',
|
|
41
|
-
},
|
|
42
35
|
{
|
|
43
36
|
id: 'zone',
|
|
44
|
-
imageSrc: '/icons/zone.
|
|
37
|
+
imageSrc: '/icons/zone.webp',
|
|
45
38
|
label: 'Zone',
|
|
46
39
|
shortcut: 'Z',
|
|
47
40
|
color: 'hover:bg-green-500/20 hover:text-green-400',
|
|
@@ -65,47 +58,20 @@ export function ControlModes() {
|
|
|
65
58
|
const setPhase = useEditor((state) => state.setPhase)
|
|
66
59
|
const setStructureLayer = useEditor((state) => state.setStructureLayer)
|
|
67
60
|
const setSelectionTool = useEditor((state) => state.setFloorplanSelectionTool)
|
|
68
|
-
const levelId = useViewer((s) => s.selection.levelId)
|
|
69
|
-
|
|
70
|
-
// Only subscribe to the primitive `level` number — when walls are added to
|
|
71
|
-
// this level the object ref changes but this number doesn't, so Object.is
|
|
72
|
-
// dedupes and we avoid a re-render.
|
|
73
|
-
const levelIndex = useScene((state) => {
|
|
74
|
-
if (!levelId) return null
|
|
75
|
-
const node = state.nodes[levelId]
|
|
76
|
-
return node?.type === 'level' ? (node as LevelNode).level : null
|
|
77
|
-
})
|
|
78
61
|
|
|
79
62
|
const isSiteEditing = phase === 'site'
|
|
80
|
-
const isGroundFloor = levelIndex === 0
|
|
81
|
-
const canEnterSiteEdit = isGroundFloor || isSiteEditing
|
|
82
63
|
|
|
83
64
|
const structureLayer = useEditor((state) => state.structureLayer)
|
|
84
65
|
|
|
85
66
|
const getIsActive = (id: ControlId): boolean => {
|
|
86
|
-
if (isSiteEditing) return id === 'site-edit'
|
|
87
67
|
if (id === 'select') return mode === 'select' && selectionTool === 'click'
|
|
88
68
|
if (id === 'box-select') return mode === 'select' && selectionTool === 'marquee'
|
|
89
|
-
if (id === 'site-edit') return false
|
|
90
69
|
if (id === 'zone')
|
|
91
70
|
return mode === 'build' && phase === 'structure' && structureLayer === 'zones'
|
|
92
71
|
return mode === id
|
|
93
72
|
}
|
|
94
73
|
|
|
95
74
|
const handleClick = (id: ControlId) => {
|
|
96
|
-
if (id === 'site-edit') {
|
|
97
|
-
if (isSiteEditing) {
|
|
98
|
-
// Toggle off → back to structure/select
|
|
99
|
-
setPhase('structure')
|
|
100
|
-
setMode('select')
|
|
101
|
-
setStructureLayer('elements')
|
|
102
|
-
} else if (isGroundFloor) {
|
|
103
|
-
useEditor.setState({ phase: 'site', mode: 'select', tool: null, catalogCategory: null })
|
|
104
|
-
selectSiteFloorplanContext()
|
|
105
|
-
}
|
|
106
|
-
return
|
|
107
|
-
}
|
|
108
|
-
|
|
109
75
|
// Exit site editing first if needed
|
|
110
76
|
if (isSiteEditing) {
|
|
111
77
|
setPhase('structure')
|
|
@@ -136,64 +102,45 @@ export function ControlModes() {
|
|
|
136
102
|
{controls.map((c) => {
|
|
137
103
|
const ModeIcon = c.icon
|
|
138
104
|
const isImageMode = Boolean(c.imageSrc)
|
|
139
|
-
const isSiteButton = c.id === 'site-edit'
|
|
140
105
|
const isActive = getIsActive(c.id)
|
|
141
|
-
const isDisabled = isSiteButton && !canEnterSiteEdit
|
|
142
106
|
|
|
143
107
|
return (
|
|
144
|
-
<
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
: 'Site editing (ground level only)'
|
|
167
|
-
: c.label
|
|
168
|
-
}
|
|
169
|
-
onClick={() => handleClick(c.id)}
|
|
170
|
-
shortcut={c.shortcut}
|
|
171
|
-
size="icon"
|
|
172
|
-
variant="ghost"
|
|
173
|
-
>
|
|
174
|
-
{c.imageSrc ? (
|
|
175
|
-
<Image
|
|
176
|
-
alt={c.label}
|
|
177
|
-
className={cn(
|
|
178
|
-
'h-[28px] w-[28px] object-contain transition-[opacity,filter] duration-200',
|
|
179
|
-
isSiteButton
|
|
180
|
-
? isActive
|
|
181
|
-
? 'opacity-100 grayscale-0'
|
|
182
|
-
: ''
|
|
183
|
-
: isActive
|
|
108
|
+
<Fragment key={c.id}>
|
|
109
|
+
{c.id === 'delete' ? <MeasurementControl /> : null}
|
|
110
|
+
<ActionButton
|
|
111
|
+
className={cn(
|
|
112
|
+
'group text-muted-foreground',
|
|
113
|
+
!(isImageMode || isActive) && c.color,
|
|
114
|
+
!isImageMode && isActive && c.activeColor,
|
|
115
|
+
isImageMode && isActive && 'bg-white/10 hover:bg-white/10',
|
|
116
|
+
isImageMode && !isActive && 'hover:bg-white/5',
|
|
117
|
+
)}
|
|
118
|
+
label={c.label}
|
|
119
|
+
onClick={() => handleClick(c.id)}
|
|
120
|
+
shortcut={c.shortcut}
|
|
121
|
+
size="icon"
|
|
122
|
+
variant="ghost"
|
|
123
|
+
>
|
|
124
|
+
{c.imageSrc ? (
|
|
125
|
+
<Image
|
|
126
|
+
alt={c.label}
|
|
127
|
+
className={cn(
|
|
128
|
+
'h-[28px] w-[28px] object-contain transition-[opacity,filter] duration-200',
|
|
129
|
+
isActive
|
|
184
130
|
? 'opacity-100 grayscale-0'
|
|
185
131
|
: 'opacity-60 grayscale group-hover:opacity-100 group-hover:grayscale-0',
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
132
|
+
)}
|
|
133
|
+
height={28}
|
|
134
|
+
src={c.imageSrc}
|
|
135
|
+
width={28}
|
|
136
|
+
/>
|
|
137
|
+
) : c.iconifyIcon ? (
|
|
138
|
+
<Icon color="currentColor" height={18} icon={c.iconifyIcon} width={18} />
|
|
139
|
+
) : (
|
|
140
|
+
ModeIcon && <ModeIcon className="h-5 w-5" />
|
|
141
|
+
)}
|
|
142
|
+
</ActionButton>
|
|
143
|
+
</Fragment>
|
|
197
144
|
)
|
|
198
145
|
})}
|
|
199
146
|
</div>
|
|
@@ -8,9 +8,9 @@ export type FurnishToolConfig = {
|
|
|
8
8
|
}
|
|
9
9
|
|
|
10
10
|
export const furnishTools: FurnishToolConfig[] = [
|
|
11
|
-
{ id: 'item', iconSrc: '/icons/couch.
|
|
12
|
-
{ id: 'item', iconSrc: '/icons/appliance.
|
|
13
|
-
{ id: 'item', iconSrc: '/icons/kitchen.
|
|
14
|
-
{ id: 'item', iconSrc: '/icons/bathroom.
|
|
15
|
-
{ id: 'item', iconSrc: '/icons/tree.
|
|
11
|
+
{ id: 'item', iconSrc: '/icons/couch.webp', label: 'Furniture', catalogCategory: 'furniture' },
|
|
12
|
+
{ id: 'item', iconSrc: '/icons/appliance.webp', label: 'Appliance', catalogCategory: 'appliance' },
|
|
13
|
+
{ id: 'item', iconSrc: '/icons/kitchen.webp', label: 'Kitchen', catalogCategory: 'kitchen' },
|
|
14
|
+
{ id: 'item', iconSrc: '/icons/bathroom.webp', label: 'Bathroom', catalogCategory: 'bathroom' },
|
|
15
|
+
{ id: 'item', iconSrc: '/icons/tree.webp', label: 'Outdoor', catalogCategory: 'outdoor' },
|
|
16
16
|
]
|
|
@@ -9,7 +9,7 @@ import { cn } from './../../../lib/utils'
|
|
|
9
9
|
import useEditor from './../../../store/use-editor'
|
|
10
10
|
import { CameraActions } from './camera-actions'
|
|
11
11
|
import { ControlModes } from './control-modes'
|
|
12
|
-
import {
|
|
12
|
+
import { SecondaryToggles } from './view-toggles'
|
|
13
13
|
|
|
14
14
|
// Mobile bottom offset matches the viewer's overlap behind the sheet's
|
|
15
15
|
// rounded corners (SHEET_OVERLAP_PX in editor-layout-mobile) so the menu sits
|
|
@@ -57,9 +57,8 @@ export function ActionMenu({ className }: { className?: string }) {
|
|
|
57
57
|
<div className="flex items-center justify-center gap-1">
|
|
58
58
|
<ControlModes />
|
|
59
59
|
</div>
|
|
60
|
-
{/* Row 2:
|
|
60
|
+
{/* Row 2: secondary toggles (orbit + top view hidden) */}
|
|
61
61
|
<div className="flex items-center justify-center gap-1 border-border/50 border-t pt-1">
|
|
62
|
-
<GridSnapControl />
|
|
63
62
|
<SecondaryToggles />
|
|
64
63
|
</div>
|
|
65
64
|
</div>
|
|
@@ -67,7 +66,6 @@ export function ActionMenu({ className }: { className?: string }) {
|
|
|
67
66
|
<div className="flex items-center justify-center gap-1 px-2 py-1.5">
|
|
68
67
|
<ControlModes />
|
|
69
68
|
<div className="mx-1 h-5 w-px bg-border" />
|
|
70
|
-
<GridSnapControl />
|
|
71
69
|
<SecondaryToggles />
|
|
72
70
|
<div className="mx-1 h-5 w-px bg-border" />
|
|
73
71
|
<CameraActions />
|