feat(editor): wall room/single + fence continuous/single chain toggles
Replace the legacy held-Alt mechanism on wall and fence drafting with a mode toggle, mirroring the snapping-mode chip: - Wall: `wallChainMode` room (auto-close on loop) / single. Room mode finishes automatically when the new endpoint lands within the join-snap radius of the chain's first vertex; single commits one wall per click. - Fence: `fenceChainMode` continuous (chain until double-click/Esc) / single. Fences are linear barriers, so continuous has no auto-close. - Both: Alt-tap cycles the active drafting tool's chain mode (clean-tap, scoped to wall/fence drafting); a clickable HUD chip shows the mode. Persisted + migrated in `useEditor`. Migrate wall and fence off held-Alt-bypass-alignment to the unified convention: alignment now follows the magnetic snap mode, which frees Alt for the toggle. 2D floorplan parity kept in sync with the 3D tools. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
6d02ad424e
commit
2666b07479
@@ -467,8 +467,7 @@ export const FenceTool: React.FC = () => {
|
||||
}
|
||||
|
||||
// Align the drafted point onto another object's nearest real anchor and
|
||||
// publish the guide. Alt bypasses alignment. Returns the possibly snapped
|
||||
// point.
|
||||
// publish the guide. Returns the possibly snapped point.
|
||||
const alignPoint = (point: FencePlanPoint, bypass: boolean): FencePlanPoint => {
|
||||
// Figma alignment pulls the endpoint onto existing corners / edges, so it
|
||||
// is a line snap — suppress it whenever magnetic snap is off (`'off'` /
|
||||
@@ -500,8 +499,9 @@ export const FenceTool: React.FC = () => {
|
||||
const localPoint: FencePlanPoint = [event.localPosition[0], event.localPosition[2]]
|
||||
// While drafting, the segment locks to 15° rays from its start.
|
||||
// Snapping is governed by the snapping mode (`'off'` is the bypass);
|
||||
// there is no Shift hold-to-bypass. Alt still bypasses alignment guides.
|
||||
const bypassAlign = event.nativeEvent?.altKey === true
|
||||
// there is no Shift hold-to-bypass. Alignment follows the magnetic snap
|
||||
// mode, not Alt (Alt-tap toggles continuous/single chaining).
|
||||
const bypassAlign = !isMagneticSnapActive()
|
||||
|
||||
if (buildingState.current === 1) {
|
||||
const angleLocked = isAngleSnapActive()
|
||||
@@ -567,7 +567,7 @@ export const FenceTool: React.FC = () => {
|
||||
|
||||
const { walls, fences } = getCurrentLevelElements()
|
||||
const localClick: FencePlanPoint = [event.localPosition[0], event.localPosition[2]]
|
||||
const bypassAlign = event.nativeEvent?.altKey === true
|
||||
const bypassAlign = !isMagneticSnapActive()
|
||||
|
||||
if (buildingState.current === 0) {
|
||||
const snappedStart = alignPoint(
|
||||
@@ -612,6 +612,13 @@ export const FenceTool: React.FC = () => {
|
||||
refreshAlignmentCandidates()
|
||||
useAlignmentGuides.getState().clear()
|
||||
|
||||
// Single mode commits one segment per click: stop drafting so the next
|
||||
// click starts a fresh segment instead of chaining off this endpoint.
|
||||
if (useEditor.getState().fenceChainMode === 'single') {
|
||||
stopDrafting()
|
||||
return
|
||||
}
|
||||
|
||||
const nextStart = createdFence.end
|
||||
// Publish the resolved chain start so the 2D floor-plan draft
|
||||
// chains its next segment from the same point (its own snap
|
||||
|
||||
@@ -30,6 +30,7 @@ import {
|
||||
useEditor,
|
||||
useSegmentDraftChain,
|
||||
useWallSnapIndicator,
|
||||
WALL_JOIN_SNAP_RADIUS,
|
||||
type WallPlanPoint,
|
||||
} from '@pascal-app/editor'
|
||||
import { getSceneTheme, useViewer } from '@pascal-app/viewer'
|
||||
@@ -142,6 +143,13 @@ function pointMatches(a: WallPlanPoint, b: WallPlanPoint, tolerance = 1e-5) {
|
||||
return distanceSquared(a, b) <= tolerance * tolerance
|
||||
}
|
||||
|
||||
function isWithinWallJoinSnapRadius(point: WallPlanPoint, vertex: Vector3) {
|
||||
const dx = point[0] - vertex.x
|
||||
const dz = point[1] - vertex.z
|
||||
|
||||
return dx * dx + dz * dz <= WALL_JOIN_SNAP_RADIUS * WALL_JOIN_SNAP_RADIUS
|
||||
}
|
||||
|
||||
function getNearestAxisAngleLabel(
|
||||
start: WallPlanPoint,
|
||||
end: WallPlanPoint,
|
||||
@@ -487,6 +495,7 @@ export const WallTool: React.FC = () => {
|
||||
const wallPreviewRef = useRef<Mesh>(null!)
|
||||
const startingPoint = useRef(new Vector3(0, 0, 0))
|
||||
const endingPoint = useRef(new Vector3(0, 0, 0))
|
||||
const chainFirstVertex = useRef<Vector3 | null>(null)
|
||||
const buildingState = useRef(0)
|
||||
const [draftMeasurement, setDraftMeasurement] = useState<DraftMeasurementState>(null)
|
||||
const [axisGuide, setAxisGuide] = useState<DraftAxisGuideState>(null)
|
||||
@@ -509,8 +518,7 @@ export const WallTool: React.FC = () => {
|
||||
}
|
||||
|
||||
// Align the drafted point onto another object's nearest real anchor and
|
||||
// publish the guide. Alt bypasses alignment. Returns the possibly snapped
|
||||
// point.
|
||||
// publish the guide. Returns the possibly snapped point.
|
||||
const alignPoint = (
|
||||
point: WallPlanPoint,
|
||||
options: { applySnap?: boolean; bypass?: boolean },
|
||||
@@ -535,6 +543,7 @@ export const WallTool: React.FC = () => {
|
||||
|
||||
const stopDrafting = () => {
|
||||
buildingState.current = 0
|
||||
chainFirstVertex.current = null
|
||||
if (wallPreviewRef.current) {
|
||||
wallPreviewRef.current.visible = false
|
||||
}
|
||||
@@ -552,7 +561,6 @@ export const WallTool: React.FC = () => {
|
||||
const localPoint: WallPlanPoint = [event.localPosition[0], event.localPosition[2]]
|
||||
// Snapping is governed entirely by the snapping mode (grid / lines /
|
||||
// angles / off). `'off'` is the bypass — there is no Shift hold-to-bypass.
|
||||
// Alt still bypasses Figma-style alignment guides independently.
|
||||
const angleLocked = buildingState.current === 1 && isAngleSnapActive()
|
||||
// Alignment guides follow the snapping mode (lines = magnetic on), not Alt.
|
||||
const bypassAlign = !isMagneticSnapActive()
|
||||
@@ -649,6 +657,7 @@ export const WallTool: React.FC = () => {
|
||||
)
|
||||
gridPosition = snappedStart
|
||||
startingPoint.current.set(snappedStart[0], event.localPosition[1], snappedStart[1])
|
||||
chainFirstVertex.current = startingPoint.current.clone()
|
||||
endingPoint.current.copy(startingPoint.current)
|
||||
buildingState.current = 1
|
||||
setAxisGuide({
|
||||
@@ -696,9 +705,16 @@ export const WallTool: React.FC = () => {
|
||||
useAlignmentGuides.getState().clear()
|
||||
useWallSnapIndicator.getState().clear()
|
||||
|
||||
// Alt commits a single wall — stop drafting instead of chaining
|
||||
// so the next click starts a fresh start point.
|
||||
if (event.nativeEvent?.altKey === true) {
|
||||
const wallChainMode = useEditor.getState().wallChainMode
|
||||
if (wallChainMode === 'single') {
|
||||
stopDrafting()
|
||||
return
|
||||
}
|
||||
|
||||
if (
|
||||
chainFirstVertex.current &&
|
||||
isWithinWallJoinSnapRadius(createdWall.end, chainFirstVertex.current)
|
||||
) {
|
||||
stopDrafting()
|
||||
return
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user