Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
52 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
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
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
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 .artifacts/browser-canopy/mono-j-correct-3d.png
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 .artifacts/browser-canopy/mono-j-correct-top.png
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 .artifacts/browser-canopy/mono-j-fixed.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
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 .artifacts/browser-canopy/mono-j-stack-final.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
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 .artifacts/browser-canopy/mono-l-fixed.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
317 changes: 239 additions & 78 deletions apps/editor/components/build-tab.tsx

Large diffs are not rendered by default.

57 changes: 57 additions & 0 deletions apps/editor/lib/build-tab-state.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { describe, expect, test } from 'bun:test'
import {
getActiveRoofFeatureId,
getRoofFootprintSource,
getRoofFootprintSources,
ROOF_TYPE_OPTIONS,
type RoofFeatureIdentity,
} from './build-tab-state'

const FEATURES: RoofFeatureIdentity[] = [
{ id: 'lean-to-extension', kind: 'lean-to-extension' },
{ id: 'skylight', kind: 'skylight' },
]

describe('roof feature selection', () => {
test('does not select every accessory for the plain roof tool', () => {
expect(getActiveRoofFeatureId(FEATURES, 'roof')).toBeNull()
})

test('selects exactly the matching accessory', () => {
expect(getActiveRoofFeatureId(FEATURES, 'lean-to-extension')).toBe('lean-to-extension')
})

test('ignores missing tool identities', () => {
const malformed = FEATURES.map(({ id }) => ({ id }))
expect(getActiveRoofFeatureId(malformed, undefined)).toBeNull()
expect(getActiveRoofFeatureId(malformed, 'skylight')).toBeNull()
})
})

test('roof creation exposes every supported roof type', () => {
expect(ROOF_TYPE_OPTIONS.map((option) => option.value)).toEqual([
'hip',
'gable',
'shed',
'flat',
'gambrel',
'dutch',
'mansard',
'conical',
])
})

test('conical roofs expose wall and draw footprint sources', () => {
expect(getRoofFootprintSources('conical').map((source) => source.value)).toEqual([
'walls',
'draw',
])
expect(getRoofFootprintSource('conical', 'room')).toBe('walls')
expect(getRoofFootprintSource('conical', 'draw')).toBe('draw')
})

test('non-conical roofs expose room and draw footprint sources', () => {
expect(getRoofFootprintSources('hip').map((source) => source.value)).toEqual(['room', 'draw'])
expect(getRoofFootprintSource('hip', 'walls')).toBe('room')
expect(getRoofFootprintSource('hip', 'draw')).toBe('draw')
})
54 changes: 54 additions & 0 deletions apps/editor/lib/build-tab-state.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import type { RoofType } from '@pascal-app/core'

export type RoofFeatureIdentity = {
id: string
kind?: string
}

const ROOF_FOOTPRINT_SOURCES = [
{ label: 'Room', value: 'room' },
{ label: 'Wall', value: 'walls' },
{ label: 'Draw', value: 'draw' },
] as const

export type RoofFootprintSource = (typeof ROOF_FOOTPRINT_SOURCES)[number]['value']

const CONICAL_ROOF_FOOTPRINT_SOURCES = [
ROOF_FOOTPRINT_SOURCES[1],
ROOF_FOOTPRINT_SOURCES[2],
] as const

const STANDARD_ROOF_FOOTPRINT_SOURCES = [
ROOF_FOOTPRINT_SOURCES[0],
ROOF_FOOTPRINT_SOURCES[2],
] as const

export function getRoofFootprintSources(roofType: RoofType) {
return roofType === 'conical' ? CONICAL_ROOF_FOOTPRINT_SOURCES : STANDARD_ROOF_FOOTPRINT_SOURCES
}

export function getRoofFootprintSource(roofType: RoofType, value: unknown): RoofFootprintSource {
const sources = getRoofFootprintSources(roofType)
return sources.some((source) => source.value === value)
? (value as RoofFootprintSource)
: sources[0].value
}

export const ROOF_TYPE_OPTIONS: ReadonlyArray<{ label: string; value: RoofType }> = [
{ label: 'Hip', value: 'hip' },
{ label: 'Gable', value: 'gable' },
{ label: 'Shed', value: 'shed' },
{ label: 'Flat', value: 'flat' },
{ label: 'Gambrel', value: 'gambrel' },
{ label: 'Dutch', value: 'dutch' },
{ label: 'Mansard', value: 'mansard' },
{ label: 'Conical', value: 'conical' },
]

export function getActiveRoofFeatureId(
features: readonly RoofFeatureIdentity[],
activeTool: string | null | undefined,
): string | null {
if (!activeTool) return null
return features.find((feature) => feature.kind === activeTool)?.id ?? null
}
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
"format": "biome format --write",
"format:check": "biome format",
"check": "biome check",
"checks": "bun run check && bun run check-types",
"check:fix": "biome check --write",
"check-types": "turbo run check-types",
"test": "turbo run test",
Expand Down
10 changes: 10 additions & 0 deletions packages/core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,15 @@ export {
} from './hooks/spatial-grid/support-host-patch'
export { useSpatialQuery } from './hooks/spatial-grid/use-spatial-query'
export { loadAssetUrl, saveAsset } from './lib/asset-storage'
export {
type ConicalRoofInvalidPlacement,
type ConicalRoofLevelPlacement,
type ConicalRoofPlacement,
type ConicalRoofSurfacePlacement,
type ResolveConicalRoofPlacementInput,
type RoofPlacementMode,
resolveConicalRoofPlacement,
} from './lib/conical-roof-placement'
export {
clampDoorOperationState,
getDoorRenderOpenAmount,
Expand Down Expand Up @@ -144,6 +153,7 @@ export {
type RoofPlanSegment,
roofOverlapEntryOwns,
roofPlanBoundsOverlap,
roofPlanOverlapEntryOwns,
} from './lib/roof-overlap'
export { resolveSelectionProxyId, selectionProxyIdFromMetadata } from './lib/selection-proxy'
export {
Expand Down
138 changes: 138 additions & 0 deletions packages/core/src/lib/conical-roof-placement.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
// @ts-expect-error - bun:test is provided by the Bun runtime; core does not depend on @types/bun.
import { describe, expect, test } from 'bun:test'
import { LevelNode, RoofNode, RoofSegmentNode } from '../schema'
import { resolveConicalRoofPlacement } from './conical-roof-placement'

function sceneWithHost() {
const level = LevelNode.parse({
id: 'level_host',
children: ['roof_host'],
})
const roof = RoofNode.parse({
id: 'roof_host',
parentId: level.id,
position: [2, 1, 3],
children: ['rseg_host'],
})
const segment = RoofSegmentNode.parse({
id: 'rseg_host',
parentId: roof.id,
roofType: 'gable',
width: 10,
depth: 8,
wallHeight: 2,
pitch: 45,
})
return {
level,
roof,
segment,
nodes: {
[level.id]: level,
[roof.id]: roof,
[segment.id]: segment,
},
}
}

describe('conical roof placement', () => {
test('ground mode creates a level-supported roof at the drawn center', () => {
const { level, nodes } = sceneWithHost()
const placement = resolveConicalRoofPlacement({
nodes,
levelId: level.id,
center: [2, 3],
radius: 1,
curbHeight: 0.5,
mode: 'ground',
})

expect(placement).toEqual({
valid: true,
kind: 'level',
position: [2, 0, 3],
wallHeight: 0.5,
support: { kind: 'level' },
})
})

test('auto mode mounts a fully contained circle on the highest roof surface', () => {
const { level, roof, segment, nodes } = sceneWithHost()
const placement = resolveConicalRoofPlacement({
nodes,
levelId: level.id,
center: [2, 3],
radius: 1,
curbHeight: 0.5,
mode: 'auto',
})

expect(placement.valid).toBe(true)
if (!(placement.valid && placement.kind === 'roof')) throw new Error('expected roof placement')
expect(placement.hostRoofId).toBe(roof.id)
expect(placement.position[0]).toBe(2)
expect(placement.position[2]).toBe(3)
expect(placement.wallHeight).toBeGreaterThan(0.5)
expect(placement.support).toEqual({
kind: 'roof',
roofSegmentId: segment.id,
localPosition: [0, 0],
curbHeight: 0.5,
})
})

test('roof mode rejects a circle that has no complete roof support', () => {
const { level, nodes } = sceneWithHost()
const placement = resolveConicalRoofPlacement({
nodes,
levelId: level.id,
center: [20, 20],
radius: 1,
curbHeight: 0.5,
mode: 'roof',
})

expect(placement).toEqual({ valid: false, reason: 'no-roof-support' })
})

test('auto mode falls back to the level when no roof supports the circle', () => {
const { level, nodes } = sceneWithHost()
const placement = resolveConicalRoofPlacement({
nodes,
levelId: level.id,
center: [20, 20],
radius: 1,
curbHeight: 0.75,
mode: 'auto',
})

expect(placement).toEqual({
valid: true,
kind: 'level',
position: [20, 0, 20],
wallHeight: 0.75,
support: { kind: 'level' },
})
})

test('roof schema preserves the optional surface attachment and parses legacy roofs', () => {
const legacy = RoofNode.parse({ id: 'roof_legacy' })
expect(legacy.support).toEqual({ kind: 'level' })

const mounted = RoofNode.parse({
id: 'roof_mounted',
support: {
kind: 'roof',
roofSegmentId: 'rseg_host',
localPosition: [1.25, -0.5],
curbHeight: 0.4,
},
})
expect(mounted.support).toEqual({
kind: 'roof',
roofSegmentId: 'rseg_host',
localPosition: [1.25, -0.5],
curbHeight: 0.4,
})
})
})
Loading