@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.
Files changed (262) hide show
  1. package/package.json +12 -9
  2. package/src/components/editor/alignment-3d-guide-layer.tsx +10 -2
  3. package/src/components/editor/bake-exporter.tsx +47 -0
  4. package/src/components/editor/custom-camera-controls.tsx +63 -25
  5. package/src/components/editor/editor-layout-mobile.tsx +5 -0
  6. package/src/components/editor/editor-layout-v2.tsx +18 -0
  7. package/src/components/editor/export-manager.tsx +71 -63
  8. package/src/components/editor/fence-tangent-lines-3d.tsx +87 -0
  9. package/src/components/editor/first-person/build-collider-world.test.ts +36 -3
  10. package/src/components/editor/first-person/build-collider-world.ts +32 -5
  11. package/src/components/editor/first-person-controls.tsx +118 -7
  12. package/src/components/editor/floating-action-menu.tsx +410 -33
  13. package/src/components/editor/floating-building-action-menu.tsx +1 -1
  14. package/src/components/editor/floorplan-background-selection.test.ts +58 -0
  15. package/src/components/editor/floorplan-background-selection.ts +3 -2
  16. package/src/components/editor/floorplan-panel.tsx +1804 -849
  17. package/src/components/editor/grid.tsx +166 -39
  18. package/src/components/editor/group-actions.ts +457 -0
  19. package/src/components/editor/group-floating-action-menu.tsx +130 -0
  20. package/src/components/editor/group-move-3d.ts +397 -0
  21. package/src/components/editor/group-rotate-handle.tsx +102 -64
  22. package/src/components/editor/group-selection-box-3d.tsx +173 -0
  23. package/src/components/editor/group-transform-shared.test.ts +192 -1
  24. package/src/components/editor/group-transform-shared.ts +240 -7
  25. package/src/components/editor/handles/handle-arrow.tsx +81 -23
  26. package/src/components/editor/handles/use-handle-drag.ts +37 -4
  27. package/src/components/editor/index.tsx +248 -27
  28. package/src/components/editor/measurement-pill.tsx +62 -22
  29. package/src/components/editor/node-action-menu.tsx +14 -1
  30. package/src/components/editor/node-arrow-handles.tsx +366 -173
  31. package/src/components/editor/opening-guides-3d-layer.tsx +144 -0
  32. package/src/components/editor/quick-measurement-card.tsx +70 -0
  33. package/src/components/editor/quick-measurement-hud.tsx +28 -0
  34. package/src/components/editor/riser-diagram-panel.tsx +137 -0
  35. package/src/components/editor/selection-manager.tsx +812 -466
  36. package/src/components/editor/site-edge-labels.tsx +4 -4
  37. package/src/components/editor/slab-hole-highlights.tsx +13 -5
  38. package/src/components/editor/snapshot-capture-overlay.tsx +255 -115
  39. package/src/components/editor/three-context-bridge.ts +22 -0
  40. package/src/components/editor/thumbnail-generator.tsx +136 -43
  41. package/src/components/editor/use-floorplan-background-placement.ts +113 -45
  42. package/src/components/editor/use-floorplan-scene-data.ts +5 -5
  43. package/src/components/editor/use-mesh-settle-epoch.ts +26 -0
  44. package/src/components/editor/wall-measurement-label.tsx +3 -13
  45. package/src/components/editor/wall-move-side-handles.tsx +36 -21
  46. package/src/components/editor/wall-snap-beacon-layer.tsx +160 -6
  47. package/src/components/editor-2d/floorplan-action-menu-layer.tsx +9 -5
  48. package/src/components/editor-2d/floorplan-cursor-indicator-overlay.tsx +1 -1
  49. package/src/components/editor-2d/floorplan-group-action-menu.tsx +87 -0
  50. package/src/components/editor-2d/floorplan-group-move.tsx +701 -0
  51. package/src/components/editor-2d/floorplan-measurement-tool-layer.test.ts +113 -0
  52. package/src/components/editor-2d/floorplan-measurement-tool-layer.tsx +1656 -0
  53. package/src/components/editor-2d/floorplan-quick-measure-layer.tsx +212 -0
  54. package/src/components/editor-2d/floorplan-registry-action-menu.tsx +169 -4
  55. package/src/components/editor-2d/floorplan-registry-move-overlay.tsx +300 -59
  56. package/src/components/editor-2d/floorplan-snap-beacon-layer.tsx +5 -3
  57. package/src/components/editor-2d/renderers/floorplan-label-angle.test.ts +15 -0
  58. package/src/components/editor-2d/renderers/floorplan-label-angle.ts +14 -0
  59. package/src/components/editor-2d/renderers/floorplan-placement-preview-layer.tsx +4 -1
  60. package/src/components/editor-2d/renderers/floorplan-registry-layer.test.ts +308 -0
  61. package/src/components/editor-2d/renderers/floorplan-registry-layer.tsx +1704 -342
  62. package/src/components/editor-2d/renderers/floorplan-voronoi-layer.tsx +128 -0
  63. package/src/components/systems/ceiling/ceiling-selection-affordance-system.tsx +419 -60
  64. package/src/components/systems/ceiling/ceiling-system.tsx +97 -4
  65. package/src/components/systems/roof/roof-edit-system.tsx +1768 -13
  66. package/src/components/systems/selection-affordance-manager.tsx +38 -0
  67. package/src/components/systems/zone/zone-label-editor-system.tsx +14 -1
  68. package/src/components/systems/zone/zone-system.tsx +42 -13
  69. package/src/components/tools/elevator/elevator-tool.tsx +34 -8
  70. package/src/components/tools/elevator/move-elevator-tool.tsx +13 -2
  71. package/src/components/tools/fence/fence-drafting.ts +80 -8
  72. package/src/components/tools/item/move-tool.tsx +17 -13
  73. package/src/components/tools/item/placement-math.test.ts +13 -1
  74. package/src/components/tools/item/placement-math.ts +28 -2
  75. package/src/components/tools/item/placement-strategies.ts +246 -5
  76. package/src/components/tools/item/placement-types.ts +13 -1
  77. package/src/components/tools/item/use-draft-node.ts +31 -1
  78. package/src/components/tools/item/use-placement-coordinator.tsx +761 -121
  79. package/src/components/tools/registry/move-registry-node-tool.tsx +561 -99
  80. package/src/components/tools/roof/roof-tool.tsx +320 -94
  81. package/src/components/tools/select/box-select-state.ts +18 -7
  82. package/src/components/tools/select/box-select-tool.tsx +176 -40
  83. package/src/components/tools/select/marquee-geometry.test.ts +161 -0
  84. package/src/components/tools/select/marquee-geometry.ts +155 -0
  85. package/src/components/tools/select/plane-box-select-tool.tsx +1 -8
  86. package/src/components/tools/select/select-candidates.ts +1 -1
  87. package/src/components/tools/shared/cursor-sphere.tsx +62 -26
  88. package/src/components/tools/shared/drag-bounding-box.tsx +28 -4
  89. package/src/components/tools/shared/facing-indicator.tsx +87 -0
  90. package/src/components/tools/shared/facing-pose-indicator.tsx +54 -0
  91. package/src/components/tools/shared/placement-box.tsx +183 -1
  92. package/src/components/tools/shared/polygon-editor.tsx +276 -35
  93. package/src/components/tools/site/site-boundary-editor.tsx +88 -36
  94. package/src/components/tools/stair/stair-defaults.ts +1 -0
  95. package/src/components/tools/stair/stair-tool.tsx +82 -11
  96. package/src/components/tools/tool-manager.tsx +86 -25
  97. package/src/components/tools/wall/wall-drafting.test.ts +330 -0
  98. package/src/components/tools/wall/wall-drafting.ts +180 -103
  99. package/src/components/tools/wall/wall-snap-geometry.test.ts +44 -0
  100. package/src/components/tools/wall/wall-snap-geometry.ts +67 -6
  101. package/src/components/tools/zone/zone-boundary-editor.tsx +5 -1
  102. package/src/components/tools/zone/zone-tool.tsx +82 -88
  103. package/src/components/ui/action-menu/camera-actions.tsx +24 -17
  104. package/src/components/ui/action-menu/control-modes.tsx +40 -93
  105. package/src/components/ui/action-menu/furnish-tools.tsx +5 -5
  106. package/src/components/ui/action-menu/index.tsx +2 -4
  107. package/src/components/ui/action-menu/measurement-control.tsx +188 -0
  108. package/src/components/ui/action-menu/structure-tools.tsx +22 -13
  109. package/src/components/ui/action-menu/view-toggles.tsx +42 -90
  110. package/src/components/ui/command-palette/editor-commands.tsx +2 -2
  111. package/src/components/ui/command-palette/index.tsx +4 -3
  112. package/src/components/ui/controls/material-paint-panel.tsx +86 -17
  113. package/src/components/ui/controls/material-picker.tsx +83 -152
  114. package/src/components/ui/controls/material-properties-editor.tsx +108 -0
  115. package/src/components/ui/controls/metric-control.tsx +118 -44
  116. package/src/components/ui/controls/scene-material-list.tsx +247 -0
  117. package/src/components/ui/controls/slider-control.tsx +88 -38
  118. package/src/components/ui/floating-level-selector.tsx +1 -1
  119. package/src/components/ui/helpers/building-helper.tsx +10 -23
  120. package/src/components/ui/helpers/contextual-helper-panel.tsx +428 -0
  121. package/src/components/ui/helpers/helper-manager.tsx +227 -14
  122. package/src/components/ui/helpers/item-helper.tsx +28 -32
  123. package/src/components/ui/helpers/registered-tool-helper.tsx +45 -11
  124. package/src/components/ui/helpers/roof-helper.tsx +10 -12
  125. package/src/components/ui/icon-ref.tsx +47 -0
  126. package/src/components/ui/item-catalog/catalog-items.tsx +39 -1
  127. package/src/components/ui/item-catalog/item-catalog.tsx +13 -36
  128. package/src/components/ui/level-duplicate-dialog.tsx +1 -1
  129. package/src/components/ui/panels/node-display.ts +17 -17
  130. package/src/components/ui/panels/panel-manager.tsx +22 -13
  131. package/src/components/ui/panels/panel-wrapper.tsx +24 -3
  132. package/src/components/ui/panels/parametric-inspector.tsx +15 -2
  133. package/src/components/ui/panels/reference-panel.tsx +54 -19
  134. package/src/components/ui/primitives/shortcut-token.tsx +39 -3
  135. package/src/components/ui/primitives/sidebar.tsx +1 -0
  136. package/src/components/ui/sidebar/app-sidebar.tsx +5 -1
  137. package/src/components/ui/sidebar/icon-rail.tsx +50 -31
  138. package/src/components/ui/sidebar/panels/plugins-panel.tsx +218 -0
  139. package/src/components/ui/sidebar/panels/settings-panel/index.tsx +71 -40
  140. package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +60 -6
  141. package/src/components/ui/sidebar/panels/settings-panel/load-build-dialog.tsx +12 -4
  142. package/src/components/ui/sidebar/panels/site-panel/ceiling-tree-node.tsx +4 -3
  143. package/src/components/ui/sidebar/panels/site-panel/chimney-tree-node.tsx +10 -7
  144. package/src/components/ui/sidebar/panels/site-panel/column-tree-node.tsx +1 -1
  145. package/src/components/ui/sidebar/panels/site-panel/door-tree-node.tsx +7 -2
  146. package/src/components/ui/sidebar/panels/site-panel/dormer-tree-node.tsx +10 -7
  147. package/src/components/ui/sidebar/panels/site-panel/elevator-tree-node.tsx +1 -1
  148. package/src/components/ui/sidebar/panels/site-panel/fence-tree-node.tsx +1 -1
  149. package/src/components/ui/sidebar/panels/site-panel/gutter-tree-node.tsx +10 -7
  150. package/src/components/ui/sidebar/panels/site-panel/index.tsx +40 -17
  151. package/src/components/ui/sidebar/panels/site-panel/item-tree-node.tsx +31 -17
  152. package/src/components/ui/sidebar/panels/site-panel/level-tree-node.tsx +1 -1
  153. package/src/components/ui/sidebar/panels/site-panel/registry-tree-node.tsx +72 -25
  154. package/src/components/ui/sidebar/panels/site-panel/roof-tree-node.tsx +7 -3
  155. package/src/components/ui/sidebar/panels/site-panel/shelf-tree-node.tsx +12 -8
  156. package/src/components/ui/sidebar/panels/site-panel/slab-tree-node.tsx +4 -3
  157. package/src/components/ui/sidebar/panels/site-panel/solar-panel-tree-node.tsx +10 -7
  158. package/src/components/ui/sidebar/panels/site-panel/spawn-tree-node.tsx +1 -1
  159. package/src/components/ui/sidebar/panels/site-panel/stair-tree-node.tsx +2 -2
  160. package/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +44 -3
  161. package/src/components/ui/sidebar/panels/site-panel/tree-structure.ts +36 -0
  162. package/src/components/ui/sidebar/panels/site-panel/wall-tree-node.tsx +1 -1
  163. package/src/components/ui/sidebar/panels/site-panel/window-tree-node.tsx +7 -2
  164. package/src/components/ui/sidebar/panels/site-panel/zone-tree-node.tsx +3 -3
  165. package/src/components/ui/sidebar/tab-bar.tsx +42 -28
  166. package/src/components/ui/sidebar/use-plugin-panels.tsx +132 -0
  167. package/src/components/ui/snap-target-badge.test.tsx +40 -0
  168. package/src/components/ui/snap-target-badge.tsx +88 -0
  169. package/src/components/viewer-overlay.tsx +85 -10
  170. package/src/components/viewer-zone-system.tsx +6 -1
  171. package/src/hooks/use-auto-save.test.ts +14 -0
  172. package/src/hooks/use-auto-save.ts +54 -7
  173. package/src/hooks/use-ceiling-events.ts +3 -2
  174. package/src/hooks/use-drag-action.ts +4 -1
  175. package/src/hooks/use-keyboard.ts +385 -40
  176. package/src/index.tsx +219 -6
  177. package/src/lib/active-placement-surface.ts +35 -0
  178. package/src/lib/ceiling-plan-snap.ts +24 -0
  179. package/src/lib/contextual-help.test.ts +112 -0
  180. package/src/lib/contextual-help.ts +144 -0
  181. package/src/lib/continuation.ts +64 -0
  182. package/src/lib/direct-manipulation.test.ts +192 -0
  183. package/src/lib/direct-manipulation.ts +109 -0
  184. package/src/lib/editor-api.ts +23 -35
  185. package/src/lib/floorplan/apply-alignment.test.ts +25 -0
  186. package/src/lib/floorplan/apply-alignment.ts +18 -12
  187. package/src/lib/floorplan/floorplan-export.tsx +364 -0
  188. package/src/lib/floorplan/geometry.ts +53 -0
  189. package/src/lib/floorplan/index.ts +3 -0
  190. package/src/lib/floorplan/items.ts +5 -2
  191. package/src/lib/floorplan/plan-coords.ts +21 -0
  192. package/src/lib/fresh-planar-placement.test.ts +240 -3
  193. package/src/lib/fresh-planar-placement.ts +68 -1
  194. package/src/lib/glb-export.test.ts +441 -0
  195. package/src/lib/glb-export.ts +874 -0
  196. package/src/lib/history.ts +9 -0
  197. package/src/lib/interaction/hot-set.test.ts +133 -0
  198. package/src/lib/interaction/hot-set.ts +67 -0
  199. package/src/lib/interaction/overlay-policy.test.ts +51 -0
  200. package/src/lib/interaction/overlay-policy.ts +59 -0
  201. package/src/lib/interaction/scope.ts +211 -0
  202. package/src/lib/level-selection.ts +2 -2
  203. package/src/lib/material-paint.ts +36 -49
  204. package/src/lib/measurement-kind.test.ts +30 -0
  205. package/src/lib/measurement-kind.ts +19 -0
  206. package/src/lib/measurement-label.test.ts +47 -0
  207. package/src/lib/measurement-label.ts +68 -0
  208. package/src/lib/measurement-parser.ts +116 -0
  209. package/src/lib/measurements.test.ts +183 -0
  210. package/src/lib/measurements.ts +199 -0
  211. package/src/lib/paint-scope.test.ts +454 -0
  212. package/src/lib/paint-scope.ts +461 -0
  213. package/src/lib/placement-drag-release.ts +23 -0
  214. package/src/lib/planar-cursor-placement.test.ts +57 -0
  215. package/src/lib/plugin-panels.test.ts +19 -0
  216. package/src/lib/plugin-panels.ts +91 -0
  217. package/src/lib/quick-measurement.test.ts +77 -0
  218. package/src/lib/quick-measurement.ts +109 -0
  219. package/src/lib/roof-duplication.ts +6 -6
  220. package/src/lib/roof-hover-outline-proxy.ts +22 -0
  221. package/src/lib/roof-wall-hit.ts +164 -0
  222. package/src/lib/scene-clipboard.test.ts +196 -0
  223. package/src/lib/scene-clipboard.ts +73 -8
  224. package/src/lib/scene.ts +28 -5
  225. package/src/lib/selection-routing.test.ts +298 -0
  226. package/src/lib/selection-routing.ts +175 -0
  227. package/src/lib/sfx/index.ts +1 -0
  228. package/src/lib/sfx/movement-tick.test.ts +65 -0
  229. package/src/lib/sfx/movement-tick.ts +18 -0
  230. package/src/lib/sfx-bus.ts +50 -13
  231. package/src/lib/sfx-player.test.ts +124 -0
  232. package/src/lib/sfx-player.ts +111 -66
  233. package/src/lib/slab-plan-snap.test.ts +93 -0
  234. package/src/lib/slab-plan-snap.ts +108 -0
  235. package/src/lib/snapping-mode.test.ts +138 -0
  236. package/src/lib/snapping-mode.ts +179 -0
  237. package/src/lib/stair-duplication.ts +4 -4
  238. package/src/lib/surface-plan-snap.test.ts +71 -0
  239. package/src/lib/surface-plan-snap.ts +256 -0
  240. package/src/lib/use-linear-display.ts +40 -0
  241. package/src/lib/world-grid-snap.ts +37 -5
  242. package/src/store/use-direct-manipulation-feedback.ts +20 -0
  243. package/src/store/use-editor.tsx +484 -114
  244. package/src/store/use-facing-pose.ts +44 -0
  245. package/src/store/use-fence-curve-draft.ts +21 -0
  246. package/src/store/use-floorplan-draft-preview.ts +101 -0
  247. package/src/store/use-floorplan-marquee.ts +50 -0
  248. package/src/store/use-interaction-scope.test.ts +186 -0
  249. package/src/store/use-interaction-scope.ts +132 -0
  250. package/src/store/use-measurement-draft.test.ts +530 -0
  251. package/src/store/use-measurement-draft.ts +543 -0
  252. package/src/store/use-opening-guides.ts +41 -0
  253. package/src/store/use-placement-preview.ts +11 -3
  254. package/src/store/use-quick-measurement-hud.test.ts +53 -0
  255. package/src/store/use-quick-measurement-hud.ts +77 -0
  256. package/src/store/use-segment-draft-chain.ts +28 -0
  257. package/src/store/use-stair-build-preview.ts +43 -0
  258. package/src/store/use-wall-snap-indicator.ts +2 -0
  259. package/src/components/editor/first-person/bvh-ecctrl.tsx +0 -860
  260. package/src/components/editor/group-move-handle.tsx +0 -316
  261. package/src/components/ui/panels/paint-panel.tsx +0 -163
  262. package/src/lib/level-name.ts +0 -11
@@ -1,51 +1,27 @@
1
- import { emitter, type GridEvent, type LevelNode, useScene, ZoneNode } from '@pascal-app/core'
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
- if (lastPoint) {
131
- const snapped = calculateSnapPoint(lastPoint, cursorPosition)
132
- if (isValidPoint(snapped)) {
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 snapped = calculateSnapPoint(lastPoint, cursorPosition)
150
- if (isValidPoint(snapped)) {
151
- const closingPoints = [
152
- new Vector3(snapped[0], y, snapped[1]),
153
- new Vector3(firstPoint[0], y, firstPoint[1]),
154
- ]
155
- closingLineRef.current.geometry.dispose()
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 lastPoint = points[points.length - 1]
161
+ const cursorPt = snappedCursorPosition ?? cursorPosition
167
162
 
168
- let cursorPt: [number, number] | null = null
169
- if (lastPoint) {
170
- cursorPt = calculateSnapPoint(lastPoint, cursorPosition)
171
- } else if (points.length === 0) {
172
- cursorPt = cursorPosition
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
- // World-grid snap projected into building-local; rotated buildings
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
- ? calculateSnapPoint(lastPoint, cursorPosition)
196
- : cursorPosition
197
-
198
- // Play snap sound when the snapped position changes during drawing
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
- if (lastPoint) {
227
- clickPoint = calculateSnapPoint(lastPoint, clickPoint)
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.png"
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.png"
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
- <ActionButton
62
- className="group hover:bg-white/5"
63
- label="Top View"
64
- onClick={goToTopView}
65
- size="icon"
66
- variant="ghost"
67
- >
68
- <Image
69
- alt="Top View"
70
- className="h-[28px] w-[28px] object-contain opacity-70 transition-opacity group-hover:opacity-100"
71
- height={28}
72
- src="/icons/topview.png"
73
- width={28}
74
- />
75
- </ActionButton>
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, { selectSiteFloorplanContext } from './../../../store/use-editor'
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' | 'site-edit' | 'zone' | 'delete'
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.png',
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.png',
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
- <ActionButton
145
- className={cn(
146
- 'group text-muted-foreground',
147
- isSiteButton
148
- ? isActive
149
- ? c.activeColor
150
- : canEnterSiteEdit
151
- ? 'opacity-60 grayscale hover:bg-white/5 hover:opacity-100 hover:grayscale-0'
152
- : 'cursor-not-allowed opacity-35 grayscale'
153
- : !(isImageMode || isActive) && c.color,
154
- !(isSiteButton || isImageMode) && isActive && c.activeColor,
155
- !isSiteButton && isImageMode && isActive && 'bg-white/10 hover:bg-white/10',
156
- !isSiteButton && isImageMode && !isActive && 'hover:bg-white/5',
157
- )}
158
- disabled={isDisabled}
159
- key={c.id}
160
- label={
161
- isSiteButton
162
- ? isActive
163
- ? 'Exit site editing'
164
- : canEnterSiteEdit
165
- ? 'Edit site'
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
- height={28}
188
- src={c.imageSrc}
189
- width={28}
190
- />
191
- ) : c.iconifyIcon ? (
192
- <Icon color="currentColor" height={18} icon={c.iconifyIcon} width={18} />
193
- ) : (
194
- ModeIcon && <ModeIcon className="h-5 w-5" />
195
- )}
196
- </ActionButton>
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.png', label: 'Furniture', catalogCategory: 'furniture' },
12
- { id: 'item', iconSrc: '/icons/appliance.png', label: 'Appliance', catalogCategory: 'appliance' },
13
- { id: 'item', iconSrc: '/icons/kitchen.png', label: 'Kitchen', catalogCategory: 'kitchen' },
14
- { id: 'item', iconSrc: '/icons/bathroom.png', label: 'Bathroom', catalogCategory: 'bathroom' },
15
- { id: 'item', iconSrc: '/icons/tree.png', label: 'Outdoor', catalogCategory: 'outdoor' },
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 { GridSnapControl, SecondaryToggles } from './view-toggles'
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: grid snap + secondary toggles (orbit + top view hidden) */}
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 />