opentakeoff-mcp 0.9.46 → 0.9.48

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.
Files changed (2) hide show
  1. package/dist/server-core.js +61 -32
  2. package/package.json +1 -1
@@ -618,6 +618,7 @@ var ARC_CUSP_SPAN_MULT = 8;
618
618
  var MASK_NODOOR_BIT = 8;
619
619
  var DOOR_R_MIN_FT = 1.5;
620
620
  var DOOR_R_MAX_FT = 4.5;
621
+ var DOOR_R_LEAF_MAX_FT = 7;
621
622
  var CLUSTER_FIT_TOL_FRAC = 0.05;
622
623
  var CLUSTER_FIT_TOL_PX = 1.5;
623
624
  var HATCH_ANGLE_TOL = 2;
@@ -2065,9 +2066,10 @@ function wedgeRimPx(maskPxPerFt) {
2065
2066
  if (!Number.isFinite(maskPxPerFt) || maskPxPerFt <= 0) return 3;
2066
2067
  return Math.max(3, Math.round(WEDGE_RIM_FT * maskPxPerFt));
2067
2068
  }
2068
- function wedgeAllowance(fit, mppf, wedgeCapPx) {
2069
+ function wedgeAllowance(fit, mppf, wedgeCapPx, leafFound = false) {
2069
2070
  if (fit.noDoorFrac > 0.5 && !fit.good) return 0;
2070
- if (fit.good && mppf > 0 && fit.r / mppf > DOOR_R_MAX_FT) return 0;
2071
+ const rMax = leafFound ? DOOR_R_LEAF_MAX_FT : DOOR_R_MAX_FT;
2072
+ if (fit.good && mppf > 0 && fit.r / mppf > rMax) return 0;
2071
2073
  const rim = wedgeRimPx(mppf);
2072
2074
  const bu = fit.good ? fit.buH : fit.bu, bn = fit.good ? fit.bnH : fit.bn;
2073
2075
  let area = bu * bn;
@@ -2075,12 +2077,13 @@ function wedgeAllowance(fit, mppf, wedgeCapPx) {
2075
2077
  area += 2 * rim * (bu + bn) + 4 * rim * rim;
2076
2078
  return Math.min(2 * wedgeCapPx, Math.round(area * WEDGE_SLACK));
2077
2079
  }
2078
- function doorLikeness(fit, mppf) {
2080
+ function doorLikeness(fit, mppf, leafFound = false) {
2079
2081
  let s = 1 - fit.noDoorFrac;
2080
2082
  const swDeg = fit.sweep * 180 / Math.PI;
2081
2083
  if (fit.good) {
2082
2084
  s += 1;
2083
- if (mppf > 0 && fit.r / mppf >= DOOR_R_MIN_FT && fit.r / mppf <= DOOR_R_MAX_FT) s += 4;
2085
+ const rMax = leafFound ? DOOR_R_LEAF_MAX_FT : DOOR_R_MAX_FT;
2086
+ if (mppf > 0 && fit.r / mppf >= DOOR_R_MIN_FT && fit.r / mppf <= rMax) s += 4;
2084
2087
  if (swDeg >= 45 && swDeg <= 190) s += 2;
2085
2088
  }
2086
2089
  return s;
@@ -2211,19 +2214,21 @@ function floodRegionSealedInner(mo, ix, iy, sensitivity, radii, wedgeCapPx, minP
2211
2214
  const { mw, mh } = mo;
2212
2215
  let region = null;
2213
2216
  let count = r1.count;
2214
- let wedges = 0, ringWedges = 0;
2217
+ let wedges = 0, ringWedges = 0, refused = 0;
2215
2218
  let hatchFiltered = !!r1.hatchFiltered;
2216
2219
  let hatchTier = r1.hatchTier;
2217
2220
  let sealedPx = r1.sealedPx, virtualFrac = r1.virtualFrac;
2218
2221
  let minPassPxOut = r1.minPassPx, minPassDelta = r1.minPassDelta;
2219
- const entry = (cl, pass, fit) => {
2222
+ const entry = (cl, pass, fit, leafFound = false) => {
2220
2223
  const f = fit ?? arcClusterFit(cl, mw, mo.mask);
2221
2224
  const ideal = 0.5 * f.r * f.r * f.sweep;
2225
+ const allow = wedgeAllowance(f, mo.mppf || 0, wedgeCapPx, leafFound);
2226
+ if (!allow && f.good && (mo.mppf || 0) > 0 && f.r / mo.mppf > (leafFound ? DOOR_R_LEAF_MAX_FT : DOOR_R_MAX_FT)) refused++;
2222
2227
  return {
2223
2228
  cl,
2224
2229
  fit: f,
2225
- allow: wedgeAllowance(f, mo.mppf || 0, wedgeCapPx),
2226
- rank: doorLikeness(f, mo.mppf || 0),
2230
+ allow,
2231
+ rank: doorLikeness(f, mo.mppf || 0, leafFound),
2227
2232
  at: cl[0],
2228
2233
  pass,
2229
2234
  minGrow: pass === 2 ? Math.max(1, Math.round(LEAF_MIN_SECTOR_FRAC * ideal)) : 0
@@ -2232,15 +2237,16 @@ function floodRegionSealedInner(mo, ix, iy, sensitivity, radii, wedgeCapPx, minP
2232
2237
  const ranked = [];
2233
2238
  for (const cl of clusters) {
2234
2239
  const fit = arcClusterFit(cl, mw, mo.mask);
2235
- ranked.push(entry(cl, 0, fit));
2236
2240
  const parts = splitMergedArcs(cl, mw, mo.mask, mo.mppf || 0);
2237
2241
  const pieces = parts.length > 1 ? parts : [cl];
2238
- if (parts.length > 1) for (const p of parts) ranked.push(entry(p, 1));
2239
- for (const p of pieces) {
2242
+ const leaves = pieces.map((p) => {
2240
2243
  const pf = p === cl ? fit : arcClusterFit(p, mw, mo.mask);
2241
- const leaf = doorLeafCells(pf, p, mw, mh, mo.mask, mo.mppf || 0);
2242
- if (leaf) ranked.push({ ...entry(leaf, 2, pf), cl: leaf });
2243
- }
2244
+ return { p, pf, leaf: doorLeafCells(pf, p, mw, mh, mo.mask, mo.mppf || 0) };
2245
+ });
2246
+ const anyLeaf = leaves.some((l) => !!l.leaf);
2247
+ ranked.push(entry(cl, 0, fit, anyLeaf));
2248
+ if (parts.length > 1) for (const { p, pf, leaf } of leaves) ranked.push(entry(p, 1, pf, !!leaf));
2249
+ for (const { pf, leaf } of leaves) if (leaf) ranked.push({ ...entry(leaf, 2, pf, true), cl: leaf });
2244
2250
  }
2245
2251
  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));
2246
2252
  let m2mask = null, m2dt = null;
@@ -2317,7 +2323,10 @@ function floodRegionSealedInner(mo, ix, iy, sensitivity, radii, wedgeCapPx, minP
2317
2323
  if (r2.minPassPx && (!minPassPxOut || r2.minPassPx > minPassPxOut)) minPassPxOut = r2.minPassPx;
2318
2324
  if (r2.minPassDelta != null && (minPassDelta == null || r2.minPassDelta > minPassDelta)) minPassDelta = r2.minPassDelta;
2319
2325
  }
2320
- if (!wedges || !region) return r1;
2326
+ if (!wedges || !region) {
2327
+ if (refused) r1.wedgesRefused = refused;
2328
+ return r1;
2329
+ }
2321
2330
  const out = {
2322
2331
  status: "ok",
2323
2332
  region,
@@ -2334,7 +2343,8 @@ function floodRegionSealedInner(mo, ix, iy, sensitivity, radii, wedgeCapPx, minP
2334
2343
  virtualFrac,
2335
2344
  minPassPx: minPassPxOut,
2336
2345
  minPassDelta,
2337
- ...ringWedges ? { ringWedges } : {}
2346
+ ...ringWedges ? { ringWedges } : {},
2347
+ ...refused ? { wedgesRefused: refused } : {}
2338
2348
  };
2339
2349
  const reg = region, reg1 = r1.region, mask = mo.mask;
2340
2350
  const isDelta = (i) => reg[i] && !reg1[i];
@@ -2678,11 +2688,13 @@ function clusterRows(spans) {
2678
2688
  }
2679
2689
  var rowY = (r) => r.reduce((s, t) => s + t.y, 0) / r.length;
2680
2690
  var SURFACE_WORDS = /* @__PURE__ */ new Set(["FLOOR", "BASE", "WALL", "WALLS", "CEILING", "NORTH", "SOUTH", "EAST", "WEST", "WAINSCOT"]);
2681
- var ROOM_HEADERS = ["ROOM", "NO", "NUMBER", "NAME", "MARK", "LOCATION", "FLOOR", "BASE", "WALL", "WALLS", "NORTH", "SOUTH", "EAST", "WEST", "CEILING", "WAINSCOT", "REMARKS", "CLG", "HT", "HEIGHT", "FINISH", "BLDG", "BUILDING"];
2691
+ var ROOM_HEADERS = ["ROOM", "NO", "NUMBER", "NAME", "MARK", "LOCATION", "FLOOR", "BASE", "WALL", "WALLS", "NORTH", "SOUTH", "EAST", "WEST", "CEILING", "WAINSCOT", "REMARKS", "CLG", "HT", "HEIGHT", "FINISH", "CASEWORK", "CABINET", "COUNTER", "COUNTERTOP", "BLDG", "BUILDING"];
2682
2692
  var FINISH_HEADERS = ["CODE", "MARK", "SYMBOL", "MATERIAL", "MANUFACTURER", "PRODUCT", "STYLE", "COLOR", "SIZE", "REMARKS", "DESCRIPTION", "PATTERN", "COMMENTS"];
2683
- var headerLabel = (s, vocab) => {
2684
- for (const w of norm(s).split(/[^A-Z]+/)) if (w && vocab.includes(w)) return w;
2685
- return null;
2693
+ var headerLabel = (s, vocab) => headerLabels(s, vocab)[0] ?? null;
2694
+ var headerLabels = (s, vocab) => {
2695
+ const out = [];
2696
+ for (const w of norm(s).split(/[^A-Z]+/)) if (w && vocab.includes(w) && !out.includes(w)) out.push(w);
2697
+ return out;
2686
2698
  };
2687
2699
  function headerHits(row, vocab) {
2688
2700
  const out = [];
@@ -2728,6 +2740,10 @@ function findHeaderRow(rows, vocab, required, minHits) {
2728
2740
  const parent = parentLabelOver(rows, idx, i, h.span.x, hi, vocab);
2729
2741
  if (parent && parent !== h.label) label = `${parent} ${h.label}`;
2730
2742
  }
2743
+ if (used.has(label)) {
2744
+ const alt = headerLabels(h.span.str, vocab).find((w) => !used.has(w));
2745
+ if (alt) label = alt;
2746
+ }
2731
2747
  if (used.has(label)) continue;
2732
2748
  used.add(label);
2733
2749
  anchors.push({ label, x: h.span.x + (h.span.w || 0) / 2 });
@@ -2877,14 +2893,15 @@ function rowKeyOf(raw, kind, buildings) {
2877
2893
  }
2878
2894
  var numOf = (key) => key.match(QUALIFIED_KEY_RE)?.[2] ?? key;
2879
2895
  var centerX = (t) => t.x + (t.w || 0) / 2;
2880
- function columnStarts(rows, anchors, cfg, x0, x1) {
2896
+ function columnMapFor(rows, anchors, cfg, x0, x1, coord) {
2897
+ const at = (t) => coord === "left" ? t.x : t.x + (t.w || 0) / 2;
2881
2898
  const xs = [];
2882
2899
  const hs = [];
2883
2900
  for (let i = Math.max(cfg.fromIdx, 0); i < rows.length; i++) {
2884
2901
  if (rowY(rows[i]) <= cfg.belowY) continue;
2885
2902
  for (const t of rows[i]) {
2886
2903
  if (t.x < x0 || t.x > x1 || revisionOf(t.str) != null) continue;
2887
- xs.push(t.x);
2904
+ xs.push(at(t));
2888
2905
  hs.push(t.h || 8);
2889
2906
  }
2890
2907
  }
@@ -2912,23 +2929,35 @@ function columnStarts(rows, anchors, cfg, x0, x1) {
2912
2929
  if (cur == null || c.start < cur) byLabel.set(own.label, c.start);
2913
2930
  }
2914
2931
  if (byLabel.size !== anchors.length) return null;
2915
- const named = [...byLabel.entries()].map(([label, start]) => ({ label, start })).sort((a, b) => a.start - b.start);
2916
- const order = anchors.map((a) => a.label).join("|");
2917
- if (named.map((n) => n.label).join("|") !== order) return null;
2918
- return named;
2932
+ const cols = [...byLabel.entries()].map(([label, start]) => ({ label, start })).sort((a, b) => a.start - b.start);
2933
+ if (cols.map((c) => c.label).join("|") !== anchors.map((a) => a.label).join("|")) return null;
2934
+ const starts = kept.map((c) => c.start);
2935
+ let on = 0;
2936
+ for (const x of xs) if (starts.some((st) => Math.abs(x - st) <= tol)) on++;
2937
+ return { coord, cols, score: on / xs.length };
2938
+ }
2939
+ function columnStarts(rows, anchors, cfg, x0, x1) {
2940
+ const FIT_FLOOR = 0.7;
2941
+ const fits = (m) => m && m.score >= FIT_FLOOR ? m : null;
2942
+ const left = fits(columnMapFor(rows, anchors, cfg, x0, x1, "left"));
2943
+ const center = fits(columnMapFor(rows, anchors, cfg, x0, x1, "center"));
2944
+ if (!left) return center;
2945
+ if (!center) return left;
2946
+ return center.score > left.score + 0.05 ? center : left;
2919
2947
  }
2920
2948
  function bandDataRows(rows, anchors, kind, sheetKey, buildings, cfg) {
2921
2949
  const { x0, x1, medGap } = bandLimits(anchors);
2922
2950
  const cols = columnStarts(rows, anchors, cfg, x0, x1);
2923
- const keyTol = cols && cols.length > 1 ? Math.max(8, (cols[1].start - cols[0].start) * 0.5) : 40;
2951
+ const keyTol = cols && cols.cols.length > 1 ? Math.max(8, (cols.cols[1].start - cols.cols[0].start) * 0.5) : 40;
2924
2952
  const out = [];
2925
2953
  const outY = [];
2926
2954
  let region = null;
2927
2955
  const columnOf = (t) => {
2928
2956
  if (!cols) return nearestAnchor(centerX(t), anchors);
2929
- let label = cols[0].label;
2930
- for (const c of cols) {
2931
- if (t.x + 1 >= c.start) label = c.label;
2957
+ const at = cols.coord === "left" ? t.x : centerX(t);
2958
+ let label = cols.cols[0].label;
2959
+ for (const c of cols.cols) {
2960
+ if (at + 1 >= c.start) label = c.label;
2932
2961
  else break;
2933
2962
  }
2934
2963
  return label;
@@ -2962,7 +2991,7 @@ function bandDataRows(rows, anchors, kind, sheetKey, buildings, cfg) {
2962
2991
  orphans.push({ toks: banded, y: rowY(rows[i]) });
2963
2992
  continue;
2964
2993
  }
2965
- if (cols && Math.abs(banded[0].x - cols[0].start) > keyTol) continue;
2994
+ if (cols && Math.abs((cols.coord === "left" ? banded[0].x : centerX(banded[0])) - cols.cols[0].start) > keyTol) continue;
2966
2995
  if (cfg.keyAlign && Math.abs(centerX(banded[0]) - cfg.keyAlign.x) > cfg.keyAlign.tol) continue;
2967
2996
  const row = { key: keyed.key, sheet: sheetKey, cells: {} };
2968
2997
  if (keyed.building) row.building = keyed.building;
@@ -11100,7 +11129,7 @@ function applyStagedTools(server, registered) {
11100
11129
  // package.json
11101
11130
  var package_default = {
11102
11131
  name: "opentakeoff-mcp",
11103
- version: "0.9.46",
11132
+ version: "0.9.48",
11104
11133
  mcpName: "io.github.Kentucky-ai/opentakeoff",
11105
11134
  type: "module",
11106
11135
  description: "OpenTakeoff MCP server \u2014 drive the takeoff engine from your MCP client over stdio.",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opentakeoff-mcp",
3
- "version": "0.9.46",
3
+ "version": "0.9.48",
4
4
  "mcpName": "io.github.Kentucky-ai/opentakeoff",
5
5
  "type": "module",
6
6
  "description": "OpenTakeoff MCP server — drive the takeoff engine from your MCP client over stdio.",