| Server IP : 217.154.3.148 / Your IP : 216.73.216.90 Web Server : Apache System : Linux blissful-wright.217-154-3-148.plesk.page 6.8.0-124-generic #124-Ubuntu SMP PREEMPT_DYNAMIC Tue May 26 13:00:45 UTC 2026 x86_64 User : gracious-boyd_hfhh1h8vo9n ( 10000) PHP Version : 8.3.31 Disable Function : opcache_get_status MySQL : OFF | cURL : ON | WGET : OFF | Perl : OFF | Python : OFF | Sudo : OFF | Pkexec : OFF Directory : /var/www/vhosts/gracious-boyd.217-154-3-148.plesk.page/nextjs/lib/masks/ |
Upload File : |
import type { GridContext } from "./types";
export interface LatticeMasks {
interiorMask: Uint8Array;
lineMask: Uint8Array;
weightMap: Float32Array;
axisMap: Int8Array;
}
export function coverageOfMask(mask: Uint8Array | null | undefined): number {
if (!mask || mask.length === 0) return 0;
let on = 0;
for (let i = 0; i < mask.length; i++) if (mask[i]) on += 1;
return on / mask.length;
}
export function computeBandPx(grid: GridContext): number {
const grout = Math.max(1, Math.round(grid.groutPx || 1));
const band = Math.round(grout * 1.5);
return Math.max(2, Math.min(6, band));
}
export function buildLatticeMasks(
width: number,
height: number,
vlines: number[],
hlines: number[],
bandPx: number,
): LatticeMasks {
const total = width * height;
const interior = new Uint8Array(total);
const lines = new Uint8Array(total);
const weights = new Float32Array(total);
const axis = new Int8Array(total);
if (bandPx <= 0 || (!vlines.length && !hlines.length)) {
interior.fill(255);
weights.fill(1);
axis.fill(-1);
return { interiorMask: interior, lineMask: lines, weightMap: weights, axisMap: axis };
}
const vDist = new Float32Array(width);
const hDist = new Float32Array(height);
vDist.fill(Number.POSITIVE_INFINITY);
hDist.fill(Number.POSITIVE_INFINITY);
for (const xRaw of vlines) {
const x = clampIdx(Math.round(xRaw), width);
for (let xi = 0; xi < width; xi++) {
const d = Math.abs(xi - x);
if (d < vDist[xi]) vDist[xi] = d;
}
}
for (const yRaw of hlines) {
const y = clampIdx(Math.round(yRaw), height);
for (let yi = 0; yi < height; yi++) {
const d = Math.abs(yi - y);
if (d < hDist[yi]) hDist[yi] = d;
}
}
const band = Math.max(1, bandPx);
const invBand = 1 / band;
interior.fill(255);
axis.fill(-1);
for (let y = 0; y < height; y++) {
const row = y * width;
const dy = hDist[y];
for (let x = 0; x < width; x++) {
const idx = row + x;
const dx = vDist[x];
const hasV = Number.isFinite(dx);
const hasH = Number.isFinite(dy);
let dist = Number.POSITIVE_INFINITY;
let orientation = -1;
if (hasV && hasH) {
if (dx <= dy) {
dist = dx;
orientation = 0; // vertical grout, normal along X axis
} else {
dist = dy;
orientation = 1; // horizontal grout, normal along Y axis
}
} else if (hasV) {
dist = dx;
orientation = 0;
} else if (hasH) {
dist = dy;
orientation = 1;
}
if (!Number.isFinite(dist)) {
weights[idx] = 1;
continue;
}
const t = clamp01(dist * invBand);
const w = smoothstep(0, 1, t);
weights[idx] = w;
if (dist <= band) {
lines[idx] = 255;
}
if (w <= 0.35) {
interior[idx] = 0;
}
if (orientation >= 0) {
axis[idx] = orientation;
}
}
}
return { interiorMask: interior, lineMask: lines, weightMap: weights, axisMap: axis };
}
export function leakRatio(
mask: Uint8Array,
lineMask: Uint8Array,
opts: {
weights?: Float32Array;
axisMap?: Int8Array;
gradX?: Float32Array;
gradY?: Float32Array;
gradientThreshold?: number;
angleThresholdRad?: number;
} = {},
): number {
let touching = 0;
let total = 0;
const len = Math.min(mask.length, lineMask.length);
const weights = opts.weights;
const axis = opts.axisMap;
const gx = opts.gradX;
const gy = opts.gradY;
const angleThreshold = opts.angleThresholdRad ?? (15 * Math.PI) / 180;
const gradientThreshold = opts.gradientThreshold ?? 8;
for (let i = 0; i < len; i++) {
if (!lineMask[i]) continue;
const baseWeight = weights ? clamp01(1 - weights[i]) : 1;
if (baseWeight <= 0) continue;
let contribution = baseWeight;
if (mask[i]) {
if (axis && gx && gy && axis[i] >= 0) {
const gxi = gx[i];
const gyi = gy[i];
const mag = Math.hypot(gxi, gyi);
if (mag >= gradientThreshold) {
const gradAngle = Math.atan2(gyi, gxi);
const normalAngle = axis[i] === 0 ? 0 : Math.PI / 2;
const diff = angleDifference(gradAngle, normalAngle);
if (diff > angleThreshold) {
contribution = 0;
}
}
}
touching += contribution;
}
total += baseWeight;
}
if (total <= 0) return 0;
return touching / total;
}
export function thresholdMask(source: Uint8Array | null | undefined, threshold: number): Uint8Array | null {
if (!source || source.length === 0) return null;
const out = new Uint8Array(source.length);
for (let i = 0; i < source.length; i++) {
if (source[i] >= threshold) out[i] = 255;
}
return out;
}
function clampIdx(value: number, limit: number): number {
if (limit <= 0) return 0;
if (value < 0) return 0;
if (value >= limit) return limit - 1;
return value;
}
function smoothstep(edge0: number, edge1: number, value: number): number {
if (edge0 === edge1) return value >= edge1 ? 1 : 0;
const t = clamp01((value - edge0) / (edge1 - edge0));
return t * t * (3 - 2 * t);
}
function clamp01(value: number): number {
if (value <= 0) return 0;
if (value >= 1) return 1;
return value;
}
function angleDifference(a: number, b: number): number {
return Math.abs(Math.atan2(Math.sin(a - b), Math.cos(a - b)));
}