温度: 31.2°C
X轴倾斜: -0.496
diff --git a/src/components/konva/MeasurementCanvas.tsx b/src/components/konva/MeasurementCanvas.tsx
new file mode 100644
index 0000000..0af7451
--- /dev/null
+++ b/src/components/konva/MeasurementCanvas.tsx
@@ -0,0 +1,596 @@
+import React, {
+ useState,
+ useRef,
+ useImperativeHandle,
+ forwardRef,
+ useEffect,
+} from "react";
+import { Stage, Layer, Line, Shape, Text } from "react-konva";
+
+// 数据类型定义
+export interface Point {
+ x: number;
+ y: number;
+}
+
+export interface BenchmarkArc {
+ type: "arc";
+ start: Point;
+ end: Point;
+ radius: number;
+ color: string;
+ side: "right" | "left" | "up" | "down";
+}
+
+export interface BenchmarkLine {
+ type: "line";
+ start: Point;
+ end: Point;
+ color: string;
+}
+
+export type BenchmarkShape = BenchmarkArc | BenchmarkLine;
+
+export interface AnalysisData {
+ pointA: Point;
+ pointB: Point;
+ describe: string;
+}
+
+// 逻辑坐标范围(单位:毫米)
+export interface LogicalExtent {
+ minX: number;
+ maxX: number;
+ minY: number;
+ maxY: number;
+}
+
+export interface MeasurementCanvasProps {
+ width: number;
+ height: number;
+ logicalExtent?: LogicalExtent;
+ gridStep?: number;
+ showGrid?: boolean;
+ showScale?: boolean;
+ scaleInterval?: number;
+ showCoordinates?: boolean;
+ coordinateInterval?: number;
+ pixelPerMm?: number;
+ origin?: Point;
+ minZoom?: number;
+ maxZoom?: number;
+ initialBenchmarkData?: BenchmarkShape[];
+ initialMeasurementDataLeft?: Point[];
+ initialMeasurementDataRight?: Point[];
+ initialAnalysisData?: AnalysisData[];
+ // 控制是否显示标准线(benchmark shapes)
+ showBenchmark?: boolean;
+ // 控制是否显示分析线
+ showAnalysis?: boolean;
+}
+
+export interface MeasurementCanvasRef {
+ resetCanvas: () => void;
+ clearShapes: () => void;
+ setBenchmarkData: (data: BenchmarkShape[]) => void;
+ setMeasurementDataLeft: (data: Point[]) => void;
+ setMeasurementDataRight: (data: Point[]) => void;
+ setMeasurementData: (data: Point[]) => void;
+ setAnalysisData: (data: AnalysisData[]) => void;
+ redraw: () => void;
+}
+
+interface PinchData {
+ initialDistance: number;
+ initialScale: number;
+ initialOffset: { x: number; y: number };
+ initialCenter: Point; // 固定的缩放中心
+}
+
+const MeasurementCanvas = forwardRef
(
+ (props, ref) => {
+ const {
+ width,
+ height,
+ logicalExtent = { minX: -100, maxX: 100, minY: -100, maxY: 100 },
+ gridStep = 1,
+ showGrid = true,
+ showScale = false,
+ scaleInterval = 10,
+ showCoordinates = false,
+ coordinateInterval = 1,
+ pixelPerMm = 10,
+ origin = { x: 0, y: 0 },
+ minZoom = 1,
+ maxZoom = 10,
+ initialBenchmarkData = [],
+ initialMeasurementDataLeft = [],
+ initialMeasurementDataRight = [],
+ initialAnalysisData = [],
+ showBenchmark = true,
+ showAnalysis = true,
+ } = props;
+
+ // Stage 物理中心(像素)
+ const canvasCenter = { x: width / 2, y: height / 2 };
+
+ // 计算初始 scale 与 offset
+ const logicalWidth = logicalExtent.maxX - logicalExtent.minX;
+ const logicalHeight = logicalExtent.maxY - logicalExtent.minY;
+ const computedScale = Math.min(width / logicalWidth, height / logicalHeight);
+ const initialScale = computedScale > pixelPerMm ? computedScale : pixelPerMm;
+ const logicalCenter = {
+ x: (logicalExtent.minX + logicalExtent.maxX) / 2,
+ y: (logicalExtent.minY + logicalExtent.maxY) / 2,
+ };
+ const initialOffset =
+ computedScale > pixelPerMm
+ ? {
+ x: -(logicalCenter.x - origin.x) * initialScale,
+ y: -(logicalCenter.y - origin.y) * initialScale,
+ }
+ : { x: 0, y: 0 };
+
+ const [offset, setOffset] = useState<{ x: number; y: number }>(initialOffset);
+ const [scale, setScale] = useState(initialScale);
+ const [benchmarkData, setBenchmarkData] = useState(initialBenchmarkData);
+ const [analysisData, setAnalysisData] = useState(initialAnalysisData);
+
+ // 定时更新测量数据(左右两侧)
+ const leftPointsRef = useRef([...initialMeasurementDataLeft]);
+ const rightPointsRef = useRef([...initialMeasurementDataRight]);
+ const [measurementDataLeft, setMeasurementDataLeftState] = useState(initialMeasurementDataLeft);
+ const [measurementDataRight, setMeasurementDataRightState] = useState(initialMeasurementDataRight);
+ const [measurementData, setMeasurementDataState] = useState([]);
+ const refreshInterval = 50;
+ const refreshTimer = useRef(null);
+ useEffect(() => {
+ if (!refreshTimer.current) {
+ refreshTimer.current = window.setInterval(() => {
+ setMeasurementDataLeftState([...leftPointsRef.current]);
+ setMeasurementDataRightState([...rightPointsRef.current]);
+ }, refreshInterval);
+ }
+ return () => {
+ if (refreshTimer.current) {
+ clearInterval(refreshTimer.current);
+ refreshTimer.current = null;
+ }
+ };
+ }, []);
+
+ useImperativeHandle(ref, () => ({
+ resetCanvas: () => {
+ setScale(pixelPerMm);
+ setOffset({ x: 0, y: 0 });
+ },
+ clearShapes: () => {
+ leftPointsRef.current = [];
+ rightPointsRef.current = [];
+ setMeasurementDataLeftState([]);
+ setMeasurementDataRightState([]);
+ setAnalysisData([]);
+ setMeasurementDataState([]);
+ },
+ setBenchmarkData: (data: BenchmarkShape[]) => {
+ setBenchmarkData(data);
+ },
+ setMeasurementDataLeft: (data: Point[]) => {
+ leftPointsRef.current = data;
+ },
+ setMeasurementDataRight: (data: Point[]) => {
+ rightPointsRef.current = data;
+ },
+ setMeasurementData: (data: Point[]) => {
+ setMeasurementDataState(data);
+ },
+ setAnalysisData: (data: AnalysisData[]) => {
+ setAnalysisData(data);
+ },
+ redraw: () => {
+ setScale((prev) => prev);
+ },
+ }));
+
+ const stageRef = useRef(null);
+
+ // 记录当前活跃 pointer(pointerId -> Point)
+ const pointersRef = useRef