'use client' import { type AnyNodeId, type CollectionId, type Control, type ControlValue, type ItemNode, useInteractive, useScene, } from '@pascal-app/core' import { ChevronDown, ChevronRight } from 'lucide-react' import { useMemo, useState } from 'react' import { useShallow } from 'zustand/react/shallow' import { cn } from '@/lib/utils' // ─── Shared control derivation ─────────────────────────────────────────────── type ItemControlRef = { itemId: AnyNodeId; controlIndex: number } type SharedControlDef = { kind: 'toggle' | 'slider' | 'temperature' label?: string min?: number max?: number step?: number unit?: string refs: ItemControlRef[] } function deriveSharedControls(items: ItemNode[]): SharedControlDef[] { if (items.length === 0) return [] const result: SharedControlDef[] = [] for (const kind of ['toggle', 'slider', 'temperature'] as const) { const refs: ItemControlRef[] = [] let ref: Control | null = null let allHave = true for (const item of items) { const idx = item.asset.interactive!.controls.findIndex((c) => c.kind === kind) if (idx === -1) { allHave = false; break } refs.push({ itemId: item.id, controlIndex: idx }) if (!ref) ref = item.asset.interactive!.controls[idx]! } if (!allHave || !ref) continue const def: SharedControlDef = { kind, label: ref.label, refs } if ('min' in ref) { def.min = ref.min; def.max = ref.max } if ('step' in ref) def.step = (ref as { step?: number }).step if ('unit' in ref) def.unit = (ref as { unit?: string }).unit result.push(def) } return result } // ─── Shared control widget ─────────────────────────────────────────────────── function SharedWidget({ def, value, onChange }: { def: SharedControlDef; value: ControlValue; onChange: (v: ControlValue) => void }) { if (def.kind === 'toggle') { return ( ) } return (
{def.label ?? def.kind} {value}{def.kind === 'temperature' ? '°' : ''}{def.unit ? ` ${def.unit}` : ''}
onChange(Number(e.target.value))} onPointerDown={(e) => e.stopPropagation()} className="w-full accent-white" />
) } // ─── Individual item control widget ────────────────────────────────────────── function ItemWidget({ control, value, onChange }: { control: Control; value: ControlValue; onChange: (v: ControlValue) => void }) { if (control.kind === 'toggle') { return ( ) } return (
{control.label ?? control.kind} {value}{control.kind === 'temperature' ? '°' : ''}{'unit' in control && control.unit ? ` ${control.unit}` : ''}
onChange(Number(e.target.value))} onPointerDown={(e) => e.stopPropagation()} className="w-full accent-white" />
) } // ─── Collection row ─────────────────────────────────────────────────────────── function CollectionRow({ collectionId }: { collectionId: CollectionId }) { const collection = useScene((s) => s.collections[collectionId]) const interactiveItems = useScene( useShallow((s) => (collection?.nodeIds ?? []) .map((id) => s.nodes[id]) .filter((n): n is ItemNode => n?.type === 'item' && !!n.asset.interactive) ), ) const allItems = useInteractive((s) => s.items) const controlValuesByItem = useMemo( () => Object.fromEntries(interactiveItems.map((n) => [n.id, allItems[n.id]?.controlValues ?? []])), [allItems, interactiveItems], ) const setControlValue = useInteractive((s) => s.setControlValue) const [expanded, setExpanded] = useState(false) const [expandedItemIds, setExpandedItemIds] = useState>(new Set()) if (!collection) return null const sharedControls = deriveSharedControls(interactiveItems) const getSharedValue = (def: SharedControlDef): ControlValue => { if (def.kind === 'toggle') { return def.refs.every(({ itemId, controlIndex }) => Boolean(controlValuesByItem[itemId]?.[controlIndex])) } const first = def.refs[0]! return controlValuesByItem[first.itemId]?.[first.controlIndex] ?? 0 } const setSharedValue = (def: SharedControlDef, value: ControlValue) => { for (const { itemId, controlIndex } of def.refs) { setControlValue(itemId, controlIndex, value) } } const toggleItemExpand = (id: AnyNodeId) => { setExpandedItemIds((prev) => { const next = new Set(prev) if (next.has(id)) next.delete(id) else next.add(id) return next }) } return (
{/* Header */} {/* Expanded */} {expanded && (
{interactiveItems.length === 0 ? (

No interactive items.

) : ( <> {/* Shared controls */} {sharedControls.length > 0 && (

All

{sharedControls.map((def, i) => ( setSharedValue(def, v)} /> ))}
)} {/* Individual items */} {interactiveItems.map((item) => { const isItemExpanded = expandedItemIds.has(item.id) const controls = item.asset.interactive!.controls const values = controlValuesByItem[item.id] ?? [] return (
{isItemExpanded && (
{controls.map((control, i) => ( setControlValue(item.id, i, v)} /> ))}
)}
) })} )}
)}
) } // ─── Main panel ─────────────────────────────────────────────────────────────── export function CollectionsPanel() { const collectionIds = useScene( useShallow((s) => Object.keys(s.collections) as CollectionId[]), ) if (collectionIds.length === 0) return null return (
Collections
{collectionIds.map((id) => ( ))}
) }