diff --git a/apps/editor/components/ui/action-menu/index.tsx b/apps/editor/components/ui/action-menu/index.tsx index cc08a43c..0579c69b 100644 --- a/apps/editor/components/ui/action-menu/index.tsx +++ b/apps/editor/components/ui/action-menu/index.tsx @@ -34,7 +34,7 @@ export function ActionMenu({ className }: { className?: string }) { {mode === "build" && tool === "item" && catalogCategory && ( state.selectedItem); const setSelectedItem = useEditor((state) => state.setSelectedItem); + const [activePlacementTag, setActivePlacementTag] = useState(null); + const [activeFunctionalTag, setActiveFunctionalTag] = useState(null); - const filteredItems = CATALOG_ITEMS.filter( + // Reset tag filters when category changes + useEffect(() => { + setActivePlacementTag(null); + setActiveFunctionalTag(null); + }, [category]); + + const categoryItems = CATALOG_ITEMS.filter( (item) => item.category === category, ); + // Collect tags available in this category + const allTags = Array.from( + new Set(categoryItems.flatMap((item) => item.tags ?? [])), + ); + const placementTags = allTags.filter((t) => PLACEMENT_TAGS.has(t)); + const functionalTags = allTags.filter((t) => !PLACEMENT_TAGS.has(t)); + const hasFilters = allTags.length > 1; + + // Count items for a placement tag given the current functional filter + const placementCount = (tag: string | null) => + categoryItems.filter((item) => { + const tags = item.tags ?? []; + if (tag !== null && !tags.includes(tag)) return false; + if (activeFunctionalTag && !tags.includes(activeFunctionalTag)) return false; + return true; + }).length; + + // Count items for a functional tag given the current placement filter + const functionalCount = (tag: string) => + categoryItems.filter((item) => { + const tags = item.tags ?? []; + if (!tags.includes(tag)) return false; + if (activePlacementTag && !tags.includes(activePlacementTag)) return false; + return true; + }).length; + + const filteredItems = categoryItems.filter((item) => { + const tags = item.tags ?? []; + if (activePlacementTag && !tags.includes(activePlacementTag)) return false; + if (activeFunctionalTag && !tags.includes(activeFunctionalTag)) return false; + return true; + }); + // Auto-select first item if current selection is not in the filtered list useEffect(() => { const isCurrentItemInCategory = filteredItems.some( @@ -44,50 +86,134 @@ export function ItemCatalog({ category }: { category: CatalogCategory }) { }; return ( -
- {filteredItems.map((item, index) => { - const isSelected = selectedItem?.src === item?.src; - const attachmentIcon = getAttachmentIcon(item?.attachTo); - return ( - - +
+ {/* Filter chips */} + {hasFilters && ( +
+ {/* Placement row */} + {placementTags.length > 0 && ( +
- - - {item.name} - - - ); - })} + {placementTags.map((tag) => { + const count = placementCount(tag); + const isActive = activePlacementTag === tag; + const isEmpty = count === 0 && !isActive; + return ( + + ); + })} +
+ )} + + {/* Functional row */} + {functionalTags.length > 0 && ( +
+ {functionalTags.map((tag) => { + const count = functionalCount(tag); + const isActive = activeFunctionalTag === tag; + const isEmpty = count === 0 && !isActive; + return ( + + ); + })} +
+ )} +
+ )} + + {/* Items */} +
+ {filteredItems.map((item, index) => { + const isSelected = selectedItem?.src === item?.src; + const attachmentIcon = getAttachmentIcon(item?.attachTo); + return ( + + + + + + {item.name} + + + ); + })} +
); } diff --git a/packages/core/src/schema/nodes/item.ts b/packages/core/src/schema/nodes/item.ts index e6e0122b..b4b3f7f5 100644 --- a/packages/core/src/schema/nodes/item.ts +++ b/packages/core/src/schema/nodes/item.ts @@ -10,6 +10,7 @@ const assetSchema = z.object({ src: z.string(), dimensions: z.tuple([z.number(), z.number(), z.number()]).default([1, 1, 1]), // [w, h, d] attachTo: z.enum(['wall', 'wall-side', 'ceiling']).optional(), + tags: z.array(z.string()).optional(), // These are "Corrective" transforms to normalize the GLB offset: z.tuple([z.number(), z.number(), z.number()]).default([0, 0, 0]), rotation: z.tuple([z.number(), z.number(), z.number()]).default([0, 0, 0]), @@ -42,6 +43,7 @@ export const ItemNode = BaseNode.extend({ - offset: corrective position offset for the model - rotation: corrective rotation for the model - scale: corrective scale for the model + - tags: tags associated with the item `) export type ItemNode = z.infer diff --git a/packages/viewer/src/components/viewer/post-processing.tsx b/packages/viewer/src/components/viewer/post-processing.tsx index c0310b27..697952d4 100644 --- a/packages/viewer/src/components/viewer/post-processing.tsx +++ b/packages/viewer/src/components/viewer/post-processing.tsx @@ -26,7 +26,7 @@ import useViewer from '../../store/use-viewer' // SSGI Parameters - adjust these to fine-tune global illumination and ambient occlusion export const SSGI_PARAMS = { enabled: true, - sliceCount: 1, + sliceCount: 2, stepCount: 8, radius: 1, expFactor: 1.5,