@pascal-app/editor 0.7.0 → 0.9.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +12 -13
- package/src/components/editor/alignment-3d-guide-layer.tsx +160 -0
- package/src/components/editor/custom-camera-controls.tsx +755 -25
- package/src/components/editor/editor-layout-mobile.tsx +3 -3
- package/src/components/editor/editor-layout-v2.tsx +62 -54
- package/src/components/editor/export-manager.tsx +32 -3
- package/src/components/editor/first-person/build-collider-world.test.ts +149 -0
- package/src/components/editor/first-person/build-collider-world.ts +203 -87
- package/src/components/editor/first-person/bvh-ecctrl.tsx +127 -62
- package/src/components/editor/first-person-controls.tsx +836 -31
- package/src/components/editor/floating-action-menu.tsx +287 -196
- package/src/components/editor/floating-building-action-menu.tsx +1 -1
- package/src/components/editor/floorplan-panel.tsx +5837 -12006
- package/src/components/editor/grid.tsx +51 -17
- package/src/components/editor/group-move-handle.tsx +316 -0
- package/src/components/editor/group-rotate-handle.tsx +382 -0
- package/src/components/editor/group-transform-shared.test.ts +205 -0
- package/src/components/editor/group-transform-shared.ts +256 -0
- package/src/components/editor/handles/handle-arrow.tsx +523 -0
- package/src/components/editor/handles/use-handle-drag.ts +213 -0
- package/src/components/editor/index.tsx +234 -190
- package/src/components/editor/measurement-pill.tsx +75 -0
- package/src/components/editor/node-arrow-handles.tsx +1453 -0
- package/src/components/editor/selection-manager.tsx +435 -158
- package/src/components/editor/site-edge-labels.tsx +44 -20
- package/src/components/editor/slab-hole-highlights.tsx +466 -0
- package/src/components/editor/snapshot-capture-overlay.tsx +539 -0
- package/src/components/editor/thumbnail-generator.tsx +27 -64
- package/src/components/editor/use-floorplan-background-placement.ts +122 -35
- package/src/components/editor/use-floorplan-hit-testing.ts +24 -0
- package/src/components/editor/use-floorplan-scene-data.ts +14 -2
- package/src/components/editor/wall-measurement-label.tsx +11 -6
- package/src/components/editor/wall-move-side-handles.tsx +855 -0
- package/src/components/editor/wall-opening-highlights.tsx +181 -0
- package/src/components/editor/wall-snap-beacon-layer.tsx +150 -0
- package/src/components/editor-2d/floorplan-action-menu-layer.tsx +6 -1
- package/src/components/editor-2d/floorplan-alignment-guide-layer.tsx +142 -0
- package/src/components/editor-2d/floorplan-cursor-indicator-overlay.tsx +5 -3
- package/src/components/editor-2d/floorplan-registry-action-menu.tsx +227 -0
- package/src/components/editor-2d/floorplan-registry-move-overlay.tsx +616 -0
- package/src/components/editor-2d/floorplan-render-context.tsx +62 -0
- package/src/components/editor-2d/floorplan-snap-beacon-layer.tsx +75 -0
- package/src/components/editor-2d/floorplan-wall-move-ghost-layer.tsx +47 -0
- package/src/components/editor-2d/renderers/floorplan-draft-layer.tsx +6 -1
- package/src/components/editor-2d/renderers/floorplan-geometry-renderer.tsx +266 -0
- package/src/components/editor-2d/renderers/floorplan-measurements-layer.tsx +108 -61
- package/src/components/editor-2d/renderers/floorplan-placement-preview-layer.tsx +59 -0
- package/src/components/editor-2d/renderers/floorplan-registry-layer.tsx +1963 -0
- package/src/components/editor-2d/renderers/floorplan-stair-layer.tsx +29 -14
- package/src/components/editor-2d/svg-paths.ts +10 -2
- package/src/components/systems/ceiling/ceiling-selection-affordance-system.tsx +30 -9
- package/src/components/systems/ceiling/ceiling-system.tsx +12 -8
- package/src/components/systems/roof/roof-edit-system.tsx +86 -12
- package/src/components/systems/stair/stair-edit-system.tsx +2 -2
- package/src/components/systems/zone/zone-label-editor-system.tsx +0 -0
- package/src/components/systems/zone/zone-system.tsx +0 -0
- package/src/components/tools/elevator/elevator-defaults.ts +8 -0
- package/src/components/tools/elevator/elevator-tool.tsx +349 -0
- package/src/components/tools/elevator/move-elevator-tool.tsx +252 -0
- package/src/components/tools/fence/fence-drafting.ts +64 -22
- package/src/components/tools/item/move-tool.tsx +40 -107
- package/src/components/tools/item/placement-math.test.ts +10 -0
- package/src/components/tools/item/placement-math.ts +33 -9
- package/src/components/tools/item/placement-strategies.ts +201 -25
- package/src/components/tools/item/placement-types.ts +10 -1
- package/src/components/tools/item/use-draft-node.ts +20 -2
- package/src/components/tools/item/use-placement-coordinator.tsx +700 -299
- package/src/components/tools/registry/move-registry-node-tool.tsx +602 -0
- package/src/components/tools/roof/roof-tool.tsx +108 -12
- package/src/components/tools/select/box-select-state.ts +77 -0
- package/src/components/tools/select/box-select-tool.tsx +324 -543
- package/src/components/tools/select/plane-box-select-tool.tsx +577 -0
- package/src/components/tools/select/screen-rectangle-selection.ts +84 -0
- package/src/components/tools/select/select-candidates.test.ts +109 -0
- package/src/components/tools/select/select-candidates.ts +85 -0
- package/src/components/tools/shared/affordance-dispatch.ts +30 -0
- package/src/components/tools/shared/drag-bounding-box.tsx +171 -0
- package/src/components/tools/shared/floor-stack-preview.ts +25 -0
- package/src/components/tools/shared/fresh-placement-visibility.ts +61 -0
- package/src/components/tools/shared/placement-box-geometry.ts +123 -0
- package/src/components/tools/shared/placement-box.tsx +123 -0
- package/src/components/tools/shared/polygon-editor.tsx +617 -167
- package/src/components/tools/shared/segment-angle.ts +57 -1
- package/src/components/tools/site/site-boundary-editor.tsx +464 -12
- package/src/components/tools/site/site-flag-model.ts +18 -0
- package/src/components/tools/stair/stair-defaults.ts +1 -1
- package/src/components/tools/stair/stair-tool.tsx +269 -35
- package/src/components/tools/tool-manager.tsx +172 -56
- package/src/components/tools/wall/wall-drafting.ts +98 -98
- package/src/components/tools/wall/wall-snap-geometry.test.ts +79 -0
- package/src/components/tools/wall/wall-snap-geometry.ts +270 -0
- package/src/components/tools/zone/zone-tool.tsx +36 -11
- package/src/components/ui/action-menu/action-button.tsx +21 -1
- package/src/components/ui/action-menu/camera-actions.tsx +0 -0
- package/src/components/ui/action-menu/control-modes.tsx +5 -76
- package/src/components/ui/action-menu/furnish-tools.tsx +6 -92
- package/src/components/ui/action-menu/index.tsx +33 -196
- package/src/components/ui/action-menu/structure-tools.tsx +13 -88
- package/src/components/ui/action-menu/view-toggles.tsx +262 -37
- package/src/components/ui/command-palette/editor-commands.tsx +24 -17
- package/src/components/ui/command-palette/index.tsx +6 -256
- package/src/components/ui/controls/material-paint-panel.tsx +83 -0
- package/src/components/ui/controls/material-picker.tsx +17 -35
- package/src/components/ui/controls/metric-control.tsx +33 -11
- package/src/components/ui/floating-level-selector.tsx +48 -11
- package/src/components/ui/helpers/helper-manager.tsx +21 -17
- package/src/components/ui/helpers/registered-tool-helper.tsx +25 -0
- package/src/components/ui/item-catalog/catalog-items.tsx +1743 -315
- package/src/components/ui/item-catalog/item-catalog.tsx +91 -46
- package/src/components/ui/level-duplicate-dialog.tsx +5 -6
- package/src/components/ui/panels/node-display.ts +1 -0
- package/src/components/ui/panels/paint-panel.tsx +66 -41
- package/src/components/ui/panels/panel-manager.tsx +33 -74
- package/src/components/ui/panels/panel-wrapper.tsx +217 -12
- package/src/components/ui/panels/parametric-inspector.tsx +469 -0
- package/src/components/ui/panels/reference-panel.tsx +28 -13
- package/src/components/ui/primitives/dropdown-menu.tsx +12 -8
- package/src/components/ui/primitives/number-input.tsx +1 -1
- package/src/components/ui/primitives/sidebar.tsx +0 -0
- package/src/components/ui/scene-loader.tsx +1 -3
- package/src/components/ui/sidebar/app-sidebar.tsx +0 -0
- package/src/components/ui/sidebar/icon-rail.tsx +0 -0
- package/src/components/ui/sidebar/panels/items-panel/function-tree-panel.tsx +263 -0
- package/src/components/ui/sidebar/panels/items-panel/index.tsx +404 -0
- package/src/components/ui/sidebar/panels/settings-panel/index.tsx +59 -9
- package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +6 -1
- package/src/components/ui/sidebar/panels/settings-panel/load-build-dialog.tsx +259 -0
- package/src/components/ui/sidebar/panels/site-panel/building-tree-node.tsx +3 -1
- package/src/components/ui/sidebar/panels/site-panel/ceiling-tree-node.tsx +5 -2
- package/src/components/ui/sidebar/panels/site-panel/chimney-tree-node.tsx +83 -0
- package/src/components/ui/sidebar/panels/site-panel/dormer-tree-node.tsx +89 -0
- package/src/components/ui/sidebar/panels/site-panel/elevator-tree-node.tsx +75 -0
- package/src/components/ui/sidebar/panels/site-panel/fence-tree-node.tsx +2 -2
- package/src/components/ui/sidebar/panels/site-panel/gutter-tree-node.tsx +83 -0
- package/src/components/ui/sidebar/panels/site-panel/index.tsx +8 -12
- package/src/components/ui/sidebar/panels/site-panel/level-tree-node.tsx +2 -1
- package/src/components/ui/sidebar/panels/site-panel/registry-tree-node.tsx +93 -0
- package/src/components/ui/sidebar/panels/site-panel/roof-tree-node.tsx +41 -5
- package/src/components/ui/sidebar/panels/site-panel/shelf-tree-node.tsx +128 -0
- package/src/components/ui/sidebar/panels/site-panel/slab-tree-node.tsx +5 -2
- package/src/components/ui/sidebar/panels/site-panel/solar-panel-tree-node.tsx +83 -0
- package/src/components/ui/sidebar/panels/site-panel/spawn-tree-node.tsx +1 -1
- package/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +68 -34
- package/src/components/ui/sidebar/panels/site-panel/zone-tree-node.tsx +3 -1
- package/src/components/ui/sidebar/panels/zone-panel/index.tsx +52 -3
- package/src/components/ui/sidebar/tab-bar.tsx +61 -1
- package/src/components/ui/slider.tsx +1 -1
- package/src/components/viewer-overlay.tsx +169 -52
- package/src/components/viewer-zone-system.tsx +1 -1
- package/src/hooks/use-auto-save.ts +24 -3
- package/src/hooks/use-ceiling-events.ts +175 -0
- package/src/hooks/use-drag-action.ts +124 -0
- package/src/hooks/use-keyboard.ts +125 -62
- package/src/hooks/use-selection.ts +64 -0
- package/src/index.tsx +243 -4
- package/src/lib/constants.ts +6 -2
- package/src/lib/editor-api.ts +62 -0
- package/src/lib/elevator-support.ts +96 -0
- package/src/lib/floorplan/apply-alignment.ts +111 -0
- package/src/lib/floorplan/index.ts +7 -0
- package/src/lib/floorplan/selection-tool.ts +40 -0
- package/src/lib/floorplan/stairs.ts +12 -1
- package/src/lib/fresh-planar-placement.test.ts +102 -0
- package/src/lib/fresh-planar-placement.ts +63 -0
- package/src/lib/history.ts +2 -1
- package/src/lib/level-duplication.test.ts +0 -2
- package/src/lib/level-duplication.ts +55 -43
- package/src/lib/level-name.ts +11 -0
- package/src/lib/material-paint.ts +212 -20
- package/src/lib/placement-metadata.ts +29 -0
- package/src/lib/planar-cursor-placement.test.ts +43 -0
- package/src/lib/planar-cursor-placement.ts +42 -0
- package/src/lib/roof-duplication.ts +2 -2
- package/src/lib/scene-bounds.ts +1 -1
- package/src/lib/scene-clipboard.ts +267 -0
- package/src/lib/scene.ts +13 -4
- package/src/lib/sfx-bus.ts +25 -1
- package/src/lib/sfx-player.ts +75 -27
- package/src/lib/site-boundary.ts +1 -0
- package/src/lib/stair-duplication.ts +2 -2
- package/src/lib/stair-levels.test.ts +146 -0
- package/src/lib/stair-levels.ts +177 -0
- package/src/lib/world-grid-snap.ts +199 -0
- package/src/lib/zone-content.ts +130 -0
- package/src/store/use-alignment-guides.ts +21 -0
- package/src/store/use-editor.tsx +342 -91
- package/src/store/use-placement-preview.ts +31 -0
- package/src/store/use-wall-move-ghosts.ts +36 -0
- package/src/store/use-wall-snap-indicator.ts +32 -0
- package/tsconfig.json +2 -1
- package/src/components/editor/preset-thumbnail-generator.tsx +0 -125
- package/src/components/editor-2d/renderers/floorplan-roof-layer.tsx +0 -113
- package/src/components/feedback-dialog.tsx +0 -265
- package/src/components/pascal-radio.tsx +0 -280
- package/src/components/preview-button.tsx +0 -16
- package/src/components/tools/building/move-building-tool.tsx +0 -157
- package/src/components/tools/ceiling/ceiling-boundary-editor.tsx +0 -43
- package/src/components/tools/ceiling/ceiling-hole-editor.tsx +0 -49
- package/src/components/tools/ceiling/ceiling-tool.tsx +0 -465
- package/src/components/tools/ceiling/move-ceiling-tool.tsx +0 -257
- package/src/components/tools/column/column-tool.tsx +0 -97
- package/src/components/tools/column/move-column-tool.tsx +0 -105
- package/src/components/tools/door/door-math.ts +0 -110
- package/src/components/tools/door/door-tool.tsx +0 -324
- package/src/components/tools/door/move-door-tool.tsx +0 -412
- package/src/components/tools/fence/curve-fence-tool.tsx +0 -179
- package/src/components/tools/fence/fence-tool.tsx +0 -346
- package/src/components/tools/fence/move-fence-endpoint-tool.tsx +0 -438
- package/src/components/tools/fence/move-fence-tool.tsx +0 -298
- package/src/components/tools/item/item-tool.tsx +0 -26
- package/src/components/tools/roof/move-roof-tool.tsx +0 -359
- package/src/components/tools/slab/move-slab-tool.tsx +0 -182
- package/src/components/tools/slab/slab-boundary-editor.tsx +0 -43
- package/src/components/tools/slab/slab-hole-editor.tsx +0 -49
- package/src/components/tools/slab/slab-tool.tsx +0 -322
- package/src/components/tools/spawn/move-spawn-tool.tsx +0 -101
- package/src/components/tools/spawn/spawn-tool.tsx +0 -130
- package/src/components/tools/wall/curve-wall-tool.tsx +0 -176
- package/src/components/tools/wall/move-wall-endpoint-tool.tsx +0 -423
- package/src/components/tools/wall/move-wall-tool.tsx +0 -356
- package/src/components/tools/wall/wall-tool.tsx +0 -332
- package/src/components/tools/window/move-window-tool.tsx +0 -447
- package/src/components/tools/window/window-math.ts +0 -117
- package/src/components/tools/window/window-tool.tsx +0 -332
- package/src/components/ui/helpers/ceiling-helper.tsx +0 -20
- package/src/components/ui/helpers/slab-helper.tsx +0 -20
- package/src/components/ui/helpers/wall-helper.tsx +0 -20
- package/src/components/ui/panels/ceiling-panel.tsx +0 -250
- package/src/components/ui/panels/column-panel.tsx +0 -715
- package/src/components/ui/panels/door-panel.tsx +0 -1292
- package/src/components/ui/panels/fence-panel.tsx +0 -227
- package/src/components/ui/panels/item-panel.tsx +0 -306
- package/src/components/ui/panels/presets/presets-popover.tsx +0 -511
- package/src/components/ui/panels/roof-panel.tsx +0 -230
- package/src/components/ui/panels/roof-segment-panel.tsx +0 -311
- package/src/components/ui/panels/slab-panel.tsx +0 -250
- package/src/components/ui/panels/spawn-panel.tsx +0 -155
- package/src/components/ui/panels/stair-panel.tsx +0 -525
- package/src/components/ui/panels/stair-segment-panel.tsx +0 -325
- package/src/components/ui/panels/wall-panel.tsx +0 -177
- package/src/components/ui/panels/window-panel.tsx +0 -960
- package/src/components/ui/viewer-toolbar.tsx +0 -436
- package/src/contexts/presets-context.tsx +0 -121
- package/src/hooks/use-contextual-tools.ts +0 -61
|
@@ -83,6 +83,14 @@ function getNormalizedFloorplanStairSweepAngle(stair: StairNode) {
|
|
|
83
83
|
return baseSweepAngle
|
|
84
84
|
}
|
|
85
85
|
|
|
86
|
+
function clampFloorplanCircularSweepAngle(sweepAngle: number) {
|
|
87
|
+
if (Math.abs(sweepAngle) >= Math.PI * 2) {
|
|
88
|
+
return Math.sign(sweepAngle || 1) * (Math.PI * 2 - 0.001)
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return sweepAngle
|
|
92
|
+
}
|
|
93
|
+
|
|
86
94
|
function getFloorplanStairStepCount(stair: StairNode, minimum: number) {
|
|
87
95
|
return Math.max(minimum, Math.round(stair.stepCount ?? 10))
|
|
88
96
|
}
|
|
@@ -139,7 +147,10 @@ export const FloorplanStairLayer = memo(function FloorplanStairLayer({
|
|
|
139
147
|
const sectorStartAngle = -stair.rotation - normalizedSweepAngle / 2
|
|
140
148
|
const sectorEndAngle = sectorStartAngle + normalizedSweepAngle
|
|
141
149
|
const spiralLandingSweep = getFloorplanSpiralLandingSweep(stair, normalizedSweepAngle)
|
|
142
|
-
const
|
|
150
|
+
const visualSweepAngle = clampFloorplanCircularSweepAngle(
|
|
151
|
+
normalizedSweepAngle + spiralLandingSweep,
|
|
152
|
+
)
|
|
153
|
+
const visualSectorEndAngle = sectorStartAngle + visualSweepAngle
|
|
143
154
|
const stairCenter = {
|
|
144
155
|
x: stair.position[0],
|
|
145
156
|
y: stair.position[2],
|
|
@@ -273,10 +284,12 @@ export const FloorplanStairLayer = memo(function FloorplanStairLayer({
|
|
|
273
284
|
fill={curvedAccent}
|
|
274
285
|
key={`${stair.id}:spiral-arrow`}
|
|
275
286
|
pointerEvents="none"
|
|
276
|
-
points={
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
287
|
+
points={formatSvgPolygonPoints(
|
|
288
|
+
buildSvgArrowHeadPoints(
|
|
289
|
+
arrowPoint,
|
|
290
|
+
tangentAngle,
|
|
291
|
+
clamp(stair.width * 0.18, 0.12, 0.18),
|
|
292
|
+
),
|
|
280
293
|
)}
|
|
281
294
|
/>
|
|
282
295
|
)
|
|
@@ -361,10 +374,12 @@ export const FloorplanStairLayer = memo(function FloorplanStairLayer({
|
|
|
361
374
|
fill={curvedAccent}
|
|
362
375
|
key={`${stair.id}:curved-arrow`}
|
|
363
376
|
pointerEvents="none"
|
|
364
|
-
points={
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
377
|
+
points={formatSvgPolygonPoints(
|
|
378
|
+
buildSvgArrowHeadPoints(
|
|
379
|
+
arrowPoint,
|
|
380
|
+
tangentAngle,
|
|
381
|
+
clamp(stair.width * 0.16, 0.1, 0.16),
|
|
382
|
+
),
|
|
368
383
|
)}
|
|
369
384
|
/>
|
|
370
385
|
)
|
|
@@ -396,8 +411,8 @@ export const FloorplanStairLayer = memo(function FloorplanStairLayer({
|
|
|
396
411
|
<>
|
|
397
412
|
<polyline
|
|
398
413
|
fill="none"
|
|
399
|
-
points={formatSvgPolygonPoints(arrow.polyline)}
|
|
400
414
|
pointerEvents="none"
|
|
415
|
+
points={formatSvgPolygonPoints(arrow.polyline)}
|
|
401
416
|
stroke={straightAccent}
|
|
402
417
|
strokeWidth="1.15"
|
|
403
418
|
vectorEffect="non-scaling-stroke"
|
|
@@ -411,8 +426,8 @@ export const FloorplanStairLayer = memo(function FloorplanStairLayer({
|
|
|
411
426
|
/>
|
|
412
427
|
<polygon
|
|
413
428
|
fill={straightAccent}
|
|
414
|
-
points={formatSvgPolygonPoints(arrow.head)}
|
|
415
429
|
pointerEvents="none"
|
|
430
|
+
points={formatSvgPolygonPoints(arrow.head)}
|
|
416
431
|
/>
|
|
417
432
|
</>
|
|
418
433
|
) : null}
|
|
@@ -438,8 +453,6 @@ export const FloorplanStairLayer = memo(function FloorplanStairLayer({
|
|
|
438
453
|
}
|
|
439
454
|
: undefined
|
|
440
455
|
}
|
|
441
|
-
onPointerEnter={canSelectStairs ? () => onStairHoverEnter(stair.id) : undefined}
|
|
442
|
-
onPointerLeave={canSelectStairs ? () => onStairHoverChange(null) : undefined}
|
|
443
456
|
onPointerDown={
|
|
444
457
|
canFocusStairs && stairSelected
|
|
445
458
|
? (event) => {
|
|
@@ -449,6 +462,8 @@ export const FloorplanStairLayer = memo(function FloorplanStairLayer({
|
|
|
449
462
|
}
|
|
450
463
|
: undefined
|
|
451
464
|
}
|
|
465
|
+
onPointerEnter={canSelectStairs ? () => onStairHoverEnter(stair.id) : undefined}
|
|
466
|
+
onPointerLeave={canSelectStairs ? () => onStairHoverChange(null) : undefined}
|
|
452
467
|
pointerEvents={canSelectStairs ? undefined : 'none'}
|
|
453
468
|
style={canSelectStairs ? { cursor } : undefined}
|
|
454
469
|
>
|
|
@@ -456,8 +471,8 @@ export const FloorplanStairLayer = memo(function FloorplanStairLayer({
|
|
|
456
471
|
<polygon
|
|
457
472
|
fill="transparent"
|
|
458
473
|
key={`${stair.id}:hit:${polygonIndex}`}
|
|
459
|
-
points={formatSvgPolygonPoints(polygon)}
|
|
460
474
|
pointerEvents={canSelectStairs ? 'all' : 'none'}
|
|
475
|
+
points={formatSvgPolygonPoints(polygon)}
|
|
461
476
|
stroke="transparent"
|
|
462
477
|
strokeLinejoin="round"
|
|
463
478
|
strokeWidth={hitStrokeWidth}
|
|
@@ -103,7 +103,15 @@ export function formatSvgPolygonPoints(points: Point2D[]) {
|
|
|
103
103
|
return points.map((point) => `${toSvgX(point.x)},${toSvgY(point.y)}`).join(' ')
|
|
104
104
|
}
|
|
105
105
|
|
|
106
|
-
|
|
106
|
+
/**
|
|
107
|
+
* Three points defining an arrow head — tip + two trailing barbs.
|
|
108
|
+
* Returned as plain `Point2D` objects so consumers can either feed them
|
|
109
|
+
* straight into `formatSvgPolygonPoints` (for SVG `points=""`) or push
|
|
110
|
+
* them onto a `FloorplanGeometry.polygon.points` array. Mixing both
|
|
111
|
+
* downstream paths through a string-returning helper was awkward — see
|
|
112
|
+
* `nodes/src/stair/floorplan.ts` which needs the points as objects.
|
|
113
|
+
*/
|
|
114
|
+
export function buildSvgArrowHeadPoints(point: Point2D, angle: number, size: number): Point2D[] {
|
|
107
115
|
const left = {
|
|
108
116
|
x: point.x - size * Math.cos(angle - Math.PI / 6),
|
|
109
117
|
y: point.y - size * Math.sin(angle - Math.PI / 6),
|
|
@@ -113,7 +121,7 @@ export function buildSvgArrowHeadPoints(point: Point2D, angle: number, size: num
|
|
|
113
121
|
y: point.y - size * Math.sin(angle + Math.PI / 6),
|
|
114
122
|
}
|
|
115
123
|
|
|
116
|
-
return
|
|
124
|
+
return [point, left, right]
|
|
117
125
|
}
|
|
118
126
|
|
|
119
127
|
export { toSvgPoint, toSvgX, toSvgY }
|
|
@@ -18,6 +18,10 @@ const BRACKET_THICKNESS = 0.04
|
|
|
18
18
|
const BRACKET_HEIGHT = 0.04
|
|
19
19
|
const BRACKET_Y_OFFSET = 0.035
|
|
20
20
|
const HIT_BOX_SIZE: [number, number, number] = [0.28, 0.08, 0.28]
|
|
21
|
+
// Draw the corner handles after everything else and with depth testing
|
|
22
|
+
// off (see materials below) so they stay visible — and clickable — even
|
|
23
|
+
// when a wall, roof, or the ceiling itself would otherwise occlude them.
|
|
24
|
+
const CORNER_RENDER_ORDER = 1000
|
|
21
25
|
|
|
22
26
|
type CornerBracketData = {
|
|
23
27
|
corner: [number, number]
|
|
@@ -75,7 +79,9 @@ const CeilingSelectionAffordance = ({
|
|
|
75
79
|
ceiling: CeilingNode
|
|
76
80
|
levelId: string
|
|
77
81
|
}) => {
|
|
78
|
-
const [levelObject, setLevelObject] = useState<Object3D | null>(
|
|
82
|
+
const [levelObject, setLevelObject] = useState<Object3D | null>(
|
|
83
|
+
() => sceneRegistry.nodes.get(levelId) ?? null,
|
|
84
|
+
)
|
|
79
85
|
|
|
80
86
|
const corners = useMemo(() => buildCornerBrackets(ceiling.polygon), [ceiling.polygon])
|
|
81
87
|
|
|
@@ -110,11 +116,7 @@ const CeilingSelectionAffordance = ({
|
|
|
110
116
|
return createPortal(
|
|
111
117
|
<group position={[0, (ceiling.height ?? 2.5) + BRACKET_Y_OFFSET, 0]}>
|
|
112
118
|
{corners.map((corner, index) => (
|
|
113
|
-
<CornerBracket
|
|
114
|
-
ceiling={ceiling}
|
|
115
|
-
corner={corner}
|
|
116
|
-
key={`${ceiling.id}-corner-${index}`}
|
|
117
|
-
/>
|
|
119
|
+
<CornerBracket ceiling={ceiling} corner={corner} key={`${ceiling.id}-corner-${index}`} />
|
|
118
120
|
))}
|
|
119
121
|
</group>,
|
|
120
122
|
levelObject,
|
|
@@ -151,6 +153,7 @@ const CornerBracket = ({
|
|
|
151
153
|
localPosition: [0, 0, 0],
|
|
152
154
|
position: [corner.corner[0], ceiling.height ?? 2.5, corner.corner[1]],
|
|
153
155
|
stopPropagation: () => e.stopPropagation(),
|
|
156
|
+
viaHandle: true,
|
|
154
157
|
})
|
|
155
158
|
}
|
|
156
159
|
|
|
@@ -181,9 +184,16 @@ const CornerBracket = ({
|
|
|
181
184
|
e.stopPropagation()
|
|
182
185
|
setIsHovered(false)
|
|
183
186
|
}}
|
|
187
|
+
renderOrder={CORNER_RENDER_ORDER}
|
|
184
188
|
>
|
|
185
189
|
<boxGeometry args={HIT_BOX_SIZE} />
|
|
186
|
-
<meshBasicMaterial
|
|
190
|
+
<meshBasicMaterial
|
|
191
|
+
color={cubeColor}
|
|
192
|
+
depthTest={false}
|
|
193
|
+
depthWrite={false}
|
|
194
|
+
opacity={cubeOpacity}
|
|
195
|
+
transparent
|
|
196
|
+
/>
|
|
187
197
|
</mesh>
|
|
188
198
|
</group>
|
|
189
199
|
)
|
|
@@ -213,10 +223,17 @@ const BracketLeg = ({
|
|
|
213
223
|
<mesh
|
|
214
224
|
onClick={onClick}
|
|
215
225
|
position={position}
|
|
226
|
+
renderOrder={CORNER_RENDER_ORDER}
|
|
216
227
|
rotation={[0, angle, 0]}
|
|
217
228
|
>
|
|
218
229
|
<boxGeometry args={[length, BRACKET_HEIGHT, BRACKET_THICKNESS]} />
|
|
219
|
-
<meshBasicMaterial
|
|
230
|
+
<meshBasicMaterial
|
|
231
|
+
color={color}
|
|
232
|
+
depthTest={false}
|
|
233
|
+
depthWrite={false}
|
|
234
|
+
opacity={opacity}
|
|
235
|
+
transparent
|
|
236
|
+
/>
|
|
220
237
|
</mesh>
|
|
221
238
|
)
|
|
222
239
|
}
|
|
@@ -234,7 +251,11 @@ function buildCornerBrackets(polygon: Array<[number, number]>): CornerBracketDat
|
|
|
234
251
|
|
|
235
252
|
const incomingLength = Math.hypot(incomingVector[0], incomingVector[1])
|
|
236
253
|
const outgoingLength = Math.hypot(outgoingVector[0], outgoingVector[1])
|
|
237
|
-
const cornerStrength =
|
|
254
|
+
const cornerStrength =
|
|
255
|
+
1 -
|
|
256
|
+
Math.abs(
|
|
257
|
+
incomingDirection[0] * outgoingDirection[0] + incomingDirection[1] * outgoingDirection[1],
|
|
258
|
+
)
|
|
238
259
|
|
|
239
260
|
return {
|
|
240
261
|
corner,
|
|
@@ -25,15 +25,19 @@ export const CeilingSystem = () => {
|
|
|
25
25
|
}
|
|
26
26
|
|
|
27
27
|
for (const id of selectedIds) {
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
28
|
+
// Only treat a directly-selected ceiling as "reveal the grid"; a
|
|
29
|
+
// selected descendant (e.g. a freshly-placed ceiling light) used to
|
|
30
|
+
// count too, which left the opaque grid overlay covering the room
|
|
31
|
+
// even after the user moved on. With the grid still in front, every
|
|
32
|
+
// subsequent click in 3D hit the grid mesh (its `useNodeEvents`
|
|
33
|
+
// handlers re-selected the ceiling) instead of the items below.
|
|
34
|
+
const selectedNode = nodes[id as AnyNodeId]
|
|
35
|
+
if (selectedNode?.type !== 'ceiling') continue
|
|
31
36
|
|
|
37
|
+
let currentId: string | null = selectedNode.parentId as string | null
|
|
38
|
+
let levelId: string | null = null
|
|
32
39
|
while (currentId && nodes[currentId as AnyNodeId]) {
|
|
33
40
|
const node = nodes[currentId as AnyNodeId]
|
|
34
|
-
if (node?.type === 'ceiling') {
|
|
35
|
-
isCeilingRelated = true
|
|
36
|
-
}
|
|
37
41
|
if (node?.type === 'level') {
|
|
38
42
|
levelId = node.id
|
|
39
43
|
break
|
|
@@ -41,12 +45,12 @@ export const CeilingSystem = () => {
|
|
|
41
45
|
currentId = node?.parentId as string | null
|
|
42
46
|
}
|
|
43
47
|
|
|
44
|
-
if (
|
|
48
|
+
if (levelId) {
|
|
45
49
|
levelsToShowCeilings.add(levelId)
|
|
46
50
|
}
|
|
47
51
|
}
|
|
48
52
|
|
|
49
|
-
const ceilings = sceneRegistry.byType.ceiling
|
|
53
|
+
const ceilings = sceneRegistry.byType.ceiling!
|
|
50
54
|
ceilings.forEach((ceiling) => {
|
|
51
55
|
const mesh = sceneRegistry.nodes.get(ceiling)
|
|
52
56
|
if (mesh) {
|
|
@@ -1,41 +1,96 @@
|
|
|
1
1
|
import { type AnyNodeId, type RoofNode, sceneRegistry, useScene } from '@pascal-app/core'
|
|
2
2
|
import { useViewer } from '@pascal-app/viewer'
|
|
3
3
|
import { useEffect, useRef } from 'react'
|
|
4
|
+
import * as THREE from 'three'
|
|
5
|
+
|
|
6
|
+
// Empty placeholder geometry used when we reveal segments-wrapper for
|
|
7
|
+
// accessory editing. The roof's CSG-merged shell is the only thing
|
|
8
|
+
// that should render the roof surface in this mode — the per-segment
|
|
9
|
+
// CSG geometry (if any was left over from a prior edit) would visually
|
|
10
|
+
// double the cut shape, so we strip each segment mesh back to nothing.
|
|
11
|
+
// `RoofSystem` rebuilds CSG on demand if the user later selects a
|
|
12
|
+
// segment, so destroying the cached geometry here only costs one
|
|
13
|
+
// recomputation per segment when the user actually wants it back.
|
|
14
|
+
function makeEmptySegmentGeometry(): THREE.BufferGeometry {
|
|
15
|
+
const g = new THREE.BufferGeometry()
|
|
16
|
+
// Three zero-vertices (one degenerate, invisible triangle), not an empty
|
|
17
|
+
// attribute: in accessory-reveal mode the segments-wrapper is shown, so these
|
|
18
|
+
// meshes are drawn. An empty position (count 0) leaves WebGPU vertex buffer
|
|
19
|
+
// slot 0 unbound and the draw is rejected, poisoning the command encoder.
|
|
20
|
+
g.setAttribute('position', new THREE.Float32BufferAttribute(new Float32Array(9), 3))
|
|
21
|
+
// Match the four material slots the roof-segment renderer's material
|
|
22
|
+
// array expects (0=top, 1=side, 2=interior, 3=shingle). Without these
|
|
23
|
+
// groups, mesh.material is a single-material lookup that mismatches
|
|
24
|
+
// the array — same crash mode the BoxGeometry workaround in
|
|
25
|
+
// `roof-system.tsx:144` guards against.
|
|
26
|
+
g.addGroup(0, 0, 0)
|
|
27
|
+
g.addGroup(0, 0, 1)
|
|
28
|
+
g.addGroup(0, 0, 2)
|
|
29
|
+
g.addGroup(0, 0, 3)
|
|
30
|
+
return g
|
|
31
|
+
}
|
|
4
32
|
|
|
5
33
|
/**
|
|
6
34
|
* Imperatively toggles the Three.js visibility of roof objects based on the
|
|
7
35
|
* editor selection — without causing React re-renders in RoofRenderer.
|
|
8
36
|
*
|
|
9
|
-
*
|
|
37
|
+
* Full edit-mode (segment selected):
|
|
10
38
|
* - merged-roof mesh is hidden
|
|
11
39
|
* - segments-wrapper group is shown (individual segments visible for editing)
|
|
12
40
|
* - all children are marked dirty so RoofSystem rebuilds their geometry
|
|
13
41
|
*
|
|
14
|
-
*
|
|
15
|
-
* - merged-roof mesh
|
|
16
|
-
* - segments-wrapper group is
|
|
42
|
+
* Accessory-reveal mode (a dormer/chimney/etc. hosted on a segment is selected):
|
|
43
|
+
* - merged-roof mesh stays visible (we don't want the appearance to jump)
|
|
44
|
+
* - segments-wrapper group is shown ANYWAY so anything portaled into a
|
|
45
|
+
* segment's registered mesh (e.g. dormer in-world handle arrows that
|
|
46
|
+
* don't use `portal: 'grandparent'`) is no longer inheriting the
|
|
47
|
+
* wrapper's hidden flag
|
|
48
|
+
* - segment placeholder geometry is empty, so revealing the wrapper has
|
|
49
|
+
* no visible cost beyond letting the handle arrows render
|
|
50
|
+
*
|
|
51
|
+
* When deselected: merged-roof shown, segments-wrapper hidden.
|
|
17
52
|
*/
|
|
18
53
|
export const RoofEditSystem = () => {
|
|
19
54
|
const selectedIds = useViewer((s) => s.selection.selectedIds)
|
|
20
55
|
const prevActiveRoofIds = useRef(new Set<string>())
|
|
56
|
+
const prevRevealRoofIds = useRef(new Set<string>())
|
|
21
57
|
|
|
22
58
|
useEffect(() => {
|
|
23
59
|
const nodes = useScene.getState().nodes
|
|
24
60
|
|
|
25
|
-
//
|
|
26
|
-
//
|
|
27
|
-
// material appearance does not jump between merged and per-segment meshes.
|
|
61
|
+
// Roofs where a segment itself is selected -> full edit mode (hide
|
|
62
|
+
// merged, show wrapper).
|
|
28
63
|
const activeRoofIds = new Set<string>()
|
|
64
|
+
// Roofs where an accessory (dormer/chimney/etc.) is selected -> only
|
|
65
|
+
// reveal the wrapper so handle portals into the segment mesh become
|
|
66
|
+
// visible. Merged stays on.
|
|
67
|
+
const revealRoofIds = new Set<string>()
|
|
68
|
+
|
|
29
69
|
for (const id of selectedIds) {
|
|
30
70
|
const node = nodes[id as AnyNodeId]
|
|
31
71
|
if (!node) continue
|
|
32
72
|
if (node.type === 'roof-segment' && node.parentId) {
|
|
33
73
|
activeRoofIds.add(node.parentId)
|
|
74
|
+
continue
|
|
75
|
+
}
|
|
76
|
+
// Walk up one level: if the parent is a roof-segment, this is a
|
|
77
|
+
// hosted accessory and we want to reveal its grandparent roof's
|
|
78
|
+
// wrapper. Two-step lookup keeps it scoped to roof children
|
|
79
|
+
// without enumerating all accessory kinds.
|
|
80
|
+
if (!node.parentId) continue
|
|
81
|
+
const parent = nodes[node.parentId as AnyNodeId]
|
|
82
|
+
if (parent?.type === 'roof-segment' && parent.parentId) {
|
|
83
|
+
revealRoofIds.add(parent.parentId)
|
|
34
84
|
}
|
|
35
85
|
}
|
|
36
86
|
|
|
37
|
-
//
|
|
38
|
-
const roofIdsToUpdate = new Set([
|
|
87
|
+
// Union of roofs that need ANY state change this tick.
|
|
88
|
+
const roofIdsToUpdate = new Set([
|
|
89
|
+
...activeRoofIds,
|
|
90
|
+
...revealRoofIds,
|
|
91
|
+
...prevActiveRoofIds.current,
|
|
92
|
+
...prevRevealRoofIds.current,
|
|
93
|
+
])
|
|
39
94
|
|
|
40
95
|
for (const roofId of roofIdsToUpdate) {
|
|
41
96
|
const group = sceneRegistry.nodes.get(roofId)
|
|
@@ -44,25 +99,44 @@ export const RoofEditSystem = () => {
|
|
|
44
99
|
const mergedMesh = group.getObjectByName('merged-roof')
|
|
45
100
|
const segmentsWrapper = group.getObjectByName('segments-wrapper')
|
|
46
101
|
const isActive = activeRoofIds.has(roofId)
|
|
102
|
+
const isReveal = revealRoofIds.has(roofId)
|
|
47
103
|
|
|
48
104
|
if (mergedMesh) mergedMesh.visible = !isActive
|
|
49
|
-
if (segmentsWrapper) segmentsWrapper.visible = isActive
|
|
105
|
+
if (segmentsWrapper) segmentsWrapper.visible = isActive || isReveal
|
|
50
106
|
|
|
51
107
|
const roofNode = nodes[roofId as AnyNodeId] as RoofNode | undefined
|
|
52
108
|
if (roofNode?.children?.length) {
|
|
53
109
|
const wasActive = prevActiveRoofIds.current.has(roofId)
|
|
110
|
+
const wasReveal = prevRevealRoofIds.current.has(roofId)
|
|
54
111
|
if (isActive !== wasActive) {
|
|
55
|
-
// Entering edit mode: rebuild
|
|
56
|
-
//
|
|
112
|
+
// Entering / exiting full edit mode: rebuild segment / merged
|
|
113
|
+
// geometries. Accessory-reveal doesn't need this — segments
|
|
114
|
+
// keep their placeholder; only their visibility flips.
|
|
57
115
|
const { markDirty } = useScene.getState()
|
|
58
116
|
for (const childId of roofNode.children) {
|
|
59
117
|
markDirty(childId as AnyNodeId)
|
|
60
118
|
}
|
|
61
119
|
}
|
|
120
|
+
// Entering reveal mode (and NOT also full-edit, which already
|
|
121
|
+
// owns its own rebuild path): strip each segment mesh back to
|
|
122
|
+
// an empty placeholder so the wrapper-now-visible doesn't
|
|
123
|
+
// re-show stale CSG geometry from a previous segment edit.
|
|
124
|
+
// Without this, the host segment's CSG cut renders ON TOP of
|
|
125
|
+
// the merged-roof, doubling the dormer's cut shape and
|
|
126
|
+
// bleeding the host wall material through the dormer body.
|
|
127
|
+
if (isReveal && !isActive && !wasReveal && segmentsWrapper) {
|
|
128
|
+
for (const child of segmentsWrapper.children) {
|
|
129
|
+
const mesh = child as THREE.Mesh
|
|
130
|
+
if (!mesh.isMesh) continue
|
|
131
|
+
mesh.geometry?.dispose()
|
|
132
|
+
mesh.geometry = makeEmptySegmentGeometry()
|
|
133
|
+
}
|
|
134
|
+
}
|
|
62
135
|
}
|
|
63
136
|
}
|
|
64
137
|
|
|
65
138
|
prevActiveRoofIds.current = activeRoofIds
|
|
139
|
+
prevRevealRoofIds.current = revealRoofIds
|
|
66
140
|
}, [selectedIds])
|
|
67
141
|
|
|
68
142
|
return null
|
|
@@ -68,7 +68,7 @@ export const StairEditSystem = () => {
|
|
|
68
68
|
const segmentsWrapper = group.getObjectByName('segments-wrapper')
|
|
69
69
|
const isActive = activeStairIds.has(stairId)
|
|
70
70
|
|
|
71
|
-
if (mergedMesh) mergedMesh.visible = !isActive
|
|
71
|
+
if (mergedMesh) mergedMesh.visible = !(isActive || isCurved)
|
|
72
72
|
if (segmentsWrapper) segmentsWrapper.visible = isActive && !isCurved
|
|
73
73
|
|
|
74
74
|
if (stairNode?.children?.length) {
|
|
@@ -85,7 +85,7 @@ export const StairEditSystem = () => {
|
|
|
85
85
|
}
|
|
86
86
|
|
|
87
87
|
prevActiveStairIds.current = activeStairIds
|
|
88
|
-
}, [selectedIds
|
|
88
|
+
}, [selectedIds])
|
|
89
89
|
|
|
90
90
|
return null
|
|
91
91
|
}
|
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export const DEFAULT_ELEVATOR_WIDTH = 1.84
|
|
2
|
+
export const DEFAULT_ELEVATOR_DEPTH = 1.84
|
|
3
|
+
export const DEFAULT_ELEVATOR_CAB_HEIGHT = 2.35
|
|
4
|
+
export const DEFAULT_ELEVATOR_DOOR_WIDTH = 0.95
|
|
5
|
+
export const DEFAULT_ELEVATOR_DOOR_HEIGHT = 2.1
|
|
6
|
+
export const DEFAULT_ELEVATOR_SPEED = 2.2
|
|
7
|
+
export const DEFAULT_ELEVATOR_DOOR_DURATION_MS = 900
|
|
8
|
+
export const DEFAULT_ELEVATOR_DWELL_MS = 1400
|