Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
59ffe0c
feat(walls): add curtain wall support
sudhir9297 Sep 22, 2026
8a341db
feat(walls): refine curtain wall controls and glass appearance
sudhir9297 Sep 22, 2026
9b584af
feat(walls): refine curtain wall controls and glass appearance
sudhir9297 Sep 22, 2026
874ed29
optimize curtain wall geometry and stick framing
sudhir9297 Sep 22, 2026
fddcbd3
move curtain wall geometry into the nodes package
sudhir9297 Sep 22, 2026
4a30c2f
docs: add curtain wall variations screenshot
sudhir9297 Sep 22, 2026
e93c992
fix(walls): address curtain wall review findings
sudhir9297 Sep 22, 2026
6805a16
test(core): make draft snapshot test self-contained
sudhir9297 Sep 22, 2026
de0d026
fix(walls): preserve opening previews and painted shadows
sudhir9297 Sep 22, 2026
c40e4f3
fix(walls): constrain curtain opening interactions
sudhir9297 Sep 22, 2026
c45bd31
fix(walls): clamp openings along curved hosts
sudhir9297 Sep 22, 2026
ed53583
fix(walls): project curved opening resize to arc
sudhir9297 Sep 22, 2026
97c3c5e
fix(walls): recover openings inside curtain perimeter
sudhir9297 Sep 22, 2026
5aaefa8
fix(walls): align shaped frames with curved placement
sudhir9297 Sep 22, 2026
fecc22d
perf(walls): keep curtain highlight stable during previews
sudhir9297 Sep 22, 2026
a95fae5
fix(walls): gate individual panel editing
sudhir9297 Sep 22, 2026
7cc7bed
fix(openings): remove documentation inspector fields
sudhir9297 Sep 22, 2026
063ee7a
fix(walls): add rounded opening controls and optimize previews
sudhir9297 Sep 22, 2026
cdbc7a5
test(walls): avoid timing-sensitive curtain rebuild assertion
sudhir9297 Sep 22, 2026
6a6d89f
feat(ui): explain single-side corner radius shortcut
sudhir9297 Sep 22, 2026
c0deecb
fix(ui): show radius hint for all openings
sudhir9297 Sep 22, 2026
73eefdc
Merge branch 'main' of github.com:pascalorg/editor into feat/curtain-…
sudhir9297 Sep 23, 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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ supabase/.temp/

# Build Outputs
.next/
.next-*/
out/
build
dist
Expand Down
1 change: 1 addition & 0 deletions biome.jsonc
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,7 @@
"apps/**/*.jsx",
"!**/node_modules",
"!**/.next",
"!**/.next-*",
"!**/dist",
"!**/build",
"!**/public",
Expand Down
Binary file added docs/images/curtain-wall-variations.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
"build": "turbo run build",
"dev": "dotenv -e ./.env -e ./.env.defaults -- turbo run dev --env-mode=loose",
"lint": "biome lint",
"lints": "bun run lint",
"lint:fix": "biome lint --write",
"format": "biome format --write",
"format:check": "biome format",
Expand Down
33 changes: 33 additions & 0 deletions packages/core/src/registry/handles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,13 @@ export type HandleAnchor = 'center' | 'min' | 'max'
/** Keyboard modifiers captured for a handle-resize tick. */
export type HandleDragModifiers = {
readonly altKey: boolean
readonly shiftKey?: boolean
}

export type HandlePreviewSession<N> = {
preview: (patch: Partial<N>) => void
commit: (patch?: Partial<N>) => void
cancel: () => void
}

/** 3D position + rotation of the arrow in its portal target's local space. */
Expand Down Expand Up @@ -294,6 +301,31 @@ export type RadialResizeHandle<N> = {
decoration?: HandleDecoration<N>
}

/**
* In-plane corner-radius knob. The editor places the knob diagonally inward
* from `corner` and converts its pointer position back into a radius. Holding
* Shift is exposed through `modifiers` so kinds can switch from a shared
* radius to per-corner radii without teaching the editor their schema.
*/
export type CornerRadiusHandle<N> = {
kind: 'corner-radius'
corner: readonly [x: -1 | 1, y: -1 | 1]
width: (node: N) => number
height: (node: N) => number
currentValue: (node: N) => number
max: number | ((node: N, sceneApi: SceneApi) => number)
apply: (
node: N,
newValue: number,
sceneApi: SceneApi,
modifiers: HandleDragModifiers,
) => Partial<N>
createPreview?: (node: N) => HandlePreviewSession<N>
visible?: (node: N, sceneApi: SceneApi) => boolean
portal?: HandlePortal
portalTarget?: HandlePortalTarget<N>
}

/**
* Curved / spiral stair sweep arrows. The renderer raycasts a horizontal
* plane through the arrow's Y and emits the angular delta (radians,
Expand Down Expand Up @@ -492,6 +524,7 @@ export type LatchHandle<N = any> = {
export type HandleDescriptor<N = any> =
| LinearResizeHandle<N>
| RadialResizeHandle<N>
| CornerRadiusHandle<N>
| ArcResizeHandle<N>
| EndpointMoveHandle<N>
| TapActionHandle<N>
Expand Down
2 changes: 2 additions & 0 deletions packages/core/src/registry/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
export type {
ArcResizeHandle,
CornerRadiusHandle,
Cursor,
EditorApi,
EndpointMoveHandle,
Expand All @@ -10,6 +11,7 @@ export type {
HandleList,
HandlePlacement,
HandlePortal,
HandlePreviewSession,
LatchHandle,
LinearResizeHandle,
RadialResizeHandle,
Expand Down
7 changes: 7 additions & 0 deletions packages/core/src/schema/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,13 @@ export {
setConstructionDimensionDrawingSuppressedSegments,
} from './nodes/construction-dimension'
export { CupolaMaterialRole, CupolaNode } from './nodes/cupola'
export {
CurtainGrid,
CurtainPanelType,
CurtainWallConfig,
DEFAULT_CURTAIN_WALL,
getCurtainWallConfig,
} from './nodes/curtain-wall'
export {
DoorNode,
DoorSegment,
Expand Down
59 changes: 59 additions & 0 deletions packages/core/src/schema/nodes/curtain-wall.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import { z } from 'zod'

export const CurtainGrid = z.object({
layout: z.enum(['count', 'maximum-spacing', 'fixed-spacing']).default('maximum-spacing'),
count: z.number().int().min(1).max(32).default(3),
spacing: z.number().finite().min(0.2).max(100).default(1.5),
alignment: z.enum(['start', 'center', 'end']).default('center'),
})
export type CurtainGrid = z.infer<typeof CurtainGrid>

export const CurtainPanelType = z.enum(['glass', 'solid', 'empty'])
export type CurtainPanelType = z.infer<typeof CurtainPanelType>

export const CurtainWallConfig = z.object({
construction: z.enum(['stick', 'unitized']).default('stick'),
framing: z
.enum(['capped', 'vertical-caps', 'horizontal-caps', 'structural-glazing'])
.default('capped'),
columns: CurtainGrid.prefault({}),
rows: CurtainGrid.prefault({ layout: 'count', count: 2 }),
mullionWidth: z.number().finite().min(0.01).max(0.3).default(0.05),
transomWidth: z.number().finite().min(0.01).max(0.3).default(0.05),
perimeterWidth: z.number().finite().min(0.01).max(0.3).default(0.05),
jointWidth: z.number().finite().min(0.002).max(0.05).default(0.01),
glassThickness: z.number().finite().min(0.004).max(0.08).default(0.024),
panelType: CurtainPanelType.default('glass'),
spandrel: z.enum(['none', 'bottom', 'top']).default('none'),
frameColor: z
.string()
.regex(/^#[0-9a-fA-F]{6}$/)
.default('#303942'),
glassColor: z
.string()
.regex(/^#[0-9a-fA-F]{6}$/)
.default('#a9d5df'),
solidColor: z
.string()
.regex(/^#[0-9a-fA-F]{6}$/)
.default('#53616d'),
glassOpacity: z.number().finite().min(0.05).max(1).default(0.3),
glassRoughness: z.number().finite().min(0).max(1).default(0.12),
panels: z
.array(
z.object({
column: z.number().int().min(0).max(31),
row: z.number().int().min(0).max(31),
type: CurtainPanelType,
}),
)
.max(1024)
.default([]),
})
export type CurtainWallConfig = z.infer<typeof CurtainWallConfig>

export const DEFAULT_CURTAIN_WALL = CurtainWallConfig.parse({})

export function getCurtainWallConfig(wall: { curtainWall?: CurtainWallConfig }): CurtainWallConfig {
return wall.curtainWall ?? DEFAULT_CURTAIN_WALL
}
3 changes: 3 additions & 0 deletions packages/core/src/schema/nodes/wall.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import dedent from 'dedent'
import { z } from 'zod'
import { BaseNode, nodeType, objectId } from '../base'
import { MaterialSchema } from '../material'
import { CurtainWallConfig } from './curtain-wall'
import { DoorNode } from './door'
import { ItemNode } from './item'
import { LeanToExtensionNode } from './lean-to-extension'
Expand Down Expand Up @@ -131,6 +132,8 @@ export type WallSurfaceSlotId = keyof typeof WALL_SURFACE_SLOT_DEFAULTS
export const WallNode = BaseNode.extend({
id: objectId('wall'),
type: nodeType('wall'),
wallType: z.enum(['standard', 'curtain']).optional(),
curtainWall: CurtainWallConfig.optional(),
children: z
.array(
z.union([
Expand Down
6 changes: 6 additions & 0 deletions packages/core/src/store/draft-attachment-snapshot.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,12 @@ import type { AnyNode, AnyNodeId } from '../schema/types'
import { subscribeSceneCommits } from './history-control'
import useScene from './use-scene'

globalThis.requestAnimationFrame ??= (callback) => {
callback(0)
return 0
}
globalThis.cancelAnimationFrame ??= () => {}

test('draft snapshots do not enumerate unaffected procedural attachment maps', () => {
const saved = useScene.getState()
const level = LevelNode.parse({})
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ type HandleDragSession = {
commit?: (patch: Partial<AnyNode>) => void
markDirty?: boolean
onBegin?: () => void
onCancel?: () => void
onEnd?: () => void
overrideId?: AnyNodeId
}
Expand Down Expand Up @@ -196,6 +197,7 @@ export function useHandleDrag(args: UseHandleDragArgs) {
let historyPaused = true
let releaseSpatialCapture: (() => void) | null = null
let altKey = event.nativeEvent.altKey
let shiftKey = event.nativeEvent.shiftKey
let pendingMoveEvent: PointerEvent | null = null
let moveFrame = 0

Expand All @@ -208,7 +210,7 @@ export function useHandleDrag(args: UseHandleDragArgs) {
const processMove = (moveEvent: PointerEvent) => {
const patch = session.move({
event: moveEvent,
modifiers: { altKey },
modifiers: { altKey, shiftKey },
getPointerRay,
intersectPlane,
})
Expand Down Expand Up @@ -287,6 +289,7 @@ export function useHandleDrag(args: UseHandleDragArgs) {
}

const onCancel = () => {
session.onCancel?.()
clearOverride()
cleanup()
}
Expand All @@ -298,6 +301,10 @@ export function useHandleDrag(args: UseHandleDragArgs) {
altKey = true
return
}
if (e.key === 'Shift') {
shiftKey = true
return
}
if (e.key !== 'Escape' && !isHistoryShortcut(e)) return
e.preventDefault()
e.stopPropagation()
Expand All @@ -306,6 +313,7 @@ export function useHandleDrag(args: UseHandleDragArgs) {
}
const onKeyUp = (e: KeyboardEvent) => {
if (e.key === 'Alt') altKey = false
if (e.key === 'Shift') shiftKey = false
}

dragCleanupRef.current = onCancel
Expand Down
Loading
Loading