Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
43 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
a7f7924
fix(editor): ceiling-attached item placement from 2D floor plan
sudhir9297 May 28, 2026
942ef28
fix(editor): chevron arrows render on SCENE_LAYER so ink-edge shader …
sudhir9297 May 28, 2026
b1f1b90
fix(editor): ceiling grid overlay no longer blocks selecting items un…
sudhir9297 May 28, 2026
c84ac20
fix(core): curve- and thickness-aware wall/slab overlap detection
sudhir9297 May 28, 2026
4ca8c3e
fix(editor): show Move button for legacy-movable kinds in floating ac…
sudhir9297 May 28, 2026
2a6882c
feat(nodes): in-world registry handles for roof accessories + axis-st…
sudhir9297 May 28, 2026
820e80d
feat(nodes): split roof-segment depth chevron into asymmetric front/b…
sudhir9297 May 28, 2026
d7072b1
feat(nodes): in-world handles for dormer + window-bottom clipping check
sudhir9297 May 28, 2026
9400f1c
fix(viewer): glazing role uses FrontSide to avoid MRT back-face pipel…
sudhir9297 May 28, 2026
7b54f88
feat(nodes): in-world handles for box-vent + ridge-vent; freeze non-a…
sudhir9297 May 28, 2026
ff9283d
feat(nodes): gutter accessory — eave-mounted rain channel with three …
sudhir9297 May 28, 2026
5ef0f2c
feat(gutter): roof-panel entry, eave-edge snap, open-top U-channel, m…
sudhir9297 May 28, 2026
e8937f8
fix(viewer/nodes): drop DoubleSide NodeMaterial MRT landmines across …
sudhir9297 May 29, 2026
a5b2a4f
feat(editor): Shift-snap rotate gizmos to 15° increments
sudhir9297 May 29, 2026
3426e94
feat(nodes): stair railings track live segment-drag overrides
sudhir9297 May 29, 2026
b81ffe0
chore(ifc-converter): next-env routes path moves under .next/dev/types
sudhir9297 May 29, 2026
7d324c7
fix(gutter): roofType-aware eave snap — 4-way on hip/flat, low side o…
sudhir9297 May 29, 2026
2cd3a18
feat(gutter): end caps + corner mitre + ghost-placed parity
sudhir9297 May 30, 2026
82a54d7
feat(gutter): drag-snap corners, live eave Y, hangers
sudhir9297 May 30, 2026
fda786f
feat(gutter): downspout outlet with real CSG-drilled hole
sudhir9297 May 30, 2026
ac309f7
feat(downspout): new node + gutter inspector list section
sudhir9297 May 30, 2026
16d1946
chore: pending floorplan alignment-guide work-in-progress
sudhir9297 May 30, 2026
cac5968
feat(core): gutter multi-outlet model + downspout routing options
sudhir9297 Jun 1, 2026
fd69b06
feat(editor): in-world rotate & move gizmos with live-drag dimension …
sudhir9297 Jun 1, 2026
ee4fcaa
feat(gutter): multi-downspout outlets with CSG drops, routing & profiles
sudhir9297 Jun 1, 2026
60faccc
feat(floorplan): 2D footprints for roof nodes + rotate-handle angle w…
sudhir9297 Jun 1, 2026
ea2b68b
fix(editor): select ceiling only via its corner handles, not the grid…
sudhir9297 Jun 1, 2026
9b4ad11
feat(editor): highlight wall openings on a selected wall
sudhir9297 Jun 1, 2026
c03ce04
fix(solar-panel): derive surface frame live so panels re-seat on roof…
sudhir9297 Jun 1, 2026
98d6a9c
fix(first-person): coerce imported item geometry attrs to Float32 for…
sudhir9297 Jun 1, 2026
e3266a6
fix(editor): lower camera minimum zoom distance to 6m
sudhir9297 Jun 1, 2026
c068109
chore(mcp): bump @pascal-app/mcp to 0.3.0 in lockfile
sudhir9297 Jun 1, 2026
dbb8a9c
refactor(nodes): registry-own move dispatch + hoist shared roof helpers
sudhir9297 Jun 1, 2026
6c5fc32
Merge remote-tracking branch 'upstream/main' into fix/may-28-thursday
sudhir9297 Jun 1, 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
2 changes: 1 addition & 1 deletion apps/ifc-converter/next-env.d.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
/// <reference types="next" />
/// <reference types="next/image-types/global" />
import "./.next/dev/types/routes.d.ts";
import "./.next/types/routes.d.ts";

// NOTE: This file should not be edited
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
11 changes: 11 additions & 0 deletions packages/core/src/events/bus.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,11 @@ import type {
ColumnNode,
DoorNode,
DormerNode,
DownspoutNode,
ElevatorNode,
FenceNode,
GuideNode,
GutterNode,
ItemNode,
LevelNode,
RidgeVentNode,
Expand Down Expand Up @@ -63,6 +65,11 @@ export interface NodeEvent<T extends AnyNode = AnyNode> {
object: Object3D
stopPropagation: () => void
nativeEvent: ThreeEvent<PointerEvent>
// Set when the click originated from a dedicated selection affordance
// (e.g. a ceiling corner handle) rather than the node's own surface
// mesh. Lets selection logic accept handle clicks while ignoring clicks
// on the body so they fall through to whatever sits below.
viaHandle?: boolean
}

export type WallEvent = NodeEvent<WallNode>
Expand All @@ -88,10 +95,12 @@ export type ScanEvent = NodeEvent<ScanNode>
export type GuideEvent = NodeEvent<GuideNode>
export type BoxVentEvent = NodeEvent<BoxVentNode>
export type RidgeVentEvent = NodeEvent<RidgeVentNode>
export type GutterEvent = NodeEvent<GutterNode>
export type ChimneyEvent = NodeEvent<ChimneyNode>
export type SolarPanelEvent = NodeEvent<SolarPanelNode>
export type SkylightEvent = NodeEvent<SkylightNode>
export type DormerEvent = NodeEvent<DormerNode>
export type DownspoutEvent = NodeEvent<DownspoutNode>

// Event suffixes - exported for use in hooks
export const eventSuffixes = [
Expand Down Expand Up @@ -229,10 +238,12 @@ type EditorEvents = GridEvents &
NodeEvents<'guide', GuideEvent> &
NodeEvents<'box-vent', BoxVentEvent> &
NodeEvents<'ridge-vent', RidgeVentEvent> &
NodeEvents<'gutter', GutterEvent> &
NodeEvents<'chimney', ChimneyEvent> &
NodeEvents<'solar-panel', SolarPanelEvent> &
NodeEvents<'skylight', SkylightEvent> &
NodeEvents<'dormer', DormerEvent> &
NodeEvents<'downspout', DownspoutEvent> &
CameraControlEvents &
ToolEvents &
GuideEvents &
Expand Down
119 changes: 108 additions & 11 deletions packages/core/src/hooks/spatial-grid/spatial-grid-manager.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import type { AnyNode, CeilingNode, ItemNode, SlabNode, WallNode } from '../../schema'
import { getScaledDimensions, isLowProfileItemSurface } from '../../schema'
import useScene from '../../store/use-scene'
import { isCurvedWall, sampleWallCenterline } from '../../systems/wall/wall-curve'
import { DEFAULT_WALL_THICKNESS } from '../../systems/wall/wall-footprint'
import { SpatialGrid } from './spatial-grid'
import { WallSpatialGrid } from './wall-spatial-grid'

Expand Down Expand Up @@ -284,21 +286,82 @@ function segmentsCollinearAndOverlap(
return a1OnB && a2OnB
}

type WallOverlapInput = {
start: [number, number]
end: [number, number]
curveOffset?: number
thickness?: number
}

/**
* Test if a wall segment overlaps with a polygon.
* A wall is considered to overlap if:
* - Its midpoint is inside the polygon (wall crosses through)
* - At least one endpoint is inside (wall partially or fully in slab)
* - It's collinear with and overlaps a polygon edge (wall on slab boundary)
* - (curved walls) any sample along the centerline is inside
*
* Note: A wall with just one endpoint touching the edge but the rest outside
* is NOT considered overlapping (adjacent only).
*/
export function wallOverlapsPolygon(
start: [number, number],
end: [number, number],
polygon: Array<[number, number]>,
startOrWall: [number, number] | WallOverlapInput,
endOrPolygon: [number, number] | Array<[number, number]>,
polygonArg?: Array<[number, number]>,
): boolean {
// Two call shapes:
// wallOverlapsPolygon(wallLike, polygon) — preferred; curve-aware
// wallOverlapsPolygon(start, end, polygon) — legacy chord-only
let start: [number, number]
let end: [number, number]
let polygon: Array<[number, number]>
let curveOffset = 0
let thickness = DEFAULT_WALL_THICKNESS
if (Array.isArray(startOrWall)) {
start = startOrWall as [number, number]
end = endOrPolygon as [number, number]
polygon = polygonArg as Array<[number, number]>
} else {
start = startOrWall.start
end = startOrWall.end
curveOffset = startOrWall.curveOffset ?? 0
thickness = startOrWall.thickness ?? DEFAULT_WALL_THICKNESS
polygon = endOrPolygon as Array<[number, number]>
}
const halfThickness = Math.max(thickness / 2, 0)

// Curved walls: sample the centerline. The chord-based checks below miss
// walls that bow into/out of the slab — e.g. endpoints on the slab
// boundary with the curve arcing inward through the slab interior. Without
// this, `getSlabElevationForWall` returns 0 (wall drops to floor) and
// `markNodesOverlappingSlab` never re-dirties the wall when the slab Y
// moves.
if (curveOffset !== 0) {
const wallLike = { start, end, curveOffset }
if (isCurvedWall(wallLike)) {
const samples = sampleWallCenterline(wallLike, 16)
for (let i = 0; i < samples.length; i++) {
const point = samples[i]!
if (pointInPolygon(point.x, point.y, polygon)) return true
// Also test ±halfThickness perpendicular at each sample so a curve
// skirting the slab edge with its centerline just outside still
// registers — its body sits inside the slab.
if (halfThickness > 0 && i > 0) {
const prev = samples[i - 1]!
const sx = point.x - prev.x
const sz = point.y - prev.y
const sl = Math.sqrt(sx * sx + sz * sz)
if (sl > 1e-10) {
const tnx = (-sz / sl) * halfThickness
const tnz = (sx / sl) * halfThickness
if (pointInPolygon(point.x + tnx, point.y + tnz, polygon)) return true
if (pointInPolygon(point.x - tnx, point.y - tnz, polygon)) return true
}
}
}
}
}

const dx = end[0] - start[0]
const dz = end[1] - start[1]
const len = Math.sqrt(dx * dx + dz * dz)
Expand Down Expand Up @@ -331,6 +394,26 @@ export function wallOverlapsPolygon(
if (pointInPolygon(bx + pnx, bz + pnz, polygon)) return true
if (pointInPolygon(bx - pnx, bz - pnz, polygon)) return true
}

// Wall-thickness perpendicular test. Walls aren't infinitely thin lines;
// a wall whose centerline sits just outside the slab boundary still has
// half its body inside the slab and should follow the slab elevation.
// Without this, the perimeter walls of a room often miss the slab-overlap
// detection (the slab polygon is the room's interior, the wall centerline
// sits on or just outside its edge) and stay at Y=0 while the slab moves
// up.
if (halfThickness > 0) {
const ux = dx / len
const uz = dz / len
const tnx = -uz * halfThickness
const tnz = ux * halfThickness
for (const t of [0, 0.25, 0.5, 0.75, 1]) {
const bx = start[0] + dx * t
const bz = start[1] + dz * t
if (pointInPolygon(bx + tnx, bz + tnz, polygon)) return true
if (pointInPolygon(bx - tnx, bz - tnz, polygon)) return true
}
}
}

// Check if midpoint is inside (catches walls crossing through)
Expand Down Expand Up @@ -762,15 +845,33 @@ export class SpatialGridManager {
* Get the slab elevation for a wall by checking if it overlaps with any slab polygon (excluding holes).
* Uses wallOverlapsPolygon which handles edge cases (points on boundary, collinear segments).
* Returns the highest slab elevation found, or 0 if none.
*
* Accepts an optional `curveOffset` so curved walls evaluate overlap
* against their actual centerline samples, not just the chord.
*/
getSlabElevationForWall(levelId: string, start: [number, number], end: [number, number]): number {
getSlabElevationForWall(
levelId: string,
start: [number, number],
end: [number, number],
curveOffset = 0,
thickness = DEFAULT_WALL_THICKNESS,
): number {
const slabMap = this.slabsByLevel.get(levelId)
if (!slabMap) return 0

const wallLike: WallOverlapInput = { start, end, curveOffset, thickness }
const isCurved = curveOffset !== 0 && isCurvedWall(wallLike)
const holeSamplePoints: Array<{ x: number; y: number }> = isCurved
? sampleWallCenterline(wallLike, 8)
: [0, 0.25, 0.5, 0.75, 1].map((t) => ({
x: start[0] + (end[0] - start[0]) * t,
y: start[1] + (end[1] - start[1]) * t,
}))

let maxElevation = Number.NEGATIVE_INFINITY
for (const slab of slabMap.values()) {
if (slab.polygon.length < 3) continue
if (!wallOverlapsPolygon(start, end, slab.polygon)) continue
if (!wallOverlapsPolygon(wallLike, slab.polygon)) continue

const holes = slab.holes || []
if (holes.length === 0) {
Expand All @@ -783,15 +884,11 @@ export class SpatialGridManager {
// Sample multiple points along the wall to check whether any portion lies on
// solid slab (not inside any hole). Checking only the midpoint fails when the
// midpoint falls in a staircase hole but the wall's endpoints are on solid slab.
const dx = end[0] - start[0]
const dz = end[1] - start[1]
let hasValidPoint = false
for (const t of [0, 0.25, 0.5, 0.75, 1]) {
const px = start[0] + dx * t
const pz = start[1] + dz * t
for (const sample of holeSamplePoints) {
let inHole = false
for (const hole of holes) {
if (hole.length >= 3 && pointInPolygon(px, pz, hole)) {
if (hole.length >= 3 && pointInPolygon(sample.x, sample.y, hole)) {
inHole = true
break
}
Expand Down
12 changes: 11 additions & 1 deletion packages/core/src/hooks/spatial-grid/spatial-grid-sync.ts
Original file line number Diff line number Diff line change
Expand Up @@ -166,7 +166,17 @@ function markNodesOverlappingSlab(
if (node.type === 'wall') {
const wall = node as WallNode
if (resolveLevelId(node, nodes) !== slabLevelId) continue
if (wallOverlapsPolygon(wall.start, wall.end, slab.polygon)) {
if (
wallOverlapsPolygon(
{
start: wall.start,
end: wall.end,
curveOffset: wall.curveOffset ?? 0,
thickness: wall.thickness,
},
slab.polygon,
)
) {
markDirty(node.id)
}
continue
Expand Down
2 changes: 2 additions & 0 deletions packages/core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ export type {
FenceEvent,
GridEvent,
GuideEvent,
GutterEvent,
ItemEvent,
LevelEvent,
NodeEvent,
Expand Down Expand Up @@ -105,6 +106,7 @@ export {
getEffectiveNode,
type LiveNodeOverrides,
} from './store/use-live-node-overrides'
export { default as useAlignmentGuides } from './store/use-alignment-guides'
export { default as useLiveTransforms, type LiveTransform } from './store/use-live-transforms'
export { clearSceneHistory, default as useScene } from './store/use-scene'
export { resolveElevatorDispatchTarget } from './systems/elevator/elevator-dispatch'
Expand Down
Loading
Loading