Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 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
06749a7
editor: per-door-type floor-plan symbols
sudhir9297 Jun 5, 2026
ed76812
Fix recessed ceiling fixtures and draw safety
sudhir9297 Jun 7, 2026
acd01ae
feat(editor): magnetic wall-snap with per-kind beacon (2D + 3D)
sudhir9297 Jun 7, 2026
4d1f8dc
editor: garage and open-doorway floor-plan symbols
sudhir9297 Jun 7, 2026
34a24bb
arch: enforce layer boundaries — registry dispatch, store relocation,…
sudhir9297 Jun 8, 2026
bffdb4a
Merge branch 'main' of github.com:pascalorg/editor
sudhir9297 Jun 8, 2026
3de7dbe
merge: resolve conflicts with main
sudhir9297 Jun 8, 2026
75c98ad
chore: fix lint, untrack .claude/launch.json
sudhir9297 Jun 8, 2026
e6e71a4
merge: upstream/main into fix/fir-5-june
sudhir9297 Jun 8, 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
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -48,3 +48,8 @@ og-test

# Worktrees
.worktrees

# Local IDE/agent configs
.claude/launch.json
.claude/settings.local.json
.vscode/launch.json
10 changes: 10 additions & 0 deletions apps/editor/components/viewer-toolbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import {
EyeOff,
Footprints,
Grid2X2,
Magnet,
PenLine,
SlidersHorizontal,
Sparkles,
Expand Down Expand Up @@ -270,6 +271,8 @@ function DisplayMenu() {
const setEdges = useViewer((state) => state.setEdges)
const shadows = useViewer((state) => state.shadows)
const setShadows = useViewer((state) => state.setShadows)
const magneticSnap = useEditor((state) => state.magneticSnap)
const setMagneticSnap = useEditor((state) => state.setMagneticSnap)

const activeShading =
SHADING_OPTIONS.find((option) => option.id === shading) ?? SHADING_OPTIONS[0]
Expand Down Expand Up @@ -311,6 +314,13 @@ function DisplayMenu() {
<EyeOff className="ml-auto h-4 w-4 text-muted-foreground" />
)}
</DropdownMenuItem>
<DropdownMenuItem onSelect={(e) => keepOpen(e, () => setMagneticSnap(!magneticSnap))}>
<Magnet className="h-4 w-4" />
<span>Magnetic snap</span>
<span className="ml-auto text-muted-foreground text-xs">
{magneticSnap ? 'On' : 'Off'}
</span>
</DropdownMenuItem>
<DropdownMenuItem onSelect={(e) => keepOpen(e, () => setShadows(!shadows))}>
<Contrast className="h-4 w-4" />
<span>Shadows</span>
Expand Down
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/types/routes.d.ts";
import "./.next/dev/types/routes.d.ts";

// NOTE: This file should not be edited
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
26 changes: 26 additions & 0 deletions packages/core/src/hooks/spatial-grid/spatial-grid-sync.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,32 @@ export function resolveLevelId(node: AnyNode, nodes: Record<string, AnyNode>): s
return 'default' // fallback for orphaned items
}

/**
* Walks the parent chain of `nodeId` and returns the id of the first ancestor
* whose `type` is `'level'`, or `null` when no level ancestor exists (orphaned
* node, top-level building node, etc.). Unlike `resolveLevelId`, this variant:
*
* - accepts a node **id** rather than a resolved node, saving the caller a
* `nodes[id]` lookup when only the id is at hand.
* - returns `null` instead of the `'default'` fallback, which lets callers
* distinguish "genuinely has no level" from "is a level".
* - has a loop guard (16 iterations) so a corrupt parent-chain cycle cannot
* hang the frame loop.
*/
export function findLevelAncestorId(
nodeId: AnyNodeId,
nodes: Record<string, AnyNode>,
): string | null {
let current: AnyNode | undefined = nodes[nodeId]
let guard = 0
while (current && guard < 16) {
if (current.type === 'level') return current.id
current = current.parentId ? nodes[current.parentId] : undefined
guard += 1
}
return null
}

/**
* Returns the building id that contains the given level, or `null` if
* the level is unparented or no enclosing building exists.
Expand Down
2 changes: 1 addition & 1 deletion packages/core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ export {
} from './hooks/spatial-grid/floor-placed-elevation'
export { pointInPolygon, spatialGridManager } from './hooks/spatial-grid/spatial-grid-manager'
export {
findLevelAncestorId,
initSpatialGridSync,
resolveBuildingForLevel,
resolveLevelId,
Expand Down Expand Up @@ -111,7 +112,6 @@ export {
resetSceneHistoryPauseDepth,
resumeSceneHistory,
} from './store/history-control'
export { default as useAlignmentGuides } from './store/use-alignment-guides'
export {
type ControlValue,
type DoorAnimationState,
Expand Down
22 changes: 22 additions & 0 deletions packages/core/src/registry/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -982,6 +982,12 @@ export type Capabilities = {
*/
alignmentFootprint?: AlignmentFootprintConfig
roofAccessory?: RoofAccessoryConfig
/**
* Kind cuts a hole in the ceiling surface it is attached to (e.g. recessed
* downlights). The viewer's `CeilingSystem` calls this for each child of a
* ceiling to collect extra holes before triangulating. See `CeilingCutCapability`.
*/
ceilingCut?: CeilingCutCapability
paint?: PaintCapability
/**
* Kind is placed by clicking on a wall (door, window). When set, the
Expand Down Expand Up @@ -1179,6 +1185,22 @@ export type RoofAccessoryConfig = {
buildCut?: (node: AnyNode, hostSegment: AnyNode) => BufferGeometry | null
}

/**
* Capability for kinds that cut a hole in their host ceiling when the node is
* attached to a ceiling surface (e.g. recessed downlights). The viewer's
* `CeilingSystem` queries children of a ceiling for this capability and merges
* the returned polygons as extra holes before triangulating, keeping the viewer
* free of per-kind branching.
*
* Returns a rotated-rectangle footprint in ceiling-local [x, z] plan space —
* the same coordinate space as `CeilingNode.polygon` and `.holes`. Return
* `null` when this particular instance should not cut a hole (e.g. a
* non-recessed variant of the same kind).
*/
export type CeilingCutCapability = {
buildCeilingHole: (node: AnyNode) => Array<[number, number]> | null
}

export type CapabilityCtx = { node: AnyNode }

export type MovableConfig = {
Expand Down
5 changes: 5 additions & 0 deletions packages/core/src/schema/nodes/item.ts
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,11 @@ const assetSchema = z.object({
src: AssetUrl,
dimensions: z.tuple([z.number(), z.number(), z.number()]).default([1, 1, 1]), // [w, h, d]
attachTo: z.enum(['wall', 'wall-side', 'ceiling']).optional(),
// Ceiling fixtures (e.g. recessed downlights) that embed *into* the ceiling
// rather than hang below it: the item seats flush with the ceiling plane
// (its body rising into the void above) and the ceiling is cut out around
// the item's footprint. Ignored unless `attachTo === 'ceiling'`.
recessed: z.boolean().optional(),
tags: z.array(z.string()).optional(),
// Function-axis tag slugs from the taxonomy. Drives the hierarchical
// Items-tab browse: a tree node matches when any of its descendant slugs
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
'use client'

import { useAlignmentGuides } from '@pascal-app/core'
import { useAlignmentGuides } from '@pascal-app/editor'
import { useViewer } from '@pascal-app/viewer'
import { memo } from 'react'
import { formatMeasurement } from '../editor/measurement-pill'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,11 @@ import {
pauseSceneHistory,
resolveAlignment,
resumeSceneHistory,
useAlignmentGuides,
useLiveNodeOverrides,
useLiveTransforms,
useScene,
} from '@pascal-app/core'
import { useAlignmentGuides } from '@pascal-app/editor'
import { useViewer } from '@pascal-app/viewer'
import { useEffect } from 'react'
import { commitFreshPlacementSubtree } from '../../lib/fresh-planar-placement'
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
'use client'

import { useWallSnapIndicator, type WallSnapKind } from '@pascal-app/editor'
import { memo } from 'react'
import { useFloorplanRender } from './floorplan-render-context'

/**
* "Magnetic" wall-snap beacon for the 2D floor plan — the top-down twin of the
* 3D `WallSnapBeaconLayer`. Subscribes to the shared `useWallSnapIndicator`
* store (published by the floor-plan wall draft + endpoint-move handlers) and
* draws a marker at the snap point whose shape tells you *what* it caught,
* matching the 3D glyphs:
*
* endpoint (corner) → square midpoint → triangle
* intersection → ✕ cross wall body (edge) → circle
*
* Indigo to match the 3D beacon and stay distinct from the red Figma alignment
* guides. Sizes are pixel-budgeted via `unitsPerPixel` so the marker stays a
* constant size on screen at any zoom. Mounted inside the `data-floorplan-scene`
* group so coordinates are world meters (XZ) 1:1, like the alignment guides.
*/
const COLOR = '#6366f1' // indigo-500 — matches the 3D beacon

export const FloorplanSnapBeaconLayer = memo(function FloorplanSnapBeaconLayer() {
const point = useWallSnapIndicator((s) => s.point)
const ctx = useFloorplanRender()

if (!point) return null

const upp = ctx?.unitsPerPixel ?? 0.01
const m = 6 * upp // base half-size of the glyph in world meters
const stroke = 1.5 * upp

return (
<g pointerEvents="none">
<SnapMarker color={COLOR} kind={point.kind} m={m} stroke={stroke} x={point.x} z={point.z} />
</g>
)
})

function SnapMarker({
color,
kind,
m,
stroke,
x,
z,
}: {
color: string
kind: WallSnapKind
m: number
stroke: number
x: number
z: number
}) {
if (kind === 'endpoint') {
return <rect fill={color} height={m * 2} width={m * 2} x={x - m} y={z - m} />
}
if (kind === 'midpoint') {
const t = m * 1.3
const points = `${x},${z - t} ${x - t},${z + t} ${x + t},${z + t}`
return <polygon fill={color} points={points} />
}
if (kind === 'intersection') {
const c = m * 1.4
return (
<g stroke={color} strokeLinecap="round" strokeWidth={stroke * 2}>
<line x1={x - c} x2={x + c} y1={z - c} y2={z + c} />
<line x1={x - c} x2={x + c} y1={z + c} y2={z - c} />
</g>
)
}
// 'wall' (edge / along-wall) → circle
return <circle cx={x} cy={z} fill={color} r={m} />
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,12 @@ import {
pauseSceneHistory,
resolveBuildingForLevel,
resumeSceneHistory,
useAlignmentGuides,
useInteractive,
useLiveNodeOverrides,
useLiveTransforms,
useScene,
} from '@pascal-app/core'
import { useAlignmentGuides } from '@pascal-app/editor'
import { useViewer } from '@pascal-app/viewer'
import {
memo,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
'use client'

import { type AlignmentGuide, sceneRegistry, useAlignmentGuides } from '@pascal-app/core'
import { type AlignmentGuide, sceneRegistry } from '@pascal-app/core'
import { useAlignmentGuides } from '@pascal-app/editor'
import { useViewer } from '@pascal-app/viewer'
import { Html } from '@react-three/drei'
import { useFrame } from '@react-three/fiber'
Expand Down
Loading
Loading