diff --git a/packages/editor/src/components/editor/floating-action-menu.tsx b/packages/editor/src/components/editor/floating-action-menu.tsx index 2f696d27..93f81a5d 100644 --- a/packages/editor/src/components/editor/floating-action-menu.tsx +++ b/packages/editor/src/components/editor/floating-action-menu.tsx @@ -434,7 +434,11 @@ export function FloatingActionMenu() { ? handleDuplicate : undefined } - onMove={node && !DELETE_ONLY_TYPES.includes(node.type) ? handleMove : undefined} + onMove={ + node && node.type !== 'wall' && !DELETE_ONLY_TYPES.includes(node.type) + ? handleMove + : undefined + } onPointerDown={(e) => e.stopPropagation()} onPointerUp={(e) => e.stopPropagation()} /> diff --git a/packages/editor/src/components/editor/index.tsx b/packages/editor/src/components/editor/index.tsx index 5e893043..8515223c 100644 --- a/packages/editor/src/components/editor/index.tsx +++ b/packages/editor/src/components/editor/index.tsx @@ -68,6 +68,7 @@ import { SiteEdgeLabels } from './site-edge-labels' import { SnapshotCaptureOverlay } from './snapshot-capture-overlay' import { type SnapshotCameraData, ThumbnailGenerator } from './thumbnail-generator' import { WallMeasurementLabel } from './wall-measurement-label' +import { WallMoveSideHandles } from './wall-move-side-handles' const CAMERA_CONTROLS_HINT_DISMISSED_STORAGE_KEY = 'editor-camera-controls-hint-dismissed:v1' const DELETE_CURSOR_BADGE_COLOR = '#ef4444' @@ -587,6 +588,7 @@ const ViewerSceneContent = memo(function ViewerSceneContent({ <> {!isFirstPersonMode && } {!(isVersionPreviewMode || isFirstPersonMode) && } + {!(isVersionPreviewMode || isFirstPersonMode) && } {!(isVersionPreviewMode || isFirstPersonMode) && } {!(isVersionPreviewMode || isFirstPersonMode) && } {!isFirstPersonMode && } diff --git a/packages/editor/src/components/editor/wall-move-side-handles.tsx b/packages/editor/src/components/editor/wall-move-side-handles.tsx new file mode 100644 index 00000000..43bd9019 --- /dev/null +++ b/packages/editor/src/components/editor/wall-move-side-handles.tsx @@ -0,0 +1,259 @@ +'use client' + +import { + type AnyNodeId, + DEFAULT_WALL_HEIGHT, + getWallThickness, + sceneRegistry, + useScene, + type WallNode, +} from '@pascal-app/core' +import { useViewer } from '@pascal-app/viewer' +import { createPortal, type ThreeEvent } from '@react-three/fiber' +import { useEffect, useMemo, useState } from 'react' +import { + BufferGeometry, + ConeGeometry, + CylinderGeometry, + DoubleSide, + Float32BufferAttribute, + type Object3D, +} from 'three' +import { sfxEmitter } from '../../lib/sfx-bus' +import useEditor from '../../store/use-editor' + +const HANDLE_OFFSET = 0.42 +const HANDLE_MIN_OFFSET = 0.5 +const HANDLE_MIN_HEIGHT = 0.62 +const HANDLE_TOP_INSET = 0.08 +const ARROW_COLOR = '#8381ed' +const ARROW_HOVER_COLOR = '#a5b4fc' + +type WallMoveHandle = { + direction: [number, number] + key: string + position: [number, number, number] + rotationY: number +} + +function createArrowHandleGeometry() { + const shaft = new CylinderGeometry(0.04, 0.064, 0.25, 36) + const head = new ConeGeometry(0.13, 0.3, 48) + shaft.rotateZ(-Math.PI / 2) + shaft.translate(-0.085, 0, 0) + head.rotateZ(-Math.PI / 2) + head.translate(0.17, 0, 0) + + const positions: number[] = [] + const normals: number[] = [] + const uvs: number[] = [] + + for (const sourceGeometry of [shaft, head]) { + const geometry = sourceGeometry.index ? sourceGeometry.toNonIndexed() : sourceGeometry + const position = geometry.getAttribute('position') + const normal = geometry.getAttribute('normal') + const uv = geometry.getAttribute('uv') + + for (let index = 0; index < position.count; index += 1) { + positions.push(position.getX(index), position.getY(index), position.getZ(index)) + normals.push(normal.getX(index), normal.getY(index), normal.getZ(index)) + uvs.push(uv?.getX(index) ?? 0, uv?.getY(index) ?? 0) + } + + if (geometry !== sourceGeometry) { + geometry.dispose() + } + sourceGeometry.dispose() + } + + const geometry = new BufferGeometry() + geometry.setAttribute('position', new Float32BufferAttribute(positions, 3)) + geometry.setAttribute('normal', new Float32BufferAttribute(normals, 3)) + geometry.setAttribute('uv', new Float32BufferAttribute(uvs, 2)) + geometry.setAttribute('uv2', new Float32BufferAttribute([...uvs], 2)) + geometry.computeVertexNormals() + geometry.computeBoundingSphere() + return geometry +} + +export function WallMoveSideHandles() { + const selectedIds = useViewer((state) => state.selection.selectedIds) + const mode = useEditor((state) => state.mode) + const isFloorplanHovered = useEditor((state) => state.isFloorplanHovered) + const movingNode = useEditor((state) => state.movingNode) + const movingWallEndpoint = useEditor((state) => state.movingWallEndpoint) + const movingFenceEndpoint = useEditor((state) => state.movingFenceEndpoint) + const curvingWall = useEditor((state) => state.curvingWall) + const curvingFence = useEditor((state) => state.curvingFence) + + const selectedId = selectedIds.length === 1 ? selectedIds[0] : null + const wall = useScene((state) => { + const node = selectedId ? state.nodes[selectedId as AnyNodeId] : null + return node?.type === 'wall' ? node : null + }) + + const shouldRender = + Boolean(wall) && + !isFloorplanHovered && + mode !== 'delete' && + !movingNode && + !movingWallEndpoint && + !movingFenceEndpoint && + !curvingWall && + !curvingFence + + if (!shouldRender || !wall) return null + + return +} + +function WallMoveSideHandlesForWall({ wall }: { wall: WallNode }) { + const [levelObject, setLevelObject] = useState(() => + wall.parentId ? (sceneRegistry.nodes.get(wall.parentId) ?? null) : null, + ) + + useEffect(() => { + let frameId = 0 + + const resolveLevelObject = () => { + const nextLevelObject = wall.parentId + ? (sceneRegistry.nodes.get(wall.parentId) ?? null) + : null + setLevelObject((currentLevelObject) => { + if (currentLevelObject === nextLevelObject) { + return currentLevelObject + } + return nextLevelObject + }) + + if (!nextLevelObject) { + frameId = window.requestAnimationFrame(resolveLevelObject) + } + } + + resolveLevelObject() + + return () => { + if (frameId) { + window.cancelAnimationFrame(frameId) + } + } + }, [wall.parentId]) + + const handles = useMemo(() => getWallMoveHandles(wall), [wall]) + + if (!levelObject || handles.length === 0) return null + + return createPortal( + + {handles.map((handle) => ( + + ))} + , + levelObject, + ) +} + +function WallMoveArrowHandle({ wall, handle }: { wall: WallNode; handle: WallMoveHandle }) { + const [isHovered, setIsHovered] = useState(false) + const arrowGeometry = useMemo(() => createArrowHandleGeometry(), []) + + useEffect(() => { + return () => { + if (document.body.style.cursor === 'grab' || document.body.style.cursor === 'grabbing') { + document.body.style.cursor = '' + } + } + }, []) + + useEffect(() => () => arrowGeometry.dispose(), [arrowGeometry]) + + const activateWallMove = (event: ThreeEvent) => { + event.stopPropagation() + event.nativeEvent.preventDefault() + document.body.style.cursor = 'grabbing' + + sfxEmitter.emit('sfx:item-pick') + useEditor.getState().setMovingNode(wall) + useEditor.getState().setMovingWallEndpoint(null) + useEditor.getState().setMovingFenceEndpoint(null) + useEditor.getState().setCurvingWall(null) + useEditor.getState().setCurvingFence(null) + useViewer.getState().setSelection({ selectedIds: [] }) + } + + return ( + + { + event.stopPropagation() + setIsHovered(true) + document.body.style.cursor = 'grab' + }} + onPointerLeave={(event) => { + event.stopPropagation() + setIsHovered(false) + if (document.body.style.cursor === 'grab') { + document.body.style.cursor = '' + } + }} + renderOrder={1002} + > + + + + + ) +} + +function getWallMoveHandles(wall: WallNode): WallMoveHandle[] { + const dx = wall.end[0] - wall.start[0] + const dz = wall.end[1] - wall.start[1] + const length = Math.hypot(dx, dz) + + if (length < 1e-6) { + return [] + } + + const normal: [number, number] = [-dz / length, dx / length] + const midpoint: [number, number] = [ + (wall.start[0] + wall.end[0]) / 2, + (wall.start[1] + wall.end[1]) / 2, + ] + const wallHeight = wall.height ?? DEFAULT_WALL_HEIGHT + const handleHeight = Math.max(wallHeight - HANDLE_TOP_INSET, HANDLE_MIN_HEIGHT) + const offset = Math.max(getWallThickness(wall) / 2 + HANDLE_OFFSET, HANDLE_MIN_OFFSET) + + return [ + buildWallMoveHandle('front', midpoint, normal, offset, handleHeight), + buildWallMoveHandle('back', midpoint, [-normal[0], -normal[1]], offset, handleHeight), + ] +} + +function buildWallMoveHandle( + key: string, + midpoint: [number, number], + direction: [number, number], + offset: number, + height: number, +): WallMoveHandle { + return { + direction, + key, + position: [midpoint[0] + direction[0] * offset, height, midpoint[1] + direction[1] * offset], + rotationY: Math.atan2(-direction[1], direction[0]), + } +} diff --git a/packages/editor/src/components/tools/wall/move-wall-tool.tsx b/packages/editor/src/components/tools/wall/move-wall-tool.tsx index f19f8d08..a52b2b5c 100644 --- a/packages/editor/src/components/tools/wall/move-wall-tool.tsx +++ b/packages/editor/src/components/tools/wall/move-wall-tool.tsx @@ -3,12 +3,19 @@ import { type AnyNodeId, constrainWallMoveDeltaToAxis, + DEFAULT_WALL_HEIGHT, + detectSpacesForLevel, emitter, type GridEvent, + getMaterialPresetByRef, getPerpendicularWallMoveAxis, + getRenderableSlabPolygon, pauseSceneHistory, planWallMoveJunctions, + resolveMaterial, resumeSceneHistory, + type SlabNode, + SlabNode as SlabSchema, useScene, type WallMoveAxis, type WallMoveBridgePlan, @@ -17,13 +24,18 @@ import { WallNode as WallSchema, } from '@pascal-app/core' import { useViewer } from '@pascal-app/viewer' -import { useCallback, useEffect, useRef, useState } from 'react' +import { useCallback, useEffect, useMemo, useRef, useState } from 'react' +import { BufferGeometry, DoubleSide, Float32BufferAttribute, ShapeUtils, Vector2 } from 'three' import { markToolCancelConsumed } from '../../../hooks/use-keyboard' +import { EDITOR_LAYER } from '../../../lib/constants' import { sfxEmitter } from '../../../lib/sfx-bus' import useEditor from '../../../store/use-editor' import { CursorSphere } from '../shared/cursor-sphere' import { getWallGridStep, isWallLongEnough, snapScalarToGrid } from './wall-drafting' +const AUTO_SLAB_PREVIEW_ELEVATION = 0.05 +const AUTO_SLAB_PREVIEW_Y = AUTO_SLAB_PREVIEW_ELEVATION + 0.025 + function rotateVector([x, z]: [number, number], angle: number): [number, number] { const cos = Math.cos(angle) const sin = Math.sin(angle) @@ -50,6 +62,19 @@ function stripWallIsNewMetadata(meta: WallNode['metadata']): WallNode['metadata' type LinkedWallSnapshot = WallNode +type GhostWallPreview = { + id: string + start: [number, number] + end: [number, number] + color: string + height: number +} + +type GhostSlabPreview = { + id: string + polygon: Array<[number, number]> +} + function getLinkedWallSnapshots(args: { wallId: WallNode['id'] wallParentId: string | null @@ -172,7 +197,11 @@ function getPlannedLinkedWallUpdates( ) } -function wallSegmentExists(walls: WallNode[], start: [number, number], end: [number, number]) { +function wallSegmentExists( + walls: Array>, + start: [number, number], + end: [number, number], +) { return walls.some( (wall) => (samePoint(wall.start, start) && samePoint(wall.end, end)) || @@ -180,6 +209,40 @@ function wallSegmentExists(walls: WallNode[], start: [number, number], end: [num ) } +function getWallGhostColor(wall: WallNode) { + const presetColor = + getMaterialPresetByRef(wall.materialPreset)?.mapProperties.color ?? + getMaterialPresetByRef(wall.interiorMaterialPreset)?.mapProperties.color ?? + getMaterialPresetByRef(wall.exteriorMaterialPreset)?.mapProperties.color + + if (presetColor) { + return presetColor + } + + return resolveMaterial(wall.material ?? wall.interiorMaterial ?? wall.exteriorMaterial).color +} + +function getMinRotatedKey(values: string[]) { + if (values.length === 0) return '' + + let best = '' + for (let index = 0; index < values.length; index += 1) { + const value = [...values.slice(index), ...values.slice(0, index)].join('|') + if (!best || value < best) { + best = value + } + } + + return best +} + +function getPolygonPreviewKey(polygon: Array<[number, number]>) { + const values = polygon.map(([x, z]) => `${x.toFixed(3)}:${z.toFixed(3)}`) + const forward = getMinRotatedKey(values) + const reversed = getMinRotatedKey([...values].reverse()) + return forward < reversed ? forward : reversed +} + function getWallsAfterUpdates( nodes: ReturnType['nodes'], updates: Array<{ id: AnyNodeId; data: Partial }>, @@ -236,6 +299,240 @@ function buildBridgeWallCreates(args: { return creates } +function buildBridgeWallPreviews(args: { + bridgePlans: Array> + nextStart: [number, number] + nextEnd: [number, number] + existingWalls: WallNode[] +}): Array<{ ghost: GhostWallPreview; wall: WallNode }> { + const { bridgePlans, nextStart, nextEnd, existingWalls } = args + const wallsForDuplicateCheck: Array> = [...existingWalls] + const previews: Array<{ ghost: GhostWallPreview; wall: WallNode }> = [] + + for (const plan of bridgePlans) { + const nextPoint = plan.movedEndpoint === 'start' ? nextStart : nextEnd + + if (!isWallLongEnough(plan.originalPoint, nextPoint)) { + continue + } + + if (wallSegmentExists(wallsForDuplicateCheck, plan.originalPoint, nextPoint)) { + continue + } + + const { id: _id, children: _children, ...sourceWall } = plan.wall + const wall = WallSchema.parse({ + ...sourceWall, + name: 'Wall Preview', + start: plan.originalPoint, + end: nextPoint, + children: [], + metadata: stripWallIsNewMetadata(plan.wall.metadata), + }) + const ghost = { + id: `${plan.wall.id}:${plan.movedEndpoint}:${previews.length}`, + start: [...plan.originalPoint] as [number, number], + end: [...nextPoint] as [number, number], + color: getWallGhostColor(plan.wall), + height: plan.wall.height ?? DEFAULT_WALL_HEIGHT, + } + previews.push({ ghost, wall }) + wallsForDuplicateCheck.push(wall) + } + + return previews +} + +function buildAutoSlabGhostPreviews(args: { + levelId: string + walls: WallNode[] + existingSlabs: SlabNode[] +}): GhostSlabPreview[] { + const { levelId, walls, existingSlabs } = args + const levelWalls = walls.filter((wall) => (wall.parentId ?? null) === levelId) + + if (levelWalls.length < 3) { + return [] + } + + const manualSlabKeys = new Set( + existingSlabs + .filter((slab) => !slab.autoFromWalls) + .map((slab) => getPolygonPreviewKey(slab.polygon)), + ) + const existingAutoKeys = new Set( + existingSlabs + .filter((slab) => slab.autoFromWalls) + .map((slab) => getPolygonPreviewKey(getRenderableSlabPolygon(slab))), + ) + const seenPreviewKeys = new Set() + const { roomPolygons } = detectSpacesForLevel(levelId, levelWalls) + const previews: GhostSlabPreview[] = [] + + for (let index = 0; index < roomPolygons.length; index += 1) { + const polygon = roomPolygons[index] + if (!polygon || polygon.length < 3) continue + + const rawPolygon = polygon.map((point) => [point.x, point.y] as [number, number]) + if (manualSlabKeys.has(getPolygonPreviewKey(rawPolygon))) { + continue + } + + const previewSlab = SlabSchema.parse({ + polygon: rawPolygon, + holes: [], + elevation: AUTO_SLAB_PREVIEW_ELEVATION, + autoFromWalls: true, + }) + const renderablePolygon = getRenderableSlabPolygon(previewSlab) + const previewKey = getPolygonPreviewKey(renderablePolygon) + + if ( + renderablePolygon.length < 3 || + existingAutoKeys.has(previewKey) || + seenPreviewKeys.has(previewKey) + ) { + continue + } + + seenPreviewKeys.add(previewKey) + previews.push({ + id: `auto-slab:${index}:${previewKey}`, + polygon: renderablePolygon, + }) + } + + return previews +} + +function setPreviewGeometryAttributes( + geometry: BufferGeometry, + positions: number[], + normals: number[], + uvs: number[], +) { + geometry.setAttribute('position', new Float32BufferAttribute(positions, 3)) + geometry.setAttribute('normal', new Float32BufferAttribute(normals, 3)) + geometry.setAttribute('uv', new Float32BufferAttribute(uvs, 2)) + geometry.setAttribute('uv2', new Float32BufferAttribute([...uvs], 2)) +} + +function createWallPreviewGeometry(length: number, height: number) { + const geometry = new BufferGeometry() + setPreviewGeometryAttributes( + geometry, + [0, 0, 0, length, 0, 0, length, height, 0, 0, height, 0], + [0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1], + [0, 0, 1, 0, 1, 1, 0, 1], + ) + geometry.setIndex([0, 1, 2, 0, 2, 3]) + geometry.computeBoundingSphere() + return geometry +} + +function createSlabPreviewGeometry(polygon: Array<[number, number]>) { + if (polygon.length < 3) { + return null + } + + const contour = polygon.map(([x, z]) => new Vector2(x, z)) + const triangles = ShapeUtils.triangulateShape(contour, []) + if (triangles.length === 0) { + return null + } + + let minX = Number.POSITIVE_INFINITY + let minZ = Number.POSITIVE_INFINITY + let maxX = Number.NEGATIVE_INFINITY + let maxZ = Number.NEGATIVE_INFINITY + + for (const [x, z] of polygon) { + minX = Math.min(minX, x) + minZ = Math.min(minZ, z) + maxX = Math.max(maxX, x) + maxZ = Math.max(maxZ, z) + } + + const width = Math.max(maxX - minX, 0.001) + const depth = Math.max(maxZ - minZ, 0.001) + const positions: number[] = [] + const normals: number[] = [] + const uvs: number[] = [] + + for (const [x, z] of polygon) { + positions.push(x, 0, z) + normals.push(0, 1, 0) + uvs.push((x - minX) / width, (z - minZ) / depth) + } + + const geometry = new BufferGeometry() + setPreviewGeometryAttributes(geometry, positions, normals, uvs) + geometry.setIndex(triangles.flat()) + geometry.computeBoundingSphere() + return geometry +} + +function GhostWallPreviewMesh({ preview }: { preview: GhostWallPreview }) { + const dx = preview.end[0] - preview.start[0] + const dz = preview.end[1] - preview.start[1] + const length = Math.hypot(dx, dz) + const angle = -Math.atan2(dz, dx) + const geometry = useMemo(() => { + return length < 0.01 ? null : createWallPreviewGeometry(length, preview.height) + }, [length, preview.height]) + + useEffect(() => () => geometry?.dispose(), [geometry]) + + if (!geometry) { + return null + } + + return ( + + + + + + + ) +} + +function GhostSlabPreviewMesh({ preview }: { preview: GhostSlabPreview }) { + const geometry = useMemo(() => createSlabPreviewGeometry(preview.polygon), [preview.polygon]) + + useEffect(() => () => geometry?.dispose(), [geometry]) + + if (!geometry) { + return null + } + + return ( + + + + + ) +} + export const MoveWallTool: React.FC<{ node: WallNode }> = ({ node }) => { const meta = typeof node.metadata === 'object' && node.metadata !== null && !Array.isArray(node.metadata) @@ -278,6 +575,8 @@ export const MoveWallTool: React.FC<{ node: WallNode }> = ({ node }) => { const centerZ = (node.start[1] + node.end[1]) / 2 return [centerX, 0, centerZ] }) + const [ghostWallPreviews, setGhostWallPreviews] = useState([]) + const [ghostSlabPreviews, setGhostSlabPreviews] = useState([]) const exitMoveMode = useCallback(() => { useEditor.getState().setMovingNode(null) @@ -323,8 +622,11 @@ export const MoveWallTool: React.FC<{ node: WallNode }> = ({ node }) => { nextEnd, ) - const getLinkedPreviewUpdates = (nextStart: [number, number], nextEnd: [number, number]) => { - const plan = getMovePlan(nextStart, nextEnd) + const getLinkedPreviewUpdates = ( + plan: WallMoveJunctionPlan, + nextStart: [number, number], + nextEnd: [number, number], + ) => { const movedUpdates = getPlannedLinkedWallUpdates( plan, originalStart, @@ -344,13 +646,53 @@ export const MoveWallTool: React.FC<{ node: WallNode }> = ({ node }) => { const centerX = (nextStart[0] + nextEnd[0]) / 2 const centerZ = (nextStart[1] + nextEnd[1]) / 2 setCursorLocalPos([centerX, 0, centerZ]) - applyNodePreview([ + const previewPlan = getMovePlan(nextStart, nextEnd) + const previewUpdates = [ { id: nodeId, start: nextStart, end: nextEnd }, - ...getLinkedPreviewUpdates(nextStart, nextEnd), + ...getLinkedPreviewUpdates(previewPlan, nextStart, nextEnd), + ] + const previewCollapsedWallIds = new Set([ + ...previewUpdates + .filter((entry) => entry.id !== nodeId && !isWallLongEnough(entry.start, entry.end)) + .map((entry) => entry.id as AnyNodeId), + ...previewPlan.wallsToDelete.map((wall) => wall.id as AnyNodeId), ]) + const previewSceneWalls = getWallsAfterUpdates( + useScene.getState().nodes, + previewUpdates.map((entry) => ({ + id: entry.id as AnyNodeId, + data: { start: entry.start, end: entry.end }, + })), + ).filter((wall) => !previewCollapsedWallIds.has(wall.id as AnyNodeId)) + const bridgePreviews = buildBridgeWallPreviews({ + bridgePlans: previewPlan.bridgePlans, + nextStart, + nextEnd, + existingWalls: previewSceneWalls, + }) + const nextGhostWalls = bridgePreviews.map((preview) => preview.ghost) + const virtualBridgeWalls = bridgePreviews.map((preview) => preview.wall) + const sceneNodes = useScene.getState().nodes + const levelId = node.parentId ?? null + setGhostWallPreviews(nextGhostWalls) + setGhostSlabPreviews( + levelId + ? buildAutoSlabGhostPreviews({ + levelId, + walls: [...previewSceneWalls, ...virtualBridgeWalls], + existingSlabs: Object.values(sceneNodes).filter( + (entry): entry is SlabNode => + entry?.type === 'slab' && (entry.parentId ?? null) === levelId, + ), + }) + : [], + ) + applyNodePreview(previewUpdates) } const restoreOriginal = () => { + setGhostWallPreviews([]) + setGhostSlabPreviews([]) applyNodePreview([ { id: nodeId, start: originalStart, end: originalEnd }, ...linkedOriginalsRef.current, @@ -400,6 +742,8 @@ export const MoveWallTool: React.FC<{ node: WallNode }> = ({ node }) => { // Restore original baseline while paused so the next resume+update // registers as a single tracked change (undo reverts to original). + setGhostWallPreviews([]) + setGhostSlabPreviews([]) applyNodePreview([ { id: nodeId, start: originalStart, end: originalEnd }, ...linkedOriginalsRef.current, @@ -534,6 +878,12 @@ export const MoveWallTool: React.FC<{ node: WallNode }> = ({ node }) => { return ( + {ghostSlabPreviews.map((preview) => ( + + ))} + {ghostWallPreviews.map((preview) => ( + + ))} ) } diff --git a/packages/editor/src/components/ui/panels/wall-panel.tsx b/packages/editor/src/components/ui/panels/wall-panel.tsx index 8ab8e6ac..c1fae2e7 100644 --- a/packages/editor/src/components/ui/panels/wall-panel.tsx +++ b/packages/editor/src/components/ui/panels/wall-panel.tsx @@ -4,60 +4,28 @@ import { type AnyNode, type AnyNodeId, getClampedWallCurveOffset, - getEffectiveWallSurfaceMaterial, getMaxWallCurveOffset, getWallCurveLength, - getWallSurfaceMaterialSignature, - type MaterialSchema, normalizeWallCurveOffset, useScene, type WallNode, - type WallSurfaceSide, } from '@pascal-app/core' import { useViewer } from '@pascal-app/viewer' import { Move, Spline } from 'lucide-react' -import { useCallback, useMemo } from 'react' +import { useCallback } from 'react' import { sfxEmitter } from '../../../lib/sfx-bus' import useEditor from '../../../store/use-editor' import { ActionButton, ActionGroup } from '../controls/action-button' -import { MaterialPicker } from '../controls/material-picker' import { PanelSection } from '../controls/panel-section' import { SliderControl } from '../controls/slider-control' import { PanelWrapper } from './panel-wrapper' -function buildWallSurfaceMaterialPatch( - node: WallNode, - targetSide: WallSurfaceSide | null, - material: MaterialSchema | undefined, - materialPreset: string | undefined, -): Partial { - const nextSurfaceMaterial = { material, materialPreset } - const nextInterior = - targetSide === null || targetSide === 'interior' - ? nextSurfaceMaterial - : getEffectiveWallSurfaceMaterial(node, 'interior') - const nextExterior = - targetSide === null || targetSide === 'exterior' - ? nextSurfaceMaterial - : getEffectiveWallSurfaceMaterial(node, 'exterior') - - return { - interiorMaterial: nextInterior.material, - interiorMaterialPreset: nextInterior.materialPreset, - exteriorMaterial: nextExterior.material, - exteriorMaterialPreset: nextExterior.materialPreset, - material: undefined, - materialPreset: undefined, - } -} - export function WallPanel() { const selectedId = useViewer((s) => s.selection.selectedIds[0]) const setSelection = useViewer((s) => s.setSelection) const updateNode = useScene((s) => s.updateNode) const setMovingNode = useEditor((s) => s.setMovingNode) const setCurvingWall = useEditor((s) => s.setCurvingWall) - const selectedMaterialTarget = useEditor((s) => s.selectedMaterialTarget) const node = useScene((s) => selectedId ? (s.nodes[selectedId as AnyNode['id']] as WallNode | undefined) : undefined, @@ -88,35 +56,6 @@ export function WallPanel() { [selectedId, updateNode], ) - const effectiveInteriorMaterial = useMemo( - () => (node ? getEffectiveWallSurfaceMaterial(node, 'interior') : {}), - [node], - ) - const effectiveExteriorMaterial = useMemo( - () => (node ? getEffectiveWallSurfaceMaterial(node, 'exterior') : {}), - [node], - ) - const surfaceMaterialsMatch = useMemo( - () => - getWallSurfaceMaterialSignature(effectiveInteriorMaterial) === - getWallSurfaceMaterialSignature(effectiveExteriorMaterial), - [effectiveExteriorMaterial, effectiveInteriorMaterial], - ) - const materialTargetSide = - selectedMaterialTarget && - selectedMaterialTarget.nodeId === node?.id && - (selectedMaterialTarget.role === 'interior' || selectedMaterialTarget.role === 'exterior') - ? selectedMaterialTarget.role - : null - const materialPickerValue = - materialTargetSide === 'interior' - ? effectiveInteriorMaterial - : materialTargetSide === 'exterior' - ? effectiveExteriorMaterial - : surfaceMaterialsMatch - ? effectiveInteriorMaterial - : {} - const handleUpdateLength = useCallback( (newLength: number) => { if (!node || newLength <= 0) return @@ -140,24 +79,6 @@ export function WallPanel() { [node, handleUpdate], ) - const handleMaterialPresetChange = useCallback( - (materialPreset: string) => { - if (!(node && materialTargetSide)) return - handleUpdate( - buildWallSurfaceMaterialPatch(node, materialTargetSide, undefined, materialPreset), - ) - }, - [handleUpdate, materialTargetSide, node], - ) - - const handleCustomMaterialChange = useCallback( - (material: MaterialSchema) => { - if (!(node && materialTargetSide)) return - handleUpdate(buildWallSurfaceMaterialPatch(node, materialTargetSide, material, undefined)) - }, - [handleUpdate, materialTargetSide, node], - ) - const handleClose = useCallback(() => { setSelection({ selectedIds: [] }) }, [setSelection]) @@ -239,23 +160,6 @@ export function WallPanel() { )} - - {materialTargetSide ? null : ( -
- Click the wall face you want to edit. Materials now apply to one side at a time. -
- )} - -
- } label="Move" onClick={handleMove} />