diff --git a/packages/editor/src/components/editor/selection-manager.tsx b/packages/editor/src/components/editor/selection-manager.tsx index 92b1d2f1..a1dd3157 100755 --- a/packages/editor/src/components/editor/selection-manager.tsx +++ b/packages/editor/src/components/editor/selection-manager.tsx @@ -2,13 +2,16 @@ import { type AnyNode, type AnyNodeId, type BuildingNode, + type CeilingNode, emitter, + type FenceNode, type ItemNode, type NodeEvent, type RoofEvent, type RoofNode, type RoofSegmentEvent, resolveLevelId, + type SlabNode, type StairEvent, type StairNode, type StairSegmentEvent, @@ -25,6 +28,7 @@ import { useCallback, useEffect, useRef } from 'react' import { type BufferGeometry, Color, type Material, type Mesh, type Object3D } from 'three' import { buildRoofSurfaceMaterialPatch, + buildSingleSurfaceMaterialPatch, buildStairSurfaceMaterialPatch, buildWallSurfaceMaterialPatch, hasActivePaintMaterial, @@ -609,7 +613,30 @@ export const SelectionManager = () => { } } - const disabledNodeTypes = ['fence', 'item', 'slab', 'ceiling', 'window', 'door', 'zone'] + if (node.type === 'fence' || node.type === 'slab' || node.type === 'ceiling') { + const target = node.type + const compatible = + isActivePaintMaterialCompatible(activePaintMaterial, target) && + hasActivePaintMaterial(activePaintMaterial) + + return { + apply: compatible + ? () => { + useScene + .getState() + .updateNode( + node.id as AnyNodeId, + buildSingleSurfaceMaterialPatch( + activePaintMaterial.material, + activePaintMaterial.materialPreset, + ), + ) + } + : null, + } + } + + const disabledNodeTypes = ['item', 'window', 'door', 'zone'] if (disabledNodeTypes.includes(node.type)) { return { apply: null, @@ -776,6 +803,14 @@ export const SelectionManager = () => { nextMaterialTargetHandled = true } + if ( + (node.type === 'fence' || node.type === 'slab' || node.type === 'ceiling') && + nodeToSelect.type === node.type + ) { + setSelectedMaterialTargetForNode(nodeToSelect, 'surface') + nextMaterialTargetHandled = true + } + if (!nextMaterialTargetHandled && useEditor.getState().selectedMaterialTarget) { useEditor.getState().setSelectedMaterialTarget(null) } @@ -1092,6 +1127,9 @@ const SelectionStateSync = () => { if ( !selectedNode || (selectedNode.type !== 'wall' && + selectedNode.type !== 'fence' && + selectedNode.type !== 'slab' && + selectedNode.type !== 'ceiling' && selectedNode.type !== 'stair' && selectedNode.type !== 'roof') ) { diff --git a/packages/editor/src/lib/material-paint.ts b/packages/editor/src/lib/material-paint.ts index 7c7fed68..88bce034 100644 --- a/packages/editor/src/lib/material-paint.ts +++ b/packages/editor/src/lib/material-paint.ts @@ -1,6 +1,8 @@ 'use client' import { + type CeilingNode, + type FenceNode, getCatalogMaterialById, getEffectiveRoofSurfaceMaterial, getEffectiveStairSurfaceMaterial, @@ -10,13 +12,19 @@ import { type MaterialTarget, type RoofNode, type RoofSurfaceMaterialRole, + type SlabNode, type StairNode, type StairSurfaceMaterialRole, type WallNode, type WallSurfaceSide, } from '@pascal-app/core' -export type PaintableMaterialTarget = Extract +export type PaintableMaterialTarget = Extract< + MaterialTarget, + 'wall' | 'roof' | 'stair' | 'fence' | 'slab' | 'ceiling' +> + +export type SingleSurfaceMaterialRole = 'surface' export type ActivePaintMaterial = { material?: MaterialSchema @@ -135,12 +143,26 @@ export function buildStairSurfaceMaterialPatch( } } +export function buildSingleSurfaceMaterialPatch( + material: MaterialSchema | undefined, + materialPreset: string | undefined, +): Partial { + return { + material, + materialPreset, + } as Partial +} + export function resolveActivePaintMaterialFromSelection(params: { nodes: Record selectedId: string | null selectedMaterialTarget: { nodeId: string - role: WallSurfaceSide | StairSurfaceMaterialRole | RoofSurfaceMaterialRole + role: + | WallSurfaceSide + | StairSurfaceMaterialRole + | RoofSurfaceMaterialRole + | SingleSurfaceMaterialRole } | null }): ActivePaintMaterial | null { const { nodes, selectedId, selectedMaterialTarget } = params @@ -208,6 +230,26 @@ export function resolveActivePaintMaterialFromSelection(params: { : null } + if ( + (selectedNode.type === 'fence' || + selectedNode.type === 'slab' || + selectedNode.type === 'ceiling') && + selectedMaterialTarget.role === 'surface' + ) { + const target = selectedNode.type + return hasActivePaintMaterial({ + material: selectedNode.material, + materialPreset: selectedNode.materialPreset, + sourceTarget: target, + }) + ? { + material: selectedNode.material, + materialPreset: selectedNode.materialPreset, + sourceTarget: target, + } + : null + } + return null } @@ -233,5 +275,17 @@ export function resolvePaintTargetFromSelection(params: { return 'stair' } + if (selectedNode.type === 'fence') { + return 'fence' + } + + if (selectedNode.type === 'slab') { + return 'slab' + } + + if (selectedNode.type === 'ceiling') { + return 'ceiling' + } + return null } diff --git a/packages/editor/src/store/use-editor.tsx b/packages/editor/src/store/use-editor.tsx index d323f12f..da6a952a 100644 --- a/packages/editor/src/store/use-editor.tsx +++ b/packages/editor/src/store/use-editor.tsx @@ -31,6 +31,7 @@ import { type PaintableMaterialTarget, resolveActivePaintMaterialFromSelection, resolvePaintTargetFromSelection, + type SingleSurfaceMaterialRole, } from '../lib/material-paint' const DEFAULT_ACTIVE_SIDEBAR_PANEL = 'site' @@ -94,6 +95,7 @@ export type MaterialTargetRole = | WallSurfaceSide | StairSurfaceMaterialRole | RoofSurfaceMaterialRole + | SingleSurfaceMaterialRole export type SelectedMaterialTarget = { nodeId: AnyNodeId