@pascal-app/editor 1.0.7 → 1.1.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/package.json +7 -7
- package/src/components/editor/custom-camera-controls.tsx +90 -34
- package/src/components/editor/duplicate-node.ts +2 -1
- package/src/components/editor/first-person-controls.tsx +5 -7
- package/src/components/editor/fit-scene-framing.test.ts +318 -0
- package/src/components/editor/fit-scene-framing.ts +142 -0
- package/src/components/editor/floating-action-menu.tsx +4 -2
- package/src/components/editor/floor-openings-overlay.tsx +1 -1
- package/src/components/editor/floor-region-controls.tsx +2 -2
- package/src/components/editor/floorplan-panel.tsx +173 -8
- package/src/components/editor/floorplan-wall-typing-keys.test.tsx +152 -0
- package/src/components/editor/floorplan-wall-typing-keys.ts +79 -0
- package/src/components/editor/footprint-height-handle.tsx +1 -1
- package/src/components/editor/group-transform-shared.ts +3 -1
- package/src/components/editor/index.tsx +20 -0
- package/src/components/editor/measurement-pill.tsx +1 -1
- package/src/components/editor/node-arrow-handles.tsx +1 -1
- package/src/components/editor/open-wall-ends-3d-layer.tsx +1 -2
- package/src/components/editor/open-wall-ends-hint.tsx +1 -1
- package/src/components/editor/quick-measurement-card.tsx +1 -1
- package/src/components/editor/room-controls.tsx +3 -2
- package/src/components/editor/room-handles-2d.tsx +2 -1
- package/src/components/editor/room-handles.tsx +3 -3
- package/src/components/editor/room-labels.tsx +4 -2
- package/src/components/editor/site-edge-labels.tsx +6 -2
- package/src/components/editor/wall-measurement-label.tsx +30 -14
- package/src/components/editor/wall-move-side-handles.tsx +10 -6
- package/src/components/editor/wall-region-handles.tsx +2 -2
- package/src/components/editor/wall-snap-beacon-layer.tsx +53 -32
- package/src/components/editor-2d/floorplan-group-move.tsx +1 -1
- package/src/components/editor-2d/floorplan-measurement-tool-layer.tsx +12 -12
- package/src/components/editor-2d/floorplan-open-wall-ends-layer.tsx +1 -2
- package/src/components/editor-2d/floorplan-registry-action-menu.tsx +2 -1
- package/src/components/editor-2d/floorplan-registry-move-overlay.tsx +22 -4
- package/src/components/editor-2d/renderers/floorplan-draft-measurement.tsx +7 -1
- package/src/components/editor-2d/renderers/floorplan-placement-preview-layer.tsx +13 -1
- package/src/components/editor-2d/renderers/floorplan-placement-typing-hud.test.tsx +35 -0
- package/src/components/editor-2d/renderers/floorplan-placement-typing-hud.tsx +65 -0
- package/src/components/editor-2d/renderers/floorplan-registry-layer.test.ts +22 -0
- package/src/components/editor-2d/renderers/floorplan-registry-layer.tsx +14 -2
- package/src/components/systems/roof/roof-edit-system.test.tsx +15 -1
- package/src/components/systems/roof/roof-edit-system.tsx +30 -19
- package/src/components/tools/distribution/distribution-run-tool.test.tsx +371 -3
- package/src/components/tools/distribution/distribution-run-tool.tsx +150 -32
- package/src/components/tools/item/move-tool.tsx +1 -6
- package/src/components/tools/item/use-placement-coordinator.tsx +1 -1
- package/src/components/tools/paint-region/wall-paint-region-tool.tsx +1 -1
- package/src/components/tools/registry/move-registry-node-tool.test.ts +42 -1
- package/src/components/tools/registry/move-registry-node-tool.tsx +38 -14
- package/src/components/tools/shared/placement-box.tsx +1 -5
- package/src/components/tools/shared/placement-coordinate-input.tsx +94 -0
- package/src/components/tools/shared/segment-angle.ts +0 -4
- package/src/components/tools/tool-manager.tsx +1 -24
- package/src/components/tools/wall/wall-typed-entry.test.ts +99 -0
- package/src/components/tools/wall/wall-typed-entry.ts +70 -0
- package/src/components/ui/controls/slider-control.tsx +18 -6
- package/src/components/ui/controls/tool-options-panel.tsx +74 -44
- package/src/components/ui/helpers/helper-manager.test.tsx +53 -0
- package/src/components/ui/helpers/helper-manager.tsx +54 -0
- package/src/components/ui/panels/floor-foundation-panel.tsx +1 -1
- package/src/components/ui/panels/floor-opening-panel.tsx +1 -1
- package/src/components/ui/panels/mobile-selection-bar.tsx +28 -24
- package/src/components/ui/panels/multi-height-mode.tsx +3 -3
- package/src/components/ui/panels/multi-selection-panel.tsx +1 -1
- package/src/components/ui/panels/panel-manager.tsx +4 -2
- package/src/components/ui/panels/panel-wrapper.tsx +41 -18
- package/src/components/ui/panels/room-construction-rows.tsx +1 -1
- package/src/components/ui/panels/room-openings-field.tsx +1 -1
- package/src/components/ui/panels/room-panel.tsx +1 -1
- package/src/components/ui/right-stack.test.ts +13 -6
- package/src/components/ui/right-stack.tsx +21 -10
- package/src/components/ui/sidebar/panels/settings-panel/load-build-dialog.tsx +1 -5
- package/src/components/ui/sidebar/panels/site-panel/ceiling-tree-node.tsx +1 -1
- package/src/components/ui/sidebar/panels/site-panel/index.tsx +1 -8
- package/src/components/ui/sidebar/panels/site-panel/slab-tree-node.tsx +1 -1
- package/src/components/ui/sidebar/panels/site-panel/zone-tree-node.tsx +1 -1
- package/src/components/ui/wall-type/wall-type-editor-dialog.tsx +1 -1
- package/src/components/viewer/viewer-rooms.tsx +1 -1
- package/src/i18n/locales/en/messages.po +856 -124
- package/src/i18n/locales/en/messages.ts +1 -1
- package/src/i18n/locales/en-XA/messages.ts +1 -1
- package/src/i18n/locales/es/messages.po +2087 -257
- package/src/i18n/locales/fr/messages.po +2087 -257
- package/src/i18n/locales/ja/messages.po +2087 -257
- package/src/i18n/locales/zh-CN/messages.po +2087 -257
- package/src/index.tsx +74 -47
- package/src/lib/contextual-help.ts +6 -0
- package/src/lib/elevation-guides.ts +21 -6
- package/src/lib/elevator-support.ts +1 -72
- package/src/lib/floor-footprints.test.ts +1 -1
- package/src/lib/floor-footprints.ts +1 -9
- package/src/lib/floor-foundation-support.test.ts +8 -3
- package/src/lib/floor-opening-draft.ts +1 -1
- package/src/lib/floor-region-session.ts +1 -1
- package/src/lib/floorplan/open-wall-ends.ts +2 -1
- package/src/lib/gesture-lifecycle.ts +3 -1
- package/src/lib/interaction/scope.ts +1 -0
- package/src/lib/layer-thickness.ts +1 -1
- package/src/lib/level-selection.test.ts +18 -2
- package/src/lib/level-selection.ts +31 -8
- package/src/lib/linear-display.ts +1 -1
- package/src/lib/live-gesture-round-trip.test.ts +1 -1
- package/src/lib/measurement-label.test.ts +1 -1
- package/src/lib/measurements.test.ts +8 -7
- package/src/lib/measurements.ts +1 -218
- package/src/lib/node-action-movement.test.tsx +115 -0
- package/src/lib/node-action-movement.ts +2 -0
- package/src/lib/placement-metadata.ts +4 -0
- package/src/lib/point-ask/describe-target.ts +6 -8
- package/src/lib/point-ask/region.ts +1 -1
- package/src/lib/print-shell-compiler.ts +12 -1
- package/src/lib/print-wall-solids.test.ts +11 -0
- package/src/lib/print-wall-solids.ts +9 -0
- package/src/lib/room-assembly-overlay.ts +1 -1
- package/src/lib/room-built-on.test.ts +2 -5
- package/src/lib/room-built-on.ts +1 -15
- package/src/lib/room-construction-commands.ts +3 -2
- package/src/lib/room-controls-phase6.test.ts +1 -1
- package/src/lib/room-handle-drag.ts +6 -25
- package/src/lib/room-push-dimensions.test.ts +1 -1
- package/src/lib/scene-bounds.test.ts +144 -0
- package/src/lib/scene-bounds.ts +79 -15
- package/src/lib/scene-clipboard.ts +2 -1
- package/src/lib/snapping-mode.test.ts +23 -1
- package/src/lib/viewer-selection.ts +1 -1
- package/src/lib/wall-region-list.ts +3 -1
- package/src/lib/window-interaction.test.ts +4 -2
- package/src/store/use-editor.tsx +5 -0
- package/src/store/use-placement-typing.test.ts +165 -0
- package/src/store/use-placement-typing.ts +186 -0
- package/src/components/tools/elevator/elevator-defaults.ts +0 -8
- package/src/components/tools/elevator/elevator-tool.tsx +0 -372
- package/src/components/tools/elevator/move-elevator-tool.tsx +0 -262
- package/src/lib/door-interaction.ts +0 -102
- package/src/lib/measurement-label.ts +0 -68
- package/src/lib/window-interaction.ts +0 -142
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
import {
|
|
4
4
|
type AnyNodeId,
|
|
5
5
|
emitter,
|
|
6
|
+
formatFixed,
|
|
6
7
|
type GridEvent,
|
|
7
8
|
sceneRegistry,
|
|
8
9
|
usePascalI18n,
|
|
@@ -18,6 +19,7 @@ import {
|
|
|
18
19
|
isGridSnapActive,
|
|
19
20
|
isMagneticSnapActive,
|
|
20
21
|
isTypingTarget,
|
|
22
|
+
isViewVisible,
|
|
21
23
|
markToolCancelConsumed,
|
|
22
24
|
publishPlacementSurface,
|
|
23
25
|
triggerSFX,
|
|
@@ -226,6 +228,11 @@ export type DistributionRunToolConfig = {
|
|
|
226
228
|
active: boolean
|
|
227
229
|
levelId: AnyNodeId | null
|
|
228
230
|
toolName: string
|
|
231
|
+
routingMode?: 'surface' | 'horizontal' | 'vertical'
|
|
232
|
+
/** Level-local centreline elevation, or the target height for a vertical leg. */
|
|
233
|
+
elevation?: number
|
|
234
|
+
targetElevation?: number
|
|
235
|
+
connectCrossings?: boolean
|
|
229
236
|
initialStart?: RunPoint | null
|
|
230
237
|
initialConnection?: RunConnection | null
|
|
231
238
|
getPorts: () => ScenePort[]
|
|
@@ -524,6 +531,7 @@ function surfacePointFromEvent(
|
|
|
524
531
|
|
|
525
532
|
export function useDistributionRunTool(config: DistributionRunToolConfig) {
|
|
526
533
|
const { camera, gl } = useThree()
|
|
534
|
+
const planVisible = useEditor((state) => isViewVisible(state, '2d'))
|
|
527
535
|
const initialStartRef = useRef<RunPoint | null>(
|
|
528
536
|
config.initialStart ? [...config.initialStart] : null,
|
|
529
537
|
)
|
|
@@ -534,6 +542,7 @@ export function useDistributionRunTool(config: DistributionRunToolConfig) {
|
|
|
534
542
|
const [cursor, setCursor] = useState<RunPoint | null>(initialStartRef.current)
|
|
535
543
|
const [snapTarget, setSnapTarget] = useState<RunPoint | null>(null)
|
|
536
544
|
const [snapScreen, setSnapScreen] = useState<PortScreenPoint | null>(null)
|
|
545
|
+
const [cursorScreen, setCursorScreen] = useState<PortScreenPoint | null>(null)
|
|
537
546
|
const [endConnection, setEndConnection] = useState<RunConnection>({
|
|
538
547
|
port: null,
|
|
539
548
|
body: null,
|
|
@@ -592,6 +601,7 @@ export function useDistributionRunTool(config: DistributionRunToolConfig) {
|
|
|
592
601
|
const typed = Number.parseFloat(lengthInputRef.current)
|
|
593
602
|
if (
|
|
594
603
|
!currentStart ||
|
|
604
|
+
configRef.current.routingMode === 'vertical' ||
|
|
595
605
|
!Number.isFinite(typed) ||
|
|
596
606
|
typed <= 0 ||
|
|
597
607
|
resolved.port ||
|
|
@@ -617,28 +627,47 @@ export function useDistributionRunTool(config: DistributionRunToolConfig) {
|
|
|
617
627
|
}
|
|
618
628
|
}
|
|
619
629
|
|
|
630
|
+
// A vertical route rises from its start to the target elevation wherever the
|
|
631
|
+
// pointer is, so it resolves without a pointer event.
|
|
632
|
+
const verticalTarget = (currentStart: RunPoint): ResolvedRunPoint => ({
|
|
633
|
+
point: [
|
|
634
|
+
currentStart[0],
|
|
635
|
+
configRef.current.targetElevation ?? currentStart[1],
|
|
636
|
+
currentStart[2],
|
|
637
|
+
],
|
|
638
|
+
frame: createRunSurfaceFrame(currentStart),
|
|
639
|
+
surfaceTarget: null,
|
|
640
|
+
snapped: null,
|
|
641
|
+
directionMode: 'vertical',
|
|
642
|
+
port: null,
|
|
643
|
+
body: null,
|
|
644
|
+
})
|
|
645
|
+
|
|
620
646
|
const resolvePoint = (event: GridEvent): ResolvedRunPoint => {
|
|
621
647
|
const adapter = configRef.current
|
|
622
648
|
const hit = surfacePointFromEvent(event, adapter.levelId)
|
|
623
649
|
const currentStart = startRef.current
|
|
624
650
|
const previous = lastResolvedRef.current
|
|
625
|
-
const
|
|
651
|
+
const explicitRoute = adapter.routingMode && adapter.routingMode !== 'surface'
|
|
652
|
+
const lockedWallTarget = currentStart && !explicitRoute ? lockedWallTargetRef.current : null
|
|
626
653
|
const lockedWallHit =
|
|
627
654
|
lockedWallTarget && isSameRunWallSurface(lockedWallTarget, event.surfaceHit)
|
|
628
|
-
const working =
|
|
629
|
-
?
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
:
|
|
641
|
-
|
|
655
|
+
const working = explicitRoute
|
|
656
|
+
? createRunSurfaceFrame([0, currentStart?.[1] ?? adapter.elevation ?? 0, 0])
|
|
657
|
+
: lockedWallTarget
|
|
658
|
+
? {
|
|
659
|
+
...lockedWallTarget.frame,
|
|
660
|
+
origin: lockedWallTarget.frame.origin.map(
|
|
661
|
+
(value, index) =>
|
|
662
|
+
value +
|
|
663
|
+
lockedWallTarget.frame.normal[index]! *
|
|
664
|
+
(adapter.surfaceClearance?.(lockedWallTarget) ?? 0),
|
|
665
|
+
) as RunPoint,
|
|
666
|
+
}
|
|
667
|
+
: !event.surfaceHit && previous?.surfaceTarget?.kind === 'wall' && currentStart
|
|
668
|
+
? createRunSurfaceFrame(currentStart, UP)
|
|
669
|
+
: (previous?.frame ?? (currentStart ? createRunSurfaceFrame(currentStart) : null))
|
|
670
|
+
const hasSurface = !explicitRoute && (!!event.surfaceHit || !working || !event.localRay)
|
|
642
671
|
const target = lockedWallTarget ?? (hasSurface ? hit.target : null)
|
|
643
672
|
const hitForCursor = lockedWallTarget
|
|
644
673
|
? lockedWallHit
|
|
@@ -651,16 +680,25 @@ export function useDistributionRunTool(config: DistributionRunToolConfig) {
|
|
|
651
680
|
hit: hitForCursor,
|
|
652
681
|
working,
|
|
653
682
|
ray: event.localRay,
|
|
654
|
-
fallback: previous?.point ?? currentStart ?? hit.point,
|
|
683
|
+
fallback: explicitRoute ? [...hit.point] : (previous?.point ?? currentStart ?? hit.point),
|
|
655
684
|
clearance: adapter.surfaceClearance?.(target) ?? 0,
|
|
656
685
|
})
|
|
686
|
+
const routeElevation = currentStart?.[1] ?? adapter.elevation ?? 0
|
|
687
|
+
if (explicitRoute) {
|
|
688
|
+
resolved.point[1] = routeElevation
|
|
689
|
+
if (currentStart && adapter.routingMode === 'vertical') return verticalTarget(currentStart)
|
|
690
|
+
}
|
|
657
691
|
const bypass = event.nativeEvent?.altKey === true
|
|
658
692
|
const gridStep = !bypass && isGridSnapActive() ? useEditor.getState().gridSnapStep : 0
|
|
659
693
|
const angleLocked = !bypass && (isAngleSnapActive() || isGridSnapActive())
|
|
660
694
|
let point = currentStart
|
|
661
695
|
? resolved.point
|
|
662
696
|
: snapRunPointToSurface(resolved.point, resolved.frame, gridStep)
|
|
663
|
-
const
|
|
697
|
+
const selectedDirection = forcedDirectionRef.current
|
|
698
|
+
const forcedDirection =
|
|
699
|
+
explicitRoute && selectedDirection && Math.abs(selectedDirection[1]) > 1e-6
|
|
700
|
+
? null
|
|
701
|
+
: selectedDirection
|
|
664
702
|
if (currentStart && forcedDirection) {
|
|
665
703
|
point = projectRunToDirection(currentStart, point, forcedDirection)
|
|
666
704
|
}
|
|
@@ -673,7 +711,11 @@ export function useDistributionRunTool(config: DistributionRunToolConfig) {
|
|
|
673
711
|
if (currentStart && gridStep > 0) {
|
|
674
712
|
point = snapRunLength(currentStart, point, gridStep)
|
|
675
713
|
}
|
|
676
|
-
const
|
|
714
|
+
const drawingTarget: RunSurfaceTarget | null =
|
|
715
|
+
explicitRoute && adapter.levelId
|
|
716
|
+
? { kind: 'surface', levelId: adapter.levelId, frame: resolved.frame }
|
|
717
|
+
: target
|
|
718
|
+
const sample = { frame: resolved.frame, surfaceTarget: drawingTarget }
|
|
677
719
|
const native = (event.nativeEvent ?? {}) as {
|
|
678
720
|
clientX?: number
|
|
679
721
|
clientY?: number
|
|
@@ -713,11 +755,22 @@ export function useDistributionRunTool(config: DistributionRunToolConfig) {
|
|
|
713
755
|
const screen = projectConnection(candidate)
|
|
714
756
|
return !!screen && Math.hypot(screen.x - pointer[0], screen.y - pointer[1]) <= 12
|
|
715
757
|
}
|
|
758
|
+
// An explicit route may only connect at the height its free end reaches above that
|
|
759
|
+
// plan point, which includes the adapter's fall.
|
|
760
|
+
const routeHeightAt = (candidate: readonly number[]): number => {
|
|
761
|
+
const level: RunPoint = [candidate[0]!, routeElevation, candidate[2]!]
|
|
762
|
+
return currentStart
|
|
763
|
+
? (adapter.resolveFreeEnd?.(currentStart, level, startConnectionRef.current) ?? level)[1]
|
|
764
|
+
: routeElevation
|
|
765
|
+
}
|
|
766
|
+
const onRoute = (candidate: readonly number[]) =>
|
|
767
|
+
!explicitRoute || Math.abs(candidate[1]! - routeHeightAt(candidate)) < 1e-4
|
|
716
768
|
const resolveConnection = (candidate: RunPoint): ResolvedRunPoint | null => {
|
|
717
769
|
if (bypass || !(isGridSnapActive() || isMagneticSnapActive() || isAngleSnapActive()))
|
|
718
770
|
return null
|
|
719
771
|
const source = startConnectionRef.current.port
|
|
720
|
-
const
|
|
772
|
+
const ports = adapter.getPorts().filter((port) => onRoute(port.position))
|
|
773
|
+
const hit = findScreenPort(ports, pointer, projectConnection, source)
|
|
721
774
|
if (hit) {
|
|
722
775
|
return {
|
|
723
776
|
frame: createRunSurfaceFrame([...hit.port.position]),
|
|
@@ -730,8 +783,19 @@ export function useDistributionRunTool(config: DistributionRunToolConfig) {
|
|
|
730
783
|
body: null,
|
|
731
784
|
}
|
|
732
785
|
}
|
|
733
|
-
const body =
|
|
734
|
-
|
|
786
|
+
const body =
|
|
787
|
+
adapter.connectCrossings === false
|
|
788
|
+
? null
|
|
789
|
+
: adapter.findBody(
|
|
790
|
+
explicitRoute ? [candidate[0], routeHeightAt(candidate), candidate[2]] : candidate,
|
|
791
|
+
drawingTarget,
|
|
792
|
+
)
|
|
793
|
+
if (
|
|
794
|
+
body &&
|
|
795
|
+
onRoute(body.point) &&
|
|
796
|
+
acceptsConnection(body.point) &&
|
|
797
|
+
body.nodeId !== source?.nodeId
|
|
798
|
+
) {
|
|
735
799
|
return {
|
|
736
800
|
...sample,
|
|
737
801
|
point: body.point,
|
|
@@ -747,7 +811,7 @@ export function useDistributionRunTool(config: DistributionRunToolConfig) {
|
|
|
747
811
|
if (connection) return connection
|
|
748
812
|
// Rank displayed directions before a surface projection discards height.
|
|
749
813
|
if (currentStart && event.localRay && !bypass && (forcedDirection || angleLocked)) {
|
|
750
|
-
const source = startConnectionRef.current.port?.direction ?? null
|
|
814
|
+
const source = explicitRoute ? null : (startConnectionRef.current.port?.direction ?? null)
|
|
751
815
|
const candidates = forcedDirection
|
|
752
816
|
? [forcedDirection]
|
|
753
817
|
: source
|
|
@@ -803,7 +867,7 @@ export function useDistributionRunTool(config: DistributionRunToolConfig) {
|
|
|
803
867
|
forcedDirection ||
|
|
804
868
|
(Math.abs(directionHit.direction[1]) > 1e-6 && target?.kind === 'floor')
|
|
805
869
|
? null
|
|
806
|
-
:
|
|
870
|
+
: drawingTarget,
|
|
807
871
|
snapped: null,
|
|
808
872
|
directionMode: 'angle',
|
|
809
873
|
port: null,
|
|
@@ -951,7 +1015,12 @@ export function useDistributionRunTool(config: DistributionRunToolConfig) {
|
|
|
951
1015
|
setAltActive(false)
|
|
952
1016
|
}
|
|
953
1017
|
|
|
1018
|
+
const updateCursorScreen = (event: GridEvent) => {
|
|
1019
|
+
const pointer = getGridEventScreenProjection(event)?.pointer
|
|
1020
|
+
setCursorScreen(pointer ? { x: pointer[0], y: pointer[1], depth: 0 } : null)
|
|
1021
|
+
}
|
|
954
1022
|
const onMove = (event: RunPointerEvent) => {
|
|
1023
|
+
updateCursorScreen(event)
|
|
955
1024
|
lastPointerRef.current = event
|
|
956
1025
|
const clientY = (event.nativeEvent as { clientY?: number } | undefined)?.clientY
|
|
957
1026
|
if (typeof clientY === 'number') lastClientYRef.current = clientY
|
|
@@ -973,6 +1042,7 @@ export function useDistributionRunTool(config: DistributionRunToolConfig) {
|
|
|
973
1042
|
}
|
|
974
1043
|
|
|
975
1044
|
const onClick = (event: GridEvent) => {
|
|
1045
|
+
updateCursorScreen(event)
|
|
976
1046
|
event.nativeEvent?.stopPropagation?.()
|
|
977
1047
|
const currentStart = startRef.current
|
|
978
1048
|
if (altAnchorRef.current && currentStart) {
|
|
@@ -993,8 +1063,10 @@ export function useDistributionRunTool(config: DistributionRunToolConfig) {
|
|
|
993
1063
|
port: resolved.port,
|
|
994
1064
|
body: resolved.port ? null : resolved.body,
|
|
995
1065
|
}
|
|
996
|
-
if (
|
|
997
|
-
|
|
1066
|
+
if (!configRef.current.routingMode || configRef.current.routingMode === 'surface') {
|
|
1067
|
+
if (shouldLockRunWallSurface(resolved.surfaceTarget, connection)) {
|
|
1068
|
+
lockedWallTargetRef.current = resolved.surfaceTarget
|
|
1069
|
+
}
|
|
998
1070
|
}
|
|
999
1071
|
triggerSFX('sfx:grid-snap')
|
|
1000
1072
|
startConnectionRef.current = connection
|
|
@@ -1005,6 +1077,10 @@ export function useDistributionRunTool(config: DistributionRunToolConfig) {
|
|
|
1005
1077
|
setSnapTarget(resolved.snapped)
|
|
1006
1078
|
setSnapScreen(resolved.snapScreen ?? null)
|
|
1007
1079
|
setEndConnection({ port: null, body: null })
|
|
1080
|
+
// A vertical route ends at the target, not under the pointer: show that
|
|
1081
|
+
// riser now so the next click or Enter places what the preview shows.
|
|
1082
|
+
if (configRef.current.routingMode === 'vertical')
|
|
1083
|
+
updateCursor(verticalTarget(resolved.point))
|
|
1008
1084
|
return
|
|
1009
1085
|
}
|
|
1010
1086
|
const typedResolved = applyTypedLength(resolved)
|
|
@@ -1036,6 +1112,11 @@ export function useDistributionRunTool(config: DistributionRunToolConfig) {
|
|
|
1036
1112
|
setAltActive(true)
|
|
1037
1113
|
return
|
|
1038
1114
|
}
|
|
1115
|
+
if (
|
|
1116
|
+
configRef.current.routingMode === 'vertical' &&
|
|
1117
|
+
(event.key === 'Tab' || /^[0-9.,]$/.test(event.key))
|
|
1118
|
+
)
|
|
1119
|
+
return
|
|
1039
1120
|
if (
|
|
1040
1121
|
event.key === 'Tab' &&
|
|
1041
1122
|
startRef.current &&
|
|
@@ -1045,7 +1126,10 @@ export function useDistributionRunTool(config: DistributionRunToolConfig) {
|
|
|
1045
1126
|
) {
|
|
1046
1127
|
event.preventDefault()
|
|
1047
1128
|
event.stopImmediatePropagation()
|
|
1048
|
-
const source =
|
|
1129
|
+
const source =
|
|
1130
|
+
configRef.current.routingMode === 'horizontal'
|
|
1131
|
+
? null
|
|
1132
|
+
: startConnectionRef.current.port?.direction
|
|
1049
1133
|
const candidates = source
|
|
1050
1134
|
? run3DDirectionCandidates(source)
|
|
1051
1135
|
: runHorizontalDirectionCandidates(null)
|
|
@@ -1117,7 +1201,10 @@ export function useDistributionRunTool(config: DistributionRunToolConfig) {
|
|
|
1117
1201
|
}
|
|
1118
1202
|
|
|
1119
1203
|
refreshCursorRef.current = () => {
|
|
1120
|
-
|
|
1204
|
+
const currentStart = startRef.current
|
|
1205
|
+
if (currentStart && configRef.current.routingMode === 'vertical')
|
|
1206
|
+
updateCursor(verticalTarget(currentStart))
|
|
1207
|
+
else if (lastPointerRef.current)
|
|
1121
1208
|
updateCursor(applyTypedLength(resolvePoint(lastPointerRef.current)))
|
|
1122
1209
|
else if (lastResolvedRef.current) updateCursor(applyTypedLength(lastResolvedRef.current))
|
|
1123
1210
|
}
|
|
@@ -1151,18 +1238,39 @@ export function useDistributionRunTool(config: DistributionRunToolConfig) {
|
|
|
1151
1238
|
}
|
|
1152
1239
|
}, [camera, gl, config.active, updateLengthInput])
|
|
1153
1240
|
|
|
1241
|
+
// The resolver reads config through a ref; changing these controls must refresh a stationary cursor.
|
|
1242
|
+
// biome-ignore lint/correctness/useExhaustiveDependencies: configuration changes invalidate the cursor
|
|
1243
|
+
useEffect(() => {
|
|
1244
|
+
forcedDirectionRef.current = null
|
|
1245
|
+
if (config.routingMode !== 'surface') lockedWallTargetRef.current = null
|
|
1246
|
+
refreshCursor()
|
|
1247
|
+
}, [
|
|
1248
|
+
config.routingMode,
|
|
1249
|
+
config.elevation,
|
|
1250
|
+
config.targetElevation,
|
|
1251
|
+
config.connectCrossings,
|
|
1252
|
+
refreshCursor,
|
|
1253
|
+
])
|
|
1254
|
+
|
|
1154
1255
|
return {
|
|
1155
1256
|
refreshCursor,
|
|
1257
|
+
routingMode: config.routingMode,
|
|
1156
1258
|
start,
|
|
1157
1259
|
cursor,
|
|
1158
1260
|
snapTarget,
|
|
1159
1261
|
snapScreen,
|
|
1262
|
+
cursorScreen: planVisible ? cursorScreen : null,
|
|
1160
1263
|
altActive,
|
|
1161
1264
|
directionMode,
|
|
1162
1265
|
lengthInput,
|
|
1163
1266
|
validationMessage,
|
|
1164
1267
|
onLengthInputChange: updateLengthInput,
|
|
1165
1268
|
onDirectionSelect: (direction: RunPoint) => {
|
|
1269
|
+
if (
|
|
1270
|
+
configRef.current.routingMode === 'vertical' ||
|
|
1271
|
+
(configRef.current.routingMode === 'horizontal' && Math.abs(direction[1]) > 1e-6)
|
|
1272
|
+
)
|
|
1273
|
+
return
|
|
1166
1274
|
forcedDirectionRef.current = direction
|
|
1167
1275
|
setDirectionMode('angle')
|
|
1168
1276
|
refreshCursorRef.current()
|
|
@@ -1183,6 +1291,7 @@ export function DistributionRunCursor({
|
|
|
1183
1291
|
extraParts = [],
|
|
1184
1292
|
cursorRef,
|
|
1185
1293
|
directionMode,
|
|
1294
|
+
routingMode,
|
|
1186
1295
|
startDirection,
|
|
1187
1296
|
lengthInput,
|
|
1188
1297
|
validationMessage,
|
|
@@ -1198,6 +1307,7 @@ export function DistributionRunCursor({
|
|
|
1198
1307
|
extraParts?: DimensionPillPart[]
|
|
1199
1308
|
cursorRef?: RefObject<Group | null>
|
|
1200
1309
|
directionMode: RunDirectionMode
|
|
1310
|
+
routingMode?: 'surface' | 'horizontal' | 'vertical'
|
|
1201
1311
|
startDirection?: readonly [number, number, number] | null
|
|
1202
1312
|
lengthInput?: string
|
|
1203
1313
|
validationMessage?: string | null
|
|
@@ -1208,8 +1318,8 @@ export function DistributionRunCursor({
|
|
|
1208
1318
|
const i18n = usePascalI18n()
|
|
1209
1319
|
const lengthFieldRef = useRef<HTMLInputElement>(null)
|
|
1210
1320
|
useEffect(() => {
|
|
1211
|
-
if (start) lengthFieldRef.current?.focus()
|
|
1212
|
-
}, [start])
|
|
1321
|
+
if (start && routingMode !== 'vertical') lengthFieldRef.current?.focus()
|
|
1322
|
+
}, [start, routingMode])
|
|
1213
1323
|
if (!cursor) return null
|
|
1214
1324
|
const parts: DimensionPillPart[] = [
|
|
1215
1325
|
...(start
|
|
@@ -1270,9 +1380,9 @@ export function DistributionRunCursor({
|
|
|
1270
1380
|
cursor={cursor}
|
|
1271
1381
|
mode={directionMode}
|
|
1272
1382
|
snapped={!!snapTarget}
|
|
1273
|
-
sourceDirection={startDirection ?? null}
|
|
1383
|
+
sourceDirection={routingMode === 'horizontal' ? null : (startDirection ?? null)}
|
|
1274
1384
|
start={start}
|
|
1275
|
-
onDirectionSelect={onDirectionSelect}
|
|
1385
|
+
onDirectionSelect={routingMode === 'vertical' ? undefined : onDirectionSelect}
|
|
1276
1386
|
/>
|
|
1277
1387
|
)}
|
|
1278
1388
|
<CursorSphere
|
|
@@ -1291,7 +1401,15 @@ export function DistributionRunCursor({
|
|
|
1291
1401
|
>
|
|
1292
1402
|
<div className="flex flex-col items-center gap-1">
|
|
1293
1403
|
<DimensionPill parts={parts} primary={primary} unit={unit} />
|
|
1294
|
-
{start ? (
|
|
1404
|
+
{start && routingMode === 'vertical' ? (
|
|
1405
|
+
<div className="rounded-full border border-border/60 bg-background/90 px-3 py-1 text-xs shadow-sm">
|
|
1406
|
+
{i18n._({
|
|
1407
|
+
id: 'editor.tools.distribution.distributionRunTool.targetElevation',
|
|
1408
|
+
message: 'Target elevation: {elevation} m · click or Enter to place',
|
|
1409
|
+
values: { elevation: formatFixed(cursor[1], 2, i18n.getLocale()) },
|
|
1410
|
+
})}
|
|
1411
|
+
</div>
|
|
1412
|
+
) : start ? (
|
|
1295
1413
|
<label className="rounded-full border border-border/60 bg-background/90 px-3 py-1 text-[11px] tabular-nums text-muted-foreground shadow-sm backdrop-blur">
|
|
1296
1414
|
{messageWithSlot(
|
|
1297
1415
|
i18n,
|
|
@@ -1,8 +1,7 @@
|
|
|
1
|
-
import type { AnyNodeId,
|
|
1
|
+
import type { AnyNodeId, SpawnNode } from '@pascal-app/core'
|
|
2
2
|
import { createSceneApi, nodeRegistry, useScene } from '@pascal-app/core'
|
|
3
3
|
import { Suspense, useMemo } from 'react'
|
|
4
4
|
import { useMovingNode } from '../../../store/use-interaction-scope'
|
|
5
|
-
import { MoveElevatorTool } from '../elevator/move-elevator-tool'
|
|
6
5
|
import { MoveRegistryNodeTool } from '../registry/move-registry-node-tool'
|
|
7
6
|
import { getRegistryAffordanceTool } from '../shared/affordance-dispatch'
|
|
8
7
|
|
|
@@ -20,8 +19,6 @@ import { getRegistryAffordanceTool } from '../shared/affordance-dispatch'
|
|
|
20
19
|
* 2. `MoveRegistryNodeTool` — generic translate-on-XZ for kinds that only
|
|
21
20
|
* declare `capabilities.movable` (shelf, spawn, duct-terminal,
|
|
22
21
|
* hvac-equipment, …).
|
|
23
|
-
* 3. `elevator` is the lone remaining legacy arm — its bespoke cab/shaft
|
|
24
|
-
* mover hasn't been ported to a kind-owned affordance yet.
|
|
25
22
|
*/
|
|
26
23
|
export const MoveTool: React.FC<{
|
|
27
24
|
onNodeMoved?: (nodeId: AnyNodeId) => void
|
|
@@ -47,7 +44,5 @@ export const MoveTool: React.FC<{
|
|
|
47
44
|
return <MoveRegistryNodeTool node={movingNode} />
|
|
48
45
|
}
|
|
49
46
|
|
|
50
|
-
if (movingNode.type === 'elevator')
|
|
51
|
-
return <MoveElevatorTool node={movingNode as ElevatorNode} onCommitted={onNodeMoved} />
|
|
52
47
|
return null
|
|
53
48
|
}
|
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
collectAlignmentAnchors,
|
|
10
10
|
emitter,
|
|
11
11
|
findLevelAncestorId,
|
|
12
|
+
formatLinearMeasurement,
|
|
12
13
|
type GridEvent,
|
|
13
14
|
getScaledDimensions,
|
|
14
15
|
getWallBodyCenterOffset,
|
|
@@ -49,7 +50,6 @@ import {
|
|
|
49
50
|
publishPlacementSurface,
|
|
50
51
|
} from '../../../lib/active-placement-surface'
|
|
51
52
|
import { EDITOR_LAYER } from '../../../lib/constants'
|
|
52
|
-
import { formatLinearMeasurement } from '../../../lib/measurements'
|
|
53
53
|
import { isFreshPlacementMetadata } from '../../../lib/placement-metadata'
|
|
54
54
|
import { createMovementSfxTick } from '../../../lib/sfx/movement-tick'
|
|
55
55
|
import { sfxEmitter } from '../../../lib/sfx-bus'
|
|
@@ -4,6 +4,7 @@ import {
|
|
|
4
4
|
type AnyNode,
|
|
5
5
|
type AnyNodeId,
|
|
6
6
|
emitter,
|
|
7
|
+
formatLinearMeasurement,
|
|
7
8
|
sceneRegistry,
|
|
8
9
|
useScene,
|
|
9
10
|
type WallFace,
|
|
@@ -27,7 +28,6 @@ import {
|
|
|
27
28
|
import { LineBasicNodeMaterial, MeshBasicNodeMaterial } from 'three/webgpu'
|
|
28
29
|
import { markToolCancelConsumed } from '../../../hooks/use-keyboard'
|
|
29
30
|
import { EDITOR_LAYER } from '../../../lib/constants'
|
|
30
|
-
import { formatLinearMeasurement } from '../../../lib/measurements'
|
|
31
31
|
import { paintRegionTargets, usePaintRegionMode } from '../../../lib/paint-region-mode'
|
|
32
32
|
import { isTypingTarget } from '../../../lib/typing-target'
|
|
33
33
|
import {
|
|
@@ -107,6 +107,7 @@ test('focus completion: mounted registry mover owns only outside R T and Alt', a
|
|
|
107
107
|
cancelAnimationFrame: { configurable: true, value: (key: number) => frames.delete(key) },
|
|
108
108
|
IS_REACT_ACT_ENVIRONMENT: { configurable: true, value: true },
|
|
109
109
|
})
|
|
110
|
+
let supportY = 0
|
|
110
111
|
core.registerNode({
|
|
111
112
|
kind: node.type,
|
|
112
113
|
schemaVersion: 1,
|
|
@@ -114,7 +115,14 @@ test('focus completion: mounted registry mover owns only outside R T and Alt', a
|
|
|
114
115
|
schema: z.object({ type: z.literal(node.type) }).passthrough(),
|
|
115
116
|
defaults: () => ({}),
|
|
116
117
|
capabilities: {
|
|
117
|
-
movable: {
|
|
118
|
+
movable: {
|
|
119
|
+
isValidPosition: () => false,
|
|
120
|
+
resolvePosition: ({ position }: { position: [number, number, number] }) => [
|
|
121
|
+
position[0],
|
|
122
|
+
supportY,
|
|
123
|
+
position[2],
|
|
124
|
+
],
|
|
125
|
+
},
|
|
118
126
|
floorPlaced: {
|
|
119
127
|
collides: true,
|
|
120
128
|
footprint: (candidate: AnyNode) => ({
|
|
@@ -251,6 +259,39 @@ test('focus completion: mounted registry mover owns only outside R T and Alt', a
|
|
|
251
259
|
expect((await key('t', false)).defaultPrevented).toBe(true)
|
|
252
260
|
expect(mesh.rotation.y).toBeCloseTo(0.25)
|
|
253
261
|
expect(core.useLiveTransforms.getState().transforms.get(id)?.rotation).toBeCloseTo(0.25)
|
|
262
|
+
supportY = 0.75
|
|
263
|
+
await act(async () => {
|
|
264
|
+
core.emitter.emit('grid:move', {
|
|
265
|
+
nativeEvent: {
|
|
266
|
+
target: { closest: () => null },
|
|
267
|
+
} as unknown as import('@pascal-app/core').GridEvent['nativeEvent'],
|
|
268
|
+
position: [7, 0, 3],
|
|
269
|
+
localPosition: [7, 0, 3],
|
|
270
|
+
stopPropagation() {},
|
|
271
|
+
} as import('@pascal-app/core').GridEvent)
|
|
272
|
+
flushFrames()
|
|
273
|
+
})
|
|
274
|
+
expect(mesh.position.y).toBeCloseTo(0.75)
|
|
275
|
+
expect(core.useLiveTransforms.getState().transforms.get(id)?.position?.[1]).toBeCloseTo(0.75)
|
|
276
|
+
expect(
|
|
277
|
+
(core.useScene.getState().nodes[id] as unknown as { position: number[] }).position[1],
|
|
278
|
+
).toBe(0)
|
|
279
|
+
supportY = 1.25
|
|
280
|
+
await key('Alt', false)
|
|
281
|
+
await act(async () => {
|
|
282
|
+
core.emitter.emit('grid:click', {
|
|
283
|
+
nativeEvent: {
|
|
284
|
+
target: { closest: () => null },
|
|
285
|
+
} as unknown as import('@pascal-app/core').GridEvent['nativeEvent'],
|
|
286
|
+
position: [7, 0, 3],
|
|
287
|
+
localPosition: [7, 0, 3],
|
|
288
|
+
stopPropagation() {},
|
|
289
|
+
} as import('@pascal-app/core').GridEvent)
|
|
290
|
+
flushFrames()
|
|
291
|
+
})
|
|
292
|
+
expect(
|
|
293
|
+
(core.useScene.getState().nodes[id] as unknown as { position: number[] }).position[1],
|
|
294
|
+
).toBeCloseTo(1.25)
|
|
254
295
|
} finally {
|
|
255
296
|
if (root)
|
|
256
297
|
await act(async () => {
|
|
@@ -484,6 +484,18 @@ export function MoveRegistryNodeTool({ node: source }: { node: AnyNode }) {
|
|
|
484
484
|
return Array.isArray(rotation) ? [rotation[0] ?? 0, y, rotation[2] ?? 0] : y
|
|
485
485
|
}
|
|
486
486
|
|
|
487
|
+
const resolvePosition = (position: [number, number, number]) => {
|
|
488
|
+
const nodes = useScene.getState().nodes
|
|
489
|
+
const current = nodes[node.id] ?? node
|
|
490
|
+
return (
|
|
491
|
+
nodeRegistry.get(current.type)?.capabilities?.movable?.resolvePosition?.({
|
|
492
|
+
node: current,
|
|
493
|
+
position,
|
|
494
|
+
nodes,
|
|
495
|
+
}) ?? position
|
|
496
|
+
)
|
|
497
|
+
}
|
|
498
|
+
|
|
487
499
|
const keepStartingElevation = (position: [number, number, number], yaw: number) => {
|
|
488
500
|
if (!preserveLevelAndElevation) return position
|
|
489
501
|
const rotation = toCommitRotation(yaw)
|
|
@@ -534,14 +546,19 @@ export function MoveRegistryNodeTool({ node: source }: { node: AnyNode }) {
|
|
|
534
546
|
// them through `useLiveNodeOverrides` (transient — no history churn;
|
|
535
547
|
// GeometrySystem merges overrides via getEffectiveNode). Each connected
|
|
536
548
|
// node is re-dirtied so its geometry rebuilds against the new override.
|
|
537
|
-
|
|
549
|
+
// False, previewing nothing, when the connected joints cannot follow.
|
|
550
|
+
const previewConnectivity = (
|
|
551
|
+
position: [number, number, number],
|
|
552
|
+
rotationY: number,
|
|
553
|
+
): boolean => {
|
|
538
554
|
const connectivity = connectivityRef.current
|
|
539
|
-
if (!connectivity) return
|
|
555
|
+
if (!connectivity) return true
|
|
540
556
|
const updates = resolveConnectivityUpdates(
|
|
541
557
|
connectivity,
|
|
542
558
|
buildPreviewNode(position, rotationY),
|
|
543
559
|
)
|
|
544
|
-
if (updates
|
|
560
|
+
if (!updates) return false
|
|
561
|
+
if (updates.length === 0) return true
|
|
545
562
|
useLiveNodeOverrides
|
|
546
563
|
.getState()
|
|
547
564
|
.setMany(updates.map((u) => [u.id, u.data as Record<string, unknown>] as const))
|
|
@@ -549,6 +566,7 @@ export function MoveRegistryNodeTool({ node: source }: { node: AnyNode }) {
|
|
|
549
566
|
for (const u of updates) {
|
|
550
567
|
if (useScene.getState().nodes[u.id]) useScene.getState().markDirty(u.id)
|
|
551
568
|
}
|
|
569
|
+
return true
|
|
552
570
|
}
|
|
553
571
|
|
|
554
572
|
const clearConnectivityOverrides = () => {
|
|
@@ -1026,7 +1044,11 @@ export function MoveRegistryNodeTool({ node: source }: { node: AnyNode }) {
|
|
|
1026
1044
|
},
|
|
1027
1045
|
).position
|
|
1028
1046
|
}
|
|
1029
|
-
position = keepStartingElevation(position, rotationRef.current)
|
|
1047
|
+
position = resolvePosition(keepStartingElevation(position, rotationRef.current))
|
|
1048
|
+
// Carry connected ductwork along (preview only — committed on drop). A
|
|
1049
|
+
// position the connected joints cannot follow holds the last frame,
|
|
1050
|
+
// which is what the drop commits.
|
|
1051
|
+
if (!previewConnectivity(position, rotationRef.current)) return
|
|
1030
1052
|
const visualPosition = getVisualPosition(position)
|
|
1031
1053
|
hasMovedRef.current = true
|
|
1032
1054
|
setCursorPosition(visualPosition)
|
|
@@ -1054,8 +1076,6 @@ export function MoveRegistryNodeTool({ node: source }: { node: AnyNode }) {
|
|
|
1054
1076
|
})
|
|
1055
1077
|
syncParentFramePreview(position)
|
|
1056
1078
|
markMovedNodeDirty()
|
|
1057
|
-
// Carry connected ductwork along (preview only — committed on drop).
|
|
1058
|
-
previewConnectivity(position, rotationRef.current)
|
|
1059
1079
|
|
|
1060
1080
|
const soundStep = {
|
|
1061
1081
|
coords: [x, z],
|
|
@@ -1116,7 +1136,7 @@ export function MoveRegistryNodeTool({ node: source }: { node: AnyNode }) {
|
|
|
1116
1136
|
// tool — unless Alt (free place) is held to force placement. Other kinds
|
|
1117
1137
|
// carry no validity box (`validRef` stays true), so they're never blocked.
|
|
1118
1138
|
if (!validRef.current && !altRef.current) return
|
|
1119
|
-
const position
|
|
1139
|
+
const position = resolvePosition([...lastCursorRef.current])
|
|
1120
1140
|
|
|
1121
1141
|
const rotation = toCommitRotation(rotationRef.current)
|
|
1122
1142
|
let committedId = node.id as AnyNodeId
|
|
@@ -1171,8 +1191,9 @@ export function MoveRegistryNodeTool({ node: source }: { node: AnyNode }) {
|
|
|
1171
1191
|
? resolveConnectivityUpdates(
|
|
1172
1192
|
connectivityRef.current,
|
|
1173
1193
|
buildPreviewNode(position, rotationRef.current),
|
|
1174
|
-
)
|
|
1194
|
+
)?.filter((u) => useScene.getState().nodes[u.id])
|
|
1175
1195
|
: []
|
|
1196
|
+
if (!connectivityUpdates) return
|
|
1176
1197
|
const surfaceId = surfaceAttachmentId(effectiveNode)
|
|
1177
1198
|
itemSurfaceMove?.restore()
|
|
1178
1199
|
useScene.temporal.getState().resume()
|
|
@@ -1298,7 +1319,6 @@ export function MoveRegistryNodeTool({ node: source }: { node: AnyNode }) {
|
|
|
1298
1319
|
attachmentRotationRef.current,
|
|
1299
1320
|
)
|
|
1300
1321
|
if (nextFreeRotation === null) return
|
|
1301
|
-
sfxEmitter.emit('sfx:item-rotate')
|
|
1302
1322
|
const hostedPose = itemSurfaceMove?.rotate(nextFreeRotation)
|
|
1303
1323
|
let position = hostedPose?.position ?? lastCursorRef.current
|
|
1304
1324
|
if (
|
|
@@ -1319,10 +1339,16 @@ export function MoveRegistryNodeTool({ node: source }: { node: AnyNode }) {
|
|
|
1319
1339
|
)
|
|
1320
1340
|
position = canonicalPositionFromPlan(planOrigin[0], position[1], planOrigin[2])
|
|
1321
1341
|
}
|
|
1322
|
-
|
|
1342
|
+
const rotationY = hostedPose?.rotationY ?? nextFreeRotation
|
|
1343
|
+
position = keepStartingElevation(position, rotationY)
|
|
1344
|
+
// Rotating the fitting swings its collars — connected ducts follow. A
|
|
1345
|
+
// swing they cannot follow holds the last frame; a surface-hosted node
|
|
1346
|
+
// has already turned with its host, so its drop refuses instead.
|
|
1347
|
+
if (!previewConnectivity(position, rotationY) && !hostedPose) return
|
|
1348
|
+
sfxEmitter.emit('sfx:item-rotate')
|
|
1323
1349
|
lastCursorRef.current = position
|
|
1324
|
-
freeRotationRef.current =
|
|
1325
|
-
rotationRef.current =
|
|
1350
|
+
freeRotationRef.current = rotationY
|
|
1351
|
+
rotationRef.current = rotationY
|
|
1326
1352
|
setCursorRotationY(previewRotationY(rotationRef.current))
|
|
1327
1353
|
const visualPosition = getVisualPosition(position)
|
|
1328
1354
|
setCursorPosition(visualPosition)
|
|
@@ -1335,8 +1361,6 @@ export function MoveRegistryNodeTool({ node: source }: { node: AnyNode }) {
|
|
|
1335
1361
|
})
|
|
1336
1362
|
syncParentFramePreview(position)
|
|
1337
1363
|
markMovedNodeDirty()
|
|
1338
|
-
// Rotating the fitting swings its collars — connected ducts follow.
|
|
1339
|
-
previewConnectivity(position, rotationRef.current)
|
|
1340
1364
|
// Rotation changes the footprint's collision span — re-check validity.
|
|
1341
1365
|
if (!itemSurfaceMove?.hosted && itemSurfaceMove?.rejection && lastSurfaceEvent)
|
|
1342
1366
|
onItemMove(lastSurfaceEvent)
|
|
@@ -2,16 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
import '../../../three-types'
|
|
4
4
|
|
|
5
|
+
import { formatLinearMeasurement, type LinearUnit, type MetricNotation } from '@pascal-app/core'
|
|
5
6
|
import { useEffect, useMemo } from 'react'
|
|
6
7
|
import { PlaneGeometry } from 'three'
|
|
7
8
|
import { distance, smoothstep, uv, vec2 } from 'three/tsl'
|
|
8
9
|
import { LineBasicNodeMaterial, MeshBasicNodeMaterial } from 'three/webgpu'
|
|
9
10
|
import { EDITOR_LAYER } from '../../../lib/constants'
|
|
10
|
-
import {
|
|
11
|
-
formatLinearMeasurement,
|
|
12
|
-
type LinearUnit,
|
|
13
|
-
type MetricNotation,
|
|
14
|
-
} from '../../../lib/measurements'
|
|
15
11
|
import { Html } from '../../editor/localized-html'
|
|
16
12
|
import { createLineGeometry, getBoxEdgePoints } from './placement-box-geometry'
|
|
17
13
|
|