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