@pascal-app/editor 1.0.0 → 1.0.3

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 (177) hide show
  1. package/package.json +12 -6
  2. package/src/components/editor/bake-exporter.tsx +4 -0
  3. package/src/components/editor/bake-thumbnail.tsx +4 -1
  4. package/src/components/editor/custom-camera-controls.tsx +22 -74
  5. package/src/components/editor/editor-layout-mobile.tsx +1 -0
  6. package/src/components/editor/floating-action-menu.tsx +14 -1
  7. package/src/components/editor/floorplan-panel.tsx +233 -166
  8. package/src/components/editor/grid.tsx +31 -7
  9. package/src/components/editor/group-actions.ts +5 -9
  10. package/src/components/editor/group-floating-action-menu.tsx +19 -9
  11. package/src/components/editor/group-move-3d.ts +4 -6
  12. package/src/components/editor/group-rotate-handle.tsx +81 -35
  13. package/src/components/editor/group-selection-box-3d.tsx +25 -17
  14. package/src/components/editor/group-transform-shared.test.ts +126 -1
  15. package/src/components/editor/group-transform-shared.ts +79 -73
  16. package/src/components/editor/handles/handle-arrow.tsx +72 -2
  17. package/src/components/editor/handles/handle-placement.test.ts +111 -0
  18. package/src/components/editor/handles/handle-placement.ts +69 -0
  19. package/src/components/editor/handles/linear-resize-drag.ts +13 -2
  20. package/src/components/editor/handles/use-handle-drag.ts +80 -8
  21. package/src/components/editor/index.tsx +36 -13
  22. package/src/components/editor/node-action-menu.tsx +2 -0
  23. package/src/components/editor/node-arrow-handles.tsx +162 -69
  24. package/src/components/editor/registry-action-contributions.tsx +53 -0
  25. package/src/components/editor/selection-manager.tsx +49 -0
  26. package/src/components/editor/thumbnail-generator.tsx +11 -2
  27. package/src/components/editor/wall-move-side-handles.tsx +71 -21
  28. package/src/components/editor/wall-opening-highlights.tsx +2 -3
  29. package/src/components/editor-2d/floorplan-group-move.tsx +17 -24
  30. package/src/components/editor-2d/floorplan-registered-tool-layer.tsx +33 -12
  31. package/src/components/editor-2d/floorplan-registry-action-menu.tsx +12 -1
  32. package/src/components/editor-2d/floorplan-registry-move-overlay.tsx +154 -49
  33. package/src/components/editor-2d/renderers/floorplan-draft-wall-measurement.tsx +166 -0
  34. package/src/components/editor-2d/renderers/floorplan-registry-layer.test.ts +49 -0
  35. package/src/components/editor-2d/renderers/floorplan-registry-layer.tsx +113 -24
  36. package/src/components/procedural-items/inspector.tsx +223 -0
  37. package/src/components/systems/ceiling/ceiling-system.tsx +3 -1
  38. package/src/components/systems/zone/zone-system.tsx +8 -4
  39. package/src/components/tools/__fixtures__/hosting.ts +10 -0
  40. package/src/components/tools/item/item-surface-placement.test.ts +169 -0
  41. package/src/components/tools/item/item-surface-strategy-resolver.test.ts +204 -0
  42. package/src/components/tools/item/placement-strategies.ts +212 -200
  43. package/src/components/tools/item/placement-types.ts +4 -0
  44. package/src/components/tools/item/shelf-surface-strategy-resolver.test.ts +257 -0
  45. package/src/components/tools/item/use-draft-node.ts +136 -47
  46. package/src/components/tools/item/use-placement-coordinator.tsx +391 -157
  47. package/src/components/tools/registry/item-surface-child-frame.test.ts +90 -0
  48. package/src/components/tools/registry/item-surface-move.test.ts +509 -0
  49. package/src/components/tools/registry/item-surface-move.ts +463 -0
  50. package/src/components/tools/registry/item-surface-resolver.test.ts +257 -0
  51. package/src/components/tools/registry/move-registry-node-tool.tsx +237 -78
  52. package/src/components/tools/shared/affordance-dispatch.test.ts +24 -0
  53. package/src/components/tools/shared/affordance-dispatch.ts +13 -0
  54. package/src/components/tools/shared/draft-measurement-label.tsx +38 -0
  55. package/src/components/tools/shared/pointer-support-cap.test.ts +27 -0
  56. package/src/components/tools/shared/pointer-support-cap.ts +27 -13
  57. package/src/components/tools/shared/shelf-stickiness.ts +54 -0
  58. package/src/components/tools/shared/surface-hit.test.ts +56 -0
  59. package/src/components/tools/shared/surface-hit.ts +23 -0
  60. package/src/components/tools/shared/surface-rejection.test.tsx +107 -0
  61. package/src/components/tools/shared/surface-rejection.tsx +43 -0
  62. package/src/components/tools/tool-manager.tsx +36 -3
  63. package/src/components/tools/zone/zone-tool.tsx +13 -4
  64. package/src/components/ui/command-palette/editor-commands.tsx +54 -1
  65. package/src/components/ui/controls/material-paint-panel.tsx +6 -67
  66. package/src/components/ui/controls/material-picker.tsx +5 -67
  67. package/src/components/ui/controls/metric-control.tsx +29 -13
  68. package/src/components/ui/controls/slider-control.tsx +87 -35
  69. package/src/components/ui/floating-level-selector.tsx +81 -5
  70. package/src/components/ui/helpers/helper-manager.tsx +24 -6
  71. package/src/components/ui/helpers/registered-tool-helper.tsx +2 -32
  72. package/src/components/ui/item-catalog/item-catalog.tsx +5 -30
  73. package/src/components/ui/panels/multi-height-mode.tsx +27 -21
  74. package/src/components/ui/panels/multi-parametric-inspector.tsx +6 -3
  75. package/src/components/ui/panels/parametric-field-control.tsx +1 -1
  76. package/src/components/ui/panels/parametric-inspector.tsx +9 -17
  77. package/src/components/ui/panels/reference-panel.tsx +3 -3
  78. package/src/components/ui/primitives/checkbox.tsx +34 -0
  79. package/src/components/ui/primitives/shortcut-token.tsx +4 -0
  80. package/src/components/ui/sidebar/panels/settings-panel/audio-settings-dialog.tsx +2 -2
  81. package/src/components/ui/sidebar/panels/settings-panel/index.tsx +669 -336
  82. package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +2 -2
  83. package/src/components/ui/sidebar/panels/settings-panel/print-export-button.test.ts +31 -4
  84. package/src/components/ui/sidebar/panels/settings-panel/print-export-button.tsx +30 -11
  85. package/src/components/ui/sidebar/panels/site-panel/building-tree-node.tsx +8 -1
  86. package/src/components/ui/sidebar/panels/site-panel/index.tsx +106 -4
  87. package/src/components/ui/sidebar/panels/site-panel/item-tree-node.tsx +2 -7
  88. package/src/components/ui/sidebar/panels/site-panel/shelf-tree-node.tsx +3 -8
  89. package/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +6 -1
  90. package/src/components/ui/sidebar/panels/site-panel/unit-tree-node.tsx +224 -0
  91. package/src/components/ui/sidebar/panels/site-panel/zone-membership-checkbox.tsx +22 -0
  92. package/src/components/ui/sidebar/panels/site-panel/zone-tree-node.tsx +24 -7
  93. package/src/components/ui/snap-target-badge.tsx +2 -0
  94. package/src/components/viewer/floorplan-preview-visibility.test.ts +35 -0
  95. package/src/components/viewer/floorplan-preview-visibility.ts +19 -0
  96. package/src/components/viewer/floorplan-preview.tsx +1 -12
  97. package/src/components/viewer/viewer-scene-header.tsx +3 -0
  98. package/src/components/viewer/viewer-units-panel.tsx +116 -0
  99. package/src/components/viewer-zone-system.tsx +7 -2
  100. package/src/hooks/use-auto-save.integration.test.tsx +197 -0
  101. package/src/hooks/use-auto-save.ts +21 -6
  102. package/src/hooks/use-ceiling-events.test.ts +25 -2
  103. package/src/hooks/use-ceiling-events.ts +7 -1
  104. package/src/hooks/use-grid-events.test.ts +2 -0
  105. package/src/hooks/use-grid-events.ts +16 -11
  106. package/src/hooks/use-keyboard.test.ts +7 -0
  107. package/src/hooks/use-keyboard.ts +43 -28
  108. package/src/index.tsx +68 -3
  109. package/src/lib/catalog-panel-model.ts +39 -0
  110. package/src/lib/catalog-selection.test.ts +27 -0
  111. package/src/lib/catalog-selection.ts +8 -0
  112. package/src/lib/contextual-help.test.ts +27 -1
  113. package/src/lib/contextual-help.ts +16 -9
  114. package/src/lib/export-texture-utils.test.ts +167 -0
  115. package/src/lib/export-texture-utils.ts +121 -0
  116. package/src/lib/export-user-data.test.ts +48 -0
  117. package/src/lib/export-user-data.ts +69 -0
  118. package/src/lib/floorplan/floorplan-export.test.ts +142 -2
  119. package/src/lib/floorplan/floorplan-export.tsx +9 -3
  120. package/src/lib/floorplan/floorplan-extension.ts +16 -0
  121. package/src/lib/floorplan/floorplan-readonly.ts +4 -0
  122. package/src/lib/fresh-planar-placement.ts +173 -33
  123. package/src/lib/glb-export-serialize.test.ts +280 -0
  124. package/src/lib/glb-export.test.ts +265 -16
  125. package/src/lib/glb-export.ts +197 -37
  126. package/src/lib/grid-frame.test.ts +19 -0
  127. package/src/lib/grid-frame.ts +18 -0
  128. package/src/lib/interaction/scope.ts +9 -1
  129. package/src/lib/keyboard-pan.test.ts +62 -0
  130. package/src/lib/keyboard-pan.ts +89 -0
  131. package/src/lib/level-duplication.test.ts +73 -0
  132. package/src/lib/level-duplication.ts +32 -2
  133. package/src/lib/linear-display.test.ts +59 -0
  134. package/src/lib/linear-display.ts +39 -0
  135. package/src/lib/local-guide-image.test.ts +24 -0
  136. package/src/lib/local-guide-image.ts +34 -1
  137. package/src/lib/material-catalog-model.ts +72 -0
  138. package/src/lib/material-paint-panel-model.ts +76 -0
  139. package/src/lib/node-action-movement.ts +21 -0
  140. package/src/lib/overlay-transform.test.ts +28 -0
  141. package/src/lib/overlay-transform.ts +12 -0
  142. package/src/lib/panel-rows.ts +75 -0
  143. package/src/lib/panel-tool-options.test.ts +34 -0
  144. package/src/lib/panel-tool-options.ts +46 -0
  145. package/src/lib/parametric-node-update.ts +37 -0
  146. package/src/lib/portable-export.test.ts +89 -0
  147. package/src/lib/portable-export.ts +84 -11
  148. package/src/lib/print-shell-compiler-manifold-core.ts +59 -10
  149. package/src/lib/print-shell-compiler-manifold-worker.ts +14 -1
  150. package/src/lib/print-shell-compiler-manifold.worker.ts +1 -1
  151. package/src/lib/print-shell-compiler-protocol.ts +8 -0
  152. package/src/lib/scene-clipboard.ts +1 -1
  153. package/src/lib/scene.test.ts +99 -0
  154. package/src/lib/scene.ts +21 -3
  155. package/src/lib/send-to-app.test.ts +108 -0
  156. package/src/lib/send-to-app.ts +121 -0
  157. package/src/lib/sfx/movement-tick.ts +37 -0
  158. package/src/lib/snapping-mode.test.ts +13 -2
  159. package/src/lib/snapping-mode.ts +11 -6
  160. package/src/lib/spatial-drag-plane.test.ts +53 -0
  161. package/src/lib/spatial-drag-plane.ts +19 -0
  162. package/src/lib/spatial-pointer-input.test.ts +72 -0
  163. package/src/lib/spatial-pointer-input.ts +62 -0
  164. package/src/lib/spatial-snap.test.ts +47 -0
  165. package/src/lib/surface-attachment.ts +77 -0
  166. package/src/lib/terrain-panel-model.ts +117 -0
  167. package/src/lib/units.ts +254 -0
  168. package/src/lib/usdz-export.ts +43 -16
  169. package/src/lib/use-linear-display.ts +8 -32
  170. package/src/lib/world-grid-snap.test.ts +48 -0
  171. package/src/lib/world-grid-snap.ts +9 -5
  172. package/src/store/rotation-snapping.test.ts +53 -0
  173. package/src/store/use-editor.tsx +10 -2
  174. package/src/store/use-floorplan-draft-preview.ts +10 -1
  175. package/src/store/use-handle-group.ts +4 -0
  176. package/src/store/use-interaction-scope.ts +155 -6
  177. package/src/store/use-send-to-app.ts +21 -0
package/package.json CHANGED
@@ -1,20 +1,21 @@
1
1
  {
2
2
  "name": "@pascal-app/editor",
3
- "version": "1.0.0",
3
+ "version": "1.0.3",
4
4
  "description": "Pascal building editor component",
5
5
  "license": "MIT",
6
6
  "type": "module",
7
7
  "exports": {
8
8
  ".": "./src/index.tsx",
9
- "./catalog": "./src/components/ui/item-catalog/catalog-items.tsx"
9
+ "./catalog": "./src/components/ui/item-catalog/catalog-items.tsx",
10
+ "./procedural-items": "./src/components/procedural-items/inspector.tsx"
10
11
  },
11
12
  "scripts": {
12
13
  "check-types": "tsgo --noEmit",
13
14
  "test": "bun test src"
14
15
  },
15
16
  "peerDependencies": {
16
- "@pascal-app/core": "^1.0.0",
17
- "@pascal-app/viewer": "^1.0.0",
17
+ "@pascal-app/core": "^1.0.3",
18
+ "@pascal-app/viewer": "^1.0.3",
18
19
  "@react-three/drei": "^10",
19
20
  "@react-three/fiber": "^9",
20
21
  "next": ">=15",
@@ -61,8 +62,8 @@
61
62
  "zustand": "^5.0.11"
62
63
  },
63
64
  "devDependencies": {
64
- "@pascal-app/core": "^1.0.0",
65
- "@pascal-app/viewer": "^1.0.0",
65
+ "@pascal-app/core": "^1.0.3",
66
+ "@pascal-app/viewer": "^1.0.3",
66
67
  "@pascal/typescript-config": "*",
67
68
  "@react-three/test-renderer": "^9.1.0",
68
69
  "@types/blob-stream": "^0.1.33",
@@ -74,5 +75,10 @@
74
75
  "@types/three": "^0.184.0",
75
76
  "fast-xml-parser": "^5.4.2",
76
77
  "typescript": "6.0.3"
78
+ },
79
+ "repository": {
80
+ "type": "git",
81
+ "url": "git+https://github.com/pascalorg/editor.git",
82
+ "directory": "packages/editor"
77
83
  }
78
84
  }
@@ -32,6 +32,10 @@ export function BakeExporter({
32
32
  const buffer = await exportSceneToGlb(sceneGroup, useScene.getState().nodes, {
33
33
  textures: 'reference',
34
34
  purpose: 'viewer',
35
+ // The bake worker owns the deadline (BAKE_CAPTURE_TIMEOUT_MS); under
36
+ // SwiftShader a moderate scene can legitimately outlast the
37
+ // interactive default.
38
+ timeoutMs: Number.POSITIVE_INFINITY,
35
39
  })
36
40
  onComplete(buffer)
37
41
  } catch (err) {
@@ -6,6 +6,7 @@ import {
6
6
  GRID_LAYER,
7
7
  heroCameraPose,
8
8
  temporarilyHideNodeTypes,
9
+ useSceneAtmosphere,
9
10
  useViewer,
10
11
  } from '@pascal-app/viewer'
11
12
  import { useThree } from '@react-three/fiber'
@@ -25,6 +26,7 @@ export function BakeThumbnail({
25
26
  }) {
26
27
  const renderer = useThree((state) => state.gl)
27
28
  const scene = useThree((state) => state.scene)
29
+ const atmosphere = useSceneAtmosphere()
28
30
  const doneRef = useRef(false)
29
31
 
30
32
  useEffect(() => {
@@ -61,6 +63,7 @@ export function BakeThumbnail({
61
63
  renderer: renderer as unknown as WebGPURenderer,
62
64
  scene,
63
65
  camera,
66
+ atmosphere,
64
67
  })
65
68
  if (!pipeline) {
66
69
  onError('thumbnail pipeline failed to build')
@@ -89,7 +92,7 @@ export function BakeThumbnail({
89
92
  }
90
93
 
91
94
  void run()
92
- }, [active, onComplete, onError, renderer, scene])
95
+ }, [active, atmosphere, onComplete, onError, renderer, scene])
93
96
 
94
97
  return null
95
98
  }
@@ -31,6 +31,17 @@ import {
31
31
  withCameraPoseDistance,
32
32
  } from '../../lib/camera-pose'
33
33
  import { EDITOR_LAYER } from '../../lib/constants'
34
+ import {
35
+ acceptsKeyboardPan,
36
+ clearKeyboardPanKeys,
37
+ hasKeyboardPanInput,
38
+ isEditableKeyboardTarget,
39
+ isKeyboardPanKey,
40
+ type KeyboardPanState,
41
+ keyboardPanDirection,
42
+ keyboardPanSpeed,
43
+ setKeyboardPanKey,
44
+ } from '../../lib/keyboard-pan'
34
45
  import { editorOwnsOneFingerDrag } from '../../lib/touch-gesture-priority'
35
46
  import { publishCameraPose } from '../../store/camera-pose-store'
36
47
  import useEditor from '../../store/use-editor'
@@ -51,11 +62,11 @@ const tempTarget = new Vector3()
51
62
  const transitionFreezePosition = new Vector3()
52
63
  const transitionFreezeTarget = new Vector3()
53
64
  const keyboardPanSpherical = new Spherical()
65
+ // In 2D-only view the canvas is paused, so the floor plan drives WASD, orbit and
66
+ // top view itself (`floorplan-panel.tsx`) and the camera stands down.
67
+ const planOwnsNavigation = () => useEditor.getState().viewMode === '2d'
54
68
  const DEFAULT_MAX_POLAR_ANGLE = Math.PI / 2 - 0.1
55
69
  const DEBUG_MAX_POLAR_ANGLE = Math.PI - 0.05
56
- const KEYBOARD_PAN_VIEW_WIDTH_PER_SECOND = 0.65
57
- const KEYBOARD_PAN_MIN_SPEED = 2
58
- const KEYBOARD_PAN_MAX_SPEED = 55
59
70
  type CameraMode = ReturnType<typeof useViewer.getState>['cameraMode']
60
71
  type CameraPoseSnapshot = {
61
72
  mode: CameraMode
@@ -114,54 +125,6 @@ function freezeCameraControlTransition(control: CameraControlsImpl) {
114
125
  )
115
126
  }
116
127
 
117
- function isEditableKeyboardTarget(target: EventTarget | null) {
118
- return (
119
- target instanceof HTMLInputElement ||
120
- target instanceof HTMLTextAreaElement ||
121
- target instanceof HTMLSelectElement ||
122
- (target instanceof HTMLElement && target.isContentEditable)
123
- )
124
- }
125
-
126
- type KeyboardPanState = {
127
- forward: boolean
128
- backward: boolean
129
- left: boolean
130
- right: boolean
131
- }
132
-
133
- function setKeyboardPanKey(state: KeyboardPanState, code: string, pressed: boolean): boolean {
134
- if (code === 'KeyW') {
135
- const changed = state.forward !== pressed
136
- state.forward = pressed
137
- return changed
138
- }
139
- if (code === 'KeyS') {
140
- const changed = state.backward !== pressed
141
- state.backward = pressed
142
- return changed
143
- }
144
- if (code === 'KeyA') {
145
- const changed = state.left !== pressed
146
- state.left = pressed
147
- return changed
148
- }
149
- if (code === 'KeyD') {
150
- const changed = state.right !== pressed
151
- state.right = pressed
152
- return changed
153
- }
154
- return false
155
- }
156
-
157
- function isKeyboardPanKey(code: string): boolean {
158
- return code === 'KeyW' || code === 'KeyA' || code === 'KeyS' || code === 'KeyD'
159
- }
160
-
161
- function hasKeyboardPanInput(state: KeyboardPanState): boolean {
162
- return state.forward || state.backward || state.left || state.right
163
- }
164
-
165
128
  type CameraViewportSize = {
166
129
  width: number
167
130
  height: number
@@ -667,19 +630,14 @@ export const CustomCameraControls = ({ paused = false }: { paused?: boolean }) =
667
630
  }
668
631
  }
669
632
 
670
- const panKeys = keyboardPanKeys.current
671
- const horizontal = (panKeys.right ? 1 : 0) - (panKeys.left ? 1 : 0)
672
- const vertical = (panKeys.forward ? 1 : 0) - (panKeys.backward ? 1 : 0)
633
+ const { horizontal, vertical } = keyboardPanDirection(keyboardPanKeys.current)
673
634
  if (horizontal === 0 && vertical === 0) return
674
635
 
675
636
  const control = controls.current
676
637
 
677
638
  control.getSpherical(keyboardPanSpherical, false)
678
639
  const viewWidth = getCameraViewWidth(camera, keyboardPanSpherical.radius, viewportSize)
679
- const speed = Math.min(
680
- Math.max(viewWidth * KEYBOARD_PAN_VIEW_WIDTH_PER_SECOND, KEYBOARD_PAN_MIN_SPEED),
681
- KEYBOARD_PAN_MAX_SPEED,
682
- )
640
+ const speed = keyboardPanSpeed(viewWidth)
683
641
  const step = (speed * Math.min(delta, 0.05)) / Math.hypot(horizontal, vertical)
684
642
 
685
643
  if (horizontal !== 0) control.truck(horizontal * step, 0, true)
@@ -766,13 +724,6 @@ export const CustomCameraControls = ({ paused = false }: { paused?: boolean }) =
766
724
  let panPointerId: number | null = null
767
725
  let panPointerButton: number | null = null
768
726
 
769
- const clearKeyboardPanKeys = () => {
770
- keyboardPanKeys.current.forward = false
771
- keyboardPanKeys.current.backward = false
772
- keyboardPanKeys.current.left = false
773
- keyboardPanKeys.current.right = false
774
- }
775
-
776
727
  const setNavigationCursor = (cursor: 'grab' | 'grabbing') => {
777
728
  document.body.style.cursor = cursor
778
729
  gl.domElement.style.cursor = cursor
@@ -835,10 +786,7 @@ export const CustomCameraControls = ({ paused = false }: { paused?: boolean }) =
835
786
 
836
787
  const onKeyDown = (event: KeyboardEvent) => {
837
788
  if (isKeyboardPanKey(event.code)) {
838
- if (
839
- !(event.metaKey || event.ctrlKey || event.altKey) &&
840
- !isEditableKeyboardTarget(event.target)
841
- ) {
789
+ if (acceptsKeyboardPan(event) && !planOwnsNavigation()) {
842
790
  const changed = setKeyboardPanKey(keyboardPanKeys.current, event.code, true)
843
791
  if (changed) beginLocalCameraInteraction()
844
792
  event.preventDefault()
@@ -930,7 +878,7 @@ export const CustomCameraControls = ({ paused = false }: { paused?: boolean }) =
930
878
 
931
879
  const onBlur = () => {
932
880
  keyState.space = false
933
- clearKeyboardPanKeys()
881
+ clearKeyboardPanKeys(keyboardPanKeys.current)
934
882
  panPointerId = null
935
883
  panPointerButton = null
936
884
  clearNavigationCursor()
@@ -955,7 +903,7 @@ export const CustomCameraControls = ({ paused = false }: { paused?: boolean }) =
955
903
  window.removeEventListener('pointercancel', onPointerUp, true)
956
904
  window.removeEventListener('blur', onBlur)
957
905
  gl.domElement.removeEventListener('wheel', onWheel, true)
958
- clearKeyboardPanKeys()
906
+ clearKeyboardPanKeys(keyboardPanKeys.current)
959
907
  clearNavigationCursor()
960
908
  cameraDraggingLifecycle.end()
961
909
  }
@@ -1196,7 +1144,7 @@ export const CustomCameraControls = ({ paused = false }: { paused?: boolean }) =
1196
1144
  }
1197
1145
 
1198
1146
  const handleTopView = () => {
1199
- if (isFirstPersonMode || !controls.current) return
1147
+ if (isFirstPersonMode || !controls.current || planOwnsNavigation()) return
1200
1148
 
1201
1149
  const currentPolarAngle = controls.current.polarAngle
1202
1150
 
@@ -1208,7 +1156,7 @@ export const CustomCameraControls = ({ paused = false }: { paused?: boolean }) =
1208
1156
  }
1209
1157
 
1210
1158
  const handleOrbitCW = () => {
1211
- if (isFirstPersonMode || !controls.current) return
1159
+ if (isFirstPersonMode || !controls.current || planOwnsNavigation()) return
1212
1160
 
1213
1161
  const currentAzimuth = controls.current.azimuthAngle
1214
1162
  const currentPolar = controls.current.polarAngle
@@ -1220,7 +1168,7 @@ export const CustomCameraControls = ({ paused = false }: { paused?: boolean }) =
1220
1168
  }
1221
1169
 
1222
1170
  const handleOrbitCCW = () => {
1223
- if (isFirstPersonMode || !controls.current) return
1171
+ if (isFirstPersonMode || !controls.current || planOwnsNavigation()) return
1224
1172
 
1225
1173
  const currentAzimuth = controls.current.azimuthAngle
1226
1174
  const currentPolar = controls.current.polarAngle
@@ -236,6 +236,7 @@ export function EditorLayoutMobile({
236
236
  <div className="relative h-full w-full overflow-hidden">{viewerContent}</div>
237
237
  {overlays && (
238
238
  <div
239
+ data-viewer-bounds
239
240
  className="pointer-events-none absolute inset-0 z-30"
240
241
  style={{ transform: 'translateZ(0)' }}
241
242
  >
@@ -51,6 +51,7 @@ import {
51
51
  } from '../../lib/fresh-planar-placement'
52
52
  import { resolveFloatingActionMenuVisibility } from '../../lib/interaction/overlay-policy'
53
53
  import { curveReshapeScope, holeEditScope } from '../../lib/interaction/scope'
54
+ import { duplicateWithoutMove, registryMoveDisabled } from '../../lib/node-action-movement'
54
55
  import { playBlockedQuickActionFeedback } from '../../lib/quick-action-feedback'
55
56
  import { collectQuickActionNodeScope } from '../../lib/quick-action-nodes'
56
57
  import { duplicateRoofSubtree } from '../../lib/roof-duplication'
@@ -501,6 +502,16 @@ export function FloatingActionMenu() {
501
502
  if (!node?.parentId) return
502
503
  sfxEmitter.emit('sfx:item-pick')
503
504
 
505
+ if (registryMoveDisabled(node)) {
506
+ try {
507
+ const id = duplicateWithoutMove(node)
508
+ if (id) setSelection({ selectedIds: [id] })
509
+ } catch (error) {
510
+ console.error('Failed to duplicate node', error)
511
+ }
512
+ return
513
+ }
514
+
504
515
  if (node.type === 'roof') {
505
516
  try {
506
517
  duplicateRoofSubtree(node.id as AnyNodeId, { mode: 'move' })
@@ -805,7 +816,9 @@ export function FloatingActionMenu() {
805
816
  // `capabilities.movable`, a `floorplanMoveTarget`, or a
806
817
  // 3D `affordanceTools.move` mover gets the Move button.
807
818
  // Adding a new movable kind never touches this file.
808
- node && isRegistryMovable(node.type) ? handleMove : undefined
819
+ node && isRegistryMovable(node.type) && !registryMoveDisabled(node)
820
+ ? handleMove
821
+ : undefined
809
822
  }
810
823
  onDelete={handleDelete}
811
824
  onDuplicate={