sync: comprehensive monorepo → editor parity (2D/3D decoupling, UX polish, crash fixes)
Squash merge of 3 commits: 1. useLiveTransforms store for 2D/3D decoupling + floorplan overhaul + Sentry crash fixes 2. Comprehensive 59-file sync bringing editor to full monorepo parity (selection highlights, delete tool, furnish/zone modes, keyboard shortcuts, all panels) 3. Missing files fix (materials.ts, merged-outline-node.ts, type fix) 75 files changed, ~6K additions.
This commit is contained in:
@@ -1,14 +1,35 @@
|
||||
import { sceneRegistry, useScene, type WallNode } from '@pascal-app/core'
|
||||
import {
|
||||
type AnyNodeId,
|
||||
baseMaterial,
|
||||
sceneRegistry,
|
||||
useScene,
|
||||
type WallNode,
|
||||
} from '@pascal-app/core'
|
||||
import { useFrame } from '@react-three/fiber'
|
||||
import { useRef } from 'react'
|
||||
import { Color } from 'three'
|
||||
import { Fn, float, fract, length, mix, positionLocal, smoothstep, step, vec2 } from 'three/tsl'
|
||||
|
||||
import { type Mesh, MeshStandardNodeMaterial, Vector3 } from 'three/webgpu'
|
||||
import useViewer from '../../store/use-viewer'
|
||||
|
||||
const tmpVec = new Vector3()
|
||||
const u = new Vector3()
|
||||
const v = new Vector3()
|
||||
const DEFAULT_WALL_COLOR = '#f2f0ed'
|
||||
const WALL_HIGHLIGHT_PROFILES = {
|
||||
delete: {
|
||||
color: new Color('#dc2626'),
|
||||
blend: 0.78,
|
||||
emissiveIntensity: 0.46,
|
||||
},
|
||||
selection: {
|
||||
color: new Color('#818cf8'),
|
||||
blend: 0.32,
|
||||
emissiveIntensity: 0.42,
|
||||
},
|
||||
} as const
|
||||
|
||||
type WallHighlightKind = keyof typeof WALL_HIGHLIGHT_PROFILES
|
||||
|
||||
const dotPattern = Fn(() => {
|
||||
const scale = float(0.1)
|
||||
@@ -30,23 +51,15 @@ const dotPattern = Fn(() => {
|
||||
interface WallMaterials {
|
||||
visible: MeshStandardNodeMaterial
|
||||
invisible: MeshStandardNodeMaterial
|
||||
deleteVisible: MeshStandardNodeMaterial
|
||||
deleteInvisible: MeshStandardNodeMaterial
|
||||
highlightedVisible: MeshStandardNodeMaterial
|
||||
highlightedInvisible: MeshStandardNodeMaterial
|
||||
materialHash: string
|
||||
}
|
||||
|
||||
const wallMaterialCache = new Map<string, WallMaterials>()
|
||||
|
||||
function getMaterialHash(wallNode: WallNode): string {
|
||||
if (!wallNode.material) return 'none'
|
||||
const mat = wallNode.material
|
||||
if (mat.preset && mat.preset !== 'custom') {
|
||||
return `preset-${mat.preset}`
|
||||
}
|
||||
if (mat.properties) {
|
||||
return `props-${mat.properties.color}-${mat.properties.roughness}-${mat.properties.metalness}`
|
||||
}
|
||||
return 'default'
|
||||
}
|
||||
|
||||
const presetColors = {
|
||||
white: '#ffffff',
|
||||
brick: '#8b4513',
|
||||
@@ -59,10 +72,43 @@ const presetColors = {
|
||||
marble: '#f5f5f5',
|
||||
} as const
|
||||
|
||||
function getMaterialHash(wallNode: WallNode): string {
|
||||
if (!wallNode.material) return 'none'
|
||||
const mat = wallNode.material
|
||||
if (mat.preset && mat.preset !== 'custom') {
|
||||
return `preset-${mat.preset}`
|
||||
}
|
||||
if (mat.properties) {
|
||||
return `props-${mat.properties.color}-${mat.properties.roughness}-${mat.properties.metalness}`
|
||||
}
|
||||
return 'default'
|
||||
}
|
||||
|
||||
function getPresetColor(preset: string): string {
|
||||
return presetColors[preset as keyof typeof presetColors] ?? '#ffffff'
|
||||
}
|
||||
|
||||
function getHighlightedColor(color: string, kind: WallHighlightKind): Color {
|
||||
const profile = WALL_HIGHLIGHT_PROFILES[kind]
|
||||
return new Color(color).lerp(profile.color, profile.blend)
|
||||
}
|
||||
|
||||
function createHighlightedWallMaterial(
|
||||
material: MeshStandardNodeMaterial,
|
||||
baseColor: string,
|
||||
kind: WallHighlightKind,
|
||||
): MeshStandardNodeMaterial {
|
||||
const highlightedMaterial = material.clone()
|
||||
const highlightedColor = getHighlightedColor(baseColor, kind)
|
||||
const profile = WALL_HIGHLIGHT_PROFILES[kind]
|
||||
|
||||
highlightedMaterial.color = highlightedColor
|
||||
highlightedMaterial.emissive = highlightedColor.clone()
|
||||
highlightedMaterial.emissiveIntensity = profile.emissiveIntensity
|
||||
|
||||
return highlightedMaterial
|
||||
}
|
||||
|
||||
function getMaterialsForWall(wallNode: WallNode): WallMaterials {
|
||||
const cacheKey = wallNode.id
|
||||
const materialHash = getMaterialHash(wallNode)
|
||||
@@ -75,20 +121,26 @@ function getMaterialsForWall(wallNode: WallNode): WallMaterials {
|
||||
if (existing) {
|
||||
existing.visible.dispose()
|
||||
existing.invisible.dispose()
|
||||
existing.deleteVisible.dispose()
|
||||
existing.deleteInvisible.dispose()
|
||||
existing.highlightedVisible.dispose()
|
||||
existing.highlightedInvisible.dispose()
|
||||
}
|
||||
|
||||
let userColor = '#ffffff'
|
||||
let userColor = DEFAULT_WALL_COLOR
|
||||
if (wallNode.material?.properties?.color) {
|
||||
userColor = wallNode.material.properties.color
|
||||
} else if (wallNode.material?.preset && wallNode.material.preset !== 'custom') {
|
||||
userColor = getPresetColor(wallNode.material.preset)
|
||||
}
|
||||
|
||||
const visibleMat = new MeshStandardNodeMaterial({
|
||||
color: userColor,
|
||||
roughness: 1,
|
||||
metalness: 0,
|
||||
})
|
||||
const visibleMat = wallNode.material
|
||||
? new MeshStandardNodeMaterial({
|
||||
color: userColor,
|
||||
roughness: 1,
|
||||
metalness: 0,
|
||||
})
|
||||
: (baseMaterial.clone() as MeshStandardNodeMaterial)
|
||||
|
||||
const invisibleMat = new MeshStandardNodeMaterial({
|
||||
transparent: true,
|
||||
@@ -98,7 +150,20 @@ function getMaterialsForWall(wallNode: WallNode): WallMaterials {
|
||||
emissive: userColor,
|
||||
})
|
||||
|
||||
const result: WallMaterials = { visible: visibleMat, invisible: invisibleMat, materialHash }
|
||||
const highlightedVisible = createHighlightedWallMaterial(visibleMat, userColor, 'selection')
|
||||
const highlightedInvisible = createHighlightedWallMaterial(invisibleMat, userColor, 'selection')
|
||||
const deleteVisible = createHighlightedWallMaterial(visibleMat, userColor, 'delete')
|
||||
const deleteInvisible = createHighlightedWallMaterial(invisibleMat, userColor, 'delete')
|
||||
|
||||
const result: WallMaterials = {
|
||||
visible: visibleMat,
|
||||
invisible: invisibleMat,
|
||||
deleteVisible,
|
||||
deleteInvisible,
|
||||
highlightedVisible,
|
||||
highlightedInvisible,
|
||||
materialHash,
|
||||
}
|
||||
wallMaterialCache.set(cacheKey, result)
|
||||
return result
|
||||
}
|
||||
@@ -135,78 +200,77 @@ export const WallCutout = () => {
|
||||
const lastUpdateTime = useRef(0)
|
||||
const lastWallMode = useRef<string>(useViewer.getState().wallMode)
|
||||
const lastNumberOfWalls = useRef(0)
|
||||
const lastWallMaterials = useRef<Map<string, WallMaterials>>(new Map())
|
||||
const lastHighlightKey = useRef('')
|
||||
|
||||
useFrame(({ camera, clock }) => {
|
||||
const wallMode = useViewer.getState().wallMode
|
||||
const selectedIds = useViewer.getState().selection.selectedIds
|
||||
const previewSelectedIds = useViewer.getState().previewSelectedIds
|
||||
const hoveredId = useViewer.getState().hoveredId
|
||||
const hoverHighlightMode = useViewer.getState().hoverHighlightMode
|
||||
const currentTime = clock.elapsedTime
|
||||
const currentCameraPosition = camera.position
|
||||
camera.getWorldDirection(tmpVec)
|
||||
tmpVec.add(currentCameraPosition)
|
||||
const highlightedWallIds = new Set(
|
||||
[...selectedIds, ...previewSelectedIds].filter(
|
||||
(id) => useScene.getState().nodes[id as AnyNodeId]?.type === 'wall',
|
||||
),
|
||||
)
|
||||
const deleteHoveredWallId =
|
||||
hoverHighlightMode === 'delete' &&
|
||||
hoveredId &&
|
||||
useScene.getState().nodes[hoveredId as AnyNodeId]?.type === 'wall'
|
||||
? hoveredId
|
||||
: null
|
||||
const highlightKey = `${Array.from(highlightedWallIds).sort().join('|')}::${deleteHoveredWallId ?? ''}`
|
||||
|
||||
const distanceMoved = currentCameraPosition.distanceTo(lastCameraPosition.current)
|
||||
const directionChanged = tmpVec.distanceTo(lastCameraTarget.current)
|
||||
const timeSinceUpdate = currentTime - lastUpdateTime.current
|
||||
|
||||
const shouldUpdate =
|
||||
if (
|
||||
((distanceMoved > 0.5 || directionChanged > 0.3) && timeSinceUpdate > 0.1) ||
|
||||
lastWallMode.current !== wallMode ||
|
||||
sceneRegistry.byType.wall.size !== lastNumberOfWalls.current
|
||||
|
||||
const walls = sceneRegistry.byType.wall
|
||||
const currentWallIds = new Set<string>()
|
||||
|
||||
walls.forEach((wallId) => {
|
||||
const wallMesh = sceneRegistry.nodes.get(wallId)
|
||||
if (!wallMesh) return
|
||||
const wallNode = useScene.getState().nodes[wallId as WallNode['id']]
|
||||
if (!wallNode || wallNode.type !== 'wall') return
|
||||
|
||||
currentWallIds.add(wallId)
|
||||
|
||||
const hideWall = getWallHideState(wallNode, wallMesh as Mesh, wallMode, u)
|
||||
|
||||
if (shouldUpdate) {
|
||||
const materials = getMaterialsForWall(wallNode)
|
||||
;(wallMesh as Mesh).material = hideWall ? materials.invisible : materials.visible
|
||||
} else {
|
||||
const currentMaterial = (wallMesh as Mesh).material
|
||||
const materials = wallMaterialCache.get(wallId)
|
||||
if (
|
||||
!materials ||
|
||||
currentMaterial !== (hideWall ? materials.invisible : materials.visible)
|
||||
) {
|
||||
const newMaterials = getMaterialsForWall(wallNode)
|
||||
;(wallMesh as Mesh).material = hideWall ? newMaterials.invisible : newMaterials.visible
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
if (shouldUpdate) {
|
||||
sceneRegistry.byType.wall.size !== lastNumberOfWalls.current ||
|
||||
lastHighlightKey.current !== highlightKey
|
||||
) {
|
||||
lastCameraPosition.current.copy(currentCameraPosition)
|
||||
lastCameraTarget.current.copy(tmpVec)
|
||||
lastUpdateTime.current = currentTime
|
||||
camera.getWorldDirection(u)
|
||||
|
||||
if (lastWallMode.current !== wallMode) {
|
||||
wallMaterialCache.clear()
|
||||
}
|
||||
const walls = sceneRegistry.byType.wall
|
||||
walls.forEach((wallId) => {
|
||||
const wallMesh = sceneRegistry.nodes.get(wallId)
|
||||
if (!wallMesh) return
|
||||
const wallNode = useScene.getState().nodes[wallId as WallNode['id']]
|
||||
if (!wallNode || wallNode.type !== 'wall') return
|
||||
|
||||
for (const [wallId, mats] of lastWallMaterials.current) {
|
||||
if (!currentWallIds.has(wallId)) {
|
||||
mats.visible.dispose()
|
||||
mats.invisible.dispose()
|
||||
wallMaterialCache.delete(wallId)
|
||||
const hideWall = getWallHideState(wallNode, wallMesh as Mesh, wallMode, u)
|
||||
const isDeleteHighlighted = deleteHoveredWallId === wallId
|
||||
const isSelectionHighlighted = !isDeleteHighlighted && highlightedWallIds.has(wallId)
|
||||
const materials = getMaterialsForWall(wallNode)
|
||||
|
||||
if (hideWall) {
|
||||
;(wallMesh as Mesh).material = isDeleteHighlighted
|
||||
? materials.deleteInvisible
|
||||
: isSelectionHighlighted
|
||||
? materials.highlightedInvisible
|
||||
: materials.invisible
|
||||
} else {
|
||||
;(wallMesh as Mesh).material = isDeleteHighlighted
|
||||
? materials.deleteVisible
|
||||
: isSelectionHighlighted
|
||||
? materials.highlightedVisible
|
||||
: wallNode.material
|
||||
? materials.visible
|
||||
: baseMaterial
|
||||
}
|
||||
}
|
||||
|
||||
lastWallMaterials.current.clear()
|
||||
for (const [wallId, mats] of wallMaterialCache) {
|
||||
lastWallMaterials.current.set(wallId, mats)
|
||||
}
|
||||
|
||||
})
|
||||
lastWallMode.current = wallMode
|
||||
lastNumberOfWalls.current = sceneRegistry.byType.wall.size
|
||||
lastHighlightKey.current = highlightKey
|
||||
}
|
||||
})
|
||||
return null
|
||||
|
||||
Reference in New Issue
Block a user