import 'dart:math'; import 'dart:ui'; import 'package:fis_measure/interfaces/date_types/point.dart'; import 'package:fis_measure/interfaces/enums/items.dart'; import 'package:fis_measure/interfaces/process/items/item.dart'; import 'package:fis_measure/interfaces/process/items/item_metas.dart'; import 'package:fis_measure/interfaces/process/items/terms.dart'; import 'package:fis_measure/interfaces/process/workspace/point_info.dart'; import 'package:fis_measure/process/calcuators/curve.dart'; import 'package:fis_measure/process/calcuators/calculator.dart'; import 'package:fis_measure/process/primitives/utils/auto_snap.dart'; import 'package:fis_measure/utils/canvas.dart'; import 'area_abstract.dart'; /// 曲线连线 class Spline extends AreaItemAbstract with AutoSnapMixin { Spline(ItemMeta meta, [IMeasureItem? parent]) : super(meta, parent); static Spline createAreaPerimeter(ItemMeta meta, [IMeasureItem? parent]) { Spline spline = Spline(meta, parent); spline.calculator = _AreaPerimeterCalc(spline); spline.isClosed = false; spline.splineTension = 0.5; return spline; } static Spline createCurveLength( ItemMeta meta, [ IMeasureItem? parent, ]) { Spline spline = Spline(meta, parent); spline.calculator = CurveLengthCal(spline); spline.isClosed = false; spline.splineTension = 0.5; return spline; } @override bool onExecuteMouse(PointInfo args) { if (state == ItemStates.finished) { if (args.pointType == PointInfoType.mouseDown) { state = ItemStates.waiting; } } if (state == ItemStates.waiting) { if (args.pointType == PointInfoType.mouseDown) { handleMouseDownWhileWaiting(args); } } else if (state == ItemStates.running) { if (feature == null) return false; if (args.pointType == PointInfoType.mouseUp) return false; final f = feature!; if (args.pointType == PointInfoType.mouseDown) { f.innerPoints.add(args); } else { f.innerPoints.last = args; } doCalculate(); if (checkAutoSnap(args)) { f.innerPoints.removeLast(); } } return true; } DPoint lastStartPoint = DPoint(0, 0); // 上一个起点 bool isFirstPointNeedOffset = false; // 第一个点是否需要施加偏移 DPoint splineTouchStartPoint = DPoint(0, 0); // 轨迹触摸起始点 bool isOtherPointNeedOffset = false; // 其他点是否需要施加偏移 @override bool onExecuteTouch(PointInfo args) { if (state == ItemStates.finished) { if (args.pointType == PointInfoType.touchDown) { state = ItemStates.waiting; } } if (state == ItemStates.waiting) { if (isFirstPointNeedOffset) args.addOffset(0, -0.2); switch (args.pointType) { case PointInfoType.touchDown: handleTouchDownWhileWaiting(args); isFirstPointNeedOffset = false; break; case PointInfoType.touchUp: lastStartPoint = args; // 设置线段起点 state = ItemStates.running; feature?.innerPoints.first = args; break; // 按下立即抬起无事发生 case PointInfoType.touchMove: if (isMoveTargetOutOfRange(args)) return true; isFirstPointNeedOffset = true; feature?.innerPoints.first = args; break; default: break; } } else if (state == ItemStates.running) { if (feature == null) return false; PointInfo newPoint = PointInfo.fromOffset( lastStartPoint .clone() .addVector(args - splineTouchStartPoint) .toOffset(), args.pointType); newPoint.hostVisualArea = args.hostVisualArea; final f = feature!; if (args.pointType == PointInfoType.touchUp) { if (!isOtherPointNeedOffset) { f.innerPoints.last = args; lastStartPoint = args; doCalculate(); } else { lastStartPoint = newPoint; } } if (args.pointType == PointInfoType.touchDown) { isOtherPointNeedOffset = false; splineTouchStartPoint = args; f.innerPoints.add(lastStartPoint); } if (args.pointType == PointInfoType.touchMove) { if (isMoveTargetOutOfRange(newPoint)) return true; isOtherPointNeedOffset = true; f.innerPoints.last = newPoint; } doCalculate(); PointInfo newPointInfo = isOtherPointNeedOffset ? PointInfo.fromOffset(newPoint.toOffset(), args.pointType) : args; if (checkAutoSnap(newPointInfo)) { f.innerPoints.removeLast(); } } return true; } void handleMouseDownWhileWaiting(PointInfo args) { // TODO: 判断是否当前area final point = args.toAreaLogicPoint(); feature = SplineFeature(this, point); if (args.hostVisualArea != null) { feature!.hostVisualArea = args.hostVisualArea; } state = ItemStates.running; } void handleTouchDownWhileWaiting(PointInfo args) { // TODO: 判断是否当前area final point = args.toAreaLogicPoint(); feature = SplineFeature.withOneStartPoint(this, point); if (args.hostVisualArea != null) { feature!.hostVisualArea = args.hostVisualArea; } } } class SplineFeature extends AreaItemFeatureAbstract { static const double _splineTolerance = 0.05; SplineFeature(Spline refItem, DPoint point) : super(refItem) { innerPoints.add(point.clone()); innerPoints.add(point.clone()); } SplineFeature.withOneStartPoint(Spline refItem, DPoint point) : super(refItem) { innerPoints.add(point.clone()); } @override void paint(Canvas canvas, Size size) { if (innerPoints.isEmpty) return; final paintPoints = innerPoints; drawId(canvas, size); final startOffset = convert2ViewPoint(size, startPoint).toOffset(); if (paintPoints.length == 1) { drawVertex(canvas, startOffset, true); return; } else if (paintPoints.length == 2) { final endOffset = convert2ViewPoint(size, endPoint).toOffset(); drawVertex(canvas, startOffset, true); drawVertex(canvas, endOffset, true); canvas.drawDashLine(startOffset, endOffset, 1, 10, paintLinePan); return; } /// 全部innerPoints点集转为Offset集绘制 for (var e in innerPoints) { drawVertex(canvas, convert2ViewPoint(size, e).toOffset()); } /// 获取拟合点集 final fittedPoints = getFitPoints( innerPoints, isClosed, splineTension, _splineTolerance / 2); /// 全部拟合点集转为Offset集 final fittedOffsets = fittedPoints.map((e) => convert2ViewPoint(size, e).toOffset()).toList(); canvas.drawDashPointsLine(fittedOffsets, 1, 10, paintLinePan, close: true); } List getFitPoints( List points, bool isClosed, double tension, double tolerance, ) { final List polyLineSegment = []; final len = points.length; for (var i = 0; i < points.length; i++) { if (i == 0) { addSegment( polyLineSegment, points, [len - 1, 0, 1, 2], tension, tolerance); } else if (i == points.length - 2) { addSegment( polyLineSegment, points, [i - 1, i, i + 1, 0], tension, tolerance); } else if (i == points.length - 1) { addSegment( polyLineSegment, points, [i - 1, i, 0, 1], tension, tolerance); } else { addSegment(polyLineSegment, points, [i - 1, i, i + 1, i + 2], tension, tolerance); } } return polyLineSegment; } static void addSegment(List polyLineSegment, List points, List pointIndex, double tension, double tolerance) { if (points.length < 2) return; segment( polyLineSegment, points[pointIndex[0]], points[pointIndex[1]], points[pointIndex[2]], points[pointIndex[3]], tension, tension, tolerance, ); } static double segment(List points, DPoint pt0, DPoint pt1, DPoint pt2, DPoint pt3, double t1, double t2, double tolerance) { double length = 0; final sx1 = t1 * (pt2.x - pt0.x); final sy1 = t1 * (pt2.y - pt0.y); final sx2 = t2 * (pt3.x - pt1.x); final sy2 = t2 * (pt3.y - pt1.y); final ax = sx1 + sx2 + 2 * pt1.x - 2 * pt2.x; final ay = sy1 + sy2 + 2 * pt1.y - 2 * pt2.y; final bx = -2 * sx1 - sx2 - 3 * pt1.x + 3 * pt2.x; final by = -2 * sy1 - sy2 - 3 * pt1.y + 3 * pt2.y; final cx = sx1; final cy = sy1; final dx = pt1.x; final dy = pt1.y; var num = (((pt1.x - pt2.x).abs() + (pt1.y - pt2.y).abs()) ~/ tolerance); // Set num = 2 to calculate the length. when the distance of pt1,pt2 is very tiny, the num will be 0, the length will be 0 if (num < 2) num = 2; // Notice begins at 1 so excludes the first point (which is just pt1) for (var i = 1; i < num; i++) { var t = i / (num - 1); var pt = DPoint(ax * t * t * t + bx * t * t + cx * t + dx, ay * t * t * t + by * t * t + cy * t + dy); if (i == 1) { length += (pt - pt1).length; } else { length += (pt - points[points.length - 1]).length; } points.add(pt); } return length; } } class _AreaPerimeterCalc extends Calculator { _AreaPerimeterCalc(Spline ref) : super(ref); @override void calculate() { if (ref.feature == null) return; final feature = ref.feature!; final viewport = feature.hostVisualArea!.viewport!; final points = feature.innerPoints.map((e) => viewport.convert(e)).toList(); final tension = feature.splineTension; feature.values.clear(); double area; double perimeter; //计算周长 final res = calcPerimeterAndArea(points, feature.isClosed, tension, 0.25); perimeter = res[0]; area = res[1]; for (var output in ref.meta.outputs) { if (output.name == MeasureTerms.Perimeter) { var value = roundDouble(perimeter, output.fractionalDigits); feature.updateFloatValue(output, value, output.unit); } else if (output.name == MeasureTerms.Area) { var value = roundDouble(area, output.fractionalDigits); feature.updateFloatValue(output, value, output.unit); } } } static double calcArea(List points) { if (points.isEmpty) { return 0; } double sum = 0; var ax = points[0].x; var ay = points[0].y; for (var i = 1; i < points.length - 1; i++) { var bx = points[i].x; var by = points[i].y; var cx = points[i + 1].x; var cy = points[i + 1].y; sum += ax * by - ay * bx + ay * cx - ax * cy + bx * cy - cx * by; } return (-sum / 2).abs(); } static List calcPerimeterAndArea( List points, bool isClosed, double tension, double tolerance, ) { final List polyLineSegment = []; final len = points.length; double perimeter = 0; if (len < 2) { return [0, 0]; } if (len == 2) { final p1 = points[0]; final p2 = points[1]; final dx = p1.x - p2.x; final dy = p1.y - p2.y; perimeter = sqrt(dx * dx + dy * dy); return [perimeter, 0]; } for (var i = 0; i < points.length; i++) { if (i == 0) { perimeter += segment( polyLineSegment, isClosed ? points[points.length - 1] : points[0], points[0], points[1], points[2], tension, tension, tolerance); } else if (i == points.length - 2) { perimeter += segment( polyLineSegment, points[i - 1], points[i], points[i + 1], isClosed ? points[0] : points[i + 1], tension, tension, tolerance); } else if (i == points.length - 1) { if (isClosed) { perimeter += segment(polyLineSegment, points[i - 1], points[i], points[0], points[1], tension, tension, tolerance); } } else { perimeter += segment(polyLineSegment, points[i - 1], points[i], points[i + 1], points[i + 2], tension, tension, tolerance); } } final area = calcArea(polyLineSegment); return [perimeter, area]; } static double segment(List points, DPoint pt0, DPoint pt1, DPoint pt2, DPoint pt3, double t1, double t2, double tolerance) { double length = 0; final sx1 = t1 * (pt2.x - pt0.x); final sy1 = t1 * (pt2.y - pt0.y); final sx2 = t2 * (pt3.x - pt1.x); final sy2 = t2 * (pt3.y - pt1.y); final ax = sx1 + sx2 + 2 * pt1.x - 2 * pt2.x; final ay = sy1 + sy2 + 2 * pt1.y - 2 * pt2.y; final bx = -2 * sx1 - sx2 - 3 * pt1.x + 3 * pt2.x; final by = -2 * sy1 - sy2 - 3 * pt1.y + 3 * pt2.y; final cx = sx1; final cy = sy1; final dx = pt1.x; final dy = pt1.y; var num = (((pt1.x - pt2.x).abs() + (pt1.y - pt2.y).abs()) ~/ tolerance); // Set num = 2 to calculate the length. when the distance of pt1,pt2 is very tiny, the num will be 0, the length will be 0 if (num < 2) num = 2; // Notice begins at 1 so excludes the first point (which is just pt1) for (var i = 1; i < num; i++) { var t = i / (num - 1); var pt = DPoint(ax * t * t * t + bx * t * t + cx * t + dx, ay * t * t * t + by * t * t + cy * t + dy); if (i == 1) { length += (pt - pt1).length; } else { length += (pt - points[points.length - 1]).length; } points.add(pt); } return length; } }