@pascal-app/editor 1.0.1 → 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.
- package/package.json +10 -5
- package/src/components/editor/custom-camera-controls.tsx +22 -74
- package/src/components/editor/editor-layout-mobile.tsx +1 -0
- package/src/components/editor/floorplan-panel.tsx +210 -163
- package/src/components/editor/grid.tsx +31 -7
- package/src/components/editor/group-actions.ts +5 -9
- package/src/components/editor/group-floating-action-menu.tsx +19 -9
- package/src/components/editor/group-move-3d.ts +4 -6
- package/src/components/editor/group-rotate-handle.tsx +81 -35
- package/src/components/editor/group-selection-box-3d.tsx +25 -17
- package/src/components/editor/group-transform-shared.test.ts +126 -1
- package/src/components/editor/group-transform-shared.ts +79 -73
- package/src/components/editor/handles/handle-arrow.tsx +72 -2
- package/src/components/editor/handles/use-handle-drag.ts +75 -6
- package/src/components/editor/index.tsx +24 -9
- package/src/components/editor/node-action-menu.tsx +2 -0
- package/src/components/editor/node-arrow-handles.tsx +141 -7
- package/src/components/editor/registry-action-contributions.tsx +53 -0
- package/src/components/editor/wall-move-side-handles.tsx +71 -21
- package/src/components/editor/wall-opening-highlights.tsx +2 -3
- package/src/components/editor-2d/floorplan-group-move.tsx +17 -24
- package/src/components/editor-2d/floorplan-registered-tool-layer.tsx +33 -12
- package/src/components/editor-2d/floorplan-registry-move-overlay.tsx +154 -49
- package/src/components/editor-2d/renderers/floorplan-draft-wall-measurement.tsx +166 -0
- package/src/components/editor-2d/renderers/floorplan-registry-layer.test.ts +49 -0
- package/src/components/editor-2d/renderers/floorplan-registry-layer.tsx +67 -21
- package/src/components/systems/zone/zone-system.tsx +8 -4
- package/src/components/tools/__fixtures__/hosting.ts +10 -0
- package/src/components/tools/item/item-surface-placement.test.ts +3 -9
- package/src/components/tools/item/item-surface-strategy-resolver.test.ts +3 -9
- package/src/components/tools/item/placement-strategies.ts +55 -43
- package/src/components/tools/item/shelf-surface-strategy-resolver.test.ts +3 -3
- package/src/components/tools/item/use-draft-node.ts +41 -2
- package/src/components/tools/item/use-placement-coordinator.tsx +166 -73
- package/src/components/tools/registry/item-surface-move.test.ts +10 -2
- package/src/components/tools/registry/item-surface-move.ts +46 -24
- package/src/components/tools/registry/item-surface-resolver.test.ts +2 -2
- package/src/components/tools/registry/move-registry-node-tool.tsx +23 -9
- package/src/components/tools/shared/affordance-dispatch.test.ts +24 -0
- package/src/components/tools/shared/affordance-dispatch.ts +13 -0
- package/src/components/tools/shared/draft-measurement-label.tsx +38 -0
- package/src/components/tools/shared/pointer-support-cap.test.ts +27 -0
- package/src/components/tools/shared/pointer-support-cap.ts +17 -5
- package/src/components/tools/shared/surface-rejection.test.tsx +5 -4
- package/src/components/tools/shared/surface-rejection.tsx +14 -0
- package/src/components/tools/tool-manager.tsx +36 -3
- package/src/components/ui/controls/material-paint-panel.tsx +6 -67
- package/src/components/ui/controls/material-picker.tsx +5 -67
- package/src/components/ui/controls/slider-control.tsx +76 -31
- package/src/components/ui/helpers/helper-manager.tsx +24 -6
- package/src/components/ui/helpers/registered-tool-helper.tsx +2 -32
- package/src/components/ui/item-catalog/item-catalog.tsx +5 -30
- package/src/components/ui/panels/multi-height-mode.tsx +27 -21
- package/src/components/ui/panels/parametric-inspector.tsx +3 -15
- package/src/components/ui/primitives/shortcut-token.tsx +4 -0
- package/src/components/ui/sidebar/panels/settings-panel/audio-settings-dialog.tsx +2 -2
- package/src/components/ui/sidebar/panels/settings-panel/index.tsx +585 -369
- package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +2 -2
- package/src/components/ui/sidebar/panels/settings-panel/print-export-button.tsx +1 -1
- package/src/components/viewer/floorplan-preview-visibility.test.ts +35 -0
- package/src/components/viewer/floorplan-preview-visibility.ts +19 -0
- package/src/components/viewer/floorplan-preview.tsx +1 -12
- package/src/components/viewer-zone-system.tsx +7 -2
- package/src/hooks/use-auto-save.integration.test.tsx +197 -0
- package/src/hooks/use-auto-save.ts +21 -6
- package/src/hooks/use-ceiling-events.test.ts +1 -0
- package/src/hooks/use-ceiling-events.ts +2 -0
- package/src/hooks/use-grid-events.test.ts +2 -0
- package/src/hooks/use-grid-events.ts +16 -11
- package/src/hooks/use-keyboard.test.ts +7 -0
- package/src/hooks/use-keyboard.ts +42 -29
- package/src/index.tsx +63 -3
- package/src/lib/catalog-panel-model.ts +39 -0
- package/src/lib/catalog-selection.test.ts +27 -0
- package/src/lib/catalog-selection.ts +8 -0
- package/src/lib/contextual-help.test.ts +27 -1
- package/src/lib/contextual-help.ts +16 -9
- package/src/lib/floorplan/floorplan-export.test.ts +3 -1
- package/src/lib/floorplan/floorplan-extension.ts +16 -0
- package/src/lib/fresh-planar-placement.ts +168 -36
- package/src/lib/glb-export.test.ts +59 -16
- package/src/lib/glb-export.ts +26 -13
- package/src/lib/grid-frame.test.ts +19 -0
- package/src/lib/grid-frame.ts +18 -0
- package/src/lib/interaction/scope.ts +9 -1
- package/src/lib/keyboard-pan.test.ts +62 -0
- package/src/lib/keyboard-pan.ts +89 -0
- package/src/lib/material-catalog-model.ts +72 -0
- package/src/lib/material-paint-panel-model.ts +76 -0
- package/src/lib/overlay-transform.test.ts +28 -0
- package/src/lib/overlay-transform.ts +12 -0
- package/src/lib/panel-rows.ts +75 -0
- package/src/lib/panel-tool-options.test.ts +34 -0
- package/src/lib/panel-tool-options.ts +46 -0
- package/src/lib/parametric-node-update.ts +37 -0
- package/src/lib/scene.test.ts +99 -0
- package/src/lib/scene.ts +21 -3
- package/src/lib/snapping-mode.test.ts +13 -2
- package/src/lib/snapping-mode.ts +11 -6
- package/src/lib/spatial-drag-plane.test.ts +53 -0
- package/src/lib/spatial-drag-plane.ts +19 -0
- package/src/lib/spatial-pointer-input.test.ts +72 -0
- package/src/lib/spatial-pointer-input.ts +62 -0
- package/src/lib/spatial-snap.test.ts +47 -0
- package/src/lib/terrain-panel-model.ts +117 -0
- package/src/lib/world-grid-snap.test.ts +48 -0
- package/src/lib/world-grid-snap.ts +9 -5
- package/src/store/rotation-snapping.test.ts +53 -0
- package/src/store/use-editor.tsx +10 -2
- package/src/store/use-floorplan-draft-preview.ts +10 -1
- package/src/store/use-interaction-scope.ts +155 -6
|
@@ -9,7 +9,6 @@ import {
|
|
|
9
9
|
} from '@pascal-app/core'
|
|
10
10
|
import { cancelPerfAction, markPerfAction, useViewer } from '@pascal-app/viewer'
|
|
11
11
|
import { useEffect } from 'react'
|
|
12
|
-
import { Vector3 } from 'three'
|
|
13
12
|
import {
|
|
14
13
|
cutSelectionToEditorClipboard,
|
|
15
14
|
deleteSelection,
|
|
@@ -18,9 +17,9 @@ import {
|
|
|
18
17
|
import {
|
|
19
18
|
classifyParticipant,
|
|
20
19
|
collectParticipants,
|
|
21
|
-
|
|
22
|
-
expandToComponent,
|
|
20
|
+
groupPlanBounds,
|
|
23
21
|
levelFrame,
|
|
22
|
+
planBoundsCenter,
|
|
24
23
|
rotateGroupPatches,
|
|
25
24
|
} from '../components/editor/group-transform-shared'
|
|
26
25
|
import { steppedRotation } from '../components/tools/item/placement-math'
|
|
@@ -36,7 +35,10 @@ import { leaveUnitFocus } from '../lib/units'
|
|
|
36
35
|
import { toggleWindowOpenState } from '../lib/window-interaction'
|
|
37
36
|
import useDeleteConfirmation from '../store/use-delete-confirmation'
|
|
38
37
|
import useEditor, { getActiveContinuationContext, getActiveSnapContext } from '../store/use-editor'
|
|
39
|
-
import useInteractionScope, {
|
|
38
|
+
import useInteractionScope, {
|
|
39
|
+
getMovingNode,
|
|
40
|
+
isInteractionSubtreeDraft,
|
|
41
|
+
} from '../store/use-interaction-scope'
|
|
40
42
|
import { groupCurrentSelection, ungroupCurrentSelection } from '../store/use-session-groups'
|
|
41
43
|
|
|
42
44
|
// References (guide/scan) are selected via `useEditor.selectedReferenceId`, not
|
|
@@ -66,27 +68,20 @@ function rotateGroupSelection(direction: 1 | -1): boolean {
|
|
|
66
68
|
(id) => classifyParticipant(nodes[id as AnyNodeId], levelId, nodes) !== null,
|
|
67
69
|
)
|
|
68
70
|
if (participantIds.length === 0) return false
|
|
69
|
-
const
|
|
70
|
-
const { starts, links } = collectParticipants(fullIds, nodes, levelId)
|
|
71
|
+
const { starts, links } = collectParticipants(participantIds, nodes, levelId)
|
|
71
72
|
if (starts.length === 0) return false
|
|
72
73
|
|
|
73
|
-
// Same pivot as the
|
|
74
|
-
//
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
const worldCenter = new Vector3(
|
|
79
|
-
(box.min.x + box.max.x) / 2,
|
|
80
|
-
box.min.y,
|
|
81
|
-
(box.min.z + box.max.z) / 2,
|
|
82
|
-
)
|
|
83
|
-
const localCenter = worldCenter.applyMatrix4(levelFrame(levelId).inverse)
|
|
74
|
+
// Same pivot as the dashed boxes and the rotate gizmo: the selection's box
|
|
75
|
+
// centre in the level frame.
|
|
76
|
+
const bounds = groupPlanBounds(starts, levelFrame(levelId).inverse)
|
|
77
|
+
if (!bounds) return false
|
|
78
|
+
const [pivotX, pivotZ] = planBoundsCenter(bounds)
|
|
84
79
|
|
|
85
80
|
// R (+45° yaw) orbits by -45° in the atan2 x→z sense: yaw = rotation - delta
|
|
86
81
|
// (see rotateGroupPatches), so keyboard direction matches the single-node
|
|
87
82
|
// steppedRotation sense.
|
|
88
83
|
const delta = -direction * (Math.PI / 4)
|
|
89
|
-
const patches = rotateGroupPatches(starts, links, { x:
|
|
84
|
+
const patches = rotateGroupPatches(starts, links, { x: pivotX, z: pivotZ }, delta)
|
|
90
85
|
// Space detection stays out: a rigid rotation of existing walls must not
|
|
91
86
|
// re-create the room's auto floors/ceilings at the new bearing.
|
|
92
87
|
pauseSpaceDetection()
|
|
@@ -116,7 +111,13 @@ const exitToSelectAfterUnconsumedCancel = () => {
|
|
|
116
111
|
const currentPhase = useEditor.getState().phase
|
|
117
112
|
const currentStructureLayer = useEditor.getState().structureLayer
|
|
118
113
|
|
|
119
|
-
useInteractionScope
|
|
114
|
+
useInteractionScope
|
|
115
|
+
.getState()
|
|
116
|
+
.endIf(
|
|
117
|
+
(sc) =>
|
|
118
|
+
(sc.kind === 'placing' && isInteractionSubtreeDraft()) ||
|
|
119
|
+
(sc.kind === 'reshaping' && sc.reshape === 'hole'),
|
|
120
|
+
)
|
|
120
121
|
|
|
121
122
|
// From zone mode, return to structure select
|
|
122
123
|
if (currentPhase === 'structure' && currentStructureLayer === 'zones') {
|
|
@@ -134,6 +135,20 @@ const exitToSelectAfterUnconsumedCancel = () => {
|
|
|
134
135
|
useEditor.getState().setSelectedReferenceId(null)
|
|
135
136
|
}
|
|
136
137
|
|
|
138
|
+
// Cancel the active editor action with the same consume-or-exit semantics as
|
|
139
|
+
// Escape. Spatial inputs use this instead of unconditionally selecting the
|
|
140
|
+
// Select tool, so multi-step tools can keep their tool active after clearing
|
|
141
|
+
// the current draft.
|
|
142
|
+
export const cancelActiveTool = () => {
|
|
143
|
+
_toolCancelConsumed = false
|
|
144
|
+
emitter.emit('tool:cancel')
|
|
145
|
+
if (!_toolCancelConsumed) {
|
|
146
|
+
if (leaveUnitFocus()) useEditor.getState().armToolMode({ mode: 'select' })
|
|
147
|
+
else exitToSelectAfterUnconsumedCancel()
|
|
148
|
+
}
|
|
149
|
+
return _toolCancelConsumed
|
|
150
|
+
}
|
|
151
|
+
|
|
137
152
|
// ⌘Z pressed mid-interaction (moving a node, drawing a wall, mid-placement…)
|
|
138
153
|
// reads as "abort this action", not history undo — behave exactly like Escape
|
|
139
154
|
// and report whether anything was in flight so the undo/redo arms know to
|
|
@@ -176,7 +191,8 @@ export const runHistoryShortcut = (direction: 'undo' | 'redo') => {
|
|
|
176
191
|
return true
|
|
177
192
|
}
|
|
178
193
|
|
|
179
|
-
|
|
194
|
+
/** Whether an armed tool owns the rotation key (`R` or `T`) instead of the selection. */
|
|
195
|
+
export const isToolOwnedRotation = (key: 'r' | 't' = 'r') => {
|
|
180
196
|
const editor = useEditor.getState()
|
|
181
197
|
const moving = getMovingNode()
|
|
182
198
|
if (
|
|
@@ -196,7 +212,10 @@ export const isToolOwnedRotation = () => {
|
|
|
196
212
|
// exist. Without this check, selecting an existing item in the 2D plan
|
|
197
213
|
// while the item tool is armed silently drops the global rotate key.
|
|
198
214
|
(editor.tool === 'item' && editor.selectedItem !== null) ||
|
|
199
|
-
editor.tool === 'lean-to-extension'
|
|
215
|
+
editor.tool === 'lean-to-extension' ||
|
|
216
|
+
// R toggles the wall tool between line and rectangle drawing; T stays
|
|
217
|
+
// the selection's.
|
|
218
|
+
(editor.tool === 'wall' && key === 'r'))
|
|
200
219
|
)
|
|
201
220
|
}
|
|
202
221
|
|
|
@@ -385,15 +404,9 @@ export const useKeyboard = ({
|
|
|
385
404
|
return
|
|
386
405
|
}
|
|
387
406
|
|
|
388
|
-
_toolCancelConsumed = false
|
|
389
|
-
emitter.emit('tool:cancel')
|
|
390
|
-
|
|
391
407
|
// Only switch to select mode if no tool had an active mid-action to cancel.
|
|
392
408
|
// (e.g. mid-wall draw or mid-slab polygon should only cancel the action, not exit the tool)
|
|
393
|
-
|
|
394
|
-
if (leaveUnitFocus()) useEditor.getState().armToolMode({ mode: 'select' })
|
|
395
|
-
else exitToSelectAfterUnconsumedCancel()
|
|
396
|
-
}
|
|
409
|
+
cancelActiveTool()
|
|
397
410
|
} else if (e.key === '1' && !e.metaKey && !e.ctrlKey) {
|
|
398
411
|
e.preventDefault()
|
|
399
412
|
useEditor.getState().setPhase('site')
|
|
@@ -619,7 +632,7 @@ export const useKeyboard = ({
|
|
|
619
632
|
} else if (
|
|
620
633
|
(e.key === 't' || e.key === 'T') &&
|
|
621
634
|
!isVersionPreviewMode &&
|
|
622
|
-
!isToolOwnedRotation() &&
|
|
635
|
+
!isToolOwnedRotation('t') &&
|
|
623
636
|
canRunGlobalRotationShortcut()
|
|
624
637
|
) {
|
|
625
638
|
// Rotate selected node counter-clockwise
|
package/src/index.tsx
CHANGED
|
@@ -49,7 +49,7 @@ export { FloatingActionMenu as FloatingMenu } from './components/editor/floating
|
|
|
49
49
|
// camera controls via the `useViewer.inputDragging` / `useEditor.movingNode`
|
|
50
50
|
// flags. Tools place onto `useViewer.selection.levelId`, so the host must set a
|
|
51
51
|
// building + level selection first.
|
|
52
|
-
export { Grid } from './components/editor/grid'
|
|
52
|
+
export { EDITOR_GRID_INPUT_NAME, Grid } from './components/editor/grid'
|
|
53
53
|
export {
|
|
54
54
|
DimensionPill,
|
|
55
55
|
type DimensionPillPart,
|
|
@@ -79,12 +79,18 @@ export {
|
|
|
79
79
|
useInvisibleHitAreaMaterial,
|
|
80
80
|
} from './components/editor/node-arrow-handles'
|
|
81
81
|
export { QuickMeasurementCard } from './components/editor/quick-measurement-card'
|
|
82
|
+
export { SelectionManager } from './components/editor/selection-manager'
|
|
82
83
|
export {
|
|
83
84
|
type SnapshotCameraData,
|
|
84
85
|
ThumbnailGenerator,
|
|
85
86
|
} from './components/editor/thumbnail-generator'
|
|
87
|
+
export { WallMoveSideHandles } from './components/editor/wall-move-side-handles'
|
|
86
88
|
export { useFloorplanRender } from './components/editor-2d/floorplan-render-context'
|
|
87
89
|
export { FloorplanDimensionRenderer } from './components/editor-2d/renderers/floorplan-dimension-renderer'
|
|
90
|
+
export {
|
|
91
|
+
type DraftWallMeasurement,
|
|
92
|
+
FloorplanDraftWallMeasurement,
|
|
93
|
+
} from './components/editor-2d/renderers/floorplan-draft-wall-measurement'
|
|
88
94
|
export { FloorplanGeometryRenderer } from './components/editor-2d/renderers/floorplan-geometry-renderer'
|
|
89
95
|
export {
|
|
90
96
|
FloorplanNodePreview,
|
|
@@ -140,6 +146,10 @@ export {
|
|
|
140
146
|
export { MoveRegistryNodeTool } from './components/tools/registry/move-registry-node-tool'
|
|
141
147
|
export { useRegistryToolContext } from './components/tools/registry-tool-context'
|
|
142
148
|
export { CursorSphere } from './components/tools/shared/cursor-sphere'
|
|
149
|
+
export {
|
|
150
|
+
DRAFT_LABEL_Y_OFFSET,
|
|
151
|
+
DraftMeasurementLabel,
|
|
152
|
+
} from './components/tools/shared/draft-measurement-label'
|
|
143
153
|
export { DragBoundingBox } from './components/tools/shared/drag-bounding-box'
|
|
144
154
|
export { getFloorStackPreviewPosition } from './components/tools/shared/floor-stack-preview'
|
|
145
155
|
export { useFreshPlacementVisibility } from './components/tools/shared/fresh-placement-visibility'
|
|
@@ -221,6 +231,7 @@ export {
|
|
|
221
231
|
CameraActions as ToolbarRight,
|
|
222
232
|
CameraActions as ViewerToolbarRight,
|
|
223
233
|
} from './components/ui/action-menu/camera-actions'
|
|
234
|
+
export { furnishTools } from './components/ui/action-menu/furnish-tools'
|
|
224
235
|
export {
|
|
225
236
|
ViewToggles as ToolbarLeft,
|
|
226
237
|
ViewToggles as ViewerToolbarLeft,
|
|
@@ -247,6 +258,19 @@ export { FloatingLevelSelector } from './components/ui/floating-level-selector'
|
|
|
247
258
|
export { CATALOG_ITEMS } from './components/ui/item-catalog/catalog-items'
|
|
248
259
|
// Item collections UI — used by the kind-owned ItemPanel in nodes/.
|
|
249
260
|
export { CollectionsPopover } from './components/ui/panels/collections/collections-popover'
|
|
261
|
+
export {
|
|
262
|
+
resolveHomogeneousSelection,
|
|
263
|
+
resolveUniqueSelectionIds,
|
|
264
|
+
} from './components/ui/panels/homogeneous-selection'
|
|
265
|
+
export {
|
|
266
|
+
commitMultiNodeFields,
|
|
267
|
+
fieldVisibleForAll,
|
|
268
|
+
firstNumericFieldValue,
|
|
269
|
+
firstVec3FieldValue,
|
|
270
|
+
reduceFieldValue,
|
|
271
|
+
reduceHeightBoundMode,
|
|
272
|
+
} from './components/ui/panels/multi-field-value'
|
|
273
|
+
export { applyMultiHeightMode } from './components/ui/panels/multi-height-mode'
|
|
250
274
|
// Phase 5 Stage E — kinds with bespoke editors (slab holes list,
|
|
251
275
|
// ceiling height presets, etc.) use `parametrics.customPanel` to mount
|
|
252
276
|
// a kind-owned panel and need PanelWrapper for the chrome.
|
|
@@ -269,6 +293,7 @@ export {
|
|
|
269
293
|
} from './components/ui/primitives/shortcut-token'
|
|
270
294
|
export { useSidebarStore } from './components/ui/primitives/sidebar'
|
|
271
295
|
export { Slider } from './components/ui/primitives/slider'
|
|
296
|
+
export { Tooltip, TooltipContent, TooltipTrigger } from './components/ui/primitives/tooltip'
|
|
272
297
|
export { SceneLoader } from './components/ui/scene-loader'
|
|
273
298
|
export type { ExtraPanel } from './components/ui/sidebar/icon-rail'
|
|
274
299
|
export { ItemsPanel } from './components/ui/sidebar/panels/items-panel'
|
|
@@ -330,7 +355,7 @@ export type { SaveStatus } from './hooks/use-auto-save'
|
|
|
330
355
|
// can express their affordances declaratively in their own folder.
|
|
331
356
|
export { type UseDragActionArgs, useDragAction } from './hooks/use-drag-action'
|
|
332
357
|
// Phase 5 Stage D — extras for kind-owned placement tools (FenceTool etc.).
|
|
333
|
-
export { markToolCancelConsumed } from './hooks/use-keyboard'
|
|
358
|
+
export { cancelActiveTool, markToolCancelConsumed } from './hooks/use-keyboard'
|
|
334
359
|
export { useReducedMotion } from './hooks/use-reduced-motion'
|
|
335
360
|
export { type Selection, useSelection } from './hooks/use-selection'
|
|
336
361
|
export {
|
|
@@ -339,6 +364,11 @@ export {
|
|
|
339
364
|
type PlacementSurface,
|
|
340
365
|
publishPlacementSurface,
|
|
341
366
|
} from './lib/active-placement-surface'
|
|
367
|
+
export {
|
|
368
|
+
activateCatalogItem,
|
|
369
|
+
filterCatalogItems,
|
|
370
|
+
isCatalogItemSelected,
|
|
371
|
+
} from './lib/catalog-panel-model'
|
|
342
372
|
export {
|
|
343
373
|
CEILING_ALIGNMENT_THRESHOLD_M,
|
|
344
374
|
type CeilingPlanSnapInput,
|
|
@@ -361,6 +391,7 @@ export {
|
|
|
361
391
|
continuationContextOf,
|
|
362
392
|
nextContinuation,
|
|
363
393
|
} from './lib/continuation'
|
|
394
|
+
export { canDirectMoveNode } from './lib/direct-manipulation'
|
|
364
395
|
export { createEditorApi } from './lib/editor-api'
|
|
365
396
|
export {
|
|
366
397
|
clearStructuralElevationGuide,
|
|
@@ -435,6 +466,7 @@ export {
|
|
|
435
466
|
type FloorplanMode,
|
|
436
467
|
isFloorplanToolAvailableInMode,
|
|
437
468
|
} from './lib/floorplan/floorplan-mode'
|
|
469
|
+
export { clientToPlan } from './lib/floorplan/plan-coords'
|
|
438
470
|
export {
|
|
439
471
|
commitFreshPlacementSubtree,
|
|
440
472
|
createFreshPlacementSubtree,
|
|
@@ -474,6 +506,8 @@ export {
|
|
|
474
506
|
movingNodeOf,
|
|
475
507
|
scopeNodeId,
|
|
476
508
|
} from './lib/interaction/scope'
|
|
509
|
+
export { isEditableKeyboardTarget } from './lib/keyboard-pan'
|
|
510
|
+
export { useMaterialCatalogModel } from './lib/material-catalog-model'
|
|
477
511
|
export {
|
|
478
512
|
type ActivePaintMaterial,
|
|
479
513
|
buildResetSurfaceMaterialUpdates,
|
|
@@ -483,6 +517,7 @@ export {
|
|
|
483
517
|
getActivePaintMaterialLabel,
|
|
484
518
|
hasActivePaintMaterial,
|
|
485
519
|
} from './lib/material-paint'
|
|
520
|
+
export { useMaterialPaintPanelModel } from './lib/material-paint-panel-model'
|
|
486
521
|
export {
|
|
487
522
|
CREATABLE_MEASUREMENT_KINDS,
|
|
488
523
|
type CreatableMeasurementKind,
|
|
@@ -529,6 +564,21 @@ export type {
|
|
|
529
564
|
ModelExportFormat,
|
|
530
565
|
ModelExportOptions,
|
|
531
566
|
} from './lib/model-export'
|
|
567
|
+
export {
|
|
568
|
+
cyclePaintScope,
|
|
569
|
+
type PaintHoverInfo,
|
|
570
|
+
type PaintScope,
|
|
571
|
+
paintScopeLabel,
|
|
572
|
+
} from './lib/paint-scope'
|
|
573
|
+
export {
|
|
574
|
+
getNodePanelModel,
|
|
575
|
+
type NodePanelModel,
|
|
576
|
+
PANEL_MODEL_EXTENSION,
|
|
577
|
+
type PanelAction,
|
|
578
|
+
type PanelRow,
|
|
579
|
+
} from './lib/panel-rows'
|
|
580
|
+
export { type PanelToolOption, usePanelToolHints } from './lib/panel-tool-options'
|
|
581
|
+
export { commitParametricNodeFields } from './lib/parametric-node-update'
|
|
532
582
|
export { consumePlacementDragRelease } from './lib/placement-drag-release'
|
|
533
583
|
export {
|
|
534
584
|
addFreshPlacementMetadata,
|
|
@@ -562,7 +612,7 @@ export { hasRoofFaceChildOverlap, type RoofWallHit, resolveRoofWallHit } from '.
|
|
|
562
612
|
export type { SceneGraph } from './lib/scene'
|
|
563
613
|
export { applySceneGraphToEditor } from './lib/scene'
|
|
564
614
|
export { movementSfxStepKey } from './lib/sfx/movement-tick'
|
|
565
|
-
export { triggerSFX } from './lib/sfx-bus'
|
|
615
|
+
export { emitDeleteSFX, triggerSFX } from './lib/sfx-bus'
|
|
566
616
|
export { playSFX, type SFXName, type SFXPlaybackOptions } from './lib/sfx-player'
|
|
567
617
|
export {
|
|
568
618
|
clearSlabSnapFeedback,
|
|
@@ -575,12 +625,14 @@ export {
|
|
|
575
625
|
type SlabPlanSnapResult,
|
|
576
626
|
} from './lib/slab-plan-snap'
|
|
577
627
|
export {
|
|
628
|
+
cycleSnappingModeIn,
|
|
578
629
|
getSnappingModeLabel,
|
|
579
630
|
resolveSnapFlags,
|
|
580
631
|
type SnapContext,
|
|
581
632
|
type SnapFlags,
|
|
582
633
|
type SnappingMode,
|
|
583
634
|
} from './lib/snapping-mode'
|
|
635
|
+
export { getSpatialPointerId, spatialPointerInput } from './lib/spatial-pointer-input'
|
|
584
636
|
export { duplicateStairSubtree } from './lib/stair-duplication'
|
|
585
637
|
export {
|
|
586
638
|
getBuildingLevelsForLevel,
|
|
@@ -590,6 +642,7 @@ export {
|
|
|
590
642
|
resolveStairPlacementLevelId,
|
|
591
643
|
resolveStairToLevelId,
|
|
592
644
|
} from './lib/stair-levels'
|
|
645
|
+
export { surfaceAttachmentId, surfaceFramePose, updateSurfaceNode } from './lib/surface-attachment'
|
|
593
646
|
export {
|
|
594
647
|
clearSurfacePlanSnapFeedback,
|
|
595
648
|
resolveSurfacePlanPointSnap,
|
|
@@ -597,14 +650,20 @@ export {
|
|
|
597
650
|
type SurfacePlanSnapInput,
|
|
598
651
|
type SurfacePlanSnapResult,
|
|
599
652
|
} from './lib/surface-plan-snap'
|
|
653
|
+
export { useTerrainPanelRows } from './lib/terrain-panel-model'
|
|
600
654
|
export {
|
|
655
|
+
brushRadiusRange,
|
|
656
|
+
clipTerrainPatchToSite,
|
|
657
|
+
commitStroke,
|
|
601
658
|
fieldExtentForSite,
|
|
602
659
|
flattenSite,
|
|
603
660
|
resetSiteTerrain,
|
|
604
661
|
resolveFlattenTarget,
|
|
605
662
|
sculptFieldForSite,
|
|
663
|
+
terrainPointInsideSite,
|
|
606
664
|
} from './lib/terrain-sculpt'
|
|
607
665
|
export { exportSceneToUsdz, type UsdzExportOptions } from './lib/usdz-export'
|
|
666
|
+
export { useLinearDisplay } from './lib/use-linear-display'
|
|
608
667
|
// `cn` (twMerge + clsx) — used by kind-owned panels in `@pascal-app/
|
|
609
668
|
// nodes` so they don't need their own copy / their own tailwind-merge
|
|
610
669
|
// dependency.
|
|
@@ -652,6 +711,7 @@ export {
|
|
|
652
711
|
isAngleSnapActive,
|
|
653
712
|
isGridSnapActive,
|
|
654
713
|
isMagneticSnapActive,
|
|
714
|
+
selectDefaultBuildingAndLevel,
|
|
655
715
|
} from './store/use-editor'
|
|
656
716
|
export { default as useFacingPose, type FacingPose } from './store/use-facing-pose'
|
|
657
717
|
export { default as useFenceCurveDraft } from './store/use-fence-curve-draft'
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { useViewer } from '@pascal-app/viewer'
|
|
2
|
+
import { CATALOG_ITEMS, type CatalogItem } from '../components/ui/item-catalog/catalog-items'
|
|
3
|
+
import useEditor, { type CatalogCategory } from '../store/use-editor'
|
|
4
|
+
|
|
5
|
+
export function filterCatalogItems({
|
|
6
|
+
category,
|
|
7
|
+
items = CATALOG_ITEMS,
|
|
8
|
+
search = '',
|
|
9
|
+
activePlacementTag,
|
|
10
|
+
activeFunctionalTag,
|
|
11
|
+
overrideItems,
|
|
12
|
+
}: {
|
|
13
|
+
category?: CatalogCategory
|
|
14
|
+
items?: readonly CatalogItem[]
|
|
15
|
+
search?: string
|
|
16
|
+
activePlacementTag?: string | null
|
|
17
|
+
activeFunctionalTag?: string | null
|
|
18
|
+
overrideItems?: readonly CatalogItem[]
|
|
19
|
+
}): readonly CatalogItem[] {
|
|
20
|
+
return (
|
|
21
|
+
overrideItems ??
|
|
22
|
+
items.filter((item) => {
|
|
23
|
+
if (!search && category && item.category !== category) return false
|
|
24
|
+
if (activePlacementTag && !item.tags?.includes(activePlacementTag)) return false
|
|
25
|
+
if (activeFunctionalTag && !item.tags?.includes(activeFunctionalTag)) return false
|
|
26
|
+
return !search || item.name.toLowerCase().includes(search.toLowerCase())
|
|
27
|
+
})
|
|
28
|
+
)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function activateCatalogItem(item: CatalogItem) {
|
|
32
|
+
useViewer.getState().setSelection({ selectedIds: [], zoneId: null })
|
|
33
|
+
const editor = useEditor.getState()
|
|
34
|
+
editor.setSelectedItem(item)
|
|
35
|
+
editor.setTool(item.tool ?? 'item')
|
|
36
|
+
editor.setMode('build')
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export { isCatalogItemSelected } from './catalog-selection'
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { test } from 'bun:test'
|
|
2
|
+
import assert from 'node:assert/strict'
|
|
3
|
+
import { isCatalogItemSelected } from './catalog-selection'
|
|
4
|
+
|
|
5
|
+
test('no selection leaves every tile unhighlighted, including items without model URLs', () => {
|
|
6
|
+
for (const item of [{ id: 'chair' }, { id: 'cabinet' }]) {
|
|
7
|
+
assert.equal(isCatalogItemSelected(item, null), false)
|
|
8
|
+
}
|
|
9
|
+
})
|
|
10
|
+
|
|
11
|
+
test('only the chosen item is highlighted when model URLs are missing or shared', () => {
|
|
12
|
+
for (const src of [undefined, '/shared-model.glb']) {
|
|
13
|
+
const items = [
|
|
14
|
+
{ id: 'first', src },
|
|
15
|
+
{ id: 'second', src },
|
|
16
|
+
]
|
|
17
|
+
assert.deepEqual(
|
|
18
|
+
items.map((item) => isCatalogItemSelected(item, items[1]!)),
|
|
19
|
+
[false, true],
|
|
20
|
+
)
|
|
21
|
+
}
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
test('selection follows identity across reloaded catalog objects', () => {
|
|
25
|
+
assert.equal(isCatalogItemSelected({ id: 'chair' }, { id: 'chair' }), true)
|
|
26
|
+
assert.equal(isCatalogItemSelected({ id: 'chair' }, { id: 'table' }), false)
|
|
27
|
+
})
|
|
@@ -1,5 +1,14 @@
|
|
|
1
1
|
import { describe, expect, test } from 'bun:test'
|
|
2
|
-
import { resolveSelectModeHelpHints } from './contextual-help'
|
|
2
|
+
import { resolveRotateHandleHelpHints, resolveSelectModeHelpHints } from './contextual-help'
|
|
3
|
+
|
|
4
|
+
test('rotation advertises Alt as the temporary free-rotation override', () => {
|
|
5
|
+
expect(resolveRotateHandleHelpHints(false)).toEqual([
|
|
6
|
+
{ keys: ['Alt'], label: 'Hold to rotate freely', active: false },
|
|
7
|
+
])
|
|
8
|
+
expect(resolveRotateHandleHelpHints(true)).toEqual([
|
|
9
|
+
{ keys: ['Alt'], label: 'Rotating freely (no angle step)', active: true },
|
|
10
|
+
])
|
|
11
|
+
})
|
|
3
12
|
|
|
4
13
|
describe('resolveSelectModeHelpHints', () => {
|
|
5
14
|
test('stays hidden in idle select mode with no selection', () => {
|
|
@@ -117,4 +126,21 @@ describe('resolveSelectModeHelpHints', () => {
|
|
|
117
126
|
},
|
|
118
127
|
])
|
|
119
128
|
})
|
|
129
|
+
|
|
130
|
+
test('shows the single-side radius hint for rounded openings', () => {
|
|
131
|
+
expect(
|
|
132
|
+
resolveSelectModeHelpHints({
|
|
133
|
+
selectedCount: 1,
|
|
134
|
+
hasMovableSelection: true,
|
|
135
|
+
hasRotatableSelection: false,
|
|
136
|
+
hasOpeningRadiusSelection: true,
|
|
137
|
+
commandPressed: false,
|
|
138
|
+
shiftPressed: false,
|
|
139
|
+
}),
|
|
140
|
+
).toContainEqual({
|
|
141
|
+
keys: ['Shift'],
|
|
142
|
+
label: 'Adjust one side’s corner radius',
|
|
143
|
+
active: false,
|
|
144
|
+
})
|
|
145
|
+
})
|
|
120
146
|
})
|
|
@@ -11,8 +11,8 @@ export type ContextualShortcutHint = {
|
|
|
11
11
|
}
|
|
12
12
|
|
|
13
13
|
// `activeHandleDrag.label` value a rotate gizmo sets while dragging, so the
|
|
14
|
-
// contextual HUD can surface the
|
|
15
|
-
//
|
|
14
|
+
// contextual HUD can surface the rotation snapping mode for the duration.
|
|
15
|
+
// Distinct from resize handles,
|
|
16
16
|
// which route their own measurement label here.
|
|
17
17
|
export const ROTATE_HANDLE_DRAG_LABEL = 'rotate-handle'
|
|
18
18
|
|
|
@@ -29,15 +29,12 @@ export const RESIZE_HANDLE_DRAG_LABEL = 'resize-handle'
|
|
|
29
29
|
export const GROUP_MOVE_DRAG_LABEL = 'group-move-handle'
|
|
30
30
|
export const GROUP_ROTATE_DRAG_LABEL = 'group-rotate-handle'
|
|
31
31
|
|
|
32
|
-
|
|
33
|
-
// (free rotation), the same toggle wall drafting exposes. `active` lights the
|
|
34
|
-
// pill while Shift is held.
|
|
35
|
-
export function resolveRotateHandleHelpHints(shiftPressed: boolean): ContextualShortcutHint[] {
|
|
32
|
+
export function resolveRotateHandleHelpHints(altPressed: boolean): ContextualShortcutHint[] {
|
|
36
33
|
return [
|
|
37
34
|
{
|
|
38
|
-
keys: [
|
|
39
|
-
label:
|
|
40
|
-
active:
|
|
35
|
+
keys: [ALT_KEY],
|
|
36
|
+
label: altPressed ? 'Rotating freely (no angle step)' : 'Hold to rotate freely',
|
|
37
|
+
active: altPressed,
|
|
41
38
|
},
|
|
42
39
|
]
|
|
43
40
|
}
|
|
@@ -46,6 +43,7 @@ export type SelectModeHelpContext = {
|
|
|
46
43
|
selectedCount: number
|
|
47
44
|
hasMovableSelection: boolean
|
|
48
45
|
hasRotatableSelection: boolean
|
|
46
|
+
hasOpeningRadiusSelection?: boolean
|
|
49
47
|
commandPressed: boolean
|
|
50
48
|
shiftPressed: boolean
|
|
51
49
|
// When a single MEP node is selected its in-world handle rig (click a dot to
|
|
@@ -67,6 +65,7 @@ export function resolveSelectModeHelpHints({
|
|
|
67
65
|
selectedCount,
|
|
68
66
|
hasMovableSelection,
|
|
69
67
|
hasRotatableSelection,
|
|
68
|
+
hasOpeningRadiusSelection = false,
|
|
70
69
|
commandPressed,
|
|
71
70
|
shiftPressed,
|
|
72
71
|
mepSelection = null,
|
|
@@ -142,6 +141,14 @@ export function resolveSelectModeHelpHints({
|
|
|
142
141
|
})
|
|
143
142
|
}
|
|
144
143
|
|
|
144
|
+
if (hasOpeningRadiusSelection) {
|
|
145
|
+
hints.push({
|
|
146
|
+
keys: [SHIFT_KEY],
|
|
147
|
+
label: 'Adjust one side’s corner radius',
|
|
148
|
+
active: shiftPressed,
|
|
149
|
+
})
|
|
150
|
+
}
|
|
151
|
+
|
|
145
152
|
hints.push({
|
|
146
153
|
keys: [[COMMAND_KEY, SHIFT_KEY], LEFT_CLICK],
|
|
147
154
|
label: 'Add or remove objects from the selection',
|
|
@@ -774,7 +774,9 @@ describe('collectFloorplanGeometry', () => {
|
|
|
774
774
|
'finished-faces',
|
|
775
775
|
[enabledPluginId],
|
|
776
776
|
)
|
|
777
|
-
|
|
777
|
+
// A hidden Site hides only its own ground and boundary; the site-scoped
|
|
778
|
+
// nodes on it keep their own flag (see `hidesDescendants`).
|
|
779
|
+
expect(hiddenSite.map(({ id }) => id)).toEqual(['site_overlay', 'level_architecture'])
|
|
778
780
|
|
|
779
781
|
const structure = collectFloorplanGeometry(
|
|
780
782
|
nodes,
|
|
@@ -57,13 +57,29 @@ export type FloorplanToolContext = {
|
|
|
57
57
|
|
|
58
58
|
export type FloorplanNodeExtension<N extends AnyNode = AnyNode> = {
|
|
59
59
|
tool?: () => Promise<{ default: ComponentType<FloorplanToolContext> }>
|
|
60
|
+
directDrag?: boolean
|
|
60
61
|
availableModes?: readonly FloorplanToolMode[]
|
|
61
62
|
preferredView?: '2d' | '3d'
|
|
62
63
|
referencedSelectionAnnotationRole?: FloorplanAnnotationRole
|
|
63
64
|
contextualDimensions?: (node: N, ctx: GeometryContext) => FloorplanGeometry | null
|
|
64
65
|
actionMenu?: {
|
|
65
66
|
canCurve?: (args: { node: N; nodes: Readonly<Record<AnyNodeId, AnyNode>> }) => boolean
|
|
67
|
+
/** Buttons the kind adds to the selection's action menu (2D and 3D); each decides its own visibility. */
|
|
68
|
+
actions?: () => Promise<{ default: ComponentType }>
|
|
66
69
|
}
|
|
70
|
+
/**
|
|
71
|
+
* Plan layers mounted while a node of this kind is in a `reshaping` scope of
|
|
72
|
+
* that name (the 2D sibling of `def.affordanceTools[reshape]`).
|
|
73
|
+
*/
|
|
74
|
+
reshapeLayers?: Record<string, () => Promise<{ default: ComponentType<FloorplanToolContext> }>>
|
|
75
|
+
/**
|
|
76
|
+
* Nodes the plan treats as one with this node when toggling it out of a
|
|
77
|
+
* selection (a slab and its ceiling share an outline and a click).
|
|
78
|
+
*/
|
|
79
|
+
selectionCounterparts?: (args: {
|
|
80
|
+
node: N
|
|
81
|
+
nodes: Readonly<Record<AnyNodeId, AnyNode>>
|
|
82
|
+
}) => AnyNodeId[]
|
|
67
83
|
schedule?: (args: {
|
|
68
84
|
siblings: ReadonlyArray<N>
|
|
69
85
|
nodes: Readonly<Record<string, AnyNode>>
|