piece.dart 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575
  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. // piece分组数据结构
  9. class PieceGroup {
  10. List<Piece> pieces = [];
  11. int get length => pieces.length;
  12. void add(Piece piece) {
  13. if (!pieces.contains(piece)) {
  14. pieces.add(piece);
  15. }
  16. piece.group = this;
  17. }
  18. void remove(Piece piece) {
  19. if (pieces.contains(piece)) {
  20. pieces.remove(piece);
  21. }
  22. piece.group = null;
  23. }
  24. bool contains(Piece piece) {
  25. return pieces.contains(piece);
  26. }
  27. // 判断本group是否包含other group
  28. bool containsGroup(PieceGroup otherGroup) {
  29. for (var p in otherGroup.pieces) {
  30. if (!pieces.contains(p)) {
  31. return false;
  32. }
  33. }
  34. return true;
  35. }
  36. // 群组中心点
  37. Offset get center {
  38. if (pieces.isEmpty) return Offset.zero;
  39. final board = pieces[0].board;
  40. // 计算群组在Canvas中的边界框
  41. double minX = double.infinity;
  42. double minY = double.infinity;
  43. double maxX = -double.infinity;
  44. double maxY = -double.infinity;
  45. for (var piece in pieces) {
  46. final transform = piece.transform;
  47. final x = transform.storage[12]; // 碎片左上角x
  48. final y = transform.storage[13]; // 碎片左上角y
  49. final w = board.pieceLogicalWidth;
  50. final h = board.pieceLogicalHeight;
  51. // 更新边界
  52. minX = min(minX, x);
  53. minY = min(minY, y);
  54. maxX = max(maxX, x + w);
  55. maxY = max(maxY, y + h);
  56. }
  57. // 计算边界框中心点
  58. return Offset((minX + maxX) / 2, (minY + maxY) / 2);
  59. }
  60. void print() {
  61. String str = '======= group size: $length =======\n';
  62. for (var p in pieces) {
  63. str += p.toString();
  64. str += '\n';
  65. }
  66. _log.info(str);
  67. }
  68. }
  69. // 碎片基础数据结构
  70. class Piece {
  71. final int index;
  72. final Board board; // 保存board的引用
  73. PieceGroup? group;
  74. // 总计的行数和列数
  75. final int rows;
  76. final int cols;
  77. // 碎片在完整图片中的行/列位置 (逻辑坐标)
  78. final int row;
  79. final int col;
  80. // 碎片当前所处的位置
  81. int curCol;
  82. int curRow;
  83. // 正确目标位置的左上角坐标 (在 Canvas 坐标系内,这是碎片的最终位置)
  84. final Offset correctOffset;
  85. // 碎片的当前几何变换状态 (包括位置、旋转等)
  86. vmath.Matrix4 transform;
  87. // 碎片在原图片中的裁剪矩形 (Source Rect of the image)
  88. final Rect sourceRect;
  89. // 5. 碎片周围四个边的拼接状态 (用于动态绘制内部边框)
  90. // [Top, Right, Bottom, Left]
  91. List<bool> borders; // 拟废弃,采用实时计算
  92. bool get isOK => row == curRow && col == curCol;
  93. // clipPath, 碎片的裁剪路径(闭合), 这是为了绘制出圆角效果
  94. Path? path;
  95. // 内边框path(可能非闭合)
  96. Path? innerLinePath;
  97. // 外边框path (可能非闭合)
  98. Path? outLinePath;
  99. static const double _cornerRadius = 8.0;
  100. static const double _outLineOffset = 0.5;
  101. static const double _innerLineOffset = 1.5;
  102. Piece({
  103. required this.board,
  104. required this.index,
  105. required this.row,
  106. required this.col,
  107. required this.rows,
  108. required this.cols,
  109. required this.correctOffset,
  110. required this.sourceRect,
  111. required this.curCol,
  112. required this.curRow,
  113. required this.transform,
  114. required this.borders, // 初始设置为 [true, true, true, true]
  115. });
  116. @override
  117. String toString() {
  118. return 'Piece($index,$row:$col)';
  119. }
  120. double get width => board.pieceLogicalWidth;
  121. double get height => board.pieceLogicalHeight;
  122. // 辅助函数:获取碎片当前的左上角位置 (tx, ty)
  123. // 平移分量在 Matrix4.storage 的索引 12 (tx) 和 13 (ty)
  124. Offset get currentOffset => Offset(transform.storage[12], transform.storage[13]);
  125. // 辅助函数:获取碎片当前的中心点 (在 Canvas 坐标系中)
  126. Offset get currentCenter {
  127. // 碎片的本地中心点
  128. final Offset pieceLocalCenter = Offset(board.pieceLogicalWidth / 2, board.pieceLogicalHeight / 2);
  129. // 应用当前变换矩阵
  130. final vmath.Vector4 transformedVector = transform.transform(vmath.Vector4(pieceLocalCenter.dx, pieceLocalCenter.dy, 0.0, 1.0));
  131. return Offset(transformedVector.x, transformedVector.y);
  132. }
  133. // 辅助函数:将碎片移动指定的位移量
  134. void applyDelta(Offset delta) {
  135. // 累加 x 轴位移
  136. transform.storage[12] += delta.dx;
  137. // 累加 y 轴位移
  138. transform.storage[13] += delta.dy;
  139. }
  140. // 归位, 回到原来的位置
  141. void revert() {
  142. final originalTransform = board.getTransformByCoordinate(curRow, curCol);
  143. transform = originalTransform;
  144. }
  145. // 判断当前piece是否可以安置到other的槽位去
  146. bool canPlaceTo(Piece other) {
  147. // 1. 如果当前碎片是单独移动,则可以安置
  148. if (group == null) {
  149. return true;
  150. }
  151. // 当前 piece 与 other piece 的位移
  152. int dr = other.curRow - curRow;
  153. int dc = other.curCol - curCol;
  154. // 判断当前piece的组成员,产生相同的位移后会不会溢出
  155. for (var p in group!.pieces) {
  156. int newRow = p.curRow + dr;
  157. int newCol = p.curCol + dc;
  158. if (newRow < 0 || newRow >= rows || newCol < 0 || newCol >= cols) {
  159. return false;
  160. }
  161. }
  162. return true;
  163. }
  164. // 判断当前piece是否可以和other piece 合并
  165. bool canMerge(Piece other) {
  166. // 原来是邻居, 现在也是邻居,才具备可以合并的基础条件
  167. if (isNeighbour(other) && isCurNeighbour(other)) {
  168. // 判断相对位置是否保持一致 (防止错位拼接)
  169. if (col == other.col) {
  170. // 同一列 (上/下相邻)
  171. if ((row - other.row) == (curRow - other.curRow)) return true;
  172. } else if (row == other.row) {
  173. // 同一行 (左/右相邻)
  174. if ((col - other.col) == (curCol - other.curCol)) return true;
  175. }
  176. }
  177. return false;
  178. }
  179. /// 创建一个新组, 合并两个piece组
  180. void groupWith(Piece other) {
  181. PieceGroup finalGroup = PieceGroup();
  182. List<Piece> list = [];
  183. if (group != null) list.addAll(group!.pieces);
  184. if (other.group != null) list.addAll(other.group!.pieces);
  185. list.add(this);
  186. list.add(other);
  187. for (var p in list) {
  188. finalGroup.add(p);
  189. }
  190. }
  191. // 获取碎片本来的邻居(原正确位置上的邻居)
  192. List<int> getNeighbourIndexes() {
  193. List<int> list = [];
  194. if (row > 0) list.add(index - cols); // 上
  195. if (row < (rows - 1)) list.add(index + cols); // 下
  196. if (col > 0) list.add(index - 1); // 左
  197. if (col < (cols - 1)) list.add(index + 1); // 右
  198. return list;
  199. }
  200. /// 是否同一个组
  201. bool isSameGroup(Piece other) {
  202. return group != null && group == other.group;
  203. }
  204. /// 是否邻居 (原始位置)
  205. bool isNeighbour(Piece other) {
  206. return row == other.row && (col - other.col).abs() == 1 || col == other.col && (row - other.row).abs() == 1;
  207. }
  208. // 当前位置是否是邻居
  209. bool isCurNeighbour(Piece other) {
  210. return curRow == other.curRow && (curCol - other.curCol).abs() == 1 || curCol == other.curCol && (curRow - other.curRow).abs() == 1;
  211. }
  212. // 是否有上边框
  213. bool get _hasTopBorder {
  214. int topCurRow = curRow - 1;
  215. int topCurCol = curCol;
  216. // 已经是顶部的宫格, 需要上边框
  217. if (topCurRow < 0) return true;
  218. // 获取本碎片当前的上边邻居
  219. final topPiece = board.getPieceByCoordinate(topCurRow, topCurCol);
  220. if (topPiece == null) {
  221. _log.warning('找不到 ${toString()} 的上邻居,有错误发生,请检查');
  222. return true;
  223. }
  224. // 如果与上邻居的相对位置是正确的,那么没有上边框
  225. if (row == topPiece.row + 1 && col == topPiece.col) {
  226. return false;
  227. }
  228. return true;
  229. }
  230. // 是否有右边框
  231. bool get _hasRightBorder {
  232. int rightCurRow = curRow;
  233. int rightCurCol = curCol + 1;
  234. // 已经是最右边的宫格, 需要右边框
  235. if (rightCurCol >= cols) return true;
  236. // 获取本碎片当前的右边邻居
  237. final rightPiece = board.getPieceByCoordinate(rightCurRow, rightCurCol);
  238. if (rightPiece == null) {
  239. _log.warning('找不到 ${toString()} 的右邻居,有错误发生,请检查');
  240. return true;
  241. }
  242. // 如果与右邻居的相对位置是正确的,那么没有右边框
  243. if (row == rightPiece.row && col == rightPiece.col - 1) {
  244. return false;
  245. }
  246. return true;
  247. }
  248. // 是否有下边框
  249. bool get _hasBottomBorder {
  250. int bottomCurRow = curRow + 1;
  251. int bottomCurCol = curCol;
  252. // 已经是底部的宫格, 需要下边框
  253. if (bottomCurRow >= rows) return true;
  254. // 获取本碎片当前的底边邻居
  255. final bottomPiece = board.getPieceByCoordinate(bottomCurRow, bottomCurCol);
  256. if (bottomPiece == null) {
  257. _log.warning('找不到 ${toString()} 的下邻居,有错误发生,请检查');
  258. return true;
  259. }
  260. // 如果与下邻居的相对位置是正确的,那么没有下边框
  261. if (row == bottomPiece.row - 1 && col == bottomPiece.col) {
  262. return false;
  263. }
  264. return true;
  265. }
  266. // 是否有左边框
  267. bool get _hasLeftBorder {
  268. int leftCurRow = curRow;
  269. int leftCurCol = curCol - 1;
  270. // 已经是最左部的宫格, 需要左边框
  271. if (leftCurCol < 0) return true;
  272. // 获取本碎片当前的左边邻居
  273. final leftPiece = board.getPieceByCoordinate(leftCurRow, leftCurCol);
  274. if (leftPiece == null) {
  275. _log.warning('找不到 ${toString()} 的左邻居,有错误发生,请检查');
  276. return true;
  277. }
  278. // 如果与下邻居的相对位置是正确的,那么没有下边框
  279. if (row == leftPiece.row && col == leftPiece.col + 1) {
  280. return false;
  281. }
  282. return true;
  283. }
  284. // 生成clip path
  285. Path _generateClipPath(double w, double h, List<bool> borders, double radius) {
  286. Path path = Path();
  287. // 一个角是否为圆角取决于相邻的两条边是否都需要绘制 (即 borders 都为 true)
  288. final bool tlRounded = borders[0] && borders[3]; // Top && Left
  289. final bool trRounded = borders[0] && borders[1]; // Top && Right
  290. final bool brRounded = borders[2] && borders[1]; // Bottom && Right
  291. final bool blRounded = borders[2] && borders[3]; // Bottom && Left
  292. // 1. 移动到起点 (左上角,T 边直线段的起点)
  293. if (tlRounded) {
  294. path.moveTo(radius, 0);
  295. } else {
  296. path.moveTo(0, 0); // 尖角起点
  297. }
  298. // A. 顶边 (T) - LineTo
  299. // 终点是 TR 圆角的起点 (w - radius, 0) 或 TR 尖角 (w, 0)
  300. if (trRounded) {
  301. path.lineTo(w - radius, 0);
  302. } else {
  303. path.lineTo(w, 0);
  304. }
  305. // B. 右上角 (TR) - Arc
  306. if (trRounded) {
  307. path.arcToPoint(
  308. Offset(w, radius), // 终点 (w, radius)
  309. radius: Radius.circular(radius),
  310. clockwise: true, // 逆时针
  311. );
  312. }
  313. // C. 右边 (R) - LineTo
  314. // 终点是 BR 圆角的起点 (w, h - radius) 或 BR 尖角 (w, h)
  315. if (brRounded) {
  316. path.lineTo(w, h - radius);
  317. } else {
  318. path.lineTo(w, h);
  319. }
  320. // D. 右下角 (BR) - Arc
  321. if (brRounded) {
  322. path.arcToPoint(
  323. Offset(w - radius, h), // 终点 (w - radius, h)
  324. radius: Radius.circular(radius),
  325. clockwise: true, // 逆时针
  326. );
  327. }
  328. // E. 底边 (B) - LineTo
  329. // 终点是 BL 圆角的起点 (radius, h) 或 BL 尖角 (0, h)
  330. if (blRounded) {
  331. path.lineTo(radius, h);
  332. } else {
  333. path.lineTo(0, h);
  334. }
  335. // F. 左下角 (BL) - Arc
  336. if (blRounded) {
  337. path.arcToPoint(
  338. Offset(0, h - radius), // 终点 (0, h - radius)
  339. radius: Radius.circular(radius),
  340. clockwise: true, // 逆时针
  341. );
  342. }
  343. // G. 左边 (L) - LineTo
  344. // 终点是 TL 圆角的起点 (0, radius) 或 TL 尖角 (0, 0)
  345. if (tlRounded) {
  346. path.lineTo(0, radius);
  347. } else {
  348. path.lineTo(0, 0);
  349. }
  350. // H. 左上角 (TL) - Arc & Close
  351. if (tlRounded) {
  352. path.arcToPoint(
  353. Offset(radius, 0), // 终点 (radius, 0),即起点
  354. radius: Radius.circular(radius),
  355. clockwise: true, // 逆时针
  356. );
  357. }
  358. // path.close() 确保路径闭合,但上面的逻辑已经把路径连回了起点。
  359. path.close();
  360. return path;
  361. }
  362. // 生成border path (重构为开放路径,仅绘制需要的边)
  363. Path _generateBorderPath(double w, double h, List<bool> borders, double radius, double offset) {
  364. Path path = Path();
  365. final double r = radius;
  366. // 边界调整后的坐标
  367. final double x0 = offset; // 左边界
  368. final double y0 = offset; // 上边界
  369. final double x1 = w - offset; // 右边界
  370. final double y1 = h - offset; // 下边界
  371. // 关键点坐标 (圆角弧的起点/终点)
  372. final Offset pTL_T = Offset(x0 + r, y0); // Top边起点
  373. final Offset pTR_T = Offset(x1 - r, y0); // Top边终点
  374. final Offset pTR_R = Offset(x1, y0 + r); // Right边起点
  375. final Offset pBR_R = Offset(x1, y1 - r); // Right边终点
  376. final Offset pBR_B = Offset(x1 - r, y1); // Bottom边起点
  377. final Offset pBL_B = Offset(x0 + r, y1); // Bottom边终点
  378. final Offset pBL_L = Offset(x0, y1 - r); // Left边起点
  379. final Offset pTL_L = Offset(x0, y0 + r); // Left边终点
  380. // [Top, Right, Bottom, Left]
  381. final bool hasT = borders[0];
  382. final bool hasR = borders[1];
  383. final bool hasB = borders[2];
  384. final bool hasL = borders[3];
  385. // --- 1. Top Border (T) ---
  386. if (hasT) {
  387. // 检查TL角是否是尖角 (Left边缺失)
  388. if (hasL) {
  389. path.moveTo(pTL_T.dx, pTL_T.dy); // 从TL弧的终点开始
  390. } else {
  391. path.moveTo(x0, y0); // 从左上尖角开始
  392. }
  393. // 绘制 Top 直线段
  394. if (hasR) {
  395. path.lineTo(pTR_T.dx, pTR_T.dy);
  396. } else {
  397. path.lineTo(x1, y0); // 到右上尖角
  398. }
  399. }
  400. // --- 2. Top-Right Corner (TR) & Right Border (R) ---
  401. if (hasT && hasR) {
  402. // 绘制 TR 弧
  403. path.arcToPoint(pTR_R, radius: Radius.circular(r), clockwise: true);
  404. } else if (hasR) {
  405. // T 缺失,R 存在:需要开始一个新的轮廓,从 TR 弧的起点开始 (pTR_R)
  406. if (hasT) {
  407. path.moveTo(pTR_R.dx, pTR_R.dy); // 从 TR 弧终点开始
  408. } else {
  409. path.moveTo(x1, y0); // 从右上尖角开始
  410. }
  411. }
  412. if (hasR) {
  413. // 绘制 Right 直线段
  414. if (hasB) {
  415. path.lineTo(pBR_R.dx, pBR_R.dy);
  416. } else {
  417. path.lineTo(x1, y1); // 到右下尖角
  418. }
  419. }
  420. // --- 3. Bottom-Right Corner (BR) & Bottom Border (B) ---
  421. if (hasR && hasB) {
  422. // 绘制 BR 弧
  423. path.arcToPoint(pBR_B, radius: Radius.circular(r), clockwise: true);
  424. } else if (hasB) {
  425. // R 缺失,B 存在:需要开始一个新的轮廓,从 BR 弧的起点开始 (pBR_B)
  426. if (hasR) {
  427. path.moveTo(pBR_B.dx, pBR_B.dy); // 从 BR 弧终点开始
  428. } else {
  429. path.moveTo(x1, y1); // 从右下尖角开始
  430. }
  431. }
  432. if (hasB) {
  433. // 绘制 Bottom 直线段
  434. if (hasL) {
  435. path.lineTo(pBL_B.dx, pBL_B.dy);
  436. } else {
  437. path.lineTo(x0, y1); // 到左下尖角
  438. }
  439. }
  440. // --- 4. Bottom-Left Corner (BL) & Left Border (L) ---
  441. if (hasB && hasL) {
  442. // 绘制 BL 弧
  443. path.arcToPoint(pBL_L, radius: Radius.circular(r), clockwise: true);
  444. } else if (hasL) {
  445. // B 缺失,L 存在:需要开始一个新的轮廓,从 BL 弧的起点开始 (pBL_L)
  446. if (hasB) {
  447. path.moveTo(pBL_L.dx, pBL_L.dy); // 从 BL 弧终点开始
  448. } else {
  449. path.moveTo(x0, y1); // 从左下尖角开始
  450. }
  451. }
  452. if (hasL) {
  453. // 绘制 Left 直线段
  454. if (hasT) {
  455. path.lineTo(pTL_L.dx, pTL_L.dy);
  456. } else {
  457. path.lineTo(x0, y0); // 到左上尖角
  458. }
  459. }
  460. // --- 5. Top-Left Corner (TL) ---
  461. if (hasL && hasT) {
  462. // 绘制 TL 弧,连接回 Top Border 的起点
  463. path.arcToPoint(pTL_T, radius: Radius.circular(r), clockwise: true);
  464. }
  465. // 注意: 不调用 path.close(),保持路径开放。
  466. return path;
  467. }
  468. // 生成碎片的clipPath, innerLinePath, outLinePath
  469. List<Path> generatePaths() {
  470. // _log.info('${toString()} generatePaths');
  471. // 先确定4条边的状态[Top, Right, Bottom, Left]
  472. List<bool> borders = [true, true, true, true];
  473. // 如果是单个碎片,4条边都需要,只有piece在group中才需要判断
  474. if (group != null) {
  475. borders = [_hasTopBorder, _hasRightBorder, _hasBottomBorder, _hasLeftBorder];
  476. }
  477. path = _generateClipPath(width, height, borders, _cornerRadius);
  478. outLinePath = _generateBorderPath(width, height, borders, _cornerRadius, _outLineOffset);
  479. innerLinePath = _generateBorderPath(width, height, borders, _cornerRadius, _innerLineOffset);
  480. return [path!, outLinePath!, innerLinePath!];
  481. }
  482. }