@pascal-app/editor 1.0.0-beta.5 → 1.0.0
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/LICENSE +21 -0
- package/bunfig.toml +4 -0
- package/package.json +13 -7
- package/scripts/generate-print-golden-house.ts +89 -0
- package/src/components/editor/bake-exporter.tsx +1 -0
- package/src/components/editor/camera-dragging-lifecycle.test.ts +41 -0
- package/src/components/editor/camera-dragging-lifecycle.ts +10 -2
- package/src/components/editor/capture-camera-rig.tsx +59 -0
- package/src/components/editor/custom-camera-controls.tsx +20 -11
- package/src/components/editor/editor-layout-mobile.tsx +6 -6
- package/src/components/editor/editor-layout-v2.tsx +9 -2
- package/src/components/editor/export-manager.tsx +175 -33
- package/src/components/editor/first-person/build-collider-world.test.ts +30 -0
- package/src/components/editor/first-person-controls.tsx +180 -12
- package/src/components/editor/floating-action-menu.tsx +42 -51
- package/src/components/editor/floorplan-panel.tsx +76 -12
- package/src/components/editor/grid.tsx +12 -7
- package/src/components/editor/group-actions.ts +34 -50
- package/src/components/editor/group-move-3d.ts +51 -21
- package/src/components/editor/group-transform-shared.test.ts +78 -0
- package/src/components/editor/group-transform-shared.ts +74 -7
- package/src/components/editor/handles/handle-arrow-raycast.test.ts +86 -0
- package/src/components/editor/handles/handle-arrow.tsx +109 -48
- package/src/components/editor/handles/linear-resize-drag.ts +67 -0
- package/src/components/editor/handles/resize-snap.test.ts +46 -0
- package/src/components/editor/handles/resize-snap.ts +10 -1
- package/src/components/editor/handles/use-handle-drag.ts +18 -1
- package/src/components/editor/index.tsx +140 -21
- package/src/components/editor/node-action-menu.tsx +14 -1
- package/src/components/editor/node-arrow-handles.tsx +47 -58
- package/src/components/editor/selection-manager.tsx +104 -42
- package/src/components/editor/snapshot-capture-overlay.tsx +298 -39
- package/src/components/editor/snapshot-capture.test.ts +422 -0
- package/src/components/editor/snapshot-capture.ts +145 -0
- package/src/components/editor/thumbnail-generator.tsx +284 -237
- package/src/components/editor/use-floorplan-background-placement.ts +53 -5
- package/src/components/editor/wall-move-side-handles.tsx +299 -7
- package/src/components/editor-2d/floorplan-group-move.tsx +67 -33
- package/src/components/editor-2d/floorplan-registered-tool-layer.tsx +3 -1
- package/src/components/editor-2d/floorplan-registry-move-overlay.tsx +129 -38
- package/src/components/editor-2d/renderers/floorplan-dimension-renderer.tsx +14 -1
- package/src/components/editor-2d/renderers/floorplan-geometry-renderer.test.tsx +16 -0
- package/src/components/editor-2d/renderers/floorplan-geometry-renderer.tsx +2 -0
- package/src/components/editor-2d/renderers/floorplan-placement-preview-layer.tsx +52 -3
- package/src/components/editor-2d/renderers/floorplan-registry-layer.test.ts +196 -1
- package/src/components/editor-2d/renderers/floorplan-registry-layer.tsx +180 -32
- package/src/components/systems/ceiling/ceiling-bracket-batch.test.ts +402 -0
- package/src/components/systems/ceiling/ceiling-bracket-batch.ts +371 -0
- package/src/components/systems/ceiling/ceiling-selection-affordance-system.test.ts +347 -0
- package/src/components/systems/ceiling/ceiling-selection-affordance-system.tsx +281 -305
- package/src/components/systems/roof/roof-edit-system.test.tsx +120 -0
- package/src/components/systems/roof/roof-edit-system.tsx +10 -19
- package/src/components/systems/selection-affordance-manager.tsx +59 -9
- package/src/components/systems/selection-affordance-services.ts +19 -0
- package/src/components/systems/stair/stair-edit-system.tsx +5 -2
- package/src/components/tools/fence/fence-drafting.test.ts +100 -0
- package/src/components/tools/fence/fence-drafting.ts +6 -25
- package/src/components/tools/item/draft-creation.test.ts +12 -0
- package/src/components/tools/item/draft-creation.ts +10 -0
- package/src/components/tools/item/face-host-commit.test.ts +65 -0
- package/src/components/tools/item/face-host-commit.ts +56 -0
- package/src/components/tools/item/face-host-preview.test.ts +77 -0
- package/src/components/tools/item/face-host-preview.ts +82 -0
- package/src/components/tools/item/move-tool.tsx +4 -3
- package/src/components/tools/item/placement-math.test.ts +11 -1
- package/src/components/tools/item/placement-math.ts +0 -20
- package/src/components/tools/item/placement-strategies.test.ts +331 -4
- package/src/components/tools/item/placement-strategies.ts +135 -2
- package/src/components/tools/item/placement-surface.test.ts +61 -0
- package/src/components/tools/item/placement-surface.ts +24 -0
- package/src/components/tools/item/placement-types.ts +6 -0
- package/src/components/tools/item/test-face-host.ts +121 -0
- package/src/components/tools/item/use-draft-node.test.tsx +336 -0
- package/src/components/tools/item/use-draft-node.ts +26 -4
- package/src/components/tools/item/use-placement-coordinator.tsx +428 -153
- package/src/components/tools/registered-tool-install-lifecycle.test.tsx +256 -0
- package/src/components/tools/registry/move-registry-node-tool.test.ts +10 -0
- package/src/components/tools/registry/move-registry-node-tool.tsx +306 -110
- package/src/components/tools/registry-tool-context.tsx +30 -0
- package/src/components/tools/select/box-select-tool.tsx +3 -1
- package/src/components/tools/select/marquee-footprint.test.ts +53 -0
- package/src/components/tools/select/marquee-footprint.ts +29 -0
- package/src/components/tools/shared/placement-box.tsx +57 -7
- package/src/components/tools/shared/placement-dimension-guides.tsx +121 -0
- package/src/components/tools/shared/pointer-support-cap.test.ts +319 -17
- package/src/components/tools/shared/pointer-support-cap.ts +45 -8
- package/src/components/tools/site/terrain-sculpt-grid.tsx +9 -5
- package/src/components/tools/site/terrain-sculpt-tool.tsx +1 -1
- package/src/components/tools/stair/stair-tool.tsx +127 -74
- package/src/components/tools/tool-manager.tsx +25 -8
- package/src/components/tools/wall/wall-drafting.test.ts +320 -2
- package/src/components/tools/wall/wall-drafting.ts +23 -114
- package/src/components/ui/action-menu/control-modes.tsx +9 -6
- package/src/components/ui/action-menu/index.tsx +11 -1
- package/src/components/ui/command-palette/editor-commands.tsx +9 -12
- package/src/components/ui/command-palette/index.tsx +7 -2
- package/src/components/ui/controls/material-paint-panel.tsx +6 -4
- package/src/components/ui/controls/material-picker.tsx +4 -4
- package/src/components/ui/controls/scene-material-list.tsx +6 -5
- package/src/components/ui/controls/segmented-control.tsx +8 -1
- package/src/components/ui/controls/slider-control.tsx +19 -3
- package/src/components/ui/controls/toggle-control.tsx +18 -5
- package/src/components/ui/controls/tool-options-panel.tsx +126 -0
- package/src/components/ui/floating-level-selector.tsx +22 -5
- package/src/components/ui/helpers/contextual-helper-panel.tsx +9 -0
- package/src/components/ui/helpers/helper-manager.tsx +47 -11
- package/src/components/ui/helpers/item-helper.tsx +1 -1
- package/src/components/ui/helpers/registered-tool-helper.tsx +21 -0
- package/src/components/ui/panels/homogeneous-selection.test.ts +80 -0
- package/src/components/ui/panels/homogeneous-selection.ts +47 -0
- package/src/components/ui/panels/mobile-selection-bar.tsx +11 -5
- package/src/components/ui/panels/multi-field-value.test.ts +209 -0
- package/src/components/ui/panels/multi-field-value.ts +163 -0
- package/src/components/ui/panels/multi-height-mode.tsx +185 -0
- package/src/components/ui/panels/multi-parametric-inspector.tsx +263 -0
- package/src/components/ui/panels/multi-selection-panel.tsx +48 -37
- package/src/components/ui/panels/node-display.ts +4 -0
- package/src/components/ui/panels/panel-manager.tsx +77 -3
- package/src/components/ui/panels/parametric-field-control.tsx +173 -0
- package/src/components/ui/panels/parametric-field-utils.ts +18 -0
- package/src/components/ui/panels/parametric-inspector.tsx +29 -162
- package/src/components/ui/panels/reference-panel.tsx +31 -1
- package/src/components/ui/primitives/shortcut-token.tsx +23 -1
- package/src/components/ui/scene-loader.tsx +35 -0
- package/src/components/ui/sidebar/panels/settings-panel/index.tsx +390 -36
- package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +20 -23
- package/src/components/ui/sidebar/panels/settings-panel/print-export-button.test.ts +140 -0
- package/src/components/ui/sidebar/panels/settings-panel/print-export-button.tsx +109 -0
- package/src/components/ui/sidebar/panels/site-panel/dormer-tree-node.tsx +19 -5
- package/src/components/ui/sidebar/panels/site-panel/index.tsx +116 -46
- package/src/components/ui/sidebar/panels/site-panel/level-tree-node.tsx +5 -2
- package/src/components/ui/sidebar/panels/site-panel/registry-tree-node.tsx +28 -11
- package/src/components/ui/sidebar/panels/site-panel/tree-node.test.ts +8 -0
- package/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +14 -6
- package/src/components/ui/sidebar/tab-bar.tsx +2 -0
- package/src/components/ui/sidebar/use-plugin-panels.tsx +17 -2
- package/src/components/viewer/viewer-scene-header.tsx +2 -1
- package/src/components/viewer/viewer-stage.tsx +4 -1
- package/src/components/viewer-overlay.tsx +1 -22
- package/src/components/viewer-zone-system.tsx +6 -2
- package/src/components/walkthrough-hud.tsx +19 -10
- package/src/hooks/use-auto-save.test.ts +73 -1
- package/src/hooks/use-auto-save.ts +76 -5
- package/src/hooks/use-ceiling-events.test.ts +122 -0
- package/src/hooks/use-drag-action.ts +17 -3
- package/src/hooks/use-grid-events.test.ts +240 -0
- package/src/hooks/use-grid-events.ts +190 -11
- package/src/hooks/use-keyboard.test.ts +244 -0
- package/src/hooks/use-keyboard.ts +129 -62
- package/src/hooks/use-registered-tool-enabled.ts +20 -0
- package/src/hooks/use-save-shortcut.ts +33 -0
- package/src/index.tsx +82 -4
- package/src/lib/active-placement-surface.test.ts +16 -0
- package/src/lib/active-placement-surface.ts +14 -0
- package/src/lib/ceiling-surface-raycast.test.ts +64 -0
- package/src/lib/ceiling-surface-raycast.ts +12 -0
- package/src/lib/contextual-help-extension.ts +17 -0
- package/src/lib/continuation.test.ts +11 -0
- package/src/lib/continuation.ts +8 -1
- package/src/lib/direct-manipulation.test.ts +132 -0
- package/src/lib/direct-manipulation.ts +38 -1
- package/src/lib/editor-api.ts +6 -3
- package/src/lib/elevation-guides.test.ts +28 -1
- package/src/lib/elevation-guides.ts +22 -7
- package/src/lib/floating-menu-scale.test.ts +29 -0
- package/src/lib/floating-menu-scale.ts +14 -0
- package/src/lib/floorplan/apply-alignment.test.ts +47 -2
- package/src/lib/floorplan/apply-alignment.ts +10 -1
- package/src/lib/floorplan/floorplan-export.test.ts +397 -1
- package/src/lib/floorplan/floorplan-export.tsx +111 -16
- package/src/lib/floorplan/floorplan-pdfkit-renderer.test.ts +26 -0
- package/src/lib/floorplan/floorplan-pdfkit-renderer.ts +32 -24
- package/src/lib/floorplan/floorplan-readonly.ts +1 -0
- package/src/lib/floorplan-grid-event-point.test.ts +29 -0
- package/src/lib/floorplan-grid-event-point.ts +13 -0
- package/src/lib/glb-export.test.ts +1169 -21
- package/src/lib/glb-export.ts +629 -101
- package/src/lib/grid-event-presentation.ts +16 -0
- package/src/lib/history.test.ts +528 -147
- package/src/lib/history.ts +81 -7
- package/src/lib/host-tree-children.test.ts +27 -0
- package/src/lib/host-tree-children.ts +79 -0
- package/src/lib/interaction/hot-set.test.ts +10 -1
- package/src/lib/interaction/hot-set.ts +3 -1
- package/src/lib/interaction/overlay-policy.test.ts +33 -1
- package/src/lib/interaction/overlay-policy.ts +16 -0
- package/src/lib/interaction/registered-drafting.ts +36 -0
- package/src/lib/interaction/scope.ts +31 -0
- package/src/lib/level-print-export.test.ts +484 -0
- package/src/lib/level-print-export.ts +526 -0
- package/src/lib/local-project-presentation-persistence.test.ts +302 -0
- package/src/lib/local-project-presentation-persistence.ts +335 -0
- package/src/lib/material-paint.ts +2 -0
- package/src/lib/model-export.ts +29 -0
- package/src/lib/paint-preview-owner.ts +71 -0
- package/src/lib/planar-cursor-placement.test.ts +178 -1
- package/src/lib/planar-cursor-placement.ts +61 -5
- package/src/lib/plugin-panels.test.ts +92 -1
- package/src/lib/plugin-panels.ts +48 -0
- package/src/lib/portable-export.test.ts +366 -0
- package/src/lib/portable-export.ts +969 -0
- package/src/lib/print-3mf.test.ts +86 -0
- package/src/lib/print-3mf.ts +161 -0
- package/src/lib/print-content-scope.test.ts +107 -0
- package/src/lib/print-content-scope.ts +90 -0
- package/src/lib/print-export.test.ts +294 -0
- package/src/lib/print-export.ts +735 -0
- package/src/lib/print-feature-thickness.test.ts +147 -0
- package/src/lib/print-feature-thickness.ts +180 -0
- package/src/lib/print-golden-house.test-fixture.ts +352 -0
- package/src/lib/print-golden-house.test.ts +249 -0
- package/src/lib/print-roof-solids.test.ts +133 -0
- package/src/lib/print-roof-solids.ts +630 -0
- package/src/lib/print-shell-compiler-baseline.test.ts +419 -0
- package/src/lib/print-shell-compiler-baseline.ts +225 -0
- package/src/lib/print-shell-compiler-manifold-core.ts +330 -0
- package/src/lib/print-shell-compiler-manifold-worker.ts +187 -0
- package/src/lib/print-shell-compiler-manifold.worker.ts +20 -0
- package/src/lib/print-shell-compiler-mesh-data.ts +32 -0
- package/src/lib/print-shell-compiler-protocol.ts +30 -0
- package/src/lib/print-shell-compiler.ts +273 -0
- package/src/lib/print-wall-solids.test.ts +194 -0
- package/src/lib/print-wall-solids.ts +328 -0
- package/src/lib/rigid-plan-svg-transform.test.ts +35 -0
- package/src/lib/rigid-plan-svg-transform.ts +24 -0
- package/src/lib/scene.ts +22 -8
- package/src/lib/selection-routing.test.ts +105 -1
- package/src/lib/selection-routing.ts +19 -0
- package/src/lib/sfx-bus.ts +4 -0
- package/src/lib/sfx-player.ts +8 -0
- package/src/lib/snapping-mode.test.ts +32 -4
- package/src/lib/snapping-mode.ts +11 -7
- package/src/lib/usdz-export.ts +38 -0
- package/src/lib/walkthrough-pointer-lock.ts +47 -0
- package/src/store/live-draft-preview-stores.test.ts +3 -0
- package/src/store/terrain-sculpt-mode.test.ts +3 -6
- package/src/store/tool-mode.test.ts +87 -0
- package/src/store/use-camera-hint-focus.ts +48 -0
- package/src/store/use-editor.tsx +299 -188
- package/src/store/use-floorplan-draft-preview.ts +9 -0
- package/src/store/use-interaction-scope.test.ts +23 -1
- package/src/store/use-interaction-scope.ts +63 -2
- package/src/store/use-placement-preview.ts +56 -3
- package/src/components/tools/roof/roof-tool.tsx +0 -656
- package/src/components/ui/helpers/roof-helper.tsx +0 -14
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
type FloorplanGeometry,
|
|
11
11
|
type FloorplanPalette,
|
|
12
12
|
type FloorplanPoint,
|
|
13
|
+
type FloorplanScope,
|
|
13
14
|
type GeometryContext,
|
|
14
15
|
isNodeKindEnabled,
|
|
15
16
|
isRegistryMovable,
|
|
@@ -26,7 +27,7 @@ import {
|
|
|
26
27
|
useLiveTransforms,
|
|
27
28
|
useScene,
|
|
28
29
|
} from '@pascal-app/core'
|
|
29
|
-
import { useViewer } from '@pascal-app/viewer'
|
|
30
|
+
import { beginPerfAction, cancelPerfAction, commitPerfAction, useViewer } from '@pascal-app/viewer'
|
|
30
31
|
import {
|
|
31
32
|
type ComponentProps,
|
|
32
33
|
memo,
|
|
@@ -46,6 +47,7 @@ import {
|
|
|
46
47
|
resolveDirectManipulationNode,
|
|
47
48
|
resolveDirectRotationDragDelta,
|
|
48
49
|
resolveDirectRotationPatch,
|
|
50
|
+
shouldStartDirectMoveDrag,
|
|
49
51
|
snapDirectRotationDelta,
|
|
50
52
|
} from '../../../lib/direct-manipulation'
|
|
51
53
|
import { createEditorApi } from '../../../lib/editor-api'
|
|
@@ -246,6 +248,7 @@ export function cancelFloorplanAffordanceDrag(
|
|
|
246
248
|
for (const id of drag.session.affectedIds) effects.clearPreview(id)
|
|
247
249
|
effects.endReshapeScope(drag)
|
|
248
250
|
effects.clearDragFeedback?.()
|
|
251
|
+
cancelPerfAction()
|
|
249
252
|
return true
|
|
250
253
|
}
|
|
251
254
|
|
|
@@ -310,6 +313,14 @@ export function floorplanAffordanceReshapeScope(
|
|
|
310
313
|
return null
|
|
311
314
|
}
|
|
312
315
|
|
|
316
|
+
function floorplanAffordancePerfAction(node: AnyNode, affordance: string): string {
|
|
317
|
+
if (affordance.includes('endpoint')) return `drag:${node.type}-endpoint`
|
|
318
|
+
if (affordance.includes('resize')) return 'drag:resize'
|
|
319
|
+
if (affordance.includes('rotate')) return 'drag:rotate'
|
|
320
|
+
if (affordance.includes('move')) return 'drag:move'
|
|
321
|
+
return 'drag:reshape'
|
|
322
|
+
}
|
|
323
|
+
|
|
313
324
|
/**
|
|
314
325
|
* Transient live-rotation readout state. Rebuilt each pointer-move while a
|
|
315
326
|
* rotate-arrow is dragged and cleared on release. World-plan coords.
|
|
@@ -328,6 +339,8 @@ type FloorplanEntryDescriptor = {
|
|
|
328
339
|
node: AnyNode
|
|
329
340
|
dependsOnSiblingInputs: boolean
|
|
330
341
|
ctxOverrides?: FloorplanContextOverrides
|
|
342
|
+
scopeRank?: number
|
|
343
|
+
visibilityRootId?: AnyNodeId
|
|
331
344
|
}
|
|
332
345
|
|
|
333
346
|
type NodeDeps = {
|
|
@@ -347,6 +360,7 @@ type NodeDeps = {
|
|
|
347
360
|
committedNodes: Record<string, AnyNode> | null
|
|
348
361
|
dependencyNodes: AnyNode[]
|
|
349
362
|
interactiveElevators: unknown
|
|
363
|
+
ctxOverrides: FloorplanContextOverrides | undefined
|
|
350
364
|
}
|
|
351
365
|
|
|
352
366
|
type CacheEntry = {
|
|
@@ -367,6 +381,40 @@ type FloorplanContextOverrides = {
|
|
|
367
381
|
children: AnyNode[]
|
|
368
382
|
siblings: AnyNode[]
|
|
369
383
|
parent: AnyNode | null
|
|
384
|
+
outputTransform?: { translate?: FloorplanPoint; rotate?: number }
|
|
385
|
+
trackAllNodes?: boolean
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
export function collectDirectFloorplanScopeNodes(
|
|
389
|
+
nodes: Record<string, AnyNode>,
|
|
390
|
+
parent: AnyNode,
|
|
391
|
+
scope: FloorplanScope,
|
|
392
|
+
): AnyNode[] {
|
|
393
|
+
const scopedKinds = new Set(kindsWithFloorplanScope(scope))
|
|
394
|
+
const declaredChildren = new Set(
|
|
395
|
+
Array.isArray((parent as { children?: AnyNodeId[] }).children)
|
|
396
|
+
? (parent as { children: AnyNodeId[] }).children
|
|
397
|
+
: [],
|
|
398
|
+
)
|
|
399
|
+
return Object.values(nodes).filter(
|
|
400
|
+
(node) =>
|
|
401
|
+
scopedKinds.has(node.type) && (node.parentId === parent.id || declaredChildren.has(node.id)),
|
|
402
|
+
)
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
export function siteToFloorplanTransform(
|
|
406
|
+
buildingPosition: readonly [number, number, number],
|
|
407
|
+
buildingRotationY: number,
|
|
408
|
+
): { translate: FloorplanPoint; rotate: number } {
|
|
409
|
+
const cos = Math.cos(buildingRotationY)
|
|
410
|
+
const sin = Math.sin(buildingRotationY)
|
|
411
|
+
return {
|
|
412
|
+
translate: [
|
|
413
|
+
-buildingPosition[0] * cos + buildingPosition[2] * sin,
|
|
414
|
+
-buildingPosition[0] * sin - buildingPosition[2] * cos,
|
|
415
|
+
],
|
|
416
|
+
rotate: buildingRotationY,
|
|
417
|
+
}
|
|
370
418
|
}
|
|
371
419
|
|
|
372
420
|
type FloorplanLevelDataHook = (args: {
|
|
@@ -477,6 +525,13 @@ export const FloorplanRegistryLayer = memo(function FloorplanRegistryLayer() {
|
|
|
477
525
|
|
|
478
526
|
const levelId = selectedLevelId ?? ambientLevelId
|
|
479
527
|
const isAmbient = !selectedLevelId && !!ambientLevelId
|
|
528
|
+
const activeBuildingId = useMemo(
|
|
529
|
+
() => (levelId ? resolveBuildingForLevel(levelId as AnyNodeId, nodes) : null),
|
|
530
|
+
[levelId, nodes],
|
|
531
|
+
)
|
|
532
|
+
const activeBuildingLiveTransform = useLiveTransforms((state) =>
|
|
533
|
+
activeBuildingId ? state.transforms.get(activeBuildingId) : undefined,
|
|
534
|
+
)
|
|
480
535
|
const renderCtx = useFloorplanStaticRender()
|
|
481
536
|
const sceneRotationDeg = renderCtx?.getSceneRotationDeg() ?? 0
|
|
482
537
|
const setMovingNode = useEditor((s) => s.setMovingNode)
|
|
@@ -644,16 +699,24 @@ export const FloorplanRegistryLayer = memo(function FloorplanRegistryLayer() {
|
|
|
644
699
|
|
|
645
700
|
const startDirectMoveDrag = useCallback(
|
|
646
701
|
(id: AnyNodeId, event: ReactPointerEvent<SVGGElement>): boolean => {
|
|
647
|
-
if (event.button !== 0
|
|
702
|
+
if (event.button !== 0) return false
|
|
648
703
|
|
|
649
704
|
const node = useScene.getState().nodes[id]
|
|
650
705
|
if (!node || !isRegistryMovable(node.type)) return false
|
|
651
|
-
// Sole selection only: per-node direct manipulation stands down for a
|
|
652
|
-
// multi-selection (the group session owns plain drags there, and Cmd is
|
|
653
|
-
// the selection-toggle key — a wobbly Cmd+click must not yank one
|
|
654
|
-
// member out of the group).
|
|
655
706
|
const currentSelectedIds = useViewer.getState().selection.selectedIds
|
|
656
|
-
|
|
707
|
+
const allowPlainDrag = nodeRegistry.get(node.type)?.capabilities?.movable?.directDrag === true
|
|
708
|
+
const commandModifier = event.metaKey || event.ctrlKey
|
|
709
|
+
if (
|
|
710
|
+
!shouldStartDirectMoveDrag({
|
|
711
|
+
allowPlainDrag,
|
|
712
|
+
commandModifier,
|
|
713
|
+
handleOwnsPointer: false,
|
|
714
|
+
nodeId: id,
|
|
715
|
+
selectedIds: currentSelectedIds,
|
|
716
|
+
})
|
|
717
|
+
) {
|
|
718
|
+
return false
|
|
719
|
+
}
|
|
657
720
|
|
|
658
721
|
event.preventDefault()
|
|
659
722
|
event.stopPropagation()
|
|
@@ -705,9 +768,8 @@ export const FloorplanRegistryLayer = memo(function FloorplanRegistryLayer() {
|
|
|
705
768
|
if (endEvent.pointerId !== pointerId) return
|
|
706
769
|
cleanup()
|
|
707
770
|
if (!engaged) {
|
|
708
|
-
// Cmd/Ctrl+click without drag: toggle member (options object, not bare boolean).
|
|
709
771
|
applyEntrySelection(id, {
|
|
710
|
-
shouldToggle:
|
|
772
|
+
shouldToggle: commandModifier,
|
|
711
773
|
isolateMember: false,
|
|
712
774
|
})
|
|
713
775
|
}
|
|
@@ -929,7 +991,12 @@ export const FloorplanRegistryLayer = memo(function FloorplanRegistryLayer() {
|
|
|
929
991
|
|
|
930
992
|
collectLevelDataKind(levelId as AnyNodeId)
|
|
931
993
|
|
|
932
|
-
const pushEntry = (
|
|
994
|
+
const pushEntry = (
|
|
995
|
+
id: AnyNodeId,
|
|
996
|
+
node: AnyNode,
|
|
997
|
+
ctxOverrides?: FloorplanContextOverrides,
|
|
998
|
+
options?: { scopeRank?: number; visibilityRootId?: AnyNodeId },
|
|
999
|
+
) => {
|
|
933
1000
|
if (!isNodeKindEnabled(node.type, installedPlugins)) return
|
|
934
1001
|
const drawingNode = resolveNodeForDrawingType(node, nodes, drawingType)
|
|
935
1002
|
if (!drawingNode) return
|
|
@@ -942,6 +1009,8 @@ export const FloorplanRegistryLayer = memo(function FloorplanRegistryLayer() {
|
|
|
942
1009
|
)
|
|
943
1010
|
const descriptor: FloorplanEntryDescriptor = { id, node: drawingNode, dependsOnSiblingInputs }
|
|
944
1011
|
if (ctxOverrides) descriptor.ctxOverrides = ctxOverrides
|
|
1012
|
+
if (options?.scopeRank !== undefined) descriptor.scopeRank = options.scopeRank
|
|
1013
|
+
if (options?.visibilityRootId) descriptor.visibilityRootId = options.visibilityRootId
|
|
945
1014
|
out.push(descriptor)
|
|
946
1015
|
}
|
|
947
1016
|
|
|
@@ -977,13 +1046,7 @@ export const FloorplanRegistryLayer = memo(function FloorplanRegistryLayer() {
|
|
|
977
1046
|
// as siblings of the level, not under it — the `visit(levelId)` DFS
|
|
978
1047
|
// above doesn't reach them. Walk every node of those kinds whose
|
|
979
1048
|
// parent matches the active level's building, and synthesise a
|
|
980
|
-
// `GeometryContext` whose `parent` is the active level
|
|
981
|
-
// builders that gate on the current floor — e.g. elevator service
|
|
982
|
-
// range — keep working). Pure registry-driven dispatch: no kind
|
|
983
|
-
// name appears in this file.
|
|
984
|
-
const activeBuildingId = activeLevelNode
|
|
985
|
-
? resolveBuildingForLevel(levelId as AnyNodeId, nodes)
|
|
986
|
-
: null
|
|
1049
|
+
// `GeometryContext` whose `parent` is the active level.
|
|
987
1050
|
if (activeLevelNode && activeBuildingId) {
|
|
988
1051
|
const buildingScopedKinds = kindsWithFloorplanScope('building')
|
|
989
1052
|
const buildingScopedKindSet = new Set(buildingScopedKinds)
|
|
@@ -1000,6 +1063,45 @@ export const FloorplanRegistryLayer = memo(function FloorplanRegistryLayer() {
|
|
|
1000
1063
|
}
|
|
1001
1064
|
}
|
|
1002
1065
|
|
|
1066
|
+
// Site-scoped kinds are semantic children of the Site and therefore
|
|
1067
|
+
// outside both the active-level DFS and the building sibling scan.
|
|
1068
|
+
// Their builders stay site-local; wrap their output in the inverse active
|
|
1069
|
+
// building transform so it aligns with this building-local plan.
|
|
1070
|
+
const activeBuildingNode = activeBuildingId ? nodes[activeBuildingId] : undefined
|
|
1071
|
+
const activeSiteNode =
|
|
1072
|
+
activeBuildingNode?.type === 'building' && activeBuildingNode.parentId
|
|
1073
|
+
? nodes[activeBuildingNode.parentId as AnyNodeId]
|
|
1074
|
+
: undefined
|
|
1075
|
+
if (activeSiteNode?.type === 'site' && activeBuildingNode?.type === 'building') {
|
|
1076
|
+
const siteScopedNodes = collectDirectFloorplanScopeNodes(nodes, activeSiteNode, 'site')
|
|
1077
|
+
const siteProjection = siteToFloorplanTransform(
|
|
1078
|
+
activeBuildingLiveTransform?.position ?? activeBuildingNode.position,
|
|
1079
|
+
activeBuildingLiveTransform?.rotation ?? activeBuildingNode.rotation[1],
|
|
1080
|
+
)
|
|
1081
|
+
for (const node of siteScopedNodes) {
|
|
1082
|
+
const children = Array.isArray((node as { children?: AnyNodeId[] }).children)
|
|
1083
|
+
? (node as { children: AnyNodeId[] }).children
|
|
1084
|
+
.map((id) => nodes[id])
|
|
1085
|
+
.filter((child): child is AnyNode => child !== undefined)
|
|
1086
|
+
: []
|
|
1087
|
+
const siblings = siteScopedNodes.filter(
|
|
1088
|
+
(candidate) => candidate.id !== node.id && candidate.type === node.type,
|
|
1089
|
+
)
|
|
1090
|
+
pushEntry(
|
|
1091
|
+
node.id,
|
|
1092
|
+
node,
|
|
1093
|
+
{
|
|
1094
|
+
children,
|
|
1095
|
+
siblings,
|
|
1096
|
+
parent: activeSiteNode,
|
|
1097
|
+
outputTransform: siteProjection,
|
|
1098
|
+
trackAllNodes: true,
|
|
1099
|
+
},
|
|
1100
|
+
{ scopeRank: -1, visibilityRootId: activeSiteNode.id },
|
|
1101
|
+
)
|
|
1102
|
+
}
|
|
1103
|
+
}
|
|
1104
|
+
|
|
1003
1105
|
// Stable z-order sort. SVG renders in document order — later siblings
|
|
1004
1106
|
// paint on top of earlier ones — so anything that should sit *under*
|
|
1005
1107
|
// other floor-plan geometry has to come first in the entries array.
|
|
@@ -1007,7 +1109,11 @@ export const FloorplanRegistryLayer = memo(function FloorplanRegistryLayer() {
|
|
|
1007
1109
|
// all belong on top of them. Within a layer bucket we preserve the
|
|
1008
1110
|
// DFS visit order (stable sort) so siblings keep their relative
|
|
1009
1111
|
// priority.
|
|
1010
|
-
out.sort(
|
|
1112
|
+
out.sort(
|
|
1113
|
+
(a, b) =>
|
|
1114
|
+
(a.scopeRank ?? 0) - (b.scopeRank ?? 0) ||
|
|
1115
|
+
floorplanLayerRank(a.node.type) - floorplanLayerRank(b.node.type),
|
|
1116
|
+
)
|
|
1011
1117
|
const entryIds = new Set(out.map((entry) => entry.id))
|
|
1012
1118
|
for (const id of geometryCacheRef.current.keys()) {
|
|
1013
1119
|
if (!entryIds.has(id as AnyNodeId)) geometryCacheRef.current.delete(id)
|
|
@@ -1016,7 +1122,7 @@ export const FloorplanRegistryLayer = memo(function FloorplanRegistryLayer() {
|
|
|
1016
1122
|
if (!levelNodeIdsByType.has(type)) levelDataCacheRef.current.delete(type)
|
|
1017
1123
|
}
|
|
1018
1124
|
return { entries: out, levelNodeIdsByType }
|
|
1019
|
-
}, [drawingType, installedPlugins, levelId, nodes])
|
|
1125
|
+
}, [activeBuildingId, activeBuildingLiveTransform, drawingType, installedPlugins, levelId, nodes])
|
|
1020
1126
|
|
|
1021
1127
|
// ── Generic 2D affordance dispatch ─────────────────────────────────
|
|
1022
1128
|
//
|
|
@@ -1048,6 +1154,7 @@ export const FloorplanRegistryLayer = memo(function FloorplanRegistryLayer() {
|
|
|
1048
1154
|
nodes: sceneNodes,
|
|
1049
1155
|
initialPlanPoint,
|
|
1050
1156
|
gridSnapStep: useEditor.getState().gridSnapStep,
|
|
1157
|
+
sceneApi: createSceneApi(useScene),
|
|
1051
1158
|
})
|
|
1052
1159
|
if (!(session.commit && session.canCommit())) return
|
|
1053
1160
|
session.commit()
|
|
@@ -1085,12 +1192,14 @@ export const FloorplanRegistryLayer = memo(function FloorplanRegistryLayer() {
|
|
|
1085
1192
|
event.stopPropagation()
|
|
1086
1193
|
suppressBoxSelectForPointer(event)
|
|
1087
1194
|
|
|
1195
|
+
beginPerfAction(floorplanAffordancePerfAction(node, affordance), `${node.type}:${node.id}`)
|
|
1088
1196
|
const session = handler.start({
|
|
1089
1197
|
node,
|
|
1090
1198
|
payload,
|
|
1091
1199
|
nodes: sceneNodes,
|
|
1092
1200
|
initialPlanPoint,
|
|
1093
1201
|
gridSnapStep: useEditor.getState().gridSnapStep,
|
|
1202
|
+
sceneApi: createSceneApi(useScene),
|
|
1094
1203
|
})
|
|
1095
1204
|
|
|
1096
1205
|
const snapshots: NodeSnapshot[] = []
|
|
@@ -1246,6 +1355,7 @@ export const FloorplanRegistryLayer = memo(function FloorplanRegistryLayer() {
|
|
|
1246
1355
|
drag.historyPaused = false
|
|
1247
1356
|
}
|
|
1248
1357
|
drag.session.commit()
|
|
1358
|
+
commitPerfAction()
|
|
1249
1359
|
sfxEmitter.emit('sfx:structure-build')
|
|
1250
1360
|
clearSurfacePlanSnapFeedback()
|
|
1251
1361
|
endReshapeScope(drag)
|
|
@@ -1287,6 +1397,7 @@ export const FloorplanRegistryLayer = memo(function FloorplanRegistryLayer() {
|
|
|
1287
1397
|
drag.historyPaused = false
|
|
1288
1398
|
}
|
|
1289
1399
|
useScene.getState().updateNodes(finalUpdates)
|
|
1400
|
+
commitPerfAction()
|
|
1290
1401
|
sfxEmitter.emit('sfx:structure-build')
|
|
1291
1402
|
} else {
|
|
1292
1403
|
// Either no net change or canCommit() rejected — revert and
|
|
@@ -1300,6 +1411,7 @@ export const FloorplanRegistryLayer = memo(function FloorplanRegistryLayer() {
|
|
|
1300
1411
|
}
|
|
1301
1412
|
const overrides = useLiveNodeOverrides.getState()
|
|
1302
1413
|
for (const id of drag.session.affectedIds) overrides.clear(id)
|
|
1414
|
+
cancelPerfAction()
|
|
1303
1415
|
}
|
|
1304
1416
|
|
|
1305
1417
|
clearSurfacePlanSnapFeedback()
|
|
@@ -1425,7 +1537,9 @@ export const FloorplanRegistryLayer = memo(function FloorplanRegistryLayer() {
|
|
|
1425
1537
|
unit={unit}
|
|
1426
1538
|
metricNotation={metricNotation}
|
|
1427
1539
|
wallDimensionReference={effectiveWallDimensionReference}
|
|
1428
|
-
visibilityRootId={
|
|
1540
|
+
visibilityRootId={
|
|
1541
|
+
entry.visibilityRootId ?? (entry.ctxOverrides ? undefined : (levelId as AnyNodeId))
|
|
1542
|
+
}
|
|
1429
1543
|
ctxOverrides={entry.ctxOverrides}
|
|
1430
1544
|
/>
|
|
1431
1545
|
))}
|
|
@@ -1471,7 +1585,9 @@ export const FloorplanRegistryLayer = memo(function FloorplanRegistryLayer() {
|
|
|
1471
1585
|
unit={unit}
|
|
1472
1586
|
metricNotation={metricNotation}
|
|
1473
1587
|
wallDimensionReference={effectiveWallDimensionReference}
|
|
1474
|
-
visibilityRootId={
|
|
1588
|
+
visibilityRootId={
|
|
1589
|
+
entry.visibilityRootId ?? (entry.ctxOverrides ? undefined : (levelId as AnyNodeId))
|
|
1590
|
+
}
|
|
1475
1591
|
ctxOverrides={entry.ctxOverrides}
|
|
1476
1592
|
/>
|
|
1477
1593
|
))}
|
|
@@ -1943,12 +2059,12 @@ const FloorplanRegistryEntry = memo(function FloorplanRegistryEntry({
|
|
|
1943
2059
|
// the body-drag gesture — the whole selection slides, not one member.
|
|
1944
2060
|
if (onGroupMovePointerDown(nodeId, event)) return
|
|
1945
2061
|
sfxEmitter.emit('sfx:item-pick')
|
|
1946
|
-
|
|
2062
|
+
createEditorApi().engageMove(currentNode)
|
|
1947
2063
|
// Claim 2D ownership of this move at the source. `setMovingNode`
|
|
1948
2064
|
// resets the origin to null, so this must follow it.
|
|
1949
2065
|
setMovingNodeOrigin('2d')
|
|
1950
2066
|
},
|
|
1951
|
-
[nodeId, onGroupMovePointerDown,
|
|
2067
|
+
[nodeId, onGroupMovePointerDown, setMovingNodeOrigin],
|
|
1952
2068
|
)
|
|
1953
2069
|
|
|
1954
2070
|
const cacheEntry = buildFloorplanEntryGeometry({
|
|
@@ -2078,7 +2194,7 @@ function floorplanEntryReferencedAnnotationRole(
|
|
|
2078
2194
|
return dependencyIds.some((id) => selectedIds.has(id)) ? role : undefined
|
|
2079
2195
|
}
|
|
2080
2196
|
|
|
2081
|
-
function buildFloorplanEntryGeometry({
|
|
2197
|
+
export function buildFloorplanEntryGeometry({
|
|
2082
2198
|
automaticDimensions,
|
|
2083
2199
|
ctxOverrides,
|
|
2084
2200
|
geometryCache,
|
|
@@ -2123,6 +2239,7 @@ function buildFloorplanEntryGeometry({
|
|
|
2123
2239
|
const deps: NodeDeps = {
|
|
2124
2240
|
automaticDimensions,
|
|
2125
2241
|
node,
|
|
2242
|
+
ctxOverrides,
|
|
2126
2243
|
live,
|
|
2127
2244
|
unit,
|
|
2128
2245
|
metricNotation,
|
|
@@ -2136,7 +2253,7 @@ function buildFloorplanEntryGeometry({
|
|
|
2136
2253
|
siblingEpoch: dependsOnSiblingInputs ? siblingEpoch : 0,
|
|
2137
2254
|
// Sibling-dependent kinds (wall miters, opening cuts) read other nodes'
|
|
2138
2255
|
// committed state via `ctx`, so committed sibling edits still invalidate.
|
|
2139
|
-
committedNodes: dependsOnSiblingInputs ? nodes : null,
|
|
2256
|
+
committedNodes: dependsOnSiblingInputs || ctxOverrides?.trackAllNodes ? nodes : null,
|
|
2140
2257
|
dependencyNodes,
|
|
2141
2258
|
interactiveElevators,
|
|
2142
2259
|
}
|
|
@@ -2161,7 +2278,12 @@ function buildFloorplanEntryGeometry({
|
|
|
2161
2278
|
: r,
|
|
2162
2279
|
} as AnyNode
|
|
2163
2280
|
}
|
|
2164
|
-
if (
|
|
2281
|
+
if (
|
|
2282
|
+
(def.capabilities?.floorPlaced ||
|
|
2283
|
+
def.floorplanScope === 'building' ||
|
|
2284
|
+
def.floorplanScope === 'site') &&
|
|
2285
|
+
hasPosition
|
|
2286
|
+
) {
|
|
2165
2287
|
return applyPositionLiveTransform(sourceNode, live)
|
|
2166
2288
|
}
|
|
2167
2289
|
if (sourceNode.type === 'slab' || sourceNode.type === 'ceiling' || sourceNode.type === 'zone') {
|
|
@@ -2256,10 +2378,18 @@ function buildFloorplanEntryGeometry({
|
|
|
2256
2378
|
const taggedContextualGeometry = withFloorplanGeometryMetadata(contextualGeometry, {
|
|
2257
2379
|
annotationRole: 'contextual-dimension',
|
|
2258
2380
|
})
|
|
2259
|
-
const
|
|
2381
|
+
const unprojectedGeometry =
|
|
2260
2382
|
modelGeometry && taggedContextualGeometry
|
|
2261
2383
|
? { kind: 'group' as const, children: [modelGeometry, taggedContextualGeometry] }
|
|
2262
2384
|
: (modelGeometry ?? taggedContextualGeometry)
|
|
2385
|
+
const geometry =
|
|
2386
|
+
unprojectedGeometry && ctxOverrides?.outputTransform
|
|
2387
|
+
? {
|
|
2388
|
+
kind: 'group' as const,
|
|
2389
|
+
children: [unprojectedGeometry],
|
|
2390
|
+
transform: ctxOverrides.outputTransform,
|
|
2391
|
+
}
|
|
2392
|
+
: unprojectedGeometry
|
|
2263
2393
|
const { base, overlay } = geometry
|
|
2264
2394
|
? splitFloorplanOverlay(geometry)
|
|
2265
2395
|
: { base: null, overlay: null }
|
|
@@ -2822,6 +2952,7 @@ export const InteractiveGeometry = memo(function InteractiveGeometry({
|
|
|
2822
2952
|
case 'midpoint-handle': {
|
|
2823
2953
|
if (!palette) return <></>
|
|
2824
2954
|
const handleId = makeHandleId(nodeId, g.payload)
|
|
2955
|
+
const isAction = g.activation === 'action'
|
|
2825
2956
|
const isHovered = hoveredHandleId === handleId
|
|
2826
2957
|
const isActive = activeDragId === handleId
|
|
2827
2958
|
const stroke = palette.endpointHandleStroke
|
|
@@ -2834,7 +2965,12 @@ export const InteractiveGeometry = memo(function InteractiveGeometry({
|
|
|
2834
2965
|
return (
|
|
2835
2966
|
<g
|
|
2836
2967
|
key={keyHint}
|
|
2837
|
-
onClick={(
|
|
2968
|
+
onClick={(event) => {
|
|
2969
|
+
event.stopPropagation()
|
|
2970
|
+
if (!isAction) return
|
|
2971
|
+
event.preventDefault()
|
|
2972
|
+
onHandleDoubleClick(g.affordance, g.payload, event)
|
|
2973
|
+
}}
|
|
2838
2974
|
onPointerEnter={() => onHandleHoverChange(handleId)}
|
|
2839
2975
|
onPointerLeave={() => onHandleHoverChange(null)}
|
|
2840
2976
|
>
|
|
@@ -2891,9 +3027,17 @@ export const InteractiveGeometry = memo(function InteractiveGeometry({
|
|
|
2891
3027
|
cx={g.point[0]}
|
|
2892
3028
|
cy={g.point[1]}
|
|
2893
3029
|
fill="transparent"
|
|
2894
|
-
onPointerDown={(
|
|
2895
|
-
|
|
2896
|
-
|
|
3030
|
+
onPointerDown={(event) => {
|
|
3031
|
+
if (isAction) {
|
|
3032
|
+
event.stopPropagation()
|
|
3033
|
+
return
|
|
3034
|
+
}
|
|
3035
|
+
onHandlePointerDown(
|
|
3036
|
+
g.affordance,
|
|
3037
|
+
g.payload,
|
|
3038
|
+
event as ReactPointerEvent<SVGGElement>,
|
|
3039
|
+
)
|
|
3040
|
+
}}
|
|
2897
3041
|
pointerEvents="all"
|
|
2898
3042
|
r={radius + unitsPerPixel * 2}
|
|
2899
3043
|
stroke="transparent"
|
|
@@ -3142,12 +3286,15 @@ export function isFloorplanNodeVisible(node: AnyNode, liveOverride?: LiveNodeOve
|
|
|
3142
3286
|
return (node as { visible?: boolean }).visible !== false
|
|
3143
3287
|
}
|
|
3144
3288
|
|
|
3145
|
-
function isFloorplanHierarchyVisible(
|
|
3289
|
+
export function isFloorplanHierarchyVisible(
|
|
3146
3290
|
node: AnyNode,
|
|
3147
3291
|
nodes: Record<string, AnyNode>,
|
|
3148
3292
|
liveOverrides: Map<string, LiveNodeOverrides>,
|
|
3149
3293
|
rootId: AnyNodeId,
|
|
3150
3294
|
): boolean {
|
|
3295
|
+
const root = nodes[rootId]
|
|
3296
|
+
if (root && !isFloorplanNodeVisible(root, liveOverrides.get(root.id))) return false
|
|
3297
|
+
|
|
3151
3298
|
let current: AnyNode | undefined = node
|
|
3152
3299
|
const seen = new Set<AnyNodeId>()
|
|
3153
3300
|
while (current) {
|
|
@@ -3427,6 +3574,7 @@ function nodeDepsEqual(a: NodeDeps, b: NodeDeps): boolean {
|
|
|
3427
3574
|
'liveOverride',
|
|
3428
3575
|
'palette',
|
|
3429
3576
|
'siblingEpoch',
|
|
3577
|
+
'ctxOverrides',
|
|
3430
3578
|
'committedNodes',
|
|
3431
3579
|
'dependencyNodes',
|
|
3432
3580
|
'interactiveElevators',
|