// piece.dart import 'dart:math'; import 'package:flutter/material.dart'; import 'package:puzzleweave/play/board.dart'; import 'package:logging/logging.dart'; import 'package:vector_math/vector_math.dart' as vmath; final Logger _log = Logger('piece.dart'); const double _outLineOffset = 0.5; const double _innerLineOffset = 1.5; // piece分组数据结构 class PieceGroup { List 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) { if (pieces.contains(piece)) { pieces.remove(piece); } piece.group = null; clearPathCache(); // 群组变化,清空缓存 } 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; } // 网格坐标边界计算辅助属性 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; double minX = double.infinity, minY = double.infinity; double maxX = -double.infinity, maxY = -double.infinity; for (var piece in pieces) { // 碎片当前在 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); maxY = max(maxY, y + h); } // 计算边界框中心点 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 r = board.cornerRadius; // 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) { str += p.toString(); str += '\n'; } _log.info(str); } } // 碎片基础数据结构 class Piece { final int index; final Board board; PieceGroup? group; // 总计的行数和列数 final int rows; final int cols; // 碎片在完整图片中的行/列位置 (逻辑坐标) final int row; final int col; // 碎片当前所处的位置 int curCol; int curRow; // 碎片的当前几何变换状态 (包括位置、旋转等) vmath.Matrix4 transform; // 碎片在原图片中的裁剪矩形 (Source Rect of the image) final Rect sourceRect; bool get isOK => row == curRow && col == curCol; // clipPath, 碎片的裁剪路径(闭合), 这是为了绘制出圆角效果 Path? path; // 内边框path(可能非闭合) Path? innerLinePath; // 外边框path (可能非闭合) Path? outLinePath; // 翻转相关属性 double flipProgress = 0.0; Piece({ required this.board, required this.index, required this.row, required this.col, required this.rows, required this.cols, required this.sourceRect, required this.curCol, required this.curRow, required this.transform, }); @override String toString() => 'Piece($index,$row:$col)'; Map toJson() => {'index': index, 'rows': rows, 'cols': cols, 'row': row, 'col': col, 'curRow': curRow, 'curCol': curCol}; 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 localCenter = Offset(width / 2, height / 2); // 应用当前变换矩阵 final vmath.Vector4 transformed = transform.transform(vmath.Vector4(localCenter.dx, localCenter.dy, 0.0, 1.0)); return Offset(transformed.x, transformed.y); } // 辅助函数:将碎片移动指定的位移量 void applyDelta(Offset delta) { // 累加 x 轴位移 transform.storage[12] += delta.dx; // 累加 y 轴位移 transform.storage[13] += delta.dy; } // 归位, 回到原来的位置 void revert() { transform = board.getTransformByCoordinate(curRow, curCol); } // 判断当前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 list = []; if (group != null) list.addAll(group!.pieces); if (other.group != null) list.addAll(other.group!.pieces); list.add(this); list.add(other); // 确保没有重复的 Piece final Set uniquePieces = list.toSet(); for (var p in uniquePieces) { finalGroup.add(p); } // 合并后需要重新计算 group paths finalGroup.generateGroupPaths(); } // 获取碎片本来的邻居(原正确位置上的邻居) List getNeighbourIndexes() { List 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) => 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 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; } // 如果它们在当前位置相邻,且原图相对位置正确,则边界被内部吸收,不需要绘制。 // 即:当前碎片(row)在邻居碎片(topPiece.row)的下方一个位置 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; } // 生成翻转变换矩阵(结合平移) 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(isFlipped ? -1.0 : 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 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), 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), 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), 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: Radius.circular(radius), clockwise: true); } path.close(); return path; } // 生成单个碎片边框路径 Path _generateBorderPath(double w, double h, List 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); // 从左上尖角开始 path.moveTo(0 - offset, y0); // 从左上尖角开始 } // 绘制 Top 直线段 if (hasR) { path.lineTo(pTR_T.dx, pTR_T.dy); } else { // path.lineTo(x1, y0); // 到右上尖角 path.lineTo(w + offset, 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); // 从右上尖角开始 path.moveTo(x1, 0 - offset); // 从右上尖角开始 } } if (hasR) { // 绘制 Right 直线段 if (hasB) { path.lineTo(pBR_R.dx, pBR_R.dy); } else { // path.lineTo(x1, y1); // 到右下尖角 path.lineTo(x1, h + offset); // 到右下尖角 } } // --- 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); // 从右下尖角开始 path.moveTo(w + offset, y1); // 从右下尖角开始 } } if (hasB) { // 绘制 Bottom 直线段 if (hasL) { path.lineTo(pBL_B.dx, pBL_B.dy); } else { // path.lineTo(x0, y1); // 到左下尖角 path.lineTo(0 - offset, 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); // 从左下尖角开始 path.moveTo(x0, h + offset); // 从左下尖角开始 } } if (hasL) { // 绘制 Left 直线段 if (hasT) { path.lineTo(pTL_L.dx, pTL_L.dy); } else { // path.lineTo(x0, y0); // 到左上尖角 path.lineTo(x0, 0 - offset); // 到左上尖角 } } // --- 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 // 增加 forceRecalculate 参数,用于群组路径计算时强制重新计算边界状态 List 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 borders = [true, true, true, true]; // 如果是单个碎片,4条边都需要,只有 piece 在 group 中才需要判断 if (group != null || forceRecalculate) { // 即使是单独计算,也需要实时检查 borders = [_hasTopBorder, _hasRightBorder, _hasBottomBorder, _hasLeftBorder]; } path = _generateClipPath(width, height, borders, board.cornerRadius); outLinePath = _generateBorderPath(width, height, borders, board.cornerRadius, _outLineOffset); innerLinePath = _generateBorderPath(width, height, borders, board.cornerRadius, _innerLineOffset); return [path!, outLinePath!, innerLinePath!]; } }