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:
co-authored by
Claude Opus 4.8
parent
7512fccdaf
commit
84cdf4519d
@@ -267,6 +267,11 @@ export const roofSegmentDefinition: NodeDefinition<typeof RoofSegmentNode> = {
|
||||
schema: RoofSegmentNode,
|
||||
category: 'structure',
|
||||
surfaceRole: 'roof',
|
||||
// Mirrors the parent roof: a body-move resolves the no-angle `polygon`
|
||||
// snap context (grid / lines / off), so dragging a segment shows the
|
||||
// snapping chip and honours the active mode like every other structural
|
||||
// move. Resize / rotate run through their own reshaping scope.
|
||||
snapProfile: 'structural',
|
||||
|
||||
defaults: () => {
|
||||
const stub = RoofSegmentNodeSchema.parse({
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
} from '@pascal-app/core'
|
||||
import { getSegmentGridStep } from '@pascal-app/editor'
|
||||
import { createFloorplanCursorResolver } from '../shared/floorplan-cursor'
|
||||
import { rotateAffordanceDelta } from '../shared/rotate-affordance'
|
||||
|
||||
const MIN_ROOF_DIM = 1
|
||||
|
||||
@@ -59,7 +60,7 @@ function resolveSegmentFrame(
|
||||
* the math survives any parent-roof rotation.
|
||||
*/
|
||||
export const roofSegmentResizeAffordance: FloorplanAffordance<RoofSegmentNode> = {
|
||||
start({ node, payload, nodes, initialPlanPoint, gridSnapStep }) {
|
||||
start({ node, payload, nodes, initialPlanPoint }) {
|
||||
const { axis, side } = payload as RoofSegmentResizePayload
|
||||
const segmentId = node.id as AnyNodeId
|
||||
const initialValue = axis === 'x' ? node.width : node.depth
|
||||
@@ -79,12 +80,15 @@ export const roofSegmentResizeAffordance: FloorplanAffordance<RoofSegmentNode> =
|
||||
|
||||
return {
|
||||
affectedIds: [segmentId],
|
||||
apply({ planPoint, modifiers }) {
|
||||
apply({ planPoint }) {
|
||||
const currentLocal = projectLocalAxis(planPoint[0], planPoint[1])
|
||||
const delta = (currentLocal - initialLocal) * side
|
||||
const rawValue = initialValue + 2 * delta
|
||||
const snappedValue =
|
||||
!modifiers.shiftKey && gridSnapStep > 0 ? snapScalar(rawValue, gridSnapStep) : rawValue
|
||||
// Mode-aware grid step (0 outside grid mode, so `lines` / `off` resize
|
||||
// freely — the "smooth" behaviour that used to need a held Shift). The
|
||||
// reshaping scope opened by the dispatcher resolves the `polygon` set.
|
||||
const step = getSegmentGridStep()
|
||||
const snappedValue = step > 0 ? snapScalar(rawValue, step) : rawValue
|
||||
const newValue = Math.max(MIN_ROOF_DIM, snappedValue)
|
||||
lastValue = newValue
|
||||
useScene
|
||||
@@ -121,11 +125,13 @@ export const roofSegmentRotateAffordance: FloorplanAffordance<RoofSegmentNode> =
|
||||
|
||||
return {
|
||||
affectedIds: [segmentId],
|
||||
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,
|
||||
})
|
||||
lastRotation = initialRotation - delta
|
||||
useScene.getState().updateNode(segmentId, { rotation: lastRotation })
|
||||
},
|
||||
@@ -168,9 +174,12 @@ export const roofSegmentMoveTarget: FloorplanMoveTarget<RoofSegmentNode> = ({ no
|
||||
|
||||
return {
|
||||
affectedIds: [segmentId],
|
||||
apply({ planPoint, modifiers }) {
|
||||
apply({ planPoint }) {
|
||||
// Mode-aware: `getSegmentGridStep()` is 0 outside grid mode (so `lines` /
|
||||
// `off` move freely), and the `moving` scope resolves the `polygon` set
|
||||
// via the kind's `snapProfile` — no held-Shift bypass.
|
||||
const step = getSegmentGridStep()
|
||||
const snap = (value: number) => (modifiers.shiftKey ? value : snapScalar(value, step))
|
||||
const snap = (value: number) => snapScalar(value, step)
|
||||
const worldPoint = resolveCursor(planPoint, { snap })
|
||||
const dx = worldPoint[0] - roofPosX
|
||||
const dz = worldPoint[1] - roofPosZ
|
||||
|
||||
Reference in New Issue
Block a user