From 6fc643b97516b65f7f7c01b29be76651ff480a28 Mon Sep 17 00:00:00 2001 From: Ivan Chirkin Date: Tue, 16 Jun 2026 02:58:39 +0300 Subject: [PATCH] feat(drill): replace ad-hoc Z buttons in FiducialPanel with DrillManualZBar in safe-descent mode --- .../src/components/drill/DrillManualZBar.tsx | 149 ++++++++++++++---- .../src/components/drill/FiducialPanel.tsx | 67 +++----- cuprum-ui/src/lib/zbar.test.ts | 26 ++- cuprum-ui/src/lib/zbar.ts | 17 ++ cuprum-ui/src/locales/en/drill.json | 6 +- cuprum-ui/src/locales/ru/drill.json | 6 +- docs/PROJECT.md | 4 +- 7 files changed, 183 insertions(+), 92 deletions(-) diff --git a/cuprum-ui/src/components/drill/DrillManualZBar.tsx b/cuprum-ui/src/components/drill/DrillManualZBar.tsx index 62aadc53..2f9553cb 100644 --- a/cuprum-ui/src/components/drill/DrillManualZBar.tsx +++ b/cuprum-ui/src/components/drill/DrillManualZBar.tsx @@ -5,7 +5,7 @@ import { useMachine } from "@/machineStore"; import { useSettings } from "@/settingsStore"; import { useJog } from "@/hooks/useJog"; import { classifyBindZ, type ZBindBand } from "@/lib/drillZHeadroom"; -import { parseZTarget } from "@/lib/zbar"; +import { isSafeDescentTarget, parseZTarget } from "@/lib/zbar"; import { cn } from "@/lib/utils"; const clamp01 = (f: number) => (f <= 0 ? 0 : f >= 1 ? 1 : f); @@ -14,6 +14,48 @@ const clamp01 = (f: number) => (f <= 0 ? 0 : f >= 1 ? 1 : f); const Z_BTN = "grid h-9 w-12 shrink-0 place-items-center rounded-md border border-border bg-card text-foreground transition-colors hover:border-primary/40 hover:bg-foreground/5 active:bg-primary/10 disabled:pointer-events-none disabled:opacity-30"; +/** Props for DrillManualZBar in default (tool-change) mode. */ +interface DefaultModeProps { + /** Machine Z (mm) of the last manual touch-off this session — yellow reference tick. */ + lastZMm: number | null; + /** Safe machine-Z bind band (from `zBindBand`). null/unknown → no forbidden zones drawn. */ + band?: ZBindBand | null; + /** Safe-descent mode disabled — this is the default tool-change behaviour. */ + safeDescent?: false; + zLabel?: never; + caption?: never; + safeSteps?: never; + descentFeedMmMin?: never; +} + +/** Props for DrillManualZBar in safe-descent (fiducial capture) mode. + * + * In this mode: + * - Steps are overridden to `safeSteps` (fine only; no "cont"). + * - Z− steps use `descentFeedMmMin` (slow); Z+ uses the normal jog feed. + * - Track clicks only move Z upward; downward clicks are silently ignored. + * - The inline readout is read-only (no keyboard jog to an arbitrary height). + * - `lastZMm` and `band` are unused (no reference tick, no forbidden zones). */ +interface SafeDescentModeProps { + lastZMm?: never; + band?: never; + /** Enable safe-descent restrictions. */ + safeDescent: true; + /** Step sizes (mm) to offer in safe mode. Defaults to [0.05, 0.1, 0.5]. */ + safeSteps?: number[]; + /** Feed rate for downward steps (mm/min). Defaults to 60. */ + descentFeedMmMin?: number; + /** Label next to the Z badge (override for the default tool-change label). */ + zLabel?: string; + /** Caption below the bar (override for the default tool-change hint). */ + caption?: string; +} + +export type DrillManualZBarProps = DefaultModeProps | SafeDescentModeProps; + +const DEFAULT_SAFE_STEPS = [0.05, 0.1, 0.5]; +const DEFAULT_DESCENT_FEED = 60; + /** Manual Z touch-off bar for the tool-change card: a Z badge + live readout, a step * selector, and a horizontal track `Z−` | bar | `Z+`. The track maps the MACHINE Z * over the travel `[-maxZMm, 0]` (left = lowest / into the material, right = the homed @@ -27,19 +69,21 @@ const Z_BTN = * the safe/tool-change rapid would punch past Z=0); the safe band stays dark. The thumb * turns red while the live Z sits in a forbidden zone — the confirm button is gated to match. * + * Optional `safeDescent` mode restricts controls for safe fiducial capture: only upward + * track clicks, no inline editing, no continuous hold, slow descent feed (see + * SafeDescentModeProps). + * * It does NOT bind Z — the card's confirm button does that (G10 L20 P1 on Z). */ -export function DrillManualZBar({ - lastZMm, - band, -}: { - lastZMm: number | null; - /** Safe machine-Z bind band (from `zBindBand`). null/unknown → no forbidden zones drawn. */ - band?: ZBindBand | null; -}) { +export function DrillManualZBar(props: DrillManualZBarProps) { const { t } = useTranslation("drill"); + const safeDescent = props.safeDescent === true; + const safeSteps = safeDescent ? (props.safeSteps ?? DEFAULT_SAFE_STEPS) : null; + const descentFeed = safeDescent ? (props.descentFeedMmMin ?? DEFAULT_DESCENT_FEED) : null; + const maxZMm = useSettings((s) => s.cncProfile.workEnvelopeMm.z); - const steps = useSettings((s) => s.cncProfile.jogStepsMm); + const profileSteps = useSettings((s) => s.cncProfile.jogStepsMm); + const steps = safeSteps ?? profileSteps; // Live machine/work Z — the work offset converts a track target to a work-frame jog. const mz = useMachine((s) => s.status.mpos[2]); @@ -55,6 +99,19 @@ export function DrillManualZBar({ const { enabled, step, setStep, continuous, go, startContinuous, stopContinuous, jogTo } = useJog({ bounds }); + // In safe-descent mode: select the first safe step if the current step is not in the list + // (e.g. user was on a coarse step from the jog pad before switching to capture mode). + // Do this on mount / when safeSteps changes rather than every render to avoid a loop. + const [safeStepInit, setSafeStepInit] = useState(false); + useEffect(() => { + if (!safeDescent) { setSafeStepInit(false); return; } + if (safeStepInit) return; + const validStep = typeof step === "number" && safeSteps!.includes(step); + if (!validStep) setStep(safeSteps![1] ?? safeSteps![0] ?? 0.1); + setSafeStepInit(true); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [safeDescent]); + // Stop any in-flight continuous jog on unmount. useEffect(() => () => stopContinuous(), [stopContinuous]); @@ -63,25 +120,22 @@ export function DrillManualZBar({ // Thumb / mark fraction: 0 at the bottom (left, -maxZMm) → 1 at the ceiling (right, 0). const fracOf = (machineZ: number) => clamp01((machineZ + maxZMm) / range); const thumbFrac = fracOf(mz); + + // Default mode: last-Z mark and forbidden zones. + const lastZMm = !safeDescent ? (props as DefaultModeProps).lastZMm : null; + const band = !safeDescent ? (props as DefaultModeProps).band : null; const lastFrac = lastZMm != null ? fracOf(lastZMm) : null; - // Forbidden-zone fractions from the headroom band: the floor zone fills [0, minZ), - // the ceiling zone fills (maxZ, 1]. fracOf clamps, so a band edge outside the bar's - // travel collapses its zone to zero width. Thumb turns red while Z sits in a zone. const known = band?.known ?? false; - const floorZoneFrac = known ? fracOf(band!.minZ) : 0; // red from 0 up to here - const ceilZoneFrac = known ? fracOf(band!.maxZ) : 1; // red from here up to 1 + const floorZoneFrac = known ? fracOf(band!.minZ) : 0; + const ceilZoneFrac = known ? fracOf(band!.maxZ) : 1; const thumbBlocked = known && classifyBindZ(band!, mz) != null; const RED = "#ef4444"; // Hover target (fraction along the track) for the tooltip + ghost line. const [hoverFrac, setHoverFrac] = useState(null); - // Inline-editable readout: click the live Z value to type an exact machine-Z target, - // then jog there (absolute) on commit — the same cancel-then-retarget as a track click. - // Editable only while motion is allowed. Out-of-travel / non-numeric input is rejected - // (invalid → revert, no jog). `doneRef` makes the first commit/cancel win, so the - // synthetic blur fired when the input unmounts on Enter/Escape doesn't double-jog. + // Inline-editable readout (default mode only — disabled in safe-descent mode). const [editing, setEditing] = useState(false); const [draft, setDraft] = useState(""); const inputRef = useRef(null); @@ -97,7 +151,7 @@ export function DrillManualZBar({ }, [editing]); const startEdit = () => { - if (!enabled) return; + if (!enabled || safeDescent) return; setDraft(mz.toFixed(1)); setEditing(true); }; @@ -125,12 +179,18 @@ export function DrillManualZBar({ const onTrackClick = (e: React.MouseEvent) => { if (!enabled) return; - // jogTo takes WORK coordinates; convert from the machine Z the track maps. - void jogTo({ z: machineZAtFrac(fracAt(e.clientX)) - wcoZ }); + const targetMachineZ = machineZAtFrac(fracAt(e.clientX)); + const targetWorkZ = targetMachineZ - wcoZ; + if (safeDescent && !isSafeDescentTarget(wz, targetWorkZ)) { + // Downward track click in safe mode: silently ignore. + return; + } + void jogTo({ z: targetWorkZ }); }; - // Z± buttons: step jog (click) or continuous (hold) — shares the active step via useJog. - const zProps = (dz: number) => + // In safe-descent mode: Z− uses slow descent feed, Z+ uses the normal jog feed. + // Neither supports hold/continuous — click-step only. + const zPropsDefault = (dz: number) => continuous ? { onPointerDown: (e: React.PointerEvent) => { @@ -143,6 +203,26 @@ export function DrillManualZBar({ } : { onClick: () => go(0, 0, dz) }; + const zPropsSafe = (dz: number) => ({ + onClick: () => { + if (!enabled || typeof step !== "number") return; + const { wpos } = useMachine.getState().status; + const targetWorkZ = wpos[2] + dz * step; + const feed = dz < 0 ? descentFeed! : undefined; + void jogTo({ z: targetWorkZ }, feed ?? undefined); + }, + }); + + const zProps = (dz: number) => (safeDescent ? zPropsSafe(dz) : zPropsDefault(dz)); + + // Labels: safe mode can override both the Z badge label and the caption. + const zLabel = safeDescent + ? (props as SafeDescentModeProps).zLabel ?? t("toolChange.zTouchLabel") + : t("toolChange.zTouchLabel"); + const caption = safeDescent + ? (props as SafeDescentModeProps).caption ?? t("toolChange.manualBarHint") + : t("toolChange.manualBarHint"); + return (
{/* Z badge + live readout */} @@ -153,7 +233,7 @@ export function DrillManualZBar({ > Z - {t("toolChange.zTouchLabel")} + {zLabel} {editing ? ( {mz.toFixed(1)} @@ -224,7 +307,7 @@ export function DrillManualZBar({ )} style={{ background: "#0c0e11" }} > - {/* Forbidden bind zones (floor / ceiling) — painted under the ticks + thumb */} + {/* Forbidden bind zones (floor / ceiling) — default mode only */} {floorZoneFrac > 0 && (
- {/* Previous manual-Z mark (vertical line + diamond cap) */} + {/* Previous manual-Z mark (vertical line + diamond cap) — default mode only */} {lastFrac != null && ( <>
- {t("toolChange.manualBarHint")} + {caption} {lastFrac != null && ( <> {" "} diff --git a/cuprum-ui/src/components/drill/FiducialPanel.tsx b/cuprum-ui/src/components/drill/FiducialPanel.tsx index 0d7df2bc..d5b734f7 100644 --- a/cuprum-ui/src/components/drill/FiducialPanel.tsx +++ b/cuprum-ui/src/components/drill/FiducialPanel.tsx @@ -39,6 +39,7 @@ import { canMove } from "@/lib/machineControls"; import { useUnitFormat } from "@/i18n/useUnitFormat"; import { Button } from "@/components/ui/Button"; import { JogStepControl } from "@/components/machine/JogStepControl"; +import { DrillManualZBar } from "@/components/drill/DrillManualZBar"; export interface FiducialPanelProps { /** Registration tooling holes from the panel manifest. */ @@ -231,21 +232,6 @@ export function FiducialPanel({ setCapturePhase("idle"); }, [stopContinuous]); - /** One step down in Z at the slow descent feed. - * Targets the current work-frame Z minus one step, clamped by the bounds. */ - const handleZDown = useCallback(async () => { - if (!enabled || !homed || typeof step !== "number") return; - const { wpos } = useMachine.getState().status; - await jogTo({ z: wpos[2] - step }, FIDUCIAL_Z_DESCENT_FEED_MM_MIN); - }, [enabled, homed, step, jogTo]); - - /** One step up in Z at the normal jog feed. */ - const handleZUp = useCallback(async () => { - if (!enabled || !homed || typeof step !== "number") return; - const { wpos } = useMachine.getState().status; - await jogTo({ z: wpos[2] + step }); - }, [enabled, homed, step, jogTo]); - /** Raise Z to the machine-frame safe retract height (rapid). */ const handleRaiseToSafeZ = useCallback(async () => { if (!enabled || !homed) return; @@ -558,39 +544,24 @@ export function FiducialPanel({
- {/* Z controls: step down / step up / raise to safe Z */} -
- - - -
+ {/* Z bar (safe-descent mode) + safe-Z retract button */} + + {/* Capture button */}