opentakeoff-mcp 0.9.27 → 0.9.29
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/server-core.js +220 -5
- package/package.json +1 -1
package/dist/server-core.js
CHANGED
|
@@ -1292,6 +1292,76 @@ function sweepHatchRuns(segs, meta, ws) {
|
|
|
1292
1292
|
}
|
|
1293
1293
|
return { clipSoft, runs };
|
|
1294
1294
|
}
|
|
1295
|
+
var ANNOT_OFFSET_MAX_FT = 2;
|
|
1296
|
+
var ANNOT_OFFSET_MIN_FT = 0.25;
|
|
1297
|
+
var ANNOT_MIN_LEN_FT = 4;
|
|
1298
|
+
var ANNOT_OVERLAP_FRAC = 0.6;
|
|
1299
|
+
var ANNOT_MIN_PEN_STEP = 1;
|
|
1300
|
+
var ANNOT_MAX_SCAN = 64;
|
|
1301
|
+
function classifyOffsetAnnotationSegs(segs, meta, ws, maxOffsetPx, minOffsetPx, minLenPx) {
|
|
1302
|
+
const n = segs.length >> 2;
|
|
1303
|
+
const soft = new Uint8Array(n);
|
|
1304
|
+
if (!meta || !n || !(maxOffsetPx > 0) || !(minLenPx > 0)) return soft;
|
|
1305
|
+
const cand = [];
|
|
1306
|
+
for (let i = 0; i < n; i++) {
|
|
1307
|
+
const mt = meta[i];
|
|
1308
|
+
if (mt & (SEG_CURVE | SEG_CLIP | SEG_FILLONLY)) continue;
|
|
1309
|
+
const x1 = segs[i * 4] * ws, y1 = segs[i * 4 + 1] * ws, x2 = segs[i * 4 + 2] * ws, y2 = segs[i * 4 + 3] * ws;
|
|
1310
|
+
const dx = x2 - x1, dy = y2 - y1;
|
|
1311
|
+
if (Math.hypot(dx, dy) < minLenPx) continue;
|
|
1312
|
+
let ang = Math.atan2(dy, dx) * 180 / Math.PI;
|
|
1313
|
+
if (ang < 0) ang += 180;
|
|
1314
|
+
if (ang >= 180) ang -= 180;
|
|
1315
|
+
cand.push({ i, ang, x1, y1, x2, y2, w: meta[i] >> 4 });
|
|
1316
|
+
}
|
|
1317
|
+
if (cand.length < 2) return soft;
|
|
1318
|
+
const BIN = HATCH_ANGLE_TOL, NBINS = Math.max(1, Math.round(180 / BIN));
|
|
1319
|
+
for (const shift of [0, BIN / 2]) {
|
|
1320
|
+
const bins = /* @__PURE__ */ new Map();
|
|
1321
|
+
for (const c of cand) {
|
|
1322
|
+
let b = Math.floor((c.ang + shift) / BIN);
|
|
1323
|
+
if (b >= NBINS) b -= NBINS;
|
|
1324
|
+
const th = c.ang * Math.PI / 180;
|
|
1325
|
+
const dxu = Math.cos(th), dyu = Math.sin(th);
|
|
1326
|
+
const nxu = -dyu, nyu = dxu;
|
|
1327
|
+
const r = {
|
|
1328
|
+
i: c.i,
|
|
1329
|
+
ang: c.ang,
|
|
1330
|
+
d: (c.x1 + c.x2) / 2 * nxu + (c.y1 + c.y2) / 2 * nyu,
|
|
1331
|
+
t0: Math.min(c.x1 * dxu + c.y1 * dyu, c.x2 * dxu + c.y2 * dyu),
|
|
1332
|
+
t1: Math.max(c.x1 * dxu + c.y1 * dyu, c.x2 * dxu + c.y2 * dyu),
|
|
1333
|
+
w: c.w
|
|
1334
|
+
};
|
|
1335
|
+
const arr2 = bins.get(b);
|
|
1336
|
+
if (arr2) arr2.push(r);
|
|
1337
|
+
else bins.set(b, [r]);
|
|
1338
|
+
}
|
|
1339
|
+
for (const runs of bins.values()) {
|
|
1340
|
+
if (runs.length < 2) continue;
|
|
1341
|
+
runs.sort((a, b) => a.d - b.d);
|
|
1342
|
+
for (let k = 0; k < runs.length; k++) {
|
|
1343
|
+
const c = runs[k];
|
|
1344
|
+
if (soft[c.i]) continue;
|
|
1345
|
+
const need = ANNOT_OVERLAP_FRAC * (c.t1 - c.t0);
|
|
1346
|
+
const alongside = (dir) => {
|
|
1347
|
+
for (let step = 1, j = k + dir; step <= ANNOT_MAX_SCAN && j >= 0 && j < runs.length; step++, j += dir) {
|
|
1348
|
+
const o = runs[j];
|
|
1349
|
+
const gap = Math.abs(o.d - c.d);
|
|
1350
|
+
if (gap > maxOffsetPx) break;
|
|
1351
|
+
if (gap < minOffsetPx) continue;
|
|
1352
|
+
if (Math.min(o.t1, c.t1) - Math.max(o.t0, c.t0) < need) continue;
|
|
1353
|
+
return o;
|
|
1354
|
+
}
|
|
1355
|
+
return null;
|
|
1356
|
+
};
|
|
1357
|
+
const up = alongside(1), dn = alongside(-1);
|
|
1358
|
+
const heavier = (o, other) => !!o && !other && o.w >= c.w + ANNOT_MIN_PEN_STEP;
|
|
1359
|
+
if (heavier(up, dn) || heavier(dn, up)) soft[c.i] = 1;
|
|
1360
|
+
}
|
|
1361
|
+
}
|
|
1362
|
+
}
|
|
1363
|
+
return soft;
|
|
1364
|
+
}
|
|
1295
1365
|
var HATCH_ID_ANGLE_Q = 0.5;
|
|
1296
1366
|
var HATCH_ID_PITCH_Q = 0.1;
|
|
1297
1367
|
function hatchFamilies(segs, meta) {
|
|
@@ -1335,6 +1405,10 @@ function buildMask(segs, imgW, imgH, maxDim = MASK_MAX_DIM, meta = null, pxPerFt
|
|
|
1335
1405
|
const mask = new Uint8Array(mw * mh);
|
|
1336
1406
|
const mppf = Number.isFinite(pxPerFt) && pxPerFt > 0 ? pxPerFt * ws : 0;
|
|
1337
1407
|
const soft = meta ? classifyHatchSegs(segs, meta, ws, mppf > 0 ? HATCH_MAX_PITCH_FT * mppf : HATCH_MAX_PITCH) : null;
|
|
1408
|
+
const annot = meta && mppf > 0 ? classifyOffsetAnnotationSegs(segs, meta, ws, ANNOT_OFFSET_MAX_FT * mppf, ANNOT_OFFSET_MIN_FT * mppf, ANNOT_MIN_LEN_FT * mppf) : null;
|
|
1409
|
+
if (soft && annot) {
|
|
1410
|
+
for (let i = 0; i < soft.length; i++) if (annot[i]) soft[i] = 1;
|
|
1411
|
+
}
|
|
1338
1412
|
const noDoor = meta ? flagNonDoorArcs(segs, meta) : null;
|
|
1339
1413
|
let softCount = 0;
|
|
1340
1414
|
for (let i = 0, si = 0; i + 3 < segs.length; i += 4, si++) {
|
|
@@ -1779,6 +1853,123 @@ function boundaryCurveClusters(mo, region) {
|
|
|
1779
1853
|
}
|
|
1780
1854
|
return clusters;
|
|
1781
1855
|
}
|
|
1856
|
+
var SPLIT_GAP = 1;
|
|
1857
|
+
var SPLIT_MIN_SEED = 8;
|
|
1858
|
+
var SAME_ARC_CENTRE_FT = 0.5;
|
|
1859
|
+
var SAME_ARC_RADIUS_FRAC = 0.15;
|
|
1860
|
+
function splitMergedArcs(cl, mw, mask, mppf) {
|
|
1861
|
+
if (cl.length < 2 * SPLIT_MIN_SEED) return [cl];
|
|
1862
|
+
if (arcClusterFit(cl, mw, mask).good) return [cl];
|
|
1863
|
+
const set = new Set(cl);
|
|
1864
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1865
|
+
const parts = [];
|
|
1866
|
+
for (const start of cl) {
|
|
1867
|
+
if (seen.has(start)) continue;
|
|
1868
|
+
seen.add(start);
|
|
1869
|
+
const comp = [start];
|
|
1870
|
+
const stack = [start];
|
|
1871
|
+
while (stack.length) {
|
|
1872
|
+
const i = stack.pop();
|
|
1873
|
+
const y = i / mw | 0, x = i - y * mw;
|
|
1874
|
+
for (let dy = -SPLIT_GAP; dy <= SPLIT_GAP; dy++) {
|
|
1875
|
+
for (let dx = -SPLIT_GAP; dx <= SPLIT_GAP; dx++) {
|
|
1876
|
+
if (!dx && !dy) continue;
|
|
1877
|
+
const j = (y + dy) * mw + (x + dx);
|
|
1878
|
+
if (set.has(j) && !seen.has(j)) {
|
|
1879
|
+
seen.add(j);
|
|
1880
|
+
comp.push(j);
|
|
1881
|
+
stack.push(j);
|
|
1882
|
+
}
|
|
1883
|
+
}
|
|
1884
|
+
}
|
|
1885
|
+
}
|
|
1886
|
+
parts.push(comp);
|
|
1887
|
+
}
|
|
1888
|
+
if (parts.length < 2) return [cl];
|
|
1889
|
+
const centreTol = mppf > 0 ? Math.max(2, SAME_ARC_CENTRE_FT * mppf) : 4;
|
|
1890
|
+
const groups = [];
|
|
1891
|
+
const orphans = [];
|
|
1892
|
+
for (const p of parts.slice().sort((a, b) => b.length - a.length)) {
|
|
1893
|
+
const f = p.length >= SPLIT_MIN_SEED ? arcClusterFit(p, mw, mask) : null;
|
|
1894
|
+
const claim = f && f.good ? groups.find((gp) => Math.hypot(gp.cx - f.cx, gp.cy - f.cy) <= centreTol && Math.abs(gp.r - f.r) <= SAME_ARC_RADIUS_FRAC * Math.max(gp.r, f.r)) : void 0;
|
|
1895
|
+
if (claim) {
|
|
1896
|
+
claim.cells.push(...p);
|
|
1897
|
+
continue;
|
|
1898
|
+
}
|
|
1899
|
+
if (f && f.good) {
|
|
1900
|
+
groups.push({ cx: f.cx, cy: f.cy, r: f.r, cells: p.slice() });
|
|
1901
|
+
continue;
|
|
1902
|
+
}
|
|
1903
|
+
orphans.push(...p);
|
|
1904
|
+
}
|
|
1905
|
+
if (groups.length < 2) return [cl];
|
|
1906
|
+
const out = groups.map((gp) => gp.cells);
|
|
1907
|
+
if (orphans.length) out.push(orphans);
|
|
1908
|
+
return out;
|
|
1909
|
+
}
|
|
1910
|
+
var LEAF_MIN_SECTOR_FRAC = 0.15;
|
|
1911
|
+
var LEAF_MIN_COVER = 0.55;
|
|
1912
|
+
var LEAF_HALF_FT = 0.3;
|
|
1913
|
+
var LEAF_HALF_FLOOR_PX = 1.5;
|
|
1914
|
+
var LEAF_T0 = 0.2;
|
|
1915
|
+
var LEAF_T1 = 0.95;
|
|
1916
|
+
function doorLeafCells(fit, cl, mw, mh, mask, mppf = 0) {
|
|
1917
|
+
if (!fit.good || !(fit.r > 2)) return null;
|
|
1918
|
+
const LEAF_HALF_W = mppf > 0 ? Math.max(LEAF_HALF_FLOOR_PX, LEAF_HALF_FT * mppf) : LEAF_HALF_FLOOR_PX;
|
|
1919
|
+
const angs = cl.map((i) => Math.atan2((i / mw | 0) - fit.cy, i % mw - fit.cx)).sort((a, b) => a - b);
|
|
1920
|
+
if (angs.length < 2) return null;
|
|
1921
|
+
let gapAt = -1, gapMax = angs[0] + 2 * Math.PI - angs[angs.length - 1];
|
|
1922
|
+
for (let k = 1; k < angs.length; k++) {
|
|
1923
|
+
const gp = angs[k] - angs[k - 1];
|
|
1924
|
+
if (gp > gapMax) {
|
|
1925
|
+
gapMax = gp;
|
|
1926
|
+
gapAt = k;
|
|
1927
|
+
}
|
|
1928
|
+
}
|
|
1929
|
+
const ends = gapAt < 0 ? [angs[0], angs[angs.length - 1]] : [angs[gapAt], angs[gapAt - 1]];
|
|
1930
|
+
let best = null, bestCover = LEAF_MIN_COVER;
|
|
1931
|
+
for (const th of ends) {
|
|
1932
|
+
const ux = Math.cos(th), uy = Math.sin(th);
|
|
1933
|
+
const ax = fit.cx + ux * fit.r * LEAF_T0, ay = fit.cy + uy * fit.r * LEAF_T0;
|
|
1934
|
+
const bx = fit.cx + ux * fit.r * LEAF_T1, by = fit.cy + uy * fit.r * LEAF_T1;
|
|
1935
|
+
const x0 = Math.max(0, Math.floor(Math.min(ax, bx) - LEAF_HALF_W - 1));
|
|
1936
|
+
const x1 = Math.min(mw - 1, Math.ceil(Math.max(ax, bx) + LEAF_HALF_W + 1));
|
|
1937
|
+
const y0 = Math.max(0, Math.floor(Math.min(ay, by) - LEAF_HALF_W - 1));
|
|
1938
|
+
const y1 = Math.min(mh - 1, Math.ceil(Math.max(ay, by) + LEAF_HALF_W + 1));
|
|
1939
|
+
const dx = bx - ax, dy = by - ay, L2 = dx * dx + dy * dy;
|
|
1940
|
+
if (!(L2 > 0)) continue;
|
|
1941
|
+
const cells = [];
|
|
1942
|
+
for (let y = y0; y <= y1; y++) {
|
|
1943
|
+
for (let x = x0; x <= x1; x++) {
|
|
1944
|
+
let t = ((x - ax) * dx + (y - ay) * dy) / L2;
|
|
1945
|
+
t = t < 0 ? 0 : t > 1 ? 1 : t;
|
|
1946
|
+
const ex = ax + t * dx - x, ey = ay + t * dy - y;
|
|
1947
|
+
if (ex * ex + ey * ey > LEAF_HALF_W * LEAF_HALF_W) continue;
|
|
1948
|
+
const i = y * mw + x;
|
|
1949
|
+
if (mask[i] & 1 && !(mask[i] & MASK_CURVE_BIT)) cells.push(i);
|
|
1950
|
+
}
|
|
1951
|
+
}
|
|
1952
|
+
let hit = 0, tries = 0;
|
|
1953
|
+
for (let t = 0; t <= 1; t += 0.02) {
|
|
1954
|
+
tries++;
|
|
1955
|
+
const px = ax + t * dx, py = ay + t * dy;
|
|
1956
|
+
let inked = false;
|
|
1957
|
+
for (let o = -LEAF_HALF_W; o <= LEAF_HALF_W && !inked; o += 0.5) {
|
|
1958
|
+
const x = Math.round(px - dy / Math.sqrt(L2) * o), y = Math.round(py + dx / Math.sqrt(L2) * o);
|
|
1959
|
+
if (x < 0 || y < 0 || x >= mw || y >= mh) continue;
|
|
1960
|
+
const i = y * mw + x;
|
|
1961
|
+
if (mask[i] & 1 && !(mask[i] & MASK_CURVE_BIT)) inked = true;
|
|
1962
|
+
}
|
|
1963
|
+
if (inked) hit++;
|
|
1964
|
+
}
|
|
1965
|
+
const cover = tries ? hit / tries : 0;
|
|
1966
|
+
if (cover > bestCover && cells.length) {
|
|
1967
|
+
bestCover = cover;
|
|
1968
|
+
best = cells;
|
|
1969
|
+
}
|
|
1970
|
+
}
|
|
1971
|
+
return best && best.length ? best : null;
|
|
1972
|
+
}
|
|
1782
1973
|
function arcClusterFit(cl, mw, mask) {
|
|
1783
1974
|
const m = cl.length;
|
|
1784
1975
|
const X = (i) => i % mw, Y = (i) => i / mw | 0;
|
|
@@ -2009,10 +2200,33 @@ function floodRegionSealedInner(mo, ix, iy, sensitivity, radii, wedgeCapPx, minP
|
|
|
2009
2200
|
let hatchTier = r1.hatchTier;
|
|
2010
2201
|
let sealedPx = r1.sealedPx, virtualFrac = r1.virtualFrac;
|
|
2011
2202
|
let minPassPxOut = r1.minPassPx, minPassDelta = r1.minPassDelta;
|
|
2012
|
-
const
|
|
2203
|
+
const entry = (cl, pass, fit) => {
|
|
2204
|
+
const f = fit ?? arcClusterFit(cl, mw, mo.mask);
|
|
2205
|
+
const ideal = 0.5 * f.r * f.r * f.sweep;
|
|
2206
|
+
return {
|
|
2207
|
+
cl,
|
|
2208
|
+
fit: f,
|
|
2209
|
+
allow: wedgeAllowance(f, mo.mppf || 0, wedgeCapPx),
|
|
2210
|
+
rank: doorLikeness(f, mo.mppf || 0),
|
|
2211
|
+
at: cl[0],
|
|
2212
|
+
pass,
|
|
2213
|
+
minGrow: pass === 2 ? Math.max(1, Math.round(LEAF_MIN_SECTOR_FRAC * ideal)) : 0
|
|
2214
|
+
};
|
|
2215
|
+
};
|
|
2216
|
+
const ranked = [];
|
|
2217
|
+
for (const cl of clusters) {
|
|
2013
2218
|
const fit = arcClusterFit(cl, mw, mo.mask);
|
|
2014
|
-
|
|
2015
|
-
|
|
2219
|
+
ranked.push(entry(cl, 0, fit));
|
|
2220
|
+
const parts = splitMergedArcs(cl, mw, mo.mask, mo.mppf || 0);
|
|
2221
|
+
const pieces = parts.length > 1 ? parts : [cl];
|
|
2222
|
+
if (parts.length > 1) for (const p of parts) ranked.push(entry(p, 1));
|
|
2223
|
+
for (const p of pieces) {
|
|
2224
|
+
const pf = p === cl ? fit : arcClusterFit(p, mw, mo.mask);
|
|
2225
|
+
const leaf = doorLeafCells(pf, p, mw, mh, mo.mask, mo.mppf || 0);
|
|
2226
|
+
if (leaf) ranked.push({ ...entry(leaf, 2, pf), cl: leaf });
|
|
2227
|
+
}
|
|
2228
|
+
}
|
|
2229
|
+
ranked.splice(0, ranked.length, ...ranked.filter((c) => c.allow >= 1).sort((a, b) => a.pass - b.pass || b.rank - a.rank || a.at - b.at));
|
|
2016
2230
|
let m2mask = null, m2dt = null;
|
|
2017
2231
|
let openedCl = null, dirty = null;
|
|
2018
2232
|
const rb1 = boxOf(r1.region, mw, mh);
|
|
@@ -2022,7 +2236,7 @@ function floodRegionSealedInner(mo, ix, iy, sensitivity, radii, wedgeCapPx, minP
|
|
|
2022
2236
|
sealCache.set(mo.mask, base);
|
|
2023
2237
|
}
|
|
2024
2238
|
const baseDT = base.dt;
|
|
2025
|
-
for (const { cl, fit, allow: clusterAllowance } of ranked.slice(0, WEDGE_MAX_DOORS)) {
|
|
2239
|
+
for (const { cl, fit, allow: clusterAllowance, minGrow } of ranked.slice(0, 2 * WEDGE_MAX_DOORS)) {
|
|
2026
2240
|
if (!m2mask) {
|
|
2027
2241
|
m2mask = mo.mask.slice();
|
|
2028
2242
|
m2dt = baseDT.slice();
|
|
@@ -2056,6 +2270,7 @@ function floodRegionSealedInner(mo, ix, iy, sensitivity, radii, wedgeCapPx, minP
|
|
|
2056
2270
|
const r2 = sealAttempt(m2, sx, sy, sensitivity, radii, minPassPx, sc2);
|
|
2057
2271
|
if (r2.status !== "ok" || r2.count <= r1.count) continue;
|
|
2058
2272
|
const growth = r2.count - r1.count;
|
|
2273
|
+
if (growth < minGrow) continue;
|
|
2059
2274
|
if (growth > clusterAllowance) continue;
|
|
2060
2275
|
if (count - r1.count + growth > globalAllowance) continue;
|
|
2061
2276
|
if (!region) {
|
|
@@ -8945,7 +9160,7 @@ function registerResources(server, session) {
|
|
|
8945
9160
|
// package.json
|
|
8946
9161
|
var package_default = {
|
|
8947
9162
|
name: "opentakeoff-mcp",
|
|
8948
|
-
version: "0.9.
|
|
9163
|
+
version: "0.9.29",
|
|
8949
9164
|
mcpName: "io.github.Kentucky-ai/opentakeoff",
|
|
8950
9165
|
type: "module",
|
|
8951
9166
|
description: "OpenTakeoff MCP server \u2014 drive the takeoff engine from your MCP client over stdio.",
|
package/package.json
CHANGED