Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
58 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
a3bf49e
feat: add wall-hosted lean-to roof extensions
sudhir9297 Aug 13, 2026
fd57b5e
feat: refine lean-to roof extensions
sudhir9297 Aug 13, 2026
3e37918
fix: refine lean-to wall and eave junctions
sudhir9297 Aug 13, 2026
00dab89
feat: automate roof gutters and downspouts
sudhir9297 Aug 14, 2026
5880fd0
fix: address roof drainage review findings
sudhir9297 Aug 14, 2026
43b3722
fix: coordinate roof extensions with drainage
sudhir9297 Aug 14, 2026
d88622d
fix: complete lean-to extension controls
sudhir9297 Aug 14, 2026
b840058
fix: complete lean-to extension behavior
sudhir9297 Aug 14, 2026
9709342
fix: refine lean-to appearance and tree
sudhir9297 Aug 14, 2026
d91c414
fix: preserve resize values and restore roof icons
sudhir9297 Aug 16, 2026
5565bd8
Fix lean-to shed roof side infill
sudhir9297 Aug 17, 2026
9715f9a
Lock managed lean-to roof affordances
sudhir9297 Aug 17, 2026
4a79ada
Use selection proxy for lean-to managed roofs
sudhir9297 Aug 17, 2026
c9548d9
Bend curved lean-to extensions as a concentric annular band
sudhir9297 Aug 18, 2026
6cb53bb
fix curved wall shed geometry
sudhir9297 Aug 18, 2026
0f85b24
align shed extension with architecture
sudhir9297 Aug 18, 2026
732accb
Merge upstream main into shed roof extensions
sudhir9297 Aug 18, 2026
afae30b
fix lean-to corner joins and curved gutters
sudhir9297 Aug 19, 2026
28f51bb
fix mesh editing snap configuration
sudhir9297 Aug 19, 2026
8b8480a
fix floating corner post and rafter on curved lean-to joins
sudhir9297 Aug 19, 2026
baaa4f2
fix lean-to gutter/downspout edge cases from review
sudhir9297 Aug 19, 2026
d4bced9
fix biome check formatting and import order
sudhir9297 Aug 19, 2026
0c7eb6a
raise timeout on lean-to gutter corner angle sweep test
sudhir9297 Aug 19, 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
28 changes: 18 additions & 10 deletions apps/editor/components/build-tab.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ function collectBuildTypes(floorplanMode: FloorplanMode): BuildType[] {
const extension = getFloorplanNodeExtension(definition)
if (
baseKinds.has(kind) ||
definition.presentation?.paletteGroup === 'roof-features' ||
!extension?.tool ||
!isFloorplanToolAvailableInMode(extension.availableModes, floorplanMode) ||
!presentation ||
Expand Down Expand Up @@ -173,13 +174,12 @@ type RoofFeature = { kind: string; label: string; iconSrc: string }
const ROOF_FEATURE_FALLBACK_ICON = '/icons/roof.webp'

/**
* Roof accessories surfaced under the Roof tile (a "Features" group). Unlike
* the community editor these aren't DB presets — each is a registry kind with
* `capabilities.roofAccessory`, enumerated from the registry at render time
* (it is populated by the app bootstrap — a module-scope const would race it)
* and activated like any structure tool (the kind's tool attaches it to the
* roof segment under the cursor). Label + icon come from the registry's
* `presentation`; non-url icons fall back to the roof icon.
* Roof accessories and extensions surfaced under the Roof tile. Unlike the
* community editor these aren't DB presets — each is a registry kind, either
* carrying `capabilities.roofAccessory` or explicitly classified as a roof
* extension. They are enumerated at render time because the registry is
* populated during app bootstrap. Label + icon come from `presentation`;
* non-url icons fall back to the roof icon.
*/
function activateRoofFeatureTool(kind: string): void {
const ed = useEditor.getState()
Expand Down Expand Up @@ -243,9 +243,15 @@ 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) continue
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.
Expand All @@ -258,7 +264,7 @@ export function BuildTab() {
})
}
return features
}, [])
}, [registryReady])

// Tile highlight derives from the single source of truth (the active tool /
// mode), never a separate local selection — so keyboard shortcuts and panel
Expand Down Expand Up @@ -362,7 +368,9 @@ export function BuildTab() {
(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</div>
<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"
Expand Down
Binary file added apps/editor/public/icons/box-vent.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added apps/editor/public/icons/chimney.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added apps/editor/public/icons/cupola.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added apps/editor/public/icons/dormer.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added apps/editor/public/icons/downspout.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added apps/editor/public/icons/eyebrow-vent.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added apps/editor/public/icons/gutter.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added apps/editor/public/icons/lean-to-extension.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added apps/editor/public/icons/ridge-vent.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added apps/editor/public/icons/skylight.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added apps/editor/public/icons/solar-panel.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added apps/editor/public/icons/turbine-vent.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 3 additions & 0 deletions packages/core/src/events/bus.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import type {
GutterNode,
HvacEquipmentNode,
ItemNode,
LeanToExtensionNode,
LevelNode,
LinesetNode,
LiquidLineNode,
Expand Down Expand Up @@ -98,6 +99,7 @@ export type BuildingEvent = NodeEvent<BuildingNode>
export type CabinetEvent = NodeEvent<CabinetNode>
export type CabinetModuleEvent = NodeEvent<CabinetModuleNode>
export type LevelEvent = NodeEvent<LevelNode>
export type LeanToExtensionEvent = NodeEvent<LeanToExtensionNode>
export type ZoneEvent = NodeEvent<ZoneNode>
export type ShelfEvent = NodeEvent<ShelfNode>
export type SlabEvent = NodeEvent<SlabNode>
Expand Down Expand Up @@ -305,6 +307,7 @@ type EditorEvents = GridEvents &
NodeEvents<'building', BuildingEvent> &
NodeEvents<'elevator', ElevatorEvent> &
NodeEvents<'level', LevelEvent> &
NodeEvents<'lean-to-extension', LeanToExtensionEvent> &
NodeEvents<'zone', ZoneEvent> &
NodeEvents<'slab', SlabEvent> &
NodeEvents<'shelf', ShelfEvent> &
Expand Down
1 change: 1 addition & 0 deletions packages/core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ export type {
GuideEvent,
GutterEvent,
ItemEvent,
LeanToExtensionEvent,
LevelEvent,
MeasurementEvent,
NodeEvent,
Expand Down
57 changes: 56 additions & 1 deletion packages/core/src/registry/scene-api.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
import type { AnyNode, AnyNodeId } from '../schema/types'
import { pauseSceneHistory, resumeSceneHistory } from '../store/history-control'
import {
activeSceneCommitNodeIds,
pauseSceneHistory,
resumeSceneHistory,
} from '../store/history-control'
import {
type CloneNodesIntoOptions,
collectSubtree,
Expand All @@ -20,10 +24,21 @@ export type SceneStoreLike = {
dirtyNodes: Set<AnyNodeId>
createNode: (node: AnyNode, parentId?: AnyNodeId) => void
createNodes?: (ops: { node: AnyNode; parentId?: AnyNodeId }[]) => void
applyNodeChanges?: (changes: {
create?: { node: AnyNode; parentId?: AnyNodeId }[]
update?: { id: AnyNodeId; data: Partial<AnyNode> }[]
delete?: AnyNodeId[]
}) => void
updateNode: (id: AnyNodeId, data: Partial<AnyNode>) => void
deleteNode: (id: AnyNodeId) => void
markDirty: (id: AnyNodeId) => void
}
subscribe?: (
listener: (
state: { nodes: Record<AnyNodeId, AnyNode> },
previous: { nodes: Record<AnyNodeId, AnyNode> },
) => void,
) => () => void
temporal: {
getState: () => { pause: () => void; resume: () => void }
}
Expand Down Expand Up @@ -71,6 +86,46 @@ export function createSceneApi(store: SceneStoreLike): SceneApi {
return node.id
},

createMany(ops) {
for (const op of ops) captureIfNeeded(op.node.id)
const batch = store.getState().createNodes
if (batch) batch(ops)
else for (const op of ops) this.upsert(op.node, op.parentId)
},

applyChanges(changes) {
for (const op of changes.create ?? []) captureIfNeeded(op.node.id)
for (const op of changes.update ?? []) captureIfNeeded(op.id)
for (const id of changes.delete ?? []) captureIfNeeded(id)
const batch = store.getState().applyNodeChanges
if (batch) {
batch(changes)
return
}
for (const op of changes.create ?? []) this.upsert(op.node, op.parentId)
for (const op of changes.update ?? []) this.update(op.id, op.data)
for (const id of changes.delete ?? []) this.delete(id)
},

subscribeNodes(listener) {
return (
store.subscribe?.((state, previous) => {
if (state.nodes === previous.nodes) return
const scopedIds = activeSceneCommitNodeIds()
const changedIds = new Set<AnyNodeId>(scopedIds)
if (!scopedIds) {
for (const id of Object.keys(state.nodes) as AnyNodeId[]) {
if (state.nodes[id] !== previous.nodes[id]) changedIds.add(id)
}
for (const id of Object.keys(previous.nodes) as AnyNodeId[]) {
if (!(id in state.nodes)) changedIds.add(id)
}
}
listener(state.nodes, previous.nodes, changedIds)
}) ?? (() => {})
)
},

delete(id) {
captureIfNeeded(id)
store.getState().deleteNode(id)
Expand Down
33 changes: 27 additions & 6 deletions packages/core/src/registry/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -793,6 +793,8 @@ export type FloorplanAffordance<N> = {
initialPlanPoint: FloorplanAffordancePoint
/** Active editor grid step in meters. */
gridSnapStep: number
/** Injected mutation/read seam for kind-owned affordances. */
sceneApi?: SceneApi
}): FloorplanAffordanceSession
}

Expand Down Expand Up @@ -875,6 +877,7 @@ export type FloorplanMoveTargetSession = {
export type FloorplanMoveTarget<N> = (args: {
node: N
nodes: Record<AnyNodeId, AnyNode>
sceneApi?: SceneApi
}) => FloorplanMoveTargetSession

// ─── Plugin manifest ─────────────────────────────────────────────────
Expand Down Expand Up @@ -1436,6 +1439,8 @@ export type Presentation = {
icon: IconRef
/** Tool palette section. Defaults to `category` when omitted. */
paletteSection?: 'site' | 'structure' | 'furnish'
/** Optional presentation-only subgroup used by palette surfaces. */
paletteGroup?: string
/** Sort key within a palette section; lower numbers come first. */
paletteOrder?: number
/** Set true for kinds that exist but should NOT appear in the palette
Expand Down Expand Up @@ -2216,7 +2221,7 @@ export type ParametricDescriptor<N> = {
* Direct store/MCP writes bypass it — keep real invariants in
* `invariants`.
*/
derive?: (next: N, patch: Partial<N>) => Partial<N>
derive?: (next: N, patch: Partial<N>, previous?: N) => Partial<N>
/**
* Cross-node companion to `derive`: after an inspector edit lands on
* this node, return patches for OTHER nodes that must follow to keep
Expand Down Expand Up @@ -2293,6 +2298,7 @@ export type ParamGroup<N> = {
export type ParamField<N> =
| {
key: keyof N
label?: string
kind: 'number'
unit?: string
min?: number
Expand All @@ -2301,27 +2307,29 @@ export type ParamField<N> =
visibleIf?: (n: N) => boolean
customEditor?: ComponentType
}
| { key: keyof N; kind: 'boolean'; visibleIf?: (n: N) => boolean }
| { key: keyof N; label?: string; kind: 'boolean'; visibleIf?: (n: N) => boolean }
| {
key: keyof N
label?: string
kind: 'enum'
options: readonly string[]
/** Defaults to 'select' (dropdown). 'segmented' renders the inline
* tabbed switcher — better for short option lists (2-4 items). */
display?: 'select' | 'segmented'
visibleIf?: (n: N) => boolean
}
| { key: keyof N; kind: 'vec3'; visibleIf?: (n: N) => boolean }
| { key: keyof N; kind: 'color'; visibleIf?: (n: N) => boolean }
| { key: keyof N; kind: 'material'; visibleIf?: (n: N) => boolean }
| { key: keyof N; kind: 'ref'; refKind: string; visibleIf?: (n: N) => boolean }
| { key: keyof N; label?: string; kind: 'vec3'; visibleIf?: (n: N) => boolean }
| { key: keyof N; label?: string; kind: 'color'; visibleIf?: (n: N) => boolean }
| { key: keyof N; label?: string; kind: 'material'; visibleIf?: (n: N) => boolean }
| { key: keyof N; label?: string; kind: 'ref'; refKind: string; visibleIf?: (n: N) => boolean }
/** Escape hatch for fields that don't map to a single node key —
* derived values (`length` from `start`/`end`), sliders with
* dynamic min/max (curve sagitta bounded by chord length),
* composed editors, etc. The kind owns the rendering and the
* update logic. `key` here is just a stable React key/label. */
| {
key: string
label?: string
kind: 'custom'
component: ComponentType<{ node: N; onUpdate: (patch: Partial<N>) => void }>
visibleIf?: (n: N) => boolean
Expand Down Expand Up @@ -2373,6 +2381,19 @@ export type SceneApi = {
nodes: () => Readonly<Record<AnyNodeId, AnyNode>>
update: (id: AnyNodeId, patch: Partial<AnyNode>) => void
upsert: (node: AnyNode, parentId?: AnyNodeId) => AnyNodeId
createMany?: (ops: { node: AnyNode; parentId?: AnyNodeId }[]) => void
applyChanges?: (changes: {
create?: { node: AnyNode; parentId?: AnyNodeId }[]
update?: { id: AnyNodeId; data: Partial<AnyNode> }[]
delete?: AnyNodeId[]
}) => void
subscribeNodes?: (
listener: (
nodes: Readonly<Record<AnyNodeId, AnyNode>>,
previous: Readonly<Record<AnyNodeId, AnyNode>>,
changedIds: ReadonlySet<AnyNodeId>,
) => void,
) => () => void
delete: (id: AnyNodeId) => void
restore: (id: AnyNodeId) => void
restoreAll: () => void
Expand Down
38 changes: 36 additions & 2 deletions packages/core/src/schema/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,12 @@ export {
resolveMaterial,
TextureWrapMode,
} from './material'
export {
type AutoDownspoutPlacement,
type AutomaticDownspoutInput,
planAutomaticDownspouts,
resolveAutomaticDownspoutLength,
} from './nodes/automatic-downspout'
export {
BlockEdge,
BlockFace,
Expand Down Expand Up @@ -101,7 +107,12 @@ export {
type DormerSurfaceMaterialSpec,
getEffectiveDormerSurfaceMaterial,
} from './nodes/dormer'
export { DownspoutNode } from './nodes/downspout'
export {
DownspoutNode,
defaultDownspoutMetadata,
isDefaultDownspoutNode,
usesAutomaticDownspoutLength,
} from './nodes/downspout'
export { DuctFittingNode } from './nodes/duct-fitting'
export { DuctSegmentNode } from './nodes/duct-segment'
export { DuctTerminalNode } from './nodes/duct-terminal'
Expand All @@ -114,7 +125,22 @@ export {
export { EyebrowVentNode } from './nodes/eyebrow-vent'
export { FenceBaseStyle, FenceNode, FenceStyle } from './nodes/fence'
export { GuideNode, GuideScaleReference } from './nodes/guide'
export { GutterNode, GutterOutlet } from './nodes/gutter'
export {
computeGutterEaveY,
createDefaultGuttersForSegment,
GUTTER_EAVE_TUCK_INWARD,
GUTTER_EAVE_TUCK_UP,
type GutterEaveSide,
type GutterEdgeExclusion,
GutterNode,
GutterOutlet,
type GutterRun,
getDefaultGutterSide,
getGutterRunsForSegment,
hasAutoGutterMetadata,
isAutoGutterEnabled,
isDefaultGutterNode,
} from './nodes/gutter'
export { HvacEquipmentNode } from './nodes/hvac-equipment'
export type {
AnimationEffect,
Expand All @@ -134,6 +160,13 @@ export {
isLowProfileItemSurface,
LOW_PROFILE_ITEM_SURFACE_MAX_HEIGHT,
} from './nodes/item'
export {
LeanToConnectionMode,
LeanToEndCondition,
LeanToExtensionNode,
LeanToResizeLock,
LeanToRoofEdge,
} from './nodes/lean-to-extension'
export { LevelNode } from './nodes/level'
export { LinesetNode } from './nodes/lineset'
export { LiquidLineNode } from './nodes/liquid-line'
Expand Down Expand Up @@ -182,6 +215,7 @@ export {
getRoofSegmentVisibleTopBounds,
getSegmentSlopeFrame,
hasSegmentMaterialOverride,
isBandedShedSegment,
MIN_ROOF_SEGMENT_TRIM_SPAN,
normalizeRoofSegmentTrim,
ROOF_SHAPE_DEFAULTS,
Expand Down
Loading
Loading