|
|
@@ -9,17 +9,28 @@ import 'package:vector_math/vector_math.dart' as vmath;
|
|
|
|
|
|
final Logger _log = Logger('piece.dart');
|
|
|
|
|
|
+const double _cornerRadius = 8.0;
|
|
|
+const double _outLineOffset = 0.5;
|
|
|
+const double _innerLineOffset = 1.5;
|
|
|
+
|
|
|
// piece分组数据结构
|
|
|
class PieceGroup {
|
|
|
List<Piece> pieces = [];
|
|
|
|
|
|
int get length => pieces.length;
|
|
|
|
|
|
+ // 群组的组合路径缓存 (路径是相对坐标,相对于群组基准点)
|
|
|
+ Path? groupClipPath;
|
|
|
+ Path? groupOutLinePath;
|
|
|
+ Path? groupInnerLinePath;
|
|
|
+
|
|
|
void add(Piece piece) {
|
|
|
if (!pieces.contains(piece)) {
|
|
|
pieces.add(piece);
|
|
|
}
|
|
|
+ // 确保 piece 的 group 指向自己
|
|
|
piece.group = this;
|
|
|
+ clearPathCache(); // 群组变化,清空缓存
|
|
|
}
|
|
|
|
|
|
void remove(Piece piece) {
|
|
|
@@ -27,6 +38,7 @@ class PieceGroup {
|
|
|
pieces.remove(piece);
|
|
|
}
|
|
|
piece.group = null;
|
|
|
+ clearPathCache(); // 群组变化,清空缓存
|
|
|
}
|
|
|
|
|
|
bool contains(Piece piece) {
|
|
|
@@ -43,26 +55,28 @@ class PieceGroup {
|
|
|
return true;
|
|
|
}
|
|
|
|
|
|
- // 群组中心点
|
|
|
+ // 网格坐标边界计算辅助属性
|
|
|
+ int get _minR => pieces.map((p) => p.curRow).reduce(min);
|
|
|
+ int get _maxR => pieces.map((p) => p.curRow).reduce(max);
|
|
|
+ int get _minC => pieces.map((p) => p.curCol).reduce(min);
|
|
|
+ int get _maxC => pieces.map((p) => p.curCol).reduce(max);
|
|
|
+ // -----------------------------
|
|
|
+
|
|
|
+ // 群组中心点(绝对坐标)
|
|
|
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;
|
|
|
+ double minX = double.infinity, minY = double.infinity;
|
|
|
+ double maxX = -double.infinity, maxY = -double.infinity;
|
|
|
|
|
|
for (var piece in pieces) {
|
|
|
- final transform = piece.transform;
|
|
|
- final x = transform.storage[12]; // 碎片左上角x
|
|
|
- final y = transform.storage[13]; // 碎片左上角y
|
|
|
+ // 碎片当前在 Canvas 上的左上角坐标
|
|
|
+ final x = piece.transform.storage[12];
|
|
|
+ final y = piece.transform.storage[13];
|
|
|
final w = board.pieceLogicalWidth;
|
|
|
final h = board.pieceLogicalHeight;
|
|
|
|
|
|
- // 更新边界
|
|
|
minX = min(minX, x);
|
|
|
minY = min(minY, y);
|
|
|
maxX = max(maxX, x + w);
|
|
|
@@ -73,6 +87,416 @@ class PieceGroup {
|
|
|
return Offset((minX + maxX) / 2, (minY + maxY) / 2);
|
|
|
}
|
|
|
|
|
|
+ // 计算群组的左上角碎片(以 curRow/curCol 为准)
|
|
|
+ Piece get topLeftPiece {
|
|
|
+ if (pieces.isEmpty) throw Exception("Group is empty");
|
|
|
+
|
|
|
+ // 找到最上面,然后最左边的格子作为起点
|
|
|
+ return pieces.reduce((a, b) {
|
|
|
+ if (a.curRow < b.curRow) return a;
|
|
|
+ if (b.curRow < a.curRow) return b;
|
|
|
+ if (a.curCol < b.curCol) return a;
|
|
|
+ return b;
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ // --- 新增辅助方法:用于不规则群组路径追踪 ---
|
|
|
+
|
|
|
+ // 检查指定网格坐标 (r, c) 是否有碎片属于本群组
|
|
|
+ // r, c 是绝对网格坐标 (curRow, curCol)
|
|
|
+ bool _pieceAtCurCoord(int r, int c) {
|
|
|
+ if (r < 0 || c < 0) return false;
|
|
|
+
|
|
|
+ // 只需要检查群组内的碎片
|
|
|
+ for (var piece in pieces) {
|
|
|
+ if (piece.curRow == r && piece.curCol == c) {
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 检查 (r, c) 单元格的指定边是否是群组的外部边界
|
|
|
+ // r, c 是绝对网格坐标 (curRow, curCol)
|
|
|
+ bool _isExternalGroupBoundary(int r, int c, String side) {
|
|
|
+ if (!_pieceAtCurCoord(r, c)) {
|
|
|
+ return false; // 该坐标没有碎片属于这个群组,不可能是边界
|
|
|
+ }
|
|
|
+
|
|
|
+ final board = pieces[0].board;
|
|
|
+ switch (side) {
|
|
|
+ case 'top':
|
|
|
+ if (r == 0) return true; // Board top
|
|
|
+ return !_pieceAtCurCoord(r - 1, c); // 上邻居不在群组内
|
|
|
+ case 'right':
|
|
|
+ if (c == board.cols - 1) return true; // Board right
|
|
|
+ return !_pieceAtCurCoord(r, c + 1); // 右邻居不在群组内
|
|
|
+ case 'bottom':
|
|
|
+ if (r == board.rows - 1) return true; // Board bottom
|
|
|
+ return !_pieceAtCurCoord(r + 1, c); // 下邻居不在群组内
|
|
|
+ case 'left':
|
|
|
+ if (c == 0) return true; // Board left
|
|
|
+ return !_pieceAtCurCoord(r, c - 1); // 左邻居不在群组内
|
|
|
+ default:
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 生成群组整体路径(裁剪/边框的核心逻辑)
|
|
|
+ Path _generateIrregularGroupPath({required bool isClipPath, double offset = 0}) {
|
|
|
+ if (pieces.isEmpty) return Path();
|
|
|
+
|
|
|
+ final path = Path();
|
|
|
+ final board = pieces[0].board;
|
|
|
+ final w = board.pieceLogicalWidth;
|
|
|
+ final h = board.pieceLogicalHeight;
|
|
|
+ final radius = _cornerRadius;
|
|
|
+
|
|
|
+ final r = radius; // 使用原始半径,偏移量通过坐标实现
|
|
|
+
|
|
|
+ // 1. 确定起始 Piece 和起始点
|
|
|
+ Piece startPiece = topLeftPiece;
|
|
|
+ Piece loopPiece = startPiece;
|
|
|
+
|
|
|
+ // 群组本地坐标系下的起始点 (top-left corner of the top-left piece)
|
|
|
+ final startRelX = (startPiece.curCol - _minC) * w;
|
|
|
+ final startRelY = (startPiece.curRow - _minR) * h;
|
|
|
+
|
|
|
+ // 决定起点 MoveTo 坐标 (Group Local)
|
|
|
+ double initialX = startRelX + offset;
|
|
|
+ double initialY = startRelY + offset;
|
|
|
+
|
|
|
+ // 初始方向:向右 (Tracing Top edge)
|
|
|
+ String direction = 'right';
|
|
|
+
|
|
|
+ final hasStartTop = _isExternalGroupBoundary(startPiece.curRow, startPiece.curCol, 'top');
|
|
|
+ final hasStartLeft = _isExternalGroupBoundary(startPiece.curRow, startPiece.curCol, 'left');
|
|
|
+
|
|
|
+ if (hasStartTop && hasStartLeft) {
|
|
|
+ path.moveTo(initialX + r, initialY);
|
|
|
+ initialX += r;
|
|
|
+ } else if (hasStartTop) {
|
|
|
+ path.moveTo(initialX, initialY);
|
|
|
+ } else if (hasStartLeft) {
|
|
|
+ // 这种情况意味着左边是外部边界,但上面是内部边界,路径起点在左边中央
|
|
|
+ path.moveTo(initialX, initialY + r);
|
|
|
+ initialY += r;
|
|
|
+ } else {
|
|
|
+ // TL 角是内部的,但 topLeftPiece 保证是群组最左上角,因此至少有一条边是外部边界
|
|
|
+ // 如果两边都是内部,说明群组内部有空隙,但此处假设群组是连通的。
|
|
|
+ path.moveTo(initialX, initialY);
|
|
|
+ }
|
|
|
+
|
|
|
+ // 当前路径点 (Group Local)
|
|
|
+ double currentX = initialX;
|
|
|
+ double currentY = initialY;
|
|
|
+
|
|
|
+ // Safety break
|
|
|
+ int maxIterations = pieces.length * 8; // 增加安全上限
|
|
|
+ int iteration = 0;
|
|
|
+
|
|
|
+ // --- 路径追踪循环 (Wall Follower - Keep Group on Left) ---
|
|
|
+ do {
|
|
|
+ iteration++;
|
|
|
+ if (iteration > maxIterations) {
|
|
|
+ _log.severe('Group path tracing failed to close after $maxIterations iterations. Breaking.');
|
|
|
+ break;
|
|
|
+ }
|
|
|
+
|
|
|
+ final absR = loopPiece.curRow;
|
|
|
+ final absC = loopPiece.curCol;
|
|
|
+ final relC = absC - _minC;
|
|
|
+ final relR = absR - _minR;
|
|
|
+
|
|
|
+ // 当前 Piece 的边界坐标 (Group Local) - 包含偏移
|
|
|
+ final x0 = relC * w + offset; // Left
|
|
|
+ final y0 = relR * h + offset; // Top
|
|
|
+ final x1 = (relC + 1) * w - offset; // Right
|
|
|
+ final y1 = (relR + 1) * h - offset; // Bottom
|
|
|
+
|
|
|
+ String nextDirection = '';
|
|
|
+ Piece? nextPiece;
|
|
|
+ bool isCornerRounded = false;
|
|
|
+
|
|
|
+ // 检查当前方向的边是否是外部边界
|
|
|
+ bool isExternalEdge = false;
|
|
|
+ switch (direction) {
|
|
|
+ case 'right':
|
|
|
+ isExternalEdge = _isExternalGroupBoundary(absR, absC, 'top');
|
|
|
+ break;
|
|
|
+ case 'down':
|
|
|
+ isExternalEdge = _isExternalGroupBoundary(absR, absC, 'right');
|
|
|
+ break;
|
|
|
+ case 'left':
|
|
|
+ isExternalEdge = _isExternalGroupBoundary(absR, absC, 'bottom');
|
|
|
+ break;
|
|
|
+ case 'up':
|
|
|
+ isExternalEdge = _isExternalGroupBoundary(absR, absC, 'left');
|
|
|
+ break;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (isExternalEdge) {
|
|
|
+ // --- 1. Edge Segment Drawing ---
|
|
|
+ if (direction == 'right') {
|
|
|
+ // Tracing Top edge to TR corner (x1, y0)
|
|
|
+ final hasR = _isExternalGroupBoundary(absR, absC, 'right');
|
|
|
+ isCornerRounded = hasR;
|
|
|
+ final targetX = x1;
|
|
|
+ final targetY = y0;
|
|
|
+
|
|
|
+ // LineTo segment
|
|
|
+ final lineToX = isCornerRounded ? targetX - r : targetX;
|
|
|
+ if (currentX < lineToX) {
|
|
|
+ // 避免重复 LineTo
|
|
|
+ path.lineTo(lineToX, targetY);
|
|
|
+ currentX = lineToX;
|
|
|
+ currentY = targetY;
|
|
|
+ }
|
|
|
+
|
|
|
+ // Arc for TR corner
|
|
|
+ if (isCornerRounded) {
|
|
|
+ path.arcToPoint(Offset(targetX, targetY + r), radius: Radius.circular(r), clockwise: true);
|
|
|
+ currentX = targetX;
|
|
|
+ currentY = targetY + r;
|
|
|
+ } else {
|
|
|
+ currentX = targetX;
|
|
|
+ currentY = targetY;
|
|
|
+ }
|
|
|
+
|
|
|
+ // --- 2. Decision: Turn Down or Move Right ---
|
|
|
+ nextPiece = board.getPieceByCoordinate(absR, absC + 1);
|
|
|
+ bool rightNeighborInGroup = nextPiece != null && pieces.contains(nextPiece);
|
|
|
+
|
|
|
+ if (hasR) {
|
|
|
+ // Right edge exposed -> TURN DOWN (Stay on loopPiece)
|
|
|
+ nextDirection = 'down';
|
|
|
+ } else if (rightNeighborInGroup) {
|
|
|
+ // Right edge internal -> MOVE RIGHT (Change loopPiece)
|
|
|
+ loopPiece = nextPiece!;
|
|
|
+ nextDirection = 'right';
|
|
|
+ } else {
|
|
|
+ // Board boundary or outer space (R is external) -> TURN DOWN (This case is covered by hasR=true, but as a fallback)
|
|
|
+ nextDirection = 'down';
|
|
|
+ }
|
|
|
+ } else if (direction == 'down') {
|
|
|
+ // Tracing Right edge to BR corner (x1, y1)
|
|
|
+ final hasB = _isExternalGroupBoundary(absR, absC, 'bottom');
|
|
|
+ isCornerRounded = hasB;
|
|
|
+ final targetX = x1;
|
|
|
+ final targetY = y1;
|
|
|
+
|
|
|
+ // LineTo segment
|
|
|
+ final lineToY = isCornerRounded ? targetY - r : targetY;
|
|
|
+ if (currentY < lineToY) {
|
|
|
+ path.lineTo(targetX, lineToY);
|
|
|
+ currentX = targetX;
|
|
|
+ currentY = lineToY;
|
|
|
+ }
|
|
|
+
|
|
|
+ // Arc for BR corner
|
|
|
+ if (isCornerRounded) {
|
|
|
+ path.arcToPoint(Offset(targetX - r, targetY), radius: Radius.circular(r), clockwise: true);
|
|
|
+ currentX = targetX - r;
|
|
|
+ currentY = targetY;
|
|
|
+ } else {
|
|
|
+ currentX = targetX;
|
|
|
+ currentY = targetY;
|
|
|
+ }
|
|
|
+
|
|
|
+ // --- 2. Decision: Turn Left or Move Down ---
|
|
|
+ nextPiece = board.getPieceByCoordinate(absR + 1, absC);
|
|
|
+ bool bottomNeighborInGroup = nextPiece != null && pieces.contains(nextPiece);
|
|
|
+
|
|
|
+ if (hasB) {
|
|
|
+ // Bottom edge exposed -> TURN LEFT
|
|
|
+ nextDirection = 'left';
|
|
|
+ } else if (bottomNeighborInGroup) {
|
|
|
+ // Bottom edge internal -> MOVE DOWN
|
|
|
+ loopPiece = nextPiece!;
|
|
|
+ nextDirection = 'down';
|
|
|
+ } else {
|
|
|
+ nextDirection = 'left';
|
|
|
+ }
|
|
|
+ } else if (direction == 'left') {
|
|
|
+ // Tracing Bottom edge to BL corner (x0, y1)
|
|
|
+ final hasL = _isExternalGroupBoundary(absR, absC, 'left');
|
|
|
+ isCornerRounded = hasL;
|
|
|
+ final targetX = x0;
|
|
|
+ final targetY = y1;
|
|
|
+
|
|
|
+ // LineTo segment
|
|
|
+ final lineToX = isCornerRounded ? targetX + r : targetX;
|
|
|
+ if (currentX > lineToX) {
|
|
|
+ path.lineTo(lineToX, targetY);
|
|
|
+ currentX = lineToX;
|
|
|
+ currentY = targetY;
|
|
|
+ }
|
|
|
+
|
|
|
+ // Arc for BL corner
|
|
|
+ if (isCornerRounded) {
|
|
|
+ path.arcToPoint(Offset(targetX, targetY - r), radius: Radius.circular(r), clockwise: true);
|
|
|
+ currentX = targetX;
|
|
|
+ currentY = targetY - r;
|
|
|
+ } else {
|
|
|
+ currentX = targetX;
|
|
|
+ currentY = targetY;
|
|
|
+ }
|
|
|
+
|
|
|
+ // --- 2. Decision: Turn Up or Move Left ---
|
|
|
+ nextPiece = board.getPieceByCoordinate(absR, absC - 1);
|
|
|
+ bool leftNeighborInGroup = nextPiece != null && pieces.contains(nextPiece);
|
|
|
+
|
|
|
+ if (hasL) {
|
|
|
+ // Left edge exposed -> TURN UP
|
|
|
+ nextDirection = 'up';
|
|
|
+ } else if (leftNeighborInGroup) {
|
|
|
+ // Left edge internal -> MOVE LEFT
|
|
|
+ loopPiece = nextPiece!;
|
|
|
+ nextDirection = 'left';
|
|
|
+ } else {
|
|
|
+ nextDirection = 'up';
|
|
|
+ }
|
|
|
+ } else if (direction == 'up') {
|
|
|
+ // Tracing Left edge to TL corner (x0, y0)
|
|
|
+ final hasT = _isExternalGroupBoundary(absR, absC, 'top');
|
|
|
+ isCornerRounded = hasT;
|
|
|
+ final targetX = x0;
|
|
|
+ final targetY = y0;
|
|
|
+
|
|
|
+ // LineTo segment
|
|
|
+ final lineToY = isCornerRounded ? targetY + r : targetY;
|
|
|
+ if (currentY > lineToY) {
|
|
|
+ path.lineTo(targetX, lineToY);
|
|
|
+ currentX = targetX;
|
|
|
+ currentY = lineToY;
|
|
|
+ }
|
|
|
+
|
|
|
+ // Arc for TL corner
|
|
|
+ if (isCornerRounded) {
|
|
|
+ path.arcToPoint(Offset(targetX + r, targetY), radius: Radius.circular(r), clockwise: true);
|
|
|
+ currentX = targetX + r;
|
|
|
+ currentY = targetY;
|
|
|
+ } else {
|
|
|
+ currentX = targetX;
|
|
|
+ currentY = targetY;
|
|
|
+ }
|
|
|
+
|
|
|
+ // --- 2. Decision: Turn Right or Move Up ---
|
|
|
+ nextPiece = board.getPieceByCoordinate(absR - 1, absC);
|
|
|
+ bool topNeighborInGroup = nextPiece != null && pieces.contains(nextPiece);
|
|
|
+
|
|
|
+ if (hasT) {
|
|
|
+ // Top edge exposed -> TURN RIGHT
|
|
|
+ nextDirection = 'right';
|
|
|
+ } else if (topNeighborInGroup) {
|
|
|
+ // Top edge internal -> MOVE UP
|
|
|
+ loopPiece = nextPiece!;
|
|
|
+ nextDirection = 'up';
|
|
|
+ } else {
|
|
|
+ nextDirection = 'right';
|
|
|
+ }
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ // --- Edge is Internal: Move Straight or Turn Right ---
|
|
|
+ // This piece's current edge is internal, meaning the external boundary is on the neighbor's side.
|
|
|
+ // We must check if we can move *through* the current piece to the next one in the same direction,
|
|
|
+ // or if we must turn right immediately because the straight path is blocked by an external boundary.
|
|
|
+
|
|
|
+ // Next piece to check (straight ahead)
|
|
|
+ Piece? straightPiece;
|
|
|
+ switch (direction) {
|
|
|
+ case 'right':
|
|
|
+ straightPiece = board.getPieceByCoordinate(absR, absC + 1);
|
|
|
+ break;
|
|
|
+ case 'down':
|
|
|
+ straightPiece = board.getPieceByCoordinate(absR + 1, absC);
|
|
|
+ break;
|
|
|
+ case 'left':
|
|
|
+ straightPiece = board.getPieceByCoordinate(absR, absC - 1);
|
|
|
+ break;
|
|
|
+ case 'up':
|
|
|
+ straightPiece = board.getPieceByCoordinate(absR - 1, absC);
|
|
|
+ break;
|
|
|
+ }
|
|
|
+
|
|
|
+ bool straightInGroup = straightPiece != null && pieces.contains(straightPiece);
|
|
|
+
|
|
|
+ if (straightInGroup) {
|
|
|
+ // Straight piece is in group -> MOVE STRAIGHT (Skip current internal piece)
|
|
|
+ loopPiece = straightPiece!;
|
|
|
+ nextDirection = direction;
|
|
|
+ } else {
|
|
|
+ // Straight piece is outside/null, and current edge is internal.
|
|
|
+ // This is an internal concave corner. We must turn right to follow the external boundary.
|
|
|
+ // Turn right (change direction, stay on current piece)
|
|
|
+ switch (direction) {
|
|
|
+ case 'right':
|
|
|
+ nextDirection = 'down';
|
|
|
+ break;
|
|
|
+ case 'down':
|
|
|
+ nextDirection = 'left';
|
|
|
+ break;
|
|
|
+ case 'left':
|
|
|
+ nextDirection = 'up';
|
|
|
+ break;
|
|
|
+ case 'up':
|
|
|
+ nextDirection = 'right';
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // Update direction and loopPiece for next iteration
|
|
|
+ direction = nextDirection;
|
|
|
+
|
|
|
+ // --- Termination Check ---
|
|
|
+ if (currentX == initialX && currentY == initialY) {
|
|
|
+ _log.info('Closed path at initial point. Iterations: $iteration');
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ } while (true);
|
|
|
+
|
|
|
+ if (isClipPath) {
|
|
|
+ path.close();
|
|
|
+ }
|
|
|
+
|
|
|
+ return path;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 生成群组裁剪路径(现在支持不规则形状)
|
|
|
+ Path _generateGroupClipPath() {
|
|
|
+ // 裁剪路径是封闭的,且没有偏移
|
|
|
+ return _generateIrregularGroupPath(isClipPath: true);
|
|
|
+ }
|
|
|
+
|
|
|
+ // 生成群组边框路径(现在支持不规则形状)
|
|
|
+ Path _generateGroupBorderPath(double offset) {
|
|
|
+ // 边框路径是开放的(不 close),且有偏移
|
|
|
+ return _generateIrregularGroupPath(isClipPath: false, offset: offset);
|
|
|
+ }
|
|
|
+
|
|
|
+ // 生成群组整体路径(裁剪+边框)
|
|
|
+ void generateGroupPaths() {
|
|
|
+ // 使用新的不规则路径生成逻辑
|
|
|
+ groupClipPath = _generateGroupClipPath();
|
|
|
+ groupOutLinePath = _generateGroupBorderPath(_outLineOffset);
|
|
|
+ groupInnerLinePath = _generateGroupBorderPath(_innerLineOffset);
|
|
|
+ }
|
|
|
+
|
|
|
+ // 群组整体位移(优化:避免遍历碎片)
|
|
|
+ void applyDelta(Offset delta) {
|
|
|
+ for (var piece in pieces) {
|
|
|
+ piece.transform.storage[12] += delta.dx;
|
|
|
+ piece.transform.storage[13] += delta.dy;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ void clearPathCache() {
|
|
|
+ groupClipPath = null;
|
|
|
+ groupOutLinePath = null;
|
|
|
+ groupInnerLinePath = null;
|
|
|
+ }
|
|
|
+
|
|
|
void print() {
|
|
|
String str = '======= group size: $length =======\n';
|
|
|
for (var p in pieces) {
|
|
|
@@ -87,9 +511,7 @@ class PieceGroup {
|
|
|
// 碎片基础数据结构
|
|
|
class Piece {
|
|
|
final int index;
|
|
|
-
|
|
|
- final Board board; // 保存board的引用
|
|
|
-
|
|
|
+ final Board board;
|
|
|
PieceGroup? group;
|
|
|
|
|
|
// 总计的行数和列数
|
|
|
@@ -113,10 +535,6 @@ class Piece {
|
|
|
// 碎片在原图片中的裁剪矩形 (Source Rect of the image)
|
|
|
final Rect sourceRect;
|
|
|
|
|
|
- // 5. 碎片周围四个边的拼接状态 (用于动态绘制内部边框)
|
|
|
- // [Top, Right, Bottom, Left]
|
|
|
- List<bool> borders; // 拟废弃,采用实时计算
|
|
|
-
|
|
|
bool get isOK => row == curRow && col == curCol;
|
|
|
|
|
|
// clipPath, 碎片的裁剪路径(闭合), 这是为了绘制出圆角效果
|
|
|
@@ -126,9 +544,8 @@ class Piece {
|
|
|
// 外边框path (可能非闭合)
|
|
|
Path? outLinePath;
|
|
|
|
|
|
- static const double _cornerRadius = 8.0;
|
|
|
- static const double _outLineOffset = 0.5;
|
|
|
- static const double _innerLineOffset = 1.5;
|
|
|
+ // 翻转相关属性
|
|
|
+ double flipProgress = 0.0;
|
|
|
|
|
|
Piece({
|
|
|
required this.board,
|
|
|
@@ -142,13 +559,10 @@ class Piece {
|
|
|
required this.curCol,
|
|
|
required this.curRow,
|
|
|
required this.transform,
|
|
|
- required this.borders, // 初始设置为 [true, true, true, true]
|
|
|
});
|
|
|
|
|
|
@override
|
|
|
- String toString() {
|
|
|
- return 'Piece($index,$row:$col)';
|
|
|
- }
|
|
|
+ String toString() => 'Piece($index,$row:$col)';
|
|
|
|
|
|
double get width => board.pieceLogicalWidth;
|
|
|
double get height => board.pieceLogicalHeight;
|
|
|
@@ -160,12 +574,12 @@ class Piece {
|
|
|
// 辅助函数:获取碎片当前的中心点 (在 Canvas 坐标系中)
|
|
|
Offset get currentCenter {
|
|
|
// 碎片的本地中心点
|
|
|
- final Offset pieceLocalCenter = Offset(board.pieceLogicalWidth / 2, board.pieceLogicalHeight / 2);
|
|
|
+ final Offset localCenter = Offset(width / 2, height / 2);
|
|
|
|
|
|
// 应用当前变换矩阵
|
|
|
- final vmath.Vector4 transformedVector = transform.transform(vmath.Vector4(pieceLocalCenter.dx, pieceLocalCenter.dy, 0.0, 1.0));
|
|
|
+ final vmath.Vector4 transformed = transform.transform(vmath.Vector4(localCenter.dx, localCenter.dy, 0.0, 1.0));
|
|
|
|
|
|
- return Offset(transformedVector.x, transformedVector.y);
|
|
|
+ return Offset(transformed.x, transformed.y);
|
|
|
}
|
|
|
|
|
|
// 辅助函数:将碎片移动指定的位移量
|
|
|
@@ -178,8 +592,7 @@ class Piece {
|
|
|
|
|
|
// 归位, 回到原来的位置
|
|
|
void revert() {
|
|
|
- final originalTransform = board.getTransformByCoordinate(curRow, curCol);
|
|
|
- transform = originalTransform;
|
|
|
+ transform = board.getTransformByCoordinate(curRow, curCol);
|
|
|
}
|
|
|
|
|
|
// 判断当前piece是否可以安置到other的槽位去
|
|
|
@@ -197,11 +610,8 @@ class 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;
|
|
|
- }
|
|
|
+ if (newRow < 0 || newRow >= rows || newCol < 0 || newCol >= cols) return false;
|
|
|
}
|
|
|
-
|
|
|
return true;
|
|
|
}
|
|
|
|
|
|
@@ -229,9 +639,15 @@ class Piece {
|
|
|
if (other.group != null) list.addAll(other.group!.pieces);
|
|
|
list.add(this);
|
|
|
list.add(other);
|
|
|
- for (var p in list) {
|
|
|
+
|
|
|
+ // 确保没有重复的 Piece
|
|
|
+ final Set<Piece> uniquePieces = list.toSet();
|
|
|
+
|
|
|
+ for (var p in uniquePieces) {
|
|
|
finalGroup.add(p);
|
|
|
}
|
|
|
+ // 合并后需要重新计算 group paths
|
|
|
+ finalGroup.generateGroupPaths();
|
|
|
}
|
|
|
|
|
|
// 获取碎片本来的邻居(原正确位置上的邻居)
|
|
|
@@ -244,22 +660,15 @@ class Piece {
|
|
|
return list;
|
|
|
}
|
|
|
|
|
|
- /// 是否同一个组
|
|
|
- bool isSameGroup(Piece other) {
|
|
|
- return group != null && group == other.group;
|
|
|
- }
|
|
|
+ bool isSameGroup(Piece other) => group != null && group == other.group;
|
|
|
+ bool isNeighbour(Piece other) => row == other.row && (col - other.col).abs() == 1 || col == other.col && (row - other.row).abs() == 1;
|
|
|
+ bool isCurNeighbour(Piece other) =>
|
|
|
+ curRow == other.curRow && (curCol - other.curCol).abs() == 1 || curCol == other.curCol && (curRow - other.curRow).abs() == 1;
|
|
|
|
|
|
- /// 是否邻居 (原始位置)
|
|
|
- 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;
|
|
|
@@ -269,15 +678,20 @@ class Piece {
|
|
|
|
|
|
// 获取本碎片当前的上边邻居
|
|
|
final topPiece = board.getPieceByCoordinate(topCurRow, topCurCol);
|
|
|
+
|
|
|
+ // 邻居缺失(空槽位),需要边框
|
|
|
if (topPiece == null) {
|
|
|
_log.warning('找不到 ${toString()} 的上邻居,有错误发生,请检查');
|
|
|
return true;
|
|
|
}
|
|
|
- // 如果与上邻居的相对位置是正确的,那么没有上边框
|
|
|
+
|
|
|
+ // 如果它们在当前位置相邻,且原图相对位置正确,则边界被内部吸收,不需要绘制。
|
|
|
+ // 即:当前碎片(row)在邻居碎片(topPiece.row)的下方一个位置
|
|
|
if (row == topPiece.row + 1 && col == topPiece.col) {
|
|
|
return false;
|
|
|
}
|
|
|
|
|
|
+ // 否则,需要绘制边界
|
|
|
return true;
|
|
|
}
|
|
|
|
|
|
@@ -339,7 +753,7 @@ class Piece {
|
|
|
_log.warning('找不到 ${toString()} 的左邻居,有错误发生,请检查');
|
|
|
return true;
|
|
|
}
|
|
|
- // 如果与下邻居的相对位置是正确的,那么没有下边框
|
|
|
+ // 如果与左邻居的相对位置是正确的,那么没有左边框
|
|
|
if (row == leftPiece.row && col == leftPiece.col + 1) {
|
|
|
return false;
|
|
|
}
|
|
|
@@ -347,6 +761,22 @@ class Piece {
|
|
|
return true;
|
|
|
}
|
|
|
|
|
|
+ // 生成翻转变换矩阵(结合平移)
|
|
|
+ void updateFlipTransform(double flipAngle, vmath.Matrix4 targetTranslate) {
|
|
|
+ flipProgress = flipAngle;
|
|
|
+ final flipMatrix = vmath.Matrix4.identity()
|
|
|
+ ..translate(width / 2, height / 2) // 1. 移到卡片中心(旋转中心)
|
|
|
+ ..rotateY(flipAngle) // 2. Y轴旋转(翻转动画)
|
|
|
+ ..scale(-1.0, 1.0, 1.0) // 3. X轴缩放-1:抵消旋转带来的左右镜像
|
|
|
+ ..translate(-width / 2, -height / 2); // 4. 移回原位
|
|
|
+ transform = targetTranslate * flipMatrix;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 判断是否显示正面
|
|
|
+ // 当flipProgress在[0, pi/2)(0~90 度)时:卡片未完全翻转,显示背面,isFlipped为false。
|
|
|
+ // 当flipProgress在[pi/2, pi](90~180 度)时:卡片已翻转到正面,isFlipped为true。
|
|
|
+ bool get isFlipped => flipProgress >= pi / 2;
|
|
|
+
|
|
|
// 生成clip path
|
|
|
Path _generateClipPath(double w, double h, List<bool> borders, double radius) {
|
|
|
Path path = Path();
|
|
|
@@ -373,11 +803,7 @@ class Piece {
|
|
|
|
|
|
// B. 右上角 (TR) - Arc
|
|
|
if (trRounded) {
|
|
|
- path.arcToPoint(
|
|
|
- Offset(w, radius), // 终点 (w, radius)
|
|
|
- radius: Radius.circular(radius),
|
|
|
- clockwise: true, // 逆时针
|
|
|
- );
|
|
|
+ path.arcToPoint(Offset(w, radius), radius: Radius.circular(radius), clockwise: true);
|
|
|
}
|
|
|
|
|
|
// C. 右边 (R) - LineTo
|
|
|
@@ -390,11 +816,7 @@ class Piece {
|
|
|
|
|
|
// D. 右下角 (BR) - Arc
|
|
|
if (brRounded) {
|
|
|
- path.arcToPoint(
|
|
|
- Offset(w - radius, h), // 终点 (w - radius, h)
|
|
|
- radius: Radius.circular(radius),
|
|
|
- clockwise: true, // 逆时针
|
|
|
- );
|
|
|
+ path.arcToPoint(Offset(w - radius, h), radius: Radius.circular(radius), clockwise: true);
|
|
|
}
|
|
|
|
|
|
// E. 底边 (B) - LineTo
|
|
|
@@ -407,11 +829,7 @@ class Piece {
|
|
|
|
|
|
// F. 左下角 (BL) - Arc
|
|
|
if (blRounded) {
|
|
|
- path.arcToPoint(
|
|
|
- Offset(0, h - radius), // 终点 (0, h - radius)
|
|
|
- radius: Radius.circular(radius),
|
|
|
- clockwise: true, // 逆时针
|
|
|
- );
|
|
|
+ path.arcToPoint(Offset(0, h - radius), radius: Radius.circular(radius), clockwise: true);
|
|
|
}
|
|
|
|
|
|
// G. 左边 (L) - LineTo
|
|
|
@@ -424,19 +842,14 @@ class Piece {
|
|
|
|
|
|
// H. 左上角 (TL) - Arc & Close
|
|
|
if (tlRounded) {
|
|
|
- path.arcToPoint(
|
|
|
- Offset(radius, 0), // 终点 (radius, 0),即起点
|
|
|
- radius: Radius.circular(radius),
|
|
|
- clockwise: true, // 逆时针
|
|
|
- );
|
|
|
+ path.arcToPoint(Offset(radius, 0), radius: Radius.circular(radius), clockwise: true);
|
|
|
}
|
|
|
- // path.close() 确保路径闭合,但上面的逻辑已经把路径连回了起点。
|
|
|
- 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;
|
|
|
@@ -469,14 +882,16 @@ class Piece {
|
|
|
if (hasL) {
|
|
|
path.moveTo(pTL_T.dx, pTL_T.dy); // 从TL弧的终点开始
|
|
|
} else {
|
|
|
- path.moveTo(x0, y0); // 从左上尖角开始
|
|
|
+ // path.moveTo(x0, y0); // 从左上尖角开始
|
|
|
+ path.moveTo(0, y0); // 从左上尖角开始
|
|
|
}
|
|
|
|
|
|
// 绘制 Top 直线段
|
|
|
if (hasR) {
|
|
|
path.lineTo(pTR_T.dx, pTR_T.dy);
|
|
|
} else {
|
|
|
- path.lineTo(x1, y0); // 到右上尖角
|
|
|
+ // path.lineTo(x1, y0); // 到右上尖角
|
|
|
+ path.lineTo(w, y0); // 到右上尖角
|
|
|
}
|
|
|
}
|
|
|
|
|
|
@@ -489,7 +904,8 @@ class Piece {
|
|
|
if (hasT) {
|
|
|
path.moveTo(pTR_R.dx, pTR_R.dy); // 从 TR 弧终点开始
|
|
|
} else {
|
|
|
- path.moveTo(x1, y0); // 从右上尖角开始
|
|
|
+ // path.moveTo(x1, y0); // 从右上尖角开始
|
|
|
+ path.moveTo(x1, 0); // 从右上尖角开始
|
|
|
}
|
|
|
}
|
|
|
|
|
|
@@ -498,7 +914,8 @@ class Piece {
|
|
|
if (hasB) {
|
|
|
path.lineTo(pBR_R.dx, pBR_R.dy);
|
|
|
} else {
|
|
|
- path.lineTo(x1, y1); // 到右下尖角
|
|
|
+ // path.lineTo(x1, y1); // 到右下尖角
|
|
|
+ path.lineTo(x1, h); // 到右下尖角
|
|
|
}
|
|
|
}
|
|
|
|
|
|
@@ -511,7 +928,8 @@ class Piece {
|
|
|
if (hasR) {
|
|
|
path.moveTo(pBR_B.dx, pBR_B.dy); // 从 BR 弧终点开始
|
|
|
} else {
|
|
|
- path.moveTo(x1, y1); // 从右下尖角开始
|
|
|
+ // path.moveTo(x1, y1); // 从右下尖角开始
|
|
|
+ path.moveTo(w, y1); // 从右下尖角开始
|
|
|
}
|
|
|
}
|
|
|
|
|
|
@@ -520,7 +938,8 @@ class Piece {
|
|
|
if (hasL) {
|
|
|
path.lineTo(pBL_B.dx, pBL_B.dy);
|
|
|
} else {
|
|
|
- path.lineTo(x0, y1); // 到左下尖角
|
|
|
+ // path.lineTo(x0, y1); // 到左下尖角
|
|
|
+ path.lineTo(0, y1); // 到左下尖角
|
|
|
}
|
|
|
}
|
|
|
|
|
|
@@ -533,7 +952,8 @@ class Piece {
|
|
|
if (hasB) {
|
|
|
path.moveTo(pBL_L.dx, pBL_L.dy); // 从 BL 弧终点开始
|
|
|
} else {
|
|
|
- path.moveTo(x0, y1); // 从左下尖角开始
|
|
|
+ // path.moveTo(x0, y1); // 从左下尖角开始
|
|
|
+ path.moveTo(x0, h); // 从左下尖角开始
|
|
|
}
|
|
|
}
|
|
|
|
|
|
@@ -542,7 +962,8 @@ class Piece {
|
|
|
if (hasT) {
|
|
|
path.lineTo(pTL_L.dx, pTL_L.dy);
|
|
|
} else {
|
|
|
- path.lineTo(x0, y0); // 到左上尖角
|
|
|
+ // path.lineTo(x0, y0); // 到左上尖角
|
|
|
+ path.lineTo(x0, 0); // 到左上尖角
|
|
|
}
|
|
|
}
|
|
|
|
|
|
@@ -557,12 +978,19 @@ class Piece {
|
|
|
}
|
|
|
|
|
|
// 生成碎片的clipPath, innerLinePath, outLinePath
|
|
|
- List<Path> generatePaths() {
|
|
|
- // _log.info('${toString()} generatePaths');
|
|
|
+ // 增加 forceRecalculate 参数,用于群组路径计算时强制重新计算边界状态
|
|
|
+ List<Path> generatePaths({bool forceRecalculate = false}) {
|
|
|
+ // 如果没有 group 且路径已缓存,则直接返回
|
|
|
+ if (group == null && !forceRecalculate && path != null && outLinePath != null && innerLinePath != null) {
|
|
|
+ return [path!, outLinePath!, innerLinePath!];
|
|
|
+ }
|
|
|
+
|
|
|
// 先确定4条边的状态[Top, Right, Bottom, Left]
|
|
|
List<bool> borders = [true, true, true, true];
|
|
|
- // 如果是单个碎片,4条边都需要,只有piece在group中才需要判断
|
|
|
- if (group != null) {
|
|
|
+
|
|
|
+ // 如果是单个碎片,4条边都需要,只有 piece 在 group 中才需要判断
|
|
|
+ if (group != null || forceRecalculate) {
|
|
|
+ // 即使是单独计算,也需要实时检查
|
|
|
borders = [_hasTopBorder, _hasRightBorder, _hasBottomBorder, _hasLeftBorder];
|
|
|
}
|
|
|
|