Merge pull request #409 from pascalorg/feat/colored-monochrome-items

Colored/Monochrome: items show real materials + Render-menu toggle
This commit is contained in:
Wassim SAMAD
2026-06-15 17:44:55 -04:00
committed by GitHub
2 changed files with 32 additions and 14 deletions
@@ -26,8 +26,10 @@ import {
ChevronRight,
Diamond,
Layers,
Palette,
PenLine,
Sparkles,
Square,
} from 'lucide-react'
import Link from 'next/link'
import { useShallow } from 'zustand/react/shallow'
@@ -37,6 +39,7 @@ import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from './ui/primitives/dropdown-menu'
import { TooltipProvider } from './ui/primitives/tooltip'
@@ -87,8 +90,14 @@ const SHADING_OPTIONS = [
{ id: 'rendered', name: 'Rendered', detail: 'Full ambient occlusion', icon: Sparkles },
] as const
const TEXTURE_OPTIONS = [
{ id: true, name: 'Colored', detail: 'Show materials, textures & colours', icon: Palette },
{ id: false, name: 'Monochrome', detail: 'Flat clay surfaces by role', icon: Square },
] as const
function RenderModeMenu() {
const shading = useViewer((s) => s.shading)
const textures = useViewer((s) => s.textures)
const active = SHADING_OPTIONS.find((o) => o.id === shading) ?? SHADING_OPTIONS[0]
const ActiveIcon = active.icon
return (
@@ -121,6 +130,23 @@ function RenderModeMenu() {
</DropdownMenuItem>
)
})}
<DropdownMenuSeparator />
{TEXTURE_OPTIONS.map((option) => {
const OptionIcon = option.icon
return (
<DropdownMenuItem
key={option.name}
onSelect={() => useViewer.getState().setTextures(option.id)}
>
<OptionIcon />
<div className="flex flex-col">
<span className="text-foreground">{option.name}</span>
<span className="text-muted-foreground text-xs">{option.detail}</span>
</div>
{textures === option.id ? <Check className="ml-auto text-foreground" /> : null}
</DropdownMenuItem>
)
})}
</DropdownMenuContent>
</DropdownMenu>
)
+6 -14
View File
@@ -9,16 +9,15 @@ import {
type ItemNode,
isSlotMaterialName,
LIBRARY_MATERIAL_REF_PREFIX,
type LightEffect,
SCENE_MATERIAL_REF_PREFIX,
toLibraryMaterialRef,
type LightEffect,
useInteractive,
useLiveNodeOverrides,
useRegistry,
useScene,
} from '@pascal-app/core'
import {
baseMaterial,
type ColorPreset,
createDefaultMaterial,
createSurfaceRoleMaterial,
@@ -121,11 +120,6 @@ const captureItemMeshMaterials = (mesh: Mesh): CapturedItemMaterialData => {
return next
}
const hasCapturedSlot = (captured: CapturedItemMaterialData): boolean =>
Array.isArray(captured.slotIds)
? captured.slotIds.some((slotId) => slotId != null)
: captured.slotIds != null
const isCapturedMaterialArray = (
captured: CapturedItemMaterialData,
): captured is CapturedMultiItemMaterialData => Array.isArray(captured.authoredMaterials)
@@ -155,20 +149,19 @@ const resolveItemMaterial = (
curatedRef: string | undefined,
{
colorPreset,
isAuthored,
nodeSlots,
sceneMaterials,
shading,
textures,
}: {
colorPreset: ColorPreset
isAuthored: boolean
nodeSlots: ItemNode['slots']
sceneMaterials: SceneMaterials
shading: RenderShading
textures: boolean
},
): Material => {
// Monochrome (textures off): collapse to the themed furnishing clay colour.
if (!textures) return createSurfaceRoleMaterial('furnishing', colorPreset)
if (authoredMaterial.name.toLowerCase() === 'glass') return glassMaterial
if (slotId != null) {
@@ -178,8 +171,10 @@ const resolveItemMaterial = (
if (curated) return curated
return authoredMaterial
}
if (isAuthored) return authoredMaterial
return baseMaterial(shading)
// Colored (textures on): show the item's real authored material — its
// textures, vertex colours, and default colours — for every item, not just
// slot-authored ones (no more strip-to-clay default).
return authoredMaterial
}
const BrokenItemFallback = ({ node }: { node: ItemNode }) => {
@@ -334,7 +329,6 @@ const ModelRenderer = ({ node }: { node: ItemNode }) => {
if (!root) return
const meshEntries: { mesh: Mesh; captured: CapturedItemMaterialData }[] = []
let isAuthored = false
root.traverse((child) => {
if (!(child as Mesh).isMesh) return
@@ -345,13 +339,11 @@ const ModelRenderer = ({ node }: { node: ItemNode }) => {
}
const captured = captureItemMeshMaterials(mesh)
if (hasCapturedSlot(captured)) isAuthored = true
if (mesh.name !== 'cutout') meshEntries.push({ mesh, captured })
})
const materialOptions = {
colorPreset,
isAuthored,
nodeSlots: node.slots,
sceneMaterials,
shading,