feat(editor): live floor-stacking, unified handle system, slab-hole editing + interaction polish (#375)

- Live slab-stacking Y previews for all floor-placed kinds (item/shelf/spawn/column/stair) during placement + both move pathways, via a shared core resolver; canonical positions unchanged.
- Unified 3D handle system (one drag pipeline + one visual primitive) with forgiving invisible hit-areas on every handle, kept on EDITOR_LAYER so they don't poison the MRT scene pass.
- Hover + click-to-edit slab holes in 3D (manual hole -> hole editor; stair/elevator hole -> select owner); generic cross-arrow polygon-move grip; normalized handle interaction colors.
- NaN-safe node mutations + non-finite shadow-light bounds guard.
- Built on #373 (level-scoped alignment / registry slab tool); #373 owns X/Z alignment, this owns Y floor-stacking.
This commit is contained in:
Aymeric Rabot
2026-06-05 16:24:48 -04:00
committed by GitHub
parent d1b40aa98d
commit 0b338cf647
51 changed files with 3942 additions and 1418 deletions
+15 -4
View File
@@ -1,6 +1,12 @@
'use client'
import { type SpawnNode, useLiveTransforms, useRegistry } from '@pascal-app/core'
import {
type AnyNodeId,
type SpawnNode,
useLiveNodeOverrides,
useLiveTransforms,
useRegistry,
} from '@pascal-app/core'
import { createDefaultMaterial, useNodeEvents, useViewer } from '@pascal-app/viewer'
import { useMemo, useRef } from 'react'
import { Color, type Group, Shape } from 'three'
@@ -20,6 +26,11 @@ const SPAWN_COLOR = new Color('#22c55e')
const SpawnRenderer = ({ node }: { node: SpawnNode }) => {
const ref = useRef<Group>(null!)
const handlers = useNodeEvents(node, 'spawn')
const liveOverride = useLiveNodeOverrides((state) => state.get(node.id as AnyNodeId))
const effectiveNode = useMemo(
() => (liveOverride ? ({ ...node, ...liveOverride } as SpawnNode) : node),
[node, liveOverride],
)
const liveTransform = useLiveTransforms((state) => state.get(node.id))
const walkthroughMode = useViewer((state) => state.walkthroughMode)
const shading = useViewer((state) => state.shading)
@@ -52,10 +63,10 @@ const SpawnRenderer = ({ node }: { node: SpawnNode }) => {
return (
<group
position={liveTransform?.position ?? node.position}
position={liveTransform?.position ?? effectiveNode.position}
ref={ref}
rotation={[0, liveTransform?.rotation ?? node.rotation, 0]}
visible={!walkthroughMode}
rotation={[0, liveTransform?.rotation ?? effectiveNode.rotation, 0]}
visible={!walkthroughMode && effectiveNode.visible !== false}
>
<mesh position={[0, 0.09, 0]} rotation={[-Math.PI / 2, 0, 0]} {...handlers}>
<ringGeometry args={[0.34, 0.48, 48]} />