@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,14 +1,28 @@
1
1
  'use client'
2
2
 
3
- import { emitter, type GridEvent, sceneRegistry } from '@pascal-app/core'
3
+ import { type AnyNodeId, emitter, type GridEvent, sceneRegistry } from '@pascal-app/core'
4
4
  import { GRID_LAYER, getSceneTheme, useViewer } from '@pascal-app/viewer'
5
5
  import { useFrame } from '@react-three/fiber'
6
6
  import { useEffect, useMemo, useRef, useState } from 'react'
7
- import { MathUtils, type Mesh, PlaneGeometry, Vector2 } from 'three'
7
+ import { DoubleSide, type Mesh, PlaneGeometry, Quaternion, Vector2, Vector3 } from 'three'
8
8
  import { color, float, fract, fwidth, mix, positionLocal, uniform } from 'three/tsl'
9
9
  import { MeshBasicNodeMaterial } from 'three/webgpu'
10
10
  import { useCeilingEvents } from '../../hooks/use-ceiling-events'
11
11
  import { useGridEvents } from '../../hooks/use-grid-events'
12
+ import { getPlacementSurface } from '../../lib/active-placement-surface'
13
+ import useEditor, { isGridSnapActive } from '../../store/use-editor'
14
+ import { getMovingNode } from '../../store/use-interaction-scope'
15
+
16
+ // Reveal radius (m) of the cursor-local grid patch shown while placing/moving in
17
+ // grid-snap mode — much tighter than the idle reveal so only the area you're
18
+ // about to snap into lights up.
19
+ const PLACEMENT_REVEAL_RADIUS = 12
20
+
21
+ const UP = new Vector3(0, 1, 0)
22
+ // PlaneGeometry faces +Z; this is the orientation that lays it flat (its normal
23
+ // → world +Y), equivalent to the old `rotation-x={-π/2}`.
24
+ const PLANE_LOCAL_NORMAL = new Vector3(0, 0, 1)
25
+ const HORIZONTAL_QUATERNION = new Quaternion().setFromUnitVectors(PLANE_LOCAL_NORMAL, UP)
12
26
 
13
27
  export const Grid = ({
14
28
  cellSize = 0.5,
@@ -38,6 +52,30 @@ export const Grid = ({
38
52
  const effectiveSectionColor = isDark ? '#666677' : sectionColor
39
53
 
40
54
  const cursorPositionRef = useRef(new Vector2(0, 0))
55
+ // Scratch for reading a moving node's world Y (surface elevation) each frame.
56
+ const worldPosRef = useRef(new Vector3())
57
+ // Scratch for the wall-anchored branch: invert the plane orientation to map the
58
+ // ghost into plane-local XY (the cursor reveal) without re-centring the mesh.
59
+ const invQuatRef = useRef(new Quaternion())
60
+ const wallCursorRef = useRef(new Vector3())
61
+ // Scratch for deriving a moving wall-item's host normal from its mesh.
62
+ const wallNormalRef = useRef(new Vector3())
63
+ // Last Y pushed to `gridY` state, so the per-frame surface follow only triggers
64
+ // a React re-render when the height actually changes (not every frame).
65
+ const lastGridYRef = useRef<number | null>(null)
66
+
67
+ // Reveal radius + baseline alpha are uniforms so a placement/move can shrink
68
+ // the grid to a tight cursor patch (and drop the always-on baseline) without
69
+ // rebuilding the shader. Driven each frame in `useFrame`.
70
+ const revealRadiusUniform = useMemo(() => uniform(revealRadius), [revealRadius])
71
+ const baseAlphaUniform = useMemo(() => uniform(0.4), [])
72
+ // Created once and driven by `.value` each frame (see `useFrame`). Keying this
73
+ // on `cellSize` rebuilt the uniform AND the material `useMemo` below on every
74
+ // `gridSnapStep` change — a full shader recompile that stalled hard whenever
75
+ // the grid resolution changed. The live cell size is a uniform write only.
76
+ // biome-ignore lint/correctness/useExhaustiveDependencies: created once on purpose; `.value` is driven each frame.
77
+ const cellSizeUniform = useMemo(() => uniform(cellSize), [])
78
+ const patchAlphaUniform = useMemo(() => uniform(1), [])
41
79
 
42
80
  const material = useMemo(() => {
43
81
  // Use xy since plane geometry is in XY space (before rotation)
@@ -48,7 +86,7 @@ export const Grid = ({
48
86
 
49
87
  // Grid line function using fwidth for anti-aliasing
50
88
  // Returns 1 on grid lines, 0 elsewhere
51
- const getGrid = (size: number, thickness: number) => {
89
+ const getGrid = (size: number | typeof cellSizeUniform, thickness: number) => {
52
90
  const r = pos.div(size)
53
91
  const fw = fwidth(r)
54
92
  // Distance to nearest grid line for each axis
@@ -70,7 +108,7 @@ export const Grid = ({
70
108
  return lineX.max(lineY)
71
109
  }
72
110
 
73
- const g1 = getGrid(cellSize, cellThickness)
111
+ const g1 = getGrid(cellSizeUniform, cellThickness)
74
112
  const g2 = getGrid(sectionSize, sectionThickness)
75
113
 
76
114
  // Distance fade from center
@@ -79,7 +117,9 @@ export const Grid = ({
79
117
 
80
118
  // Cursor reveal effect - distance from cursor
81
119
  const cursorDist = pos.sub(cursorPos).length()
82
- const cursorFade = float(1).sub(cursorDist.div(revealRadius).clamp(0, 1)).smoothstep(0, 1)
120
+ const cursorFade = float(1)
121
+ .sub(cursorDist.div(revealRadiusUniform).clamp(0, 1))
122
+ .smoothstep(0, 1)
83
123
 
84
124
  // Mix colors based on section grid
85
125
  const gridColor = mix(
@@ -88,21 +128,26 @@ export const Grid = ({
88
128
  float(sectionThickness).mul(g2).min(1),
89
129
  )
90
130
 
91
- // Baseline alpha: small amount of opacity everywhere the grid exists
92
- const baseAlpha = float(0.4) // Subtle global visibility
93
-
94
131
  // Combined alpha with cursor fade and baseline minimum
95
- const alpha = g1.add(g2).mul(fade).mul(cursorFade.max(baseAlpha))
96
- const finalAlpha = mix(alpha.mul(0.75), alpha, g2)
132
+ const alpha = g1.add(g2).mul(fade).mul(cursorFade.max(baseAlphaUniform))
133
+ const boostedAlpha = alpha.mul(patchAlphaUniform).min(1)
134
+ const finalAlpha = mix(boostedAlpha.mul(0.75), boostedAlpha, g2)
97
135
 
98
136
  return new MeshBasicNodeMaterial({
99
137
  transparent: true,
100
138
  colorNode: gridColor,
101
139
  opacityNode: finalAlpha,
102
140
  depthWrite: false,
141
+ // `depthTest` is toggled per-frame in `useFrame`: ON for the floor lattice
142
+ // (so the ground occludes a sub-floor grid) and OFF on a wall (so the
143
+ // lattice shows through the wall when the opening is handled from the far
144
+ // side). Default ON for the floor case.
145
+ depthTest: true,
146
+ // Wall-plane placements are handled from either side of the wall, so the
147
+ // lattice must render from both faces.
148
+ side: DoubleSide,
103
149
  })
104
150
  }, [
105
- cellSize,
106
151
  cellThickness,
107
152
  effectiveCellColor,
108
153
  sectionSize,
@@ -110,7 +155,10 @@ export const Grid = ({
110
155
  effectiveSectionColor,
111
156
  fadeDistance,
112
157
  fadeStrength,
113
- revealRadius,
158
+ revealRadiusUniform,
159
+ baseAlphaUniform,
160
+ cellSizeUniform,
161
+ patchAlphaUniform,
114
162
  ])
115
163
 
116
164
  const gridRef = useRef<Mesh>(null!)
@@ -124,13 +172,10 @@ export const Grid = ({
124
172
  useCeilingEvents()
125
173
 
126
174
  // Track the last world-space cursor hit. The reveal-fade shader reads
127
- // `positionLocal.xy` (vertex position on the un-transformed plane), and
128
- // the mesh's -π/2 X rotation maps `positionLocal.y` to world `-Z`
129
- // relative to the mesh origin. The mesh origin itself is lerped each
130
- // frame toward the active building's world XZ (see `useFrame` below),
131
- // so the local-frame cursor must be recomputed every frame from the
132
- // stored world cursor — otherwise the ring drifts whenever the grid is
133
- // mid-lerp (e.g. just after a building rotation commits).
175
+ // `positionLocal.xy` (vertex position on the un-transformed plane), and the
176
+ // laid-flat orientation maps `positionLocal.y` to world `-Z` relative to the
177
+ // mesh origin. The cursor is recomputed every frame from the stored world hit
178
+ // so the reveal stays put regardless of where the mesh origin sits.
134
179
  const lastWorldCursorRef = useRef<{ x: number; z: number } | null>(null)
135
180
  useEffect(() => {
136
181
  const onGridMove = (event: GridEvent) => {
@@ -143,32 +188,113 @@ export const Grid = ({
143
188
  }
144
189
  }, [])
145
190
 
146
- useFrame((_, delta) => {
191
+ useFrame(() => {
147
192
  const { levelId } = useViewer.getState().selection
148
- // Grid stays anchored to world XZ (0, 0) — never chases the active
149
- // building. The Y origin still lerps to the active level so the grid
150
- // sits at floor height when a level is open.
151
- let targetY = 0
193
+ let levelY = 0
152
194
  if (levelId) {
153
195
  const levelMesh = sceneRegistry.nodes.get(levelId)
154
196
  if (levelMesh) {
155
- targetY = levelMesh.position.y
197
+ levelY = levelMesh.position.y
156
198
  }
157
199
  }
158
- const newY = MathUtils.lerp(gridRef.current.position.y, targetY, 12 * delta)
159
- gridRef.current.position.y = newY
160
- setGridY(newY)
161
-
162
- // Grid XZ is fixed at world origin, so the local-frame cursor uniform
163
- // is just the world cursor (mirrored on Z to match the -π/2 X-rotation
164
- // of the plane).
165
- const world = lastWorldCursorRef.current
166
- if (world) {
167
- cursorPositionRef.current.set(world.x, -world.z)
200
+
201
+ // Resolve the surface the active ghost is snapped to (contact point +
202
+ // normal). A fresh GLB item / drawn kind publishes via the surface module; a
203
+ // moving node is read straight off its mesh (treated as horizontal). Null
204
+ // when nothing is being placed.
205
+ const published = getPlacementSurface()
206
+ const movingForGrid = getMovingNode()
207
+ let surfacePoint: Vector3 | null = null
208
+ let surfaceNormal = UP
209
+ if (published) {
210
+ surfacePoint = published.point
211
+ surfaceNormal = published.normal
212
+ } else if (movingForGrid) {
213
+ const ghostMesh = sceneRegistry.nodes.get(movingForGrid.id as AnyNodeId)
214
+ if (ghostMesh) {
215
+ surfacePoint = ghostMesh.getWorldPosition(worldPosRef.current)
216
+ // A wall-hosted item read off its mesh gets its host's orientation
217
+ // immediately: its local +Z faces out of the wall (same convention the
218
+ // placement coordinator publishes). Without this the grid flashes
219
+ // horizontal on move-start until the first pointer move publishes a
220
+ // wall surface.
221
+ const attachTo = movingForGrid.type === 'item' ? movingForGrid.asset?.attachTo : undefined
222
+ if (attachTo === 'wall' || attachTo === 'wall-side') {
223
+ ghostMesh.getWorldQuaternion(invQuatRef.current)
224
+ const fwd = wallNormalRef.current.set(0, 0, 1).applyQuaternion(invQuatRef.current)
225
+ fwd.y = 0
226
+ if (fwd.lengthSq() > 1e-6) surfaceNormal = fwd.normalize()
227
+ }
228
+ }
168
229
  }
169
- })
170
230
 
171
- const showGrid = useViewer((state) => state.showGrid)
231
+ const gridMesh = gridRef.current
232
+ const onWall = surfacePoint != null && Math.abs(surfaceNormal.y) < 0.5
233
+ if (onWall && surfacePoint) {
234
+ // Wall-anchored lattice: orient the plane into the wall and pin the mesh to
235
+ // the plane's FOOT (the point on the wall plane closest to the world origin)
236
+ // — never the moving ghost. Sliding the opening along the wall then only
237
+ // moves the reveal patch (the cursor uniform); the snap lattice stays put.
238
+ // (Copying `surfacePoint` here made the grid follow the item — useless.)
239
+ gridMesh.quaternion.setFromUnitVectors(PLANE_LOCAL_NORMAL, surfaceNormal)
240
+ const planeOffset = surfacePoint.dot(surfaceNormal)
241
+ gridMesh.position.copy(surfaceNormal).multiplyScalar(planeOffset)
242
+ // Cursor → plane-local XY: rotate (ghost − anchor) by the inverse plane
243
+ // orientation. Both lie in the plane, so the resulting local Z is ~0.
244
+ invQuatRef.current.copy(gridMesh.quaternion).invert()
245
+ wallCursorRef.current
246
+ .copy(surfacePoint)
247
+ .sub(gridMesh.position)
248
+ .applyQuaternion(invQuatRef.current)
249
+ cursorPositionRef.current.set(wallCursorRef.current.x, wallCursorRef.current.y)
250
+ if (lastGridYRef.current !== surfacePoint.y) {
251
+ lastGridYRef.current = surfacePoint.y
252
+ setGridY(surfacePoint.y)
253
+ }
254
+ } else {
255
+ // Horizontal: keep the lattice anchored to world XZ (0,0); only the Y
256
+ // origin follows the surface height (floor / shelf top). Snap directly —
257
+ // the old lerp made the grid visibly drift up to a new floor height.
258
+ // Cursor uniform tracks the world cursor (mirrored on Z for the flat plane).
259
+ const targetY = surfacePoint ? surfacePoint.y : levelY
260
+ // Visual mesh rides 1mm above the surface: at exactly the surface Y the
261
+ // lattice is coplanar with the slab top and z-fights it. The event plane
262
+ // (`gridY` → useGridEvents) stays at the true surface height.
263
+ gridMesh.position.set(0, targetY + 0.001, 0)
264
+ gridMesh.quaternion.copy(HORIZONTAL_QUATERNION)
265
+ const world = lastWorldCursorRef.current
266
+ if (world) {
267
+ cursorPositionRef.current.set(world.x, -world.z)
268
+ }
269
+ if (lastGridYRef.current !== targetY) {
270
+ lastGridYRef.current = targetY
271
+ setGridY(targetY)
272
+ }
273
+ }
274
+
275
+ // Floor grid depth-tests against the scene (ground occludes a sub-floor
276
+ // lattice); the wall grid ignores depth so it stays visible through the wall
277
+ // when the opening is being handled from the opposite side.
278
+ if (material.depthTest === onWall) {
279
+ material.depthTest = !onWall
280
+ material.needsUpdate = true
281
+ }
282
+
283
+ // The grid is a placement aid: a tight cursor patch (no always-on baseline)
284
+ // shown whenever the active context is in grid-snap mode — ANY armed
285
+ // draft/build tool (wall / slab / fence / ceiling / zone / column / MEP / …),
286
+ // a node move, or a reshape — and hidden in select/idle, paint, and non-grid
287
+ // (lines/off) modes. `isGridSnapActive()` already derives the snap context
288
+ // from the interaction scope OR the armed build tool and is true only when
289
+ // that context resolves to grid, so it IS the gate. (Previously this also
290
+ // required a ghost in flight, so a merely-armed draft tool showed nothing.)
291
+ const snapPatchVisible = isGridSnapActive()
292
+ revealRadiusUniform.value = PLACEMENT_REVEAL_RADIUS
293
+ baseAlphaUniform.value = 0
294
+ cellSizeUniform.value = useEditor.getState().gridSnapStep
295
+ patchAlphaUniform.value = 1.5
296
+ gridRef.current.visible = snapPatchVisible
297
+ })
172
298
 
173
299
  // Pass the geometry as a prop instead of a JSX child so the mesh
174
300
  // is never reconciled with R3F's empty placeholder `BufferGeometry`.
@@ -183,13 +309,14 @@ export const Grid = ({
183
309
  useEffect(() => () => geometry.dispose(), [geometry])
184
310
 
185
311
  return (
312
+ // Orientation is driven imperatively in `useFrame` (horizontal by default,
313
+ // tilted into the wall plane while placing on a wall), so no static rotation.
186
314
  <mesh
187
315
  geometry={geometry}
188
316
  layers={GRID_LAYER}
189
317
  material={material}
190
318
  ref={gridRef}
191
- rotation-x={-Math.PI / 2}
192
- visible={showGrid}
319
+ renderOrder={1}
193
320
  />
194
321
  )
195
322
  }