import React, { useRef, useState, useCallback } from 'react'; export const CAPTURE_SELECTOR = '.angle-overlay'; import { getAngleLabelBasePosition, getAngleLabelPosition, measureAngleDegrees, moveAngleWidget, round3, } from './angleMeasureGeometry.js'; function clamp01(value) { return Math.max(0, Math.min(1, Number(value) || 0)); } function sanitizeHexColor(value, fallback = '#ff9800') { if (typeof value !== 'string') return fallback; const text = value.trim(); return /^#[0-9a-fA-F]{6}$/.test(text) ? text.toLowerCase() : fallback; } function hexToRgb(value) { const color = sanitizeHexColor(value); return { r: parseInt(color.slice(1, 3), 16), g: parseInt(color.slice(3, 5), 16), b: parseInt(color.slice(5, 7), 16), }; } function mixColor(baseColor, mixWith, weight) { const alpha = Math.max(0, Math.min(1, Number(weight) || 0)); const base = hexToRgb(baseColor); const target = hexToRgb(mixWith); const r = Math.round((base.r * (1 - alpha)) + (target.r * alpha)); const g = Math.round((base.g * (1 - alpha)) + (target.g * alpha)); const b = Math.round((base.b * (1 - alpha)) + (target.b * alpha)); return `rgb(${r}, ${g}, ${b})`; } function formatAngle(value) { const numeric = Number(value); if (!Number.isFinite(numeric)) return '0.0 deg'; return `${numeric.toFixed(1)} deg`; } function buildAngleArcPath(x1, y1, xm, ym, x2, y2) { const va = { x: x1 - xm, y: y1 - ym }; const vb = { x: x2 - xm, y: y2 - ym }; const lenA = Math.hypot(va.x, va.y); const lenB = Math.hypot(vb.x, vb.y); if (lenA <= 1e-6 || lenB <= 1e-6) return ''; const radius = Math.min(0.12, 0.38 * Math.min(lenA, lenB)); const start = { x: xm + (va.x / lenA) * radius, y: ym + (va.y / lenA) * radius }; const end = { x: xm + (vb.x / lenB) * radius, y: ym + (vb.y / lenB) * radius }; const cross = (va.x * vb.y) - (va.y * vb.x); return [ `M ${start.x * 100} ${start.y * 100}`, `A ${radius * 100} ${radius * 100} 0 0 ${cross >= 0 ? 1 : 0} ${end.x * 100} ${end.y * 100}`, ].join(' '); } export default function AngleMeasureOverlay({ image, x1, y1, xm, ym, x2, y2, labelDx, labelDy, angleDeg, color, strokeWidth, nodeId, onWidgetChange, }) { const containerRef = useRef(null); const [dragging, setDragging] = useState(null); const resolvedColor = sanitizeHexColor(color, '#ff9800'); const resolvedStrokeWidth = Math.max(0.35, Math.min(6, Number(strokeWidth) || 1.35)); const resolvedArcColor = mixColor(resolvedColor, '#ffffff', 0.42); const resolvedMidColor = mixColor(resolvedColor, '#ffffff', 0.72); const resolvedBadgeTextColor = mixColor(resolvedColor, '#ffffff', 0.72); const resolvedBadgeBorderColor = mixColor(resolvedColor, '#ffffff', 0.32); const getCoords = useCallback((event) => { const rect = containerRef.current.getBoundingClientRect(); return { fx: clamp01((event.clientX - rect.left) / rect.width), fy: clamp01((event.clientY - rect.top) / rect.height), }; }, []); const updateWidgets = useCallback((updates) => { Object.entries(updates).forEach(([name, value]) => { onWidgetChange(nodeId, name, value); }); }, [nodeId, onWidgetChange]); const onPointerDown = useCallback((handle) => (event) => { event.stopPropagation(); event.preventDefault(); event.currentTarget.setPointerCapture(event.pointerId); if (handle === 'mid') { const start = getCoords(event); setDragging({ handle, start, points: { x1, y1, xm, ym, x2, y2 }, }); return; } setDragging({ handle }); }, [getCoords, x1, y1, xm, ym, x2, y2]); const onPointerMove = useCallback((event) => { if (!dragging || !containerRef.current) return; const { fx, fy } = getCoords(event); if (dragging.handle === 'mid') { updateWidgets(moveAngleWidget( dragging.points, fx - dragging.start.fx, fy - dragging.start.fy, )); return; } if (dragging.handle === 'p1') { updateWidgets({ x1: round3(fx), y1: round3(fy) }); } else if (dragging.handle === 'p2') { updateWidgets({ x2: round3(fx), y2: round3(fy) }); } else if (dragging.handle === 'label') { const base = getAngleLabelBasePosition(x1, y1, xm, ym, x2, y2); updateWidgets({ label_dx: round3(fx - base.x), label_dy: round3(fy - base.y), }); } }, [dragging, getCoords, updateWidgets, x1, y1, xm, ym, x2, y2]); const onPointerUp = useCallback(() => { setDragging(null); }, []); const displayedAngle = Number.isFinite(Number(angleDeg)) ? Number(angleDeg) : measureAngleDegrees(x1, y1, xm, ym, x2, y2); const arcPath = buildAngleArcPath(x1, y1, xm, ym, x2, y2); const labelPosition = getAngleLabelPosition({ x1, y1, xm, ym, x2, y2 }, labelDx, labelDy); return (
angle source {arcPath && }
{formatAngle(displayedAngle)}
A
V
B
); }