/* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ /** * State for the Split tool. * * Single-element flow today: user hovers a wall, the overlay * projects the cursor onto the wall's axis to compute a candidate * cut distance, click commits via `MutationSlice.splitWallAtDistance`. * * Bigger surface (slab cut-line, multi-element plane split, beam / * column / member) is in `docs/design/element-splitting.md` and * arrives in subsequent phases. The slice is shaped to grow without * a breaking rename — `splitMode` covers the future modes; * `splitHoverPoint` and `splitTargetExpressId` will be reused. */ import type { StateCreator } from 'zustand'; /** * Split-tool state machine: * - `'idle'` — cursor isn't over a splittable element * - `'aiming'` — preview cut is live for a single-click element * (wall / beam / column / member) * - `'first-anchor'` — slab two-click flow: user clicked once, * the anchor is latched, the cursor's second click commits * the cut line. Esc clears back to `'idle'`. */ export type SplitMode = 'idle' | 'aiming' | 'first-anchor'; export interface SplitToolSlice { /** Tool state machine. `'idle'` while the cursor isn't over a * splittable element; `'aiming'` while a preview cut is live. */ splitMode: SplitMode; /** Federated model id that owns the hovered target. */ splitTargetModelId: string | null; /** Express id of the hovered wall. */ splitTargetExpressId: number | null; /** * Cursor in storey-local IFC space (Z-up). The overlay reads * this to drive a per-frame preview without trampolining through * camera projection. */ splitHoverPoint: [number, number, number] | null; /** * Cached projection from the last hover update — distance along * the wall axis from start. Single source of truth for both the * SVG preview label and the commit handler. */ splitHoverDistance: number | null; /** Total length of the hovered wall (for the "1.42 / 3.50 m" label). */ splitHoverLength: number | null; /** Cut point in storey-local space, derived from distance + wall axis. */ splitHoverCutPoint: [number, number, number] | null; /** * Unit-length axis direction in storey-local IFC space (Z-up). * Used by the overlay to compute the perpendicular guide line * without re-walking the IFC chain. Element-type-agnostic — wall * axis lies on the storey plane, beam/member axis can have a Z * component, column axis is `[0, 0, 1]`. */ splitHoverAxisDirection: [number, number, number] | null; /** * Slab two-click flow: storey-local XY of the first click. When * set, the tool is in `'first-anchor'` state and the next click * commits a cut line from the anchor to the cursor. */ slabCutAnchor: [number, number] | null; /** * Slab cached footprint polygon (storey-local 2D) — read once on * first-anchor latch so the overlay can render the polygon * outline and clip the ghost cut line against it without * re-walking the IFC chain per frame. */ slabCutFootprint: [number, number][] | null; slabCutStoreyElevation: number | null; setSplitTarget: (modelId: string | null, expressId: number | null) => void; setSplitHover: ( hoverPoint: [number, number, number] | null, distance: number | null, length: number | null, cutPoint: [number, number, number] | null, axisDirection: [number, number, number] | null, ) => void; /** * Slab flow: latch the first click + cache the footprint so the * overlay's per-frame work stays cheap. Promotes the tool to * `'first-anchor'`. */ setSlabCutAnchor: ( anchor: [number, number] | null, footprint: [number, number][] | null, storeyElevation: number | null, ) => void; clearSplitHover: () => void; } export const createSplitToolSlice: StateCreator = (set) => ({ splitMode: 'idle', splitTargetModelId: null, splitTargetExpressId: null, splitHoverPoint: null, splitHoverDistance: null, splitHoverLength: null, splitHoverCutPoint: null, splitHoverAxisDirection: null, slabCutAnchor: null, slabCutFootprint: null, slabCutStoreyElevation: null, setSplitTarget: (modelId, expressId) => set((s) => ({ splitTargetModelId: modelId, splitTargetExpressId: expressId, // Entering / leaving a target without a hover yet means we're // back to 'idle' (unless we're mid-slab-cut, in which case // we preserve the latched anchor — the user might re-enter // the slab from outside). splitMode: s.splitMode === 'first-anchor' ? 'first-anchor' : 'idle', splitHoverPoint: null, splitHoverDistance: null, splitHoverLength: null, splitHoverCutPoint: null, splitHoverAxisDirection: null, })), setSplitHover: (hoverPoint, distance, length, cutPoint, axisDirection) => set((s) => ({ splitHoverPoint: hoverPoint, splitHoverDistance: distance, splitHoverLength: length, splitHoverCutPoint: cutPoint, splitHoverAxisDirection: axisDirection, // Promote to 'aiming' on first hover; don't downgrade the // slab two-click state when hover ends mid-flow. splitMode: s.splitMode === 'first-anchor' ? 'first-anchor' : hoverPoint !== null ? 'aiming' : 'idle', })), setSlabCutAnchor: (anchor, footprint, storeyElevation) => set({ slabCutAnchor: anchor, slabCutFootprint: footprint, slabCutStoreyElevation: storeyElevation, splitMode: anchor === null ? 'idle' : 'first-anchor', }), clearSplitHover: () => set({ splitMode: 'idle', splitTargetModelId: null, splitTargetExpressId: null, splitHoverPoint: null, splitHoverDistance: null, splitHoverLength: null, splitHoverCutPoint: null, splitHoverAxisDirection: null, slabCutAnchor: null, slabCutFootprint: null, slabCutStoreyElevation: null, }), });