opentakeoff-mcp 0.9.86 → 0.9.90
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 +878 -102
- 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
|
}
|
|
@@ -6096,7 +6219,82 @@ function labelPlacements(placements, spans, segs, lum) {
|
|
|
6096
6219
|
return out;
|
|
6097
6220
|
}
|
|
6098
6221
|
|
|
6222
|
+
// ../web/src/lib/arc.js
|
|
6223
|
+
var TAU = Math.PI * 2;
|
|
6224
|
+
var norm2 = (a) => (a % TAU + TAU) % TAU;
|
|
6225
|
+
function circleThrough(a, m, b) {
|
|
6226
|
+
const [ax, ay] = a, [mx, my] = m, [bx, by] = b;
|
|
6227
|
+
const d = 2 * (ax * (my - by) + mx * (by - ay) + bx * (ay - my));
|
|
6228
|
+
if (!Number.isFinite(d) || Math.abs(d) < 1e-9) return null;
|
|
6229
|
+
const a2 = ax * ax + ay * ay, m2 = mx * mx + my * my, b2 = bx * bx + by * by;
|
|
6230
|
+
const cx = (a2 * (my - by) + m2 * (by - ay) + b2 * (ay - my)) / d;
|
|
6231
|
+
const cy = (a2 * (bx - mx) + m2 * (ax - bx) + b2 * (mx - ax)) / d;
|
|
6232
|
+
const r = Math.hypot(ax - cx, ay - cy);
|
|
6233
|
+
if (!Number.isFinite(r) || r > 1e7) return null;
|
|
6234
|
+
const a0 = Math.atan2(ay - cy, ax - cx);
|
|
6235
|
+
const a1 = Math.atan2(by - cy, bx - cx);
|
|
6236
|
+
const am = Math.atan2(my - cy, mx - cx);
|
|
6237
|
+
const fwd = norm2(a1 - a0);
|
|
6238
|
+
const fwdM = norm2(am - a0);
|
|
6239
|
+
const sweep = fwdM <= fwd ? fwd : fwd - TAU;
|
|
6240
|
+
return { cx, cy, r, a0, a1, am, sweep };
|
|
6241
|
+
}
|
|
6242
|
+
function stepsFor(r, sweep) {
|
|
6243
|
+
const len = Math.abs(sweep) * r;
|
|
6244
|
+
const bySpan = Math.abs(sweep) / (4 * Math.PI / 180);
|
|
6245
|
+
return Math.max(8, Math.min(96, Math.ceil(Math.max(len / 4, bySpan))));
|
|
6246
|
+
}
|
|
6247
|
+
function flattenArc(a, m, b) {
|
|
6248
|
+
const c = circleThrough(a, m, b);
|
|
6249
|
+
if (!c) return [];
|
|
6250
|
+
const total = stepsFor(c.r, c.sweep);
|
|
6251
|
+
const toM = c.sweep >= 0 ? norm2(c.am - c.a0) : -norm2(c.a0 - c.am);
|
|
6252
|
+
const f = Math.abs(c.sweep) > 1e-12 ? Math.min(1, Math.abs(toM) / Math.abs(c.sweep)) : 0;
|
|
6253
|
+
const n1 = Math.max(1, Math.min(total - 1, Math.round(total * f)));
|
|
6254
|
+
const n2 = Math.max(1, total - n1);
|
|
6255
|
+
const at = (t) => [c.cx + c.r * Math.cos(t), c.cy + c.r * Math.sin(t)];
|
|
6256
|
+
const out = [];
|
|
6257
|
+
for (let i = 1; i < n1; i++) out.push(at(c.a0 + toM * (i / n1)));
|
|
6258
|
+
out.push([m[0], m[1]]);
|
|
6259
|
+
for (let i = 1; i < n2; i++) out.push(at(c.am + (c.sweep - toM) * (i / n2)));
|
|
6260
|
+
return out;
|
|
6261
|
+
}
|
|
6262
|
+
function flattenArcRing(pts, throughAt, closed2 = true) {
|
|
6263
|
+
const src = pts || [];
|
|
6264
|
+
const n = src.length;
|
|
6265
|
+
const copy = () => src.map((p) => [p[0], p[1]]);
|
|
6266
|
+
const marks = throughAt instanceof Set ? throughAt : new Set(throughAt || []);
|
|
6267
|
+
if (n < 3 || !marks.size) return copy();
|
|
6268
|
+
const live = new Array(n).fill(false);
|
|
6269
|
+
for (let i = 0; i < n; i++) {
|
|
6270
|
+
if (!marks.has(i)) continue;
|
|
6271
|
+
const hasPrev = closed2 || i > 0;
|
|
6272
|
+
const hasNext = closed2 || i < n - 1;
|
|
6273
|
+
if (!hasPrev || !hasNext) continue;
|
|
6274
|
+
const pi = (i - 1 + n) % n, ni = (i + 1) % n;
|
|
6275
|
+
if (marks.has(pi) || marks.has(ni)) continue;
|
|
6276
|
+
live[i] = true;
|
|
6277
|
+
}
|
|
6278
|
+
const out = [];
|
|
6279
|
+
for (let i = 0; i < n; i++) {
|
|
6280
|
+
if (!live[i]) {
|
|
6281
|
+
out.push([src[i][0], src[i][1]]);
|
|
6282
|
+
continue;
|
|
6283
|
+
}
|
|
6284
|
+
for (const p of flattenArc(src[(i - 1 + n) % n], src[i], src[(i + 1) % n])) out.push(p);
|
|
6285
|
+
}
|
|
6286
|
+
return out;
|
|
6287
|
+
}
|
|
6288
|
+
|
|
6099
6289
|
// ../web/src/lib/shapeMetrics.js
|
|
6290
|
+
function linearVerticalFt(s, cond) {
|
|
6291
|
+
if (s?.origin?.derived) return { rise: 0, drop: 0 };
|
|
6292
|
+
const pick = (own, dflt) => {
|
|
6293
|
+
const v = own != null && own !== "" ? Number(own) : Number(dflt);
|
|
6294
|
+
return Number.isFinite(v) && v > 0 ? v : 0;
|
|
6295
|
+
};
|
|
6296
|
+
return { rise: pick(s?.rise_ft, cond?.rise_ft), drop: pick(s?.drop_ft, cond?.drop_ft) };
|
|
6297
|
+
}
|
|
6100
6298
|
function computeShapeMetrics(s, dims, upp, cond) {
|
|
6101
6299
|
const pts = (s.verts_norm || []).map(([nx, ny]) => [nx * dims.w, ny * dims.h]);
|
|
6102
6300
|
const u = upp || 0;
|
|
@@ -6107,9 +6305,16 @@ function computeShapeMetrics(s, dims, upp, cond) {
|
|
|
6107
6305
|
return { area_sf: +(LF * h).toFixed(2), perimeter_lf: +LF.toFixed(2) };
|
|
6108
6306
|
}
|
|
6109
6307
|
if (s.measure_role === "linear") {
|
|
6110
|
-
const
|
|
6308
|
+
const plan = openLen(s.curved ? flattenCurve(pts) : pts) * u;
|
|
6309
|
+
const { rise, drop } = linearVerticalFt(s, cond);
|
|
6310
|
+
const vert = rise + drop;
|
|
6311
|
+
const LF = plan + vert;
|
|
6111
6312
|
const tIn = Number(cond?.thickness_in) || 0;
|
|
6112
|
-
return {
|
|
6313
|
+
return {
|
|
6314
|
+
perimeter_lf: +LF.toFixed(2),
|
|
6315
|
+
area_sf: tIn > 0 ? +(LF * tIn / 12).toFixed(2) : 0,
|
|
6316
|
+
...vert > 0 ? { plan_lf: +plan.toFixed(2), vertical_lf: +vert.toFixed(2) } : {}
|
|
6317
|
+
};
|
|
6113
6318
|
}
|
|
6114
6319
|
const holesPx = (s.verts_norm_holes || []).map((ring) => ring.map(([nx, ny]) => [nx * dims.w, ny * dims.h]));
|
|
6115
6320
|
const met = holesPx.length ? polyWithHolesMetrics(pts, holesPx) : closedMetrics(pts);
|
|
@@ -6236,25 +6441,25 @@ function sharedRuns(ringA, ringB, opts) {
|
|
|
6236
6441
|
for (const r of raw) {
|
|
6237
6442
|
const idx = [];
|
|
6238
6443
|
for (let i = r.from; i <= r.to; i++) idx.push(i % samples.length);
|
|
6239
|
-
const
|
|
6444
|
+
const path6 = idx.map((i) => samples[i]);
|
|
6240
6445
|
if (idx.length < samples.length) {
|
|
6241
6446
|
const before = samples[((r.from - 1) % samples.length + samples.length) % samples.length];
|
|
6242
6447
|
const after = samples[(r.to + 1) % samples.length];
|
|
6243
|
-
const head2 = refineEnd(
|
|
6244
|
-
const tail = refineEnd(
|
|
6245
|
-
if (Math.hypot(head2[0] -
|
|
6246
|
-
if (Math.hypot(tail[0] -
|
|
6448
|
+
const head2 = refineEnd(path6[0], before);
|
|
6449
|
+
const tail = refineEnd(path6[path6.length - 1], after);
|
|
6450
|
+
if (Math.hypot(head2[0] - path6[0][0], head2[1] - path6[0][1]) > 1e-6) path6.unshift(head2);
|
|
6451
|
+
if (Math.hypot(tail[0] - path6[path6.length - 1][0], tail[1] - path6[path6.length - 1][1]) > 1e-6) path6.push(tail);
|
|
6247
6452
|
}
|
|
6248
6453
|
let length_px = 0;
|
|
6249
|
-
for (let i = 1; i <
|
|
6250
|
-
length_px += Math.hypot(
|
|
6454
|
+
for (let i = 1; i < path6.length; i++) {
|
|
6455
|
+
length_px += Math.hypot(path6[i][0] - path6[i - 1][0], path6[i][1] - path6[i - 1][1]);
|
|
6251
6456
|
}
|
|
6252
6457
|
if (length_px < min_len_px) continue;
|
|
6253
6458
|
const gap_px = median(idx.map((i) => dists[i]));
|
|
6254
|
-
const mid =
|
|
6459
|
+
const mid = path6[path6.length >> 1];
|
|
6255
6460
|
runs.push({
|
|
6256
6461
|
kind: gap_px <= touch_px ? "butt" : "wall",
|
|
6257
|
-
path:
|
|
6462
|
+
path: path6,
|
|
6258
6463
|
length_px,
|
|
6259
6464
|
gap_px,
|
|
6260
6465
|
at: [mid[0], mid[1]]
|
|
@@ -6265,18 +6470,18 @@ function sharedRuns(ringA, ringB, opts) {
|
|
|
6265
6470
|
var TRANSITION_DEFAULTS = { max_gap_in: 12, min_run_in: 12 };
|
|
6266
6471
|
var round22 = (n) => +n.toFixed(2);
|
|
6267
6472
|
var round12 = (n) => +n.toFixed(1);
|
|
6268
|
-
function dropCollinear(
|
|
6269
|
-
if (
|
|
6270
|
-
const out = [[
|
|
6271
|
-
for (let i = 1; i <
|
|
6272
|
-
const a = out[out.length - 1], b =
|
|
6473
|
+
function dropCollinear(path6, tol = 1e-6) {
|
|
6474
|
+
if (path6.length < 3) return path6.map((p) => [p[0], p[1]]);
|
|
6475
|
+
const out = [[path6[0][0], path6[0][1]]];
|
|
6476
|
+
for (let i = 1; i < path6.length - 1; i++) {
|
|
6477
|
+
const a = out[out.length - 1], b = path6[i], c = path6[i + 1];
|
|
6273
6478
|
const abx = b[0] - a[0], aby = b[1] - a[1];
|
|
6274
6479
|
const acx = c[0] - a[0], acy = c[1] - a[1];
|
|
6275
6480
|
const len = Math.hypot(acx, acy);
|
|
6276
6481
|
const off = len > 0 ? Math.abs(abx * acy - aby * acx) / len : Infinity;
|
|
6277
6482
|
if (off > tol) out.push([b[0], b[1]]);
|
|
6278
6483
|
}
|
|
6279
|
-
out.push([
|
|
6484
|
+
out.push([path6[path6.length - 1][0], path6[path6.length - 1][1]]);
|
|
6280
6485
|
return out;
|
|
6281
6486
|
}
|
|
6282
6487
|
function deriveTransitionRuns(a, b, sheets, opts = {}) {
|
|
@@ -8840,24 +9045,66 @@ var Session = class _Session {
|
|
|
8840
9045
|
...s.upp == null ? { warning: `No scale set for ${s.key} \u2014 quantities unavailable. Call set_scale${s.detected ? ` (detected: ${s.detected.label})` : ""}.` } : {}
|
|
8841
9046
|
};
|
|
8842
9047
|
}
|
|
9048
|
+
/** Bend a trace the way the canvas's Curve mode does (#284): `arcThrough`
|
|
9049
|
+
* lists the indices of points that are the MIDDLE of a three-point arc —
|
|
9050
|
+
* the boundary runs pts[i-1] → pts[i] → pts[i+1] as the unique circle
|
|
9051
|
+
* through those three instead of two chords. The arc is baked to ordinary
|
|
9052
|
+
* vertices here (the canvas's flattenArcRing, same steps, same budget), so
|
|
9053
|
+
* SF/LF, the marked set, edit_shape and every export keep seeing a plain
|
|
9054
|
+
* polygon — and the shape's origin carries `curved: true`, the canvas's own
|
|
9055
|
+
* stamp. Refusal over guessing: an index off the trace, a bow point at the
|
|
9056
|
+
* end of an open run (no far corner to bend to), or two bows in a row (no
|
|
9057
|
+
* clean triple) refuse whole rather than silently demoting to a corner. */
|
|
9058
|
+
bend(pts, arcThrough, closed2, tool) {
|
|
9059
|
+
if (!arcThrough?.length) return { pts, arcs: 0 };
|
|
9060
|
+
const n = pts.length;
|
|
9061
|
+
const marks = [...new Set(arcThrough)].sort((a, b) => a - b);
|
|
9062
|
+
for (const i of marks) {
|
|
9063
|
+
if (!Number.isInteger(i) || i < 0 || i >= n) throw new UserError(`${tool}: arc_through index ${i} is off the trace (${n} points, indices 0\u2013${n - 1}).`);
|
|
9064
|
+
if (!closed2 && (i === 0 || i === n - 1)) throw new UserError(`${tool}: arc_through ${i} is an END of the open run \u2014 a bow needs a corner on both sides. State the arc's start, its bow, and its far end as three consecutive points and mark the middle one.`);
|
|
9065
|
+
}
|
|
9066
|
+
for (let k = 1; k < marks.length; k++) {
|
|
9067
|
+
if (marks[k] - marks[k - 1] === 1 || closed2 && marks[0] === 0 && marks[marks.length - 1] === n - 1) {
|
|
9068
|
+
throw new UserError(`${tool}: arc_through marks ${marks[k - 1]} and ${marks[k]} are adjacent \u2014 every arc needs a corner between it and the next. Put a plain vertex where one arc ends and the next begins.`);
|
|
9069
|
+
}
|
|
9070
|
+
}
|
|
9071
|
+
if (n < 3) throw new UserError(`${tool}: an arc needs three points (start, bow, far end); got ${n}.`);
|
|
9072
|
+
const flat = flattenArcRing(pts, marks, closed2);
|
|
9073
|
+
return { pts: flat, arcs: marks.length };
|
|
9074
|
+
}
|
|
8843
9075
|
measurePolygon(name, verts, opts) {
|
|
8844
9076
|
const s = this.sheet(name);
|
|
8845
9077
|
if (s.upp == null) throw new UserError(this.scaleGate(s));
|
|
8846
|
-
const {
|
|
9078
|
+
const { pts: ring, arcs } = this.bend(verts, opts.arc_through, true, "measure_polygon");
|
|
9079
|
+
const { area_sf = 0, perimeter_lf = 0 } = this.quantify(s, opts.role, ring);
|
|
8847
9080
|
let shape_id;
|
|
8848
|
-
if (opts.condition) shape_id = this.commit(s, opts.condition, opts.role,
|
|
9081
|
+
if (opts.condition) shape_id = this.commit(s, opts.condition, opts.role, ring, { area_sf, perimeter_lf }, { method: "manual", actor: "agent", ...arcs ? { curved: true } : {} }).id;
|
|
8849
9082
|
this.flushCommits("measure_polygon");
|
|
8850
|
-
const mixed = this.scaleWarningFor(s,
|
|
8851
|
-
return { area_sf, perimeter_lf, nverts:
|
|
9083
|
+
const mixed = this.scaleWarningFor(s, ring);
|
|
9084
|
+
return { area_sf, perimeter_lf, nverts: ring.length, ...arcs ? { arcs } : {}, ...shape_id ? { shape_id } : {}, ...mixed ? { warning: mixed } : {} };
|
|
8852
9085
|
}
|
|
8853
9086
|
measureLine(name, pts, opts) {
|
|
8854
9087
|
const s = this.sheet(name);
|
|
8855
9088
|
if (s.upp == null) throw new UserError(this.scaleGate(s));
|
|
8856
|
-
const {
|
|
9089
|
+
const { pts: run2, arcs } = this.bend(pts, opts.arc_through, false, "measure_line");
|
|
9090
|
+
const own = { ...opts.rise_ft !== void 0 ? { rise_ft: opts.rise_ft } : {}, ...opts.drop_ft !== void 0 ? { drop_ft: opts.drop_ft } : {} };
|
|
9091
|
+
const vertical = this.verticalFor(opts.condition, own);
|
|
9092
|
+
const computed = this.quantify(s, "linear", run2, void 0, vertical);
|
|
9093
|
+
const length_lf = computed.perimeter_lf ?? 0;
|
|
8857
9094
|
let shape_id;
|
|
8858
|
-
if (opts.condition)
|
|
9095
|
+
if (opts.condition) {
|
|
9096
|
+
const shape = this.commit(s, opts.condition, "linear", run2, computed, { method: "manual", actor: "agent", ...arcs ? { curved: true } : {} });
|
|
9097
|
+
Object.assign(shape, own);
|
|
9098
|
+
shape_id = shape.id;
|
|
9099
|
+
}
|
|
8859
9100
|
this.flushCommits("measure_line");
|
|
8860
|
-
return {
|
|
9101
|
+
return {
|
|
9102
|
+
length_lf,
|
|
9103
|
+
npts: run2.length,
|
|
9104
|
+
...computed.vertical_lf ? { plan_lf: computed.plan_lf, vertical_lf: computed.vertical_lf, rise_ft: vertical.rise, drop_ft: vertical.drop } : {},
|
|
9105
|
+
...arcs ? { arcs } : {},
|
|
9106
|
+
...shape_id ? { shape_id } : {}
|
|
9107
|
+
};
|
|
8861
9108
|
}
|
|
8862
9109
|
/** Surface Area — the canvas's Surface tool (commitSurface): an OPEN run
|
|
8863
9110
|
* traced along the wall in plan view, quantified as traced LF × height.
|
|
@@ -8868,6 +9115,8 @@ var Session = class _Session {
|
|
|
8868
9115
|
measureSurface(name, pts, opts) {
|
|
8869
9116
|
const s = this.sheet(name);
|
|
8870
9117
|
if (s.upp == null) throw new UserError(this.scaleGate(s));
|
|
9118
|
+
const bent = this.bend(pts, opts.arc_through, false, "measure_surface");
|
|
9119
|
+
pts = bent.pts;
|
|
8871
9120
|
const existing = this.conditions.find((x) => x.finish_tag === opts.condition);
|
|
8872
9121
|
const h = opts.height_ft ?? (Number(existing?.height_ft) || 0);
|
|
8873
9122
|
if (!(h > 0)) {
|
|
@@ -8881,8 +9130,9 @@ var Session = class _Session {
|
|
|
8881
9130
|
const LF = openLen(pts) * s.upp;
|
|
8882
9131
|
const shape = this.commit(s, opts.condition, "surface_area", pts, { area_sf: round2(LF * h), perimeter_lf: round2(LF) }, { method: "manual", actor: "agent" });
|
|
8883
9132
|
shape.height_ft = h;
|
|
9133
|
+
if (bent.arcs && shape.origin) shape.origin = { ...shape.origin, curved: true };
|
|
8884
9134
|
this.flushCommits("measure_surface");
|
|
8885
|
-
return { condition: c.finish_tag, height_ft: h, length_lf: round2(LF), area_sf: round2(LF * h), npts: pts.length, shape_id: shape.id };
|
|
9135
|
+
return { condition: c.finish_tag, height_ft: h, length_lf: round2(LF), area_sf: round2(LF * h), npts: pts.length, ...bent.arcs ? { arcs: bent.arcs } : {}, shape_id: shape.id };
|
|
8886
9136
|
}
|
|
8887
9137
|
// ── Proposals (#365) ──────────────────────────────────────────────────────
|
|
8888
9138
|
// A shape an agent commits lands reviewed:false and the estimator accepts
|
|
@@ -8905,14 +9155,31 @@ var Session = class _Session {
|
|
|
8905
9155
|
}
|
|
8906
9156
|
return p;
|
|
8907
9157
|
}
|
|
9158
|
+
/** A linear run's quantities from its plan length and vertical legs (#441)
|
|
9159
|
+
* — the server twin of shapeMetrics' linear branch: perimeter_lf is the
|
|
9160
|
+
* TOTAL the summers read; plan_lf / vertical_lf appear only when a leg
|
|
9161
|
+
* exists, so a flat run's record is what it always was. area_sf stays 0 —
|
|
9162
|
+
* the canvas mints border SF only from a condition thickness. */
|
|
9163
|
+
static linearQty(planLf, vertical) {
|
|
9164
|
+
const vert = (vertical?.rise ?? 0) + (vertical?.drop ?? 0);
|
|
9165
|
+
const plan = round2(planLf);
|
|
9166
|
+
return { area_sf: 0, perimeter_lf: round2(planLf + vert), ...vert > 0 ? { plan_lf: plan, vertical_lf: round2(vert) } : {} };
|
|
9167
|
+
}
|
|
9168
|
+
/** The legs a linear run resolves to: its own rise_ft / drop_ft where
|
|
9169
|
+
* given, else the condition's defaults (by tag — the condition may not
|
|
9170
|
+
* exist yet on a first commit, in which case there are no defaults). */
|
|
9171
|
+
verticalFor(tag, own) {
|
|
9172
|
+
const cond = tag ? this.conditions.find((x) => _Session.tagKey(x.finish_tag) === _Session.tagKey(tag)) : void 0;
|
|
9173
|
+
return linearVerticalFt(own, cond);
|
|
9174
|
+
}
|
|
8908
9175
|
/** The quantities a shape of `role` carries for `verts` on sheet `s` — the
|
|
8909
9176
|
* same arithmetic measure_polygon / measure_line / measure_surface /
|
|
8910
9177
|
* place_count run, in one place so a revised batch measures exactly as a
|
|
8911
9178
|
* fresh commit would. */
|
|
8912
|
-
quantify(s, role, verts, heightFt) {
|
|
9179
|
+
quantify(s, role, verts, heightFt, vertical) {
|
|
8913
9180
|
if (role === "count") return { count: 1 };
|
|
8914
9181
|
const upp = s.upp ?? 0;
|
|
8915
|
-
if (role === "linear") return
|
|
9182
|
+
if (role === "linear") return _Session.linearQty(openLen(verts) * upp, vertical);
|
|
8916
9183
|
if (role === "surface_area") {
|
|
8917
9184
|
const LF = openLen(verts) * upp;
|
|
8918
9185
|
return { area_sf: round2(LF * (heightFt ?? 0)), perimeter_lf: round2(LF) };
|
|
@@ -9046,6 +9313,8 @@ var Session = class _Session {
|
|
|
9046
9313
|
if (proposed.waste_pct !== void 0 && proposed.waste_pct !== c.waste_pct) diff.waste_pct = proposed.waste_pct;
|
|
9047
9314
|
if (proposed.multiplier !== void 0 && proposed.multiplier !== c.multiplier) diff.multiplier = proposed.multiplier;
|
|
9048
9315
|
if (proposed.height_ft !== void 0 && proposed.height_ft !== c.height_ft) diff.height_ft = proposed.height_ft;
|
|
9316
|
+
if (proposed.rise_ft !== void 0 && proposed.rise_ft !== (c.rise_ft ?? 0)) diff.rise_ft = proposed.rise_ft;
|
|
9317
|
+
if (proposed.drop_ft !== void 0 && proposed.drop_ft !== (c.drop_ft ?? 0)) diff.drop_ft = proposed.drop_ft;
|
|
9049
9318
|
if (proposed.roll_setup !== void 0) {
|
|
9050
9319
|
if (proposed.roll_setup === null) {
|
|
9051
9320
|
if (c.roll_setup) diff.roll_setup = null;
|
|
@@ -9100,6 +9369,7 @@ var Session = class _Session {
|
|
|
9100
9369
|
const before = structuredClone(c);
|
|
9101
9370
|
const { finish_tag, ...knobs } = proposal.proposed;
|
|
9102
9371
|
this.applyConditionKnobs(c, knobs);
|
|
9372
|
+
if (knobs.rise_ft !== void 0 || knobs.drop_ft !== void 0) this.reflowLinears(c);
|
|
9103
9373
|
if (finish_tag !== void 0) c.finish_tag = finish_tag;
|
|
9104
9374
|
this.conditionEditProposals.splice(i, 1);
|
|
9105
9375
|
this.record({ op: "condition_proposal_accept", tool: "accept_condition_edit", proposal, index: i, before });
|
|
@@ -9111,9 +9381,30 @@ var Session = class _Session {
|
|
|
9111
9381
|
waste_pct: c.waste_pct,
|
|
9112
9382
|
multiplier: c.multiplier,
|
|
9113
9383
|
...c.height_ft !== void 0 ? { height_ft: c.height_ft } : {},
|
|
9384
|
+
...c.rise_ft !== void 0 ? { rise_ft: c.rise_ft } : {},
|
|
9385
|
+
...c.drop_ft !== void 0 ? { drop_ft: c.drop_ft } : {},
|
|
9114
9386
|
...c.roll_setup ? { roll_setup: c.roll_setup } : {}
|
|
9115
9387
|
};
|
|
9116
9388
|
}
|
|
9389
|
+
/** Re-price every linear run of `c` from its geometry and the legs it now
|
|
9390
|
+
* resolves to (#441) — the canvas's setCondParam re-flow, on the server. A
|
|
9391
|
+
* run carrying its own rise_ft / drop_ft keeps that field; a derived run
|
|
9392
|
+
* never takes a leg. Nothing is journaled here: the condition step that
|
|
9393
|
+
* called it is the undo unit, and undo calls it again with the old values. */
|
|
9394
|
+
reflowLinears(c) {
|
|
9395
|
+
for (const sh of this.shapes) {
|
|
9396
|
+
if (sh.condition_id !== c.id || sh.measure_role !== "linear") continue;
|
|
9397
|
+
const s = this.sheets.get(sh.sheet_id);
|
|
9398
|
+
if (!s || s.upp == null) continue;
|
|
9399
|
+
const px = sh.verts_norm.map(([nx, ny]) => [nx * s.widthPx, ny * s.heightPx]);
|
|
9400
|
+
const vertical = sh.origin?.derived ? { rise: 0, drop: 0 } : linearVerticalFt(sh, c);
|
|
9401
|
+
sh.computed = { ...sh.computed, ..._Session.linearQty(openLen(px) * (s.upp ?? 0), vertical) };
|
|
9402
|
+
if (!vertical.rise && !vertical.drop) {
|
|
9403
|
+
delete sh.computed.plan_lf;
|
|
9404
|
+
delete sh.computed.vertical_lf;
|
|
9405
|
+
}
|
|
9406
|
+
}
|
|
9407
|
+
}
|
|
9117
9408
|
/** The pending condition diffs as the report and the summary print them:
|
|
9118
9409
|
* current values beside proposed, per condition. */
|
|
9119
9410
|
proposedConditionEdits() {
|
|
@@ -9488,22 +9779,26 @@ var Session = class _Session {
|
|
|
9488
9779
|
};
|
|
9489
9780
|
const wasLf = parent.computed?.perimeter_lf ?? 0;
|
|
9490
9781
|
const wasSf = parent.computed?.area_sf ?? 0;
|
|
9491
|
-
const
|
|
9492
|
-
|
|
9493
|
-
|
|
9494
|
-
|
|
9782
|
+
const parentCond = this.conditions.find((x) => x.id === parent.condition_id);
|
|
9783
|
+
const parentLegs = parent.origin?.derived ? { rise: 0, drop: 0 } : linearVerticalFt(parent, parentCond);
|
|
9784
|
+
const wasPlan = parent.computed?.plan_lf ?? wasLf;
|
|
9785
|
+
const qty = (lenPx, legs) => {
|
|
9786
|
+
const plan = round2(lenPx * upp);
|
|
9787
|
+
const k = wasPlan > 0 ? plan / wasPlan : 0;
|
|
9788
|
+
return { ..._Session.linearQty(plan, legs), area_sf: round2(wasSf * k) };
|
|
9495
9789
|
};
|
|
9496
9790
|
const toNorm = (run2) => run2.map(([x, y]) => [x / s.widthPx, y / s.heightPx]);
|
|
9497
9791
|
const survivors = r.runs ?? [];
|
|
9498
9792
|
const [head2, ...rest] = survivors;
|
|
9499
9793
|
if (!head2) throw new UserError(`That ring covers the whole of ${parent.id}. Removing a run outright is delete_shape, not a cut.`);
|
|
9500
9794
|
parent.verts_norm = toNorm(head2);
|
|
9501
|
-
parent.computed = qty(openLen(head2));
|
|
9795
|
+
parent.computed = qty(openLen(head2), parentLegs);
|
|
9502
9796
|
const minted = rest.map((piece) => ({
|
|
9503
9797
|
...structuredClone(parent),
|
|
9504
9798
|
id: uid("shp"),
|
|
9505
9799
|
verts_norm: toNorm(piece),
|
|
9506
|
-
|
|
9800
|
+
...parentLegs.rise || parentLegs.drop ? { rise_ft: 0, drop_ft: 0 } : {},
|
|
9801
|
+
computed: qty(openLen(piece), { rise: 0, drop: 0 })
|
|
9507
9802
|
}));
|
|
9508
9803
|
this.shapes.push(...minted);
|
|
9509
9804
|
this.record({ op: "runcut", tool: "cut_out", target_id: parent.id, target_prev, minted_ids: minted.map((m) => m.id) });
|
|
@@ -9514,7 +9809,7 @@ var Session = class _Session {
|
|
|
9514
9809
|
pieces,
|
|
9515
9810
|
removed_lf: round2(Math.max(0, wasLf - pieces.reduce((n, p) => n + p.lf, 0))),
|
|
9516
9811
|
removed_sf: round2(Math.max(0, wasSf - pieces.reduce((n, p) => n + p.sf, 0))),
|
|
9517
|
-
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."
|
|
9812
|
+
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."
|
|
9518
9813
|
};
|
|
9519
9814
|
}
|
|
9520
9815
|
/** The canvas's cutoutParentPrevSans, ported verbatim in spirit (#206 —
|
|
@@ -9535,10 +9830,10 @@ var Session = class _Session {
|
|
|
9535
9830
|
const base = chain[0].origin.parent_prev;
|
|
9536
9831
|
const r = recomposeCutouts(px(base.verts_norm), (base.verts_norm_holes ?? []).map(px), rest.map((x) => px(x.verts_norm)));
|
|
9537
9832
|
if (!r) return null;
|
|
9538
|
-
const
|
|
9833
|
+
const norm3 = (ring) => ring.map(([x, y]) => [x / s.widthPx, y / s.heightPx]);
|
|
9539
9834
|
return {
|
|
9540
|
-
verts_norm:
|
|
9541
|
-
...r.holes.length ? { verts_norm_holes: r.holes.map(
|
|
9835
|
+
verts_norm: norm3(r.outer),
|
|
9836
|
+
...r.holes.length ? { verts_norm_holes: r.holes.map(norm3) } : {},
|
|
9542
9837
|
computed: { area_sf: round2(r.area * upp * upp), perimeter_lf: round2(r.perim * upp) }
|
|
9543
9838
|
};
|
|
9544
9839
|
}
|
|
@@ -10609,8 +10904,15 @@ var Session = class _Session {
|
|
|
10609
10904
|
if (cur.origin?.reviewed === true) {
|
|
10610
10905
|
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.`);
|
|
10611
10906
|
}
|
|
10612
|
-
if (patch.verts === void 0 && patch.condition === void 0 && patch.role === void 0 && patch.label === void 0) {
|
|
10613
|
-
throw new UserError("Nothing to change \u2014 pass at least one of verts, condition, role, label.");
|
|
10907
|
+
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) {
|
|
10908
|
+
throw new UserError("Nothing to change \u2014 pass at least one of verts, condition, role, label, rise_ft, drop_ft.");
|
|
10909
|
+
}
|
|
10910
|
+
const roleAfter = patch.role ?? cur.measure_role;
|
|
10911
|
+
if ((patch.rise_ft !== void 0 || patch.drop_ft !== void 0) && roleAfter !== "linear") {
|
|
10912
|
+
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.`);
|
|
10913
|
+
}
|
|
10914
|
+
if (cur.origin?.derived && (patch.rise_ft !== void 0 || patch.drop_ft !== void 0)) {
|
|
10915
|
+
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.`);
|
|
10614
10916
|
}
|
|
10615
10917
|
if (cur.cuts_shape_id && (patch.verts !== void 0 || patch.role !== void 0 || patch.condition !== void 0)) {
|
|
10616
10918
|
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.`);
|
|
@@ -10634,7 +10936,17 @@ var Session = class _Session {
|
|
|
10634
10936
|
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.`);
|
|
10635
10937
|
return h;
|
|
10636
10938
|
};
|
|
10637
|
-
const
|
|
10939
|
+
const legs = role === "linear" ? {
|
|
10940
|
+
...cur.rise_ft !== void 0 ? { rise_ft: cur.rise_ft } : {},
|
|
10941
|
+
...cur.drop_ft !== void 0 ? { drop_ft: cur.drop_ft } : {}
|
|
10942
|
+
} : {};
|
|
10943
|
+
for (const k of ["rise_ft", "drop_ft"]) {
|
|
10944
|
+
const v = patch[k];
|
|
10945
|
+
if (v === null) delete legs[k];
|
|
10946
|
+
else if (v !== void 0) legs[k] = v;
|
|
10947
|
+
}
|
|
10948
|
+
const condTagAfter = patch.condition !== void 0 ? patch.condition : this.conditions.find((x) => x.id === cur.condition_id)?.finish_tag;
|
|
10949
|
+
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" ? (() => {
|
|
10638
10950
|
const LF = openLen(vertsPx) * upp;
|
|
10639
10951
|
return { area_sf: round2(LF * heightFor()), perimeter_lf: round2(LF) };
|
|
10640
10952
|
})() : (() => {
|
|
@@ -10652,6 +10964,7 @@ var Session = class _Session {
|
|
|
10652
10964
|
computed,
|
|
10653
10965
|
...nextLabel ? { label: nextLabel } : {},
|
|
10654
10966
|
...role === "surface_area" ? { height_ft: Number(cur.height_ft) || heightFor() } : {},
|
|
10967
|
+
...legs,
|
|
10655
10968
|
...cur.origin ? { origin: {
|
|
10656
10969
|
...cur.origin,
|
|
10657
10970
|
agent_edits: (cur.origin.agent_edits ?? 0) + 1,
|
|
@@ -10661,12 +10974,16 @@ var Session = class _Session {
|
|
|
10661
10974
|
} } : {}
|
|
10662
10975
|
};
|
|
10663
10976
|
if (!nextLabel) delete this.shapes[i].label;
|
|
10977
|
+
if (legs.rise_ft === void 0) delete this.shapes[i].rise_ft;
|
|
10978
|
+
if (legs.drop_ft === void 0) delete this.shapes[i].drop_ft;
|
|
10664
10979
|
this.record({ op: "edit", tool: "edit_shape", before });
|
|
10665
10980
|
const changed = [
|
|
10666
10981
|
...patch.verts !== void 0 ? ["verts"] : [],
|
|
10667
10982
|
...patch.condition !== void 0 ? ["condition"] : [],
|
|
10668
10983
|
...patch.role !== void 0 ? ["role"] : [],
|
|
10669
|
-
...patch.label !== void 0 ? ["label"] : []
|
|
10984
|
+
...patch.label !== void 0 ? ["label"] : [],
|
|
10985
|
+
...patch.rise_ft !== void 0 ? ["rise_ft"] : [],
|
|
10986
|
+
...patch.drop_ft !== void 0 ? ["drop_ft"] : []
|
|
10670
10987
|
];
|
|
10671
10988
|
return {
|
|
10672
10989
|
shape_id: id,
|
|
@@ -10820,6 +11137,8 @@ var Session = class _Session {
|
|
|
10820
11137
|
if (opts.waste_pct !== void 0) c.waste_pct = opts.waste_pct;
|
|
10821
11138
|
if (opts.multiplier !== void 0) c.multiplier = opts.multiplier;
|
|
10822
11139
|
if (opts.height_ft !== void 0) c.height_ft = opts.height_ft;
|
|
11140
|
+
if (opts.rise_ft !== void 0) c.rise_ft = opts.rise_ft;
|
|
11141
|
+
if (opts.drop_ft !== void 0) c.drop_ft = opts.drop_ft;
|
|
10823
11142
|
if (opts.roll_setup !== void 0) {
|
|
10824
11143
|
if (opts.roll_setup === null) {
|
|
10825
11144
|
delete c.roll_setup;
|
|
@@ -10833,8 +11152,8 @@ var Session = class _Session {
|
|
|
10833
11152
|
}
|
|
10834
11153
|
}
|
|
10835
11154
|
editCondition(tag, opts) {
|
|
10836
|
-
if (opts.waste_pct === void 0 && opts.multiplier === void 0 && opts.height_ft === void 0 && opts.roll_setup === void 0) {
|
|
10837
|
-
throw new UserError("Nothing to change \u2014 pass at least one of waste_pct, multiplier, height_ft, roll_setup.");
|
|
11155
|
+
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) {
|
|
11156
|
+
throw new UserError("Nothing to change \u2014 pass at least one of waste_pct, multiplier, height_ft, rise_ft, drop_ft, roll_setup.");
|
|
10838
11157
|
}
|
|
10839
11158
|
const c = this.conditions.find((x) => x.finish_tag === tag);
|
|
10840
11159
|
if (!c) {
|
|
@@ -10845,10 +11164,13 @@ var Session = class _Session {
|
|
|
10845
11164
|
waste_pct: c.waste_pct,
|
|
10846
11165
|
multiplier: c.multiplier,
|
|
10847
11166
|
height_ft: c.height_ft,
|
|
11167
|
+
rise_ft: c.rise_ft,
|
|
11168
|
+
drop_ft: c.drop_ft,
|
|
10848
11169
|
roll_setup: c.roll_setup ? structuredClone(c.roll_setup) : void 0
|
|
10849
11170
|
};
|
|
10850
11171
|
this.applyConditionKnobs(c, opts);
|
|
10851
11172
|
this.record({ op: "condition", tool: "edit_condition", condition_id: c.id, before });
|
|
11173
|
+
if (opts.rise_ft !== void 0 || opts.drop_ft !== void 0) this.reflowLinears(c);
|
|
10852
11174
|
let roll;
|
|
10853
11175
|
if (hasRollSetup(c)) {
|
|
10854
11176
|
const { dimsFor, uppFor } = this.rollInputs();
|
|
@@ -10862,6 +11184,8 @@ var Session = class _Session {
|
|
|
10862
11184
|
waste_pct: c.waste_pct,
|
|
10863
11185
|
multiplier: c.multiplier,
|
|
10864
11186
|
...c.height_ft !== void 0 ? { height_ft: c.height_ft } : {},
|
|
11187
|
+
...c.rise_ft !== void 0 ? { rise_ft: c.rise_ft } : {},
|
|
11188
|
+
...c.drop_ft !== void 0 ? { drop_ft: c.drop_ft } : {},
|
|
10865
11189
|
...c.roll_setup ? { roll_setup: c.roll_setup } : {},
|
|
10866
11190
|
...roll ? { roll } : {}
|
|
10867
11191
|
};
|
|
@@ -11026,8 +11350,13 @@ var Session = class _Session {
|
|
|
11026
11350
|
c.multiplier = e.before.multiplier;
|
|
11027
11351
|
if (e.before.height_ft === void 0) delete c.height_ft;
|
|
11028
11352
|
else c.height_ft = e.before.height_ft;
|
|
11353
|
+
if (e.before.rise_ft === void 0) delete c.rise_ft;
|
|
11354
|
+
else c.rise_ft = e.before.rise_ft;
|
|
11355
|
+
if (e.before.drop_ft === void 0) delete c.drop_ft;
|
|
11356
|
+
else c.drop_ft = e.before.drop_ft;
|
|
11029
11357
|
if (e.before.roll_setup === void 0) delete c.roll_setup;
|
|
11030
11358
|
else c.roll_setup = e.before.roll_setup;
|
|
11359
|
+
this.reflowLinears(c);
|
|
11031
11360
|
}
|
|
11032
11361
|
undone.push({ seq: e.seq, op: e.op, tool: e.tool, shapes: 0 });
|
|
11033
11362
|
} else if (e.op === "duplicate_condition") {
|
|
@@ -11955,6 +12284,7 @@ var measurePolygonOutput = {
|
|
|
11955
12284
|
area_sf: z.number(),
|
|
11956
12285
|
perimeter_lf: z.number(),
|
|
11957
12286
|
nverts: z.number().int(),
|
|
12287
|
+
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"),
|
|
11958
12288
|
shape_id: z.string().optional().describe("Present when condition was passed and the shape committed"),
|
|
11959
12289
|
warning: z.string().optional().describe("Mixed-scale warning (#153): a scale note disagreeing with the sheet's sits in the measured region \u2014 verify before trusting these numbers")
|
|
11960
12290
|
};
|
|
@@ -11964,6 +12294,7 @@ var measureSurfaceOutput = {
|
|
|
11964
12294
|
length_lf: z.number().describe("The traced run's open length"),
|
|
11965
12295
|
area_sf: z.number().describe("length_lf \xD7 height_ft \u2014 the wall SF committed"),
|
|
11966
12296
|
npts: z.number().int(),
|
|
12297
|
+
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"),
|
|
11967
12298
|
shape_id: z.string()
|
|
11968
12299
|
};
|
|
11969
12300
|
var placeCountOutput = {
|
|
@@ -12062,8 +12393,13 @@ var symbolSweepOutput = {
|
|
|
12062
12393
|
warning: z.string().optional().describe("Present when the work cap dropped candidates \u2014 what a tighter seed rect would recover")
|
|
12063
12394
|
};
|
|
12064
12395
|
var measureLineOutput = {
|
|
12065
|
-
length_lf: z.number(),
|
|
12396
|
+
length_lf: z.number().describe("The run's TOTAL length: plan trace + rise + drop (#441)"),
|
|
12066
12397
|
npts: z.number().int(),
|
|
12398
|
+
plan_lf: z.number().optional().describe("The flat X\u2013Y trace alone \u2014 present only when the run carries a vertical leg"),
|
|
12399
|
+
vertical_lf: z.number().optional().describe("rise_ft + drop_ft \u2014 present only when a leg exists"),
|
|
12400
|
+
rise_ft: z.number().optional().describe("The rise this run resolved to (its own, else the condition default) \u2014 present with vertical_lf"),
|
|
12401
|
+
drop_ft: z.number().optional().describe("The drop this run resolved to \u2014 present with vertical_lf"),
|
|
12402
|
+
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"),
|
|
12067
12403
|
shape_id: z.string().optional().describe("Present when condition was passed and the shape committed")
|
|
12068
12404
|
};
|
|
12069
12405
|
var summaryRow = z.object({
|
|
@@ -12131,6 +12467,8 @@ var conditionKnobs = z.object({
|
|
|
12131
12467
|
waste_pct: z.number().optional(),
|
|
12132
12468
|
multiplier: z.number().optional(),
|
|
12133
12469
|
height_ft: z.number().optional(),
|
|
12470
|
+
rise_ft: z.number().optional(),
|
|
12471
|
+
drop_ft: z.number().optional(),
|
|
12134
12472
|
roll_setup: z.object({}).passthrough().nullable().optional()
|
|
12135
12473
|
}).passthrough();
|
|
12136
12474
|
var proposalRow = z.object({
|
|
@@ -12371,11 +12709,13 @@ var deleteShapeOutput = {
|
|
|
12371
12709
|
};
|
|
12372
12710
|
var editShapeOutput = {
|
|
12373
12711
|
shape_id: z.string(),
|
|
12374
|
-
changed: z.array(z.enum(["verts", "condition", "role", "label"])).describe("Which fields this call actually changed"),
|
|
12712
|
+
changed: z.array(z.enum(["verts", "condition", "role", "label", "rise_ft", "drop_ft"])).describe("Which fields this call actually changed"),
|
|
12375
12713
|
measure_role: z.enum(["floor_area", "deduct", "linear", "surface_area", "count"]),
|
|
12376
12714
|
nverts: z.number().int(),
|
|
12377
12715
|
area_sf: z.number().optional().describe("0 for linear shapes; LF \xD7 height for surface_area; absent for count"),
|
|
12378
|
-
perimeter_lf: z.number().optional().describe("Length for linear/surface runs, perimeter for closed ones; absent for count"),
|
|
12716
|
+
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"),
|
|
12717
|
+
plan_lf: z.number().optional().describe("Linear runs with a vertical leg: the flat trace alone (#441)"),
|
|
12718
|
+
vertical_lf: z.number().optional().describe("Linear runs with a vertical leg: rise + drop (#441)"),
|
|
12379
12719
|
count: z.number().optional().describe("count shapes only \u2014 the marker's EA (preserved across the edit)"),
|
|
12380
12720
|
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)"),
|
|
12381
12721
|
agent_edits: z.number().int().describe("How many times the agent has revised this shape \u2014 separate from the human-correction tally")
|
|
@@ -12518,6 +12858,8 @@ var editConditionOutput = {
|
|
|
12518
12858
|
waste_pct: z.number().describe("The condition's waste % after this write"),
|
|
12519
12859
|
multiplier: z.number().describe("The condition's quantity multiplier after this write"),
|
|
12520
12860
|
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)"),
|
|
12861
|
+
rise_ft: z.number().optional().describe("The condition's default rise for its linear runs after this write \u2014 present once set (#441)"),
|
|
12862
|
+
drop_ft: z.number().optional().describe("The condition's default drop for its linear runs after this write \u2014 present once set (#441)"),
|
|
12521
12863
|
roll_setup: z.object({}).passthrough().optional().describe("The condition's roll-goods setup after this write \u2014 present while opted in"),
|
|
12522
12864
|
roll: z.object({
|
|
12523
12865
|
condition_id: z.string(),
|
|
@@ -12862,9 +13204,132 @@ var countMarksOutput = {
|
|
|
12862
13204
|
};
|
|
12863
13205
|
|
|
12864
13206
|
// src/marked.ts
|
|
12865
|
-
import
|
|
13207
|
+
import path4 from "node:path";
|
|
12866
13208
|
import { readFile as readFile2, writeFile } from "node:fs/promises";
|
|
12867
13209
|
|
|
13210
|
+
// ../web/src/lib/annotationTools.js
|
|
13211
|
+
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" };
|
|
13212
|
+
var ANNOTATION_PRESETS = [
|
|
13213
|
+
{ id: "review", name: "Review", tool: "cloud", style: { ...ANNOTATION_DEFAULTS } },
|
|
13214
|
+
{ id: "direction", name: "Direction", tool: "arrow", style: { ...ANNOTATION_DEFAULTS, color: "#2563eb", stroke_pt: 2 } },
|
|
13215
|
+
{ id: "key", name: "Key information", tool: "highlighter", style: { ...ANNOTATION_DEFAULTS, color: "#ffd60a", mode: "text" } }
|
|
13216
|
+
];
|
|
13217
|
+
var clamp2 = (n, a, b, fallback) => Number.isFinite(+n) ? Math.max(a, Math.min(b, +n)) : fallback;
|
|
13218
|
+
function annotationStyle(s = {}) {
|
|
13219
|
+
s = s && typeof s === "object" ? s : {};
|
|
13220
|
+
return {
|
|
13221
|
+
color: /^#[0-9a-f]{6}$/i.test(s.color) ? s.color : ANNOTATION_DEFAULTS.color,
|
|
13222
|
+
stroke_pt: clamp2(s.stroke_pt, 0.5, 6, 1.5),
|
|
13223
|
+
font_pt: clamp2(s.font_pt, 6, 24, 10),
|
|
13224
|
+
opacity: clamp2(s.opacity, 0.1, 0.75, 0.32),
|
|
13225
|
+
width_pt: clamp2(s.width_pt, 2, 40, 10),
|
|
13226
|
+
head: ["filled", "open", "none"].includes(s.head) ? s.head : "filled",
|
|
13227
|
+
both: !!s.both,
|
|
13228
|
+
mode: ["freehand", "straight", "text"].includes(s.mode) ? s.mode : "freehand",
|
|
13229
|
+
line_style: ["solid", "dashed", "dotted"].includes(s.line_style) ? s.line_style : "solid"
|
|
13230
|
+
};
|
|
13231
|
+
}
|
|
13232
|
+
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])]];
|
|
13233
|
+
function noteLines(text, limit = 36) {
|
|
13234
|
+
const lines = [];
|
|
13235
|
+
for (const para of String(text || "").split("\n")) {
|
|
13236
|
+
let line = "";
|
|
13237
|
+
for (const word of para.split(/\s+/).filter(Boolean)) {
|
|
13238
|
+
if (line && line.length + word.length + 1 > limit) {
|
|
13239
|
+
lines.push(line);
|
|
13240
|
+
line = "";
|
|
13241
|
+
}
|
|
13242
|
+
let rest = word;
|
|
13243
|
+
while (rest.length > limit) {
|
|
13244
|
+
if (line) {
|
|
13245
|
+
lines.push(line);
|
|
13246
|
+
line = "";
|
|
13247
|
+
}
|
|
13248
|
+
lines.push(rest.slice(0, limit));
|
|
13249
|
+
rest = rest.slice(limit);
|
|
13250
|
+
}
|
|
13251
|
+
line += (line ? " " : "") + rest;
|
|
13252
|
+
}
|
|
13253
|
+
lines.push(line);
|
|
13254
|
+
}
|
|
13255
|
+
return lines;
|
|
13256
|
+
}
|
|
13257
|
+
var path3 = (pts) => pts.map((p, i) => [i ? "L" : "M", ...p]);
|
|
13258
|
+
function pathString(commands, transform = (p) => p) {
|
|
13259
|
+
return commands.map(([verb, ...v]) => {
|
|
13260
|
+
const ps = [];
|
|
13261
|
+
for (let i = 0; i < v.length; i += 2) ps.push(transform([v[i], v[i + 1]]).join(","));
|
|
13262
|
+
return verb + ps.join(" ");
|
|
13263
|
+
}).join(" ");
|
|
13264
|
+
}
|
|
13265
|
+
function annotationScene(m, W, H, pixelScale = 2) {
|
|
13266
|
+
const s = annotationStyle({ ...m.annotation_style, color: m.color || m.annotation_style?.color });
|
|
13267
|
+
const P = (p) => [p[0] * W, p[1] * H];
|
|
13268
|
+
const paths = [], texts = [];
|
|
13269
|
+
const stroke = s.stroke_pt * pixelScale;
|
|
13270
|
+
const add = (commands, extra = {}) => paths.push({ commands, stroke: s.color, width: stroke, opacity: 1, ...extra });
|
|
13271
|
+
const head2 = (a, b) => {
|
|
13272
|
+
if (s.head === "none") return;
|
|
13273
|
+
const size = Math.max(6, s.stroke_pt * 4) * pixelScale, len = Math.hypot(b[0] - a[0], b[1] - a[1]);
|
|
13274
|
+
if (!len) return;
|
|
13275
|
+
const u = [(b[0] - a[0]) / len, (b[1] - a[1]) / len], base = [b[0] - u[0] * size, b[1] - u[1] * size];
|
|
13276
|
+
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]];
|
|
13277
|
+
add([...path3(pts), ...s.head === "filled" ? [["Z"]] : []], { fill: s.head === "filled" ? s.color : void 0, dash: void 0 });
|
|
13278
|
+
};
|
|
13279
|
+
const leader = (a, b) => {
|
|
13280
|
+
add(path3([a, b]));
|
|
13281
|
+
head2(a, b);
|
|
13282
|
+
if (s.both) head2(b, a);
|
|
13283
|
+
};
|
|
13284
|
+
const noteOrigin = (at) => {
|
|
13285
|
+
const lines = noteLines(m.text), font = s.font_pt * pixelScale, pad = 5 * pixelScale;
|
|
13286
|
+
const w = Math.max(45 * pixelScale, ...lines.map((l) => l.length * font * 0.62)) + pad * 2;
|
|
13287
|
+
const h = Math.max(1, lines.length) * font * 1.35 + pad * 2;
|
|
13288
|
+
return [Math.max(0, Math.min(W - w, at[0])), Math.max(0, Math.min(H - h, at[1]))];
|
|
13289
|
+
};
|
|
13290
|
+
const note = (raw) => {
|
|
13291
|
+
const at = noteOrigin(raw);
|
|
13292
|
+
const lines = noteLines(m.text), font = s.font_pt * pixelScale, pad = 5 * pixelScale;
|
|
13293
|
+
const width = Math.max(45 * pixelScale, ...lines.map((l) => l.length * font * 0.62)) + pad * 2;
|
|
13294
|
+
const height = Math.max(1, lines.length) * font * 1.35 + pad * 2;
|
|
13295
|
+
add([...path3([at, [at[0] + width, at[1]], [at[0] + width, at[1] + height], [at[0], at[1] + height]]), ["Z"]], { fill: "#ffffff" });
|
|
13296
|
+
lines.forEach((text, i) => texts.push({ text, x: at[0] + pad, y: at[1] + pad + font * (1 + i * 1.35), size: font, color: "#172033" }));
|
|
13297
|
+
return [[at[0], at[1]], [at[0] + width, at[1] + height]];
|
|
13298
|
+
};
|
|
13299
|
+
let noteBox2 = null;
|
|
13300
|
+
if (m.type === "arrow" && m.from && m.to) leader(P(m.from), P(m.to));
|
|
13301
|
+
if (m.type === "callout" && m.at && m.target) {
|
|
13302
|
+
leader(noteOrigin(P(m.at)), P(m.target));
|
|
13303
|
+
noteBox2 = note(P(m.at));
|
|
13304
|
+
}
|
|
13305
|
+
if (m.type === "text" && m.at) noteBox2 = note(P(m.at));
|
|
13306
|
+
if (m.type === "cloud" && m.rect) {
|
|
13307
|
+
const r = box(...m.rect), a = P(r[0]), b = P(r[1]), cb = cloudBezier(...a, ...b);
|
|
13308
|
+
add([["M", ...cb.start], ...cb.segments.map((q) => ["C", ...q.flat()]), ["Z"]]);
|
|
13309
|
+
if (m.text) {
|
|
13310
|
+
const at = noteOrigin(P(m.note_at || [r[0][0], r[1][1] + 12 * pixelScale / H]));
|
|
13311
|
+
add(path3([at, [a[0], b[1]]]));
|
|
13312
|
+
noteBox2 = note(at);
|
|
13313
|
+
}
|
|
13314
|
+
}
|
|
13315
|
+
if (m.type === "highlight") {
|
|
13316
|
+
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" });
|
|
13317
|
+
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" });
|
|
13318
|
+
else if (m.rect) {
|
|
13319
|
+
const [a, b] = box(...m.rect);
|
|
13320
|
+
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" });
|
|
13321
|
+
}
|
|
13322
|
+
}
|
|
13323
|
+
const dash = s.line_style === "dashed" ? [5 * pixelScale, 3 * pixelScale] : s.line_style === "dotted" ? [pixelScale, 3 * pixelScale] : void 0;
|
|
13324
|
+
for (const p of paths) if (p.stroke && !p.fill && m.type !== "highlight") p.dash = dash;
|
|
13325
|
+
if (m.type === "cloud" && m.rect && Number.isFinite(m.rev) && m.rev > 0) {
|
|
13326
|
+
const r = box(...m.rect), x = r[1][0] * W, y = r[0][1] * H - 12 * pixelScale;
|
|
13327
|
+
add([["M", x, y - 7 * pixelScale], ["L", x + 7 * pixelScale, y + 5 * pixelScale], ["L", x - 7 * pixelScale, y + 5 * pixelScale], ["Z"]], { fill: "#ffffff" });
|
|
13328
|
+
texts.push({ text: String(m.rev), x: x - 2.5 * pixelScale, y: y + 3 * pixelScale, size: 8 * pixelScale, color: s.color });
|
|
13329
|
+
}
|
|
13330
|
+
return { paths, texts, noteBox: noteBox2 };
|
|
13331
|
+
}
|
|
13332
|
+
|
|
12868
13333
|
// ../web/src/lib/svgpath.js
|
|
12869
13334
|
function isSep(c) {
|
|
12870
13335
|
return c === " " || c === " " || c === "\n" || c === "\r" || c === "\f" || c === ",";
|
|
@@ -13360,6 +13825,78 @@ function boostForDark(hex2) {
|
|
|
13360
13825
|
return "#" + toHex2(nr) + toHex2(ng) + toHex2(nb);
|
|
13361
13826
|
}
|
|
13362
13827
|
|
|
13828
|
+
// ../web/src/lib/markupText.js
|
|
13829
|
+
var NOTE_PT = 8.5;
|
|
13830
|
+
var NOTE_MAX_W_PT = 216;
|
|
13831
|
+
var LINE_PITCH = 1.28;
|
|
13832
|
+
var NOTE_PAD_PT = 3;
|
|
13833
|
+
function ptToImg(pt) {
|
|
13834
|
+
return pt * RENDER_SCALE;
|
|
13835
|
+
}
|
|
13836
|
+
function wrapLines(text, maxW, measure) {
|
|
13837
|
+
const src = String(text ?? "");
|
|
13838
|
+
if (!src.trim()) return [];
|
|
13839
|
+
const out = [];
|
|
13840
|
+
for (const para of src.split(/\r?\n/)) {
|
|
13841
|
+
const words = para.trim().split(/\s+/).filter(Boolean);
|
|
13842
|
+
if (!words.length) {
|
|
13843
|
+
out.push("");
|
|
13844
|
+
continue;
|
|
13845
|
+
}
|
|
13846
|
+
let line = "";
|
|
13847
|
+
for (const w of words) {
|
|
13848
|
+
const cand = line ? `${line} ${w}` : w;
|
|
13849
|
+
if (measure(cand) <= maxW || !line && measure(w) <= maxW) {
|
|
13850
|
+
line = cand;
|
|
13851
|
+
continue;
|
|
13852
|
+
}
|
|
13853
|
+
if (line) {
|
|
13854
|
+
out.push(line);
|
|
13855
|
+
line = "";
|
|
13856
|
+
}
|
|
13857
|
+
if (measure(w) <= maxW) {
|
|
13858
|
+
line = w;
|
|
13859
|
+
continue;
|
|
13860
|
+
}
|
|
13861
|
+
let piece = "";
|
|
13862
|
+
for (const ch of w) {
|
|
13863
|
+
if (measure(piece + ch) > maxW && piece) {
|
|
13864
|
+
out.push(piece);
|
|
13865
|
+
piece = ch;
|
|
13866
|
+
} else piece += ch;
|
|
13867
|
+
}
|
|
13868
|
+
line = piece;
|
|
13869
|
+
}
|
|
13870
|
+
out.push(line);
|
|
13871
|
+
}
|
|
13872
|
+
return out;
|
|
13873
|
+
}
|
|
13874
|
+
function layoutNote({ text, fontPx, measure, maxWPx, padPx }) {
|
|
13875
|
+
const ratio = fontPx / ptToImg(NOTE_PT);
|
|
13876
|
+
const maxW = maxWPx > 0 ? maxWPx : ptToImg(NOTE_MAX_W_PT) * ratio;
|
|
13877
|
+
const pad = padPx >= 0 ? padPx : ptToImg(NOTE_PAD_PT) * ratio;
|
|
13878
|
+
const lines = wrapLines(text, maxW - 2 * pad, measure);
|
|
13879
|
+
const lineH = fontPx * LINE_PITCH;
|
|
13880
|
+
const widest = lines.reduce((m, l) => Math.max(m, measure(l)), 0);
|
|
13881
|
+
return {
|
|
13882
|
+
lines,
|
|
13883
|
+
fontPx,
|
|
13884
|
+
lineH,
|
|
13885
|
+
padX: pad,
|
|
13886
|
+
padY: pad,
|
|
13887
|
+
w: lines.length ? widest + 2 * pad : 0,
|
|
13888
|
+
h: lines.length ? lines.length * lineH + 2 * pad : 0
|
|
13889
|
+
};
|
|
13890
|
+
}
|
|
13891
|
+
function noteBox(ax, ay, L) {
|
|
13892
|
+
const x0 = ax - L.padX;
|
|
13893
|
+
const y0 = ay - L.fontPx - L.padY;
|
|
13894
|
+
return { x0, y0, x1: x0 + L.w, y1: y0 + L.h };
|
|
13895
|
+
}
|
|
13896
|
+
function lineBaseline(ay, L, i) {
|
|
13897
|
+
return ay + i * L.lineH;
|
|
13898
|
+
}
|
|
13899
|
+
|
|
13363
13900
|
// ../web/src/lib/markedsetSource.js
|
|
13364
13901
|
function sourcePageMode({ dark = false, encrypted = false } = {}) {
|
|
13365
13902
|
if (dark) return "raster-inverted";
|
|
@@ -13561,12 +14098,13 @@ function invertPixels(cv) {
|
|
|
13561
14098
|
ctx.restore();
|
|
13562
14099
|
}
|
|
13563
14100
|
async function buildMarkedSetPdf({ projectName, dark, sheets, shapes, markups, approvals = [], rfis: rfisIn = [], conditions, getPage, loadPdfData, company, clientInfo, credit = null, provenance = null, coverTitle = "Marked Set", units = "imperial" }) {
|
|
14101
|
+
markups = (markups || []).filter((m) => !m.reference_only);
|
|
13564
14102
|
const rfis = liveRfis(rfisIn);
|
|
13565
14103
|
const M = units === "metric";
|
|
13566
14104
|
const uA = (sf) => M ? sf * 0.09290304 : sf;
|
|
13567
14105
|
const uL = (lf) => M ? lf * 0.3048 : lf;
|
|
13568
14106
|
const AU = M ? "m2" : "SF", LU = M ? "m" : "LF";
|
|
13569
|
-
const { PDFDocument, StandardFonts, rgb, degrees, LineCapStyle } = await import("pdf-lib");
|
|
14107
|
+
const { PDFDocument, StandardFonts, rgb, degrees, LineCapStyle, BlendMode } = await import("pdf-lib");
|
|
13570
14108
|
const condById = Object.fromEntries(conditions.map((c) => [c.id, c]));
|
|
13571
14109
|
const rfiNum = new Map((rfis || []).map((r) => [r.id, r.number]));
|
|
13572
14110
|
const byKey = (arr3) => {
|
|
@@ -13896,6 +14434,28 @@ async function buildMarkedSetPdf({ projectName, dark, sheets, shapes, markups, a
|
|
|
13896
14434
|
const [px, py] = toPage(x, y);
|
|
13897
14435
|
pg.drawText(winAnsiSafe(t), { x: px, y: py, size, font: fnt, color: colorRgb, rotate: chipRot });
|
|
13898
14436
|
};
|
|
14437
|
+
const noteBlock = (raw, ax, ay, colorRgb, backing) => {
|
|
14438
|
+
const t = winAnsiSafe(raw);
|
|
14439
|
+
const fs = NOTE_PT / ptScale;
|
|
14440
|
+
const L = layoutNote({ text: t, fontPx: fs, measure: (str) => bold.widthOfTextAtSize(str, NOTE_PT) / ptScale });
|
|
14441
|
+
if (!L.lines.length) return;
|
|
14442
|
+
const b = noteBox(ax, ay, L);
|
|
14443
|
+
const dp = imageDrawParams(toPage, b.x0, b.y0, L.w, L.h);
|
|
14444
|
+
pg.drawRectangle({
|
|
14445
|
+
x: dp.x,
|
|
14446
|
+
y: dp.y,
|
|
14447
|
+
width: dp.width,
|
|
14448
|
+
height: dp.height,
|
|
14449
|
+
rotate: degrees(dp.rotateDeg),
|
|
14450
|
+
color: backing,
|
|
14451
|
+
opacity: 0.92,
|
|
14452
|
+
borderColor: colorRgb,
|
|
14453
|
+
borderWidth: 0.7
|
|
14454
|
+
});
|
|
14455
|
+
L.lines.forEach((ln, i) => {
|
|
14456
|
+
if (ln) text(ln, ax, lineBaseline(ay, L, i), NOTE_PT, colorRgb, bold);
|
|
14457
|
+
});
|
|
14458
|
+
};
|
|
13899
14459
|
const chip = (raw, x, y, borderRgb) => {
|
|
13900
14460
|
const t = winAnsiSafe(raw);
|
|
13901
14461
|
const size = 7.5;
|
|
@@ -13958,7 +14518,27 @@ async function buildMarkedSetPdf({ projectName, dark, sheets, shapes, markups, a
|
|
|
13958
14518
|
const mcol = rgb(...hex(dark ? boostForDark(mbase) : mbase));
|
|
13959
14519
|
const mdash = pdfDashFor(m.line_style || "solid");
|
|
13960
14520
|
const mw = clampWeight(m.weight);
|
|
13961
|
-
if (m.
|
|
14521
|
+
if (m.annotation_style && ["arrow", "highlight", "callout", "cloud", "text"].includes(m.type)) {
|
|
14522
|
+
const scene = annotationScene(m, W, H, RENDER_SCALE);
|
|
14523
|
+
const P = ([x, y]) => {
|
|
14524
|
+
const [px, py] = toPage(x, y);
|
|
14525
|
+
return [px, -py];
|
|
14526
|
+
};
|
|
14527
|
+
for (const ink2 of scene.paths) pg.drawSvgPath(pathString(ink2.commands, P), {
|
|
14528
|
+
x: 0,
|
|
14529
|
+
y: 0,
|
|
14530
|
+
...ink2.fill ? { color: rgb(...hex(ink2.fill)), opacity: ink2.opacity } : {},
|
|
14531
|
+
...ink2.stroke ? { borderColor: rgb(...hex(ink2.stroke)), borderWidth: ink2.width * ptScale, borderOpacity: ink2.opacity } : {},
|
|
14532
|
+
...ink2.dash ? { borderDashArray: ink2.dash.map((n) => n * ptScale) } : {},
|
|
14533
|
+
...ink2.blend ? { blendMode: BlendMode.Multiply } : {},
|
|
14534
|
+
borderLineCap: LineCapStyle.Round
|
|
14535
|
+
});
|
|
14536
|
+
for (const ink2 of scene.texts) text(ink2.text, ink2.x, ink2.y, ink2.size * ptScale, rgb(...hex(ink2.color)));
|
|
14537
|
+
if (rlabel) {
|
|
14538
|
+
const at = m.at || m.from || m.rect?.[0] || m.pts?.[0];
|
|
14539
|
+
if (at) text(rlabel, at[0] * W, at[1] * H - 10 / ptScale, 8, mcol, bold);
|
|
14540
|
+
}
|
|
14541
|
+
} else if (m.type === "highlight" && (m.pts || []).length >= 2) {
|
|
13962
14542
|
const ipts = m.pts.map(([nx, ny]) => [nx * W, ny * H]);
|
|
13963
14543
|
const inkCol = rgb(...hex(m.color || "#ffd60a"));
|
|
13964
14544
|
const wImg = (m.w || 0.01) * W * mw;
|
|
@@ -14040,7 +14620,9 @@ async function buildMarkedSetPdf({ projectName, dark, sheets, shapes, markups, a
|
|
|
14040
14620
|
const [ptx, pty] = toPage(m.target[0] * W, m.target[1] * H);
|
|
14041
14621
|
pg.drawSvgPath(arrowheadPath(pax, -pay, ptx, -pty, 5), { x: 0, y: 0, color: mcol, opacity: 0.9 });
|
|
14042
14622
|
}
|
|
14043
|
-
|
|
14623
|
+
noteBlock(lbl(m.text), m.at[0] * W, m.at[1] * H, mcol, dark ? rgb(0.08, 0.1, 0.12) : rgb(1, 1, 1));
|
|
14624
|
+
} else if (m.type === "text" && m.at) {
|
|
14625
|
+
noteBlock(lbl(m.text), m.at[0] * W, m.at[1] * H, mcol, dark ? rgb(0.08, 0.1, 0.12) : rgb(1, 0.97, 0.93));
|
|
14044
14626
|
} else if (m.type === "svg" && m.at && Array.isArray(m.vb) && typeof m.path === "string") {
|
|
14045
14627
|
const { s: sx, bw, bh } = svgPlacedBox(m.vb, m.w, W);
|
|
14046
14628
|
if (sx > 0) {
|
|
@@ -14203,7 +14785,7 @@ async function exportMarkedPdf(session, opts) {
|
|
|
14203
14785
|
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.");
|
|
14204
14786
|
}
|
|
14205
14787
|
const base = file.replace(/\.pdf$/i, "");
|
|
14206
|
-
const outPath =
|
|
14788
|
+
const outPath = path4.resolve(opts.path ?? path4.join(path4.dirname(filePath), `${base} - marked set.pdf`));
|
|
14207
14789
|
await assertWritable(outPath, "pdf", opts.overwrite);
|
|
14208
14790
|
const sheetStates = session.sheetList();
|
|
14209
14791
|
const byFilePage = new Map(sheetStates.map((s) => [`${session.fileFor(s.key)}#${s.pageNum}`, s.page]));
|
|
@@ -14281,7 +14863,7 @@ async function exportMarkedPdf(session, opts) {
|
|
|
14281
14863
|
}
|
|
14282
14864
|
|
|
14283
14865
|
// src/importing.ts
|
|
14284
|
-
import
|
|
14866
|
+
import path5 from "node:path";
|
|
14285
14867
|
import { readFile as readFile3 } from "node:fs/promises";
|
|
14286
14868
|
|
|
14287
14869
|
// ../web/src/lib/reviewState.js
|
|
@@ -14289,6 +14871,11 @@ function normalizeAgentReview(shape) {
|
|
|
14289
14871
|
return shape?.origin?.actor === "agent" && shape.origin.reviewed == null ? { ...shape, origin: { ...shape.origin, reviewed: false } } : shape;
|
|
14290
14872
|
}
|
|
14291
14873
|
|
|
14874
|
+
// ../web/src/lib/multiplier.js
|
|
14875
|
+
function isValidMultiplier(v) {
|
|
14876
|
+
return v === void 0 || v === null || typeof v === "number" && Number.isFinite(v) && v > 0;
|
|
14877
|
+
}
|
|
14878
|
+
|
|
14292
14879
|
// ../web/src/lib/importTakeoff.js
|
|
14293
14880
|
function parseTakeoffImport(text) {
|
|
14294
14881
|
let doc;
|
|
@@ -14313,6 +14900,11 @@ function mergeTakeoffImport(current, imported, knownFiles = null) {
|
|
|
14313
14900
|
const cur = current && typeof current === "object" ? current : {};
|
|
14314
14901
|
const impShapes = arr2(imported.shapes).filter((s) => s && typeof s === "object" && typeof s.sheet_id === "string" && typeof s.id === "string").map(normalizeAgentReview);
|
|
14315
14902
|
const impConds = arr2(imported.conditions).filter((c) => c && typeof c === "object" && typeof c.id === "string");
|
|
14903
|
+
const badMult = impConds.filter((c) => !isValidMultiplier(c.multiplier));
|
|
14904
|
+
if (badMult.length) {
|
|
14905
|
+
const list = badMult.map((c) => `${c.finish_tag ?? c.id} (${JSON.stringify(c.multiplier)})`).join(", ");
|
|
14906
|
+
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.`);
|
|
14907
|
+
}
|
|
14316
14908
|
const localScales = new Map(arr2(cur.sheets).filter((s) => typeof s?.sheet_id === "string").map((s) => [s.sheet_id, s.units_per_px]));
|
|
14317
14909
|
const incomingScales = new Map(arr2(imported.sheets).filter((s) => typeof s?.sheet_id === "string").map((s) => [s.sheet_id, s.units_per_px]));
|
|
14318
14910
|
const existingIds = new Set(arr2(cur.shapes).map((s) => s.id));
|
|
@@ -14477,7 +15069,7 @@ async function importTakeoff(session, filePath) {
|
|
|
14477
15069
|
const addedIds = session.shapes.filter((s) => !prevShapeIds.has(s.id)).map((s) => s.id);
|
|
14478
15070
|
if (addedIds.length) session.journalCommit("import_takeoff", addedIds);
|
|
14479
15071
|
return {
|
|
14480
|
-
file:
|
|
15072
|
+
file: path5.basename(filePath),
|
|
14481
15073
|
...note,
|
|
14482
15074
|
rules_imported: rulesImported,
|
|
14483
15075
|
shapes_total: session.shapes.length,
|
|
@@ -14522,8 +15114,8 @@ function registerTools(realServer, session, opts = {}) {
|
|
|
14522
15114
|
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)")
|
|
14523
15115
|
},
|
|
14524
15116
|
outputSchema: loadPlanOutput
|
|
14525
|
-
}, run("load_plan", async ({ path:
|
|
14526
|
-
const loaded = await session.loadPlan(
|
|
15117
|
+
}, run("load_plan", async ({ path: path6, merge: merge2 }) => {
|
|
15118
|
+
const loaded = await session.loadPlan(path6, { merge: merge2 });
|
|
14527
15119
|
server.sendResourceListChanged();
|
|
14528
15120
|
return loaded;
|
|
14529
15121
|
}));
|
|
@@ -14591,15 +15183,16 @@ function registerTools(realServer, session, opts = {}) {
|
|
|
14591
15183
|
outputSchema: proposeTakeoffOutput
|
|
14592
15184
|
}, run("propose_takeoff", (a) => session.proposeTakeoff(a.label, a.rationale)));
|
|
14593
15185
|
server.registerTool("measure_polygon", {
|
|
14594
|
-
description: `Measure a closed polygon you supply (min 3 vertices, image px): area_sf and perimeter_lf at the sheet's scale. Requires the scale to be set. Pass condition to commit it; role "deduct" subtracts. A room ring belongs on the innermost wall-face strokes from get_sheet_vectors, crossing each door opening on the wall centerline and wrapping columns and stubs; never on a hatch edge, casework or a door leaf. Check it with view_sheet overlay:true on a tight crop and fix it with edit_shape. ${COORDS}`,
|
|
15186
|
+
description: `Measure a closed polygon you supply (min 3 vertices, image px): area_sf and perimeter_lf at the sheet's scale. Requires the scale to be set. Pass condition to commit it; role "deduct" subtracts. A room ring belongs on the innermost wall-face strokes from get_sheet_vectors, crossing each door opening on the wall centerline and wrapping columns and stubs; never on a hatch edge, casework or a door leaf. Check it with view_sheet overlay:true on a tight crop and fix it with edit_shape. A CURVED wall is a circle: do not chord it and do not hand-tessellate it \u2014 give the bow one point on the wall and list its index in arc_through. ${COORDS}`,
|
|
14595
15187
|
inputSchema: {
|
|
14596
15188
|
sheet: z2.string(),
|
|
14597
15189
|
verts: z2.array(pointSchema).min(3),
|
|
14598
15190
|
condition: z2.string().optional(),
|
|
14599
|
-
role: roleSchema
|
|
15191
|
+
role: roleSchema,
|
|
15192
|
+
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.")
|
|
14600
15193
|
},
|
|
14601
15194
|
outputSchema: measurePolygonOutput
|
|
14602
|
-
}, run("measure_polygon", (a) => session.measurePolygon(a.sheet, a.verts, { condition: a.condition, role: a.role })));
|
|
15195
|
+
}, run("measure_polygon", (a) => session.measurePolygon(a.sheet, a.verts, { condition: a.condition, role: a.role, arc_through: a.arc_through })));
|
|
14603
15196
|
server.registerTool("cut_out", {
|
|
14604
15197
|
description: `Cut a REAL hole in a committed floor_area shape (#206) \u2014 the way the canvas cuts one (#137): the same lib/cutout.js boolean subtract, so the two surfaces can never disagree about what a hole holds. The parent keeps its outer ring plus the reconciled hole(s) (verts_norm_holes), its computed nets for real \u2014 N cuts compose, overlap between cuts never double-deducts (set subtraction), a hole ADDS perimeter \u2014 and the deduct commits carrying cuts_shape_id so the report and legend read the reconciled number, never a second arithmetic pass. This is the verb for a column, a floor drain, an island of casework INSIDE a room; an independent measure_polygon role:"deduct" stays the tool for a deduction that isn't a hole in one parent. Refusal over guessing: the ring must sit FULLY inside the parent's outer ring (an edge-crossing cut is a boundary correction \u2014 edit_shape the parent instead), and a cut that would erase the parent or split it in two refuses whole (trace the pieces as rooms). One journal entry \u2014 undo_last restores parent and hole together; delete_shape on the deduct later reverts the cut too (a multi-cut parent rebuilds from the chain's pristine snapshot minus the survivors). AN OPEN RUN IS CLIPPED, NOT SUBTRACTED: wall tile (surface_area) and base/transitions (linear) are polylines traced in plan, so the ring removes the stretch it covers, the run keeps its id and takes what survives, and a cut through the MIDDLE leaves the far side as its own shape (same condition, same height) \u2014 quantities ride the surviving length, which is exact, since wall SF is LF \xD7 height and a border's SF is LF \xD7 thickness. No deduct is minted for a run: there is no area for one to sit on, and a deduct's SF counts against the FLOOR total a run never fills. A ring that misses the run, one that swallows it whole (delete_shape it), and a curved run (its verts are control points) all refuse. A derived base with numeric openings also refuses: those deductions have no stored location; use measure_line for installed runs so a geometric cut cannot erase the numeric allowance. ${COORDS}`,
|
|
14605
15198
|
inputSchema: {
|
|
@@ -14609,24 +15202,28 @@ function registerTools(realServer, session, opts = {}) {
|
|
|
14609
15202
|
outputSchema: cutOutOutput
|
|
14610
15203
|
}, run("cut_out", (a) => session.cutOut(a)));
|
|
14611
15204
|
server.registerTool("measure_line", {
|
|
14612
|
-
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). ${COORDS}`,
|
|
15205
|
+
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}`,
|
|
14613
15206
|
inputSchema: {
|
|
14614
15207
|
sheet: z2.string(),
|
|
14615
15208
|
pts: z2.array(pointSchema).min(2),
|
|
14616
|
-
condition: z2.string().optional()
|
|
15209
|
+
condition: z2.string().optional(),
|
|
15210
|
+
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)"),
|
|
15211
|
+
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)"),
|
|
15212
|
+
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.")
|
|
14617
15213
|
},
|
|
14618
15214
|
outputSchema: measureLineOutput
|
|
14619
|
-
}, run("measure_line", (a) => session.measureLine(a.sheet, a.pts, { condition: a.condition })));
|
|
15215
|
+
}, 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 })));
|
|
14620
15216
|
server.registerTool("measure_surface", {
|
|
14621
15217
|
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}`,
|
|
14622
15218
|
inputSchema: {
|
|
14623
15219
|
sheet: z2.string(),
|
|
14624
15220
|
pts: z2.array(pointSchema).min(2).describe("The wall run, an open polyline (image px)"),
|
|
14625
15221
|
condition: z2.string().describe("Finish tag to commit under (minted on first use), e.g. 'CT-W1'"),
|
|
14626
|
-
height_ft: z2.number().positive().optional().describe("Wall height in feet \u2014 written to the condition's H knob first, then used")
|
|
15222
|
+
height_ft: z2.number().positive().optional().describe("Wall height in feet \u2014 written to the condition's H knob first, then used"),
|
|
15223
|
+
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.")
|
|
14627
15224
|
},
|
|
14628
15225
|
outputSchema: measureSurfaceOutput
|
|
14629
|
-
}, run("measure_surface", (a) => session.measureSurface(a.sheet, a.pts, { condition: a.condition, height_ft: a.height_ft })));
|
|
15226
|
+
}, run("measure_surface", (a) => session.measureSurface(a.sheet, a.pts, { condition: a.condition, height_ft: a.height_ft, arc_through: a.arc_through })));
|
|
14630
15227
|
server.registerTool("place_count", {
|
|
14631
15228
|
description: `Count markers \u2014 EA (#146): one point, one each. Thresholds, stair nosings, floor boxes, entrance mats \u2014 the scale-free quantity family. Commits one count shape per point (computed {count: 1}, exactly the canvas's Count tool), NO scale required, and the whole call is ONE undo step${oneClick ? " like a detect_rooms sweep" : ""}. takeoff_summary reports them as ea; the marked set draws each marker. ${COORDS}`,
|
|
14632
15229
|
inputSchema: {
|
|
@@ -14888,10 +15485,12 @@ All-or-nothing, like derive_base: an unknown tag, a transition landing on either
|
|
|
14888
15485
|
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"),
|
|
14889
15486
|
condition: z2.string().optional().describe("Reassign to this finish tag (minted on first use)"),
|
|
14890
15487
|
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"),
|
|
14891
|
-
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')
|
|
15488
|
+
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'),
|
|
15489
|
+
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"),
|
|
15490
|
+
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")
|
|
14892
15491
|
},
|
|
14893
15492
|
outputSchema: editShapeOutput
|
|
14894
|
-
}, run("edit_shape", (a) => session.editShape(a.shape_id, { verts: a.verts, condition: a.condition, role: a.role, label: a.label })));
|
|
15493
|
+
}, 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 })));
|
|
14895
15494
|
server.registerTool("edit_materials", {
|
|
14896
15495
|
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).`,
|
|
14897
15496
|
inputSchema: {
|
|
@@ -14919,6 +15518,8 @@ All-or-nothing, like derive_base: an unknown tag, a transition landing on either
|
|
|
14919
15518
|
waste_pct: z2.number().min(0).optional().describe("Waste percentage applied to net order quantities, e.g. 10 for 10%"),
|
|
14920
15519
|
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'"),
|
|
14921
15520
|
height_ft: z2.number().positive().optional().describe("Wall height in feet \u2014 the canvas's H knob; measure_surface quantifies traced LF \xD7 this"),
|
|
15521
|
+
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"),
|
|
15522
|
+
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"),
|
|
14922
15523
|
roll_setup: z2.union([
|
|
14923
15524
|
z2.null().describe("Opt the condition OUT of roll goods"),
|
|
14924
15525
|
z2.object({
|
|
@@ -14934,7 +15535,7 @@ All-or-nothing, like derive_base: an unknown tag, a transition landing on either
|
|
|
14934
15535
|
]).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")
|
|
14935
15536
|
},
|
|
14936
15537
|
outputSchema: editConditionOutput
|
|
14937
|
-
}, 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 })));
|
|
15538
|
+
}, 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 })));
|
|
14938
15539
|
server.registerTool("propose_condition_edit", {
|
|
14939
15540
|
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.`,
|
|
14940
15541
|
inputSchema: {
|
|
@@ -14943,11 +15544,13 @@ All-or-nothing, like derive_base: an unknown tag, a transition landing on either
|
|
|
14943
15544
|
waste_pct: z2.number().min(0).optional(),
|
|
14944
15545
|
multiplier: z2.number().positive().optional(),
|
|
14945
15546
|
height_ft: z2.number().positive().optional(),
|
|
15547
|
+
rise_ft: z2.number().min(0).optional().describe("Proposed default vertical leg UP for the condition's linear runs (#441)"),
|
|
15548
|
+
drop_ft: z2.number().min(0).optional().describe("Proposed default vertical leg DOWN for the condition's linear runs (#441)"),
|
|
14946
15549
|
roll_setup: z2.union([z2.null(), z2.object({}).passthrough()]).optional().describe("Proposed roll-goods setup, or null to propose opting out"),
|
|
14947
15550
|
rationale: z2.string().min(1).describe("Why \u2014 the schedule row, the spec section, the sheet note that decided it")
|
|
14948
15551
|
},
|
|
14949
15552
|
outputSchema: proposeConditionEditOutput
|
|
14950
|
-
}, 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)));
|
|
15553
|
+
}, 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)));
|
|
14951
15554
|
server.registerTool("withdraw_condition_edit", {
|
|
14952
15555
|
description: `Drop a pending condition-edit proposal (#365) without touching the condition. undo_last re-seats it.`,
|
|
14953
15556
|
inputSchema: { proposal_id: z2.string().describe("From propose_condition_edit, or takeoff_summary's proposed_condition_edits") },
|
|
@@ -15138,7 +15741,7 @@ No review gate: the pencil-not-ink rule exists to stop an agent inventing geomet
|
|
|
15138
15741
|
import { ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
15139
15742
|
|
|
15140
15743
|
// src/wiki.generated.ts
|
|
15141
|
-
var WIKI_VERSION = "0.9.
|
|
15744
|
+
var WIKI_VERSION = "0.9.90";
|
|
15142
15745
|
var WIKI_PAGES = [
|
|
15143
15746
|
{
|
|
15144
15747
|
"key": "index",
|
|
@@ -15177,8 +15780,8 @@ var WIKI_PAGES = [
|
|
|
15177
15780
|
"uri": "takeoff://wiki/workflows",
|
|
15178
15781
|
"title": "Workflows",
|
|
15179
15782
|
"source": "docs/wiki/workflows.md",
|
|
15180
|
-
"source_sha256": "
|
|
15181
|
-
"text": "# Workflows\n\n## Human: open, stitch and measure\n\nOpen the plans and check their revision. Press **G** (or choose **Sheets \u2192 Open\ngallery\u2026**) to open the sheet gallery, then select the\n2\u20134 split sheets in left-to-right order and click **Stitch N into one surface**.\nOn the composite, choose **Align**, click a recognizable point near the joint,\nthen click the same drawn point on the other sheet. Check a second recognizable\npoint and the scale before tracing. Align translates a member; it does not\nrotate or resize it. Once shapes exist on a stitch, alignment is locked.\n\nIf the controls seem missing, **Stitch N into one surface** is in the gallery\nfooter and requires 2\u20134 selected sheets; **Align** appears only on an open\nstitched surface. Create a fresh stitch if Align refuses because takeoffs\nalready exist; deleting a stitch also refuses while takeoffs or markups exist.\n\nUse the composite as one measuring surface. Its marked-set page is labeled as\na composite, not an architect-issued sheet. Save the editable project archive\nwhen preserving the composite is required. MCP has no stitch/alignment tool and\nits current import/export path omits stitches: it cannot be used as a lossless\nhandoff for that document. Have an MCP agent measure source sheets individually\nwith explicit scope boundaries instead.\n\nSource: [Human stitching instructions](https://github.com/Kentucky-ai/opentakeoff/blob/main/docs/USER_GUIDE.md#stitching-a-floor-split-at-a-match-line),\n[stitch records](https://github.com/Kentucky-ai/opentakeoff/blob/main/web/src/lib/stitches.ts),\n[compatibility evidence](https://github.com/Kentucky-ai/opentakeoff/blob/main/protocol/COMPATIBILITY.md).\n\n## Agent: source to reviewed handoff\n\n1. `load_plan` \u2192 inspect sheets/revisions \u2192 `set_scale` on each measured sheet.\n Confirm the relevant detail's scale, which may differ from the overall plan.\n2. Read source text and vectors; inspect the matching region with `view_sheet`.\n Use each room's schedule evidence for its finish. Missing/ambiguous evidence\n is a qualification or RFI, not an inferred assignment.\n3. `propose_takeoff` names a batch; it creates no geometry. Measure small batches\n with the appropriate area, run, surface or count tool.\n4. Inspect overlays and actual boundaries, openings, jambs and deductions.\n `edit_shape` corrects pending shapes. Physical base gaps use explicit runs;\n stepped wall faces use separate height bands. See the [geometry workflow](https://github.com/Kentucky-ai/opentakeoff/blob/main/docs/GEOMETRY_WORKFLOW.md).\n\n### Trace a room the way an estimator does\n\nBlind agent runs against reviewed references showed that the ring, not the\ntotal, is what fails. These rules are what the references are drawn to:\n\n1. The boundary is the **innermost interior wall face**. Never the wall\n centerline, never the far face, never under a wall. Casework, counters,\n fixtures, equipment, hatch patterns, dimension strings, text and leaders\n never define the boundary; the finish runs under casework and fixtures.\n2. Corners sit where two adjacent wall-face strokes meet. Read the strokes with\n `get_sheet_vectors` over a tight region and put each vertex on a stroke; do\n not trace a hatch edge or a raster guess.\n3. At every **door or cased opening** the ring follows the face to the jamb,\n turns into the opening, runs across it on the **wall centerline** (midway\n between that wall's two faces, the full wall on a composite wall), and\n returns along the far jamb. Both rooms sharing the door share that segment.\n A door leaf and its swing arc are never part of the boundary; a leaf drawn\n standing open looks like a wall line and is not one.\n4. **Windows** and other non-passable openings do not break the ring.\n5. Columns, chases, pilasters and wall stubs that project into the room are\n traced around; an enclosed cell drawn with wall-weight lines is not floor.\n6. A **finish split** inside one room is two rings sharing the drawn\n transition line exactly, no overlap and no gap. Where two rooms meet with no\n wall, split on the drawn transition line or the partition's centerline.\n7. Take the finish from the schedule row; a plan tag alone is a cross-check.\n8. After each ring, `view_sheet` a tight crop with `overlay: true` at a high\n `px` and look at every corner and notch before the next room; `edit_shape`\n fixes what the crop shows. A full-sheet render cannot audit a ring.\n\nWhen something is genuinely ambiguous, follow the rule most literally, carry it,\nand say so in the shape's label or an annotation. Do not stop.\n5. `takeoff_summary` and `export_report` check quantities and material coverage.\n Shorten notes through `list_annotations` \u2192 `edit_annotation` where permitted.\n6. Export editable takeoff JSON and a marked-set PDF, reopen the JSON against\n the same source, and check the handoff. Leave agent work pending for the\n human's review; exported files and agent verdicts do not create approval.\n\nThe [MCP route map](takeoff://wiki/mcp) gives the next tool at each step. The\n[agent guide](https://github.com/Kentucky-ai/opentakeoff/blob/main/docs/AGENT_GUIDE.md) covers staging and refusal recovery.\n\n## Evidence that another person can review\n\nKeep source/revision identifiers, calibration choices, measured geometry,\nexpected-versus-observed checks and an overlay or marked-set view. Compare\nspatial agreement as well as quantities. Disclose reference-assisted work;\nit is not a blind accuracy benchmark. Public PRs include screenshots, video or\nreproducible stats, using publishable fixtures. Private drawings and pricing stay\noutside the public repository. See the [repository guide](takeoff://wiki/repo-guide).\n"
|
|
15783
|
+
"source_sha256": "0281270b8f1a87532b6bf1aa1add8e4f39f4adb750a3676398d93b1ac4ee51c8",
|
|
15784
|
+
"text": "# Workflows\n\n## Human: open, stitch and measure\n\nOpen the plans and check their revision. Press **G** (or choose **Sheets \u2192 Open\ngallery\u2026**) to open the sheet gallery, then select the\n2\u20134 split sheets in left-to-right order and click **Stitch N into one surface**.\nOn the composite, choose **Align**, click a recognizable point near the joint,\nthen click the same drawn point on the other sheet. Check a second recognizable\npoint and the scale before tracing. Align translates a member; it does not\nrotate or resize it. Once shapes exist on a stitch, alignment is locked.\n\nIf the controls seem missing, **Stitch N into one surface** is in the gallery\nfooter and requires 2\u20134 selected sheets; **Align** appears only on an open\nstitched surface. Create a fresh stitch if Align refuses because takeoffs\nalready exist; deleting a stitch also refuses while takeoffs or markups exist.\n\nUse the composite as one measuring surface. Its marked-set page is labeled as\na composite, not an architect-issued sheet. Save the editable project archive\nwhen preserving the composite is required. MCP has no stitch/alignment tool and\nits current import/export path omits stitches: it cannot be used as a lossless\nhandoff for that document. Have an MCP agent measure source sheets individually\nwith explicit scope boundaries instead.\n\nSource: [Human stitching instructions](https://github.com/Kentucky-ai/opentakeoff/blob/main/docs/USER_GUIDE.md#stitching-a-floor-split-at-a-match-line),\n[stitch records](https://github.com/Kentucky-ai/opentakeoff/blob/main/web/src/lib/stitches.ts),\n[compatibility evidence](https://github.com/Kentucky-ai/opentakeoff/blob/main/protocol/COMPATIBILITY.md).\n\n## Agent: source to reviewed handoff\n\n1. `load_plan` \u2192 inspect sheets/revisions \u2192 `set_scale` on each measured sheet.\n Confirm the relevant detail's scale, which may differ from the overall plan.\n2. Read source text and vectors; inspect the matching region with `view_sheet`.\n Use each room's schedule evidence for its finish. Missing/ambiguous evidence\n is a qualification or RFI, not an inferred assignment.\n3. `propose_takeoff` names a batch; it creates no geometry. Measure small batches\n with the appropriate area, run, surface or count tool.\n4. Inspect overlays and actual boundaries, openings, jambs and deductions.\n `edit_shape` corrects pending shapes. Physical base gaps use explicit runs;\n stepped wall faces use separate height bands. See the [geometry workflow](https://github.com/Kentucky-ai/opentakeoff/blob/main/docs/GEOMETRY_WORKFLOW.md).\n\n### Trace a room the way an estimator does\n\nBlind agent runs against reviewed references showed that the ring, not the\ntotal, is what fails. These rules are what the references are drawn to:\n\n1. The boundary is the **innermost interior wall face**. Never the wall\n centerline, never the far face, never under a wall. Casework, counters,\n fixtures, equipment, hatch patterns, dimension strings, text and leaders\n never define the boundary; the finish runs under casework and fixtures.\n2. Corners sit where two adjacent wall-face strokes meet. Read the strokes with\n `get_sheet_vectors` over a tight region and put each vertex on a stroke; do\n not trace a hatch edge or a raster guess.\n3. At every **door or cased opening** the ring follows the face to the jamb,\n turns into the opening, runs across it on the **wall centerline** (midway\n between that wall's two faces, the full wall on a composite wall), and\n returns along the far jamb. Both rooms sharing the door share that segment.\n A door leaf and its swing arc are never part of the boundary; a leaf drawn\n standing open looks like a wall line and is not one.\n4. **Windows** and other non-passable openings do not break the ring.\n5. Columns, chases, pilasters and wall stubs that project into the room are\n traced around; an enclosed cell drawn with wall-weight lines is not floor.\n6. A **finish split** inside one room is two rings sharing the drawn\n transition line exactly, no overlap and no gap. Where two rooms meet with no\n wall, split on the drawn transition line or the partition's centerline.\n7. Take the finish from the schedule row; a plan tag alone is a cross-check.\n8. After each ring, `view_sheet` a tight crop with `overlay: true` at a high\n `px` and look at every corner and notch before the next room; `edit_shape`\n fixes what the crop shows. A full-sheet render cannot audit a ring.\n9. A **curved wall is a circle**: the architect drew it with a center and a\n radius. Never chord it and never hand-place a run of points along it. Give\n the bow one point anywhere on the wall face between the arc's two ends and\n list that point's index in `arc_through` on `measure_polygon`,\n `measure_line` or `measure_surface`; the server lays the unique circle\n through the three and bakes it to vertices, exactly as the canvas's Curve\n mode does. `get_sheet_vectors` flags curve chords in its `meta` byte (bit 1)\n \u2014 a window full of them is a radius wall, so read the bow from a render.\n\nWhen something is genuinely ambiguous, follow the rule most literally, carry it,\nand say so in the shape's label or an annotation. Do not stop.\n5. `takeoff_summary` and `export_report` check quantities and material coverage.\n Shorten notes through `list_annotations` \u2192 `edit_annotation` where permitted.\n6. Export editable takeoff JSON and a marked-set PDF, reopen the JSON against\n the same source, and check the handoff. Leave agent work pending for the\n human's review; exported files and agent verdicts do not create approval.\n\nThe [MCP route map](takeoff://wiki/mcp) gives the next tool at each step. The\n[agent guide](https://github.com/Kentucky-ai/opentakeoff/blob/main/docs/AGENT_GUIDE.md) covers staging and refusal recovery.\n\n## Evidence that another person can review\n\nKeep source/revision identifiers, calibration choices, measured geometry,\nexpected-versus-observed checks and an overlay or marked-set view. Compare\nspatial agreement as well as quantities. Disclose reference-assisted work;\nit is not a blind accuracy benchmark. Public PRs include screenshots, video or\nreproducible stats, using publishable fixtures. Private drawings and pricing stay\noutside the public repository. See the [repository guide](takeoff://wiki/repo-guide).\n"
|
|
15182
15785
|
},
|
|
15183
15786
|
{
|
|
15184
15787
|
"key": "mcp",
|
|
@@ -15215,7 +15818,7 @@ var WIKI_PAGES = [
|
|
|
15215
15818
|
];
|
|
15216
15819
|
|
|
15217
15820
|
// src/protocol.generated.ts
|
|
15218
|
-
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": "
|
|
15821
|
+
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';
|
|
15219
15822
|
var PROTOCOL_SCHEMAS = [
|
|
15220
15823
|
{
|
|
15221
15824
|
"uri": "takeoff://protocol/legacy/takeoff-canvas.v1.schema.json",
|
|
@@ -15238,16 +15841,93 @@ var PROTOCOL_SCHEMAS = [
|
|
|
15238
15841
|
"id": "https://opentakeoff.kentucky-ai.com/protocol/v1/common.schema.json",
|
|
15239
15842
|
"title": "Shared value types (draft)",
|
|
15240
15843
|
"source": "protocol/v1/common.schema.json",
|
|
15241
|
-
"sha256": "
|
|
15242
|
-
"schema_json":
|
|
15844
|
+
"sha256": "2c929da4d7db6af6f6d8d20d354bfb11216db6fd6edd04847fb6a0a4405e5539",
|
|
15845
|
+
"schema_json": `{
|
|
15846
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
15847
|
+
"$id": "https://opentakeoff.kentucky-ai.com/protocol/v1/common.schema.json",
|
|
15848
|
+
"title": "Shared value types (draft)",
|
|
15849
|
+
"$defs": {
|
|
15850
|
+
"point": {
|
|
15851
|
+
"type": "array",
|
|
15852
|
+
"prefixItems": [
|
|
15853
|
+
{
|
|
15854
|
+
"type": "number"
|
|
15855
|
+
},
|
|
15856
|
+
{
|
|
15857
|
+
"type": "number"
|
|
15858
|
+
}
|
|
15859
|
+
],
|
|
15860
|
+
"items": false,
|
|
15861
|
+
"minItems": 2,
|
|
15862
|
+
"maxItems": 2,
|
|
15863
|
+
"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."
|
|
15864
|
+
},
|
|
15865
|
+
"timestamp": {
|
|
15866
|
+
"type": "string",
|
|
15867
|
+
"format": "date-time"
|
|
15868
|
+
},
|
|
15869
|
+
"computed": {
|
|
15870
|
+
"type": "object",
|
|
15871
|
+
"properties": {
|
|
15872
|
+
"area_sf": {
|
|
15873
|
+
"type": "number"
|
|
15874
|
+
},
|
|
15875
|
+
"perimeter_lf": {
|
|
15876
|
+
"type": "number"
|
|
15877
|
+
},
|
|
15878
|
+
"count": {
|
|
15879
|
+
"type": "number"
|
|
15880
|
+
},
|
|
15881
|
+
"plan_lf": {
|
|
15882
|
+
"type": "number",
|
|
15883
|
+
"description": "Linear runs with a vertical leg: the flat X-Y trace alone. perimeter_lf is then plan_lf + vertical_lf."
|
|
15884
|
+
},
|
|
15885
|
+
"vertical_lf": {
|
|
15886
|
+
"type": "number",
|
|
15887
|
+
"description": "Linear runs with a vertical leg: rise + drop in feet."
|
|
15888
|
+
}
|
|
15889
|
+
},
|
|
15890
|
+
"additionalProperties": true,
|
|
15891
|
+
"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."
|
|
15892
|
+
},
|
|
15893
|
+
"parentSnapshot": {
|
|
15894
|
+
"type": "object",
|
|
15895
|
+
"properties": {
|
|
15896
|
+
"verts_norm": {
|
|
15897
|
+
"type": "array",
|
|
15898
|
+
"items": {
|
|
15899
|
+
"$ref": "common.schema.json#/$defs/point"
|
|
15900
|
+
}
|
|
15901
|
+
},
|
|
15902
|
+
"verts_norm_holes": {
|
|
15903
|
+
"type": "array",
|
|
15904
|
+
"items": {
|
|
15905
|
+
"type": "array",
|
|
15906
|
+
"items": {
|
|
15907
|
+
"$ref": "common.schema.json#/$defs/point"
|
|
15908
|
+
}
|
|
15909
|
+
}
|
|
15910
|
+
},
|
|
15911
|
+
"computed": {
|
|
15912
|
+
"$ref": "#/$defs/computed"
|
|
15913
|
+
}
|
|
15914
|
+
},
|
|
15915
|
+
"required": [
|
|
15916
|
+
"verts_norm"
|
|
15917
|
+
],
|
|
15918
|
+
"additionalProperties": true
|
|
15919
|
+
}
|
|
15920
|
+
}
|
|
15921
|
+
}
|
|
15922
|
+
`
|
|
15243
15923
|
},
|
|
15244
15924
|
{
|
|
15245
15925
|
"uri": "takeoff://protocol/v1/condition.schema.json",
|
|
15246
15926
|
"id": "https://opentakeoff.kentucky-ai.com/protocol/v1/condition.schema.json",
|
|
15247
15927
|
"title": "Condition (draft)",
|
|
15248
15928
|
"source": "protocol/v1/condition.schema.json",
|
|
15249
|
-
"sha256": "
|
|
15250
|
-
"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'
|
|
15929
|
+
"sha256": "c45284ba2c390bb0b0be9208f1b51fd37d969184956aa73b1b1e5276109aca86",
|
|
15930
|
+
"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'
|
|
15251
15931
|
},
|
|
15252
15932
|
{
|
|
15253
15933
|
"uri": "takeoff://protocol/v1/document-fields.schema.json",
|
|
@@ -15270,8 +15950,104 @@ var PROTOCOL_SCHEMAS = [
|
|
|
15270
15950
|
"id": "https://opentakeoff.kentucky-ai.com/protocol/v1/measurement.schema.json",
|
|
15271
15951
|
"title": "Measurement (draft)",
|
|
15272
15952
|
"source": "protocol/v1/measurement.schema.json",
|
|
15273
|
-
"sha256": "
|
|
15274
|
-
"schema_json":
|
|
15953
|
+
"sha256": "0e1b050d38963a1256f3647922ca90031eb04998d78e0f53bb3fb577bf590648",
|
|
15954
|
+
"schema_json": `{
|
|
15955
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
15956
|
+
"$id": "https://opentakeoff.kentucky-ai.com/protocol/v1/measurement.schema.json",
|
|
15957
|
+
"title": "Measurement (draft)",
|
|
15958
|
+
"type": "object",
|
|
15959
|
+
"properties": {
|
|
15960
|
+
"id": {
|
|
15961
|
+
"type": "string",
|
|
15962
|
+
"minLength": 1
|
|
15963
|
+
},
|
|
15964
|
+
"sheet_id": {
|
|
15965
|
+
"type": "string",
|
|
15966
|
+
"minLength": 1
|
|
15967
|
+
},
|
|
15968
|
+
"condition_id": {
|
|
15969
|
+
"type": "string",
|
|
15970
|
+
"minLength": 1
|
|
15971
|
+
},
|
|
15972
|
+
"measure_role": {
|
|
15973
|
+
"enum": [
|
|
15974
|
+
"floor_area",
|
|
15975
|
+
"deduct",
|
|
15976
|
+
"linear",
|
|
15977
|
+
"surface_area",
|
|
15978
|
+
"count"
|
|
15979
|
+
]
|
|
15980
|
+
},
|
|
15981
|
+
"verts_norm": {
|
|
15982
|
+
"type": "array",
|
|
15983
|
+
"items": {
|
|
15984
|
+
"$ref": "common.schema.json#/$defs/point"
|
|
15985
|
+
},
|
|
15986
|
+
"minItems": 1
|
|
15987
|
+
},
|
|
15988
|
+
"verts_norm_holes": {
|
|
15989
|
+
"type": "array",
|
|
15990
|
+
"items": {
|
|
15991
|
+
"type": "array",
|
|
15992
|
+
"items": {
|
|
15993
|
+
"$ref": "common.schema.json#/$defs/point"
|
|
15994
|
+
}
|
|
15995
|
+
}
|
|
15996
|
+
},
|
|
15997
|
+
"computed": {
|
|
15998
|
+
"$ref": "common.schema.json#/$defs/computed"
|
|
15999
|
+
},
|
|
16000
|
+
"height_ft": {
|
|
16001
|
+
"type": "number"
|
|
16002
|
+
},
|
|
16003
|
+
"rise_ft": {
|
|
16004
|
+
"type": "number",
|
|
16005
|
+
"minimum": 0,
|
|
16006
|
+
"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."
|
|
16007
|
+
},
|
|
16008
|
+
"drop_ft": {
|
|
16009
|
+
"type": "number",
|
|
16010
|
+
"minimum": 0,
|
|
16011
|
+
"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."
|
|
16012
|
+
},
|
|
16013
|
+
"label": {
|
|
16014
|
+
"type": "string"
|
|
16015
|
+
},
|
|
16016
|
+
"curved": {
|
|
16017
|
+
"type": "boolean"
|
|
16018
|
+
},
|
|
16019
|
+
"cuts_shape_id": {
|
|
16020
|
+
"type": "string",
|
|
16021
|
+
"minLength": 1
|
|
16022
|
+
},
|
|
16023
|
+
"origin": {
|
|
16024
|
+
"$ref": "provenance.schema.json"
|
|
16025
|
+
},
|
|
16026
|
+
"created_at": {
|
|
16027
|
+
"$ref": "common.schema.json#/$defs/timestamp"
|
|
16028
|
+
},
|
|
16029
|
+
"updated_at": {
|
|
16030
|
+
"$ref": "common.schema.json#/$defs/timestamp"
|
|
16031
|
+
},
|
|
16032
|
+
"author": {
|
|
16033
|
+
"type": "string"
|
|
16034
|
+
},
|
|
16035
|
+
"updated_by": {
|
|
16036
|
+
"type": "string"
|
|
16037
|
+
}
|
|
16038
|
+
},
|
|
16039
|
+
"required": [
|
|
16040
|
+
"id",
|
|
16041
|
+
"sheet_id",
|
|
16042
|
+
"condition_id",
|
|
16043
|
+
"measure_role",
|
|
16044
|
+
"verts_norm",
|
|
16045
|
+
"computed"
|
|
16046
|
+
],
|
|
16047
|
+
"additionalProperties": true,
|
|
16048
|
+
"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."
|
|
16049
|
+
}
|
|
16050
|
+
`
|
|
15275
16051
|
},
|
|
15276
16052
|
{
|
|
15277
16053
|
"uri": "takeoff://protocol/v1/provenance.schema.json",
|
|
@@ -15576,7 +16352,7 @@ function nameTheStageInRefusals(server) {
|
|
|
15576
16352
|
// package.json
|
|
15577
16353
|
var package_default = {
|
|
15578
16354
|
name: "opentakeoff-mcp",
|
|
15579
|
-
version: "0.9.
|
|
16355
|
+
version: "0.9.90",
|
|
15580
16356
|
mcpName: "io.github.Kentucky-ai/opentakeoff",
|
|
15581
16357
|
type: "module",
|
|
15582
16358
|
description: "OpenTakeoff MCP server \u2014 drive the takeoff engine from your MCP client over stdio.",
|
|
@@ -15596,7 +16372,7 @@ var package_default = {
|
|
|
15596
16372
|
"check:wiki": "node scripts/check-wiki.mjs",
|
|
15597
16373
|
"check:versions": "node ../scripts/check-version-consistency.mjs",
|
|
15598
16374
|
"check:tool-count": "node --import tsx scripts/check-tool-count.mjs",
|
|
15599
|
-
test: "node --import tsx --test 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"
|
|
16375
|
+
test: "node --import tsx --test test/arc.test.ts test/checkers.test.ts test/conformance.test.ts test/constants.test.ts test/context.test.ts test/document.test.ts test/dxf.test.ts test/e2e.test.ts test/gate.test.ts test/graphEvalRowsym.test.ts test/labels.test.ts test/mep.test.ts test/overlap.test.ts test/parity.test.ts test/proposals.test.ts test/raster.test.ts test/resources.test.ts test/safewrite.test.ts test/scalewarn.test.ts test/scope.test.ts test/session.test.ts test/splittags.test.ts test/staging.test.ts test/sweepguard.test.ts test/tools.test.ts test/transitions.test.ts test/twins.test.ts test/view.test.ts test/wiki.test.ts"
|
|
15600
16376
|
},
|
|
15601
16377
|
dependencies: {
|
|
15602
16378
|
"@modelcontextprotocol/sdk": "^1.12.0",
|