diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 54a3a47ba..028d449e8 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -102,8 +102,8 @@ export { } from './store/use-interactive' export { default as useLiveNodeOverrides, - type LiveNodeOverrides, getEffectiveNode, + type LiveNodeOverrides, } from './store/use-live-node-overrides' export { default as useLiveTransforms, type LiveTransform } from './store/use-live-transforms' export { clearSceneHistory, default as useScene } from './store/use-scene' diff --git a/packages/core/src/registry/handles.ts b/packages/core/src/registry/handles.ts index 754a3c3d2..c664a19d0 100644 --- a/packages/core/src/registry/handles.ts +++ b/packages/core/src/registry/handles.ts @@ -189,11 +189,7 @@ export type EndpointMoveHandle = { endpoint: 'start' | 'end' placement: HandlePlacement /** Called with the world-space hit on the ground plane. */ - apply: ( - node: N, - worldPoint: readonly [number, number, number], - sceneApi: SceneApi, - ) => Partial + apply: (node: N, worldPoint: readonly [number, number, number], sceneApi: SceneApi) => Partial portal?: HandlePortal } diff --git a/packages/editor/src/components/editor-2d/floorplan-registry-action-menu.tsx b/packages/editor/src/components/editor-2d/floorplan-registry-action-menu.tsx index 4fe78cf45..d6adf6ab7 100644 --- a/packages/editor/src/components/editor-2d/floorplan-registry-action-menu.tsx +++ b/packages/editor/src/components/editor-2d/floorplan-registry-action-menu.tsx @@ -100,9 +100,7 @@ export function FloorplanRegistryActionMenu() { return } - const el = sceneEl.querySelector( - `[data-node-id="${selectedId}"]`, - ) as SVGGElement | null + const el = sceneEl.querySelector(`[data-node-id="${selectedId}"]`) as SVGGElement | null if (el) { const rect = el.getBoundingClientRect() setPosition({ left: rect.left + rect.width / 2, top: rect.top }) diff --git a/packages/editor/src/components/editor-2d/renderers/floorplan-registry-layer.tsx b/packages/editor/src/components/editor-2d/renderers/floorplan-registry-layer.tsx index 73aa326f5..45b9267d2 100644 --- a/packages/editor/src/components/editor-2d/renderers/floorplan-registry-layer.tsx +++ b/packages/editor/src/components/editor-2d/renderers/floorplan-registry-layer.tsx @@ -169,8 +169,7 @@ export const FloorplanRegistryLayer = memo(function FloorplanRegistryLayer() { (editorPhase === 'structure' && editorMode === 'build' && (editorTool === 'door' || editorTool === 'window')) || - (movingNode != null && - !!nodeRegistry.get(movingNode.type)?.capabilities?.wallOpeningPlacement) + (movingNode != null && !!nodeRegistry.get(movingNode.type)?.capabilities?.wallOpeningPlacement) // Subscribe to the live-transforms map ref so the layer re-renders // whenever a 3D mover publishes a per-frame position (see // `usePlacementCoordinator`). Without this the 2D floor plan only @@ -376,8 +375,7 @@ export const FloorplanRegistryLayer = memo(function FloorplanRegistryLayer() { const hovered = hoveredId === cid const moving = movingNode?.id === cid const ctx: GeometryContext = { - resolve: (rid: AnyNodeId): N | undefined => - nodes[rid] as N | undefined, + resolve: (rid: AnyNodeId): N | undefined => nodes[rid] as N | undefined, children: [], siblings: [], parent: activeLevelNode, @@ -391,9 +389,10 @@ export const FloorplanRegistryLayer = memo(function FloorplanRegistryLayer() { } : undefined, } - const geometry = ( - builder as (n: AnyNode, c: GeometryContext) => FloorplanGeometry | null - )(node, ctx) + const geometry = (builder as (n: AnyNode, c: GeometryContext) => FloorplanGeometry | null)( + node, + ctx, + ) if (geometry) { const { base, overlay } = splitFloorplanOverlay(geometry) out.push({ id: cid, node, base, overlay, selected, highlighted }) @@ -1101,11 +1100,7 @@ function InteractiveGeometry({ fill="transparent" onPointerDown={(e) => { if (affordance) { - onHandlePointerDown( - affordance, - payload, - e as ReactPointerEvent, - ) + onHandlePointerDown(affordance, payload, e as ReactPointerEvent) } else { onMoveHandlePointerDown(e as ReactPointerEvent) } diff --git a/packages/editor/src/components/editor/editor-layout-v2.tsx b/packages/editor/src/components/editor/editor-layout-v2.tsx index 498e7251c..842b2a100 100644 --- a/packages/editor/src/components/editor/editor-layout-v2.tsx +++ b/packages/editor/src/components/editor/editor-layout-v2.tsx @@ -11,8 +11,8 @@ import { EditorLayoutMobile } from './editor-layout-mobile' const SIDEBAR_MIN_WIDTH = 300 const SIDEBAR_MAX_WIDTH = 800 const SIDEBAR_COLLAPSE_THRESHOLD = 220 -// Matches the `w-12` rail in ; the resize math is relative to it. -const RAIL_WIDTH = 48 +// Matches the `w-14` rail in ; the resize math is relative to it. +const RAIL_WIDTH = 56 // ── Left column: resizable panel with tab bar ──────────────────────────────── diff --git a/packages/editor/src/components/editor/floating-action-menu.tsx b/packages/editor/src/components/editor/floating-action-menu.tsx index 2efd1ec27..efd5691c2 100644 --- a/packages/editor/src/components/editor/floating-action-menu.tsx +++ b/packages/editor/src/components/editor/floating-action-menu.tsx @@ -100,9 +100,7 @@ const MENU_Y_OFFSETS: Record = { function getMenuYOffset(node: AnyNode | null): number { if (!node) return MENU_Y_OFFSET_DEFAULT + EXTRA_MENU_LIFT if (node.type === 'stair-segment') { - return ( - (MENU_Y_OFFSETS[`stair-${node.segmentType}`] ?? MENU_Y_OFFSET_DEFAULT) + EXTRA_MENU_LIFT - ) + return (MENU_Y_OFFSETS[`stair-${node.segmentType}`] ?? MENU_Y_OFFSET_DEFAULT) + EXTRA_MENU_LIFT } return (MENU_Y_OFFSETS[node.type] ?? MENU_Y_OFFSET_DEFAULT) + EXTRA_MENU_LIFT } @@ -181,7 +179,6 @@ export function FloatingActionMenu() { // in-world chrome (height-resize arrows, measurement labels). groupRef.current.position.set(center.x, box.max.y + getMenuYOffset(node), center.z) } - } }) diff --git a/packages/editor/src/components/editor/floorplan-panel.tsx b/packages/editor/src/components/editor/floorplan-panel.tsx index e41a9ab75..965e77afd 100644 --- a/packages/editor/src/components/editor/floorplan-panel.tsx +++ b/packages/editor/src/components/editor/floorplan-panel.tsx @@ -5045,7 +5045,9 @@ export function FloorplanPanel() { // space. Length renders at the segment midpoint; angle arcs sit at // each endpoint that meets an existing wall. const draftWallMeasurement = useMemo(() => { - if (!(isWallBuildActive && draftStart && draftEnd && isSegmentLongEnough(draftStart, draftEnd))) { + if ( + !(isWallBuildActive && draftStart && draftEnd && isSegmentLongEnough(draftStart, draftEnd)) + ) { return null } diff --git a/packages/editor/src/components/editor/node-arrow-handles.tsx b/packages/editor/src/components/editor/node-arrow-handles.tsx index b9d5e1a03..81074b777 100644 --- a/packages/editor/src/components/editor/node-arrow-handles.tsx +++ b/packages/editor/src/components/editor/node-arrow-handles.tsx @@ -237,8 +237,7 @@ export function NodeArrowHandles() { rawNode ? s.overrides.get(rawNode.id) : undefined, ) const node = useMemo( - () => - rawNode && liveOverride ? ({ ...rawNode, ...liveOverride } as AnyNode) : rawNode, + () => (rawNode && liveOverride ? ({ ...rawNode, ...liveOverride } as AnyNode) : rawNode), [rawNode, liveOverride], ) @@ -251,10 +250,7 @@ export function NodeArrowHandles() { }, [node, def]) const shouldRender = - Boolean(node && descriptors?.length) && - !isFloorplanHovered && - mode !== 'delete' && - !movingNode + Boolean(node && descriptors?.length) && !isFloorplanHovered && mode !== 'delete' && !movingNode if (!shouldRender || !node || !descriptors) return null return @@ -436,7 +432,10 @@ function pickCursor(descriptor: LinearResizeHandle | RadialResizeHandle } function resolveBound( - bound: number | ((node: AnyNode, sceneApi: ReturnType) => number) | undefined, + bound: + | number + | ((node: AnyNode, sceneApi: ReturnType) => number) + | undefined, fallback: number, node: AnyNode, sceneApi: ReturnType, @@ -572,10 +571,7 @@ function LinearArrow({ ? intersectionLocal.y : intersectionLocal.z const delta = currentPointer - initialPointer - const next = Math.min( - maxBound, - Math.max(minBound, initialValue + delta * factor), - ) + const next = Math.min(maxBound, Math.max(minBound, initialValue + delta * factor)) // apply sees the node-at-drag-start so it can compute anchors from // pre-drag geometry (door-width re-centers on the opposite edge). const patch = descriptor.apply(initialNode as never, next, sceneApi) @@ -928,8 +924,7 @@ function TapActionArrow({ const position = descriptor.placement.position(node, placementSceneApi) const rotationY = descriptor.placement.rotationY?.(node, placementSceneApi) ?? 0 const shape = descriptor.shape ?? 'arrow' - const cursor: Cursor = - descriptor.cursor ?? (shape === 'corner-picker' ? 'move' : 'ew-resize') + const cursor: Cursor = descriptor.cursor ?? (shape === 'corner-picker' ? 'move' : 'ew-resize') const onActivate = (event: ThreeEvent) => { event.stopPropagation() diff --git a/packages/editor/src/components/editor/wall-move-side-handles.tsx b/packages/editor/src/components/editor/wall-move-side-handles.tsx index 473063b8a..5630badbd 100644 --- a/packages/editor/src/components/editor/wall-move-side-handles.tsx +++ b/packages/editor/src/components/editor/wall-move-side-handles.tsx @@ -658,7 +658,6 @@ function FenceMoveArrowHandle({ fence, handle }: { fence: FenceNode; handle: Wal frustumCulled={false} geometry={arrowGeometry} material={arrowMaterial} - onPointerDown={activateFenceMove} onPointerEnter={(event) => { event.stopPropagation() diff --git a/packages/editor/src/components/tools/shared/polygon-editor.tsx b/packages/editor/src/components/tools/shared/polygon-editor.tsx index fba9917a9..edf8e71da 100644 --- a/packages/editor/src/components/tools/shared/polygon-editor.tsx +++ b/packages/editor/src/components/tools/shared/polygon-editor.tsx @@ -514,9 +514,7 @@ export const PolygonEditor: React.FC = ({ position={[x!, editY + height / 2, z!]} > - + ) })} diff --git a/packages/editor/src/components/ui/action-menu/control-modes.tsx b/packages/editor/src/components/ui/action-menu/control-modes.tsx index 691f286d2..e81344624 100644 --- a/packages/editor/src/components/ui/action-menu/control-modes.tsx +++ b/packages/editor/src/components/ui/action-menu/control-modes.tsx @@ -9,15 +9,7 @@ import { cn } from './../../../lib/utils' import useEditor from './../../../store/use-editor' import { ActionButton } from './action-button' -type ControlId = - | 'select' - | 'box-select' - | 'site-edit' - | 'build' - | 'material-paint' - | 'furnish' - | 'zone' - | 'delete' +type ControlId = 'select' | 'box-select' | 'site-edit' | 'zone' | 'delete' type ControlConfig = { id: ControlId @@ -54,30 +46,6 @@ const controls: ControlConfig[] = [ color: 'hover:bg-white/5', activeColor: 'bg-white/10 hover:bg-white/10', }, - { - id: 'build', - imageSrc: '/icons/build.png', - label: 'Build', - shortcut: 'B', - color: 'hover:bg-green-500/20 hover:text-green-400', - activeColor: 'bg-green-500/20 text-green-400', - }, - { - id: 'material-paint', - imageSrc: '/icons/paint.png', - label: 'Material Paint', - shortcut: 'P', - color: 'hover:bg-amber-500/20 hover:text-amber-400', - activeColor: 'bg-amber-500/20 text-amber-400', - }, - { - id: 'furnish', - imageSrc: '/icons/couch.png', - label: 'Furnish', - shortcut: 'F', - color: 'hover:bg-green-500/20 hover:text-green-400', - activeColor: 'bg-green-500/20 text-green-400', - }, { id: 'zone', imageSrc: '/icons/zone.png', @@ -104,9 +72,6 @@ export function ControlModes() { const setPhase = useEditor((state) => state.setPhase) const setStructureLayer = useEditor((state) => state.setStructureLayer) const setSelectionTool = useEditor((state) => state.setFloorplanSelectionTool) - const primeMaterialPaintFromSelection = useEditor( - (state) => state.primeMaterialPaintFromSelection, - ) const levelId = useViewer((s) => s.selection.levelId) // Only subscribe to the primitive `level` number — when walls are added to @@ -129,10 +94,6 @@ export function ControlModes() { if (id === 'select') return mode === 'select' && selectionTool === 'click' if (id === 'box-select') return mode === 'select' && selectionTool === 'marquee' if (id === 'site-edit') return false - if (id === 'build') - return mode === 'build' && phase === 'structure' && structureLayer === 'elements' - if (id === 'material-paint') return mode === 'material-paint' - if (id === 'furnish') return mode === 'build' && phase === 'furnish' if (id === 'zone') return mode === 'build' && phase === 'structure' && structureLayer === 'zones' return mode === id @@ -168,33 +129,6 @@ export function ControlModes() { } else if (id === 'box-select') { setMode('select') setSelectionTool('marquee') - } else if (id === 'build') { - // Toggle: if already in structure build, go back to select - if (getIsActive('build')) { - setMode('select') - } else { - setPhase('structure') - setStructureLayer('elements') - setMode('build') - } - } else if (id === 'material-paint') { - if (getIsActive('material-paint')) { - setMode('select') - } else { - primeMaterialPaintFromSelection() - setPhase('structure') - setStructureLayer('elements') - setMode('material-paint') - } - } else if (id === 'furnish') { - if (getIsActive('furnish')) { - setMode('select') - } else { - setPhase('furnish') - setMode('build') - // Auto-switch sidebar to the items panel so the user can pick furniture - useEditor.getState().setActiveSidebarPanel('items') - } } else if (id === 'zone') { if (getIsActive('zone')) { setMode('select') diff --git a/packages/editor/src/components/ui/action-menu/index.tsx b/packages/editor/src/components/ui/action-menu/index.tsx index 8a10b8dea..bf1ce152a 100644 --- a/packages/editor/src/components/ui/action-menu/index.tsx +++ b/packages/editor/src/components/ui/action-menu/index.tsx @@ -1,19 +1,14 @@ 'use client' -import { useScene } from '@pascal-app/core' import { useViewer } from '@pascal-app/viewer' -import { AnimatePresence, motion } from 'motion/react' -import { useEffect, useMemo } from 'react' -import { MaterialPicker } from './../../../components/ui/controls/material-picker' +import { motion } from 'motion/react' import { TooltipProvider } from './../../../components/ui/primitives/tooltip' import { useIsMobile } from './../../../hooks/use-mobile' import { useReducedMotion } from './../../../hooks/use-reduced-motion' -import { resolvePaintTargetFromSelection } from './../../../lib/material-paint' import { cn } from './../../../lib/utils' import useEditor from './../../../store/use-editor' import { CameraActions } from './camera-actions' import { ControlModes } from './control-modes' -import { StructureTools } from './structure-tools' import { GridSnapControl, SecondaryToggles } from './view-toggles' // Mobile bottom offset matches the viewer's overlap behind the sheet's @@ -21,47 +16,7 @@ import { GridSnapControl, SecondaryToggles } from './view-toggles' // just above that strip instead of inside it. const MOBILE_BOTTOM_OFFSET = 24 -function PaintMaterialTray() { - const activePaintMaterial = useEditor((state) => state.activePaintMaterial) - const activePaintTarget = useEditor((state) => state.activePaintTarget) - const setActivePaintMaterial = useEditor((state) => state.setActivePaintMaterial) - const setActivePaintTarget = useEditor((state) => state.setActivePaintTarget) - const selectedIds = useViewer((state) => state.selection.selectedIds) - const nodes = useScene((state) => state.nodes) - const selectedId = selectedIds.length === 1 ? (selectedIds[0] ?? null) : null - - useEffect(() => { - const selectedPaintTarget = resolvePaintTargetFromSelection({ - nodes, - selectedId, - }) - - if (selectedPaintTarget) { - setActivePaintTarget(selectedPaintTarget) - } - }, [nodes, selectedId, setActivePaintTarget]) - - return ( -
- { - setActivePaintMaterial({ material, sourceTarget: activePaintTarget }) - }} - onSelectMaterialPreset={(materialPreset) => { - setActivePaintMaterial({ materialPreset, sourceTarget: activePaintTarget }) - }} - selectedMaterialPreset={activePaintMaterial?.materialPreset} - value={activePaintMaterial?.material} - /> -
- ) -} - export function ActionMenu({ className }: { className?: string }) { - const phase = useEditor((state) => state.phase) - const mode = useEditor((state) => state.mode) - const tool = useEditor((state) => state.tool) - const catalogCategory = useEditor((state) => state.catalogCategory) const isMobile = useIsMobile() const hasSelectionOnMobile = useViewer((s) => isMobile && s.selection.selectedIds.length > 0) const hasReferenceOnMobile = useEditor((s) => isMobile && Boolean(s.selectedReferenceId)) @@ -70,7 +25,6 @@ export function ActionMenu({ className }: { className?: string }) { (s) => isMobile && CONTEXTUAL_TABS.has(s.activeSidebarPanel), ) const reducedMotion = useReducedMotion() - const showPaintTray = useMemo(() => mode === 'material-paint', [mode]) // On mobile, defer the bottom rail to the selection bar when something // is selected — the contextual actions take priority over mode controls. @@ -97,72 +51,6 @@ export function ActionMenu({ className }: { className?: string }) { style={isMobile ? { bottom: MOBILE_BOTTOM_OFFSET } : undefined} transition={transition} > - {/* Structure Tools Row - Animated */} - - {phase === 'structure' && mode === 'build' && ( - -
- -
-
- )} -
- - - {showPaintTray && ( - - - - )} - {isMobile ? (
{/* Row 1: control modes only */} diff --git a/packages/editor/src/components/ui/action-menu/structure-tools.tsx b/packages/editor/src/components/ui/action-menu/structure-tools.tsx index 081f4a96f..76e7d6eff 100644 --- a/packages/editor/src/components/ui/action-menu/structure-tools.tsx +++ b/packages/editor/src/components/ui/action-menu/structure-tools.tsx @@ -1,15 +1,4 @@ -'use client' - -import NextImage from 'next/image' -import { useContextualTools } from '../../../hooks/use-contextual-tools' - -import { cn } from '../../../lib/utils' -import useEditor, { - type CatalogCategory, - type StructureTool, - type Tool, -} from '../../../store/use-editor' -import { ActionButton } from './action-button' +import type { CatalogCategory, StructureTool } from '../../../store/use-editor' export type ToolConfig = { id: StructureTool @@ -18,6 +7,10 @@ export type ToolConfig = { catalogCategory?: CatalogCategory } +// Shared structure-tool metadata (icons + labels). The build palette now lives +// in the community Build sidebar; this list survives only as the lookup table +// for cursor/floorplan indicators. Roof-mounted accessories are intentionally +// absent — they're placed from the roof inspector's "Add element" section. export const tools: ToolConfig[] = [ { id: 'wall', iconSrc: '/icons/wall.png', label: 'Wall' }, { id: 'door', iconSrc: '/icons/door.png', label: 'Door' }, @@ -27,85 +20,9 @@ export const tools: ToolConfig[] = [ { id: 'fence', iconSrc: '/icons/fence.png', label: 'Fence' }, { id: 'column', iconSrc: '/icons/column.png', label: 'Column' }, { id: 'elevator', iconSrc: '/icons/elevator.png', label: 'Elevator' }, - // { id: 'room', iconSrc: '/icons/room.png', label: 'Room' }, - // { id: 'custom-room', iconSrc: '/icons/custom-room.png', label: 'Custom Room' }, { id: 'slab', iconSrc: '/icons/floor.png', label: 'Slab' }, { id: 'ceiling', iconSrc: '/icons/ceiling.png', label: 'Ceiling' }, { id: 'zone', iconSrc: '/icons/zone.png', label: 'Zone' }, { id: 'spawn', iconSrc: '/icons/site.png', label: 'Spawn Point' }, { id: 'shelf', iconSrc: '/icons/shelf.png', label: 'Shelf' }, - // Roof-mounted accessories (box-vent / ridge-vent / chimney / - // solar-panel / skylight / dormer) are intentionally NOT in the top - // palette — they only make sense in the context of a selected roof. - // The roof inspector's "Add element" section is the entry point - // (`packages/nodes/src/roof/panel.tsx`), which activates the same - // registry-driven placement tools via `setTool(kind)`. ] - -export function StructureTools() { - const activeTool = useEditor((state) => state.tool) - const catalogCategory = useEditor((state) => state.catalogCategory) - const structureLayer = useEditor((state) => state.structureLayer) - const setTool = useEditor((state) => state.setTool) - const setCatalogCategory = useEditor((state) => state.setCatalogCategory) - - const contextualTools = useContextualTools() - - // Filter tools based on structureLayer - const visibleTools = - structureLayer === 'zones' - ? tools.filter((t) => t.id === 'zone') - : tools.filter((t) => t.id !== 'zone') - - const hasActiveTool = visibleTools.some( - (t) => - activeTool === t.id && (t.catalogCategory ? catalogCategory === t.catalogCategory : true), - ) - - return ( -
- {visibleTools.map((tool, index) => { - // For item tools with catalog category, check both tool and category match - const isActive = - activeTool === tool.id && - (tool.catalogCategory ? catalogCategory === tool.catalogCategory : true) - - const isContextual = contextualTools.includes(tool.id) - - return ( - { - if (!isActive) { - setTool(tool.id) - setCatalogCategory(tool.catalogCategory ?? null) - - // Automatically switch to build mode if we select a tool - if (useEditor.getState().mode !== 'build') { - useEditor.getState().setMode('build') - } - } - }} - size="icon" - variant="ghost" - > - - - ) - })} -
- ) -} diff --git a/packages/editor/src/components/ui/controls/material-paint-panel.tsx b/packages/editor/src/components/ui/controls/material-paint-panel.tsx new file mode 100644 index 000000000..1507cc9c8 --- /dev/null +++ b/packages/editor/src/components/ui/controls/material-paint-panel.tsx @@ -0,0 +1,46 @@ +'use client' + +import { useScene } from '@pascal-app/core' +import { useViewer } from '@pascal-app/viewer' +import { useEffect } from 'react' +import { resolvePaintTargetFromSelection } from './../../../lib/material-paint' +import useEditor from './../../../store/use-editor' +import { MaterialPicker } from './material-picker' + +/** + * Material picker for paint mode. Embedders render this wherever paint controls + * belong (the community editor places it in the Build sidebar while paint mode + * is active). It owns the paint-target/material wiring so the host only needs + * to mount it; it fills its container's width. + */ +export function MaterialPaintPanel() { + const activePaintMaterial = useEditor((state) => state.activePaintMaterial) + const activePaintTarget = useEditor((state) => state.activePaintTarget) + const setActivePaintMaterial = useEditor((state) => state.setActivePaintMaterial) + const setActivePaintTarget = useEditor((state) => state.setActivePaintTarget) + const selectedIds = useViewer((state) => state.selection.selectedIds) + const nodes = useScene((state) => state.nodes) + const selectedId = selectedIds.length === 1 ? (selectedIds[0] ?? null) : null + + useEffect(() => { + const selectedPaintTarget = resolvePaintTargetFromSelection({ nodes, selectedId }) + if (selectedPaintTarget) { + setActivePaintTarget(selectedPaintTarget) + } + }, [nodes, selectedId, setActivePaintTarget]) + + return ( +
+ { + setActivePaintMaterial({ material, sourceTarget: activePaintTarget }) + }} + onSelectMaterialPreset={(materialPreset) => { + setActivePaintMaterial({ materialPreset, sourceTarget: activePaintTarget }) + }} + selectedMaterialPreset={activePaintMaterial?.materialPreset} + value={activePaintMaterial?.material} + /> +
+ ) +} diff --git a/packages/editor/src/components/ui/controls/material-picker.tsx b/packages/editor/src/components/ui/controls/material-picker.tsx index b9918fb24..da35979ec 100644 --- a/packages/editor/src/components/ui/controls/material-picker.tsx +++ b/packages/editor/src/components/ui/controls/material-picker.tsx @@ -39,7 +39,6 @@ export function MaterialPicker({ const [selectedCategory, setSelectedCategory] = useState<(typeof MATERIAL_CATEGORIES)[number]>( MATERIAL_CATEGORIES[0], ) - const catalogScrollRef = useRef(null) const categoryScrollRef = useRef(null) const catalogItems = selectedCategory === 'other' @@ -73,27 +72,6 @@ export function MaterialPicker({ onSelectMaterialPreset?.(toLibraryMaterialRef(materialId)) } - useEffect(() => { - const container = catalogScrollRef.current - if (!container) return - - const handleWheel = (event: WheelEvent) => { - const deltaX = event.deltaX - const deltaY = event.deltaY - const nextScrollLeft = container.scrollLeft + deltaX + deltaY - - if (nextScrollLeft === container.scrollLeft) return - - event.preventDefault() - container.scrollLeft = nextScrollLeft - } - - container.addEventListener('wheel', handleWheel, { passive: false }) - return () => { - container.removeEventListener('wheel', handleWheel) - } - }, [catalogItems.length, onChange, showCustom]) - useEffect(() => { const container = categoryScrollRef.current if (!container) return @@ -167,14 +145,12 @@ export function MaterialPicker({
-
- {catalogItems.map((item) => ( + {catalogItems.map((item) => (
)} diff --git a/packages/editor/src/components/ui/panels/panel-wrapper.tsx b/packages/editor/src/components/ui/panels/panel-wrapper.tsx index d389f5aa2..c38c9f855 100644 --- a/packages/editor/src/components/ui/panels/panel-wrapper.tsx +++ b/packages/editor/src/components/ui/panels/panel-wrapper.tsx @@ -2,9 +2,19 @@ import { ChevronLeft, RotateCcw, X } from 'lucide-react' import Image from 'next/image' +import { createContext, useContext } from 'react' import { useIsMobile } from '../../../hooks/use-mobile' import { cn } from '../../../lib/utils' +/** + * Host-supplied inspector footer (e.g. community's "Save as preset"). The + * `PanelManager` provides it so every panel — including kind-owned + * `customPanel`s that render their own `` without threading a + * `footer` prop — picks it up without per-kind wiring. An explicit `footer` + * prop still wins over the context. + */ +export const InspectorFooterContext = createContext(null) + interface PanelWrapperProps { title: string /** Either a URL path (legacy panels pass `/icons/floor.png` etc., @@ -34,6 +44,8 @@ export function PanelWrapper({ width = 320, // default width }: PanelWrapperProps) { const isMobile = useIsMobile() + const contextFooter = useContext(InspectorFooterContext) + const resolvedFooter = footer ?? contextFooter return (
{children}
- {footer &&
{footer}
} + {resolvedFooter && ( +
{resolvedFooter}
+ )} ) } diff --git a/packages/editor/src/components/ui/panels/parametric-inspector.tsx b/packages/editor/src/components/ui/panels/parametric-inspector.tsx index 5f4a38fc2..783d45c92 100644 --- a/packages/editor/src/components/ui/panels/parametric-inspector.tsx +++ b/packages/editor/src/components/ui/panels/parametric-inspector.tsx @@ -19,7 +19,7 @@ import { PanelSection } from '../controls/panel-section' import { SegmentedControl } from '../controls/segmented-control' import { SliderControl } from '../controls/slider-control' import { ToggleControl } from '../controls/toggle-control' -import { PanelWrapper } from './panel-wrapper' +import { InspectorFooterContext, PanelWrapper } from './panel-wrapper' /** * Auto-derived right-panel inspector for any registry-backed node. @@ -87,10 +87,14 @@ export function ParametricInspector({ footer }: { footer?: React.ReactNode } = { // panel to cover them. if (parametrics.customPanel) { const CustomPanel = resolveCustomPanel(parametrics.customPanel) + // Custom panels render their own `` and don't thread a + // `footer` prop, so hand the host footer down via context. return ( - - - + + + + + ) } diff --git a/packages/editor/src/components/ui/sidebar/tab-bar.tsx b/packages/editor/src/components/ui/sidebar/tab-bar.tsx index 32807eb55..3e3f5b05c 100644 --- a/packages/editor/src/components/ui/sidebar/tab-bar.tsx +++ b/packages/editor/src/components/ui/sidebar/tab-bar.tsx @@ -2,6 +2,7 @@ import type { ReactNode } from 'react' import { cn } from './../../../lib/utils' +import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '../primitives/tooltip' export type SidebarTab = { id: string @@ -56,34 +57,37 @@ interface IconRailProps { /** * Vertical icon rail for the v2 left column. Always visible (even when the * panel is collapsed) so the user can reopen the panel by clicking an icon. - * The label renders as a hover tooltip via the native `title`. + * The label renders as a hover tooltip on the right. */ export function IconRail({ tabs, activeTab, collapsed, onIconClick }: IconRailProps) { return ( -
- {tabs.map((tab) => { - // While expanded, the active tab is filled. While collapsed, nothing - // is "open", so the active tab reads as a muted highlight instead. - const isActive = activeTab === tab.id - return ( - - ) - })} -
+ +
+ {tabs.map((tab) => { + // Only show the active highlight while the panel is open. When + // collapsed nothing is "open", so every icon reads as unselected. + const showActive = activeTab === tab.id && !collapsed + return ( + + + + + {tab.label} + + ) + })} +
+
) } diff --git a/packages/editor/src/hooks/use-contextual-tools.ts b/packages/editor/src/hooks/use-contextual-tools.ts deleted file mode 100644 index e51121b19..000000000 --- a/packages/editor/src/hooks/use-contextual-tools.ts +++ /dev/null @@ -1,62 +0,0 @@ -import { type AnyNodeId, useScene } from '@pascal-app/core' -import { useViewer } from '@pascal-app/viewer' -import { useMemo } from 'react' -import { useShallow } from 'zustand/react/shallow' -import useEditor, { type StructureTool } from '../store/use-editor' - -export function useContextualTools() { - const selection = useViewer((s) => s.selection) - // Only resubscribe when the *types* of selected nodes change, not when any - // node in the scene mutates. - const selectedTypes = useScene( - useShallow((s) => - selection.selectedIds.map((id) => s.nodes[id as AnyNodeId]?.type).filter(Boolean), - ), - ) - const structureLayer = useEditor((s) => s.structureLayer) - - return useMemo(() => { - // If we are in the zones layer, only zone tool is relevant - if (structureLayer === 'zones') { - return ['zone'] as StructureTool[] - } - - // Default tools when nothing is selected - const defaultTools: StructureTool[] = [ - 'wall', - 'fence', - 'slab', - 'ceiling', - 'roof', - 'elevator', - 'door', - 'window', - ] - - if (selectedTypes.length === 0) { - return defaultTools - } - - // If a wall is selected, prioritize wall-hosted elements - if (selectedTypes.includes('wall')) { - return ['window', 'door', 'wall', 'fence'] as StructureTool[] - } - - // If a slab is selected, prioritize slab editing - if (selectedTypes.includes('slab')) { - return ['slab', 'wall'] as StructureTool[] - } - - // If a ceiling is selected, prioritize ceiling editing - if (selectedTypes.includes('ceiling')) { - return ['ceiling'] as StructureTool[] - } - - // If a roof is selected, prioritize roof editing - if (selectedTypes.includes('roof')) { - return ['roof'] as StructureTool[] - } - - return defaultTools - }, [selectedTypes, structureLayer]) -} diff --git a/packages/editor/src/index.tsx b/packages/editor/src/index.tsx index 6d7e442ad..c5718baf1 100644 --- a/packages/editor/src/index.tsx +++ b/packages/editor/src/index.tsx @@ -116,6 +116,7 @@ export { } from './components/ui/action-menu/view-toggles' export { useCommandPalette } from './components/ui/command-palette' export { ActionButton, ActionGroup } from './components/ui/controls/action-button' +export { MaterialPaintPanel } from './components/ui/controls/material-paint-panel' export { MaterialPicker } from './components/ui/controls/material-picker' export { MetricControl } from './components/ui/controls/metric-control' export { PanelSection } from './components/ui/controls/panel-section' diff --git a/packages/nodes/src/ceiling/boundary-editor.tsx b/packages/nodes/src/ceiling/boundary-editor.tsx index 88ac26524..eb174e50d 100644 --- a/packages/nodes/src/ceiling/boundary-editor.tsx +++ b/packages/nodes/src/ceiling/boundary-editor.tsx @@ -1,11 +1,6 @@ 'use client' -import { - type CeilingNode, - resolveLevelId, - useLiveNodeOverrides, - useScene, -} from '@pascal-app/core' +import { type CeilingNode, resolveLevelId, useLiveNodeOverrides, useScene } from '@pascal-app/core' import { PolygonEditor } from '@pascal-app/editor' import { useViewer } from '@pascal-app/viewer' import { useCallback, useEffect } from 'react' diff --git a/packages/nodes/src/ceiling/hole-editor.tsx b/packages/nodes/src/ceiling/hole-editor.tsx index 0ddcfd823..1b0ac28ca 100644 --- a/packages/nodes/src/ceiling/hole-editor.tsx +++ b/packages/nodes/src/ceiling/hole-editor.tsx @@ -1,11 +1,6 @@ 'use client' -import { - type CeilingNode, - resolveLevelId, - useLiveNodeOverrides, - useScene, -} from '@pascal-app/core' +import { type CeilingNode, resolveLevelId, useLiveNodeOverrides, useScene } from '@pascal-app/core' import { PolygonEditor } from '@pascal-app/editor' import { useViewer } from '@pascal-app/viewer' import { useCallback, useEffect } from 'react' diff --git a/packages/nodes/src/column/definition.ts b/packages/nodes/src/column/definition.ts index 485f7c1bb..09a4ad10d 100644 --- a/packages/nodes/src/column/definition.ts +++ b/packages/nodes/src/column/definition.ts @@ -154,20 +154,14 @@ function columnBraceHandle(axis: 'x' | 'z'): HandleDescriptor { axis, anchor: 'center', min: MIN_BRACE_DIMENSION, - currentValue: (n) => - axis === 'x' ? (n.braceWidth ?? n.width) : (n.braceDepth ?? n.depth), - apply: (_n, newValue) => - axis === 'x' ? { braceWidth: newValue } : { braceDepth: newValue }, + currentValue: (n) => (axis === 'x' ? (n.braceWidth ?? n.width) : (n.braceDepth ?? n.depth)), + apply: (_n, newValue) => (axis === 'x' ? { braceWidth: newValue } : { braceDepth: newValue }), placement: { position: (n) => { // Position outside any splay so the arrow clears the legs. const half = axis === 'x' - ? Math.max( - n.braceBottomSpread ?? 0, - n.braceTopSpread ?? 0, - n.braceWidth ?? n.width, - ) / 2 + ? Math.max(n.braceBottomSpread ?? 0, n.braceTopSpread ?? 0, n.braceWidth ?? n.width) / 2 : (n.braceDepth ?? n.depth) / 2 return axis === 'x' ? [half + BRACE_HANDLE_OFFSET, n.height / 2, 0] @@ -205,12 +199,7 @@ function columnFootprintHalf(n: ColumnNodeType): { halfX: number; halfZ: number } return { halfX: - Math.max( - n.width, - n.braceWidth ?? 0, - n.braceBottomSpread ?? 0, - n.braceTopSpread ?? 0, - ) / 2, + Math.max(n.width, n.braceWidth ?? 0, n.braceBottomSpread ?? 0, n.braceTopSpread ?? 0) / 2, halfZ: Math.max(n.depth, n.braceDepth ?? 0) / 2, } } diff --git a/packages/nodes/src/column/floorplan-affordances.ts b/packages/nodes/src/column/floorplan-affordances.ts index d74d55d63..ec95b4e21 100644 --- a/packages/nodes/src/column/floorplan-affordances.ts +++ b/packages/nodes/src/column/floorplan-affordances.ts @@ -53,8 +53,7 @@ export const columnResizeAffordance: FloorplanAffordance = { const initialRadius = node.radius const initialBraceWidth = node.braceWidth ?? node.width const initialBraceDepth = node.braceDepth ?? node.depth - const initialBraceBottomSpread = - node.braceBottomSpread ?? Math.max(node.width * 3, 1.2) + const initialBraceBottomSpread = node.braceBottomSpread ?? Math.max(node.width * 3, 1.2) const initialBraceTopSpread = node.braceTopSpread ?? 0.12 let lastPatch: Partial = {} @@ -110,10 +109,7 @@ export const columnResizeAffordance: FloorplanAffordance = { return case 'brace-top-spread': commitPatch({ - braceTopSpread: Math.max( - MIN_BRACE_TOP_SPREAD, - initialBraceTopSpread + 2 * projDelta, - ), + braceTopSpread: Math.max(MIN_BRACE_TOP_SPREAD, initialBraceTopSpread + 2 * projDelta), }) return } diff --git a/packages/nodes/src/column/floorplan.ts b/packages/nodes/src/column/floorplan.ts index 28fd5268d..ea6c44225 100644 --- a/packages/nodes/src/column/floorplan.ts +++ b/packages/nodes/src/column/floorplan.ts @@ -102,8 +102,7 @@ export function buildColumnFloorplan( // outward tip in plan coords. Captured at emit-time so the // affordance doesn't need to recompute `column.rotation` (and // a mid-drag rotation can't drift the projection basis). - const outwardLocal: [number, number] = - localDirection === 'x' ? [1, 0] : [0, 1] + const outwardLocal: [number, number] = localDirection === 'x' ? [1, 0] : [0, 1] const [planAxisX, planAxisY] = rotatePlanVector(outwardLocal[0], outwardLocal[1], rot) children.push({ kind: 'move-arrow', diff --git a/packages/nodes/src/column/panel.tsx b/packages/nodes/src/column/panel.tsx index fad37c85b..c181b5ad3 100644 --- a/packages/nodes/src/column/panel.tsx +++ b/packages/nodes/src/column/panel.tsx @@ -355,9 +355,7 @@ export default function ColumnPanel() { // so the preset's braceWidth / braceDepth win over // the carried-from-previous-style values. const stylePreset = - option.value === 'vertical' - ? {} - : SUPPORT_STYLE_DEFAULTS[option.value] + option.value === 'vertical' ? {} : SUPPORT_STYLE_DEFAULTS[option.value] handleUpdate({ supportStyle: option.value, ...(option.value !== 'vertical' diff --git a/packages/nodes/src/elevator/definition.ts b/packages/nodes/src/elevator/definition.ts index cf101303f..a840d0164 100644 --- a/packages/nodes/src/elevator/definition.ts +++ b/packages/nodes/src/elevator/definition.ts @@ -1,6 +1,6 @@ import { - type ElevatorNode as ElevatorNodeType, ElevatorNode as ElevatorNodeSchema, + type ElevatorNode as ElevatorNodeType, getElevatorCabDepth, getElevatorCabWidth, getElevatorShaftDepth, @@ -10,11 +10,8 @@ import { type NodeDefinition, resolveElevatorLevels, } from '@pascal-app/core' -import { - elevatorResizeAffordance, - elevatorRotateAffordance, -} from './floorplan-affordances' import { buildElevatorFloorplan } from './floorplan' +import { elevatorResizeAffordance, elevatorRotateAffordance } from './floorplan-affordances' import { elevatorParametrics } from './parametrics' import { ElevatorNode } from './schema' diff --git a/packages/nodes/src/elevator/floorplan.ts b/packages/nodes/src/elevator/floorplan.ts index 70c519f53..1a026d2a9 100644 --- a/packages/nodes/src/elevator/floorplan.ts +++ b/packages/nodes/src/elevator/floorplan.ts @@ -326,7 +326,12 @@ export function buildElevatorFloorplan( { local: [outerHalfW + sideArrowOffset, 0], localAngle: 0, axis: 'x', side: 1 }, { local: [-(outerHalfW + sideArrowOffset), 0], localAngle: Math.PI, axis: 'x', side: -1 }, { local: [0, outerHalfD + sideArrowOffset], localAngle: Math.PI / 2, axis: 'z', side: 1 }, - { local: [0, -(outerHalfD + sideArrowOffset)], localAngle: -Math.PI / 2, axis: 'z', side: -1 }, + { + local: [0, -(outerHalfD + sideArrowOffset)], + localAngle: -Math.PI / 2, + axis: 'z', + side: -1, + }, ] for (const side of sides) { const [ox, oz] = rotate(side.local[0], side.local[1]) diff --git a/packages/nodes/src/roof-segment/definition.ts b/packages/nodes/src/roof-segment/definition.ts index 397ccd822..673f9cd49 100644 --- a/packages/nodes/src/roof-segment/definition.ts +++ b/packages/nodes/src/roof-segment/definition.ts @@ -3,15 +3,15 @@ import { getPitchFromActiveRoofHeight, type HandleDescriptor, type NodeDefinition, - type RoofSegmentNode as RoofSegmentNodeType, RoofSegmentNode as RoofSegmentNodeSchema, + type RoofSegmentNode as RoofSegmentNodeType, } from '@pascal-app/core' +import { buildRoofSegmentFloorplan } from './floorplan' import { roofSegmentMoveTarget, roofSegmentResizeAffordance, roofSegmentRotateAffordance, } from './floorplan-affordances' -import { buildRoofSegmentFloorplan } from './floorplan' import { roofSegmentParametrics } from './parametrics' import { RoofSegmentNode } from './schema' diff --git a/packages/nodes/src/roof-segment/floorplan-affordances.ts b/packages/nodes/src/roof-segment/floorplan-affordances.ts index 498288da2..cbf7ab86a 100644 --- a/packages/nodes/src/roof-segment/floorplan-affordances.ts +++ b/packages/nodes/src/roof-segment/floorplan-affordances.ts @@ -154,11 +154,7 @@ export const roofSegmentMoveTarget: FloorplanMoveTarget = ({ no // is `[cosRoof, sinRoof; -sinRoof, cosRoof]`. Used to project world cursor // back into roof-local coords. void roofRot - let lastLocal: [number, number, number] = [ - node.position[0], - node.position[1], - node.position[2], - ] + let lastLocal: [number, number, number] = [node.position[0], node.position[1], node.position[2]] return { affectedIds: [segmentId], diff --git a/packages/nodes/src/shelf/definition.ts b/packages/nodes/src/shelf/definition.ts index b5a6cdcb8..0bc2e4699 100644 --- a/packages/nodes/src/shelf/definition.ts +++ b/packages/nodes/src/shelf/definition.ts @@ -1,10 +1,6 @@ -import type { - HandleDescriptor, - NodeDefinition, - ShelfNode as ShelfNodeType, -} from '@pascal-app/core' -import { shelfResizeAffordance, shelfRotateAffordance } from './floorplan-affordances' +import type { HandleDescriptor, NodeDefinition, ShelfNode as ShelfNodeType } from '@pascal-app/core' import { buildShelfFloorplan } from './floorplan' +import { shelfResizeAffordance, shelfRotateAffordance } from './floorplan-affordances' import { shelfFloorplanMoveTarget } from './floorplan-move' import { buildShelfGeometry, shelfRowSurfaceYs } from './geometry' import { shelfParametrics } from './parametrics' diff --git a/packages/nodes/src/shelf/floorplan.ts b/packages/nodes/src/shelf/floorplan.ts index 6ed96ce06..4578dbeff 100644 --- a/packages/nodes/src/shelf/floorplan.ts +++ b/packages/nodes/src/shelf/floorplan.ts @@ -24,10 +24,7 @@ const ROTATE_ARROW_CORNER_OFFSET = 0.22 * corner. Body move continues to flow through `shelfFloorplanMoveTarget` * (engaged from the action-menu Move button, not from these arrows). */ -export function buildShelfFloorplan( - node: ShelfNode, - ctx?: GeometryContext, -): FloorplanGeometry { +export function buildShelfFloorplan(node: ShelfNode, ctx?: GeometryContext): FloorplanGeometry { const [px, , pz] = node.position const ry = node.rotation[1] ?? 0 // Floor-plan plots at `-ry` so SVG's CW-with-y-down `rotate` direction diff --git a/packages/nodes/src/stair/definition.ts b/packages/nodes/src/stair/definition.ts index 6f53cbb37..f5db51432 100644 --- a/packages/nodes/src/stair/definition.ts +++ b/packages/nodes/src/stair/definition.ts @@ -1,8 +1,8 @@ import { type HandleDescriptor, type NodeDefinition, - type StairNode as StairNodeType, StairNode as StairNodeSchema, + type StairNode as StairNodeType, } from '@pascal-app/core' const MIN_CURVED_RISE = 0.3 @@ -223,11 +223,7 @@ function stairRotateGizmoPosition(n: StairNodeType): [number, number, number] { } const width = Math.max(n.width ?? 1, MIN_CURVED_WIDTH) const yMid = Math.max(n.totalRise ?? 2.5, 0.1) / 2 - return [ - width / 2 + STAIR_ROTATE_CORNER_OFFSET, - yMid, - -STAIR_ROTATE_CORNER_OFFSET, - ] + return [width / 2 + STAIR_ROTATE_CORNER_OFFSET, yMid, -STAIR_ROTATE_CORNER_OFFSET] } function stairRotateHandle(): HandleDescriptor { @@ -288,6 +284,8 @@ function stairHandles(node: StairNodeType): HandleDescriptor[] { handles.push(stairRotateHandle()) return handles } + +import { buildStairFloorplan } from './floorplan' import { curvedStairInnerRadiusAffordance, curvedStairSweepAffordance, @@ -296,7 +294,6 @@ import { segmentWidthAffordance, stairRotateAffordance, } from './floorplan-affordances' -import { buildStairFloorplan } from './floorplan' import { stairFloorplanMoveTarget } from './floorplan-move' import { stairParametrics } from './parametrics' import { StairNode } from './schema' diff --git a/packages/nodes/src/stair/floorplan-affordances.ts b/packages/nodes/src/stair/floorplan-affordances.ts index ed60329ce..2cc70e240 100644 --- a/packages/nodes/src/stair/floorplan-affordances.ts +++ b/packages/nodes/src/stair/floorplan-affordances.ts @@ -146,10 +146,7 @@ export const curvedStairWidthAffordance: FloorplanAffordance = { affectedIds: [stairId], apply({ planPoint }) { const currentRadial = (planPoint[0] - cx) * radialX + (planPoint[1] - cz) * radialZ - const newWidth = Math.max( - MIN_CURVED_WIDTH, - initialWidth + (currentRadial - initialRadial), - ) + const newWidth = Math.max(MIN_CURVED_WIDTH, initialWidth + (currentRadial - initialRadial)) lastWidth = newWidth useScene.getState().updateNode(stairId, { width: newWidth }) }, @@ -172,7 +169,9 @@ export const curvedStairInnerRadiusAffordance: FloorplanAffordance = start({ node, initialPlanPoint }) { const stairId = node.id as AnyNodeId const isSpiral = node.stairType === 'spiral' - const minInnerRadius = isSpiral ? MIN_CURVED_INNER_RADIUS_SPIRAL : MIN_CURVED_INNER_RADIUS_CURVED + const minInnerRadius = isSpiral + ? MIN_CURVED_INNER_RADIUS_SPIRAL + : MIN_CURVED_INNER_RADIUS_CURVED const initialInnerRadius = Math.max(minInnerRadius, node.innerRadius ?? 0.9) const initialWidth = Math.max(node.width ?? 1, MIN_CURVED_WIDTH) const initialOuterRadius = initialInnerRadius + initialWidth @@ -303,9 +302,7 @@ export const curvedStairSweepAffordance: FloorplanAffordance = { return true }, commit() { - useScene - .getState() - .updateNode(stairId, { sweepAngle: lastSweep, rotation: lastRotation }) + useScene.getState().updateNode(stairId, { sweepAngle: lastSweep, rotation: lastRotation }) }, } }, diff --git a/packages/nodes/src/stair/floorplan.ts b/packages/nodes/src/stair/floorplan.ts index 75a751f46..ead2f758e 100644 --- a/packages/nodes/src/stair/floorplan.ts +++ b/packages/nodes/src/stair/floorplan.ts @@ -11,6 +11,7 @@ import type { // `definition.ts` so the 2D handle visually lines up with where the 3D // curved-arrow gizmo would sit at the matching world point. const STAIR_ROTATE_PLAN_OFFSET = 0.4 + import { buildFloorplanStairEntry, buildSvgAnnularSectorPath, @@ -134,14 +135,8 @@ export function buildStairFloorplan( // Segment-local +X (width axis) and +Z (run axis) in plan coords, // captured here so the affordance handler can project pointer // deltas without re-walking the stair chain. - const axisX: readonly [number, number] = [ - (c1.x - c0.x) / width, - (c1.y - c0.y) / width, - ] - const axisZ: readonly [number, number] = [ - (c3.x - c0.x) / length, - (c3.y - c0.y) / length, - ] + const axisX: readonly [number, number] = [(c1.x - c0.x) / width, (c1.y - c0.y) / width] + const axisZ: readonly [number, number] = [(c3.x - c0.x) / length, (c3.y - c0.y) / length] const rightMid: [number, number] = [(c1.x + c2.x) / 2, (c1.y + c2.y) / 2] const leftMid: [number, number] = [(c0.x + c3.x) / 2, (c0.y + c3.y) / 2] const frontEdgeMid: [number, number] = [(c2.x + c3.x) / 2, (c2.y + c3.y) / 2] @@ -277,13 +272,7 @@ export function buildStairFloorplan( // steps past `dashedFromIndex` are dashed. const isEmphasised = stairType === 'spiral' ? isLast : isFirst || isLast const stepWidth = - stairType === 'spiral' - ? isEmphasised - ? 1.8 - : 1.15 - : isEmphasised - ? 1.5 - : 1.1 + stairType === 'spiral' ? (isEmphasised ? 1.8 : 1.15) : isEmphasised ? 1.5 : 1.1 children.push({ kind: 'line', x1: inner.x, @@ -455,10 +444,7 @@ export function buildStairFloorplan( localZ = -STAIR_ROTATE_PLAN_OFFSET } else { const isSpiral = stairType === 'spiral' - const innerR = Math.max( - isSpiral ? 0.05 : 0.2, - stair.innerRadius ?? (isSpiral ? 0.2 : 0.9), - ) + const innerR = Math.max(isSpiral ? 0.05 : 0.2, stair.innerRadius ?? (isSpiral ? 0.2 : 0.9)) const outerR = innerR + (stair.width ?? 1) const sweep = stair.sweepAngle ?? (isSpiral ? Math.PI * 2 : Math.PI / 2) const radius = outerR + STAIR_ROTATE_PLAN_OFFSET diff --git a/packages/nodes/src/stair/renderer.tsx b/packages/nodes/src/stair/renderer.tsx index 9457c6206..136781c20 100644 --- a/packages/nodes/src/stair/renderer.tsx +++ b/packages/nodes/src/stair/renderer.tsx @@ -651,10 +651,7 @@ function SpiralStepSupportMesh({ const sizeX = Math.max(0.04, innerRadius - spiralColumnRadius + 0.04) const sizeY = Math.max(thickness * 0.55, 0.025) const sizeZ = Math.max(0.04, Math.min(0.12, sizeY * 1.5)) - const geometry = useMemo( - () => new THREE.BoxGeometry(sizeX, sizeY, sizeZ), - [sizeX, sizeY, sizeZ], - ) + const geometry = useMemo(() => new THREE.BoxGeometry(sizeX, sizeY, sizeZ), [sizeX, sizeY, sizeZ]) useEffect( () => () => { geometry.dispose() diff --git a/packages/nodes/src/wall/floorplan-move.ts b/packages/nodes/src/wall/floorplan-move.ts index cd808efc9..c891eec4a 100644 --- a/packages/nodes/src/wall/floorplan-move.ts +++ b/packages/nodes/src/wall/floorplan-move.ts @@ -187,13 +187,15 @@ export const wallFloorplanMoveTarget: FloorplanMoveTarget = ({ node }) // until the user commits. Batched into a single zustand // notification — otherwise each per-wall `.set` would re-render // every override subscriber once per linked wall per tick. - useLiveNodeOverrides.getState().setMany([ - [wallId, { start: nextStart, end: nextEnd }], - ...linkedUpdates.map( - (upd) => - [upd.id, { start: upd.start, end: upd.end }] as [string, Record], - ), - ]) + useLiveNodeOverrides + .getState() + .setMany([ + [wallId, { start: nextStart, end: nextEnd }], + ...linkedUpdates.map( + (upd) => + [upd.id, { start: upd.start, end: upd.end }] as [string, Record], + ), + ]) // Surface bridge-wall previews so the floor-plan SVG layer can // render dashed outlines of what `commit()` will insert. Mirrors diff --git a/packages/nodes/src/wall/move-tool.tsx b/packages/nodes/src/wall/move-tool.tsx index d83c3d6fd..d9503aa74 100644 --- a/packages/nodes/src/wall/move-tool.tsx +++ b/packages/nodes/src/wall/move-tool.tsx @@ -460,9 +460,7 @@ export const MoveWallTool: React.FC<{ node: WallNode }> = ({ node }) => { if (axis) { const originalProj = originalCenter[0] * axis[0] + originalCenter[1] * axis[1] const rawProj = originalProj + rawDeltaX * axis[0] + rawDeltaZ * axis[1] - const snappedProj = shiftPressedRef.current - ? rawProj - : snapScalarToGrid(rawProj, snapStep) + const snappedProj = shiftPressedRef.current ? rawProj : snapScalarToGrid(rawProj, snapStep) const perpDelta = snappedProj - originalProj deltaX = axis[0] * perpDelta deltaZ = axis[1] * perpDelta diff --git a/packages/viewer/src/components/renderers/parametric-node-renderer.tsx b/packages/viewer/src/components/renderers/parametric-node-renderer.tsx index eb7d36560..be18eedc5 100644 --- a/packages/viewer/src/components/renderers/parametric-node-renderer.tsx +++ b/packages/viewer/src/components/renderers/parametric-node-renderer.tsx @@ -62,10 +62,7 @@ export const ParametricNodeRenderer = ({ node }: { node: AnyNode }) => { // of snapping only on commit. Per-node subscription so unrelated // override writes don't re-render the whole tree. const liveOverride = useLiveNodeOverrides((s) => s.overrides.get(node.id)) - const overrideRotation = liveOverride?.rotation as - | [number, number, number] - | number - | undefined + const overrideRotation = liveOverride?.rotation as [number, number, number] | number | undefined const overridePosition = liveOverride?.position as [number, number, number] | undefined useRegistry(node.id, node.type, ref) diff --git a/packages/viewer/src/systems/wall/wall-system.tsx b/packages/viewer/src/systems/wall/wall-system.tsx index 5a578cc49..ec7bc92fe 100644 --- a/packages/viewer/src/systems/wall/wall-system.tsx +++ b/packages/viewer/src/systems/wall/wall-system.tsx @@ -5,9 +5,9 @@ import { DEFAULT_WALL_HEIGHT, type DoorNode, getAdjacentWallIds, + getEffectiveNode, getWallCurveFrameAt, getWallMiterBoundaryPoints, - getEffectiveNode, getWallPlanFootprint, getWallSurfacePolygon, getWallThickness,