piece.dart 33 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003
  1. // piece.dart
  2. import 'dart:math';
  3. import 'package:flutter/material.dart';
  4. import 'package:image_puzzle/play/board.dart';
  5. import 'package:logging/logging.dart';
  6. import 'package:vector_math/vector_math.dart' as vmath;
  7. final Logger _log = Logger('piece.dart');
  8. const double _cornerRadius = 8.0;
  9. const double _outLineOffset = 0.5;
  10. const double _innerLineOffset = 1.5;
  11. // piece分组数据结构
  12. class PieceGroup {
  13. List<Piece> pieces = [];
  14. int get length => pieces.length;
  15. // 群组的组合路径缓存 (路径是相对坐标,相对于群组基准点)
  16. Path? groupClipPath;
  17. Path? groupOutLinePath;
  18. Path? groupInnerLinePath;
  19. void add(Piece piece) {
  20. if (!pieces.contains(piece)) {
  21. pieces.add(piece);
  22. }
  23. // 确保 piece 的 group 指向自己
  24. piece.group = this;
  25. clearPathCache(); // 群组变化,清空缓存
  26. }
  27. void remove(Piece piece) {
  28. if (pieces.contains(piece)) {
  29. pieces.remove(piece);
  30. }
  31. piece.group = null;
  32. clearPathCache(); // 群组变化,清空缓存
  33. }
  34. bool contains(Piece piece) {
  35. return pieces.contains(piece);
  36. }
  37. // 判断本group是否包含other group
  38. bool containsGroup(PieceGroup otherGroup) {
  39. for (var p in otherGroup.pieces) {
  40. if (!pieces.contains(p)) {
  41. return false;
  42. }
  43. }
  44. return true;
  45. }
  46. // 网格坐标边界计算辅助属性
  47. int get _minR => pieces.map((p) => p.curRow).reduce(min);
  48. int get _maxR => pieces.map((p) => p.curRow).reduce(max);
  49. int get _minC => pieces.map((p) => p.curCol).reduce(min);
  50. int get _maxC => pieces.map((p) => p.curCol).reduce(max);
  51. // -----------------------------
  52. // 群组中心点(绝对坐标)
  53. Offset get center {
  54. if (pieces.isEmpty) return Offset.zero;
  55. final board = pieces[0].board;
  56. double minX = double.infinity, minY = double.infinity;
  57. double maxX = -double.infinity, maxY = -double.infinity;
  58. for (var piece in pieces) {
  59. // 碎片当前在 Canvas 上的左上角坐标
  60. final x = piece.transform.storage[12];
  61. final y = piece.transform.storage[13];
  62. final w = board.pieceLogicalWidth;
  63. final h = board.pieceLogicalHeight;
  64. minX = min(minX, x);
  65. minY = min(minY, y);
  66. maxX = max(maxX, x + w);
  67. maxY = max(maxY, y + h);
  68. }
  69. // 计算边界框中心点
  70. return Offset((minX + maxX) / 2, (minY + maxY) / 2);
  71. }
  72. // 计算群组的左上角碎片(以 curRow/curCol 为准)
  73. Piece get topLeftPiece {
  74. if (pieces.isEmpty) throw Exception("Group is empty");
  75. // 找到最上面,然后最左边的格子作为起点
  76. return pieces.reduce((a, b) {
  77. if (a.curRow < b.curRow) return a;
  78. if (b.curRow < a.curRow) return b;
  79. if (a.curCol < b.curCol) return a;
  80. return b;
  81. });
  82. }
  83. // --- 新增辅助方法:用于不规则群组路径追踪 ---
  84. // 检查指定网格坐标 (r, c) 是否有碎片属于本群组
  85. // r, c 是绝对网格坐标 (curRow, curCol)
  86. bool _pieceAtCurCoord(int r, int c) {
  87. if (r < 0 || c < 0) return false;
  88. // 只需要检查群组内的碎片
  89. for (var piece in pieces) {
  90. if (piece.curRow == r && piece.curCol == c) {
  91. return true;
  92. }
  93. }
  94. return false;
  95. }
  96. // 检查 (r, c) 单元格的指定边是否是群组的外部边界
  97. // r, c 是绝对网格坐标 (curRow, curCol)
  98. bool _isExternalGroupBoundary(int r, int c, String side) {
  99. if (!_pieceAtCurCoord(r, c)) {
  100. return false; // 该坐标没有碎片属于这个群组,不可能是边界
  101. }
  102. final board = pieces[0].board;
  103. switch (side) {
  104. case 'top':
  105. if (r == 0) return true; // Board top
  106. return !_pieceAtCurCoord(r - 1, c); // 上邻居不在群组内
  107. case 'right':
  108. if (c == board.cols - 1) return true; // Board right
  109. return !_pieceAtCurCoord(r, c + 1); // 右邻居不在群组内
  110. case 'bottom':
  111. if (r == board.rows - 1) return true; // Board bottom
  112. return !_pieceAtCurCoord(r + 1, c); // 下邻居不在群组内
  113. case 'left':
  114. if (c == 0) return true; // Board left
  115. return !_pieceAtCurCoord(r, c - 1); // 左邻居不在群组内
  116. default:
  117. return false;
  118. }
  119. }
  120. // 生成群组整体路径(裁剪/边框的核心逻辑)
  121. Path _generateIrregularGroupPath({required bool isClipPath, double offset = 0}) {
  122. if (pieces.isEmpty) return Path();
  123. final path = Path();
  124. final board = pieces[0].board;
  125. final w = board.pieceLogicalWidth;
  126. final h = board.pieceLogicalHeight;
  127. final radius = _cornerRadius;
  128. final r = radius; // 使用原始半径,偏移量通过坐标实现
  129. // 1. 确定起始 Piece 和起始点
  130. Piece startPiece = topLeftPiece;
  131. Piece loopPiece = startPiece;
  132. // 群组本地坐标系下的起始点 (top-left corner of the top-left piece)
  133. final startRelX = (startPiece.curCol - _minC) * w;
  134. final startRelY = (startPiece.curRow - _minR) * h;
  135. // 决定起点 MoveTo 坐标 (Group Local)
  136. double initialX = startRelX + offset;
  137. double initialY = startRelY + offset;
  138. // 初始方向:向右 (Tracing Top edge)
  139. String direction = 'right';
  140. final hasStartTop = _isExternalGroupBoundary(startPiece.curRow, startPiece.curCol, 'top');
  141. final hasStartLeft = _isExternalGroupBoundary(startPiece.curRow, startPiece.curCol, 'left');
  142. if (hasStartTop && hasStartLeft) {
  143. path.moveTo(initialX + r, initialY);
  144. initialX += r;
  145. } else if (hasStartTop) {
  146. path.moveTo(initialX, initialY);
  147. } else if (hasStartLeft) {
  148. // 这种情况意味着左边是外部边界,但上面是内部边界,路径起点在左边中央
  149. path.moveTo(initialX, initialY + r);
  150. initialY += r;
  151. } else {
  152. // TL 角是内部的,但 topLeftPiece 保证是群组最左上角,因此至少有一条边是外部边界
  153. // 如果两边都是内部,说明群组内部有空隙,但此处假设群组是连通的。
  154. path.moveTo(initialX, initialY);
  155. }
  156. // 当前路径点 (Group Local)
  157. double currentX = initialX;
  158. double currentY = initialY;
  159. // Safety break
  160. int maxIterations = pieces.length * 8; // 增加安全上限
  161. int iteration = 0;
  162. // --- 路径追踪循环 (Wall Follower - Keep Group on Left) ---
  163. do {
  164. iteration++;
  165. if (iteration > maxIterations) {
  166. _log.severe('Group path tracing failed to close after $maxIterations iterations. Breaking.');
  167. break;
  168. }
  169. final absR = loopPiece.curRow;
  170. final absC = loopPiece.curCol;
  171. final relC = absC - _minC;
  172. final relR = absR - _minR;
  173. // 当前 Piece 的边界坐标 (Group Local) - 包含偏移
  174. final x0 = relC * w + offset; // Left
  175. final y0 = relR * h + offset; // Top
  176. final x1 = (relC + 1) * w - offset; // Right
  177. final y1 = (relR + 1) * h - offset; // Bottom
  178. String nextDirection = '';
  179. Piece? nextPiece;
  180. bool isCornerRounded = false;
  181. // 检查当前方向的边是否是外部边界
  182. bool isExternalEdge = false;
  183. switch (direction) {
  184. case 'right':
  185. isExternalEdge = _isExternalGroupBoundary(absR, absC, 'top');
  186. break;
  187. case 'down':
  188. isExternalEdge = _isExternalGroupBoundary(absR, absC, 'right');
  189. break;
  190. case 'left':
  191. isExternalEdge = _isExternalGroupBoundary(absR, absC, 'bottom');
  192. break;
  193. case 'up':
  194. isExternalEdge = _isExternalGroupBoundary(absR, absC, 'left');
  195. break;
  196. }
  197. if (isExternalEdge) {
  198. // --- 1. Edge Segment Drawing ---
  199. if (direction == 'right') {
  200. // Tracing Top edge to TR corner (x1, y0)
  201. final hasR = _isExternalGroupBoundary(absR, absC, 'right');
  202. isCornerRounded = hasR;
  203. final targetX = x1;
  204. final targetY = y0;
  205. // LineTo segment
  206. final lineToX = isCornerRounded ? targetX - r : targetX;
  207. if (currentX < lineToX) {
  208. // 避免重复 LineTo
  209. path.lineTo(lineToX, targetY);
  210. currentX = lineToX;
  211. currentY = targetY;
  212. }
  213. // Arc for TR corner
  214. if (isCornerRounded) {
  215. path.arcToPoint(Offset(targetX, targetY + r), radius: Radius.circular(r), clockwise: true);
  216. currentX = targetX;
  217. currentY = targetY + r;
  218. } else {
  219. currentX = targetX;
  220. currentY = targetY;
  221. }
  222. // --- 2. Decision: Turn Down or Move Right ---
  223. nextPiece = board.getPieceByCoordinate(absR, absC + 1);
  224. bool rightNeighborInGroup = nextPiece != null && pieces.contains(nextPiece);
  225. if (hasR) {
  226. // Right edge exposed -> TURN DOWN (Stay on loopPiece)
  227. nextDirection = 'down';
  228. } else if (rightNeighborInGroup) {
  229. // Right edge internal -> MOVE RIGHT (Change loopPiece)
  230. loopPiece = nextPiece!;
  231. nextDirection = 'right';
  232. } else {
  233. // Board boundary or outer space (R is external) -> TURN DOWN (This case is covered by hasR=true, but as a fallback)
  234. nextDirection = 'down';
  235. }
  236. } else if (direction == 'down') {
  237. // Tracing Right edge to BR corner (x1, y1)
  238. final hasB = _isExternalGroupBoundary(absR, absC, 'bottom');
  239. isCornerRounded = hasB;
  240. final targetX = x1;
  241. final targetY = y1;
  242. // LineTo segment
  243. final lineToY = isCornerRounded ? targetY - r : targetY;
  244. if (currentY < lineToY) {
  245. path.lineTo(targetX, lineToY);
  246. currentX = targetX;
  247. currentY = lineToY;
  248. }
  249. // Arc for BR corner
  250. if (isCornerRounded) {
  251. path.arcToPoint(Offset(targetX - r, targetY), radius: Radius.circular(r), clockwise: true);
  252. currentX = targetX - r;
  253. currentY = targetY;
  254. } else {
  255. currentX = targetX;
  256. currentY = targetY;
  257. }
  258. // --- 2. Decision: Turn Left or Move Down ---
  259. nextPiece = board.getPieceByCoordinate(absR + 1, absC);
  260. bool bottomNeighborInGroup = nextPiece != null && pieces.contains(nextPiece);
  261. if (hasB) {
  262. // Bottom edge exposed -> TURN LEFT
  263. nextDirection = 'left';
  264. } else if (bottomNeighborInGroup) {
  265. // Bottom edge internal -> MOVE DOWN
  266. loopPiece = nextPiece!;
  267. nextDirection = 'down';
  268. } else {
  269. nextDirection = 'left';
  270. }
  271. } else if (direction == 'left') {
  272. // Tracing Bottom edge to BL corner (x0, y1)
  273. final hasL = _isExternalGroupBoundary(absR, absC, 'left');
  274. isCornerRounded = hasL;
  275. final targetX = x0;
  276. final targetY = y1;
  277. // LineTo segment
  278. final lineToX = isCornerRounded ? targetX + r : targetX;
  279. if (currentX > lineToX) {
  280. path.lineTo(lineToX, targetY);
  281. currentX = lineToX;
  282. currentY = targetY;
  283. }
  284. // Arc for BL corner
  285. if (isCornerRounded) {
  286. path.arcToPoint(Offset(targetX, targetY - r), radius: Radius.circular(r), clockwise: true);
  287. currentX = targetX;
  288. currentY = targetY - r;
  289. } else {
  290. currentX = targetX;
  291. currentY = targetY;
  292. }
  293. // --- 2. Decision: Turn Up or Move Left ---
  294. nextPiece = board.getPieceByCoordinate(absR, absC - 1);
  295. bool leftNeighborInGroup = nextPiece != null && pieces.contains(nextPiece);
  296. if (hasL) {
  297. // Left edge exposed -> TURN UP
  298. nextDirection = 'up';
  299. } else if (leftNeighborInGroup) {
  300. // Left edge internal -> MOVE LEFT
  301. loopPiece = nextPiece!;
  302. nextDirection = 'left';
  303. } else {
  304. nextDirection = 'up';
  305. }
  306. } else if (direction == 'up') {
  307. // Tracing Left edge to TL corner (x0, y0)
  308. final hasT = _isExternalGroupBoundary(absR, absC, 'top');
  309. isCornerRounded = hasT;
  310. final targetX = x0;
  311. final targetY = y0;
  312. // LineTo segment
  313. final lineToY = isCornerRounded ? targetY + r : targetY;
  314. if (currentY > lineToY) {
  315. path.lineTo(targetX, lineToY);
  316. currentX = targetX;
  317. currentY = lineToY;
  318. }
  319. // Arc for TL corner
  320. if (isCornerRounded) {
  321. path.arcToPoint(Offset(targetX + r, targetY), radius: Radius.circular(r), clockwise: true);
  322. currentX = targetX + r;
  323. currentY = targetY;
  324. } else {
  325. currentX = targetX;
  326. currentY = targetY;
  327. }
  328. // --- 2. Decision: Turn Right or Move Up ---
  329. nextPiece = board.getPieceByCoordinate(absR - 1, absC);
  330. bool topNeighborInGroup = nextPiece != null && pieces.contains(nextPiece);
  331. if (hasT) {
  332. // Top edge exposed -> TURN RIGHT
  333. nextDirection = 'right';
  334. } else if (topNeighborInGroup) {
  335. // Top edge internal -> MOVE UP
  336. loopPiece = nextPiece!;
  337. nextDirection = 'up';
  338. } else {
  339. nextDirection = 'right';
  340. }
  341. }
  342. } else {
  343. // --- Edge is Internal: Move Straight or Turn Right ---
  344. // This piece's current edge is internal, meaning the external boundary is on the neighbor's side.
  345. // We must check if we can move *through* the current piece to the next one in the same direction,
  346. // or if we must turn right immediately because the straight path is blocked by an external boundary.
  347. // Next piece to check (straight ahead)
  348. Piece? straightPiece;
  349. switch (direction) {
  350. case 'right':
  351. straightPiece = board.getPieceByCoordinate(absR, absC + 1);
  352. break;
  353. case 'down':
  354. straightPiece = board.getPieceByCoordinate(absR + 1, absC);
  355. break;
  356. case 'left':
  357. straightPiece = board.getPieceByCoordinate(absR, absC - 1);
  358. break;
  359. case 'up':
  360. straightPiece = board.getPieceByCoordinate(absR - 1, absC);
  361. break;
  362. }
  363. bool straightInGroup = straightPiece != null && pieces.contains(straightPiece);
  364. if (straightInGroup) {
  365. // Straight piece is in group -> MOVE STRAIGHT (Skip current internal piece)
  366. loopPiece = straightPiece!;
  367. nextDirection = direction;
  368. } else {
  369. // Straight piece is outside/null, and current edge is internal.
  370. // This is an internal concave corner. We must turn right to follow the external boundary.
  371. // Turn right (change direction, stay on current piece)
  372. switch (direction) {
  373. case 'right':
  374. nextDirection = 'down';
  375. break;
  376. case 'down':
  377. nextDirection = 'left';
  378. break;
  379. case 'left':
  380. nextDirection = 'up';
  381. break;
  382. case 'up':
  383. nextDirection = 'right';
  384. break;
  385. }
  386. }
  387. }
  388. // Update direction and loopPiece for next iteration
  389. direction = nextDirection;
  390. // --- Termination Check ---
  391. if (currentX == initialX && currentY == initialY) {
  392. _log.info('Closed path at initial point. Iterations: $iteration');
  393. break;
  394. }
  395. } while (true);
  396. if (isClipPath) {
  397. path.close();
  398. }
  399. return path;
  400. }
  401. // 生成群组裁剪路径(现在支持不规则形状)
  402. Path _generateGroupClipPath() {
  403. // 裁剪路径是封闭的,且没有偏移
  404. return _generateIrregularGroupPath(isClipPath: true);
  405. }
  406. // 生成群组边框路径(现在支持不规则形状)
  407. Path _generateGroupBorderPath(double offset) {
  408. // 边框路径是开放的(不 close),且有偏移
  409. return _generateIrregularGroupPath(isClipPath: false, offset: offset);
  410. }
  411. // 生成群组整体路径(裁剪+边框)
  412. void generateGroupPaths() {
  413. // 使用新的不规则路径生成逻辑
  414. groupClipPath = _generateGroupClipPath();
  415. groupOutLinePath = _generateGroupBorderPath(_outLineOffset);
  416. groupInnerLinePath = _generateGroupBorderPath(_innerLineOffset);
  417. }
  418. // 群组整体位移(优化:避免遍历碎片)
  419. void applyDelta(Offset delta) {
  420. for (var piece in pieces) {
  421. piece.transform.storage[12] += delta.dx;
  422. piece.transform.storage[13] += delta.dy;
  423. }
  424. }
  425. void clearPathCache() {
  426. groupClipPath = null;
  427. groupOutLinePath = null;
  428. groupInnerLinePath = null;
  429. }
  430. void print() {
  431. String str = '======= group size: $length =======\n';
  432. for (var p in pieces) {
  433. str += p.toString();
  434. str += '\n';
  435. }
  436. _log.info(str);
  437. }
  438. }
  439. // 碎片基础数据结构
  440. class Piece {
  441. final int index;
  442. final Board board;
  443. PieceGroup? group;
  444. // 总计的行数和列数
  445. final int rows;
  446. final int cols;
  447. // 碎片在完整图片中的行/列位置 (逻辑坐标)
  448. final int row;
  449. final int col;
  450. // 碎片当前所处的位置
  451. int curCol;
  452. int curRow;
  453. // 正确目标位置的左上角坐标 (在 Canvas 坐标系内,这是碎片的最终位置)
  454. final Offset correctOffset;
  455. // 碎片的当前几何变换状态 (包括位置、旋转等)
  456. vmath.Matrix4 transform;
  457. // 碎片在原图片中的裁剪矩形 (Source Rect of the image)
  458. final Rect sourceRect;
  459. bool get isOK => row == curRow && col == curCol;
  460. // clipPath, 碎片的裁剪路径(闭合), 这是为了绘制出圆角效果
  461. Path? path;
  462. // 内边框path(可能非闭合)
  463. Path? innerLinePath;
  464. // 外边框path (可能非闭合)
  465. Path? outLinePath;
  466. // 翻转相关属性
  467. double flipProgress = 0.0;
  468. Piece({
  469. required this.board,
  470. required this.index,
  471. required this.row,
  472. required this.col,
  473. required this.rows,
  474. required this.cols,
  475. required this.correctOffset,
  476. required this.sourceRect,
  477. required this.curCol,
  478. required this.curRow,
  479. required this.transform,
  480. });
  481. @override
  482. String toString() => 'Piece($index,$row:$col)';
  483. double get width => board.pieceLogicalWidth;
  484. double get height => board.pieceLogicalHeight;
  485. // 辅助函数:获取碎片当前的左上角位置 (tx, ty)
  486. // 平移分量在 Matrix4.storage 的索引 12 (tx) 和 13 (ty)
  487. Offset get currentOffset => Offset(transform.storage[12], transform.storage[13]);
  488. // 辅助函数:获取碎片当前的中心点 (在 Canvas 坐标系中)
  489. Offset get currentCenter {
  490. // 碎片的本地中心点
  491. final Offset localCenter = Offset(width / 2, height / 2);
  492. // 应用当前变换矩阵
  493. final vmath.Vector4 transformed = transform.transform(vmath.Vector4(localCenter.dx, localCenter.dy, 0.0, 1.0));
  494. return Offset(transformed.x, transformed.y);
  495. }
  496. // 辅助函数:将碎片移动指定的位移量
  497. void applyDelta(Offset delta) {
  498. // 累加 x 轴位移
  499. transform.storage[12] += delta.dx;
  500. // 累加 y 轴位移
  501. transform.storage[13] += delta.dy;
  502. }
  503. // 归位, 回到原来的位置
  504. void revert() {
  505. transform = board.getTransformByCoordinate(curRow, curCol);
  506. }
  507. // 判断当前piece是否可以安置到other的槽位去
  508. bool canPlaceTo(Piece other) {
  509. // 1. 如果当前碎片是单独移动,则可以安置
  510. if (group == null) {
  511. return true;
  512. }
  513. // 当前 piece 与 other piece 的位移
  514. int dr = other.curRow - curRow;
  515. int dc = other.curCol - curCol;
  516. // 判断当前piece的组成员,产生相同的位移后会不会溢出
  517. for (var p in group!.pieces) {
  518. int newRow = p.curRow + dr;
  519. int newCol = p.curCol + dc;
  520. if (newRow < 0 || newRow >= rows || newCol < 0 || newCol >= cols) return false;
  521. }
  522. return true;
  523. }
  524. // 判断当前piece是否可以和other piece 合并
  525. bool canMerge(Piece other) {
  526. // 原来是邻居, 现在也是邻居,才具备可以合并的基础条件
  527. if (isNeighbour(other) && isCurNeighbour(other)) {
  528. // 判断相对位置是否保持一致 (防止错位拼接)
  529. if (col == other.col) {
  530. // 同一列 (上/下相邻)
  531. if ((row - other.row) == (curRow - other.curRow)) return true;
  532. } else if (row == other.row) {
  533. // 同一行 (左/右相邻)
  534. if ((col - other.col) == (curCol - other.curCol)) return true;
  535. }
  536. }
  537. return false;
  538. }
  539. /// 创建一个新组, 合并两个piece组
  540. void groupWith(Piece other) {
  541. PieceGroup finalGroup = PieceGroup();
  542. List<Piece> list = [];
  543. if (group != null) list.addAll(group!.pieces);
  544. if (other.group != null) list.addAll(other.group!.pieces);
  545. list.add(this);
  546. list.add(other);
  547. // 确保没有重复的 Piece
  548. final Set<Piece> uniquePieces = list.toSet();
  549. for (var p in uniquePieces) {
  550. finalGroup.add(p);
  551. }
  552. // 合并后需要重新计算 group paths
  553. finalGroup.generateGroupPaths();
  554. }
  555. // 获取碎片本来的邻居(原正确位置上的邻居)
  556. List<int> getNeighbourIndexes() {
  557. List<int> list = [];
  558. if (row > 0) list.add(index - cols); // 上
  559. if (row < (rows - 1)) list.add(index + cols); // 下
  560. if (col > 0) list.add(index - 1); // 左
  561. if (col < (cols - 1)) list.add(index + 1); // 右
  562. return list;
  563. }
  564. bool isSameGroup(Piece other) => group != null && group == other.group;
  565. bool isNeighbour(Piece other) => row == other.row && (col - other.col).abs() == 1 || col == other.col && (row - other.row).abs() == 1;
  566. bool isCurNeighbour(Piece other) =>
  567. curRow == other.curRow && (curCol - other.curCol).abs() == 1 || curCol == other.curCol && (curRow - other.curRow).abs() == 1;
  568. // 以下四个边界检测逻辑:
  569. // 仅当邻居在当前位置相邻,且它们的原图相对位置正确时,才移除边界(即边界被“吸收”)。
  570. // 否则,需要绘制边界(群组内部边界或群组外部边界)。
  571. bool get _hasTopBorder {
  572. int topCurRow = curRow - 1;
  573. int topCurCol = curCol;
  574. // 已经是顶部的宫格, 需要上边框
  575. if (topCurRow < 0) return true;
  576. // 获取本碎片当前的上边邻居
  577. final topPiece = board.getPieceByCoordinate(topCurRow, topCurCol);
  578. // 邻居缺失(空槽位),需要边框
  579. if (topPiece == null) {
  580. _log.warning('找不到 ${toString()} 的上邻居,有错误发生,请检查');
  581. return true;
  582. }
  583. // 如果它们在当前位置相邻,且原图相对位置正确,则边界被内部吸收,不需要绘制。
  584. // 即:当前碎片(row)在邻居碎片(topPiece.row)的下方一个位置
  585. if (row == topPiece.row + 1 && col == topPiece.col) {
  586. return false;
  587. }
  588. // 否则,需要绘制边界
  589. return true;
  590. }
  591. // 是否有右边框
  592. bool get _hasRightBorder {
  593. int rightCurRow = curRow;
  594. int rightCurCol = curCol + 1;
  595. // 已经是最右边的宫格, 需要右边框
  596. if (rightCurCol >= cols) return true;
  597. // 获取本碎片当前的右边邻居
  598. final rightPiece = board.getPieceByCoordinate(rightCurRow, rightCurCol);
  599. if (rightPiece == null) {
  600. _log.warning('找不到 ${toString()} 的右邻居,有错误发生,请检查');
  601. return true;
  602. }
  603. // 如果与右邻居的相对位置是正确的,那么没有右边框
  604. if (row == rightPiece.row && col == rightPiece.col - 1) {
  605. return false;
  606. }
  607. return true;
  608. }
  609. // 是否有下边框
  610. bool get _hasBottomBorder {
  611. int bottomCurRow = curRow + 1;
  612. int bottomCurCol = curCol;
  613. // 已经是底部的宫格, 需要下边框
  614. if (bottomCurRow >= rows) return true;
  615. // 获取本碎片当前的底边邻居
  616. final bottomPiece = board.getPieceByCoordinate(bottomCurRow, bottomCurCol);
  617. if (bottomPiece == null) {
  618. _log.warning('找不到 ${toString()} 的下邻居,有错误发生,请检查');
  619. return true;
  620. }
  621. // 如果与下邻居的相对位置是正确的,那么没有下边框
  622. if (row == bottomPiece.row - 1 && col == bottomPiece.col) {
  623. return false;
  624. }
  625. return true;
  626. }
  627. // 是否有左边框
  628. bool get _hasLeftBorder {
  629. int leftCurRow = curRow;
  630. int leftCurCol = curCol - 1;
  631. // 已经是最左部的宫格, 需要左边框
  632. if (leftCurCol < 0) return true;
  633. // 获取本碎片当前的左边邻居
  634. final leftPiece = board.getPieceByCoordinate(leftCurRow, leftCurCol);
  635. if (leftPiece == null) {
  636. _log.warning('找不到 ${toString()} 的左邻居,有错误发生,请检查');
  637. return true;
  638. }
  639. // 如果与左邻居的相对位置是正确的,那么没有左边框
  640. if (row == leftPiece.row && col == leftPiece.col + 1) {
  641. return false;
  642. }
  643. return true;
  644. }
  645. // 生成翻转变换矩阵(结合平移)
  646. void updateFlipTransform(double flipAngle, vmath.Matrix4 targetTranslate) {
  647. flipProgress = flipAngle;
  648. final flipMatrix = vmath.Matrix4.identity()
  649. ..translate(width / 2, height / 2) // 1. 移到卡片中心(旋转中心)
  650. ..rotateY(flipAngle) // 2. Y轴旋转(翻转动画)
  651. ..scale(-1.0, 1.0, 1.0) // 3. X轴缩放-1:抵消旋转带来的左右镜像
  652. ..translate(-width / 2, -height / 2); // 4. 移回原位
  653. transform = targetTranslate * flipMatrix;
  654. }
  655. // 判断是否显示正面
  656. // 当flipProgress在[0, pi/2)(0~90 度)时:卡片未完全翻转,显示背面,isFlipped为false。
  657. // 当flipProgress在[pi/2, pi](90~180 度)时:卡片已翻转到正面,isFlipped为true。
  658. bool get isFlipped => flipProgress >= pi / 2;
  659. // 生成clip path
  660. Path _generateClipPath(double w, double h, List<bool> borders, double radius) {
  661. Path path = Path();
  662. // 一个角是否为圆角取决于相邻的两条边是否都需要绘制 (即 borders 都为 true)
  663. final bool tlRounded = borders[0] && borders[3]; // Top && Left
  664. final bool trRounded = borders[0] && borders[1]; // Top && Right
  665. final bool brRounded = borders[2] && borders[1]; // Bottom && Right
  666. final bool blRounded = borders[2] && borders[3]; // Bottom && Left
  667. // 1. 移动到起点 (左上角,T 边直线段的起点)
  668. if (tlRounded) {
  669. path.moveTo(radius, 0);
  670. } else {
  671. path.moveTo(0, 0); // 尖角起点
  672. }
  673. // A. 顶边 (T) - LineTo
  674. // 终点是 TR 圆角的起点 (w - radius, 0) 或 TR 尖角 (w, 0)
  675. if (trRounded) {
  676. path.lineTo(w - radius, 0);
  677. } else {
  678. path.lineTo(w, 0);
  679. }
  680. // B. 右上角 (TR) - Arc
  681. if (trRounded) {
  682. path.arcToPoint(Offset(w, radius), radius: Radius.circular(radius), clockwise: true);
  683. }
  684. // C. 右边 (R) - LineTo
  685. // 终点是 BR 圆角的起点 (w, h - radius) 或 BR 尖角 (w, h)
  686. if (brRounded) {
  687. path.lineTo(w, h - radius);
  688. } else {
  689. path.lineTo(w, h);
  690. }
  691. // D. 右下角 (BR) - Arc
  692. if (brRounded) {
  693. path.arcToPoint(Offset(w - radius, h), radius: Radius.circular(radius), clockwise: true);
  694. }
  695. // E. 底边 (B) - LineTo
  696. // 终点是 BL 圆角的起点 (radius, h) 或 BL 尖角 (0, h)
  697. if (blRounded) {
  698. path.lineTo(radius, h);
  699. } else {
  700. path.lineTo(0, h);
  701. }
  702. // F. 左下角 (BL) - Arc
  703. if (blRounded) {
  704. path.arcToPoint(Offset(0, h - radius), radius: Radius.circular(radius), clockwise: true);
  705. }
  706. // G. 左边 (L) - LineTo
  707. // 终点是 TL 圆角的起点 (0, radius) 或 TL 尖角 (0, 0)
  708. if (tlRounded) {
  709. path.lineTo(0, radius);
  710. } else {
  711. path.lineTo(0, 0);
  712. }
  713. // H. 左上角 (TL) - Arc & Close
  714. if (tlRounded) {
  715. path.arcToPoint(Offset(radius, 0), radius: Radius.circular(radius), clockwise: true);
  716. }
  717. path.close();
  718. return path;
  719. }
  720. // 生成单个碎片边框路径
  721. Path _generateBorderPath(double w, double h, List<bool> borders, double radius, double offset) {
  722. Path path = Path();
  723. final double r = radius;
  724. // 边界调整后的坐标
  725. final double x0 = offset; // 左边界
  726. final double y0 = offset; // 上边界
  727. final double x1 = w - offset; // 右边界
  728. final double y1 = h - offset; // 下边界
  729. // 关键点坐标 (圆角弧的起点/终点)
  730. final Offset pTL_T = Offset(x0 + r, y0); // Top边起点
  731. final Offset pTR_T = Offset(x1 - r, y0); // Top边终点
  732. final Offset pTR_R = Offset(x1, y0 + r); // Right边起点
  733. final Offset pBR_R = Offset(x1, y1 - r); // Right边终点
  734. final Offset pBR_B = Offset(x1 - r, y1); // Bottom边起点
  735. final Offset pBL_B = Offset(x0 + r, y1); // Bottom边终点
  736. final Offset pBL_L = Offset(x0, y1 - r); // Left边起点
  737. final Offset pTL_L = Offset(x0, y0 + r); // Left边终点
  738. // [Top, Right, Bottom, Left]
  739. final bool hasT = borders[0];
  740. final bool hasR = borders[1];
  741. final bool hasB = borders[2];
  742. final bool hasL = borders[3];
  743. // --- 1. Top Border (T) ---
  744. if (hasT) {
  745. // 检查TL角是否是尖角 (Left边缺失)
  746. if (hasL) {
  747. path.moveTo(pTL_T.dx, pTL_T.dy); // 从TL弧的终点开始
  748. } else {
  749. // path.moveTo(x0, y0); // 从左上尖角开始
  750. path.moveTo(0, y0); // 从左上尖角开始
  751. }
  752. // 绘制 Top 直线段
  753. if (hasR) {
  754. path.lineTo(pTR_T.dx, pTR_T.dy);
  755. } else {
  756. // path.lineTo(x1, y0); // 到右上尖角
  757. path.lineTo(w, y0); // 到右上尖角
  758. }
  759. }
  760. // --- 2. Top-Right Corner (TR) & Right Border (R) ---
  761. if (hasT && hasR) {
  762. // 绘制 TR 弧
  763. path.arcToPoint(pTR_R, radius: Radius.circular(r), clockwise: true);
  764. } else if (hasR) {
  765. // T 缺失,R 存在:需要开始一个新的轮廓,从 TR 弧的起点开始 (pTR_R)
  766. if (hasT) {
  767. path.moveTo(pTR_R.dx, pTR_R.dy); // 从 TR 弧终点开始
  768. } else {
  769. // path.moveTo(x1, y0); // 从右上尖角开始
  770. path.moveTo(x1, 0); // 从右上尖角开始
  771. }
  772. }
  773. if (hasR) {
  774. // 绘制 Right 直线段
  775. if (hasB) {
  776. path.lineTo(pBR_R.dx, pBR_R.dy);
  777. } else {
  778. // path.lineTo(x1, y1); // 到右下尖角
  779. path.lineTo(x1, h); // 到右下尖角
  780. }
  781. }
  782. // --- 3. Bottom-Right Corner (BR) & Bottom Border (B) ---
  783. if (hasR && hasB) {
  784. // 绘制 BR 弧
  785. path.arcToPoint(pBR_B, radius: Radius.circular(r), clockwise: true);
  786. } else if (hasB) {
  787. // R 缺失,B 存在:需要开始一个新的轮廓,从 BR 弧的起点开始 (pBR_B)
  788. if (hasR) {
  789. path.moveTo(pBR_B.dx, pBR_B.dy); // 从 BR 弧终点开始
  790. } else {
  791. // path.moveTo(x1, y1); // 从右下尖角开始
  792. path.moveTo(w, y1); // 从右下尖角开始
  793. }
  794. }
  795. if (hasB) {
  796. // 绘制 Bottom 直线段
  797. if (hasL) {
  798. path.lineTo(pBL_B.dx, pBL_B.dy);
  799. } else {
  800. // path.lineTo(x0, y1); // 到左下尖角
  801. path.lineTo(0, y1); // 到左下尖角
  802. }
  803. }
  804. // --- 4. Bottom-Left Corner (BL) & Left Border (L) ---
  805. if (hasB && hasL) {
  806. // 绘制 BL 弧
  807. path.arcToPoint(pBL_L, radius: Radius.circular(r), clockwise: true);
  808. } else if (hasL) {
  809. // B 缺失,L 存在:需要开始一个新的轮廓,从 BL 弧的起点开始 (pBL_L)
  810. if (hasB) {
  811. path.moveTo(pBL_L.dx, pBL_L.dy); // 从 BL 弧终点开始
  812. } else {
  813. // path.moveTo(x0, y1); // 从左下尖角开始
  814. path.moveTo(x0, h); // 从左下尖角开始
  815. }
  816. }
  817. if (hasL) {
  818. // 绘制 Left 直线段
  819. if (hasT) {
  820. path.lineTo(pTL_L.dx, pTL_L.dy);
  821. } else {
  822. // path.lineTo(x0, y0); // 到左上尖角
  823. path.lineTo(x0, 0); // 到左上尖角
  824. }
  825. }
  826. // --- 5. Top-Left Corner (TL) ---
  827. if (hasL && hasT) {
  828. // 绘制 TL 弧,连接回 Top Border 的起点
  829. path.arcToPoint(pTL_T, radius: Radius.circular(r), clockwise: true);
  830. }
  831. // 注意: 不调用 path.close(),保持路径开放。
  832. return path;
  833. }
  834. // 生成碎片的clipPath, innerLinePath, outLinePath
  835. // 增加 forceRecalculate 参数,用于群组路径计算时强制重新计算边界状态
  836. List<Path> generatePaths({bool forceRecalculate = false}) {
  837. // 如果没有 group 且路径已缓存,则直接返回
  838. if (group == null && !forceRecalculate && path != null && outLinePath != null && innerLinePath != null) {
  839. return [path!, outLinePath!, innerLinePath!];
  840. }
  841. // 先确定4条边的状态[Top, Right, Bottom, Left]
  842. List<bool> borders = [true, true, true, true];
  843. // 如果是单个碎片,4条边都需要,只有 piece 在 group 中才需要判断
  844. if (group != null || forceRecalculate) {
  845. // 即使是单独计算,也需要实时检查
  846. borders = [_hasTopBorder, _hasRightBorder, _hasBottomBorder, _hasLeftBorder];
  847. }
  848. path = _generateClipPath(width, height, borders, _cornerRadius);
  849. outLinePath = _generateBorderPath(width, height, borders, _cornerRadius, _outLineOffset);
  850. innerLinePath = _generateBorderPath(width, height, borders, _cornerRadius, _innerLineOffset);
  851. return [path!, outLinePath!, innerLinePath!];
  852. }
  853. }