@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
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import {
|
|
2
|
+
type AlignmentAnchor,
|
|
2
3
|
type AnyNode,
|
|
3
4
|
type AnyNodeId,
|
|
4
5
|
collectAlignmentAnchors,
|
|
@@ -7,33 +8,93 @@ import {
|
|
|
7
8
|
type LevelNode,
|
|
8
9
|
RoofNode,
|
|
9
10
|
RoofSegmentNode,
|
|
11
|
+
resolveBuildingForLevel,
|
|
10
12
|
sceneRegistry,
|
|
11
|
-
snapScalar,
|
|
12
13
|
useScene,
|
|
14
|
+
type WallNode,
|
|
15
|
+
wallSegmentAnchors,
|
|
13
16
|
} from '@pascal-app/core'
|
|
14
|
-
import {
|
|
17
|
+
import { clearSurfacePlanSnapFeedback, resolveSurfacePlanPointSnap } from '@pascal-app/editor'
|
|
15
18
|
import { useViewer } from '@pascal-app/viewer'
|
|
16
19
|
import { useEffect, useMemo, useRef, useState } from 'react'
|
|
17
20
|
import * as THREE from 'three'
|
|
18
|
-
import {
|
|
21
|
+
import {
|
|
22
|
+
BufferGeometry,
|
|
23
|
+
DoubleSide,
|
|
24
|
+
Float32BufferAttribute,
|
|
25
|
+
type Group,
|
|
26
|
+
type Line,
|
|
27
|
+
Vector3,
|
|
28
|
+
} from 'three'
|
|
19
29
|
import { markToolCancelConsumed } from '../../../hooks/use-keyboard'
|
|
20
30
|
import { EDITOR_LAYER } from '../../../lib/constants'
|
|
21
31
|
import { sfxEmitter } from '../../../lib/sfx-bus'
|
|
22
|
-
import {
|
|
23
|
-
|
|
24
|
-
snapWorldXZForActiveBuilding,
|
|
25
|
-
} from '../../../lib/world-grid-snap'
|
|
26
|
-
import useEditor from '../../../store/use-editor'
|
|
32
|
+
import { snapWorldXZForActiveBuilding } from '../../../lib/world-grid-snap'
|
|
33
|
+
import useEditor, { isGridSnapActive, isMagneticSnapActive } from '../../../store/use-editor'
|
|
27
34
|
import { CursorSphere } from '../shared/cursor-sphere'
|
|
28
35
|
|
|
29
36
|
const DEFAULT_WALL_HEIGHT = 0.5
|
|
30
37
|
const DEFAULT_PITCH_DEG = 40
|
|
31
38
|
const GRID_OFFSET = 0.02
|
|
32
|
-
/** Figma-style alignment-snap threshold (meters), matching the move tools. */
|
|
33
|
-
const ALIGNMENT_THRESHOLD_M = 0.08
|
|
34
39
|
|
|
35
|
-
|
|
36
|
-
|
|
40
|
+
// Walls that are direct children of a level.
|
|
41
|
+
function getLevelWalls(
|
|
42
|
+
levelId: string | null,
|
|
43
|
+
nodes: Readonly<Record<string, AnyNode>>,
|
|
44
|
+
): WallNode[] {
|
|
45
|
+
if (!levelId) return []
|
|
46
|
+
const levelNode = nodes[levelId]
|
|
47
|
+
if (levelNode?.type !== 'level') return []
|
|
48
|
+
return (levelNode as LevelNode).children
|
|
49
|
+
.map((childId) => nodes[childId])
|
|
50
|
+
.filter((node): node is WallNode => node?.type === 'wall')
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Walls on the level directly beneath the active one. Levels share the same
|
|
54
|
+
// local XZ origin (they only differ in world Y), so these walls live in the
|
|
55
|
+
// identical coordinate frame and feed straight into both the alignment pool
|
|
56
|
+
// and the magnetic wall-snap pipeline — letting a roof drawn on the upper
|
|
57
|
+
// floor snap onto the wall corners of the floor below.
|
|
58
|
+
function getBelowLevelWalls(
|
|
59
|
+
currentLevelId: string | null,
|
|
60
|
+
nodes: Readonly<Record<string, AnyNode>>,
|
|
61
|
+
): WallNode[] {
|
|
62
|
+
if (!currentLevelId) return []
|
|
63
|
+
const currentLevel = nodes[currentLevelId]
|
|
64
|
+
if (currentLevel?.type !== 'level') return []
|
|
65
|
+
const buildingId = resolveBuildingForLevel(currentLevel.id, nodes)
|
|
66
|
+
if (!buildingId) return []
|
|
67
|
+
const building = nodes[buildingId]
|
|
68
|
+
if (building?.type !== 'building') return []
|
|
69
|
+
const currentIndex = (currentLevel as LevelNode).level
|
|
70
|
+
const belowLevel = (building.children ?? [])
|
|
71
|
+
.map((childId) => nodes[childId])
|
|
72
|
+
.filter((node): node is LevelNode => node?.type === 'level' && node.level < currentIndex)
|
|
73
|
+
.sort((a, b) => b.level - a.level)[0]
|
|
74
|
+
return getLevelWalls(belowLevel?.id ?? null, nodes)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Current-level + floor-below walls — the magnetic snap targets the roof draft
|
|
78
|
+
// locks onto (corners, midpoints, crossings, wall bodies), matching the wall
|
|
79
|
+
// tool. Same coordinate frame, so no transform is needed.
|
|
80
|
+
function getRoofSnapWalls(
|
|
81
|
+
currentLevelId: string | null,
|
|
82
|
+
nodes: Readonly<Record<string, AnyNode>>,
|
|
83
|
+
): WallNode[] {
|
|
84
|
+
return [...getLevelWalls(currentLevelId, nodes), ...getBelowLevelWalls(currentLevelId, nodes)]
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Current-level alignment anchors plus the floor-below wall corners.
|
|
88
|
+
function collectRoofAlignmentAnchors(
|
|
89
|
+
nodes: Readonly<Record<string, AnyNode>>,
|
|
90
|
+
currentLevelId: string | null,
|
|
91
|
+
): AlignmentAnchor[] {
|
|
92
|
+
return [
|
|
93
|
+
...collectAlignmentAnchors(nodes, '', currentLevelId),
|
|
94
|
+
...getBelowLevelWalls(currentLevelId, nodes).flatMap((wall) =>
|
|
95
|
+
wallSegmentAnchors(wall.id, wall.start, wall.end, wall.thickness),
|
|
96
|
+
),
|
|
97
|
+
]
|
|
37
98
|
}
|
|
38
99
|
|
|
39
100
|
/**
|
|
@@ -148,14 +209,168 @@ type PreviewState = {
|
|
|
148
209
|
levelY: number
|
|
149
210
|
}
|
|
150
211
|
|
|
212
|
+
function buildRoofGhostGeometry(
|
|
213
|
+
width: number,
|
|
214
|
+
depth: number,
|
|
215
|
+
wallHeight: number,
|
|
216
|
+
pitchDeg: number,
|
|
217
|
+
) {
|
|
218
|
+
const safeWidth = Math.max(width, 0.1)
|
|
219
|
+
const safeDepth = Math.max(depth, 0.1)
|
|
220
|
+
const halfWidth = safeWidth / 2
|
|
221
|
+
const halfDepth = safeDepth / 2
|
|
222
|
+
const ridgeHeight = wallHeight + Math.tan((pitchDeg * Math.PI) / 180) * halfDepth
|
|
223
|
+
|
|
224
|
+
const vertices = [
|
|
225
|
+
// Front slope
|
|
226
|
+
-halfWidth,
|
|
227
|
+
wallHeight,
|
|
228
|
+
-halfDepth,
|
|
229
|
+
halfWidth,
|
|
230
|
+
wallHeight,
|
|
231
|
+
-halfDepth,
|
|
232
|
+
halfWidth,
|
|
233
|
+
ridgeHeight,
|
|
234
|
+
0,
|
|
235
|
+
|
|
236
|
+
-halfWidth,
|
|
237
|
+
wallHeight,
|
|
238
|
+
-halfDepth,
|
|
239
|
+
halfWidth,
|
|
240
|
+
ridgeHeight,
|
|
241
|
+
0,
|
|
242
|
+
-halfWidth,
|
|
243
|
+
ridgeHeight,
|
|
244
|
+
0,
|
|
245
|
+
|
|
246
|
+
// Back slope
|
|
247
|
+
-halfWidth,
|
|
248
|
+
ridgeHeight,
|
|
249
|
+
0,
|
|
250
|
+
halfWidth,
|
|
251
|
+
ridgeHeight,
|
|
252
|
+
0,
|
|
253
|
+
halfWidth,
|
|
254
|
+
wallHeight,
|
|
255
|
+
halfDepth,
|
|
256
|
+
|
|
257
|
+
-halfWidth,
|
|
258
|
+
ridgeHeight,
|
|
259
|
+
0,
|
|
260
|
+
halfWidth,
|
|
261
|
+
wallHeight,
|
|
262
|
+
halfDepth,
|
|
263
|
+
-halfWidth,
|
|
264
|
+
wallHeight,
|
|
265
|
+
halfDepth,
|
|
266
|
+
|
|
267
|
+
// Left gable
|
|
268
|
+
-halfWidth,
|
|
269
|
+
wallHeight,
|
|
270
|
+
-halfDepth,
|
|
271
|
+
-halfWidth,
|
|
272
|
+
ridgeHeight,
|
|
273
|
+
0,
|
|
274
|
+
-halfWidth,
|
|
275
|
+
wallHeight,
|
|
276
|
+
halfDepth,
|
|
277
|
+
|
|
278
|
+
// Right gable
|
|
279
|
+
halfWidth,
|
|
280
|
+
wallHeight,
|
|
281
|
+
-halfDepth,
|
|
282
|
+
halfWidth,
|
|
283
|
+
wallHeight,
|
|
284
|
+
halfDepth,
|
|
285
|
+
halfWidth,
|
|
286
|
+
ridgeHeight,
|
|
287
|
+
0,
|
|
288
|
+
]
|
|
289
|
+
|
|
290
|
+
const geometry = new BufferGeometry()
|
|
291
|
+
geometry.setAttribute('position', new Float32BufferAttribute(vertices, 3))
|
|
292
|
+
geometry.computeVertexNormals()
|
|
293
|
+
return geometry
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
function buildRoofGhostEdges(width: number, depth: number, wallHeight: number, pitchDeg: number) {
|
|
297
|
+
const safeWidth = Math.max(width, 0.1)
|
|
298
|
+
const safeDepth = Math.max(depth, 0.1)
|
|
299
|
+
const halfWidth = safeWidth / 2
|
|
300
|
+
const halfDepth = safeDepth / 2
|
|
301
|
+
const ridgeHeight = wallHeight + Math.tan((pitchDeg * Math.PI) / 180) * halfDepth
|
|
302
|
+
|
|
303
|
+
const vertices = [
|
|
304
|
+
// Base rectangle
|
|
305
|
+
-halfWidth,
|
|
306
|
+
wallHeight,
|
|
307
|
+
-halfDepth,
|
|
308
|
+
halfWidth,
|
|
309
|
+
wallHeight,
|
|
310
|
+
-halfDepth,
|
|
311
|
+
halfWidth,
|
|
312
|
+
wallHeight,
|
|
313
|
+
-halfDepth,
|
|
314
|
+
halfWidth,
|
|
315
|
+
wallHeight,
|
|
316
|
+
halfDepth,
|
|
317
|
+
halfWidth,
|
|
318
|
+
wallHeight,
|
|
319
|
+
halfDepth,
|
|
320
|
+
-halfWidth,
|
|
321
|
+
wallHeight,
|
|
322
|
+
halfDepth,
|
|
323
|
+
-halfWidth,
|
|
324
|
+
wallHeight,
|
|
325
|
+
halfDepth,
|
|
326
|
+
-halfWidth,
|
|
327
|
+
wallHeight,
|
|
328
|
+
-halfDepth,
|
|
329
|
+
|
|
330
|
+
// Ridge + gable edges
|
|
331
|
+
-halfWidth,
|
|
332
|
+
ridgeHeight,
|
|
333
|
+
0,
|
|
334
|
+
halfWidth,
|
|
335
|
+
ridgeHeight,
|
|
336
|
+
0,
|
|
337
|
+
-halfWidth,
|
|
338
|
+
wallHeight,
|
|
339
|
+
-halfDepth,
|
|
340
|
+
-halfWidth,
|
|
341
|
+
ridgeHeight,
|
|
342
|
+
0,
|
|
343
|
+
-halfWidth,
|
|
344
|
+
ridgeHeight,
|
|
345
|
+
0,
|
|
346
|
+
-halfWidth,
|
|
347
|
+
wallHeight,
|
|
348
|
+
halfDepth,
|
|
349
|
+
halfWidth,
|
|
350
|
+
wallHeight,
|
|
351
|
+
-halfDepth,
|
|
352
|
+
halfWidth,
|
|
353
|
+
ridgeHeight,
|
|
354
|
+
0,
|
|
355
|
+
halfWidth,
|
|
356
|
+
ridgeHeight,
|
|
357
|
+
0,
|
|
358
|
+
halfWidth,
|
|
359
|
+
wallHeight,
|
|
360
|
+
halfDepth,
|
|
361
|
+
]
|
|
362
|
+
|
|
363
|
+
const geometry = new BufferGeometry()
|
|
364
|
+
geometry.setAttribute('position', new Float32BufferAttribute(vertices, 3))
|
|
365
|
+
return geometry
|
|
366
|
+
}
|
|
367
|
+
|
|
151
368
|
export const RoofTool: React.FC = () => {
|
|
152
369
|
const cursorRef = useRef<Group>(null)
|
|
153
370
|
const outlineRef = useRef<Line>(null!)
|
|
154
371
|
const currentLevelId = useViewer((state) => state.selection.levelId)
|
|
155
372
|
const selectedIds = useViewer((state) => state.selection.selectedIds)
|
|
156
373
|
const setSelection = useViewer((state) => state.setSelection)
|
|
157
|
-
const setTool = useEditor((state) => state.setTool)
|
|
158
|
-
const setMode = useEditor((state) => state.setMode)
|
|
159
374
|
|
|
160
375
|
const selectedIdsRef = useRef(selectedIds)
|
|
161
376
|
useEffect(() => {
|
|
@@ -179,43 +394,38 @@ export const RoofTool: React.FC = () => {
|
|
|
179
394
|
|
|
180
395
|
outlineRef.current.geometry = new BufferGeometry()
|
|
181
396
|
|
|
182
|
-
// Alignment candidates — anchors of every alignable object
|
|
183
|
-
//
|
|
184
|
-
|
|
185
|
-
//
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
//
|
|
189
|
-
//
|
|
190
|
-
//
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
397
|
+
// Alignment candidates — anchors of every alignable object on the active
|
|
398
|
+
// level plus the wall corners of the floor directly below, so a roof drawn
|
|
399
|
+
// on the upper floor aligns to the walls beneath it. Refreshed after each
|
|
400
|
+
// roof commits. Both corners of the rectangle align.
|
|
401
|
+
let alignmentCandidates = collectRoofAlignmentAnchors(useScene.getState().nodes, currentLevelId)
|
|
402
|
+
|
|
403
|
+
// Resolve a grid:move/click into the drafted corner via the shared surface
|
|
404
|
+
// snap pipeline: magnetic lock onto wall corners / midpoints / crossings /
|
|
405
|
+
// bodies on the active level + floor below (raising the green beacon),
|
|
406
|
+
// falling back to alignment guides, then to the world-grid snap. The same
|
|
407
|
+
// path the slab/ceiling tools use, so the beacon and coloring match. The
|
|
408
|
+
// pipeline reads the snapping mode itself (Shift bypass, magnetic on/off),
|
|
409
|
+
// so this tool never inspects the flags. `levelId` is intentionally omitted
|
|
410
|
+
// so the explicit floor-below `walls` aren't filtered back out.
|
|
411
|
+
const resolveDraftPoint = (event: GridEvent): [number, number] => {
|
|
412
|
+
const rawPoint: [number, number] = [event.localPosition[0], event.localPosition[2]]
|
|
413
|
+
const gridFallback: [number, number] = isGridSnapActive()
|
|
414
|
+
? snapWorldXZForActiveBuilding(
|
|
415
|
+
event.position[0],
|
|
416
|
+
event.position[2],
|
|
417
|
+
useEditor.getState().gridSnapStep,
|
|
418
|
+
).local
|
|
419
|
+
: rawPoint
|
|
420
|
+
const nodes = useScene.getState().nodes
|
|
421
|
+
return resolveSurfacePlanPointSnap({
|
|
422
|
+
rawPoint,
|
|
423
|
+
fallbackPoint: gridFallback,
|
|
424
|
+
walls: getRoofSnapWalls(currentLevelId, nodes),
|
|
204
425
|
candidates: alignmentCandidates,
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
useAlignmentGuides.getState().clear()
|
|
209
|
-
return [gridX, gridZ]
|
|
210
|
-
}
|
|
211
|
-
useAlignmentGuides.getState().set(ar.guides)
|
|
212
|
-
let x = gridX
|
|
213
|
-
let z = gridZ
|
|
214
|
-
for (const guide of ar.guides) {
|
|
215
|
-
if (guide.axis === 'x') x = guide.coord
|
|
216
|
-
else z = guide.coord
|
|
217
|
-
}
|
|
218
|
-
return [x, z]
|
|
426
|
+
movingId: '__roof-draft__',
|
|
427
|
+
highlightWalls: true,
|
|
428
|
+
}).point
|
|
219
429
|
}
|
|
220
430
|
|
|
221
431
|
const updateOutline = (
|
|
@@ -240,20 +450,7 @@ export const RoofTool: React.FC = () => {
|
|
|
240
450
|
const onGridMove = (event: GridEvent) => {
|
|
241
451
|
if (!cursorRef.current) return
|
|
242
452
|
|
|
243
|
-
|
|
244
|
-
// used to drag every roof corner off the visible grid.
|
|
245
|
-
const snapped = snapWorldXZForActiveBuilding(
|
|
246
|
-
event.position[0],
|
|
247
|
-
event.position[2],
|
|
248
|
-
useEditor.getState().gridSnapStep,
|
|
249
|
-
).local
|
|
250
|
-
const [gridX, gridZ] = alignPoint(
|
|
251
|
-
snapped[0],
|
|
252
|
-
snapped[1],
|
|
253
|
-
event.localPosition[0],
|
|
254
|
-
event.localPosition[2],
|
|
255
|
-
event.nativeEvent?.altKey === true,
|
|
256
|
-
)
|
|
453
|
+
const [gridX, gridZ] = resolveDraftPoint(event)
|
|
257
454
|
const y = event.localPosition[1]
|
|
258
455
|
|
|
259
456
|
const cursorPosition: [number, number, number] = [gridX, y, gridZ]
|
|
@@ -262,6 +459,7 @@ export const RoofTool: React.FC = () => {
|
|
|
262
459
|
cursorRef.current.position.set(gridX, gridY, gridZ)
|
|
263
460
|
|
|
264
461
|
if (
|
|
462
|
+
(isGridSnapActive() || isMagneticSnapActive()) &&
|
|
265
463
|
corner1Ref.current &&
|
|
266
464
|
previousGridPosRef.current &&
|
|
267
465
|
(gridX !== previousGridPosRef.current[0] || gridZ !== previousGridPosRef.current[1])
|
|
@@ -285,20 +483,7 @@ export const RoofTool: React.FC = () => {
|
|
|
285
483
|
const onGridClick = (event: GridEvent) => {
|
|
286
484
|
if (!currentLevelId) return
|
|
287
485
|
|
|
288
|
-
|
|
289
|
-
// used to drag every roof corner off the visible grid.
|
|
290
|
-
const snapped = snapWorldXZForActiveBuilding(
|
|
291
|
-
event.position[0],
|
|
292
|
-
event.position[2],
|
|
293
|
-
useEditor.getState().gridSnapStep,
|
|
294
|
-
).local
|
|
295
|
-
const [gridX, gridZ] = alignPoint(
|
|
296
|
-
snapped[0],
|
|
297
|
-
snapped[1],
|
|
298
|
-
event.localPosition[0],
|
|
299
|
-
event.localPosition[2],
|
|
300
|
-
event.nativeEvent?.altKey === true,
|
|
301
|
-
)
|
|
486
|
+
const [gridX, gridZ] = resolveDraftPoint(event)
|
|
302
487
|
const y = event.localPosition[1]
|
|
303
488
|
|
|
304
489
|
if (corner1Ref.current) {
|
|
@@ -313,8 +498,8 @@ export const RoofTool: React.FC = () => {
|
|
|
313
498
|
|
|
314
499
|
corner1Ref.current = null
|
|
315
500
|
outlineRef.current.visible = false
|
|
316
|
-
alignmentCandidates =
|
|
317
|
-
|
|
501
|
+
alignmentCandidates = collectRoofAlignmentAnchors(useScene.getState().nodes, currentLevelId)
|
|
502
|
+
clearSurfacePlanSnapFeedback()
|
|
318
503
|
} else {
|
|
319
504
|
corner1Ref.current = [gridX, y, gridZ]
|
|
320
505
|
sfxEmitter.emit('sfx:structure-build-start')
|
|
@@ -332,7 +517,7 @@ export const RoofTool: React.FC = () => {
|
|
|
332
517
|
outlineRef.current.visible = false
|
|
333
518
|
setPreview((prev) => ({ ...prev, corner1: null }))
|
|
334
519
|
}
|
|
335
|
-
|
|
520
|
+
clearSurfacePlanSnapFeedback()
|
|
336
521
|
}
|
|
337
522
|
|
|
338
523
|
emitter.on('grid:move', onGridMove)
|
|
@@ -343,7 +528,7 @@ export const RoofTool: React.FC = () => {
|
|
|
343
528
|
emitter.off('grid:move', onGridMove)
|
|
344
529
|
emitter.off('grid:click', onGridClick)
|
|
345
530
|
emitter.off('tool:cancel', onCancel)
|
|
346
|
-
|
|
531
|
+
clearSurfacePlanSnapFeedback()
|
|
347
532
|
|
|
348
533
|
corner1Ref.current = null
|
|
349
534
|
}
|
|
@@ -360,6 +545,34 @@ export const RoofTool: React.FC = () => {
|
|
|
360
545
|
return { length, width, centerX, centerZ }
|
|
361
546
|
}, [corner1, cursorPosition])
|
|
362
547
|
|
|
548
|
+
const roofGhostGeometry = useMemo(() => {
|
|
549
|
+
if (!previewDimensions) return null
|
|
550
|
+
return buildRoofGhostGeometry(
|
|
551
|
+
previewDimensions.length,
|
|
552
|
+
previewDimensions.width,
|
|
553
|
+
DEFAULT_WALL_HEIGHT,
|
|
554
|
+
DEFAULT_PITCH_DEG,
|
|
555
|
+
)
|
|
556
|
+
}, [previewDimensions])
|
|
557
|
+
|
|
558
|
+
const roofGhostEdges = useMemo(() => {
|
|
559
|
+
if (!previewDimensions) return null
|
|
560
|
+
return buildRoofGhostEdges(
|
|
561
|
+
previewDimensions.length,
|
|
562
|
+
previewDimensions.width,
|
|
563
|
+
DEFAULT_WALL_HEIGHT,
|
|
564
|
+
DEFAULT_PITCH_DEG,
|
|
565
|
+
)
|
|
566
|
+
}, [previewDimensions])
|
|
567
|
+
|
|
568
|
+
useEffect(
|
|
569
|
+
() => () => {
|
|
570
|
+
roofGhostGeometry?.dispose()
|
|
571
|
+
roofGhostEdges?.dispose()
|
|
572
|
+
},
|
|
573
|
+
[roofGhostEdges, roofGhostGeometry],
|
|
574
|
+
)
|
|
575
|
+
|
|
363
576
|
return (
|
|
364
577
|
<group>
|
|
365
578
|
<CursorSphere ref={cursorRef} />
|
|
@@ -393,21 +606,34 @@ export const RoofTool: React.FC = () => {
|
|
|
393
606
|
)}
|
|
394
607
|
|
|
395
608
|
{previewDimensions && previewDimensions.length > 0.1 && previewDimensions.width > 0.1 && (
|
|
396
|
-
<
|
|
609
|
+
<group
|
|
397
610
|
layers={EDITOR_LAYER}
|
|
398
611
|
position={[previewDimensions.centerX, levelY + GRID_OFFSET, previewDimensions.centerZ]}
|
|
399
|
-
rotation={[-Math.PI / 2, 0, 0]}
|
|
400
612
|
>
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
613
|
+
{roofGhostGeometry && (
|
|
614
|
+
<mesh geometry={roofGhostGeometry} layers={EDITOR_LAYER} renderOrder={1}>
|
|
615
|
+
<meshBasicMaterial
|
|
616
|
+
color="#818cf8"
|
|
617
|
+
depthTest={false}
|
|
618
|
+
depthWrite={false}
|
|
619
|
+
opacity={0.16}
|
|
620
|
+
side={DoubleSide}
|
|
621
|
+
transparent
|
|
622
|
+
/>
|
|
623
|
+
</mesh>
|
|
624
|
+
)}
|
|
625
|
+
{roofGhostEdges && (
|
|
626
|
+
<lineSegments geometry={roofGhostEdges} layers={EDITOR_LAYER} renderOrder={2}>
|
|
627
|
+
<lineBasicMaterial
|
|
628
|
+
color="#818cf8"
|
|
629
|
+
depthTest={false}
|
|
630
|
+
depthWrite={false}
|
|
631
|
+
opacity={0.5}
|
|
632
|
+
transparent
|
|
633
|
+
/>
|
|
634
|
+
</lineSegments>
|
|
635
|
+
)}
|
|
636
|
+
</group>
|
|
411
637
|
)}
|
|
412
638
|
</group>
|
|
413
639
|
)
|
|
@@ -9,6 +9,10 @@ type PointerEventLike = {
|
|
|
9
9
|
nativeEvent?: PointerEvent | PointerEventLike
|
|
10
10
|
}
|
|
11
11
|
|
|
12
|
+
type SuppressBoxSelectOptions = {
|
|
13
|
+
markHandled?: boolean
|
|
14
|
+
}
|
|
15
|
+
|
|
12
16
|
function pointerIdFor(event: PointerEvent | PointerEventLike): number | null {
|
|
13
17
|
if ('pointerId' in event && typeof event.pointerId === 'number') {
|
|
14
18
|
return event.pointerId
|
|
@@ -28,8 +32,12 @@ export function markBoxSelectHandled() {
|
|
|
28
32
|
}, 50)
|
|
29
33
|
}
|
|
30
34
|
|
|
31
|
-
export function suppressBoxSelectForPointer(
|
|
32
|
-
|
|
35
|
+
export function suppressBoxSelectForPointer(
|
|
36
|
+
event: PointerEvent | PointerEventLike,
|
|
37
|
+
options: SuppressBoxSelectOptions = {},
|
|
38
|
+
) {
|
|
39
|
+
const markHandled = options.markHandled ?? true
|
|
40
|
+
if (markHandled) markBoxSelectHandled()
|
|
33
41
|
|
|
34
42
|
const pointerId = pointerIdFor(event)
|
|
35
43
|
if (pointerId === null || suppressedPointerIds.has(pointerId)) return
|
|
@@ -38,7 +46,7 @@ export function suppressBoxSelectForPointer(event: PointerEvent | PointerEventLi
|
|
|
38
46
|
|
|
39
47
|
const clear = (releaseEvent?: PointerEvent) => {
|
|
40
48
|
if (releaseEvent && releaseEvent.pointerId !== pointerId) return
|
|
41
|
-
markBoxSelectHandled()
|
|
49
|
+
if (markHandled) markBoxSelectHandled()
|
|
42
50
|
suppressedPointerIds.delete(pointerId)
|
|
43
51
|
const cleanup = suppressionCleanups.get(pointerId)
|
|
44
52
|
suppressionCleanups.delete(pointerId)
|
|
@@ -48,15 +56,18 @@ export function suppressBoxSelectForPointer(event: PointerEvent | PointerEventLi
|
|
|
48
56
|
const onPointerUp = (releaseEvent: PointerEvent) => clear(releaseEvent)
|
|
49
57
|
const onPointerCancel = (releaseEvent: PointerEvent) => clear(releaseEvent)
|
|
50
58
|
const onBlur = () => clear()
|
|
59
|
+
// Click-preserving handle interactions need suppression cleared before
|
|
60
|
+
// canvas-level pointerup handlers decide whether to block the follow-up click.
|
|
61
|
+
const releaseListenerOptions = markHandled ? undefined : { capture: true }
|
|
51
62
|
const cleanup = () => {
|
|
52
|
-
window.removeEventListener('pointerup', onPointerUp)
|
|
53
|
-
window.removeEventListener('pointercancel', onPointerCancel)
|
|
63
|
+
window.removeEventListener('pointerup', onPointerUp, releaseListenerOptions)
|
|
64
|
+
window.removeEventListener('pointercancel', onPointerCancel, releaseListenerOptions)
|
|
54
65
|
window.removeEventListener('blur', onBlur)
|
|
55
66
|
}
|
|
56
67
|
|
|
57
68
|
suppressionCleanups.set(pointerId, cleanup)
|
|
58
|
-
window.addEventListener('pointerup', onPointerUp)
|
|
59
|
-
window.addEventListener('pointercancel', onPointerCancel)
|
|
69
|
+
window.addEventListener('pointerup', onPointerUp, releaseListenerOptions)
|
|
70
|
+
window.addEventListener('pointercancel', onPointerCancel, releaseListenerOptions)
|
|
60
71
|
window.addEventListener('blur', onBlur)
|
|
61
72
|
}
|
|
62
73
|
|