Refactor material paint preview handling
This commit is contained in:
@@ -684,13 +684,8 @@ function PaintCursorLayer({
|
|||||||
const mode = useEditor((s) => s.mode)
|
const mode = useEditor((s) => s.mode)
|
||||||
const activePaintMaterial = useEditor((s) => s.activePaintMaterial)
|
const activePaintMaterial = useEditor((s) => s.activePaintMaterial)
|
||||||
const activePaintTarget = useEditor((s) => s.activePaintTarget)
|
const activePaintTarget = useEditor((s) => s.activePaintTarget)
|
||||||
const paintDisabledFeedbackTick = useEditor((s) => s.paintDisabledFeedbackTick)
|
|
||||||
const hoverHighlightMode = useViewer((s) => s.hoverHighlightMode)
|
|
||||||
const badgeRef = useRef<HTMLDivElement>(null)
|
const badgeRef = useRef<HTMLDivElement>(null)
|
||||||
const lastPointerPositionRef = useRef<{ x: number; y: number } | null>(null)
|
|
||||||
const [showDisabledFeedback, setShowDisabledFeedback] = useState(false)
|
|
||||||
const active = mode === 'material-paint' && !isVersionPreviewMode
|
const active = mode === 'material-paint' && !isVersionPreviewMode
|
||||||
const showBlockedState = showDisabledFeedback || hoverHighlightMode === 'paint-disabled'
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!active) {
|
if (!active) {
|
||||||
@@ -717,7 +712,6 @@ function PaintCursorLayer({
|
|||||||
const rect = el.getBoundingClientRect()
|
const rect = el.getBoundingClientRect()
|
||||||
nextX = e.clientX - rect.left
|
nextX = e.clientX - rect.left
|
||||||
nextY = e.clientY - rect.top
|
nextY = e.clientY - rect.top
|
||||||
lastPointerPositionRef.current = { x: nextX, y: nextY }
|
|
||||||
|
|
||||||
if (frame === 0) {
|
if (frame === 0) {
|
||||||
frame = window.requestAnimationFrame(flushPosition)
|
frame = window.requestAnimationFrame(flushPosition)
|
||||||
@@ -748,57 +742,8 @@ function PaintCursorLayer({
|
|||||||
(activePaintMaterial.material !== undefined ||
|
(activePaintMaterial.material !== undefined ||
|
||||||
activePaintMaterial.materialPreset !== undefined),
|
activePaintMaterial.materialPreset !== undefined),
|
||||||
)
|
)
|
||||||
const label = !hasMaterial
|
const label = !hasMaterial ? 'Choose material' : `Paint ${activePaintTarget}`
|
||||||
? 'Choose material'
|
const icon = 'mdi:format-color-fill'
|
||||||
: showBlockedState
|
|
||||||
? 'Unsupported'
|
|
||||||
: `Paint ${activePaintTarget}`
|
|
||||||
const icon = showBlockedState ? 'mdi:block-helper' : 'mdi:format-color-fill'
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!active) return
|
|
||||||
|
|
||||||
const el = containerRef.current
|
|
||||||
if (!el) return
|
|
||||||
|
|
||||||
if (hoverHighlightMode === 'paint-disabled') {
|
|
||||||
el.style.cursor = 'not-allowed'
|
|
||||||
return () => {
|
|
||||||
el.style.cursor = ''
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
el.style.cursor = ''
|
|
||||||
}, [active, containerRef, hoverHighlightMode])
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!paintDisabledFeedbackTick) return
|
|
||||||
|
|
||||||
const el = containerRef.current
|
|
||||||
const badge = badgeRef.current
|
|
||||||
const lastPointerPosition = lastPointerPositionRef.current
|
|
||||||
setShowDisabledFeedback(true)
|
|
||||||
if (el) {
|
|
||||||
el.style.cursor = 'not-allowed'
|
|
||||||
}
|
|
||||||
if (badge && lastPointerPosition) {
|
|
||||||
badge.style.display = 'block'
|
|
||||||
badge.style.transform = `translate(${lastPointerPosition.x + PAINT_CURSOR_BADGE_OFFSET_X}px, ${lastPointerPosition.y + PAINT_CURSOR_BADGE_OFFSET_Y}px)`
|
|
||||||
}
|
|
||||||
const timeoutId = window.setTimeout(() => {
|
|
||||||
setShowDisabledFeedback(false)
|
|
||||||
if (containerRef.current) {
|
|
||||||
containerRef.current.style.cursor = ''
|
|
||||||
}
|
|
||||||
}, 320)
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
window.clearTimeout(timeoutId)
|
|
||||||
if (el) {
|
|
||||||
el.style.cursor = ''
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, [containerRef, paintDisabledFeedbackTick])
|
|
||||||
|
|
||||||
useLayoutEffect(() => {
|
useLayoutEffect(() => {
|
||||||
if (!active && badgeRef.current) {
|
if (!active && badgeRef.current) {
|
||||||
@@ -815,7 +760,7 @@ function PaintCursorLayer({
|
|||||||
style={{ display: 'none', position: 'absolute', left: 0, top: 0 }}
|
style={{ display: 'none', position: 'absolute', left: 0, top: 0 }}
|
||||||
>
|
>
|
||||||
<PaintCursorBadge
|
<PaintCursorBadge
|
||||||
disabled={!hasMaterial || showBlockedState}
|
disabled={!hasMaterial}
|
||||||
icon={icon}
|
icon={icon}
|
||||||
label={label}
|
label={label}
|
||||||
position={{ x: 0, y: 0 }}
|
position={{ x: 0, y: 0 }}
|
||||||
|
|||||||
@@ -5,12 +5,14 @@ import {
|
|||||||
type CeilingNode,
|
type CeilingNode,
|
||||||
emitter,
|
emitter,
|
||||||
type FenceNode,
|
type FenceNode,
|
||||||
|
getMaterialPresetByRef,
|
||||||
type ItemNode,
|
type ItemNode,
|
||||||
type NodeEvent,
|
type NodeEvent,
|
||||||
type RoofEvent,
|
type RoofEvent,
|
||||||
type RoofNode,
|
type RoofNode,
|
||||||
type RoofSegmentEvent,
|
type RoofSegmentEvent,
|
||||||
resolveLevelId,
|
resolveLevelId,
|
||||||
|
resolveMaterial,
|
||||||
type SlabNode,
|
type SlabNode,
|
||||||
type StairEvent,
|
type StairEvent,
|
||||||
type StairNode,
|
type StairNode,
|
||||||
@@ -27,6 +29,18 @@ import { useViewer } from '@pascal-app/viewer'
|
|||||||
import { useCallback, useEffect, useRef } from 'react'
|
import { useCallback, useEffect, useRef } from 'react'
|
||||||
import { type BufferGeometry, Color, type Material, type Mesh, type Object3D } from 'three'
|
import { type BufferGeometry, Color, type Material, type Mesh, type Object3D } from 'three'
|
||||||
import {
|
import {
|
||||||
|
applyMaterialPresetToMaterials,
|
||||||
|
createMaterial,
|
||||||
|
createMaterialFromPresetRef,
|
||||||
|
} from '../../../../viewer/src/lib/materials'
|
||||||
|
import { getRoofMaterialArray } from '../../../../viewer/src/systems/roof/roof-materials'
|
||||||
|
import {
|
||||||
|
getStairBodyMaterials,
|
||||||
|
getStairRailingMaterial,
|
||||||
|
} from '../../../../viewer/src/systems/stair/stair-materials'
|
||||||
|
import { getVisibleWallMaterials } from '../../../../viewer/src/systems/wall/wall-materials'
|
||||||
|
import {
|
||||||
|
type ActivePaintMaterial,
|
||||||
buildRoofSurfaceMaterialPatch,
|
buildRoofSurfaceMaterialPatch,
|
||||||
buildSingleSurfaceMaterialPatch,
|
buildSingleSurfaceMaterialPatch,
|
||||||
buildStairSurfaceMaterialPatch,
|
buildStairSurfaceMaterialPatch,
|
||||||
@@ -70,12 +84,12 @@ type ModifierKeys = {
|
|||||||
ctrl: boolean
|
ctrl: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
type SceneMaterialPreview = ReturnType<typeof useViewer.getState>['materialPreview']
|
type PaintPreviewCleanup = () => void
|
||||||
|
|
||||||
type PaintInteraction = {
|
type PaintInteraction = {
|
||||||
|
key: string
|
||||||
apply: (() => void) | null
|
apply: (() => void) | null
|
||||||
preview: SceneMaterialPreview
|
preview: (() => PaintPreviewCleanup | null) | null
|
||||||
hoveredId: AnyNodeId
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interface SelectionStrategy {
|
interface SelectionStrategy {
|
||||||
@@ -201,6 +215,196 @@ function getIntersectionMaterialIndex(
|
|||||||
return group?.materialIndex
|
return group?.materialIndex
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getRegisteredNodeObject(nodeId: string): Object3D | null {
|
||||||
|
return sceneRegistry.nodes.get(nodeId) ?? null
|
||||||
|
}
|
||||||
|
|
||||||
|
function getRegisteredMesh(nodeId: string): Mesh | null {
|
||||||
|
const object = getRegisteredNodeObject(nodeId)
|
||||||
|
return object && (object as Mesh).isMesh ? (object as Mesh) : null
|
||||||
|
}
|
||||||
|
|
||||||
|
function previewMeshMaterial(mesh: Mesh, material: Material | Material[]): PaintPreviewCleanup {
|
||||||
|
const previousMaterial = mesh.material
|
||||||
|
mesh.material = material
|
||||||
|
return () => {
|
||||||
|
mesh.material = previousMaterial
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function previewCursor(cursor: string): PaintPreviewCleanup {
|
||||||
|
const previousCursor = document.body.style.cursor
|
||||||
|
document.body.style.cursor = cursor
|
||||||
|
return () => {
|
||||||
|
document.body.style.cursor = previousCursor
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getSingleSurfacePreviewMaterial(material: ActivePaintMaterial): Material | null {
|
||||||
|
if (material.materialPreset) {
|
||||||
|
return createMaterialFromPresetRef(material.materialPreset)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (material.material) {
|
||||||
|
return createMaterial(material.material)
|
||||||
|
}
|
||||||
|
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyWallPaintPreview(
|
||||||
|
node: WallNode,
|
||||||
|
role: WallSurfaceSide,
|
||||||
|
material: ActivePaintMaterial,
|
||||||
|
): PaintPreviewCleanup | null {
|
||||||
|
const mesh = getRegisteredMesh(node.id)
|
||||||
|
if (!mesh) return null
|
||||||
|
|
||||||
|
const previewNode = {
|
||||||
|
...node,
|
||||||
|
...buildWallSurfaceMaterialPatch(node, role, material.material, material.materialPreset),
|
||||||
|
}
|
||||||
|
|
||||||
|
return previewMeshMaterial(mesh, getVisibleWallMaterials(previewNode))
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyRoofPaintPreview(
|
||||||
|
node: RoofNode,
|
||||||
|
role: 'top' | 'edge' | 'wall',
|
||||||
|
material: ActivePaintMaterial,
|
||||||
|
): PaintPreviewCleanup | null {
|
||||||
|
const root = getRegisteredNodeObject(node.id)
|
||||||
|
const mesh = root?.getObjectByName('merged-roof') as Mesh | undefined
|
||||||
|
if (!mesh) return null
|
||||||
|
|
||||||
|
const previewNode = {
|
||||||
|
...node,
|
||||||
|
...buildRoofSurfaceMaterialPatch(node, role, material.material, material.materialPreset),
|
||||||
|
}
|
||||||
|
const previewMaterial = getRoofMaterialArray(previewNode)
|
||||||
|
if (!previewMaterial) return null
|
||||||
|
|
||||||
|
return previewMeshMaterial(mesh, previewMaterial)
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyStairPaintPreview(
|
||||||
|
node: StairNode,
|
||||||
|
role: StairSurfaceMaterialRole,
|
||||||
|
material: ActivePaintMaterial,
|
||||||
|
): PaintPreviewCleanup | null {
|
||||||
|
const root = getRegisteredNodeObject(node.id)
|
||||||
|
if (!root) return null
|
||||||
|
|
||||||
|
const previewNode = {
|
||||||
|
...node,
|
||||||
|
...buildStairSurfaceMaterialPatch(node, role, material.material, material.materialPreset),
|
||||||
|
}
|
||||||
|
const bodyMaterials = getStairBodyMaterials(previewNode)
|
||||||
|
const railingMaterial = getStairRailingMaterial(previewNode)
|
||||||
|
const restores: PaintPreviewCleanup[] = []
|
||||||
|
|
||||||
|
root.traverse((object) => {
|
||||||
|
if (!(object as Mesh).isMesh) return
|
||||||
|
const mesh = object as Mesh
|
||||||
|
if (mesh.name.startsWith('stair-railing')) {
|
||||||
|
restores.push(previewMeshMaterial(mesh, railingMaterial))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (Array.isArray(mesh.material) && mesh.material.length === 2) {
|
||||||
|
restores.push(previewMeshMaterial(mesh, bodyMaterials))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (mesh.name === 'merged-stair') {
|
||||||
|
restores.push(previewMeshMaterial(mesh, bodyMaterials))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (mesh.name.startsWith('stair-side')) {
|
||||||
|
restores.push(previewMeshMaterial(mesh, bodyMaterials[1]))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if (restores.length === 0) return null
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
for (let index = restores.length - 1; index >= 0; index -= 1) {
|
||||||
|
restores[index]?.()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function applySingleSurfacePaintPreview(
|
||||||
|
node: FenceNode | SlabNode | CeilingNode,
|
||||||
|
material: ActivePaintMaterial,
|
||||||
|
): PaintPreviewCleanup | null {
|
||||||
|
if (node.type === 'ceiling') {
|
||||||
|
const root = getRegisteredMesh(node.id)
|
||||||
|
const overlay = root?.getObjectByName('ceiling-grid') as Mesh | undefined
|
||||||
|
if (!root || !overlay) return null
|
||||||
|
|
||||||
|
const previewColor =
|
||||||
|
getMaterialPresetByRef(material.materialPreset)?.mapProperties.color ??
|
||||||
|
resolveMaterial(material.material).color ??
|
||||||
|
'#999999'
|
||||||
|
|
||||||
|
const previousRootMaterial = root.material
|
||||||
|
const previousOverlayMaterial = overlay.material
|
||||||
|
const rootPreviewMaterial = Array.isArray(previousRootMaterial)
|
||||||
|
? previousRootMaterial.map((entry) => entry.clone())
|
||||||
|
: previousRootMaterial.clone()
|
||||||
|
const overlayPreviewMaterial = Array.isArray(previousOverlayMaterial)
|
||||||
|
? previousOverlayMaterial.map((entry) => entry.clone())
|
||||||
|
: previousOverlayMaterial.clone()
|
||||||
|
|
||||||
|
const applyColor = (input: Material | Material[]) => {
|
||||||
|
const materials = Array.isArray(input) ? input : [input]
|
||||||
|
for (const entry of materials) {
|
||||||
|
const materialWithColor = entry as Material & { color?: Color; needsUpdate?: boolean }
|
||||||
|
if (materialWithColor.color instanceof Color) {
|
||||||
|
materialWithColor.color = new Color(previewColor)
|
||||||
|
}
|
||||||
|
materialWithColor.needsUpdate = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
applyColor(rootPreviewMaterial)
|
||||||
|
applyColor(overlayPreviewMaterial)
|
||||||
|
root.material = rootPreviewMaterial
|
||||||
|
overlay.material = overlayPreviewMaterial
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
root.material = previousRootMaterial
|
||||||
|
overlay.material = previousOverlayMaterial
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const mesh = getRegisteredMesh(node.id)
|
||||||
|
if (!mesh) return null
|
||||||
|
|
||||||
|
const previewMaterial = getSingleSurfacePreviewMaterial(material)
|
||||||
|
if (!previewMaterial) return null
|
||||||
|
|
||||||
|
if (node.type === 'slab') {
|
||||||
|
const slabMaterial = previewMaterial.clone()
|
||||||
|
applyMaterialPresetToMaterials(slabMaterial, getMaterialPresetByRef(material.materialPreset))
|
||||||
|
const previewMeshMaterialInput = slabMaterial as Material & {
|
||||||
|
alphaMap?: unknown
|
||||||
|
depthWrite?: boolean
|
||||||
|
needsUpdate?: boolean
|
||||||
|
opacity?: number
|
||||||
|
side?: number
|
||||||
|
transparent?: boolean
|
||||||
|
}
|
||||||
|
previewMeshMaterialInput.transparent = false
|
||||||
|
previewMeshMaterialInput.opacity = 1
|
||||||
|
previewMeshMaterialInput.alphaMap = null
|
||||||
|
previewMeshMaterialInput.depthWrite = true
|
||||||
|
previewMeshMaterialInput.needsUpdate = true
|
||||||
|
return previewMeshMaterial(mesh, slabMaterial)
|
||||||
|
}
|
||||||
|
|
||||||
|
return previewMeshMaterial(mesh, previewMaterial)
|
||||||
|
}
|
||||||
|
|
||||||
function setSelectedMaterialTargetForNode(node: AnyNode, role: MaterialTargetRole | null) {
|
function setSelectedMaterialTargetForNode(node: AnyNode, role: MaterialTargetRole | null) {
|
||||||
if (!role) {
|
if (!role) {
|
||||||
const currentTarget = useEditor.getState().selectedMaterialTarget
|
const currentTarget = useEditor.getState().selectedMaterialTarget
|
||||||
@@ -510,32 +714,11 @@ export const SelectionManager = () => {
|
|||||||
if (mode !== 'material-paint') return
|
if (mode !== 'material-paint') return
|
||||||
if (movingNode || curvingWall) return
|
if (movingNode || curvingWall) return
|
||||||
|
|
||||||
const triggerPaintDisabledFeedback = useEditor.getState().triggerPaintDisabledFeedback
|
let activePreview: { key: string; restore: PaintPreviewCleanup } | null = null
|
||||||
let hoverFrame = 0
|
|
||||||
let pendingHoveredId: AnyNodeId | null = null
|
|
||||||
let pendingHoverMode: HoverHighlightMode = 'default'
|
|
||||||
let pendingPreview: SceneMaterialPreview = null
|
|
||||||
|
|
||||||
const flushHoverState = () => {
|
const clearActivePreview = () => {
|
||||||
hoverFrame = 0
|
activePreview?.restore()
|
||||||
const viewerState = useViewer.getState()
|
activePreview = null
|
||||||
|
|
||||||
if (viewerState.hoveredId !== pendingHoveredId) {
|
|
||||||
useViewer.setState({ hoveredId: pendingHoveredId })
|
|
||||||
}
|
|
||||||
|
|
||||||
setHoverHighlightMode(pendingHoverMode)
|
|
||||||
|
|
||||||
if (pendingPreview) {
|
|
||||||
viewerState.setMaterialPreview(pendingPreview)
|
|
||||||
} else {
|
|
||||||
viewerState.clearMaterialPreview()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const scheduleHoverState = () => {
|
|
||||||
if (hoverFrame !== 0) return
|
|
||||||
hoverFrame = window.requestAnimationFrame(flushHoverState)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const resolveActivePaintMaterial = () =>
|
const resolveActivePaintMaterial = () =>
|
||||||
@@ -560,7 +743,7 @@ export const SelectionManager = () => {
|
|||||||
const compatible =
|
const compatible =
|
||||||
role !== null && isActivePaintMaterialCompatible(activePaintMaterial, 'wall')
|
role !== null && isActivePaintMaterialCompatible(activePaintMaterial, 'wall')
|
||||||
return {
|
return {
|
||||||
hoveredId: node.id as AnyNodeId,
|
key: `wall:${node.id}:${role ?? 'unsupported'}`,
|
||||||
apply:
|
apply:
|
||||||
compatible && hasActivePaintMaterial(activePaintMaterial)
|
compatible && hasActivePaintMaterial(activePaintMaterial)
|
||||||
? () => {
|
? () => {
|
||||||
@@ -579,14 +762,8 @@ export const SelectionManager = () => {
|
|||||||
: null,
|
: null,
|
||||||
preview:
|
preview:
|
||||||
compatible && hasActivePaintMaterial(activePaintMaterial) && role
|
compatible && hasActivePaintMaterial(activePaintMaterial) && role
|
||||||
? {
|
? () => applyWallPaintPreview(node as WallNode, role, activePaintMaterial)
|
||||||
nodeId: node.id as AnyNodeId,
|
: () => previewCursor('not-allowed'),
|
||||||
target: 'wall',
|
|
||||||
role,
|
|
||||||
material: activePaintMaterial.material,
|
|
||||||
materialPreset: activePaintMaterial.materialPreset,
|
|
||||||
}
|
|
||||||
: null,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -603,7 +780,7 @@ export const SelectionManager = () => {
|
|||||||
const compatible =
|
const compatible =
|
||||||
role !== null && isActivePaintMaterialCompatible(activePaintMaterial, 'roof')
|
role !== null && isActivePaintMaterialCompatible(activePaintMaterial, 'roof')
|
||||||
return {
|
return {
|
||||||
hoveredId: roofNode.id as AnyNodeId,
|
key: `roof:${roofNode.id}:${role ?? 'unsupported'}`,
|
||||||
apply:
|
apply:
|
||||||
compatible && hasActivePaintMaterial(activePaintMaterial)
|
compatible && hasActivePaintMaterial(activePaintMaterial)
|
||||||
? () => {
|
? () => {
|
||||||
@@ -622,14 +799,8 @@ export const SelectionManager = () => {
|
|||||||
: null,
|
: null,
|
||||||
preview:
|
preview:
|
||||||
compatible && hasActivePaintMaterial(activePaintMaterial) && role
|
compatible && hasActivePaintMaterial(activePaintMaterial) && role
|
||||||
? {
|
? () => applyRoofPaintPreview(roofNode as RoofNode, role, activePaintMaterial)
|
||||||
nodeId: roofNode.id as AnyNodeId,
|
: () => previewCursor('not-allowed'),
|
||||||
target: 'roof',
|
|
||||||
role,
|
|
||||||
material: activePaintMaterial.material,
|
|
||||||
materialPreset: activePaintMaterial.materialPreset,
|
|
||||||
}
|
|
||||||
: null,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -646,7 +817,7 @@ export const SelectionManager = () => {
|
|||||||
const compatible =
|
const compatible =
|
||||||
role !== null && isActivePaintMaterialCompatible(activePaintMaterial, 'stair')
|
role !== null && isActivePaintMaterialCompatible(activePaintMaterial, 'stair')
|
||||||
return {
|
return {
|
||||||
hoveredId: stairNode.id as AnyNodeId,
|
key: `stair:${stairNode.id}:${role ?? 'unsupported'}`,
|
||||||
apply:
|
apply:
|
||||||
compatible && hasActivePaintMaterial(activePaintMaterial)
|
compatible && hasActivePaintMaterial(activePaintMaterial)
|
||||||
? () => {
|
? () => {
|
||||||
@@ -665,14 +836,8 @@ export const SelectionManager = () => {
|
|||||||
: null,
|
: null,
|
||||||
preview:
|
preview:
|
||||||
compatible && hasActivePaintMaterial(activePaintMaterial) && role
|
compatible && hasActivePaintMaterial(activePaintMaterial) && role
|
||||||
? {
|
? () => applyStairPaintPreview(stairNode as StairNode, role, activePaintMaterial)
|
||||||
nodeId: stairNode.id as AnyNodeId,
|
: () => previewCursor('not-allowed'),
|
||||||
target: 'stair',
|
|
||||||
role,
|
|
||||||
material: activePaintMaterial.material,
|
|
||||||
materialPreset: activePaintMaterial.materialPreset,
|
|
||||||
}
|
|
||||||
: null,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -683,7 +848,7 @@ export const SelectionManager = () => {
|
|||||||
hasActivePaintMaterial(activePaintMaterial)
|
hasActivePaintMaterial(activePaintMaterial)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
hoveredId: node.id as AnyNodeId,
|
key: `${target}:${node.id}:surface`,
|
||||||
apply: compatible
|
apply: compatible
|
||||||
? () => {
|
? () => {
|
||||||
useScene
|
useScene
|
||||||
@@ -698,23 +863,21 @@ export const SelectionManager = () => {
|
|||||||
}
|
}
|
||||||
: null,
|
: null,
|
||||||
preview: compatible
|
preview: compatible
|
||||||
? {
|
? () =>
|
||||||
nodeId: node.id as AnyNodeId,
|
applySingleSurfacePaintPreview(
|
||||||
target,
|
node as FenceNode | SlabNode | CeilingNode,
|
||||||
role: 'surface',
|
activePaintMaterial,
|
||||||
material: activePaintMaterial.material,
|
)
|
||||||
materialPreset: activePaintMaterial.materialPreset,
|
: () => previewCursor('not-allowed'),
|
||||||
}
|
|
||||||
: null,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const disabledNodeTypes = ['item', 'window', 'door', 'zone']
|
const disabledNodeTypes = ['item', 'window', 'door', 'zone']
|
||||||
if (disabledNodeTypes.includes(node.type)) {
|
if (disabledNodeTypes.includes(node.type)) {
|
||||||
return {
|
return {
|
||||||
hoveredId: node.id as AnyNodeId,
|
key: `${node.type}:${node.id}:unsupported`,
|
||||||
apply: null,
|
apply: null,
|
||||||
preview: null,
|
preview: () => previewCursor('not-allowed'),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -729,28 +892,27 @@ export const SelectionManager = () => {
|
|||||||
|
|
||||||
event.stopPropagation()
|
event.stopPropagation()
|
||||||
|
|
||||||
if (!interaction.preview) {
|
if (activePreview?.key === interaction.key) {
|
||||||
pendingHoveredId = interaction.hoveredId
|
|
||||||
pendingHoverMode = 'paint-disabled'
|
|
||||||
pendingPreview = null
|
|
||||||
scheduleHoverState()
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
pendingHoveredId = interaction.hoveredId
|
clearActivePreview()
|
||||||
pendingHoverMode = 'paint-ready'
|
|
||||||
pendingPreview = interaction.preview
|
const restore = interaction.preview?.()
|
||||||
scheduleHoverState()
|
if (restore) {
|
||||||
|
activePreview = { key: interaction.key, restore }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const onLeave = (event: NodeEvent) => {
|
const onLeave = (event: NodeEvent) => {
|
||||||
const interaction = getPaintInteraction(event)
|
const interaction = getPaintInteraction(event)
|
||||||
if (!interaction) return
|
if (!interaction) return
|
||||||
|
|
||||||
pendingHoveredId = null
|
if (activePreview?.key !== interaction.key) {
|
||||||
pendingHoverMode = 'default'
|
return
|
||||||
pendingPreview = null
|
}
|
||||||
scheduleHoverState()
|
|
||||||
|
clearActivePreview()
|
||||||
}
|
}
|
||||||
|
|
||||||
const onClick = (event: NodeEvent) => {
|
const onClick = (event: NodeEvent) => {
|
||||||
@@ -761,19 +923,16 @@ export const SelectionManager = () => {
|
|||||||
|
|
||||||
event.stopPropagation()
|
event.stopPropagation()
|
||||||
|
|
||||||
if (hoverFrame !== 0) {
|
|
||||||
window.cancelAnimationFrame(hoverFrame)
|
|
||||||
flushHoverState()
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!interaction.apply) {
|
if (!interaction.apply) {
|
||||||
useViewer.getState().clearMaterialPreview()
|
|
||||||
triggerPaintDisabledFeedback()
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
interaction.apply()
|
interaction.apply()
|
||||||
useViewer.getState().clearMaterialPreview()
|
if (activePreview?.key === interaction.key) {
|
||||||
|
activePreview = null
|
||||||
|
} else {
|
||||||
|
clearActivePreview()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const allTypes = [
|
const allTypes = [
|
||||||
@@ -792,25 +951,20 @@ export const SelectionManager = () => {
|
|||||||
] as const
|
] as const
|
||||||
|
|
||||||
for (const type of allTypes) {
|
for (const type of allTypes) {
|
||||||
emitter.on(`${type}:click` as any, onClick as any)
|
|
||||||
emitter.on(`${type}:enter` as any, onEnter as any)
|
emitter.on(`${type}:enter` as any, onEnter as any)
|
||||||
emitter.on(`${type}:leave` as any, onLeave as any)
|
emitter.on(`${type}:leave` as any, onLeave as any)
|
||||||
|
emitter.on(`${type}:click` as any, onClick as any)
|
||||||
}
|
}
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
for (const type of allTypes) {
|
for (const type of allTypes) {
|
||||||
emitter.off(`${type}:click` as any, onClick as any)
|
|
||||||
emitter.off(`${type}:enter` as any, onEnter as any)
|
emitter.off(`${type}:enter` as any, onEnter as any)
|
||||||
emitter.off(`${type}:leave` as any, onLeave as any)
|
emitter.off(`${type}:leave` as any, onLeave as any)
|
||||||
|
emitter.off(`${type}:click` as any, onClick as any)
|
||||||
}
|
}
|
||||||
if (hoverFrame !== 0) {
|
clearActivePreview()
|
||||||
window.cancelAnimationFrame(hoverFrame)
|
|
||||||
}
|
|
||||||
useViewer.setState({ hoveredId: null })
|
|
||||||
setHoverHighlightMode('default')
|
|
||||||
useViewer.getState().clearMaterialPreview()
|
|
||||||
}
|
}
|
||||||
}, [curvingWall, mode, movingNode, setHoverHighlightMode])
|
}, [curvingWall, mode, movingNode])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const onKeyDown = (event: KeyboardEvent) => {
|
const onKeyDown = (event: KeyboardEvent) => {
|
||||||
|
|||||||
@@ -162,8 +162,6 @@ type EditorState = {
|
|||||||
activePaintTarget: PaintableMaterialTarget
|
activePaintTarget: PaintableMaterialTarget
|
||||||
setActivePaintTarget: (target: PaintableMaterialTarget) => void
|
setActivePaintTarget: (target: PaintableMaterialTarget) => void
|
||||||
primeMaterialPaintFromSelection: () => MaterialPaintSelectionSnapshot
|
primeMaterialPaintFromSelection: () => MaterialPaintSelectionSnapshot
|
||||||
paintDisabledFeedbackTick: number
|
|
||||||
triggerPaintDisabledFeedback: () => void
|
|
||||||
hoveredPaintTarget: PaintableMaterialTarget | null
|
hoveredPaintTarget: PaintableMaterialTarget | null
|
||||||
setHoveredPaintTarget: (target: PaintableMaterialTarget | null) => void
|
setHoveredPaintTarget: (target: PaintableMaterialTarget | null) => void
|
||||||
selectedReferenceId: string | null
|
selectedReferenceId: string | null
|
||||||
@@ -579,9 +577,6 @@ const useEditor = create<EditorState>()(
|
|||||||
activePaintMaterial: activePaintMaterial ?? get().activePaintMaterial,
|
activePaintMaterial: activePaintMaterial ?? get().activePaintMaterial,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
paintDisabledFeedbackTick: 0,
|
|
||||||
triggerPaintDisabledFeedback: () =>
|
|
||||||
set((state) => ({ paintDisabledFeedbackTick: state.paintDisabledFeedbackTick + 1 })),
|
|
||||||
hoveredPaintTarget: null,
|
hoveredPaintTarget: null,
|
||||||
setHoveredPaintTarget: (target) =>
|
setHoveredPaintTarget: (target) =>
|
||||||
set((state) =>
|
set((state) =>
|
||||||
|
|||||||
Reference in New Issue
Block a user