Bin
2025-12-17 21f0498f62ada55651f4d232327e15fc47f498b1
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
import simplify from "simplify-js";
 
/**
 * Generates outline contours from a pixel-based region/mask
 *
 * @param item - Object containing the canvas with pixel data and rendering properties
 * @returns Array of flattened point coordinates forming contours around the shapes
 */
export function generateMultiShapeOutline(item: {
  highlighted: boolean;
  offscreenCanvasRef: HTMLCanvasElement;
  parent: any;
}) {
  if (!item.offscreenCanvasRef) return [];
 
  const ctx = item.offscreenCanvasRef.getContext("2d");
  if (!ctx) return [];
 
  const { width, height } = item.offscreenCanvasRef;
  const data = ctx.getImageData(0, 0, width, height).data;
 
  // Create a binary grid from the image data (1 for visible pixels, 0 for transparent)
  const grid: number[][] = [];
  for (let y = 0; y < height; y++) {
    const row: number[] = [];
    for (let x = 0; x < width; x++) {
      const alpha = data[(y * width + x) * 4 + 3];
      row.push(alpha > 0 ? 1 : 0);
    }
    grid.push(row);
  }
 
  const visited = Array.from({ length: height }, () => Array(width).fill(false));
  const dirs = [
    [1, 0],
    [0, 1],
    [-1, 0],
    [0, -1],
  ];
 
  // Helper to check if two points are within 1 pixel (including diagonals)
  const isNear = (x1: number, y1: number, x2: number, y2: number) => Math.abs(x1 - x2) <= 1 && Math.abs(y1 - y2) <= 1;
 
  /**
   * Determines if a pixel is on the edge of a shape
   * A pixel is an edge if it's non-transparent and has at least one transparent neighbor
   */
  const isEdge = (x: number, y: number): boolean => {
    if (grid[y][x] === 0) return false;
    for (let dy = -1; dy <= 1; dy++) {
      for (let dx = -1; dx <= 1; dx++) {
        if (dx === 0 && dy === 0) continue;
        const nx = x + dx;
        const ny = y + dy;
        if (nx < 0 || ny < 0 || nx >= width || ny >= height || grid[ny][nx] === 0) {
          return true;
        }
      }
    }
    return false;
  };
 
  /**
   * Traces a contour starting from a given point
   * Uses a boundary-following algorithm to create a closed path
   */
  const trace = (sx: number, sy: number) => {
    const path = [];
    const seen = new Set();
    let x = sx;
    let y = sy;
    let dir = 0;
    let closed = false;
 
    for (let steps = 0; steps < 5000; steps++) {
      path.push([x, y]);
      seen.add(`${x},${y}`);
      visited[y][x] = true;
      let moved = false;
 
      for (let i = 0; i < 4; i++) {
        const d = (dir + i) % 4;
        const [dx, dy] = dirs[d];
        const nx = x + dx;
        const ny = y + dy;
        if (nx >= 0 && ny >= 0 && nx < width && ny < height && isEdge(nx, ny) && !seen.has(`${nx},${ny}`)) {
          x = nx;
          y = ny;
          dir = d;
          moved = true;
          break;
        }
      }
 
      // Only close if we're back near the start and the path is long enough
      if (!moved || (path.length > 10 && isNear(x, y, sx, sy))) {
        closed = isNear(x, y, sx, sy) && path.length > 10;
        break;
      }
    }
 
    // Only accept closed contours
    if (closed) {
      return path;
    }
    return [];
  };
 
  // Find and trace all contours in the image
  const contours: number[][][] = [];
  for (let y = 1; y < height - 1; y++) {
    for (let x = 1; x < width - 1; x++) {
      if (isEdge(x, y) && !visited[y][x]) {
        const contour = trace(x, y);
        if (contour.length > 5) {
          contours.push(contour);
        }
      }
    }
  }
 
  // Scale and simplify the contours for rendering
  const scale = item.parent.stageZoom;
  return contours.map((contour) => {
    const simplified = simplify(
      contour.map(([x, y]) => ({ x: x * scale, y: y * scale })),
      1.5,
      true,
    );
    return simplified.flatMap(({ x, y }) => [x, y]);
  });
}