diff --git a/apps/editor/app/page.tsx b/apps/editor/app/page.tsx index 80fb1a96..a20826c0 100644 --- a/apps/editor/app/page.tsx +++ b/apps/editor/app/page.tsx @@ -1,13 +1,22 @@ 'use client' import { Editor, ItemsPanel } from '@pascal-app/editor' -import { Layers, Package, Settings } from 'lucide-react' +import { Hammer, Layers, Package, Settings } from 'lucide-react' +import Image from 'next/image' import Link from 'next/link' +import { BuildTab } from '@/components/build-tab' import { CommunityViewerToolbarLeft, CommunityViewerToolbarRight, } from '@/components/viewer-toolbar' +// The open-source editor only ships the built-in catalog (no uploaded items), +// so the Library/Community/Mine source chips and tag filters add nothing — +// drop them and keep the panel to plain categories. +function EditorItemsPanel() { + return +} + const SIDEBAR_TABS = [ { id: 'site', @@ -15,13 +24,47 @@ const SIDEBAR_TABS = [ component: () => null, mobileDefaultSnap: 0.5, mobileIcon: , + icon: ( + + ), + }, + { + id: 'build', + label: 'Build', + component: BuildTab, + mobileDefaultSnap: 0.5, + mobileIcon: , + icon: ( + + ), }, { id: 'items', label: 'Items', - component: ItemsPanel, + component: EditorItemsPanel, mobileDefaultSnap: 0.5, mobileIcon: , + icon: ( + + ), }, { id: 'settings', @@ -29,6 +72,15 @@ const SIDEBAR_TABS = [ component: () => null, mobileDefaultSnap: 0.5, mobileIcon: , + icon: ( + + ), }, ] diff --git a/apps/editor/components/build-tab.tsx b/apps/editor/components/build-tab.tsx new file mode 100644 index 00000000..48552e55 --- /dev/null +++ b/apps/editor/components/build-tab.tsx @@ -0,0 +1,160 @@ +'use client' + +import { MaterialPaintPanel, useEditor } from '@pascal-app/editor' +import Image from 'next/image' +import { useCallback, useEffect, useRef } from 'react' +import { + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger, +} from '@/components/toolbar-tooltip' +import { cn } from '@/lib/utils' + +/** + * Raw structure-tool kinds the Build tab can activate. These map 1:1 to the + * editor's `StructureTool` ids. + */ +type BuildToolKind = + | 'wall' + | 'fence' + | 'slab' + | 'ceiling' + | 'roof' + | 'stair' + | 'elevator' + | 'door' + | 'window' + | 'column' + | 'spawn' + +type BuildType = { + /** Selection id — equals `kind` for tool types, `'painting'` for paint mode. */ + id: string + label: string + iconSrc: string + /** Present for structure-tool types (absent for the paint mode). */ + kind?: BuildToolKind + /** Non-placement special mode. */ + mode?: 'material-paint' +} + +// Same icons + ordering as the community Build sidebar, minus presets. +const BUILD_TYPES: BuildType[] = [ + { id: 'wall', label: 'Wall', iconSrc: '/icons/wall.png', kind: 'wall' }, + { id: 'fence', label: 'Fence', iconSrc: '/icons/fence.png', kind: 'fence' }, + { id: 'slab', label: 'Slab', iconSrc: '/icons/floor.png', kind: 'slab' }, + { id: 'ceiling', label: 'Ceiling', iconSrc: '/icons/ceiling.png', kind: 'ceiling' }, + { id: 'roof', label: 'Roof', iconSrc: '/icons/roof.png', kind: 'roof' }, + { id: 'stair', label: 'Stairs', iconSrc: '/icons/stairs.png', kind: 'stair' }, + { id: 'elevator', label: 'Elevator', iconSrc: '/icons/elevator.png', kind: 'elevator' }, + { id: 'door', label: 'Door', iconSrc: '/icons/door.png', kind: 'door' }, + { id: 'window', label: 'Window', iconSrc: '/icons/window.png', kind: 'window' }, + { id: 'column', label: 'Column', iconSrc: '/icons/column.png', kind: 'column' }, + { id: 'spawn', label: 'Spawn Point', iconSrc: '/icons/site.png', kind: 'spawn' }, + { id: 'painting', label: 'Painting', iconSrc: '/icons/paint.png', mode: 'material-paint' }, +] + +/** + * Activate a raw structure draw/cursor tool. Mirrors the editor's own + * structure-tool activation (`setPhase`/`setStructureLayer`/`setMode`/`setTool`). + */ +function activateBuildTool(kind: BuildToolKind): void { + const ed = useEditor.getState() + ed.setPhase('structure') + ed.setStructureLayer('elements') + ed.setCatalogCategory(null) + ed.setToolDefaults(kind, null) + ed.setMode('build') + ed.setTool(kind) +} + +/** Enter material-paint mode — the Build tab's "Painting" category. */ +function activatePaintMode(): void { + const ed = useEditor.getState() + ed.setPhase('structure') + ed.setStructureLayer('elements') + ed.setMode('material-paint') +} + +/** + * Build tab for the open-source standalone editor — a preset-less replica of + * the community Build sidebar. Clicking a type activates its raw tool, drawn + * with the kind's own `def.defaults()`. The "Painting" type swaps in the + * material-paint panel. + */ +export function BuildTab() { + const activeTool = useEditor((s) => s.tool) + const mode = useEditor((s) => s.mode) + + const isTypeActive = (type: BuildType) => + type.mode === 'material-paint' + ? mode === 'material-paint' + : mode === 'build' && activeTool === type.kind + + const handleTypeClick = useCallback((type: BuildType) => { + if (type.mode === 'material-paint') { + activatePaintMode() + } else if (type.kind) { + activateBuildTool(type.kind) + } + }, []) + + // On open, land on the first build tool — parity with the community Build + // sidebar, so switching to Build immediately arms a usable tool. + const didInitRef = useRef(false) + useEffect(() => { + if (didInitRef.current) return + didInitRef.current = true + const firstType = BUILD_TYPES.find((t) => t.kind) + if (firstType) handleTypeClick(firstType) + }, [handleTypeClick]) + + return ( +
+ +
+ {BUILD_TYPES.map((type) => { + const active = isTypeActive(type) + return ( + + + + + + {type.label} + + + ) + })} +
+
+ + {mode === 'material-paint' ? ( +
+ +
+ ) : null} +
+ ) +} diff --git a/apps/editor/components/scene-loader.tsx b/apps/editor/components/scene-loader.tsx index 1d8dd224..a3706740 100644 --- a/apps/editor/components/scene-loader.tsx +++ b/apps/editor/components/scene-loader.tsx @@ -9,9 +9,12 @@ import { type SceneGraph, type SidebarTab, } from '@pascal-app/editor' +import { Hammer, Layers } from 'lucide-react' +import Image from 'next/image' import Link from 'next/link' import { useRouter } from 'next/navigation' import { useCallback, useEffect, useRef, useState } from 'react' +import { BuildTab } from './build-tab' import { CommunityViewerToolbarLeft, CommunityViewerToolbarRight } from './viewer-toolbar' export interface SceneMeta { @@ -32,6 +35,33 @@ const SIDEBAR_TABS: (SidebarTab & { component: React.ComponentType })[] = [ id: 'site', label: 'Scene', component: () => null, // Built-in SitePanel handles this + mobileDefaultSnap: 0.5, + mobileIcon: , + icon: ( + + ), + }, + { + id: 'build', + label: 'Build', + component: BuildTab, + mobileDefaultSnap: 0.5, + mobileIcon: , + icon: ( + + ), }, ] diff --git a/apps/editor/public/icons/scene.png b/apps/editor/public/icons/scene.png new file mode 100644 index 00000000..99036b8c Binary files /dev/null and b/apps/editor/public/icons/scene.png differ diff --git a/packages/editor/src/components/ui/sidebar/panels/items-panel/index.tsx b/packages/editor/src/components/ui/sidebar/panels/items-panel/index.tsx index 73d58092..6f3c61d2 100644 --- a/packages/editor/src/components/ui/sidebar/panels/items-panel/index.tsx +++ b/packages/editor/src/components/ui/sidebar/panels/items-panel/index.tsx @@ -20,6 +20,8 @@ export function ItemsPanel({ leadingTile, emptyState, functionTree, + showSourceFilter = true, + showTagFilters = true, }: { items?: AssetInput[] /** Called when the search query changes (community edition uses this for server-side search) */ @@ -41,6 +43,16 @@ export function ItemsPanel({ * hierarchical tree browse instead of the legacy hardcoded category tabs. */ functionTree?: FunctionTreeNode[] + /** + * Library/Community/Mine source chips. The open-source editor has no + * uploaded items (only the built-in catalog), so it hides these. + */ + showSourceFilter?: boolean + /** + * Placement/functional tag filter chips under the search row. The + * open-source editor hides these to keep the panel to plain categories. + */ + showTagFilters?: boolean }) { // When the embedder supplies a function taxonomy, the hierarchical browse // replaces the legacy `furnishTools` category tabs entirely. @@ -63,6 +75,8 @@ export function ItemsPanel({ leadingTile={leadingTile} onSearchChange={onSearchChange} searchResults={searchResults} + showSourceFilter={showSourceFilter} + showTagFilters={showTagFilters} /> } @@ -72,12 +86,16 @@ function LegacyItemsPanel({ searchResults, leadingTile, emptyState, + showSourceFilter = true, + showTagFilters = true, }: { items?: AssetInput[] onSearchChange?: (query: string) => void searchResults?: AssetInput[] | null leadingTile?: React.ReactNode emptyState?: React.ReactNode + showSourceFilter?: boolean + showTagFilters?: boolean }) { const mode = useEditor((s) => s.mode) const catalogCategory = useEditor((s) => s.catalogCategory) @@ -89,8 +107,11 @@ function LegacyItemsPanel({ const [activeFunctionalTag, setActiveFunctionalTag] = useState(null) // Library / Community / Mine. Default to Library so first-time users see // the curated catalog rather than every uploaded item; clicking the chip - // again clears the filter (`null` = show everything). - const [activeSource, setActiveSource] = useState('library') + // again clears the filter (`null` = show everything). With the chips hidden + // there is nothing to filter by, so start unfiltered. + const [activeSource, setActiveSource] = useState( + showSourceFilter ? 'library' : null, + ) const [search, setSearch] = useState('') const isServerSearch = onSearchChange !== undefined // True when server search is active but results haven't come back yet @@ -152,7 +173,7 @@ function LegacyItemsPanel({ 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 + const hasFilters = showTagFilters && allTags.length > 1 const placementCount = (tag: string | null) => categoryItems.filter((item) => { @@ -205,9 +226,13 @@ function LegacyItemsPanel({
{/* Search and source filter take 50/50 of the row. `min-w-0` on - both sides lets each half shrink to fit when the panel narrows. */} + both sides lets each half shrink to fit when the panel narrows. + With the source chips hidden, search spans the full row. */} { setSearch(e.target.value) onSearchChange?.(e.target.value) @@ -216,7 +241,7 @@ function LegacyItemsPanel({ type="text" value={search} /> - {sourceChips.length > 0 && ( + {showSourceFilter && sourceChips.length > 0 && (
{sourceChips.map((chip) => { const isActive = activeSource === chip.id