ellipse.dart 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518
  1. import 'dart:ui';
  2. import 'dart:math' as math;
  3. import 'package:fis_measure/interfaces/date_types/point.dart';
  4. import 'package:fis_measure/interfaces/enums/items.dart';
  5. import 'package:fis_measure/interfaces/process/items/item.dart';
  6. import 'package:fis_measure/interfaces/process/items/item_metas.dart';
  7. import 'package:fis_measure/interfaces/process/workspace/point_info.dart';
  8. import 'package:fis_measure/process/calcuators/calculator.dart';
  9. import 'package:fis_measure/process/calcuators/curve.dart';
  10. import 'package:fis_measure/process/calcuators/formulas/general.dart';
  11. import 'package:fis_measure/process/workspace/urm/application.dart';
  12. import 'package:fis_measure/utils/canvas.dart';
  13. import 'package:flutter/rendering.dart';
  14. import 'package:path_drawing/path_drawing.dart';
  15. import 'package:vid/us/vid_us_unit.dart';
  16. import 'area_abstract.dart';
  17. class Ellipse extends AreaItemAbstract {
  18. Ellipse(ItemMeta meta, IMeasureItem? parent) : super(meta, parent);
  19. @override
  20. bool onExecuteMouse(PointInfo args) {
  21. if (state == ItemStates.finished) {
  22. if (args.pointType == PointInfoType.mouseDown) {
  23. state = ItemStates.waiting;
  24. }
  25. }
  26. if (state == ItemStates.waiting) {
  27. if (args.pointType == PointInfoType.mouseDown) {
  28. handleMouseDownWhileWaiting(args);
  29. }
  30. } else if (state == ItemStates.running) {
  31. if (feature == null) return false;
  32. final f = feature! as EllipseFeature;
  33. final activeIndex = f.activeIndex;
  34. switch (args.pointType) {
  35. case PointInfoType.mouseMove:
  36. f.innerPoints[activeIndex] = args;
  37. f.adjustPoints(args);
  38. break;
  39. case PointInfoType.mouseDown:
  40. if (activeIndex == 1) {
  41. if (f.xAxisEnd == f.xAxisStart) {
  42. break;
  43. }
  44. f.adjustPoints(args);
  45. f.activeIndex = 2;
  46. } else if (activeIndex == 2) {
  47. doFeatureFinish();
  48. }
  49. break;
  50. default:
  51. return false;
  52. }
  53. doCalculate();
  54. return true;
  55. }
  56. return false;
  57. }
  58. bool isFirstPointMove = true;
  59. DPoint touchStartPosition = DPoint(0, 0); // 相对位移起始触摸点
  60. DPoint currPointLastPosition = DPoint(0, 0); // 当前操作的点之前所在位置
  61. @override
  62. bool onExecuteTouch(PointInfo args) {
  63. if (state == ItemStates.finished) {
  64. if (args.pointType == PointInfoType.touchDown) {
  65. state = ItemStates.waiting;
  66. }
  67. }
  68. if (state == ItemStates.waiting) {
  69. if (args.pointType == PointInfoType.touchDown) {
  70. isFirstPointMove = true;
  71. handleTouchDownWhileWaiting(args);
  72. }
  73. } else if (state == ItemStates.running) {
  74. if (feature == null) return false;
  75. final f = feature! as EllipseFeature;
  76. if (args.pointType == PointInfoType.touchDown) {
  77. touchStartPosition = args;
  78. currPointLastPosition = f.innerPoints[f.activeIndex];
  79. }
  80. if (args.pointType == PointInfoType.touchMove) {
  81. if (isFirstPointMove) {
  82. PointInfo newStartPoint = args;
  83. newStartPoint.addOffset(0, -0.2);
  84. if (isMoveTargetOutOfRange(newStartPoint)) return true;
  85. for (var element in f.innerPoints) {
  86. element.update(newStartPoint);
  87. }
  88. } else {
  89. PointInfo newPoint = PointInfo.fromOffset(
  90. currPointLastPosition
  91. .clone()
  92. .addVector(args - touchStartPosition)
  93. .toOffset(),
  94. args.pointType);
  95. newPoint.hostVisualArea = args.hostVisualArea;
  96. if (isMoveTargetOutOfRange(newPoint)) return true;
  97. f.innerPoints[f.activeIndex] = newPoint;
  98. f.adjustPoints(newPoint);
  99. }
  100. }
  101. if (args.pointType == PointInfoType.touchUp) {
  102. if (isFirstPointMove) {
  103. isFirstPointMove = false;
  104. f.activeIndex = 1;
  105. }
  106. if (f.activeIndex == 1) {
  107. if (f.xAxisEnd != f.xAxisStart) {
  108. f.adjustPoints(args);
  109. f.activeIndex = 2;
  110. }
  111. } else if (f.activeIndex == 2) {
  112. doFeatureFinish();
  113. }
  114. }
  115. doCalculate();
  116. return true;
  117. }
  118. return false;
  119. }
  120. void handleMouseDownWhileWaiting(PointInfo args) {
  121. // TODO: 判断是否当前area
  122. // 转换为Area逻辑位置
  123. final point = args.toAreaLogicPoint();
  124. feature = EllipseFeature(this, point);
  125. if (args.hostVisualArea != null) {
  126. feature!.hostVisualArea = args.hostVisualArea;
  127. }
  128. feature!.activeIndex = 1;
  129. state = ItemStates.running;
  130. }
  131. void handleTouchDownWhileWaiting(PointInfo args) {
  132. // TODO: 判断是否当前area
  133. // 转换为Area逻辑位置
  134. final point = args.toAreaLogicPoint();
  135. feature = EllipseFeature(this, point);
  136. if (args.hostVisualArea != null) {
  137. feature!.hostVisualArea = args.hostVisualArea;
  138. }
  139. feature!.activeIndex = 0;
  140. state = ItemStates.running;
  141. }
  142. static Ellipse createAreaPerimeter(ItemMeta meta, [IMeasureItem? parent]) {
  143. final ellipse = Ellipse(meta, parent);
  144. ellipse.calculator = AreaPerimeterEllipseCal(ellipse);
  145. return ellipse;
  146. }
  147. static Ellipse createVolume(ItemMeta meta, [IMeasureItem? parent]) {
  148. final ellipse = Ellipse(meta, parent);
  149. ellipse.calculator = _EllipseVolumeCal(ellipse);
  150. return ellipse;
  151. }
  152. }
  153. class EllipseFeature extends AreaItemFeatureAbstract {
  154. EllipseFeature(Ellipse refItem, DPoint point) : super(refItem) {
  155. innerPoints.add(point.clone());
  156. innerPoints.add(point.clone());
  157. innerPoints.add(point.clone());
  158. innerPoints.add(point.clone());
  159. }
  160. /// 质心
  161. DPoint get centroid {
  162. final x = (xAxisStart.x + xAxisEnd.x) * 0.5;
  163. final y = (xAxisStart.y + xAxisEnd.y) * 0.5;
  164. return DPoint(x, y);
  165. }
  166. DPoint get xAxisStart => innerPoints[0];
  167. DPoint get xAxisEnd => innerPoints[1];
  168. DPoint get yAxisStart => innerPoints[2];
  169. DPoint get yAxisEnd => innerPoints[3];
  170. @override
  171. void paint(Canvas canvas, Size size) {
  172. if (innerPoints.isEmpty) return;
  173. drawId(canvas, size, idText);
  174. final xStartOffset = convert2ViewPoint(size, xAxisStart).toOffset();
  175. if (activeIndex < 1) {
  176. drawVertex(canvas, xStartOffset, true);
  177. return;
  178. } else {
  179. drawVertex(canvas, xStartOffset);
  180. }
  181. final xEndOffset = convert2ViewPoint(size, xAxisEnd).toOffset();
  182. canvas.drawDashLine(xStartOffset, xEndOffset, 1, 10, paintLinePan);
  183. final yStartOffset = convert2ViewPoint(size, innerPoints[2]).toOffset();
  184. final yEndOffset = convert2ViewPoint(size, innerPoints[3]).toOffset();
  185. final centroidOffset = convert2ViewPoint(size, centroid).toOffset();
  186. canvas.drawDashLine(yStartOffset, yEndOffset, 1, 10, paintLinePan);
  187. // canvas.drawDashLine(yStartOffset, xEndOffset, 1, 10, paintLinePan);
  188. // canvas.drawDashLine(yEndOffset, xEndOffset, 1, 10, paintLinePan);
  189. final radiusX = getRadiusX(size);
  190. final radiusY = getRadiusY(size);
  191. canvas.save();
  192. final path = Path();
  193. path.moveTo(xStartOffset.dx, xStartOffset.dy);
  194. // 以质心为原点旋转到水平方向
  195. canvas.translate(centroidOffset.dx, centroidOffset.dy);
  196. final rotateRad = (xEndOffset - xStartOffset).direction;
  197. canvas.rotate(rotateRad);
  198. canvas.translate(-centroidOffset.dx, -centroidOffset.dy);
  199. path.addOval(
  200. Rect.fromCenter(
  201. center: centroidOffset,
  202. width: radiusX * 2,
  203. height: radiusY * 2,
  204. ),
  205. );
  206. canvas.drawPath(
  207. dashPath(
  208. path,
  209. dashArray: CircularIntervalList<double>(<double>[2.0, 10.0]),
  210. ),
  211. paintPan,
  212. );
  213. canvas.restore();
  214. if (activeIndex == 1) {
  215. drawVertex(canvas, xEndOffset, true);
  216. drawVertex(canvas, yStartOffset);
  217. drawVertex(canvas, yEndOffset);
  218. } else if (activeIndex == 2) {
  219. drawVertex(canvas, xEndOffset);
  220. drawVertex(canvas, yEndOffset);
  221. drawVertex(canvas, yStartOffset, isActive);
  222. }
  223. }
  224. /// 绘制灌注图
  225. @override
  226. void paintPerfusion(Canvas canvas, Size size) {}
  227. void paintSharpOnly(Canvas canvas, Size size, {Paint? customPaintPen}) {
  228. if (innerPoints.isEmpty) return;
  229. final xStartOffset = convert2ViewPoint(size, xAxisStart).toOffset();
  230. final xEndOffset = convert2ViewPoint(size, xAxisEnd).toOffset();
  231. canvas.drawDashLine(
  232. xStartOffset, xEndOffset, 1, 10, customPaintPen ?? paintLinePan);
  233. final yStartOffset = convert2ViewPoint(size, innerPoints[2]).toOffset();
  234. final yEndOffset = convert2ViewPoint(size, innerPoints[3]).toOffset();
  235. final centroidOffset = convert2ViewPoint(size, centroid).toOffset();
  236. canvas.drawDashLine(
  237. yStartOffset, yEndOffset, 1, 10, customPaintPen ?? paintLinePan);
  238. final radiusX = getRadiusX(size);
  239. final radiusY = getRadiusY(size);
  240. canvas.save();
  241. final path = Path();
  242. path.moveTo(xStartOffset.dx, xStartOffset.dy);
  243. // 以质心为原点旋转到水平方向
  244. canvas.translate(centroidOffset.dx, centroidOffset.dy);
  245. final rotateRad = (xEndOffset - xStartOffset).direction;
  246. canvas.rotate(rotateRad);
  247. canvas.translate(-centroidOffset.dx, -centroidOffset.dy);
  248. path.addOval(
  249. Rect.fromCenter(
  250. center: centroidOffset,
  251. width: radiusX * 2,
  252. height: radiusY * 2,
  253. ),
  254. );
  255. canvas.drawPath(
  256. dashPath(
  257. path,
  258. dashArray: CircularIntervalList<double>(<double>[2.0, 10.0]),
  259. ),
  260. customPaintPen ?? paintPan,
  261. );
  262. canvas.restore();
  263. }
  264. void paintShapeFitInSize(Canvas canvas, Size size, Paint paintPan) {
  265. if (innerPoints.isEmpty) return;
  266. final xStartOffset = convert2ViewPoint(size, xAxisStart).toOffset();
  267. final xEndOffset = convert2ViewPoint(size, xAxisEnd).toOffset();
  268. final centroidOffset = convert2ViewPoint(size, centroid).toOffset();
  269. final radiusX = getRadiusX(size);
  270. final radiusY = getRadiusY(size);
  271. canvas.save();
  272. // 创建椭圆路径
  273. final path = Path();
  274. path.addOval(
  275. Rect.fromCenter(
  276. center: centroidOffset,
  277. width: radiusX * 2,
  278. height: radiusY * 2,
  279. ),
  280. );
  281. // 计算旋转角度
  282. final rotateRad = (xEndOffset - xStartOffset).direction;
  283. // 创建一个矩阵来应用旋转
  284. final rotateMatrix = Matrix4.rotationZ(rotateRad);
  285. // 获取旋转后的路径
  286. final rotatedPath = path.transform(rotateMatrix.storage);
  287. // 获取外接矩形
  288. final rect = rotatedPath.getBounds();
  289. // 计算缩放比例
  290. final scaleW = size.width / rect.width;
  291. final scaleH = size.height / rect.height;
  292. final scale = scaleW < scaleH ? scaleW : scaleH;
  293. // 计算平移距离
  294. final dx = (size.width - rect.width * scale) / 2 - rect.left * scale;
  295. final dy = (size.height - rect.height * scale) / 2 - rect.top * scale;
  296. // 应用变换
  297. canvas.translate(dx, dy);
  298. canvas.scale(scale, scale);
  299. // 调整画笔宽度
  300. paintPan.strokeWidth = 1 / scale;
  301. // 绘制路径
  302. canvas.drawPath(rotatedPath, paintPan);
  303. canvas.restore();
  304. }
  305. /// X轴半径
  306. double getRadiusX([Size? fitSize]) {
  307. if (innerPoints.length < 2) {
  308. return 0;
  309. }
  310. var p1 = xAxisStart;
  311. var p2 = xAxisEnd;
  312. if (fitSize != null) {
  313. p1 = convert2ViewPoint(fitSize, p1);
  314. p2 = convert2ViewPoint(fitSize, p2);
  315. // p1 = p1.scale2Size(fitSize);
  316. // p2 = p2.scale2Size(fitSize);
  317. }
  318. return (p2 - p1).length / 2;
  319. }
  320. // 获取未缩放的基准半径
  321. double getBaseRadiusX([Size? fitSize]) {
  322. if (innerPoints.length < 2) {
  323. return 0;
  324. }
  325. var p1 = xAxisStart;
  326. var p2 = xAxisEnd;
  327. if (fitSize != null) {
  328. p1 = p1.scale2Size(fitSize);
  329. p2 = p2.scale2Size(fitSize);
  330. }
  331. return (p2 - p1).length / 2;
  332. }
  333. /// Y轴半径
  334. double getRadiusY([Size? fitSize]) {
  335. if (activeIndex > 1) {
  336. var p = yAxisStart;
  337. var p2 = yAxisEnd;
  338. var c = centroid;
  339. if (fitSize != null) {
  340. p = convert2ViewPoint(fitSize, p);
  341. p2 = convert2ViewPoint(fitSize, p2);
  342. c = convert2ViewPoint(fitSize, c);
  343. // p = p.scale2Size(fitSize);
  344. // p2 = p2.scale2Size(fitSize);
  345. // c = c.scale2Size(fitSize);
  346. }
  347. return (p - p2).length / 2;
  348. } else {
  349. return getRadiusX(fitSize);
  350. }
  351. }
  352. /// 圆周长
  353. double getCircumference([Size? fitSize]) {
  354. double a = getRadiusX(fitSize), b = getRadiusY(fitSize);
  355. if (a < b) {
  356. a = b;
  357. b = a;
  358. }
  359. return 2 * math.pi * b + 4 * (a - b);
  360. }
  361. /// 面积
  362. double getArea([Size? fitSize]) {
  363. return math.pi * getRadiusX(fitSize) * getRadiusY(fitSize);
  364. }
  365. /// 计算Y轴坐标点
  366. void adjustPoints(DPoint point) {
  367. if (activeIndex < 1) return;
  368. Size refSize = refItem.application.displaySize;
  369. // 判断是否为 URMApp 如果是左右双幅,则需要宽度减半,此处其实是需要一个当前画幅长宽比来确定垂直位置
  370. if (refItem.application is URMApplication &&
  371. (refItem.application as URMApplication).urmDataProcessor.isLeftRight) {
  372. refSize = Size(refSize.width / 2, refSize.height);
  373. }
  374. final restoreSize = Size(1 / refSize.width, 1 / refSize.height);
  375. final p = point.scale2Size(refSize);
  376. final p1 = xAxisStart.scale2Size(refSize);
  377. final p2 = xAxisEnd.scale2Size(refSize);
  378. final double radius;
  379. if (activeIndex == 2) {
  380. radius = GeneralFormulas.distance2Line(p1, p2, p);
  381. } else {
  382. radius = getBaseRadiusX(refSize);
  383. }
  384. final crossPoints = findCrossPoints(p2, p1, radius);
  385. final p3Index = findNearest(p, crossPoints);
  386. innerPoints[2] = crossPoints[p3Index].scale2Size(restoreSize);
  387. innerPoints[3] = crossPoints[1 - p3Index].scale2Size(restoreSize);
  388. }
  389. List<DPoint> findCrossPoints(DPoint a, DPoint b, distance) {
  390. final dx = b.x - a.x;
  391. final dy = b.y - a.y;
  392. final len = (b - a).length;
  393. /// 单位长度轴上距离增加量
  394. final udx = dx / len;
  395. final udy = dy / len;
  396. final px = -udy;
  397. final py = udx;
  398. final centroidX = (a.x + b.x) * 0.5;
  399. final centroidY = (a.y + b.y) * 0.5;
  400. final centroid = DPoint(centroidX, centroidY);
  401. final x1 = centroid.x + px * distance;
  402. final y1 = centroid.y + py * distance;
  403. final out1 = DPoint(x1, y1);
  404. final x2 = centroid.x - px * distance;
  405. final y2 = centroid.y - py * distance;
  406. final out2 = DPoint(x2, y2);
  407. return [out1, out2];
  408. }
  409. int findNearest(DPoint p, List<DPoint> source) {
  410. int rst = 0;
  411. double minLen = (source[0] - p).length;
  412. for (var i = 1; i < source.length; i++) {
  413. final len = (source[i] - p).length;
  414. if (len < minLen) {
  415. minLen = len;
  416. rst = i;
  417. }
  418. }
  419. return rst;
  420. }
  421. }
  422. class _EllipseVolumeCal extends Calculator<Ellipse, double> {
  423. _EllipseVolumeCal(Ellipse ref) : super(ref);
  424. @override
  425. void calculate() {
  426. if (ref.feature == null) return;
  427. final feature = ref.feature! as EllipseFeature;
  428. final viewport = feature.hostVisualArea!.viewport!;
  429. final size = viewport.convertBoundary;
  430. double xAxis = feature.getRadiusX(size);
  431. double yAxis = feature.getRadiusY(size);
  432. var a = math.max(xAxis, yAxis);
  433. var b = math.min(xAxis, yAxis);
  434. var value = math.pi / 6.0 * a * b * b;
  435. updateFloatValue(value, unit: VidUsUnit.cm3, useRound: true);
  436. }
  437. }
  438. class EllipseFeatureForRoiCopy extends EllipseFeature {
  439. EllipseFeatureForRoiCopy(Ellipse refItem, DPoint point)
  440. : super(refItem, point);
  441. @override
  442. bool needRecordHistory = false; // 不记录历史
  443. }