feat(paint-slots): create-in-place scene materials + paint-panel polish
Custom-create now pre-creates a scene material and opens its inline editor in the build pane (no separate right-side PaintPanel, which is removed); the brush + "Paint with" use a scene: ref so painting stores the ref and edits propagate everywhere. Slot preview (shared + item) resolves scene refs so hover shows the real material. Material properties editor uses the shared SliderControl for roughness/ metalness/opacity; row action buttons use Tooltip instead of title; the color input renders as a clean filled swatch. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
c6423e7c73
commit
6b67ab6949
@@ -1,9 +1,15 @@
|
|||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import { type AnyNodeId, useScene } from '@pascal-app/core'
|
import {
|
||||||
|
type AnyNodeId,
|
||||||
|
generateSceneMaterialId,
|
||||||
|
type SceneMaterialId,
|
||||||
|
toSceneMaterialRef,
|
||||||
|
useScene,
|
||||||
|
} from '@pascal-app/core'
|
||||||
import { useViewer } from '@pascal-app/viewer'
|
import { useViewer } from '@pascal-app/viewer'
|
||||||
import { Eraser, RotateCcw } from 'lucide-react'
|
import { Eraser, RotateCcw } from 'lucide-react'
|
||||||
import { useEffect } from 'react'
|
import { useEffect, useState } from 'react'
|
||||||
import {
|
import {
|
||||||
buildResetSurfaceMaterialUpdates,
|
buildResetSurfaceMaterialUpdates,
|
||||||
resolvePaintTargetFromSelection,
|
resolvePaintTargetFromSelection,
|
||||||
@@ -27,6 +33,8 @@ export function MaterialPaintPanel() {
|
|||||||
const setActivePaintTarget = useEditor((state) => state.setActivePaintTarget)
|
const setActivePaintTarget = useEditor((state) => state.setActivePaintTarget)
|
||||||
const paintEraser = useEditor((state) => state.paintEraser)
|
const paintEraser = useEditor((state) => state.paintEraser)
|
||||||
const setPaintEraser = useEditor((state) => state.setPaintEraser)
|
const setPaintEraser = useEditor((state) => state.setPaintEraser)
|
||||||
|
// Id of a just-created scene material whose inline editor should open on mount.
|
||||||
|
const [autoEditMaterialId, setAutoEditMaterialId] = useState<SceneMaterialId | null>(null)
|
||||||
const selectedIds = useViewer((state) => state.selection.selectedIds)
|
const selectedIds = useViewer((state) => state.selection.selectedIds)
|
||||||
const nodes = useScene((state) => state.nodes)
|
const nodes = useScene((state) => state.nodes)
|
||||||
const materialCount = useScene((state) => Object.keys(state.materials).length)
|
const materialCount = useScene((state) => Object.keys(state.materials).length)
|
||||||
@@ -73,7 +81,18 @@ export function MaterialPaintPanel() {
|
|||||||
</div>
|
</div>
|
||||||
<MaterialPicker
|
<MaterialPicker
|
||||||
onChange={(material) => {
|
onChange={(material) => {
|
||||||
setActivePaintMaterial({ material, sourceTarget: activePaintTarget })
|
// Custom-create: pre-create a scene material and select it as the
|
||||||
|
// brush via a `scene:` ref so painting stores the ref and edits to
|
||||||
|
// it propagate everywhere. The user edits it inline in the scene-
|
||||||
|
// material list below (auto-opened) — no separate right-side pane.
|
||||||
|
const id = generateSceneMaterialId()
|
||||||
|
const count = Object.keys(useScene.getState().materials).length
|
||||||
|
useScene.getState().addSceneMaterial({ id, name: `Material ${count + 1}`, material })
|
||||||
|
setActivePaintMaterial({
|
||||||
|
materialPreset: toSceneMaterialRef(id),
|
||||||
|
sourceTarget: activePaintTarget,
|
||||||
|
})
|
||||||
|
setAutoEditMaterialId(id)
|
||||||
}}
|
}}
|
||||||
onSelectMaterialPreset={(materialPreset) => {
|
onSelectMaterialPreset={(materialPreset) => {
|
||||||
setActivePaintMaterial({ materialPreset, sourceTarget: activePaintTarget })
|
setActivePaintMaterial({ materialPreset, sourceTarget: activePaintTarget })
|
||||||
@@ -83,7 +102,7 @@ export function MaterialPaintPanel() {
|
|||||||
/>
|
/>
|
||||||
{materialCount > 0 ? (
|
{materialCount > 0 ? (
|
||||||
<PanelSection title="Scene materials">
|
<PanelSection title="Scene materials">
|
||||||
<SceneMaterialList />
|
<SceneMaterialList autoEditId={autoEditMaterialId} />
|
||||||
</PanelSection>
|
</PanelSection>
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ import {
|
|||||||
} from '@pascal-app/core'
|
} from '@pascal-app/core'
|
||||||
import { useEffect, useState } from 'react'
|
import { useEffect, useState } from 'react'
|
||||||
import { triggerSFX } from '../../../lib/sfx-bus'
|
import { triggerSFX } from '../../../lib/sfx-bus'
|
||||||
import useEditor from '../../../store/use-editor'
|
|
||||||
|
|
||||||
type MaterialPickerProps = {
|
type MaterialPickerProps = {
|
||||||
value?: MaterialSchema
|
value?: MaterialSchema
|
||||||
@@ -34,7 +33,6 @@ export function MaterialPicker({
|
|||||||
onSelectMaterialPreset,
|
onSelectMaterialPreset,
|
||||||
disabled = false,
|
disabled = false,
|
||||||
}: MaterialPickerProps) {
|
}: MaterialPickerProps) {
|
||||||
const setPaintPanelOpen = useEditor((state) => state.setPaintPanelOpen)
|
|
||||||
const [showCustom, setShowCustom] = useState<boolean>(!!value?.properties)
|
const [showCustom, setShowCustom] = useState<boolean>(!!value?.properties)
|
||||||
const [selectedCategory, setSelectedCategory] = useState<(typeof MATERIAL_CATEGORIES)[number]>(
|
const [selectedCategory, setSelectedCategory] = useState<(typeof MATERIAL_CATEGORIES)[number]>(
|
||||||
MATERIAL_CATEGORIES[0],
|
MATERIAL_CATEGORIES[0],
|
||||||
@@ -70,14 +68,14 @@ export function MaterialPicker({
|
|||||||
const handleCatalogSelect = (materialId: string) => {
|
const handleCatalogSelect = (materialId: string) => {
|
||||||
if (disabled) return
|
if (disabled) return
|
||||||
setShowCustom(false)
|
setShowCustom(false)
|
||||||
setPaintPanelOpen(false)
|
|
||||||
onSelectMaterialPreset?.(toLibraryMaterialRef(materialId))
|
onSelectMaterialPreset?.(toLibraryMaterialRef(materialId))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Seed a new custom material from the current/forked colour and hand it to
|
||||||
|
// the host (MaterialPaintPanel), which pre-creates a scene material the user
|
||||||
|
// edits inline in the build pane — no separate right-side editor pane.
|
||||||
const handleCustomOpen = () => {
|
const handleCustomOpen = () => {
|
||||||
if (disabled) return
|
if (disabled) return
|
||||||
setShowCustom(true)
|
|
||||||
setPaintPanelOpen(true)
|
|
||||||
const forkColor = selectedMaterialPreset
|
const forkColor = selectedMaterialPreset
|
||||||
? (selectedCatalogEntry?.previewColor ?? '#ffffff')
|
? (selectedCatalogEntry?.previewColor ?? '#ffffff')
|
||||||
: '#ffffff'
|
: '#ffffff'
|
||||||
@@ -112,9 +110,6 @@ export function MaterialPicker({
|
|||||||
if (showCustom) {
|
if (showCustom) {
|
||||||
setShowCustom(false)
|
setShowCustom(false)
|
||||||
}
|
}
|
||||||
if (category !== 'colors') {
|
|
||||||
setPaintPanelOpen(false)
|
|
||||||
}
|
|
||||||
}}
|
}}
|
||||||
type="button"
|
type="button"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
import type { MaterialProperties, MaterialSchema } from '@pascal-app/core'
|
import type { MaterialProperties, MaterialSchema } from '@pascal-app/core'
|
||||||
import { Input } from '../primitives/input'
|
import { Input } from '../primitives/input'
|
||||||
|
import { SliderControl } from './slider-control'
|
||||||
|
|
||||||
const DEFAULT_MATERIAL_PROPERTIES: MaterialProperties = {
|
const DEFAULT_MATERIAL_PROPERTIES: MaterialProperties = {
|
||||||
color: '#ffffff',
|
color: '#ffffff',
|
||||||
@@ -44,7 +45,7 @@ export function MaterialPropertiesEditor({
|
|||||||
</label>
|
</label>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<input
|
<input
|
||||||
className="h-10 w-14 cursor-pointer rounded-md border border-input bg-transparent"
|
className="h-9 w-9 shrink-0 cursor-pointer rounded-md border border-input bg-transparent p-0 [&::-moz-color-swatch]:rounded-[5px] [&::-moz-color-swatch]:border-none [&::-webkit-color-swatch-wrapper]:p-0 [&::-webkit-color-swatch]:rounded-[5px] [&::-webkit-color-swatch]:border-none"
|
||||||
onChange={(e) => updateMaterial({ color: e.target.value })}
|
onChange={(e) => updateMaterial({ color: e.target.value })}
|
||||||
type="color"
|
type="color"
|
||||||
value={currentProps.color}
|
value={currentProps.color}
|
||||||
@@ -56,68 +57,35 @@ export function MaterialPropertiesEditor({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-2">
|
<SliderControl
|
||||||
<div className="flex items-center justify-between">
|
label="Roughness"
|
||||||
<label className="font-medium text-muted-foreground text-xs uppercase tracking-[0.12em]">
|
max={1}
|
||||||
Roughness
|
min={0}
|
||||||
</label>
|
onChange={(value) => updateMaterial({ roughness: value })}
|
||||||
<span className="font-mono text-muted-foreground text-xs">
|
precision={2}
|
||||||
{currentProps.roughness.toFixed(2)}
|
step={0.01}
|
||||||
</span>
|
value={currentProps.roughness}
|
||||||
</div>
|
/>
|
||||||
<input
|
|
||||||
className="h-2 w-full cursor-pointer appearance-none rounded-full bg-accent"
|
|
||||||
max={1}
|
|
||||||
min={0}
|
|
||||||
onChange={(e) => updateMaterial({ roughness: Number.parseFloat(e.target.value) })}
|
|
||||||
step={0.01}
|
|
||||||
type="range"
|
|
||||||
value={currentProps.roughness}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="space-y-2">
|
<SliderControl
|
||||||
<div className="flex items-center justify-between">
|
label="Metalness"
|
||||||
<label className="font-medium text-muted-foreground text-xs uppercase tracking-[0.12em]">
|
max={1}
|
||||||
Metalness
|
min={0}
|
||||||
</label>
|
onChange={(value) => updateMaterial({ metalness: value })}
|
||||||
<span className="font-mono text-muted-foreground text-xs">
|
precision={2}
|
||||||
{currentProps.metalness.toFixed(2)}
|
step={0.01}
|
||||||
</span>
|
value={currentProps.metalness}
|
||||||
</div>
|
/>
|
||||||
<input
|
|
||||||
className="h-2 w-full cursor-pointer appearance-none rounded-full bg-accent"
|
|
||||||
max={1}
|
|
||||||
min={0}
|
|
||||||
onChange={(e) => updateMaterial({ metalness: Number.parseFloat(e.target.value) })}
|
|
||||||
step={0.01}
|
|
||||||
type="range"
|
|
||||||
value={currentProps.metalness}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="space-y-2">
|
<SliderControl
|
||||||
<div className="flex items-center justify-between">
|
label="Opacity"
|
||||||
<label className="font-medium text-muted-foreground text-xs uppercase tracking-[0.12em]">
|
max={1}
|
||||||
Opacity
|
min={0}
|
||||||
</label>
|
onChange={(value) => updateMaterial({ opacity: value }, value < 1 || currentProps.transparent)}
|
||||||
<span className="font-mono text-muted-foreground text-xs">
|
precision={2}
|
||||||
{currentProps.opacity.toFixed(2)}
|
step={0.01}
|
||||||
</span>
|
value={currentProps.opacity}
|
||||||
</div>
|
/>
|
||||||
<input
|
|
||||||
className="h-2 w-full cursor-pointer appearance-none rounded-full bg-accent"
|
|
||||||
max={1}
|
|
||||||
min={0}
|
|
||||||
onChange={(e) => {
|
|
||||||
const opacity = Number.parseFloat(e.target.value)
|
|
||||||
updateMaterial({ opacity }, opacity < 1 || currentProps.transparent)
|
|
||||||
}}
|
|
||||||
step={0.01}
|
|
||||||
type="range"
|
|
||||||
value={currentProps.opacity}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<label className="block font-medium text-muted-foreground text-xs uppercase tracking-[0.12em]">
|
<label className="block font-medium text-muted-foreground text-xs uppercase tracking-[0.12em]">
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import { useEffect, useMemo, useState } from 'react'
|
|||||||
import useEditor from '../../../store/use-editor'
|
import useEditor from '../../../store/use-editor'
|
||||||
import { Button } from '../primitives/button'
|
import { Button } from '../primitives/button'
|
||||||
import { Input } from '../primitives/input'
|
import { Input } from '../primitives/input'
|
||||||
|
import { Tooltip, TooltipContent, TooltipTrigger } from '../primitives/tooltip'
|
||||||
import { MaterialPropertiesEditor } from './material-properties-editor'
|
import { MaterialPropertiesEditor } from './material-properties-editor'
|
||||||
|
|
||||||
type SlotRecord = Record<string, string | undefined>
|
type SlotRecord = Record<string, string | undefined>
|
||||||
@@ -24,7 +25,7 @@ function getSlotRecord(node: unknown): SlotRecord | null {
|
|||||||
return slots as SlotRecord
|
return slots as SlotRecord
|
||||||
}
|
}
|
||||||
|
|
||||||
export function SceneMaterialList() {
|
export function SceneMaterialList({ autoEditId }: { autoEditId?: SceneMaterialId | null }) {
|
||||||
const materials = useScene((state) => state.materials)
|
const materials = useScene((state) => state.materials)
|
||||||
const nodes = useScene((state) => state.nodes)
|
const nodes = useScene((state) => state.nodes)
|
||||||
const addSceneMaterial = useScene((state) => state.addSceneMaterial)
|
const addSceneMaterial = useScene((state) => state.addSceneMaterial)
|
||||||
@@ -68,6 +69,7 @@ export function SceneMaterialList() {
|
|||||||
<SceneMaterialRow
|
<SceneMaterialRow
|
||||||
addSceneMaterial={addSceneMaterial}
|
addSceneMaterial={addSceneMaterial}
|
||||||
activePaintTarget={activePaintTarget}
|
activePaintTarget={activePaintTarget}
|
||||||
|
autoEdit={autoEditId === id}
|
||||||
id={id}
|
id={id}
|
||||||
key={id}
|
key={id}
|
||||||
removeSceneMaterial={removeSceneMaterial}
|
removeSceneMaterial={removeSceneMaterial}
|
||||||
@@ -86,6 +88,7 @@ function SceneMaterialRow({
|
|||||||
sceneMaterial,
|
sceneMaterial,
|
||||||
usageCount,
|
usageCount,
|
||||||
activePaintTarget,
|
activePaintTarget,
|
||||||
|
autoEdit,
|
||||||
addSceneMaterial,
|
addSceneMaterial,
|
||||||
updateSceneMaterial,
|
updateSceneMaterial,
|
||||||
removeSceneMaterial,
|
removeSceneMaterial,
|
||||||
@@ -95,12 +98,14 @@ function SceneMaterialRow({
|
|||||||
sceneMaterial: SceneMaterial
|
sceneMaterial: SceneMaterial
|
||||||
usageCount: number
|
usageCount: number
|
||||||
activePaintTarget: ReturnType<typeof useEditor.getState>['activePaintTarget']
|
activePaintTarget: ReturnType<typeof useEditor.getState>['activePaintTarget']
|
||||||
|
autoEdit: boolean
|
||||||
addSceneMaterial: ReturnType<typeof useScene.getState>['addSceneMaterial']
|
addSceneMaterial: ReturnType<typeof useScene.getState>['addSceneMaterial']
|
||||||
updateSceneMaterial: ReturnType<typeof useScene.getState>['updateSceneMaterial']
|
updateSceneMaterial: ReturnType<typeof useScene.getState>['updateSceneMaterial']
|
||||||
removeSceneMaterial: ReturnType<typeof useScene.getState>['removeSceneMaterial']
|
removeSceneMaterial: ReturnType<typeof useScene.getState>['removeSceneMaterial']
|
||||||
setActivePaintMaterial: ReturnType<typeof useEditor.getState>['setActivePaintMaterial']
|
setActivePaintMaterial: ReturnType<typeof useEditor.getState>['setActivePaintMaterial']
|
||||||
}) {
|
}) {
|
||||||
const [isEditingMaterial, setIsEditingMaterial] = useState(false)
|
// A freshly-created material (via "+ Custom") mounts with its editor open.
|
||||||
|
const [isEditingMaterial, setIsEditingMaterial] = useState(autoEdit)
|
||||||
const [draftName, setDraftName] = useState(sceneMaterial.name)
|
const [draftName, setDraftName] = useState(sceneMaterial.name)
|
||||||
const swatchColor = sceneMaterial.material.properties?.color ?? '#ffffff'
|
const swatchColor = sceneMaterial.material.properties?.color ?? '#ffffff'
|
||||||
|
|
||||||
@@ -156,52 +161,68 @@ function SceneMaterialRow({
|
|||||||
Used by {usageCount} {usageCount === 1 ? 'part' : 'parts'}
|
Used by {usageCount} {usageCount === 1 ? 'part' : 'parts'}
|
||||||
</span>
|
</span>
|
||||||
<div className="flex items-center gap-1">
|
<div className="flex items-center gap-1">
|
||||||
<Button
|
<Tooltip>
|
||||||
aria-label="Paint with"
|
<TooltipTrigger asChild>
|
||||||
onClick={() =>
|
<Button
|
||||||
setActivePaintMaterial({
|
aria-label="Paint with"
|
||||||
material: sceneMaterial.material,
|
onClick={() =>
|
||||||
sourceTarget: activePaintTarget,
|
setActivePaintMaterial({
|
||||||
})
|
materialPreset: toSceneMaterialRef(id),
|
||||||
}
|
sourceTarget: activePaintTarget,
|
||||||
size="icon-sm"
|
})
|
||||||
title="Paint with"
|
}
|
||||||
type="button"
|
size="icon-sm"
|
||||||
variant="outline"
|
type="button"
|
||||||
>
|
variant="outline"
|
||||||
<Paintbrush />
|
>
|
||||||
</Button>
|
<Paintbrush />
|
||||||
<Button
|
</Button>
|
||||||
aria-label="Edit"
|
</TooltipTrigger>
|
||||||
aria-pressed={isEditingMaterial}
|
<TooltipContent>Paint with</TooltipContent>
|
||||||
onClick={() => setIsEditingMaterial((value) => !value)}
|
</Tooltip>
|
||||||
size="icon-sm"
|
<Tooltip>
|
||||||
title="Edit"
|
<TooltipTrigger asChild>
|
||||||
type="button"
|
<Button
|
||||||
variant={isEditingMaterial ? 'default' : 'outline'}
|
aria-label="Edit"
|
||||||
>
|
aria-pressed={isEditingMaterial}
|
||||||
<Pencil />
|
onClick={() => setIsEditingMaterial((value) => !value)}
|
||||||
</Button>
|
size="icon-sm"
|
||||||
<Button
|
type="button"
|
||||||
aria-label="Duplicate"
|
variant={isEditingMaterial ? 'default' : 'outline'}
|
||||||
onClick={duplicateMaterial}
|
>
|
||||||
size="icon-sm"
|
<Pencil />
|
||||||
title="Duplicate"
|
</Button>
|
||||||
type="button"
|
</TooltipTrigger>
|
||||||
variant="outline"
|
<TooltipContent>Edit</TooltipContent>
|
||||||
>
|
</Tooltip>
|
||||||
<Copy />
|
<Tooltip>
|
||||||
</Button>
|
<TooltipTrigger asChild>
|
||||||
<Button
|
<Button
|
||||||
aria-label="Delete"
|
aria-label="Duplicate"
|
||||||
onClick={() => removeSceneMaterial(id)}
|
onClick={duplicateMaterial}
|
||||||
size="icon-sm"
|
size="icon-sm"
|
||||||
title="Delete"
|
type="button"
|
||||||
type="button"
|
variant="outline"
|
||||||
variant="outline"
|
>
|
||||||
>
|
<Copy />
|
||||||
<Trash2 />
|
</Button>
|
||||||
</Button>
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>Duplicate</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<Button
|
||||||
|
aria-label="Delete"
|
||||||
|
onClick={() => removeSceneMaterial(id)}
|
||||||
|
size="icon-sm"
|
||||||
|
type="button"
|
||||||
|
variant="outline"
|
||||||
|
>
|
||||||
|
<Trash2 />
|
||||||
|
</Button>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>Delete</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -1,36 +0,0 @@
|
|||||||
'use client'
|
|
||||||
|
|
||||||
import useEditor from '../../../store/use-editor'
|
|
||||||
import { MaterialPropertiesEditor } from '../controls/material-properties-editor'
|
|
||||||
import { PanelSection } from '../controls/panel-section'
|
|
||||||
import { PanelWrapper } from './panel-wrapper'
|
|
||||||
|
|
||||||
export function PaintPanel() {
|
|
||||||
const activePaintMaterial = useEditor((state) => state.activePaintMaterial)
|
|
||||||
const activePaintTarget = useEditor((state) => state.activePaintTarget)
|
|
||||||
const setActivePaintMaterial = useEditor((state) => state.setActivePaintMaterial)
|
|
||||||
const setPaintPanelOpen = useEditor((state) => state.setPaintPanelOpen)
|
|
||||||
|
|
||||||
const customMaterial =
|
|
||||||
activePaintMaterial?.material?.properties && !activePaintMaterial.materialPreset
|
|
||||||
? activePaintMaterial.material
|
|
||||||
: null
|
|
||||||
|
|
||||||
if (!customMaterial) return null
|
|
||||||
|
|
||||||
return (
|
|
||||||
<PanelWrapper onClose={() => setPaintPanelOpen(false)} title="Material" width={320}>
|
|
||||||
<PanelSection title="Custom material">
|
|
||||||
<MaterialPropertiesEditor
|
|
||||||
onChange={(material) =>
|
|
||||||
setActivePaintMaterial({
|
|
||||||
material,
|
|
||||||
sourceTarget: activePaintMaterial?.sourceTarget ?? activePaintTarget,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
value={customMaterial}
|
|
||||||
/>
|
|
||||||
</PanelSection>
|
|
||||||
</PanelWrapper>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -29,7 +29,6 @@ import useEditor from '../../../store/use-editor'
|
|||||||
import { MobilePanelSheet } from './mobile-panel-sheet'
|
import { MobilePanelSheet } from './mobile-panel-sheet'
|
||||||
import { MobileSelectionBar } from './mobile-selection-bar'
|
import { MobileSelectionBar } from './mobile-selection-bar'
|
||||||
import { getNodeDisplay } from './node-display'
|
import { getNodeDisplay } from './node-display'
|
||||||
import { PaintPanel } from './paint-panel'
|
|
||||||
import { ParametricInspector } from './parametric-inspector'
|
import { ParametricInspector } from './parametric-inspector'
|
||||||
import { ReferencePanel } from './reference-panel'
|
import { ReferencePanel } from './reference-panel'
|
||||||
|
|
||||||
@@ -174,9 +173,6 @@ export function PanelManager({ inspectorFooter }: { inspectorFooter?: React.Reac
|
|||||||
const selectedZoneId = useViewer((s) => s.selection.zoneId)
|
const selectedZoneId = useViewer((s) => s.selection.zoneId)
|
||||||
const setSelection = useViewer((s) => s.setSelection)
|
const setSelection = useViewer((s) => s.setSelection)
|
||||||
const selectedReferenceId = useEditor((s) => s.selectedReferenceId)
|
const selectedReferenceId = useEditor((s) => s.selectedReferenceId)
|
||||||
const isPaintPanelOpen = useEditor((s) => s.isPaintPanelOpen)
|
|
||||||
const mode = useEditor((s) => s.mode)
|
|
||||||
const activePaintMaterial = useEditor((s) => s.activePaintMaterial)
|
|
||||||
// Only subscribe to the *type* of the single-selected node — string primitive
|
// Only subscribe to the *type* of the single-selected node — string primitive
|
||||||
// so we don't re-render on unrelated scene mutations.
|
// so we don't re-render on unrelated scene mutations.
|
||||||
const selectedNodeType = useScene((s) => {
|
const selectedNodeType = useScene((s) => {
|
||||||
@@ -208,15 +204,6 @@ export function PanelManager({ inspectorFooter }: { inspectorFooter?: React.Reac
|
|||||||
return <ReferencePanel />
|
return <ReferencePanel />
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
|
||||||
isPaintPanelOpen &&
|
|
||||||
mode === 'material-paint' &&
|
|
||||||
activePaintMaterial?.material?.properties &&
|
|
||||||
!activePaintMaterial.materialPreset
|
|
||||||
) {
|
|
||||||
return <PaintPanel />
|
|
||||||
}
|
|
||||||
|
|
||||||
if (selectedZoneId && selectedIds.length === 0) {
|
if (selectedZoneId && selectedIds.length === 0) {
|
||||||
return (
|
return (
|
||||||
<ParametricInspector
|
<ParametricInspector
|
||||||
|
|||||||
@@ -309,8 +309,6 @@ type EditorState = {
|
|||||||
primeMaterialPaintFromSelection: () => MaterialPaintSelectionSnapshot
|
primeMaterialPaintFromSelection: () => MaterialPaintSelectionSnapshot
|
||||||
hoveredPaintTarget: PaintableMaterialTarget | null
|
hoveredPaintTarget: PaintableMaterialTarget | null
|
||||||
setHoveredPaintTarget: (target: PaintableMaterialTarget | null) => void
|
setHoveredPaintTarget: (target: PaintableMaterialTarget | null) => void
|
||||||
isPaintPanelOpen: boolean
|
|
||||||
setPaintPanelOpen: (open: boolean) => void
|
|
||||||
selectedReferenceId: string | null
|
selectedReferenceId: string | null
|
||||||
setSelectedReferenceId: (id: string | null) => void
|
setSelectedReferenceId: (id: string | null) => void
|
||||||
guideUi: Record<string, GuideUiState>
|
guideUi: Record<string, GuideUiState>
|
||||||
@@ -854,8 +852,6 @@ const useEditor = create<EditorState>()(
|
|||||||
set((state) =>
|
set((state) =>
|
||||||
state.hoveredPaintTarget === target ? state : { hoveredPaintTarget: target },
|
state.hoveredPaintTarget === target ? state : { hoveredPaintTarget: target },
|
||||||
),
|
),
|
||||||
isPaintPanelOpen: false,
|
|
||||||
setPaintPanelOpen: (open) => set({ isPaintPanelOpen: open }),
|
|
||||||
selectedReferenceId: null,
|
selectedReferenceId: null,
|
||||||
setSelectedReferenceId: (id) => set({ selectedReferenceId: id }),
|
setSelectedReferenceId: (id) => set({ selectedReferenceId: id }),
|
||||||
guideUi: {},
|
guideUi: {},
|
||||||
|
|||||||
@@ -169,7 +169,14 @@ function buildPreviewMaterial(
|
|||||||
materialPreset: string | undefined,
|
materialPreset: string | undefined,
|
||||||
): Material | null {
|
): Material | null {
|
||||||
const shading = useViewer.getState().shading
|
const shading = useViewer.getState().shading
|
||||||
if (materialPreset) return createMaterialFromPresetRef(materialPreset, shading)
|
if (materialPreset) {
|
||||||
|
const parsed = parseMaterialRef(materialPreset)
|
||||||
|
if (parsed?.kind === 'scene') {
|
||||||
|
const sceneMaterial = useScene.getState().materials[parsed.id as SceneMaterialId]
|
||||||
|
return sceneMaterial ? createMaterial(sceneMaterial.material, shading) : null
|
||||||
|
}
|
||||||
|
return createMaterialFromPresetRef(materialPreset, shading)
|
||||||
|
}
|
||||||
if (material) return createMaterial(material, shading)
|
if (material) return createMaterial(material, shading)
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -134,7 +134,14 @@ export function buildSlotPreviewMaterial(
|
|||||||
materialPreset: string | undefined,
|
materialPreset: string | undefined,
|
||||||
): Material | null {
|
): Material | null {
|
||||||
const shading = useViewer.getState().shading
|
const shading = useViewer.getState().shading
|
||||||
if (materialPreset) return createMaterialFromPresetRef(materialPreset, shading)
|
if (materialPreset) {
|
||||||
|
const parsed = parseMaterialRef(materialPreset)
|
||||||
|
if (parsed?.kind === 'scene') {
|
||||||
|
const sceneMaterial = useScene.getState().materials[parsed.id as SceneMaterialId]
|
||||||
|
return sceneMaterial ? createMaterial(sceneMaterial.material, shading) : null
|
||||||
|
}
|
||||||
|
return createMaterialFromPresetRef(materialPreset, shading)
|
||||||
|
}
|
||||||
if (material) return createMaterial(material, shading)
|
if (material) return createMaterial(material, shading)
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user