Thanks to visit codestin.com
Credit goes to github.com

Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
55 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
48b7f3e
wall: 2D floor-plan move, side arrows, drag-to-move endpoints
sudhir9297 May 22, 2026
e10294a
wall: 2D floor-plan move adopts 3D junction planner
sudhir9297 May 22, 2026
3376bc9
door/window: R flips side, E toggles open/close
sudhir9297 May 22, 2026
c7d8edf
door/window: keep 2D plan in sync during placement drag
sudhir9297 May 22, 2026
1d872d8
floor-plan: live wall-draft measurement + opening placement event order
sudhir9297 May 22, 2026
c3c94bd
wall: pass geometry as prop on move arrow handles
sudhir9297 May 22, 2026
21caac8
ifc-converter: regenerate next-env.d.ts after Next route path move
sudhir9297 May 22, 2026
03651da
Merge remote-tracking branch 'upstream/main' into fix/may-22-friday
sudhir9297 May 22, 2026
4382fb5
move: track which view finalised so split-view cleanups don't race
sudhir9297 May 22, 2026
36358da
wall: 2D drag publishes to useLiveNodeOverrides, zustand only on commit
sudhir9297 May 22, 2026
f68d0d9
wall: new in-world selection UI — side arrows, height handle, corner …
sudhir9297 May 23, 2026
bc08187
wall: smooth ground-menu side-flip with hysteresis + lerp
sudhir9297 May 23, 2026
12facb6
wall: raise ground-action menu to 10 cm so icons clear floor textures
sudhir9297 May 23, 2026
6024d52
wall: anchor 2D action menu and 3D height arrow at curve apex
sudhir9297 May 23, 2026
5bd2a12
door: in-world selection UI — side width arrows, height arrow, ground…
sudhir9297 May 23, 2026
be87821
door: 2D width arrows + world-relative move dot, deterministic commits
sudhir9297 May 23, 2026
4994675
window: in-world selection UI, 2D width arrows, deterministic commits
sudhir9297 May 23, 2026
4744d92
stair: in-world selection UI — side/length/height arrows, ground menu
sudhir9297 May 23, 2026
44344bf
stair: 2D move-revert fix, parent ground menu, curved/spiral in-world…
sudhir9297 May 25, 2026
a0b56ed
Merge branch 'main' into fix/may-22-friday
sudhir9297 May 25, 2026
f0a0c3d
editor: reuse measurement-bar geometry; fix two post-merge dangling refs
sudhir9297 May 25, 2026
8ec41a0
editor: pin building bbox center to cursor during move
sudhir9297 May 25, 2026
cc2be42
stair: 2D resize affordances + dispose curved/spiral geometry on swap
sudhir9297 May 25, 2026
cbb9970
editor: revert in-world ground menus to HTML floating; align grid to …
sudhir9297 May 25, 2026
7cfad53
handles: registry-driven in-world resize arrows
sudhir9297 May 25, 2026
5756f24
handles: migrate wall + parent stair to registry; add arc-resize
sudhir9297 May 25, 2026
a1bc6cb
handles: tap-action descriptor + EditorApi; finish wall + fence migra…
sudhir9297 May 25, 2026
0e207a7
wall: revert side-move arrows + corner pickers to legacy component
sudhir9297 May 25, 2026
0f9274c
editor: level naming helper + ambient floorplan render during buildin…
sudhir9297 May 25, 2026
a80942d
handles: 3D resize arrows publish to useLiveNodeOverrides, commit on …
sudhir9297 May 25, 2026
9f54a79
floating menu: enable Move icon for wall / door / window
sudhir9297 May 25, 2026
3815af5
handles: live drag, guide rings, dimension chips; floating-menu Move …
sudhir9297 May 26, 2026
071f7cf
fence + column: registry handles, brace spread arrows, per-style defa…
sudhir9297 May 26, 2026
0727875
elevator + column: registry handles, rotation gizmo with curved arrow
sudhir9297 May 26, 2026
713c19b
slab + ceiling + shelf: registry handles, live polygon preview, curso…
sudhir9297 May 26, 2026
c97f702
roof-segment: registry handles, live override path, analytical-normal…
sudhir9297 May 26, 2026
7ddb229
floorplan: resize/rotate arrows for column, shelf, elevator, fence; r…
sudhir9297 May 26, 2026
d1ed88d
registry: floorplanScope + movable capability; cursor focus under mouse
sudhir9297 May 27, 2026
e89a822
wall: corner billboard, perpendicular grid snap, drop 45° from move/d…
sudhir9297 May 27, 2026
fbb4841
fence: drop 45° angle snap from draft + endpoint move, Shift = fine step
sudhir9297 May 27, 2026
4474f97
floorplan menu: show Move button for selected walls
sudhir9297 May 27, 2026
3746d63
floorplan wall move: axis-lock to wall normal, match 3D MoveWallTool
sudhir9297 May 27, 2026
8132c8f
floorplan move overlay: commit at last pointermove, not pointer-up
sudhir9297 May 27, 2026
3cc004c
floorplan: door wall-hit placement, fence/stair move fixes, wall auto…
sudhir9297 May 27, 2026
c6bb46b
Merge remote-tracking branch 'upstream/main' into fix/may-22-friday
sudhir9297 May 27, 2026
eea1e30
open-pr skill: refresh existing PR description instead of bailing
sudhir9297 May 27, 2026
dd06577
floorplan: capability/hook dispatch, generic resize + endpoint state
sudhir9297 May 27, 2026
d6b74fb
3d wall measurement: hide label for selected walls
sudhir9297 May 27, 2026
b47359a
editor: set EDITOR_LAYER on arrow handles to exclude from thumbnails
open-pascal May 27, 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
Prev Previous commit
Next Next commit
stair: 2D move-revert fix, parent ground menu, curved/spiral in-world…
… arrows

- Route stair 2D moves through `floorplanMoveTarget` and honor
  `movingNodeOrigin === '2d'` in `MoveRoofTool` cleanup so the 3D
  tool's restore-from-snapshot no longer stomps the 2D commit.
- Parent stair selection shows an in-world ground action menu
  (move / duplicate / delete) anchored beside the stair; the
  screen-space floating menu is suppressed for `type === 'stair'`
  to match door / window / segment.
- Curved & spiral stairs gain in-world resize arrows: rise (centered
  on the pillar for spirals), width, inner radius, and two sweep
  handles (one per arc end) clustered beside the width arrow.
- Camera controls pause during curved-stair drags.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
  • Loading branch information
sudhir9297 and claude committed May 25, 2026
commit 44344bffbfde43ea68eee7ffd9c536cd720ea912
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,10 @@ export const CustomCameraControls = () => {
const resizingStairSegmentWidth = useEditor((s) => s.resizingStairSegmentWidth)
const resizingStairSegmentLength = useEditor((s) => s.resizingStairSegmentLength)
const resizingStairSegmentHeight = useEditor((s) => s.resizingStairSegmentHeight)
const resizingCurvedStairRise = useEditor((s) => s.resizingCurvedStairRise)
const resizingCurvedStairWidth = useEditor((s) => s.resizingCurvedStairWidth)
const resizingCurvedStairInnerRadius = useEditor((s) => s.resizingCurvedStairInnerRadius)
const resizingCurvedStairSweep = useEditor((s) => s.resizingCurvedStairSweep)
const isBoxSelectActive = mode === 'select' && selectionTool === 'marquee'
const isInteracting = Boolean(
tool ||
Expand All @@ -156,6 +160,10 @@ export const CustomCameraControls = () => {
resizingStairSegmentWidth ||
resizingStairSegmentLength ||
resizingStairSegmentHeight ||
resizingCurvedStairRise ||
resizingCurvedStairWidth ||
resizingCurvedStairInnerRadius ||
resizingCurvedStairSweep ||
isBoxSelectActive,
)
const touches = useMemo(() => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -459,6 +459,8 @@ export function FloatingActionMenu() {
if (node?.type === 'window') return null
// Stair segments use the in-world `StairSegmentGroundActionMenu`.
if (node?.type === 'stair-segment') return null
// Parent stairs use the in-world `StairGroundActionMenu`.
if (node?.type === 'stair') return null

return (
<group>
Expand Down
3 changes: 2 additions & 1 deletion packages/editor/src/components/editor/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ import { PresetThumbnailGenerator } from './preset-thumbnail-generator'
import { SelectionManager } from './selection-manager'
import { SiteEdgeLabels } from './site-edge-labels'
import { SnapshotCaptureOverlay } from './snapshot-capture-overlay'
import { StairSegmentHandles } from './stair-segment-handles'
import { StairHandles, StairSegmentHandles } from './stair-segment-handles'
import { type SnapshotCameraData, ThumbnailGenerator } from './thumbnail-generator'
import { WallMeasurementLabel } from './wall-measurement-label'
import { WallMoveSideHandles } from './wall-move-side-handles'
Expand Down Expand Up @@ -594,6 +594,7 @@ const ViewerSceneContent = memo(function ViewerSceneContent({
{!(isVersionPreviewMode || isFirstPersonMode) && <DoorSideHandles />}
{!(isVersionPreviewMode || isFirstPersonMode) && <WindowSideHandles />}
{!(isVersionPreviewMode || isFirstPersonMode) && <StairSegmentHandles />}
{!(isVersionPreviewMode || isFirstPersonMode) && <StairHandles />}
{!(isVersionPreviewMode || isFirstPersonMode) && <FloatingActionMenu />}
{!(isVersionPreviewMode || isFirstPersonMode) && <FloatingBuildingActionMenu />}
{!isFirstPersonMode && <WallMeasurementLabel />}
Expand Down
1,179 changes: 1,105 additions & 74 deletions packages/editor/src/components/editor/stair-segment-handles.tsx

Large diffs are not rendered by default.

11 changes: 10 additions & 1 deletion packages/editor/src/components/tools/roof/move-roof-tool.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -341,7 +341,16 @@ export const MoveRoofTool: React.FC<{
// Clear ephemeral live transform
useLiveTransforms.getState().clear(movingNode.id)

if (!(wasCommitted || wasCancelled || isNew)) {
// Skip restore when the 2D floor-plan overlay claimed teardown
// ownership — same contract `FloorplanRegistryMoveOverlay` uses to
// decide whether to revert its own apply() writes. Without this,
// a stair / roof move committed in the floor plan unmounts this
// tool with `wasCommitted === false` (this tool's own grid-click
// never fired), and the restore below stomps the just-committed
// position back to the snapshot.
const finalisedBy2D = useEditor.getState().movingNodeOrigin === '2d'

if (!(wasCommitted || wasCancelled || isNew || finalisedBy2D)) {
useScene.getState().updateNode(movingNode.id, {
position: original.position,
rotation: original.rotation,
Expand Down
16 changes: 16 additions & 0 deletions packages/editor/src/store/use-editor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,14 @@ type EditorState = {
setResizingStairSegmentLength: (segment: StairSegmentNode | null) => void
resizingStairSegmentHeight: StairSegmentNode | null
setResizingStairSegmentHeight: (segment: StairSegmentNode | null) => void
resizingCurvedStairRise: StairNode | null
setResizingCurvedStairRise: (stair: StairNode | null) => void
resizingCurvedStairWidth: StairNode | null
setResizingCurvedStairWidth: (stair: StairNode | null) => void
resizingCurvedStairInnerRadius: StairNode | null
setResizingCurvedStairInnerRadius: (stair: StairNode | null) => void
resizingCurvedStairSweep: StairNode | null
setResizingCurvedStairSweep: (stair: StairNode | null) => void
curvingWall: WallNode | null
setCurvingWall: (wall: WallNode | null) => void
curvingFence: FenceNode | null
Expand Down Expand Up @@ -664,6 +672,14 @@ const useEditor = create<EditorState>()(
setResizingStairSegmentLength: (segment) => set({ resizingStairSegmentLength: segment }),
resizingStairSegmentHeight: null,
setResizingStairSegmentHeight: (segment) => set({ resizingStairSegmentHeight: segment }),
resizingCurvedStairRise: null,
setResizingCurvedStairRise: (stair) => set({ resizingCurvedStairRise: stair }),
resizingCurvedStairWidth: null,
setResizingCurvedStairWidth: (stair) => set({ resizingCurvedStairWidth: stair }),
resizingCurvedStairInnerRadius: null,
setResizingCurvedStairInnerRadius: (stair) => set({ resizingCurvedStairInnerRadius: stair }),
resizingCurvedStairSweep: null,
setResizingCurvedStairSweep: (stair) => set({ resizingCurvedStairSweep: stair }),
curvingWall: null,
setCurvingWall: (wall) => set({ curvingWall: wall }),
curvingFence: null,
Expand Down
2 changes: 2 additions & 0 deletions packages/nodes/src/stair/definition.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { type NodeDefinition, StairNode as StairNodeSchema } from '@pascal-app/core'
import { buildStairFloorplan } from './floorplan'
import { stairFloorplanMoveTarget } from './floorplan-move'
import { stairParametrics } from './parametrics'
import { StairNode } from './schema'

Expand Down Expand Up @@ -43,6 +44,7 @@ export const stairDefinition: NodeDefinition<typeof StairNode> = {
// compute their own polygon in isolation. See
// `nodes/src/stair/floorplan.ts` for the emitter.
floorplan: buildStairFloorplan,
floorplanMoveTarget: stairFloorplanMoveTarget,

presentation: {
label: 'Stair',
Expand Down
72 changes: 72 additions & 0 deletions packages/nodes/src/stair/floorplan-move.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import {
type AnyNodeId,
type FloorplanMoveTarget,
type FloorplanMoveTargetSession,
type StairNode,
snapPointToGrid,
useScene,
} from '@pascal-app/core'

/**
* 2D floor-plan move handler for stair — kicks in when the user clicks
* "Move" on the stair action menu and the floor-plan view is active.
* Stairs are free-floating on the slab (no wall anchor), so the apply
* logic is just: pointer in plan space → snap to 0.5 m grid (Shift
* bypasses) → write `position` via `useScene.updateNodes`.
*
* Routing through `floorplanMoveTarget` (instead of the overlay's
* generic Path 2 translate) fixes two latent bugs the generic path
* has for stair:
*
* 1. Path 2's `onPointerUp` bails when `event.target.closest(
* '[data-floorplan-scene]')` fails — which happens when the
* pointer-up lands on empty grid background (the SVG `<g>` only
* covers painted entries, so `target` resolves to the parent
* `<svg>` with no `[data-floorplan-scene]` ancestor). Path 1
* uses the overlay's bounding-rect check, which accepts any
* pointer inside the SVG viewport.
* 2. Path 2 commits via a single `updateNode` call that has no
* "self-owned commit" hook, so the overlay's diff path can
* silently revert when the final state matches the snapshot.
* The `commit()` below mirrors door's pattern: take ownership
* of the atomic write so the deterministic
* revert → resume → `session.commit()` path runs.
*/
export const stairFloorplanMoveTarget: FloorplanMoveTarget<StairNode> = ({ node }) => {
// Y stays put — stair's elevation is set by its parent level, not by
// the 2D plan drag. Snapshot at start so apply() doesn't need to read
// it from a possibly-mutated scene each tick.
const startY = node.position[1]

// Track the last successful placement so `commit()` can write it
// atomically — see the comment on `commit` below.
let lastValid: { position: [number, number, number] } | null = null

const session: FloorplanMoveTargetSession = {
affectedIds: [node.id as AnyNodeId],
apply({ planPoint, modifiers }) {
const [sx, sz] = modifiers.shiftKey ? planPoint : snapPointToGrid(planPoint, 0.5)
lastValid = { position: [sx, startY, sz] }
useScene.getState().updateNodes([{ id: node.id as AnyNodeId, data: lastValid }])
},
canCommit() {
// No overlap / placement rules for stairs in 2D — any pointer-up
// position commits. Mirrors the 3D move tool which also lets
// stairs land anywhere on the slab.
return true
},
commit() {
// Own the atomic write so the overlay takes the deterministic
// commit-path (revert → resume → session.commit()). The dispatcher's
// diff path would otherwise re-derive the final state by comparing
// the post-apply scene to the snapshot — which produces an empty
// diff (and silent revert) when the committed move happens to have
// identical key/value pairs to the snapshot. Owning commit removes
// that foot-gun. Same pattern door / window use.
if (!lastValid) return
useScene.getState().updateNodes([{ id: node.id as AnyNodeId, data: lastValid }])
},
}

return session
}