piece.dart 33 KB

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