Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
68 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
3b2e774
Add modular cabinet node
sudhir9297 Jul 1, 2026
92f2c25
Add modular cabinet run and tall cabinet workflows
sudhir9297 Jul 1, 2026
2b5964e
Add cabinet paint slots and material groups
sudhir9297 Jul 1, 2026
b1a7764
Improve modular cabinet presets and wall snapping
sudhir9297 Jul 2, 2026
0c64b77
Improve cabinet wall snapping
sudhir9297 Jul 2, 2026
21e6195
Fix cabinet wall snapping and countertop joins
sudhir9297 Jul 2, 2026
c6f3271
Improve cabinet appliance compartments
sudhir9297 Jul 2, 2026
b01df0e
Improve fridge visuals and smooth cabinet animations
sudhir9297 Jul 2, 2026
9608792
Improve gas hob flames and cooktop knob controls
sudhir9297 Jul 3, 2026
9ccfaad
Fix cabinet UVs and registry actions
sudhir9297 Jul 3, 2026
5691212
Merge remote-tracking branch 'upstream/main' into fix/wed-jul-1
sudhir9297 Jul 3, 2026
28eb570
Fix registry move snap typecheck
sudhir9297 Jul 3, 2026
eb171f9
Fix cabinet knob animation concurrency
sudhir9297 Jul 3, 2026
a2ab25d
Fix cabinet material disposal, undo flood, and 2D move parity
sudhir9297 Jul 4, 2026
89a284c
Restructure cabinet schema and centralize run math/mutations
sudhir9297 Jul 4, 2026
7b50f09
Add cabinet snap tests and split the cabinet panel
sudhir9297 Jul 4, 2026
306a87d
Split cabinet geometry.ts into per-appliance modules
sudhir9297 Jul 4, 2026
113c94d
Add undermount sink compartment with CSG-cut countertops
sudhir9297 Jul 5, 2026
4b9ba64
Update ifc-converter next-env reference to build-mode route types
sudhir9297 Jul 5, 2026
45cee0e
Draw cabinet floor plans with kitchen drafting symbols
sudhir9297 Jul 6, 2026
d528ec1
Play cabinet open/close animation on the E interaction key
sudhir9297 Jul 6, 2026
36f84d3
Add kitchen island and bar counter support to cabinet runs
sudhir9297 Jul 6, 2026
fff3cf5
Merge quick-action arrow and plus into one directional add glyph
sudhir9297 Jul 6, 2026
a2d63cb
Extend plugin discovery instead of replacing the host source
sudhir9297 Jul 6, 2026
ccaeae1
Add L-corner runs to modular cabinets with run-scoped grouping
sudhir9297 Jul 6, 2026
845cb01
Delete emptied cabinet run groups in the same delete gesture
sudhir9297 Jul 6, 2026
ea97e3a
Remove filler front gap treatment
sudhir9297 Jul 7, 2026
67d67e2
Apply architecture review fixes across cabinet, plugin-trees, and fra…
sudhir9297 Jul 7, 2026
42dc290
Propagate front styling to L-corner runs even when re-layout bails
sudhir9297 Jul 7, 2026
2417b47
Improve editor viewer integration
sudhir9297 Jul 7, 2026
baa5711
Refactor plugin panels and floorplan invalidation
sudhir9297 Jul 7, 2026
6b58b40
UV-unwrap and material-assign roof-segment trim planes
sudhir9297 Jul 7, 2026
592cc68
Fix cabinet placement snap feedback
sudhir9297 Jul 8, 2026
2938ab1
Add cabinet wall snap bypass
sudhir9297 Jul 8, 2026
267b4e2
Fix cabinet continuous placement flow
sudhir9297 Jul 8, 2026
16d3b97
Implement wall band material slots
sudhir9297 Jul 8, 2026
8a74f98
Add corner addition preview functionality and related tests
sudhir9297 Jul 8, 2026
aa2ba28
Fix cabinet and wall band behavior
sudhir9297 Jul 8, 2026
e72312b
Fix architecture review blockers
sudhir9297 Jul 8, 2026
800dc0d
Merge remote-tracking branch 'upstream/main' into fix/wed-jul-1
sudhir9297 Jul 8, 2026
d5c9a79
Fix post-merge quality check
sudhir9297 Jul 8, 2026
33d5cc2
fix cursor review feedback
sudhir9297 Jul 8, 2026
a6b0c27
Fix cabinet placement type and quick action history
sudhir9297 Jul 9, 2026
f3774e1
Improve cabinet placement feedback
sudhir9297 Jul 9, 2026
1c3a764
Make wall bands configurable
sudhir9297 Jul 9, 2026
7d704c7
Add wall trim profiles and default materials
sudhir9297 Jul 9, 2026
83163e6
Fix architecture review issues
sudhir9297 Jul 9, 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
14 changes: 8 additions & 6 deletions apps/editor/lib/bootstrap.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
import {
type AnyNodeDefinition,
discoverPlugins,
extendPluginDiscovery,
loadPlugin,
nodeRegistry,
registerNode,
setPluginDiscovery,
} from '@pascal-app/core'
import { registerEditorHostPanel } from '@pascal-app/editor'
import { builtinPlugin } from '@pascal-app/nodes'
import { treesPlugin } from '@pascal-app/plugin-trees'
import { treesHostPanel, treesPlugin } from '@pascal-app/plugin-trees'

// Idempotency guards: HMR can reload this module, but `registerNode`
// throws on duplicate kinds. Flags live in the module closure so they
Expand Down Expand Up @@ -79,10 +80,11 @@ export async function loadExternalPlugins(): Promise<void> {
}
}

// Register the first-party example plugin (trees node + presets rail panel)
// through the same discovery hook a third-party pack would use. Must be set
// before `loadExternalPlugins()` reads it below.
setPluginDiscovery(async () => [treesPlugin])
// Register the first-party example node plugin alongside any host-provided
// discovery source instead of replacing it. Its Nature rail panel is host UI,
// so it is registered separately from the core plugin manifest.
extendPluginDiscovery(async () => [treesPlugin])
registerEditorHostPanel(treesHostPanel)

loadBuiltinsSync()
void loadExternalPlugins()
2 changes: 1 addition & 1 deletion apps/editor/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
"type": "module",
"private": true,
"scripts": {
"dev": "dotenv -e ../../.env.local -- next dev --port 3002",
"dev": "dotenv -e ../../.env.local -- next dev --port ${PORT:-3002}",
"build": "dotenv -e ../../.env.local -- next build",
"start": "next start",
"lint": "biome lint",
Expand Down
18 changes: 17 additions & 1 deletion packages/core/src/events/bus.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import type { Object3D } from 'three'
import type {
BoxVentNode,
BuildingNode,
CabinetModuleNode,
CabinetNode,
CeilingNode,
ChimneyNode,
ColumnNode,
Expand Down Expand Up @@ -89,6 +91,8 @@ export type FenceEvent = NodeEvent<FenceNode>
export type ItemEvent = NodeEvent<ItemNode>
export type SiteEvent = NodeEvent<SiteNode>
export type BuildingEvent = NodeEvent<BuildingNode>
export type CabinetEvent = NodeEvent<CabinetNode>
export type CabinetModuleEvent = NodeEvent<CabinetModuleNode>
export type LevelEvent = NodeEvent<LevelNode>
export type ZoneEvent = NodeEvent<ZoneNode>
export type ShelfEvent = NodeEvent<ShelfNode>
Expand Down Expand Up @@ -251,9 +255,20 @@ type RoomPresetEvents = {
'room-preset:create': RoomPresetCreateEvent
}

type SelectionEvents = {
/**
* "Reveal this node" intent — the editor's node action menu emits it with the
* selected node; whoever owns the node's catalog/panel (host browser, a
* plugin's presets panel) listens and reveals it.
*/
'selection:find-node': AnyNode
}

type EditorEvents = GridEvents &
NodeEvents<'wall', WallEvent> &
NodeEvents<'fence', FenceEvent> &
NodeEvents<'cabinet', CabinetEvent> &
NodeEvents<'cabinet-module', CabinetModuleEvent> &
NodeEvents<'item', ItemEvent> &
NodeEvents<'site', SiteEvent> &
NodeEvents<'building', BuildingEvent> &
Expand Down Expand Up @@ -301,6 +316,7 @@ type EditorEvents = GridEvents &
ThumbnailEvents &
SnapshotEvents &
AIChatEvents &
RoomPresetEvents
RoomPresetEvents &
SelectionEvents

export const emitter = mitt<EditorEvents>()
114 changes: 114 additions & 0 deletions packages/core/src/hooks/spatial-grid/floor-placed-elevation.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { z } from 'zod'
import { nodeRegistry, registerNode } from '../../registry'
import type { AnyNodeDefinition } from '../../registry/types'
import type { AnyNode, SlabNode } from '../../schema'
import useScene from '../../store/use-scene'
import { getFloorPlacedElevation, getFloorStackedPosition } from './floor-placed-elevation'
import { spatialGridManager } from './spatial-grid-manager'

Expand Down Expand Up @@ -86,6 +87,7 @@ describe('floor-placed elevation resolver', () => {
beforeEach(() => {
nodeRegistry._reset()
spatialGridManager.clear()
useScene.setState({ nodes: {} })
})

test('returns 0 without a floorPlaced capability', () => {
Expand Down Expand Up @@ -113,6 +115,118 @@ describe('floor-placed elevation resolver', () => {
).toBe(0)
})

test('canPlaceOnFloorFootprints accepts an L-shaped draft in the open corner gap', () => {
const baseAsset = (makeFloorNode() as { asset: Record<string, unknown> }).asset
registerNode(
makeDefinition('item', {
floorPlaced: {
footprint: (node) => ({
dimensions: (node as { asset: { dimensions: [number, number, number] } }).asset
.dimensions,
rotation: [0, (node as { rotation: [number, number, number] }).rotation[1] ?? 0, 0],
}),
collides: true,
},
}),
)

const level = makeLevel()
const blocker = makeFloorNode({
id: 'item_blocker',
position: [0.85, 0, 0.85],
asset: {
...baseAsset,
id: 'asset_blocker',
name: 'Blocker',
src: 'asset:blocker',
dimensions: [0.3, 1, 0.3],
} as AnyNode extends { asset: infer T } ? T : never,
})
useScene.setState({ nodes: nodesFor(level, blocker) })

const coarse = spatialGridManager.canPlaceOnFloor(LEVEL_ID, [0.5, 0, 0.5], [1, 1, 1], [0, 0, 0])
const precise = spatialGridManager.canPlaceOnFloorFootprints(LEVEL_ID, [
{ position: [0.2, 0, 0.5], dimensions: [0.4, 1, 1], rotation: [0, 0, 0] },
{ position: [0.8, 0, 0.2], dimensions: [0.4, 1, 0.4], rotation: [0, 0, 0] },
])

expect(coarse.valid).toBe(false)
expect(coarse.conflictIds).toEqual(['item_blocker'])
expect(precise.valid).toBe(true)
expect(precise.conflictIds).toEqual([])
})

test('canPlaceOnFloorFootprints rejects overlapping draft footprints', () => {
useScene.setState({ nodes: nodesFor(makeLevel()) })

const result = spatialGridManager.canPlaceOnFloorFootprints(LEVEL_ID, [
{ position: [0, 0, 0], dimensions: [1, 1, 1], rotation: [0, 0, 0] },
{ position: [0.25, 0, 0], dimensions: [1, 1, 1], rotation: [0, 0, 0] },
])

expect(result.valid).toBe(false)
expect(result.conflictIds).toEqual([])
})

test('canPlaceOnFloorFootprints ignores descendants of an ignored composite node', () => {
registerNode(
makeDefinition('cabinet', {
floorPlaced: {
footprint: () => ({
dimensions: [1, 1, 1],
rotation: [0, 0, 0],
}),
collides: true,
},
}),
)
registerNode(
makeDefinition('cabinet-module', {
floorPlaced: {
footprint: () => ({
dimensions: [1, 1, 1],
rotation: [0, 0, 0],
}),
collides: true,
},
}),
)

const level = makeLevel()
const run = {
id: 'cabinet_run',
type: 'cabinet',
object: 'node',
parentId: LEVEL_ID,
visible: true,
metadata: {},
children: ['cabinet-module_child'],
position: [0, 0, 0],
rotation: 0,
} as unknown as AnyNode
const module = {
id: 'cabinet-module_child',
type: 'cabinet-module',
object: 'node',
parentId: run.id,
visible: true,
metadata: {},
children: [],
position: [0, 0, 0],
rotation: 0,
} as unknown as AnyNode
useScene.setState({ nodes: nodesFor(level, run, module) })

const result = spatialGridManager.canPlaceOnFloorFootprints(
LEVEL_ID,
[{ position: [0, 0, 0], dimensions: [1, 1, 1], rotation: [0, 0, 0] }],
[run.id],
)

expect(result.valid).toBe(true)
expect(result.conflictIds).toEqual([])
})

test('returns 0 when applies returns false', () => {
registerNode(
makeDefinition('item', {
Expand Down
66 changes: 59 additions & 7 deletions packages/core/src/hooks/spatial-grid/spatial-grid-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,28 @@ function resolveNodeLevelId(node: AnyNode, nodes: Record<string, AnyNode>): stri
return 'default'
}

function expandIgnoredNodeIds(
ignoreIds: readonly string[] | undefined,
nodes: Record<string, AnyNode>,
): Set<string> {
const ignored = new Set(ignoreIds ?? [])
const queue = [...ignored]

while (queue.length > 0) {
const id = queue.pop()!
const node = nodes[id]
const children = (node as { children?: unknown } | undefined)?.children
if (!Array.isArray(children)) continue
for (const childId of children) {
if (typeof childId !== 'string' || ignored.has(childId)) continue
ignored.add(childId)
queue.push(childId)
}
}

return ignored
}

/**
* Test if two line segments (a1->a2) and (b1->b2) intersect.
*/
Expand Down Expand Up @@ -704,16 +726,43 @@ export class SpatialGridManager {
dimensions: [number, number, number],
rotation: [number, number, number],
ignoreIds?: string[],
) {
return this.canPlaceOnFloorFootprints(levelId, [{ position, dimensions, rotation }], ignoreIds)
}

canPlaceOnFloorFootprints(
levelId: string,
footprints: readonly {
position: [number, number, number]
dimensions: [number, number, number]
rotation: [number, number, number]
}[],
ignoreIds?: string[],
) {
const nodes = useScene.getState().nodes
const ignoreSet = new Set(ignoreIds ?? [])
const draftBounds = footprintBoundsXZ(position, dimensions, rotation[1])
const ignoreSet = expandIgnoredNodeIds(ignoreIds, nodes)
const draftBounds = footprints.map((footprint) =>
footprintBoundsXZ(footprint.position, footprint.dimensions, footprint.rotation[1] ?? 0),
)
for (let i = 0; i < draftBounds.length; i += 1) {
const a = draftBounds[i]!
for (let j = i + 1; j < draftBounds.length; j += 1) {
const b = draftBounds[j]!
if (
intervalsOverlap(a.minX, a.maxX, b.minX, b.maxX) &&
intervalsOverlap(a.minZ, a.maxZ, b.minZ, b.maxZ)
) {
return { valid: false, conflictIds: [] }
}
}
}

// A floor placement conflicts with any other COLLIDING floor-resting node,
// not just items — every kind whose `floorPlaced.collides` is set (item /
// shelf / column) contributes its footprint(s) as an obstacle. Each
// candidate's XZ extent is read from the same declarative footprint the
// elevation + sync paths use, so adding a colliding kind needs no change here.
// shelf / column / cabinet / stair) contributes its footprint(s) as an
// obstacle. Each candidate's XZ extent is read from the same declarative
// footprint the elevation + sync paths use, so adding a colliding kind
// needs no change here.
const conflicts: string[] = []
for (const node of Object.values(nodes)) {
if (ignoreSet.has(node.id)) continue
Expand All @@ -733,8 +782,11 @@ export class SpatialGridManager {
fpRotation,
)
if (
intervalsOverlap(draftBounds.minX, draftBounds.maxX, bounds.minX, bounds.maxX) &&
intervalsOverlap(draftBounds.minZ, draftBounds.maxZ, bounds.minZ, bounds.maxZ)
draftBounds.some(
(draft) =>
intervalsOverlap(draft.minX, draft.maxX, bounds.minX, bounds.maxX) &&
intervalsOverlap(draft.minZ, draft.maxZ, bounds.minZ, bounds.maxZ),
)
Comment thread
cursor[bot] marked this conversation as resolved.
) {
conflicts.push(node.id)
break
Expand Down
5 changes: 5 additions & 0 deletions packages/core/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
export type {
BoxVentEvent,
BuildingEvent,
CabinetEvent,
CabinetModuleEvent,
CameraControlEvent,
CameraControlFitSceneEvent,
CeilingEvent,
Expand Down Expand Up @@ -72,6 +74,7 @@ export {
polygonsOverlap,
segmentsIntersect,
} from './lib/polygon-relations'
export { resolveSelectionProxyId, selectionProxyIdFromMetadata } from './lib/selection-proxy'
export { getRenderableSlabPolygon } from './lib/slab-polygon'
export {
deriveSlotId,
Expand Down Expand Up @@ -132,11 +135,13 @@ export type {
export * from './registry'
export * from './schema'
export * from './services'
export { isMovable, movePlanToward, moveToward, resolveMovable } from './services/movement'
export {
getSceneHistoryPauseDepth,
pauseSceneHistory,
resetSceneHistoryPauseDepth,
resumeSceneHistory,
runAsSingleSceneHistoryStep,
} from './store/history-control'
export {
type ControlValue,
Expand Down
16 changes: 16 additions & 0 deletions packages/core/src/lib/selection-proxy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import type { AnyNode, AnyNodeId } from '../schema/types'

export function selectionProxyIdFromMetadata(metadata: unknown): AnyNodeId | null {
if (!metadata || typeof metadata !== 'object' || Array.isArray(metadata)) return null
const value = (metadata as Record<string, unknown>).nodeSelectionProxyId
return typeof value === 'string' ? (value as AnyNodeId) : null
}

export function resolveSelectionProxyId(
node: AnyNode,
nodes: Readonly<Record<string, AnyNode | undefined>>,
): AnyNodeId {
const proxyId = selectionProxyIdFromMetadata((node as { metadata?: unknown }).metadata)
if (!proxyId || !nodes[proxyId]) return node.id as AnyNodeId
return proxyId
}
14 changes: 13 additions & 1 deletion packages/core/src/registry/handles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,9 +100,11 @@ export type Cursor = 'ew-resize' | 'ns-resize' | 'move' | 'grab' | 'grabbing'
export type HandleDecoration<N> = {
kind: 'ring'
/** Node-local radius of the ring (XZ plane). */
radius: (node: N) => number
radius: (node: N, sceneApi: SceneApi) => number
/** Node-local Y of the ring. Defaults to 0. */
y?: (node: N) => number
/** Node-local center of the ring. Defaults to the node origin. */
center?: (node: N, sceneApi: SceneApi) => readonly [number, number, number]
}

/**
Expand All @@ -127,6 +129,16 @@ export type LinearResizeHandle<N> = {
anchor: HandleAnchor
currentValue: (node: N) => number
apply: (node: N, newValue: number, sceneApi: SceneApi) => Partial<N>
/** Optional live-scene visibility gate for context-dependent arrows. */
visible?: (node: N, sceneApi: SceneApi) => boolean
/**
* Optional committed-write hook. The generic handle renderer previews the
* selected node with the `apply` patch during drag; on release it normally
* writes that patch back to the same node. Composite nodes can override the
* final write here to fan the resize out to siblings / parents while keeping
* the handle UI generic.
*/
commit?: (node: N, patch: Partial<N>, sceneApi: SceneApi) => void
/**
* Optional per-tick hook fired while this handle is being dragged, with the
* live (in-progress, override-merged) node. A pure side-channel for transient
Expand Down
Loading
Loading