opentakeoff-mcp 0.9.44 → 0.9.45
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 +80 -10
- package/package.json +1 -1
package/dist/server-core.js
CHANGED
|
@@ -2678,7 +2678,7 @@ function clusterRows(spans) {
|
|
|
2678
2678
|
}
|
|
2679
2679
|
var rowY = (r) => r.reduce((s, t) => s + t.y, 0) / r.length;
|
|
2680
2680
|
var ROOM_HEADERS = ["ROOM", "NO", "NUMBER", "NAME", "FLOOR", "BASE", "WALL", "WALLS", "NORTH", "SOUTH", "EAST", "WEST", "CEILING", "WAINSCOT", "REMARKS", "CLG", "HT", "BLDG", "BUILDING"];
|
|
2681
|
-
var FINISH_HEADERS = ["CODE", "MARK", "MATERIAL", "MANUFACTURER", "PRODUCT", "STYLE", "COLOR", "SIZE", "REMARKS", "DESCRIPTION", "PATTERN"];
|
|
2681
|
+
var FINISH_HEADERS = ["CODE", "MARK", "SYMBOL", "MATERIAL", "MANUFACTURER", "PRODUCT", "STYLE", "COLOR", "SIZE", "REMARKS", "DESCRIPTION", "PATTERN", "COMMENTS"];
|
|
2682
2682
|
var headerLabel = (s, vocab) => {
|
|
2683
2683
|
for (const w of norm(s).split(/[^A-Z]+/)) if (w && vocab.includes(w)) return w;
|
|
2684
2684
|
return null;
|
|
@@ -2695,10 +2695,57 @@ function findHeaderRow(rows, vocab, required, minHits) {
|
|
|
2695
2695
|
}
|
|
2696
2696
|
}
|
|
2697
2697
|
if (anchors.length < minHits || !required.some((r) => seen.has(r))) continue;
|
|
2698
|
-
return { anchors: anchors.sort((a, b) => a.x - b.x), rowIndex: i };
|
|
2698
|
+
return { anchors: subTierAnchors(rows, i, anchors.sort((a, b) => a.x - b.x), vocab), rowIndex: i };
|
|
2699
2699
|
}
|
|
2700
2700
|
return null;
|
|
2701
2701
|
}
|
|
2702
|
+
var SUB_LABEL_RE = /^[A-Z0-9][A-Z0-9.\/-]{0,5}$/;
|
|
2703
|
+
function parentLabelOver(rows, hdrIdx, gx0, gx1, vocab) {
|
|
2704
|
+
const width = Math.max(gx1 - gx0, 1);
|
|
2705
|
+
for (let j = hdrIdx - 1; j >= 0 && j >= hdrIdx - 2; j--) {
|
|
2706
|
+
for (const t of rows[j]) {
|
|
2707
|
+
if (Math.min(t.x + (t.w || 0), gx1) - Math.max(t.x, gx0) <= width * 0.3) continue;
|
|
2708
|
+
const lbl = headerLabel(t.str, vocab);
|
|
2709
|
+
if (lbl) return lbl;
|
|
2710
|
+
}
|
|
2711
|
+
}
|
|
2712
|
+
return null;
|
|
2713
|
+
}
|
|
2714
|
+
function subTierAnchors(rows, hdrIdx, anchors, vocab) {
|
|
2715
|
+
const lo = anchors[0].x, hi = anchors[anchors.length - 1].x;
|
|
2716
|
+
const loose = rows[hdrIdx].filter((t) => !headerLabel(t.str, vocab) && SUB_LABEL_RE.test(norm(t.str))).filter((t) => t.x + (t.w || 0) / 2 > lo && t.x + (t.w || 0) / 2 < hi).sort((a, b) => a.x - b.x);
|
|
2717
|
+
if (loose.length < 2) return anchors;
|
|
2718
|
+
const mid = (t) => t.x + (t.w || 0) / 2;
|
|
2719
|
+
const gaps = loose.slice(1).map((t, i) => mid(t) - mid(loose[i])).sort((a, b) => a - b);
|
|
2720
|
+
const med = gaps[gaps.length >> 1] || 1;
|
|
2721
|
+
const runs = [];
|
|
2722
|
+
let run2 = [loose[0]];
|
|
2723
|
+
for (let i = 1; i < loose.length; i++) {
|
|
2724
|
+
if (mid(loose[i]) - mid(loose[i - 1]) > med * 3) {
|
|
2725
|
+
runs.push(run2);
|
|
2726
|
+
run2 = [];
|
|
2727
|
+
}
|
|
2728
|
+
run2.push(loose[i]);
|
|
2729
|
+
}
|
|
2730
|
+
runs.push(run2);
|
|
2731
|
+
const out = anchors.slice();
|
|
2732
|
+
const used = new Set(anchors.map((a) => a.label));
|
|
2733
|
+
for (const r of runs) {
|
|
2734
|
+
if (r.length < 2) continue;
|
|
2735
|
+
const last = r[r.length - 1];
|
|
2736
|
+
const parent = parentLabelOver(rows, hdrIdx, r[0].x, last.x + (last.w || 0), vocab);
|
|
2737
|
+
if (!parent) continue;
|
|
2738
|
+
const pitch = r.length > 1 ? r.slice(1).map((t, i) => mid(t) - mid(r[i])).sort((a, b) => a - b)[r.length - 1 >> 1] : 0;
|
|
2739
|
+
for (const t of r) {
|
|
2740
|
+
const label = `${parent} ${norm(t.str)}`;
|
|
2741
|
+
if (used.has(label)) continue;
|
|
2742
|
+
used.add(label);
|
|
2743
|
+
const c = mid(t);
|
|
2744
|
+
out.push(pitch > 0 ? { label, x: c, x0: c - pitch / 2, x1: c + pitch / 2 } : { label, x: c });
|
|
2745
|
+
}
|
|
2746
|
+
}
|
|
2747
|
+
return out.sort((a, b) => a.x - b.x);
|
|
2748
|
+
}
|
|
2702
2749
|
function findRotatedHeader(vert, vocab, required, minHits) {
|
|
2703
2750
|
const cands = vert.map((sp) => ({ sp, label: headerLabel(sp.str, vocab) })).filter((c) => !!c.label).sort((a, b) => a.sp.x - b.sp.x);
|
|
2704
2751
|
let band = [];
|
|
@@ -2733,18 +2780,35 @@ function findRotatedHeader(vert, vocab, required, minHits) {
|
|
|
2733
2780
|
return band.length ? flush() : null;
|
|
2734
2781
|
}
|
|
2735
2782
|
var nearestAnchor = (x, anchors) => {
|
|
2736
|
-
let
|
|
2737
|
-
for (const a of anchors)
|
|
2738
|
-
|
|
2783
|
+
let inside2 = null;
|
|
2784
|
+
for (const a of anchors) {
|
|
2785
|
+
if (a.x0 == null || a.x1 == null || x < a.x0 || x > a.x1) continue;
|
|
2786
|
+
if (!inside2 || Math.abs(a.x - x) < Math.abs(inside2.x - x)) inside2 = a;
|
|
2787
|
+
}
|
|
2788
|
+
if (inside2) return inside2.label;
|
|
2789
|
+
let best = null;
|
|
2790
|
+
for (const a of anchors) {
|
|
2791
|
+
if (a.x0 != null) continue;
|
|
2792
|
+
if (!best || Math.abs(a.x - x) < Math.abs(best.x - x)) best = a;
|
|
2793
|
+
}
|
|
2794
|
+
return (best ?? anchors[0]).label;
|
|
2739
2795
|
};
|
|
2796
|
+
var WIDE_LAST = /* @__PURE__ */ new Set(["REMARKS", "DESCRIPTION", "NOTES"]);
|
|
2740
2797
|
function bandLimits(anchors) {
|
|
2741
2798
|
const gaps = anchors.slice(1).map((a, i) => a.x - anchors[i].x).sort((a, b) => a - b);
|
|
2742
2799
|
const medGap = gaps.length ? gaps[gaps.length >> 1] : 150;
|
|
2743
|
-
|
|
2800
|
+
const last = anchors[anchors.length - 1];
|
|
2801
|
+
const rightMargin = WIDE_LAST.has(last.label) ? Math.max(300, medGap * 3) : Math.max(120, medGap);
|
|
2802
|
+
return { x0: anchors[0].x - Math.max(80, medGap / 2), x1: last.x + rightMargin, medGap };
|
|
2744
2803
|
}
|
|
2745
2804
|
var CODE_RE = /^[A-Z]{1,4}(-?[A-Z0-9]{1,4})?$/;
|
|
2746
2805
|
var ROW_KEY_RE = /^\d{1,3}[A-Z]{0,2}$/;
|
|
2747
2806
|
var QUALIFIED_KEY_RE = /^([A-Z]{1,2})-(\d{1,3}[A-Z]{0,2})$/;
|
|
2807
|
+
var OTHER_FAMILY_RE = /\b(DOOR|WINDOW|PARTITION|EQUIPMENT|HARDWARE|LOUVER|SIGNAGE|LIGHTING|LUMINAIRE|PLUMBING|MECHANICAL|ELECTRICAL|STOREFRONT|GLAZING|CASEWORK|MILLWORK|APPLIANCE)S?\b/;
|
|
2808
|
+
var isNonFinishSchedule = (title) => {
|
|
2809
|
+
const u = norm(title);
|
|
2810
|
+
return OTHER_FAMILY_RE.test(u) && !/\b(FINISH|MATERIAL)S?\b/.test(u);
|
|
2811
|
+
};
|
|
2748
2812
|
function rowKeyOf(raw, kind, buildings) {
|
|
2749
2813
|
const key = norm(raw).replace(/[^A-Z0-9-]/g, "");
|
|
2750
2814
|
if (kind === "finish") return CODE_RE.test(key) ? { key } : null;
|
|
@@ -2833,7 +2897,7 @@ function extractTable(sheet, kind, opts = {}) {
|
|
|
2833
2897
|
const vert = sheet.spans.filter(isVertical);
|
|
2834
2898
|
const rows = clusterRows(horiz);
|
|
2835
2899
|
const vocab = kind === "room-finish" ? ROOM_HEADERS : FINISH_HEADERS;
|
|
2836
|
-
const required = kind === "room-finish" ? ["FLOOR", "BASE"] : ["CODE", "MARK"];
|
|
2900
|
+
const required = kind === "room-finish" ? ["FLOOR", "BASE"] : ["CODE", "MARK", "SYMBOL"];
|
|
2837
2901
|
const minHits = kind === "room-finish" ? 4 : 3;
|
|
2838
2902
|
let anchors;
|
|
2839
2903
|
let headerSpans;
|
|
@@ -2943,7 +3007,7 @@ function roomTags(sheet, opts = {}) {
|
|
|
2943
3007
|
const dy = b[1] - cb[3];
|
|
2944
3008
|
if (dy < -hgt * 0.2 || dy > hgt * 2.2) continue;
|
|
2945
3009
|
if (cb[2] < b[0] - hgt || cb[0] > b[2] + hgt) continue;
|
|
2946
|
-
if (!/^[A-Z][A-Z
|
|
3010
|
+
if (!/^[A-Z][A-Z .'’\/&-]{2,}$/.test(norm(cand.str))) continue;
|
|
2947
3011
|
if (dy < best) {
|
|
2948
3012
|
best = dy;
|
|
2949
3013
|
name = cand.str.trim();
|
|
@@ -3022,6 +3086,10 @@ function buildSheetGraph(sheets) {
|
|
|
3022
3086
|
for (const kind of ["room-finish", "finish"]) {
|
|
3023
3087
|
const t = extractTable(s, kind, { buildings, deltas: deltasBySheet.get(s.key) });
|
|
3024
3088
|
if (!t) continue;
|
|
3089
|
+
if (kind === "finish" && t.title && isNonFinishSchedule(t.title.text)) {
|
|
3090
|
+
notes.push(`${s.key}: "${t.title.text}" names another schedule family, not a finish/material schedule \u2014 its ${t.rows.length} rows are NOT indexed as finish definitions`);
|
|
3091
|
+
continue;
|
|
3092
|
+
}
|
|
3025
3093
|
const titleB = t.title ? buildingMentions(t.title.text) : [];
|
|
3026
3094
|
const b = titleB.length === 1 ? titleB[0] : ctxBySheet.get(s.key);
|
|
3027
3095
|
if (b) t.building = b;
|
|
@@ -3103,6 +3171,7 @@ function buildSheetGraph(sheets) {
|
|
|
3103
3171
|
return { available: true, sheets: outSheets, rooms, tables, callouts, buildings: [...buildings].sort(), revisions, notes };
|
|
3104
3172
|
}
|
|
3105
3173
|
var SURFACE_HEADERS = ["FLOOR", "BASE", "WALL", "WALLS", "NORTH", "SOUTH", "EAST", "WEST", "CEILING", "WAINSCOT"];
|
|
3174
|
+
var surfaceRank = (label) => SURFACE_HEADERS.indexOf(label.split(" ")[0]);
|
|
3106
3175
|
function resolveTag(graph, tag) {
|
|
3107
3176
|
const t = norm(tag).replace(/\s+/g, "");
|
|
3108
3177
|
const q = t.match(QUALIFIED_KEY_RE);
|
|
@@ -3171,7 +3240,8 @@ function resolveTag(graph, tag) {
|
|
|
3171
3240
|
const finishes = [];
|
|
3172
3241
|
const sources = [{ sheet: r.sheet, text: `${tab.title?.text || "room-finish schedule"} row ${r.key}`, bbox: r.cells[Object.keys(r.cells)[0]]?.bbox || tab.region }];
|
|
3173
3242
|
if (room) sources.unshift({ sheet: room.sheet, text: `${room.name ? room.name + " " : ""}${room.tag}`.trim(), bbox: room.bbox });
|
|
3174
|
-
|
|
3243
|
+
const surfaces = Object.keys(r.cells).filter((k) => surfaceRank(k) >= 0).sort((a, b) => surfaceRank(a) - surfaceRank(b) || a.localeCompare(b));
|
|
3244
|
+
for (const surface of surfaces) {
|
|
3175
3245
|
const cell = r.cells[surface];
|
|
3176
3246
|
if (!cell || !cell.text.trim()) continue;
|
|
3177
3247
|
const code = norm(cell.text).replace(/[^A-Z0-9-]/g, "");
|
|
@@ -10848,7 +10918,7 @@ function applyStagedTools(server, registered) {
|
|
|
10848
10918
|
// package.json
|
|
10849
10919
|
var package_default = {
|
|
10850
10920
|
name: "opentakeoff-mcp",
|
|
10851
|
-
version: "0.9.
|
|
10921
|
+
version: "0.9.45",
|
|
10852
10922
|
mcpName: "io.github.Kentucky-ai/opentakeoff",
|
|
10853
10923
|
type: "module",
|
|
10854
10924
|
description: "OpenTakeoff MCP server \u2014 drive the takeoff engine from your MCP client over stdio.",
|
package/package.json
CHANGED