@pascal-app/editor 0.9.2 → 1.0.0-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (203) hide show
  1. package/package.json +9 -7
  2. package/src/components/editor/bake-exporter.tsx +3 -1
  3. package/src/components/editor/bake-thumbnail.tsx +95 -0
  4. package/src/components/editor/camera-dragging-lifecycle.test.ts +49 -0
  5. package/src/components/editor/camera-dragging-lifecycle.ts +41 -0
  6. package/src/components/editor/custom-camera-controls.tsx +307 -212
  7. package/src/components/editor/delete-confirmation-dialog.tsx +54 -0
  8. package/src/components/editor/elevation-3d-guide-layer.tsx +120 -0
  9. package/src/components/editor/first-person/build-collider-world.test.ts +66 -2
  10. package/src/components/editor/first-person/build-collider-world.ts +20 -0
  11. package/src/components/editor/first-person/terrain-collider.test.ts +132 -0
  12. package/src/components/editor/first-person/terrain-collider.ts +212 -0
  13. package/src/components/editor/first-person-controls.tsx +295 -132
  14. package/src/components/editor/floating-action-menu.tsx +59 -65
  15. package/src/components/editor/floorplan-background-selection.test.ts +16 -2
  16. package/src/components/editor/floorplan-background-selection.ts +4 -18
  17. package/src/components/editor/floorplan-camera-sync.test.ts +249 -0
  18. package/src/components/editor/floorplan-camera-sync.ts +213 -0
  19. package/src/components/editor/floorplan-mode-coordinator.tsx +82 -0
  20. package/src/components/editor/floorplan-navigation-presentation.test.ts +142 -0
  21. package/src/components/editor/floorplan-navigation-presentation.ts +156 -0
  22. package/src/components/editor/floorplan-panel.tsx +931 -861
  23. package/src/components/editor/group-actions.ts +155 -15
  24. package/src/components/editor/group-floating-action-menu.tsx +1 -1
  25. package/src/components/editor/group-rotate-handle.tsx +1 -1
  26. package/src/components/editor/group-selection-box-3d.tsx +1 -1
  27. package/src/components/editor/handles/handle-drag-history.test.ts +96 -0
  28. package/src/components/editor/handles/handle-drag-history.ts +14 -0
  29. package/src/components/editor/handles/preview-overrides.test.ts +26 -0
  30. package/src/components/editor/handles/preview-overrides.ts +13 -0
  31. package/src/components/editor/handles/resize-snap.test.ts +52 -0
  32. package/src/components/editor/handles/resize-snap.ts +23 -0
  33. package/src/components/editor/handles/use-handle-drag.ts +17 -7
  34. package/src/components/editor/index.tsx +42 -9
  35. package/src/components/editor/measurement-pill.tsx +6 -1
  36. package/src/components/editor/node-arrow-handles.tsx +51 -15
  37. package/src/components/editor/quick-measurement-card.tsx +9 -3
  38. package/src/components/editor/quick-measurement-hud.tsx +7 -1
  39. package/src/components/editor/selection-manager.tsx +3 -1
  40. package/src/components/editor/site-edge-labels.tsx +24 -2
  41. package/src/components/editor/snapshot-capture-overlay.tsx +4 -1
  42. package/src/components/editor/thumbnail-generator.tsx +66 -294
  43. package/src/components/editor/use-floorplan-hit-testing.test.ts +82 -0
  44. package/src/components/editor/use-floorplan-hit-testing.ts +144 -161
  45. package/src/components/editor/use-mesh-settle-epoch.ts +1 -0
  46. package/src/components/editor/wall-measurement-label.tsx +8 -16
  47. package/src/components/editor/wall-move-side-handles.tsx +193 -18
  48. package/src/components/editor/wall-opening-highlights.tsx +63 -32
  49. package/src/components/editor/wall-snap-beacon-layer.tsx +6 -4
  50. package/src/components/editor-2d/floorplan-alignment-guide-layer.tsx +2 -1
  51. package/src/components/editor-2d/floorplan-cursor-indicator-overlay.tsx +39 -8
  52. package/src/components/editor-2d/floorplan-cursor-indicator-position.test.ts +31 -0
  53. package/src/components/editor-2d/floorplan-cursor-indicator-position.ts +39 -0
  54. package/src/components/editor-2d/floorplan-group-move.tsx +47 -4
  55. package/src/components/editor-2d/floorplan-measurement-tool-layer.tsx +21 -5
  56. package/src/components/editor-2d/floorplan-registered-tool-layer.tsx +72 -0
  57. package/src/components/editor-2d/floorplan-registry-action-menu.tsx +93 -59
  58. package/src/components/editor-2d/floorplan-registry-move-overlay.tsx +27 -0
  59. package/src/components/editor-2d/floorplan-render-context.test.ts +27 -0
  60. package/src/components/editor-2d/floorplan-render-context.tsx +113 -8
  61. package/src/components/editor-2d/renderers/floorplan-annotation-layout.test.ts +285 -0
  62. package/src/components/editor-2d/renderers/floorplan-annotation-layout.ts +516 -0
  63. package/src/components/editor-2d/renderers/floorplan-dimension-renderer.test.tsx +236 -0
  64. package/src/components/editor-2d/renderers/floorplan-dimension-renderer.tsx +618 -0
  65. package/src/components/editor-2d/renderers/floorplan-geometry-renderer.test.tsx +369 -0
  66. package/src/components/editor-2d/renderers/floorplan-geometry-renderer.tsx +431 -32
  67. package/src/components/editor-2d/renderers/floorplan-label-angle.test.ts +75 -1
  68. package/src/components/editor-2d/renderers/floorplan-label-angle.ts +92 -0
  69. package/src/components/editor-2d/renderers/floorplan-placement-preview-layer.tsx +93 -28
  70. package/src/components/editor-2d/renderers/floorplan-registry-layer.test.ts +279 -0
  71. package/src/components/editor-2d/renderers/floorplan-registry-layer.tsx +621 -281
  72. package/src/components/editor-2d/renderers/floorplan-stair-layer.tsx +20 -10
  73. package/src/components/systems/ceiling/ceiling-selection-affordance-system.tsx +12 -7
  74. package/src/components/tools/fence/fence-drafting.ts +27 -0
  75. package/src/components/tools/item/use-draft-node.ts +131 -72
  76. package/src/components/tools/item/use-placement-coordinator.tsx +52 -4
  77. package/src/components/tools/registry/move-registry-node-tool.tsx +75 -4
  78. package/src/components/tools/roof/roof-tool.tsx +16 -0
  79. package/src/components/tools/shared/floor-stack-preview.ts +4 -0
  80. package/src/components/tools/shared/horizontal-construction-plane.ts +123 -0
  81. package/src/components/tools/shared/pointer-support-cap.test.ts +98 -0
  82. package/src/components/tools/shared/pointer-support-cap.ts +273 -0
  83. package/src/components/tools/site/site-boundary-editor.tsx +13 -6
  84. package/src/components/tools/site/stroke-pointer-ownership.test.ts +91 -0
  85. package/src/components/tools/site/stroke-pointer-ownership.ts +57 -0
  86. package/src/components/tools/site/terrain-brush-cursor.tsx +247 -0
  87. package/src/components/tools/site/terrain-sculpt-grid.tsx +192 -0
  88. package/src/components/tools/site/terrain-sculpt-tool.tsx +356 -0
  89. package/src/components/tools/stair/stair-click-guard.test.ts +58 -0
  90. package/src/components/tools/stair/stair-click-guard.ts +72 -0
  91. package/src/components/tools/stair/stair-tool.tsx +98 -47
  92. package/src/components/tools/tool-manager.tsx +67 -9
  93. package/src/components/tools/wall/wall-drafting.test.ts +118 -1
  94. package/src/components/tools/wall/wall-drafting.ts +128 -9
  95. package/src/components/tools/wall/wall-snap-geometry.test.ts +8 -0
  96. package/src/components/tools/wall/wall-snap-geometry.ts +70 -3
  97. package/src/components/tools/zone/zone-tool.tsx +16 -0
  98. package/src/components/ui/action-menu/control-modes.tsx +4 -1
  99. package/src/components/ui/action-menu/measurement-control.tsx +107 -28
  100. package/src/components/ui/command-palette/editor-commands.tsx +14 -1
  101. package/src/components/ui/controls/material-paint-panel.tsx +7 -1
  102. package/src/components/ui/controls/material-picker.tsx +86 -7
  103. package/src/components/ui/controls/terrain-sculpt-panel.tsx +193 -0
  104. package/src/components/ui/floating-level-selector.tsx +73 -19
  105. package/src/components/ui/helpers/helper-manager.tsx +46 -0
  106. package/src/components/ui/panels/multi-selection-panel.tsx +57 -0
  107. package/src/components/ui/panels/panel-manager.tsx +14 -1
  108. package/src/components/ui/panels/parametric-inspector.tsx +4 -2
  109. package/src/components/ui/panels/selection-breakdown.test.ts +24 -0
  110. package/src/components/ui/panels/selection-breakdown.ts +20 -0
  111. package/src/components/ui/sidebar/panels/settings-panel/index.tsx +14 -4
  112. package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +16 -0
  113. package/src/components/ui/sidebar/panels/site-panel/building-tree-node.tsx +2 -1
  114. package/src/components/ui/sidebar/panels/site-panel/index.tsx +2 -0
  115. package/src/components/viewer/viewer-controls-bar.tsx +466 -0
  116. package/src/components/viewer/viewer-scene-header.tsx +235 -0
  117. package/src/components/viewer-overlay.tsx +20 -655
  118. package/src/components/walkthrough-hud.tsx +111 -0
  119. package/src/hooks/use-grid-events.ts +24 -0
  120. package/src/hooks/use-keyboard.ts +53 -30
  121. package/src/index.tsx +137 -4
  122. package/src/lib/active-placement-surface.test.ts +24 -0
  123. package/src/lib/active-placement-surface.ts +8 -1
  124. package/src/lib/camera-pose.test.ts +237 -0
  125. package/src/lib/camera-pose.ts +172 -0
  126. package/src/lib/direct-manipulation.test.ts +83 -0
  127. package/src/lib/direct-manipulation.ts +9 -0
  128. package/src/lib/door-interaction.ts +1 -1
  129. package/src/lib/elevation-guides.test.ts +107 -0
  130. package/src/lib/elevation-guides.ts +236 -0
  131. package/src/lib/floorplan/annotation-visibility.test.ts +327 -0
  132. package/src/lib/floorplan/annotation-visibility.ts +133 -0
  133. package/src/lib/floorplan/drawing-coordination.test.ts +46 -0
  134. package/src/lib/floorplan/drawing-coordination.ts +13 -0
  135. package/src/lib/floorplan/floorplan-export.test.ts +319 -0
  136. package/src/lib/floorplan/floorplan-export.tsx +745 -131
  137. package/src/lib/floorplan/floorplan-extension.test.ts +43 -0
  138. package/src/lib/floorplan/floorplan-extension.ts +181 -0
  139. package/src/lib/floorplan/floorplan-mode.test.ts +120 -0
  140. package/src/lib/floorplan/floorplan-mode.ts +73 -0
  141. package/src/lib/floorplan/floorplan-pdfkit-document.ts +194 -0
  142. package/src/lib/floorplan/floorplan-pdfkit-renderer.test.ts +107 -0
  143. package/src/lib/floorplan/floorplan-pdfkit-renderer.ts +680 -0
  144. package/src/lib/floorplan/geometry.ts +2 -3
  145. package/src/lib/fresh-planar-placement.test.ts +44 -1
  146. package/src/lib/fresh-planar-placement.ts +21 -4
  147. package/src/lib/glb-export.test.ts +116 -1
  148. package/src/lib/glb-export.ts +204 -7
  149. package/src/lib/ground-surface.ts +95 -0
  150. package/src/lib/history.test.ts +96 -0
  151. package/src/lib/history.ts +22 -0
  152. package/src/lib/interaction/overlay-policy.test.ts +2 -1
  153. package/src/lib/interaction/scope.ts +40 -8
  154. package/src/lib/level-duplication.test.ts +2 -1
  155. package/src/lib/measurements.test.ts +5 -0
  156. package/src/lib/measurements.ts +12 -1
  157. package/src/lib/quick-action-feedback.ts +33 -0
  158. package/src/lib/quick-action-nodes.test.ts +94 -0
  159. package/src/lib/quick-action-nodes.ts +47 -0
  160. package/src/lib/scene-clipboard.test.ts +130 -1
  161. package/src/lib/scene-clipboard.ts +237 -13
  162. package/src/lib/selection-routing.test.ts +16 -1
  163. package/src/lib/selection-routing.ts +5 -0
  164. package/src/lib/sfx-bus.ts +33 -2
  165. package/src/lib/sfx-player.test.ts +148 -6
  166. package/src/lib/sfx-player.ts +156 -8
  167. package/src/lib/site-boundary.test.ts +53 -0
  168. package/src/lib/site-boundary.ts +27 -0
  169. package/src/lib/snapping-mode.test.ts +29 -2
  170. package/src/lib/snapping-mode.ts +15 -3
  171. package/src/lib/stair-duplication.test.ts +103 -0
  172. package/src/lib/stair-duplication.ts +57 -85
  173. package/src/lib/stair-levels.ts +2 -0
  174. package/src/lib/terrain-sculpt.test.ts +429 -0
  175. package/src/lib/terrain-sculpt.ts +301 -0
  176. package/src/lib/terrain-verb-color.test.ts +51 -0
  177. package/src/lib/terrain-verb-color.ts +58 -0
  178. package/src/lib/touch-gesture-priority.test.ts +45 -0
  179. package/src/lib/touch-gesture-priority.ts +30 -0
  180. package/src/lib/window-interaction.ts +1 -1
  181. package/src/store/camera-pose-store.test.ts +29 -0
  182. package/src/store/camera-pose-store.ts +27 -0
  183. package/src/store/live-draft-preview-stores.test.ts +136 -0
  184. package/src/store/navigation-sync-pose-store.test.ts +34 -0
  185. package/src/store/navigation-sync-pose-store.ts +27 -0
  186. package/src/store/terrain-sculpt-mode.test.ts +223 -0
  187. package/src/store/use-delete-confirmation.ts +27 -0
  188. package/src/store/use-drawing-view.test.ts +23 -0
  189. package/src/store/use-drawing-view.ts +84 -0
  190. package/src/store/use-editor.tsx +184 -15
  191. package/src/store/use-elevation-guides.ts +40 -0
  192. package/src/store/use-fence-curve-draft.ts +38 -8
  193. package/src/store/use-first-person-hud.ts +40 -0
  194. package/src/store/use-floorplan-annotation-visibility.ts +60 -0
  195. package/src/store/use-floorplan-draft-preview.ts +54 -4
  196. package/src/store/use-floorplan-mode.ts +110 -0
  197. package/src/store/use-interaction-scope.test.ts +23 -3
  198. package/src/store/use-interaction-scope.ts +8 -0
  199. package/src/store/use-measurement-draft.test.ts +29 -0
  200. package/src/store/use-measurement-draft.ts +33 -6
  201. package/src/store/use-path-draft-preview.ts +90 -0
  202. package/src/store/use-stair-build-preview.ts +12 -2
  203. package/src/lib/floorplan/selection-tool.ts +0 -271
@@ -0,0 +1,111 @@
1
+ 'use client'
2
+
3
+ import type { ReactNode } from 'react'
4
+ import { cn } from '../lib/utils'
5
+ import type { WalkthroughInteract } from '../store/use-first-person-hud'
6
+
7
+ export type { WalkthroughInteract } from '../store/use-first-person-hud'
8
+
9
+ export type WalkthroughHudProps = {
10
+ floorLabel?: string | null
11
+ zoneLabel?: string | null
12
+ interact?: WalkthroughInteract
13
+ /** Pointer lock temporarily released (OS screenshot) — the pill flips to
14
+ * "Click to resume" and lets clicks fall through to the canvas. */
15
+ suspended?: boolean
16
+ onExit?: () => void
17
+ children?: ReactNode
18
+ }
19
+
20
+ export function WalkthroughHud({
21
+ floorLabel,
22
+ zoneLabel,
23
+ interact = null,
24
+ suspended = false,
25
+ onExit,
26
+ children,
27
+ }: WalkthroughHudProps) {
28
+ const kbdClass = 'rounded border border-border/60 bg-white/10 px-1.5 py-0.5 font-mono text-[10px]'
29
+ const pillClass =
30
+ 'flex items-center gap-1.5 rounded-full border border-border/40 bg-background/70 px-3 py-1 text-muted-foreground text-xs backdrop-blur-xl'
31
+ const exitContent = (
32
+ <>
33
+ <kbd className={kbdClass}>Esc</kbd>
34
+ to exit
35
+ </>
36
+ )
37
+
38
+ return (
39
+ <div className="dark pointer-events-none absolute inset-0 z-30 text-foreground">
40
+ <div className="absolute top-6 left-1/2 flex -translate-x-1/2 flex-col items-center gap-1.5">
41
+ {floorLabel && (
42
+ <div className="font-medium text-[10px] text-muted-foreground uppercase tracking-wider">
43
+ {floorLabel}
44
+ </div>
45
+ )}
46
+ {zoneLabel && (
47
+ <div className="corner-smooth rounded-full border border-border/40 bg-background/80 px-3 py-1 font-medium text-sm shadow-elevation-3 backdrop-blur-xl">
48
+ {zoneLabel}
49
+ </div>
50
+ )}
51
+ {children}
52
+ </div>
53
+
54
+ <div className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2">
55
+ <div
56
+ className={cn(
57
+ 'rounded-full transition-all duration-150',
58
+ interact
59
+ ? 'h-4 w-4 border-2 border-emerald-400 bg-emerald-400/10'
60
+ : 'h-1.5 w-1.5 bg-white/80 shadow-[0_0_2px_rgba(0,0,0,0.6)]',
61
+ )}
62
+ />
63
+ </div>
64
+
65
+ <div className="absolute bottom-6 left-1/2 flex -translate-x-1/2 items-center gap-2">
66
+ {suspended ? (
67
+ <div className={pillClass}>
68
+ <span className="font-medium text-foreground">Click</span>
69
+ <span>or</span>
70
+ <kbd className={kbdClass}>P</kbd>
71
+ <span>to resume</span>
72
+ <span className="text-muted-foreground/60">·</span>
73
+ {exitContent}
74
+ </div>
75
+ ) : (
76
+ <>
77
+ <div className={pillClass}>
78
+ <kbd className={kbdClass}>P</kbd>
79
+ free cursor
80
+ </div>
81
+ {onExit ? (
82
+ <button
83
+ className={cn(pillClass, 'pointer-events-auto')}
84
+ onClick={onExit}
85
+ type="button"
86
+ >
87
+ {exitContent}
88
+ </button>
89
+ ) : (
90
+ <div className={pillClass}>{exitContent}</div>
91
+ )}
92
+ </>
93
+ )}
94
+ </div>
95
+
96
+ {interact && (
97
+ <div className="absolute top-1/2 left-1/2 mt-7 -translate-x-1/2 whitespace-nowrap">
98
+ <div className="corner-smooth flex items-center gap-1.5 rounded-full border border-border/40 bg-background/80 px-3 py-1 text-xs shadow-elevation-3 backdrop-blur-xl">
99
+ <kbd className="rounded border border-border/60 bg-white/10 px-1.5 py-0.5 font-mono text-[10px]">
100
+ E
101
+ </kbd>
102
+ <span className="text-muted-foreground">or click to</span>
103
+ <span className="font-medium">
104
+ {interact.verb} {interact.label}
105
+ </span>
106
+ </div>
107
+ </div>
108
+ )}
109
+ </div>
110
+ )
111
+ }
@@ -9,6 +9,8 @@ import { useViewer } from '@pascal-app/viewer'
9
9
  import { useThree } from '@react-three/fiber'
10
10
  import { useEffect, useRef } from 'react'
11
11
  import { Plane, Raycaster, Vector2, Vector3 } from 'three'
12
+ import { getPlacementSurface } from '../lib/active-placement-surface'
13
+ import { resolveTerrainGroundHit } from '../lib/ground-surface'
12
14
 
13
15
  /**
14
16
  * Custom grid events hook that uses manual raycasting instead of mesh events.
@@ -38,6 +40,28 @@ export function useGridEvents(gridY: number) {
38
40
  // Update raycaster
39
41
  raycaster.current.setFromCamera(pointer.current, camera)
40
42
 
43
+ // Sculpted ground wins over the plane, but only while the plane IS the
44
+ // ground (see `isSiteGroundPlane`): a plane riding a storey base or a slab
45
+ // top is a real flat surface and must stay planar. The march is what removes
46
+ // the perspective skew — a ray intersected at the datum instead of at the
47
+ // hillside reports an XZ that slides along the ray, so every tool consuming
48
+ // `position`/`localPosition` was placing short of the cursor on a slope.
49
+ //
50
+ // The plane's own height is the query height, not the `gridY` argument: this
51
+ // effect is keyed on `[camera, gl]` (re-attaching listeners mid-drag would
52
+ // drop the gesture), so the argument in this closure is the value from mount.
53
+ // `constant = -gridY` by construction in the effect above.
54
+ const { origin, direction } = raycaster.current.ray
55
+ const fixedConstructionPlane = getPlacementSurface()?.projection === 'fixed-plane'
56
+ const hit = fixedConstructionPlane
57
+ ? null
58
+ : resolveTerrainGroundHit(
59
+ [origin.x, origin.y, origin.z],
60
+ [direction.x, direction.y, direction.z],
61
+ -groundPlane.current.constant,
62
+ )
63
+ if (hit) return intersectionPoint.current.set(hit.x, hit.y, hit.z).clone()
64
+
41
65
  // Intersect with ground plane
42
66
  if (raycaster.current.ray.intersectPlane(groundPlane.current, intersectionPoint.current)) {
43
67
  return intersectionPoint.current.clone()
@@ -10,6 +10,11 @@ import {
10
10
  import { useViewer } from '@pascal-app/viewer'
11
11
  import { useEffect } from 'react'
12
12
  import { Vector3 } from 'three'
13
+ import {
14
+ cutSelectionToEditorClipboard,
15
+ deleteSelection,
16
+ pasteSelectionAndPickUp,
17
+ } from '../components/editor/group-actions'
13
18
  import {
14
19
  classifyParticipant,
15
20
  collectParticipants,
@@ -24,12 +29,11 @@ import { toggleDoorOpenState } from '../lib/door-interaction'
24
29
  import { guideEmitter } from '../lib/guide-events'
25
30
  import { runRedo, runUndo } from '../lib/history'
26
31
  import { isActive } from '../lib/interaction/scope'
27
- import {
28
- copySelectedNodesToEditorClipboard,
29
- pasteEditorClipboardToLevel,
30
- } from '../lib/scene-clipboard'
31
- import { emitDeleteSFX, sfxEmitter } from '../lib/sfx-bus'
32
+ import { copySelectedNodesToEditorClipboard } from '../lib/scene-clipboard'
33
+ import { sfxEmitter } from '../lib/sfx-bus'
34
+ import { activeSiteNode, clampBrushRadius } from '../lib/terrain-sculpt'
32
35
  import { toggleWindowOpenState } from '../lib/window-interaction'
36
+ import useDeleteConfirmation from '../store/use-delete-confirmation'
33
37
  import useEditor, { getActiveContinuationContext, getActiveSnapContext } from '../store/use-editor'
34
38
  import useInteractionScope, { getMovingNode } from '../store/use-interaction-scope'
35
39
 
@@ -210,6 +214,10 @@ export const useKeyboard = ({
210
214
  return
211
215
  }
212
216
 
217
+ if (useDeleteConfirmation.getState().request) {
218
+ return
219
+ }
220
+
213
221
  if (e.key === 'Shift' && !e.repeat && useEditor.getState().mode === 'material-paint') {
214
222
  // In paint mode Shift cycles the application scope (this surface →
215
223
  // whole item / all matching / room) — the paint-mode analogue of the
@@ -220,6 +228,34 @@ export const useKeyboard = ({
220
228
  return
221
229
  }
222
230
 
231
+ // Brush size, on the keys every sculpting tool in the industry uses. Gated
232
+ // on sculpt mode so `[`/`]` stay free everywhere else. Key-repeat is
233
+ // allowed (unlike the cycles above) because holding to resize is the
234
+ // expected feel, and the step is multiplicative so one press is a
235
+ // proportional change at both the floor and 20 m rather than 40× coarser at
236
+ // the bottom of the range. The range comes from `brushRadiusRange` so this
237
+ // and the panel's slider cannot disagree about it — and so the low end
238
+ // tracks the field's sample spacing, below which a dab paints nothing.
239
+ if (
240
+ (e.key === '[' || e.key === ']') &&
241
+ !e.metaKey &&
242
+ !e.ctrlKey &&
243
+ useEditor.getState().mode === 'terrain-sculpt'
244
+ ) {
245
+ e.preventDefault()
246
+ const { terrainBrush, setTerrainBrush } = useEditor.getState()
247
+ const factor = e.key === ']' ? 1.25 : 1 / 1.25
248
+ const radius = clampBrushRadius(
249
+ activeSiteNode(),
250
+ Math.round(terrainBrush.radius * factor * 10) / 10,
251
+ )
252
+ if (radius !== terrainBrush.radius) {
253
+ setTerrainBrush({ radius })
254
+ sfxEmitter.emit('sfx:grid-snap')
255
+ }
256
+ return
257
+ }
258
+
223
259
  if (e.key === 'Shift' && !e.repeat && isSnappingCycleContext()) {
224
260
  // Cycle the global snapping mode (grid → lines → angles → off).
225
261
  // `'off'` is the snap bypass now, so Shift no longer holds-to-bypass.
@@ -357,17 +393,24 @@ export const useKeyboard = ({
357
393
  useEditor.getState().setPhase('structure')
358
394
  useEditor.getState().setStructureLayer('elements')
359
395
  useEditor.getState().setMode('material-paint')
396
+ } else if (e.key === 'g' && !e.metaKey && !e.ctrlKey) {
397
+ if (isVersionPreviewMode) return
398
+ e.preventDefault()
399
+ // G for ground. No `setPhase` — `setMode` moves to the site phase itself,
400
+ // and doing it here would set the phase twice with a mode reset between.
401
+ useEditor.getState().setMode('terrain-sculpt')
360
402
  } else if (e.key === 'c' && (e.metaKey || e.ctrlKey) && !e.shiftKey) {
361
403
  if (isVersionPreviewMode) return
362
404
  e.preventDefault()
363
405
  copySelectedNodesToEditorClipboard()
406
+ } else if (e.key === 'x' && (e.metaKey || e.ctrlKey) && !e.shiftKey) {
407
+ if (isVersionPreviewMode) return
408
+ e.preventDefault()
409
+ cutSelectionToEditorClipboard()
364
410
  } else if (e.key === 'v' && (e.metaKey || e.ctrlKey) && !e.shiftKey) {
365
411
  if (isVersionPreviewMode) return
366
412
  e.preventDefault()
367
- const result = pasteEditorClipboardToLevel()
368
- if (result?.pastedIds.length) {
369
- sfxEmitter.emit('sfx:item-place')
370
- }
413
+ void pasteSelectionAndPickUp()
371
414
  } else if (e.key.toLowerCase() === 'z' && e.shiftKey && (e.metaKey || e.ctrlKey)) {
372
415
  if (isVersionPreviewMode) return
373
416
  e.preventDefault()
@@ -621,27 +664,7 @@ export const useKeyboard = ({
621
664
  }
622
665
  }
623
666
 
624
- const selectedNodeIds = useViewer.getState().selection.selectedIds as AnyNodeId[]
625
-
626
- if (selectedNodeIds.length > 0) {
627
- // Guard against accidental bulk deletion (e.g. box-select all + Delete)
628
- const BULK_DELETE_THRESHOLD = 10
629
- if (selectedNodeIds.length >= BULK_DELETE_THRESHOLD) {
630
- const confirmed = window.confirm(
631
- `Delete ${selectedNodeIds.length} selected elements? This cannot be undone if the undo history is exhausted.`,
632
- )
633
- if (!confirmed) return
634
- }
635
-
636
- // Play appropriate SFX based on what's being deleted
637
- if (selectedNodeIds.length === 1) {
638
- const node = useScene.getState().nodes[selectedNodeIds[0]!]
639
- emitDeleteSFX(node?.type)
640
- } else {
641
- sfxEmitter.emit('sfx:structure-delete')
642
- }
643
-
644
- useScene.getState().deleteNodes(selectedNodeIds)
667
+ if (deleteSelection()) {
645
668
  return
646
669
  }
647
670
 
package/src/index.tsx CHANGED
@@ -2,7 +2,21 @@
2
2
  // own shells on top of `@pascal-app/editor` (community-app, embedders)
3
3
  // don't have to learn three separate package imports. The canonical
4
4
  // definitions still live in `@pascal-app/core` / `@pascal-app/viewer`.
5
- export { useScene } from '@pascal-app/core'
5
+ export {
6
+ type ApplySceneSnapshotOptions,
7
+ acquireSceneReadOnlyLease,
8
+ applyScenePatch,
9
+ applySceneSnapshot,
10
+ type SceneCommit,
11
+ type SceneCommitListener,
12
+ type SceneCommitOrigin,
13
+ type SceneMaterialPatch,
14
+ type SceneNodePatch,
15
+ type ScenePatch,
16
+ type SceneSnapshot,
17
+ subscribeSceneCommits,
18
+ useScene,
19
+ } from '@pascal-app/core'
6
20
  export { useViewer } from '@pascal-app/viewer'
7
21
  export type { EditorProps } from './components/editor'
8
22
  export { default as Editor } from './components/editor'
@@ -12,6 +26,8 @@ export { default as Editor } from './components/editor'
12
26
  // surface uses the shorter, shell-friendly names from the unified
13
27
  // preset-system spec.
14
28
  export { BakeExporter } from './components/editor/bake-exporter'
29
+ export { BakeThumbnail } from './components/editor/bake-thumbnail'
30
+ export { FirstPersonControls } from './components/editor/first-person-controls'
15
31
  export { FloatingActionMenu as FloatingMenu } from './components/editor/floating-action-menu'
16
32
  // Embed surface — the editor's real in-canvas affordances, so a host can mount
17
33
  // authentic selection handles, interactive build tools, and the mover on top
@@ -66,6 +82,13 @@ export {
66
82
  type SnapshotCameraData,
67
83
  ThumbnailGenerator,
68
84
  } from './components/editor/thumbnail-generator'
85
+ export { useFloorplanRender } from './components/editor-2d/floorplan-render-context'
86
+ export { FloorplanDimensionRenderer } from './components/editor-2d/renderers/floorplan-dimension-renderer'
87
+ export { FloorplanGeometryRenderer } from './components/editor-2d/renderers/floorplan-geometry-renderer'
88
+ export {
89
+ FloorplanNodePreview,
90
+ type FloorplanNodePreviewProps,
91
+ } from './components/editor-2d/renderers/floorplan-placement-preview-layer'
69
92
  // SVG path builders for arc / annular-sector / arrow-head shapes —
70
93
  // inlined into `kind: 'path'` / `kind: 'polygon'` primitives by curved
71
94
  // stair rendering in `nodes/src/stair/floorplan.ts`.
@@ -113,7 +136,21 @@ export { CursorSphere } from './components/tools/shared/cursor-sphere'
113
136
  export { DragBoundingBox } from './components/tools/shared/drag-bounding-box'
114
137
  export { getFloorStackPreviewPosition } from './components/tools/shared/floor-stack-preview'
115
138
  export { useFreshPlacementVisibility } from './components/tools/shared/fresh-placement-visibility'
139
+ export {
140
+ type HorizontalConstructionPlane,
141
+ publishHorizontalConstructionPlane,
142
+ resampleTerrainConstructionPlane,
143
+ resolveEventConstructionPlane,
144
+ resolveLevelConstructionPlane,
145
+ } from './components/tools/shared/horizontal-construction-plane'
116
146
  export { PlacementBox } from './components/tools/shared/placement-box'
147
+ // Pointer-decided support surface (deck top vs floor underneath) — the
148
+ // draw tools (wall / fence) ride their grid plane and commit cap on it.
149
+ export {
150
+ type PointerSupportSurface,
151
+ resolvePointerSupportElevation,
152
+ resolvePointerSupportSurface,
153
+ } from './components/tools/shared/pointer-support-cap'
117
154
  // Phase 5 Stage D — PolygonEditor for slab/ceiling boundary + hole editors.
118
155
  export {
119
156
  PolygonEditor,
@@ -148,7 +185,7 @@ export {
148
185
  DEFAULT_STAIR_TYPE,
149
186
  DEFAULT_STAIR_WIDTH,
150
187
  } from './components/tools/stair/stair-defaults'
151
- export { ToolManager } from './components/tools/tool-manager'
188
+ export { preloadRegistryToolModules, ToolManager } from './components/tools/tool-manager'
152
189
  export {
153
190
  chainEndJoinsExistingWall,
154
191
  createWallOnCurrentLevel,
@@ -182,12 +219,20 @@ export {
182
219
  } from './components/ui/action-menu/view-toggles'
183
220
  export { useCommandPalette } from './components/ui/command-palette'
184
221
  export { ActionButton, ActionGroup } from './components/ui/controls/action-button'
185
- export { MaterialPaintPanel } from './components/ui/controls/material-paint-panel'
186
- export { MaterialPicker } from './components/ui/controls/material-picker'
222
+ export {
223
+ MaterialPaintPanel,
224
+ type MaterialPaintPanelProps,
225
+ } from './components/ui/controls/material-paint-panel'
226
+ export {
227
+ MaterialPicker,
228
+ type MaterialPickerProps,
229
+ type MaterialSourceFilter,
230
+ } from './components/ui/controls/material-picker'
187
231
  export { MetricControl } from './components/ui/controls/metric-control'
188
232
  export { PanelSection } from './components/ui/controls/panel-section'
189
233
  export { SegmentedControl } from './components/ui/controls/segmented-control'
190
234
  export { SliderControl } from './components/ui/controls/slider-control'
235
+ export { TerrainSculptPanel } from './components/ui/controls/terrain-sculpt-panel'
191
236
  export { ToggleControl } from './components/ui/controls/toggle-control'
192
237
  export { FloatingLevelSelector } from './components/ui/floating-level-selector'
193
238
  export { CATALOG_ITEMS } from './components/ui/item-catalog/catalog-items'
@@ -229,6 +274,19 @@ export {
229
274
  SnapTargetBadge,
230
275
  SnapTargetIcon,
231
276
  } from './components/ui/snap-target-badge'
277
+ export {
278
+ ViewerControlsBar,
279
+ type ViewerControlsBarProps,
280
+ } from './components/viewer/viewer-controls-bar'
281
+ export {
282
+ ViewerSceneHeader,
283
+ type ViewerSceneHeaderProps,
284
+ } from './components/viewer/viewer-scene-header'
285
+ export {
286
+ WalkthroughHud,
287
+ type WalkthroughHudProps,
288
+ type WalkthroughInteract,
289
+ } from './components/walkthrough-hud'
232
290
  export type { SaveStatus } from './hooks/use-auto-save'
233
291
  // useDragAction is the React-side glue for the registry's DragAction
234
292
  // primitive. Public so registry-driven kinds (Phase 5+ Stage D ports)
@@ -236,6 +294,7 @@ export type { SaveStatus } from './hooks/use-auto-save'
236
294
  export { type UseDragActionArgs, useDragAction } from './hooks/use-drag-action'
237
295
  // Phase 5 Stage D — extras for kind-owned placement tools (FenceTool etc.).
238
296
  export { markToolCancelConsumed } from './hooks/use-keyboard'
297
+ export { useReducedMotion } from './hooks/use-reduced-motion'
239
298
  export { type Selection, useSelection } from './hooks/use-selection'
240
299
  export {
241
300
  clearPlacementSurface,
@@ -259,6 +318,17 @@ export {
259
318
  continuationContextOf,
260
319
  nextContinuation,
261
320
  } from './lib/continuation'
321
+ export {
322
+ clearStructuralElevationGuide,
323
+ collectElevationSnapTargets,
324
+ ELEVATION_ALIGNMENT_THRESHOLD_M,
325
+ type ElevationGuideSource,
326
+ type ElevationSnapMatch,
327
+ type ElevationSnapTarget,
328
+ publishStructuralElevationGuide,
329
+ resolveElevationSnapMatch,
330
+ resolveStructuralElevationSnap,
331
+ } from './lib/elevation-guides'
262
332
  export {
263
333
  resolveCurrentBuildingId,
264
334
  resolveElevatorNodeSupportY,
@@ -285,14 +355,50 @@ export {
285
355
  type FloorplanStairSegmentEntry,
286
356
  getFloorplanWallThickness,
287
357
  } from './lib/floorplan'
358
+ export type {
359
+ FloorplanAnnotationCategory,
360
+ FloorplanAnnotationVisibility,
361
+ } from './lib/floorplan/annotation-visibility'
362
+ export {
363
+ createFloorplanContextExtensions,
364
+ FLOORPLAN_CONTEXT_EXTENSION_KEY,
365
+ FLOORPLAN_GEOMETRY_METADATA_KEY,
366
+ FLOORPLAN_NODE_EXTENSION_KEY,
367
+ type FloorplanAnnotationRole,
368
+ type FloorplanMetricNotation,
369
+ type FloorplanNodeExtension,
370
+ type FloorplanRenderPurpose,
371
+ type FloorplanSchedule,
372
+ type FloorplanToolContext,
373
+ type FloorplanToolMode,
374
+ floorplanGeometryMetadata,
375
+ getFloorplanNodeExtension,
376
+ readFloorplanContext,
377
+ readFloorplanGeometryMetadata,
378
+ readFloorplanMetricNotationOverride,
379
+ withFloorplanGeometryMetadata,
380
+ } from './lib/floorplan/floorplan-extension'
381
+ export {
382
+ DEFAULT_FLOORPLAN_MODE,
383
+ FLOORPLAN_MODES,
384
+ type FloorplanMode,
385
+ isFloorplanToolAvailableInMode,
386
+ } from './lib/floorplan/floorplan-mode'
288
387
  export { commitFreshPlacementSubtree } from './lib/fresh-planar-placement'
289
388
  export { exportSceneToGlb } from './lib/glb-export'
389
+ export {
390
+ type HistoryCommandDelegate,
391
+ installHistoryCommandDelegate,
392
+ runRedo,
393
+ runUndo,
394
+ } from './lib/history'
290
395
  export {
291
396
  boundaryReshapeScope,
292
397
  curveReshapeScope,
293
398
  endpointReshapeScope,
294
399
  holeEditScope,
295
400
  movingNodeOf,
401
+ scopeNodeId,
296
402
  } from './lib/interaction/scope'
297
403
  export {
298
404
  buildResetSurfaceMaterialUpdates,
@@ -366,6 +472,7 @@ export type { SceneGraph } from './lib/scene'
366
472
  export { applySceneGraphToEditor } from './lib/scene'
367
473
  export { movementSfxStepKey } from './lib/sfx/movement-tick'
368
474
  export { triggerSFX } from './lib/sfx-bus'
475
+ export { playSFX, type SFXName, type SFXPlaybackOptions } from './lib/sfx-player'
369
476
  export {
370
477
  clearSlabSnapFeedback,
371
478
  resolveSlabEdgeBandSnap,
@@ -392,6 +499,13 @@ export {
392
499
  type SurfacePlanSnapInput,
393
500
  type SurfacePlanSnapResult,
394
501
  } from './lib/surface-plan-snap'
502
+ export {
503
+ fieldExtentForSite,
504
+ flattenSite,
505
+ resetSiteTerrain,
506
+ resolveFlattenTarget,
507
+ sculptFieldForSite,
508
+ } from './lib/terrain-sculpt'
395
509
  // `cn` (twMerge + clsx) — used by kind-owned panels in `@pascal-app/
396
510
  // nodes` so they don't need their own copy / their own tailwind-merge
397
511
  // dependency.
@@ -407,6 +521,10 @@ export {
407
521
  export { default as useAlignmentGuides } from './store/use-alignment-guides'
408
522
  export { default as useAudio } from './store/use-audio'
409
523
  export { type CommandAction, useCommandRegistry } from './store/use-command-registry'
524
+ export {
525
+ DRAWING_TYPE_OPTIONS,
526
+ default as useDrawingView,
527
+ } from './store/use-drawing-view'
410
528
  export type {
411
529
  CaptureMode,
412
530
  FloorplanSelectionTool,
@@ -429,6 +547,10 @@ export {
429
547
  } from './store/use-editor'
430
548
  export { default as useFacingPose, type FacingPose } from './store/use-facing-pose'
431
549
  export { default as useFenceCurveDraft } from './store/use-fence-curve-draft'
550
+ export { type FirstPersonHudState, useFirstPersonHud } from './store/use-first-person-hud'
551
+ export { default as useFloorplanAnnotationVisibility } from './store/use-floorplan-annotation-visibility'
552
+ export { useFloorplanDraftPreview } from './store/use-floorplan-draft-preview'
553
+ export { default as useFloorplanMode } from './store/use-floorplan-mode'
432
554
  export {
433
555
  default as useInteractionScope,
434
556
  getEditingHole,
@@ -466,6 +588,13 @@ export {
466
588
  type PaletteViewProps,
467
589
  usePaletteViewRegistry,
468
590
  } from './store/use-palette-view-registry'
591
+ export {
592
+ type PathDraftKind,
593
+ type PathDraftParameter,
594
+ type PathDraftParameters,
595
+ type PathDraftPoint,
596
+ usePathDraftPreview,
597
+ } from './store/use-path-draft-preview'
469
598
  export { default as usePlacementPreview } from './store/use-placement-preview'
470
599
  export {
471
600
  activateQuickMeasurementHudSource,
@@ -477,6 +606,10 @@ export {
477
606
  useQuickMeasurementHud,
478
607
  } from './store/use-quick-measurement-hud'
479
608
  export { default as useSegmentDraftChain } from './store/use-segment-draft-chain'
609
+ export {
610
+ type StairPreviewPoint,
611
+ useStairBuildPreview,
612
+ } from './store/use-stair-build-preview'
480
613
  export { useUploadStore } from './store/use-upload'
481
614
  export { useWallMoveGhosts, type WallMoveGhostBridge } from './store/use-wall-move-ghosts'
482
615
  export {
@@ -0,0 +1,24 @@
1
+ import { afterEach, describe, expect, test } from 'bun:test'
2
+ import { Vector3 } from 'three'
3
+ import {
4
+ clearPlacementSurface,
5
+ getPlacementSurface,
6
+ publishPlacementSurface,
7
+ } from './active-placement-surface'
8
+
9
+ describe('active placement surface', () => {
10
+ afterEach(() => clearPlacementSurface())
11
+
12
+ test('marks a frozen construction plane so terrain raycasting can bypass it', () => {
13
+ publishPlacementSurface(new Vector3(1, 0, 2), new Vector3(0, 1, 0), 'fixed-plane')
14
+
15
+ expect(getPlacementSurface()?.projection).toBe('fixed-plane')
16
+ })
17
+
18
+ test('ordinary surface publishes reset the projection mode', () => {
19
+ publishPlacementSurface(new Vector3(), new Vector3(0, 1, 0), 'fixed-plane')
20
+ publishPlacementSurface(new Vector3(), new Vector3(0, 1, 0))
21
+
22
+ expect(getPlacementSurface()?.projection).toBe('surface')
23
+ })
24
+ })
@@ -12,17 +12,24 @@ import { Vector3 } from 'three'
12
12
  export type PlacementSurface = {
13
13
  point: Vector3
14
14
  normal: Vector3
15
+ projection: 'surface' | 'fixed-plane'
15
16
  }
16
17
 
17
18
  const surface: PlacementSurface = {
18
19
  point: new Vector3(),
19
20
  normal: new Vector3(0, 1, 0),
21
+ projection: 'surface',
20
22
  }
21
23
  let active = false
22
24
 
23
- export function publishPlacementSurface(point: Vector3, normal: Vector3): void {
25
+ export function publishPlacementSurface(
26
+ point: Vector3,
27
+ normal: Vector3,
28
+ projection: PlacementSurface['projection'] = 'surface',
29
+ ): void {
24
30
  surface.point.copy(point)
25
31
  surface.normal.copy(normal)
32
+ surface.projection = projection
26
33
  active = true
27
34
  }
28
35