@pascal-app/editor 1.0.0 → 1.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +12 -6
- package/src/components/editor/bake-exporter.tsx +4 -0
- package/src/components/editor/bake-thumbnail.tsx +4 -1
- package/src/components/editor/custom-camera-controls.tsx +22 -74
- package/src/components/editor/editor-layout-mobile.tsx +1 -0
- package/src/components/editor/floating-action-menu.tsx +14 -1
- package/src/components/editor/floorplan-panel.tsx +233 -166
- package/src/components/editor/grid.tsx +31 -7
- package/src/components/editor/group-actions.ts +5 -9
- package/src/components/editor/group-floating-action-menu.tsx +19 -9
- package/src/components/editor/group-move-3d.ts +4 -6
- package/src/components/editor/group-rotate-handle.tsx +81 -35
- package/src/components/editor/group-selection-box-3d.tsx +25 -17
- package/src/components/editor/group-transform-shared.test.ts +126 -1
- package/src/components/editor/group-transform-shared.ts +79 -73
- package/src/components/editor/handles/handle-arrow.tsx +72 -2
- package/src/components/editor/handles/handle-placement.test.ts +111 -0
- package/src/components/editor/handles/handle-placement.ts +69 -0
- package/src/components/editor/handles/linear-resize-drag.ts +13 -2
- package/src/components/editor/handles/use-handle-drag.ts +80 -8
- package/src/components/editor/index.tsx +36 -13
- package/src/components/editor/node-action-menu.tsx +2 -0
- package/src/components/editor/node-arrow-handles.tsx +162 -69
- package/src/components/editor/registry-action-contributions.tsx +53 -0
- package/src/components/editor/selection-manager.tsx +49 -0
- package/src/components/editor/thumbnail-generator.tsx +11 -2
- package/src/components/editor/wall-move-side-handles.tsx +71 -21
- package/src/components/editor/wall-opening-highlights.tsx +2 -3
- package/src/components/editor-2d/floorplan-group-move.tsx +17 -24
- package/src/components/editor-2d/floorplan-registered-tool-layer.tsx +33 -12
- package/src/components/editor-2d/floorplan-registry-action-menu.tsx +12 -1
- package/src/components/editor-2d/floorplan-registry-move-overlay.tsx +154 -49
- package/src/components/editor-2d/renderers/floorplan-draft-wall-measurement.tsx +166 -0
- package/src/components/editor-2d/renderers/floorplan-registry-layer.test.ts +49 -0
- package/src/components/editor-2d/renderers/floorplan-registry-layer.tsx +113 -24
- package/src/components/procedural-items/inspector.tsx +223 -0
- package/src/components/systems/ceiling/ceiling-system.tsx +3 -1
- package/src/components/systems/zone/zone-system.tsx +8 -4
- package/src/components/tools/__fixtures__/hosting.ts +10 -0
- package/src/components/tools/item/item-surface-placement.test.ts +169 -0
- package/src/components/tools/item/item-surface-strategy-resolver.test.ts +204 -0
- package/src/components/tools/item/placement-strategies.ts +212 -200
- package/src/components/tools/item/placement-types.ts +4 -0
- package/src/components/tools/item/shelf-surface-strategy-resolver.test.ts +257 -0
- package/src/components/tools/item/use-draft-node.ts +136 -47
- package/src/components/tools/item/use-placement-coordinator.tsx +391 -157
- package/src/components/tools/registry/item-surface-child-frame.test.ts +90 -0
- package/src/components/tools/registry/item-surface-move.test.ts +509 -0
- package/src/components/tools/registry/item-surface-move.ts +463 -0
- package/src/components/tools/registry/item-surface-resolver.test.ts +257 -0
- package/src/components/tools/registry/move-registry-node-tool.tsx +237 -78
- package/src/components/tools/shared/affordance-dispatch.test.ts +24 -0
- package/src/components/tools/shared/affordance-dispatch.ts +13 -0
- package/src/components/tools/shared/draft-measurement-label.tsx +38 -0
- package/src/components/tools/shared/pointer-support-cap.test.ts +27 -0
- package/src/components/tools/shared/pointer-support-cap.ts +27 -13
- package/src/components/tools/shared/shelf-stickiness.ts +54 -0
- package/src/components/tools/shared/surface-hit.test.ts +56 -0
- package/src/components/tools/shared/surface-hit.ts +23 -0
- package/src/components/tools/shared/surface-rejection.test.tsx +107 -0
- package/src/components/tools/shared/surface-rejection.tsx +43 -0
- package/src/components/tools/tool-manager.tsx +36 -3
- package/src/components/tools/zone/zone-tool.tsx +13 -4
- package/src/components/ui/command-palette/editor-commands.tsx +54 -1
- package/src/components/ui/controls/material-paint-panel.tsx +6 -67
- package/src/components/ui/controls/material-picker.tsx +5 -67
- package/src/components/ui/controls/metric-control.tsx +29 -13
- package/src/components/ui/controls/slider-control.tsx +87 -35
- package/src/components/ui/floating-level-selector.tsx +81 -5
- package/src/components/ui/helpers/helper-manager.tsx +24 -6
- package/src/components/ui/helpers/registered-tool-helper.tsx +2 -32
- package/src/components/ui/item-catalog/item-catalog.tsx +5 -30
- package/src/components/ui/panels/multi-height-mode.tsx +27 -21
- package/src/components/ui/panels/multi-parametric-inspector.tsx +6 -3
- package/src/components/ui/panels/parametric-field-control.tsx +1 -1
- package/src/components/ui/panels/parametric-inspector.tsx +9 -17
- package/src/components/ui/panels/reference-panel.tsx +3 -3
- package/src/components/ui/primitives/checkbox.tsx +34 -0
- package/src/components/ui/primitives/shortcut-token.tsx +4 -0
- package/src/components/ui/sidebar/panels/settings-panel/audio-settings-dialog.tsx +2 -2
- package/src/components/ui/sidebar/panels/settings-panel/index.tsx +669 -336
- package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +2 -2
- package/src/components/ui/sidebar/panels/settings-panel/print-export-button.test.ts +31 -4
- package/src/components/ui/sidebar/panels/settings-panel/print-export-button.tsx +30 -11
- package/src/components/ui/sidebar/panels/site-panel/building-tree-node.tsx +8 -1
- package/src/components/ui/sidebar/panels/site-panel/index.tsx +106 -4
- package/src/components/ui/sidebar/panels/site-panel/item-tree-node.tsx +2 -7
- package/src/components/ui/sidebar/panels/site-panel/shelf-tree-node.tsx +3 -8
- package/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +6 -1
- package/src/components/ui/sidebar/panels/site-panel/unit-tree-node.tsx +224 -0
- package/src/components/ui/sidebar/panels/site-panel/zone-membership-checkbox.tsx +22 -0
- package/src/components/ui/sidebar/panels/site-panel/zone-tree-node.tsx +24 -7
- package/src/components/ui/snap-target-badge.tsx +2 -0
- package/src/components/viewer/floorplan-preview-visibility.test.ts +35 -0
- package/src/components/viewer/floorplan-preview-visibility.ts +19 -0
- package/src/components/viewer/floorplan-preview.tsx +1 -12
- package/src/components/viewer/viewer-scene-header.tsx +3 -0
- package/src/components/viewer/viewer-units-panel.tsx +116 -0
- package/src/components/viewer-zone-system.tsx +7 -2
- package/src/hooks/use-auto-save.integration.test.tsx +197 -0
- package/src/hooks/use-auto-save.ts +21 -6
- package/src/hooks/use-ceiling-events.test.ts +25 -2
- package/src/hooks/use-ceiling-events.ts +7 -1
- package/src/hooks/use-grid-events.test.ts +2 -0
- package/src/hooks/use-grid-events.ts +16 -11
- package/src/hooks/use-keyboard.test.ts +7 -0
- package/src/hooks/use-keyboard.ts +43 -28
- package/src/index.tsx +68 -3
- package/src/lib/catalog-panel-model.ts +39 -0
- package/src/lib/catalog-selection.test.ts +27 -0
- package/src/lib/catalog-selection.ts +8 -0
- package/src/lib/contextual-help.test.ts +27 -1
- package/src/lib/contextual-help.ts +16 -9
- package/src/lib/export-texture-utils.test.ts +167 -0
- package/src/lib/export-texture-utils.ts +121 -0
- package/src/lib/export-user-data.test.ts +48 -0
- package/src/lib/export-user-data.ts +69 -0
- package/src/lib/floorplan/floorplan-export.test.ts +142 -2
- package/src/lib/floorplan/floorplan-export.tsx +9 -3
- package/src/lib/floorplan/floorplan-extension.ts +16 -0
- package/src/lib/floorplan/floorplan-readonly.ts +4 -0
- package/src/lib/fresh-planar-placement.ts +173 -33
- package/src/lib/glb-export-serialize.test.ts +280 -0
- package/src/lib/glb-export.test.ts +265 -16
- package/src/lib/glb-export.ts +197 -37
- package/src/lib/grid-frame.test.ts +19 -0
- package/src/lib/grid-frame.ts +18 -0
- package/src/lib/interaction/scope.ts +9 -1
- package/src/lib/keyboard-pan.test.ts +62 -0
- package/src/lib/keyboard-pan.ts +89 -0
- package/src/lib/level-duplication.test.ts +73 -0
- package/src/lib/level-duplication.ts +32 -2
- package/src/lib/linear-display.test.ts +59 -0
- package/src/lib/linear-display.ts +39 -0
- package/src/lib/local-guide-image.test.ts +24 -0
- package/src/lib/local-guide-image.ts +34 -1
- package/src/lib/material-catalog-model.ts +72 -0
- package/src/lib/material-paint-panel-model.ts +76 -0
- package/src/lib/node-action-movement.ts +21 -0
- package/src/lib/overlay-transform.test.ts +28 -0
- package/src/lib/overlay-transform.ts +12 -0
- package/src/lib/panel-rows.ts +75 -0
- package/src/lib/panel-tool-options.test.ts +34 -0
- package/src/lib/panel-tool-options.ts +46 -0
- package/src/lib/parametric-node-update.ts +37 -0
- package/src/lib/portable-export.test.ts +89 -0
- package/src/lib/portable-export.ts +84 -11
- package/src/lib/print-shell-compiler-manifold-core.ts +59 -10
- package/src/lib/print-shell-compiler-manifold-worker.ts +14 -1
- package/src/lib/print-shell-compiler-manifold.worker.ts +1 -1
- package/src/lib/print-shell-compiler-protocol.ts +8 -0
- package/src/lib/scene-clipboard.ts +1 -1
- package/src/lib/scene.test.ts +99 -0
- package/src/lib/scene.ts +21 -3
- package/src/lib/send-to-app.test.ts +108 -0
- package/src/lib/send-to-app.ts +121 -0
- package/src/lib/sfx/movement-tick.ts +37 -0
- package/src/lib/snapping-mode.test.ts +13 -2
- package/src/lib/snapping-mode.ts +11 -6
- package/src/lib/spatial-drag-plane.test.ts +53 -0
- package/src/lib/spatial-drag-plane.ts +19 -0
- package/src/lib/spatial-pointer-input.test.ts +72 -0
- package/src/lib/spatial-pointer-input.ts +62 -0
- package/src/lib/spatial-snap.test.ts +47 -0
- package/src/lib/surface-attachment.ts +77 -0
- package/src/lib/terrain-panel-model.ts +117 -0
- package/src/lib/units.ts +254 -0
- package/src/lib/usdz-export.ts +43 -16
- package/src/lib/use-linear-display.ts +8 -32
- package/src/lib/world-grid-snap.test.ts +48 -0
- package/src/lib/world-grid-snap.ts +9 -5
- package/src/store/rotation-snapping.test.ts +53 -0
- package/src/store/use-editor.tsx +10 -2
- package/src/store/use-floorplan-draft-preview.ts +10 -1
- package/src/store/use-handle-group.ts +4 -0
- package/src/store/use-interaction-scope.ts +155 -6
- package/src/store/use-send-to-app.ts +21 -0
|
@@ -34,6 +34,7 @@ import {
|
|
|
34
34
|
type RoofSegmentNode,
|
|
35
35
|
resolveSlabPlacementElevation,
|
|
36
36
|
resolveTerrainWallConstructionOptions,
|
|
37
|
+
runAsSingleSceneHistoryStep,
|
|
37
38
|
type SiteNode,
|
|
38
39
|
type SlabNode,
|
|
39
40
|
SlabNode as SlabNodeSchema,
|
|
@@ -54,6 +55,7 @@ import {
|
|
|
54
55
|
type WindowNode,
|
|
55
56
|
WindowNode as WindowNodeSchema,
|
|
56
57
|
wallClosesRoom,
|
|
58
|
+
wallRectangleCorners,
|
|
57
59
|
ZoneNode as ZoneNodeSchema,
|
|
58
60
|
type ZoneNode as ZoneNodeType,
|
|
59
61
|
} from '@pascal-app/core'
|
|
@@ -93,11 +95,21 @@ import { resolveGenericFloorplanGridEventPoint } from '../../lib/floorplan-grid-
|
|
|
93
95
|
import type { EditorGridEvent } from '../../lib/grid-event-presentation'
|
|
94
96
|
import { groundHeightAt } from '../../lib/ground-surface'
|
|
95
97
|
import { guideEmitter } from '../../lib/guide-events'
|
|
98
|
+
import {
|
|
99
|
+
acceptsKeyboardPan,
|
|
100
|
+
clearKeyboardPanKeys,
|
|
101
|
+
isKeyboardPanKey,
|
|
102
|
+
type KeyboardPanState,
|
|
103
|
+
keyboardPanDirection,
|
|
104
|
+
keyboardPanSpeed,
|
|
105
|
+
setKeyboardPanKey,
|
|
106
|
+
} from '../../lib/keyboard-pan'
|
|
96
107
|
import { measurementHint, parseMeasurement } from '../../lib/measurement-parser'
|
|
97
108
|
import { formatLinearMeasurement, linearUnitToMeters } from '../../lib/measurements'
|
|
98
109
|
import { sfxEmitter } from '../../lib/sfx-bus'
|
|
99
110
|
import { SITE_BOUNDARY_DRAG_LABEL, siteBoundaryHandlesEnabled } from '../../lib/site-boundary'
|
|
100
111
|
import { resolveSlabPlanPointSnap } from '../../lib/slab-plan-snap'
|
|
112
|
+
import { cancelPendingZonePaint, focusedUnitNode, paintZoneMembership } from '../../lib/units'
|
|
101
113
|
import { cn } from '../../lib/utils'
|
|
102
114
|
import { snapBuildingLocalToWorldGrid } from '../../lib/world-grid-snap'
|
|
103
115
|
import { subscribeNavigationSyncPose } from '../../store/navigation-sync-pose-store'
|
|
@@ -135,6 +147,7 @@ import {
|
|
|
135
147
|
import { FloorplanSnapBeaconLayer } from '../editor-2d/floorplan-snap-beacon-layer'
|
|
136
148
|
import { FloorplanWallMoveGhostLayer } from '../editor-2d/floorplan-wall-move-ghost-layer'
|
|
137
149
|
import { FloorplanDraftLayer } from '../editor-2d/renderers/floorplan-draft-layer'
|
|
150
|
+
import { FloorplanDraftWallMeasurement } from '../editor-2d/renderers/floorplan-draft-wall-measurement'
|
|
138
151
|
import { FloorplanGeometryRenderer } from '../editor-2d/renderers/floorplan-geometry-renderer'
|
|
139
152
|
import { FloorplanMarqueeLayer } from '../editor-2d/renderers/floorplan-marquee-layer'
|
|
140
153
|
import { FloorplanPlacementPreviewLayer } from '../editor-2d/renderers/floorplan-placement-preview-layer'
|
|
@@ -2318,168 +2331,6 @@ function buildDraftWall(levelId: string, start: WallPlanPoint, end: WallPlanPoin
|
|
|
2318
2331
|
}
|
|
2319
2332
|
}
|
|
2320
2333
|
|
|
2321
|
-
type DraftWallMeasurement = {
|
|
2322
|
-
lengthLabel: string
|
|
2323
|
-
midpoint: WallPlanPoint
|
|
2324
|
-
direction: WallPlanPoint
|
|
2325
|
-
angleLabels: {
|
|
2326
|
-
id: string
|
|
2327
|
-
label: string
|
|
2328
|
-
center: WallPlanPoint
|
|
2329
|
-
radius: number
|
|
2330
|
-
startAngle: number
|
|
2331
|
-
endAngle: number
|
|
2332
|
-
midAngle: number
|
|
2333
|
-
}[]
|
|
2334
|
-
}
|
|
2335
|
-
|
|
2336
|
-
function FloorplanDraftWallMeasurement({
|
|
2337
|
-
measurement,
|
|
2338
|
-
measurementStroke,
|
|
2339
|
-
labelBackground,
|
|
2340
|
-
labelText,
|
|
2341
|
-
sceneRotationDeg,
|
|
2342
|
-
unitsPerPixel,
|
|
2343
|
-
}: {
|
|
2344
|
-
measurement: DraftWallMeasurement
|
|
2345
|
-
measurementStroke: string
|
|
2346
|
-
labelBackground: string
|
|
2347
|
-
labelText: string
|
|
2348
|
-
sceneRotationDeg: number
|
|
2349
|
-
unitsPerPixel: number
|
|
2350
|
-
}) {
|
|
2351
|
-
const stroke = measurementStroke
|
|
2352
|
-
const labelBg = labelBackground
|
|
2353
|
-
|
|
2354
|
-
const upx = unitsPerPixel
|
|
2355
|
-
const fontSize = Math.max(upx * 10, 0.08)
|
|
2356
|
-
const padX = upx * 6
|
|
2357
|
-
const padY = upx * 3
|
|
2358
|
-
|
|
2359
|
-
// Length plate: rotates to follow the wall direction, but flips 180°
|
|
2360
|
-
// when its on-screen orientation would read upside-down (same trick as
|
|
2361
|
-
// `floorplan-registry-layer.tsx` for dimension labels).
|
|
2362
|
-
const wallAngleDeg =
|
|
2363
|
-
(Math.atan2(measurement.direction[1], measurement.direction[0]) * 180) / Math.PI
|
|
2364
|
-
let labelAngleDeg = wallAngleDeg
|
|
2365
|
-
let screenDeg = wallAngleDeg + sceneRotationDeg
|
|
2366
|
-
screenDeg = ((((screenDeg + 180) % 360) + 360) % 360) - 180
|
|
2367
|
-
if (screenDeg > 90) labelAngleDeg -= 180
|
|
2368
|
-
else if (screenDeg <= -90) labelAngleDeg += 180
|
|
2369
|
-
|
|
2370
|
-
// Push the plate perpendicular to the wall so the dashed footprint
|
|
2371
|
-
// stays visible underneath.
|
|
2372
|
-
const perpX = -measurement.direction[1]
|
|
2373
|
-
const perpY = measurement.direction[0]
|
|
2374
|
-
const offset = upx * 18
|
|
2375
|
-
const cx = measurement.midpoint[0] + perpX * offset
|
|
2376
|
-
const cy = measurement.midpoint[1] + perpY * offset
|
|
2377
|
-
|
|
2378
|
-
const lengthTextWidth = measurement.lengthLabel.length * upx * 6.2
|
|
2379
|
-
const lengthPlateW = lengthTextWidth + padX * 2
|
|
2380
|
-
const lengthPlateH = fontSize + padY * 2
|
|
2381
|
-
|
|
2382
|
-
const arcSampleCount = 32
|
|
2383
|
-
|
|
2384
|
-
return (
|
|
2385
|
-
<g pointerEvents="none">
|
|
2386
|
-
<g transform={`translate(${cx} ${cy}) rotate(${labelAngleDeg})`}>
|
|
2387
|
-
<rect
|
|
2388
|
-
fill={labelBg}
|
|
2389
|
-
height={lengthPlateH}
|
|
2390
|
-
opacity={0.92}
|
|
2391
|
-
rx={upx * 3}
|
|
2392
|
-
ry={upx * 3}
|
|
2393
|
-
stroke={stroke}
|
|
2394
|
-
strokeWidth={upx * 0.5}
|
|
2395
|
-
vectorEffect="non-scaling-stroke"
|
|
2396
|
-
width={lengthPlateW}
|
|
2397
|
-
x={-lengthPlateW / 2}
|
|
2398
|
-
y={-lengthPlateH / 2}
|
|
2399
|
-
/>
|
|
2400
|
-
<text
|
|
2401
|
-
dominantBaseline="middle"
|
|
2402
|
-
fill={labelText}
|
|
2403
|
-
fontFamily="ui-monospace, SFMono-Regular, Menlo, monospace"
|
|
2404
|
-
fontSize={fontSize}
|
|
2405
|
-
fontWeight={600}
|
|
2406
|
-
textAnchor="middle"
|
|
2407
|
-
x={0}
|
|
2408
|
-
y={0}
|
|
2409
|
-
>
|
|
2410
|
-
{measurement.lengthLabel}
|
|
2411
|
-
</text>
|
|
2412
|
-
</g>
|
|
2413
|
-
|
|
2414
|
-
{measurement.angleLabels.map((arc) => {
|
|
2415
|
-
// Sample the arc as a polyline — avoids the SVG arc command's
|
|
2416
|
-
// sweep-flag direction quirks across negative/positive sweeps.
|
|
2417
|
-
const points: string[] = []
|
|
2418
|
-
for (let i = 0; i <= arcSampleCount; i += 1) {
|
|
2419
|
-
const t = i / arcSampleCount
|
|
2420
|
-
const a = arc.startAngle + (arc.endAngle - arc.startAngle) * t
|
|
2421
|
-
const px = arc.center[0] + Math.cos(a) * arc.radius
|
|
2422
|
-
const py = arc.center[1] + Math.sin(a) * arc.radius
|
|
2423
|
-
points.push(`${px},${py}`)
|
|
2424
|
-
}
|
|
2425
|
-
|
|
2426
|
-
const aFontSize = Math.max(upx * 9, 0.075)
|
|
2427
|
-
const aPadX = upx * 5
|
|
2428
|
-
const aPadY = upx * 2.5
|
|
2429
|
-
const aTextWidth = arc.label.length * upx * 6.2
|
|
2430
|
-
const aPlateW = aTextWidth + aPadX * 2
|
|
2431
|
-
const aPlateH = aFontSize + aPadY * 2
|
|
2432
|
-
|
|
2433
|
-
const labelDist = arc.radius + upx * 16
|
|
2434
|
-
const lx = arc.center[0] + Math.cos(arc.midAngle) * labelDist
|
|
2435
|
-
const ly = arc.center[1] + Math.sin(arc.midAngle) * labelDist
|
|
2436
|
-
|
|
2437
|
-
return (
|
|
2438
|
-
<g key={`draft-angle-${arc.id}`}>
|
|
2439
|
-
<polyline
|
|
2440
|
-
fill="none"
|
|
2441
|
-
points={points.join(' ')}
|
|
2442
|
-
stroke={stroke}
|
|
2443
|
-
strokeLinecap="round"
|
|
2444
|
-
strokeLinejoin="round"
|
|
2445
|
-
strokeOpacity={0.95}
|
|
2446
|
-
strokeWidth={upx * 1.2}
|
|
2447
|
-
vectorEffect="non-scaling-stroke"
|
|
2448
|
-
/>
|
|
2449
|
-
<g transform={`translate(${lx} ${ly})`}>
|
|
2450
|
-
<rect
|
|
2451
|
-
fill={labelBg}
|
|
2452
|
-
height={aPlateH}
|
|
2453
|
-
opacity={0.92}
|
|
2454
|
-
rx={upx * 3}
|
|
2455
|
-
ry={upx * 3}
|
|
2456
|
-
stroke={stroke}
|
|
2457
|
-
strokeWidth={upx * 0.5}
|
|
2458
|
-
vectorEffect="non-scaling-stroke"
|
|
2459
|
-
width={aPlateW}
|
|
2460
|
-
x={-aPlateW / 2}
|
|
2461
|
-
y={-aPlateH / 2}
|
|
2462
|
-
/>
|
|
2463
|
-
<text
|
|
2464
|
-
dominantBaseline="middle"
|
|
2465
|
-
fill={labelText}
|
|
2466
|
-
fontFamily="ui-monospace, SFMono-Regular, Menlo, monospace"
|
|
2467
|
-
fontSize={aFontSize}
|
|
2468
|
-
fontWeight={600}
|
|
2469
|
-
textAnchor="middle"
|
|
2470
|
-
x={0}
|
|
2471
|
-
y={0}
|
|
2472
|
-
>
|
|
2473
|
-
{arc.label}
|
|
2474
|
-
</text>
|
|
2475
|
-
</g>
|
|
2476
|
-
</g>
|
|
2477
|
-
)
|
|
2478
|
-
})}
|
|
2479
|
-
</g>
|
|
2480
|
-
)
|
|
2481
|
-
}
|
|
2482
|
-
|
|
2483
2334
|
function pointsEqual(a: WallPlanPoint, b: WallPlanPoint): boolean {
|
|
2484
2335
|
return a[0] === b[0] && a[1] === b[1]
|
|
2485
2336
|
}
|
|
@@ -4568,7 +4419,7 @@ function FloorplanDraftCursorLayer({
|
|
|
4568
4419
|
)}
|
|
4569
4420
|
|
|
4570
4421
|
{cursorPoint && (
|
|
4571
|
-
<g>
|
|
4422
|
+
<g pointerEvents="none">
|
|
4572
4423
|
<circle
|
|
4573
4424
|
cx={toSvgX(cursorPoint[0])}
|
|
4574
4425
|
cy={toSvgY(cursorPoint[1])}
|
|
@@ -4678,6 +4529,12 @@ function FloorplanLinearDraftLayer({
|
|
|
4678
4529
|
const fenceDraftEnd = useFloorplanDraftPreview((s) => s.fenceDraftEnd)
|
|
4679
4530
|
const roofDraftEnd = useFloorplanDraftPreview((s) => s.roofDraftEnd)
|
|
4680
4531
|
const roofDraftQuarterTurn = useFloorplanDraftPreview((s) => s.roofDraftQuarterTurn)
|
|
4532
|
+
const wallRectangleDraftStart = useFloorplanDraftPreview((s) => s.wallRectangleDraftStart)
|
|
4533
|
+
// The live cursor is the rectangle's opposite corner; select it only while a
|
|
4534
|
+
// rectangle draft is open so idle moves don't re-render this layer.
|
|
4535
|
+
const wallRectangleDraftEnd = useFloorplanDraftPreview((s) =>
|
|
4536
|
+
s.wallRectangleDraftStart ? s.cursorPoint : null,
|
|
4537
|
+
)
|
|
4681
4538
|
|
|
4682
4539
|
const draftPolygon = useMemo(() => {
|
|
4683
4540
|
if (
|
|
@@ -4695,6 +4552,53 @@ function FloorplanLinearDraftLayer({
|
|
|
4695
4552
|
return getWallPlanFootprint(draftWall, EMPTY_WALL_MITER_DATA)
|
|
4696
4553
|
}, [levelId, wallDraftStart, wallDraftEnd])
|
|
4697
4554
|
|
|
4555
|
+
// Rectangle mode drafts four walls at once; they read exactly like a line
|
|
4556
|
+
// draft — same mitered footprints, measurement plates and axis guides.
|
|
4557
|
+
const rectangleDraft = useMemo(() => {
|
|
4558
|
+
if (!(levelId && isWallBuildActive && wallRectangleDraftStart && wallRectangleDraftEnd)) {
|
|
4559
|
+
return null
|
|
4560
|
+
}
|
|
4561
|
+
const corners = wallRectangleCorners(wallRectangleDraftStart, wallRectangleDraftEnd)
|
|
4562
|
+
if (corners.length !== 4) return null
|
|
4563
|
+
const draftWalls = corners.map((start, index) =>
|
|
4564
|
+
getSharedFloorplanWall({
|
|
4565
|
+
...buildDraftWall(levelId, start, corners[(index + 1) % 4]!),
|
|
4566
|
+
id: `wall_draft_${index}` as WallNode['id'],
|
|
4567
|
+
}),
|
|
4568
|
+
)
|
|
4569
|
+
const miterData = calculateLevelMiters(draftWalls)
|
|
4570
|
+
const polygons = draftWalls.map((wall) =>
|
|
4571
|
+
formatPolygonPoints(getWallPlanFootprint(wall, miterData)),
|
|
4572
|
+
)
|
|
4573
|
+
// Measure the two sides meeting at the cursor corner. Corners wind
|
|
4574
|
+
// counter-clockwise, so each side is measured end → start to push its
|
|
4575
|
+
// plate outside the rectangle.
|
|
4576
|
+
const cursorIndex = corners.findIndex(
|
|
4577
|
+
([x, z]) => x === wallRectangleDraftEnd[0] && z === wallRectangleDraftEnd[1],
|
|
4578
|
+
)
|
|
4579
|
+
const measurements = [cursorIndex - 1, cursorIndex].map((side) => {
|
|
4580
|
+
const from = corners[(side + 5) % 4]!
|
|
4581
|
+
const to = corners[(side + 4) % 4]!
|
|
4582
|
+
const dx = to[0] - from[0]
|
|
4583
|
+
const dy = to[1] - from[1]
|
|
4584
|
+
const length = Math.hypot(dx, dy)
|
|
4585
|
+
return {
|
|
4586
|
+
lengthLabel: formatMeasurement(length, unit, null, metricNotation),
|
|
4587
|
+
midpoint: [(from[0] + to[0]) / 2, (from[1] + to[1]) / 2] as WallPlanPoint,
|
|
4588
|
+
direction: [dx / length, dy / length] as WallPlanPoint,
|
|
4589
|
+
angleLabels: [],
|
|
4590
|
+
}
|
|
4591
|
+
})
|
|
4592
|
+
return { polygons, measurements }
|
|
4593
|
+
}, [
|
|
4594
|
+
isWallBuildActive,
|
|
4595
|
+
levelId,
|
|
4596
|
+
metricNotation,
|
|
4597
|
+
unit,
|
|
4598
|
+
wallRectangleDraftEnd,
|
|
4599
|
+
wallRectangleDraftStart,
|
|
4600
|
+
])
|
|
4601
|
+
|
|
4698
4602
|
const draftPolygonPoints = useMemo(() => {
|
|
4699
4603
|
if (isRoofBuildActive && roofDraftStart && roofDraftEnd) {
|
|
4700
4604
|
const minX = Math.min(roofDraftStart[0], roofDraftEnd[0])
|
|
@@ -4862,6 +4766,10 @@ function FloorplanLinearDraftLayer({
|
|
|
4862
4766
|
if (isWallBuildActive && wallDraftStart && wallDraftEnd) {
|
|
4863
4767
|
pushDraft(wallDraftStart, wallDraftEnd)
|
|
4864
4768
|
}
|
|
4769
|
+
if (isWallBuildActive && wallRectangleDraftStart && wallRectangleDraftEnd) {
|
|
4770
|
+
pushCross(wallRectangleDraftStart)
|
|
4771
|
+
pushCross(wallRectangleDraftEnd)
|
|
4772
|
+
}
|
|
4865
4773
|
if (isFenceBuildActive && fenceDraftStart && fenceDraftEnd) {
|
|
4866
4774
|
pushDraft(fenceDraftStart, fenceDraftEnd)
|
|
4867
4775
|
}
|
|
@@ -4873,6 +4781,8 @@ function FloorplanLinearDraftLayer({
|
|
|
4873
4781
|
fenceDraftStart,
|
|
4874
4782
|
wallDraftEnd,
|
|
4875
4783
|
wallDraftStart,
|
|
4784
|
+
wallRectangleDraftEnd,
|
|
4785
|
+
wallRectangleDraftStart,
|
|
4876
4786
|
])
|
|
4877
4787
|
|
|
4878
4788
|
return (
|
|
@@ -4930,6 +4840,34 @@ function FloorplanLinearDraftLayer({
|
|
|
4930
4840
|
unitsPerPixel={unitsPerPixel}
|
|
4931
4841
|
/>
|
|
4932
4842
|
)}
|
|
4843
|
+
|
|
4844
|
+
{rectangleDraft?.polygons.map((points, index) => (
|
|
4845
|
+
<FloorplanDraftLayer
|
|
4846
|
+
anchorFill={draftStroke}
|
|
4847
|
+
draftAnchorPoints={EMPTY_DRAFT_ANCHOR_POINTS}
|
|
4848
|
+
draftFill={draftFill}
|
|
4849
|
+
draftPolygonPoints={points}
|
|
4850
|
+
draftStroke={draftStroke}
|
|
4851
|
+
key={index}
|
|
4852
|
+
linearDraftSegment={null}
|
|
4853
|
+
polygonDraftClosingSegment={null}
|
|
4854
|
+
polygonDraftPolygonPoints={null}
|
|
4855
|
+
polygonDraftPolylinePoints={null}
|
|
4856
|
+
unitsPerPixel={unitsPerPixel}
|
|
4857
|
+
/>
|
|
4858
|
+
))}
|
|
4859
|
+
|
|
4860
|
+
{rectangleDraft?.measurements.map((measurement, index) => (
|
|
4861
|
+
<FloorplanDraftWallMeasurement
|
|
4862
|
+
key={index}
|
|
4863
|
+
labelBackground={isDark ? '#0f172a' : '#ffffff'}
|
|
4864
|
+
labelText={isDark ? '#e2e8f0' : '#171717'}
|
|
4865
|
+
measurement={measurement}
|
|
4866
|
+
measurementStroke={measurementStroke}
|
|
4867
|
+
sceneRotationDeg={sceneRotationDeg}
|
|
4868
|
+
unitsPerPixel={unitsPerPixel}
|
|
4869
|
+
/>
|
|
4870
|
+
))}
|
|
4933
4871
|
</>
|
|
4934
4872
|
)
|
|
4935
4873
|
}
|
|
@@ -5034,6 +4972,7 @@ export function FloorplanPanel({
|
|
|
5034
4972
|
// the user closes and re-opens the 2D editor instead of restoring the
|
|
5035
4973
|
// stale viewport from before they closed it.
|
|
5036
4974
|
const isFloorplanOpen = useEditor((state) => state.isFloorplanOpen)
|
|
4975
|
+
const viewMode = useEditor((state) => state.viewMode)
|
|
5037
4976
|
// Mirror for callbacks that fire outside React's render (the per-frame
|
|
5038
4977
|
// navigation-pose subscriber): when the 2D panel is hidden (`display:none` in
|
|
5039
4978
|
// 3D mode) it must NOT re-render on every camera-zoom frame.
|
|
@@ -7782,6 +7721,116 @@ export function FloorplanPanel({
|
|
|
7782
7721
|
[publishFloorplanNavigationPose],
|
|
7783
7722
|
)
|
|
7784
7723
|
|
|
7724
|
+
// WASD and the orbit buttons in 2D-only view. The 3D canvas is paused there
|
|
7725
|
+
// (`renderPaused`), so the plan drives them itself — same keys and speed as
|
|
7726
|
+
// the camera — and hands the pose to 3D when the move ends.
|
|
7727
|
+
const keyboardPanKeysRef = useRef<KeyboardPanState>({
|
|
7728
|
+
forward: false,
|
|
7729
|
+
backward: false,
|
|
7730
|
+
left: false,
|
|
7731
|
+
right: false,
|
|
7732
|
+
})
|
|
7733
|
+
useEffect(() => {
|
|
7734
|
+
if (viewMode !== '2d') return
|
|
7735
|
+
const keys = keyboardPanKeysRef.current
|
|
7736
|
+
let frame: number | null = null
|
|
7737
|
+
let lastTime: number | null = null
|
|
7738
|
+
const sceneRotationDeg = (userRotationDeg: number) =>
|
|
7739
|
+
FLOORPLAN_VIEW_ROTATION_DEG + userRotationDeg - buildingRotationDeg
|
|
7740
|
+
const step = (time: number) => {
|
|
7741
|
+
const { horizontal, vertical } = keyboardPanDirection(keys)
|
|
7742
|
+
const viewport = latestViewportRef.current ?? latestFittedViewportRef.current
|
|
7743
|
+
if ((horizontal === 0 && vertical === 0) || !viewport) {
|
|
7744
|
+
frame = null
|
|
7745
|
+
lastTime = null
|
|
7746
|
+
commitFloorplanPan()
|
|
7747
|
+
return
|
|
7748
|
+
}
|
|
7749
|
+
const elapsed = lastTime === null ? 0 : Math.min((time - lastTime) / 1000, 0.05)
|
|
7750
|
+
lastTime = time
|
|
7751
|
+
const distance =
|
|
7752
|
+
(keyboardPanSpeed(viewport.width) * elapsed) / Math.hypot(horizontal, vertical)
|
|
7753
|
+
const next = {
|
|
7754
|
+
centerX: viewport.centerX + horizontal * distance,
|
|
7755
|
+
centerY: viewport.centerY - vertical * distance,
|
|
7756
|
+
width: viewport.width,
|
|
7757
|
+
}
|
|
7758
|
+
const userRotationDeg = latestFloorplanUserRotationDegRef.current
|
|
7759
|
+
floorplanViewportInteractionInProgressRef.current = true
|
|
7760
|
+
applyFloorplanViewportImperatively(next)
|
|
7761
|
+
floorplanPanPoseRef.current = {
|
|
7762
|
+
localCenter: rotateSvgPoint(
|
|
7763
|
+
{ x: next.centerX, y: next.centerY },
|
|
7764
|
+
-sceneRotationDeg(userRotationDeg),
|
|
7765
|
+
),
|
|
7766
|
+
userRotationDeg,
|
|
7767
|
+
viewWidth: next.width,
|
|
7768
|
+
}
|
|
7769
|
+
frame = requestAnimationFrame(step)
|
|
7770
|
+
}
|
|
7771
|
+
const start = () => {
|
|
7772
|
+
if (frame === null) frame = requestAnimationFrame(step)
|
|
7773
|
+
}
|
|
7774
|
+
const onKeyDown = (event: KeyboardEvent) => {
|
|
7775
|
+
if (!(isKeyboardPanKey(event.code) && acceptsKeyboardPan(event))) return
|
|
7776
|
+
if (setKeyboardPanKey(keys, event.code, true)) start()
|
|
7777
|
+
event.preventDefault()
|
|
7778
|
+
event.stopPropagation()
|
|
7779
|
+
}
|
|
7780
|
+
const onKeyUp = (event: KeyboardEvent) => {
|
|
7781
|
+
if (isKeyboardPanKey(event.code) && setKeyboardPanKey(keys, event.code, false)) {
|
|
7782
|
+
event.preventDefault()
|
|
7783
|
+
event.stopPropagation()
|
|
7784
|
+
}
|
|
7785
|
+
}
|
|
7786
|
+
const onBlur = () => clearKeyboardPanKeys(keys)
|
|
7787
|
+
// Same rule as the 3D buttons: snap to the nearest quarter turn, then turn one more.
|
|
7788
|
+
const orbit = (clockwise: boolean) => {
|
|
7789
|
+
const viewport = latestViewportRef.current ?? latestFittedViewportRef.current
|
|
7790
|
+
if (!viewport) return
|
|
7791
|
+
const userRotationDeg = latestFloorplanUserRotationDegRef.current
|
|
7792
|
+
const quarter = Math.PI / 2
|
|
7793
|
+
const azimuth =
|
|
7794
|
+
Math.round(cameraAzimuthFromFloorplanRotation(userRotationDeg) / quarter) * quarter +
|
|
7795
|
+
(clockwise ? -quarter : quarter)
|
|
7796
|
+
const nextRotationDeg = floorplanRotationFromCameraAzimuth(azimuth, userRotationDeg)
|
|
7797
|
+
const localCenter = rotateSvgPoint(
|
|
7798
|
+
{ x: viewport.centerX, y: viewport.centerY },
|
|
7799
|
+
-sceneRotationDeg(userRotationDeg),
|
|
7800
|
+
)
|
|
7801
|
+
smoothFloorplanNavigationView(localCenter, nextRotationDeg, viewport.width)
|
|
7802
|
+
publishFloorplanNavigationPose(localCenter, nextRotationDeg, viewport.width)
|
|
7803
|
+
}
|
|
7804
|
+
const orbitClockwise = () => orbit(true)
|
|
7805
|
+
const orbitCounterClockwise = () => orbit(false)
|
|
7806
|
+
document.addEventListener('keydown', onKeyDown)
|
|
7807
|
+
document.addEventListener('keyup', onKeyUp)
|
|
7808
|
+
window.addEventListener('blur', onBlur)
|
|
7809
|
+
emitter.on('camera-controls:orbit-cw', orbitClockwise)
|
|
7810
|
+
emitter.on('camera-controls:orbit-ccw', orbitCounterClockwise)
|
|
7811
|
+
return () => {
|
|
7812
|
+
document.removeEventListener('keydown', onKeyDown)
|
|
7813
|
+
document.removeEventListener('keyup', onKeyUp)
|
|
7814
|
+
window.removeEventListener('blur', onBlur)
|
|
7815
|
+
emitter.off('camera-controls:orbit-cw', orbitClockwise)
|
|
7816
|
+
emitter.off('camera-controls:orbit-ccw', orbitCounterClockwise)
|
|
7817
|
+
// A key released while 3D owns the keys would otherwise still read as
|
|
7818
|
+
// held when the plan takes them back; the camera clears its own the same way.
|
|
7819
|
+
clearKeyboardPanKeys(keys)
|
|
7820
|
+
if (frame !== null) {
|
|
7821
|
+
cancelAnimationFrame(frame)
|
|
7822
|
+
commitFloorplanPan()
|
|
7823
|
+
}
|
|
7824
|
+
}
|
|
7825
|
+
}, [
|
|
7826
|
+
applyFloorplanViewportImperatively,
|
|
7827
|
+
buildingRotationDeg,
|
|
7828
|
+
commitFloorplanPan,
|
|
7829
|
+
publishFloorplanNavigationPose,
|
|
7830
|
+
smoothFloorplanNavigationView,
|
|
7831
|
+
viewMode,
|
|
7832
|
+
])
|
|
7833
|
+
|
|
7785
7834
|
useLayoutEffect(() => {
|
|
7786
7835
|
flushFloorplanRotationPresentationRestore(pendingFloorplanRotationRestoreRef)
|
|
7787
7836
|
})
|
|
@@ -7952,7 +8001,7 @@ export function FloorplanPanel({
|
|
|
7952
8001
|
return null
|
|
7953
8002
|
}
|
|
7954
8003
|
|
|
7955
|
-
const { createNode, nodes } = useScene.getState()
|
|
8004
|
+
const { createNode, updateNode, nodes } = useScene.getState()
|
|
7956
8005
|
const zoneCount = Object.values(nodes).filter((node) => node.type === 'zone').length
|
|
7957
8006
|
const zone = ZoneNodeSchema.parse({
|
|
7958
8007
|
color: PALETTE_COLORS[zoneCount % PALETTE_COLORS.length],
|
|
@@ -7960,9 +8009,15 @@ export function FloorplanPanel({
|
|
|
7960
8009
|
polygon: points.map(([x, z]) => [x, z] as [number, number]),
|
|
7961
8010
|
})
|
|
7962
8011
|
|
|
7963
|
-
|
|
8012
|
+
// Joining the focused unit rides in the zone's own undo step; selecting
|
|
8013
|
+
// the zone would end focus, so a painted unit keeps it instead.
|
|
8014
|
+
const focusedUnit = focusedUnitNode()
|
|
8015
|
+
runAsSingleSceneHistoryStep(useScene, () => {
|
|
8016
|
+
createNode(zone, levelId)
|
|
8017
|
+
if (focusedUnit) updateNode(focusedUnit.id, { members: [...focusedUnit.members, zone.id] })
|
|
8018
|
+
})
|
|
7964
8019
|
sfxEmitter.emit('sfx:structure-build')
|
|
7965
|
-
setSelection({ zoneId: zone.id })
|
|
8020
|
+
if (!focusedUnit) setSelection({ zoneId: zone.id })
|
|
7966
8021
|
return zone.id
|
|
7967
8022
|
},
|
|
7968
8023
|
[levelId, setSelection],
|
|
@@ -10008,6 +10063,18 @@ export function FloorplanPanel({
|
|
|
10008
10063
|
setSelectedReferenceId(null)
|
|
10009
10064
|
|
|
10010
10065
|
if (backgroundSelection.kind === 'select-zone') {
|
|
10066
|
+
// Unit focus: a click paints membership, a double-click selects the
|
|
10067
|
+
// zone and keeps focus (the SVG gets the second click as detail 2).
|
|
10068
|
+
const focusedUnitId = useViewer.getState().focusedUnitId
|
|
10069
|
+
if (focusedUnitId) {
|
|
10070
|
+
if (event.detail >= 2) {
|
|
10071
|
+
cancelPendingZonePaint(backgroundSelection.zoneId)
|
|
10072
|
+
setSelection({ zoneId: backgroundSelection.zoneId })
|
|
10073
|
+
} else {
|
|
10074
|
+
paintZoneMembership(focusedUnitId, backgroundSelection.zoneId)
|
|
10075
|
+
}
|
|
10076
|
+
return
|
|
10077
|
+
}
|
|
10011
10078
|
setSelection({ zoneId: backgroundSelection.zoneId })
|
|
10012
10079
|
return
|
|
10013
10080
|
}
|
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
3
|
import { type AnyNodeId, emitter, type GridEvent, sceneRegistry } from '@pascal-app/core'
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
GRID_LAYER,
|
|
6
|
+
getSceneTheme,
|
|
7
|
+
useImmersiveXRPresentation,
|
|
8
|
+
useViewer,
|
|
9
|
+
} from '@pascal-app/viewer'
|
|
5
10
|
import { useFrame } from '@react-three/fiber'
|
|
6
11
|
import { useEffect, useMemo, useRef, useState } from 'react'
|
|
7
12
|
import { DoubleSide, type Mesh, PlaneGeometry, Quaternion, Vector2, Vector3 } from 'three'
|
|
@@ -10,9 +15,12 @@ import { MeshBasicNodeMaterial } from 'three/webgpu'
|
|
|
10
15
|
import { useCeilingEvents } from '../../hooks/use-ceiling-events'
|
|
11
16
|
import { useGridEvents } from '../../hooks/use-grid-events'
|
|
12
17
|
import { getPlacementSurface, usesOrientedPlacementPlane } from '../../lib/active-placement-surface'
|
|
13
|
-
import
|
|
18
|
+
import { gridLocalNormal, gridLocalPoint } from '../../lib/grid-frame'
|
|
19
|
+
import useEditor, { getActiveSnapContext, isGridSnapActive } from '../../store/use-editor'
|
|
14
20
|
import { getMovingNode } from '../../store/use-interaction-scope'
|
|
15
21
|
|
|
22
|
+
export const EDITOR_GRID_INPUT_NAME = 'pascal-editor-grid-input'
|
|
23
|
+
|
|
16
24
|
// Reveal radius (m) of the cursor-local grid patch shown while placing/moving in
|
|
17
25
|
// grid-snap mode — much tighter than the idle reveal so only the area you're
|
|
18
26
|
// about to snap into lights up.
|
|
@@ -45,6 +53,7 @@ export const Grid = ({
|
|
|
45
53
|
fadeStrength?: number
|
|
46
54
|
revealRadius?: number
|
|
47
55
|
}) => {
|
|
56
|
+
const immersive = useImmersiveXRPresentation()
|
|
48
57
|
const isDark = useViewer((state) => getSceneTheme(state.sceneTheme).appearance === 'dark')
|
|
49
58
|
|
|
50
59
|
// Use slightly lighter colors for dark themes' grid to make it apparent
|
|
@@ -60,6 +69,9 @@ export const Grid = ({
|
|
|
60
69
|
const wallCursorRef = useRef(new Vector3())
|
|
61
70
|
// Scratch for deriving a moving wall-item's host normal from its mesh.
|
|
62
71
|
const wallNormalRef = useRef(new Vector3())
|
|
72
|
+
const localSurfaceRef = useRef(new Vector3())
|
|
73
|
+
const localNormalRef = useRef(new Vector3())
|
|
74
|
+
const localCursorRef = useRef(new Vector3())
|
|
63
75
|
// Last Y pushed to `gridY` state, so the per-frame surface follow only triggers
|
|
64
76
|
// a React re-render when the height actually changes (not every frame).
|
|
65
77
|
const lastGridYRef = useRef<number | null>(null)
|
|
@@ -176,10 +188,11 @@ export const Grid = ({
|
|
|
176
188
|
// laid-flat orientation maps `positionLocal.y` to world `-Z` relative to the
|
|
177
189
|
// mesh origin. The cursor is recomputed every frame from the stored world hit
|
|
178
190
|
// so the reveal stays put regardless of where the mesh origin sits.
|
|
179
|
-
const lastWorldCursorRef = useRef<
|
|
191
|
+
const lastWorldCursorRef = useRef<Vector3 | null>(null)
|
|
180
192
|
useEffect(() => {
|
|
181
193
|
const onGridMove = (event: GridEvent) => {
|
|
182
|
-
lastWorldCursorRef.current
|
|
194
|
+
lastWorldCursorRef.current ??= new Vector3()
|
|
195
|
+
lastWorldCursorRef.current.set(...event.position)
|
|
183
196
|
}
|
|
184
197
|
|
|
185
198
|
emitter.on('grid:move', onGridMove)
|
|
@@ -194,7 +207,12 @@ export const Grid = ({
|
|
|
194
207
|
if (levelId) {
|
|
195
208
|
const levelMesh = sceneRegistry.nodes.get(levelId)
|
|
196
209
|
if (levelMesh) {
|
|
197
|
-
|
|
210
|
+
levelMesh.getWorldPosition(worldPosRef.current)
|
|
211
|
+
levelY = gridLocalPoint(
|
|
212
|
+
gridRef.current.parent,
|
|
213
|
+
worldPosRef.current,
|
|
214
|
+
localSurfaceRef.current,
|
|
215
|
+
).y
|
|
198
216
|
}
|
|
199
217
|
}
|
|
200
218
|
|
|
@@ -229,6 +247,10 @@ export const Grid = ({
|
|
|
229
247
|
}
|
|
230
248
|
|
|
231
249
|
const gridMesh = gridRef.current
|
|
250
|
+
if (surfacePoint) {
|
|
251
|
+
surfacePoint = gridLocalPoint(gridMesh.parent, surfacePoint, localSurfaceRef.current)
|
|
252
|
+
surfaceNormal = gridLocalNormal(gridMesh.parent, surfaceNormal, localNormalRef.current)
|
|
253
|
+
}
|
|
232
254
|
const onOrientedPlane = surfacePoint != null && usesOrientedPlacementPlane(surfaceNormal)
|
|
233
255
|
if (onOrientedPlane && surfacePoint) {
|
|
234
256
|
// Surface-anchored lattice: orient the plane into the host and pin the mesh to
|
|
@@ -269,7 +291,8 @@ export const Grid = ({
|
|
|
269
291
|
gridMesh.quaternion.copy(HORIZONTAL_QUATERNION)
|
|
270
292
|
const world = lastWorldCursorRef.current
|
|
271
293
|
if (world) {
|
|
272
|
-
|
|
294
|
+
const local = gridLocalPoint(gridMesh.parent, world, localCursorRef.current)
|
|
295
|
+
cursorPositionRef.current.set(local.x, -local.z)
|
|
273
296
|
}
|
|
274
297
|
if (lastGridYRef.current !== targetY) {
|
|
275
298
|
lastGridYRef.current = targetY
|
|
@@ -293,7 +316,7 @@ export const Grid = ({
|
|
|
293
316
|
// from the interaction scope OR the armed build tool and is true only when
|
|
294
317
|
// that context resolves to grid, so it IS the gate. (Previously this also
|
|
295
318
|
// required a ghost in flight, so a merely-armed draft tool showed nothing.)
|
|
296
|
-
const snapPatchVisible = isGridSnapActive()
|
|
319
|
+
const snapPatchVisible = isGridSnapActive() || (immersive && getActiveSnapContext() !== null)
|
|
297
320
|
revealRadiusUniform.value = PLACEMENT_REVEAL_RADIUS
|
|
298
321
|
baseAlphaUniform.value = 0
|
|
299
322
|
cellSizeUniform.value = useEditor.getState().gridSnapStep
|
|
@@ -320,6 +343,7 @@ export const Grid = ({
|
|
|
320
343
|
geometry={geometry}
|
|
321
344
|
layers={GRID_LAYER}
|
|
322
345
|
material={material}
|
|
346
|
+
name={EDITOR_GRID_INPUT_NAME}
|
|
323
347
|
ref={gridRef}
|
|
324
348
|
renderOrder={1}
|
|
325
349
|
/>
|
|
@@ -39,7 +39,6 @@ import {
|
|
|
39
39
|
classifyParticipant,
|
|
40
40
|
collectParticipants,
|
|
41
41
|
computeGroupBox,
|
|
42
|
-
expandToComponent,
|
|
43
42
|
groupPlanBounds,
|
|
44
43
|
levelFrame,
|
|
45
44
|
planBoundsCenter,
|
|
@@ -83,8 +82,8 @@ export function canGroupPickUp(): boolean {
|
|
|
83
82
|
* until a click commits, mirroring the single-node `movingNode` flow. Returns
|
|
84
83
|
* false when the selection holds no transformable participants.
|
|
85
84
|
*
|
|
86
|
-
* `scopeToSelection`
|
|
87
|
-
*
|
|
85
|
+
* `scopeToSelection` drops the welded-neighbor endpoints, so connected walls
|
|
86
|
+
* don't stretch along with the move. The
|
|
88
87
|
* Duplicate flow needs this: its clones sit EXACTLY on the originals, so
|
|
89
88
|
* junction coincidence would otherwise weld the originals into the pick-up
|
|
90
89
|
* and drag them along with the copies.
|
|
@@ -96,10 +95,7 @@ export function startGroupPickUp(
|
|
|
96
95
|
const participantIds = groupParticipantIds()
|
|
97
96
|
if (participantIds.length === 0) return false
|
|
98
97
|
const nodes = useScene.getState().nodes
|
|
99
|
-
const
|
|
100
|
-
? participantIds
|
|
101
|
-
: expandToComponent(participantIds, nodes, levelId)
|
|
102
|
-
const collected = collectParticipants(fullIds, nodes, levelId)
|
|
98
|
+
const collected = collectParticipants(participantIds, nodes, levelId)
|
|
103
99
|
// Mutable: mid-carry R/T rotates these snapshots in place.
|
|
104
100
|
let starts = collected.starts
|
|
105
101
|
let links = opts.scopeToSelection ? [] : collected.links
|
|
@@ -107,8 +103,8 @@ export function startGroupPickUp(
|
|
|
107
103
|
const affectedIds: AnyNodeId[] = [...starts.map((s) => s.id), ...links.map((l) => l.id)]
|
|
108
104
|
|
|
109
105
|
const { inverse: frameInv } = levelFrame(levelId)
|
|
110
|
-
const restBox = computeGroupBox(
|
|
111
|
-
const startBounds = groupPlanBounds(
|
|
106
|
+
const restBox = computeGroupBox(participantIds)
|
|
107
|
+
const startBounds = groupPlanBounds(starts, frameInv)
|
|
112
108
|
if (!startBounds) return false
|
|
113
109
|
// Mutable: mid-carry R/T re-seeds the footprint around the same pivot.
|
|
114
110
|
let restBounds = startBounds
|