opentakeoff-mcp 0.9.43 → 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/README.md +3 -3
- package/dist/server-core.js +348 -79
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -162,9 +162,9 @@ reads the tool list once. ([#230](https://github.com/Kentucky-ai/opentakeoff/iss
|
|
|
162
162
|
| `delete_verdict` | Lift an agent verdict mark by id. Agent marks only — the estimator's seal is human ink and is refused, the same line `edit_shape` holds on reviewed shapes. `undo_last` re-seats a lifted mark exactly where it was. |
|
|
163
163
|
| `read_sheet_text` | Positioned page text (image px), optionally restricted to a region — title blocks, room labels, finish schedules. |
|
|
164
164
|
| `find_text` | **Locate** a known string — the complement to `read_sheet_text` (which returns what a region *says*; this finds *where* a string sits). Case-insensitive substring match per pdf.js text run; each hit's center feeds straight into `one_click`'s seed. |
|
|
165
|
-
| `sheet_graph` | The plan-set INDEX (#87): every sheet's role with evidence, the schedule tables found, every room tag with its stacked name, the detail callouts — how an agent decides WHAT to measure without a human enumerating rooms. |
|
|
166
|
-
| `resolve_tag` | ONE room tag → its room-finish schedule row → each code's finish/material definition, every edge cited (sheet + literal text + bbox). Refusal over guessing: `unresolved` comes back with a reason, never as silence. |
|
|
167
|
-
| `find_schedule` | Locate a schedule table by kind ("room finish", "material") — sheet, title, headers, row count,
|
|
165
|
+
| `sheet_graph` | The plan-set INDEX (#87): every sheet's role with evidence, the schedule tables found, every room tag with its stacked name, the detail callouts, and every revision marker (text `Δ2`/`REV 2` tags AND drawn deltas — a bare digit inside a triangle of linework, proven from the sheet's vector geometry — in `revisions`) — how an agent decides WHAT to measure without a human enumerating rooms. |
|
|
166
|
+
| `resolve_tag` | ONE room tag → its room-finish schedule row → each code's finish/material definition, every edge cited (sheet + literal text + bbox). Refusal over guessing: `unresolved` comes back with a reason, never as silence. A delta/REV marker on the answering row rides the result as `revisions` — the codes are the post-revision answer, and you're told the ink changed. |
|
|
167
|
+
| `find_schedule` | Locate a schedule table by kind ("room finish", "material") — sheet, title, headers, row count, a `view_sheet`-ready region, and `revised_rows` when delta/REV-marked rows exist. |
|
|
168
168
|
| `sheet_context` | The region's STRUCTURE in one frame: classified vector segments (endpoints as drawn, meta byte per segment), text spans with bboxes, and hatch-family instances with content-derived ids — same pattern spec ⇒ same id anywhere on the sheet, so plan↔legend matching is `id === id`. Decimation is declared and counted on every reply: `kept + dropped === total_in_region`, cap applies longest-first so walls survive. |
|
|
169
169
|
| `view_sheet` | The agent's eyes: render the sheet (or an image-px crop) to PNG. `overlay` burns committed shapes in (solid = human-affirmed, dashed = unreviewed) to verify geometry landed; `grid` burns in a calibrated 1-ft/5-ft measuring grid with foot labels (`"auto"` from the set scale, or the drawing scale like `"1/4"`) so dimensions are counted off cells, not guessed. |
|
|
170
170
|
|
package/dist/server-core.js
CHANGED
|
@@ -181,6 +181,12 @@ async function openPdf(filePath) {
|
|
|
181
181
|
viewport: { width: vp.width, height: vp.height, transform: vp.transform },
|
|
182
182
|
textContent,
|
|
183
183
|
operatorList: async () => await page.getOperatorList(),
|
|
184
|
+
cleanup: () => {
|
|
185
|
+
try {
|
|
186
|
+
page.cleanup();
|
|
187
|
+
} catch {
|
|
188
|
+
}
|
|
189
|
+
},
|
|
184
190
|
async renderPng(scale) {
|
|
185
191
|
await ensureCanvasGlobals();
|
|
186
192
|
const rvp = page.getViewport({ scale });
|
|
@@ -2580,6 +2586,79 @@ function sheetBuilding(sheet) {
|
|
|
2580
2586
|
const [building, span] = [...seen.entries()][0];
|
|
2581
2587
|
return { building, evidence: { sheet: sheet.key, text: span.str.trim(), bbox: bboxOf(span) } };
|
|
2582
2588
|
}
|
|
2589
|
+
var DELTA_MARK_RE = /^[Δ∆△▲]\s*(\d{1,2}[A-Z]?)$|^(\d{1,2}[A-Z]?)\s*[Δ∆△▲]$/;
|
|
2590
|
+
var REV_MARK_RE = /^REV(?:ISION)?\.?\s*#?\s*(\d{1,2}[A-Z]?)$/;
|
|
2591
|
+
var revisionOf = (s) => {
|
|
2592
|
+
const t = norm(s);
|
|
2593
|
+
if (!t || t.length > 12) return null;
|
|
2594
|
+
const d = t.match(DELTA_MARK_RE);
|
|
2595
|
+
if (d) return d[1] ?? d[2];
|
|
2596
|
+
const r = t.match(REV_MARK_RE);
|
|
2597
|
+
return r ? r[1] : null;
|
|
2598
|
+
};
|
|
2599
|
+
var BARE_DIGIT_RE = /^\d{1,2}$/;
|
|
2600
|
+
function drawnDeltaMarkers(spans, segs) {
|
|
2601
|
+
const cands = spans.filter((s) => BARE_DIGIT_RE.test((s.str || "").trim()));
|
|
2602
|
+
if (!cands.length || !segs.length) return [];
|
|
2603
|
+
const CELL = 64;
|
|
2604
|
+
const grid = /* @__PURE__ */ new Map();
|
|
2605
|
+
const nSeg = Math.floor(segs.length / 4);
|
|
2606
|
+
for (let i = 0; i < nSeg; i++) {
|
|
2607
|
+
const dx = segs[i * 4 + 2] - segs[i * 4], dy = segs[i * 4 + 3] - segs[i * 4 + 1];
|
|
2608
|
+
const len = Math.hypot(dx, dy);
|
|
2609
|
+
if (len < 4 || len > 400) continue;
|
|
2610
|
+
const mx = (segs[i * 4] + segs[i * 4 + 2]) / 2, my = (segs[i * 4 + 1] + segs[i * 4 + 3]) / 2;
|
|
2611
|
+
const k = `${Math.floor(mx / CELL)},${Math.floor(my / CELL)}`;
|
|
2612
|
+
let cell = grid.get(k);
|
|
2613
|
+
if (!cell) grid.set(k, cell = []);
|
|
2614
|
+
cell.push(i);
|
|
2615
|
+
}
|
|
2616
|
+
const out = [];
|
|
2617
|
+
for (const sp of cands) {
|
|
2618
|
+
const h = Math.max(sp.h || 8, 6);
|
|
2619
|
+
const cx = sp.x + (sp.w || 0) / 2, cy = sp.y + h / 2;
|
|
2620
|
+
const R = h * 5;
|
|
2621
|
+
const near = [];
|
|
2622
|
+
for (let gx = Math.floor((cx - R) / CELL); gx <= Math.floor((cx + R) / CELL); gx++) {
|
|
2623
|
+
for (let gy = Math.floor((cy - R) / CELL); gy <= Math.floor((cy + R) / CELL); gy++) {
|
|
2624
|
+
for (const i of grid.get(`${gx},${gy}`) || []) {
|
|
2625
|
+
const mx = (segs[i * 4] + segs[i * 4 + 2]) / 2, my = (segs[i * 4 + 1] + segs[i * 4 + 3]) / 2;
|
|
2626
|
+
const len = Math.hypot(segs[i * 4 + 2] - segs[i * 4], segs[i * 4 + 3] - segs[i * 4 + 1]);
|
|
2627
|
+
if (Math.hypot(mx - cx, my - cy) <= R && len >= h * 1.2 && len <= h * 8) near.push(i);
|
|
2628
|
+
}
|
|
2629
|
+
}
|
|
2630
|
+
}
|
|
2631
|
+
if (near.length < 3 || near.length > 60) continue;
|
|
2632
|
+
const tol = Math.max(2, h * 0.35);
|
|
2633
|
+
let best = null;
|
|
2634
|
+
let bestArea = Infinity;
|
|
2635
|
+
const P = (i, end) => [segs[i * 4 + end * 2], segs[i * 4 + 1 + end * 2]];
|
|
2636
|
+
const close = (a, b) => Math.hypot(a[0] - b[0], a[1] - b[1]) <= tol;
|
|
2637
|
+
for (let a = 0; a < near.length; a++) for (let b = a + 1; b < near.length; b++) for (let c = b + 1; c < near.length; c++) {
|
|
2638
|
+
for (const fa of [0, 1]) for (const fb of [0, 1]) for (const fc of [0, 1]) {
|
|
2639
|
+
const [a0, a1] = [P(near[a], fa), P(near[a], 1 - fa)];
|
|
2640
|
+
const [b0, b1] = [P(near[b], fb), P(near[b], 1 - fb)];
|
|
2641
|
+
const [c0, c1] = [P(near[c], fc), P(near[c], 1 - fc)];
|
|
2642
|
+
if (!close(a1, b0) || !close(b1, c0) || !close(c1, a0)) continue;
|
|
2643
|
+
const v = [a0, b0, c0];
|
|
2644
|
+
const side = (p, q) => Math.hypot(p[0] - q[0], p[1] - q[1]);
|
|
2645
|
+
const s01 = side(v[0], v[1]), s12 = side(v[1], v[2]), s20 = side(v[2], v[0]);
|
|
2646
|
+
const mx = Math.max(s01, s12, s20), mn = Math.min(s01, s12, s20);
|
|
2647
|
+
if (mn < h * 1.2 || mx > h * 8 || mx / mn > 2.5) continue;
|
|
2648
|
+
const cross = (p, q) => (q[0] - p[0]) * (cy - p[1]) - (q[1] - p[1]) * (cx - p[0]);
|
|
2649
|
+
const d0 = cross(v[0], v[1]), d1 = cross(v[1], v[2]), d2 = cross(v[2], v[0]);
|
|
2650
|
+
if (!(d0 > 0 && d1 > 0 && d2 > 0 || d0 < 0 && d1 < 0 && d2 < 0)) continue;
|
|
2651
|
+
const area = Math.abs((v[1][0] - v[0][0]) * (v[2][1] - v[0][1]) - (v[2][0] - v[0][0]) * (v[1][1] - v[0][1])) / 2;
|
|
2652
|
+
if (area < bestArea) {
|
|
2653
|
+
bestArea = area;
|
|
2654
|
+
best = [Math.min(v[0][0], v[1][0], v[2][0]), Math.min(v[0][1], v[1][1], v[2][1]), Math.max(v[0][0], v[1][0], v[2][0]), Math.max(v[0][1], v[1][1], v[2][1])];
|
|
2655
|
+
}
|
|
2656
|
+
}
|
|
2657
|
+
}
|
|
2658
|
+
if (best) out.push({ span: sp, tri: best });
|
|
2659
|
+
}
|
|
2660
|
+
return out;
|
|
2661
|
+
}
|
|
2583
2662
|
function clusterRows(spans) {
|
|
2584
2663
|
const toks = spans.filter((t) => t.str && t.str.trim()).sort((a, b) => a.y - b.y || a.x - b.x);
|
|
2585
2664
|
const rows = [];
|
|
@@ -2599,7 +2678,7 @@ function clusterRows(spans) {
|
|
|
2599
2678
|
}
|
|
2600
2679
|
var rowY = (r) => r.reduce((s, t) => s + t.y, 0) / r.length;
|
|
2601
2680
|
var ROOM_HEADERS = ["ROOM", "NO", "NUMBER", "NAME", "FLOOR", "BASE", "WALL", "WALLS", "NORTH", "SOUTH", "EAST", "WEST", "CEILING", "WAINSCOT", "REMARKS", "CLG", "HT", "BLDG", "BUILDING"];
|
|
2602
|
-
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"];
|
|
2603
2682
|
var headerLabel = (s, vocab) => {
|
|
2604
2683
|
for (const w of norm(s).split(/[^A-Z]+/)) if (w && vocab.includes(w)) return w;
|
|
2605
2684
|
return null;
|
|
@@ -2612,14 +2691,61 @@ function findHeaderRow(rows, vocab, required, minHits) {
|
|
|
2612
2691
|
const w = headerLabel(t.str, vocab);
|
|
2613
2692
|
if (w && !seen.has(w)) {
|
|
2614
2693
|
seen.add(w);
|
|
2615
|
-
anchors.push({ label: w, x: t.x });
|
|
2694
|
+
anchors.push({ label: w, x: t.x + (t.w || 0) / 2 });
|
|
2616
2695
|
}
|
|
2617
2696
|
}
|
|
2618
2697
|
if (anchors.length < minHits || !required.some((r) => seen.has(r))) continue;
|
|
2619
|
-
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
|
+
}
|
|
2700
|
+
return null;
|
|
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
|
+
}
|
|
2620
2711
|
}
|
|
2621
2712
|
return null;
|
|
2622
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
|
+
}
|
|
2623
2749
|
function findRotatedHeader(vert, vocab, required, minHits) {
|
|
2624
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);
|
|
2625
2751
|
let band = [];
|
|
@@ -2654,18 +2780,35 @@ function findRotatedHeader(vert, vocab, required, minHits) {
|
|
|
2654
2780
|
return band.length ? flush() : null;
|
|
2655
2781
|
}
|
|
2656
2782
|
var nearestAnchor = (x, anchors) => {
|
|
2657
|
-
let
|
|
2658
|
-
for (const a of anchors)
|
|
2659
|
-
|
|
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;
|
|
2660
2795
|
};
|
|
2796
|
+
var WIDE_LAST = /* @__PURE__ */ new Set(["REMARKS", "DESCRIPTION", "NOTES"]);
|
|
2661
2797
|
function bandLimits(anchors) {
|
|
2662
2798
|
const gaps = anchors.slice(1).map((a, i) => a.x - anchors[i].x).sort((a, b) => a - b);
|
|
2663
2799
|
const medGap = gaps.length ? gaps[gaps.length >> 1] : 150;
|
|
2664
|
-
|
|
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 };
|
|
2665
2803
|
}
|
|
2666
2804
|
var CODE_RE = /^[A-Z]{1,4}(-?[A-Z0-9]{1,4})?$/;
|
|
2667
2805
|
var ROW_KEY_RE = /^\d{1,3}[A-Z]{0,2}$/;
|
|
2668
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
|
+
};
|
|
2669
2812
|
function rowKeyOf(raw, kind, buildings) {
|
|
2670
2813
|
const key = norm(raw).replace(/[^A-Z0-9-]/g, "");
|
|
2671
2814
|
if (kind === "finish") return CODE_RE.test(key) ? { key } : null;
|
|
@@ -2675,12 +2818,86 @@ function rowKeyOf(raw, kind, buildings) {
|
|
|
2675
2818
|
return null;
|
|
2676
2819
|
}
|
|
2677
2820
|
var numOf = (key) => key.match(QUALIFIED_KEY_RE)?.[2] ?? key;
|
|
2821
|
+
var centerX = (t) => t.x + (t.w || 0) / 2;
|
|
2822
|
+
function bandDataRows(rows, anchors, kind, sheetKey, buildings, cfg) {
|
|
2823
|
+
const { x0, x1, medGap } = bandLimits(anchors);
|
|
2824
|
+
const out = [];
|
|
2825
|
+
const outY = [];
|
|
2826
|
+
let region = null;
|
|
2827
|
+
const add = (row, toks) => {
|
|
2828
|
+
for (const t of toks) {
|
|
2829
|
+
const label = nearestAnchor(centerX(t), anchors);
|
|
2830
|
+
const text = t.str.trim();
|
|
2831
|
+
if (!row.cells[label]) row.cells[label] = { text, bbox: bboxOf(t) };
|
|
2832
|
+
else row.cells[label] = { text: `${row.cells[label].text} ${text}`, bbox: merge(row.cells[label].bbox, bboxOf(t)) };
|
|
2833
|
+
region = region ? merge(region, bboxOf(t)) : bboxOf(t);
|
|
2834
|
+
}
|
|
2835
|
+
};
|
|
2836
|
+
const orphans = [];
|
|
2837
|
+
const markers = [];
|
|
2838
|
+
for (let i = Math.max(cfg.fromIdx, 0); i < rows.length; i++) {
|
|
2839
|
+
if (rowY(rows[i]) <= cfg.belowY) continue;
|
|
2840
|
+
const banded = [];
|
|
2841
|
+
for (const t of rows[i]) {
|
|
2842
|
+
const tri = cfg.deltas?.get(t);
|
|
2843
|
+
const rev = tri ? norm(t.str) : revisionOf(t.str);
|
|
2844
|
+
if (rev != null) {
|
|
2845
|
+
if (centerX(t) >= x0 - 2.5 * medGap && centerX(t) <= x1 + medGap) markers.push({ rev, span: t, ...tri ? { drawn: true, tri } : {} });
|
|
2846
|
+
continue;
|
|
2847
|
+
}
|
|
2848
|
+
if (t.x >= x0 && t.x <= x1) banded.push(t);
|
|
2849
|
+
}
|
|
2850
|
+
if (!banded.length) continue;
|
|
2851
|
+
const keyed = rowKeyOf(banded[0].str, kind, buildings);
|
|
2852
|
+
if (!keyed) {
|
|
2853
|
+
orphans.push({ toks: banded, y: rowY(rows[i]) });
|
|
2854
|
+
continue;
|
|
2855
|
+
}
|
|
2856
|
+
if (cfg.keyAlign && Math.abs(centerX(banded[0]) - cfg.keyAlign.x) > cfg.keyAlign.tol) continue;
|
|
2857
|
+
const row = { key: keyed.key, sheet: sheetKey, cells: {} };
|
|
2858
|
+
if (keyed.building) row.building = keyed.building;
|
|
2859
|
+
add(row, banded);
|
|
2860
|
+
out.push(row);
|
|
2861
|
+
outY.push(rowY(rows[i]));
|
|
2862
|
+
}
|
|
2863
|
+
const gaps = outY.slice(1).map((y, i) => y - outY[i]).filter((d) => d > 0).sort((a, b) => a - b);
|
|
2864
|
+
const pitch = gaps.length ? gaps[gaps.length >> 1] : 0;
|
|
2865
|
+
const nearest = (y) => {
|
|
2866
|
+
let bi = -1, bd = Infinity;
|
|
2867
|
+
outY.forEach((ry, i) => {
|
|
2868
|
+
const d = Math.abs(y - ry);
|
|
2869
|
+
if (d < bd) {
|
|
2870
|
+
bd = d;
|
|
2871
|
+
bi = i;
|
|
2872
|
+
}
|
|
2873
|
+
});
|
|
2874
|
+
return { i: bi, d: bd };
|
|
2875
|
+
};
|
|
2876
|
+
const radius = (h) => pitch ? pitch * 0.6 : Math.max(h, 8) * 1.6;
|
|
2877
|
+
for (const o of orphans) {
|
|
2878
|
+
const { i, d } = nearest(o.y);
|
|
2879
|
+
if (i < 0 || d > radius(Math.max(...o.toks.map((t) => t.h || 8)))) continue;
|
|
2880
|
+
add(out[i], o.toks);
|
|
2881
|
+
}
|
|
2882
|
+
for (const m of markers) {
|
|
2883
|
+
const { i, d } = nearest(m.span.y);
|
|
2884
|
+
if (i < 0 || d > radius(m.span.h || 8) || out[i].revision) continue;
|
|
2885
|
+
const ebox = m.tri ? merge(bboxOf(m.span), m.tri) : bboxOf(m.span);
|
|
2886
|
+
out[i].revision = { rev: m.rev, source: { sheet: sheetKey, text: m.span.str.trim(), bbox: ebox }, ...m.drawn ? { drawn: true } : {} };
|
|
2887
|
+
}
|
|
2888
|
+
for (const row of out) {
|
|
2889
|
+
if (row.building) continue;
|
|
2890
|
+
const cellB = norm(row.cells.BLDG?.text || row.cells.BUILDING?.text || "");
|
|
2891
|
+
if (DESIGNATOR_RE.test(cellB)) row.building = cellB;
|
|
2892
|
+
}
|
|
2893
|
+
return { out, region };
|
|
2894
|
+
}
|
|
2678
2895
|
function extractTable(sheet, kind, opts = {}) {
|
|
2679
2896
|
const horiz = sheet.spans.filter((s) => !isVertical(s));
|
|
2680
2897
|
const vert = sheet.spans.filter(isVertical);
|
|
2681
2898
|
const rows = clusterRows(horiz);
|
|
2682
2899
|
const vocab = kind === "room-finish" ? ROOM_HEADERS : FINISH_HEADERS;
|
|
2683
|
-
const required = kind === "room-finish" ? ["FLOOR", "BASE"] : ["CODE", "MARK"];
|
|
2900
|
+
const required = kind === "room-finish" ? ["FLOOR", "BASE"] : ["CODE", "MARK", "SYMBOL"];
|
|
2684
2901
|
const minHits = kind === "room-finish" ? 4 : 3;
|
|
2685
2902
|
let anchors;
|
|
2686
2903
|
let headerSpans;
|
|
@@ -2705,33 +2922,13 @@ function extractTable(sheet, kind, opts = {}) {
|
|
|
2705
2922
|
titleFrom = rows.findIndex((r) => rowY(r) >= rot.top) - 1;
|
|
2706
2923
|
if (titleFrom < -1) titleFrom = rows.length - 1;
|
|
2707
2924
|
}
|
|
2708
|
-
const out = [];
|
|
2709
2925
|
let region = null;
|
|
2710
2926
|
for (const t of headerSpans) region = region ? merge(region, bboxOf(t)) : bboxOf(t);
|
|
2711
|
-
const {
|
|
2712
|
-
|
|
2713
|
-
|
|
2714
|
-
const inBand = rows[i].filter((t) => t.x >= x0 && t.x <= x1);
|
|
2715
|
-
if (!inBand.length) continue;
|
|
2716
|
-
const keyed = rowKeyOf(inBand[0].str, kind, opts.buildings);
|
|
2717
|
-
if (!keyed) continue;
|
|
2718
|
-
const cells = {};
|
|
2719
|
-
for (const t of inBand) {
|
|
2720
|
-
const label = nearestAnchor(t.x, anchors);
|
|
2721
|
-
const text = t.str.trim();
|
|
2722
|
-
if (!cells[label]) cells[label] = { text, bbox: bboxOf(t) };
|
|
2723
|
-
else {
|
|
2724
|
-
cells[label] = { text: `${cells[label].text} ${text}`, bbox: merge(cells[label].bbox, bboxOf(t)) };
|
|
2725
|
-
}
|
|
2726
|
-
region = region ? merge(region, bboxOf(t)) : bboxOf(t);
|
|
2727
|
-
}
|
|
2728
|
-
const row = { key: keyed.key, sheet: sheet.key, cells };
|
|
2729
|
-
const cellB = norm(cells.BLDG?.text || cells.BUILDING?.text || "");
|
|
2730
|
-
const b = keyed.building ?? (DESIGNATOR_RE.test(cellB) ? cellB : void 0);
|
|
2731
|
-
if (b) row.building = b;
|
|
2732
|
-
out.push(row);
|
|
2733
|
-
}
|
|
2927
|
+
const banded = bandDataRows(rows, anchors, kind, sheet.key, opts.buildings, { fromIdx: dataFrom, belowY: dataBelowY, deltas: opts.deltas });
|
|
2928
|
+
const out = banded.out;
|
|
2929
|
+
if (banded.region) region = region ? merge(region, banded.region) : banded.region;
|
|
2734
2930
|
if (!out.length) return null;
|
|
2931
|
+
const { x0, x1 } = bandLimits(anchors);
|
|
2735
2932
|
let title = null;
|
|
2736
2933
|
for (let i = titleFrom; i >= 0 && i >= titleFrom - 5 && !title; i--) {
|
|
2737
2934
|
const hit = rows[i].find((t) => /SCHEDULE/.test(norm(t.str)) && t.x >= x0 && t.x <= x1);
|
|
@@ -2763,40 +2960,25 @@ function mergeContinuation(base, frag) {
|
|
|
2763
2960
|
base.parts.push({ sheet: frag.sheet, title: frag.title?.text || "", rows: frag.rows.length, region: frag.region, ...frag.rotated_headers ? { rotated_headers: true } : {} });
|
|
2764
2961
|
base.rows.push(...frag.rows);
|
|
2765
2962
|
}
|
|
2766
|
-
function adoptContinuationRows(sheet, titleSpan, base, buildings) {
|
|
2963
|
+
function adoptContinuationRows(sheet, titleSpan, base, buildings, deltas) {
|
|
2767
2964
|
if (!base.anchors?.length || base.kind === "unknown") return null;
|
|
2768
2965
|
const rows = clusterRows(sheet.spans.filter((s) => !isVertical(s)));
|
|
2769
|
-
const {
|
|
2966
|
+
const { medGap } = bandLimits(base.anchors);
|
|
2770
2967
|
const keyTol = Math.max(40, medGap / 2);
|
|
2771
|
-
const
|
|
2772
|
-
|
|
2773
|
-
|
|
2774
|
-
|
|
2775
|
-
|
|
2776
|
-
|
|
2777
|
-
|
|
2778
|
-
|
|
2779
|
-
const cells = {};
|
|
2780
|
-
for (const t of inBand) {
|
|
2781
|
-
const label = nearestAnchor(t.x, base.anchors);
|
|
2782
|
-
const text = t.str.trim();
|
|
2783
|
-
if (!cells[label]) cells[label] = { text, bbox: bboxOf(t) };
|
|
2784
|
-
else {
|
|
2785
|
-
cells[label] = { text: `${cells[label].text} ${text}`, bbox: merge(cells[label].bbox, bboxOf(t)) };
|
|
2786
|
-
}
|
|
2787
|
-
region = merge(region, bboxOf(t));
|
|
2788
|
-
}
|
|
2789
|
-
const r = { key: keyed.key, sheet: sheet.key, cells };
|
|
2790
|
-
if (keyed.building) r.building = keyed.building;
|
|
2791
|
-
out.push(r);
|
|
2792
|
-
}
|
|
2793
|
-
if (!out.length) return null;
|
|
2968
|
+
const banded = bandDataRows(rows, base.anchors, base.kind, sheet.key, buildings, {
|
|
2969
|
+
fromIdx: 0,
|
|
2970
|
+
belowY: titleSpan.y,
|
|
2971
|
+
keyAlign: { x: base.anchors[0].x, tol: keyTol },
|
|
2972
|
+
deltas
|
|
2973
|
+
});
|
|
2974
|
+
if (!banded.out.length) return null;
|
|
2975
|
+
const region = banded.region ? merge(bboxOf(titleSpan), banded.region) : bboxOf(titleSpan);
|
|
2794
2976
|
return {
|
|
2795
2977
|
kind: base.kind,
|
|
2796
2978
|
sheet: sheet.key,
|
|
2797
2979
|
title: { sheet: sheet.key, text: titleSpan.str.trim(), bbox: bboxOf(titleSpan) },
|
|
2798
2980
|
headers: base.headers,
|
|
2799
|
-
rows: out,
|
|
2981
|
+
rows: banded.out,
|
|
2800
2982
|
region
|
|
2801
2983
|
};
|
|
2802
2984
|
}
|
|
@@ -2811,6 +2993,7 @@ function roomTags(sheet, opts = {}) {
|
|
|
2811
2993
|
return { ok: false };
|
|
2812
2994
|
};
|
|
2813
2995
|
for (const sp of spans) {
|
|
2996
|
+
if (opts.deltas?.has(sp)) continue;
|
|
2814
2997
|
const t = sp.str.trim();
|
|
2815
2998
|
const a = accept(t);
|
|
2816
2999
|
if (!a.ok) continue;
|
|
@@ -2824,7 +3007,7 @@ function roomTags(sheet, opts = {}) {
|
|
|
2824
3007
|
const dy = b[1] - cb[3];
|
|
2825
3008
|
if (dy < -hgt * 0.2 || dy > hgt * 2.2) continue;
|
|
2826
3009
|
if (cb[2] < b[0] - hgt || cb[0] > b[2] + hgt) continue;
|
|
2827
|
-
if (!/^[A-Z][A-Z
|
|
3010
|
+
if (!/^[A-Z][A-Z .'’\/&-]{2,}$/.test(norm(cand.str))) continue;
|
|
2828
3011
|
if (dy < best) {
|
|
2829
3012
|
best = dy;
|
|
2830
3013
|
name = cand.str.trim();
|
|
@@ -2834,6 +3017,30 @@ function roomTags(sheet, opts = {}) {
|
|
|
2834
3017
|
if (a.building) tag.building = a.building;
|
|
2835
3018
|
out.push(tag);
|
|
2836
3019
|
}
|
|
3020
|
+
const markers = [];
|
|
3021
|
+
for (const c of spans) {
|
|
3022
|
+
const tri = opts.deltas?.get(c);
|
|
3023
|
+
if (tri) markers.push({ rev: norm(c.str), span: c, box: merge(bboxOf(c), tri), drawn: true });
|
|
3024
|
+
else {
|
|
3025
|
+
const rev = revisionOf(c.str);
|
|
3026
|
+
if (rev != null) markers.push({ rev, span: c, box: bboxOf(c) });
|
|
3027
|
+
}
|
|
3028
|
+
}
|
|
3029
|
+
for (const tag of out) {
|
|
3030
|
+
const hgt = Math.max(tag.bbox[3] - tag.bbox[1], 6);
|
|
3031
|
+
let bestM = null;
|
|
3032
|
+
let bd = Infinity;
|
|
3033
|
+
for (const m of markers) {
|
|
3034
|
+
const dx = Math.max(tag.bbox[0] - m.box[2], m.box[0] - tag.bbox[2], 0);
|
|
3035
|
+
const dy = Math.max(tag.bbox[1] - m.box[3], m.box[1] - tag.bbox[3], 0);
|
|
3036
|
+
const d = Math.hypot(dx, dy);
|
|
3037
|
+
if (d <= hgt * 2.5 && d < bd) {
|
|
3038
|
+
bd = d;
|
|
3039
|
+
bestM = m;
|
|
3040
|
+
}
|
|
3041
|
+
}
|
|
3042
|
+
if (bestM) tag.revision = { rev: bestM.rev, source: { sheet: sheet.key, text: bestM.span.str.trim(), bbox: bestM.box }, ...bestM.drawn ? { drawn: true } : {} };
|
|
3043
|
+
}
|
|
2837
3044
|
return out;
|
|
2838
3045
|
}
|
|
2839
3046
|
var CALLOUT_RE = /^(\d{1,2})\s*\/\s*([A-Z]{1,2}-?\d{1,3}(?:\.\d+)?)$/;
|
|
@@ -2847,8 +3054,23 @@ function detailCallouts(sheet) {
|
|
|
2847
3054
|
}
|
|
2848
3055
|
function buildSheetGraph(sheets) {
|
|
2849
3056
|
const withText = sheets.filter((s) => s.spans.length > 0);
|
|
2850
|
-
if (!withText.length) return { available: false, sheets: [], rooms: [], tables: [], callouts: [], buildings: [], notes: [] };
|
|
3057
|
+
if (!withText.length) return { available: false, sheets: [], rooms: [], tables: [], callouts: [], buildings: [], revisions: [], notes: [] };
|
|
2851
3058
|
const notes = [];
|
|
3059
|
+
const deltasBySheet = /* @__PURE__ */ new Map();
|
|
3060
|
+
const revisions = [];
|
|
3061
|
+
for (const s of withText) {
|
|
3062
|
+
const deltas = /* @__PURE__ */ new Map();
|
|
3063
|
+
if (s.segs?.length) for (const d of drawnDeltaMarkers(s.spans, s.segs)) deltas.set(d.span, d.tri);
|
|
3064
|
+
if (deltas.size) deltasBySheet.set(s.key, deltas);
|
|
3065
|
+
for (const sp of s.spans) {
|
|
3066
|
+
const tri = deltas.get(sp);
|
|
3067
|
+
if (tri) revisions.push({ rev: norm(sp.str), sheet: s.key, bbox: merge(bboxOf(sp), tri), drawn: true });
|
|
3068
|
+
else {
|
|
3069
|
+
const rev = revisionOf(sp.str);
|
|
3070
|
+
if (rev != null) revisions.push({ rev, sheet: s.key, bbox: bboxOf(sp) });
|
|
3071
|
+
}
|
|
3072
|
+
}
|
|
3073
|
+
}
|
|
2852
3074
|
const ctxBySheet = /* @__PURE__ */ new Map();
|
|
2853
3075
|
const buildings = /* @__PURE__ */ new Set();
|
|
2854
3076
|
for (const s of withText) {
|
|
@@ -2862,8 +3084,12 @@ function buildSheetGraph(sheets) {
|
|
|
2862
3084
|
for (const s of withText) {
|
|
2863
3085
|
roles.set(s.key, classifySheetRole(s));
|
|
2864
3086
|
for (const kind of ["room-finish", "finish"]) {
|
|
2865
|
-
const t = extractTable(s, kind, { buildings });
|
|
3087
|
+
const t = extractTable(s, kind, { buildings, deltas: deltasBySheet.get(s.key) });
|
|
2866
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
|
+
}
|
|
2867
3093
|
const titleB = t.title ? buildingMentions(t.title.text) : [];
|
|
2868
3094
|
const b = titleB.length === 1 ? titleB[0] : ctxBySheet.get(s.key);
|
|
2869
3095
|
if (b) t.building = b;
|
|
@@ -2891,7 +3117,7 @@ function buildSheetGraph(sheets) {
|
|
|
2891
3117
|
const fragBase = baseTitleOf(text);
|
|
2892
3118
|
const base = [...tables].reverse().find((t) => t.kind !== "unknown" && t.title && baseTitleOf(t.title.text) === fragBase && t.sheet !== s.key && !t.parts?.some((p) => p.sheet === s.key));
|
|
2893
3119
|
if (!base || fragmentKinds.get(s.key)?.has(base.kind)) continue;
|
|
2894
|
-
const adopted = adoptContinuationRows(s, sp, base, buildings);
|
|
3120
|
+
const adopted = adoptContinuationRows(s, sp, base, buildings, deltasBySheet.get(s.key));
|
|
2895
3121
|
if (adopted) {
|
|
2896
3122
|
if (adopted.building == null && ctxBySheet.get(s.key)) adopted.building = ctxBySheet.get(s.key);
|
|
2897
3123
|
mergeContinuation(base, adopted);
|
|
@@ -2912,7 +3138,7 @@ function buildSheetGraph(sheets) {
|
|
|
2912
3138
|
const role = roles.get(s.key);
|
|
2913
3139
|
if (role.role === "plan" || role.role === "unknown" || role.role === "demolition") {
|
|
2914
3140
|
const ctxB = ctxBySheet.get(s.key);
|
|
2915
|
-
for (const r of roomTags(s, { buildings, exclude: sheetNumbers })) {
|
|
3141
|
+
for (const r of roomTags(s, { buildings, exclude: sheetNumbers, deltas: deltasBySheet.get(s.key) })) {
|
|
2916
3142
|
if (r.building == null && ctxB) r.building = ctxB;
|
|
2917
3143
|
rooms.push(r);
|
|
2918
3144
|
}
|
|
@@ -2942,9 +3168,10 @@ function buildSheetGraph(sheets) {
|
|
|
2942
3168
|
if (b) entry.building = b;
|
|
2943
3169
|
return entry;
|
|
2944
3170
|
});
|
|
2945
|
-
return { available: true, sheets: outSheets, rooms, tables, callouts, buildings: [...buildings].sort(), notes };
|
|
3171
|
+
return { available: true, sheets: outSheets, rooms, tables, callouts, buildings: [...buildings].sort(), revisions, notes };
|
|
2946
3172
|
}
|
|
2947
3173
|
var SURFACE_HEADERS = ["FLOOR", "BASE", "WALL", "WALLS", "NORTH", "SOUTH", "EAST", "WEST", "CEILING", "WAINSCOT"];
|
|
3174
|
+
var surfaceRank = (label) => SURFACE_HEADERS.indexOf(label.split(" ")[0]);
|
|
2948
3175
|
function resolveTag(graph, tag) {
|
|
2949
3176
|
const t = norm(tag).replace(/\s+/g, "");
|
|
2950
3177
|
const q = t.match(QUALIFIED_KEY_RE);
|
|
@@ -3013,7 +3240,8 @@ function resolveTag(graph, tag) {
|
|
|
3013
3240
|
const finishes = [];
|
|
3014
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 }];
|
|
3015
3242
|
if (room) sources.unshift({ sheet: room.sheet, text: `${room.name ? room.name + " " : ""}${room.tag}`.trim(), bbox: room.bbox });
|
|
3016
|
-
|
|
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) {
|
|
3017
3245
|
const cell = r.cells[surface];
|
|
3018
3246
|
if (!cell || !cell.text.trim()) continue;
|
|
3019
3247
|
const code = norm(cell.text).replace(/[^A-Z0-9-]/g, "");
|
|
@@ -3030,7 +3258,10 @@ function resolveTag(graph, tag) {
|
|
|
3030
3258
|
finishes.push(fin);
|
|
3031
3259
|
}
|
|
3032
3260
|
if (!finishes.length) return { status: "unresolved", tag: t, room, reason: `schedule row ${t} exists but carries no finish cells the extractor could band` };
|
|
3033
|
-
|
|
3261
|
+
const revs = [];
|
|
3262
|
+
if (r.revision) revs.push(r.revision);
|
|
3263
|
+
if (room?.revision && !revs.some((v) => v.rev === room.revision.rev)) revs.push(room.revision);
|
|
3264
|
+
return { status: "resolved", tag: t, room, ...chosen.building ? { building: chosen.building } : {}, finishes, sources, ...revs.length ? { revisions: revs } : {} };
|
|
3034
3265
|
}
|
|
3035
3266
|
|
|
3036
3267
|
// src/format.ts
|
|
@@ -7524,15 +7755,31 @@ var Session = class _Session {
|
|
|
7524
7755
|
if (!this.docs.size) throw new UserError("No plan loaded \u2014 call load_plan first.");
|
|
7525
7756
|
if (!this.graph) {
|
|
7526
7757
|
const inputs = [];
|
|
7758
|
+
let vecBudget = 3e7;
|
|
7759
|
+
let skippedHeavy = 0;
|
|
7527
7760
|
for (const s of this.sheets.values()) {
|
|
7528
7761
|
if (!s.spans) s.spans = textSpans(s.page);
|
|
7529
|
-
|
|
7530
|
-
|
|
7531
|
-
|
|
7532
|
-
|
|
7533
|
-
|
|
7762
|
+
const spans = s.spans.map((t) => ({ str: t.str, x: t.x0, y: t.y0, w: t.x1 - t.x0, h: t.y1 - t.y0, ...t.rot ? { rot: t.rot } : {} }));
|
|
7763
|
+
let segs;
|
|
7764
|
+
if (spans.some((t) => /^\d{1,2}$/.test(t.str.trim()))) {
|
|
7765
|
+
const role = classifySheetRole({ key: s.key, sheet_number: s.sheetNumber, spans }).role;
|
|
7766
|
+
if (role === "plan" || role === "schedule" || role === "demolition" || role === "unknown") {
|
|
7767
|
+
if (vecBudget <= 0) skippedHeavy++;
|
|
7768
|
+
else if (s.geo) {
|
|
7769
|
+
segs = s.geo.segs;
|
|
7770
|
+
vecBudget -= segs.length / 4;
|
|
7771
|
+
} else {
|
|
7772
|
+
const opList = await s.page.operatorList();
|
|
7773
|
+
segs = extractVectorGeometry(opList, s.page.viewport.transform, OPS2).segs;
|
|
7774
|
+
vecBudget -= segs.length / 4;
|
|
7775
|
+
s.page.cleanup();
|
|
7776
|
+
}
|
|
7777
|
+
}
|
|
7778
|
+
}
|
|
7779
|
+
inputs.push({ key: s.key, sheet_number: s.sheetNumber, spans, ...segs?.length ? { segs } : {} });
|
|
7534
7780
|
}
|
|
7535
7781
|
this.graph = buildSheetGraph(inputs);
|
|
7782
|
+
if (skippedHeavy) this.graph.notes.push(`drawn-delta hunt skipped on ${skippedHeavy} sheet(s) \u2014 the set's linework exceeded the vector budget; text revision markers (\u03942 / REV 2) were still read everywhere`);
|
|
7536
7783
|
}
|
|
7537
7784
|
return this.graph;
|
|
7538
7785
|
}
|
|
@@ -7542,6 +7789,16 @@ var Session = class _Session {
|
|
|
7542
7789
|
static wireEvidence(e) {
|
|
7543
7790
|
return { sheet: e.sheet, text: e.text, bbox: _Session.wireBox(e.bbox) };
|
|
7544
7791
|
}
|
|
7792
|
+
static wireRoom(r) {
|
|
7793
|
+
return {
|
|
7794
|
+
tag: r.tag,
|
|
7795
|
+
name: r.name,
|
|
7796
|
+
sheet: r.sheet,
|
|
7797
|
+
bbox: _Session.wireBox(r.bbox),
|
|
7798
|
+
...r.building ? { building: r.building } : {},
|
|
7799
|
+
...r.revision ? { revision: { rev: r.revision.rev, source: _Session.wireEvidence(r.revision.source), ...r.revision.drawn ? { drawn: true } : {} } } : {}
|
|
7800
|
+
};
|
|
7801
|
+
}
|
|
7545
7802
|
async sheetGraph() {
|
|
7546
7803
|
const g = await this.ensureGraph();
|
|
7547
7804
|
return {
|
|
@@ -7561,9 +7818,10 @@ var Session = class _Session {
|
|
|
7561
7818
|
...t.rotated_headers ? { rotated_headers: true } : {}
|
|
7562
7819
|
}))
|
|
7563
7820
|
})),
|
|
7564
|
-
rooms: g.rooms.map(
|
|
7821
|
+
rooms: g.rooms.map(_Session.wireRoom),
|
|
7565
7822
|
callouts: g.callouts.map((c) => ({ detail: c.detail, target_sheet: c.target_sheet, sheet: c.sheet, bbox: _Session.wireBox(c.bbox) })),
|
|
7566
7823
|
...g.buildings.length ? { buildings: g.buildings } : {},
|
|
7824
|
+
...g.revisions.length ? { revisions: g.revisions.map((r) => ({ rev: r.rev, sheet: r.sheet, bbox: _Session.wireBox(r.bbox), ...r.drawn ? { drawn: true } : {} })) } : {},
|
|
7567
7825
|
...g.notes.length ? { notes: g.notes } : {},
|
|
7568
7826
|
counts: { rooms: g.rooms.length, schedules: g.tables.length, callouts: g.callouts.length }
|
|
7569
7827
|
};
|
|
@@ -7591,7 +7849,7 @@ var Session = class _Session {
|
|
|
7591
7849
|
const g = await this.ensureGraph();
|
|
7592
7850
|
if (!g.available) throw new UserError("This set has no text layer (a scan) \u2014 the sheet graph is unavailable, not empty.");
|
|
7593
7851
|
const res = resolveTag(g, tag);
|
|
7594
|
-
const room = res.room ?
|
|
7852
|
+
const room = res.room ? _Session.wireRoom(res.room) : null;
|
|
7595
7853
|
if (res.status === "unresolved") {
|
|
7596
7854
|
return {
|
|
7597
7855
|
status: "unresolved",
|
|
@@ -7612,7 +7870,8 @@ var Session = class _Session {
|
|
|
7612
7870
|
source: _Session.wireEvidence(f.source),
|
|
7613
7871
|
...f.definition ? { definition: { cells: f.definition.cells, source: _Session.wireEvidence(f.definition.source) } } : {}
|
|
7614
7872
|
})),
|
|
7615
|
-
sources: res.sources.map(_Session.wireEvidence)
|
|
7873
|
+
sources: res.sources.map(_Session.wireEvidence),
|
|
7874
|
+
...res.revisions?.length ? { revisions: res.revisions.map((v) => ({ rev: v.rev, source: _Session.wireEvidence(v.source), ...v.drawn ? { drawn: true } : {} })) } : {}
|
|
7616
7875
|
};
|
|
7617
7876
|
}
|
|
7618
7877
|
async findSchedule(kind) {
|
|
@@ -7635,6 +7894,7 @@ var Session = class _Session {
|
|
|
7635
7894
|
region: _Session.wireBox(t.region),
|
|
7636
7895
|
...t.building ? { building: t.building } : {},
|
|
7637
7896
|
...t.rotated_headers ? { rotated_headers: true } : {},
|
|
7897
|
+
...t.rows.some((r) => r.revision) ? { revised_rows: t.rows.filter((r) => r.revision).length } : {},
|
|
7638
7898
|
...t.parts ? { parts: t.parts.map((p) => ({ sheet: p.sheet, title: p.title, rows: p.rows, region: _Session.wireBox(p.region) })) } : {}
|
|
7639
7899
|
}))
|
|
7640
7900
|
};
|
|
@@ -8195,12 +8455,18 @@ var readSheetTextOutput = {
|
|
|
8195
8455
|
};
|
|
8196
8456
|
var wireBox = z.object({ x0: z.number(), y0: z.number(), x1: z.number(), y1: z.number() });
|
|
8197
8457
|
var wireEvidence = z.object({ sheet: z.string(), text: z.string(), bbox: wireBox }).describe("An evidence pointer \u2014 the sheet, the literal text, and where it sits (image px). Every edge in the graph carries one; pass the bbox to view_sheet to LOOK at the source.");
|
|
8458
|
+
var wireRevision = z.object({
|
|
8459
|
+
rev: z.string(),
|
|
8460
|
+
source: wireEvidence,
|
|
8461
|
+
drawn: z.boolean().optional().describe("true = a DRAWN delta: a bare digit inside a triangle of linework (the common CAD convention \u2014 the text layer carries only the digit; the geometry proved the triangle). The evidence bbox spans digit and triangle")
|
|
8462
|
+
}).describe("A revision marker (delta triangle / 'REV 2' tag) attached to this item: the ink CHANGED under that revision. The value read is the post-revision answer \u2014 view_sheet the marker's bbox and check the addendum before pricing");
|
|
8198
8463
|
var graphRoom = z.object({
|
|
8199
8464
|
tag: z.string(),
|
|
8200
8465
|
name: z.string().describe("The name span stacked over the tag ('' when none)"),
|
|
8201
8466
|
sheet: z.string(),
|
|
8202
8467
|
bbox: wireBox,
|
|
8203
|
-
building: z.string().optional().describe("The building the room belongs to, when the set names one \u2014 its plan sheet's BUILDING/BLDG context, or the tag's own qualifier ('A-134')")
|
|
8468
|
+
building: z.string().optional().describe("The building the room belongs to, when the set names one \u2014 its plan sheet's BUILDING/BLDG context, or the tag's own qualifier ('A-134')"),
|
|
8469
|
+
revision: wireRevision.optional()
|
|
8204
8470
|
});
|
|
8205
8471
|
var sheetGraphOutput = {
|
|
8206
8472
|
available: z.boolean().describe("false = the set has no text layer (a scan) \u2014 the graph degrades to unavailable, never half-populates"),
|
|
@@ -8222,6 +8488,7 @@ var sheetGraphOutput = {
|
|
|
8222
8488
|
rooms: z.array(graphRoom).describe("Room tags read off plan-role sheets \u2014 schedule sheets contribute rows, never phantom rooms"),
|
|
8223
8489
|
callouts: z.array(z.object({ detail: z.string(), target_sheet: z.string(), sheet: z.string(), bbox: wireBox })).describe("Detail callouts (3/A-601) \u2014 edges to their target sheets"),
|
|
8224
8490
|
buildings: z.array(z.string()).optional().describe("Every building designator the set names (sorted) \u2014 present only on multi-building-aware sets. Room numbers reused across these need qualified tags ('A-134')"),
|
|
8491
|
+
revisions: z.array(z.object({ rev: z.string(), sheet: z.string(), bbox: wireBox, drawn: z.boolean().optional() })).optional().describe("Every delta-triangle / REV-tag marker the set carries \u2014 text markers ('\u03942', 'REV 2') and DRAWN deltas (a bare digit inside a triangle of linework, drawn: true) \u2014 where one sits, the ink changed under that revision. Markers on a schedule row or room bubble also attach there (and ride resolve_tag). A revision CLOUD is arc-chain linework these detectors do not read \u2014 absence here is not absence of revisions"),
|
|
8225
8492
|
notes: z.array(z.string()).optional().describe("Named gaps found while indexing (e.g. a continuation whose rows could not be aligned) \u2014 the graph refuses silently dropping anything"),
|
|
8226
8493
|
counts: z.object({ rooms: z.number().int(), schedules: z.number().int().describe("LOGICAL tables \u2014 a schedule continued across sheets counts once"), callouts: z.number().int() })
|
|
8227
8494
|
};
|
|
@@ -8237,6 +8504,7 @@ var resolveTagOutput = {
|
|
|
8237
8504
|
definition: z.object({ cells: z.record(z.string()), source: wireEvidence }).optional().describe("The finish/material-schedule row this code chains to, when one exists")
|
|
8238
8505
|
})).optional(),
|
|
8239
8506
|
sources: z.array(wireEvidence).optional().describe("The chain: plan tag \u2192 schedule row (the row cites the sheet that CARRIES it \u2014 under a continuation that is the CONT'D sheet)"),
|
|
8507
|
+
revisions: z.array(wireRevision).optional().describe("resolved only \u2014 delta/REV markers on the answering schedule row or the plan bubble. The finishes above are the POST-revision answer, but the ink changed: check the marker (view_sheet its bbox) and the addendum before pricing"),
|
|
8240
8508
|
reason: z.string().optional().describe("unresolved only \u2014 WHY (no schedule row / ambiguous / no schedule found). A room that appears on the plan with no row comes back here, never as a silent omission"),
|
|
8241
8509
|
candidates: z.array(z.object({
|
|
8242
8510
|
key: z.string(),
|
|
@@ -8255,6 +8523,7 @@ var findScheduleOutput = {
|
|
|
8255
8523
|
region: wireBox.describe("Pass to view_sheet to look at the table (the BASE fragment's region when the table continues)"),
|
|
8256
8524
|
building: z.string().optional().describe("The building this table answers for, when its title or sheet names one"),
|
|
8257
8525
|
rotated_headers: z.boolean().optional().describe("true when the column headers were read at a quarter-turn"),
|
|
8526
|
+
revised_rows: z.number().int().optional().describe("Rows carrying a delta/REV marker \u2014 the ink changed there; resolve those tags to see which"),
|
|
8258
8527
|
parts: z.array(z.object({ sheet: z.string(), title: z.string(), rows: z.number().int(), region: wireBox })).optional().describe("Present when the table CONTINUES across sheets ('\u2026 SCHEDULE \u2014 CONT'D'): every fragment, base first, each with its own viewable region")
|
|
8259
8528
|
}))
|
|
8260
8529
|
};
|
|
@@ -10358,17 +10627,17 @@ All-or-nothing, like derive_base: an unknown tag, a transition landing on either
|
|
|
10358
10627
|
outputSchema: undoLastOutput
|
|
10359
10628
|
}, run("undo_last", ({ n }) => session.undoLast(n)));
|
|
10360
10629
|
server.registerTool("sheet_graph", {
|
|
10361
|
-
description: `The plan-set INDEX (#87): every sheet's role (plan / schedule / legend / \u2026, with confidence and the title evidence), the schedule tables found (kind, row count, region \u2014 a schedule CONTINUED across sheets ("\u2026 SCHEDULE \u2014 CONT'D") reads as ONE table, the continuation fragment naming its base in "continues"; rotated column headers are read at their quarter-turn and flagged), every room tag on the plan sheets (with the stacked room NAME when one exists, and the room's BUILDING on multi-building sets), the detail callouts (3/A-601 \u2192 sheet edges), the set's building designators, and named indexing gaps in "notes". Built once per document from the text layer and cached. This is how an agent decides WHAT to measure without a human enumerating the rooms: list the rooms here, resolve each with resolve_tag, then measure with one_click/detect_rooms. A scanned set (no text layer) returns available: false \u2014 unavailable, never half-populated. ${COORDS}`,
|
|
10630
|
+
description: `The plan-set INDEX (#87): every sheet's role (plan / schedule / legend / \u2026, with confidence and the title evidence), the schedule tables found (kind, row count, region \u2014 a schedule CONTINUED across sheets ("\u2026 SCHEDULE \u2014 CONT'D") reads as ONE table, the continuation fragment naming its base in "continues"; rotated column headers are read at their quarter-turn and flagged), every room tag on the plan sheets (with the stacked room NAME when one exists, and the room's BUILDING on multi-building sets), the detail callouts (3/A-601 \u2192 sheet edges), the set's building designators, every REVISION marker the set carries (text markers "\u03942"/"REV 2" AND drawn deltas \u2014 a bare digit inside a triangle of linework, proven from vector geometry and flagged drawn \u2014 in "revisions", and attached to the schedule row / room tag they sit on), and named indexing gaps in "notes". Built once per document from the text layer and cached. This is how an agent decides WHAT to measure without a human enumerating the rooms: list the rooms here, resolve each with resolve_tag, then measure with one_click/detect_rooms. A scanned set (no text layer) returns available: false \u2014 unavailable, never half-populated. ${COORDS}`,
|
|
10362
10631
|
inputSchema: {},
|
|
10363
10632
|
outputSchema: sheetGraphOutput
|
|
10364
10633
|
}, run("sheet_graph", () => session.sheetGraph()));
|
|
10365
10634
|
server.registerTool("resolve_tag", {
|
|
10366
|
-
description: `Resolve ONE room tag across the set (#87): the plan tag \u2192 its room-finish schedule row \u2192 each finish code's definition in the finish/material schedule, EVERY edge carrying an evidence pointer (sheet + literal text + bbox \u2014 pass a bbox to view_sheet to look at the source). Rows carried by a continuation sheet ("\u2026 SCHEDULE \u2014 CONT'D") resolve exactly like base-sheet rows, citing the sheet the ink is on. The doctrine is refusal over guessing: a room that appears on the plan with no schedule row returns status "unresolved" with the reason (and still cites the plan tag); reused room numbers return "ambiguous" rather than picking one \u2014 on a multi-building set the refusal LISTS the candidate rows per building, and a building-qualified tag ("A-134") picks the building the set names. ${COORDS}`,
|
|
10635
|
+
description: `Resolve ONE room tag across the set (#87): the plan tag \u2192 its room-finish schedule row \u2192 each finish code's definition in the finish/material schedule, EVERY edge carrying an evidence pointer (sheet + literal text + bbox \u2014 pass a bbox to view_sheet to look at the source). Rows carried by a continuation sheet ("\u2026 SCHEDULE \u2014 CONT'D") resolve exactly like base-sheet rows, citing the sheet the ink is on. The doctrine is refusal over guessing: a room that appears on the plan with no schedule row returns status "unresolved" with the reason (and still cites the plan tag); reused room numbers return "ambiguous" rather than picking one \u2014 on a multi-building set the refusal LISTS the candidate rows per building, and a building-qualified tag ("A-134") picks the building the set names. A delta triangle or REV tag on the answering row (or the plan bubble) rides the result as "revisions": the codes returned are the POST-revision answer, but the ink changed under that delta \u2014 view_sheet the marker's bbox and check the addendum before pricing. ${COORDS}`,
|
|
10367
10636
|
inputSchema: { tag: z2.string().describe('The room tag as drawn, e.g. "134" or "139A" \u2014 or building-qualified on a multi-building set, e.g. "A-134" (building A, room 134)') },
|
|
10368
10637
|
outputSchema: resolveTagOutput
|
|
10369
10638
|
}, run("resolve_tag", ({ tag }) => session.resolveRoomTag(tag)));
|
|
10370
10639
|
server.registerTool("find_schedule", {
|
|
10371
|
-
description: `Locate a schedule table in the set (#87): pass a kind ("room finish", "material"/"finish") and get every matching table's sheet, title, headers, TOTAL row count, and REGION \u2014 sized for a view_sheet look or a read_sheet_text pull of exactly the table. A schedule continued across sheets is ONE match whose "parts" list every fragment (base first) with its own viewable region; tables read through rotated headers say so; a table answering for one building carries "building". Errors with what WAS found when the asked-for kind isn't in the set. ${COORDS}`,
|
|
10640
|
+
description: `Locate a schedule table in the set (#87): pass a kind ("room finish", "material"/"finish") and get every matching table's sheet, title, headers, TOTAL row count, and REGION \u2014 sized for a view_sheet look or a read_sheet_text pull of exactly the table. A schedule continued across sheets is ONE match whose "parts" list every fragment (base first) with its own viewable region; tables read through rotated headers say so; a table answering for one building carries "building"; a table with delta/REV-marked rows says how many in "revised_rows". Errors with what WAS found when the asked-for kind isn't in the set. ${COORDS}`,
|
|
10372
10641
|
inputSchema: { kind: z2.string().describe('"room finish" (rooms \u2192 surface finishes) or "finish"/"material" (codes \u2192 products)') },
|
|
10373
10642
|
outputSchema: findScheduleOutput
|
|
10374
10643
|
}, run("find_schedule", ({ kind }) => session.findSchedule(kind)));
|
|
@@ -10649,7 +10918,7 @@ function applyStagedTools(server, registered) {
|
|
|
10649
10918
|
// package.json
|
|
10650
10919
|
var package_default = {
|
|
10651
10920
|
name: "opentakeoff-mcp",
|
|
10652
|
-
version: "0.9.
|
|
10921
|
+
version: "0.9.45",
|
|
10653
10922
|
mcpName: "io.github.Kentucky-ai/opentakeoff",
|
|
10654
10923
|
type: "module",
|
|
10655
10924
|
description: "OpenTakeoff MCP server \u2014 drive the takeoff engine from your MCP client over stdio.",
|
package/package.json
CHANGED