feat(nodes): mode-aware roof-segment edit + 2D rotation parity with 3D

Roof-segment edit/move ignored the active snap mode and showed no chip:
- add snapProfile:'structural' so a body-move resolves the no-angle polygon
  context (grid/lines/off) like every other structural move;
- resize uses getSegmentGridStep() (0 outside grid mode = the "smooth" resize
  that used to need a held Shift), dropping the captured gridSnapStep + Shift;
- move drops its Shift bypass;
- the affordance dispatcher opens a boundary reshape scope for the resize so
  the snapping chip shows and the context resolves.

2D rotation handles now match the 3D gizmo across all six rotate affordances
(column / elevator / roof-segment / shelf / spawn / stair): a shared
rotateAffordanceDelta snaps to the 15° step unless Shift (free), the
dispatcher opens the same ROTATE_HANDLE_DRAG_LABEL handle-drag scope the 3D
gizmo uses so the contextual HUD shows the "Shift = rotate freely" hint, and
the live degree readout snaps to match the committed rotation.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Wassim SAMAD
2026-06-25 16:12:26 -04:00
co-authored by Claude Opus 4.8
parent 7512fccdaf
commit 84cdf4519d
9 changed files with 116 additions and 40 deletions
@@ -4,6 +4,7 @@ import {
type FloorplanAffordance,
useScene,
} from '@pascal-app/core'
import { rotateAffordanceDelta } from '../shared/rotate-affordance'
// Floor minimums — mirror the 3D handles in `column/definition.ts` so a
// drag can't push a value past what the renderer accepts.
@@ -146,11 +147,13 @@ export const columnRotateAffordance: FloorplanAffordance<ColumnNode> = {
return {
affectedIds: [columnId],
apply({ planPoint }) {
const currentAngle = Math.atan2(planPoint[1] - cz, planPoint[0] - cx)
let delta = currentAngle - initialAngle
while (delta > Math.PI) delta -= 2 * Math.PI
while (delta < -Math.PI) delta += 2 * Math.PI
apply({ planPoint, modifiers }) {
const delta = rotateAffordanceDelta({
center: [cx, cz],
initialAngle,
planPoint,
free: modifiers.shiftKey,
})
const newRotation = initialRotation - delta
lastRotation = newRotation
useScene.getState().updateNode(columnId, { rotation: newRotation })