@pascal-app/editor 0.7.0 → 0.9.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.
- package/package.json +12 -13
- package/src/components/editor/alignment-3d-guide-layer.tsx +160 -0
- package/src/components/editor/custom-camera-controls.tsx +755 -25
- package/src/components/editor/editor-layout-mobile.tsx +3 -3
- package/src/components/editor/editor-layout-v2.tsx +62 -54
- package/src/components/editor/export-manager.tsx +32 -3
- package/src/components/editor/first-person/build-collider-world.test.ts +149 -0
- package/src/components/editor/first-person/build-collider-world.ts +203 -87
- package/src/components/editor/first-person/bvh-ecctrl.tsx +127 -62
- package/src/components/editor/first-person-controls.tsx +836 -31
- package/src/components/editor/floating-action-menu.tsx +287 -196
- package/src/components/editor/floating-building-action-menu.tsx +1 -1
- package/src/components/editor/floorplan-panel.tsx +5837 -12006
- package/src/components/editor/grid.tsx +51 -17
- package/src/components/editor/group-move-handle.tsx +316 -0
- package/src/components/editor/group-rotate-handle.tsx +382 -0
- package/src/components/editor/group-transform-shared.test.ts +205 -0
- package/src/components/editor/group-transform-shared.ts +256 -0
- package/src/components/editor/handles/handle-arrow.tsx +523 -0
- package/src/components/editor/handles/use-handle-drag.ts +213 -0
- package/src/components/editor/index.tsx +234 -190
- package/src/components/editor/measurement-pill.tsx +75 -0
- package/src/components/editor/node-arrow-handles.tsx +1453 -0
- package/src/components/editor/selection-manager.tsx +435 -158
- package/src/components/editor/site-edge-labels.tsx +44 -20
- package/src/components/editor/slab-hole-highlights.tsx +466 -0
- package/src/components/editor/snapshot-capture-overlay.tsx +539 -0
- package/src/components/editor/thumbnail-generator.tsx +27 -64
- package/src/components/editor/use-floorplan-background-placement.ts +122 -35
- package/src/components/editor/use-floorplan-hit-testing.ts +24 -0
- package/src/components/editor/use-floorplan-scene-data.ts +14 -2
- package/src/components/editor/wall-measurement-label.tsx +11 -6
- package/src/components/editor/wall-move-side-handles.tsx +855 -0
- package/src/components/editor/wall-opening-highlights.tsx +181 -0
- package/src/components/editor/wall-snap-beacon-layer.tsx +150 -0
- package/src/components/editor-2d/floorplan-action-menu-layer.tsx +6 -1
- package/src/components/editor-2d/floorplan-alignment-guide-layer.tsx +142 -0
- package/src/components/editor-2d/floorplan-cursor-indicator-overlay.tsx +5 -3
- package/src/components/editor-2d/floorplan-registry-action-menu.tsx +227 -0
- package/src/components/editor-2d/floorplan-registry-move-overlay.tsx +616 -0
- package/src/components/editor-2d/floorplan-render-context.tsx +62 -0
- package/src/components/editor-2d/floorplan-snap-beacon-layer.tsx +75 -0
- package/src/components/editor-2d/floorplan-wall-move-ghost-layer.tsx +47 -0
- package/src/components/editor-2d/renderers/floorplan-draft-layer.tsx +6 -1
- package/src/components/editor-2d/renderers/floorplan-geometry-renderer.tsx +266 -0
- package/src/components/editor-2d/renderers/floorplan-measurements-layer.tsx +108 -61
- package/src/components/editor-2d/renderers/floorplan-placement-preview-layer.tsx +59 -0
- package/src/components/editor-2d/renderers/floorplan-registry-layer.tsx +1963 -0
- package/src/components/editor-2d/renderers/floorplan-stair-layer.tsx +29 -14
- package/src/components/editor-2d/svg-paths.ts +10 -2
- package/src/components/systems/ceiling/ceiling-selection-affordance-system.tsx +30 -9
- package/src/components/systems/ceiling/ceiling-system.tsx +12 -8
- package/src/components/systems/roof/roof-edit-system.tsx +86 -12
- package/src/components/systems/stair/stair-edit-system.tsx +2 -2
- package/src/components/systems/zone/zone-label-editor-system.tsx +0 -0
- package/src/components/systems/zone/zone-system.tsx +0 -0
- package/src/components/tools/elevator/elevator-defaults.ts +8 -0
- package/src/components/tools/elevator/elevator-tool.tsx +349 -0
- package/src/components/tools/elevator/move-elevator-tool.tsx +252 -0
- package/src/components/tools/fence/fence-drafting.ts +64 -22
- package/src/components/tools/item/move-tool.tsx +40 -107
- package/src/components/tools/item/placement-math.test.ts +10 -0
- package/src/components/tools/item/placement-math.ts +33 -9
- package/src/components/tools/item/placement-strategies.ts +201 -25
- package/src/components/tools/item/placement-types.ts +10 -1
- package/src/components/tools/item/use-draft-node.ts +20 -2
- package/src/components/tools/item/use-placement-coordinator.tsx +700 -299
- package/src/components/tools/registry/move-registry-node-tool.tsx +602 -0
- package/src/components/tools/roof/roof-tool.tsx +108 -12
- package/src/components/tools/select/box-select-state.ts +77 -0
- package/src/components/tools/select/box-select-tool.tsx +324 -543
- package/src/components/tools/select/plane-box-select-tool.tsx +577 -0
- package/src/components/tools/select/screen-rectangle-selection.ts +84 -0
- package/src/components/tools/select/select-candidates.test.ts +109 -0
- package/src/components/tools/select/select-candidates.ts +85 -0
- package/src/components/tools/shared/affordance-dispatch.ts +30 -0
- package/src/components/tools/shared/drag-bounding-box.tsx +171 -0
- package/src/components/tools/shared/floor-stack-preview.ts +25 -0
- package/src/components/tools/shared/fresh-placement-visibility.ts +61 -0
- package/src/components/tools/shared/placement-box-geometry.ts +123 -0
- package/src/components/tools/shared/placement-box.tsx +123 -0
- package/src/components/tools/shared/polygon-editor.tsx +617 -167
- package/src/components/tools/shared/segment-angle.ts +57 -1
- package/src/components/tools/site/site-boundary-editor.tsx +464 -12
- package/src/components/tools/site/site-flag-model.ts +18 -0
- package/src/components/tools/stair/stair-defaults.ts +1 -1
- package/src/components/tools/stair/stair-tool.tsx +269 -35
- package/src/components/tools/tool-manager.tsx +172 -56
- package/src/components/tools/wall/wall-drafting.ts +98 -98
- package/src/components/tools/wall/wall-snap-geometry.test.ts +79 -0
- package/src/components/tools/wall/wall-snap-geometry.ts +270 -0
- package/src/components/tools/zone/zone-tool.tsx +36 -11
- package/src/components/ui/action-menu/action-button.tsx +21 -1
- package/src/components/ui/action-menu/camera-actions.tsx +0 -0
- package/src/components/ui/action-menu/control-modes.tsx +5 -76
- package/src/components/ui/action-menu/furnish-tools.tsx +6 -92
- package/src/components/ui/action-menu/index.tsx +33 -196
- package/src/components/ui/action-menu/structure-tools.tsx +13 -88
- package/src/components/ui/action-menu/view-toggles.tsx +262 -37
- package/src/components/ui/command-palette/editor-commands.tsx +24 -17
- package/src/components/ui/command-palette/index.tsx +6 -256
- package/src/components/ui/controls/material-paint-panel.tsx +83 -0
- package/src/components/ui/controls/material-picker.tsx +17 -35
- package/src/components/ui/controls/metric-control.tsx +33 -11
- package/src/components/ui/floating-level-selector.tsx +48 -11
- package/src/components/ui/helpers/helper-manager.tsx +21 -17
- package/src/components/ui/helpers/registered-tool-helper.tsx +25 -0
- package/src/components/ui/item-catalog/catalog-items.tsx +1743 -315
- package/src/components/ui/item-catalog/item-catalog.tsx +91 -46
- package/src/components/ui/level-duplicate-dialog.tsx +5 -6
- package/src/components/ui/panels/node-display.ts +1 -0
- package/src/components/ui/panels/paint-panel.tsx +66 -41
- package/src/components/ui/panels/panel-manager.tsx +33 -74
- package/src/components/ui/panels/panel-wrapper.tsx +217 -12
- package/src/components/ui/panels/parametric-inspector.tsx +469 -0
- package/src/components/ui/panels/reference-panel.tsx +28 -13
- package/src/components/ui/primitives/dropdown-menu.tsx +12 -8
- package/src/components/ui/primitives/number-input.tsx +1 -1
- package/src/components/ui/primitives/sidebar.tsx +0 -0
- package/src/components/ui/scene-loader.tsx +1 -3
- package/src/components/ui/sidebar/app-sidebar.tsx +0 -0
- package/src/components/ui/sidebar/icon-rail.tsx +0 -0
- package/src/components/ui/sidebar/panels/items-panel/function-tree-panel.tsx +263 -0
- package/src/components/ui/sidebar/panels/items-panel/index.tsx +404 -0
- package/src/components/ui/sidebar/panels/settings-panel/index.tsx +59 -9
- package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +6 -1
- package/src/components/ui/sidebar/panels/settings-panel/load-build-dialog.tsx +259 -0
- package/src/components/ui/sidebar/panels/site-panel/building-tree-node.tsx +3 -1
- package/src/components/ui/sidebar/panels/site-panel/ceiling-tree-node.tsx +5 -2
- package/src/components/ui/sidebar/panels/site-panel/chimney-tree-node.tsx +83 -0
- package/src/components/ui/sidebar/panels/site-panel/dormer-tree-node.tsx +89 -0
- package/src/components/ui/sidebar/panels/site-panel/elevator-tree-node.tsx +75 -0
- package/src/components/ui/sidebar/panels/site-panel/fence-tree-node.tsx +2 -2
- package/src/components/ui/sidebar/panels/site-panel/gutter-tree-node.tsx +83 -0
- package/src/components/ui/sidebar/panels/site-panel/index.tsx +8 -12
- package/src/components/ui/sidebar/panels/site-panel/level-tree-node.tsx +2 -1
- package/src/components/ui/sidebar/panels/site-panel/registry-tree-node.tsx +93 -0
- package/src/components/ui/sidebar/panels/site-panel/roof-tree-node.tsx +41 -5
- package/src/components/ui/sidebar/panels/site-panel/shelf-tree-node.tsx +128 -0
- package/src/components/ui/sidebar/panels/site-panel/slab-tree-node.tsx +5 -2
- package/src/components/ui/sidebar/panels/site-panel/solar-panel-tree-node.tsx +83 -0
- package/src/components/ui/sidebar/panels/site-panel/spawn-tree-node.tsx +1 -1
- package/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +68 -34
- package/src/components/ui/sidebar/panels/site-panel/zone-tree-node.tsx +3 -1
- package/src/components/ui/sidebar/panels/zone-panel/index.tsx +52 -3
- package/src/components/ui/sidebar/tab-bar.tsx +61 -1
- package/src/components/ui/slider.tsx +1 -1
- package/src/components/viewer-overlay.tsx +169 -52
- package/src/components/viewer-zone-system.tsx +1 -1
- package/src/hooks/use-auto-save.ts +24 -3
- package/src/hooks/use-ceiling-events.ts +175 -0
- package/src/hooks/use-drag-action.ts +124 -0
- package/src/hooks/use-keyboard.ts +125 -62
- package/src/hooks/use-selection.ts +64 -0
- package/src/index.tsx +243 -4
- package/src/lib/constants.ts +6 -2
- package/src/lib/editor-api.ts +62 -0
- package/src/lib/elevator-support.ts +96 -0
- package/src/lib/floorplan/apply-alignment.ts +111 -0
- package/src/lib/floorplan/index.ts +7 -0
- package/src/lib/floorplan/selection-tool.ts +40 -0
- package/src/lib/floorplan/stairs.ts +12 -1
- package/src/lib/fresh-planar-placement.test.ts +102 -0
- package/src/lib/fresh-planar-placement.ts +63 -0
- package/src/lib/history.ts +2 -1
- package/src/lib/level-duplication.test.ts +0 -2
- package/src/lib/level-duplication.ts +55 -43
- package/src/lib/level-name.ts +11 -0
- package/src/lib/material-paint.ts +212 -20
- package/src/lib/placement-metadata.ts +29 -0
- package/src/lib/planar-cursor-placement.test.ts +43 -0
- package/src/lib/planar-cursor-placement.ts +42 -0
- package/src/lib/roof-duplication.ts +2 -2
- package/src/lib/scene-bounds.ts +1 -1
- package/src/lib/scene-clipboard.ts +267 -0
- package/src/lib/scene.ts +13 -4
- package/src/lib/sfx-bus.ts +25 -1
- package/src/lib/sfx-player.ts +75 -27
- package/src/lib/site-boundary.ts +1 -0
- package/src/lib/stair-duplication.ts +2 -2
- package/src/lib/stair-levels.test.ts +146 -0
- package/src/lib/stair-levels.ts +177 -0
- package/src/lib/world-grid-snap.ts +199 -0
- package/src/lib/zone-content.ts +130 -0
- package/src/store/use-alignment-guides.ts +21 -0
- package/src/store/use-editor.tsx +342 -91
- package/src/store/use-placement-preview.ts +31 -0
- package/src/store/use-wall-move-ghosts.ts +36 -0
- package/src/store/use-wall-snap-indicator.ts +32 -0
- package/tsconfig.json +2 -1
- package/src/components/editor/preset-thumbnail-generator.tsx +0 -125
- package/src/components/editor-2d/renderers/floorplan-roof-layer.tsx +0 -113
- package/src/components/feedback-dialog.tsx +0 -265
- package/src/components/pascal-radio.tsx +0 -280
- package/src/components/preview-button.tsx +0 -16
- package/src/components/tools/building/move-building-tool.tsx +0 -157
- package/src/components/tools/ceiling/ceiling-boundary-editor.tsx +0 -43
- package/src/components/tools/ceiling/ceiling-hole-editor.tsx +0 -49
- package/src/components/tools/ceiling/ceiling-tool.tsx +0 -465
- package/src/components/tools/ceiling/move-ceiling-tool.tsx +0 -257
- package/src/components/tools/column/column-tool.tsx +0 -97
- package/src/components/tools/column/move-column-tool.tsx +0 -105
- package/src/components/tools/door/door-math.ts +0 -110
- package/src/components/tools/door/door-tool.tsx +0 -324
- package/src/components/tools/door/move-door-tool.tsx +0 -412
- package/src/components/tools/fence/curve-fence-tool.tsx +0 -179
- package/src/components/tools/fence/fence-tool.tsx +0 -346
- package/src/components/tools/fence/move-fence-endpoint-tool.tsx +0 -438
- package/src/components/tools/fence/move-fence-tool.tsx +0 -298
- package/src/components/tools/item/item-tool.tsx +0 -26
- package/src/components/tools/roof/move-roof-tool.tsx +0 -359
- package/src/components/tools/slab/move-slab-tool.tsx +0 -182
- package/src/components/tools/slab/slab-boundary-editor.tsx +0 -43
- package/src/components/tools/slab/slab-hole-editor.tsx +0 -49
- package/src/components/tools/slab/slab-tool.tsx +0 -322
- package/src/components/tools/spawn/move-spawn-tool.tsx +0 -101
- package/src/components/tools/spawn/spawn-tool.tsx +0 -130
- package/src/components/tools/wall/curve-wall-tool.tsx +0 -176
- package/src/components/tools/wall/move-wall-endpoint-tool.tsx +0 -423
- package/src/components/tools/wall/move-wall-tool.tsx +0 -356
- package/src/components/tools/wall/wall-tool.tsx +0 -332
- package/src/components/tools/window/move-window-tool.tsx +0 -447
- package/src/components/tools/window/window-math.ts +0 -117
- package/src/components/tools/window/window-tool.tsx +0 -332
- package/src/components/ui/helpers/ceiling-helper.tsx +0 -20
- package/src/components/ui/helpers/slab-helper.tsx +0 -20
- package/src/components/ui/helpers/wall-helper.tsx +0 -20
- package/src/components/ui/panels/ceiling-panel.tsx +0 -250
- package/src/components/ui/panels/column-panel.tsx +0 -715
- package/src/components/ui/panels/door-panel.tsx +0 -1292
- package/src/components/ui/panels/fence-panel.tsx +0 -227
- package/src/components/ui/panels/item-panel.tsx +0 -306
- package/src/components/ui/panels/presets/presets-popover.tsx +0 -511
- package/src/components/ui/panels/roof-panel.tsx +0 -230
- package/src/components/ui/panels/roof-segment-panel.tsx +0 -311
- package/src/components/ui/panels/slab-panel.tsx +0 -250
- package/src/components/ui/panels/spawn-panel.tsx +0 -155
- package/src/components/ui/panels/stair-panel.tsx +0 -525
- package/src/components/ui/panels/stair-segment-panel.tsx +0 -325
- package/src/components/ui/panels/wall-panel.tsx +0 -177
- package/src/components/ui/panels/window-panel.tsx +0 -960
- package/src/components/ui/viewer-toolbar.tsx +0 -436
- package/src/contexts/presets-context.tsx +0 -121
- package/src/hooks/use-contextual-tools.ts +0 -61
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import type { AnyNode, AnyNodeId } from '@pascal-app/core'
|
|
4
|
+
import { useScene } from '@pascal-app/core'
|
|
5
|
+
import { useViewer } from '@pascal-app/viewer'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Resolved current selection — selected node IDs plus a convenience
|
|
9
|
+
* lookup into the live scene store. Returned shape is intentionally
|
|
10
|
+
* narrow: hosts that need richer per-node state can compose this with
|
|
11
|
+
* `useScene()` themselves.
|
|
12
|
+
*/
|
|
13
|
+
export type Selection = {
|
|
14
|
+
/** Multi-select node IDs (drives the rest of the editor's UI). */
|
|
15
|
+
selectedIds: AnyNodeId[]
|
|
16
|
+
/** Currently active building context — surfaces / palettes filter on this. */
|
|
17
|
+
buildingId: AnyNodeId | null
|
|
18
|
+
/** Currently active level context. */
|
|
19
|
+
levelId: AnyNodeId | null
|
|
20
|
+
/** Currently active zone context. */
|
|
21
|
+
zoneId: AnyNodeId | null
|
|
22
|
+
/**
|
|
23
|
+
* Resolved nodes for `selectedIds`. Missing entries (deleted nodes) are
|
|
24
|
+
* filtered out, so the array length may be ≤ `selectedIds.length`.
|
|
25
|
+
*/
|
|
26
|
+
selectedNodes: AnyNode[]
|
|
27
|
+
/**
|
|
28
|
+
* The single selected node, or `null` when zero or multiple nodes are
|
|
29
|
+
* selected. Useful for "save as preset" / inspector gating where the
|
|
30
|
+
* UI only makes sense for a unique selection.
|
|
31
|
+
*/
|
|
32
|
+
selectedNode: AnyNode | null
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Subscribe to the current selection. Equivalent to reading from
|
|
37
|
+
* `useViewer().selection` plus a live `useScene()` lookup, packaged as
|
|
38
|
+
* a single hook so consumers building their own shells (community,
|
|
39
|
+
* standalone editor app, embedders) don't have to learn the two
|
|
40
|
+
* separate stores.
|
|
41
|
+
*
|
|
42
|
+
* Selection state intentionally lives in `useViewer` (it tracks the
|
|
43
|
+
* camera / visibility hierarchy: building → level → zone → nodes), not
|
|
44
|
+
* `useScene` — see `wiki/architecture/scene-registry.md`.
|
|
45
|
+
*/
|
|
46
|
+
export function useSelection(): Selection {
|
|
47
|
+
const selection = useViewer((s) => s.selection)
|
|
48
|
+
const nodes = useScene((s) => s.nodes)
|
|
49
|
+
|
|
50
|
+
const selectedIds = selection.selectedIds as AnyNodeId[]
|
|
51
|
+
const selectedNodes = selectedIds
|
|
52
|
+
.map((id) => nodes[id])
|
|
53
|
+
.filter((n): n is AnyNode => n !== undefined)
|
|
54
|
+
const selectedNode = selectedNodes.length === 1 ? (selectedNodes[0] ?? null) : null
|
|
55
|
+
|
|
56
|
+
return {
|
|
57
|
+
selectedIds,
|
|
58
|
+
buildingId: (selection.buildingId ?? null) as AnyNodeId | null,
|
|
59
|
+
levelId: (selection.levelId ?? null) as AnyNodeId | null,
|
|
60
|
+
zoneId: (selection.zoneId ?? null) as AnyNodeId | null,
|
|
61
|
+
selectedNodes,
|
|
62
|
+
selectedNode,
|
|
63
|
+
}
|
|
64
|
+
}
|
package/src/index.tsx
CHANGED
|
@@ -1,13 +1,162 @@
|
|
|
1
|
+
// Re-exports of the scene / viewer hooks so consumers composing their
|
|
2
|
+
// own shells on top of `@pascal-app/editor` (community-app, embedders)
|
|
3
|
+
// don't have to learn three separate package imports. The canonical
|
|
4
|
+
// definitions still live in `@pascal-app/core` / `@pascal-app/viewer`.
|
|
5
|
+
export { useScene } from '@pascal-app/core'
|
|
6
|
+
export { useViewer } from '@pascal-app/viewer'
|
|
1
7
|
export type { EditorProps } from './components/editor'
|
|
2
8
|
export { default as Editor } from './components/editor'
|
|
9
|
+
// Headless component aliases: the implementation files keep their
|
|
10
|
+
// internal names (`ParametricInspector`, `FloatingActionMenu`) because
|
|
11
|
+
// they're referenced throughout the editor's own internals; the public
|
|
12
|
+
// surface uses the shorter, shell-friendly names from the unified
|
|
13
|
+
// preset-system spec.
|
|
14
|
+
export { FloatingActionMenu as FloatingMenu } from './components/editor/floating-action-menu'
|
|
15
|
+
export { formatMeasurement, MeasurementPill } from './components/editor/measurement-pill'
|
|
16
|
+
export {
|
|
17
|
+
type SnapshotCameraData,
|
|
18
|
+
ThumbnailGenerator,
|
|
19
|
+
} from './components/editor/thumbnail-generator'
|
|
20
|
+
// SVG path builders for arc / annular-sector / arrow-head shapes —
|
|
21
|
+
// inlined into `kind: 'path'` / `kind: 'polygon'` primitives by curved
|
|
22
|
+
// stair rendering in `nodes/src/stair/floorplan.ts`.
|
|
23
|
+
export {
|
|
24
|
+
buildSvgAnnularSectorPath,
|
|
25
|
+
buildSvgArcPath,
|
|
26
|
+
buildSvgArrowHeadPoints,
|
|
27
|
+
getArcPlanPoint,
|
|
28
|
+
} from './components/editor-2d/svg-paths'
|
|
29
|
+
// Phase 5 Stage D transitional exports — pure drafting / angle helpers
|
|
30
|
+
// consumed by kind-owned drag actions in @pascal-app/nodes. Stage F
|
|
31
|
+
// cleanup moves these into @pascal-app/nodes (fence/drafting.ts +
|
|
32
|
+
// shared/segment-angle.ts) once every Stage D port is in.
|
|
33
|
+
export {
|
|
34
|
+
createFenceOnCurrentLevel,
|
|
35
|
+
type FencePlanPoint,
|
|
36
|
+
snapFenceDraftPoint,
|
|
37
|
+
} from './components/tools/fence/fence-drafting'
|
|
38
|
+
// Placement-math helpers — shared by kind-owned placement tools in
|
|
39
|
+
// `@pascal-app/nodes` (wall curve sagitta snap, door / window placement,
|
|
40
|
+
// item drop) so kinds don't reach into editor internals.
|
|
41
|
+
export {
|
|
42
|
+
calculateCursorRotation,
|
|
43
|
+
calculateItemRotation,
|
|
44
|
+
getSideFromNormal,
|
|
45
|
+
isValidWallSideFace,
|
|
46
|
+
snapToGrid,
|
|
47
|
+
snapToHalf,
|
|
48
|
+
snapUpToGridStep,
|
|
49
|
+
stripTransient,
|
|
50
|
+
} from './components/tools/item/placement-math'
|
|
51
|
+
export type { PlacementState } from './components/tools/item/placement-types'
|
|
52
|
+
// Item placement / move primitives. Re-exported here so the registry-driven
|
|
53
|
+
// item move-tool in `@pascal-app/nodes` can compose them — same hooks the
|
|
54
|
+
// legacy `MoveItemContent` + `ItemTool` use. Once item placement is fully
|
|
55
|
+
// owned by `nodes`, these can be inlined there and dropped from editor.
|
|
56
|
+
export { type DraftNodeHandle, useDraftNode } from './components/tools/item/use-draft-node'
|
|
57
|
+
export {
|
|
58
|
+
type PlacementCoordinatorConfig,
|
|
59
|
+
usePlacementCoordinator,
|
|
60
|
+
} from './components/tools/item/use-placement-coordinator'
|
|
61
|
+
export { CursorSphere } from './components/tools/shared/cursor-sphere'
|
|
62
|
+
export { DragBoundingBox } from './components/tools/shared/drag-bounding-box'
|
|
63
|
+
export { getFloorStackPreviewPosition } from './components/tools/shared/floor-stack-preview'
|
|
64
|
+
export { useFreshPlacementVisibility } from './components/tools/shared/fresh-placement-visibility'
|
|
65
|
+
// Phase 5 Stage D — PolygonEditor for slab/ceiling boundary + hole editors.
|
|
66
|
+
export {
|
|
67
|
+
PolygonEditor,
|
|
68
|
+
type PolygonEditorProps,
|
|
69
|
+
} from './components/tools/shared/polygon-editor'
|
|
70
|
+
export {
|
|
71
|
+
formatAngleRadians,
|
|
72
|
+
getAngleArcToSegmentReference,
|
|
73
|
+
getAngleToSegmentReference,
|
|
74
|
+
getSegmentAngleReferenceAtPoint,
|
|
75
|
+
type SegmentAngleReference,
|
|
76
|
+
} from './components/tools/shared/segment-angle'
|
|
77
|
+
// Stair placement defaults — used by the kind-owned stair / stair-segment
|
|
78
|
+
// panels. Re-exported from `components/tools/stair/stair-defaults.ts`.
|
|
79
|
+
export {
|
|
80
|
+
DEFAULT_CURVED_STAIR_INNER_RADIUS,
|
|
81
|
+
DEFAULT_CURVED_STAIR_SWEEP_ANGLE,
|
|
82
|
+
DEFAULT_SPIRAL_SHOW_CENTER_COLUMN,
|
|
83
|
+
DEFAULT_SPIRAL_SHOW_STEP_SUPPORTS,
|
|
84
|
+
DEFAULT_SPIRAL_STAIR_SWEEP_ANGLE,
|
|
85
|
+
DEFAULT_SPIRAL_TOP_LANDING_DEPTH,
|
|
86
|
+
DEFAULT_SPIRAL_TOP_LANDING_MODE,
|
|
87
|
+
DEFAULT_STAIR_ATTACHMENT_SIDE,
|
|
88
|
+
DEFAULT_STAIR_FILL_TO_FLOOR,
|
|
89
|
+
DEFAULT_STAIR_HEIGHT,
|
|
90
|
+
DEFAULT_STAIR_LENGTH,
|
|
91
|
+
DEFAULT_STAIR_RAILING_HEIGHT,
|
|
92
|
+
DEFAULT_STAIR_RAILING_MODE,
|
|
93
|
+
DEFAULT_STAIR_STEP_COUNT,
|
|
94
|
+
DEFAULT_STAIR_THICKNESS,
|
|
95
|
+
DEFAULT_STAIR_TYPE,
|
|
96
|
+
DEFAULT_STAIR_WIDTH,
|
|
97
|
+
} from './components/tools/stair/stair-defaults'
|
|
98
|
+
export {
|
|
99
|
+
createWallOnCurrentLevel,
|
|
100
|
+
getSegmentGridStep,
|
|
101
|
+
isSegmentLongEnough,
|
|
102
|
+
snapPointToGrid,
|
|
103
|
+
snapScalarToGrid,
|
|
104
|
+
snapWallDraftPoint,
|
|
105
|
+
snapWallDraftPointDetailed,
|
|
106
|
+
WALL_FINE_GRID_STEP,
|
|
107
|
+
WALL_GRID_STEP,
|
|
108
|
+
type WallDraftSnapKind,
|
|
109
|
+
type WallDraftSnapResult,
|
|
110
|
+
type WallPlanPoint,
|
|
111
|
+
} from './components/tools/wall/wall-drafting'
|
|
112
|
+
// `ToolbarLeft` / `ToolbarRight` are the headless-spec aliases for the
|
|
113
|
+
// existing `ViewerToolbarLeft` / `ViewerToolbarRight` exports — the
|
|
114
|
+
// underlying components are the same; the alias just matches the names
|
|
115
|
+
// used in `pascalorg/private-editor:plans/community-preset-system.md`
|
|
116
|
+
// so consumer code stays close to the spec vocabulary.
|
|
117
|
+
export {
|
|
118
|
+
CameraActions as ToolbarRight,
|
|
119
|
+
CameraActions as ViewerToolbarRight,
|
|
120
|
+
} from './components/ui/action-menu/camera-actions'
|
|
121
|
+
export {
|
|
122
|
+
ViewToggles as ToolbarLeft,
|
|
123
|
+
ViewToggles as ViewerToolbarLeft,
|
|
124
|
+
} from './components/ui/action-menu/view-toggles'
|
|
3
125
|
export { useCommandPalette } from './components/ui/command-palette'
|
|
126
|
+
export { ActionButton, ActionGroup } from './components/ui/controls/action-button'
|
|
127
|
+
export { MaterialPaintPanel } from './components/ui/controls/material-paint-panel'
|
|
128
|
+
export { MaterialPicker } from './components/ui/controls/material-picker'
|
|
129
|
+
export { MetricControl } from './components/ui/controls/metric-control'
|
|
130
|
+
export { PanelSection } from './components/ui/controls/panel-section'
|
|
131
|
+
export { SegmentedControl } from './components/ui/controls/segmented-control'
|
|
4
132
|
export { SliderControl } from './components/ui/controls/slider-control'
|
|
133
|
+
export { ToggleControl } from './components/ui/controls/toggle-control'
|
|
5
134
|
export { FloatingLevelSelector } from './components/ui/floating-level-selector'
|
|
6
135
|
export { CATALOG_ITEMS } from './components/ui/item-catalog/catalog-items'
|
|
136
|
+
// Item collections UI — used by the kind-owned ItemPanel in nodes/.
|
|
137
|
+
export { CollectionsPopover } from './components/ui/panels/collections/collections-popover'
|
|
138
|
+
// Phase 5 Stage E — kinds with bespoke editors (slab holes list,
|
|
139
|
+
// ceiling height presets, etc.) use `parametrics.customPanel` to mount
|
|
140
|
+
// a kind-owned panel and need PanelWrapper for the chrome.
|
|
141
|
+
export { PanelWrapper } from './components/ui/panels/panel-wrapper'
|
|
142
|
+
export { ParametricInspector as Inspector } from './components/ui/panels/parametric-inspector'
|
|
143
|
+
export { PALETTE_COLORS } from './components/ui/primitives/color-dot'
|
|
144
|
+
export {
|
|
145
|
+
DropdownMenu,
|
|
146
|
+
DropdownMenuContent,
|
|
147
|
+
DropdownMenuItem,
|
|
148
|
+
DropdownMenuSeparator,
|
|
149
|
+
DropdownMenuSub,
|
|
150
|
+
DropdownMenuSubContent,
|
|
151
|
+
DropdownMenuSubTrigger,
|
|
152
|
+
DropdownMenuTrigger,
|
|
153
|
+
} from './components/ui/primitives/dropdown-menu'
|
|
7
154
|
export { useSidebarStore } from './components/ui/primitives/sidebar'
|
|
8
155
|
export { Slider } from './components/ui/primitives/slider'
|
|
9
156
|
export { SceneLoader } from './components/ui/scene-loader'
|
|
10
157
|
export type { ExtraPanel } from './components/ui/sidebar/icon-rail'
|
|
158
|
+
export { ItemsPanel } from './components/ui/sidebar/panels/items-panel'
|
|
159
|
+
export type { FunctionTreeNode } from './components/ui/sidebar/panels/items-panel/function-tree-panel'
|
|
11
160
|
export {
|
|
12
161
|
type ProjectVisibility,
|
|
13
162
|
SettingsPanel,
|
|
@@ -15,20 +164,110 @@ export {
|
|
|
15
164
|
} from './components/ui/sidebar/panels/settings-panel'
|
|
16
165
|
export type { SitePanelProps } from './components/ui/sidebar/panels/site-panel'
|
|
17
166
|
export type { SidebarTab } from './components/ui/sidebar/tab-bar'
|
|
18
|
-
export { ViewerToolbarLeft, ViewerToolbarRight } from './components/ui/viewer-toolbar'
|
|
19
|
-
export type { PresetsAdapter, PresetsTab } from './contexts/presets-context'
|
|
20
|
-
export { PresetsProvider } from './contexts/presets-context'
|
|
21
167
|
export type { SaveStatus } from './hooks/use-auto-save'
|
|
168
|
+
// useDragAction is the React-side glue for the registry's DragAction
|
|
169
|
+
// primitive. Public so registry-driven kinds (Phase 5+ Stage D ports)
|
|
170
|
+
// can express their affordances declaratively in their own folder.
|
|
171
|
+
export { type UseDragActionArgs, useDragAction } from './hooks/use-drag-action'
|
|
172
|
+
// Phase 5 Stage D — extras for kind-owned placement tools (FenceTool etc.).
|
|
173
|
+
export { markToolCancelConsumed } from './hooks/use-keyboard'
|
|
174
|
+
export { type Selection, useSelection } from './hooks/use-selection'
|
|
175
|
+
export { EDITOR_LAYER } from './lib/constants'
|
|
176
|
+
// Helper libs used by the kind-owned roof / stair / elevator panels.
|
|
177
|
+
export {
|
|
178
|
+
resolveCurrentBuildingId,
|
|
179
|
+
resolveElevatorNodeSupportY,
|
|
180
|
+
resolveElevatorSupportLevelId,
|
|
181
|
+
resolveElevatorSupportY,
|
|
182
|
+
} from './lib/elevator-support'
|
|
183
|
+
// Floor-plan stair helpers — the cumulative-transform walk
|
|
184
|
+
// (`computeFloorplanStairSegmentTransforms`) and the rich segment-entry
|
|
185
|
+
// builder (`buildFloorplanStairEntry`) used by the kind-owned stair
|
|
186
|
+
// floor-plan emitter in `@pascal-app/nodes/src/stair/floorplan.ts`.
|
|
187
|
+
// Each flight's transform depends on every prior sibling's length /
|
|
188
|
+
// height / `attachmentSide`, so individual stair-segments can't compute
|
|
189
|
+
// their own polygon in isolation — the stair (parent) owns the
|
|
190
|
+
// computation and emits the whole stack as one registry entry.
|
|
191
|
+
export {
|
|
192
|
+
alignFloorplanDraftPoint,
|
|
193
|
+
applyFloorplanAlignment,
|
|
194
|
+
buildFloorplanStairEntry,
|
|
195
|
+
FLOORPLAN_ALIGNMENT_THRESHOLD_M,
|
|
196
|
+
FLOORPLAN_DRAFT_ALIGN_ID,
|
|
197
|
+
type FloorplanAlignmentResult,
|
|
198
|
+
type FloorplanStairArrowEntry,
|
|
199
|
+
type FloorplanStairEntry,
|
|
200
|
+
type FloorplanStairSegmentEntry,
|
|
201
|
+
getFloorplanWallThickness,
|
|
202
|
+
} from './lib/floorplan'
|
|
203
|
+
export { commitFreshPlacementSubtree } from './lib/fresh-planar-placement'
|
|
204
|
+
export {
|
|
205
|
+
buildResetSurfaceMaterialUpdates,
|
|
206
|
+
buildRoofSurfaceMaterialPatch,
|
|
207
|
+
buildSingleSurfaceMaterialPatch,
|
|
208
|
+
buildStairSurfaceMaterialPatch,
|
|
209
|
+
buildWallSurfaceMaterialPatch,
|
|
210
|
+
getActivePaintMaterialLabel,
|
|
211
|
+
hasActivePaintMaterial,
|
|
212
|
+
} from './lib/material-paint'
|
|
213
|
+
export {
|
|
214
|
+
addFreshPlacementMetadata,
|
|
215
|
+
getPlacementMetadataRecord,
|
|
216
|
+
isFreshPlacementMetadata,
|
|
217
|
+
stripPlacementMetadataFlags,
|
|
218
|
+
} from './lib/placement-metadata'
|
|
219
|
+
export {
|
|
220
|
+
type PlanarCursorPlacementMode,
|
|
221
|
+
type PlanarPoint,
|
|
222
|
+
resolvePlanarCursorPosition,
|
|
223
|
+
} from './lib/planar-cursor-placement'
|
|
224
|
+
export { clearRoofDuplicateMetadata, duplicateRoofSubtree } from './lib/roof-duplication'
|
|
22
225
|
export type { SceneGraph } from './lib/scene'
|
|
23
226
|
export { applySceneGraphToEditor } from './lib/scene'
|
|
24
227
|
export { triggerSFX } from './lib/sfx-bus'
|
|
228
|
+
export { duplicateStairSubtree } from './lib/stair-duplication'
|
|
229
|
+
export {
|
|
230
|
+
getBuildingLevelsForLevel,
|
|
231
|
+
getStairLevelOptions,
|
|
232
|
+
resolveStairDestinationLevel,
|
|
233
|
+
resolveStairFromLevelId,
|
|
234
|
+
resolveStairPlacementLevelId,
|
|
235
|
+
resolveStairToLevelId,
|
|
236
|
+
} from './lib/stair-levels'
|
|
237
|
+
// `cn` (twMerge + clsx) — used by kind-owned panels in `@pascal-app/
|
|
238
|
+
// nodes` so they don't need their own copy / their own tailwind-merge
|
|
239
|
+
// dependency.
|
|
240
|
+
export { cn } from './lib/utils'
|
|
241
|
+
export {
|
|
242
|
+
getActiveBuildingPose,
|
|
243
|
+
resolveAlignmentForActiveBuilding,
|
|
244
|
+
snapBuildingLocalToWorldGrid,
|
|
245
|
+
snapWorldXZForActiveBuilding,
|
|
246
|
+
} from './lib/world-grid-snap'
|
|
247
|
+
export { default as useAlignmentGuides } from './store/use-alignment-guides'
|
|
25
248
|
export { default as useAudio } from './store/use-audio'
|
|
26
249
|
export { type CommandAction, useCommandRegistry } from './store/use-command-registry'
|
|
27
|
-
export type {
|
|
250
|
+
export type {
|
|
251
|
+
FloorplanSelectionTool,
|
|
252
|
+
MovingFenceEndpoint,
|
|
253
|
+
MovingWallEndpoint,
|
|
254
|
+
SplitOrientation,
|
|
255
|
+
Tool,
|
|
256
|
+
ToolDefaults,
|
|
257
|
+
ViewMode,
|
|
258
|
+
WorkspaceMode,
|
|
259
|
+
} from './store/use-editor'
|
|
28
260
|
export { default as useEditor } from './store/use-editor'
|
|
29
261
|
export {
|
|
30
262
|
type PaletteView,
|
|
31
263
|
type PaletteViewProps,
|
|
32
264
|
usePaletteViewRegistry,
|
|
33
265
|
} from './store/use-palette-view-registry'
|
|
266
|
+
export { default as usePlacementPreview } from './store/use-placement-preview'
|
|
34
267
|
export { useUploadStore } from './store/use-upload'
|
|
268
|
+
export { useWallMoveGhosts, type WallMoveGhostBridge } from './store/use-wall-move-ghosts'
|
|
269
|
+
export {
|
|
270
|
+
default as useWallSnapIndicator,
|
|
271
|
+
type WallSnapKind,
|
|
272
|
+
type WallSnapPoint,
|
|
273
|
+
} from './store/use-wall-snap-indicator'
|
package/src/lib/constants.ts
CHANGED
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
import { OVERLAY_LAYER } from '@pascal-app/viewer'
|
|
2
|
+
|
|
1
3
|
/** Three.js layer used for editor-only objects (helpers, grid, polygon editors).
|
|
2
|
-
* The thumbnail camera renders only layer 0, so these are excluded from thumbnails.
|
|
3
|
-
|
|
4
|
+
* The thumbnail camera renders only layer 0, so these are excluded from thumbnails.
|
|
5
|
+
* Aliased to viewer's `OVERLAY_LAYER` so the post-processing overlay pass and the
|
|
6
|
+
* editor's overlay meshes stay on the same layer. */
|
|
7
|
+
export const EDITOR_LAYER = OVERLAY_LAYER
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import type { AnyNode, EditorApi, FenceNode, WallNode } from '@pascal-app/core'
|
|
2
|
+
import useEditor from '../store/use-editor'
|
|
3
|
+
|
|
4
|
+
type EditorState = ReturnType<typeof useEditor.getState>
|
|
5
|
+
type EndpointEngager = (node: AnyNode, endpoint: 'start' | 'end', editor: EditorState) => void
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Per-kind endpoint-move engagement. Kinds whose 2D endpoint drag
|
|
9
|
+
* needs its own store field (wall ↔ `movingWallEndpoint`, fence ↔
|
|
10
|
+
* `movingFenceEndpoint`) register their bridge here. The dispatcher
|
|
11
|
+
* is a table lookup rather than an `if (type === 'wall')` chain so
|
|
12
|
+
* adding a new endpoint-draggable kind is a one-line entry instead
|
|
13
|
+
* of a new branch. Each entry casts the generic `AnyNode` to its
|
|
14
|
+
* concrete kind — the lookup key already guarantees the type.
|
|
15
|
+
*/
|
|
16
|
+
const endpointEngagers: Record<string, EndpointEngager> = {
|
|
17
|
+
wall: (node, endpoint, editor) =>
|
|
18
|
+
editor.setMovingWallEndpoint({ wall: node as WallNode, endpoint }),
|
|
19
|
+
fence: (node, endpoint, editor) =>
|
|
20
|
+
editor.setMovingFenceEndpoint({ fence: node as FenceNode, endpoint }),
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Concrete {@link EditorApi} backed by `useEditor`. Descriptors call into
|
|
25
|
+
* editor state through this interface; the editor owns the actual setter
|
|
26
|
+
* names so core stays decoupled.
|
|
27
|
+
*
|
|
28
|
+
* `engageMove` clears any in-progress endpoint drag or curve gesture so
|
|
29
|
+
* the move tool takes over cleanly — mirrors the legacy bookkeeping that
|
|
30
|
+
* lived inside `WallMoveArrowHandle.activateWallMove` / `FenceMoveArrowHandle`.
|
|
31
|
+
*/
|
|
32
|
+
export function createEditorApi(): EditorApi {
|
|
33
|
+
return {
|
|
34
|
+
engageMove(node: AnyNode) {
|
|
35
|
+
const editor = useEditor.getState()
|
|
36
|
+
editor.setPlacementDragMode(false)
|
|
37
|
+
// `setMovingNode` is typed against a narrower union than `AnyNode`
|
|
38
|
+
// (every concrete kind enumerated). Descriptors pass any node; the
|
|
39
|
+
// cast lets registry-driven move kinds through without forcing a
|
|
40
|
+
// schema-level type widening.
|
|
41
|
+
editor.setMovingNode(node as Parameters<typeof editor.setMovingNode>[0])
|
|
42
|
+
editor.setMovingWallEndpoint(null)
|
|
43
|
+
editor.setMovingFenceEndpoint(null)
|
|
44
|
+
editor.setCurvingWall(null)
|
|
45
|
+
editor.setCurvingFence(null)
|
|
46
|
+
},
|
|
47
|
+
engageMoveDrag(node: AnyNode) {
|
|
48
|
+
const editor = useEditor.getState()
|
|
49
|
+
// Flag drag mode BEFORE mounting the move tool so the coordinator reads
|
|
50
|
+
// it at setup and wires its commit-on-release listener.
|
|
51
|
+
editor.setPlacementDragMode(true)
|
|
52
|
+
editor.setMovingNode(node as Parameters<typeof editor.setMovingNode>[0])
|
|
53
|
+
editor.setMovingWallEndpoint(null)
|
|
54
|
+
editor.setMovingFenceEndpoint(null)
|
|
55
|
+
editor.setCurvingWall(null)
|
|
56
|
+
editor.setCurvingFence(null)
|
|
57
|
+
},
|
|
58
|
+
engageEndpointMove(node: AnyNode, endpoint: 'start' | 'end') {
|
|
59
|
+
endpointEngagers[node.type]?.(node, endpoint, useEditor.getState())
|
|
60
|
+
},
|
|
61
|
+
}
|
|
62
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type AnyNode,
|
|
3
|
+
type AnyNodeId,
|
|
4
|
+
type BuildingNode,
|
|
5
|
+
type ElevatorNode,
|
|
6
|
+
type LevelNode,
|
|
7
|
+
spatialGridManager,
|
|
8
|
+
useScene,
|
|
9
|
+
} from '@pascal-app/core'
|
|
10
|
+
|
|
11
|
+
function getBuildingLevels(
|
|
12
|
+
buildingId: string | null | undefined,
|
|
13
|
+
nodes: Record<string, AnyNode>,
|
|
14
|
+
): LevelNode[] {
|
|
15
|
+
if (!buildingId) return []
|
|
16
|
+
const building = nodes[buildingId as AnyNodeId]
|
|
17
|
+
if (building?.type !== 'building') return []
|
|
18
|
+
|
|
19
|
+
return building.children
|
|
20
|
+
.map((childId) => nodes[childId as AnyNodeId])
|
|
21
|
+
.filter((entry): entry is LevelNode => entry?.type === 'level')
|
|
22
|
+
.sort((left, right) => left.level - right.level)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function resolveCurrentBuildingId({
|
|
26
|
+
buildingId,
|
|
27
|
+
levelId,
|
|
28
|
+
nodes,
|
|
29
|
+
}: {
|
|
30
|
+
buildingId: BuildingNode['id'] | null
|
|
31
|
+
levelId: LevelNode['id'] | null
|
|
32
|
+
nodes: Record<string, AnyNode>
|
|
33
|
+
}): BuildingNode['id'] | null {
|
|
34
|
+
if (buildingId) return buildingId
|
|
35
|
+
if (!levelId) return null
|
|
36
|
+
|
|
37
|
+
const level = nodes[levelId as AnyNodeId]
|
|
38
|
+
if (
|
|
39
|
+
level?.type === 'level' &&
|
|
40
|
+
level.parentId &&
|
|
41
|
+
nodes[level.parentId as AnyNodeId]?.type === 'building'
|
|
42
|
+
) {
|
|
43
|
+
return level.parentId as BuildingNode['id']
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return null
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function resolveElevatorSupportLevelId({
|
|
50
|
+
buildingId,
|
|
51
|
+
preferredLevelId,
|
|
52
|
+
}: {
|
|
53
|
+
buildingId: string | null | undefined
|
|
54
|
+
preferredLevelId?: string | null
|
|
55
|
+
}): LevelNode['id'] | null {
|
|
56
|
+
const nodes = useScene.getState().nodes
|
|
57
|
+
const preferred = preferredLevelId ? nodes[preferredLevelId as AnyNodeId] : undefined
|
|
58
|
+
const levels = getBuildingLevels(buildingId, nodes)
|
|
59
|
+
const preferredInBuilding = preferredLevelId
|
|
60
|
+
? levels.find((level) => level.id === preferredLevelId)
|
|
61
|
+
: undefined
|
|
62
|
+
|
|
63
|
+
if (preferredInBuilding) return preferredInBuilding.id
|
|
64
|
+
if (levels.length === 0) return preferred?.type === 'level' ? preferred.id : null
|
|
65
|
+
|
|
66
|
+
return levels[0]?.id ?? null
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function resolveElevatorSupportY({
|
|
70
|
+
buildingId,
|
|
71
|
+
preferredLevelId,
|
|
72
|
+
x,
|
|
73
|
+
z,
|
|
74
|
+
}: {
|
|
75
|
+
buildingId: string | null | undefined
|
|
76
|
+
preferredLevelId?: string | null
|
|
77
|
+
x: number
|
|
78
|
+
z: number
|
|
79
|
+
}): number {
|
|
80
|
+
const levelId = resolveElevatorSupportLevelId({ buildingId, preferredLevelId })
|
|
81
|
+
if (!levelId) return 0
|
|
82
|
+
|
|
83
|
+
return Math.max(0, spatialGridManager.getSlabElevationAt(levelId, x, z))
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function resolveElevatorNodeSupportY(
|
|
87
|
+
node: ElevatorNode,
|
|
88
|
+
position: [number, number, number] = node.position,
|
|
89
|
+
): number {
|
|
90
|
+
return resolveElevatorSupportY({
|
|
91
|
+
buildingId: node.parentId,
|
|
92
|
+
preferredLevelId: node.fromLevelId ?? node.defaultLevelId,
|
|
93
|
+
x: position[0],
|
|
94
|
+
z: position[2],
|
|
95
|
+
})
|
|
96
|
+
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type AlignmentAnchor,
|
|
3
|
+
type AlignmentGuide,
|
|
4
|
+
collectAlignmentAnchors,
|
|
5
|
+
resolveAlignment,
|
|
6
|
+
useScene,
|
|
7
|
+
} from '@pascal-app/core'
|
|
8
|
+
import { useAlignmentGuides } from '@pascal-app/editor'
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Fixed Figma-style alignment threshold (meters) for floor-plan placement /
|
|
12
|
+
* move — parity with the 3D tools' `ALIGNMENT_THRESHOLD_M`. Pure-2D drafting
|
|
13
|
+
* can pass a zoom-scaled `threshold` instead so the magnetic pull stays
|
|
14
|
+
* constant in screen pixels across zoom levels.
|
|
15
|
+
*/
|
|
16
|
+
export const FLOORPLAN_ALIGNMENT_THRESHOLD_M = 0.08
|
|
17
|
+
|
|
18
|
+
export type FloorplanAlignmentResult = {
|
|
19
|
+
/** Plan point with the alignment snap delta applied (grid snap should
|
|
20
|
+
* already be baked into the input point). */
|
|
21
|
+
point: [number, number]
|
|
22
|
+
snapped: boolean
|
|
23
|
+
guides: AlignmentGuide[]
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Layer Figma-style alignment on top of an already grid-snapped plan point,
|
|
28
|
+
* shared by the 2D move sessions (`*FloorplanMoveTarget`) and the structural
|
|
29
|
+
* drafting branches in `floorplan-panel`.
|
|
30
|
+
*
|
|
31
|
+
* Publishes guides to the `useAlignmentGuides` store as a side effect — set
|
|
32
|
+
* on a match, cleared otherwise — so the mounted `FloorplanAlignmentGuideLayer`
|
|
33
|
+
* renders them. Returns the adjusted point. When `bypass` is true (Alt held)
|
|
34
|
+
* the point is returned unchanged and guides are cleared, matching the
|
|
35
|
+
* "No snap" affordance the placement tools advertise.
|
|
36
|
+
*
|
|
37
|
+
* `candidates` should be gathered ONCE per drag (`collectAlignmentAnchors`);
|
|
38
|
+
* the scene is stable during a single drag, so re-collecting per pointer-move
|
|
39
|
+
* is wasted work.
|
|
40
|
+
*/
|
|
41
|
+
export function applyFloorplanAlignment(
|
|
42
|
+
point: readonly [number, number],
|
|
43
|
+
movingAnchors: AlignmentAnchor[],
|
|
44
|
+
candidates: AlignmentAnchor[],
|
|
45
|
+
opts?: { bypass?: boolean; threshold?: number },
|
|
46
|
+
): FloorplanAlignmentResult {
|
|
47
|
+
if (opts?.bypass) {
|
|
48
|
+
useAlignmentGuides.getState().clear()
|
|
49
|
+
return { point: [point[0], point[1]], snapped: false, guides: [] }
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const result = resolveAlignment({
|
|
53
|
+
moving: movingAnchors,
|
|
54
|
+
candidates,
|
|
55
|
+
threshold: opts?.threshold ?? FLOORPLAN_ALIGNMENT_THRESHOLD_M,
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
useAlignmentGuides.getState().set(result.guides)
|
|
59
|
+
|
|
60
|
+
if (!result.snap) {
|
|
61
|
+
return { point: [point[0], point[1]], snapped: false, guides: result.guides }
|
|
62
|
+
}
|
|
63
|
+
return {
|
|
64
|
+
point: [point[0] + result.snap.dx, point[1] + result.snap.dz],
|
|
65
|
+
snapped: true,
|
|
66
|
+
guides: result.guides,
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** Synthetic node id for the in-progress structural-draft vertex. Never
|
|
71
|
+
* collides with a real scene node, so `collectAlignmentAnchors` excludes
|
|
72
|
+
* nothing real. */
|
|
73
|
+
export const FLOORPLAN_DRAFT_ALIGN_ID = '__floorplan_draft__'
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Align a single grid-snapped structural-draft vertex (wall / fence / polygon
|
|
77
|
+
* / roof endpoint) against every other node's anchors (incl. wall faces from
|
|
78
|
+
* the wall-face anchor work). Treats the vertex as one corner anchor, gathers
|
|
79
|
+
* candidates from the live scene, publishes guides (cleared on `bypass`), and
|
|
80
|
+
* returns the possibly-snapped point.
|
|
81
|
+
*
|
|
82
|
+
* Used by BOTH the move-preview branch and the click-commit handler so the
|
|
83
|
+
* committed vertex lands exactly where the preview showed it. Caller owns the
|
|
84
|
+
* per-kind precedence (existing-wall endpoint/join snap wins; angle-snap
|
|
85
|
+
* suppresses alignment) and only calls this when alignment should apply.
|
|
86
|
+
*
|
|
87
|
+
* `excludeIds` drops those nodes' anchors from the candidate pool — used when
|
|
88
|
+
* dragging a wall / fence endpoint so the moving endpoint doesn't try to
|
|
89
|
+
* align to its own (and its linked siblings') geometry that moves with it.
|
|
90
|
+
*/
|
|
91
|
+
export function alignFloorplanDraftPoint(
|
|
92
|
+
point: readonly [number, number],
|
|
93
|
+
opts?: { bypass?: boolean; threshold?: number; excludeIds?: readonly string[] },
|
|
94
|
+
): [number, number] {
|
|
95
|
+
if (opts?.bypass) {
|
|
96
|
+
useAlignmentGuides.getState().clear()
|
|
97
|
+
return [point[0], point[1]]
|
|
98
|
+
}
|
|
99
|
+
let candidates = collectAlignmentAnchors(useScene.getState().nodes, FLOORPLAN_DRAFT_ALIGN_ID)
|
|
100
|
+
if (opts?.excludeIds?.length) {
|
|
101
|
+
const excluded = new Set(opts.excludeIds)
|
|
102
|
+
candidates = candidates.filter((anchor) => !excluded.has(anchor.nodeId))
|
|
103
|
+
}
|
|
104
|
+
const { point: snapped } = applyFloorplanAlignment(
|
|
105
|
+
point,
|
|
106
|
+
[{ nodeId: FLOORPLAN_DRAFT_ALIGN_ID, kind: 'corner', x: point[0], z: point[1] }],
|
|
107
|
+
candidates,
|
|
108
|
+
{ threshold: opts?.threshold },
|
|
109
|
+
)
|
|
110
|
+
return snapped
|
|
111
|
+
}
|