opentakeoff-mcp 0.9.87 → 0.9.93
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 +1031 -135
- package/package.json +2 -2
package/dist/server-core.js
CHANGED
|
@@ -311,6 +311,84 @@ function detectScale(textContent, viewport) {
|
|
|
311
311
|
return null;
|
|
312
312
|
}
|
|
313
313
|
|
|
314
|
+
// ../web/src/lib/textjoin.ts
|
|
315
|
+
var JOIN_GAP = 0.08;
|
|
316
|
+
var JOIN_OVERLAP = 0.05;
|
|
317
|
+
var LINE_TOL = 0.25;
|
|
318
|
+
var SIZE_TOL = 0.25;
|
|
319
|
+
function axis(s) {
|
|
320
|
+
const r = s.rot ?? 0;
|
|
321
|
+
if (r === 90) return { a0: s.y0, a1: s.y1, p: (s.x0 + s.x1) / 2, h: s.x1 - s.x0 };
|
|
322
|
+
if (r === 270) return { a0: -s.y1, a1: -s.y0, p: (s.x0 + s.x1) / 2, h: s.x1 - s.x0 };
|
|
323
|
+
if (r === 180) return { a0: -s.x1, a1: -s.x0, p: (s.y0 + s.y1) / 2, h: s.y1 - s.y0 };
|
|
324
|
+
return { a0: s.x0, a1: s.x1, p: (s.y0 + s.y1) / 2, h: s.y1 - s.y0 };
|
|
325
|
+
}
|
|
326
|
+
function abuttingGroups(spans) {
|
|
327
|
+
const n = spans.length;
|
|
328
|
+
const next = new Int32Array(n).fill(-1);
|
|
329
|
+
const hasPrev = new Uint8Array(n);
|
|
330
|
+
const byRot = /* @__PURE__ */ new Map();
|
|
331
|
+
for (let i = 0; i < n; i++) {
|
|
332
|
+
const r = spans[i].rot ?? 0;
|
|
333
|
+
let l = byRot.get(r);
|
|
334
|
+
if (!l) byRot.set(r, l = []);
|
|
335
|
+
l.push(i);
|
|
336
|
+
}
|
|
337
|
+
const ax = spans.map(axis);
|
|
338
|
+
for (const [rot, idx] of byRot) {
|
|
339
|
+
if (rot % 90 !== 0) continue;
|
|
340
|
+
idx.sort((i, j) => ax[i].a0 - ax[j].a0);
|
|
341
|
+
for (let k = 0; k < idx.length; k++) {
|
|
342
|
+
const i = idx[k], A = ax[i];
|
|
343
|
+
if (!(A.h > 0) || !spans[i].str) continue;
|
|
344
|
+
let best = -1, bestGap = Infinity;
|
|
345
|
+
for (let m = k + 1; m < idx.length; m++) {
|
|
346
|
+
const j = idx[m], B = ax[j];
|
|
347
|
+
if (B.a0 - A.a1 > JOIN_GAP * A.h) break;
|
|
348
|
+
if (!(B.h > 0) || !spans[j].str) continue;
|
|
349
|
+
const h = Math.max(A.h, B.h);
|
|
350
|
+
if (Math.abs(A.h - B.h) > SIZE_TOL * h) continue;
|
|
351
|
+
if (Math.abs(A.p - B.p) > LINE_TOL * h) continue;
|
|
352
|
+
const gap = B.a0 - A.a1;
|
|
353
|
+
if (gap < -JOIN_OVERLAP * h || gap > JOIN_GAP * h) continue;
|
|
354
|
+
if (/\d$/.test(spans[i].str) && /^\d/.test(spans[j].str)) continue;
|
|
355
|
+
if (Math.abs(gap) < Math.abs(bestGap)) {
|
|
356
|
+
best = j;
|
|
357
|
+
bestGap = gap;
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
if (best >= 0 && !hasPrev[best] && next[i] < 0) {
|
|
361
|
+
next[i] = best;
|
|
362
|
+
hasPrev[best] = 1;
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
const groups = [];
|
|
367
|
+
for (let i = 0; i < n; i++) {
|
|
368
|
+
if (hasPrev[i]) continue;
|
|
369
|
+
const g = [i];
|
|
370
|
+
for (let j = next[i], guard = 0; j >= 0 && guard < n; j = next[j], guard++) g.push(j);
|
|
371
|
+
groups.push(g);
|
|
372
|
+
}
|
|
373
|
+
return groups;
|
|
374
|
+
}
|
|
375
|
+
function joinAbuttingSpans(spans) {
|
|
376
|
+
return abuttingGroups(spans).map((g) => {
|
|
377
|
+
if (g.length === 1) return spans[g[0]];
|
|
378
|
+
const first = spans[g[0]];
|
|
379
|
+
let { x0, y0, x1, y1 } = first, str = first.str;
|
|
380
|
+
for (const j of g.slice(1)) {
|
|
381
|
+
const s = spans[j];
|
|
382
|
+
str += s.str;
|
|
383
|
+
x0 = Math.min(x0, s.x0);
|
|
384
|
+
y0 = Math.min(y0, s.y0);
|
|
385
|
+
x1 = Math.max(x1, s.x1);
|
|
386
|
+
y1 = Math.max(y1, s.y1);
|
|
387
|
+
}
|
|
388
|
+
return { ...first, str, x0, y0, x1, y1 };
|
|
389
|
+
});
|
|
390
|
+
}
|
|
391
|
+
|
|
314
392
|
// src/pdf.ts
|
|
315
393
|
var requireHere = createRequire(import.meta.url);
|
|
316
394
|
var PDFJS_ROOT = path.dirname(requireHere.resolve("pdfjs-dist/package.json"));
|
|
@@ -422,14 +500,7 @@ async function openPdf(filePath) {
|
|
|
422
500
|
};
|
|
423
501
|
}
|
|
424
502
|
function positionedText(ph) {
|
|
425
|
-
|
|
426
|
-
for (const it of ph.textContent.items || []) {
|
|
427
|
-
const str = it.str || "";
|
|
428
|
-
if (!str.trim()) continue;
|
|
429
|
-
const t = pdfjs.Util.transform(ph.viewport.transform, it.transform);
|
|
430
|
-
out.push({ str, x: +t[4].toFixed(1), y: +t[5].toFixed(1) });
|
|
431
|
-
}
|
|
432
|
-
return out;
|
|
503
|
+
return joinAbuttingSpans(rawSpans(ph)).map((s) => ({ str: s.str, x: s.ox, y: s.oy }));
|
|
433
504
|
}
|
|
434
505
|
function textItemsInRegion(ph, r) {
|
|
435
506
|
const items = [];
|
|
@@ -440,7 +511,7 @@ function textItemsInRegion(ph, r) {
|
|
|
440
511
|
}
|
|
441
512
|
return { items };
|
|
442
513
|
}
|
|
443
|
-
function
|
|
514
|
+
function rawSpans(ph) {
|
|
444
515
|
const out = [];
|
|
445
516
|
for (const it of ph.textContent.items || []) {
|
|
446
517
|
const str = it.str || "";
|
|
@@ -458,6 +529,8 @@ function textSpans(ph) {
|
|
|
458
529
|
const rot = (Math.round(Math.atan2(dy, dx) * 180 / Math.PI) % 360 + 360) % 360;
|
|
459
530
|
out.push({
|
|
460
531
|
str,
|
|
532
|
+
ox: +x.toFixed(1),
|
|
533
|
+
oy: +y.toFixed(1),
|
|
461
534
|
x0: +Math.min(...xs).toFixed(1),
|
|
462
535
|
y0: +Math.min(...ys).toFixed(1),
|
|
463
536
|
x1: +Math.max(...xs).toFixed(1),
|
|
@@ -467,6 +540,9 @@ function textSpans(ph) {
|
|
|
467
540
|
}
|
|
468
541
|
return out;
|
|
469
542
|
}
|
|
543
|
+
function textSpans(ph) {
|
|
544
|
+
return joinAbuttingSpans(rawSpans(ph)).map(({ ox: _ox, oy: _oy, ...span }) => span);
|
|
545
|
+
}
|
|
470
546
|
|
|
471
547
|
// src/scalewarn.ts
|
|
472
548
|
var SCALE_NOTE_MARGIN_XY = 0.1;
|
|
@@ -830,6 +906,13 @@ function escalationParams(sensitivity) {
|
|
|
830
906
|
}
|
|
831
907
|
function strokeLuminance(args) {
|
|
832
908
|
const first = args?.[0];
|
|
909
|
+
if (typeof first === "string") {
|
|
910
|
+
const hex2 = /^#([0-9a-f]{6})$/i.exec(first);
|
|
911
|
+
if (!hex2) return null;
|
|
912
|
+
const v = parseInt(hex2[1], 16);
|
|
913
|
+
const L2 = 0.2126 * (v >> 16 & 255) + 0.7152 * (v >> 8 & 255) + 0.0722 * (v & 255);
|
|
914
|
+
return Math.max(0, Math.min(255, Math.round(L2)));
|
|
915
|
+
}
|
|
833
916
|
const a = Array.isArray(first) || ArrayBuffer.isView(first) && !(first instanceof DataView) ? first : args;
|
|
834
917
|
if (!a || a.length < 3) return null;
|
|
835
918
|
const [r, g, b] = [a[0], a[1], a[2]].map((v) => typeof v === "number" && Number.isFinite(v) ? v : NaN);
|
|
@@ -838,6 +921,43 @@ function strokeLuminance(args) {
|
|
|
838
921
|
const L = (0.2126 * r + 0.7152 * g + 0.0722 * b) * k;
|
|
839
922
|
return Math.max(0, Math.min(255, Math.round(L)));
|
|
840
923
|
}
|
|
924
|
+
var DRAW_MOVETO = 0;
|
|
925
|
+
var DRAW_LINETO = 1;
|
|
926
|
+
var DRAW_CURVETO = 2;
|
|
927
|
+
var DRAW_CLOSEPATH = 3;
|
|
928
|
+
function decodeConstructPath(args, OPS3) {
|
|
929
|
+
if (!args) return null;
|
|
930
|
+
const first = args[0];
|
|
931
|
+
if (Array.isArray(first) || ArrayBuffer.isView(first)) {
|
|
932
|
+
return { ops: first, co: args[1], paintFn: null };
|
|
933
|
+
}
|
|
934
|
+
if (typeof first !== "number") return null;
|
|
935
|
+
const flat = Array.isArray(args[1]) ? args[1][0] : null;
|
|
936
|
+
if (!flat || !(Array.isArray(flat) || ArrayBuffer.isView(flat))) return null;
|
|
937
|
+
const f = flat;
|
|
938
|
+
const ops = [], co = [];
|
|
939
|
+
for (let k = 0; k < f.length; ) {
|
|
940
|
+
const c = f[k++];
|
|
941
|
+
if (c === DRAW_MOVETO) {
|
|
942
|
+
ops.push(OPS3.moveTo);
|
|
943
|
+
co.push(f[k++], f[k++]);
|
|
944
|
+
} else if (c === DRAW_LINETO) {
|
|
945
|
+
ops.push(OPS3.lineTo);
|
|
946
|
+
co.push(f[k++], f[k++]);
|
|
947
|
+
} else if (c === DRAW_CURVETO) {
|
|
948
|
+
ops.push(OPS3.curveTo);
|
|
949
|
+
co.push(f[k++], f[k++], f[k++], f[k++], f[k++], f[k++]);
|
|
950
|
+
} else if (c === DRAW_CLOSEPATH) {
|
|
951
|
+
ops.push(OPS3.closePath);
|
|
952
|
+
} else break;
|
|
953
|
+
}
|
|
954
|
+
return { ops, co, paintFn: first };
|
|
955
|
+
}
|
|
956
|
+
function embeddedPaintFlags(paintFn, OPS3) {
|
|
957
|
+
if (paintFn === OPS3.endPath) return SEG_CLIP;
|
|
958
|
+
if (paintFn === OPS3.fill || paintFn === OPS3.eoFill) return SEG_FILLONLY;
|
|
959
|
+
return 0;
|
|
960
|
+
}
|
|
841
961
|
function extractVectorGeometry(opList, transform, OPS3) {
|
|
842
962
|
const points = [];
|
|
843
963
|
const segs = [];
|
|
@@ -978,9 +1098,11 @@ function extractVectorGeometry(opList, transform, OPS3) {
|
|
|
978
1098
|
if (t) imageArea += ctmDet * Math.abs(t[0] * t[3] - t[1] * t[2]);
|
|
979
1099
|
}
|
|
980
1100
|
} else if (fn === OPS3.constructPath) {
|
|
1101
|
+
const decoded = decodeConstructPath(args, OPS3);
|
|
1102
|
+
if (!decoded) continue;
|
|
981
1103
|
const devW = Math.min(15, Math.max(0, Math.ceil((lw || 0) * Math.sqrt(Math.abs(m[0] * m[3] - m[1] * m[2])))));
|
|
982
|
-
const flags = paintFlags(i) | devW << 4;
|
|
983
|
-
const ops =
|
|
1104
|
+
const flags = (decoded.paintFn === null ? paintFlags(i) : embeddedPaintFlags(decoded.paintFn, OPS3)) | devW << 4;
|
|
1105
|
+
const ops = decoded.ops, co = decoded.co;
|
|
984
1106
|
let c = 0, cur = null, start = null;
|
|
985
1107
|
const pathLayer = curLayer;
|
|
986
1108
|
const pathLum = lum;
|
|
@@ -999,7 +1121,8 @@ function extractVectorGeometry(opList, transform, OPS3) {
|
|
|
999
1121
|
cur = p;
|
|
1000
1122
|
visit(p);
|
|
1001
1123
|
};
|
|
1002
|
-
for (
|
|
1124
|
+
for (let oi = 0; oi < ops.length; oi++) {
|
|
1125
|
+
const op = ops[oi];
|
|
1003
1126
|
if (op === OPS3.moveTo) {
|
|
1004
1127
|
cur = tx(co[c], co[c + 1]);
|
|
1005
1128
|
start = cur;
|
|
@@ -1760,8 +1883,8 @@ function sweepDimensionStrings(segs, meta, ws, ftPx, rejects, dimTexts) {
|
|
|
1760
1883
|
if (claimed[c.i]) continue;
|
|
1761
1884
|
let b = Math.floor((c.ang + shift) / BIN);
|
|
1762
1885
|
if (b >= NB) b -= NB;
|
|
1763
|
-
const
|
|
1764
|
-
const th =
|
|
1886
|
+
const axis2 = ((b * BIN - shift + BIN / 2) % 180 + 180) % 180;
|
|
1887
|
+
const th = axis2 * Math.PI / 180, dx = Math.cos(th), dy = Math.sin(th), nx = -dy, ny = dx;
|
|
1765
1888
|
const r = {
|
|
1766
1889
|
c,
|
|
1767
1890
|
d: (c.x1 + c.x2) / 2 * nx + (c.y1 + c.y2) / 2 * ny,
|
|
@@ -1773,7 +1896,7 @@ function sweepDimensionStrings(segs, meta, ws, ftPx, rejects, dimTexts) {
|
|
|
1773
1896
|
else bins.set(b, [r]);
|
|
1774
1897
|
}
|
|
1775
1898
|
for (const [b, rows] of bins) {
|
|
1776
|
-
const
|
|
1899
|
+
const axis2 = ((b * BIN - shift + BIN / 2) % 180 + 180) % 180;
|
|
1777
1900
|
rows.sort((a, z4) => a.d - z4.d || a.t0 - z4.t0);
|
|
1778
1901
|
let g0 = 0;
|
|
1779
1902
|
for (let k = 1; k <= rows.length; k++) {
|
|
@@ -1789,7 +1912,7 @@ function sweepDimensionStrings(segs, meta, ws, ftPx, rejects, dimTexts) {
|
|
|
1789
1912
|
}
|
|
1790
1913
|
cur.d = cur.dSum / cur.members.length;
|
|
1791
1914
|
cur.len = cur.t1 - cur.t0;
|
|
1792
|
-
const th =
|
|
1915
|
+
const th = axis2 * Math.PI / 180, dx = Math.cos(th), dy = Math.sin(th), nx = -dy, ny = dx;
|
|
1793
1916
|
cur.p0 = [cur.t0 * dx + cur.d * nx, cur.t0 * dy + cur.d * ny];
|
|
1794
1917
|
cur.p1 = [cur.t1 * dx + cur.d * nx, cur.t1 * dy + cur.d * ny];
|
|
1795
1918
|
chains.push(cur);
|
|
@@ -1804,7 +1927,7 @@ function sweepDimensionStrings(segs, meta, ws, ftPx, rejects, dimTexts) {
|
|
|
1804
1927
|
cur.dSum += r.d;
|
|
1805
1928
|
} else {
|
|
1806
1929
|
flush();
|
|
1807
|
-
cur = { axis, d: 0, dSum: r.d, t0: r.t0, t1: r.t1, len: 0, members: [r.c.i], p0: [0, 0], p1: [0, 0] };
|
|
1930
|
+
cur = { axis: axis2, d: 0, dSum: r.d, t0: r.t0, t1: r.t1, len: 0, members: [r.c.i], p0: [0, 0], p1: [0, 0] };
|
|
1808
1931
|
}
|
|
1809
1932
|
}
|
|
1810
1933
|
flush();
|
|
@@ -2775,14 +2898,14 @@ function sealAttempt(mo, ix, iy, sensitivity, radii, minPassPx = 0, given) {
|
|
|
2775
2898
|
const { mw, mh, mask, ws } = mo;
|
|
2776
2899
|
const isDim = (i) => (mask[i] & MASK_DIMSTR_BIT) !== 0 && (mask[i] & 1) === 0;
|
|
2777
2900
|
const capCells = mw * mh * SEAL_MAX_SHEET_FRAC;
|
|
2778
|
-
const
|
|
2901
|
+
const box2 = boxOf(ref.region, mw, mh);
|
|
2779
2902
|
const region = ref.region.slice();
|
|
2780
2903
|
let count = ref.count, mergedPieces = 0;
|
|
2781
2904
|
const tried = /* @__PURE__ */ new Set();
|
|
2782
2905
|
let frontier = true;
|
|
2783
2906
|
for (let pass = 0; pass < 8 && frontier; pass++) {
|
|
2784
2907
|
frontier = false;
|
|
2785
|
-
const b = { x0: Math.max(1,
|
|
2908
|
+
const b = { x0: Math.max(1, box2.x0 - 2), y0: Math.max(1, box2.y0 - 2), x1: Math.min(mw - 2, box2.x1 + 2), y1: Math.min(mh - 2, box2.y1 + 2) };
|
|
2786
2909
|
for (let y = b.y0; y <= b.y1; y++) for (let x = b.x0; x <= b.x1; x++) {
|
|
2787
2910
|
const i = y * mw + x;
|
|
2788
2911
|
if (!isDim(i)) continue;
|
|
@@ -2819,10 +2942,10 @@ function sealAttempt(mo, ix, iy, sensitivity, radii, minPassPx = 0, given) {
|
|
|
2819
2942
|
count++;
|
|
2820
2943
|
}
|
|
2821
2944
|
const fb = boxOf(fr.region, mw, mh);
|
|
2822
|
-
|
|
2823
|
-
|
|
2824
|
-
|
|
2825
|
-
|
|
2945
|
+
box2.x0 = Math.min(box2.x0, fb.x0);
|
|
2946
|
+
box2.y0 = Math.min(box2.y0, fb.y0);
|
|
2947
|
+
box2.x1 = Math.max(box2.x1, fb.x1);
|
|
2948
|
+
box2.y1 = Math.max(box2.y1, fb.y1);
|
|
2826
2949
|
mergedPieces++;
|
|
2827
2950
|
frontier = true;
|
|
2828
2951
|
}
|
|
@@ -3311,6 +3434,38 @@ function isLabelBubblePx(ring, b) {
|
|
|
3311
3434
|
return rh <= BUBBLE_RATIO * lh && rw <= BUBBLE_WIDE_RATIO * lw;
|
|
3312
3435
|
}
|
|
3313
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
|
+
|
|
3314
3469
|
// ../web/src/lib/sheetgraph.ts
|
|
3315
3470
|
var bboxOf = (s) => [s.x, s.y, s.x + (s.w || 0), s.y + (s.h || 0)];
|
|
3316
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])];
|
|
@@ -3623,16 +3778,26 @@ var EQUIPMENT_ONLY = /* @__PURE__ */ new Set([
|
|
|
3623
3778
|
var EQUIP_KEY_RE = /^[A-Z]{1,4}(?:-\d{1,3}|\d{1,2})[A-Z]?$/;
|
|
3624
3779
|
var TYPE_KEY_RE = /^[A-Z]{1,2}\d{0,2}[A-Z]?$/;
|
|
3625
3780
|
var headerLabel = (s, vocab) => headerLabels(s, vocab)[0] ?? null;
|
|
3626
|
-
var headerLabels = (s, vocab) => {
|
|
3781
|
+
var headerLabels = (s, vocab, wholeWords = false) => {
|
|
3627
3782
|
const out = [];
|
|
3628
|
-
|
|
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
|
+
}
|
|
3629
3794
|
return out;
|
|
3630
3795
|
};
|
|
3631
|
-
function headerHits(row, vocab) {
|
|
3796
|
+
function headerHits(row, vocab, wholeWords = false) {
|
|
3632
3797
|
const out = [];
|
|
3633
3798
|
const used = /* @__PURE__ */ new Set();
|
|
3634
3799
|
for (const t of row) {
|
|
3635
|
-
const words = headerLabels(t.str, vocab);
|
|
3800
|
+
const words = headerLabels(t.str, vocab, wholeWords);
|
|
3636
3801
|
if (!words.length) continue;
|
|
3637
3802
|
const all = norm(t.str).split(/[^A-Z]+/).filter(Boolean);
|
|
3638
3803
|
if (all.length >= 2 && all.every((w2) => SURFACE_WORDS.has(w2))) {
|
|
@@ -3655,15 +3820,15 @@ var qualifies = (hits, required, minHits) => {
|
|
|
3655
3820
|
};
|
|
3656
3821
|
function findHeaderRow(rows, vocab, required, minHits) {
|
|
3657
3822
|
for (let i = 0; i < rows.length; i++) {
|
|
3823
|
+
if (!qualifies(headerHits(rows[i], vocab, true), required, minHits)) continue;
|
|
3658
3824
|
let hits = headerHits(rows[i], vocab);
|
|
3659
|
-
if (!qualifies(hits, required, minHits)) continue;
|
|
3660
3825
|
let idx = i;
|
|
3661
3826
|
for (; ; ) {
|
|
3662
3827
|
let next = -1;
|
|
3663
3828
|
for (let j = idx + 1; j < Math.min(idx + 4, rows.length); j++) {
|
|
3664
3829
|
const h = headerHits(rows[j], vocab);
|
|
3665
3830
|
const ratio = h.length / Math.max(1, rows[j].length);
|
|
3666
|
-
if (qualifies(
|
|
3831
|
+
if (qualifies(headerHits(rows[j], vocab, true), [], minHits) && h.length > hits.length && ratio >= 0.6) {
|
|
3667
3832
|
next = j;
|
|
3668
3833
|
break;
|
|
3669
3834
|
}
|
|
@@ -3716,7 +3881,7 @@ function findHeaderRow(rows, vocab, required, minHits) {
|
|
|
3716
3881
|
}
|
|
3717
3882
|
}
|
|
3718
3883
|
}
|
|
3719
|
-
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 };
|
|
3720
3885
|
}
|
|
3721
3886
|
return null;
|
|
3722
3887
|
}
|
|
@@ -3776,7 +3941,7 @@ function subTierAnchors(rows, hdrIdx, anchors, vocab) {
|
|
|
3776
3941
|
return out.sort((a, b) => a.x - b.x);
|
|
3777
3942
|
}
|
|
3778
3943
|
function findRotatedHeader(vert, vocab, required, minHits) {
|
|
3779
|
-
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);
|
|
3780
3945
|
let band = [];
|
|
3781
3946
|
let y0 = 0, y1 = 0;
|
|
3782
3947
|
const flush = () => {
|
|
@@ -3831,6 +3996,7 @@ function bandLimits(anchors) {
|
|
|
3831
3996
|
return { x0: anchors[0].x - Math.max(80, medGap / 2), x1: last.x + rightMargin, medGap };
|
|
3832
3997
|
}
|
|
3833
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);
|
|
3834
4000
|
var ROW_KEY_RE = /^\d{1,3}[A-Z]{0,2}$/;
|
|
3835
4001
|
var QUALIFIED_KEY_RE = /^([A-Z]{1,2})-(\d{1,3}[A-Z]{0,2})$/;
|
|
3836
4002
|
var CORRIDOR_KEY_RE = /^[A-Z]{1,3}(?:\d{1,3}-\d{1,3}|\d{3})[A-Z]?$/;
|
|
@@ -3850,8 +4016,8 @@ function rowKeyOf(raw, kind, buildings, typeKeyed = false) {
|
|
|
3850
4016
|
}
|
|
3851
4017
|
if (kind === "finish") {
|
|
3852
4018
|
const parts = kept.split("/").filter(Boolean);
|
|
3853
|
-
if (parts.length > 1 && parts.every(
|
|
3854
|
-
return
|
|
4019
|
+
if (parts.length > 1 && parts.every(finishCodeOk)) return { key: parts.join("/") };
|
|
4020
|
+
return finishCodeOk(key) ? { key } : null;
|
|
3855
4021
|
}
|
|
3856
4022
|
if (ROW_KEY_RE.test(key)) return { key };
|
|
3857
4023
|
if (CORRIDOR_KEY_RE.test(key)) return { key };
|
|
@@ -3867,15 +4033,33 @@ var rowKeyAnswersFor = (key, want) => {
|
|
|
3867
4033
|
var numOf = (key) => key.match(QUALIFIED_KEY_RE)?.[2] ?? key;
|
|
3868
4034
|
var centerX = (t) => t.x + (t.w || 0) / 2;
|
|
3869
4035
|
var PLACEHOLDER_RE = /^[-–—]{1,3}$/;
|
|
3870
|
-
function columnMapFor(rows, anchors, cfg, x0, x1, coord) {
|
|
4036
|
+
function columnMapFor(rows, anchors, cfg, x0, x1, coord, kind) {
|
|
3871
4037
|
const at = (t) => coord === "left" ? t.x : t.x + (t.w || 0) / 2;
|
|
3872
4038
|
const xs = [];
|
|
3873
4039
|
const hs = [];
|
|
3874
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
|
+
}
|
|
3875
4052
|
for (let i = Math.max(cfg.fromIdx, 0); i < rows.length; i++) {
|
|
3876
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
|
+
}
|
|
3877
4061
|
for (const t of rows[i]) {
|
|
3878
|
-
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;
|
|
3879
4063
|
if (PLACEHOLDER_RE.test(t.str.trim())) {
|
|
3880
4064
|
dashes.push(at(t));
|
|
3881
4065
|
continue;
|
|
@@ -3899,13 +4083,48 @@ function columnMapFor(rows, anchors, cfg, x0, x1, coord) {
|
|
|
3899
4083
|
}
|
|
3900
4084
|
const maxN = Math.max(...clusters.map((c) => c.n));
|
|
3901
4085
|
const kept = clusters.filter((c) => c.n >= Math.max(2, maxN * 0.25));
|
|
3902
|
-
|
|
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;
|
|
3903
4112
|
const byLabel = /* @__PURE__ */ new Map();
|
|
3904
|
-
|
|
3905
|
-
|
|
4113
|
+
const byLabelCont = /* @__PURE__ */ new Map();
|
|
4114
|
+
for (const c of cols0) {
|
|
4115
|
+
const own = ownerOf(c);
|
|
3906
4116
|
if (!own) continue;
|
|
3907
|
-
const
|
|
3908
|
-
|
|
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);
|
|
3909
4128
|
}
|
|
3910
4129
|
if (byLabel.size < anchors.length && dashes.length) {
|
|
3911
4130
|
dashes.sort((a, b) => a - b);
|
|
@@ -3925,19 +4144,53 @@ function columnMapFor(rows, anchors, cfg, x0, x1, coord) {
|
|
|
3925
4144
|
byLabel.set(own.label, real ? real.start : c.start);
|
|
3926
4145
|
}
|
|
3927
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
|
+
}
|
|
3928
4156
|
if (byLabel.size !== anchors.length) return null;
|
|
3929
4157
|
const cols = [...byLabel.entries()].map(([label, start]) => ({ label, start })).sort((a, b) => a.start - b.start);
|
|
3930
4158
|
if (cols.map((c) => c.label).join("|") !== anchors.map((a) => a.label).join("|")) return null;
|
|
3931
|
-
|
|
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);
|
|
3932
4169
|
let on = 0;
|
|
3933
4170
|
for (const x of xs) if (starts.some((st) => Math.abs(x - st) <= tol)) on++;
|
|
3934
|
-
|
|
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 } : {} };
|
|
3935
4188
|
}
|
|
3936
|
-
function columnStarts(rows, anchors, cfg, x0, x1) {
|
|
4189
|
+
function columnStarts(rows, anchors, cfg, x0, x1, kind) {
|
|
3937
4190
|
const FIT_FLOOR = 0.7;
|
|
3938
4191
|
const fits = (m) => m && m.score >= FIT_FLOOR ? m : null;
|
|
3939
|
-
const left = fits(columnMapFor(rows, anchors, cfg, x0, x1, "left"));
|
|
3940
|
-
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));
|
|
3941
4194
|
if (!left) return center;
|
|
3942
4195
|
if (!center) return left;
|
|
3943
4196
|
return center.score > left.score + 0.05 ? center : left;
|
|
@@ -3945,10 +4198,59 @@ function columnStarts(rows, anchors, cfg, x0, x1) {
|
|
|
3945
4198
|
function bandDataRows(rows, anchors, kind, sheetKey, buildings, cfg) {
|
|
3946
4199
|
const { x0, x1, medGap } = bandLimits(anchors);
|
|
3947
4200
|
const typeKeyed = kind === "equipment" && (anchors[0]?.label === "TYPE" || anchors[0]?.label === "FIXTURE");
|
|
3948
|
-
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
|
+
};
|
|
3949
4250
|
const keyTol = cols && cols.cols.length > 1 ? Math.max(8, (cols.cols[1].start - cols.cols[0].start) * 0.5) : 40;
|
|
3950
4251
|
const out = [];
|
|
3951
4252
|
const outY = [];
|
|
4253
|
+
const everRows = [];
|
|
3952
4254
|
let region = null;
|
|
3953
4255
|
const columnOf = (t) => {
|
|
3954
4256
|
if (!cols) return nearestAnchor(centerX(t), anchors);
|
|
@@ -3994,9 +4296,36 @@ function bandDataRows(rows, anchors, kind, sheetKey, buildings, cfg) {
|
|
|
3994
4296
|
if (centerX(t) >= x0 - 2.5 * medGap && centerX(t) <= x1 + medGap) markers.push({ rev, span: t, ...tri ? { drawn: true, tri } : {} });
|
|
3995
4297
|
continue;
|
|
3996
4298
|
}
|
|
3997
|
-
if (t
|
|
4299
|
+
if (inBand(t)) banded.push(t);
|
|
3998
4300
|
}
|
|
3999
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
|
+
}
|
|
4000
4329
|
if (kind === "equipment" && out.length) {
|
|
4001
4330
|
if (rows[i].some((t) => /SCHEDULE/.test(norm(t.str)) && !EQUIP_KEY_RE.test(norm(t.str).replace(/[^A-Z0-9-]/g, "")))) break;
|
|
4002
4331
|
const hh = headerHits(rows[i], EQUIPMENT_HEADERS);
|
|
@@ -4015,11 +4344,13 @@ function bandDataRows(rows, anchors, kind, sheetKey, buildings, cfg) {
|
|
|
4015
4344
|
if (cfg.keyAlign && Math.abs(centerX(banded[0]) - cfg.keyAlign.x) > cfg.keyAlign.tol) continue;
|
|
4016
4345
|
const row = { key: keyed.key, sheet: sheetKey, cells: {} };
|
|
4017
4346
|
if (keyed.building) row.building = keyed.building;
|
|
4347
|
+
if (curSection) row.section = curSection;
|
|
4018
4348
|
add(row, banded);
|
|
4019
4349
|
out.push(row);
|
|
4020
4350
|
outY.push(rowY(rows[i]));
|
|
4351
|
+
everRows.push(row);
|
|
4021
4352
|
}
|
|
4022
|
-
if (out.length > 2) {
|
|
4353
|
+
if (out.length > 2 && !(finish && cfg.marquee)) {
|
|
4023
4354
|
const d = outY.slice(1).map((y, i) => y - outY[i]).filter((g) => g > 0).sort((a, b) => a - b);
|
|
4024
4355
|
const pitch0 = d.length ? d[d.length >> 1] : 0;
|
|
4025
4356
|
if (pitch0 > 0) {
|
|
@@ -4034,7 +4365,9 @@ function bandDataRows(rows, anchors, kind, sheetKey, buildings, cfg) {
|
|
|
4034
4365
|
}
|
|
4035
4366
|
}
|
|
4036
4367
|
}
|
|
4037
|
-
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);
|
|
4038
4371
|
const pitch = gaps.length ? gaps[gaps.length >> 1] : 0;
|
|
4039
4372
|
const nearest = (y) => {
|
|
4040
4373
|
let bi = -1, bd = Infinity;
|
|
@@ -4048,9 +4381,11 @@ function bandDataRows(rows, anchors, kind, sheetKey, buildings, cfg) {
|
|
|
4048
4381
|
return { i: bi, d: bd };
|
|
4049
4382
|
};
|
|
4050
4383
|
const radius = (h) => pitch ? pitch * 0.6 : Math.max(h, 8) * 1.6;
|
|
4384
|
+
const headYs = [...headY].sort((p, q) => p - q);
|
|
4051
4385
|
for (const o of orphans) {
|
|
4052
4386
|
const { i, d } = nearest(o.y);
|
|
4053
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;
|
|
4054
4389
|
add(out[i], o.toks);
|
|
4055
4390
|
}
|
|
4056
4391
|
for (const m of markers) {
|
|
@@ -4059,17 +4394,45 @@ function bandDataRows(rows, anchors, kind, sheetKey, buildings, cfg) {
|
|
|
4059
4394
|
const ebox = m.tri ? merge(bboxOf(m.span), m.tri) : bboxOf(m.span);
|
|
4060
4395
|
out[i].revision = { rev: m.rev, source: { sheet: sheetKey, text: m.span.str.trim(), bbox: ebox }, ...m.drawn ? { drawn: true } : {} };
|
|
4061
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
|
+
}
|
|
4062
4417
|
for (const row of out) {
|
|
4063
4418
|
if (row.building) continue;
|
|
4064
4419
|
const cellB = norm(row.cells.BLDG?.text || row.cells.BUILDING?.text || "");
|
|
4065
4420
|
if (DESIGNATOR_RE.test(cellB)) row.building = cellB;
|
|
4066
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;
|
|
4067
4424
|
return { out, region };
|
|
4068
4425
|
}
|
|
4069
4426
|
function extractTable(sheet, kind, opts = {}) {
|
|
4070
4427
|
const r = extractTableCore(sheet, kind, opts);
|
|
4071
4428
|
return r && "table" in r ? r.table : null;
|
|
4072
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
|
+
}
|
|
4073
4436
|
function extractTables(sheet, kind, opts = {}) {
|
|
4074
4437
|
const out = [];
|
|
4075
4438
|
let spans = sheet.spans;
|
|
@@ -4099,6 +4462,7 @@ function extractTableCore(sheet, kind, opts = {}) {
|
|
|
4099
4462
|
const minHits = kind === "room-finish" ? 4 : 3;
|
|
4100
4463
|
let anchors;
|
|
4101
4464
|
let headerSpans;
|
|
4465
|
+
let hdrBlock;
|
|
4102
4466
|
let dataFrom;
|
|
4103
4467
|
let dataBelowY = -Infinity;
|
|
4104
4468
|
let titleFrom;
|
|
@@ -4107,6 +4471,14 @@ function extractTableCore(sheet, kind, opts = {}) {
|
|
|
4107
4471
|
if (flat) {
|
|
4108
4472
|
anchors = flat.anchors;
|
|
4109
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]);
|
|
4110
4482
|
dataFrom = flat.rowIndex + 1;
|
|
4111
4483
|
titleFrom = flat.rowIndex - 1;
|
|
4112
4484
|
} else {
|
|
@@ -4131,7 +4503,7 @@ function extractTableCore(sheet, kind, opts = {}) {
|
|
|
4131
4503
|
if (centerX(t) < hdrBand.x0 || centerX(t) > hdrBand.x1) continue;
|
|
4132
4504
|
region = region ? merge(region, bboxOf(t)) : bboxOf(t);
|
|
4133
4505
|
}
|
|
4134
|
-
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 });
|
|
4135
4507
|
const out = banded.out;
|
|
4136
4508
|
if (banded.region) region = region ? merge(region, banded.region) : banded.region;
|
|
4137
4509
|
if (!out.length) {
|
|
@@ -4146,7 +4518,8 @@ function extractTableCore(sheet, kind, opts = {}) {
|
|
|
4146
4518
|
}
|
|
4147
4519
|
const table = { kind, sheet: sheet.key, title, headers: anchors.map((a) => a.label), rows: out, region, anchors };
|
|
4148
4520
|
if (rotated) table.rotated_headers = true;
|
|
4149
|
-
|
|
4521
|
+
const headerWords = headerSpans.flatMap((t) => t.str.toUpperCase().split(/[^A-Z]+/)).filter(Boolean);
|
|
4522
|
+
return { table, headerWords };
|
|
4150
4523
|
}
|
|
4151
4524
|
var CONT_TAIL_RE = /[\s\-–—:.,(]*(?:CONTINUATION|CONTINUED|CONT['’]?D?)[\s.)]*$/;
|
|
4152
4525
|
var isContinuationTitle = (text) => {
|
|
@@ -4304,17 +4677,15 @@ function buildSheetGraph(sheets) {
|
|
|
4304
4677
|
roles.set(s.key, classifySheetRole(s));
|
|
4305
4678
|
const sheetFrags = [];
|
|
4306
4679
|
const found2 = [];
|
|
4307
|
-
|
|
4308
|
-
|
|
4309
|
-
|
|
4310
|
-
|
|
4311
|
-
|
|
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));
|
|
4312
4688
|
for (const t of found2) {
|
|
4313
|
-
const kind = t.kind;
|
|
4314
|
-
if (kind === "finish" && t.title && isNonFinishSchedule(t.title.text)) {
|
|
4315
|
-
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`);
|
|
4316
|
-
continue;
|
|
4317
|
-
}
|
|
4318
4689
|
const titleB = t.title ? buildingMentions(t.title.text) : [];
|
|
4319
4690
|
const b = titleB.length === 1 ? titleB[0] : ctxBySheet.get(s.key);
|
|
4320
4691
|
if (b) t.building = b;
|
|
@@ -6164,6 +6535,14 @@ function flattenArcRing(pts, throughAt, closed2 = true) {
|
|
|
6164
6535
|
}
|
|
6165
6536
|
|
|
6166
6537
|
// ../web/src/lib/shapeMetrics.js
|
|
6538
|
+
function linearVerticalFt(s, cond) {
|
|
6539
|
+
if (s?.origin?.derived) return { rise: 0, drop: 0 };
|
|
6540
|
+
const pick = (own, dflt) => {
|
|
6541
|
+
const v = own != null && own !== "" ? Number(own) : Number(dflt);
|
|
6542
|
+
return Number.isFinite(v) && v > 0 ? v : 0;
|
|
6543
|
+
};
|
|
6544
|
+
return { rise: pick(s?.rise_ft, cond?.rise_ft), drop: pick(s?.drop_ft, cond?.drop_ft) };
|
|
6545
|
+
}
|
|
6167
6546
|
function computeShapeMetrics(s, dims, upp, cond) {
|
|
6168
6547
|
const pts = (s.verts_norm || []).map(([nx, ny]) => [nx * dims.w, ny * dims.h]);
|
|
6169
6548
|
const u = upp || 0;
|
|
@@ -6174,9 +6553,16 @@ function computeShapeMetrics(s, dims, upp, cond) {
|
|
|
6174
6553
|
return { area_sf: +(LF * h).toFixed(2), perimeter_lf: +LF.toFixed(2) };
|
|
6175
6554
|
}
|
|
6176
6555
|
if (s.measure_role === "linear") {
|
|
6177
|
-
const
|
|
6556
|
+
const plan = openLen(s.curved ? flattenCurve(pts) : pts) * u;
|
|
6557
|
+
const { rise, drop } = linearVerticalFt(s, cond);
|
|
6558
|
+
const vert = rise + drop;
|
|
6559
|
+
const LF = plan + vert;
|
|
6178
6560
|
const tIn = Number(cond?.thickness_in) || 0;
|
|
6179
|
-
return {
|
|
6561
|
+
return {
|
|
6562
|
+
perimeter_lf: +LF.toFixed(2),
|
|
6563
|
+
area_sf: tIn > 0 ? +(LF * tIn / 12).toFixed(2) : 0,
|
|
6564
|
+
...vert > 0 ? { plan_lf: +plan.toFixed(2), vertical_lf: +vert.toFixed(2) } : {}
|
|
6565
|
+
};
|
|
6180
6566
|
}
|
|
6181
6567
|
const holesPx = (s.verts_norm_holes || []).map((ring) => ring.map(([nx, ny]) => [nx * dims.w, ny * dims.h]));
|
|
6182
6568
|
const met = holesPx.length ? polyWithHolesMetrics(pts, holesPx) : closedMetrics(pts);
|
|
@@ -6303,25 +6689,25 @@ function sharedRuns(ringA, ringB, opts) {
|
|
|
6303
6689
|
for (const r of raw) {
|
|
6304
6690
|
const idx = [];
|
|
6305
6691
|
for (let i = r.from; i <= r.to; i++) idx.push(i % samples.length);
|
|
6306
|
-
const
|
|
6692
|
+
const path6 = idx.map((i) => samples[i]);
|
|
6307
6693
|
if (idx.length < samples.length) {
|
|
6308
6694
|
const before = samples[((r.from - 1) % samples.length + samples.length) % samples.length];
|
|
6309
6695
|
const after = samples[(r.to + 1) % samples.length];
|
|
6310
|
-
const head2 = refineEnd(
|
|
6311
|
-
const tail = refineEnd(
|
|
6312
|
-
if (Math.hypot(head2[0] -
|
|
6313
|
-
if (Math.hypot(tail[0] -
|
|
6696
|
+
const head2 = refineEnd(path6[0], before);
|
|
6697
|
+
const tail = refineEnd(path6[path6.length - 1], after);
|
|
6698
|
+
if (Math.hypot(head2[0] - path6[0][0], head2[1] - path6[0][1]) > 1e-6) path6.unshift(head2);
|
|
6699
|
+
if (Math.hypot(tail[0] - path6[path6.length - 1][0], tail[1] - path6[path6.length - 1][1]) > 1e-6) path6.push(tail);
|
|
6314
6700
|
}
|
|
6315
6701
|
let length_px = 0;
|
|
6316
|
-
for (let i = 1; i <
|
|
6317
|
-
length_px += Math.hypot(
|
|
6702
|
+
for (let i = 1; i < path6.length; i++) {
|
|
6703
|
+
length_px += Math.hypot(path6[i][0] - path6[i - 1][0], path6[i][1] - path6[i - 1][1]);
|
|
6318
6704
|
}
|
|
6319
6705
|
if (length_px < min_len_px) continue;
|
|
6320
6706
|
const gap_px = median(idx.map((i) => dists[i]));
|
|
6321
|
-
const mid =
|
|
6707
|
+
const mid = path6[path6.length >> 1];
|
|
6322
6708
|
runs.push({
|
|
6323
6709
|
kind: gap_px <= touch_px ? "butt" : "wall",
|
|
6324
|
-
path:
|
|
6710
|
+
path: path6,
|
|
6325
6711
|
length_px,
|
|
6326
6712
|
gap_px,
|
|
6327
6713
|
at: [mid[0], mid[1]]
|
|
@@ -6332,18 +6718,18 @@ function sharedRuns(ringA, ringB, opts) {
|
|
|
6332
6718
|
var TRANSITION_DEFAULTS = { max_gap_in: 12, min_run_in: 12 };
|
|
6333
6719
|
var round22 = (n) => +n.toFixed(2);
|
|
6334
6720
|
var round12 = (n) => +n.toFixed(1);
|
|
6335
|
-
function dropCollinear(
|
|
6336
|
-
if (
|
|
6337
|
-
const out = [[
|
|
6338
|
-
for (let i = 1; i <
|
|
6339
|
-
const a = out[out.length - 1], b =
|
|
6721
|
+
function dropCollinear(path6, tol = 1e-6) {
|
|
6722
|
+
if (path6.length < 3) return path6.map((p) => [p[0], p[1]]);
|
|
6723
|
+
const out = [[path6[0][0], path6[0][1]]];
|
|
6724
|
+
for (let i = 1; i < path6.length - 1; i++) {
|
|
6725
|
+
const a = out[out.length - 1], b = path6[i], c = path6[i + 1];
|
|
6340
6726
|
const abx = b[0] - a[0], aby = b[1] - a[1];
|
|
6341
6727
|
const acx = c[0] - a[0], acy = c[1] - a[1];
|
|
6342
6728
|
const len = Math.hypot(acx, acy);
|
|
6343
6729
|
const off = len > 0 ? Math.abs(abx * acy - aby * acx) / len : Infinity;
|
|
6344
6730
|
if (off > tol) out.push([b[0], b[1]]);
|
|
6345
6731
|
}
|
|
6346
|
-
out.push([
|
|
6732
|
+
out.push([path6[path6.length - 1][0], path6[path6.length - 1][1]]);
|
|
6347
6733
|
return out;
|
|
6348
6734
|
}
|
|
6349
6735
|
function deriveTransitionRuns(a, b, sheets, opts = {}) {
|
|
@@ -6890,6 +7276,11 @@ function attrValue(attrs, colId) {
|
|
|
6890
7276
|
|
|
6891
7277
|
// ../web/src/lib/units.ts
|
|
6892
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";
|
|
6893
7284
|
function dimLabel(feet, units = "imperial") {
|
|
6894
7285
|
if (!Number.isFinite(feet)) return "";
|
|
6895
7286
|
if (units === "metric") return `${(feet * M_PER_FT).toFixed(2)} m`;
|
|
@@ -6903,6 +7294,49 @@ function dimLabel(feet, units = "imperial") {
|
|
|
6903
7294
|
return `${sign}${ft}'-${inch}"`;
|
|
6904
7295
|
}
|
|
6905
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
|
+
|
|
6906
7340
|
// ../web/src/lib/totals.js
|
|
6907
7341
|
function accumulateRole(acc, s) {
|
|
6908
7342
|
const cp = s.computed || {};
|
|
@@ -7058,6 +7492,7 @@ function grandTotals(rows) {
|
|
|
7058
7492
|
}
|
|
7059
7493
|
function reportJson({ projectName = "", rows = [], bySheet = [], scaleInfo = [], markups = [], rfis = [], sheetLabel = null, conditionColumns = [], attrsByCond = null, shapeLabels = [], byLabel = [], displayUnits = "imperial", rollGoods = [], proposedConditionEdits = [] }) {
|
|
7060
7494
|
const label = (id) => sheetLabel ? sheetLabel(id) : id;
|
|
7495
|
+
const noteQty = qtyLabels(rows || [], displayUnits === "metric" ? "metric" : "imperial");
|
|
7061
7496
|
const colDefs = (Array.isArray(conditionColumns) ? conditionColumns : []).filter((cc) => cc && typeof cc === "object" && typeof cc.id === "string");
|
|
7062
7497
|
const attrs = attrsByCond instanceof Map ? attrsByCond : /* @__PURE__ */ new Map();
|
|
7063
7498
|
return {
|
|
@@ -7094,9 +7529,14 @@ function reportJson({ projectName = "", rows = [], bySheet = [], scaleInfo = [],
|
|
|
7094
7529
|
// resolved finish_tag rather than only the id, so a reader of the export
|
|
7095
7530
|
// can see WHICH scope an annotation is about without joining two arrays;
|
|
7096
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.
|
|
7097
7537
|
markups: markups.map((m) => {
|
|
7098
7538
|
const c = m.condition_id ? (rows || []).find((r) => r.id === m.condition_id) : null;
|
|
7099
|
-
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 } : {} };
|
|
7100
7540
|
}),
|
|
7101
7541
|
// rfis APPENDS after markups (additive-only v1 — old exports had no RFI
|
|
7102
7542
|
// register). linked_markups/linked_sheets are DERIVED from markup.rfi_id,
|
|
@@ -8949,11 +9389,24 @@ var Session = class _Session {
|
|
|
8949
9389
|
const s = this.sheet(name);
|
|
8950
9390
|
if (s.upp == null) throw new UserError(this.scaleGate(s));
|
|
8951
9391
|
const { pts: run2, arcs } = this.bend(pts, opts.arc_through, false, "measure_line");
|
|
8952
|
-
const {
|
|
9392
|
+
const own = { ...opts.rise_ft !== void 0 ? { rise_ft: opts.rise_ft } : {}, ...opts.drop_ft !== void 0 ? { drop_ft: opts.drop_ft } : {} };
|
|
9393
|
+
const vertical = this.verticalFor(opts.condition, own);
|
|
9394
|
+
const computed = this.quantify(s, "linear", run2, void 0, vertical);
|
|
9395
|
+
const length_lf = computed.perimeter_lf ?? 0;
|
|
8953
9396
|
let shape_id;
|
|
8954
|
-
if (opts.condition)
|
|
9397
|
+
if (opts.condition) {
|
|
9398
|
+
const shape = this.commit(s, opts.condition, "linear", run2, computed, { method: "manual", actor: "agent", ...arcs ? { curved: true } : {} });
|
|
9399
|
+
Object.assign(shape, own);
|
|
9400
|
+
shape_id = shape.id;
|
|
9401
|
+
}
|
|
8955
9402
|
this.flushCommits("measure_line");
|
|
8956
|
-
return {
|
|
9403
|
+
return {
|
|
9404
|
+
length_lf,
|
|
9405
|
+
npts: run2.length,
|
|
9406
|
+
...computed.vertical_lf ? { plan_lf: computed.plan_lf, vertical_lf: computed.vertical_lf, rise_ft: vertical.rise, drop_ft: vertical.drop } : {},
|
|
9407
|
+
...arcs ? { arcs } : {},
|
|
9408
|
+
...shape_id ? { shape_id } : {}
|
|
9409
|
+
};
|
|
8957
9410
|
}
|
|
8958
9411
|
/** Surface Area — the canvas's Surface tool (commitSurface): an OPEN run
|
|
8959
9412
|
* traced along the wall in plan view, quantified as traced LF × height.
|
|
@@ -9004,14 +9457,31 @@ var Session = class _Session {
|
|
|
9004
9457
|
}
|
|
9005
9458
|
return p;
|
|
9006
9459
|
}
|
|
9460
|
+
/** A linear run's quantities from its plan length and vertical legs (#441)
|
|
9461
|
+
* — the server twin of shapeMetrics' linear branch: perimeter_lf is the
|
|
9462
|
+
* TOTAL the summers read; plan_lf / vertical_lf appear only when a leg
|
|
9463
|
+
* exists, so a flat run's record is what it always was. area_sf stays 0 —
|
|
9464
|
+
* the canvas mints border SF only from a condition thickness. */
|
|
9465
|
+
static linearQty(planLf, vertical) {
|
|
9466
|
+
const vert = (vertical?.rise ?? 0) + (vertical?.drop ?? 0);
|
|
9467
|
+
const plan = round2(planLf);
|
|
9468
|
+
return { area_sf: 0, perimeter_lf: round2(planLf + vert), ...vert > 0 ? { plan_lf: plan, vertical_lf: round2(vert) } : {} };
|
|
9469
|
+
}
|
|
9470
|
+
/** The legs a linear run resolves to: its own rise_ft / drop_ft where
|
|
9471
|
+
* given, else the condition's defaults (by tag — the condition may not
|
|
9472
|
+
* exist yet on a first commit, in which case there are no defaults). */
|
|
9473
|
+
verticalFor(tag, own) {
|
|
9474
|
+
const cond = tag ? this.conditions.find((x) => _Session.tagKey(x.finish_tag) === _Session.tagKey(tag)) : void 0;
|
|
9475
|
+
return linearVerticalFt(own, cond);
|
|
9476
|
+
}
|
|
9007
9477
|
/** The quantities a shape of `role` carries for `verts` on sheet `s` — the
|
|
9008
9478
|
* same arithmetic measure_polygon / measure_line / measure_surface /
|
|
9009
9479
|
* place_count run, in one place so a revised batch measures exactly as a
|
|
9010
9480
|
* fresh commit would. */
|
|
9011
|
-
quantify(s, role, verts, heightFt) {
|
|
9481
|
+
quantify(s, role, verts, heightFt, vertical) {
|
|
9012
9482
|
if (role === "count") return { count: 1 };
|
|
9013
9483
|
const upp = s.upp ?? 0;
|
|
9014
|
-
if (role === "linear") return
|
|
9484
|
+
if (role === "linear") return _Session.linearQty(openLen(verts) * upp, vertical);
|
|
9015
9485
|
if (role === "surface_area") {
|
|
9016
9486
|
const LF = openLen(verts) * upp;
|
|
9017
9487
|
return { area_sf: round2(LF * (heightFt ?? 0)), perimeter_lf: round2(LF) };
|
|
@@ -9145,6 +9615,8 @@ var Session = class _Session {
|
|
|
9145
9615
|
if (proposed.waste_pct !== void 0 && proposed.waste_pct !== c.waste_pct) diff.waste_pct = proposed.waste_pct;
|
|
9146
9616
|
if (proposed.multiplier !== void 0 && proposed.multiplier !== c.multiplier) diff.multiplier = proposed.multiplier;
|
|
9147
9617
|
if (proposed.height_ft !== void 0 && proposed.height_ft !== c.height_ft) diff.height_ft = proposed.height_ft;
|
|
9618
|
+
if (proposed.rise_ft !== void 0 && proposed.rise_ft !== (c.rise_ft ?? 0)) diff.rise_ft = proposed.rise_ft;
|
|
9619
|
+
if (proposed.drop_ft !== void 0 && proposed.drop_ft !== (c.drop_ft ?? 0)) diff.drop_ft = proposed.drop_ft;
|
|
9148
9620
|
if (proposed.roll_setup !== void 0) {
|
|
9149
9621
|
if (proposed.roll_setup === null) {
|
|
9150
9622
|
if (c.roll_setup) diff.roll_setup = null;
|
|
@@ -9199,6 +9671,7 @@ var Session = class _Session {
|
|
|
9199
9671
|
const before = structuredClone(c);
|
|
9200
9672
|
const { finish_tag, ...knobs } = proposal.proposed;
|
|
9201
9673
|
this.applyConditionKnobs(c, knobs);
|
|
9674
|
+
if (knobs.rise_ft !== void 0 || knobs.drop_ft !== void 0) this.reflowLinears(c);
|
|
9202
9675
|
if (finish_tag !== void 0) c.finish_tag = finish_tag;
|
|
9203
9676
|
this.conditionEditProposals.splice(i, 1);
|
|
9204
9677
|
this.record({ op: "condition_proposal_accept", tool: "accept_condition_edit", proposal, index: i, before });
|
|
@@ -9210,9 +9683,30 @@ var Session = class _Session {
|
|
|
9210
9683
|
waste_pct: c.waste_pct,
|
|
9211
9684
|
multiplier: c.multiplier,
|
|
9212
9685
|
...c.height_ft !== void 0 ? { height_ft: c.height_ft } : {},
|
|
9686
|
+
...c.rise_ft !== void 0 ? { rise_ft: c.rise_ft } : {},
|
|
9687
|
+
...c.drop_ft !== void 0 ? { drop_ft: c.drop_ft } : {},
|
|
9213
9688
|
...c.roll_setup ? { roll_setup: c.roll_setup } : {}
|
|
9214
9689
|
};
|
|
9215
9690
|
}
|
|
9691
|
+
/** Re-price every linear run of `c` from its geometry and the legs it now
|
|
9692
|
+
* resolves to (#441) — the canvas's setCondParam re-flow, on the server. A
|
|
9693
|
+
* run carrying its own rise_ft / drop_ft keeps that field; a derived run
|
|
9694
|
+
* never takes a leg. Nothing is journaled here: the condition step that
|
|
9695
|
+
* called it is the undo unit, and undo calls it again with the old values. */
|
|
9696
|
+
reflowLinears(c) {
|
|
9697
|
+
for (const sh of this.shapes) {
|
|
9698
|
+
if (sh.condition_id !== c.id || sh.measure_role !== "linear") continue;
|
|
9699
|
+
const s = this.sheets.get(sh.sheet_id);
|
|
9700
|
+
if (!s || s.upp == null) continue;
|
|
9701
|
+
const px = sh.verts_norm.map(([nx, ny]) => [nx * s.widthPx, ny * s.heightPx]);
|
|
9702
|
+
const vertical = sh.origin?.derived ? { rise: 0, drop: 0 } : linearVerticalFt(sh, c);
|
|
9703
|
+
sh.computed = { ...sh.computed, ..._Session.linearQty(openLen(px) * (s.upp ?? 0), vertical) };
|
|
9704
|
+
if (!vertical.rise && !vertical.drop) {
|
|
9705
|
+
delete sh.computed.plan_lf;
|
|
9706
|
+
delete sh.computed.vertical_lf;
|
|
9707
|
+
}
|
|
9708
|
+
}
|
|
9709
|
+
}
|
|
9216
9710
|
/** The pending condition diffs as the report and the summary print them:
|
|
9217
9711
|
* current values beside proposed, per condition. */
|
|
9218
9712
|
proposedConditionEdits() {
|
|
@@ -9587,22 +10081,26 @@ var Session = class _Session {
|
|
|
9587
10081
|
};
|
|
9588
10082
|
const wasLf = parent.computed?.perimeter_lf ?? 0;
|
|
9589
10083
|
const wasSf = parent.computed?.area_sf ?? 0;
|
|
9590
|
-
const
|
|
9591
|
-
|
|
9592
|
-
|
|
9593
|
-
|
|
10084
|
+
const parentCond = this.conditions.find((x) => x.id === parent.condition_id);
|
|
10085
|
+
const parentLegs = parent.origin?.derived ? { rise: 0, drop: 0 } : linearVerticalFt(parent, parentCond);
|
|
10086
|
+
const wasPlan = parent.computed?.plan_lf ?? wasLf;
|
|
10087
|
+
const qty = (lenPx, legs) => {
|
|
10088
|
+
const plan = round2(lenPx * upp);
|
|
10089
|
+
const k = wasPlan > 0 ? plan / wasPlan : 0;
|
|
10090
|
+
return { ..._Session.linearQty(plan, legs), area_sf: round2(wasSf * k) };
|
|
9594
10091
|
};
|
|
9595
10092
|
const toNorm = (run2) => run2.map(([x, y]) => [x / s.widthPx, y / s.heightPx]);
|
|
9596
10093
|
const survivors = r.runs ?? [];
|
|
9597
10094
|
const [head2, ...rest] = survivors;
|
|
9598
10095
|
if (!head2) throw new UserError(`That ring covers the whole of ${parent.id}. Removing a run outright is delete_shape, not a cut.`);
|
|
9599
10096
|
parent.verts_norm = toNorm(head2);
|
|
9600
|
-
parent.computed = qty(openLen(head2));
|
|
10097
|
+
parent.computed = qty(openLen(head2), parentLegs);
|
|
9601
10098
|
const minted = rest.map((piece) => ({
|
|
9602
10099
|
...structuredClone(parent),
|
|
9603
10100
|
id: uid("shp"),
|
|
9604
10101
|
verts_norm: toNorm(piece),
|
|
9605
|
-
|
|
10102
|
+
...parentLegs.rise || parentLegs.drop ? { rise_ft: 0, drop_ft: 0 } : {},
|
|
10103
|
+
computed: qty(openLen(piece), { rise: 0, drop: 0 })
|
|
9606
10104
|
}));
|
|
9607
10105
|
this.shapes.push(...minted);
|
|
9608
10106
|
this.record({ op: "runcut", tool: "cut_out", target_id: parent.id, target_prev, minted_ids: minted.map((m) => m.id) });
|
|
@@ -9613,7 +10111,7 @@ var Session = class _Session {
|
|
|
9613
10111
|
pieces,
|
|
9614
10112
|
removed_lf: round2(Math.max(0, wasLf - pieces.reduce((n, p) => n + p.lf, 0))),
|
|
9615
10113
|
removed_sf: round2(Math.max(0, wasSf - pieces.reduce((n, p) => n + p.sf, 0))),
|
|
9616
|
-
note: minted.length ? `The cut fell inside the run, so it comes back in ${pieces.length} pieces \u2014 same condition, same height, each measured on its own. One undo_last puts the run back whole.` : "The run keeps its id and its condition; only its length (and the SF that rides on it) changed. One undo_last puts it back whole."
|
|
10114
|
+
note: minted.length ? `The cut fell inside the run, so it comes back in ${pieces.length} pieces \u2014 same condition, same height, each measured on its own${parentLegs.rise || parentLegs.drop ? `; the run's rise/drop stay on ${parent.id}, the new piece(s) carry none (rise_ft/drop_ft 0 \u2014 edit_shape to move a leg)` : ""}. One undo_last puts the run back whole.` : "The run keeps its id and its condition; only its length (and the SF that rides on it) changed. One undo_last puts it back whole."
|
|
9617
10115
|
};
|
|
9618
10116
|
}
|
|
9619
10117
|
/** The canvas's cutoutParentPrevSans, ported verbatim in spirit (#206 —
|
|
@@ -10708,8 +11206,15 @@ var Session = class _Session {
|
|
|
10708
11206
|
if (cur.origin?.reviewed === true) {
|
|
10709
11207
|
throw new UserError(`Shape ${JSON.stringify(id)} was affirmed by a human \u2014 reviewed work is ink, not pencil, and cannot be edited by an agent.`);
|
|
10710
11208
|
}
|
|
10711
|
-
if (patch.verts === void 0 && patch.condition === void 0 && patch.role === void 0 && patch.label === void 0) {
|
|
10712
|
-
throw new UserError("Nothing to change \u2014 pass at least one of verts, condition, role, label.");
|
|
11209
|
+
if (patch.verts === void 0 && patch.condition === void 0 && patch.role === void 0 && patch.label === void 0 && patch.rise_ft === void 0 && patch.drop_ft === void 0) {
|
|
11210
|
+
throw new UserError("Nothing to change \u2014 pass at least one of verts, condition, role, label, rise_ft, drop_ft.");
|
|
11211
|
+
}
|
|
11212
|
+
const roleAfter = patch.role ?? cur.measure_role;
|
|
11213
|
+
if ((patch.rise_ft !== void 0 || patch.drop_ft !== void 0) && roleAfter !== "linear") {
|
|
11214
|
+
throw new UserError(`rise_ft / drop_ft are a linear run's vertical legs \u2014 ${JSON.stringify(id)} ${patch.role !== void 0 ? `would be ${roleAfter}` : `is ${roleAfter}`}. A wall's vertical is its height_ft.`);
|
|
11215
|
+
}
|
|
11216
|
+
if (cur.origin?.derived && (patch.rise_ft !== void 0 || patch.drop_ft !== void 0)) {
|
|
11217
|
+
throw new UserError(`Shape ${JSON.stringify(id)} is a derived run (base or transition) \u2014 a floor-level line by construction; it never takes a rise or drop. Trace the vertical run with measure_line.`);
|
|
10713
11218
|
}
|
|
10714
11219
|
if (cur.cuts_shape_id && (patch.verts !== void 0 || patch.role !== void 0 || patch.condition !== void 0)) {
|
|
10715
11220
|
throw new UserError(`Shape ${JSON.stringify(id)} is a reconciled cutout \u2014 its ring IS the hole in ${cur.cuts_shape_id}. delete_shape it (the parent's geometry is restored) and cut_out again where you mean it.`);
|
|
@@ -10733,7 +11238,17 @@ var Session = class _Session {
|
|
|
10733
11238
|
if (!(h > 0)) throw new UserError(`Surface Area needs a height \u2014 set height_ft on ${cond?.finish_tag ?? "the condition"} with edit_condition first.`);
|
|
10734
11239
|
return h;
|
|
10735
11240
|
};
|
|
10736
|
-
const
|
|
11241
|
+
const legs = role === "linear" ? {
|
|
11242
|
+
...cur.rise_ft !== void 0 ? { rise_ft: cur.rise_ft } : {},
|
|
11243
|
+
...cur.drop_ft !== void 0 ? { drop_ft: cur.drop_ft } : {}
|
|
11244
|
+
} : {};
|
|
11245
|
+
for (const k of ["rise_ft", "drop_ft"]) {
|
|
11246
|
+
const v = patch[k];
|
|
11247
|
+
if (v === null) delete legs[k];
|
|
11248
|
+
else if (v !== void 0) legs[k] = v;
|
|
11249
|
+
}
|
|
11250
|
+
const condTagAfter = patch.condition !== void 0 ? patch.condition : this.conditions.find((x) => x.id === cur.condition_id)?.finish_tag;
|
|
11251
|
+
const computed = role === "count" ? { count: cur.computed.count ?? 1 } : role === "linear" ? _Session.linearQty(openLen(vertsPx) * upp, cur.origin?.derived ? { rise: 0, drop: 0 } : this.verticalFor(condTagAfter, legs)) : role === "surface_area" ? (() => {
|
|
10737
11252
|
const LF = openLen(vertsPx) * upp;
|
|
10738
11253
|
return { area_sf: round2(LF * heightFor()), perimeter_lf: round2(LF) };
|
|
10739
11254
|
})() : (() => {
|
|
@@ -10751,6 +11266,7 @@ var Session = class _Session {
|
|
|
10751
11266
|
computed,
|
|
10752
11267
|
...nextLabel ? { label: nextLabel } : {},
|
|
10753
11268
|
...role === "surface_area" ? { height_ft: Number(cur.height_ft) || heightFor() } : {},
|
|
11269
|
+
...legs,
|
|
10754
11270
|
...cur.origin ? { origin: {
|
|
10755
11271
|
...cur.origin,
|
|
10756
11272
|
agent_edits: (cur.origin.agent_edits ?? 0) + 1,
|
|
@@ -10760,12 +11276,16 @@ var Session = class _Session {
|
|
|
10760
11276
|
} } : {}
|
|
10761
11277
|
};
|
|
10762
11278
|
if (!nextLabel) delete this.shapes[i].label;
|
|
11279
|
+
if (legs.rise_ft === void 0) delete this.shapes[i].rise_ft;
|
|
11280
|
+
if (legs.drop_ft === void 0) delete this.shapes[i].drop_ft;
|
|
10763
11281
|
this.record({ op: "edit", tool: "edit_shape", before });
|
|
10764
11282
|
const changed = [
|
|
10765
11283
|
...patch.verts !== void 0 ? ["verts"] : [],
|
|
10766
11284
|
...patch.condition !== void 0 ? ["condition"] : [],
|
|
10767
11285
|
...patch.role !== void 0 ? ["role"] : [],
|
|
10768
|
-
...patch.label !== void 0 ? ["label"] : []
|
|
11286
|
+
...patch.label !== void 0 ? ["label"] : [],
|
|
11287
|
+
...patch.rise_ft !== void 0 ? ["rise_ft"] : [],
|
|
11288
|
+
...patch.drop_ft !== void 0 ? ["drop_ft"] : []
|
|
10769
11289
|
];
|
|
10770
11290
|
return {
|
|
10771
11291
|
shape_id: id,
|
|
@@ -10919,6 +11439,8 @@ var Session = class _Session {
|
|
|
10919
11439
|
if (opts.waste_pct !== void 0) c.waste_pct = opts.waste_pct;
|
|
10920
11440
|
if (opts.multiplier !== void 0) c.multiplier = opts.multiplier;
|
|
10921
11441
|
if (opts.height_ft !== void 0) c.height_ft = opts.height_ft;
|
|
11442
|
+
if (opts.rise_ft !== void 0) c.rise_ft = opts.rise_ft;
|
|
11443
|
+
if (opts.drop_ft !== void 0) c.drop_ft = opts.drop_ft;
|
|
10922
11444
|
if (opts.roll_setup !== void 0) {
|
|
10923
11445
|
if (opts.roll_setup === null) {
|
|
10924
11446
|
delete c.roll_setup;
|
|
@@ -10932,8 +11454,8 @@ var Session = class _Session {
|
|
|
10932
11454
|
}
|
|
10933
11455
|
}
|
|
10934
11456
|
editCondition(tag, opts) {
|
|
10935
|
-
if (opts.waste_pct === void 0 && opts.multiplier === void 0 && opts.height_ft === void 0 && opts.roll_setup === void 0) {
|
|
10936
|
-
throw new UserError("Nothing to change \u2014 pass at least one of waste_pct, multiplier, height_ft, roll_setup.");
|
|
11457
|
+
if (opts.waste_pct === void 0 && opts.multiplier === void 0 && opts.height_ft === void 0 && opts.rise_ft === void 0 && opts.drop_ft === void 0 && opts.roll_setup === void 0) {
|
|
11458
|
+
throw new UserError("Nothing to change \u2014 pass at least one of waste_pct, multiplier, height_ft, rise_ft, drop_ft, roll_setup.");
|
|
10937
11459
|
}
|
|
10938
11460
|
const c = this.conditions.find((x) => x.finish_tag === tag);
|
|
10939
11461
|
if (!c) {
|
|
@@ -10944,10 +11466,13 @@ var Session = class _Session {
|
|
|
10944
11466
|
waste_pct: c.waste_pct,
|
|
10945
11467
|
multiplier: c.multiplier,
|
|
10946
11468
|
height_ft: c.height_ft,
|
|
11469
|
+
rise_ft: c.rise_ft,
|
|
11470
|
+
drop_ft: c.drop_ft,
|
|
10947
11471
|
roll_setup: c.roll_setup ? structuredClone(c.roll_setup) : void 0
|
|
10948
11472
|
};
|
|
10949
11473
|
this.applyConditionKnobs(c, opts);
|
|
10950
11474
|
this.record({ op: "condition", tool: "edit_condition", condition_id: c.id, before });
|
|
11475
|
+
if (opts.rise_ft !== void 0 || opts.drop_ft !== void 0) this.reflowLinears(c);
|
|
10951
11476
|
let roll;
|
|
10952
11477
|
if (hasRollSetup(c)) {
|
|
10953
11478
|
const { dimsFor, uppFor } = this.rollInputs();
|
|
@@ -10961,6 +11486,8 @@ var Session = class _Session {
|
|
|
10961
11486
|
waste_pct: c.waste_pct,
|
|
10962
11487
|
multiplier: c.multiplier,
|
|
10963
11488
|
...c.height_ft !== void 0 ? { height_ft: c.height_ft } : {},
|
|
11489
|
+
...c.rise_ft !== void 0 ? { rise_ft: c.rise_ft } : {},
|
|
11490
|
+
...c.drop_ft !== void 0 ? { drop_ft: c.drop_ft } : {},
|
|
10964
11491
|
...c.roll_setup ? { roll_setup: c.roll_setup } : {},
|
|
10965
11492
|
...roll ? { roll } : {}
|
|
10966
11493
|
};
|
|
@@ -11125,8 +11652,13 @@ var Session = class _Session {
|
|
|
11125
11652
|
c.multiplier = e.before.multiplier;
|
|
11126
11653
|
if (e.before.height_ft === void 0) delete c.height_ft;
|
|
11127
11654
|
else c.height_ft = e.before.height_ft;
|
|
11655
|
+
if (e.before.rise_ft === void 0) delete c.rise_ft;
|
|
11656
|
+
else c.rise_ft = e.before.rise_ft;
|
|
11657
|
+
if (e.before.drop_ft === void 0) delete c.drop_ft;
|
|
11658
|
+
else c.drop_ft = e.before.drop_ft;
|
|
11128
11659
|
if (e.before.roll_setup === void 0) delete c.roll_setup;
|
|
11129
11660
|
else c.roll_setup = e.before.roll_setup;
|
|
11661
|
+
this.reflowLinears(c);
|
|
11130
11662
|
}
|
|
11131
11663
|
undone.push({ seq: e.seq, op: e.op, tool: e.tool, shapes: 0 });
|
|
11132
11664
|
} else if (e.op === "duplicate_condition") {
|
|
@@ -11317,6 +11849,12 @@ var Session = class _Session {
|
|
|
11317
11849
|
seals = seals.filter((a) => a.shape_id !== void 0 && shapeById.get(a.shape_id)?.condition_id === c.id);
|
|
11318
11850
|
}
|
|
11319
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
|
+
};
|
|
11320
11858
|
const px = (m, p) => {
|
|
11321
11859
|
const sh = s0.get(m.sheet_id);
|
|
11322
11860
|
if (!p || !sh) return void 0;
|
|
@@ -11336,7 +11874,8 @@ var Session = class _Session {
|
|
|
11336
11874
|
...m.from ? { from: px(m, m.from) } : {},
|
|
11337
11875
|
...m.to ? { to: px(m, m.to) } : {},
|
|
11338
11876
|
...m.r != null ? { r: round1(m.r * (s0.get(m.sheet_id)?.widthPx ?? 0)) } : {},
|
|
11339
|
-
...m.len_ft != null ? { length_lf: m.len_ft } : {}
|
|
11877
|
+
...m.len_ft != null ? { length_lf: m.len_ft } : {},
|
|
11878
|
+
...fields(m)
|
|
11340
11879
|
})),
|
|
11341
11880
|
count: rows.length,
|
|
11342
11881
|
unattached: rows.filter((m) => !m.condition_id).length,
|
|
@@ -12163,8 +12702,12 @@ var symbolSweepOutput = {
|
|
|
12163
12702
|
warning: z.string().optional().describe("Present when the work cap dropped candidates \u2014 what a tighter seed rect would recover")
|
|
12164
12703
|
};
|
|
12165
12704
|
var measureLineOutput = {
|
|
12166
|
-
length_lf: z.number(),
|
|
12705
|
+
length_lf: z.number().describe("The run's TOTAL length: plan trace + rise + drop (#441)"),
|
|
12167
12706
|
npts: z.number().int(),
|
|
12707
|
+
plan_lf: z.number().optional().describe("The flat X\u2013Y trace alone \u2014 present only when the run carries a vertical leg"),
|
|
12708
|
+
vertical_lf: z.number().optional().describe("rise_ft + drop_ft \u2014 present only when a leg exists"),
|
|
12709
|
+
rise_ft: z.number().optional().describe("The rise this run resolved to (its own, else the condition default) \u2014 present with vertical_lf"),
|
|
12710
|
+
drop_ft: z.number().optional().describe("The drop this run resolved to \u2014 present with vertical_lf"),
|
|
12168
12711
|
arcs: z.number().int().optional().describe("How many arc_through bows were laid \u2014 present only when the trace was bent; the vertices reported are the baked arc, not the three points you gave"),
|
|
12169
12712
|
shape_id: z.string().optional().describe("Present when condition was passed and the shape committed")
|
|
12170
12713
|
};
|
|
@@ -12233,6 +12776,8 @@ var conditionKnobs = z.object({
|
|
|
12233
12776
|
waste_pct: z.number().optional(),
|
|
12234
12777
|
multiplier: z.number().optional(),
|
|
12235
12778
|
height_ft: z.number().optional(),
|
|
12779
|
+
rise_ft: z.number().optional(),
|
|
12780
|
+
drop_ft: z.number().optional(),
|
|
12236
12781
|
roll_setup: z.object({}).passthrough().nullable().optional()
|
|
12237
12782
|
}).passthrough();
|
|
12238
12783
|
var proposalRow = z.object({
|
|
@@ -12473,11 +13018,13 @@ var deleteShapeOutput = {
|
|
|
12473
13018
|
};
|
|
12474
13019
|
var editShapeOutput = {
|
|
12475
13020
|
shape_id: z.string(),
|
|
12476
|
-
changed: z.array(z.enum(["verts", "condition", "role", "label"])).describe("Which fields this call actually changed"),
|
|
13021
|
+
changed: z.array(z.enum(["verts", "condition", "role", "label", "rise_ft", "drop_ft"])).describe("Which fields this call actually changed"),
|
|
12477
13022
|
measure_role: z.enum(["floor_area", "deduct", "linear", "surface_area", "count"]),
|
|
12478
13023
|
nverts: z.number().int(),
|
|
12479
13024
|
area_sf: z.number().optional().describe("0 for linear shapes; LF \xD7 height for surface_area; absent for count"),
|
|
12480
|
-
perimeter_lf: z.number().optional().describe("Length for linear/surface runs, perimeter for closed ones; absent for count"),
|
|
13025
|
+
perimeter_lf: z.number().optional().describe("Length for linear/surface runs (a linear run's TOTAL incl. rise + drop), perimeter for closed ones; absent for count"),
|
|
13026
|
+
plan_lf: z.number().optional().describe("Linear runs with a vertical leg: the flat trace alone (#441)"),
|
|
13027
|
+
vertical_lf: z.number().optional().describe("Linear runs with a vertical leg: rise + drop (#441)"),
|
|
12481
13028
|
count: z.number().optional().describe("count shapes only \u2014 the marker's EA (preserved across the edit)"),
|
|
12482
13029
|
label: z.string().optional().describe("The shape's room/phase label after this call \u2014 absent when it carries none (a cleared label reports as absent, not as an empty string)"),
|
|
12483
13030
|
agent_edits: z.number().int().describe("How many times the agent has revised this shape \u2014 separate from the human-correction tally")
|
|
@@ -12620,6 +13167,8 @@ var editConditionOutput = {
|
|
|
12620
13167
|
waste_pct: z.number().describe("The condition's waste % after this write"),
|
|
12621
13168
|
multiplier: z.number().describe("The condition's quantity multiplier after this write"),
|
|
12622
13169
|
height_ft: z.number().optional().describe("The condition's wall height after this write \u2014 present once set (measure_surface multiplies traced LF by it)"),
|
|
13170
|
+
rise_ft: z.number().optional().describe("The condition's default rise for its linear runs after this write \u2014 present once set (#441)"),
|
|
13171
|
+
drop_ft: z.number().optional().describe("The condition's default drop for its linear runs after this write \u2014 present once set (#441)"),
|
|
12623
13172
|
roll_setup: z.object({}).passthrough().optional().describe("The condition's roll-goods setup after this write \u2014 present while opted in"),
|
|
12624
13173
|
roll: z.object({
|
|
12625
13174
|
condition_id: z.string(),
|
|
@@ -12843,7 +13392,9 @@ var annotationRow = z.object({
|
|
|
12843
13392
|
from: z.tuple([z.number(), z.number()]).optional().describe("Arrow tail / dimension start (image px)"),
|
|
12844
13393
|
to: z.tuple([z.number(), z.number()]).optional().describe("Arrow head / dimension end (image px)"),
|
|
12845
13394
|
r: z.number().optional().describe("Bubble radius (image px)"),
|
|
12846
|
-
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")
|
|
12847
13398
|
});
|
|
12848
13399
|
var editAnnotationOutput = { id: z.string(), text: z.string(), note: z.string() };
|
|
12849
13400
|
var annotateOutput = {
|
|
@@ -12964,9 +13515,132 @@ var countMarksOutput = {
|
|
|
12964
13515
|
};
|
|
12965
13516
|
|
|
12966
13517
|
// src/marked.ts
|
|
12967
|
-
import
|
|
13518
|
+
import path4 from "node:path";
|
|
12968
13519
|
import { readFile as readFile2, writeFile } from "node:fs/promises";
|
|
12969
13520
|
|
|
13521
|
+
// ../web/src/lib/annotationTools.js
|
|
13522
|
+
var ANNOTATION_DEFAULTS = { color: "#dc3d43", stroke_pt: 1.5, font_pt: 10, opacity: 0.32, head: "filled", both: false, mode: "freehand", width_pt: 10, line_style: "solid" };
|
|
13523
|
+
var ANNOTATION_PRESETS = [
|
|
13524
|
+
{ id: "review", name: "Review", tool: "cloud", style: { ...ANNOTATION_DEFAULTS } },
|
|
13525
|
+
{ id: "direction", name: "Direction", tool: "arrow", style: { ...ANNOTATION_DEFAULTS, color: "#2563eb", stroke_pt: 2 } },
|
|
13526
|
+
{ id: "key", name: "Key information", tool: "highlighter", style: { ...ANNOTATION_DEFAULTS, color: "#ffd60a", mode: "text" } }
|
|
13527
|
+
];
|
|
13528
|
+
var clamp2 = (n, a, b, fallback) => Number.isFinite(+n) ? Math.max(a, Math.min(b, +n)) : fallback;
|
|
13529
|
+
function annotationStyle(s = {}) {
|
|
13530
|
+
s = s && typeof s === "object" ? s : {};
|
|
13531
|
+
return {
|
|
13532
|
+
color: /^#[0-9a-f]{6}$/i.test(s.color) ? s.color : ANNOTATION_DEFAULTS.color,
|
|
13533
|
+
stroke_pt: clamp2(s.stroke_pt, 0.5, 6, 1.5),
|
|
13534
|
+
font_pt: clamp2(s.font_pt, 6, 24, 10),
|
|
13535
|
+
opacity: clamp2(s.opacity, 0.1, 0.75, 0.32),
|
|
13536
|
+
width_pt: clamp2(s.width_pt, 2, 40, 10),
|
|
13537
|
+
head: ["filled", "open", "none"].includes(s.head) ? s.head : "filled",
|
|
13538
|
+
both: !!s.both,
|
|
13539
|
+
mode: ["freehand", "straight", "text"].includes(s.mode) ? s.mode : "freehand",
|
|
13540
|
+
line_style: ["solid", "dashed", "dotted"].includes(s.line_style) ? s.line_style : "solid"
|
|
13541
|
+
};
|
|
13542
|
+
}
|
|
13543
|
+
var box = (a, b) => [[Math.min(a[0], b[0]), Math.min(a[1], b[1])], [Math.max(a[0], b[0]), Math.max(a[1], b[1])]];
|
|
13544
|
+
function noteLines(text, limit = 36) {
|
|
13545
|
+
const lines = [];
|
|
13546
|
+
for (const para of String(text || "").split("\n")) {
|
|
13547
|
+
let line = "";
|
|
13548
|
+
for (const word of para.split(/\s+/).filter(Boolean)) {
|
|
13549
|
+
if (line && line.length + word.length + 1 > limit) {
|
|
13550
|
+
lines.push(line);
|
|
13551
|
+
line = "";
|
|
13552
|
+
}
|
|
13553
|
+
let rest = word;
|
|
13554
|
+
while (rest.length > limit) {
|
|
13555
|
+
if (line) {
|
|
13556
|
+
lines.push(line);
|
|
13557
|
+
line = "";
|
|
13558
|
+
}
|
|
13559
|
+
lines.push(rest.slice(0, limit));
|
|
13560
|
+
rest = rest.slice(limit);
|
|
13561
|
+
}
|
|
13562
|
+
line += (line ? " " : "") + rest;
|
|
13563
|
+
}
|
|
13564
|
+
lines.push(line);
|
|
13565
|
+
}
|
|
13566
|
+
return lines;
|
|
13567
|
+
}
|
|
13568
|
+
var path3 = (pts) => pts.map((p, i) => [i ? "L" : "M", ...p]);
|
|
13569
|
+
function pathString(commands, transform = (p) => p) {
|
|
13570
|
+
return commands.map(([verb, ...v]) => {
|
|
13571
|
+
const ps = [];
|
|
13572
|
+
for (let i = 0; i < v.length; i += 2) ps.push(transform([v[i], v[i + 1]]).join(","));
|
|
13573
|
+
return verb + ps.join(" ");
|
|
13574
|
+
}).join(" ");
|
|
13575
|
+
}
|
|
13576
|
+
function annotationScene(m, W, H, pixelScale = 2) {
|
|
13577
|
+
const s = annotationStyle({ ...m.annotation_style, color: m.color || m.annotation_style?.color });
|
|
13578
|
+
const P = (p) => [p[0] * W, p[1] * H];
|
|
13579
|
+
const paths = [], texts = [];
|
|
13580
|
+
const stroke = s.stroke_pt * pixelScale;
|
|
13581
|
+
const add = (commands, extra = {}) => paths.push({ commands, stroke: s.color, width: stroke, opacity: 1, ...extra });
|
|
13582
|
+
const head2 = (a, b) => {
|
|
13583
|
+
if (s.head === "none") return;
|
|
13584
|
+
const size = Math.max(6, s.stroke_pt * 4) * pixelScale, len = Math.hypot(b[0] - a[0], b[1] - a[1]);
|
|
13585
|
+
if (!len) return;
|
|
13586
|
+
const u = [(b[0] - a[0]) / len, (b[1] - a[1]) / len], base = [b[0] - u[0] * size, b[1] - u[1] * size];
|
|
13587
|
+
const pts = [[base[0] - u[1] * size * 0.42, base[1] + u[0] * size * 0.42], b, [base[0] + u[1] * size * 0.42, base[1] - u[0] * size * 0.42]];
|
|
13588
|
+
add([...path3(pts), ...s.head === "filled" ? [["Z"]] : []], { fill: s.head === "filled" ? s.color : void 0, dash: void 0 });
|
|
13589
|
+
};
|
|
13590
|
+
const leader = (a, b) => {
|
|
13591
|
+
add(path3([a, b]));
|
|
13592
|
+
head2(a, b);
|
|
13593
|
+
if (s.both) head2(b, a);
|
|
13594
|
+
};
|
|
13595
|
+
const noteOrigin = (at) => {
|
|
13596
|
+
const lines = noteLines(m.text), font = s.font_pt * pixelScale, pad = 5 * pixelScale;
|
|
13597
|
+
const w = Math.max(45 * pixelScale, ...lines.map((l) => l.length * font * 0.62)) + pad * 2;
|
|
13598
|
+
const h = Math.max(1, lines.length) * font * 1.35 + pad * 2;
|
|
13599
|
+
return [Math.max(0, Math.min(W - w, at[0])), Math.max(0, Math.min(H - h, at[1]))];
|
|
13600
|
+
};
|
|
13601
|
+
const note = (raw) => {
|
|
13602
|
+
const at = noteOrigin(raw);
|
|
13603
|
+
const lines = noteLines(m.text), font = s.font_pt * pixelScale, pad = 5 * pixelScale;
|
|
13604
|
+
const width = Math.max(45 * pixelScale, ...lines.map((l) => l.length * font * 0.62)) + pad * 2;
|
|
13605
|
+
const height = Math.max(1, lines.length) * font * 1.35 + pad * 2;
|
|
13606
|
+
add([...path3([at, [at[0] + width, at[1]], [at[0] + width, at[1] + height], [at[0], at[1] + height]]), ["Z"]], { fill: "#ffffff" });
|
|
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" }));
|
|
13608
|
+
return [[at[0], at[1]], [at[0] + width, at[1] + height]];
|
|
13609
|
+
};
|
|
13610
|
+
let noteBox2 = null;
|
|
13611
|
+
if (m.type === "arrow" && m.from && m.to) leader(P(m.from), P(m.to));
|
|
13612
|
+
if (m.type === "callout" && m.at && m.target) {
|
|
13613
|
+
leader(noteOrigin(P(m.at)), P(m.target));
|
|
13614
|
+
noteBox2 = note(P(m.at));
|
|
13615
|
+
}
|
|
13616
|
+
if (m.type === "text" && m.at) noteBox2 = note(P(m.at));
|
|
13617
|
+
if (m.type === "cloud" && m.rect) {
|
|
13618
|
+
const r = box(...m.rect), a = P(r[0]), b = P(r[1]), cb = cloudBezier(...a, ...b);
|
|
13619
|
+
add([["M", ...cb.start], ...cb.segments.map((q) => ["C", ...q.flat()]), ["Z"]]);
|
|
13620
|
+
if (m.text) {
|
|
13621
|
+
const at = noteOrigin(P(m.note_at || [r[0][0], r[1][1] + 12 * pixelScale / H]));
|
|
13622
|
+
add(path3([at, [a[0], b[1]]]));
|
|
13623
|
+
noteBox2 = note(at);
|
|
13624
|
+
}
|
|
13625
|
+
}
|
|
13626
|
+
if (m.type === "highlight") {
|
|
13627
|
+
if (m.quads?.length) for (const q of m.quads) add([...path3(q.map(P)), ["Z"]], { fill: s.color, stroke: void 0, opacity: s.opacity, blend: "multiply" });
|
|
13628
|
+
else if (m.pts?.length > 1) add(path3(m.pts.map(P)), { width: m.w ? m.w * W : s.width_pt * pixelScale, opacity: s.opacity, blend: "multiply" });
|
|
13629
|
+
else if (m.rect) {
|
|
13630
|
+
const [a, b] = box(...m.rect);
|
|
13631
|
+
add([...path3([P(a), P([b[0], a[1]]), P(b), P([a[0], b[1]])]), ["Z"]], { fill: s.color, stroke: void 0, opacity: s.opacity, blend: "multiply" });
|
|
13632
|
+
}
|
|
13633
|
+
}
|
|
13634
|
+
const dash = s.line_style === "dashed" ? [5 * pixelScale, 3 * pixelScale] : s.line_style === "dotted" ? [pixelScale, 3 * pixelScale] : void 0;
|
|
13635
|
+
for (const p of paths) if (p.stroke && !p.fill && m.type !== "highlight") p.dash = dash;
|
|
13636
|
+
if (m.type === "cloud" && m.rect && Number.isFinite(m.rev) && m.rev > 0) {
|
|
13637
|
+
const r = box(...m.rect), x = r[1][0] * W, y = r[0][1] * H - 12 * pixelScale;
|
|
13638
|
+
add([["M", x, y - 7 * pixelScale], ["L", x + 7 * pixelScale, y + 5 * pixelScale], ["L", x - 7 * pixelScale, y + 5 * pixelScale], ["Z"]], { fill: "#ffffff" });
|
|
13639
|
+
texts.push({ text: String(m.rev), x: x - 2.5 * pixelScale, y: y + 3 * pixelScale, size: 8 * pixelScale, color: s.color });
|
|
13640
|
+
}
|
|
13641
|
+
return { paths, texts, noteBox: noteBox2 };
|
|
13642
|
+
}
|
|
13643
|
+
|
|
12970
13644
|
// ../web/src/lib/svgpath.js
|
|
12971
13645
|
function isSep(c) {
|
|
12972
13646
|
return c === " " || c === " " || c === "\n" || c === "\r" || c === "\f" || c === ",";
|
|
@@ -13735,12 +14409,22 @@ function invertPixels(cv) {
|
|
|
13735
14409
|
ctx.restore();
|
|
13736
14410
|
}
|
|
13737
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" }) {
|
|
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
|
+
}
|
|
13738
14422
|
const rfis = liveRfis(rfisIn);
|
|
13739
14423
|
const M = units === "metric";
|
|
13740
14424
|
const uA = (sf) => M ? sf * 0.09290304 : sf;
|
|
13741
14425
|
const uL = (lf) => M ? lf * 0.3048 : lf;
|
|
13742
14426
|
const AU = M ? "m2" : "SF", LU = M ? "m" : "LF";
|
|
13743
|
-
const { PDFDocument, StandardFonts, rgb, degrees, LineCapStyle } = await import("pdf-lib");
|
|
14427
|
+
const { PDFDocument, StandardFonts, rgb, degrees, LineCapStyle, BlendMode } = await import("pdf-lib");
|
|
13744
14428
|
const condById = Object.fromEntries(conditions.map((c) => [c.id, c]));
|
|
13745
14429
|
const rfiNum = new Map((rfis || []).map((r) => [r.id, r.number]));
|
|
13746
14430
|
const byKey = (arr3) => {
|
|
@@ -14152,9 +14836,30 @@ async function buildMarkedSetPdf({ projectName, dark, sheets, shapes, markups, a
|
|
|
14152
14836
|
const mCond = m.condition_id ? (conditions || []).find((c) => c.id === m.condition_id) : null;
|
|
14153
14837
|
const mbase = m.color || mCond?.color || (m.rfi_id ? COBALT : "#c47a10");
|
|
14154
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;
|
|
14155
14840
|
const mdash = pdfDashFor(m.line_style || "solid");
|
|
14156
14841
|
const mw = clampWeight(m.weight);
|
|
14157
|
-
if (m.
|
|
14842
|
+
if (m.annotation_style && ["arrow", "highlight", "callout", "cloud", "text"].includes(m.type)) {
|
|
14843
|
+
const scene = annotationScene(m, W, H, RENDER_SCALE);
|
|
14844
|
+
const P = ([x, y]) => {
|
|
14845
|
+
const [px, py] = toPage(x, y);
|
|
14846
|
+
return [px, -py];
|
|
14847
|
+
};
|
|
14848
|
+
for (const ink2 of scene.paths) pg.drawSvgPath(pathString(ink2.commands, P), {
|
|
14849
|
+
x: 0,
|
|
14850
|
+
y: 0,
|
|
14851
|
+
...ink2.fill ? { color: rgb(...hex(ink2.fill)), opacity: ink2.opacity } : {},
|
|
14852
|
+
...ink2.stroke ? { borderColor: rgb(...hex(ink2.stroke)), borderWidth: ink2.width * ptScale, borderOpacity: ink2.opacity } : {},
|
|
14853
|
+
...ink2.dash ? { borderDashArray: ink2.dash.map((n) => n * ptScale) } : {},
|
|
14854
|
+
...ink2.blend ? { blendMode: BlendMode.Multiply } : {},
|
|
14855
|
+
borderLineCap: LineCapStyle.Round
|
|
14856
|
+
});
|
|
14857
|
+
for (const ink2 of scene.texts) text(ink2.text, ink2.x, ink2.y, ink2.size * ptScale, rgb(...hex(ink2.color)));
|
|
14858
|
+
if (rlabel) {
|
|
14859
|
+
const at = m.at || m.from || m.rect?.[0] || m.pts?.[0];
|
|
14860
|
+
if (at) text(rlabel, at[0] * W, at[1] * H - 10 / ptScale, 8, mcol, bold);
|
|
14861
|
+
}
|
|
14862
|
+
} else if (m.type === "highlight" && (m.pts || []).length >= 2) {
|
|
14158
14863
|
const ipts = m.pts.map(([nx, ny]) => [nx * W, ny * H]);
|
|
14159
14864
|
const inkCol = rgb(...hex(m.color || "#ffd60a"));
|
|
14160
14865
|
const wImg = (m.w || 0.01) * W * mw;
|
|
@@ -14176,7 +14881,7 @@ async function buildMarkedSetPdf({ projectName, dark, sheets, shapes, markups, a
|
|
|
14176
14881
|
const r = [[nx0 * W, ny0 * H], [nx1 * W, ny0 * H], [nx1 * W, ny1 * H], [nx0 * W, ny1 * H]];
|
|
14177
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 } : {} });
|
|
14178
14883
|
const t = lbl(m.text);
|
|
14179
|
-
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);
|
|
14180
14885
|
} else if (m.type === "cloud" && m.rect) {
|
|
14181
14886
|
const [[nx0, ny0], [nx1, ny1]] = m.rect;
|
|
14182
14887
|
const cb = cloudBezier(nx0 * W, ny0 * H, nx1 * W, ny1 * H);
|
|
@@ -14188,7 +14893,7 @@ async function buildMarkedSetPdf({ projectName, dark, sheets, shapes, markups, a
|
|
|
14188
14893
|
for (const [c1, c2, end] of cb.segments) d += ` C${P(c1)} ${P(c2)} ${P(end)}`;
|
|
14189
14894
|
pg.drawSvgPath(d + " Z", { x: 0, y: 0, borderColor: mcol, borderWidth: 1.3 * mw, borderOpacity: 0.95, ...mdash ? { borderDashArray: mdash } : {} });
|
|
14190
14895
|
const t = lbl(m.text);
|
|
14191
|
-
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);
|
|
14192
14897
|
if (Number.isFinite(m.rev) && m.rev > 0) {
|
|
14193
14898
|
const cxImg = Math.max(nx0, nx1) * W, cyImg = Math.min(ny0, ny1) * H, s = 9 / ptScale;
|
|
14194
14899
|
const tri = [[cxImg, cyImg - s], [cxImg + s, cyImg + s], [cxImg - s, cyImg + s]];
|
|
@@ -14202,7 +14907,7 @@ async function buildMarkedSetPdf({ projectName, dark, sheets, shapes, markups, a
|
|
|
14202
14907
|
const [ptx, pty] = toPage(m.to[0] * W, m.to[1] * H);
|
|
14203
14908
|
pg.drawSvgPath(arrowheadPath(pfx, -pfy, ptx, -pty, 6 * mw), { x: 0, y: 0, color: mcol, opacity: 0.95 });
|
|
14204
14909
|
const t = lbl(m.text);
|
|
14205
|
-
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);
|
|
14206
14911
|
} else if (m.type === "dimension" && m.from && m.to) {
|
|
14207
14912
|
const fx = m.from[0] * W, fy = m.from[1] * H, dxq = m.to[0] * W, dyq = m.to[1] * H;
|
|
14208
14913
|
line(fx, fy, dxq, dyq, mcol, 1.1 * mw, 0.95, mdash);
|
|
@@ -14216,7 +14921,7 @@ async function buildMarkedSetPdf({ projectName, dark, sheets, shapes, markups, a
|
|
|
14216
14921
|
const size = 8;
|
|
14217
14922
|
const tw = bold.widthOfTextAtSize(winAnsiSafe(t), size);
|
|
14218
14923
|
const [pmx, pmy] = toPage((fx + dxq) / 2 + dnx * (14 / ptScale), (fy + dyq) / 2 + dny * (14 / ptScale));
|
|
14219
|
-
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 });
|
|
14220
14925
|
}
|
|
14221
14926
|
} else if (m.type === "bubble" && m.at) {
|
|
14222
14927
|
const cxImg = m.at[0] * W, cyImg = m.at[1] * H;
|
|
@@ -14227,7 +14932,7 @@ async function buildMarkedSetPdf({ projectName, dark, sheets, shapes, markups, a
|
|
|
14227
14932
|
if (t) {
|
|
14228
14933
|
const size = 8;
|
|
14229
14934
|
const tw = bold.widthOfTextAtSize(winAnsiSafe(t), size);
|
|
14230
|
-
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 });
|
|
14231
14936
|
}
|
|
14232
14937
|
} else if (m.type === "callout" && m.at) {
|
|
14233
14938
|
if (m.target) {
|
|
@@ -14236,9 +14941,9 @@ async function buildMarkedSetPdf({ projectName, dark, sheets, shapes, markups, a
|
|
|
14236
14941
|
const [ptx, pty] = toPage(m.target[0] * W, m.target[1] * H);
|
|
14237
14942
|
pg.drawSvgPath(arrowheadPath(pax, -pay, ptx, -pty, 5), { x: 0, y: 0, color: mcol, opacity: 0.9 });
|
|
14238
14943
|
}
|
|
14239
|
-
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));
|
|
14240
14945
|
} else if (m.type === "text" && m.at) {
|
|
14241
|
-
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));
|
|
14242
14947
|
} else if (m.type === "svg" && m.at && Array.isArray(m.vb) && typeof m.path === "string") {
|
|
14243
14948
|
const { s: sx, bw, bh } = svgPlacedBox(m.vb, m.w, W);
|
|
14244
14949
|
if (sx > 0) {
|
|
@@ -14250,7 +14955,7 @@ async function buildMarkedSetPdf({ projectName, dark, sheets, shapes, markups, a
|
|
|
14250
14955
|
const fillOn = m.fill && m.fill !== "none";
|
|
14251
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 } : {} });
|
|
14252
14957
|
const t = lbl(m.text);
|
|
14253
|
-
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);
|
|
14254
14959
|
}
|
|
14255
14960
|
} else if (m.type === "image" && m.at && m.src) {
|
|
14256
14961
|
const fmt3 = pickEmbedFormat(m.src);
|
|
@@ -14401,7 +15106,7 @@ async function exportMarkedPdf(session, opts) {
|
|
|
14401
15106
|
throw new UserError("Nothing to mark yet \u2014 commit shapes (one_click / detect_rooms / measure_polygon / measure_line with a condition) or annotate before exporting the marked set.");
|
|
14402
15107
|
}
|
|
14403
15108
|
const base = file.replace(/\.pdf$/i, "");
|
|
14404
|
-
const outPath =
|
|
15109
|
+
const outPath = path4.resolve(opts.path ?? path4.join(path4.dirname(filePath), `${base} - marked set.pdf`));
|
|
14405
15110
|
await assertWritable(outPath, "pdf", opts.overwrite);
|
|
14406
15111
|
const sheetStates = session.sheetList();
|
|
14407
15112
|
const byFilePage = new Map(sheetStates.map((s) => [`${session.fileFor(s.key)}#${s.pageNum}`, s.page]));
|
|
@@ -14479,7 +15184,7 @@ async function exportMarkedPdf(session, opts) {
|
|
|
14479
15184
|
}
|
|
14480
15185
|
|
|
14481
15186
|
// src/importing.ts
|
|
14482
|
-
import
|
|
15187
|
+
import path5 from "node:path";
|
|
14483
15188
|
import { readFile as readFile3 } from "node:fs/promises";
|
|
14484
15189
|
|
|
14485
15190
|
// ../web/src/lib/reviewState.js
|
|
@@ -14487,6 +15192,11 @@ function normalizeAgentReview(shape) {
|
|
|
14487
15192
|
return shape?.origin?.actor === "agent" && shape.origin.reviewed == null ? { ...shape, origin: { ...shape.origin, reviewed: false } } : shape;
|
|
14488
15193
|
}
|
|
14489
15194
|
|
|
15195
|
+
// ../web/src/lib/multiplier.js
|
|
15196
|
+
function isValidMultiplier(v) {
|
|
15197
|
+
return v === void 0 || v === null || typeof v === "number" && Number.isFinite(v) && v > 0;
|
|
15198
|
+
}
|
|
15199
|
+
|
|
14490
15200
|
// ../web/src/lib/importTakeoff.js
|
|
14491
15201
|
function parseTakeoffImport(text) {
|
|
14492
15202
|
let doc;
|
|
@@ -14511,6 +15221,11 @@ function mergeTakeoffImport(current, imported, knownFiles = null) {
|
|
|
14511
15221
|
const cur = current && typeof current === "object" ? current : {};
|
|
14512
15222
|
const impShapes = arr2(imported.shapes).filter((s) => s && typeof s === "object" && typeof s.sheet_id === "string" && typeof s.id === "string").map(normalizeAgentReview);
|
|
14513
15223
|
const impConds = arr2(imported.conditions).filter((c) => c && typeof c === "object" && typeof c.id === "string");
|
|
15224
|
+
const badMult = impConds.filter((c) => !isValidMultiplier(c.multiplier));
|
|
15225
|
+
if (badMult.length) {
|
|
15226
|
+
const list = badMult.map((c) => `${c.finish_tag ?? c.id} (${JSON.stringify(c.multiplier)})`).join(", ");
|
|
15227
|
+
throw new Error(`Couldn't import takeoff: condition multiplier must be a positive number \u2014 ${list}. Fix the file and re-import. Nothing was imported.`);
|
|
15228
|
+
}
|
|
14514
15229
|
const localScales = new Map(arr2(cur.sheets).filter((s) => typeof s?.sheet_id === "string").map((s) => [s.sheet_id, s.units_per_px]));
|
|
14515
15230
|
const incomingScales = new Map(arr2(imported.sheets).filter((s) => typeof s?.sheet_id === "string").map((s) => [s.sheet_id, s.units_per_px]));
|
|
14516
15231
|
const existingIds = new Set(arr2(cur.shapes).map((s) => s.id));
|
|
@@ -14675,7 +15390,7 @@ async function importTakeoff(session, filePath) {
|
|
|
14675
15390
|
const addedIds = session.shapes.filter((s) => !prevShapeIds.has(s.id)).map((s) => s.id);
|
|
14676
15391
|
if (addedIds.length) session.journalCommit("import_takeoff", addedIds);
|
|
14677
15392
|
return {
|
|
14678
|
-
file:
|
|
15393
|
+
file: path5.basename(filePath),
|
|
14679
15394
|
...note,
|
|
14680
15395
|
rules_imported: rulesImported,
|
|
14681
15396
|
shapes_total: session.shapes.length,
|
|
@@ -14720,8 +15435,8 @@ function registerTools(realServer, session, opts = {}) {
|
|
|
14720
15435
|
merge: z2.boolean().optional().describe("true = ADD this document to the working set, keeping all existing work (merge into an empty session is just a load)")
|
|
14721
15436
|
},
|
|
14722
15437
|
outputSchema: loadPlanOutput
|
|
14723
|
-
}, run("load_plan", async ({ path:
|
|
14724
|
-
const loaded = await session.loadPlan(
|
|
15438
|
+
}, run("load_plan", async ({ path: path6, merge: merge2 }) => {
|
|
15439
|
+
const loaded = await session.loadPlan(path6, { merge: merge2 });
|
|
14725
15440
|
server.sendResourceListChanged();
|
|
14726
15441
|
return loaded;
|
|
14727
15442
|
}));
|
|
@@ -14808,15 +15523,17 @@ function registerTools(realServer, session, opts = {}) {
|
|
|
14808
15523
|
outputSchema: cutOutOutput
|
|
14809
15524
|
}, run("cut_out", (a) => session.cutOut(a)));
|
|
14810
15525
|
server.registerTool("measure_line", {
|
|
14811
|
-
description: `Measure an open polyline (min 2 points, image px): length_lf at the sheet's scale. Requires the scale to be set. Pass condition to commit it as a linear shape (base, transitions, feature strips). A curved run (base along a radius wall, a curved feature strip) takes arc_through: one point on the bow, marked. ${COORDS}`,
|
|
15526
|
+
description: `Measure an open polyline (min 2 points, image px): length_lf at the sheet's scale. Requires the scale to be set. Pass condition to commit it as a linear shape (base, transitions, feature strips, conduit and home runs). A curved run (base along a radius wall, a curved feature strip) takes arc_through: one point on the bow, marked. DROP AND RISE (#441): a plan trace is the flat X\u2013Y path; the material also travels VERTICALLY \u2014 a home run drops from the ceiling to a panel, rises to a box. length_lf is the TOTAL: plan + rise + drop. The condition's rise_ft / drop_ft (edit_condition) are the defaults for every run under it; pass rise_ft / drop_ft here to give THIS run its own legs (0 included \u2014 "no drop on this one" is a statement), and the reply splits plan_lf / vertical_lf beside the total when a leg exists. ${COORDS}`,
|
|
14812
15527
|
inputSchema: {
|
|
14813
15528
|
sheet: z2.string(),
|
|
14814
15529
|
pts: z2.array(pointSchema).min(2),
|
|
14815
15530
|
condition: z2.string().optional(),
|
|
15531
|
+
rise_ft: z2.number().min(0).optional().describe("This run's vertical leg UP, in feet, added to its plan length \u2014 overrides the condition's rise_ft default for this run (0 = no rise here, whatever the default)"),
|
|
15532
|
+
drop_ft: z2.number().min(0).optional().describe("This run's vertical leg DOWN, in feet, added to its plan length \u2014 overrides the condition's drop_ft default for this run (0 = no drop here, whatever the default)"),
|
|
14816
15533
|
arc_through: z2.array(z2.number().int().nonnegative()).optional().describe("Indices of points that are the MIDDLE of an arc: the trace runs the point before \u2192 this point \u2192 the point after as the unique circle through the three (the canvas's Curve mode). For a curved wall put one point anywhere ON the bow between its two ends and mark it. The arc is baked to ordinary vertices on commit and origin.curved is stamped; a mark on an end of an open run, or two marks in a row, refuses.")
|
|
14817
15534
|
},
|
|
14818
15535
|
outputSchema: measureLineOutput
|
|
14819
|
-
}, run("measure_line", (a) => session.measureLine(a.sheet, a.pts, { condition: a.condition, arc_through: a.arc_through })));
|
|
15536
|
+
}, run("measure_line", (a) => session.measureLine(a.sheet, a.pts, { condition: a.condition, arc_through: a.arc_through, rise_ft: a.rise_ft, drop_ft: a.drop_ft })));
|
|
14820
15537
|
server.registerTool("measure_surface", {
|
|
14821
15538
|
description: `Surface Area \u2014 wall SF (#146): trace an OPEN run along the wall in plan view (min 2 points, image px) and the quantity is traced LF \xD7 height. This is how wall tile, wainscot, and wall systems are taken off \u2014 the quantity family ${oneClick ? "one_click and " : ""}measure_polygon cannot produce. Height lives on the CONDITION (the canvas's H knob): pass height_ft to set it on this call (journals as its own undo step, like typing H before tracing), or set it once with edit_condition; with neither, this refuses and mints nothing. The shape snapshots the height it was quantified at. Requires the sheet's scale. ${COORDS}`,
|
|
14822
15539
|
inputSchema: {
|
|
@@ -15089,10 +15806,12 @@ All-or-nothing, like derive_base: an unknown tag, a transition landing on either
|
|
|
15089
15806
|
verts: z2.array(pointSchema).optional().describe("Replacement geometry (image px): \u22653 vertices for an area shape, \u22652 points for a linear/surface run, \u22651 for a count marker"),
|
|
15090
15807
|
condition: z2.string().optional().describe("Reassign to this finish tag (minted on first use)"),
|
|
15091
15808
|
role: z2.enum(["floor_area", "deduct", "linear", "surface_area", "count"]).optional().describe("Switch what the shape measures \u2014 flipping INTO surface_area needs a height on the shape or its condition"),
|
|
15092
|
-
label: z2.string().optional().describe('The room (or phase/area) this shape belongs to, e.g. "134" or "OFFICE 101" \u2014 what per-room reporting groups by. Pass "" to clear it')
|
|
15809
|
+
label: z2.string().optional().describe('The room (or phase/area) this shape belongs to, e.g. "134" or "OFFICE 101" \u2014 what per-room reporting groups by. Pass "" to clear it'),
|
|
15810
|
+
rise_ft: z2.number().min(0).nullable().optional().describe("Linear runs only (#441): this run's vertical leg UP in feet, overriding the condition's rise_ft default (0 = none). null clears the override so the condition's default applies again. perimeter_lf is recomputed as plan + rise + drop"),
|
|
15811
|
+
drop_ft: z2.number().min(0).nullable().optional().describe("Linear runs only (#441): this run's vertical leg DOWN in feet, overriding the condition's drop_ft default (0 = none). null clears the override so the condition's default applies again")
|
|
15093
15812
|
},
|
|
15094
15813
|
outputSchema: editShapeOutput
|
|
15095
|
-
}, run("edit_shape", (a) => session.editShape(a.shape_id, { verts: a.verts, condition: a.condition, role: a.role, label: a.label })));
|
|
15814
|
+
}, run("edit_shape", (a) => session.editShape(a.shape_id, { verts: a.verts, condition: a.condition, role: a.role, label: a.label, rise_ft: a.rise_ft, drop_ft: a.drop_ft })));
|
|
15096
15815
|
server.registerTool("edit_materials", {
|
|
15097
15816
|
description: `Add, remove, or patch supporting-materials rows on a condition \u2014 the coverage-rate lines that turn a measured area/length/count into an order quantity (adhesive at N sf/gal, grout at N lf/bag, \u2026), matching the canvas's per-condition Supporting Materials panel. Each row is {name, per, basis, unit, round, note}: quantity = the condition's basis total (area/linear/count/seam_lf) \xF7 per, rounded up to whole purchase units unless round:false. basis "seam_lf" is the one basis that is FIGURED rather than measured: it is the length where two cuts meet on the floor, read off the condition's roll layout (set roll_setup with edit_condition), which is what a heat-weld rod or a carpet seam tape is bought by. A 20-ft-wide room off a 12-ft roll seams once down its length; the same square footage as two 10-ft rooms seams not at all, and no percentage of the area or the perimeter can tell those two jobs apart. Without a roll_setup \u2014 or with no committed floor shapes to lay out \u2014 a seam_lf row reads 0, which is the honest state rather than a guess. condition names an existing OR NEW finish tag (minted on first touch, same as ${oneClick ? "one_click/" : ""}measure_polygon) \u2014 add alone is enough to seed materials on a condition before you've traced anything. remove/patch target existing row ids from this reply or export_takeoff (takeoff_summary strips materials for a compact quantities-only reply); a bad id 404s the WHOLE call before anything is written, and referencing an id on a tag with no condition yet errors rather than silently minting an empty one. No review gate here \u2014 materials rows are quantity config, not traced geometry, so this edits directly; undo_last reverses a call in one step (the condition's whole materials array, snapshotted before the write, restored verbatim).`,
|
|
15098
15817
|
inputSchema: {
|
|
@@ -15120,6 +15839,8 @@ All-or-nothing, like derive_base: an unknown tag, a transition landing on either
|
|
|
15120
15839
|
waste_pct: z2.number().min(0).optional().describe("Waste percentage applied to net order quantities, e.g. 10 for 10%"),
|
|
15121
15840
|
multiplier: z2.number().positive().optional().describe("Quantity multiplier (\xD7N identical areas). Note: the canvas treats 0 as 1, so 0 is rejected here rather than silently meaning 'off'"),
|
|
15122
15841
|
height_ft: z2.number().positive().optional().describe("Wall height in feet \u2014 the canvas's H knob; measure_surface quantifies traced LF \xD7 this"),
|
|
15842
|
+
rise_ft: z2.number().min(0).optional().describe("Drop and Rise (#441): the vertical leg UP, in feet, every linear run of this condition adds to its plan length (LF = plan + rise + drop). Re-flows existing runs that do not carry their own rise_ft; derived base/transitions never take a leg. 0 turns it off"),
|
|
15843
|
+
drop_ft: z2.number().min(0).optional().describe("Drop and Rise (#441): the vertical leg DOWN, in feet, every linear run of this condition adds to its plan length. Re-flows existing runs that do not carry their own drop_ft. 0 turns it off"),
|
|
15123
15844
|
roll_setup: z2.union([
|
|
15124
15845
|
z2.null().describe("Opt the condition OUT of roll goods"),
|
|
15125
15846
|
z2.object({
|
|
@@ -15135,7 +15856,7 @@ All-or-nothing, like derive_base: an unknown tag, a transition landing on either
|
|
|
15135
15856
|
]).optional().describe("Roll-goods opt-in (#147): presence of a setup is what makes the condition roll goods \u2014 seams figured, cuts packed, order footage beside the measured quantities. Same-material partial edits patch the existing setup; null opts out. The reply echoes the figured order (cuts, order_lf, rolls, order_qty) whenever floor shapes exist on scaled sheets, and export_report's roll_goods block carries the same rows")
|
|
15136
15857
|
},
|
|
15137
15858
|
outputSchema: editConditionOutput
|
|
15138
|
-
}, run("edit_condition", (a) => session.editCondition(a.condition, { waste_pct: a.waste_pct, multiplier: a.multiplier, height_ft: a.height_ft, roll_setup: a.roll_setup })));
|
|
15859
|
+
}, run("edit_condition", (a) => session.editCondition(a.condition, { waste_pct: a.waste_pct, multiplier: a.multiplier, height_ft: a.height_ft, roll_setup: a.roll_setup, rise_ft: a.rise_ft, drop_ft: a.drop_ft })));
|
|
15139
15860
|
server.registerTool("propose_condition_edit", {
|
|
15140
15861
|
description: `PROPOSE a change to a condition instead of making it (#365): a diff \u2014 a new finish tag (rename), waste %, \xD7N multiplier, height_ft, roll_setup \u2014 held PENDING until the estimator accepts it from the panel. edit_condition is the wrong power for "I think this condition is wrong": a tag rename or a knob change should be a decision the estimator makes, not one they discover. Until acceptance NOTHING changes \u2014 takeoff_summary and export_report keep computing from the current values and carry the diff beside them (proposed_condition_edits), and once accepted the report is byte-for-byte what a direct edit_condition would have produced (the same write path). Only fields that differ from the current value are recorded; a proposal that changes nothing is refused, and a rename onto a tag another condition already carries is refused (two conditions on one tag would make one unreachable). One pending diff per condition \u2014 proposing again replaces the earlier one (undo_last restores it). rationale is required: the estimator accepts a reason.`,
|
|
15141
15862
|
inputSchema: {
|
|
@@ -15144,11 +15865,13 @@ All-or-nothing, like derive_base: an unknown tag, a transition landing on either
|
|
|
15144
15865
|
waste_pct: z2.number().min(0).optional(),
|
|
15145
15866
|
multiplier: z2.number().positive().optional(),
|
|
15146
15867
|
height_ft: z2.number().positive().optional(),
|
|
15868
|
+
rise_ft: z2.number().min(0).optional().describe("Proposed default vertical leg UP for the condition's linear runs (#441)"),
|
|
15869
|
+
drop_ft: z2.number().min(0).optional().describe("Proposed default vertical leg DOWN for the condition's linear runs (#441)"),
|
|
15147
15870
|
roll_setup: z2.union([z2.null(), z2.object({}).passthrough()]).optional().describe("Proposed roll-goods setup, or null to propose opting out"),
|
|
15148
15871
|
rationale: z2.string().min(1).describe("Why \u2014 the schedule row, the spec section, the sheet note that decided it")
|
|
15149
15872
|
},
|
|
15150
15873
|
outputSchema: proposeConditionEditOutput
|
|
15151
|
-
}, run("propose_condition_edit", (a) => session.proposeConditionEdit(a.condition, { finish_tag: a.finish_tag, waste_pct: a.waste_pct, multiplier: a.multiplier, height_ft: a.height_ft, roll_setup: a.roll_setup }, a.rationale)));
|
|
15874
|
+
}, run("propose_condition_edit", (a) => session.proposeConditionEdit(a.condition, { finish_tag: a.finish_tag, waste_pct: a.waste_pct, multiplier: a.multiplier, height_ft: a.height_ft, rise_ft: a.rise_ft, drop_ft: a.drop_ft, roll_setup: a.roll_setup }, a.rationale)));
|
|
15152
15875
|
server.registerTool("withdraw_condition_edit", {
|
|
15153
15876
|
description: `Drop a pending condition-edit proposal (#365) without touching the condition. undo_last re-seats it.`,
|
|
15154
15877
|
inputSchema: { proposal_id: z2.string().describe("From propose_condition_edit, or takeoff_summary's proposed_condition_edits") },
|
|
@@ -15238,7 +15961,7 @@ All-or-nothing, like derive_base: an unknown tag, a transition landing on either
|
|
|
15238
15961
|
server.registerTool("annotate", {
|
|
15239
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.
|
|
15240
15963
|
|
|
15241
|
-
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.
|
|
15242
15965
|
|
|
15243
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}`,
|
|
15244
15967
|
inputSchema: {
|
|
@@ -15339,7 +16062,7 @@ No review gate: the pencil-not-ink rule exists to stop an agent inventing geomet
|
|
|
15339
16062
|
import { ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
15340
16063
|
|
|
15341
16064
|
// src/wiki.generated.ts
|
|
15342
|
-
var WIKI_VERSION = "0.9.
|
|
16065
|
+
var WIKI_VERSION = "0.9.93";
|
|
15343
16066
|
var WIKI_PAGES = [
|
|
15344
16067
|
{
|
|
15345
16068
|
"key": "index",
|
|
@@ -15416,7 +16139,7 @@ var WIKI_PAGES = [
|
|
|
15416
16139
|
];
|
|
15417
16140
|
|
|
15418
16141
|
// src/protocol.generated.ts
|
|
15419
|
-
var PROTOCOL_INDEX_JSON = '{\n "protocol": "TakeoffDocument v1 (draft)",\n "legacy": "takeoff_canvas.v1 structural profile (draft)",\n "scope": "Machine-readable discovery and contract resources; writers and runtime validation are unchanged.",\n "coordinate_contract": "Tool coordinates are full-sheet image pixels at render scale 2.0, top-left origin, y-down; persisted verts_norm are normalized to the sheet/composite frame.",\n "roles": [\n "floor_area",\n "deduct",\n "linear",\n "surface_area",\n "count"\n ],\n "identifiers": {\n "draft": "opentakeoff.takeoff-document.v1",\n "canvas": "opentakeoff.takeoff_canvas.v1"\n },\n "calibration": "Calibration is feet per logical image pixel; metric display does not rewrite stored geometry.",\n "records": "Evidence/provenance and review fields are records and claims, not authentication. Unknown extension fields remain opaque.",\n "limits": [\n "Schemas do not prove polygon topology, geometry accuracy, quantity recomputation, source/PDF availability, complete history, active reference integrity, MCP transport preservation, or approval authenticity.",\n "Secondary families such as markups, RFIs, rules, and layout/workspace data are not fully semantically validated.",\n "MCP exportPayload() is a projection: it omits rules and browser stitches, strips RFI tombstones, and does not represent the complete browser workspace/project document.",\n "The $id URLs are stable identifiers for offline resolution, not a promise that the files are deployed at those URLs.",\n "Both the draft TakeoffDocument v1 and existing canvas identifier are described; neither writer is changed by this increment.",\n "MCP cannot create human approval; schema-valid records do not authenticate their author or grant review authority."\n ],\n "wiki": "takeoff://wiki/protocol",\n "resources": [\n {\n "uri": "takeoff://protocol/legacy/takeoff-canvas.v1.schema.json",\n "id": "https://opentakeoff.kentucky-ai.com/protocol/legacy/takeoff-canvas.v1.schema.json",\n "title": "takeoff_canvas.v1 structural profile (draft)",\n "source": "protocol/legacy/takeoff-canvas.v1.schema.json",\n "sha256": "111ca2e5294dca11d742d5ae240d2910dd78ad52a631075b791f1483c736f01d"\n },\n {\n "uri": "takeoff://protocol/v1/calibration.schema.json",\n "id": "https://opentakeoff.kentucky-ai.com/protocol/v1/calibration.schema.json",\n "title": "Calibration (draft)",\n "source": "protocol/v1/calibration.schema.json",\n "sha256": "53ab69f4df0ce57d869db576aef46f95f7592672315f7ba772e728f6c2d6ca12"\n },\n {\n "uri": "takeoff://protocol/v1/common.schema.json",\n "id": "https://opentakeoff.kentucky-ai.com/protocol/v1/common.schema.json",\n "title": "Shared value types (draft)",\n "source": "protocol/v1/common.schema.json",\n "sha256": "
|
|
16142
|
+
var PROTOCOL_INDEX_JSON = '{\n "protocol": "TakeoffDocument v1 (draft)",\n "legacy": "takeoff_canvas.v1 structural profile (draft)",\n "scope": "Machine-readable discovery and contract resources; writers and runtime validation are unchanged.",\n "coordinate_contract": "Tool coordinates are full-sheet image pixels at render scale 2.0, top-left origin, y-down; persisted verts_norm are normalized to the sheet/composite frame.",\n "roles": [\n "floor_area",\n "deduct",\n "linear",\n "surface_area",\n "count"\n ],\n "identifiers": {\n "draft": "opentakeoff.takeoff-document.v1",\n "canvas": "opentakeoff.takeoff_canvas.v1"\n },\n "calibration": "Calibration is feet per logical image pixel; metric display does not rewrite stored geometry.",\n "records": "Evidence/provenance and review fields are records and claims, not authentication. Unknown extension fields remain opaque.",\n "limits": [\n "Schemas do not prove polygon topology, geometry accuracy, quantity recomputation, source/PDF availability, complete history, active reference integrity, MCP transport preservation, or approval authenticity.",\n "Secondary families such as markups, RFIs, rules, and layout/workspace data are not fully semantically validated.",\n "MCP exportPayload() is a projection: it omits rules and browser stitches, strips RFI tombstones, and does not represent the complete browser workspace/project document.",\n "The $id URLs are stable identifiers for offline resolution, not a promise that the files are deployed at those URLs.",\n "Both the draft TakeoffDocument v1 and existing canvas identifier are described; neither writer is changed by this increment.",\n "MCP cannot create human approval; schema-valid records do not authenticate their author or grant review authority."\n ],\n "wiki": "takeoff://wiki/protocol",\n "resources": [\n {\n "uri": "takeoff://protocol/legacy/takeoff-canvas.v1.schema.json",\n "id": "https://opentakeoff.kentucky-ai.com/protocol/legacy/takeoff-canvas.v1.schema.json",\n "title": "takeoff_canvas.v1 structural profile (draft)",\n "source": "protocol/legacy/takeoff-canvas.v1.schema.json",\n "sha256": "111ca2e5294dca11d742d5ae240d2910dd78ad52a631075b791f1483c736f01d"\n },\n {\n "uri": "takeoff://protocol/v1/calibration.schema.json",\n "id": "https://opentakeoff.kentucky-ai.com/protocol/v1/calibration.schema.json",\n "title": "Calibration (draft)",\n "source": "protocol/v1/calibration.schema.json",\n "sha256": "53ab69f4df0ce57d869db576aef46f95f7592672315f7ba772e728f6c2d6ca12"\n },\n {\n "uri": "takeoff://protocol/v1/common.schema.json",\n "id": "https://opentakeoff.kentucky-ai.com/protocol/v1/common.schema.json",\n "title": "Shared value types (draft)",\n "source": "protocol/v1/common.schema.json",\n "sha256": "2c929da4d7db6af6f6d8d20d354bfb11216db6fd6edd04847fb6a0a4405e5539"\n },\n {\n "uri": "takeoff://protocol/v1/condition.schema.json",\n "id": "https://opentakeoff.kentucky-ai.com/protocol/v1/condition.schema.json",\n "title": "Condition (draft)",\n "source": "protocol/v1/condition.schema.json",\n "sha256": "c45284ba2c390bb0b0be9208f1b51fd37d969184956aa73b1b1e5276109aca86"\n },\n {\n "uri": "takeoff://protocol/v1/document-fields.schema.json",\n "id": "https://opentakeoff.kentucky-ai.com/protocol/v1/document-fields.schema.json",\n "title": "Existing document collections (draft)",\n "source": "protocol/v1/document-fields.schema.json",\n "sha256": "9ec6b05869bae0eb0e4c9d6caa12a525b6565a0e795134cfb64e6a7a232416b4"\n },\n {\n "uri": "takeoff://protocol/v1/evidence.schema.json",\n "id": "https://opentakeoff.kentucky-ai.com/protocol/v1/evidence.schema.json",\n "title": "Evidence (draft)",\n "source": "protocol/v1/evidence.schema.json",\n "sha256": "5602648262758e35cf503a46fe65fb58f7e8d2d22717e313dd5e974664d771b9"\n },\n {\n "uri": "takeoff://protocol/v1/measurement.schema.json",\n "id": "https://opentakeoff.kentucky-ai.com/protocol/v1/measurement.schema.json",\n "title": "Measurement (draft)",\n "source": "protocol/v1/measurement.schema.json",\n "sha256": "0e1b050d38963a1256f3647922ca90031eb04998d78e0f53bb3fb577bf590648"\n },\n {\n "uri": "takeoff://protocol/v1/provenance.schema.json",\n "id": "https://opentakeoff.kentucky-ai.com/protocol/v1/provenance.schema.json",\n "title": "Provenance (draft)",\n "source": "protocol/v1/provenance.schema.json",\n "sha256": "fd5347aaed173391361b0372ef73c0d45547f9abdb4e7a6ca226de53ae24cd89"\n },\n {\n "uri": "takeoff://protocol/v1/review.schema.json",\n "id": "https://opentakeoff.kentucky-ai.com/protocol/v1/review.schema.json",\n "title": "Review (draft)",\n "source": "protocol/v1/review.schema.json",\n "sha256": "3d1f326ff85e3498521e81df0cc2f2758b998be803400a25c21c65014657f245"\n },\n {\n "uri": "takeoff://protocol/v1/stitch.schema.json",\n "id": "https://opentakeoff.kentucky-ai.com/protocol/v1/stitch.schema.json",\n "title": "Composite surface (draft)",\n "source": "protocol/v1/stitch.schema.json",\n "sha256": "db9ea1dffe5fd2360e1d231efc86ba84309316ba9add72c5c0fafef006e76154"\n },\n {\n "uri": "takeoff://protocol/v1/takeoff-document.schema.json",\n "id": "https://opentakeoff.kentucky-ai.com/protocol/v1/takeoff-document.schema.json",\n "title": "TakeoffDocument v1 (draft)",\n "source": "protocol/v1/takeoff-document.schema.json",\n "sha256": "9c7800a9423a12e9334cda507fdd3c216205a4304e220df376dd67753accbd69"\n }\n ]\n}\n';
|
|
15420
16143
|
var PROTOCOL_SCHEMAS = [
|
|
15421
16144
|
{
|
|
15422
16145
|
"uri": "takeoff://protocol/legacy/takeoff-canvas.v1.schema.json",
|
|
@@ -15439,16 +16162,93 @@ var PROTOCOL_SCHEMAS = [
|
|
|
15439
16162
|
"id": "https://opentakeoff.kentucky-ai.com/protocol/v1/common.schema.json",
|
|
15440
16163
|
"title": "Shared value types (draft)",
|
|
15441
16164
|
"source": "protocol/v1/common.schema.json",
|
|
15442
|
-
"sha256": "
|
|
15443
|
-
"schema_json":
|
|
16165
|
+
"sha256": "2c929da4d7db6af6f6d8d20d354bfb11216db6fd6edd04847fb6a0a4405e5539",
|
|
16166
|
+
"schema_json": `{
|
|
16167
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
16168
|
+
"$id": "https://opentakeoff.kentucky-ai.com/protocol/v1/common.schema.json",
|
|
16169
|
+
"title": "Shared value types (draft)",
|
|
16170
|
+
"$defs": {
|
|
16171
|
+
"point": {
|
|
16172
|
+
"type": "array",
|
|
16173
|
+
"prefixItems": [
|
|
16174
|
+
{
|
|
16175
|
+
"type": "number"
|
|
16176
|
+
},
|
|
16177
|
+
{
|
|
16178
|
+
"type": "number"
|
|
16179
|
+
}
|
|
16180
|
+
],
|
|
16181
|
+
"items": false,
|
|
16182
|
+
"minItems": 2,
|
|
16183
|
+
"maxItems": 2,
|
|
16184
|
+
"description": "Two finite coordinates. Stored geometry is normalized to its sheet or stitch; values outside 0..1 are retained, not clamped. Tool input image pixels are a different frame."
|
|
16185
|
+
},
|
|
16186
|
+
"timestamp": {
|
|
16187
|
+
"type": "string",
|
|
16188
|
+
"format": "date-time"
|
|
16189
|
+
},
|
|
16190
|
+
"computed": {
|
|
16191
|
+
"type": "object",
|
|
16192
|
+
"properties": {
|
|
16193
|
+
"area_sf": {
|
|
16194
|
+
"type": "number"
|
|
16195
|
+
},
|
|
16196
|
+
"perimeter_lf": {
|
|
16197
|
+
"type": "number"
|
|
16198
|
+
},
|
|
16199
|
+
"count": {
|
|
16200
|
+
"type": "number"
|
|
16201
|
+
},
|
|
16202
|
+
"plan_lf": {
|
|
16203
|
+
"type": "number",
|
|
16204
|
+
"description": "Linear runs with a vertical leg: the flat X-Y trace alone. perimeter_lf is then plan_lf + vertical_lf."
|
|
16205
|
+
},
|
|
16206
|
+
"vertical_lf": {
|
|
16207
|
+
"type": "number",
|
|
16208
|
+
"description": "Linear runs with a vertical leg: rise + drop in feet."
|
|
16209
|
+
}
|
|
16210
|
+
},
|
|
16211
|
+
"additionalProperties": true,
|
|
16212
|
+
"description": "Stored quantity cache: square feet, linear feet (a linear run's perimeter_lf is its TOTAL incl. any rise + drop), and each. Metric is display conversion. Validation never recomputes values."
|
|
16213
|
+
},
|
|
16214
|
+
"parentSnapshot": {
|
|
16215
|
+
"type": "object",
|
|
16216
|
+
"properties": {
|
|
16217
|
+
"verts_norm": {
|
|
16218
|
+
"type": "array",
|
|
16219
|
+
"items": {
|
|
16220
|
+
"$ref": "common.schema.json#/$defs/point"
|
|
16221
|
+
}
|
|
16222
|
+
},
|
|
16223
|
+
"verts_norm_holes": {
|
|
16224
|
+
"type": "array",
|
|
16225
|
+
"items": {
|
|
16226
|
+
"type": "array",
|
|
16227
|
+
"items": {
|
|
16228
|
+
"$ref": "common.schema.json#/$defs/point"
|
|
16229
|
+
}
|
|
16230
|
+
}
|
|
16231
|
+
},
|
|
16232
|
+
"computed": {
|
|
16233
|
+
"$ref": "#/$defs/computed"
|
|
16234
|
+
}
|
|
16235
|
+
},
|
|
16236
|
+
"required": [
|
|
16237
|
+
"verts_norm"
|
|
16238
|
+
],
|
|
16239
|
+
"additionalProperties": true
|
|
16240
|
+
}
|
|
16241
|
+
}
|
|
16242
|
+
}
|
|
16243
|
+
`
|
|
15444
16244
|
},
|
|
15445
16245
|
{
|
|
15446
16246
|
"uri": "takeoff://protocol/v1/condition.schema.json",
|
|
15447
16247
|
"id": "https://opentakeoff.kentucky-ai.com/protocol/v1/condition.schema.json",
|
|
15448
16248
|
"title": "Condition (draft)",
|
|
15449
16249
|
"source": "protocol/v1/condition.schema.json",
|
|
15450
|
-
"sha256": "
|
|
15451
|
-
"schema_json": '{\n "$schema": "https://json-schema.org/draft/2020-12/schema",\n "$id": "https://opentakeoff.kentucky-ai.com/protocol/v1/condition.schema.json",\n "title": "Condition (draft)",\n "type": "object",\n "properties": {\n "id": {\n "type": "string",\n "minLength": 1\n },\n "finish_tag": {\n "type": "string"\n },\n "color": {\n "type": "string"\n },\n "fill": {\n "type": "string"\n },\n "hatch": {\n "type": "string"\n },\n "multiplier": {\n "type": "number"\n },\n "waste_pct": {\n "type": "number"\n },\n "height_ft": {\n "type": "number"\n },\n "thickness_in": {\n "type": "number"\n },\n "materials": {\n "type": "array",\n "items": {\n "type": "object",\n "properties": {\n "id": {\n "type": "string"\n },\n "name": {\n "type": "string"\n },\n "per": {\n "type": "number"\n },\n "basis": {\n "type": "string"\n },\n "unit": {\n "type": "string"\n },\n "round": {\n "type": "boolean"\n },\n "note": {\n "type": "string"\n },\n "lib_id": {\n "type": "string"\n },\n "origin_id": {\n "type": "string"\n },\n "inherited": {\n "type": "boolean"\n }\n },\n "additionalProperties": true\n }\n },\n "roll_setup": {\n "type": [\n "object",\n "null"\n ]\n },\n "tile_setup": {\n "type": [\n "object",\n "null"\n ]\n },\n "created_at": {\n "$ref": "common.schema.json#/$defs/timestamp"\n },\n "updated_at": {\n "$ref": "common.schema.json#/$defs/timestamp"\n }\n },\n "required": [\n "id"\n ],\n "additionalProperties": true,\n "description": "Existing condition configuration, not a computed report or price. Additional setup, variant, style, and material fields remain opaque and preserved in this draft."\n}\n'
|
|
16250
|
+
"sha256": "c45284ba2c390bb0b0be9208f1b51fd37d969184956aa73b1b1e5276109aca86",
|
|
16251
|
+
"schema_json": '{\n "$schema": "https://json-schema.org/draft/2020-12/schema",\n "$id": "https://opentakeoff.kentucky-ai.com/protocol/v1/condition.schema.json",\n "title": "Condition (draft)",\n "type": "object",\n "properties": {\n "id": {\n "type": "string",\n "minLength": 1\n },\n "finish_tag": {\n "type": "string"\n },\n "color": {\n "type": "string"\n },\n "fill": {\n "type": "string"\n },\n "hatch": {\n "type": "string"\n },\n "multiplier": {\n "type": "number"\n },\n "waste_pct": {\n "type": "number"\n },\n "height_ft": {\n "type": "number"\n },\n "thickness_in": {\n "type": "number"\n },\n "rise_ft": {\n "type": "number",\n "minimum": 0,\n "description": "Default vertical leg UP, in feet, every linear run of this condition adds to its plan length (LF = plan + rise + drop). A run may override it with its own rise_ft."\n },\n "drop_ft": {\n "type": "number",\n "minimum": 0,\n "description": "Default vertical leg DOWN, in feet, every linear run of this condition adds to its plan length. A run may override it with its own drop_ft."\n },\n "materials": {\n "type": "array",\n "items": {\n "type": "object",\n "properties": {\n "id": {\n "type": "string"\n },\n "name": {\n "type": "string"\n },\n "per": {\n "type": "number"\n },\n "basis": {\n "type": "string"\n },\n "unit": {\n "type": "string"\n },\n "round": {\n "type": "boolean"\n },\n "note": {\n "type": "string"\n },\n "lib_id": {\n "type": "string"\n },\n "origin_id": {\n "type": "string"\n },\n "inherited": {\n "type": "boolean"\n }\n },\n "additionalProperties": true\n }\n },\n "roll_setup": {\n "type": [\n "object",\n "null"\n ]\n },\n "tile_setup": {\n "type": [\n "object",\n "null"\n ]\n },\n "created_at": {\n "$ref": "common.schema.json#/$defs/timestamp"\n },\n "updated_at": {\n "$ref": "common.schema.json#/$defs/timestamp"\n }\n },\n "required": [\n "id"\n ],\n "additionalProperties": true,\n "description": "Existing condition configuration, not a computed report or price. Additional setup, variant, style, and material fields remain opaque and preserved in this draft."\n}\n'
|
|
15452
16252
|
},
|
|
15453
16253
|
{
|
|
15454
16254
|
"uri": "takeoff://protocol/v1/document-fields.schema.json",
|
|
@@ -15471,8 +16271,104 @@ var PROTOCOL_SCHEMAS = [
|
|
|
15471
16271
|
"id": "https://opentakeoff.kentucky-ai.com/protocol/v1/measurement.schema.json",
|
|
15472
16272
|
"title": "Measurement (draft)",
|
|
15473
16273
|
"source": "protocol/v1/measurement.schema.json",
|
|
15474
|
-
"sha256": "
|
|
15475
|
-
"schema_json":
|
|
16274
|
+
"sha256": "0e1b050d38963a1256f3647922ca90031eb04998d78e0f53bb3fb577bf590648",
|
|
16275
|
+
"schema_json": `{
|
|
16276
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
16277
|
+
"$id": "https://opentakeoff.kentucky-ai.com/protocol/v1/measurement.schema.json",
|
|
16278
|
+
"title": "Measurement (draft)",
|
|
16279
|
+
"type": "object",
|
|
16280
|
+
"properties": {
|
|
16281
|
+
"id": {
|
|
16282
|
+
"type": "string",
|
|
16283
|
+
"minLength": 1
|
|
16284
|
+
},
|
|
16285
|
+
"sheet_id": {
|
|
16286
|
+
"type": "string",
|
|
16287
|
+
"minLength": 1
|
|
16288
|
+
},
|
|
16289
|
+
"condition_id": {
|
|
16290
|
+
"type": "string",
|
|
16291
|
+
"minLength": 1
|
|
16292
|
+
},
|
|
16293
|
+
"measure_role": {
|
|
16294
|
+
"enum": [
|
|
16295
|
+
"floor_area",
|
|
16296
|
+
"deduct",
|
|
16297
|
+
"linear",
|
|
16298
|
+
"surface_area",
|
|
16299
|
+
"count"
|
|
16300
|
+
]
|
|
16301
|
+
},
|
|
16302
|
+
"verts_norm": {
|
|
16303
|
+
"type": "array",
|
|
16304
|
+
"items": {
|
|
16305
|
+
"$ref": "common.schema.json#/$defs/point"
|
|
16306
|
+
},
|
|
16307
|
+
"minItems": 1
|
|
16308
|
+
},
|
|
16309
|
+
"verts_norm_holes": {
|
|
16310
|
+
"type": "array",
|
|
16311
|
+
"items": {
|
|
16312
|
+
"type": "array",
|
|
16313
|
+
"items": {
|
|
16314
|
+
"$ref": "common.schema.json#/$defs/point"
|
|
16315
|
+
}
|
|
16316
|
+
}
|
|
16317
|
+
},
|
|
16318
|
+
"computed": {
|
|
16319
|
+
"$ref": "common.schema.json#/$defs/computed"
|
|
16320
|
+
},
|
|
16321
|
+
"height_ft": {
|
|
16322
|
+
"type": "number"
|
|
16323
|
+
},
|
|
16324
|
+
"rise_ft": {
|
|
16325
|
+
"type": "number",
|
|
16326
|
+
"minimum": 0,
|
|
16327
|
+
"description": "Linear runs only: this run's own vertical leg UP in feet, overriding the condition's rise_ft (0 included). Absent means the condition default applies."
|
|
16328
|
+
},
|
|
16329
|
+
"drop_ft": {
|
|
16330
|
+
"type": "number",
|
|
16331
|
+
"minimum": 0,
|
|
16332
|
+
"description": "Linear runs only: this run's own vertical leg DOWN in feet, overriding the condition's drop_ft (0 included). Absent means the condition default applies."
|
|
16333
|
+
},
|
|
16334
|
+
"label": {
|
|
16335
|
+
"type": "string"
|
|
16336
|
+
},
|
|
16337
|
+
"curved": {
|
|
16338
|
+
"type": "boolean"
|
|
16339
|
+
},
|
|
16340
|
+
"cuts_shape_id": {
|
|
16341
|
+
"type": "string",
|
|
16342
|
+
"minLength": 1
|
|
16343
|
+
},
|
|
16344
|
+
"origin": {
|
|
16345
|
+
"$ref": "provenance.schema.json"
|
|
16346
|
+
},
|
|
16347
|
+
"created_at": {
|
|
16348
|
+
"$ref": "common.schema.json#/$defs/timestamp"
|
|
16349
|
+
},
|
|
16350
|
+
"updated_at": {
|
|
16351
|
+
"$ref": "common.schema.json#/$defs/timestamp"
|
|
16352
|
+
},
|
|
16353
|
+
"author": {
|
|
16354
|
+
"type": "string"
|
|
16355
|
+
},
|
|
16356
|
+
"updated_by": {
|
|
16357
|
+
"type": "string"
|
|
16358
|
+
}
|
|
16359
|
+
},
|
|
16360
|
+
"required": [
|
|
16361
|
+
"id",
|
|
16362
|
+
"sheet_id",
|
|
16363
|
+
"condition_id",
|
|
16364
|
+
"measure_role",
|
|
16365
|
+
"verts_norm",
|
|
16366
|
+
"computed"
|
|
16367
|
+
],
|
|
16368
|
+
"additionalProperties": true,
|
|
16369
|
+
"description": "One existing shape record. Legacy curved=true means spline control points; new baked arcs use ordinary vertices and origin.curved. Do not conflate or regenerate them."
|
|
16370
|
+
}
|
|
16371
|
+
`
|
|
15476
16372
|
},
|
|
15477
16373
|
{
|
|
15478
16374
|
"uri": "takeoff://protocol/v1/provenance.schema.json",
|
|
@@ -15777,7 +16673,7 @@ function nameTheStageInRefusals(server) {
|
|
|
15777
16673
|
// package.json
|
|
15778
16674
|
var package_default = {
|
|
15779
16675
|
name: "opentakeoff-mcp",
|
|
15780
|
-
version: "0.9.
|
|
16676
|
+
version: "0.9.93",
|
|
15781
16677
|
mcpName: "io.github.Kentucky-ai/opentakeoff",
|
|
15782
16678
|
type: "module",
|
|
15783
16679
|
description: "OpenTakeoff MCP server \u2014 drive the takeoff engine from your MCP client over stdio.",
|
|
@@ -15797,7 +16693,7 @@ var package_default = {
|
|
|
15797
16693
|
"check:wiki": "node scripts/check-wiki.mjs",
|
|
15798
16694
|
"check:versions": "node ../scripts/check-version-consistency.mjs",
|
|
15799
16695
|
"check:tool-count": "node --import tsx scripts/check-tool-count.mjs",
|
|
15800
|
-
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/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"
|
|
15801
16697
|
},
|
|
15802
16698
|
dependencies: {
|
|
15803
16699
|
"@modelcontextprotocol/sdk": "^1.12.0",
|