@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
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
type FloorplanPoint,
|
|
13
13
|
type FloorplanScope,
|
|
14
14
|
type GeometryContext,
|
|
15
|
+
hidesDescendants,
|
|
15
16
|
isNodeKindEnabled,
|
|
16
17
|
isRegistryMovable,
|
|
17
18
|
kindsWithFloorplanScope,
|
|
@@ -40,6 +41,7 @@ import {
|
|
|
40
41
|
useRef,
|
|
41
42
|
useState,
|
|
42
43
|
} from 'react'
|
|
44
|
+
import { useShallow } from 'zustand/react/shallow'
|
|
43
45
|
import { markToolCancelConsumed } from '../../../hooks/use-keyboard'
|
|
44
46
|
import { ROTATE_HANDLE_DRAG_LABEL } from '../../../lib/contextual-help'
|
|
45
47
|
import {
|
|
@@ -87,9 +89,10 @@ import {
|
|
|
87
89
|
import { emitCanvasNodeSelection } from '../../../lib/selection-routing'
|
|
88
90
|
import { sfxEmitter } from '../../../lib/sfx-bus'
|
|
89
91
|
import { clearSurfacePlanSnapFeedback } from '../../../lib/surface-plan-snap'
|
|
92
|
+
import { paintZoneMembership } from '../../../lib/units'
|
|
90
93
|
import useDirectManipulationFeedback from '../../../store/use-direct-manipulation-feedback'
|
|
91
94
|
import useDrawingView from '../../../store/use-drawing-view'
|
|
92
|
-
import useEditor, { isAngleSnapActive } from '../../../store/use-editor'
|
|
95
|
+
import useEditor, { isAngleSnapActive, type Mode } from '../../../store/use-editor'
|
|
93
96
|
import useFloorplanAnnotationVisibility from '../../../store/use-floorplan-annotation-visibility'
|
|
94
97
|
import useFloorplanMode from '../../../store/use-floorplan-mode'
|
|
95
98
|
import useInteractionScope, {
|
|
@@ -343,6 +346,13 @@ type FloorplanEntryDescriptor = {
|
|
|
343
346
|
visibilityRootId?: AnyNodeId
|
|
344
347
|
}
|
|
345
348
|
|
|
349
|
+
/** The focused unit and its members; one stable object per focus change so
|
|
350
|
+
* cached zone geometry invalidates exactly when the focus does. */
|
|
351
|
+
export type FloorplanUnitFocus = {
|
|
352
|
+
focusedUnitId: string
|
|
353
|
+
focusedUnitMemberIds: readonly string[]
|
|
354
|
+
}
|
|
355
|
+
|
|
346
356
|
type NodeDeps = {
|
|
347
357
|
automaticDimensions: boolean
|
|
348
358
|
node: AnyNode
|
|
@@ -356,6 +366,7 @@ type NodeDeps = {
|
|
|
356
366
|
moving: boolean
|
|
357
367
|
liveOverride: LiveNodeOverrides | undefined
|
|
358
368
|
palette: FloorplanPalette | undefined
|
|
369
|
+
unitFocus: FloorplanUnitFocus | undefined
|
|
359
370
|
siblingEpoch: number
|
|
360
371
|
committedNodes: Record<string, AnyNode> | null
|
|
361
372
|
dependencyNodes: AnyNode[]
|
|
@@ -446,15 +457,32 @@ export function isFloorplanOpeningPlacementState({
|
|
|
446
457
|
)
|
|
447
458
|
}
|
|
448
459
|
|
|
449
|
-
|
|
460
|
+
/**
|
|
461
|
+
* Whether a press on an entry belongs to the active tool instead of selecting
|
|
462
|
+
* the entry. Build tools own the plan the way their 3D tools own the canvas,
|
|
463
|
+
* where selection only runs in select mode: a wall drawn onto another wall
|
|
464
|
+
* must place its point (T-junction), not select the wall under the cursor.
|
|
465
|
+
*/
|
|
466
|
+
export function floorplanEntryYieldsToTool(state: {
|
|
467
|
+
mode: Mode
|
|
468
|
+
openingPlacement: boolean
|
|
469
|
+
}): boolean {
|
|
470
|
+
return state.mode === 'build' || state.openingPlacement
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
function floorplanEntryYieldsToToolNow(): boolean {
|
|
450
474
|
const { phase, mode, tool } = useEditor.getState()
|
|
451
475
|
const movingNode = getMovingNode()
|
|
452
|
-
return
|
|
453
|
-
phase,
|
|
476
|
+
return floorplanEntryYieldsToTool({
|
|
454
477
|
mode,
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
478
|
+
openingPlacement: isFloorplanOpeningPlacementState({
|
|
479
|
+
phase,
|
|
480
|
+
mode,
|
|
481
|
+
tool,
|
|
482
|
+
movingNodeHasWallOpeningPlacement:
|
|
483
|
+
movingNode != null &&
|
|
484
|
+
!!nodeRegistry.get(movingNode.type)?.capabilities?.wallOpeningPlacement,
|
|
485
|
+
}),
|
|
458
486
|
})
|
|
459
487
|
}
|
|
460
488
|
|
|
@@ -476,6 +504,7 @@ function snapshotsToUpdates(snapshots: NodeSnapshot[]) {
|
|
|
476
504
|
// Stable empty sentinel used by per-entry builders while the floor plan is
|
|
477
505
|
// hidden; committed scene edits still flow through `useScene`.
|
|
478
506
|
const EMPTY_LIVE_OVERRIDES: Map<string, LiveNodeOverrides> = new Map()
|
|
507
|
+
const NO_MEMBER_IDS: readonly string[] = []
|
|
479
508
|
|
|
480
509
|
export const FloorplanRegistryLayer = memo(function FloorplanRegistryLayer() {
|
|
481
510
|
const selectedLevelId = useViewer((s) => s.selection.levelId)
|
|
@@ -484,6 +513,17 @@ export const FloorplanRegistryLayer = memo(function FloorplanRegistryLayer() {
|
|
|
484
513
|
const metricNotation = useViewer((s) => s.metricNotation)
|
|
485
514
|
const setSelection = useViewer((s) => s.setSelection)
|
|
486
515
|
const nodes = useScene((s) => s.nodes)
|
|
516
|
+
const focusedUnitId = useViewer((s) => s.focusedUnitId)
|
|
517
|
+
const focusedUnitMemberIds = useScene(
|
|
518
|
+
useShallow((s) => {
|
|
519
|
+
const focusedUnit = focusedUnitId ? s.nodes[focusedUnitId] : undefined
|
|
520
|
+
return focusedUnit?.type === 'unit' ? focusedUnit.members : NO_MEMBER_IDS
|
|
521
|
+
}),
|
|
522
|
+
)
|
|
523
|
+
const unitFocus = useMemo<FloorplanUnitFocus | undefined>(
|
|
524
|
+
() => (focusedUnitId ? { focusedUnitId, focusedUnitMemberIds } : undefined),
|
|
525
|
+
[focusedUnitId, focusedUnitMemberIds],
|
|
526
|
+
)
|
|
487
527
|
const installedPlugins = useScene((s) => s.installedPlugins)
|
|
488
528
|
const movingNode = useMovingNode()
|
|
489
529
|
// When a building is being moved, its explicit selection may be
|
|
@@ -536,13 +576,13 @@ export const FloorplanRegistryLayer = memo(function FloorplanRegistryLayer() {
|
|
|
536
576
|
const sceneRotationDeg = renderCtx?.getSceneRotationDeg() ?? 0
|
|
537
577
|
const setMovingNode = useEditor((s) => s.setMovingNode)
|
|
538
578
|
const setMovingNodeOrigin = useEditor((s) => s.setMovingNodeOrigin)
|
|
539
|
-
//
|
|
540
|
-
// background
|
|
541
|
-
//
|
|
542
|
-
//
|
|
579
|
+
// Build tools and door / window placement (both build and move) need the
|
|
580
|
+
// SVG's background handlers to run — wall drafting places its point there,
|
|
581
|
+
// and opening placement finds the closest wall via `findClosestWallPoint`
|
|
582
|
+
// and emits `wall:click`. When the user clicks *on top of* a wall, the
|
|
543
583
|
// wall's registry entry would otherwise swallow the click via
|
|
544
|
-
// `handleClickStop` / `handleSelect`, so the
|
|
545
|
-
//
|
|
584
|
+
// `handleClickStop` / `handleSelect`, so the tool never sees it. Pass
|
|
585
|
+
// clicks through in that case (`floorplanEntryYieldsToToolNow`).
|
|
546
586
|
const editorMode = useEditor((s) => s.mode)
|
|
547
587
|
const structureLayer = useEditor((s) => s.structureLayer)
|
|
548
588
|
const floorplanSelectionTool = useEditor((s) => s.floorplanSelectionTool)
|
|
@@ -650,12 +690,33 @@ export const FloorplanRegistryLayer = memo(function FloorplanRegistryLayer() {
|
|
|
650
690
|
|
|
651
691
|
const applyEntrySelection = useCallback(
|
|
652
692
|
(id: AnyNodeId, options: { shouldToggle: boolean; isolateMember: boolean }) => {
|
|
693
|
+
const focusedUnitId = useViewer.getState().focusedUnitId
|
|
694
|
+
const clickedNode = useScene.getState().nodes[id]
|
|
695
|
+
if (focusedUnitId && clickedNode?.type === 'zone') {
|
|
696
|
+
paintZoneMembership(focusedUnitId, clickedNode.id)
|
|
697
|
+
swallowNextClick(200)
|
|
698
|
+
return
|
|
699
|
+
}
|
|
653
700
|
const currentSelectedIds = useViewer.getState().selection.selectedIds
|
|
654
701
|
let nextSelectedIds: string[]
|
|
655
702
|
if (options.shouldToggle) {
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
703
|
+
if (currentSelectedIds.includes(id)) {
|
|
704
|
+
// A room's slab and ceiling overlap in the plan and only the slab
|
|
705
|
+
// takes the click, so removing one removes the other too (a marquee
|
|
706
|
+
// picks both). 3D keeps single toggles: each is clickable there.
|
|
707
|
+
const nodes = useScene.getState().nodes
|
|
708
|
+
const node = nodes[id as AnyNodeId]
|
|
709
|
+
const counterparts = node
|
|
710
|
+
? (getFloorplanNodeExtension(nodeRegistry.get(node.type))?.selectionCounterparts?.({
|
|
711
|
+
node,
|
|
712
|
+
nodes,
|
|
713
|
+
}) ?? [])
|
|
714
|
+
: []
|
|
715
|
+
const removed = new Set<string>([id, ...counterparts])
|
|
716
|
+
nextSelectedIds = currentSelectedIds.filter((selectedId) => !removed.has(selectedId))
|
|
717
|
+
} else {
|
|
718
|
+
nextSelectedIds = [...currentSelectedIds, id]
|
|
719
|
+
}
|
|
659
720
|
} else if (options.isolateMember) {
|
|
660
721
|
nextSelectedIds = [id]
|
|
661
722
|
} else {
|
|
@@ -693,7 +754,7 @@ export const FloorplanRegistryLayer = memo(function FloorplanRegistryLayer() {
|
|
|
693
754
|
)
|
|
694
755
|
|
|
695
756
|
const handleClickStop = useCallback((event: React.MouseEvent<SVGGElement>) => {
|
|
696
|
-
if (
|
|
757
|
+
if (floorplanEntryYieldsToToolNow()) return
|
|
697
758
|
event.stopPropagation()
|
|
698
759
|
}, [])
|
|
699
760
|
|
|
@@ -704,7 +765,10 @@ export const FloorplanRegistryLayer = memo(function FloorplanRegistryLayer() {
|
|
|
704
765
|
const node = useScene.getState().nodes[id]
|
|
705
766
|
if (!node || !isRegistryMovable(node.type)) return false
|
|
706
767
|
const currentSelectedIds = useViewer.getState().selection.selectedIds
|
|
707
|
-
const
|
|
768
|
+
const definition = nodeRegistry.get(node.type)
|
|
769
|
+
const allowPlainDrag =
|
|
770
|
+
getFloorplanNodeExtension(definition)?.directDrag === true ||
|
|
771
|
+
definition?.capabilities?.movable?.directDrag === true
|
|
708
772
|
const commandModifier = event.metaKey || event.ctrlKey
|
|
709
773
|
if (
|
|
710
774
|
!shouldStartDirectMoveDrag({
|
|
@@ -951,7 +1015,7 @@ export const FloorplanRegistryLayer = memo(function FloorplanRegistryLayer() {
|
|
|
951
1015
|
// the stores at event time. Commit clears the interaction scope before
|
|
952
1016
|
// React paints the next frame; a render-time `undefined` handler leaves
|
|
953
1017
|
// a short dead zone where the first post-placement selection is lost.
|
|
954
|
-
if (
|
|
1018
|
+
if (floorplanEntryYieldsToToolNow()) return
|
|
955
1019
|
if (startDirectMoveDrag(id, event)) return
|
|
956
1020
|
if (startDirectRotateDrag(id, event)) return
|
|
957
1021
|
if (startGroupMoveDrag(id, event)) return
|
|
@@ -1529,6 +1593,7 @@ export const FloorplanRegistryLayer = memo(function FloorplanRegistryLayer() {
|
|
|
1529
1593
|
onHandleDoubleClick={commitAffordanceAction}
|
|
1530
1594
|
onHandlePointerDown={startAffordanceDrag}
|
|
1531
1595
|
palette={palette}
|
|
1596
|
+
unitFocus={unitFocus}
|
|
1532
1597
|
pass="base"
|
|
1533
1598
|
sceneRotationDeg={sceneRotationDeg}
|
|
1534
1599
|
setMovingNode={setMovingNode}
|
|
@@ -1577,6 +1642,7 @@ export const FloorplanRegistryLayer = memo(function FloorplanRegistryLayer() {
|
|
|
1577
1642
|
onHandleDoubleClick={commitAffordanceAction}
|
|
1578
1643
|
onHandlePointerDown={startAffordanceDrag}
|
|
1579
1644
|
palette={palette}
|
|
1645
|
+
unitFocus={unitFocus}
|
|
1580
1646
|
pass="overlay"
|
|
1581
1647
|
sceneRotationDeg={sceneRotationDeg}
|
|
1582
1648
|
setMovingNode={setMovingNode}
|
|
@@ -1908,6 +1974,7 @@ type FloorplanRegistryEntryProps = {
|
|
|
1908
1974
|
rotationPivot?: FloorplanPoint,
|
|
1909
1975
|
) => void
|
|
1910
1976
|
palette: FloorplanPalette | undefined
|
|
1977
|
+
unitFocus: FloorplanUnitFocus | undefined
|
|
1911
1978
|
pass: FloorplanRenderPass
|
|
1912
1979
|
sceneRotationDeg: number
|
|
1913
1980
|
setMovingNode: ReturnType<typeof useEditor.getState>['setMovingNode']
|
|
@@ -1943,6 +2010,7 @@ const FloorplanRegistryEntry = memo(function FloorplanRegistryEntry({
|
|
|
1943
2010
|
onHandleDoubleClick,
|
|
1944
2011
|
onHandlePointerDown,
|
|
1945
2012
|
palette,
|
|
2013
|
+
unitFocus,
|
|
1946
2014
|
pass,
|
|
1947
2015
|
sceneRotationDeg,
|
|
1948
2016
|
setMovingNode,
|
|
@@ -1967,7 +2035,7 @@ const FloorplanRegistryEntry = memo(function FloorplanRegistryEntry({
|
|
|
1967
2035
|
const hovered = useViewer((state) => state.hoveredId === selectionProxyId)
|
|
1968
2036
|
const setHoveredId = useViewer((state) => state.setHoveredId)
|
|
1969
2037
|
const referencedAnnotationRole = useViewer((state) =>
|
|
1970
|
-
floorplanEntryReferencedAnnotationRole(node, new Set(state.selection.selectedIds)),
|
|
2038
|
+
floorplanEntryReferencedAnnotationRole(node, nodes, new Set(state.selection.selectedIds)),
|
|
1971
2039
|
)
|
|
1972
2040
|
const activeRotateNodeId = useDirectManipulationFeedback((state) =>
|
|
1973
2041
|
state.activeRotateNodeId === nodeId ? nodeId : null,
|
|
@@ -2084,6 +2152,7 @@ const FloorplanRegistryEntry = memo(function FloorplanRegistryEntry({
|
|
|
2084
2152
|
nodeId,
|
|
2085
2153
|
nodes,
|
|
2086
2154
|
palette,
|
|
2155
|
+
unitFocus,
|
|
2087
2156
|
selected,
|
|
2088
2157
|
siblingEpoch,
|
|
2089
2158
|
unit,
|
|
@@ -2153,6 +2222,7 @@ type BuildFloorplanEntryGeometryArgs = {
|
|
|
2153
2222
|
nodeId: AnyNodeId
|
|
2154
2223
|
nodes: Record<string, AnyNode>
|
|
2155
2224
|
palette: FloorplanPalette | undefined
|
|
2225
|
+
unitFocus?: FloorplanUnitFocus
|
|
2156
2226
|
selected: boolean
|
|
2157
2227
|
siblingEpoch: number
|
|
2158
2228
|
unit: 'metric' | 'imperial'
|
|
@@ -2167,7 +2237,7 @@ export function collectFloorplanDependencyNodes(
|
|
|
2167
2237
|
nodes: Record<string, AnyNode>,
|
|
2168
2238
|
liveOverrides?: Map<string, LiveNodeOverrides>,
|
|
2169
2239
|
): AnyNode[] {
|
|
2170
|
-
return (def.floorplanDependencies?.(node) ?? []).flatMap((id) => {
|
|
2240
|
+
return (def.floorplanDependencies?.(node, nodes) ?? []).flatMap((id) => {
|
|
2171
2241
|
const dependency = nodes[id]
|
|
2172
2242
|
if (!dependency) return []
|
|
2173
2243
|
const dependencyOverride = liveOverrides?.get(dependency.id)
|
|
@@ -2184,13 +2254,14 @@ export function collectFloorplanDependencyNodes(
|
|
|
2184
2254
|
|
|
2185
2255
|
function floorplanEntryReferencedAnnotationRole(
|
|
2186
2256
|
node: AnyNode,
|
|
2257
|
+
nodes: Record<string, AnyNode>,
|
|
2187
2258
|
selectedIds: ReadonlySet<string>,
|
|
2188
2259
|
): FloorplanAnnotationRole | undefined {
|
|
2189
2260
|
if (selectedIds.size === 0) return undefined
|
|
2190
2261
|
const definition = nodeRegistry.get(node.type)
|
|
2191
2262
|
const role = getFloorplanNodeExtension(definition)?.referencedSelectionAnnotationRole
|
|
2192
2263
|
if (!role) return undefined
|
|
2193
|
-
const dependencyIds = definition?.floorplanDependencies?.(node) ?? []
|
|
2264
|
+
const dependencyIds = definition?.floorplanDependencies?.(node, nodes) ?? []
|
|
2194
2265
|
return dependencyIds.some((id) => selectedIds.has(id)) ? role : undefined
|
|
2195
2266
|
}
|
|
2196
2267
|
|
|
@@ -2211,6 +2282,7 @@ export function buildFloorplanEntryGeometry({
|
|
|
2211
2282
|
nodeId,
|
|
2212
2283
|
nodes,
|
|
2213
2284
|
palette,
|
|
2285
|
+
unitFocus,
|
|
2214
2286
|
selected,
|
|
2215
2287
|
siblingEpoch,
|
|
2216
2288
|
unit,
|
|
@@ -2250,6 +2322,7 @@ export function buildFloorplanEntryGeometry({
|
|
|
2250
2322
|
moving,
|
|
2251
2323
|
liveOverride,
|
|
2252
2324
|
palette,
|
|
2325
|
+
unitFocus,
|
|
2253
2326
|
siblingEpoch: dependsOnSiblingInputs ? siblingEpoch : 0,
|
|
2254
2327
|
// Sibling-dependent kinds (wall miters, opening cuts) read other nodes'
|
|
2255
2328
|
// committed state via `ctx`, so committed sibling edits still invalidate.
|
|
@@ -2332,6 +2405,8 @@ export function buildFloorplanEntryGeometry({
|
|
|
2332
2405
|
highlighted,
|
|
2333
2406
|
hovered,
|
|
2334
2407
|
moving,
|
|
2408
|
+
focusedUnitId: unitFocus?.focusedUnitId,
|
|
2409
|
+
focusedUnitMemberIds: unitFocus?.focusedUnitMemberIds,
|
|
2335
2410
|
palette,
|
|
2336
2411
|
}
|
|
2337
2412
|
const resolveContextNode = <N = AnyNode>(rid: AnyNodeId): N | undefined => {
|
|
@@ -2360,6 +2435,8 @@ export function buildFloorplanEntryGeometry({
|
|
|
2360
2435
|
highlighted,
|
|
2361
2436
|
hovered,
|
|
2362
2437
|
moving,
|
|
2438
|
+
focusedUnitId: unitFocus?.focusedUnitId,
|
|
2439
|
+
focusedUnitMemberIds: unitFocus?.focusedUnitMemberIds,
|
|
2363
2440
|
palette,
|
|
2364
2441
|
}
|
|
2365
2442
|
: undefined,
|
|
@@ -3292,15 +3369,26 @@ export function isFloorplanHierarchyVisible(
|
|
|
3292
3369
|
liveOverrides: Map<string, LiveNodeOverrides>,
|
|
3293
3370
|
rootId: AnyNodeId,
|
|
3294
3371
|
): boolean {
|
|
3372
|
+
// The root is checked on its own because a site-scoped node can be declared
|
|
3373
|
+
// on the Site without a `parentId` link. A Site's flag never reaches the
|
|
3374
|
+
// nodes on it; see `hidesDescendants`.
|
|
3295
3375
|
const root = nodes[rootId]
|
|
3296
|
-
if (
|
|
3376
|
+
if (
|
|
3377
|
+
root &&
|
|
3378
|
+
root.id !== node.id &&
|
|
3379
|
+
hidesDescendants(root) &&
|
|
3380
|
+
!isFloorplanNodeVisible(root, liveOverrides.get(root.id))
|
|
3381
|
+
) {
|
|
3382
|
+
return false
|
|
3383
|
+
}
|
|
3297
3384
|
|
|
3298
3385
|
let current: AnyNode | undefined = node
|
|
3299
3386
|
const seen = new Set<AnyNodeId>()
|
|
3300
3387
|
while (current) {
|
|
3301
3388
|
if (seen.has(current.id)) return true
|
|
3302
3389
|
seen.add(current.id)
|
|
3303
|
-
|
|
3390
|
+
const reaches = current.id === node.id || hidesDescendants(current)
|
|
3391
|
+
if (reaches && !isFloorplanNodeVisible(current, liveOverrides.get(current.id))) return false
|
|
3304
3392
|
if (current.id === rootId) return true
|
|
3305
3393
|
const parentId = current.parentId as AnyNodeId | null
|
|
3306
3394
|
if (!parentId) return true
|
|
@@ -3573,6 +3661,7 @@ function nodeDepsEqual(a: NodeDeps, b: NodeDeps): boolean {
|
|
|
3573
3661
|
'moving',
|
|
3574
3662
|
'liveOverride',
|
|
3575
3663
|
'palette',
|
|
3664
|
+
'unitFocus',
|
|
3576
3665
|
'siblingEpoch',
|
|
3577
3666
|
'ctxOverrides',
|
|
3578
3667
|
'committedNodes',
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
import { type AnyNodeId, useLiveNodeOverrides, useScene } from '@pascal-app/core'
|
|
3
|
+
import {
|
|
4
|
+
ProceduralItemNode,
|
|
5
|
+
snapParameters,
|
|
6
|
+
validateProceduralRelations,
|
|
7
|
+
} from '@pascal-app/core/procedural-items'
|
|
8
|
+
import { useViewer } from '@pascal-app/viewer'
|
|
9
|
+
import { useEffect, useState } from 'react'
|
|
10
|
+
import { useHandleGroup } from '../../store/use-handle-group'
|
|
11
|
+
import { PanelSection } from '../ui/controls/panel-section'
|
|
12
|
+
import { SliderControl } from '../ui/controls/slider-control'
|
|
13
|
+
import { PanelWrapper } from '../ui/panels/panel-wrapper'
|
|
14
|
+
export function ProceduralInspector({
|
|
15
|
+
nodeId,
|
|
16
|
+
partId,
|
|
17
|
+
onPartChange,
|
|
18
|
+
}: {
|
|
19
|
+
nodeId: string
|
|
20
|
+
partId: string | null
|
|
21
|
+
onPartChange: (id: string | null) => void
|
|
22
|
+
}) {
|
|
23
|
+
const committed = useScene((s) => s.nodes[nodeId as AnyNodeId]) as unknown as
|
|
24
|
+
| ProceduralItemNode
|
|
25
|
+
| undefined
|
|
26
|
+
const override = useLiveNodeOverrides((s) => s.overrides.get(nodeId))
|
|
27
|
+
const node = committed ? ({ ...committed, ...override } as ProceduralItemNode) : null
|
|
28
|
+
useEffect(() => {
|
|
29
|
+
useHandleGroup.setState({ active: partId ? { nodeId, group: partId } : null })
|
|
30
|
+
return () => {
|
|
31
|
+
if (useHandleGroup.getState().active?.nodeId === nodeId)
|
|
32
|
+
useHandleGroup.setState({ active: null })
|
|
33
|
+
}
|
|
34
|
+
}, [nodeId, partId])
|
|
35
|
+
const [error, setError] = useState('')
|
|
36
|
+
useEffect(() => () => useLiveNodeOverrides.getState().clear(nodeId as AnyNodeId), [nodeId])
|
|
37
|
+
if (!node || !committed) return null
|
|
38
|
+
const change = (id: string, value: number, commit: boolean) => {
|
|
39
|
+
const values = { ...node.parameters, [id]: value }
|
|
40
|
+
const parameters = commit ? snapParameters(node.recipe, values) : values
|
|
41
|
+
const parsed = ProceduralItemNode.safeParse({ ...node, parameters })
|
|
42
|
+
if (!parsed.success) {
|
|
43
|
+
setError(parsed.error.issues[0]?.message ?? 'Invalid edit')
|
|
44
|
+
if (commit) useLiveNodeOverrides.getState().clear(nodeId as AnyNodeId)
|
|
45
|
+
return
|
|
46
|
+
}
|
|
47
|
+
try {
|
|
48
|
+
validateProceduralRelations({ ...node, parameters }, useScene.getState().nodes)
|
|
49
|
+
} catch (e) {
|
|
50
|
+
setError(e instanceof Error ? e.message : 'Invalid attachment')
|
|
51
|
+
if (commit) useLiveNodeOverrides.getState().clear(nodeId as AnyNodeId)
|
|
52
|
+
return
|
|
53
|
+
}
|
|
54
|
+
setError('')
|
|
55
|
+
if (commit) {
|
|
56
|
+
useLiveNodeOverrides.getState().clear(nodeId as AnyNodeId)
|
|
57
|
+
useScene.getState().updateNode(nodeId as AnyNodeId, { parameters } as never)
|
|
58
|
+
} else useLiveNodeOverrides.getState().set(nodeId as AnyNodeId, { parameters } as never)
|
|
59
|
+
}
|
|
60
|
+
const fields = node.recipe.parameters.filter((p) => (partId ? p.part === partId : !p.part))
|
|
61
|
+
return (
|
|
62
|
+
<div className="space-y-5">
|
|
63
|
+
<div className="flex items-center justify-between">
|
|
64
|
+
<h2 className="font-semibold text-lg">
|
|
65
|
+
{partId ? node.recipe.parts.find((p) => p.id === partId)?.label : 'Adjust design'}
|
|
66
|
+
</h2>
|
|
67
|
+
{partId && (
|
|
68
|
+
<button
|
|
69
|
+
className="rounded-full border px-3 py-1 text-xs"
|
|
70
|
+
onClick={() => onPartChange(null)}
|
|
71
|
+
type="button"
|
|
72
|
+
>
|
|
73
|
+
Back
|
|
74
|
+
</button>
|
|
75
|
+
)}
|
|
76
|
+
</div>
|
|
77
|
+
<div className="space-y-3">
|
|
78
|
+
{fields.map((p) => (
|
|
79
|
+
<SliderControl
|
|
80
|
+
key={p.id}
|
|
81
|
+
label={p.label}
|
|
82
|
+
value={node.parameters[p.id] ?? p.default}
|
|
83
|
+
min={p.min}
|
|
84
|
+
max={p.max}
|
|
85
|
+
step={p.step}
|
|
86
|
+
precision={3}
|
|
87
|
+
unit={p.unit === 'm' ? 'm' : ''}
|
|
88
|
+
onChange={(v) => change(p.id, v, false)}
|
|
89
|
+
onCommit={(v) => change(p.id, v, true)}
|
|
90
|
+
restoreOnCommit={false}
|
|
91
|
+
/>
|
|
92
|
+
))}
|
|
93
|
+
</div>
|
|
94
|
+
{partId && (
|
|
95
|
+
<button
|
|
96
|
+
type="button"
|
|
97
|
+
className="rounded-full border px-3 py-1 text-xs"
|
|
98
|
+
onClick={() => {
|
|
99
|
+
const parameters = { ...committed.parameters }
|
|
100
|
+
for (const p of fields) delete parameters[p.id]
|
|
101
|
+
try {
|
|
102
|
+
ProceduralItemNode.parse({ ...committed, parameters })
|
|
103
|
+
useScene.getState().updateNode(nodeId as AnyNodeId, { parameters } as never)
|
|
104
|
+
setError('')
|
|
105
|
+
} catch (e) {
|
|
106
|
+
setError(e instanceof Error ? e.message : 'Invalid reset')
|
|
107
|
+
}
|
|
108
|
+
}}
|
|
109
|
+
>
|
|
110
|
+
Reset this part
|
|
111
|
+
</button>
|
|
112
|
+
)}
|
|
113
|
+
{error && (
|
|
114
|
+
<p role="alert" className="text-red-600 text-sm">
|
|
115
|
+
{error}
|
|
116
|
+
</p>
|
|
117
|
+
)}
|
|
118
|
+
{!partId && node.recipe.parameters.some((p) => p.part) && (
|
|
119
|
+
<div>
|
|
120
|
+
<h3 className="mb-2 font-medium text-sm">Edit parts</h3>
|
|
121
|
+
<div className="flex flex-wrap gap-2">
|
|
122
|
+
{node.recipe.parts
|
|
123
|
+
.filter((part) => node.recipe.parameters.some((p) => p.part === part.id))
|
|
124
|
+
.map((p) => (
|
|
125
|
+
<button
|
|
126
|
+
key={p.id}
|
|
127
|
+
type="button"
|
|
128
|
+
className="rounded-full border px-3 py-1.5 text-sm text-foreground hover:bg-muted hover:text-foreground"
|
|
129
|
+
onClick={() => onPartChange(p.id)}
|
|
130
|
+
>
|
|
131
|
+
{p.label}
|
|
132
|
+
</button>
|
|
133
|
+
))}
|
|
134
|
+
</div>
|
|
135
|
+
</div>
|
|
136
|
+
)}
|
|
137
|
+
</div>
|
|
138
|
+
)
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export default function ProceduralItemPanel({ node }: { node: ProceduralItemNode }) {
|
|
142
|
+
return <ProceduralPanel key={node.id} node={node} />
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function ProceduralPanel({ node }: { node: ProceduralItemNode }) {
|
|
146
|
+
const [partId, setPartId] = useState<string | null>(null)
|
|
147
|
+
return (
|
|
148
|
+
<PanelWrapper
|
|
149
|
+
title={node.name ?? node.recipe.name}
|
|
150
|
+
onClose={() => useViewer.getState().setSelection({ selectedIds: [] })}
|
|
151
|
+
>
|
|
152
|
+
<div className="space-y-5 p-3">
|
|
153
|
+
<ProceduralInspector
|
|
154
|
+
key={node.id}
|
|
155
|
+
nodeId={node.id}
|
|
156
|
+
partId={partId}
|
|
157
|
+
onPartChange={setPartId}
|
|
158
|
+
/>
|
|
159
|
+
<ProceduralPlacementControls node={node} />
|
|
160
|
+
</div>
|
|
161
|
+
</PanelWrapper>
|
|
162
|
+
)
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function ProceduralPlacementControls({ node }: { node: ProceduralItemNode }) {
|
|
166
|
+
const [error, setError] = useState('')
|
|
167
|
+
const update = (patch: Partial<ProceduralItemNode>) => {
|
|
168
|
+
try {
|
|
169
|
+
useScene.getState().updateNode(node.id as AnyNodeId, patch as never)
|
|
170
|
+
setError('')
|
|
171
|
+
} catch (e) {
|
|
172
|
+
setError(e instanceof Error ? e.message : 'Invalid placement')
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
const changePosition = (index: number, value: number, commit: boolean) => {
|
|
176
|
+
const live = useLiveNodeOverrides.getState().overrides.get(node.id)
|
|
177
|
+
const position = [
|
|
178
|
+
...((live?.position as typeof node.position | undefined) ?? node.position),
|
|
179
|
+
] as typeof node.position
|
|
180
|
+
position[index] = Number(value.toFixed(3))
|
|
181
|
+
const patch = { position }
|
|
182
|
+
try {
|
|
183
|
+
validateProceduralRelations({ ...node, ...patch }, useScene.getState().nodes)
|
|
184
|
+
if (commit) {
|
|
185
|
+
useLiveNodeOverrides.getState().clear(node.id as AnyNodeId)
|
|
186
|
+
update(patch)
|
|
187
|
+
} else useLiveNodeOverrides.getState().set(node.id as AnyNodeId, patch)
|
|
188
|
+
setError('')
|
|
189
|
+
} catch (e) {
|
|
190
|
+
if (commit) useLiveNodeOverrides.getState().clear(node.id as AnyNodeId)
|
|
191
|
+
setError(e instanceof Error ? e.message : 'Invalid position')
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
return (
|
|
195
|
+
<PanelSection title="Position">
|
|
196
|
+
{(['X', 'Y', 'Z'] as const).map((axis, index) => (
|
|
197
|
+
<SliderControl
|
|
198
|
+
key={axis}
|
|
199
|
+
label={
|
|
200
|
+
<>
|
|
201
|
+
{axis}
|
|
202
|
+
<sub className="ml-[1px] text-[11px] opacity-70">pos</sub>
|
|
203
|
+
</>
|
|
204
|
+
}
|
|
205
|
+
value={node.position[index]!}
|
|
206
|
+
min={node.position[index]! - 2}
|
|
207
|
+
max={node.position[index]! + 2}
|
|
208
|
+
precision={2}
|
|
209
|
+
step={0.01}
|
|
210
|
+
unit="m"
|
|
211
|
+
onChange={(value) => changePosition(index, value, false)}
|
|
212
|
+
onCommit={(value) => changePosition(index, value, true)}
|
|
213
|
+
restoreOnCommit={false}
|
|
214
|
+
/>
|
|
215
|
+
))}
|
|
216
|
+
{error && (
|
|
217
|
+
<p role="alert" className="text-destructive text-xs">
|
|
218
|
+
{error}
|
|
219
|
+
</p>
|
|
220
|
+
)}
|
|
221
|
+
</PanelSection>
|
|
222
|
+
)
|
|
223
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { type AnyNodeId, sceneRegistry, useScene } from '@pascal-app/core'
|
|
2
|
+
import { isProceduralItem } from '@pascal-app/core/procedural-items'
|
|
2
3
|
import { useViewer } from '@pascal-app/viewer'
|
|
3
4
|
import { useEffect } from 'react'
|
|
4
5
|
import { Color, type Material, type Mesh } from 'three'
|
|
@@ -98,7 +99,8 @@ export const CeilingSystem = () => {
|
|
|
98
99
|
const isCeilingToolActive =
|
|
99
100
|
tool === 'ceiling' ||
|
|
100
101
|
selectedItem?.attachTo === 'ceiling' ||
|
|
101
|
-
(movingNode?.type === 'item' && movingNode?.asset?.attachTo === 'ceiling')
|
|
102
|
+
(movingNode?.type === 'item' && movingNode?.asset?.attachTo === 'ceiling') ||
|
|
103
|
+
(isProceduralItem(movingNode) && movingNode.recipe.mounting?.attachTo === 'ceiling')
|
|
102
104
|
|
|
103
105
|
if (isCeilingToolActive && activeLevelId) {
|
|
104
106
|
levelsToShowCeilings.add(activeLevelId)
|
|
@@ -58,9 +58,13 @@ export const ZoneSystem = () => {
|
|
|
58
58
|
const isDeleteHovered = editorMode === 'delete' && hoveredId === zoneId
|
|
59
59
|
|
|
60
60
|
// Keep group visible (so <Html> labels stay active), hide/show meshes only.
|
|
61
|
-
// Show meshes when: in zone mode, selected, or delete-hovered.
|
|
62
|
-
|
|
63
|
-
|
|
61
|
+
// Show meshes when: in zone mode, selected, or delete-hovered. A zone the
|
|
62
|
+
// author hid (sidebar eye) is the one case where the group itself goes:
|
|
63
|
+
// otherwise this per-frame write would undo the renderer's `visible` prop.
|
|
64
|
+
const nodeVisible = zone?.visible !== false
|
|
65
|
+
if (obj.visible !== nodeVisible) obj.visible = nodeVisible
|
|
66
|
+
const meshVisible =
|
|
67
|
+
nodeVisible && !isCaptureMode && (zoneGeometryVisible || isSelected || isDeleteHovered)
|
|
64
68
|
const targetOpacity = isCaptureMode
|
|
65
69
|
? 0
|
|
66
70
|
: isSelected || isDeleteHovered
|
|
@@ -103,7 +107,7 @@ export const ZoneSystem = () => {
|
|
|
103
107
|
// Labels: visible on the current level (regardless of mode), but never
|
|
104
108
|
// during snapshot capture.
|
|
105
109
|
const showLabel =
|
|
106
|
-
!isCaptureMode && !zoneLabelsHidden && !!selectedLevelId && isOnSelectedLevel
|
|
110
|
+
nodeVisible && !isCaptureMode && !zoneLabelsHidden && !!selectedLevelId && isOnSelectedLevel
|
|
107
111
|
const labelOpacity = showLabel ? '1' : '0'
|
|
108
112
|
const labelEl = document.getElementById(`${zoneId}-label`)
|
|
109
113
|
if (labelEl && labelEl.style.opacity !== labelOpacity) {
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { type AnyNodeDefinition, registerNode } from '@pascal-app/core'
|
|
2
|
+
|
|
3
|
+
export function registerHostingTestNode(definition: AnyNodeDefinition) {
|
|
4
|
+
registerNode({
|
|
5
|
+
...definition,
|
|
6
|
+
geometry: () => {
|
|
7
|
+
throw new Error('Unit-test host geometry must not be rendered')
|
|
8
|
+
},
|
|
9
|
+
})
|
|
10
|
+
}
|