| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575 |
- // piece.dart
- import 'dart:math';
- import 'package:flutter/material.dart';
- import 'package:image_puzzle/play/board.dart';
- import 'package:logging/logging.dart';
- import 'package:vector_math/vector_math.dart' as vmath;
- final Logger _log = Logger('piece.dart');
- // piece分组数据结构
- class PieceGroup {
- List<Piece> pieces = [];
- int get length => pieces.length;
- void add(Piece piece) {
- if (!pieces.contains(piece)) {
- pieces.add(piece);
- }
- piece.group = this;
- }
- void remove(Piece piece) {
- if (pieces.contains(piece)) {
- pieces.remove(piece);
- }
- piece.group = null;
- }
- bool contains(Piece piece) {
- return pieces.contains(piece);
- }
- // 判断本group是否包含other group
- bool containsGroup(PieceGroup otherGroup) {
- for (var p in otherGroup.pieces) {
- if (!pieces.contains(p)) {
- return false;
- }
- }
- return true;
- }
- // 群组中心点
- Offset get center {
- if (pieces.isEmpty) return Offset.zero;
- final board = pieces[0].board;
- // 计算群组在Canvas中的边界框
- double minX = double.infinity;
- double minY = double.infinity;
- double maxX = -double.infinity;
- double maxY = -double.infinity;
- for (var piece in pieces) {
- final transform = piece.transform;
- final x = transform.storage[12]; // 碎片左上角x
- final y = transform.storage[13]; // 碎片左上角y
- final w = board.pieceLogicalWidth;
- final h = board.pieceLogicalHeight;
- // 更新边界
- minX = min(minX, x);
- minY = min(minY, y);
- maxX = max(maxX, x + w);
- maxY = max(maxY, y + h);
- }
- // 计算边界框中心点
- return Offset((minX + maxX) / 2, (minY + maxY) / 2);
- }
- void print() {
- String str = '======= group size: $length =======\n';
- for (var p in pieces) {
- str += p.toString();
- str += '\n';
- }
- _log.info(str);
- }
- }
- // 碎片基础数据结构
- class Piece {
- final int index;
- final Board board; // 保存board的引用
- PieceGroup? group;
- // 总计的行数和列数
- final int rows;
- final int cols;
- // 碎片在完整图片中的行/列位置 (逻辑坐标)
- final int row;
- final int col;
- // 碎片当前所处的位置
- int curCol;
- int curRow;
- // 正确目标位置的左上角坐标 (在 Canvas 坐标系内,这是碎片的最终位置)
- final Offset correctOffset;
- // 碎片的当前几何变换状态 (包括位置、旋转等)
- vmath.Matrix4 transform;
- // 碎片在原图片中的裁剪矩形 (Source Rect of the image)
- final Rect sourceRect;
- // 5. 碎片周围四个边的拼接状态 (用于动态绘制内部边框)
- // [Top, Right, Bottom, Left]
- List<bool> borders; // 拟废弃,采用实时计算
- bool get isOK => row == curRow && col == curCol;
- // clipPath, 碎片的裁剪路径(闭合), 这是为了绘制出圆角效果
- Path? path;
- // 内边框path(可能非闭合)
- Path? innerLinePath;
- // 外边框path (可能非闭合)
- Path? outLinePath;
- static const double _cornerRadius = 8.0;
- static const double _outLineOffset = 0.5;
- static const double _innerLineOffset = 1.5;
- Piece({
- required this.board,
- required this.index,
- required this.row,
- required this.col,
- required this.rows,
- required this.cols,
- required this.correctOffset,
- required this.sourceRect,
- required this.curCol,
- required this.curRow,
- required this.transform,
- required this.borders, // 初始设置为 [true, true, true, true]
- });
- @override
- String toString() {
- return 'Piece($index,$row:$col)';
- }
- double get width => board.pieceLogicalWidth;
- double get height => board.pieceLogicalHeight;
- // 辅助函数:获取碎片当前的左上角位置 (tx, ty)
- // 平移分量在 Matrix4.storage 的索引 12 (tx) 和 13 (ty)
- Offset get currentOffset => Offset(transform.storage[12], transform.storage[13]);
- // 辅助函数:获取碎片当前的中心点 (在 Canvas 坐标系中)
- Offset get currentCenter {
- // 碎片的本地中心点
- final Offset pieceLocalCenter = Offset(board.pieceLogicalWidth / 2, board.pieceLogicalHeight / 2);
- // 应用当前变换矩阵
- final vmath.Vector4 transformedVector = transform.transform(vmath.Vector4(pieceLocalCenter.dx, pieceLocalCenter.dy, 0.0, 1.0));
- return Offset(transformedVector.x, transformedVector.y);
- }
- // 辅助函数:将碎片移动指定的位移量
- void applyDelta(Offset delta) {
- // 累加 x 轴位移
- transform.storage[12] += delta.dx;
- // 累加 y 轴位移
- transform.storage[13] += delta.dy;
- }
- // 归位, 回到原来的位置
- void revert() {
- final originalTransform = board.getTransformByCoordinate(curRow, curCol);
- transform = originalTransform;
- }
- // 判断当前piece是否可以安置到other的槽位去
- bool canPlaceTo(Piece other) {
- // 1. 如果当前碎片是单独移动,则可以安置
- if (group == null) {
- return true;
- }
- // 当前 piece 与 other piece 的位移
- int dr = other.curRow - curRow;
- int dc = other.curCol - curCol;
- // 判断当前piece的组成员,产生相同的位移后会不会溢出
- for (var p in group!.pieces) {
- int newRow = p.curRow + dr;
- int newCol = p.curCol + dc;
- if (newRow < 0 || newRow >= rows || newCol < 0 || newCol >= cols) {
- return false;
- }
- }
- return true;
- }
- // 判断当前piece是否可以和other piece 合并
- bool canMerge(Piece other) {
- // 原来是邻居, 现在也是邻居,才具备可以合并的基础条件
- if (isNeighbour(other) && isCurNeighbour(other)) {
- // 判断相对位置是否保持一致 (防止错位拼接)
- if (col == other.col) {
- // 同一列 (上/下相邻)
- if ((row - other.row) == (curRow - other.curRow)) return true;
- } else if (row == other.row) {
- // 同一行 (左/右相邻)
- if ((col - other.col) == (curCol - other.curCol)) return true;
- }
- }
- return false;
- }
- /// 创建一个新组, 合并两个piece组
- void groupWith(Piece other) {
- PieceGroup finalGroup = PieceGroup();
- List<Piece> list = [];
- if (group != null) list.addAll(group!.pieces);
- if (other.group != null) list.addAll(other.group!.pieces);
- list.add(this);
- list.add(other);
- for (var p in list) {
- finalGroup.add(p);
- }
- }
- // 获取碎片本来的邻居(原正确位置上的邻居)
- List<int> getNeighbourIndexes() {
- List<int> list = [];
- if (row > 0) list.add(index - cols); // 上
- if (row < (rows - 1)) list.add(index + cols); // 下
- if (col > 0) list.add(index - 1); // 左
- if (col < (cols - 1)) list.add(index + 1); // 右
- return list;
- }
- /// 是否同一个组
- bool isSameGroup(Piece other) {
- return group != null && group == other.group;
- }
- /// 是否邻居 (原始位置)
- bool isNeighbour(Piece other) {
- return row == other.row && (col - other.col).abs() == 1 || col == other.col && (row - other.row).abs() == 1;
- }
- // 当前位置是否是邻居
- bool isCurNeighbour(Piece other) {
- return curRow == other.curRow && (curCol - other.curCol).abs() == 1 || curCol == other.curCol && (curRow - other.curRow).abs() == 1;
- }
- // 是否有上边框
- bool get _hasTopBorder {
- int topCurRow = curRow - 1;
- int topCurCol = curCol;
- // 已经是顶部的宫格, 需要上边框
- if (topCurRow < 0) return true;
- // 获取本碎片当前的上边邻居
- final topPiece = board.getPieceByCoordinate(topCurRow, topCurCol);
- if (topPiece == null) {
- _log.warning('找不到 ${toString()} 的上邻居,有错误发生,请检查');
- return true;
- }
- // 如果与上邻居的相对位置是正确的,那么没有上边框
- if (row == topPiece.row + 1 && col == topPiece.col) {
- return false;
- }
- return true;
- }
- // 是否有右边框
- bool get _hasRightBorder {
- int rightCurRow = curRow;
- int rightCurCol = curCol + 1;
- // 已经是最右边的宫格, 需要右边框
- if (rightCurCol >= cols) return true;
- // 获取本碎片当前的右边邻居
- final rightPiece = board.getPieceByCoordinate(rightCurRow, rightCurCol);
- if (rightPiece == null) {
- _log.warning('找不到 ${toString()} 的右邻居,有错误发生,请检查');
- return true;
- }
- // 如果与右邻居的相对位置是正确的,那么没有右边框
- if (row == rightPiece.row && col == rightPiece.col - 1) {
- return false;
- }
- return true;
- }
- // 是否有下边框
- bool get _hasBottomBorder {
- int bottomCurRow = curRow + 1;
- int bottomCurCol = curCol;
- // 已经是底部的宫格, 需要下边框
- if (bottomCurRow >= rows) return true;
- // 获取本碎片当前的底边邻居
- final bottomPiece = board.getPieceByCoordinate(bottomCurRow, bottomCurCol);
- if (bottomPiece == null) {
- _log.warning('找不到 ${toString()} 的下邻居,有错误发生,请检查');
- return true;
- }
- // 如果与下邻居的相对位置是正确的,那么没有下边框
- if (row == bottomPiece.row - 1 && col == bottomPiece.col) {
- return false;
- }
- return true;
- }
- // 是否有左边框
- bool get _hasLeftBorder {
- int leftCurRow = curRow;
- int leftCurCol = curCol - 1;
- // 已经是最左部的宫格, 需要左边框
- if (leftCurCol < 0) return true;
- // 获取本碎片当前的左边邻居
- final leftPiece = board.getPieceByCoordinate(leftCurRow, leftCurCol);
- if (leftPiece == null) {
- _log.warning('找不到 ${toString()} 的左邻居,有错误发生,请检查');
- return true;
- }
- // 如果与下邻居的相对位置是正确的,那么没有下边框
- if (row == leftPiece.row && col == leftPiece.col + 1) {
- return false;
- }
- return true;
- }
- // 生成clip path
- Path _generateClipPath(double w, double h, List<bool> borders, double radius) {
- Path path = Path();
- // 一个角是否为圆角取决于相邻的两条边是否都需要绘制 (即 borders 都为 true)
- final bool tlRounded = borders[0] && borders[3]; // Top && Left
- final bool trRounded = borders[0] && borders[1]; // Top && Right
- final bool brRounded = borders[2] && borders[1]; // Bottom && Right
- final bool blRounded = borders[2] && borders[3]; // Bottom && Left
- // 1. 移动到起点 (左上角,T 边直线段的起点)
- if (tlRounded) {
- path.moveTo(radius, 0);
- } else {
- path.moveTo(0, 0); // 尖角起点
- }
- // A. 顶边 (T) - LineTo
- // 终点是 TR 圆角的起点 (w - radius, 0) 或 TR 尖角 (w, 0)
- if (trRounded) {
- path.lineTo(w - radius, 0);
- } else {
- path.lineTo(w, 0);
- }
- // B. 右上角 (TR) - Arc
- if (trRounded) {
- path.arcToPoint(
- Offset(w, radius), // 终点 (w, radius)
- radius: Radius.circular(radius),
- clockwise: true, // 逆时针
- );
- }
- // C. 右边 (R) - LineTo
- // 终点是 BR 圆角的起点 (w, h - radius) 或 BR 尖角 (w, h)
- if (brRounded) {
- path.lineTo(w, h - radius);
- } else {
- path.lineTo(w, h);
- }
- // D. 右下角 (BR) - Arc
- if (brRounded) {
- path.arcToPoint(
- Offset(w - radius, h), // 终点 (w - radius, h)
- radius: Radius.circular(radius),
- clockwise: true, // 逆时针
- );
- }
- // E. 底边 (B) - LineTo
- // 终点是 BL 圆角的起点 (radius, h) 或 BL 尖角 (0, h)
- if (blRounded) {
- path.lineTo(radius, h);
- } else {
- path.lineTo(0, h);
- }
- // F. 左下角 (BL) - Arc
- if (blRounded) {
- path.arcToPoint(
- Offset(0, h - radius), // 终点 (0, h - radius)
- radius: Radius.circular(radius),
- clockwise: true, // 逆时针
- );
- }
- // G. 左边 (L) - LineTo
- // 终点是 TL 圆角的起点 (0, radius) 或 TL 尖角 (0, 0)
- if (tlRounded) {
- path.lineTo(0, radius);
- } else {
- path.lineTo(0, 0);
- }
- // H. 左上角 (TL) - Arc & Close
- if (tlRounded) {
- path.arcToPoint(
- Offset(radius, 0), // 终点 (radius, 0),即起点
- radius: Radius.circular(radius),
- clockwise: true, // 逆时针
- );
- }
- // path.close() 确保路径闭合,但上面的逻辑已经把路径连回了起点。
- path.close();
- return path;
- }
- // 生成border path (重构为开放路径,仅绘制需要的边)
- Path _generateBorderPath(double w, double h, List<bool> borders, double radius, double offset) {
- Path path = Path();
- final double r = radius;
- // 边界调整后的坐标
- final double x0 = offset; // 左边界
- final double y0 = offset; // 上边界
- final double x1 = w - offset; // 右边界
- final double y1 = h - offset; // 下边界
- // 关键点坐标 (圆角弧的起点/终点)
- final Offset pTL_T = Offset(x0 + r, y0); // Top边起点
- final Offset pTR_T = Offset(x1 - r, y0); // Top边终点
- final Offset pTR_R = Offset(x1, y0 + r); // Right边起点
- final Offset pBR_R = Offset(x1, y1 - r); // Right边终点
- final Offset pBR_B = Offset(x1 - r, y1); // Bottom边起点
- final Offset pBL_B = Offset(x0 + r, y1); // Bottom边终点
- final Offset pBL_L = Offset(x0, y1 - r); // Left边起点
- final Offset pTL_L = Offset(x0, y0 + r); // Left边终点
- // [Top, Right, Bottom, Left]
- final bool hasT = borders[0];
- final bool hasR = borders[1];
- final bool hasB = borders[2];
- final bool hasL = borders[3];
- // --- 1. Top Border (T) ---
- if (hasT) {
- // 检查TL角是否是尖角 (Left边缺失)
- if (hasL) {
- path.moveTo(pTL_T.dx, pTL_T.dy); // 从TL弧的终点开始
- } else {
- path.moveTo(x0, y0); // 从左上尖角开始
- }
- // 绘制 Top 直线段
- if (hasR) {
- path.lineTo(pTR_T.dx, pTR_T.dy);
- } else {
- path.lineTo(x1, y0); // 到右上尖角
- }
- }
- // --- 2. Top-Right Corner (TR) & Right Border (R) ---
- if (hasT && hasR) {
- // 绘制 TR 弧
- path.arcToPoint(pTR_R, radius: Radius.circular(r), clockwise: true);
- } else if (hasR) {
- // T 缺失,R 存在:需要开始一个新的轮廓,从 TR 弧的起点开始 (pTR_R)
- if (hasT) {
- path.moveTo(pTR_R.dx, pTR_R.dy); // 从 TR 弧终点开始
- } else {
- path.moveTo(x1, y0); // 从右上尖角开始
- }
- }
- if (hasR) {
- // 绘制 Right 直线段
- if (hasB) {
- path.lineTo(pBR_R.dx, pBR_R.dy);
- } else {
- path.lineTo(x1, y1); // 到右下尖角
- }
- }
- // --- 3. Bottom-Right Corner (BR) & Bottom Border (B) ---
- if (hasR && hasB) {
- // 绘制 BR 弧
- path.arcToPoint(pBR_B, radius: Radius.circular(r), clockwise: true);
- } else if (hasB) {
- // R 缺失,B 存在:需要开始一个新的轮廓,从 BR 弧的起点开始 (pBR_B)
- if (hasR) {
- path.moveTo(pBR_B.dx, pBR_B.dy); // 从 BR 弧终点开始
- } else {
- path.moveTo(x1, y1); // 从右下尖角开始
- }
- }
- if (hasB) {
- // 绘制 Bottom 直线段
- if (hasL) {
- path.lineTo(pBL_B.dx, pBL_B.dy);
- } else {
- path.lineTo(x0, y1); // 到左下尖角
- }
- }
- // --- 4. Bottom-Left Corner (BL) & Left Border (L) ---
- if (hasB && hasL) {
- // 绘制 BL 弧
- path.arcToPoint(pBL_L, radius: Radius.circular(r), clockwise: true);
- } else if (hasL) {
- // B 缺失,L 存在:需要开始一个新的轮廓,从 BL 弧的起点开始 (pBL_L)
- if (hasB) {
- path.moveTo(pBL_L.dx, pBL_L.dy); // 从 BL 弧终点开始
- } else {
- path.moveTo(x0, y1); // 从左下尖角开始
- }
- }
- if (hasL) {
- // 绘制 Left 直线段
- if (hasT) {
- path.lineTo(pTL_L.dx, pTL_L.dy);
- } else {
- path.lineTo(x0, y0); // 到左上尖角
- }
- }
- // --- 5. Top-Left Corner (TL) ---
- if (hasL && hasT) {
- // 绘制 TL 弧,连接回 Top Border 的起点
- path.arcToPoint(pTL_T, radius: Radius.circular(r), clockwise: true);
- }
- // 注意: 不调用 path.close(),保持路径开放。
- return path;
- }
- // 生成碎片的clipPath, innerLinePath, outLinePath
- List<Path> generatePaths() {
- // _log.info('${toString()} generatePaths');
- // 先确定4条边的状态[Top, Right, Bottom, Left]
- List<bool> borders = [true, true, true, true];
- // 如果是单个碎片,4条边都需要,只有piece在group中才需要判断
- if (group != null) {
- borders = [_hasTopBorder, _hasRightBorder, _hasBottomBorder, _hasLeftBorder];
- }
- path = _generateClipPath(width, height, borders, _cornerRadius);
- outLinePath = _generateBorderPath(width, height, borders, _cornerRadius, _outLineOffset);
- innerLinePath = _generateBorderPath(width, height, borders, _cornerRadius, _innerLineOffset);
- return [path!, outLinePath!, innerLinePath!];
- }
- }
|