3 changed files with 619 additions and 3 deletions
-
2src/components/CustomNavBar.tsx
-
596src/components/konva/MeasurementCanvas.tsx
-
24src/pages/Measure.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<MeasurementCanvasRef, MeasurementCanvasProps>( |
||||
|
(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<number>(initialScale); |
||||
|
const [benchmarkData, setBenchmarkData] = useState<BenchmarkShape[]>(initialBenchmarkData); |
||||
|
const [analysisData, setAnalysisData] = useState<AnalysisData[]>(initialAnalysisData); |
||||
|
|
||||
|
// 定时更新测量数据(左右两侧)
|
||||
|
const leftPointsRef = useRef<Point[]>([...initialMeasurementDataLeft]); |
||||
|
const rightPointsRef = useRef<Point[]>([...initialMeasurementDataRight]); |
||||
|
const [measurementDataLeft, setMeasurementDataLeftState] = useState<Point[]>(initialMeasurementDataLeft); |
||||
|
const [measurementDataRight, setMeasurementDataRightState] = useState<Point[]>(initialMeasurementDataRight); |
||||
|
const [measurementData, setMeasurementDataState] = useState<Point[]>([]); |
||||
|
const refreshInterval = 50; |
||||
|
const refreshTimer = useRef<number | null>(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<any>(null); |
||||
|
|
||||
|
// 记录当前活跃 pointer(pointerId -> Point)
|
||||
|
const pointersRef = useRef<Map<number, Point>>(new Map()); |
||||
|
// 单指拖拽的上一次位置
|
||||
|
const lastDragPosRef = useRef<Point | null>(null); |
||||
|
// 针对鼠标,记录是否按下
|
||||
|
const isDraggingRef = useRef<boolean>(false); |
||||
|
// 记录双指缩放的初始数据(只在两指刚开始时记录一次)
|
||||
|
const pinchDataRef = useRef<PinchData | null>(null); |
||||
|
|
||||
|
// 辅助函数:从事件中获取正确的指针位置
|
||||
|
const getPointerPosition = (e: any): Point => { |
||||
|
const pos = stageRef.current.getPointerPosition(); |
||||
|
return pos ? pos : { x: e.evt.clientX, y: e.evt.clientY }; |
||||
|
}; |
||||
|
|
||||
|
const handlePointerDown = (e: any) => { |
||||
|
const point = getPointerPosition(e); |
||||
|
const pointerId = e.evt.pointerId; |
||||
|
pointersRef.current.set(pointerId, point); |
||||
|
// 判断是否为触摸事件
|
||||
|
const isTouch = e.evt.touches && e.evt.touches.length > 0; |
||||
|
if (!isTouch) { |
||||
|
isDraggingRef.current = true; |
||||
|
} |
||||
|
if (pointersRef.current.size === 1) { |
||||
|
lastDragPosRef.current = point; |
||||
|
} |
||||
|
// 当检测到两个手指时,记录初始数据——固定缩放中心
|
||||
|
if (pointersRef.current.size === 2) { |
||||
|
const pts = Array.from(pointersRef.current.values()); |
||||
|
const initialCenter = { x: (pts[0].x + pts[1].x) / 2, y: (pts[0].y + pts[1].y) / 2 }; |
||||
|
const initialDistance = Math.hypot(pts[1].x - pts[0].x, pts[1].y - pts[0].y); |
||||
|
pinchDataRef.current = { |
||||
|
initialDistance, |
||||
|
initialScale: scale, |
||||
|
initialOffset: offset, |
||||
|
initialCenter, // 固定的缩放中心
|
||||
|
}; |
||||
|
} |
||||
|
}; |
||||
|
|
||||
|
const handlePointerMove = (e: any) => { |
||||
|
const isTouch = e.evt.touches && e.evt.touches.length > 0; |
||||
|
if (!isTouch && !isDraggingRef.current) { |
||||
|
return; |
||||
|
} |
||||
|
const pointerId = e.evt.pointerId; |
||||
|
const point = getPointerPosition(e); |
||||
|
pointersRef.current.set(pointerId, point); |
||||
|
if (pointersRef.current.size === 1) { |
||||
|
// 单指拖拽
|
||||
|
if (lastDragPosRef.current) { |
||||
|
const dx = point.x - lastDragPosRef.current.x; |
||||
|
const dy = point.y - lastDragPosRef.current.y; |
||||
|
setOffset((prev) => clampOffset({ x: prev.x + dx, y: prev.y + dy }, scale)); |
||||
|
} |
||||
|
lastDragPosRef.current = point; |
||||
|
} else if (pointersRef.current.size >= 2 && pinchDataRef.current) { |
||||
|
// 双指缩放:固定缩放中心,与鼠标滚轮缩放逻辑一致
|
||||
|
const pts = Array.from(pointersRef.current.values()); |
||||
|
const currentDistance = Math.hypot(pts[1].x - pts[0].x, pts[1].y - pts[0].y); |
||||
|
const { initialDistance, initialScale, initialOffset, initialCenter } = pinchDataRef.current; |
||||
|
const scaleFactor = currentDistance / initialDistance; |
||||
|
let newScale = initialScale * scaleFactor; |
||||
|
newScale = Math.max(minZoom * pixelPerMm, Math.min(newScale, maxZoom * pixelPerMm)); |
||||
|
// 计算逻辑坐标 L:与鼠标滚轮缩放公式一致,固定 pointer 为初始记录的 initialCenter
|
||||
|
const L = { |
||||
|
x: origin.x + (initialCenter.x - (canvasCenter.x + initialOffset.x)) / initialScale, |
||||
|
y: origin.y + (initialCenter.y - (canvasCenter.y + initialOffset.y)) / initialScale, |
||||
|
}; |
||||
|
// 计算新的 offset
|
||||
|
const newOffset = { |
||||
|
x: initialCenter.x - canvasCenter.x - (L.x - origin.x) * newScale, |
||||
|
y: initialCenter.y - canvasCenter.y - (L.y - origin.y) * newScale, |
||||
|
}; |
||||
|
setScale(newScale); |
||||
|
// 此处调用 clampOffset 确保缩放过程中不会超出边界
|
||||
|
setOffset(clampOffset(newOffset, newScale)); |
||||
|
} |
||||
|
}; |
||||
|
|
||||
|
const handlePointerUp = (e: any) => { |
||||
|
const pointerId = e.evt.pointerId; |
||||
|
pointersRef.current.delete(pointerId); |
||||
|
const isTouch = e.evt.touches && e.evt.touches.length > 0; |
||||
|
if (!isTouch) { |
||||
|
isDraggingRef.current = false; |
||||
|
} |
||||
|
// 当手指数不足2时重置双指缩放初始数据
|
||||
|
if (pointersRef.current.size < 2) { |
||||
|
pinchDataRef.current = null; |
||||
|
} |
||||
|
if (pointersRef.current.size === 1) { |
||||
|
const remaining = Array.from(pointersRef.current.values())[0]; |
||||
|
lastDragPosRef.current = remaining; |
||||
|
} else if (pointersRef.current.size === 0) { |
||||
|
lastDragPosRef.current = null; |
||||
|
} |
||||
|
}; |
||||
|
|
||||
|
const handlePointerCancel = (e: any) => { |
||||
|
handlePointerUp(e); |
||||
|
}; |
||||
|
|
||||
|
// 鼠标滚轮缩放(仅适用于 PC)
|
||||
|
const handleWheel = (e: any) => { |
||||
|
e.evt.preventDefault(); |
||||
|
const oldScale = scale; |
||||
|
const pointer = stageRef.current.getPointerPosition(); |
||||
|
if (!pointer) return; |
||||
|
const L = { |
||||
|
x: origin.x + (pointer.x - (canvasCenter.x + offset.x)) / oldScale, |
||||
|
y: origin.y + (pointer.y - (canvasCenter.y + offset.y)) / oldScale, |
||||
|
}; |
||||
|
let newScale = e.evt.deltaY < 0 ? oldScale * 1.1 : oldScale / 1.1; |
||||
|
newScale = Math.max(minZoom * pixelPerMm, Math.min(newScale, maxZoom * pixelPerMm)); |
||||
|
const newOffset = { |
||||
|
x: pointer.x - canvasCenter.x - (L.x - origin.x) * newScale, |
||||
|
y: pointer.y - canvasCenter.y - (L.y - origin.y) * newScale, |
||||
|
}; |
||||
|
setScale(newScale); |
||||
|
setOffset(clampOffset(newOffset, newScale)); |
||||
|
}; |
||||
|
|
||||
|
// 辅助函数:将逻辑坐标转换为屏幕坐标
|
||||
|
const transform = (pt: Point | null): { x: number; y: number } => { |
||||
|
if (!pt) return { x: 0, y: 0 }; |
||||
|
return { |
||||
|
x: canvasCenter.x + offset.x + (pt.x - origin.x) * scale, |
||||
|
y: canvasCenter.y + offset.y + (pt.y - origin.y) * scale, |
||||
|
}; |
||||
|
}; |
||||
|
|
||||
|
const clampOffset = (newOffset: { x: number; y: number }, currentScale: number) => { |
||||
|
const left = canvasCenter.x + newOffset.x + (logicalExtent.minX - origin.x) * currentScale; |
||||
|
const right = canvasCenter.x + newOffset.x + (logicalExtent.maxX - origin.x) * currentScale; |
||||
|
const top = canvasCenter.y + newOffset.y + (logicalExtent.minY - origin.y) * currentScale; |
||||
|
const bottom = canvasCenter.y + newOffset.y + (logicalExtent.maxY - origin.y) * currentScale; |
||||
|
let clampedX = newOffset.x; |
||||
|
let clampedY = newOffset.y; |
||||
|
if (left > 0) clampedX -= left; |
||||
|
if (right < width) clampedX += width - right; |
||||
|
if (top > 0) clampedY -= top; |
||||
|
if (bottom < height) clampedY += height - bottom; |
||||
|
return { x: clampedX, y: clampedY }; |
||||
|
}; |
||||
|
|
||||
|
const renderGridAndAxes = () => { |
||||
|
const lines = []; |
||||
|
for (let x = logicalExtent.minX; x <= logicalExtent.maxX; x += gridStep) { |
||||
|
const p1 = transform({ x, y: logicalExtent.minY }); |
||||
|
const p2 = transform({ x, y: logicalExtent.maxY }); |
||||
|
lines.push( |
||||
|
<Line key={`v-${x}`} points={[p1.x, p1.y, p2.x, p2.y]} stroke="#eee" strokeWidth={1} /> |
||||
|
); |
||||
|
} |
||||
|
for (let y = logicalExtent.minY; y <= logicalExtent.maxY; y += gridStep) { |
||||
|
const p1 = transform({ x: logicalExtent.minX, y }); |
||||
|
const p2 = transform({ x: logicalExtent.maxX, y }); |
||||
|
lines.push( |
||||
|
<Line key={`h-${y}`} points={[p1.x, p1.y, p2.x, p2.y]} stroke="#eee" strokeWidth={1} /> |
||||
|
); |
||||
|
} |
||||
|
const xAxisStart = transform({ x: logicalExtent.minX, y: 0 }); |
||||
|
const xAxisEnd = transform({ x: logicalExtent.maxX, y: 0 }); |
||||
|
lines.push( |
||||
|
<Line key="x-axis" points={[xAxisStart.x, xAxisStart.y, xAxisEnd.x, xAxisEnd.y]} stroke="gray" strokeWidth={1} /> |
||||
|
); |
||||
|
const yAxisStart = transform({ x: 0, y: logicalExtent.minY }); |
||||
|
const yAxisEnd = transform({ x: 0, y: logicalExtent.maxY }); |
||||
|
lines.push( |
||||
|
<Line key="y-axis" points={[yAxisStart.x, yAxisStart.y, yAxisEnd.x, yAxisEnd.y]} stroke="gray" strokeWidth={1} /> |
||||
|
); |
||||
|
return lines; |
||||
|
}; |
||||
|
|
||||
|
const renderCoordinates = () => { |
||||
|
const texts = []; |
||||
|
const minSpacing = 30; |
||||
|
const dynamicXInterval = Math.max(coordinateInterval, Math.ceil(minSpacing / scale)); |
||||
|
const dynamicYInterval = Math.max(coordinateInterval, Math.ceil(minSpacing / scale)); |
||||
|
for (let x = logicalExtent.minX; x <= logicalExtent.maxX; x += dynamicXInterval) { |
||||
|
const pos = transform({ x, y: 0 }); |
||||
|
texts.push( |
||||
|
<Text key={`coord-x-${x}`} x={pos.x - 10} y={height - 20} text={x.toFixed(0)} fontSize={12} fill="black" /> |
||||
|
); |
||||
|
} |
||||
|
for (let y = logicalExtent.minY; y <= logicalExtent.maxY; y += dynamicYInterval) { |
||||
|
const pos = transform({ x: 0, y }); |
||||
|
texts.push( |
||||
|
<Text key={`coord-y-${y}`} x={5} y={pos.y - 6} text={y.toFixed(0)} fontSize={12} fill="black" /> |
||||
|
); |
||||
|
} |
||||
|
return texts; |
||||
|
}; |
||||
|
|
||||
|
const renderBenchmarkShapes = () => { |
||||
|
return benchmarkData.map((shape, idx) => { |
||||
|
if (shape.type === "line") { |
||||
|
const p1 = transform(shape.start); |
||||
|
const p2 = transform(shape.end); |
||||
|
return ( |
||||
|
<Line |
||||
|
key={`benchmark-line-${idx}`} |
||||
|
points={[p1.x, p1.y, p2.x, p2.y]} |
||||
|
stroke={shape.color} |
||||
|
strokeWidth={1} |
||||
|
/> |
||||
|
); |
||||
|
} else if (shape.type === "arc") { |
||||
|
const dx = shape.end.x - shape.start.x; |
||||
|
const dy = shape.end.y - shape.start.y; |
||||
|
const d = Math.hypot(dx, dy); |
||||
|
const Mx = (shape.start.x + shape.end.x) / 2; |
||||
|
const My = (shape.start.y + shape.end.y) / 2; |
||||
|
const halfChord = d / 2; |
||||
|
const h = Math.sqrt(shape.radius * shape.radius - halfChord * halfChord); |
||||
|
const candidate1 = { |
||||
|
cx: Mx - (dy / d) * h, |
||||
|
cy: My + (dx / d) * h, |
||||
|
}; |
||||
|
const candidate2 = { |
||||
|
cx: Mx + (dy / d) * h, |
||||
|
cy: My - (dx / d) * h, |
||||
|
}; |
||||
|
const chosen = shape.side === "right" ? candidate2 : candidate1; |
||||
|
const startAngle = Math.atan2(shape.start.y - chosen.cy, shape.start.x - chosen.cx); |
||||
|
const endAngle = Math.atan2(shape.end.y - chosen.cy, shape.end.x - chosen.cx); |
||||
|
const normalize = (angle: number): number => { |
||||
|
while (angle < 0) angle += 2 * Math.PI; |
||||
|
while (angle >= 2 * Math.PI) angle -= 2 * Math.PI; |
||||
|
return angle; |
||||
|
}; |
||||
|
const params = { |
||||
|
cx: chosen.cx, |
||||
|
cy: chosen.cy, |
||||
|
r: shape.radius, |
||||
|
startAngle: normalize(startAngle), |
||||
|
endAngle: normalize(endAngle), |
||||
|
anticlockwise: |
||||
|
(normalize(endAngle) - normalize(startAngle) + 2 * Math.PI) % (2 * Math.PI) > Math.PI, |
||||
|
}; |
||||
|
return ( |
||||
|
<Shape |
||||
|
key={`benchmark-arc-${idx}`} |
||||
|
sceneFunc={(ctx, shapeObj) => { |
||||
|
ctx.beginPath(); |
||||
|
ctx.arc( |
||||
|
canvasCenter.x + offset.x + (params.cx - origin.x) * scale, |
||||
|
canvasCenter.y + offset.y + (params.cy - origin.y) * scale, |
||||
|
params.r * scale, |
||||
|
params.startAngle, |
||||
|
params.endAngle, |
||||
|
params.anticlockwise |
||||
|
); |
||||
|
ctx.strokeStyle = shape.color; |
||||
|
ctx.lineWidth = 1; |
||||
|
ctx.stroke(); |
||||
|
ctx.fillStrokeShape(shapeObj); |
||||
|
}} |
||||
|
/> |
||||
|
); |
||||
|
} |
||||
|
return null; |
||||
|
}); |
||||
|
}; |
||||
|
|
||||
|
const renderMeasurementCurveLeft = () => { |
||||
|
if (measurementDataLeft.length === 0) return null; |
||||
|
const pts = measurementDataLeft |
||||
|
.map((pt) => { |
||||
|
const p = transform(pt); |
||||
|
return [p.x, p.y]; |
||||
|
}) |
||||
|
.flat(); |
||||
|
return ( |
||||
|
<Line |
||||
|
points={pts} |
||||
|
stroke="red" |
||||
|
strokeWidth={2} |
||||
|
tension={1} |
||||
|
lineCap="round" |
||||
|
lineJoin="round" |
||||
|
/> |
||||
|
); |
||||
|
}; |
||||
|
|
||||
|
const renderMeasurementCurveRight = () => { |
||||
|
if (measurementDataRight.length === 0) return null; |
||||
|
const pts = measurementDataRight |
||||
|
.map((pt) => { |
||||
|
const p = transform(pt); |
||||
|
return [p.x, p.y]; |
||||
|
}) |
||||
|
.flat(); |
||||
|
return ( |
||||
|
<Line |
||||
|
points={pts} |
||||
|
stroke="red" |
||||
|
strokeWidth={2} |
||||
|
tension={1} |
||||
|
lineCap="round" |
||||
|
lineJoin="round" |
||||
|
/> |
||||
|
); |
||||
|
}; |
||||
|
|
||||
|
const renderMeasurementCurve = () => { |
||||
|
if (measurementData.length === 0) return null; |
||||
|
const pts = measurementData |
||||
|
.map((pt) => { |
||||
|
const p = transform(pt); |
||||
|
return [p.x, p.y]; |
||||
|
}) |
||||
|
.flat(); |
||||
|
return ( |
||||
|
<Line |
||||
|
points={pts} |
||||
|
stroke="purple" |
||||
|
strokeWidth={2} |
||||
|
tension={1} |
||||
|
lineCap="round" |
||||
|
lineJoin="round" |
||||
|
/> |
||||
|
); |
||||
|
}; |
||||
|
|
||||
|
const renderAnalysis = () => { |
||||
|
return analysisData.map((item, idx) => { |
||||
|
const pA = transform(item.pointA); |
||||
|
const pB = transform(item.pointB); |
||||
|
return ( |
||||
|
<React.Fragment key={`analysis-${idx}`}> |
||||
|
<Line points={[pA.x, pA.y, pB.x, pB.y]} stroke="red" strokeWidth={1} /> |
||||
|
<Text x={pA.x - 15} y={pA.y - 15} text={item.describe} fontSize={14} fill="black" /> |
||||
|
</React.Fragment> |
||||
|
); |
||||
|
}); |
||||
|
}; |
||||
|
|
||||
|
return ( |
||||
|
<div |
||||
|
style={{ |
||||
|
width, |
||||
|
height, |
||||
|
border: "1px solid #ccc", |
||||
|
position: "relative", |
||||
|
touchAction: "none", |
||||
|
}} |
||||
|
> |
||||
|
<Stage |
||||
|
ref={stageRef} |
||||
|
width={width} |
||||
|
height={height} |
||||
|
onPointerDown={handlePointerDown} |
||||
|
onPointerMove={handlePointerMove} |
||||
|
onPointerUp={handlePointerUp} |
||||
|
onPointerCancel={handlePointerCancel} |
||||
|
onWheel={handleWheel} |
||||
|
style={{ background: "#fff" }} |
||||
|
> |
||||
|
<Layer> |
||||
|
{showGrid && renderGridAndAxes()} |
||||
|
{showBenchmark && renderBenchmarkShapes()} |
||||
|
{renderMeasurementCurveLeft()} |
||||
|
{renderMeasurementCurveRight()} |
||||
|
{renderMeasurementCurve()} |
||||
|
{showAnalysis && renderAnalysis()} |
||||
|
</Layer> |
||||
|
{showCoordinates && <Layer>{renderCoordinates()}</Layer>} |
||||
|
</Stage> |
||||
|
{showScale && ( |
||||
|
<div |
||||
|
style={{ |
||||
|
position: "absolute", |
||||
|
bottom: 10, |
||||
|
left: 10, |
||||
|
background: "rgba(255,255,255,0.8)", |
||||
|
padding: "2px 4px", |
||||
|
border: "1px solid #ccc", |
||||
|
fontSize: 12, |
||||
|
pointerEvents: "none", |
||||
|
}} |
||||
|
> |
||||
|
<div |
||||
|
style={{ |
||||
|
width: gridStep * scaleInterval * scale, |
||||
|
borderBottom: "2px solid black", |
||||
|
marginBottom: 2, |
||||
|
}} |
||||
|
></div> |
||||
|
<div>{`${gridStep * scaleInterval} mm`}</div> |
||||
|
</div> |
||||
|
)} |
||||
|
</div> |
||||
|
); |
||||
|
} |
||||
|
); |
||||
|
|
||||
|
export default MeasurementCanvas; |
Write
Preview
Loading…
Cancel
Save
Reference in new issue