Merge remote-tracking branch 'origin/main' into feat/paint-slots
# Conflicts: # packages/core/src/store/use-scene.ts # packages/editor/src/components/editor/index.tsx
@@ -34,6 +34,7 @@ Read the relevant page in `wiki/architecture/` **before** writing code. The page
|
|||||||
|
|
||||||
- Adding a node type → `node-schemas.md`, `renderers.md`, `systems.md`
|
- Adding a node type → `node-schemas.md`, `renderers.md`, `systems.md`
|
||||||
- Adding a tool → `tools.md`, `spatial-queries.md`, `events.md`
|
- Adding a tool → `tools.md`, `spatial-queries.md`, `events.md`
|
||||||
|
- Adding / changing a placement or move interaction → `tools.md` ("2D ↔ 3D behavioral parity": applicable behaviors must exist in both views; port the change to the sibling 2D/3D file in the same PR)
|
||||||
- Adding a system → `systems.md`, `scene-registry.md`
|
- Adding a system → `systems.md`, `scene-registry.md`
|
||||||
- Anything in `packages/viewer` → `viewer-isolation.md`, `layers.md`
|
- Anything in `packages/viewer` → `viewer-isolation.md`, `layers.md`
|
||||||
- Anything touching selection → `selection-managers.md`, `scene-registry.md`, `events.md`
|
- Anything touching selection → `selection-managers.md`, `scene-registry.md`, `events.md`
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ const SIDEBAR_TABS = [
|
|||||||
alt=""
|
alt=""
|
||||||
className="h-8 w-8 object-contain"
|
className="h-8 w-8 object-contain"
|
||||||
height={32}
|
height={32}
|
||||||
src="/icons/scene.png"
|
src="/icons/scene.webp"
|
||||||
width={32}
|
width={32}
|
||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
@@ -45,7 +45,7 @@ const SIDEBAR_TABS = [
|
|||||||
alt=""
|
alt=""
|
||||||
className="h-8 w-8 object-contain"
|
className="h-8 w-8 object-contain"
|
||||||
height={32}
|
height={32}
|
||||||
src="/icons/build.png"
|
src="/icons/build.webp"
|
||||||
width={32}
|
width={32}
|
||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
@@ -61,7 +61,7 @@ const SIDEBAR_TABS = [
|
|||||||
alt=""
|
alt=""
|
||||||
className="h-8 w-8 object-contain"
|
className="h-8 w-8 object-contain"
|
||||||
height={32}
|
height={32}
|
||||||
src="/icons/couch.png"
|
src="/icons/couch.webp"
|
||||||
width={32}
|
width={32}
|
||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
@@ -77,7 +77,7 @@ const SIDEBAR_TABS = [
|
|||||||
alt=""
|
alt=""
|
||||||
className="h-8 w-8 object-contain"
|
className="h-8 w-8 object-contain"
|
||||||
height={32}
|
height={32}
|
||||||
src="/icons/settings.png"
|
src="/icons/settings.webp"
|
||||||
width={32}
|
width={32}
|
||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
import { nodeRegistry } from '@pascal-app/core'
|
import { nodeRegistry } from '@pascal-app/core'
|
||||||
import { MaterialPaintPanel, triggerSFX, useEditor } from '@pascal-app/editor'
|
import { MaterialPaintPanel, triggerSFX, useEditor } from '@pascal-app/editor'
|
||||||
|
import { useLiquidLineToolOptions } from '@pascal-app/nodes'
|
||||||
import Image from 'next/image'
|
import Image from 'next/image'
|
||||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||||
import {
|
import {
|
||||||
@@ -30,39 +31,82 @@ type BuildToolKind =
|
|||||||
| 'shelf'
|
| 'shelf'
|
||||||
| 'spawn'
|
| 'spawn'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* MEP (mechanical / plumbing) tool kinds surfaced under the Build tab's "MEP"
|
||||||
|
* group tile — its own sub-grid, like Roof's "Features".
|
||||||
|
*/
|
||||||
|
type MepToolKind =
|
||||||
|
| 'duct-segment'
|
||||||
|
| 'duct-fitting'
|
||||||
|
| 'duct-terminal'
|
||||||
|
| 'hvac-equipment'
|
||||||
|
| 'lineset'
|
||||||
|
| 'liquid-line'
|
||||||
|
| 'pipe-segment'
|
||||||
|
| 'pipe-fitting'
|
||||||
|
| 'pipe-trap'
|
||||||
|
|
||||||
type BuildType = {
|
type BuildType = {
|
||||||
/** Selection id — equals `kind` for tool types, `'painting'` for paint mode. */
|
/** Selection id — equals `kind` for tool types, `'painting'` for paint mode, `'mep'` for the MEP group. */
|
||||||
id: string
|
id: string
|
||||||
label: string
|
label: string
|
||||||
|
/** Raster asset tile (legacy Build sidebar artwork). */
|
||||||
iconSrc: string
|
iconSrc: string
|
||||||
/** Present for structure-tool types (absent for the paint mode). */
|
/** Present for structure-tool types (absent for paint mode and the MEP group). */
|
||||||
kind?: BuildToolKind
|
kind?: BuildToolKind
|
||||||
/** Non-placement special mode. */
|
/** Non-placement special mode. */
|
||||||
mode?: 'material-paint'
|
mode?: 'material-paint'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type MepItem = {
|
||||||
|
/** Selection id — equals `kind`. */
|
||||||
|
id: string
|
||||||
|
label: string
|
||||||
|
iconSrc: string
|
||||||
|
kind: MepToolKind
|
||||||
|
}
|
||||||
|
|
||||||
// Same icons + ordering as the community Build sidebar, minus presets.
|
// Same icons + ordering as the community Build sidebar, minus presets.
|
||||||
const BUILD_TYPES: BuildType[] = [
|
const BUILD_TYPES: BuildType[] = [
|
||||||
{ id: 'wall', label: 'Wall', iconSrc: '/icons/wall.png', kind: 'wall' },
|
{ id: 'wall', label: 'Wall', iconSrc: '/icons/wall.webp', kind: 'wall' },
|
||||||
{ id: 'fence', label: 'Fence', iconSrc: '/icons/fence.png', kind: 'fence' },
|
{ id: 'fence', label: 'Fence', iconSrc: '/icons/fence.webp', kind: 'fence' },
|
||||||
{ id: 'slab', label: 'Slab', iconSrc: '/icons/floor.png', kind: 'slab' },
|
{ id: 'slab', label: 'Slab', iconSrc: '/icons/floor.webp', kind: 'slab' },
|
||||||
{ id: 'ceiling', label: 'Ceiling', iconSrc: '/icons/ceiling.png', kind: 'ceiling' },
|
{ id: 'ceiling', label: 'Ceiling', iconSrc: '/icons/ceiling.webp', kind: 'ceiling' },
|
||||||
{ id: 'roof', label: 'Roof', iconSrc: '/icons/roof.png', kind: 'roof' },
|
{ id: 'roof', label: 'Roof', iconSrc: '/icons/roof.webp', kind: 'roof' },
|
||||||
{ id: 'stair', label: 'Stairs', iconSrc: '/icons/stairs.png', kind: 'stair' },
|
{ id: 'stair', label: 'Stairs', iconSrc: '/icons/stairs.webp', kind: 'stair' },
|
||||||
{ id: 'elevator', label: 'Elevator', iconSrc: '/icons/elevator.png', kind: 'elevator' },
|
{ id: 'elevator', label: 'Elevator', iconSrc: '/icons/elevator.webp', kind: 'elevator' },
|
||||||
{ id: 'door', label: 'Door', iconSrc: '/icons/door.png', kind: 'door' },
|
{ id: 'door', label: 'Door', iconSrc: '/icons/door.webp', kind: 'door' },
|
||||||
{ id: 'window', label: 'Window', iconSrc: '/icons/window.png', kind: 'window' },
|
{ id: 'window', label: 'Window', iconSrc: '/icons/window.webp', kind: 'window' },
|
||||||
{ id: 'column', label: 'Column', iconSrc: '/icons/column.png', kind: 'column' },
|
{ id: 'column', label: 'Column', iconSrc: '/icons/column.webp', kind: 'column' },
|
||||||
{ id: 'shelf', label: 'Shelf', iconSrc: '/icons/shelf.png', kind: 'shelf' },
|
{ id: 'shelf', label: 'Shelf', iconSrc: '/icons/shelf.webp', kind: 'shelf' },
|
||||||
{ id: 'spawn', label: 'Spawn Point', iconSrc: '/icons/spawn-point.png', kind: 'spawn' },
|
{ id: 'spawn', label: 'Spawn Point', iconSrc: '/icons/spawn-point.webp', kind: 'spawn' },
|
||||||
{ id: 'painting', label: 'Painting', iconSrc: '/icons/paint.png', mode: 'material-paint' },
|
// Group tile — no tool of its own; opens the MEP sub-grid below (like Roof).
|
||||||
|
{ id: 'mep', label: 'MEP', iconSrc: '/icons/HVAC.webp' },
|
||||||
|
{ id: 'painting', label: 'Painting', iconSrc: '/icons/paint.webp', mode: 'material-paint' },
|
||||||
|
]
|
||||||
|
|
||||||
|
// MEP sub-grid surfaced under the "MEP" tile — same icons + ordering the MEP
|
||||||
|
// tools had in the community Build sidebar.
|
||||||
|
const MEP_ITEMS: MepItem[] = [
|
||||||
|
{ id: 'duct-segment', label: 'Duct', iconSrc: '/icons/duct.webp', kind: 'duct-segment' },
|
||||||
|
{
|
||||||
|
id: 'duct-terminal',
|
||||||
|
label: 'Register',
|
||||||
|
iconSrc: '/icons/registers.webp',
|
||||||
|
kind: 'duct-terminal',
|
||||||
|
},
|
||||||
|
{ id: 'hvac-equipment', label: 'HVAC Unit', iconSrc: '/icons/HVAC.webp', kind: 'hvac-equipment' },
|
||||||
|
{ id: 'lineset', label: 'Lineset', iconSrc: '/icons/lineset.webp', kind: 'lineset' },
|
||||||
|
{ id: 'liquid-line', label: 'Liquid Line', iconSrc: '/icons/lineset.webp', kind: 'liquid-line' },
|
||||||
|
{ id: 'pipe-segment', label: 'DWV Pipe', iconSrc: '/icons/dwv-pipes.webp', kind: 'pipe-segment' },
|
||||||
|
{ id: 'pipe-trap', label: 'Trap', iconSrc: '/icons/dwv-pipes.webp', kind: 'pipe-trap' },
|
||||||
]
|
]
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Activate a raw structure draw/cursor tool. Mirrors the editor's own
|
* Activate a raw structure draw/cursor tool. Mirrors the editor's own
|
||||||
* structure-tool activation (`setPhase`/`setStructureLayer`/`setMode`/`setTool`).
|
* structure-tool activation (`setPhase`/`setStructureLayer`/`setMode`/`setTool`).
|
||||||
*/
|
*/
|
||||||
function activateBuildTool(kind: BuildToolKind): void {
|
function activateBuildTool(kind: BuildToolKind | MepToolKind): void {
|
||||||
const ed = useEditor.getState()
|
const ed = useEditor.getState()
|
||||||
ed.setPhase('structure')
|
ed.setPhase('structure')
|
||||||
ed.setStructureLayer('elements')
|
ed.setStructureLayer('elements')
|
||||||
@@ -82,7 +126,7 @@ function activatePaintMode(): void {
|
|||||||
|
|
||||||
type RoofFeature = { kind: string; label: string; iconSrc: string }
|
type RoofFeature = { kind: string; label: string; iconSrc: string }
|
||||||
|
|
||||||
const ROOF_FEATURE_FALLBACK_ICON = '/icons/roof.png'
|
const ROOF_FEATURE_FALLBACK_ICON = '/icons/roof.webp'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Roof accessories surfaced under the Roof tile (a "Features" group). Unlike
|
* Roof accessories surfaced under the Roof tile (a "Features" group). Unlike
|
||||||
@@ -111,10 +155,31 @@ function activateRoofFeatureTool(kind: string): void {
|
|||||||
export function BuildTab() {
|
export function BuildTab() {
|
||||||
const activeTool = useEditor((s) => s.tool)
|
const activeTool = useEditor((s) => s.tool)
|
||||||
const mode = useEditor((s) => s.mode)
|
const mode = useEditor((s) => s.mode)
|
||||||
// Which build tile's panel is showing. Roof is the only tile with a panel
|
const follow = useLiquidLineToolOptions((s) => s.follow)
|
||||||
// (its Features group); others arm a tool and show nothing below.
|
const toggleFollow = useLiquidLineToolOptions((s) => s.toggleFollow)
|
||||||
|
// Which build tile's panel is showing. Roof (Features) and MEP (its tool
|
||||||
|
// sub-grid) are the tiles with a panel; others arm a tool and show nothing
|
||||||
|
// below.
|
||||||
const [selectedTypeId, setSelectedTypeId] = useState<string | null>(null)
|
const [selectedTypeId, setSelectedTypeId] = useState<string | null>(null)
|
||||||
|
|
||||||
|
// The fitting / follow tools are armed from a segment's panel, not a grid
|
||||||
|
// tile — keep the segment tile lit so the panel (and the way back) stays
|
||||||
|
// visible.
|
||||||
|
const ductContext =
|
||||||
|
mode === 'build' && (activeTool === 'duct-segment' || activeTool === 'duct-fitting')
|
||||||
|
const pipeContext =
|
||||||
|
mode === 'build' && (activeTool === 'pipe-segment' || activeTool === 'pipe-fitting')
|
||||||
|
const liquidLineContext = mode === 'build' && activeTool === 'liquid-line'
|
||||||
|
|
||||||
|
const isMepItemActive = (item: MepItem) =>
|
||||||
|
item.kind === 'duct-segment'
|
||||||
|
? ductContext
|
||||||
|
: item.kind === 'pipe-segment'
|
||||||
|
? pipeContext
|
||||||
|
: item.kind === 'liquid-line'
|
||||||
|
? liquidLineContext
|
||||||
|
: mode === 'build' && activeTool === item.kind
|
||||||
|
|
||||||
// Read at render time (not module scope): the registry is populated by the
|
// Read at render time (not module scope): the registry is populated by the
|
||||||
// app bootstrap, so enumerating earlier would race it and see no kinds.
|
// app bootstrap, so enumerating earlier would race it and see no kinds.
|
||||||
const roofFeatures = useMemo<RoofFeature[]>(() => {
|
const roofFeatures = useMemo<RoofFeature[]>(() => {
|
||||||
@@ -141,6 +206,10 @@ export function BuildTab() {
|
|||||||
const handleTypeClick = useCallback((type: BuildType) => {
|
const handleTypeClick = useCallback((type: BuildType) => {
|
||||||
if (type.mode === 'material-paint') {
|
if (type.mode === 'material-paint') {
|
||||||
activatePaintMode()
|
activatePaintMode()
|
||||||
|
} else if (type.id === 'mep') {
|
||||||
|
// MEP is a group tile: arm its first tool so a usable tool is active
|
||||||
|
// (and we leave any prior paint mode), then reveal the MEP sub-grid.
|
||||||
|
activateBuildTool('duct-segment')
|
||||||
} else if (type.kind) {
|
} else if (type.kind) {
|
||||||
activateBuildTool(type.kind)
|
activateBuildTool(type.kind)
|
||||||
}
|
}
|
||||||
@@ -250,6 +319,137 @@ export function BuildTab() {
|
|||||||
</div>
|
</div>
|
||||||
</TooltipProvider>
|
</TooltipProvider>
|
||||||
</div>
|
</div>
|
||||||
|
) : selectedTypeId === 'mep' ? (
|
||||||
|
<div className="flex min-h-0 flex-1 flex-col gap-2 overflow-y-auto">
|
||||||
|
<div className="px-0.5 pt-1 font-medium text-muted-foreground text-xs">MEP</div>
|
||||||
|
<TooltipProvider delayDuration={0} disableHoverableContent>
|
||||||
|
<div
|
||||||
|
className="grid gap-1.5 px-0.5"
|
||||||
|
style={{ gridTemplateColumns: 'repeat(auto-fill, minmax(56px, 1fr))' }}
|
||||||
|
>
|
||||||
|
{MEP_ITEMS.map((item) => {
|
||||||
|
const active = isMepItemActive(item)
|
||||||
|
return (
|
||||||
|
<Tooltip key={item.id}>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<button
|
||||||
|
className={cn(
|
||||||
|
'group relative flex aspect-square items-center justify-center rounded-xl transition-all duration-200',
|
||||||
|
active
|
||||||
|
? 'bg-primary/10 ring-1 ring-primary/50'
|
||||||
|
: 'bg-muted/40 opacity-70 grayscale hover:bg-muted hover:opacity-100 hover:grayscale-0',
|
||||||
|
)}
|
||||||
|
onClick={() => {
|
||||||
|
triggerSFX('sfx:menu-click')
|
||||||
|
activateBuildTool(item.kind)
|
||||||
|
}}
|
||||||
|
onMouseEnter={() => triggerSFX('sfx:menu-hover')}
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
<Image
|
||||||
|
alt={item.label}
|
||||||
|
className="size-full object-contain transition-transform duration-200 group-hover:scale-110"
|
||||||
|
height={48}
|
||||||
|
src={item.iconSrc}
|
||||||
|
width={48}
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent className="pointer-events-none" side="top">
|
||||||
|
{item.label}
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</TooltipProvider>
|
||||||
|
|
||||||
|
{ductContext ? (
|
||||||
|
<div className="flex flex-col gap-1.5">
|
||||||
|
<span className="text-muted-foreground text-xs">Duct</span>
|
||||||
|
<button
|
||||||
|
className={cn(
|
||||||
|
'flex items-center gap-2 rounded-lg px-3 py-2 text-sm transition-all duration-200',
|
||||||
|
activeTool === 'duct-fitting'
|
||||||
|
? 'bg-primary/10 ring-1 ring-primary/50'
|
||||||
|
: 'bg-muted/40 hover:bg-muted',
|
||||||
|
)}
|
||||||
|
onClick={() => {
|
||||||
|
triggerSFX('sfx:menu-click')
|
||||||
|
activateBuildTool(activeTool === 'duct-fitting' ? 'duct-segment' : 'duct-fitting')
|
||||||
|
}}
|
||||||
|
onMouseEnter={() => triggerSFX('sfx:menu-hover')}
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
<Image
|
||||||
|
alt=""
|
||||||
|
aria-hidden
|
||||||
|
className="size-4 object-contain"
|
||||||
|
height={16}
|
||||||
|
src="/icons/duct-fitting.webp"
|
||||||
|
width={16}
|
||||||
|
/>
|
||||||
|
Add Fitting
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{pipeContext ? (
|
||||||
|
<div className="flex flex-col gap-1.5">
|
||||||
|
<span className="text-muted-foreground text-xs">DWV Pipe</span>
|
||||||
|
<button
|
||||||
|
className={cn(
|
||||||
|
'flex items-center gap-2 rounded-lg px-3 py-2 text-sm transition-all duration-200',
|
||||||
|
activeTool === 'pipe-fitting'
|
||||||
|
? 'bg-primary/10 ring-1 ring-primary/50'
|
||||||
|
: 'bg-muted/40 hover:bg-muted',
|
||||||
|
)}
|
||||||
|
onClick={() => {
|
||||||
|
triggerSFX('sfx:menu-click')
|
||||||
|
activateBuildTool(activeTool === 'pipe-fitting' ? 'pipe-segment' : 'pipe-fitting')
|
||||||
|
}}
|
||||||
|
onMouseEnter={() => triggerSFX('sfx:menu-hover')}
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
<Image
|
||||||
|
alt=""
|
||||||
|
aria-hidden
|
||||||
|
className="size-4 object-contain"
|
||||||
|
height={16}
|
||||||
|
src="/icons/duct-fitting.webp"
|
||||||
|
width={16}
|
||||||
|
/>
|
||||||
|
Add Fitting
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{liquidLineContext ? (
|
||||||
|
<div className="flex flex-col gap-1.5">
|
||||||
|
<span className="text-muted-foreground text-xs">Liquid Line</span>
|
||||||
|
<button
|
||||||
|
className={cn(
|
||||||
|
'flex items-center justify-between gap-2 rounded-lg px-3 py-2 text-sm transition-all duration-200',
|
||||||
|
follow ? 'bg-primary/10 ring-1 ring-primary/50' : 'bg-muted/40 hover:bg-muted',
|
||||||
|
)}
|
||||||
|
onClick={() => {
|
||||||
|
triggerSFX('sfx:menu-click')
|
||||||
|
toggleFollow()
|
||||||
|
}}
|
||||||
|
onMouseEnter={() => triggerSFX('sfx:menu-hover')}
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
<span>Follow lineset</span>
|
||||||
|
<span className="text-muted-foreground text-xs">{follow ? 'On' : 'Off'}</span>
|
||||||
|
</button>
|
||||||
|
<span className="px-1 text-[11px] text-muted-foreground">
|
||||||
|
{follow
|
||||||
|
? 'Click a lineset to lay the line beside it.'
|
||||||
|
: 'Trace a line alongside an existing lineset (F).'}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ const SIDEBAR_TABS: (SidebarTab & { component: React.ComponentType })[] = [
|
|||||||
alt=""
|
alt=""
|
||||||
className="h-8 w-8 object-contain"
|
className="h-8 w-8 object-contain"
|
||||||
height={32}
|
height={32}
|
||||||
src="/icons/scene.png"
|
src="/icons/scene.webp"
|
||||||
width={32}
|
width={32}
|
||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
@@ -58,7 +58,7 @@ const SIDEBAR_TABS: (SidebarTab & { component: React.ComponentType })[] = [
|
|||||||
alt=""
|
alt=""
|
||||||
className="h-8 w-8 object-contain"
|
className="h-8 w-8 object-contain"
|
||||||
height={32}
|
height={32}
|
||||||
src="/icons/build.png"
|
src="/icons/build.webp"
|
||||||
width={32}
|
width={32}
|
||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -86,7 +86,7 @@ const VIEW_MODES: { id: ViewMode; label: string; icon: React.ReactNode }[] = [
|
|||||||
alt=""
|
alt=""
|
||||||
className="h-3.5 w-3.5 object-contain"
|
className="h-3.5 w-3.5 object-contain"
|
||||||
height={14}
|
height={14}
|
||||||
src="/icons/building.png"
|
src="/icons/building.webp"
|
||||||
width={14}
|
width={14}
|
||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
@@ -99,7 +99,7 @@ const VIEW_MODES: { id: ViewMode; label: string; icon: React.ReactNode }[] = [
|
|||||||
alt=""
|
alt=""
|
||||||
className="h-3.5 w-3.5 object-contain"
|
className="h-3.5 w-3.5 object-contain"
|
||||||
height={14}
|
height={14}
|
||||||
src="/icons/blueprint.png"
|
src="/icons/blueprint.webp"
|
||||||
width={14}
|
width={14}
|
||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
@@ -121,9 +121,9 @@ const levelModeLabels: Record<string, string> = {
|
|||||||
|
|
||||||
const wallModeOrder = ['cutaway', 'up', 'down'] as const
|
const wallModeOrder = ['cutaway', 'up', 'down'] as const
|
||||||
const wallModeConfig: Record<string, { icon: string; label: string }> = {
|
const wallModeConfig: Record<string, { icon: string; label: string }> = {
|
||||||
up: { icon: '/icons/room.png', label: 'Full height' },
|
up: { icon: '/icons/room.webp', label: 'Full height' },
|
||||||
cutaway: { icon: '/icons/wallcut.png', label: 'Cutaway' },
|
cutaway: { icon: '/icons/wallcut.webp', label: 'Cutaway' },
|
||||||
down: { icon: '/icons/walllow.png', label: 'Low' },
|
down: { icon: '/icons/walllow.webp', label: 'Low' },
|
||||||
}
|
}
|
||||||
|
|
||||||
const SHADING_OPTIONS = [
|
const SHADING_OPTIONS = [
|
||||||
|
|||||||
|
After Width: | Height: | Size: 141 KiB |
|
Before Width: | Height: | Size: 89 KiB |
|
After Width: | Height: | Size: 17 KiB |
|
Before Width: | Height: | Size: 138 KiB |
|
After Width: | Height: | Size: 20 KiB |
|
Before Width: | Height: | Size: 5.9 KiB |
|
After Width: | Height: | Size: 5.0 KiB |
|
Before Width: | Height: | Size: 98 KiB |
|
After Width: | Height: | Size: 22 KiB |
|
Before Width: | Height: | Size: 136 KiB |
|
After Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 121 KiB |
|
After Width: | Height: | Size: 19 KiB |
|
Before Width: | Height: | Size: 95 KiB |
|
After Width: | Height: | Size: 26 KiB |
|
Before Width: | Height: | Size: 32 KiB |
|
After Width: | Height: | Size: 6.4 KiB |
|
Before Width: | Height: | Size: 19 KiB |
|
After Width: | Height: | Size: 3.0 KiB |
|
Before Width: | Height: | Size: 134 KiB |
|
After Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 78 KiB |
|
After Width: | Height: | Size: 9.7 KiB |
|
Before Width: | Height: | Size: 28 KiB |
|
After Width: | Height: | Size: 6.8 KiB |
|
Before Width: | Height: | Size: 86 KiB |
|
After Width: | Height: | Size: 10 KiB |
|
After Width: | Height: | Size: 84 KiB |
|
After Width: | Height: | Size: 84 KiB |
|
After Width: | Height: | Size: 59 KiB |
|
Before Width: | Height: | Size: 124 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 162 KiB |
|
After Width: | Height: | Size: 22 KiB |
|
Before Width: | Height: | Size: 103 KiB |
|
After Width: | Height: | Size: 19 KiB |
|
Before Width: | Height: | Size: 28 KiB |
|
After Width: | Height: | Size: 7.3 KiB |
|
Before Width: | Height: | Size: 109 KiB |
|
After Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 50 KiB |
|
After Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 144 KiB |
|
After Width: | Height: | Size: 23 KiB |
|
Before Width: | Height: | Size: 99 KiB |
|
After Width: | Height: | Size: 19 KiB |
|
After Width: | Height: | Size: 122 KiB |
|
Before Width: | Height: | Size: 90 KiB |
|
After Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 66 KiB |
|
After Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 97 KiB |
|
After Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 77 KiB |
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 87 KiB |
|
Before Width: | Height: | Size: 84 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 35 KiB |
|
After Width: | Height: | Size: 8.6 KiB |
|
Before Width: | Height: | Size: 78 KiB |
|
After Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 322 KiB |
|
After Width: | Height: | Size: 42 KiB |
|
Before Width: | Height: | Size: 85 KiB |
|
After Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 122 KiB |
|
After Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 409 KiB |
|
After Width: | Height: | Size: 48 KiB |
|
Before Width: | Height: | Size: 52 KiB |
|
After Width: | Height: | Size: 20 KiB |
|
Before Width: | Height: | Size: 71 KiB |
|
After Width: | Height: | Size: 8.8 KiB |
|
Before Width: | Height: | Size: 85 KiB |
|
After Width: | Height: | Size: 30 KiB |
|
Before Width: | Height: | Size: 48 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 96 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 50 KiB |
|
After Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 30 KiB |
|
After Width: | Height: | Size: 5.5 KiB |
|
Before Width: | Height: | Size: 101 KiB |
|
After Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 65 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 75 KiB |
|
After Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 95 KiB |
|
After Width: | Height: | Size: 26 KiB |
@@ -115,6 +115,7 @@ export default function IfcConverter() {
|
|||||||
return results
|
return results
|
||||||
}, [pascalData, searchQuery])
|
}, [pascalData, searchQuery])
|
||||||
|
|
||||||
|
// biome-ignore lint/correctness/useExhaustiveDependencies: runs once on mount to load the initial file from the URL.
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const params = new URLSearchParams(window.location.search)
|
const params = new URLSearchParams(window.location.search)
|
||||||
const requested = params.get('file')
|
const requested = params.get('file')
|
||||||
@@ -201,6 +202,7 @@ export default function IfcConverter() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// biome-ignore lint/correctness/useExhaustiveDependencies: stable drop handler; handleFile only calls setState setters, so a mount-time capture stays correct.
|
||||||
const handleDrop = useCallback((e: React.DragEvent) => {
|
const handleDrop = useCallback((e: React.DragEvent) => {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
setIsDragging(false)
|
setIsDragging(false)
|
||||||
@@ -234,7 +236,7 @@ export default function IfcConverter() {
|
|||||||
const url = URL.createObjectURL(blob)
|
const url = URL.createObjectURL(blob)
|
||||||
const a = document.createElement('a')
|
const a = document.createElement('a')
|
||||||
a.href = url
|
a.href = url
|
||||||
a.download = fileName.replace('.ifc', '') + '_pascal.json'
|
a.download = `${fileName.replace('.ifc', '')}_pascal.json`
|
||||||
a.click()
|
a.click()
|
||||||
URL.revokeObjectURL(url)
|
URL.revokeObjectURL(url)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,13 +11,22 @@ import type {
|
|||||||
DoorNode,
|
DoorNode,
|
||||||
DormerNode,
|
DormerNode,
|
||||||
DownspoutNode,
|
DownspoutNode,
|
||||||
|
DuctFittingNode,
|
||||||
|
DuctSegmentNode,
|
||||||
|
DuctTerminalNode,
|
||||||
ElevatorNode,
|
ElevatorNode,
|
||||||
EyebrowVentNode,
|
EyebrowVentNode,
|
||||||
FenceNode,
|
FenceNode,
|
||||||
GuideNode,
|
GuideNode,
|
||||||
GutterNode,
|
GutterNode,
|
||||||
|
HvacEquipmentNode,
|
||||||
ItemNode,
|
ItemNode,
|
||||||
LevelNode,
|
LevelNode,
|
||||||
|
LinesetNode,
|
||||||
|
LiquidLineNode,
|
||||||
|
PipeFittingNode,
|
||||||
|
PipeSegmentNode,
|
||||||
|
PipeTrapNode,
|
||||||
RidgeVentNode,
|
RidgeVentNode,
|
||||||
RoofNode,
|
RoofNode,
|
||||||
RoofSegmentNode,
|
RoofSegmentNode,
|
||||||
@@ -107,6 +116,15 @@ export type SolarPanelEvent = NodeEvent<SolarPanelNode>
|
|||||||
export type SkylightEvent = NodeEvent<SkylightNode>
|
export type SkylightEvent = NodeEvent<SkylightNode>
|
||||||
export type DormerEvent = NodeEvent<DormerNode>
|
export type DormerEvent = NodeEvent<DormerNode>
|
||||||
export type DownspoutEvent = NodeEvent<DownspoutNode>
|
export type DownspoutEvent = NodeEvent<DownspoutNode>
|
||||||
|
export type DuctSegmentEvent = NodeEvent<DuctSegmentNode>
|
||||||
|
export type DuctFittingEvent = NodeEvent<DuctFittingNode>
|
||||||
|
export type DuctTerminalEvent = NodeEvent<DuctTerminalNode>
|
||||||
|
export type HvacEquipmentEvent = NodeEvent<HvacEquipmentNode>
|
||||||
|
export type PipeSegmentEvent = NodeEvent<PipeSegmentNode>
|
||||||
|
export type PipeFittingEvent = NodeEvent<PipeFittingNode>
|
||||||
|
export type PipeTrapEvent = NodeEvent<PipeTrapNode>
|
||||||
|
export type LinesetEvent = NodeEvent<LinesetNode>
|
||||||
|
export type LiquidLineEvent = NodeEvent<LiquidLineNode>
|
||||||
|
|
||||||
// Event suffixes - exported for use in hooks
|
// Event suffixes - exported for use in hooks
|
||||||
export const eventSuffixes = [
|
export const eventSuffixes = [
|
||||||
@@ -261,6 +279,15 @@ type EditorEvents = GridEvents &
|
|||||||
NodeEvents<'skylight', SkylightEvent> &
|
NodeEvents<'skylight', SkylightEvent> &
|
||||||
NodeEvents<'dormer', DormerEvent> &
|
NodeEvents<'dormer', DormerEvent> &
|
||||||
NodeEvents<'downspout', DownspoutEvent> &
|
NodeEvents<'downspout', DownspoutEvent> &
|
||||||
|
NodeEvents<'duct-segment', DuctSegmentEvent> &
|
||||||
|
NodeEvents<'duct-fitting', DuctFittingEvent> &
|
||||||
|
NodeEvents<'duct-terminal', DuctTerminalEvent> &
|
||||||
|
NodeEvents<'hvac-equipment', HvacEquipmentEvent> &
|
||||||
|
NodeEvents<'pipe-segment', PipeSegmentEvent> &
|
||||||
|
NodeEvents<'pipe-fitting', PipeFittingEvent> &
|
||||||
|
NodeEvents<'pipe-trap', PipeTrapEvent> &
|
||||||
|
NodeEvents<'lineset', LinesetEvent> &
|
||||||
|
NodeEvents<'liquid-line', LiquidLineEvent> &
|
||||||
CameraControlEvents &
|
CameraControlEvents &
|
||||||
ToolEvents &
|
ToolEvents &
|
||||||
GuideEvents &
|
GuideEvents &
|
||||||
|
|||||||
@@ -93,6 +93,14 @@ export {
|
|||||||
type Space,
|
type Space,
|
||||||
wallTouchesOthers,
|
wallTouchesOthers,
|
||||||
} from './lib/space-detection'
|
} from './lib/space-detection'
|
||||||
|
export {
|
||||||
|
closestOnSegment,
|
||||||
|
collectLevelWallSegments,
|
||||||
|
nearestWallSegment,
|
||||||
|
WALL_SNAP_DISTANCE_M,
|
||||||
|
type WallSegment,
|
||||||
|
type WallSegmentClosest,
|
||||||
|
} from './lib/wall-distance'
|
||||||
export {
|
export {
|
||||||
getCatalogMaterialById,
|
getCatalogMaterialById,
|
||||||
getLibraryMaterialIdFromRef,
|
getLibraryMaterialIdFromRef,
|
||||||
|
|||||||