2026-07-01 09:13:38 +00:00
|
|
|
import { Skia, type SkPath } from '@shopify/react-native-skia';
|
|
|
|
|
import { getMaskSegmentRuntimeConfig } from './maskSegmentRuntime';
|
|
|
|
|
import type { SegmentRegion, RegionMaskData } from './maskSegmentation';
|
|
|
|
|
|
2026-07-02 02:07:32 +00:00
|
|
|
/** partition dashed highlight: extract outer contour from the same pixel grid as the fill mask */
|
2026-07-01 09:13:38 +00:00
|
|
|
type GridPoint = { x: number; y: number };
|
|
|
|
|
|
|
|
|
|
type GridEdge = {
|
|
|
|
|
x0: number;
|
|
|
|
|
y0: number;
|
|
|
|
|
x1: number;
|
|
|
|
|
y1: number;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
function isMaskPixelOn(
|
|
|
|
|
binary: Uint8Array,
|
|
|
|
|
cols: number,
|
|
|
|
|
rows: number,
|
|
|
|
|
x: number,
|
|
|
|
|
y: number,
|
|
|
|
|
): boolean {
|
|
|
|
|
return (
|
|
|
|
|
x >= 0 && x < cols && y >= 0 && y < rows && binary[y * cols + x] > 0
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function collectBoundaryEdges(
|
|
|
|
|
binary: Uint8Array,
|
|
|
|
|
cols: number,
|
|
|
|
|
rows: number,
|
|
|
|
|
): GridEdge[] {
|
|
|
|
|
const edges: GridEdge[] = [];
|
|
|
|
|
for (let y = 0; y < rows; y++) {
|
|
|
|
|
const row = y * cols;
|
|
|
|
|
for (let x = 0; x < cols; x++) {
|
|
|
|
|
if (!binary[row + x]) {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
if (!isMaskPixelOn(binary, cols, rows, x, y - 1)) {
|
|
|
|
|
edges.push({ x0: x, y0: y, x1: x + 1, y1: y });
|
|
|
|
|
}
|
|
|
|
|
if (!isMaskPixelOn(binary, cols, rows, x, y + 1)) {
|
|
|
|
|
edges.push({ x0: x + 1, y0: y + 1, x1: x, y1: y + 1 });
|
|
|
|
|
}
|
|
|
|
|
if (!isMaskPixelOn(binary, cols, rows, x - 1, y)) {
|
|
|
|
|
edges.push({ x0: x, y0: y + 1, x1: x, y1: y });
|
|
|
|
|
}
|
|
|
|
|
if (!isMaskPixelOn(binary, cols, rows, x + 1, y)) {
|
|
|
|
|
edges.push({ x0: x + 1, y0: y, x1: x + 1, y1: y + 1 });
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return edges;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function chainBoundaryLoops(edges: GridEdge[]): GridPoint[][] {
|
|
|
|
|
const outgoing = new Map<string, GridEdge[]>();
|
|
|
|
|
const edgeKey = (edge: GridEdge) =>
|
|
|
|
|
`${edge.x0},${edge.y0}->${edge.x1},${edge.y1}`;
|
|
|
|
|
|
|
|
|
|
for (const edge of edges) {
|
|
|
|
|
const key = `${edge.x0},${edge.y0}`;
|
|
|
|
|
const list = outgoing.get(key);
|
|
|
|
|
if (list) {
|
|
|
|
|
list.push(edge);
|
|
|
|
|
} else {
|
|
|
|
|
outgoing.set(key, [edge]);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const used = new Set<string>();
|
|
|
|
|
const loops: GridPoint[][] = [];
|
|
|
|
|
|
|
|
|
|
for (const edge of edges) {
|
|
|
|
|
const startEdgeKey = edgeKey(edge);
|
|
|
|
|
if (used.has(startEdgeKey)) {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const loop: GridPoint[] = [{ x: edge.x0, y: edge.y0 }];
|
|
|
|
|
let current = edge;
|
|
|
|
|
used.add(startEdgeKey);
|
|
|
|
|
loop.push({ x: current.x1, y: current.y1 });
|
|
|
|
|
|
|
|
|
|
while (true) {
|
|
|
|
|
const endKey = `${current.x1},${current.y1}`;
|
|
|
|
|
const startKey = `${loop[0].x},${loop[0].y}`;
|
|
|
|
|
if (endKey === startKey && loop.length > 2) {
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
const candidates = outgoing.get(endKey);
|
|
|
|
|
const next = candidates?.find(candidate => !used.has(edgeKey(candidate)));
|
|
|
|
|
if (!next) {
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
current = next;
|
|
|
|
|
used.add(edgeKey(current));
|
|
|
|
|
loop.push({ x: current.x1, y: current.y1 });
|
|
|
|
|
if (loop.length > edges.length + 1) {
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (loop.length >= 4) {
|
|
|
|
|
loops.push(loop);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return loops;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function simplifyOrthogonalLoop(points: GridPoint[]): GridPoint[] {
|
|
|
|
|
if (points.length <= 3) {
|
|
|
|
|
return points;
|
|
|
|
|
}
|
|
|
|
|
const out: GridPoint[] = [points[0]];
|
|
|
|
|
for (let i = 1; i < points.length - 1; i++) {
|
|
|
|
|
const prev = out[out.length - 1];
|
|
|
|
|
const curr = points[i];
|
|
|
|
|
const next = points[i + 1];
|
|
|
|
|
const collinearX = prev.x === curr.x && curr.x === next.x;
|
|
|
|
|
const collinearY = prev.y === curr.y && curr.y === next.y;
|
|
|
|
|
if (!collinearX && !collinearY) {
|
|
|
|
|
out.push(curr);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
out.push(points[points.length - 1]);
|
|
|
|
|
return out;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function perpendicularDistance2(
|
|
|
|
|
point: GridPoint,
|
|
|
|
|
lineStart: GridPoint,
|
|
|
|
|
lineEnd: GridPoint,
|
|
|
|
|
): number {
|
|
|
|
|
const dx = lineEnd.x - lineStart.x;
|
|
|
|
|
const dy = lineEnd.y - lineStart.y;
|
|
|
|
|
if (dx === 0 && dy === 0) {
|
|
|
|
|
return Math.hypot(point.x - lineStart.x, point.y - lineStart.y);
|
|
|
|
|
}
|
|
|
|
|
const t =
|
|
|
|
|
((point.x - lineStart.x) * dx + (point.y - lineStart.y) * dy) /
|
|
|
|
|
(dx * dx + dy * dy);
|
|
|
|
|
const projX = lineStart.x + t * dx;
|
|
|
|
|
const projY = lineStart.y + t * dy;
|
|
|
|
|
return Math.hypot(point.x - projX, point.y - projY);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Ramer-Douglas-Peucker simplification for outline loops.
|
|
|
|
|
* Reduces stair-step jagginess from grid boundary tracing so that dashed
|
|
|
|
|
* outlines render smoothly instead of zigzagging across every pixel edge.
|
|
|
|
|
* Epsilon is in grid-pixel units (1.0 = one mask pixel).
|
|
|
|
|
*/
|
|
|
|
|
function simplifyLoopRdp(points: GridPoint[], epsilon: number): GridPoint[] {
|
|
|
|
|
if (points.length <= 2) {
|
|
|
|
|
return points;
|
|
|
|
|
}
|
|
|
|
|
let maxDist = 0;
|
|
|
|
|
let index = 0;
|
|
|
|
|
const end = points.length - 1;
|
|
|
|
|
const lineStart = points[0]!;
|
|
|
|
|
const lineEnd = points[end]!;
|
|
|
|
|
for (let i = 1; i < end; i++) {
|
|
|
|
|
const p = points[i]!;
|
|
|
|
|
const dist = perpendicularDistance2(p, lineStart, lineEnd);
|
|
|
|
|
if (dist > maxDist) {
|
|
|
|
|
maxDist = dist;
|
|
|
|
|
index = i;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if (maxDist > epsilon) {
|
|
|
|
|
const left = simplifyLoopRdp(points.slice(0, index + 1), epsilon);
|
|
|
|
|
const right = simplifyLoopRdp(points.slice(index), epsilon);
|
|
|
|
|
return [...left.slice(0, -1), ...right];
|
|
|
|
|
}
|
|
|
|
|
return [lineStart, lineEnd];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function loopsToSkPath(
|
|
|
|
|
loops: GridPoint[][],
|
|
|
|
|
cols: number,
|
|
|
|
|
rows: number,
|
|
|
|
|
rect: { x: number; y: number; w: number; h: number },
|
|
|
|
|
): SkPath {
|
|
|
|
|
const path = Skia.Path.Make();
|
|
|
|
|
for (const rawLoop of loops) {
|
|
|
|
|
// Two-pass simplification: first remove collinear points, then RDP to
|
|
|
|
|
// smooth stair-step artifacts from grid boundary tracing.
|
|
|
|
|
const orthogonal = simplifyOrthogonalLoop(rawLoop);
|
|
|
|
|
const loop = simplifyLoopRdp(orthogonal, 1.0);
|
|
|
|
|
if (loop.length < 2) {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
const [first, ...rest] = loop;
|
|
|
|
|
path.moveTo(
|
|
|
|
|
rect.x + (first.x / cols) * rect.w,
|
|
|
|
|
rect.y + (first.y / rows) * rect.h,
|
|
|
|
|
);
|
|
|
|
|
for (const point of rest) {
|
|
|
|
|
path.lineTo(
|
|
|
|
|
rect.x + (point.x / cols) * rect.w,
|
|
|
|
|
rect.y + (point.y / rows) * rect.h,
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
path.close();
|
|
|
|
|
}
|
|
|
|
|
return path;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function pointInIntegerLoop(px: number, py: number, loop: GridPoint[]): boolean {
|
|
|
|
|
let inside = false;
|
|
|
|
|
for (let i = 0, j = loop.length - 1; i < loop.length; j = i++) {
|
|
|
|
|
const xi = loop[i].x;
|
|
|
|
|
const yi = loop[i].y;
|
|
|
|
|
const xj = loop[j].x;
|
|
|
|
|
const yj = loop[j].y;
|
|
|
|
|
const intersect =
|
|
|
|
|
yi > py !== yj > py &&
|
|
|
|
|
px < ((xj - xi) * (py - yi)) / (yj - yi + Number.EPSILON) + xi;
|
|
|
|
|
if (intersect) {
|
|
|
|
|
inside = !inside;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return inside;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function loopBoundingArea(loop: GridPoint[]): number {
|
|
|
|
|
if (loop.length === 0) {
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
let minX = loop[0].x;
|
|
|
|
|
let maxX = loop[0].x;
|
|
|
|
|
let minY = loop[0].y;
|
|
|
|
|
let maxY = loop[0].y;
|
|
|
|
|
for (const point of loop) {
|
|
|
|
|
minX = Math.min(minX, point.x);
|
|
|
|
|
maxX = Math.max(maxX, point.x);
|
|
|
|
|
minY = Math.min(minY, point.y);
|
|
|
|
|
maxY = Math.max(maxY, point.y);
|
|
|
|
|
}
|
|
|
|
|
return (maxX - minX) * (maxY - minY);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function filterOutlineLoops(
|
|
|
|
|
loops: GridPoint[][],
|
|
|
|
|
cols: number,
|
|
|
|
|
rows: number,
|
|
|
|
|
seedPx?: { x: number; y: number },
|
|
|
|
|
): GridPoint[][] {
|
|
|
|
|
if (loops.length === 0) {
|
|
|
|
|
return loops;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const minLoopArea = Math.max(16, Math.floor(cols * rows * 0.00005));
|
|
|
|
|
const significant = loops.filter(
|
|
|
|
|
loop => loopBoundingArea(loop) >= minLoopArea,
|
|
|
|
|
);
|
|
|
|
|
const candidates = significant.length > 0 ? significant : loops;
|
|
|
|
|
|
|
|
|
|
if (seedPx) {
|
|
|
|
|
const sampleX = seedPx.x + 0.5;
|
|
|
|
|
const sampleY = seedPx.y + 0.5;
|
|
|
|
|
const containing = candidates.filter(loop =>
|
|
|
|
|
pointInIntegerLoop(sampleX, sampleY, loop),
|
|
|
|
|
);
|
|
|
|
|
if (containing.length > 0) {
|
|
|
|
|
containing.sort((a, b) => loopBoundingArea(b) - loopBoundingArea(a));
|
|
|
|
|
return containing;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
candidates.sort((a, b) => loopBoundingArea(b) - loopBoundingArea(a));
|
|
|
|
|
// Keep loops ≥ 5% of the largest area to filter isolated noise speckles
|
|
|
|
|
// while retaining genuine disconnected fragments of the same region.
|
|
|
|
|
const minKeepArea = loopBoundingArea(candidates[0]!) * 0.05;
|
|
|
|
|
return candidates.filter(loop => loopBoundingArea(loop) >= minKeepArea);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function floodFillComponent(
|
|
|
|
|
binary: Uint8Array,
|
|
|
|
|
cols: number,
|
|
|
|
|
rows: number,
|
|
|
|
|
seedX: number,
|
|
|
|
|
seedY: number,
|
|
|
|
|
): Uint8Array | null {
|
|
|
|
|
if (
|
|
|
|
|
seedX < 0 ||
|
|
|
|
|
seedY < 0 ||
|
|
|
|
|
seedX >= cols ||
|
|
|
|
|
seedY >= rows ||
|
|
|
|
|
!binary[seedY * cols + seedX]
|
|
|
|
|
) {
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const out = new Uint8Array(cols * rows);
|
|
|
|
|
const stack = [seedY * cols + seedX];
|
|
|
|
|
out[stack[0]] = 255;
|
|
|
|
|
|
|
|
|
|
while (stack.length > 0) {
|
|
|
|
|
const index = stack.pop()!;
|
|
|
|
|
const x = index % cols;
|
|
|
|
|
const y = (index - x) / cols;
|
|
|
|
|
|
|
|
|
|
if (x > 0) {
|
|
|
|
|
const left = index - 1;
|
|
|
|
|
if (binary[left] && !out[left]) {
|
|
|
|
|
out[left] = 255;
|
|
|
|
|
stack.push(left);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if (x + 1 < cols) {
|
|
|
|
|
const right = index + 1;
|
|
|
|
|
if (binary[right] && !out[right]) {
|
|
|
|
|
out[right] = 255;
|
|
|
|
|
stack.push(right);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if (y > 0) {
|
|
|
|
|
const up = index - cols;
|
|
|
|
|
if (binary[up] && !out[up]) {
|
|
|
|
|
out[up] = 255;
|
|
|
|
|
stack.push(up);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if (y + 1 < rows) {
|
|
|
|
|
const down = index + cols;
|
|
|
|
|
if (binary[down] && !out[down]) {
|
|
|
|
|
out[down] = 255;
|
|
|
|
|
stack.push(down);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return out;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function findLargestComponentSeed(
|
|
|
|
|
binary: Uint8Array,
|
|
|
|
|
cols: number,
|
|
|
|
|
rows: number,
|
|
|
|
|
): { x: number; y: number } | null {
|
|
|
|
|
const visited = new Uint8Array(cols * rows);
|
|
|
|
|
let bestArea = 0;
|
|
|
|
|
let bestSeed: { x: number; y: number } | null = null;
|
|
|
|
|
|
|
|
|
|
for (let y = 0; y < rows; y++) {
|
|
|
|
|
const row = y * cols;
|
|
|
|
|
for (let x = 0; x < cols; x++) {
|
|
|
|
|
const start = row + x;
|
|
|
|
|
if (!binary[start] || visited[start]) {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let area = 0;
|
|
|
|
|
let sumX = 0;
|
|
|
|
|
let sumY = 0;
|
|
|
|
|
const stack = [start];
|
|
|
|
|
visited[start] = 1;
|
|
|
|
|
|
|
|
|
|
while (stack.length > 0) {
|
|
|
|
|
const index = stack.pop()!;
|
|
|
|
|
area += 1;
|
|
|
|
|
const px = index % cols;
|
|
|
|
|
const py = (index - px) / cols;
|
|
|
|
|
sumX += px;
|
|
|
|
|
sumY += py;
|
|
|
|
|
|
|
|
|
|
if (px > 0) {
|
|
|
|
|
const left = index - 1;
|
|
|
|
|
if (binary[left] && !visited[left]) {
|
|
|
|
|
visited[left] = 1;
|
|
|
|
|
stack.push(left);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if (px + 1 < cols) {
|
|
|
|
|
const right = index + 1;
|
|
|
|
|
if (binary[right] && !visited[right]) {
|
|
|
|
|
visited[right] = 1;
|
|
|
|
|
stack.push(right);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if (py > 0) {
|
|
|
|
|
const up = index - cols;
|
|
|
|
|
if (binary[up] && !visited[up]) {
|
|
|
|
|
visited[up] = 1;
|
|
|
|
|
stack.push(up);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if (py + 1 < rows) {
|
|
|
|
|
const down = index + cols;
|
|
|
|
|
if (binary[down] && !visited[down]) {
|
|
|
|
|
visited[down] = 1;
|
|
|
|
|
stack.push(down);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (area > bestArea) {
|
|
|
|
|
bestArea = area;
|
|
|
|
|
bestSeed = {
|
|
|
|
|
x: Math.floor(sumX / area),
|
|
|
|
|
y: Math.floor(sumY / area),
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return bestSeed;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function buildRegionOutlinePathFromBinary(
|
|
|
|
|
binary: Uint8Array,
|
|
|
|
|
cols: number,
|
|
|
|
|
rows: number,
|
|
|
|
|
rect: { x: number; y: number; w: number; h: number },
|
|
|
|
|
seedPx?: { x: number; y: number },
|
|
|
|
|
): SkPath {
|
|
|
|
|
let working = binary;
|
|
|
|
|
if (seedPx) {
|
|
|
|
|
const component = floodFillComponent(
|
|
|
|
|
binary,
|
|
|
|
|
cols,
|
|
|
|
|
rows,
|
|
|
|
|
seedPx.x,
|
|
|
|
|
seedPx.y,
|
|
|
|
|
);
|
|
|
|
|
if (!component) {
|
|
|
|
|
return Skia.Path.Make();
|
|
|
|
|
}
|
|
|
|
|
working = component;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const edges = collectBoundaryEdges(working, cols, rows);
|
|
|
|
|
const loops = chainBoundaryLoops(edges);
|
|
|
|
|
const filtered = filterOutlineLoops(loops, cols, rows, seedPx);
|
|
|
|
|
return loopsToSkPath(filtered, cols, rows, rect);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function resolveRegionOutlineSeedPx(
|
|
|
|
|
binary: Uint8Array,
|
|
|
|
|
cols: number,
|
|
|
|
|
rows: number,
|
|
|
|
|
normSeed?: { x: number; y: number },
|
|
|
|
|
): { x: number; y: number } | undefined {
|
|
|
|
|
if (normSeed) {
|
|
|
|
|
return {
|
|
|
|
|
x: Math.min(cols - 1, Math.max(0, Math.floor(normSeed.x * cols))),
|
|
|
|
|
y: Math.min(rows - 1, Math.max(0, Math.floor(normSeed.y * rows))),
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
return findLargestComponentSeed(binary, cols, rows) ?? undefined;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function buildRegionOutlinePathForRegion(
|
|
|
|
|
regionId: number,
|
|
|
|
|
regions: SegmentRegion[],
|
|
|
|
|
maskData: RegionMaskData,
|
|
|
|
|
rect: { x: number; y: number; w: number; h: number },
|
|
|
|
|
normSeed?: { x: number; y: number },
|
|
|
|
|
): SkPath {
|
|
|
|
|
const binaries = extractRegionBinaries(regions, maskData);
|
|
|
|
|
const binary = binaries.get(regionId);
|
|
|
|
|
if (!binary) {
|
|
|
|
|
return Skia.Path.Make();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const { cols, rows } = maskData;
|
|
|
|
|
const seedPx = resolveRegionOutlineSeedPx(binary, cols, rows, normSeed);
|
|
|
|
|
return buildRegionOutlinePathFromBinary(binary, cols, rows, rect, seedPx);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function extractRegionBinaries(
|
|
|
|
|
regions: SegmentRegion[],
|
|
|
|
|
maskData: RegionMaskData,
|
|
|
|
|
): Map<number, Uint8Array> {
|
|
|
|
|
const { labels, baseboardBinary, cols, rows, wallSubLabels } = maskData;
|
|
|
|
|
const size = cols * rows;
|
|
|
|
|
const binaries = new Map<number, Uint8Array>();
|
|
|
|
|
const semanticColors = getMaskSegmentRuntimeConfig().mask.semanticColors;
|
|
|
|
|
const regionIdBySemantic = new Int32Array(semanticColors.length);
|
|
|
|
|
regionIdBySemantic.fill(-1);
|
|
|
|
|
const wallSubRegionIds = new Map<number, number>();
|
|
|
|
|
let baseboardRegionId: number | null = null;
|
|
|
|
|
|
|
|
|
|
for (const reg of regions) {
|
|
|
|
|
binaries.set(reg.id, new Uint8Array(size));
|
|
|
|
|
if (reg.thinStrip) {
|
|
|
|
|
baseboardRegionId = reg.id;
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
const wallMatch = /^wall-(\d+)$/.exec(reg.name);
|
|
|
|
|
if (wallMatch && wallSubLabels) {
|
|
|
|
|
wallSubRegionIds.set(Number(wallMatch[1]) - 1, reg.id);
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
const semanticIndex = semanticColors.findIndex(
|
|
|
|
|
entry => entry.name === reg.name,
|
|
|
|
|
);
|
|
|
|
|
if (semanticIndex >= 0) {
|
|
|
|
|
regionIdBySemantic[semanticIndex] = reg.id;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const semanticCount = semanticColors.length;
|
|
|
|
|
const wallIdx = semanticColors.findIndex(entry => entry.name === 'wall');
|
|
|
|
|
for (let i = 0; i < size; i++) {
|
|
|
|
|
if (baseboardRegionId != null && baseboardBinary[i] > 0) {
|
|
|
|
|
binaries.get(baseboardRegionId)![i] = 255;
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
if (wallSubLabels && wallIdx >= 0 && labels[i] === wallIdx) {
|
|
|
|
|
const subIdx = wallSubLabels[i];
|
|
|
|
|
if (subIdx !== 255) {
|
|
|
|
|
const regionId = wallSubRegionIds.get(subIdx);
|
|
|
|
|
if (regionId !== undefined) {
|
|
|
|
|
binaries.get(regionId)![i] = 255;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
const semanticIndex = labels[i];
|
|
|
|
|
if (semanticIndex < semanticCount && regionIdBySemantic[semanticIndex] >= 0) {
|
|
|
|
|
binaries.get(regionIdBySemantic[semanticIndex])![i] = 255;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return binaries;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function buildAllRegionOutlinePaths(
|
|
|
|
|
regions: SegmentRegion[],
|
|
|
|
|
maskData: RegionMaskData,
|
|
|
|
|
rect: { x: number; y: number; w: number; h: number },
|
|
|
|
|
): Map<number, SkPath> {
|
|
|
|
|
const { cols, rows } = maskData;
|
|
|
|
|
const binaries = extractRegionBinaries(regions, maskData);
|
|
|
|
|
const map = new Map<number, SkPath>();
|
|
|
|
|
for (const reg of regions) {
|
|
|
|
|
const binary = binaries.get(reg.id);
|
|
|
|
|
if (!binary) {
|
|
|
|
|
map.set(reg.id, Skia.Path.Make());
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
// No seed — build outlines from the full binary so all disconnected
|
|
|
|
|
// fragments (common after mask downsampling) get dashed outlines during
|
|
|
|
|
// the init flash loop. Per-region hold highlights still use a touch-seed
|
|
|
|
|
// via buildRegionOutlinePathForRegion for precise fragment isolation.
|
|
|
|
|
map.set(
|
|
|
|
|
reg.id,
|
|
|
|
|
buildRegionOutlinePathFromBinary(binary, cols, rows, rect),
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
return map;
|
|
|
|
|
}
|