9 changed files with 1366 additions and 416 deletions
-
90package-lock.json
-
2package.json
-
16src/components/SideMenu.tsx
-
2src/index.tsx
-
561src/pages/measure/components/MeasureAction.tsx
-
18src/pages/measure/components/MeasureConfig.tsx
-
651src/pages/measure/components/konva/MeasurementCanvas.tsx
-
50src/services/measure/analysis.ts
-
40src/services/measure/type.ts
@ -0,0 +1,651 @@ |
|||
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; |
|||
} |
|||
|
|||
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 }; |
|||
|
|||
// 当 logicalExtent 范围较小时,自动计算一个 scale 使其铺满整个 Stage
|
|||
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); |
|||
|
|||
const transform = (pt: Point) => ({ |
|||
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 isDragging = useRef(false); |
|||
const lastPos = useRef<{ x: number; y: number } | null>(null); |
|||
const dragFrame = useRef<number | null>(null); |
|||
|
|||
const handleMouseDown = () => { |
|||
isDragging.current = true; |
|||
lastPos.current = stageRef.current.getPointerPosition(); |
|||
}; |
|||
|
|||
const handleMouseMove = () => { |
|||
if (!isDragging.current) return; |
|||
const currentPos = stageRef.current.getPointerPosition(); |
|||
if (lastPos.current && currentPos) { |
|||
if (!dragFrame.current) { |
|||
dragFrame.current = requestAnimationFrame(() => { |
|||
const last = lastPos.current!; |
|||
const newOff = { |
|||
x: offset.x + currentPos.x - last.x, |
|||
y: offset.y + currentPos.y - last.y, |
|||
}; |
|||
setOffset(clampOffset(newOff, scale)); |
|||
lastPos.current = currentPos; |
|||
dragFrame.current = null; |
|||
}); |
|||
} |
|||
} |
|||
}; |
|||
|
|||
const handleMouseUp = () => { |
|||
isDragging.current = false; |
|||
if (dragFrame.current) { |
|||
cancelAnimationFrame(dragFrame.current); |
|||
dragFrame.current = null; |
|||
} |
|||
}; |
|||
|
|||
const handleMouseLeave = () => { |
|||
isDragging.current = false; |
|||
if (dragFrame.current) { |
|||
cancelAnimationFrame(dragFrame.current); |
|||
dragFrame.current = null; |
|||
} |
|||
}; |
|||
|
|||
const handleWheel = (e: any) => { |
|||
e.evt.preventDefault(); |
|||
const oldScale = scale; |
|||
const pointer = stageRef.current.getPointerPosition(); |
|||
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; |
|||
if (newScale < minZoom * pixelPerMm) newScale = minZoom * pixelPerMm; |
|||
if (newScale > maxZoom * pixelPerMm) 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 lastTouchDistance = useRef<number | null>(null); |
|||
const handleTouchStart = (e: any) => { |
|||
const touches = e.evt.touches; |
|||
if (touches && touches.length === 2) { |
|||
e.evt.preventDefault(); |
|||
const [t1, t2] = touches; |
|||
const dist = Math.hypot(t2.clientX - t1.clientX, t2.clientY - t1.clientY); |
|||
lastTouchDistance.current = dist; |
|||
} else if (touches && touches.length === 1) { |
|||
isDragging.current = true; |
|||
lastPos.current = { x: touches[0].clientX, y: touches[0].clientY }; |
|||
} |
|||
}; |
|||
|
|||
const handleTouchMove = (e: any) => { |
|||
const touches = e.evt.touches; |
|||
if (touches && touches.length === 2) { |
|||
e.evt.preventDefault(); |
|||
const [t1, t2] = touches; |
|||
const dist = Math.hypot(t2.clientX - t1.clientX, t2.clientY - t1.clientY); |
|||
if (lastTouchDistance.current) { |
|||
const delta = dist / lastTouchDistance.current; |
|||
let newScale = scale * delta; |
|||
if (newScale < minZoom * pixelPerMm) newScale = minZoom * pixelPerMm; |
|||
if (newScale > maxZoom * pixelPerMm) newScale = maxZoom * pixelPerMm; |
|||
const center = { |
|||
x: (t1.clientX + t2.clientX) / 2, |
|||
y: (t1.clientY + t2.clientY) / 2, |
|||
}; |
|||
const L = { |
|||
x: origin.x + (center.x - (canvasCenter.x + offset.x)) / scale, |
|||
y: origin.y + (center.y - (canvasCenter.y + offset.y)) / scale, |
|||
}; |
|||
const newOffset = { |
|||
x: center.x - canvasCenter.x - (L.x - origin.x) * newScale, |
|||
y: center.y - canvasCenter.y - (L.y - origin.y) * newScale, |
|||
}; |
|||
setScale(newScale); |
|||
setOffset(clampOffset(newOffset, newScale)); |
|||
lastTouchDistance.current = dist; |
|||
} |
|||
} else if (touches && touches.length === 1 && isDragging.current) { |
|||
const t = touches[0]; |
|||
const pos = { x: t.clientX, y: t.clientY }; |
|||
if (lastPos.current) { |
|||
setOffset((prev) => |
|||
clampOffset( |
|||
{ |
|||
x: prev.x + pos.x - lastPos.current!.x, |
|||
y: prev.y + pos.y - lastPos.current!.y, |
|||
}, |
|||
scale |
|||
) |
|||
); |
|||
} |
|||
lastPos.current = pos; |
|||
} |
|||
}; |
|||
|
|||
const handleTouchEnd = () => { |
|||
isDragging.current = false; |
|||
lastTouchDistance.current = null; |
|||
lastPos.current = null; |
|||
}; |
|||
|
|||
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={2} |
|||
/> |
|||
); |
|||
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={2} |
|||
/> |
|||
); |
|||
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="green" |
|||
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="blue" |
|||
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} |
|||
onMouseDown={handleMouseDown} |
|||
onMouseMove={handleMouseMove} |
|||
onMouseUp={handleMouseUp} |
|||
onMouseLeave={handleMouseLeave} |
|||
onWheel={handleWheel} |
|||
onTouchStart={handleTouchStart} |
|||
onTouchMove={handleTouchMove} |
|||
onTouchEnd={handleTouchEnd} |
|||
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