Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
64 commits
Select commit Hold shift + click to select a range
3731eb3
Add roof surface placement support for items
sudhir9297 May 18, 2026
ed53bc2
Merge branch 'main' of github.com:pascalorg/editor
sudhir9297 May 20, 2026
fd8e02c
Merge branch 'main' of github.com:pascalorg/editor
sudhir9297 May 20, 2026
7c1e383
fixed conflict
sudhir9297 May 20, 2026
b3377da
Merge branch 'main' of github.com:pascalorg/editor
sudhir9297 May 20, 2026
f177a65
Merge branch 'main' of github.com:pascalorg/editor
sudhir9297 May 22, 2026
9af7491
Merge branch 'main' of github.com:pascalorg/editor
sudhir9297 May 22, 2026
fd27524
Merge branch 'main' of github.com:pascalorg/editor
sudhir9297 May 27, 2026
b516298
Merge branch 'main' of github.com:pascalorg/editor
sudhir9297 May 28, 2026
ebfc8ce
Merge branch 'main' of github.com:pascalorg/editor
sudhir9297 Jun 3, 2026
b7b313b
Merge branch 'main' of github.com:pascalorg/editor
sudhir9297 Jun 4, 2026
b2ad645
Merge branch 'main' of github.com:pascalorg/editor
sudhir9297 Jun 4, 2026
bffdb4a
Merge branch 'main' of github.com:pascalorg/editor
sudhir9297 Jun 8, 2026
ee7b10c
Merge branch 'main' of github.com:pascalorg/editor
sudhir9297 Jun 9, 2026
7d4b474
Merge branch 'main' of github.com:pascalorg/editor
sudhir9297 Jun 10, 2026
3a3318c
Merge branch 'main' of github.com:pascalorg/editor
sudhir9297 Jun 13, 2026
26df69f
Merge branch 'main' of github.com:pascalorg/editor
sudhir9297 Jun 17, 2026
5376e07
Merge branch 'main' of github.com:pascalorg/editor
sudhir9297 Jun 22, 2026
d2204aa
Merge branch 'main' of github.com:pascalorg/editor
sudhir9297 Jun 23, 2026
f2a5186
Merge branch 'main' of github.com:pascalorg/editor
sudhir9297 Jun 29, 2026
5841052
Merge branch 'main' of github.com:pascalorg/editor
sudhir9297 Jul 1, 2026
a6acaa3
Merge branch 'main' of github.com:pascalorg/editor
sudhir9297 Jul 8, 2026
e0fec5b
Merge branch 'main' of github.com:pascalorg/editor
sudhir9297 Jul 10, 2026
7fa9276
Merge branch 'main' of github.com:pascalorg/editor
sudhir9297 Jul 13, 2026
c3ff9d6
Merge branch 'main' of github.com:pascalorg/editor
sudhir9297 Jul 14, 2026
00d84d5
Merge branch 'main' of github.com:pascalorg/editor
sudhir9297 Jul 19, 2026
2c2dabc
Merge branch 'main' of github.com:pascalorg/editor
sudhir9297 Jul 22, 2026
29f914f
Merge branch 'main' of github.com:pascalorg/editor
sudhir9297 Jul 22, 2026
1cbf910
Merge branch 'main' of github.com:pascalorg/editor
sudhir9297 Jul 23, 2026
8f59da7
Merge branch 'main' of github.com:pascalorg/editor
sudhir9297 Jul 24, 2026
650abd6
Merge branch 'main' of github.com:pascalorg/editor
sudhir9297 Jul 27, 2026
1f8dc8b
Merge branch 'main' of github.com:pascalorg/editor
sudhir9297 Jul 28, 2026
7c4b791
Merge branch 'main' of github.com:pascalorg/editor
sudhir9297 Jul 31, 2026
ef7184f
Merge branch 'main' of github.com:sudhir9297/editor; branch 'main' of…
sudhir9297 Aug 10, 2026
c92a376
Merge branch 'main' of github.com:pascalorg/editor
sudhir9297 Aug 12, 2026
da49781
Merge branch 'main' of github.com:pascalorg/editor
sudhir9297 Aug 19, 2026
0f66e8f
Merge branch 'main' of github.com:sudhir9297/editor; branch 'main' of…
sudhir9297 Aug 21, 2026
0928a63
Merge branch 'main' of github.com:pascalorg/editor
sudhir9297 Aug 24, 2026
fc58e4f
feat: improve roof placement and hosted extensions
sudhir9297 Aug 24, 2026
91ef1ec
fix: preserve curved lean-to roof connections
sudhir9297 Aug 24, 2026
af98936
fix roof and dormer editing behavior
sudhir9297 Aug 24, 2026
6ae92c1
feat: improve roof openings and lean-to canopies
sudhir9297 Aug 25, 2026
dc6b4a9
Fix centered lean-to placement
sudhir9297 Aug 25, 2026
c03ab30
Align placement previews with architecture
sudhir9297 Aug 25, 2026
40c6c8a
Merge canonical main into placement preview branch
sudhir9297 Aug 25, 2026
1f790d3
Fix dormer window placement grid orientation
sudhir9297 Aug 25, 2026
e48fe73
fix roof canopy geometry and continuous placement
sudhir9297 Aug 26, 2026
52cd22d
stabilize canopy miter quality assertions
sudhir9297 Aug 26, 2026
0ed0424
stabilize canopy gutter quality assertions
sudhir9297 Aug 26, 2026
a2a4c85
fix conical roof hover elevation across levels
sudhir9297 Aug 26, 2026
a15ca02
fix conical roof placement on higher floors
sudhir9297 Aug 26, 2026
7fb4340
limit conical wall roofs to adjacent levels
sudhir9297 Aug 26, 2026
a0f7e77
limit continuous mono canopy mitering to single-corner L runs
sudhir9297 Aug 26, 2026
83513cf
restrict conical roofs to curved wall footprints
sudhir9297 Aug 28, 2026
f8f3349
fix conical roof placement architecture
sudhir9297 Aug 28, 2026
296f390
Merge upstream main and resolve PR conflicts
sudhir9297 Aug 28, 2026
8307ad4
fix remaining Bugbot findings
sudhir9297 Aug 28, 2026
b9a9bf1
restrict standard roofs to rectangular rooms
sudhir9297 Aug 28, 2026
9f3febf
ignore curved walls for standard roof drawing
sudhir9297 Aug 28, 2026
54de0c9
fix lower-floor roof room preview elevation
sudhir9297 Aug 28, 2026
c7962ca
test L-shaped roof footprint eligibility
sudhir9297 Aug 28, 2026
e188863
ignore diagonal walls for standard roof guides
sudhir9297 Aug 28, 2026
f88754e
fix roof placement architecture and window previews
sudhir9297 Aug 28, 2026
28ddc09
fix roof footprint previews and wall guides
sudhir9297 Aug 28, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
264 changes: 186 additions & 78 deletions apps/editor/components/build-tab.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
'use client'

import { nodeRegistry } from '@pascal-app/core'
import {
nodeRegistry,
type RoofType,
RoofType as RoofTypeSchema,
useRegistryVersion,
} from '@pascal-app/core'
import {
type FloorplanMode,
getFloorplanNodeExtension,
Expand All @@ -13,13 +18,20 @@ import {
} from '@pascal-app/editor'
import { useLiquidLineToolOptions } from '@pascal-app/nodes'
import Image from 'next/image'
import { useCallback, useEffect, useMemo, useRef, useSyncExternalStore } from 'react'
import { useCallback, useEffect, useRef, useSyncExternalStore } from 'react'
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from '@/components/toolbar-tooltip'
import {
getActiveRoofFeatureId,
getRoofFootprintSource,
getRoofFootprintSources,
ROOF_TYPE_OPTIONS,
type RoofFootprintSource,
} from '@/lib/build-tab-state'
import { cn } from '@/lib/utils'

/**
Expand Down Expand Up @@ -169,10 +181,36 @@ function activateTerrainSculptMode(): void {
useEditor.getState().setMode('terrain-sculpt')
}

type RoofFeature = { kind: string; label: string; iconSrc: string }
type RoofFeature = {
id: string
label: string
iconSrc: string
kind?: string
}

const ROOF_FEATURE_FALLBACK_ICON = '/icons/roof.webp'

function collectRoofFeatures(): RoofFeature[] {
const features: RoofFeature[] = []
for (const [kind, def] of nodeRegistry.entries()) {
if (
def.capabilities.roofAccessory === undefined &&
def.presentation?.paletteGroup !== 'roof-features'
) {
continue
}
if (def.capabilities.wallOpeningPlacement) continue
const icon = def.presentation?.icon
features.push({
id: kind,
kind,
label: def.presentation?.label ?? kind,
iconSrc: icon?.kind === 'url' ? icon.src : ROOF_FEATURE_FALLBACK_ICON,
})
}
return features
}

/**
* Roof accessories and extensions surfaced under the Roof tile. Unlike the
* community editor these aren't DB presets — each is a registry kind, either
Expand All @@ -181,13 +219,29 @@ const ROOF_FEATURE_FALLBACK_ICON = '/icons/roof.webp'
* populated during app bootstrap. Label + icon come from `presentation`;
* non-url icons fall back to the roof icon.
*/
function activateRoofFeatureTool(kind: string): void {
function activateRoofFeatureTool(feature: RoofFeature): void {
const ed = useEditor.getState()
ed.setPhase('structure')
ed.setStructureLayer('elements')
ed.setCatalogCategory(null)
ed.setMode('build')
ed.setTool(kind)
if (feature.kind) ed.setTool(feature.kind)
}

function activateRoofType(roofType: RoofType): void {
const editor = useEditor.getState()
if (!(editor.mode === 'build' && editor.tool === 'roof')) activateBuildTool('roof')
const footprintSource = getRoofFootprintSource(
roofType,
editor.toolDefaults.roof?.footprintSource,
)
editor.setToolDefaults('roof', { ...editor.toolDefaults.roof, roofType, footprintSource })
}

function activateRoofFootprintSource(footprintSource: RoofFootprintSource): void {
const editor = useEditor.getState()
if (!(editor.mode === 'build' && editor.tool === 'roof')) activateBuildTool('roof')
editor.setToolDefaults('roof', { ...editor.toolDefaults.roof, footprintSource })
}

/**
Expand All @@ -208,18 +262,17 @@ const MEP_TOOL_KINDS = new Set<string>([
export function BuildTab() {
const activeTool = useEditor((s) => s.tool)
const mode = useEditor((s) => s.mode)
const roofDefaults = useEditor((s) => s.toolDefaults.roof)
const floorplanMode = useFloorplanMode((s) => s.mode)
const follow = useLiquidLineToolOptions((s) => s.follow)
const toggleFollow = useLiquidLineToolOptions((s) => s.toggleFollow)
useRegistryVersion()
const registryReady = useSyncExternalStore(
subscribeToClientMount,
() => true,
() => false,
)
const buildTypes = useMemo(
() => (registryReady ? collectBuildTypes(floorplanMode) : BASE_BUILD_TYPES),
[floorplanMode, registryReady],
)
const buildTypes = registryReady ? collectBuildTypes(floorplanMode) : BASE_BUILD_TYPES

// 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
Expand All @@ -242,39 +295,24 @@ export function BuildTab() {

// 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.
const roofFeatures = useMemo<RoofFeature[]>(() => {
if (!registryReady) return []
const features: RoofFeature[] = []
for (const [kind, def] of nodeRegistry.entries()) {
if (
def.capabilities.roofAccessory === undefined &&
def.presentation?.paletteGroup !== 'roof-features'
) {
continue
}
// Door / window declare `roofAccessory` for the wall-face cut but
// already have their own Build tiles — listing them here too
// would duplicate the entry under Roof → Features.
if (def.capabilities.wallOpeningPlacement) continue
const icon = def.presentation?.icon
features.push({
kind,
label: def.presentation?.label ?? kind,
iconSrc: icon?.kind === 'url' ? icon.src : ROOF_FEATURE_FALLBACK_ICON,
})
}
return features
}, [registryReady])
const roofFeatures = registryReady ? collectRoofFeatures() : []

// Tile highlight derives from the single source of truth (the active tool /
// mode), never a separate local selection — so keyboard shortcuts and panel
// clicks always agree on which tile is lit.
// The roof Features sub-grid arms roof-accessory tools (skylight, chimney,
// …); keep the Roof tile lit (and its panel open) while any of them is the
// active tool, the same way MEP stays lit for its sub-grid tools.
const isRoofFeatureActive =
mode === 'build' && !!activeTool && roofFeatures.some((f) => f.kind === activeTool)
const activeRoofFeatureId = getActiveRoofFeatureId(roofFeatures, activeTool)
const isRoofFeatureActive = mode === 'build' && activeRoofFeatureId !== null
const isMepActive = mode === 'build' && !!activeTool && MEP_TOOL_KINDS.has(activeTool)
const parsedRoofType = RoofTypeSchema.safeParse(roofDefaults?.roofType)
const activeRoofType = parsedRoofType.success ? parsedRoofType.data : 'gable'
const footprintSources = getRoofFootprintSources(activeRoofType)
const activeFootprintSource = getRoofFootprintSource(
activeRoofType,
roofDefaults?.footprintSource,
)

const isTypeActive = (type: BuildType) => {
if (type.mode) return mode === type.mode
Expand Down Expand Up @@ -364,54 +402,124 @@ export function BuildTab() {
<div className="min-h-0 flex-1 overflow-y-auto">
<TerrainSculptPanel />
</div>
) : mode === 'build' &&
(activeTool === 'roof' || isRoofFeatureActive) &&
roofFeatures.length > 0 ? (
<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">
Features & extensions
</div>
<TooltipProvider delayDuration={0} disableHoverableContent>
<div
className="grid gap-1.5"
style={{ gridTemplateColumns: 'repeat(auto-fill, minmax(56px, 1fr))' }}
>
{roofFeatures.map((feature) => {
const active = mode === 'build' && activeTool === feature.kind
) : mode === 'build' && (activeTool === 'roof' || isRoofFeatureActive) ? (
<div className="flex min-h-0 flex-1 flex-col gap-3 overflow-y-auto">
<div className="flex flex-col gap-2">
<div className="px-0.5 pt-1 font-medium text-muted-foreground text-xs">Roof type</div>
<div className="grid grid-cols-2 gap-1.5">
{ROOF_TYPE_OPTIONS.map((roofType) => {
const active = activeTool === 'roof' && activeRoofType === roofType.value
return (
<Tooltip key={feature.kind}>
<TooltipTrigger asChild>
<button
className={cn(
'group relative flex aspect-square items-center justify-center rounded-xl p-1 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')
activateRoofFeatureTool(feature.kind)
}}
onMouseEnter={() => triggerSFX('sfx:menu-hover')}
type="button"
>
<Image
alt={feature.label}
className="size-full object-contain transition-transform duration-200 group-hover:scale-110"
height={48}
src={feature.iconSrc}
width={48}
/>
</button>
</TooltipTrigger>
<TooltipContent className="pointer-events-none" side="top">
{feature.label}
</TooltipContent>
</Tooltip>
<button
aria-pressed={active}
className={cn(
'rounded-lg px-2.5 py-2 text-left font-medium text-xs transition-colors',
active
? 'bg-primary/10 text-primary ring-1 ring-primary/50'
: 'bg-muted/40 text-muted-foreground hover:bg-muted hover:text-foreground',
)}
key={roofType.value}
onClick={() => {
triggerSFX('sfx:menu-click')
activateRoofType(roofType.value)
}}
onMouseEnter={() => triggerSFX('sfx:menu-hover')}
type="button"
>
{roofType.label}
</button>
)
})}
</div>
</TooltipProvider>
</div>

{activeRoofType !== 'conical' && (
<div className="flex flex-col gap-2 border-border/50 border-t pt-3">
<div className="px-0.5 font-medium text-muted-foreground text-xs">Create from</div>
<div className="grid grid-cols-2 gap-1.5">
{footprintSources.map((source) => {
const active = activeTool === 'roof' && activeFootprintSource === source.value
return (
<button
aria-pressed={active}
className={cn(
'rounded-lg px-2 py-2 text-center font-medium text-xs transition-colors',
active
? 'bg-primary/10 text-primary ring-1 ring-primary/50'
: 'bg-muted/40 text-muted-foreground hover:bg-muted hover:text-foreground',
)}
key={source.value}
onClick={() => {
triggerSFX('sfx:menu-click')
activateRoofFootprintSource(source.value)
}}
onMouseEnter={() => triggerSFX('sfx:menu-hover')}
type="button"
>
{source.label}
</button>
)
})}
</div>
<p className="px-0.5 text-[11px] text-muted-foreground leading-relaxed">
{activeFootprintSource === 'room'
? 'Hover a room to preview its boundary, then click to place.'
: activeFootprintSource === 'walls'
? 'Select a curved wall to match its radius and arc.'
: 'Draw the roof footprint with two corner clicks.'}
</p>
</div>
)}

{roofFeatures.length > 0 ? (
<div className="flex flex-col gap-2 border-border/50 border-t pt-3">
<div className="px-0.5 font-medium text-muted-foreground text-xs">
Features & extensions
</div>
<TooltipProvider delayDuration={0} disableHoverableContent>
<div
className="grid gap-1.5"
style={{ gridTemplateColumns: 'repeat(auto-fill, minmax(56px, 1fr))' }}
>
{roofFeatures.map((feature) => {
const active = mode === 'build' && feature.id === activeRoofFeatureId
return (
<Tooltip key={feature.id}>
<TooltipTrigger asChild>
<button
aria-pressed={active}
className={cn(
'group relative flex aspect-square items-center justify-center rounded-xl p-1 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')
activateRoofFeatureTool(feature)
}}
onMouseEnter={() => triggerSFX('sfx:menu-hover')}
type="button"
>
<Image
alt={feature.label}
className="size-full object-contain transition-transform duration-200 group-hover:scale-110"
height={48}
src={feature.iconSrc}
width={48}
/>
</button>
</TooltipTrigger>
<TooltipContent className="pointer-events-none" side="top">
{feature.label}
</TooltipContent>
</Tooltip>
)
})}
</div>
</TooltipProvider>
</div>
) : null}
</div>
) : isMepActive ? (
<div className="flex min-h-0 flex-1 flex-col gap-2 overflow-y-auto">
Expand Down
Loading
Loading