Refactor editor selection and viewer integration

This commit is contained in:
sudhir
2026-05-01 13:54:23 +05:30
parent 09bfb0b484
commit b3f7957076
10 changed files with 1242 additions and 249 deletions
+2
View File
@@ -35,6 +35,8 @@ export const DoorNode = BaseNode.extend({
// Opening mode
openingKind: z.enum(['door', 'opening']).default('door'),
openingShape: z.enum(['rectangle', 'rounded', 'arch']).default('rectangle'),
openingRadiusMode: z.enum(['all', 'individual']).default('all'),
openingTopRadii: z.tuple([z.number(), z.number()]).default([0.15, 0.15]),
cornerRadius: z.number().min(0).default(0.15),
archHeight: z.number().min(0).default(0.45),
openingRevealRadius: z.number().min(0).default(0.025),
+11
View File
@@ -19,6 +19,17 @@ export const WindowNode = BaseNode.extend({
width: z.number().default(1.5),
height: z.number().default(1.5),
// Opening mode - when set to "opening", the window is only a shaped cutout
openingKind: z.enum(['window', 'opening']).default('window'),
openingShape: z.enum(['rectangle', 'rounded', 'arch']).default('rectangle'),
openingRadiusMode: z.enum(['all', 'individual']).default('all'),
openingCornerRadii: z
.tuple([z.number(), z.number(), z.number(), z.number()])
.default([0.15, 0.15, 0.15, 0.15]),
cornerRadius: z.number().default(0.15),
archHeight: z.number().default(0.35),
openingRevealRadius: z.number().default(0.025),
// Frame
frameThickness: z.number().default(0.05),
frameDepth: z.number().default(0.07),
+140 -28
View File
@@ -5,7 +5,7 @@ import { computeBoundsTree } from 'three-mesh-bvh'
import { sceneRegistry } from '../../hooks/scene-registry/scene-registry'
import { spatialGridManager } from '../../hooks/spatial-grid/spatial-grid-manager'
import { resolveLevelId } from '../../hooks/spatial-grid/spatial-grid-sync'
import type { AnyNode, AnyNodeId, DoorNode, WallNode } from '../../schema'
import type { AnyNode, AnyNodeId, DoorNode, WallNode, WindowNode } from '../../schema'
import useScene from '../../store/use-scene'
import { getWallCurveFrameAt, getWallSurfacePolygon, isCurvedWall } from './wall-curve'
import { DEFAULT_WALL_HEIGHT, getWallPlanFootprint, getWallThickness } from './wall-footprint'
@@ -546,8 +546,11 @@ function collectCutoutBrushes(
for (const child of childrenNodes) {
if (child.type !== 'item' && child.type !== 'window' && child.type !== 'door') continue
if (child.type === 'door' && child.openingKind === 'opening') {
brushes.push(createDoorOpeningCutoutBrush(child, wallThickness))
if (
(child.type === 'door' && child.openingKind === 'opening') ||
(child.type === 'window' && child.openingKind === 'opening')
) {
brushes.push(createShapedOpeningCutoutBrush(child, wallThickness))
continue
}
@@ -606,15 +609,23 @@ function collectCutoutBrushes(
return brushes
}
function createDoorOpeningCutoutBrush(door: DoorNode, wallThickness: number): Brush {
const shape = createDoorOpeningCutoutShape(door)
type ShapedOpeningNode = DoorNode | WindowNode
type CornerRadii = {
topLeft: number
topRight: number
bottomRight: number
bottomLeft: number
}
function createShapedOpeningCutoutBrush(opening: ShapedOpeningNode, wallThickness: number): Brush {
const shape = createShapedOpeningCutoutShape(opening)
const depth = wallThickness * 2
const bevelSize =
door.openingShape === 'rounded'
opening.openingShape === 'rounded'
? Math.min(
Math.max(door.openingRevealRadius ?? 0.025, 0),
Math.max(opening.openingRevealRadius ?? 0.025, 0),
Math.max(wallThickness * 0.45, 0.001),
Math.max((door.cornerRadius ?? 0.15) * 0.45, 0.001),
Math.max((opening.cornerRadius ?? 0.15) * 0.45, 0.001),
)
: 0
const geometry = new THREE.ExtrudeGeometry(shape, {
@@ -633,19 +644,19 @@ function createDoorOpeningCutoutBrush(door: DoorNode, wallThickness: number): Br
return new Brush(geometry)
}
function createDoorOpeningCutoutShape(door: DoorNode): THREE.Shape {
const halfWidth = door.width / 2
const bottom = door.position[1] - door.height / 2
const top = door.position[1] + door.height / 2
const centerX = door.position[0]
function createShapedOpeningCutoutShape(opening: ShapedOpeningNode): THREE.Shape {
const halfWidth = opening.width / 2
const bottom = opening.position[1] - opening.height / 2
const top = opening.position[1] + opening.height / 2
const centerX = opening.position[0]
const left = centerX - halfWidth
const right = centerX + halfWidth
const width = Math.max(door.width, 1e-6)
const height = Math.max(door.height, 1e-6)
const width = Math.max(opening.width, 1e-6)
const height = Math.max(opening.height, 1e-6)
const shape = new THREE.Shape()
if (door.openingShape === 'arch') {
const archHeight = Math.min(Math.max(door.archHeight ?? width / 2, 0.01), height)
if (opening.openingShape === 'arch') {
const archHeight = Math.min(Math.max(opening.archHeight ?? width / 2, 0.01), height)
const springY = top - archHeight
shape.moveTo(left, bottom)
@@ -657,17 +668,9 @@ function createDoorOpeningCutoutShape(door: DoorNode): THREE.Shape {
return shape
}
if (door.openingShape === 'rounded') {
const radius = Math.min(Math.max(door.cornerRadius ?? 0.15, 0), width / 2, height)
shape.moveTo(left, bottom)
shape.lineTo(right, bottom)
shape.lineTo(right, top - radius)
shape.absarc(right - radius, top - radius, radius, 0, Math.PI / 2, false)
shape.lineTo(left + radius, top)
shape.absarc(left + radius, top - radius, radius, Math.PI / 2, Math.PI, false)
shape.lineTo(left, bottom)
shape.closePath()
if (opening.openingShape === 'rounded') {
const radii = getRoundedOpeningRadii(opening, width, height)
applyRoundedOpeningShape(shape, left, right, bottom, top, radii)
return shape
}
@@ -678,3 +681,112 @@ function createDoorOpeningCutoutShape(door: DoorNode): THREE.Shape {
shape.closePath()
return shape
}
function getRoundedOpeningRadii(
opening: ShapedOpeningNode,
width: number,
height: number,
): CornerRadii {
if (opening.type !== 'window') {
if (opening.openingRadiusMode === 'individual') {
const [topLeft = 0, topRight = 0] = opening.openingTopRadii ?? [0.15, 0.15]
return normalizeCornerRadii(
{
topLeft: Math.max(topLeft, 0),
topRight: Math.max(topRight, 0),
bottomRight: 0,
bottomLeft: 0,
},
width,
height,
)
}
const maxRadius = Math.min(width / 2, height)
const radius = Math.min(Math.max(opening.cornerRadius ?? 0.15, 0), maxRadius)
return { topLeft: radius, topRight: radius, bottomRight: 0, bottomLeft: 0 }
}
if (opening.openingRadiusMode === 'individual') {
const [topLeft = 0, topRight = 0, bottomRight = 0, bottomLeft = 0] =
opening.openingCornerRadii ?? [0.15, 0.15, 0.15, 0.15]
return normalizeCornerRadii(
{
topLeft: Math.max(topLeft, 0),
topRight: Math.max(topRight, 0),
bottomRight: Math.max(bottomRight, 0),
bottomLeft: Math.max(bottomLeft, 0),
},
width,
height,
)
}
const maxRadius = Math.min(width / 2, height / 2)
const radius = Math.min(Math.max(opening.cornerRadius ?? 0.15, 0), maxRadius)
return { topLeft: radius, topRight: radius, bottomRight: radius, bottomLeft: radius }
}
function normalizeCornerRadii(radii: CornerRadii, width: number, height: number): CornerRadii {
const next = { ...radii }
const maxScale = Math.min(
1,
width / Math.max(next.topLeft + next.topRight, 1e-6),
width / Math.max(next.bottomLeft + next.bottomRight, 1e-6),
height / Math.max(next.topLeft + next.bottomLeft, 1e-6),
height / Math.max(next.topRight + next.bottomRight, 1e-6),
)
if (maxScale < 1) {
next.topLeft *= maxScale
next.topRight *= maxScale
next.bottomRight *= maxScale
next.bottomLeft *= maxScale
}
return next
}
function applyRoundedOpeningShape(
shape: THREE.Shape,
left: number,
right: number,
bottom: number,
top: number,
radii: CornerRadii,
) {
const { topLeft, topRight, bottomRight, bottomLeft } = radii
shape.moveTo(left + bottomLeft, bottom)
shape.lineTo(right - bottomRight, bottom)
if (bottomRight > 1e-6) {
shape.absarc(right - bottomRight, bottom + bottomRight, bottomRight, -Math.PI / 2, 0, false)
} else {
shape.lineTo(right, bottom)
}
shape.lineTo(right, top - topRight)
if (topRight > 1e-6) {
shape.absarc(right - topRight, top - topRight, topRight, 0, Math.PI / 2, false)
} else {
shape.lineTo(right, top)
}
shape.lineTo(left + topLeft, top)
if (topLeft > 1e-6) {
shape.absarc(left + topLeft, top - topLeft, topLeft, Math.PI / 2, Math.PI, false)
} else {
shape.lineTo(left, top)
}
shape.lineTo(left, bottom + bottomLeft)
if (bottomLeft > 1e-6) {
shape.absarc(left + bottomLeft, bottom + bottomLeft, bottomLeft, Math.PI, Math.PI * 1.5, false)
} else {
shape.lineTo(left, bottom)
}
shape.closePath()
}
@@ -81,8 +81,14 @@ function updateWindowMesh(node: WindowNode, mesh: THREE.Mesh) {
sill,
sillDepth,
sillThickness,
openingKind,
} = node
if (openingKind === 'opening') {
syncWindowCutout(node, mesh)
return
}
const innerW = width - 2 * frameThickness
const innerH = height - 2 * frameThickness
@@ -231,6 +237,10 @@ function updateWindowMesh(node: WindowNode, mesh: THREE.Mesh) {
)
}
syncWindowCutout(node, mesh)
}
function syncWindowCutout(node: WindowNode, mesh: THREE.Mesh) {
// ── Cutout (for wall CSG) — always full window dimensions, 1m deep ──
let cutout = mesh.getObjectByName('cutout') as THREE.Mesh | undefined
if (!cutout) {