opentakeoff-mcp 0.9.90 → 0.9.94
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 +377 -56
- package/package.json +2 -2
package/dist/server-core.js
CHANGED
|
@@ -3434,6 +3434,38 @@ function isLabelBubblePx(ring, b) {
|
|
|
3434
3434
|
return rh <= BUBBLE_RATIO * lh && rw <= BUBBLE_WIDE_RATIO * lw;
|
|
3435
3435
|
}
|
|
3436
3436
|
|
|
3437
|
+
// ../web/src/lib/finishSections.ts
|
|
3438
|
+
var FINISH_SECTION_HEADINGS = [
|
|
3439
|
+
"FLOORING",
|
|
3440
|
+
"FLOORS",
|
|
3441
|
+
"FLOOR",
|
|
3442
|
+
"FLOOR FINISHES",
|
|
3443
|
+
"BASE",
|
|
3444
|
+
"BASES",
|
|
3445
|
+
"WALL BASE",
|
|
3446
|
+
"WALLS",
|
|
3447
|
+
"WALL",
|
|
3448
|
+
"WALL FINISHES",
|
|
3449
|
+
"WALL PROTECTION",
|
|
3450
|
+
"TRANSITIONS",
|
|
3451
|
+
"TRANSITION",
|
|
3452
|
+
"TRIM",
|
|
3453
|
+
"CEILINGS",
|
|
3454
|
+
"CEILING",
|
|
3455
|
+
"MILLWORK",
|
|
3456
|
+
"MISC",
|
|
3457
|
+
"ACCESSORIES"
|
|
3458
|
+
];
|
|
3459
|
+
var PHRASES = FINISH_SECTION_HEADINGS.filter((h) => h.includes(" ")).map((h) => ({ h, words: h.split(" ") })).sort((a, b) => b.words.length - a.words.length || b.h.length - a.h.length);
|
|
3460
|
+
var WORDS = new Set(FINISH_SECTION_HEADINGS.filter((h) => !h.includes(" ")));
|
|
3461
|
+
function finishSectionOf(text) {
|
|
3462
|
+
const words = (text || "").trim().toUpperCase().split(/\s+/);
|
|
3463
|
+
if (!words[0] || /[-\d]/.test(words[0])) return null;
|
|
3464
|
+
const clean = words.map((w) => w.replace(/[^A-Z]/g, ""));
|
|
3465
|
+
for (const p of PHRASES) if (p.words.every((w, i) => clean[i] === w)) return p.h;
|
|
3466
|
+
return WORDS.has(clean[0]) ? clean[0] : null;
|
|
3467
|
+
}
|
|
3468
|
+
|
|
3437
3469
|
// ../web/src/lib/sheetgraph.ts
|
|
3438
3470
|
var bboxOf = (s) => [s.x, s.y, s.x + (s.w || 0), s.y + (s.h || 0)];
|
|
3439
3471
|
var merge = (a, b) => [Math.min(a[0], b[0]), Math.min(a[1], b[1]), Math.max(a[2], b[2]), Math.max(a[3], b[3])];
|
|
@@ -3746,16 +3778,26 @@ var EQUIPMENT_ONLY = /* @__PURE__ */ new Set([
|
|
|
3746
3778
|
var EQUIP_KEY_RE = /^[A-Z]{1,4}(?:-\d{1,3}|\d{1,2})[A-Z]?$/;
|
|
3747
3779
|
var TYPE_KEY_RE = /^[A-Z]{1,2}\d{0,2}[A-Z]?$/;
|
|
3748
3780
|
var headerLabel = (s, vocab) => headerLabels(s, vocab)[0] ?? null;
|
|
3749
|
-
var headerLabels = (s, vocab) => {
|
|
3781
|
+
var headerLabels = (s, vocab, wholeWords = false) => {
|
|
3750
3782
|
const out = [];
|
|
3751
|
-
|
|
3783
|
+
const words = norm(s).split(/[^A-Z]+/);
|
|
3784
|
+
for (const w of words) if (w && vocab.includes(w) && !out.includes(w)) out.push(w);
|
|
3785
|
+
if (vocab === FINISH_HEADERS && !wholeWords) {
|
|
3786
|
+
for (const w of words) {
|
|
3787
|
+
if (!w) continue;
|
|
3788
|
+
for (const l of vocab) if (w.startsWith(l.slice(0, 5)) && !out.includes(l)) {
|
|
3789
|
+
out.push(l);
|
|
3790
|
+
break;
|
|
3791
|
+
}
|
|
3792
|
+
}
|
|
3793
|
+
}
|
|
3752
3794
|
return out;
|
|
3753
3795
|
};
|
|
3754
|
-
function headerHits(row, vocab) {
|
|
3796
|
+
function headerHits(row, vocab, wholeWords = false) {
|
|
3755
3797
|
const out = [];
|
|
3756
3798
|
const used = /* @__PURE__ */ new Set();
|
|
3757
3799
|
for (const t of row) {
|
|
3758
|
-
const words = headerLabels(t.str, vocab);
|
|
3800
|
+
const words = headerLabels(t.str, vocab, wholeWords);
|
|
3759
3801
|
if (!words.length) continue;
|
|
3760
3802
|
const all = norm(t.str).split(/[^A-Z]+/).filter(Boolean);
|
|
3761
3803
|
if (all.length >= 2 && all.every((w2) => SURFACE_WORDS.has(w2))) {
|
|
@@ -3778,15 +3820,15 @@ var qualifies = (hits, required, minHits) => {
|
|
|
3778
3820
|
};
|
|
3779
3821
|
function findHeaderRow(rows, vocab, required, minHits) {
|
|
3780
3822
|
for (let i = 0; i < rows.length; i++) {
|
|
3823
|
+
if (!qualifies(headerHits(rows[i], vocab, true), required, minHits)) continue;
|
|
3781
3824
|
let hits = headerHits(rows[i], vocab);
|
|
3782
|
-
if (!qualifies(hits, required, minHits)) continue;
|
|
3783
3825
|
let idx = i;
|
|
3784
3826
|
for (; ; ) {
|
|
3785
3827
|
let next = -1;
|
|
3786
3828
|
for (let j = idx + 1; j < Math.min(idx + 4, rows.length); j++) {
|
|
3787
3829
|
const h = headerHits(rows[j], vocab);
|
|
3788
3830
|
const ratio = h.length / Math.max(1, rows[j].length);
|
|
3789
|
-
if (qualifies(
|
|
3831
|
+
if (qualifies(headerHits(rows[j], vocab, true), [], minHits) && h.length > hits.length && ratio >= 0.6) {
|
|
3790
3832
|
next = j;
|
|
3791
3833
|
break;
|
|
3792
3834
|
}
|
|
@@ -3839,7 +3881,7 @@ function findHeaderRow(rows, vocab, required, minHits) {
|
|
|
3839
3881
|
}
|
|
3840
3882
|
}
|
|
3841
3883
|
}
|
|
3842
|
-
return { anchors: subTierAnchors(rows, idx, anchors.sort((a, b) => a.x - b.x), vocab), rowIndex: idx };
|
|
3884
|
+
return { anchors: subTierAnchors(rows, idx, anchors.sort((a, b) => a.x - b.x), vocab), rowIndex: idx, top: i };
|
|
3843
3885
|
}
|
|
3844
3886
|
return null;
|
|
3845
3887
|
}
|
|
@@ -3899,7 +3941,7 @@ function subTierAnchors(rows, hdrIdx, anchors, vocab) {
|
|
|
3899
3941
|
return out.sort((a, b) => a.x - b.x);
|
|
3900
3942
|
}
|
|
3901
3943
|
function findRotatedHeader(vert, vocab, required, minHits) {
|
|
3902
|
-
const cands = vert.map((sp) => ({ sp, label:
|
|
3944
|
+
const cands = vert.map((sp) => ({ sp, label: headerLabels(sp.str, vocab, true)[0] ?? null })).filter((c) => !!c.label).sort((a, b) => a.sp.x - b.sp.x);
|
|
3903
3945
|
let band = [];
|
|
3904
3946
|
let y0 = 0, y1 = 0;
|
|
3905
3947
|
const flush = () => {
|
|
@@ -3954,6 +3996,7 @@ function bandLimits(anchors) {
|
|
|
3954
3996
|
return { x0: anchors[0].x - Math.max(80, medGap / 2), x1: last.x + rightMargin, medGap };
|
|
3955
3997
|
}
|
|
3956
3998
|
var CODE_RE = /^[A-Z]{1,4}(-?[A-Z0-9]{1,4})?$/;
|
|
3999
|
+
var finishCodeOk = (p) => !/^[A-Z]{4,}$/.test(p) && CODE_RE.test(p);
|
|
3957
4000
|
var ROW_KEY_RE = /^\d{1,3}[A-Z]{0,2}$/;
|
|
3958
4001
|
var QUALIFIED_KEY_RE = /^([A-Z]{1,2})-(\d{1,3}[A-Z]{0,2})$/;
|
|
3959
4002
|
var CORRIDOR_KEY_RE = /^[A-Z]{1,3}(?:\d{1,3}-\d{1,3}|\d{3})[A-Z]?$/;
|
|
@@ -3973,8 +4016,8 @@ function rowKeyOf(raw, kind, buildings, typeKeyed = false) {
|
|
|
3973
4016
|
}
|
|
3974
4017
|
if (kind === "finish") {
|
|
3975
4018
|
const parts = kept.split("/").filter(Boolean);
|
|
3976
|
-
if (parts.length > 1 && parts.every(
|
|
3977
|
-
return
|
|
4019
|
+
if (parts.length > 1 && parts.every(finishCodeOk)) return { key: parts.join("/") };
|
|
4020
|
+
return finishCodeOk(key) ? { key } : null;
|
|
3978
4021
|
}
|
|
3979
4022
|
if (ROW_KEY_RE.test(key)) return { key };
|
|
3980
4023
|
if (CORRIDOR_KEY_RE.test(key)) return { key };
|
|
@@ -3990,15 +4033,33 @@ var rowKeyAnswersFor = (key, want) => {
|
|
|
3990
4033
|
var numOf = (key) => key.match(QUALIFIED_KEY_RE)?.[2] ?? key;
|
|
3991
4034
|
var centerX = (t) => t.x + (t.w || 0) / 2;
|
|
3992
4035
|
var PLACEHOLDER_RE = /^[-–—]{1,3}$/;
|
|
3993
|
-
function columnMapFor(rows, anchors, cfg, x0, x1, coord) {
|
|
4036
|
+
function columnMapFor(rows, anchors, cfg, x0, x1, coord, kind) {
|
|
3994
4037
|
const at = (t) => coord === "left" ? t.x : t.x + (t.w || 0) / 2;
|
|
3995
4038
|
const xs = [];
|
|
3996
4039
|
const hs = [];
|
|
3997
4040
|
const dashes = [];
|
|
4041
|
+
let keyCellX = -Infinity;
|
|
4042
|
+
if (kind === "finish") {
|
|
4043
|
+
const kx = [];
|
|
4044
|
+
for (let i = Math.max(cfg.fromIdx, 0); i < rows.length; i++) {
|
|
4045
|
+
if (rowY(rows[i]) <= cfg.belowY) continue;
|
|
4046
|
+
const first = rows[i].filter((t) => t.x >= x0 && t.x <= x1).reduce((m, t) => !m || t.x < m.x ? t : m, null);
|
|
4047
|
+
if (first && rowKeyOf(first.str, kind)) kx.push(first.x);
|
|
4048
|
+
}
|
|
4049
|
+
kx.sort((p, q) => p - q);
|
|
4050
|
+
if (kx.length) keyCellX = kx[kx.length >> 1];
|
|
4051
|
+
}
|
|
3998
4052
|
for (let i = Math.max(cfg.fromIdx, 0); i < rows.length; i++) {
|
|
3999
4053
|
if (rowY(rows[i]) <= cfg.belowY) continue;
|
|
4054
|
+
let lead = null;
|
|
4055
|
+
if (kind === "finish") {
|
|
4056
|
+
const inb = rows[i].filter((t) => t.x >= x0 && t.x <= x1);
|
|
4057
|
+
if (inb.length === 1 && finishSectionOf(inb[0].str) && ((inb[0].w || 0) > 0 || inb[0].x < keyCellX + 4)) continue;
|
|
4058
|
+
const first = inb.reduce((m, t) => !m || t.x < m.x ? t : m, null);
|
|
4059
|
+
if (first && (first.w || 0) > 0 && finishSectionOf(first.str)) lead = first;
|
|
4060
|
+
}
|
|
4000
4061
|
for (const t of rows[i]) {
|
|
4001
|
-
if (t.x < x0 || t.x > x1 || revisionOf(t.str) != null) continue;
|
|
4062
|
+
if (t === lead || t.x < x0 || t.x > x1 || revisionOf(t.str) != null) continue;
|
|
4002
4063
|
if (PLACEHOLDER_RE.test(t.str.trim())) {
|
|
4003
4064
|
dashes.push(at(t));
|
|
4004
4065
|
continue;
|
|
@@ -4022,13 +4083,48 @@ function columnMapFor(rows, anchors, cfg, x0, x1, coord) {
|
|
|
4022
4083
|
}
|
|
4023
4084
|
const maxN = Math.max(...clusters.map((c) => c.n));
|
|
4024
4085
|
const kept = clusters.filter((c) => c.n >= Math.max(2, maxN * 0.25));
|
|
4025
|
-
|
|
4086
|
+
const widthless = rows.every((r) => r.every((t) => !((t.w || 0) > 0)));
|
|
4087
|
+
const hMed = hs[hs.length >> 1];
|
|
4088
|
+
const contWord = (c) => kind === "finish" && widthless && kept.some((k) => k.start < c.start && c.start - k.start < 5 * hMed);
|
|
4089
|
+
const ownerOf = (c) => anchors.find((a) => a.x >= c.start);
|
|
4090
|
+
const rescued = [];
|
|
4091
|
+
const block = (cfg.hdrBand ?? cfg.hdrSpans ?? []).filter((t) => cfg.hdrSpans?.includes(t) || !/\b(LEGEND|ABBREVIATIONS?|SYMBOLS?|KEY|NOTES?)\b/.test(norm(t.str)));
|
|
4092
|
+
const named = (cfg.hdrSpans ?? []).filter((t) => anchors.some((a) => t.x - 0.5 <= a.x && a.x <= t.x + (t.w || 0) + 0.5));
|
|
4093
|
+
const unnamed = block.some((t) => t.x >= x0 && t.x <= x1 && !named.some((n) => t.x <= n.x + (n.w || 0) && n.x <= t.x + (t.w || 0)));
|
|
4094
|
+
const spanOf = (a) => cfg.hdrSpans?.find((t) => t.x - 0.5 <= a.x && a.x <= t.x + (t.w || 0) + 0.5);
|
|
4095
|
+
const fromHeader = kind === "finish" && coord === "left" && !!cfg.hdrSpans && !unnamed;
|
|
4096
|
+
if (fromHeader && !widthless) {
|
|
4097
|
+
const owned = /* @__PURE__ */ new Set();
|
|
4098
|
+
for (const c of kept) {
|
|
4099
|
+
const own = ownerOf(c);
|
|
4100
|
+
if (own && !contWord(c)) owned.add(own.label);
|
|
4101
|
+
}
|
|
4102
|
+
for (const c of clusters) {
|
|
4103
|
+
if (kept.includes(c)) continue;
|
|
4104
|
+
const k = anchors.findIndex((a) => a.x >= c.start);
|
|
4105
|
+
if (k <= 0 || owned.has(anchors[k].label)) continue;
|
|
4106
|
+
owned.add(anchors[k].label);
|
|
4107
|
+
rescued.push(c);
|
|
4108
|
+
}
|
|
4109
|
+
}
|
|
4110
|
+
const cols0 = [...kept, ...rescued];
|
|
4111
|
+
if (cols0.length < anchors.length && !fromHeader) return null;
|
|
4026
4112
|
const byLabel = /* @__PURE__ */ new Map();
|
|
4027
|
-
|
|
4028
|
-
|
|
4113
|
+
const byLabelCont = /* @__PURE__ */ new Map();
|
|
4114
|
+
for (const c of cols0) {
|
|
4115
|
+
const own = ownerOf(c);
|
|
4029
4116
|
if (!own) continue;
|
|
4030
|
-
const
|
|
4031
|
-
|
|
4117
|
+
const m = contWord(c) ? byLabelCont : byLabel;
|
|
4118
|
+
const cur = m.get(own.label);
|
|
4119
|
+
if (cur == null || c.start < cur) m.set(own.label, c.start);
|
|
4120
|
+
}
|
|
4121
|
+
const keyStart = byLabel.get(anchors[0].label);
|
|
4122
|
+
const flushHeaders = keyStart != null && Math.abs(anchors[0].x - keyStart) <= tol;
|
|
4123
|
+
for (const [l, st] of byLabelCont) {
|
|
4124
|
+
if (byLabel.has(l)) continue;
|
|
4125
|
+
const a = anchors.find((q) => q.label === l);
|
|
4126
|
+
const contMax = Math.max(...cols0.filter((c) => contWord(c) && ownerOf(c) === a).map((c) => c.start));
|
|
4127
|
+
byLabel.set(l, widthless && flushHeaders && a.x > contMax + tol ? a.x : st);
|
|
4032
4128
|
}
|
|
4033
4129
|
if (byLabel.size < anchors.length && dashes.length) {
|
|
4034
4130
|
dashes.sort((a, b) => a - b);
|
|
@@ -4048,19 +4144,53 @@ function columnMapFor(rows, anchors, cfg, x0, x1, coord) {
|
|
|
4048
4144
|
byLabel.set(own.label, real ? real.start : c.start);
|
|
4049
4145
|
}
|
|
4050
4146
|
}
|
|
4147
|
+
let filled = false;
|
|
4148
|
+
if (fromHeader && !widthless && byLabel.size < anchors.length) {
|
|
4149
|
+
for (let k = 1; k < anchors.length; k++) {
|
|
4150
|
+
const own = spanOf(anchors[k]);
|
|
4151
|
+
if (byLabel.has(anchors[k].label) || !own) continue;
|
|
4152
|
+
byLabel.set(anchors[k].label, own.x);
|
|
4153
|
+
filled = true;
|
|
4154
|
+
}
|
|
4155
|
+
}
|
|
4051
4156
|
if (byLabel.size !== anchors.length) return null;
|
|
4052
4157
|
const cols = [...byLabel.entries()].map(([label, start]) => ({ label, start })).sort((a, b) => a.start - b.start);
|
|
4053
4158
|
if (cols.map((c) => c.label).join("|") !== anchors.map((a) => a.label).join("|")) return null;
|
|
4054
|
-
|
|
4159
|
+
if (rescued.length || filled) {
|
|
4160
|
+
for (let k = 1; k < anchors.length; k++) {
|
|
4161
|
+
const own = spanOf(anchors[k]), prev = spanOf(anchors[k - 1]);
|
|
4162
|
+
if (!own || !prev) return null;
|
|
4163
|
+
const st = cols[k].start;
|
|
4164
|
+
const lo = prev === own ? anchors[k - 1].x : prev.x + (prev.w || 0);
|
|
4165
|
+
if (st < lo - tol || st > Math.min(own.x, anchors[k].x) + tol) return null;
|
|
4166
|
+
}
|
|
4167
|
+
}
|
|
4168
|
+
const starts = cols0.map((c) => c.start);
|
|
4055
4169
|
let on = 0;
|
|
4056
4170
|
for (const x of xs) if (starts.some((st) => Math.abs(x - st) <= tol)) on++;
|
|
4057
|
-
|
|
4171
|
+
let capX;
|
|
4172
|
+
const lastHdr = cfg.hdrSpans ? cfg.hdrSpans.find((t) => t.x - 0.5 <= anchors[anchors.length - 1].x && anchors[anchors.length - 1].x <= t.x + (t.w || 0) + 0.5) : void 0;
|
|
4173
|
+
if (kind === "finish" && lastHdr) {
|
|
4174
|
+
const lastR = lastHdr.x + (lastHdr.w || 0);
|
|
4175
|
+
const band = block;
|
|
4176
|
+
const ps = cols.slice(1).map((c, k) => c.start - cols[k].start).sort((p, q) => p - q);
|
|
4177
|
+
const clear = Math.max(tol, 0.5 * (ps.length ? ps[ps.length - 1 >> 1] : 0));
|
|
4178
|
+
const foreign = kept.filter((c) => c.start > lastR + clear && !contWord(c)).map((c) => c.start).sort((a, b) => a - b);
|
|
4179
|
+
const right = (t) => t.x + (t.w || 0);
|
|
4180
|
+
const heads = band.filter((t) => t !== lastHdr && right(t) > lastR).map((t) => ({
|
|
4181
|
+
lo: Math.max(lastR, ...band.filter((u) => u !== t && right(u) <= t.x).map(right)),
|
|
4182
|
+
hi: right(t)
|
|
4183
|
+
}));
|
|
4184
|
+
const free = foreign.filter((st) => !heads.some((h) => st >= h.lo - tol && st <= h.hi + tol));
|
|
4185
|
+
if (free.length) capX = free[0];
|
|
4186
|
+
}
|
|
4187
|
+
return { coord, cols, score: on / xs.length, tol, ...capX != null ? { capX } : {} };
|
|
4058
4188
|
}
|
|
4059
|
-
function columnStarts(rows, anchors, cfg, x0, x1) {
|
|
4189
|
+
function columnStarts(rows, anchors, cfg, x0, x1, kind) {
|
|
4060
4190
|
const FIT_FLOOR = 0.7;
|
|
4061
4191
|
const fits = (m) => m && m.score >= FIT_FLOOR ? m : null;
|
|
4062
|
-
const left = fits(columnMapFor(rows, anchors, cfg, x0, x1, "left"));
|
|
4063
|
-
const center = fits(columnMapFor(rows, anchors, cfg, x0, x1, "center"));
|
|
4192
|
+
const left = fits(columnMapFor(rows, anchors, cfg, x0, x1, "left", kind));
|
|
4193
|
+
const center = fits(columnMapFor(rows, anchors, cfg, x0, x1, "center", kind));
|
|
4064
4194
|
if (!left) return center;
|
|
4065
4195
|
if (!center) return left;
|
|
4066
4196
|
return center.score > left.score + 0.05 ? center : left;
|
|
@@ -4068,10 +4198,59 @@ function columnStarts(rows, anchors, cfg, x0, x1) {
|
|
|
4068
4198
|
function bandDataRows(rows, anchors, kind, sheetKey, buildings, cfg) {
|
|
4069
4199
|
const { x0, x1, medGap } = bandLimits(anchors);
|
|
4070
4200
|
const typeKeyed = kind === "equipment" && (anchors[0]?.label === "TYPE" || anchors[0]?.label === "FIXTURE");
|
|
4071
|
-
const cols = columnStarts(rows, anchors, cfg, x0, x1);
|
|
4201
|
+
const cols = columnStarts(rows, anchors, cfg, x0, x1, kind);
|
|
4202
|
+
const xCap = cols?.capX;
|
|
4203
|
+
const inBand = (t) => t.x >= x0 && t.x <= x1 && (xCap == null || t.x < xCap);
|
|
4204
|
+
const finish = kind === "finish";
|
|
4205
|
+
let curSection;
|
|
4206
|
+
const halfGap = anchors.length > 1 ? (anchors[1].x - anchors[0].x) / 2 : 40;
|
|
4207
|
+
const keyLo = cols ? cols.cols[0].start : anchors[0].x - halfGap;
|
|
4208
|
+
const keyHi = cols ? cols.cols.length > 1 ? cols.cols[1].start : Infinity : anchors[0].x + halfGap;
|
|
4209
|
+
const colTol = cols?.tol ?? 4;
|
|
4210
|
+
const atOf = (t) => cols && cols.coord === "center" ? centerX(t) : t.x;
|
|
4211
|
+
const inKey = (t) => {
|
|
4212
|
+
const a = atOf(t);
|
|
4213
|
+
return a >= Math.min(x0, keyLo - colTol) && a < keyHi;
|
|
4214
|
+
};
|
|
4215
|
+
let tLeft = 0, tRight = 0, medPitch = 0;
|
|
4216
|
+
let prevBandY = null;
|
|
4217
|
+
if (finish) {
|
|
4218
|
+
let ys = rows.slice(Math.max(cfg.fromIdx, 0)).filter((r) => r.some(inBand)).map(rowY);
|
|
4219
|
+
const med = (v) => {
|
|
4220
|
+
const d = v.slice(1).map((y, k) => y - v[k]).sort((p, q) => p - q);
|
|
4221
|
+
return d.length ? d[d.length >> 1] : 0;
|
|
4222
|
+
};
|
|
4223
|
+
const p0 = med(ys);
|
|
4224
|
+
const cut = p0 > 0 ? ys.findIndex((y, k) => k > 0 && y - ys[k - 1] > 8 * p0) : -1;
|
|
4225
|
+
if (cut > 0) ys = ys.slice(0, cut);
|
|
4226
|
+
const lastTableY = ys.length ? ys[ys.length - 1] : Infinity;
|
|
4227
|
+
if (cols) {
|
|
4228
|
+
const cs = cols.cols;
|
|
4229
|
+
const pitch2 = cs.length > 1 ? cs.slice(1).map((c, k) => c.start - cs[k].start).sort((p, q) => p - q)[cs.length - 1 >> 1] : medGap;
|
|
4230
|
+
tLeft = cs[0].start;
|
|
4231
|
+
tRight = cs[cs.length - 1].start + pitch2;
|
|
4232
|
+
} else {
|
|
4233
|
+
tLeft = Infinity;
|
|
4234
|
+
tRight = -Infinity;
|
|
4235
|
+
for (let i = Math.max(cfg.fromIdx - 1, 0); i < rows.length && rowY(rows[i]) <= lastTableY; i++) for (const t of rows[i]) {
|
|
4236
|
+
if (!inBand(t)) continue;
|
|
4237
|
+
tLeft = Math.min(tLeft, t.x);
|
|
4238
|
+
tRight = Math.max(tRight, t.x + (t.w || 0));
|
|
4239
|
+
}
|
|
4240
|
+
}
|
|
4241
|
+
medPitch = med(ys);
|
|
4242
|
+
}
|
|
4243
|
+
const headings = [];
|
|
4244
|
+
const headLines = [];
|
|
4245
|
+
const consumeHeading = (banded, y) => {
|
|
4246
|
+
let hb = null;
|
|
4247
|
+
for (const t of banded) hb = hb ? merge(hb, bboxOf(t)) : bboxOf(t);
|
|
4248
|
+
headings.push({ bbox: hb, y });
|
|
4249
|
+
};
|
|
4072
4250
|
const keyTol = cols && cols.cols.length > 1 ? Math.max(8, (cols.cols[1].start - cols.cols[0].start) * 0.5) : 40;
|
|
4073
4251
|
const out = [];
|
|
4074
4252
|
const outY = [];
|
|
4253
|
+
const everRows = [];
|
|
4075
4254
|
let region = null;
|
|
4076
4255
|
const columnOf = (t) => {
|
|
4077
4256
|
if (!cols) return nearestAnchor(centerX(t), anchors);
|
|
@@ -4117,9 +4296,36 @@ function bandDataRows(rows, anchors, kind, sheetKey, buildings, cfg) {
|
|
|
4117
4296
|
if (centerX(t) >= x0 - 2.5 * medGap && centerX(t) <= x1 + medGap) markers.push({ rev, span: t, ...tri ? { drawn: true, tri } : {} });
|
|
4118
4297
|
continue;
|
|
4119
4298
|
}
|
|
4120
|
-
if (t
|
|
4299
|
+
if (inBand(t)) banded.push(t);
|
|
4121
4300
|
}
|
|
4122
4301
|
if (!banded.length) continue;
|
|
4302
|
+
const bandYBefore = prevBandY;
|
|
4303
|
+
prevBandY = rowY(rows[i]);
|
|
4304
|
+
if (finish) {
|
|
4305
|
+
const h = finishSectionOf(banded[0].str);
|
|
4306
|
+
const joinedLen = banded.map((t) => t.str.trim()).join(" ").length;
|
|
4307
|
+
const inKeyColumn = inKey(banded[0]) && banded.every((t) => t.x + (t.w || 0) <= keyHi);
|
|
4308
|
+
const one = banded.length === 1 ? banded[0] : null;
|
|
4309
|
+
const straddles = !!one && !!cols && cols.cols.slice(1).some((c) => one.x < c.start - colTol && one.x + (one.w || 0) > c.start + colTol);
|
|
4310
|
+
const hugs = bandYBefore != null && rowY(rows[i]) - bandYBefore < 0.6 * medPitch;
|
|
4311
|
+
const tMid = (tLeft + tRight) / 2;
|
|
4312
|
+
const centered = !!one && !hugs && (Math.abs(centerX(one) - tMid) <= Math.max(0.1 * (tRight - tLeft), 3 * (one.h || 8)) || straddles);
|
|
4313
|
+
if (h && (inKeyColumn || centered) && joinedLen < 24) {
|
|
4314
|
+
curSection = h;
|
|
4315
|
+
consumeHeading(banded, rowY(rows[i]));
|
|
4316
|
+
continue;
|
|
4317
|
+
}
|
|
4318
|
+
if (h && !hugs) headLines.push(rowY(rows[i]));
|
|
4319
|
+
if (banded.length <= 2 && inKey(banded[0]) && /^\d{2} ?\d{2} ?\d{2}(\.\d+)?\b/.test(norm(banded[0].str)) && !rowKeyOf(banded[0].str, kind, buildings, typeKeyed)) {
|
|
4320
|
+
curSection = void 0;
|
|
4321
|
+
consumeHeading(banded, rowY(rows[i]));
|
|
4322
|
+
continue;
|
|
4323
|
+
}
|
|
4324
|
+
if (one && inKey(one) && !rowKeyOf(one.str, kind, buildings, typeKeyed)) {
|
|
4325
|
+
curSection = void 0;
|
|
4326
|
+
if (!hugs && joinedLen < 24 && !/\d/.test(one.str)) continue;
|
|
4327
|
+
}
|
|
4328
|
+
}
|
|
4123
4329
|
if (kind === "equipment" && out.length) {
|
|
4124
4330
|
if (rows[i].some((t) => /SCHEDULE/.test(norm(t.str)) && !EQUIP_KEY_RE.test(norm(t.str).replace(/[^A-Z0-9-]/g, "")))) break;
|
|
4125
4331
|
const hh = headerHits(rows[i], EQUIPMENT_HEADERS);
|
|
@@ -4138,11 +4344,13 @@ function bandDataRows(rows, anchors, kind, sheetKey, buildings, cfg) {
|
|
|
4138
4344
|
if (cfg.keyAlign && Math.abs(centerX(banded[0]) - cfg.keyAlign.x) > cfg.keyAlign.tol) continue;
|
|
4139
4345
|
const row = { key: keyed.key, sheet: sheetKey, cells: {} };
|
|
4140
4346
|
if (keyed.building) row.building = keyed.building;
|
|
4347
|
+
if (curSection) row.section = curSection;
|
|
4141
4348
|
add(row, banded);
|
|
4142
4349
|
out.push(row);
|
|
4143
4350
|
outY.push(rowY(rows[i]));
|
|
4351
|
+
everRows.push(row);
|
|
4144
4352
|
}
|
|
4145
|
-
if (out.length > 2) {
|
|
4353
|
+
if (out.length > 2 && !(finish && cfg.marquee)) {
|
|
4146
4354
|
const d = outY.slice(1).map((y, i) => y - outY[i]).filter((g) => g > 0).sort((a, b) => a - b);
|
|
4147
4355
|
const pitch0 = d.length ? d[d.length >> 1] : 0;
|
|
4148
4356
|
if (pitch0 > 0) {
|
|
@@ -4157,7 +4365,9 @@ function bandDataRows(rows, anchors, kind, sheetKey, buildings, cfg) {
|
|
|
4157
4365
|
}
|
|
4158
4366
|
}
|
|
4159
4367
|
}
|
|
4160
|
-
const
|
|
4368
|
+
const headY = [...headings.map((h) => h.y), ...headLines];
|
|
4369
|
+
const ly2 = outY.length > 1 ? [...outY, ...headY].filter((y) => y <= outY[outY.length - 1]).sort((a, b) => a - b) : [];
|
|
4370
|
+
const gaps = ly2.slice(1).map((y, i) => y - ly2[i]).filter((d) => d > 0).sort((a, b) => a - b);
|
|
4161
4371
|
const pitch = gaps.length ? gaps[gaps.length >> 1] : 0;
|
|
4162
4372
|
const nearest = (y) => {
|
|
4163
4373
|
let bi = -1, bd = Infinity;
|
|
@@ -4171,9 +4381,11 @@ function bandDataRows(rows, anchors, kind, sheetKey, buildings, cfg) {
|
|
|
4171
4381
|
return { i: bi, d: bd };
|
|
4172
4382
|
};
|
|
4173
4383
|
const radius = (h) => pitch ? pitch * 0.6 : Math.max(h, 8) * 1.6;
|
|
4384
|
+
const headYs = [...headY].sort((p, q) => p - q);
|
|
4174
4385
|
for (const o of orphans) {
|
|
4175
4386
|
const { i, d } = nearest(o.y);
|
|
4176
4387
|
if (i < 0 || d > radius(Math.max(...o.toks.map((t) => t.h || 8)))) continue;
|
|
4388
|
+
if (headYs.some((hy) => hy !== o.y && (Math.abs(o.y - hy) < d || Math.abs(o.y - hy) === d && hy < outY[i]))) continue;
|
|
4177
4389
|
add(out[i], o.toks);
|
|
4178
4390
|
}
|
|
4179
4391
|
for (const m of markers) {
|
|
@@ -4182,17 +4394,45 @@ function bandDataRows(rows, anchors, kind, sheetKey, buildings, cfg) {
|
|
|
4182
4394
|
const ebox = m.tri ? merge(bboxOf(m.span), m.tri) : bboxOf(m.span);
|
|
4183
4395
|
out[i].revision = { rev: m.rev, source: { sheet: sheetKey, text: m.span.str.trim(), bbox: ebox }, ...m.drawn ? { drawn: true } : {} };
|
|
4184
4396
|
}
|
|
4397
|
+
if (kind === "finish") {
|
|
4398
|
+
const keyCol = cols ? cols.cols[0].label : anchors[0].label;
|
|
4399
|
+
const dropped = /* @__PURE__ */ new Set();
|
|
4400
|
+
for (let i = out.length - 1; i >= 0; i--) {
|
|
4401
|
+
const r = out[i];
|
|
4402
|
+
if (!/^[A-Z]{1,3}$/.test(r.key)) continue;
|
|
4403
|
+
const keyRest = norm(r.cells[keyCol]?.text ?? r.key).replace(/[^A-Z0-9 ]/g, "").trim().replace(new RegExp("^" + r.key + "\\b"), "").trim();
|
|
4404
|
+
const texts = [...keyRest ? [keyRest] : [], ...Object.entries(r.cells).filter(([k]) => k !== keyCol).map(([, c]) => norm(c.text))];
|
|
4405
|
+
if (texts.length && !texts.some((x) => /^\d{2} ?\d{2}[ .]?\d{2}\b/.test(x))) continue;
|
|
4406
|
+
if (out.slice(i + 1, i + 4).some((n) => n.key.startsWith(r.key + "-") || new RegExp("^" + r.key + "\\d").test(n.key))) {
|
|
4407
|
+
dropped.add(r);
|
|
4408
|
+
out.splice(i, 1);
|
|
4409
|
+
outY.splice(i, 1);
|
|
4410
|
+
}
|
|
4411
|
+
}
|
|
4412
|
+
if (dropped.size) {
|
|
4413
|
+
region = null;
|
|
4414
|
+
for (const r of everRows) if (!dropped.has(r)) for (const c of Object.values(r.cells)) region = region ? merge(region, c.bbox) : c.bbox;
|
|
4415
|
+
}
|
|
4416
|
+
}
|
|
4185
4417
|
for (const row of out) {
|
|
4186
4418
|
if (row.building) continue;
|
|
4187
4419
|
const cellB = norm(row.cells.BLDG?.text || row.cells.BUILDING?.text || "");
|
|
4188
4420
|
if (DESIGNATOR_RE.test(cellB)) row.building = cellB;
|
|
4189
4421
|
}
|
|
4422
|
+
const lastY = outY.length ? outY[outY.length - 1] : -Infinity;
|
|
4423
|
+
for (const hd of headings) if (hd.y <= lastY) region = region ? merge(region, hd.bbox) : hd.bbox;
|
|
4190
4424
|
return { out, region };
|
|
4191
4425
|
}
|
|
4192
4426
|
function extractTable(sheet, kind, opts = {}) {
|
|
4193
4427
|
const r = extractTableCore(sheet, kind, opts);
|
|
4194
4428
|
return r && "table" in r ? r.table : null;
|
|
4195
4429
|
}
|
|
4430
|
+
function readFinishTable(sheet, opts = {}) {
|
|
4431
|
+
const r = extractTableCore(sheet, "finish", opts);
|
|
4432
|
+
if (!r || !("table" in r)) return null;
|
|
4433
|
+
if (r.table.title && isNonFinishSchedule(r.table.title.text)) return { refused: "other-family", table: r.table };
|
|
4434
|
+
return { table: r.table, headerWords: r.headerWords };
|
|
4435
|
+
}
|
|
4196
4436
|
function extractTables(sheet, kind, opts = {}) {
|
|
4197
4437
|
const out = [];
|
|
4198
4438
|
let spans = sheet.spans;
|
|
@@ -4222,6 +4462,7 @@ function extractTableCore(sheet, kind, opts = {}) {
|
|
|
4222
4462
|
const minHits = kind === "room-finish" ? 4 : 3;
|
|
4223
4463
|
let anchors;
|
|
4224
4464
|
let headerSpans;
|
|
4465
|
+
let hdrBlock;
|
|
4225
4466
|
let dataFrom;
|
|
4226
4467
|
let dataBelowY = -Infinity;
|
|
4227
4468
|
let titleFrom;
|
|
@@ -4230,6 +4471,14 @@ function extractTableCore(sheet, kind, opts = {}) {
|
|
|
4230
4471
|
if (flat) {
|
|
4231
4472
|
anchors = flat.anchors;
|
|
4232
4473
|
headerSpans = rows[flat.rowIndex];
|
|
4474
|
+
const hy = rowY(rows[flat.rowIndex]);
|
|
4475
|
+
const hhs = headerSpans.map((t) => t.h || 8).sort((a, b) => a - b);
|
|
4476
|
+
const hh = hhs[hhs.length >> 1];
|
|
4477
|
+
const band = /* @__PURE__ */ new Set();
|
|
4478
|
+
for (let k = flat.top; k <= flat.rowIndex; k++) band.add(k);
|
|
4479
|
+
for (let k = flat.rowIndex + 1; k < rows.length && rowY(rows[k]) - hy <= 1.5 * hh; k++) band.add(k);
|
|
4480
|
+
for (let k = flat.top, n = 0; n < 2 && k > 0 && rowY(rows[k]) - rowY(rows[k - 1]) <= 3 * hh; n++) band.add(--k);
|
|
4481
|
+
hdrBlock = [...band].flatMap((k) => rows[k]);
|
|
4233
4482
|
dataFrom = flat.rowIndex + 1;
|
|
4234
4483
|
titleFrom = flat.rowIndex - 1;
|
|
4235
4484
|
} else {
|
|
@@ -4254,7 +4503,7 @@ function extractTableCore(sheet, kind, opts = {}) {
|
|
|
4254
4503
|
if (centerX(t) < hdrBand.x0 || centerX(t) > hdrBand.x1) continue;
|
|
4255
4504
|
region = region ? merge(region, bboxOf(t)) : bboxOf(t);
|
|
4256
4505
|
}
|
|
4257
|
-
const banded = bandDataRows(rows, anchors, kind, sheet.key, opts.buildings, { fromIdx: dataFrom, belowY: dataBelowY, deltas: opts.deltas, sheetNumbers: opts.sheetNumbers });
|
|
4506
|
+
const banded = bandDataRows(rows, anchors, kind, sheet.key, opts.buildings, { fromIdx: dataFrom, belowY: dataBelowY, deltas: opts.deltas, sheetNumbers: opts.sheetNumbers, hdrSpans: headerSpans, hdrBand: hdrBlock, marquee: opts.marquee });
|
|
4258
4507
|
const out = banded.out;
|
|
4259
4508
|
if (banded.region) region = region ? merge(region, banded.region) : banded.region;
|
|
4260
4509
|
if (!out.length) {
|
|
@@ -4269,7 +4518,8 @@ function extractTableCore(sheet, kind, opts = {}) {
|
|
|
4269
4518
|
}
|
|
4270
4519
|
const table = { kind, sheet: sheet.key, title, headers: anchors.map((a) => a.label), rows: out, region, anchors };
|
|
4271
4520
|
if (rotated) table.rotated_headers = true;
|
|
4272
|
-
|
|
4521
|
+
const headerWords = headerSpans.flatMap((t) => t.str.toUpperCase().split(/[^A-Z]+/)).filter(Boolean);
|
|
4522
|
+
return { table, headerWords };
|
|
4273
4523
|
}
|
|
4274
4524
|
var CONT_TAIL_RE = /[\s\-–—:.,(]*(?:CONTINUATION|CONTINUED|CONT['’]?D?)[\s.)]*$/;
|
|
4275
4525
|
var isContinuationTitle = (text) => {
|
|
@@ -4427,17 +4677,15 @@ function buildSheetGraph(sheets) {
|
|
|
4427
4677
|
roles.set(s.key, classifySheetRole(s));
|
|
4428
4678
|
const sheetFrags = [];
|
|
4429
4679
|
const found2 = [];
|
|
4430
|
-
|
|
4431
|
-
|
|
4432
|
-
|
|
4433
|
-
|
|
4434
|
-
|
|
4680
|
+
const xo = { buildings, deltas: deltasBySheet.get(s.key), sheetNumbers: sheetNumberSet };
|
|
4681
|
+
const rf = extractTable(s, "room-finish", xo);
|
|
4682
|
+
if (rf) found2.push(rf);
|
|
4683
|
+
const fin = readFinishTable(s, xo);
|
|
4684
|
+
if (fin && "refused" in fin) {
|
|
4685
|
+
notes.push(`${s.key}: "${fin.table.title.text}" names another schedule family, not a finish/material schedule \u2014 its ${fin.table.rows.length} rows are NOT indexed as finish definitions`);
|
|
4686
|
+
} else if (fin) found2.push(fin.table);
|
|
4687
|
+
found2.push(...extractTables(s, "equipment", xo));
|
|
4435
4688
|
for (const t of found2) {
|
|
4436
|
-
const kind = t.kind;
|
|
4437
|
-
if (kind === "finish" && t.title && isNonFinishSchedule(t.title.text)) {
|
|
4438
|
-
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`);
|
|
4439
|
-
continue;
|
|
4440
|
-
}
|
|
4441
4689
|
const titleB = t.title ? buildingMentions(t.title.text) : [];
|
|
4442
4690
|
const b = titleB.length === 1 ? titleB[0] : ctxBySheet.get(s.key);
|
|
4443
4691
|
if (b) t.building = b;
|
|
@@ -7028,6 +7276,11 @@ function attrValue(attrs, colId) {
|
|
|
7028
7276
|
|
|
7029
7277
|
// ../web/src/lib/units.ts
|
|
7030
7278
|
var M_PER_FT = 0.3048;
|
|
7279
|
+
var M2_PER_SF = 0.09290304;
|
|
7280
|
+
var areaVal = (sf, units) => units === "metric" ? sf * M2_PER_SF : sf;
|
|
7281
|
+
var areaUnit = (units) => units === "metric" ? "m\xB2" : "SF";
|
|
7282
|
+
var lenVal = (lf, units) => units === "metric" ? lf * M_PER_FT : lf;
|
|
7283
|
+
var lenUnit = (units) => units === "metric" ? "m" : "LF";
|
|
7031
7284
|
function dimLabel(feet, units = "imperial") {
|
|
7032
7285
|
if (!Number.isFinite(feet)) return "";
|
|
7033
7286
|
if (units === "metric") return `${(feet * M_PER_FT).toFixed(2)} m`;
|
|
@@ -7041,6 +7294,49 @@ function dimLabel(feet, units = "imperial") {
|
|
|
7041
7294
|
return `${sign}${ft}'-${inch}"`;
|
|
7042
7295
|
}
|
|
7043
7296
|
|
|
7297
|
+
// ../web/src/lib/liveCounter.js
|
|
7298
|
+
function fmtQty(n) {
|
|
7299
|
+
const v = Math.round((Number(n) || 0) * 100) / 100;
|
|
7300
|
+
return v.toLocaleString("en-US", { maximumFractionDigits: 2 });
|
|
7301
|
+
}
|
|
7302
|
+
|
|
7303
|
+
// ../web/src/lib/noteFields.ts
|
|
7304
|
+
var FIELD_RE = /\{\{\s*([^{}]*?)\s*\}\}/g;
|
|
7305
|
+
var FIELD_WARN_INK = "#b03a26";
|
|
7306
|
+
var FIELD_WARN_INK_DARK = "#ff6b57";
|
|
7307
|
+
function hasFields(text) {
|
|
7308
|
+
return typeof text === "string" && text.includes("{{") && new RegExp(FIELD_RE.source).test(text);
|
|
7309
|
+
}
|
|
7310
|
+
function qtyLabels(totals, units = "imperial") {
|
|
7311
|
+
const out = /* @__PURE__ */ new Map();
|
|
7312
|
+
for (const t of totals || []) {
|
|
7313
|
+
if (!t || !t.shape_count) continue;
|
|
7314
|
+
const parts = [];
|
|
7315
|
+
if (t.total_sf) parts.push(`${fmtQty(areaVal(t.total_sf, units))} ${areaUnit(units)}`);
|
|
7316
|
+
if (t.lf) parts.push(`${fmtQty(lenVal(t.lf, units))} ${lenUnit(units)}`);
|
|
7317
|
+
if (t.ea) parts.push(`${fmtQty(t.ea)} EA`);
|
|
7318
|
+
if (parts.length) out.set(t.id, parts.join(" \xB7 "));
|
|
7319
|
+
}
|
|
7320
|
+
return out;
|
|
7321
|
+
}
|
|
7322
|
+
function resolveNote(text, conditionId, labels) {
|
|
7323
|
+
const s = typeof text === "string" ? text : "";
|
|
7324
|
+
if (!hasFields(s)) return { text: s, unresolved: [] };
|
|
7325
|
+
const unresolved = [];
|
|
7326
|
+
const label = typeof conditionId === "string" && conditionId ? labels.get(conditionId) : void 0;
|
|
7327
|
+
const out = s.replace(new RegExp(FIELD_RE.source, "g"), (whole, name) => {
|
|
7328
|
+
if (name.toLowerCase() === "qty" && label) return label;
|
|
7329
|
+
unresolved.push(whole);
|
|
7330
|
+
return whole;
|
|
7331
|
+
});
|
|
7332
|
+
return { text: out, unresolved };
|
|
7333
|
+
}
|
|
7334
|
+
function resolveMarkup(m, labels) {
|
|
7335
|
+
if (!m || !hasFields(m.text)) return { m, unresolved: [] };
|
|
7336
|
+
const r = resolveNote(m.text, m.condition_id, labels);
|
|
7337
|
+
return { m: r.unresolved.length ? { ...m, text: r.text, field_warn: true } : { ...m, text: r.text }, unresolved: r.unresolved };
|
|
7338
|
+
}
|
|
7339
|
+
|
|
7044
7340
|
// ../web/src/lib/totals.js
|
|
7045
7341
|
function accumulateRole(acc, s) {
|
|
7046
7342
|
const cp = s.computed || {};
|
|
@@ -7196,6 +7492,7 @@ function grandTotals(rows) {
|
|
|
7196
7492
|
}
|
|
7197
7493
|
function reportJson({ projectName = "", rows = [], bySheet = [], scaleInfo = [], markups = [], rfis = [], sheetLabel = null, conditionColumns = [], attrsByCond = null, shapeLabels = [], byLabel = [], displayUnits = "imperial", rollGoods = [], proposedConditionEdits = [] }) {
|
|
7198
7494
|
const label = (id) => sheetLabel ? sheetLabel(id) : id;
|
|
7495
|
+
const noteQty = qtyLabels(rows || [], displayUnits === "metric" ? "metric" : "imperial");
|
|
7199
7496
|
const colDefs = (Array.isArray(conditionColumns) ? conditionColumns : []).filter((cc) => cc && typeof cc === "object" && typeof cc.id === "string");
|
|
7200
7497
|
const attrs = attrsByCond instanceof Map ? attrsByCond : /* @__PURE__ */ new Map();
|
|
7201
7498
|
return {
|
|
@@ -7232,9 +7529,14 @@ function reportJson({ projectName = "", rows = [], bySheet = [], scaleInfo = [],
|
|
|
7232
7529
|
// resolved finish_tag rather than only the id, so a reader of the export
|
|
7233
7530
|
// can see WHICH scope an annotation is about without joining two arrays;
|
|
7234
7531
|
// the id stays authoritative. Unattached markups: "" for both.
|
|
7532
|
+
// text_resolved APPENDS (same rule), and only on a note that carries a
|
|
7533
|
+
// {{field}} — omit-when-empty, so every other markup row is unchanged.
|
|
7534
|
+
// `text` stays the stored note, template and all; text_resolved is what the
|
|
7535
|
+
// sheet shows, {{qty}} filled from the linked condition's measured quantity
|
|
7536
|
+
// (noteFields, #474). An unresolved field stays literal in both.
|
|
7235
7537
|
markups: markups.map((m) => {
|
|
7236
7538
|
const c = m.condition_id ? (rows || []).find((r) => r.id === m.condition_id) : null;
|
|
7237
|
-
return { type: m.type, sheet_id: m.sheet_id, sheet: label(m.sheet_id), text: m.text || "", id: m.id ?? null, rfi_id: m.rfi_id || "", condition_id: m.condition_id || "", condition: c?.finish_tag || "" };
|
|
7539
|
+
return { type: m.type, sheet_id: m.sheet_id, sheet: label(m.sheet_id), text: m.text || "", id: m.id ?? null, rfi_id: m.rfi_id || "", condition_id: m.condition_id || "", condition: c?.finish_tag || "", ...hasFields(m.text) ? { text_resolved: resolveNote(m.text, m.condition_id, noteQty).text } : {} };
|
|
7238
7540
|
}),
|
|
7239
7541
|
// rfis APPENDS after markups (additive-only v1 — old exports had no RFI
|
|
7240
7542
|
// register). linked_markups/linked_sheets are DERIVED from markup.rfi_id,
|
|
@@ -11547,6 +11849,12 @@ var Session = class _Session {
|
|
|
11547
11849
|
seals = seals.filter((a) => a.shape_id !== void 0 && shapeById.get(a.shape_id)?.condition_id === c.id);
|
|
11548
11850
|
}
|
|
11549
11851
|
const s0 = this.sheets;
|
|
11852
|
+
const qtyL = rows.some((m) => hasFields(m.text)) ? qtyLabels(conditionTotals(this.conditions, this.shapes)) : /* @__PURE__ */ new Map();
|
|
11853
|
+
const fields = (m) => {
|
|
11854
|
+
if (!hasFields(m.text)) return {};
|
|
11855
|
+
const r = resolveNote(m.text, m.condition_id, qtyL);
|
|
11856
|
+
return { text_resolved: r.text, ...r.unresolved.length ? { unresolved_fields: r.unresolved } : {} };
|
|
11857
|
+
};
|
|
11550
11858
|
const px = (m, p) => {
|
|
11551
11859
|
const sh = s0.get(m.sheet_id);
|
|
11552
11860
|
if (!p || !sh) return void 0;
|
|
@@ -11566,7 +11874,8 @@ var Session = class _Session {
|
|
|
11566
11874
|
...m.from ? { from: px(m, m.from) } : {},
|
|
11567
11875
|
...m.to ? { to: px(m, m.to) } : {},
|
|
11568
11876
|
...m.r != null ? { r: round1(m.r * (s0.get(m.sheet_id)?.widthPx ?? 0)) } : {},
|
|
11569
|
-
...m.len_ft != null ? { length_lf: m.len_ft } : {}
|
|
11877
|
+
...m.len_ft != null ? { length_lf: m.len_ft } : {},
|
|
11878
|
+
...fields(m)
|
|
11570
11879
|
})),
|
|
11571
11880
|
count: rows.length,
|
|
11572
11881
|
unattached: rows.filter((m) => !m.condition_id).length,
|
|
@@ -13083,7 +13392,9 @@ var annotationRow = z.object({
|
|
|
13083
13392
|
from: z.tuple([z.number(), z.number()]).optional().describe("Arrow tail / dimension start (image px)"),
|
|
13084
13393
|
to: z.tuple([z.number(), z.number()]).optional().describe("Arrow head / dimension end (image px)"),
|
|
13085
13394
|
r: z.number().optional().describe("Bubble radius (image px)"),
|
|
13086
|
-
length_lf: z.number().optional().describe("Dimension only: the measured length in real feet, snapshotted at annotate time from the sheet scale")
|
|
13395
|
+
length_lf: z.number().optional().describe("Dimension only: the measured length in real feet, snapshotted at annotate time from the sheet scale"),
|
|
13396
|
+
text_resolved: z.string().optional().describe("Only when text carries a {{qty}} field: the note as the sheet shows it, {{qty}} filled with the linked condition's measured quantity (multiplier applied, no waste)"),
|
|
13397
|
+
unresolved_fields: z.array(z.string()).optional().describe("Fields that could not fill (no linked condition, nothing measured yet, or an unknown name) \u2014 they stay literal and print in the warning ink; link_annotation fixes the first case")
|
|
13087
13398
|
});
|
|
13088
13399
|
var editAnnotationOutput = { id: z.string(), text: z.string(), note: z.string() };
|
|
13089
13400
|
var annotateOutput = {
|
|
@@ -13293,7 +13604,7 @@ function annotationScene(m, W, H, pixelScale = 2) {
|
|
|
13293
13604
|
const width = Math.max(45 * pixelScale, ...lines.map((l) => l.length * font * 0.62)) + pad * 2;
|
|
13294
13605
|
const height = Math.max(1, lines.length) * font * 1.35 + pad * 2;
|
|
13295
13606
|
add([...path3([at, [at[0] + width, at[1]], [at[0] + width, at[1] + height], [at[0], at[1] + height]]), ["Z"]], { fill: "#ffffff" });
|
|
13296
|
-
lines.forEach((text, i) => texts.push({ text, x: at[0] + pad, y: at[1] + pad + font * (1 + i * 1.35), size: font, color: "#172033" }));
|
|
13607
|
+
lines.forEach((text, i) => texts.push({ text, x: at[0] + pad, y: at[1] + pad + font * (1 + i * 1.35), size: font, color: m.field_warn ? FIELD_WARN_INK : "#172033" }));
|
|
13297
13608
|
return [[at[0], at[1]], [at[0] + width, at[1] + height]];
|
|
13298
13609
|
};
|
|
13299
13610
|
let noteBox2 = null;
|
|
@@ -14099,6 +14410,15 @@ function invertPixels(cv) {
|
|
|
14099
14410
|
}
|
|
14100
14411
|
async function buildMarkedSetPdf({ projectName, dark, sheets, shapes, markups, approvals = [], rfis: rfisIn = [], conditions, getPage, loadPdfData, company, clientInfo, credit = null, provenance = null, coverTitle = "Marked Set", units = "imperial" }) {
|
|
14101
14412
|
markups = (markups || []).filter((m) => !m.reference_only);
|
|
14413
|
+
const fieldWarn = /* @__PURE__ */ new Set();
|
|
14414
|
+
{
|
|
14415
|
+
const qtyL = qtyLabels(conditionTotals(conditions || [], shapes || []), units);
|
|
14416
|
+
markups = markups.map((m0) => {
|
|
14417
|
+
const r = resolveMarkup(m0, qtyL);
|
|
14418
|
+
if (r.unresolved.length) fieldWarn.add(m0.id);
|
|
14419
|
+
return r.m;
|
|
14420
|
+
});
|
|
14421
|
+
}
|
|
14102
14422
|
const rfis = liveRfis(rfisIn);
|
|
14103
14423
|
const M = units === "metric";
|
|
14104
14424
|
const uA = (sf) => M ? sf * 0.09290304 : sf;
|
|
@@ -14516,6 +14836,7 @@ async function buildMarkedSetPdf({ projectName, dark, sheets, shapes, markups, a
|
|
|
14516
14836
|
const mCond = m.condition_id ? (conditions || []).find((c) => c.id === m.condition_id) : null;
|
|
14517
14837
|
const mbase = m.color || mCond?.color || (m.rfi_id ? COBALT : "#c47a10");
|
|
14518
14838
|
const mcol = rgb(...hex(dark ? boostForDark(mbase) : mbase));
|
|
14839
|
+
const tcol = fieldWarn.has(m.id) ? rgb(...hex(dark ? FIELD_WARN_INK_DARK : FIELD_WARN_INK)) : mcol;
|
|
14519
14840
|
const mdash = pdfDashFor(m.line_style || "solid");
|
|
14520
14841
|
const mw = clampWeight(m.weight);
|
|
14521
14842
|
if (m.annotation_style && ["arrow", "highlight", "callout", "cloud", "text"].includes(m.type)) {
|
|
@@ -14560,7 +14881,7 @@ async function buildMarkedSetPdf({ projectName, dark, sheets, shapes, markups, a
|
|
|
14560
14881
|
const r = [[nx0 * W, ny0 * H], [nx1 * W, ny0 * H], [nx1 * W, ny1 * H], [nx0 * W, ny1 * H]];
|
|
14561
14882
|
pg.drawSvgPath(svgPath(r), { x: 0, y: 0, color: mcol, opacity: 0.18 + alphaBoost / 2, borderColor: mcol, borderWidth: 1 * mw, borderOpacity: 0.9, ...mdash ? { borderDashArray: mdash } : {} });
|
|
14562
14883
|
const t = lbl(m.text);
|
|
14563
|
-
if (t) text(t, Math.min(nx0, nx1) * W, Math.min(ny0, ny1) * H - 10 / ptScale, 8,
|
|
14884
|
+
if (t) text(t, Math.min(nx0, nx1) * W, Math.min(ny0, ny1) * H - 10 / ptScale, 8, tcol, bold);
|
|
14564
14885
|
} else if (m.type === "cloud" && m.rect) {
|
|
14565
14886
|
const [[nx0, ny0], [nx1, ny1]] = m.rect;
|
|
14566
14887
|
const cb = cloudBezier(nx0 * W, ny0 * H, nx1 * W, ny1 * H);
|
|
@@ -14572,7 +14893,7 @@ async function buildMarkedSetPdf({ projectName, dark, sheets, shapes, markups, a
|
|
|
14572
14893
|
for (const [c1, c2, end] of cb.segments) d += ` C${P(c1)} ${P(c2)} ${P(end)}`;
|
|
14573
14894
|
pg.drawSvgPath(d + " Z", { x: 0, y: 0, borderColor: mcol, borderWidth: 1.3 * mw, borderOpacity: 0.95, ...mdash ? { borderDashArray: mdash } : {} });
|
|
14574
14895
|
const t = lbl(m.text);
|
|
14575
|
-
if (t) text(t, Math.min(nx0, nx1) * W, Math.min(ny0, ny1) * H - 10 / ptScale, 8,
|
|
14896
|
+
if (t) text(t, Math.min(nx0, nx1) * W, Math.min(ny0, ny1) * H - 10 / ptScale, 8, tcol, bold);
|
|
14576
14897
|
if (Number.isFinite(m.rev) && m.rev > 0) {
|
|
14577
14898
|
const cxImg = Math.max(nx0, nx1) * W, cyImg = Math.min(ny0, ny1) * H, s = 9 / ptScale;
|
|
14578
14899
|
const tri = [[cxImg, cyImg - s], [cxImg + s, cyImg + s], [cxImg - s, cyImg + s]];
|
|
@@ -14586,7 +14907,7 @@ async function buildMarkedSetPdf({ projectName, dark, sheets, shapes, markups, a
|
|
|
14586
14907
|
const [ptx, pty] = toPage(m.to[0] * W, m.to[1] * H);
|
|
14587
14908
|
pg.drawSvgPath(arrowheadPath(pfx, -pfy, ptx, -pty, 6 * mw), { x: 0, y: 0, color: mcol, opacity: 0.95 });
|
|
14588
14909
|
const t = lbl(m.text);
|
|
14589
|
-
if (t) text(t, (m.from[0] + m.to[0]) / 2 * W, (m.from[1] + m.to[1]) / 2 * H - 6 / ptScale, 8,
|
|
14910
|
+
if (t) text(t, (m.from[0] + m.to[0]) / 2 * W, (m.from[1] + m.to[1]) / 2 * H - 6 / ptScale, 8, tcol, bold);
|
|
14590
14911
|
} else if (m.type === "dimension" && m.from && m.to) {
|
|
14591
14912
|
const fx = m.from[0] * W, fy = m.from[1] * H, dxq = m.to[0] * W, dyq = m.to[1] * H;
|
|
14592
14913
|
line(fx, fy, dxq, dyq, mcol, 1.1 * mw, 0.95, mdash);
|
|
@@ -14600,7 +14921,7 @@ async function buildMarkedSetPdf({ projectName, dark, sheets, shapes, markups, a
|
|
|
14600
14921
|
const size = 8;
|
|
14601
14922
|
const tw = bold.widthOfTextAtSize(winAnsiSafe(t), size);
|
|
14602
14923
|
const [pmx, pmy] = toPage((fx + dxq) / 2 + dnx * (14 / ptScale), (fy + dyq) / 2 + dny * (14 / ptScale));
|
|
14603
|
-
pg.drawText(winAnsiSafe(t), { x: pmx - tw / 2, y: pmy - size / 2.7, size, font: bold, color:
|
|
14924
|
+
pg.drawText(winAnsiSafe(t), { x: pmx - tw / 2, y: pmy - size / 2.7, size, font: bold, color: tcol, rotate: chipRot });
|
|
14604
14925
|
}
|
|
14605
14926
|
} else if (m.type === "bubble" && m.at) {
|
|
14606
14927
|
const cxImg = m.at[0] * W, cyImg = m.at[1] * H;
|
|
@@ -14611,7 +14932,7 @@ async function buildMarkedSetPdf({ projectName, dark, sheets, shapes, markups, a
|
|
|
14611
14932
|
if (t) {
|
|
14612
14933
|
const size = 8;
|
|
14613
14934
|
const tw = bold.widthOfTextAtSize(winAnsiSafe(t), size);
|
|
14614
|
-
pg.drawText(winAnsiSafe(t), { x: pcx - tw / 2, y: pcy - size / 2.7, size, font: bold, color:
|
|
14935
|
+
pg.drawText(winAnsiSafe(t), { x: pcx - tw / 2, y: pcy - size / 2.7, size, font: bold, color: tcol, rotate: chipRot });
|
|
14615
14936
|
}
|
|
14616
14937
|
} else if (m.type === "callout" && m.at) {
|
|
14617
14938
|
if (m.target) {
|
|
@@ -14620,9 +14941,9 @@ async function buildMarkedSetPdf({ projectName, dark, sheets, shapes, markups, a
|
|
|
14620
14941
|
const [ptx, pty] = toPage(m.target[0] * W, m.target[1] * H);
|
|
14621
14942
|
pg.drawSvgPath(arrowheadPath(pax, -pay, ptx, -pty, 5), { x: 0, y: 0, color: mcol, opacity: 0.9 });
|
|
14622
14943
|
}
|
|
14623
|
-
noteBlock(lbl(m.text), m.at[0] * W, m.at[1] * H,
|
|
14944
|
+
noteBlock(lbl(m.text), m.at[0] * W, m.at[1] * H, tcol, dark ? rgb(0.08, 0.1, 0.12) : rgb(1, 1, 1));
|
|
14624
14945
|
} else if (m.type === "text" && m.at) {
|
|
14625
|
-
noteBlock(lbl(m.text), m.at[0] * W, m.at[1] * H,
|
|
14946
|
+
noteBlock(lbl(m.text), m.at[0] * W, m.at[1] * H, tcol, dark ? rgb(0.08, 0.1, 0.12) : rgb(1, 0.97, 0.93));
|
|
14626
14947
|
} else if (m.type === "svg" && m.at && Array.isArray(m.vb) && typeof m.path === "string") {
|
|
14627
14948
|
const { s: sx, bw, bh } = svgPlacedBox(m.vb, m.w, W);
|
|
14628
14949
|
if (sx > 0) {
|
|
@@ -14634,7 +14955,7 @@ async function buildMarkedSetPdf({ projectName, dark, sheets, shapes, markups, a
|
|
|
14634
14955
|
const fillOn = m.fill && m.fill !== "none";
|
|
14635
14956
|
if (d) pg.drawSvgPath(d, { x: 0, y: 0, borderColor: mcol, borderWidth: 1.2 * mw, borderOpacity: 0.95, ...fillOn ? { color: rgb(...hex(dark ? boostForDark(m.fill) : m.fill)), opacity: 0.9 } : {} });
|
|
14636
14957
|
const t = lbl(m.text);
|
|
14637
|
-
if (t) text(t, m.at[0] * W - bw / 2, y0 - 6 / ptScale, 8,
|
|
14958
|
+
if (t) text(t, m.at[0] * W - bw / 2, y0 - 6 / ptScale, 8, tcol, bold);
|
|
14638
14959
|
}
|
|
14639
14960
|
} else if (m.type === "image" && m.at && m.src) {
|
|
14640
14961
|
const fmt3 = pickEmbedFormat(m.src);
|
|
@@ -15640,7 +15961,7 @@ All-or-nothing, like derive_base: an unknown tag, a transition landing on either
|
|
|
15640
15961
|
server.registerTool("annotate", {
|
|
15641
15962
|
description: `Place an annotation on a sheet \u2014 a note ABOUT the work, never a measurement of it. Types: cloud and highlight take rect:[[x0,y0],[x1,y1]] (a revision cloud around an area, a highlight box over it), text takes at:[x,y], callout takes at:[x,y] plus target:[x,y] (the point its leader aims at), arrow takes from:[x,y] and to:[x,y] (tail and head \u2014 plank/seam direction, the markup flooring drawings use most; #150), bubble takes at:[x,y] plus optional r (a keynote/detail circle carrying centered text), dimension takes from:[x,y] and to:[x,y] (its two measured endpoints) and labels itself with the length between them at the sheet's scale \u2014 drawn as a dimension line with end ticks and the measurement centered. A dimension states a REAL length, so it is the one annotation the scale gate applies to: on an unscaled sheet it refuses exactly like the measure tools (set_scale first) rather than dressing a px figure up as feet. It still touches no quantity \u2014 a dimension is a note about a distance, not a takeoff line item.
|
|
15642
15963
|
|
|
15643
|
-
Pass condition to attach the note to a finish tag, which is what makes it part of that SCOPE rather than a floating remark: it then wears the condition's colour on the canvas and in the marked-set PDF, and travels with it into the report. The tag is minted on first touch like ${oneClick ? "one_click/" : ""}measure_polygon, so you can annotate CPT-1 before anything is traced for it. Omit condition for a note about the sheet itself.
|
|
15964
|
+
Pass condition to attach the note to a finish tag, which is what makes it part of that SCOPE rather than a floating remark: it then wears the condition's colour on the canvas and in the marked-set PDF, and travels with it into the report. The tag is minted on first touch like ${oneClick ? "one_click/" : ""}measure_polygon, so you can annotate CPT-1 before anything is traced for it. Omit condition for a note about the sheet itself. Write {{qty}} in the text to quote the linked condition's live measured quantity (multiplier applied, no waste): "x{{qty}} fixtures" reads "x6 EA fixtures" on the sheet and follows every later count; the stored text keeps the field, and list_annotations reports text_resolved.
|
|
15644
15965
|
|
|
15645
15966
|
No review gate: the pencil-not-ink rule exists to stop an agent inventing geometry, and a cloud reading "verify substrate" is not geometry. It touches no quantity. ${COORDS}`,
|
|
15646
15967
|
inputSchema: {
|
|
@@ -15741,7 +16062,7 @@ No review gate: the pencil-not-ink rule exists to stop an agent inventing geomet
|
|
|
15741
16062
|
import { ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
15742
16063
|
|
|
15743
16064
|
// src/wiki.generated.ts
|
|
15744
|
-
var WIKI_VERSION = "0.9.
|
|
16065
|
+
var WIKI_VERSION = "0.9.94";
|
|
15745
16066
|
var WIKI_PAGES = [
|
|
15746
16067
|
{
|
|
15747
16068
|
"key": "index",
|
|
@@ -15756,8 +16077,8 @@ var WIKI_PAGES = [
|
|
|
15756
16077
|
"uri": "takeoff://wiki/status",
|
|
15757
16078
|
"title": "Capability status",
|
|
15758
16079
|
"source": "docs/wiki/status.md",
|
|
15759
|
-
"source_sha256": "
|
|
15760
|
-
"text": "# Capability status\n\nThis page describes implementation support, not a promise that every plan can\nbe measured automatically. Follow its source links before changing behavior.\n\n| Capability | Current boundary | Evidence |\n|---|---|---|\n| Human and agent measurements | Shared geometry/quantity modules; source interpretation still requires inspection | [Geometry workflow](https://github.com/Kentucky-ai/opentakeoff/blob/main/docs/GEOMETRY_WORKFLOW.md) |\n| Original machine geometry after human correction | Outer vertices preserved, including manual agent traces; missing historical originals cannot be recovered | [Compatibility](https://github.com/Kentucky-ai/opentakeoff/blob/main/protocol/COMPATIBILITY.md) |\n| Human stitching | Browser gallery and alignment; editable browser archive preserves composites | [Workflow](takeoff://wiki/workflows) |\n| MCP stitched documents | No stitch tool; current import/export omits stitch records | [Field inventory](https://github.com/Kentucky-ai/opentakeoff/blob/main/protocol/INVENTORY.md) |\n| One-Click | Temporarily gated in the default browser/MCP builds | [Gate](https://github.com/Kentucky-ai/opentakeoff/blob/main/docs/design/ONE_CLICK_GATE.md) |\n| Browser/MCP room boundaries | Shared quantity modules do not prove identical detector boundaries; the open [#385](https://github.com/Kentucky-ai/opentakeoff/issues/385) report remains unresolved | [Architecture](takeoff://wiki/architecture), [geometry evidence](https://github.com/Kentucky-ai/opentakeoff/blob/main/docs/GEOMETRY_WORKFLOW.md) |\n| Toolbar command and voice | Gated in the default build; use normal controls and keyboard shortcuts | [User guide](https://github.com/Kentucky-ai/opentakeoff/blob/main/docs/USER_GUIDE.md#17-voice-and-the-command-box) |\n| TakeoffDocument v1 | Draft schemas, preflight and opt-in repository adapters for a bounded profile; legacy writer format remains | [Protocol](takeoff://wiki/protocol) |\n| Annotation cleanup | Text-only MCP edit with undo; RFI-linked notes refuse | [MCP routing](takeoff://wiki/mcp) |\n| Agent knowledge | Packaged wiki resources and source-generated tool/schema references | [Index](takeoff://wiki) |\n| Academy interoperability | Incompatibilities inventoried; no adapter or certification bridge claimed | [Academy report](https://github.com/Kentucky-ai/opentakeoff/blob/main/protocol/ACADEMY_COMPATIBILITY.md) |\n| Shared work review | Personal layouts/shared review proposal is not merged; do not describe [PR #388](https://github.com/Kentucky-ai/opentakeoff/pull/388) as shipped | [Roadmap](https://github.com/Kentucky-ai/opentakeoff/blob/main/docs/ROADMAP.md) |\n| Microsoft 365 annotation sync | Unproven across machines; the live validation remains open in [#315](https://github.com/Kentucky-ai/opentakeoff/issues/315) | [Roadmap](https://github.com/Kentucky-ai/opentakeoff/blob/main/docs/ROADMAP.md) |\n| Learned floor-model bridge | Not on `main`; no model-generated geometry integration is advertised | [Roadmap](https://github.com/Kentucky-ai/opentakeoff/blob/main/docs/ROADMAP.md) |\n| Independent geometry benchmark | Reference-assisted and synthetic checks are not independently reviewed ground truth | [Geometry evidence](https://github.com/Kentucky-ai/opentakeoff/blob/main/docs/GEOMETRY_WORKFLOW.md#reusable-evidence-from-a-project-test) |\n\nNext protocol work connects the tested contracts to focused MCP workflows and\nmeasures tool-selection/completion results. Broader adapter profiles remain\nbounded by preservation tests. Package extraction, verified identity/signatures, Academy credentials\nand optional anchoring come later. Rendering optimization is separate from\nthis protocol and knowledge work.\n"
|
|
16080
|
+
"source_sha256": "a5957a23818ad11fedc3af59af7ae1c094267873dbc306db7d1145154816b148",
|
|
16081
|
+
"text": "# Capability status\n\nThis page describes implementation support, not a promise that every plan can\nbe measured automatically. Follow its source links before changing behavior.\n\n| Capability | Current boundary | Evidence |\n|---|---|---|\n| Human and agent measurements | Shared geometry/quantity modules; source interpretation still requires inspection | [Geometry workflow](https://github.com/Kentucky-ai/opentakeoff/blob/main/docs/GEOMETRY_WORKFLOW.md) |\n| Original machine geometry after human correction | Outer vertices preserved, including manual agent traces; missing historical originals cannot be recovered | [Compatibility](https://github.com/Kentucky-ai/opentakeoff/blob/main/protocol/COMPATIBILITY.md) |\n| Human stitching | Browser gallery and alignment; editable browser archive preserves composites | [Workflow](takeoff://wiki/workflows) |\n| MCP stitched documents | No stitch tool; current import/export omits stitch records | [Field inventory](https://github.com/Kentucky-ai/opentakeoff/blob/main/protocol/INVENTORY.md) |\n| One-Click | Temporarily gated in the default browser/MCP builds | [Gate](https://github.com/Kentucky-ai/opentakeoff/blob/main/docs/design/ONE_CLICK_GATE.md) |\n| Browser/MCP room boundaries | Shared quantity modules do not prove identical detector boundaries; the open [#385](https://github.com/Kentucky-ai/opentakeoff/issues/385) report remains unresolved | [Architecture](takeoff://wiki/architecture), [geometry evidence](https://github.com/Kentucky-ai/opentakeoff/blob/main/docs/GEOMETRY_WORKFLOW.md) |\n| Plan-set search, Copy text, on-device page reads | Browser gallery and canvas; MCP text tools read the text layer only, no OCR. OCR reads scanned pages after a download notice and is labelled OCR; Copy text works on single sheets, not stitched surfaces. Kept OCR reads stay in this browser after sign-out, kept by the PDF's content hash | [User guide](https://github.com/Kentucky-ai/opentakeoff/blob/main/docs/USER_GUIDE.md#copy-text) |\n| Toolbar command and voice | Gated in the default build; use normal controls and keyboard shortcuts | [User guide](https://github.com/Kentucky-ai/opentakeoff/blob/main/docs/USER_GUIDE.md#17-voice-and-the-command-box) |\n| TakeoffDocument v1 | Draft schemas, preflight and opt-in repository adapters for a bounded profile; legacy writer format remains | [Protocol](takeoff://wiki/protocol) |\n| Annotation cleanup | Text-only MCP edit with undo; RFI-linked notes refuse | [MCP routing](takeoff://wiki/mcp) |\n| Agent knowledge | Packaged wiki resources and source-generated tool/schema references | [Index](takeoff://wiki) |\n| Academy interoperability | Incompatibilities inventoried; no adapter or certification bridge claimed | [Academy report](https://github.com/Kentucky-ai/opentakeoff/blob/main/protocol/ACADEMY_COMPATIBILITY.md) |\n| Shared work review | Personal layouts/shared review proposal is not merged; do not describe [PR #388](https://github.com/Kentucky-ai/opentakeoff/pull/388) as shipped | [Roadmap](https://github.com/Kentucky-ai/opentakeoff/blob/main/docs/ROADMAP.md) |\n| Microsoft 365 annotation sync | Unproven across machines; the live validation remains open in [#315](https://github.com/Kentucky-ai/opentakeoff/issues/315) | [Roadmap](https://github.com/Kentucky-ai/opentakeoff/blob/main/docs/ROADMAP.md) |\n| Learned floor-model bridge | Not on `main`; no model-generated geometry integration is advertised | [Roadmap](https://github.com/Kentucky-ai/opentakeoff/blob/main/docs/ROADMAP.md) |\n| Independent geometry benchmark | Reference-assisted and synthetic checks are not independently reviewed ground truth | [Geometry evidence](https://github.com/Kentucky-ai/opentakeoff/blob/main/docs/GEOMETRY_WORKFLOW.md#reusable-evidence-from-a-project-test) |\n\nNext protocol work connects the tested contracts to focused MCP workflows and\nmeasures tool-selection/completion results. Broader adapter profiles remain\nbounded by preservation tests. Package extraction, verified identity/signatures, Academy credentials\nand optional anchoring come later. Rendering optimization is separate from\nthis protocol and knowledge work.\n"
|
|
15761
16082
|
},
|
|
15762
16083
|
{
|
|
15763
16084
|
"key": "architecture",
|
|
@@ -16352,7 +16673,7 @@ function nameTheStageInRefusals(server) {
|
|
|
16352
16673
|
// package.json
|
|
16353
16674
|
var package_default = {
|
|
16354
16675
|
name: "opentakeoff-mcp",
|
|
16355
|
-
version: "0.9.
|
|
16676
|
+
version: "0.9.94",
|
|
16356
16677
|
mcpName: "io.github.Kentucky-ai/opentakeoff",
|
|
16357
16678
|
type: "module",
|
|
16358
16679
|
description: "OpenTakeoff MCP server \u2014 drive the takeoff engine from your MCP client over stdio.",
|
|
@@ -16372,7 +16693,7 @@ var package_default = {
|
|
|
16372
16693
|
"check:wiki": "node scripts/check-wiki.mjs",
|
|
16373
16694
|
"check:versions": "node ../scripts/check-version-consistency.mjs",
|
|
16374
16695
|
"check:tool-count": "node --import tsx scripts/check-tool-count.mjs",
|
|
16375
|
-
test: "node --import tsx --test test/arc.test.ts test/checkers.test.ts test/conformance.test.ts test/constants.test.ts test/context.test.ts test/document.test.ts test/dxf.test.ts test/e2e.test.ts test/gate.test.ts test/graphEvalRowsym.test.ts test/labels.test.ts test/mep.test.ts test/overlap.test.ts test/parity.test.ts test/proposals.test.ts test/raster.test.ts test/resources.test.ts test/safewrite.test.ts test/scalewarn.test.ts test/scope.test.ts test/session.test.ts test/splittags.test.ts test/staging.test.ts test/sweepguard.test.ts test/tools.test.ts test/transitions.test.ts test/twins.test.ts test/view.test.ts test/wiki.test.ts"
|
|
16696
|
+
test: "node --import tsx --test test/arc.test.ts test/checkers.test.ts test/conformance.test.ts test/constants.test.ts test/context.test.ts test/document.test.ts test/dxf.test.ts test/e2e.test.ts test/finishSchedule.test.ts test/gate.test.ts test/graphEvalRowsym.test.ts test/labels.test.ts test/mep.test.ts test/overlap.test.ts test/pageSpans.test.ts test/parity.test.ts test/proposals.test.ts test/raster.test.ts test/resources.test.ts test/safewrite.test.ts test/scalewarn.test.ts test/scheduleImport.test.ts test/scope.test.ts test/session.test.ts test/splittags.test.ts test/staging.test.ts test/sweepguard.test.ts test/tools.test.ts test/transitions.test.ts test/twins.test.ts test/view.test.ts test/wiki.test.ts"
|
|
16376
16697
|
},
|
|
16377
16698
|
dependencies: {
|
|
16378
16699
|
"@modelcontextprotocol/sdk": "^1.12.0",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "opentakeoff-mcp",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.94",
|
|
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.",
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
"check:wiki": "node scripts/check-wiki.mjs",
|
|
21
21
|
"check:versions": "node ../scripts/check-version-consistency.mjs",
|
|
22
22
|
"check:tool-count": "node --import tsx scripts/check-tool-count.mjs",
|
|
23
|
-
"test": "node --import tsx --test test/arc.test.ts test/checkers.test.ts test/conformance.test.ts test/constants.test.ts test/context.test.ts test/document.test.ts test/dxf.test.ts test/e2e.test.ts test/gate.test.ts test/graphEvalRowsym.test.ts test/labels.test.ts test/mep.test.ts test/overlap.test.ts test/parity.test.ts test/proposals.test.ts test/raster.test.ts test/resources.test.ts test/safewrite.test.ts test/scalewarn.test.ts test/scope.test.ts test/session.test.ts test/splittags.test.ts test/staging.test.ts test/sweepguard.test.ts test/tools.test.ts test/transitions.test.ts test/twins.test.ts test/view.test.ts test/wiki.test.ts"
|
|
23
|
+
"test": "node --import tsx --test test/arc.test.ts test/checkers.test.ts test/conformance.test.ts test/constants.test.ts test/context.test.ts test/document.test.ts test/dxf.test.ts test/e2e.test.ts test/finishSchedule.test.ts test/gate.test.ts test/graphEvalRowsym.test.ts test/labels.test.ts test/mep.test.ts test/overlap.test.ts test/pageSpans.test.ts test/parity.test.ts test/proposals.test.ts test/raster.test.ts test/resources.test.ts test/safewrite.test.ts test/scalewarn.test.ts test/scheduleImport.test.ts test/scope.test.ts test/session.test.ts test/splittags.test.ts test/staging.test.ts test/sweepguard.test.ts test/tools.test.ts test/transitions.test.ts test/twins.test.ts test/view.test.ts test/wiki.test.ts"
|
|
24
24
|
},
|
|
25
25
|
"dependencies": {
|
|
26
26
|
"@modelcontextprotocol/sdk": "^1.12.0",
|