opentakeoff-mcp 0.9.85 → 0.9.87
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 +359 -109
- package/package.json +2 -2
package/dist/server-core.js
CHANGED
|
@@ -1461,8 +1461,8 @@ function sweepHatchRuns(segs, meta, ws) {
|
|
|
1461
1461
|
clusters.pop();
|
|
1462
1462
|
}
|
|
1463
1463
|
}
|
|
1464
|
-
const median2 = (
|
|
1465
|
-
const a =
|
|
1464
|
+
const median2 = (arr3) => {
|
|
1465
|
+
const a = arr3.slice().sort((x, y) => x - y);
|
|
1466
1466
|
return a[a.length >> 1];
|
|
1467
1467
|
};
|
|
1468
1468
|
for (const members of clusters) {
|
|
@@ -1590,8 +1590,8 @@ function classifyOffsetAnnotationSegs(segs, meta, ws, maxOffsetPx, minOffsetPx,
|
|
|
1590
1590
|
t1: Math.max(c.x1 * dxu + c.y1 * dyu, c.x2 * dxu + c.y2 * dyu),
|
|
1591
1591
|
w: c.w
|
|
1592
1592
|
};
|
|
1593
|
-
const
|
|
1594
|
-
if (
|
|
1593
|
+
const arr3 = bins.get(b);
|
|
1594
|
+
if (arr3) arr3.push(r);
|
|
1595
1595
|
else bins.set(b, [r]);
|
|
1596
1596
|
}
|
|
1597
1597
|
for (const runs of bins.values()) {
|
|
@@ -1768,8 +1768,8 @@ function sweepDimensionStrings(segs, meta, ws, ftPx, rejects, dimTexts) {
|
|
|
1768
1768
|
t0: Math.min(c.x1 * dx + c.y1 * dy, c.x2 * dx + c.y2 * dy),
|
|
1769
1769
|
t1: Math.max(c.x1 * dx + c.y1 * dy, c.x2 * dx + c.y2 * dy)
|
|
1770
1770
|
};
|
|
1771
|
-
const
|
|
1772
|
-
if (
|
|
1771
|
+
const arr3 = bins.get(b);
|
|
1772
|
+
if (arr3) arr3.push(r);
|
|
1773
1773
|
else bins.set(b, [r]);
|
|
1774
1774
|
}
|
|
1775
1775
|
for (const [b, rows] of bins) {
|
|
@@ -6096,6 +6096,73 @@ function labelPlacements(placements, spans, segs, lum) {
|
|
|
6096
6096
|
return out;
|
|
6097
6097
|
}
|
|
6098
6098
|
|
|
6099
|
+
// ../web/src/lib/arc.js
|
|
6100
|
+
var TAU = Math.PI * 2;
|
|
6101
|
+
var norm2 = (a) => (a % TAU + TAU) % TAU;
|
|
6102
|
+
function circleThrough(a, m, b) {
|
|
6103
|
+
const [ax, ay] = a, [mx, my] = m, [bx, by] = b;
|
|
6104
|
+
const d = 2 * (ax * (my - by) + mx * (by - ay) + bx * (ay - my));
|
|
6105
|
+
if (!Number.isFinite(d) || Math.abs(d) < 1e-9) return null;
|
|
6106
|
+
const a2 = ax * ax + ay * ay, m2 = mx * mx + my * my, b2 = bx * bx + by * by;
|
|
6107
|
+
const cx = (a2 * (my - by) + m2 * (by - ay) + b2 * (ay - my)) / d;
|
|
6108
|
+
const cy = (a2 * (bx - mx) + m2 * (ax - bx) + b2 * (mx - ax)) / d;
|
|
6109
|
+
const r = Math.hypot(ax - cx, ay - cy);
|
|
6110
|
+
if (!Number.isFinite(r) || r > 1e7) return null;
|
|
6111
|
+
const a0 = Math.atan2(ay - cy, ax - cx);
|
|
6112
|
+
const a1 = Math.atan2(by - cy, bx - cx);
|
|
6113
|
+
const am = Math.atan2(my - cy, mx - cx);
|
|
6114
|
+
const fwd = norm2(a1 - a0);
|
|
6115
|
+
const fwdM = norm2(am - a0);
|
|
6116
|
+
const sweep = fwdM <= fwd ? fwd : fwd - TAU;
|
|
6117
|
+
return { cx, cy, r, a0, a1, am, sweep };
|
|
6118
|
+
}
|
|
6119
|
+
function stepsFor(r, sweep) {
|
|
6120
|
+
const len = Math.abs(sweep) * r;
|
|
6121
|
+
const bySpan = Math.abs(sweep) / (4 * Math.PI / 180);
|
|
6122
|
+
return Math.max(8, Math.min(96, Math.ceil(Math.max(len / 4, bySpan))));
|
|
6123
|
+
}
|
|
6124
|
+
function flattenArc(a, m, b) {
|
|
6125
|
+
const c = circleThrough(a, m, b);
|
|
6126
|
+
if (!c) return [];
|
|
6127
|
+
const total = stepsFor(c.r, c.sweep);
|
|
6128
|
+
const toM = c.sweep >= 0 ? norm2(c.am - c.a0) : -norm2(c.a0 - c.am);
|
|
6129
|
+
const f = Math.abs(c.sweep) > 1e-12 ? Math.min(1, Math.abs(toM) / Math.abs(c.sweep)) : 0;
|
|
6130
|
+
const n1 = Math.max(1, Math.min(total - 1, Math.round(total * f)));
|
|
6131
|
+
const n2 = Math.max(1, total - n1);
|
|
6132
|
+
const at = (t) => [c.cx + c.r * Math.cos(t), c.cy + c.r * Math.sin(t)];
|
|
6133
|
+
const out = [];
|
|
6134
|
+
for (let i = 1; i < n1; i++) out.push(at(c.a0 + toM * (i / n1)));
|
|
6135
|
+
out.push([m[0], m[1]]);
|
|
6136
|
+
for (let i = 1; i < n2; i++) out.push(at(c.am + (c.sweep - toM) * (i / n2)));
|
|
6137
|
+
return out;
|
|
6138
|
+
}
|
|
6139
|
+
function flattenArcRing(pts, throughAt, closed2 = true) {
|
|
6140
|
+
const src = pts || [];
|
|
6141
|
+
const n = src.length;
|
|
6142
|
+
const copy = () => src.map((p) => [p[0], p[1]]);
|
|
6143
|
+
const marks = throughAt instanceof Set ? throughAt : new Set(throughAt || []);
|
|
6144
|
+
if (n < 3 || !marks.size) return copy();
|
|
6145
|
+
const live = new Array(n).fill(false);
|
|
6146
|
+
for (let i = 0; i < n; i++) {
|
|
6147
|
+
if (!marks.has(i)) continue;
|
|
6148
|
+
const hasPrev = closed2 || i > 0;
|
|
6149
|
+
const hasNext = closed2 || i < n - 1;
|
|
6150
|
+
if (!hasPrev || !hasNext) continue;
|
|
6151
|
+
const pi = (i - 1 + n) % n, ni = (i + 1) % n;
|
|
6152
|
+
if (marks.has(pi) || marks.has(ni)) continue;
|
|
6153
|
+
live[i] = true;
|
|
6154
|
+
}
|
|
6155
|
+
const out = [];
|
|
6156
|
+
for (let i = 0; i < n; i++) {
|
|
6157
|
+
if (!live[i]) {
|
|
6158
|
+
out.push([src[i][0], src[i][1]]);
|
|
6159
|
+
continue;
|
|
6160
|
+
}
|
|
6161
|
+
for (const p of flattenArc(src[(i - 1 + n) % n], src[i], src[(i + 1) % n])) out.push(p);
|
|
6162
|
+
}
|
|
6163
|
+
return out;
|
|
6164
|
+
}
|
|
6165
|
+
|
|
6099
6166
|
// ../web/src/lib/shapeMetrics.js
|
|
6100
6167
|
function computeShapeMetrics(s, dims, upp, cond) {
|
|
6101
6168
|
const pts = (s.verts_norm || []).map(([nx, ny]) => [nx * dims.w, ny * dims.h]);
|
|
@@ -6658,12 +6725,12 @@ function applyRuleToProject(rule, shapes, sheetData) {
|
|
|
6658
6725
|
const d = sheetData.get(s.sheet_id);
|
|
6659
6726
|
if (!d) continue;
|
|
6660
6727
|
const ring = s.verts_norm.map(([nx, ny]) => [nx * d.imgW, ny * d.imgH]);
|
|
6661
|
-
let
|
|
6662
|
-
if (!
|
|
6663
|
-
|
|
6664
|
-
deductsBySheet.set(s.sheet_id,
|
|
6728
|
+
let arr3 = deductsBySheet.get(s.sheet_id);
|
|
6729
|
+
if (!arr3) {
|
|
6730
|
+
arr3 = [];
|
|
6731
|
+
deductsBySheet.set(s.sheet_id, arr3);
|
|
6665
6732
|
}
|
|
6666
|
-
|
|
6733
|
+
arr3.push(ring);
|
|
6667
6734
|
}
|
|
6668
6735
|
const out = [];
|
|
6669
6736
|
const acceptedBySheet = /* @__PURE__ */ new Map();
|
|
@@ -6688,12 +6755,12 @@ function applyRuleToProject(rule, shapes, sheetData) {
|
|
|
6688
6755
|
verts_norm: ring.map(([x, y]) => [x / d.imgW, y / d.imgH]),
|
|
6689
6756
|
area_sf: +areaSf.toFixed(2)
|
|
6690
6757
|
});
|
|
6691
|
-
let
|
|
6692
|
-
if (!
|
|
6693
|
-
|
|
6694
|
-
acceptedBySheet.set(room.sheet_id,
|
|
6758
|
+
let arr3 = acceptedBySheet.get(room.sheet_id);
|
|
6759
|
+
if (!arr3) {
|
|
6760
|
+
arr3 = [];
|
|
6761
|
+
acceptedBySheet.set(room.sheet_id, arr3);
|
|
6695
6762
|
}
|
|
6696
|
-
|
|
6763
|
+
arr3.push(ring);
|
|
6697
6764
|
}
|
|
6698
6765
|
}
|
|
6699
6766
|
return out;
|
|
@@ -7712,11 +7779,82 @@ function drawMarks(ctx, toCanvas, marks, longEdge) {
|
|
|
7712
7779
|
return drawn;
|
|
7713
7780
|
}
|
|
7714
7781
|
|
|
7782
|
+
// ../web/src/lib/takeoffDocument.js
|
|
7783
|
+
var arr = (v) => Array.isArray(v) ? v : [];
|
|
7784
|
+
var obj = (v) => v && typeof v === "object" && !Array.isArray(v) ? v : {};
|
|
7785
|
+
var TAKEOFF_DOCUMENT_KEYS = Object.freeze([
|
|
7786
|
+
"schema",
|
|
7787
|
+
"project_name",
|
|
7788
|
+
"units",
|
|
7789
|
+
"client_info",
|
|
7790
|
+
"sheets",
|
|
7791
|
+
"conditions",
|
|
7792
|
+
"condition_columns",
|
|
7793
|
+
"shape_labels",
|
|
7794
|
+
"palette",
|
|
7795
|
+
"shapes",
|
|
7796
|
+
"markups",
|
|
7797
|
+
"rfis",
|
|
7798
|
+
"approvals",
|
|
7799
|
+
"proposals",
|
|
7800
|
+
"condition_edit_proposals",
|
|
7801
|
+
"rules",
|
|
7802
|
+
"sheet_group",
|
|
7803
|
+
"last_group",
|
|
7804
|
+
"sheet_tabs",
|
|
7805
|
+
"stitches",
|
|
7806
|
+
"sheet_levels",
|
|
7807
|
+
"layer_overrides",
|
|
7808
|
+
"provenance_counters"
|
|
7809
|
+
]);
|
|
7810
|
+
function sheetEntry({ sheet_id, units_per_px, scale_source = void 0, scale_confirmed = void 0 }) {
|
|
7811
|
+
return {
|
|
7812
|
+
sheet_id,
|
|
7813
|
+
units_per_px,
|
|
7814
|
+
...scale_source ? { scale_source } : {},
|
|
7815
|
+
...scale_confirmed === false ? { scale_confirmed: false } : {}
|
|
7816
|
+
};
|
|
7817
|
+
}
|
|
7818
|
+
function buildTakeoffDocument(f = {}) {
|
|
7819
|
+
const conditions = arr(f.conditions);
|
|
7820
|
+
const pinned = arr(f.palette).filter((id) => conditions.some((c) => c && c.id === id));
|
|
7821
|
+
const clientInfo = obj(f.client_info);
|
|
7822
|
+
const hasClient = Object.values(clientInfo).some((v) => v && String(v).trim());
|
|
7823
|
+
const sheetLevels = obj(f.sheet_levels);
|
|
7824
|
+
const layerOverrides = obj(f.layer_overrides);
|
|
7825
|
+
const prov = f.provenance_counters;
|
|
7826
|
+
const hasProv = !!(prov && prov.shapes_deleted && Object.keys(prov.shapes_deleted).length);
|
|
7827
|
+
return {
|
|
7828
|
+
schema: TAKEOFF_SCHEMA,
|
|
7829
|
+
project_name: typeof f.project_name === "string" ? f.project_name : "",
|
|
7830
|
+
...f.units === "metric" ? { units: "metric" } : {},
|
|
7831
|
+
...hasClient ? { client_info: clientInfo } : {},
|
|
7832
|
+
sheets: arr(f.sheets),
|
|
7833
|
+
conditions,
|
|
7834
|
+
...arr(f.condition_columns).length ? { condition_columns: f.condition_columns } : {},
|
|
7835
|
+
...arr(f.shape_labels).length ? { shape_labels: f.shape_labels } : {},
|
|
7836
|
+
...pinned.length ? { palette: pinned } : {},
|
|
7837
|
+
shapes: arr(f.shapes),
|
|
7838
|
+
markups: arr(f.markups),
|
|
7839
|
+
rfis: arr(f.rfis),
|
|
7840
|
+
...arr(f.approvals).length ? { approvals: f.approvals } : {},
|
|
7841
|
+
...arr(f.proposals).length ? { proposals: f.proposals } : {},
|
|
7842
|
+
...arr(f.condition_edit_proposals).length ? { condition_edit_proposals: f.condition_edit_proposals } : {},
|
|
7843
|
+
...arr(f.rules).length ? { rules: f.rules } : {},
|
|
7844
|
+
sheet_group: arr(f.sheet_group),
|
|
7845
|
+
last_group: arr(f.last_group),
|
|
7846
|
+
sheet_tabs: arr(f.sheet_tabs),
|
|
7847
|
+
...arr(f.stitches).length ? { stitches: f.stitches } : {},
|
|
7848
|
+
...Object.keys(sheetLevels).length ? { sheet_levels: sheetLevels } : {},
|
|
7849
|
+
...Object.keys(layerOverrides).length ? { layer_overrides: layerOverrides } : {},
|
|
7850
|
+
...hasProv ? { provenance_counters: prov } : {}
|
|
7851
|
+
};
|
|
7852
|
+
}
|
|
7853
|
+
|
|
7715
7854
|
// src/session.ts
|
|
7716
7855
|
var mintUuid2 = () => globalThis.crypto && typeof globalThis.crypto.randomUUID === "function" ? globalThis.crypto.randomUUID() : `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`;
|
|
7717
7856
|
var uid = (p) => `${p}-${mintUuid2()}`;
|
|
7718
7857
|
var nowIso2 = () => (/* @__PURE__ */ new Date()).toISOString();
|
|
7719
|
-
var ANN_SCHEMA = TAKEOFF_SCHEMA;
|
|
7720
7858
|
var sanitizeApprovals2 = sanitizeApprovals;
|
|
7721
7859
|
var applyApprovalCommand2 = applyApprovalCommand;
|
|
7722
7860
|
var CONTEXT_MIN_LEN_PX = 2;
|
|
@@ -8430,6 +8568,7 @@ var Session = class _Session {
|
|
|
8430
8568
|
const lc = nextPaletteColor(this.conditions.length);
|
|
8431
8569
|
c = {
|
|
8432
8570
|
id: uid("cnd"),
|
|
8571
|
+
created_at: nowIso2(),
|
|
8433
8572
|
finish_tag: tag,
|
|
8434
8573
|
color: lc,
|
|
8435
8574
|
fill: lc,
|
|
@@ -8768,24 +8907,53 @@ var Session = class _Session {
|
|
|
8768
8907
|
...s.upp == null ? { warning: `No scale set for ${s.key} \u2014 quantities unavailable. Call set_scale${s.detected ? ` (detected: ${s.detected.label})` : ""}.` } : {}
|
|
8769
8908
|
};
|
|
8770
8909
|
}
|
|
8910
|
+
/** Bend a trace the way the canvas's Curve mode does (#284): `arcThrough`
|
|
8911
|
+
* lists the indices of points that are the MIDDLE of a three-point arc —
|
|
8912
|
+
* the boundary runs pts[i-1] → pts[i] → pts[i+1] as the unique circle
|
|
8913
|
+
* through those three instead of two chords. The arc is baked to ordinary
|
|
8914
|
+
* vertices here (the canvas's flattenArcRing, same steps, same budget), so
|
|
8915
|
+
* SF/LF, the marked set, edit_shape and every export keep seeing a plain
|
|
8916
|
+
* polygon — and the shape's origin carries `curved: true`, the canvas's own
|
|
8917
|
+
* stamp. Refusal over guessing: an index off the trace, a bow point at the
|
|
8918
|
+
* end of an open run (no far corner to bend to), or two bows in a row (no
|
|
8919
|
+
* clean triple) refuse whole rather than silently demoting to a corner. */
|
|
8920
|
+
bend(pts, arcThrough, closed2, tool) {
|
|
8921
|
+
if (!arcThrough?.length) return { pts, arcs: 0 };
|
|
8922
|
+
const n = pts.length;
|
|
8923
|
+
const marks = [...new Set(arcThrough)].sort((a, b) => a - b);
|
|
8924
|
+
for (const i of marks) {
|
|
8925
|
+
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}).`);
|
|
8926
|
+
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.`);
|
|
8927
|
+
}
|
|
8928
|
+
for (let k = 1; k < marks.length; k++) {
|
|
8929
|
+
if (marks[k] - marks[k - 1] === 1 || closed2 && marks[0] === 0 && marks[marks.length - 1] === n - 1) {
|
|
8930
|
+
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.`);
|
|
8931
|
+
}
|
|
8932
|
+
}
|
|
8933
|
+
if (n < 3) throw new UserError(`${tool}: an arc needs three points (start, bow, far end); got ${n}.`);
|
|
8934
|
+
const flat = flattenArcRing(pts, marks, closed2);
|
|
8935
|
+
return { pts: flat, arcs: marks.length };
|
|
8936
|
+
}
|
|
8771
8937
|
measurePolygon(name, verts, opts) {
|
|
8772
8938
|
const s = this.sheet(name);
|
|
8773
8939
|
if (s.upp == null) throw new UserError(this.scaleGate(s));
|
|
8774
|
-
const {
|
|
8940
|
+
const { pts: ring, arcs } = this.bend(verts, opts.arc_through, true, "measure_polygon");
|
|
8941
|
+
const { area_sf = 0, perimeter_lf = 0 } = this.quantify(s, opts.role, ring);
|
|
8775
8942
|
let shape_id;
|
|
8776
|
-
if (opts.condition) shape_id = this.commit(s, opts.condition, opts.role,
|
|
8943
|
+
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;
|
|
8777
8944
|
this.flushCommits("measure_polygon");
|
|
8778
|
-
const mixed = this.scaleWarningFor(s,
|
|
8779
|
-
return { area_sf, perimeter_lf, nverts:
|
|
8945
|
+
const mixed = this.scaleWarningFor(s, ring);
|
|
8946
|
+
return { area_sf, perimeter_lf, nverts: ring.length, ...arcs ? { arcs } : {}, ...shape_id ? { shape_id } : {}, ...mixed ? { warning: mixed } : {} };
|
|
8780
8947
|
}
|
|
8781
8948
|
measureLine(name, pts, opts) {
|
|
8782
8949
|
const s = this.sheet(name);
|
|
8783
8950
|
if (s.upp == null) throw new UserError(this.scaleGate(s));
|
|
8784
|
-
const {
|
|
8951
|
+
const { pts: run2, arcs } = this.bend(pts, opts.arc_through, false, "measure_line");
|
|
8952
|
+
const { perimeter_lf: length_lf = 0 } = this.quantify(s, "linear", run2);
|
|
8785
8953
|
let shape_id;
|
|
8786
|
-
if (opts.condition) shape_id = this.commit(s, opts.condition, "linear",
|
|
8954
|
+
if (opts.condition) shape_id = this.commit(s, opts.condition, "linear", run2, { area_sf: 0, perimeter_lf: length_lf }, { method: "manual", actor: "agent", ...arcs ? { curved: true } : {} }).id;
|
|
8787
8955
|
this.flushCommits("measure_line");
|
|
8788
|
-
return { length_lf, npts:
|
|
8956
|
+
return { length_lf, npts: run2.length, ...arcs ? { arcs } : {}, ...shape_id ? { shape_id } : {} };
|
|
8789
8957
|
}
|
|
8790
8958
|
/** Surface Area — the canvas's Surface tool (commitSurface): an OPEN run
|
|
8791
8959
|
* traced along the wall in plan view, quantified as traced LF × height.
|
|
@@ -8796,6 +8964,8 @@ var Session = class _Session {
|
|
|
8796
8964
|
measureSurface(name, pts, opts) {
|
|
8797
8965
|
const s = this.sheet(name);
|
|
8798
8966
|
if (s.upp == null) throw new UserError(this.scaleGate(s));
|
|
8967
|
+
const bent = this.bend(pts, opts.arc_through, false, "measure_surface");
|
|
8968
|
+
pts = bent.pts;
|
|
8799
8969
|
const existing = this.conditions.find((x) => x.finish_tag === opts.condition);
|
|
8800
8970
|
const h = opts.height_ft ?? (Number(existing?.height_ft) || 0);
|
|
8801
8971
|
if (!(h > 0)) {
|
|
@@ -8809,8 +8979,9 @@ var Session = class _Session {
|
|
|
8809
8979
|
const LF = openLen(pts) * s.upp;
|
|
8810
8980
|
const shape = this.commit(s, opts.condition, "surface_area", pts, { area_sf: round2(LF * h), perimeter_lf: round2(LF) }, { method: "manual", actor: "agent" });
|
|
8811
8981
|
shape.height_ft = h;
|
|
8982
|
+
if (bent.arcs && shape.origin) shape.origin = { ...shape.origin, curved: true };
|
|
8812
8983
|
this.flushCommits("measure_surface");
|
|
8813
|
-
return { condition: c.finish_tag, height_ft: h, length_lf: round2(LF), area_sf: round2(LF * h), npts: pts.length, shape_id: shape.id };
|
|
8984
|
+
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 };
|
|
8814
8985
|
}
|
|
8815
8986
|
// ── Proposals (#365) ──────────────────────────────────────────────────────
|
|
8816
8987
|
// A shape an agent commits lands reviewed:false and the estimator accepts
|
|
@@ -9463,10 +9634,10 @@ var Session = class _Session {
|
|
|
9463
9634
|
const base = chain[0].origin.parent_prev;
|
|
9464
9635
|
const r = recomposeCutouts(px(base.verts_norm), (base.verts_norm_holes ?? []).map(px), rest.map((x) => px(x.verts_norm)));
|
|
9465
9636
|
if (!r) return null;
|
|
9466
|
-
const
|
|
9637
|
+
const norm3 = (ring) => ring.map(([x, y]) => [x / s.widthPx, y / s.heightPx]);
|
|
9467
9638
|
return {
|
|
9468
|
-
verts_norm:
|
|
9469
|
-
...r.holes.length ? { verts_norm_holes: r.holes.map(
|
|
9639
|
+
verts_norm: norm3(r.outer),
|
|
9640
|
+
...r.holes.length ? { verts_norm_holes: r.holes.map(norm3) } : {},
|
|
9470
9641
|
computed: { area_sf: round2(r.area * upp * upp), perimeter_lf: round2(r.perim * upp) }
|
|
9471
9642
|
};
|
|
9472
9643
|
}
|
|
@@ -9637,9 +9808,9 @@ var Session = class _Session {
|
|
|
9637
9808
|
}
|
|
9638
9809
|
const tableRegions = /* @__PURE__ */ new Map();
|
|
9639
9810
|
for (const tb of graph.tables) {
|
|
9640
|
-
const
|
|
9641
|
-
|
|
9642
|
-
tableRegions.set(tb.sheet,
|
|
9811
|
+
const arr3 = tableRegions.get(tb.sheet) ?? [];
|
|
9812
|
+
arr3.push(tb.region);
|
|
9813
|
+
tableRegions.set(tb.sheet, arr3);
|
|
9643
9814
|
}
|
|
9644
9815
|
const VAL_RE = /^[0-9][0-9,]{0,6}$/;
|
|
9645
9816
|
const perMark = /* @__PURE__ */ new Map();
|
|
@@ -11505,41 +11676,18 @@ var Session = class _Session {
|
|
|
11505
11676
|
}
|
|
11506
11677
|
exportPayload() {
|
|
11507
11678
|
if (!this.docs.size) throw new UserError("No plan loaded \u2014 call load_plan first.");
|
|
11508
|
-
return {
|
|
11509
|
-
schema: ANN_SCHEMA,
|
|
11679
|
+
return buildTakeoffDocument({
|
|
11510
11680
|
project_name: "",
|
|
11511
11681
|
units: "imperial",
|
|
11512
|
-
sheets: [...this.sheets.values()].filter((s) => s.upp != null).map((s) => ({
|
|
11513
|
-
sheet_id: s.key,
|
|
11514
|
-
units_per_px: s.upp,
|
|
11515
|
-
// provenance rides the payload (it used to be dropped here): the canvas
|
|
11516
|
-
// hydrates scale_source for its report and scale_confirmed for the
|
|
11517
|
-
// scale gate's confirm affordance — absent = confirmed (pre-flag docs)
|
|
11518
|
-
...s.scaleSource ? { scale_source: s.scaleSource } : {},
|
|
11519
|
-
...s.scaleConfirmed === false ? { scale_confirmed: false } : {}
|
|
11520
|
-
})),
|
|
11682
|
+
sheets: [...this.sheets.values()].filter((s) => s.upp != null).map((s) => sheetEntry({ sheet_id: s.key, units_per_px: s.upp, scale_source: s.scaleSource, scale_confirmed: s.scaleConfirmed })),
|
|
11521
11683
|
conditions: this.conditions,
|
|
11522
11684
|
shapes: this.shapes,
|
|
11523
11685
|
markups: this.markups,
|
|
11524
|
-
|
|
11525
|
-
|
|
11526
|
-
|
|
11527
|
-
|
|
11528
|
-
|
|
11529
|
-
// has no tombstone notion (its delete is a removal), and a withdrawn
|
|
11530
|
-
// question must not resurface there as a stray Void row. Same
|
|
11531
|
-
// present-only-when-any convention as approvals.
|
|
11532
|
-
...this.liveRfis().length ? { rfis: this.liveRfis() } : {},
|
|
11533
|
-
// proposals (#365): the batches and the pending condition diffs ride
|
|
11534
|
-
// the payload as transport — present only when any exist, so a
|
|
11535
|
-
// proposal-free export stays byte-identical to a pre-#365 one.
|
|
11536
|
-
...this.proposals.length ? { proposals: this.proposals } : {},
|
|
11537
|
-
...this.conditionEditProposals.length ? { condition_edit_proposals: this.conditionEditProposals } : {},
|
|
11538
|
-
sheet_group: [],
|
|
11539
|
-
last_group: [],
|
|
11540
|
-
sheet_tabs: [],
|
|
11541
|
-
sheet_levels: {}
|
|
11542
|
-
};
|
|
11686
|
+
rfis: this.liveRfis(),
|
|
11687
|
+
approvals: this.approvals,
|
|
11688
|
+
proposals: this.proposals,
|
|
11689
|
+
condition_edit_proposals: this.conditionEditProposals
|
|
11690
|
+
});
|
|
11543
11691
|
}
|
|
11544
11692
|
/** The computed Report document — "opentakeoff.report.v1", the SAME schema
|
|
11545
11693
|
* and math as the canvas Report's JSON export (web reportJson, totals.js):
|
|
@@ -11906,6 +12054,7 @@ var measurePolygonOutput = {
|
|
|
11906
12054
|
area_sf: z.number(),
|
|
11907
12055
|
perimeter_lf: z.number(),
|
|
11908
12056
|
nverts: z.number().int(),
|
|
12057
|
+
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"),
|
|
11909
12058
|
shape_id: z.string().optional().describe("Present when condition was passed and the shape committed"),
|
|
11910
12059
|
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")
|
|
11911
12060
|
};
|
|
@@ -11915,6 +12064,7 @@ var measureSurfaceOutput = {
|
|
|
11915
12064
|
length_lf: z.number().describe("The traced run's open length"),
|
|
11916
12065
|
area_sf: z.number().describe("length_lf \xD7 height_ft \u2014 the wall SF committed"),
|
|
11917
12066
|
npts: z.number().int(),
|
|
12067
|
+
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"),
|
|
11918
12068
|
shape_id: z.string()
|
|
11919
12069
|
};
|
|
11920
12070
|
var placeCountOutput = {
|
|
@@ -12015,6 +12165,7 @@ var symbolSweepOutput = {
|
|
|
12015
12165
|
var measureLineOutput = {
|
|
12016
12166
|
length_lf: z.number(),
|
|
12017
12167
|
npts: z.number().int(),
|
|
12168
|
+
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"),
|
|
12018
12169
|
shape_id: z.string().optional().describe("Present when condition was passed and the shape committed")
|
|
12019
12170
|
};
|
|
12020
12171
|
var summaryRow = z.object({
|
|
@@ -12169,7 +12320,7 @@ var exportDxfOutput = {
|
|
|
12169
12320
|
var exportTakeoffOutput = {
|
|
12170
12321
|
schema: z.string(),
|
|
12171
12322
|
project_name: z.string(),
|
|
12172
|
-
units: z.string(),
|
|
12323
|
+
units: z.string().optional().describe("Present only for a metric project; absent means imperial \u2014 the app's own diff-only convention"),
|
|
12173
12324
|
sheets: z.array(z.object({
|
|
12174
12325
|
sheet_id: z.string(),
|
|
12175
12326
|
units_per_px: z.number(),
|
|
@@ -12201,7 +12352,7 @@ var exportTakeoffOutput = {
|
|
|
12201
12352
|
sheet_group: z.array(z.unknown()),
|
|
12202
12353
|
last_group: z.array(z.unknown()),
|
|
12203
12354
|
sheet_tabs: z.array(z.unknown()),
|
|
12204
|
-
sheet_levels: z.object({}).passthrough(),
|
|
12355
|
+
sheet_levels: z.object({}).passthrough().optional().describe("Present only when a sheet carries a level label (the app omits it when empty)"),
|
|
12205
12356
|
proposals: z.array(z.object({ id: z.string(), label: z.string(), rationale: z.string(), created_at: z.string(), withdrawn_at: z.string().optional() }).passthrough()).optional().describe("Proposal batches (#365) \u2014 present only when any exist. Shapes reference them by origin.proposal_id; the canvas shows one Accept per batch"),
|
|
12206
12357
|
condition_edit_proposals: z.array(z.object({ id: z.string(), condition_id: z.string(), proposed: z.object({}).passthrough(), rationale: z.string(), proposed_at: z.string() }).passthrough()).optional().describe("Pending condition-edit diffs (#365) \u2014 present only when any exist. Nothing on the condition changes until the estimator accepts in the canvas")
|
|
12207
12358
|
};
|
|
@@ -13311,6 +13462,78 @@ function boostForDark(hex2) {
|
|
|
13311
13462
|
return "#" + toHex2(nr) + toHex2(ng) + toHex2(nb);
|
|
13312
13463
|
}
|
|
13313
13464
|
|
|
13465
|
+
// ../web/src/lib/markupText.js
|
|
13466
|
+
var NOTE_PT = 8.5;
|
|
13467
|
+
var NOTE_MAX_W_PT = 216;
|
|
13468
|
+
var LINE_PITCH = 1.28;
|
|
13469
|
+
var NOTE_PAD_PT = 3;
|
|
13470
|
+
function ptToImg(pt) {
|
|
13471
|
+
return pt * RENDER_SCALE;
|
|
13472
|
+
}
|
|
13473
|
+
function wrapLines(text, maxW, measure) {
|
|
13474
|
+
const src = String(text ?? "");
|
|
13475
|
+
if (!src.trim()) return [];
|
|
13476
|
+
const out = [];
|
|
13477
|
+
for (const para of src.split(/\r?\n/)) {
|
|
13478
|
+
const words = para.trim().split(/\s+/).filter(Boolean);
|
|
13479
|
+
if (!words.length) {
|
|
13480
|
+
out.push("");
|
|
13481
|
+
continue;
|
|
13482
|
+
}
|
|
13483
|
+
let line = "";
|
|
13484
|
+
for (const w of words) {
|
|
13485
|
+
const cand = line ? `${line} ${w}` : w;
|
|
13486
|
+
if (measure(cand) <= maxW || !line && measure(w) <= maxW) {
|
|
13487
|
+
line = cand;
|
|
13488
|
+
continue;
|
|
13489
|
+
}
|
|
13490
|
+
if (line) {
|
|
13491
|
+
out.push(line);
|
|
13492
|
+
line = "";
|
|
13493
|
+
}
|
|
13494
|
+
if (measure(w) <= maxW) {
|
|
13495
|
+
line = w;
|
|
13496
|
+
continue;
|
|
13497
|
+
}
|
|
13498
|
+
let piece = "";
|
|
13499
|
+
for (const ch of w) {
|
|
13500
|
+
if (measure(piece + ch) > maxW && piece) {
|
|
13501
|
+
out.push(piece);
|
|
13502
|
+
piece = ch;
|
|
13503
|
+
} else piece += ch;
|
|
13504
|
+
}
|
|
13505
|
+
line = piece;
|
|
13506
|
+
}
|
|
13507
|
+
out.push(line);
|
|
13508
|
+
}
|
|
13509
|
+
return out;
|
|
13510
|
+
}
|
|
13511
|
+
function layoutNote({ text, fontPx, measure, maxWPx, padPx }) {
|
|
13512
|
+
const ratio = fontPx / ptToImg(NOTE_PT);
|
|
13513
|
+
const maxW = maxWPx > 0 ? maxWPx : ptToImg(NOTE_MAX_W_PT) * ratio;
|
|
13514
|
+
const pad = padPx >= 0 ? padPx : ptToImg(NOTE_PAD_PT) * ratio;
|
|
13515
|
+
const lines = wrapLines(text, maxW - 2 * pad, measure);
|
|
13516
|
+
const lineH = fontPx * LINE_PITCH;
|
|
13517
|
+
const widest = lines.reduce((m, l) => Math.max(m, measure(l)), 0);
|
|
13518
|
+
return {
|
|
13519
|
+
lines,
|
|
13520
|
+
fontPx,
|
|
13521
|
+
lineH,
|
|
13522
|
+
padX: pad,
|
|
13523
|
+
padY: pad,
|
|
13524
|
+
w: lines.length ? widest + 2 * pad : 0,
|
|
13525
|
+
h: lines.length ? lines.length * lineH + 2 * pad : 0
|
|
13526
|
+
};
|
|
13527
|
+
}
|
|
13528
|
+
function noteBox(ax, ay, L) {
|
|
13529
|
+
const x0 = ax - L.padX;
|
|
13530
|
+
const y0 = ay - L.fontPx - L.padY;
|
|
13531
|
+
return { x0, y0, x1: x0 + L.w, y1: y0 + L.h };
|
|
13532
|
+
}
|
|
13533
|
+
function lineBaseline(ay, L, i) {
|
|
13534
|
+
return ay + i * L.lineH;
|
|
13535
|
+
}
|
|
13536
|
+
|
|
13314
13537
|
// ../web/src/lib/markedsetSource.js
|
|
13315
13538
|
function sourcePageMode({ dark = false, encrypted = false } = {}) {
|
|
13316
13539
|
if (dark) return "raster-inverted";
|
|
@@ -13520,9 +13743,9 @@ async function buildMarkedSetPdf({ projectName, dark, sheets, shapes, markups, a
|
|
|
13520
13743
|
const { PDFDocument, StandardFonts, rgb, degrees, LineCapStyle } = await import("pdf-lib");
|
|
13521
13744
|
const condById = Object.fromEntries(conditions.map((c) => [c.id, c]));
|
|
13522
13745
|
const rfiNum = new Map((rfis || []).map((r) => [r.id, r.number]));
|
|
13523
|
-
const byKey = (
|
|
13746
|
+
const byKey = (arr3) => {
|
|
13524
13747
|
const m = /* @__PURE__ */ new Map();
|
|
13525
|
-
for (const s of
|
|
13748
|
+
for (const s of arr3) {
|
|
13526
13749
|
const a = m.get(s.sheet_id) || [];
|
|
13527
13750
|
a.push(s);
|
|
13528
13751
|
m.set(s.sheet_id, a);
|
|
@@ -13847,6 +14070,28 @@ async function buildMarkedSetPdf({ projectName, dark, sheets, shapes, markups, a
|
|
|
13847
14070
|
const [px, py] = toPage(x, y);
|
|
13848
14071
|
pg.drawText(winAnsiSafe(t), { x: px, y: py, size, font: fnt, color: colorRgb, rotate: chipRot });
|
|
13849
14072
|
};
|
|
14073
|
+
const noteBlock = (raw, ax, ay, colorRgb, backing) => {
|
|
14074
|
+
const t = winAnsiSafe(raw);
|
|
14075
|
+
const fs = NOTE_PT / ptScale;
|
|
14076
|
+
const L = layoutNote({ text: t, fontPx: fs, measure: (str) => bold.widthOfTextAtSize(str, NOTE_PT) / ptScale });
|
|
14077
|
+
if (!L.lines.length) return;
|
|
14078
|
+
const b = noteBox(ax, ay, L);
|
|
14079
|
+
const dp = imageDrawParams(toPage, b.x0, b.y0, L.w, L.h);
|
|
14080
|
+
pg.drawRectangle({
|
|
14081
|
+
x: dp.x,
|
|
14082
|
+
y: dp.y,
|
|
14083
|
+
width: dp.width,
|
|
14084
|
+
height: dp.height,
|
|
14085
|
+
rotate: degrees(dp.rotateDeg),
|
|
14086
|
+
color: backing,
|
|
14087
|
+
opacity: 0.92,
|
|
14088
|
+
borderColor: colorRgb,
|
|
14089
|
+
borderWidth: 0.7
|
|
14090
|
+
});
|
|
14091
|
+
L.lines.forEach((ln, i) => {
|
|
14092
|
+
if (ln) text(ln, ax, lineBaseline(ay, L, i), NOTE_PT, colorRgb, bold);
|
|
14093
|
+
});
|
|
14094
|
+
};
|
|
13850
14095
|
const chip = (raw, x, y, borderRgb) => {
|
|
13851
14096
|
const t = winAnsiSafe(raw);
|
|
13852
14097
|
const size = 7.5;
|
|
@@ -13991,7 +14236,9 @@ async function buildMarkedSetPdf({ projectName, dark, sheets, shapes, markups, a
|
|
|
13991
14236
|
const [ptx, pty] = toPage(m.target[0] * W, m.target[1] * H);
|
|
13992
14237
|
pg.drawSvgPath(arrowheadPath(pax, -pay, ptx, -pty, 5), { x: 0, y: 0, color: mcol, opacity: 0.9 });
|
|
13993
14238
|
}
|
|
13994
|
-
|
|
14239
|
+
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));
|
|
14240
|
+
} else if (m.type === "text" && m.at) {
|
|
14241
|
+
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));
|
|
13995
14242
|
} else if (m.type === "svg" && m.at && Array.isArray(m.vb) && typeof m.path === "string") {
|
|
13996
14243
|
const { s: sx, bw, bh } = svgPlacedBox(m.vb, m.w, W);
|
|
13997
14244
|
if (sx > 0) {
|
|
@@ -14253,7 +14500,7 @@ function parseTakeoffImport(text) {
|
|
|
14253
14500
|
}
|
|
14254
14501
|
return doc;
|
|
14255
14502
|
}
|
|
14256
|
-
var
|
|
14503
|
+
var arr2 = (v) => Array.isArray(v) ? v : [];
|
|
14257
14504
|
var tagKey = (t) => String(t || "").trim().toUpperCase();
|
|
14258
14505
|
var freeId = (id, taken) => {
|
|
14259
14506
|
let n = 2, next = id;
|
|
@@ -14262,11 +14509,11 @@ var freeId = (id, taken) => {
|
|
|
14262
14509
|
};
|
|
14263
14510
|
function mergeTakeoffImport(current, imported, knownFiles = null) {
|
|
14264
14511
|
const cur = current && typeof current === "object" ? current : {};
|
|
14265
|
-
const impShapes =
|
|
14266
|
-
const impConds =
|
|
14267
|
-
const localScales = new Map(
|
|
14268
|
-
const incomingScales = new Map(
|
|
14269
|
-
const existingIds = new Set(
|
|
14512
|
+
const impShapes = arr2(imported.shapes).filter((s) => s && typeof s === "object" && typeof s.sheet_id === "string" && typeof s.id === "string").map(normalizeAgentReview);
|
|
14513
|
+
const impConds = arr2(imported.conditions).filter((c) => c && typeof c === "object" && typeof c.id === "string");
|
|
14514
|
+
const localScales = new Map(arr2(cur.sheets).filter((s) => typeof s?.sheet_id === "string").map((s) => [s.sheet_id, s.units_per_px]));
|
|
14515
|
+
const incomingScales = new Map(arr2(imported.sheets).filter((s) => typeof s?.sheet_id === "string").map((s) => [s.sheet_id, s.units_per_px]));
|
|
14516
|
+
const existingIds = new Set(arr2(cur.shapes).map((s) => s.id));
|
|
14270
14517
|
for (const s of impShapes) {
|
|
14271
14518
|
if (existingIds.has(s.id) || s.measure_role === "count") continue;
|
|
14272
14519
|
const local = localScales.get(s.sheet_id), incoming = incomingScales.get(s.sheet_id);
|
|
@@ -14280,20 +14527,20 @@ function mergeTakeoffImport(current, imported, knownFiles = null) {
|
|
|
14280
14527
|
return [...new Set(added.map((s) => String(s.sheet_id).split("#")[0]).filter((f) => !known.has(f)))];
|
|
14281
14528
|
};
|
|
14282
14529
|
const pendingCount = (shapes) => shapes.filter((s) => s.origin?.reviewed === false).length;
|
|
14283
|
-
if (!
|
|
14530
|
+
if (!arr2(cur.shapes).length && !arr2(cur.markups).length && !arr2(cur.approvals).length && !arr2(cur.sheets).some((s) => s?.units_per_px > 0)) {
|
|
14284
14531
|
const payload2 = {
|
|
14285
14532
|
...imported,
|
|
14286
14533
|
shapes: impShapes,
|
|
14287
|
-
...
|
|
14288
|
-
...
|
|
14289
|
-
...
|
|
14534
|
+
...arr2(imported.sheet_tabs).length ? {} : { sheet_tabs: arr2(cur.sheet_tabs) },
|
|
14535
|
+
...arr2(imported.sheet_group).length ? {} : { sheet_group: arr2(cur.sheet_group) },
|
|
14536
|
+
...arr2(imported.last_group).length ? {} : { last_group: arr2(cur.last_group) }
|
|
14290
14537
|
};
|
|
14291
14538
|
return {
|
|
14292
14539
|
payload: payload2,
|
|
14293
|
-
note: { replaced: true, shapes_added: impShapes.length, shapes_pending: pendingCount(impShapes), conditions_merged: 0, conditions_added: impConds.length, scales_adopted:
|
|
14540
|
+
note: { replaced: true, shapes_added: impShapes.length, shapes_pending: pendingCount(impShapes), conditions_merged: 0, conditions_added: impConds.length, scales_adopted: arr2(imported.sheets).length, unknown_files: unknownFiles(impShapes) }
|
|
14294
14541
|
};
|
|
14295
14542
|
}
|
|
14296
|
-
const conditions = [...
|
|
14543
|
+
const conditions = [...arr2(cur.conditions)];
|
|
14297
14544
|
const byTag = new Map(conditions.map((c) => [tagKey(c.finish_tag), c.id]));
|
|
14298
14545
|
const condIds = new Set(conditions.map((c) => c.id));
|
|
14299
14546
|
const condMap = /* @__PURE__ */ new Map();
|
|
@@ -14329,23 +14576,23 @@ function mergeTakeoffImport(current, imported, knownFiles = null) {
|
|
|
14329
14576
|
conditions[i] = next;
|
|
14330
14577
|
}
|
|
14331
14578
|
}
|
|
14332
|
-
const shapeIds = new Set(
|
|
14579
|
+
const shapeIds = new Set(arr2(cur.shapes).map((s) => s.id));
|
|
14333
14580
|
const addedShapes = impShapes.filter((s) => !shapeIds.has(s.id)).map((s) => condMap.has(s.condition_id) && condMap.get(s.condition_id) !== s.condition_id ? { ...s, condition_id: condMap.get(s.condition_id) } : s);
|
|
14334
|
-
const markupIds = new Set(
|
|
14335
|
-
const addedMarkups =
|
|
14336
|
-
const rfiIds = new Set(
|
|
14337
|
-
const addedRfis =
|
|
14338
|
-
const proposalIds = new Set(
|
|
14339
|
-
const addedProposals =
|
|
14340
|
-
const editIds = new Set(
|
|
14581
|
+
const markupIds = new Set(arr2(cur.markups).map((m) => m?.id).filter(Boolean));
|
|
14582
|
+
const addedMarkups = arr2(imported.markups).filter((m) => m && typeof m === "object" && (!m.id || !markupIds.has(m.id)));
|
|
14583
|
+
const rfiIds = new Set(arr2(cur.rfis).map((r) => r?.id).filter(Boolean));
|
|
14584
|
+
const addedRfis = arr2(imported.rfis).filter((r) => r && typeof r === "object" && r.id && !rfiIds.has(r.id));
|
|
14585
|
+
const proposalIds = new Set(arr2(cur.proposals).map((p) => p?.id).filter(Boolean));
|
|
14586
|
+
const addedProposals = arr2(imported.proposals).filter((p) => p && typeof p === "object" && typeof p.id === "string" && !proposalIds.has(p.id));
|
|
14587
|
+
const editIds = new Set(arr2(cur.condition_edit_proposals).map((p) => p?.id).filter(Boolean));
|
|
14341
14588
|
const takenTags = new Set(conditions.map((c) => tagKey(c.finish_tag)));
|
|
14342
|
-
const addedEdits =
|
|
14343
|
-
const approvalIds = new Set(
|
|
14589
|
+
const addedEdits = arr2(imported.condition_edit_proposals).filter((p) => p && typeof p === "object" && typeof p.id === "string" && !editIds.has(p.id) && p.proposed && typeof p.proposed === "object").map((p) => condMap.has(p.condition_id) ? { ...p, condition_id: condMap.get(p.condition_id) } : p).filter((p) => condIds.has(p.condition_id)).filter((p) => p.proposed.finish_tag === void 0 || !takenTags.has(tagKey(p.proposed.finish_tag)) || tagKey(p.proposed.finish_tag) === tagKey(conditions.find((c) => c.id === p.condition_id)?.finish_tag)).filter((p) => !arr2(cur.condition_edit_proposals).some((q) => q?.condition_id === p.condition_id));
|
|
14590
|
+
const approvalIds = new Set(arr2(cur.approvals).map((a) => a?.id).filter(Boolean));
|
|
14344
14591
|
const addedApprovals = sanitizeApprovals(imported.approvals).filter((a) => !approvalIds.has(a.id));
|
|
14345
|
-
const sheets = [...
|
|
14592
|
+
const sheets = [...arr2(cur.sheets)];
|
|
14346
14593
|
const scaled = new Set(sheets.map((s) => s?.sheet_id));
|
|
14347
14594
|
let scalesAdopted = 0;
|
|
14348
|
-
for (const s of
|
|
14595
|
+
for (const s of arr2(imported.sheets)) {
|
|
14349
14596
|
if (s && typeof s === "object" && s.sheet_id && s.units_per_px && !scaled.has(s.sheet_id)) {
|
|
14350
14597
|
sheets.push(s);
|
|
14351
14598
|
scaled.add(s.sheet_id);
|
|
@@ -14355,12 +14602,12 @@ function mergeTakeoffImport(current, imported, knownFiles = null) {
|
|
|
14355
14602
|
const payload = {
|
|
14356
14603
|
...cur,
|
|
14357
14604
|
conditions,
|
|
14358
|
-
shapes: [...
|
|
14359
|
-
markups: [...
|
|
14360
|
-
...addedRfis.length ? { rfis: [...
|
|
14361
|
-
...addedProposals.length ? { proposals: [...
|
|
14362
|
-
...addedEdits.length ? { condition_edit_proposals: [...
|
|
14363
|
-
...addedApprovals.length ? { approvals: [...
|
|
14605
|
+
shapes: [...arr2(cur.shapes), ...addedShapes],
|
|
14606
|
+
markups: [...arr2(cur.markups), ...addedMarkups],
|
|
14607
|
+
...addedRfis.length ? { rfis: [...arr2(cur.rfis), ...addedRfis] } : {},
|
|
14608
|
+
...addedProposals.length ? { proposals: [...arr2(cur.proposals), ...addedProposals] } : {},
|
|
14609
|
+
...addedEdits.length ? { condition_edit_proposals: [...arr2(cur.condition_edit_proposals), ...addedEdits] } : {},
|
|
14610
|
+
...addedApprovals.length ? { approvals: [...arr2(cur.approvals), ...addedApprovals] } : {},
|
|
14364
14611
|
sheets
|
|
14365
14612
|
};
|
|
14366
14613
|
return {
|
|
@@ -14542,15 +14789,16 @@ function registerTools(realServer, session, opts = {}) {
|
|
|
14542
14789
|
outputSchema: proposeTakeoffOutput
|
|
14543
14790
|
}, run("propose_takeoff", (a) => session.proposeTakeoff(a.label, a.rationale)));
|
|
14544
14791
|
server.registerTool("measure_polygon", {
|
|
14545
|
-
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}`,
|
|
14792
|
+
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}`,
|
|
14546
14793
|
inputSchema: {
|
|
14547
14794
|
sheet: z2.string(),
|
|
14548
14795
|
verts: z2.array(pointSchema).min(3),
|
|
14549
14796
|
condition: z2.string().optional(),
|
|
14550
|
-
role: roleSchema
|
|
14797
|
+
role: roleSchema,
|
|
14798
|
+
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.")
|
|
14551
14799
|
},
|
|
14552
14800
|
outputSchema: measurePolygonOutput
|
|
14553
|
-
}, run("measure_polygon", (a) => session.measurePolygon(a.sheet, a.verts, { condition: a.condition, role: a.role })));
|
|
14801
|
+
}, run("measure_polygon", (a) => session.measurePolygon(a.sheet, a.verts, { condition: a.condition, role: a.role, arc_through: a.arc_through })));
|
|
14554
14802
|
server.registerTool("cut_out", {
|
|
14555
14803
|
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}`,
|
|
14556
14804
|
inputSchema: {
|
|
@@ -14560,24 +14808,26 @@ function registerTools(realServer, session, opts = {}) {
|
|
|
14560
14808
|
outputSchema: cutOutOutput
|
|
14561
14809
|
}, run("cut_out", (a) => session.cutOut(a)));
|
|
14562
14810
|
server.registerTool("measure_line", {
|
|
14563
|
-
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}`,
|
|
14811
|
+
description: `Measure an open polyline (min 2 points, image px): length_lf at the sheet's scale. Requires the scale to be set. Pass condition to commit it as a linear shape (base, transitions, feature strips). A curved run (base along a radius wall, a curved feature strip) takes arc_through: one point on the bow, marked. ${COORDS}`,
|
|
14564
14812
|
inputSchema: {
|
|
14565
14813
|
sheet: z2.string(),
|
|
14566
14814
|
pts: z2.array(pointSchema).min(2),
|
|
14567
|
-
condition: z2.string().optional()
|
|
14815
|
+
condition: z2.string().optional(),
|
|
14816
|
+
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.")
|
|
14568
14817
|
},
|
|
14569
14818
|
outputSchema: measureLineOutput
|
|
14570
|
-
}, run("measure_line", (a) => session.measureLine(a.sheet, a.pts, { condition: a.condition })));
|
|
14819
|
+
}, run("measure_line", (a) => session.measureLine(a.sheet, a.pts, { condition: a.condition, arc_through: a.arc_through })));
|
|
14571
14820
|
server.registerTool("measure_surface", {
|
|
14572
14821
|
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}`,
|
|
14573
14822
|
inputSchema: {
|
|
14574
14823
|
sheet: z2.string(),
|
|
14575
14824
|
pts: z2.array(pointSchema).min(2).describe("The wall run, an open polyline (image px)"),
|
|
14576
14825
|
condition: z2.string().describe("Finish tag to commit under (minted on first use), e.g. 'CT-W1'"),
|
|
14577
|
-
height_ft: z2.number().positive().optional().describe("Wall height in feet \u2014 written to the condition's H knob first, then used")
|
|
14826
|
+
height_ft: z2.number().positive().optional().describe("Wall height in feet \u2014 written to the condition's H knob first, then used"),
|
|
14827
|
+
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.")
|
|
14578
14828
|
},
|
|
14579
14829
|
outputSchema: measureSurfaceOutput
|
|
14580
|
-
}, run("measure_surface", (a) => session.measureSurface(a.sheet, a.pts, { condition: a.condition, height_ft: a.height_ft })));
|
|
14830
|
+
}, run("measure_surface", (a) => session.measureSurface(a.sheet, a.pts, { condition: a.condition, height_ft: a.height_ft, arc_through: a.arc_through })));
|
|
14581
14831
|
server.registerTool("place_count", {
|
|
14582
14832
|
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}`,
|
|
14583
14833
|
inputSchema: {
|
|
@@ -15089,7 +15339,7 @@ No review gate: the pencil-not-ink rule exists to stop an agent inventing geomet
|
|
|
15089
15339
|
import { ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
15090
15340
|
|
|
15091
15341
|
// src/wiki.generated.ts
|
|
15092
|
-
var WIKI_VERSION = "0.9.
|
|
15342
|
+
var WIKI_VERSION = "0.9.87";
|
|
15093
15343
|
var WIKI_PAGES = [
|
|
15094
15344
|
{
|
|
15095
15345
|
"key": "index",
|
|
@@ -15128,8 +15378,8 @@ var WIKI_PAGES = [
|
|
|
15128
15378
|
"uri": "takeoff://wiki/workflows",
|
|
15129
15379
|
"title": "Workflows",
|
|
15130
15380
|
"source": "docs/wiki/workflows.md",
|
|
15131
|
-
"source_sha256": "
|
|
15132
|
-
"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"
|
|
15381
|
+
"source_sha256": "0281270b8f1a87532b6bf1aa1add8e4f39f4adb750a3676398d93b1ac4ee51c8",
|
|
15382
|
+
"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"
|
|
15133
15383
|
},
|
|
15134
15384
|
{
|
|
15135
15385
|
"key": "mcp",
|
|
@@ -15527,7 +15777,7 @@ function nameTheStageInRefusals(server) {
|
|
|
15527
15777
|
// package.json
|
|
15528
15778
|
var package_default = {
|
|
15529
15779
|
name: "opentakeoff-mcp",
|
|
15530
|
-
version: "0.9.
|
|
15780
|
+
version: "0.9.87",
|
|
15531
15781
|
mcpName: "io.github.Kentucky-ai/opentakeoff",
|
|
15532
15782
|
type: "module",
|
|
15533
15783
|
description: "OpenTakeoff MCP server \u2014 drive the takeoff engine from your MCP client over stdio.",
|
|
@@ -15547,7 +15797,7 @@ var package_default = {
|
|
|
15547
15797
|
"check:wiki": "node scripts/check-wiki.mjs",
|
|
15548
15798
|
"check:versions": "node ../scripts/check-version-consistency.mjs",
|
|
15549
15799
|
"check:tool-count": "node --import tsx scripts/check-tool-count.mjs",
|
|
15550
|
-
test: "node --import tsx --test test/checkers.test.ts test/conformance.test.ts test/constants.test.ts test/context.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"
|
|
15800
|
+
test: "node --import tsx --test test/arc.test.ts test/checkers.test.ts test/conformance.test.ts test/constants.test.ts test/context.test.ts test/document.test.ts test/dxf.test.ts test/e2e.test.ts test/gate.test.ts test/graphEvalRowsym.test.ts test/labels.test.ts test/mep.test.ts test/overlap.test.ts test/parity.test.ts test/proposals.test.ts test/raster.test.ts test/resources.test.ts test/safewrite.test.ts test/scalewarn.test.ts test/scope.test.ts test/session.test.ts test/staging.test.ts test/sweepguard.test.ts test/tools.test.ts test/transitions.test.ts test/twins.test.ts test/view.test.ts test/wiki.test.ts"
|
|
15551
15801
|
},
|
|
15552
15802
|
dependencies: {
|
|
15553
15803
|
"@modelcontextprotocol/sdk": "^1.12.0",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "opentakeoff-mcp",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.87",
|
|
4
4
|
"mcpName": "io.github.Kentucky-ai/opentakeoff",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"description": "OpenTakeoff MCP server — drive the takeoff engine from your MCP client over stdio.",
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
"check:wiki": "node scripts/check-wiki.mjs",
|
|
21
21
|
"check:versions": "node ../scripts/check-version-consistency.mjs",
|
|
22
22
|
"check:tool-count": "node --import tsx scripts/check-tool-count.mjs",
|
|
23
|
-
"test": "node --import tsx --test test/checkers.test.ts test/conformance.test.ts test/constants.test.ts test/context.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"
|
|
23
|
+
"test": "node --import tsx --test test/arc.test.ts test/checkers.test.ts test/conformance.test.ts test/constants.test.ts test/context.test.ts test/document.test.ts test/dxf.test.ts test/e2e.test.ts test/gate.test.ts test/graphEvalRowsym.test.ts test/labels.test.ts test/mep.test.ts test/overlap.test.ts test/parity.test.ts test/proposals.test.ts test/raster.test.ts test/resources.test.ts test/safewrite.test.ts test/scalewarn.test.ts test/scope.test.ts test/session.test.ts test/staging.test.ts test/sweepguard.test.ts test/tools.test.ts test/transitions.test.ts test/twins.test.ts test/view.test.ts test/wiki.test.ts"
|
|
24
24
|
},
|
|
25
25
|
"dependencies": {
|
|
26
26
|
"@modelcontextprotocol/sdk": "^1.12.0",
|