@pascal-app/editor 0.9.1 → 0.9.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +12 -9
- package/src/components/editor/alignment-3d-guide-layer.tsx +10 -2
- package/src/components/editor/bake-exporter.tsx +47 -0
- package/src/components/editor/custom-camera-controls.tsx +63 -25
- package/src/components/editor/editor-layout-mobile.tsx +5 -0
- package/src/components/editor/editor-layout-v2.tsx +18 -0
- package/src/components/editor/export-manager.tsx +71 -63
- package/src/components/editor/fence-tangent-lines-3d.tsx +87 -0
- package/src/components/editor/first-person/build-collider-world.test.ts +36 -3
- package/src/components/editor/first-person/build-collider-world.ts +32 -5
- package/src/components/editor/first-person-controls.tsx +118 -7
- package/src/components/editor/floating-action-menu.tsx +410 -33
- package/src/components/editor/floating-building-action-menu.tsx +1 -1
- package/src/components/editor/floorplan-background-selection.test.ts +58 -0
- package/src/components/editor/floorplan-background-selection.ts +3 -2
- package/src/components/editor/floorplan-panel.tsx +1804 -849
- package/src/components/editor/grid.tsx +166 -39
- package/src/components/editor/group-actions.ts +457 -0
- package/src/components/editor/group-floating-action-menu.tsx +130 -0
- package/src/components/editor/group-move-3d.ts +397 -0
- package/src/components/editor/group-rotate-handle.tsx +102 -64
- package/src/components/editor/group-selection-box-3d.tsx +173 -0
- package/src/components/editor/group-transform-shared.test.ts +192 -1
- package/src/components/editor/group-transform-shared.ts +240 -7
- package/src/components/editor/handles/handle-arrow.tsx +81 -23
- package/src/components/editor/handles/use-handle-drag.ts +37 -4
- package/src/components/editor/index.tsx +248 -27
- package/src/components/editor/measurement-pill.tsx +62 -22
- package/src/components/editor/node-action-menu.tsx +14 -1
- package/src/components/editor/node-arrow-handles.tsx +366 -173
- package/src/components/editor/opening-guides-3d-layer.tsx +144 -0
- package/src/components/editor/quick-measurement-card.tsx +70 -0
- package/src/components/editor/quick-measurement-hud.tsx +28 -0
- package/src/components/editor/riser-diagram-panel.tsx +137 -0
- package/src/components/editor/selection-manager.tsx +812 -466
- package/src/components/editor/site-edge-labels.tsx +4 -4
- package/src/components/editor/slab-hole-highlights.tsx +13 -5
- package/src/components/editor/snapshot-capture-overlay.tsx +255 -115
- package/src/components/editor/three-context-bridge.ts +22 -0
- package/src/components/editor/thumbnail-generator.tsx +136 -43
- package/src/components/editor/use-floorplan-background-placement.ts +113 -45
- package/src/components/editor/use-floorplan-scene-data.ts +5 -5
- package/src/components/editor/use-mesh-settle-epoch.ts +26 -0
- package/src/components/editor/wall-measurement-label.tsx +3 -13
- package/src/components/editor/wall-move-side-handles.tsx +36 -21
- package/src/components/editor/wall-snap-beacon-layer.tsx +160 -6
- package/src/components/editor-2d/floorplan-action-menu-layer.tsx +9 -5
- package/src/components/editor-2d/floorplan-cursor-indicator-overlay.tsx +1 -1
- package/src/components/editor-2d/floorplan-group-action-menu.tsx +87 -0
- package/src/components/editor-2d/floorplan-group-move.tsx +701 -0
- package/src/components/editor-2d/floorplan-measurement-tool-layer.test.ts +113 -0
- package/src/components/editor-2d/floorplan-measurement-tool-layer.tsx +1656 -0
- package/src/components/editor-2d/floorplan-quick-measure-layer.tsx +212 -0
- package/src/components/editor-2d/floorplan-registry-action-menu.tsx +169 -4
- package/src/components/editor-2d/floorplan-registry-move-overlay.tsx +300 -59
- package/src/components/editor-2d/floorplan-snap-beacon-layer.tsx +5 -3
- package/src/components/editor-2d/renderers/floorplan-label-angle.test.ts +15 -0
- package/src/components/editor-2d/renderers/floorplan-label-angle.ts +14 -0
- package/src/components/editor-2d/renderers/floorplan-placement-preview-layer.tsx +4 -1
- package/src/components/editor-2d/renderers/floorplan-registry-layer.test.ts +308 -0
- package/src/components/editor-2d/renderers/floorplan-registry-layer.tsx +1704 -342
- package/src/components/editor-2d/renderers/floorplan-voronoi-layer.tsx +128 -0
- package/src/components/systems/ceiling/ceiling-selection-affordance-system.tsx +419 -60
- package/src/components/systems/ceiling/ceiling-system.tsx +97 -4
- package/src/components/systems/roof/roof-edit-system.tsx +1768 -13
- package/src/components/systems/selection-affordance-manager.tsx +38 -0
- package/src/components/systems/zone/zone-label-editor-system.tsx +14 -1
- package/src/components/systems/zone/zone-system.tsx +42 -13
- package/src/components/tools/elevator/elevator-tool.tsx +34 -8
- package/src/components/tools/elevator/move-elevator-tool.tsx +13 -2
- package/src/components/tools/fence/fence-drafting.ts +80 -8
- package/src/components/tools/item/move-tool.tsx +17 -13
- package/src/components/tools/item/placement-math.test.ts +13 -1
- package/src/components/tools/item/placement-math.ts +28 -2
- package/src/components/tools/item/placement-strategies.ts +246 -5
- package/src/components/tools/item/placement-types.ts +13 -1
- package/src/components/tools/item/use-draft-node.ts +31 -1
- package/src/components/tools/item/use-placement-coordinator.tsx +761 -121
- package/src/components/tools/registry/move-registry-node-tool.tsx +561 -99
- package/src/components/tools/roof/roof-tool.tsx +320 -94
- package/src/components/tools/select/box-select-state.ts +18 -7
- package/src/components/tools/select/box-select-tool.tsx +176 -40
- package/src/components/tools/select/marquee-geometry.test.ts +161 -0
- package/src/components/tools/select/marquee-geometry.ts +155 -0
- package/src/components/tools/select/plane-box-select-tool.tsx +1 -8
- package/src/components/tools/select/select-candidates.ts +1 -1
- package/src/components/tools/shared/cursor-sphere.tsx +62 -26
- package/src/components/tools/shared/drag-bounding-box.tsx +28 -4
- package/src/components/tools/shared/facing-indicator.tsx +87 -0
- package/src/components/tools/shared/facing-pose-indicator.tsx +54 -0
- package/src/components/tools/shared/placement-box.tsx +183 -1
- package/src/components/tools/shared/polygon-editor.tsx +276 -35
- package/src/components/tools/site/site-boundary-editor.tsx +88 -36
- package/src/components/tools/stair/stair-defaults.ts +1 -0
- package/src/components/tools/stair/stair-tool.tsx +82 -11
- package/src/components/tools/tool-manager.tsx +86 -25
- package/src/components/tools/wall/wall-drafting.test.ts +330 -0
- package/src/components/tools/wall/wall-drafting.ts +180 -103
- package/src/components/tools/wall/wall-snap-geometry.test.ts +44 -0
- package/src/components/tools/wall/wall-snap-geometry.ts +67 -6
- package/src/components/tools/zone/zone-boundary-editor.tsx +5 -1
- package/src/components/tools/zone/zone-tool.tsx +82 -88
- package/src/components/ui/action-menu/camera-actions.tsx +24 -17
- package/src/components/ui/action-menu/control-modes.tsx +40 -93
- package/src/components/ui/action-menu/furnish-tools.tsx +5 -5
- package/src/components/ui/action-menu/index.tsx +2 -4
- package/src/components/ui/action-menu/measurement-control.tsx +188 -0
- package/src/components/ui/action-menu/structure-tools.tsx +22 -13
- package/src/components/ui/action-menu/view-toggles.tsx +42 -90
- package/src/components/ui/command-palette/editor-commands.tsx +2 -2
- package/src/components/ui/command-palette/index.tsx +4 -3
- package/src/components/ui/controls/material-paint-panel.tsx +86 -17
- package/src/components/ui/controls/material-picker.tsx +83 -152
- package/src/components/ui/controls/material-properties-editor.tsx +108 -0
- package/src/components/ui/controls/metric-control.tsx +118 -44
- package/src/components/ui/controls/scene-material-list.tsx +247 -0
- package/src/components/ui/controls/slider-control.tsx +88 -38
- package/src/components/ui/floating-level-selector.tsx +1 -1
- package/src/components/ui/helpers/building-helper.tsx +10 -23
- package/src/components/ui/helpers/contextual-helper-panel.tsx +428 -0
- package/src/components/ui/helpers/helper-manager.tsx +227 -14
- package/src/components/ui/helpers/item-helper.tsx +28 -32
- package/src/components/ui/helpers/registered-tool-helper.tsx +45 -11
- package/src/components/ui/helpers/roof-helper.tsx +10 -12
- package/src/components/ui/icon-ref.tsx +47 -0
- package/src/components/ui/item-catalog/catalog-items.tsx +39 -1
- package/src/components/ui/item-catalog/item-catalog.tsx +13 -36
- package/src/components/ui/level-duplicate-dialog.tsx +1 -1
- package/src/components/ui/panels/node-display.ts +17 -17
- package/src/components/ui/panels/panel-manager.tsx +22 -13
- package/src/components/ui/panels/panel-wrapper.tsx +24 -3
- package/src/components/ui/panels/parametric-inspector.tsx +15 -2
- package/src/components/ui/panels/reference-panel.tsx +54 -19
- package/src/components/ui/primitives/shortcut-token.tsx +39 -3
- package/src/components/ui/primitives/sidebar.tsx +1 -0
- package/src/components/ui/sidebar/app-sidebar.tsx +5 -1
- package/src/components/ui/sidebar/icon-rail.tsx +50 -31
- package/src/components/ui/sidebar/panels/plugins-panel.tsx +218 -0
- package/src/components/ui/sidebar/panels/settings-panel/index.tsx +71 -40
- package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +60 -6
- package/src/components/ui/sidebar/panels/settings-panel/load-build-dialog.tsx +12 -4
- package/src/components/ui/sidebar/panels/site-panel/ceiling-tree-node.tsx +4 -3
- package/src/components/ui/sidebar/panels/site-panel/chimney-tree-node.tsx +10 -7
- package/src/components/ui/sidebar/panels/site-panel/column-tree-node.tsx +1 -1
- package/src/components/ui/sidebar/panels/site-panel/door-tree-node.tsx +7 -2
- package/src/components/ui/sidebar/panels/site-panel/dormer-tree-node.tsx +10 -7
- package/src/components/ui/sidebar/panels/site-panel/elevator-tree-node.tsx +1 -1
- package/src/components/ui/sidebar/panels/site-panel/fence-tree-node.tsx +1 -1
- package/src/components/ui/sidebar/panels/site-panel/gutter-tree-node.tsx +10 -7
- package/src/components/ui/sidebar/panels/site-panel/index.tsx +40 -17
- package/src/components/ui/sidebar/panels/site-panel/item-tree-node.tsx +31 -17
- package/src/components/ui/sidebar/panels/site-panel/level-tree-node.tsx +1 -1
- package/src/components/ui/sidebar/panels/site-panel/registry-tree-node.tsx +72 -25
- package/src/components/ui/sidebar/panels/site-panel/roof-tree-node.tsx +7 -3
- package/src/components/ui/sidebar/panels/site-panel/shelf-tree-node.tsx +12 -8
- package/src/components/ui/sidebar/panels/site-panel/slab-tree-node.tsx +4 -3
- package/src/components/ui/sidebar/panels/site-panel/solar-panel-tree-node.tsx +10 -7
- package/src/components/ui/sidebar/panels/site-panel/spawn-tree-node.tsx +1 -1
- package/src/components/ui/sidebar/panels/site-panel/stair-tree-node.tsx +2 -2
- package/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +44 -3
- package/src/components/ui/sidebar/panels/site-panel/tree-structure.ts +36 -0
- package/src/components/ui/sidebar/panels/site-panel/wall-tree-node.tsx +1 -1
- package/src/components/ui/sidebar/panels/site-panel/window-tree-node.tsx +7 -2
- package/src/components/ui/sidebar/panels/site-panel/zone-tree-node.tsx +3 -3
- package/src/components/ui/sidebar/tab-bar.tsx +42 -28
- package/src/components/ui/sidebar/use-plugin-panels.tsx +132 -0
- package/src/components/ui/snap-target-badge.test.tsx +40 -0
- package/src/components/ui/snap-target-badge.tsx +88 -0
- package/src/components/viewer-overlay.tsx +85 -10
- package/src/components/viewer-zone-system.tsx +6 -1
- package/src/hooks/use-auto-save.test.ts +14 -0
- package/src/hooks/use-auto-save.ts +54 -7
- package/src/hooks/use-ceiling-events.ts +3 -2
- package/src/hooks/use-drag-action.ts +4 -1
- package/src/hooks/use-keyboard.ts +385 -40
- package/src/index.tsx +219 -6
- package/src/lib/active-placement-surface.ts +35 -0
- package/src/lib/ceiling-plan-snap.ts +24 -0
- package/src/lib/contextual-help.test.ts +112 -0
- package/src/lib/contextual-help.ts +144 -0
- package/src/lib/continuation.ts +64 -0
- package/src/lib/direct-manipulation.test.ts +192 -0
- package/src/lib/direct-manipulation.ts +109 -0
- package/src/lib/editor-api.ts +23 -35
- package/src/lib/floorplan/apply-alignment.test.ts +25 -0
- package/src/lib/floorplan/apply-alignment.ts +18 -12
- package/src/lib/floorplan/floorplan-export.tsx +364 -0
- package/src/lib/floorplan/geometry.ts +53 -0
- package/src/lib/floorplan/index.ts +3 -0
- package/src/lib/floorplan/items.ts +5 -2
- package/src/lib/floorplan/plan-coords.ts +21 -0
- package/src/lib/fresh-planar-placement.test.ts +240 -3
- package/src/lib/fresh-planar-placement.ts +68 -1
- package/src/lib/glb-export.test.ts +441 -0
- package/src/lib/glb-export.ts +874 -0
- package/src/lib/history.ts +9 -0
- package/src/lib/interaction/hot-set.test.ts +133 -0
- package/src/lib/interaction/hot-set.ts +67 -0
- package/src/lib/interaction/overlay-policy.test.ts +51 -0
- package/src/lib/interaction/overlay-policy.ts +59 -0
- package/src/lib/interaction/scope.ts +211 -0
- package/src/lib/level-selection.ts +2 -2
- package/src/lib/material-paint.ts +36 -49
- package/src/lib/measurement-kind.test.ts +30 -0
- package/src/lib/measurement-kind.ts +19 -0
- package/src/lib/measurement-label.test.ts +47 -0
- package/src/lib/measurement-label.ts +68 -0
- package/src/lib/measurement-parser.ts +116 -0
- package/src/lib/measurements.test.ts +183 -0
- package/src/lib/measurements.ts +199 -0
- package/src/lib/paint-scope.test.ts +454 -0
- package/src/lib/paint-scope.ts +461 -0
- package/src/lib/placement-drag-release.ts +23 -0
- package/src/lib/planar-cursor-placement.test.ts +57 -0
- package/src/lib/plugin-panels.test.ts +19 -0
- package/src/lib/plugin-panels.ts +91 -0
- package/src/lib/quick-measurement.test.ts +77 -0
- package/src/lib/quick-measurement.ts +109 -0
- package/src/lib/roof-duplication.ts +6 -6
- package/src/lib/roof-hover-outline-proxy.ts +22 -0
- package/src/lib/roof-wall-hit.ts +164 -0
- package/src/lib/scene-clipboard.test.ts +196 -0
- package/src/lib/scene-clipboard.ts +73 -8
- package/src/lib/scene.ts +28 -5
- package/src/lib/selection-routing.test.ts +298 -0
- package/src/lib/selection-routing.ts +175 -0
- package/src/lib/sfx/index.ts +1 -0
- package/src/lib/sfx/movement-tick.test.ts +65 -0
- package/src/lib/sfx/movement-tick.ts +18 -0
- package/src/lib/sfx-bus.ts +50 -13
- package/src/lib/sfx-player.test.ts +124 -0
- package/src/lib/sfx-player.ts +111 -66
- package/src/lib/slab-plan-snap.test.ts +93 -0
- package/src/lib/slab-plan-snap.ts +108 -0
- package/src/lib/snapping-mode.test.ts +138 -0
- package/src/lib/snapping-mode.ts +179 -0
- package/src/lib/stair-duplication.ts +4 -4
- package/src/lib/surface-plan-snap.test.ts +71 -0
- package/src/lib/surface-plan-snap.ts +256 -0
- package/src/lib/use-linear-display.ts +40 -0
- package/src/lib/world-grid-snap.ts +37 -5
- package/src/store/use-direct-manipulation-feedback.ts +20 -0
- package/src/store/use-editor.tsx +484 -114
- package/src/store/use-facing-pose.ts +44 -0
- package/src/store/use-fence-curve-draft.ts +21 -0
- package/src/store/use-floorplan-draft-preview.ts +101 -0
- package/src/store/use-floorplan-marquee.ts +50 -0
- package/src/store/use-interaction-scope.test.ts +186 -0
- package/src/store/use-interaction-scope.ts +132 -0
- package/src/store/use-measurement-draft.test.ts +530 -0
- package/src/store/use-measurement-draft.ts +543 -0
- package/src/store/use-opening-guides.ts +41 -0
- package/src/store/use-placement-preview.ts +11 -3
- package/src/store/use-quick-measurement-hud.test.ts +53 -0
- package/src/store/use-quick-measurement-hud.ts +77 -0
- package/src/store/use-segment-draft-chain.ts +28 -0
- package/src/store/use-stair-build-preview.ts +43 -0
- package/src/store/use-wall-snap-indicator.ts +2 -0
- package/src/components/editor/first-person/bvh-ecctrl.tsx +0 -860
- package/src/components/editor/group-move-handle.tsx +0 -316
- package/src/components/ui/panels/paint-panel.tsx +0 -163
- package/src/lib/level-name.ts +0 -11
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type AlignmentAnchor,
|
|
3
|
+
type AlignmentGuide,
|
|
4
|
+
type AnyNode,
|
|
5
|
+
collectAlignmentAnchors,
|
|
6
|
+
getWallCurveFrameAt,
|
|
7
|
+
getWallCurveLength,
|
|
8
|
+
isCurvedWall,
|
|
9
|
+
resolveLevelId,
|
|
10
|
+
useScene,
|
|
11
|
+
type WallNode,
|
|
12
|
+
} from '@pascal-app/core'
|
|
13
|
+
import {
|
|
14
|
+
getSegmentGridStep,
|
|
15
|
+
snapWallDraftPointDetailed,
|
|
16
|
+
WALL_CONNECT_SNAP_RADIUS,
|
|
17
|
+
type WallDraftSnapKind,
|
|
18
|
+
type WallPlanPoint,
|
|
19
|
+
type WallSnapRadii,
|
|
20
|
+
} from '../components/tools/wall/wall-drafting'
|
|
21
|
+
import useAlignmentGuides from '../store/use-alignment-guides'
|
|
22
|
+
import { isMagneticSnapActive } from '../store/use-editor'
|
|
23
|
+
import useWallSnapIndicator from '../store/use-wall-snap-indicator'
|
|
24
|
+
import { resolveAlignmentForFloorplanView } from './world-grid-snap'
|
|
25
|
+
|
|
26
|
+
const SURFACE_SNAP_MOVING_ID = '__surface_snap__'
|
|
27
|
+
export const SURFACE_ALIGNMENT_THRESHOLD_M = 0.08
|
|
28
|
+
const SURFACE_WALL_SNAP_RADII = {
|
|
29
|
+
endpoint: 0.38,
|
|
30
|
+
midpoint: 0.28,
|
|
31
|
+
intersection: 0.28,
|
|
32
|
+
wall: 0.18,
|
|
33
|
+
} satisfies WallSnapRadii
|
|
34
|
+
const WALL_SOURCE_MATCH_EPSILON = 0.035
|
|
35
|
+
|
|
36
|
+
export type SurfacePlanSnapInput = {
|
|
37
|
+
rawPoint: WallPlanPoint
|
|
38
|
+
fallbackPoint?: WallPlanPoint
|
|
39
|
+
levelId?: string | null
|
|
40
|
+
excludeId?: string | null
|
|
41
|
+
movingId?: string
|
|
42
|
+
nodes?: Readonly<Record<string, AnyNode>>
|
|
43
|
+
walls?: readonly WallNode[]
|
|
44
|
+
candidates?: readonly AlignmentAnchor[]
|
|
45
|
+
threshold?: number
|
|
46
|
+
magnetic?: boolean
|
|
47
|
+
align?: boolean
|
|
48
|
+
highlightWalls?: boolean
|
|
49
|
+
step?: number
|
|
50
|
+
snapRadii?: WallSnapRadii
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export type SurfacePlanSnapResult = {
|
|
54
|
+
point: WallPlanPoint
|
|
55
|
+
wallSnap: WallDraftSnapKind | null
|
|
56
|
+
guides: AlignmentGuide[]
|
|
57
|
+
wallIds: string[]
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function getLevelWalls(
|
|
61
|
+
nodes: Readonly<Record<string, AnyNode>>,
|
|
62
|
+
levelId: string | null | undefined,
|
|
63
|
+
walls?: readonly WallNode[],
|
|
64
|
+
): WallNode[] {
|
|
65
|
+
const source =
|
|
66
|
+
walls ?? Object.values(nodes).filter((node): node is WallNode => node.type === 'wall')
|
|
67
|
+
if (!levelId) return source.filter((wall) => wall.visible !== false)
|
|
68
|
+
|
|
69
|
+
return source.filter(
|
|
70
|
+
(wall) =>
|
|
71
|
+
wall.visible !== false && resolveLevelId(wall, nodes as Record<string, AnyNode>) === levelId,
|
|
72
|
+
)
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function distanceSquared(a: WallPlanPoint, b: WallPlanPoint) {
|
|
76
|
+
const dx = a[0] - b[0]
|
|
77
|
+
const dz = a[1] - b[1]
|
|
78
|
+
return dx * dx + dz * dz
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function wallMidpoint(wall: WallNode): WallPlanPoint {
|
|
82
|
+
if (isCurvedWall(wall)) {
|
|
83
|
+
const frame = getWallCurveFrameAt(wall, 0.5)
|
|
84
|
+
return [frame.point.x, frame.point.y]
|
|
85
|
+
}
|
|
86
|
+
return [(wall.start[0] + wall.end[0]) / 2, (wall.start[1] + wall.end[1]) / 2]
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function distanceToSegmentSquared(point: WallPlanPoint, start: WallPlanPoint, end: WallPlanPoint) {
|
|
90
|
+
const dx = end[0] - start[0]
|
|
91
|
+
const dz = end[1] - start[1]
|
|
92
|
+
const lengthSquared = dx * dx + dz * dz
|
|
93
|
+
if (lengthSquared < 1e-9) return distanceSquared(point, start)
|
|
94
|
+
|
|
95
|
+
const t = Math.max(
|
|
96
|
+
0,
|
|
97
|
+
Math.min(1, ((point[0] - start[0]) * dx + (point[1] - start[1]) * dz) / lengthSquared),
|
|
98
|
+
)
|
|
99
|
+
const projected: WallPlanPoint = [start[0] + dx * t, start[1] + dz * t]
|
|
100
|
+
return distanceSquared(point, projected)
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function distanceToWallSquared(point: WallPlanPoint, wall: WallNode) {
|
|
104
|
+
if (!isCurvedWall(wall)) {
|
|
105
|
+
return distanceToSegmentSquared(point, wall.start, wall.end)
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const sampleCount = Math.max(8, Math.ceil(getWallCurveLength(wall) / 0.3))
|
|
109
|
+
let bestDistanceSquared = Number.POSITIVE_INFINITY
|
|
110
|
+
let previous = getWallCurveFrameAt(wall, 0).point
|
|
111
|
+
for (let index = 1; index <= sampleCount; index += 1) {
|
|
112
|
+
const current = getWallCurveFrameAt(wall, index / sampleCount).point
|
|
113
|
+
const distance = distanceToSegmentSquared(
|
|
114
|
+
point,
|
|
115
|
+
[previous.x, previous.y],
|
|
116
|
+
[current.x, current.y],
|
|
117
|
+
)
|
|
118
|
+
bestDistanceSquared = Math.min(bestDistanceSquared, distance)
|
|
119
|
+
previous = current
|
|
120
|
+
}
|
|
121
|
+
return bestDistanceSquared
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function closestWallIds(point: WallPlanPoint, walls: readonly WallNode[], count: number) {
|
|
125
|
+
return walls
|
|
126
|
+
.map((wall) => ({ id: wall.id, distance: distanceToWallSquared(point, wall) }))
|
|
127
|
+
.sort((a, b) => a.distance - b.distance)
|
|
128
|
+
.slice(0, count)
|
|
129
|
+
.map(({ id }) => id)
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function findSnapSourceWallIds(
|
|
133
|
+
point: WallPlanPoint,
|
|
134
|
+
kind: WallDraftSnapKind,
|
|
135
|
+
walls: readonly WallNode[],
|
|
136
|
+
): string[] {
|
|
137
|
+
const epsilonSquared = WALL_SOURCE_MATCH_EPSILON ** 2
|
|
138
|
+
|
|
139
|
+
if (kind === 'endpoint') {
|
|
140
|
+
const endpointMatches = walls.filter(
|
|
141
|
+
(wall) =>
|
|
142
|
+
distanceSquared(point, wall.start) <= epsilonSquared ||
|
|
143
|
+
distanceSquared(point, wall.end) <= epsilonSquared,
|
|
144
|
+
)
|
|
145
|
+
if (endpointMatches.length > 0) return endpointMatches.map((wall) => wall.id)
|
|
146
|
+
return closestWallIds(point, walls, 1)
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
if (kind === 'midpoint') {
|
|
150
|
+
const midpointMatches = walls.filter(
|
|
151
|
+
(wall) => distanceSquared(point, wallMidpoint(wall)) <= epsilonSquared,
|
|
152
|
+
)
|
|
153
|
+
if (midpointMatches.length > 0) return midpointMatches.map((wall) => wall.id)
|
|
154
|
+
return closestWallIds(point, walls, 1)
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
if (kind === 'intersection') {
|
|
158
|
+
const crossingMatches = walls.filter(
|
|
159
|
+
(wall) => distanceToWallSquared(point, wall) <= epsilonSquared,
|
|
160
|
+
)
|
|
161
|
+
if (crossingMatches.length > 0) return crossingMatches.map((wall) => wall.id).slice(0, 2)
|
|
162
|
+
return closestWallIds(point, walls, 2)
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
return closestWallIds(point, walls, 1)
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
export function clearSurfacePlanSnapFeedback() {
|
|
169
|
+
useAlignmentGuides.getState().clear()
|
|
170
|
+
useWallSnapIndicator.getState().clear()
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
export function resolveSurfacePlanPointSnap(input: SurfacePlanSnapInput): SurfacePlanSnapResult {
|
|
174
|
+
const nodes = input.nodes ?? useScene.getState().nodes
|
|
175
|
+
const walls = getLevelWalls(nodes, input.levelId, input.walls)
|
|
176
|
+
const fallbackPoint = input.fallbackPoint
|
|
177
|
+
const magnetic = input.magnetic ?? isMagneticSnapActive()
|
|
178
|
+
|
|
179
|
+
const wallSnap = snapWallDraftPointDetailed({
|
|
180
|
+
point: input.rawPoint,
|
|
181
|
+
walls,
|
|
182
|
+
step: input.step ?? getSegmentGridStep(),
|
|
183
|
+
magnetic,
|
|
184
|
+
snapRadii: input.snapRadii ?? SURFACE_WALL_SNAP_RADII,
|
|
185
|
+
gridSnap: fallbackPoint ? () => fallbackPoint : undefined,
|
|
186
|
+
})
|
|
187
|
+
|
|
188
|
+
if (wallSnap.snap) {
|
|
189
|
+
const wallIds =
|
|
190
|
+
input.highlightWalls === false
|
|
191
|
+
? []
|
|
192
|
+
: findSnapSourceWallIds(wallSnap.point, wallSnap.snap, walls)
|
|
193
|
+
useWallSnapIndicator.getState().set({
|
|
194
|
+
x: wallSnap.point[0],
|
|
195
|
+
z: wallSnap.point[1],
|
|
196
|
+
kind: wallSnap.snap,
|
|
197
|
+
...(wallIds.length > 0 ? { wallIds } : {}),
|
|
198
|
+
})
|
|
199
|
+
useAlignmentGuides.getState().clear()
|
|
200
|
+
return { point: wallSnap.point, wallSnap: wallSnap.snap, guides: [], wallIds }
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
useWallSnapIndicator.getState().clear()
|
|
204
|
+
|
|
205
|
+
// Alignment "lines" are DISPLAYED in every mode (grid / lines / angles /
|
|
206
|
+
// off); the magnetic pull onto an axis is applied only when magnetic
|
|
207
|
+
// ('lines'). `align: false` fully bypasses (no guides).
|
|
208
|
+
const basePoint = fallbackPoint ?? wallSnap.point
|
|
209
|
+
if (input.align === false) {
|
|
210
|
+
useAlignmentGuides.getState().clear()
|
|
211
|
+
return { point: basePoint, wallSnap: null, guides: [], wallIds: [] }
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const movingId = input.movingId ?? SURFACE_SNAP_MOVING_ID
|
|
215
|
+
const allCandidates =
|
|
216
|
+
input.candidates ??
|
|
217
|
+
collectAlignmentAnchors(nodes, input.excludeId ?? movingId, input.levelId ?? null)
|
|
218
|
+
|
|
219
|
+
// In non-magnetic modes nothing pulls the point onto a guide, so restrict
|
|
220
|
+
// alignment to anchors already within connect distance — a corner then reads
|
|
221
|
+
// no differently from any other nearby point, and far corners stop lighting up
|
|
222
|
+
// from across the plan. Magnetic ('lines') keeps the full-range guides since
|
|
223
|
+
// its snap closes the gap. Mirrors the wall draft/endpoint tools. Filtering the
|
|
224
|
+
// candidates (both local-frame, like `basePoint`) rather than the resolved
|
|
225
|
+
// guides avoids the floor-plan view rotation baked into the guide coords.
|
|
226
|
+
const candidates = magnetic
|
|
227
|
+
? allCandidates
|
|
228
|
+
: allCandidates.filter(
|
|
229
|
+
(candidate) =>
|
|
230
|
+
distanceSquared(basePoint, [candidate.x, candidate.z]) <= WALL_CONNECT_SNAP_RADIUS ** 2,
|
|
231
|
+
)
|
|
232
|
+
|
|
233
|
+
if (candidates.length === 0) {
|
|
234
|
+
useAlignmentGuides.getState().clear()
|
|
235
|
+
return { point: basePoint, wallSnap: null, guides: [], wallIds: [] }
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
const alignment = resolveAlignmentForFloorplanView({
|
|
239
|
+
moving: [{ nodeId: movingId, kind: 'corner', x: basePoint[0], z: basePoint[1] }],
|
|
240
|
+
candidates,
|
|
241
|
+
threshold: input.threshold ?? SURFACE_ALIGNMENT_THRESHOLD_M,
|
|
242
|
+
})
|
|
243
|
+
|
|
244
|
+
useAlignmentGuides.getState().set(alignment.guides)
|
|
245
|
+
|
|
246
|
+
if (!alignment.snap || !magnetic) {
|
|
247
|
+
return { point: basePoint, wallSnap: null, guides: alignment.guides, wallIds: [] }
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
return {
|
|
251
|
+
point: [basePoint[0] + alignment.snap.dx, basePoint[1] + alignment.snap.dz],
|
|
252
|
+
wallSnap: null,
|
|
253
|
+
guides: alignment.guides,
|
|
254
|
+
wallIds: [],
|
|
255
|
+
}
|
|
256
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { useViewer } from '@pascal-app/viewer'
|
|
4
|
+
import { useCallback } from 'react'
|
|
5
|
+
import { getLinearUnitLabel, linearUnitToMeters, metersToLinearUnit } from './measurements'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Shared display/storage conversion for numeric property controls so that
|
|
9
|
+
* every length input honors the metric/imperial toggle identically.
|
|
10
|
+
*
|
|
11
|
+
* Values are always STORED in the field's own unit (meters for `unit === 'm'`).
|
|
12
|
+
* When the viewer preference is imperial AND the field is a meter length, the
|
|
13
|
+
* value is DISPLAYED (and edited) in feet; otherwise the conversions are the
|
|
14
|
+
* identity, so metric fields and non-length units (`'°'`, `'%'`, `'in'`, `''`,
|
|
15
|
+
* …) behave exactly as before.
|
|
16
|
+
*
|
|
17
|
+
* Used by both `SliderControl` and `MetricControl` — keep the two in sync via
|
|
18
|
+
* this single source of truth.
|
|
19
|
+
*/
|
|
20
|
+
export function useLinearDisplay(unit: string, precision: number) {
|
|
21
|
+
const viewerUnit = useViewer((state) => state.unit)
|
|
22
|
+
const isImperial = viewerUnit === 'imperial' && unit === 'm'
|
|
23
|
+
const displayUnit = isImperial ? getLinearUnitLabel('imperial') : unit
|
|
24
|
+
|
|
25
|
+
const toDisplay = useCallback(
|
|
26
|
+
(stored: number) => (isImperial ? metersToLinearUnit(stored, 'imperial') : stored),
|
|
27
|
+
[isImperial],
|
|
28
|
+
)
|
|
29
|
+
const toStored = useCallback(
|
|
30
|
+
(display: number) => (isImperial ? linearUnitToMeters(display, 'imperial') : display),
|
|
31
|
+
[isImperial],
|
|
32
|
+
)
|
|
33
|
+
// Round a stored value so it lands on a clean number of DISPLAY-unit digits.
|
|
34
|
+
const roundStored = useCallback(
|
|
35
|
+
(stored: number) => toStored(Number.parseFloat(toDisplay(stored).toFixed(precision))),
|
|
36
|
+
[toDisplay, toStored, precision],
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
return { isImperial, displayUnit, toDisplay, toStored, roundStored }
|
|
40
|
+
}
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
*/
|
|
11
11
|
import {
|
|
12
12
|
type AlignmentAnchor,
|
|
13
|
+
type AlignmentGuide,
|
|
13
14
|
type AnyNodeId,
|
|
14
15
|
type BuildingPose,
|
|
15
16
|
type ResolveAlignmentInBuildingResult,
|
|
@@ -54,7 +55,7 @@ export function getActiveBuildingPose(): BuildingPose | null {
|
|
|
54
55
|
}
|
|
55
56
|
if (!buildingId) buildingId = sel.buildingId ?? null
|
|
56
57
|
const building = buildingId ? nodes[buildingId] : null
|
|
57
|
-
if (
|
|
58
|
+
if (building?.type !== 'building') return null
|
|
58
59
|
const live = useLiveTransforms.getState().transforms.get(buildingId as string)
|
|
59
60
|
return {
|
|
60
61
|
position: live?.position ?? building.position,
|
|
@@ -80,12 +81,43 @@ export function resolveAlignmentForActiveBuilding(args: {
|
|
|
80
81
|
return resolveAlignmentInBuildingWorld({ ...args, pose: getActiveBuildingPose() })
|
|
81
82
|
}
|
|
82
83
|
|
|
84
|
+
function worldXZToPoseLocal(x: number, z: number, pose: BuildingPose | null): [number, number] {
|
|
85
|
+
if (!pose) return [x, z]
|
|
86
|
+
const cos = Math.cos(pose.rotationY)
|
|
87
|
+
const sin = Math.sin(pose.rotationY)
|
|
88
|
+
const dx = x - pose.position[0]
|
|
89
|
+
const dz = z - pose.position[2]
|
|
90
|
+
return [dx * cos - dz * sin, dx * sin + dz * cos]
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Project WORLD-frame alignment guides into the active building's LOCAL frame.
|
|
95
|
+
*
|
|
96
|
+
* The 3D alignment layer is still mounted inside the building-local tool group,
|
|
97
|
+
* so tools that resolve alignment on the world axes (item placement, slab move)
|
|
98
|
+
* need their guides converted before publishing to `useAlignmentGuides`.
|
|
99
|
+
*/
|
|
100
|
+
export function projectAlignmentGuidesWorldToActiveBuildingLocal(
|
|
101
|
+
guides: readonly AlignmentGuide[],
|
|
102
|
+
): AlignmentGuide[] {
|
|
103
|
+
const pose = getActiveBuildingPose()
|
|
104
|
+
return guides.map((guide) => {
|
|
105
|
+
const [fromX, fromZ] = worldXZToPoseLocal(guide.from.x, guide.from.z, pose)
|
|
106
|
+
const [toX, toZ] = worldXZToPoseLocal(guide.to.x, guide.to.z, pose)
|
|
107
|
+
return {
|
|
108
|
+
...guide,
|
|
109
|
+
from: { x: fromX, z: fromZ },
|
|
110
|
+
to: { x: toX, z: toZ },
|
|
111
|
+
}
|
|
112
|
+
})
|
|
113
|
+
}
|
|
114
|
+
|
|
83
115
|
/**
|
|
84
116
|
* Baseline rotation the floor-plan view applies on top of the building
|
|
85
|
-
* rotation. Mirrors `FLOORPLAN_VIEW_ROTATION_DEG =
|
|
117
|
+
* rotation. Mirrors `FLOORPLAN_VIEW_ROTATION_DEG = 0` in lib/floorplan/geometry.ts —
|
|
86
118
|
* the scene group reads it via `floorplanSceneRotationDeg = FVR - buildingRot`.
|
|
87
119
|
*/
|
|
88
|
-
const FLOORPLAN_VIEW_ROTATION_RAD =
|
|
120
|
+
const FLOORPLAN_VIEW_ROTATION_RAD = 0
|
|
89
121
|
|
|
90
122
|
function rotateAnchorsBy(
|
|
91
123
|
anchors: readonly AlignmentAnchor[],
|
|
@@ -159,7 +191,7 @@ export function snapWorldXZForActiveBuilding(
|
|
|
159
191
|
): { world: [number, number]; local: [number, number] } {
|
|
160
192
|
const buildingId = useViewer.getState().selection.buildingId
|
|
161
193
|
const building = buildingId ? useScene.getState().nodes[buildingId] : null
|
|
162
|
-
if (
|
|
194
|
+
if (building?.type !== 'building') {
|
|
163
195
|
if (step <= 0) return { world: [worldX, worldZ], local: [worldX, worldZ] }
|
|
164
196
|
const sx = Math.round(worldX / step) * step
|
|
165
197
|
const sz = Math.round(worldZ / step) * step
|
|
@@ -186,7 +218,7 @@ export function snapBuildingLocalToWorldGrid(
|
|
|
186
218
|
): [number, number] {
|
|
187
219
|
const buildingId = useViewer.getState().selection.buildingId
|
|
188
220
|
const building = buildingId ? useScene.getState().nodes[buildingId] : null
|
|
189
|
-
if (
|
|
221
|
+
if (building?.type !== 'building') {
|
|
190
222
|
if (step <= 0) return [local[0], local[1]]
|
|
191
223
|
return [Math.round(local[0] / step) * step, Math.round(local[1] / step) * step]
|
|
192
224
|
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { AnyNodeId } from '@pascal-app/core'
|
|
2
|
+
import { create } from 'zustand'
|
|
3
|
+
|
|
4
|
+
type DirectManipulationFeedbackState = {
|
|
5
|
+
activeRotateNodeId: AnyNodeId | null
|
|
6
|
+
setActiveRotateNodeId(nodeId: AnyNodeId | null): void
|
|
7
|
+
clearActiveRotateNodeId(nodeId?: AnyNodeId): void
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const useDirectManipulationFeedback = create<DirectManipulationFeedbackState>((set) => ({
|
|
11
|
+
activeRotateNodeId: null,
|
|
12
|
+
setActiveRotateNodeId: (activeRotateNodeId) => set({ activeRotateNodeId }),
|
|
13
|
+
clearActiveRotateNodeId: (nodeId) =>
|
|
14
|
+
set((state) => {
|
|
15
|
+
if (nodeId !== undefined && state.activeRotateNodeId !== nodeId) return {}
|
|
16
|
+
return { activeRotateNodeId: null }
|
|
17
|
+
}),
|
|
18
|
+
}))
|
|
19
|
+
|
|
20
|
+
export default useDirectManipulationFeedback
|