Snap roof segment resize handles to grid
This commit is contained in:
@@ -130,6 +130,8 @@ export type LinearResizeHandle<N> = {
|
|||||||
overrideTarget?: (node: N, sceneApi: SceneApi) => AnyNodeId | undefined
|
overrideTarget?: (node: N, sceneApi: SceneApi) => AnyNodeId | undefined
|
||||||
min?: number | ((node: N, sceneApi: SceneApi) => number)
|
min?: number | ((node: N, sceneApi: SceneApi) => number)
|
||||||
max?: number | ((node: N, sceneApi: SceneApi) => number)
|
max?: number | ((node: N, sceneApi: SceneApi) => number)
|
||||||
|
/** Snap the resized scalar to the editor's active grid step before apply. */
|
||||||
|
gridSnap?: boolean
|
||||||
placement: HandlePlacement<N>
|
placement: HandlePlacement<N>
|
||||||
/**
|
/**
|
||||||
* Dimension this handle steers (e.g. `'height'`). When set, the editor
|
* Dimension this handle steers (e.g. `'height'`). When set, the editor
|
||||||
|
|||||||
@@ -541,6 +541,8 @@ export type FloorplanAffordance<N> = {
|
|||||||
nodes: Record<AnyNodeId, AnyNode>
|
nodes: Record<AnyNodeId, AnyNode>
|
||||||
/** Initial pointer position in plan coordinates. */
|
/** Initial pointer position in plan coordinates. */
|
||||||
initialPlanPoint: FloorplanAffordancePoint
|
initialPlanPoint: FloorplanAffordancePoint
|
||||||
|
/** Active editor grid step in meters. */
|
||||||
|
gridSnapStep: number
|
||||||
}): FloorplanAffordanceSession
|
}): FloorplanAffordanceSession
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -471,6 +471,7 @@ export const FloorplanRegistryLayer = memo(function FloorplanRegistryLayer() {
|
|||||||
payload,
|
payload,
|
||||||
nodes: sceneNodes,
|
nodes: sceneNodes,
|
||||||
initialPlanPoint,
|
initialPlanPoint,
|
||||||
|
gridSnapStep: useEditor.getState().gridSnapStep,
|
||||||
})
|
})
|
||||||
|
|
||||||
const snapshots: NodeSnapshot[] = []
|
const snapshots: NodeSnapshot[] = []
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import {
|
|||||||
nodeRegistry,
|
nodeRegistry,
|
||||||
type RadialResizeHandle,
|
type RadialResizeHandle,
|
||||||
sceneRegistry,
|
sceneRegistry,
|
||||||
|
snapScalar,
|
||||||
type TapActionHandle,
|
type TapActionHandle,
|
||||||
type TranslateHandle,
|
type TranslateHandle,
|
||||||
useLiveNodeOverrides,
|
useLiveNodeOverrides,
|
||||||
@@ -581,6 +582,10 @@ function LinearArrow({
|
|||||||
descriptor.axis === 'x' ? hitLocal.x : descriptor.axis === 'y' ? hitLocal.y : hitLocal.z
|
descriptor.axis === 'x' ? hitLocal.x : descriptor.axis === 'y' ? hitLocal.y : hitLocal.z
|
||||||
const minBound = resolveBound(descriptor.min, Number.NEGATIVE_INFINITY, initialNode, sceneApi)
|
const minBound = resolveBound(descriptor.min, Number.NEGATIVE_INFINITY, initialNode, sceneApi)
|
||||||
const maxBound = resolveBound(descriptor.max, Number.POSITIVE_INFINITY, initialNode, sceneApi)
|
const maxBound = resolveBound(descriptor.max, Number.POSITIVE_INFINITY, initialNode, sceneApi)
|
||||||
|
const gridSnapStep =
|
||||||
|
descriptor.kind === 'linear-resize' && descriptor.gridSnap
|
||||||
|
? useEditor.getState().gridSnapStep
|
||||||
|
: null
|
||||||
const factor =
|
const factor =
|
||||||
descriptor.kind === 'radial-resize'
|
descriptor.kind === 'radial-resize'
|
||||||
? 1
|
? 1
|
||||||
@@ -615,7 +620,10 @@ function LinearArrow({
|
|||||||
? intersectionLocal.y
|
? intersectionLocal.y
|
||||||
: intersectionLocal.z
|
: intersectionLocal.z
|
||||||
const delta = currentPointer - initialPointer
|
const delta = currentPointer - initialPointer
|
||||||
const next = Math.min(maxBound, Math.max(minBound, initialValue + delta * factor))
|
const rawNext = initialValue + delta * factor
|
||||||
|
const snappedNext =
|
||||||
|
gridSnapStep && gridSnapStep > 0 ? snapScalar(rawNext, gridSnapStep) : rawNext
|
||||||
|
const next = Math.min(maxBound, Math.max(minBound, snappedNext))
|
||||||
return descriptor.apply(initialNode as never, next, sceneApi) as Partial<AnyNode>
|
return descriptor.apply(initialNode as never, next, sceneApi) as Partial<AnyNode>
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -56,6 +56,7 @@ function roofSegmentWidthHandle(side: 'left' | 'right'): HandleDescriptor<RoofSe
|
|||||||
// 'max' = +X edge anchored (left arrow grows the -X edge outward).
|
// 'max' = +X edge anchored (left arrow grows the -X edge outward).
|
||||||
anchor: side === 'right' ? 'min' : 'max',
|
anchor: side === 'right' ? 'min' : 'max',
|
||||||
min: MIN_ROOF_DIM,
|
min: MIN_ROOF_DIM,
|
||||||
|
gridSnap: true,
|
||||||
currentValue: (n) => n.width,
|
currentValue: (n) => n.width,
|
||||||
apply: (initial, newWidth) => {
|
apply: (initial, newWidth) => {
|
||||||
const rotY = initial.rotation ?? 0
|
const rotY = initial.rotation ?? 0
|
||||||
@@ -100,6 +101,7 @@ function roofSegmentDepthHandle(side: 'front' | 'back'): HandleDescriptor<RoofSe
|
|||||||
axis: 'z',
|
axis: 'z',
|
||||||
anchor: side === 'front' ? 'min' : 'max',
|
anchor: side === 'front' ? 'min' : 'max',
|
||||||
min: MIN_ROOF_DIM,
|
min: MIN_ROOF_DIM,
|
||||||
|
gridSnap: true,
|
||||||
currentValue: (n) => n.depth,
|
currentValue: (n) => n.depth,
|
||||||
apply: (initial, newDepth) => {
|
apply: (initial, newDepth) => {
|
||||||
// Recenter so the anchored Z edge stays at the same world point.
|
// Recenter so the anchored Z edge stays at the same world point.
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import {
|
|||||||
type FloorplanMoveTarget,
|
type FloorplanMoveTarget,
|
||||||
type RoofNode,
|
type RoofNode,
|
||||||
type RoofSegmentNode,
|
type RoofSegmentNode,
|
||||||
|
snapScalar,
|
||||||
useScene,
|
useScene,
|
||||||
} from '@pascal-app/core'
|
} from '@pascal-app/core'
|
||||||
|
|
||||||
@@ -56,7 +57,7 @@ function resolveSegmentFrame(
|
|||||||
* the math survives any parent-roof rotation.
|
* the math survives any parent-roof rotation.
|
||||||
*/
|
*/
|
||||||
export const roofSegmentResizeAffordance: FloorplanAffordance<RoofSegmentNode> = {
|
export const roofSegmentResizeAffordance: FloorplanAffordance<RoofSegmentNode> = {
|
||||||
start({ node, payload, nodes, initialPlanPoint }) {
|
start({ node, payload, nodes, initialPlanPoint, gridSnapStep }) {
|
||||||
const { axis, side } = payload as RoofSegmentResizePayload
|
const { axis, side } = payload as RoofSegmentResizePayload
|
||||||
const segmentId = node.id as AnyNodeId
|
const segmentId = node.id as AnyNodeId
|
||||||
const initialValue = axis === 'x' ? node.width : node.depth
|
const initialValue = axis === 'x' ? node.width : node.depth
|
||||||
@@ -79,7 +80,9 @@ export const roofSegmentResizeAffordance: FloorplanAffordance<RoofSegmentNode> =
|
|||||||
apply({ planPoint }) {
|
apply({ planPoint }) {
|
||||||
const currentLocal = projectLocalAxis(planPoint[0], planPoint[1])
|
const currentLocal = projectLocalAxis(planPoint[0], planPoint[1])
|
||||||
const delta = (currentLocal - initialLocal) * side
|
const delta = (currentLocal - initialLocal) * side
|
||||||
const newValue = Math.max(MIN_ROOF_DIM, initialValue + 2 * delta)
|
const rawValue = initialValue + 2 * delta
|
||||||
|
const snappedValue = gridSnapStep > 0 ? snapScalar(rawValue, gridSnapStep) : rawValue
|
||||||
|
const newValue = Math.max(MIN_ROOF_DIM, snappedValue)
|
||||||
lastValue = newValue
|
lastValue = newValue
|
||||||
useScene
|
useScene
|
||||||
.getState()
|
.getState()
|
||||||
|
|||||||
Reference in New Issue
Block a user