opentakeoff-mcp 0.9.61 → 0.9.63
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/server-core.js +210 -76
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -172,7 +172,7 @@ reads the tool list once. ([#230](https://github.com/Kentucky-ai/opentakeoff/iss
|
|
|
172
172
|
| `resolve_tag` | ONE room tag → its room-finish schedule row → each code's finish/material definition, every edge cited (sheet + literal text + bbox). Refusal over guessing: `unresolved` comes back with a reason, never as silence. A delta/REV marker on the answering row rides the result as `revisions`—the codes are the post-revision answer, and you're told the ink changed. |
|
|
173
173
|
| `find_schedule` | Locate a schedule table by kind ("room finish", "material")—sheet, title, headers, row count, a `view_sheet`-ready region, and `revised_rows` when delta/REV-marked rows exist. |
|
|
174
174
|
| `sheet_context` | The region's STRUCTURE in one frame: classified vector segments (endpoints as drawn, meta byte per segment), text spans with bboxes, and hatch-family instances with content-derived ids—same pattern spec ⇒ same id anywhere on the sheet, so plan↔legend matching is `id === id`. Decimation is declared and counted on every reply: `kept + dropped === total_in_region`, cap applies longest-first so walls survive. |
|
|
175
|
-
| `view_sheet` | The agent's eyes: render the sheet (or an image-px crop) to PNG. `overlay` burns committed shapes in (solid = human-affirmed, dashed = unreviewed) to verify geometry landed; `grid` burns in a calibrated 1-ft/5-ft measuring grid with foot labels (`"auto"` from the set scale, or the drawing scale like `"1/4"`) so dimensions are counted off cells, not guessed. |
|
|
175
|
+
| `view_sheet` | The agent's eyes: render the sheet (or an image-px crop) to PNG. `overlay` burns committed shapes in (solid = human-affirmed, dashed = unreviewed) to verify geometry landed; `grid` burns in a calibrated 1-ft/5-ft measuring grid with foot labels (`"auto"` from the set scale, or the drawing scale like `"1/4"`) so dimensions are counted off cells, not guessed; `marks` (#297) burns disclosure layers in — `question` (withheld placements, orange ?-circle), `struck` (rejections, magenta struck ×), `ring` (the sweep's seed, violet double ring) — in colors off the common CAD pens, so what a reply names, the picture shows. |
|
|
176
176
|
|
|
177
177
|
### The agent revises its own work
|
|
178
178
|
|
package/dist/server-core.js
CHANGED
|
@@ -4638,6 +4638,64 @@ function sharedRuns(ringA, ringB, opts) {
|
|
|
4638
4638
|
}
|
|
4639
4639
|
return runs;
|
|
4640
4640
|
}
|
|
4641
|
+
var TRANSITION_DEFAULTS = { max_gap_in: 12, min_run_in: 12 };
|
|
4642
|
+
var round22 = (n) => +n.toFixed(2);
|
|
4643
|
+
var round12 = (n) => +n.toFixed(1);
|
|
4644
|
+
function dropCollinear(path5, tol = 1e-6) {
|
|
4645
|
+
if (path5.length < 3) return path5.map((p) => [p[0], p[1]]);
|
|
4646
|
+
const out = [[path5[0][0], path5[0][1]]];
|
|
4647
|
+
for (let i = 1; i < path5.length - 1; i++) {
|
|
4648
|
+
const a = out[out.length - 1], b = path5[i], c = path5[i + 1];
|
|
4649
|
+
const abx = b[0] - a[0], aby = b[1] - a[1];
|
|
4650
|
+
const acx = c[0] - a[0], acy = c[1] - a[1];
|
|
4651
|
+
const len = Math.hypot(acx, acy);
|
|
4652
|
+
const off = len > 0 ? Math.abs(abx * acy - aby * acx) / len : Infinity;
|
|
4653
|
+
if (off > tol) out.push([b[0], b[1]]);
|
|
4654
|
+
}
|
|
4655
|
+
out.push([path5[path5.length - 1][0], path5[path5.length - 1][1]]);
|
|
4656
|
+
return out;
|
|
4657
|
+
}
|
|
4658
|
+
function deriveTransitionRuns(a, b, sheets, opts = {}) {
|
|
4659
|
+
const maxGapIn = opts.max_gap_in ?? TRANSITION_DEFAULTS.max_gap_in;
|
|
4660
|
+
const minRunIn = opts.min_run_in ?? TRANSITION_DEFAULTS.min_run_in;
|
|
4661
|
+
const runs = [], withheld = [];
|
|
4662
|
+
if (!(maxGapIn > 0) || !(minRunIn > 0)) return { runs, withheld };
|
|
4663
|
+
for (const [key, frame] of sheets) {
|
|
4664
|
+
if (!(frame.upp > 0) || !(frame.widthPx > 0) || !(frame.heightPx > 0)) continue;
|
|
4665
|
+
const onA = a.shapes.filter((s) => s.sheet_id === key);
|
|
4666
|
+
const onB = b.shapes.filter((s) => s.sheet_id === key);
|
|
4667
|
+
if (!onA.length || !onB.length) continue;
|
|
4668
|
+
const pxPerFt = 1 / frame.upp;
|
|
4669
|
+
const toPx = (s) => s.verts_norm.map(([x, y]) => [x * frame.widthPx, y * frame.heightPx]);
|
|
4670
|
+
const runOpts = {
|
|
4671
|
+
step_px: Math.max(1, pxPerFt * 0.25),
|
|
4672
|
+
// a quarter-foot walk — finer than any transition matters
|
|
4673
|
+
touch_px: pxPerFt * (1 / 12),
|
|
4674
|
+
// within an inch: one open space, not two rooms
|
|
4675
|
+
max_gap_px: pxPerFt * (maxGapIn / 12),
|
|
4676
|
+
min_len_px: pxPerFt * (minRunIn / 12)
|
|
4677
|
+
};
|
|
4678
|
+
for (const ra of onA) {
|
|
4679
|
+
for (const rb of onB) {
|
|
4680
|
+
if (ra.id === rb.id) continue;
|
|
4681
|
+
for (const r of sharedRuns(toPx(ra), toPx(rb), runOpts)) {
|
|
4682
|
+
const row = {
|
|
4683
|
+
sheet_id: key,
|
|
4684
|
+
between_shape_ids: [ra.id, rb.id],
|
|
4685
|
+
between: [a.tag, b.tag],
|
|
4686
|
+
length_lf: round22(r.length_px * frame.upp),
|
|
4687
|
+
gap_in: round12(r.gap_px * frame.upp * 12),
|
|
4688
|
+
at: [Math.round(r.at[0]), Math.round(r.at[1])],
|
|
4689
|
+
verts_norm: dropCollinear(r.path).map(([x, y]) => [x / frame.widthPx, y / frame.heightPx])
|
|
4690
|
+
};
|
|
4691
|
+
if (r.kind === "wall") withheld.push({ ...row, reason: "wall_separated" });
|
|
4692
|
+
else runs.push(row);
|
|
4693
|
+
}
|
|
4694
|
+
}
|
|
4695
|
+
}
|
|
4696
|
+
}
|
|
4697
|
+
return { runs, withheld };
|
|
4698
|
+
}
|
|
4641
4699
|
|
|
4642
4700
|
// ../web/src/lib/cutout.js
|
|
4643
4701
|
import { polygon as turfPolygon, featureCollection } from "@turf/helpers";
|
|
@@ -4950,7 +5008,7 @@ function applyApprovalCommand(approvals, cmd) {
|
|
|
4950
5008
|
}
|
|
4951
5009
|
|
|
4952
5010
|
// ../web/src/lib/num.js
|
|
4953
|
-
var
|
|
5011
|
+
var round23 = (n) => Math.round((n + Number.EPSILON) * 100) / 100;
|
|
4954
5012
|
|
|
4955
5013
|
// ../web/src/lib/conditionColumns.js
|
|
4956
5014
|
var visible = (v) => typeof v === "string" && v.trim() ? v : "";
|
|
@@ -5025,8 +5083,8 @@ function conditionTotals(conditions, shapes, ctx = null) {
|
|
|
5025
5083
|
const per = Math.max(0, Number(m.per) || 0);
|
|
5026
5084
|
const basisVal = m.basis === "linear" ? lf : m.basis === "count" ? ea : m.basis === "seam_lf" ? seam : total;
|
|
5027
5085
|
let qty = per > 0 ? basisVal / per : 0;
|
|
5028
|
-
qty = m.round === false ?
|
|
5029
|
-
return { name: m.name, unit: m.unit || "", per, basis: m.basis || "area", round: m.round !== false, note: m.note || "", basis_qty:
|
|
5086
|
+
qty = m.round === false ? round23(qty) : Math.ceil(qty - 1e-9);
|
|
5087
|
+
return { name: m.name, unit: m.unit || "", per, basis: m.basis || "area", round: m.round !== false, note: m.note || "", basis_qty: round23(basisVal), qty };
|
|
5030
5088
|
});
|
|
5031
5089
|
return {
|
|
5032
5090
|
id: c.id,
|
|
@@ -5037,19 +5095,19 @@ function conditionTotals(conditions, shapes, ctx = null) {
|
|
|
5037
5095
|
multiplier: mult,
|
|
5038
5096
|
waste_pct: waste,
|
|
5039
5097
|
shape_count: cs.length,
|
|
5040
|
-
floor_sf:
|
|
5041
|
-
wall_sf:
|
|
5042
|
-
border_sf:
|
|
5043
|
-
lf:
|
|
5098
|
+
floor_sf: round23(floor),
|
|
5099
|
+
wall_sf: round23(wall),
|
|
5100
|
+
border_sf: round23(border),
|
|
5101
|
+
lf: round23(lf),
|
|
5044
5102
|
ea,
|
|
5045
|
-
total_sf:
|
|
5103
|
+
total_sf: round23(total),
|
|
5046
5104
|
// waste-adjusted (order quantities)
|
|
5047
|
-
floor_sf_net:
|
|
5048
|
-
wall_sf_net:
|
|
5049
|
-
border_sf_net:
|
|
5050
|
-
lf_net:
|
|
5051
|
-
total_sf_net:
|
|
5052
|
-
sy_net:
|
|
5105
|
+
floor_sf_net: round23(floor * w),
|
|
5106
|
+
wall_sf_net: round23(wall * w),
|
|
5107
|
+
border_sf_net: round23(border * w),
|
|
5108
|
+
lf_net: round23(lf * w),
|
|
5109
|
+
total_sf_net: round23(total * w),
|
|
5110
|
+
sy_net: round23(total * w / 9),
|
|
5053
5111
|
materials
|
|
5054
5112
|
};
|
|
5055
5113
|
});
|
|
@@ -5098,11 +5156,11 @@ function hasMultipliers(bySheet) {
|
|
|
5098
5156
|
function roundSheetRow(r) {
|
|
5099
5157
|
return {
|
|
5100
5158
|
...r,
|
|
5101
|
-
floor_sf:
|
|
5102
|
-
wall_sf:
|
|
5103
|
-
border_sf:
|
|
5104
|
-
lf:
|
|
5105
|
-
ea:
|
|
5159
|
+
floor_sf: round23(r.floor_sf),
|
|
5160
|
+
wall_sf: round23(r.wall_sf),
|
|
5161
|
+
border_sf: round23(r.border_sf),
|
|
5162
|
+
lf: round23(r.lf),
|
|
5163
|
+
ea: round23(r.ea)
|
|
5106
5164
|
};
|
|
5107
5165
|
}
|
|
5108
5166
|
function materialsSummary(rows) {
|
|
@@ -5113,17 +5171,17 @@ function materialsSummary(rows) {
|
|
|
5113
5171
|
cur.qty += m.qty;
|
|
5114
5172
|
map.set(key, cur);
|
|
5115
5173
|
}
|
|
5116
|
-
return [...map.values()].map((x) => ({ ...x, qty:
|
|
5174
|
+
return [...map.values()].map((x) => ({ ...x, qty: round23(x.qty) }));
|
|
5117
5175
|
}
|
|
5118
5176
|
function grandTotals(rows) {
|
|
5119
5177
|
const sum = (k) => rows.reduce((n, r) => n + (r[k] || 0), 0);
|
|
5120
5178
|
return {
|
|
5121
|
-
total_sf:
|
|
5122
|
-
total_sf_net:
|
|
5123
|
-
lf:
|
|
5124
|
-
lf_net:
|
|
5179
|
+
total_sf: round23(sum("total_sf")),
|
|
5180
|
+
total_sf_net: round23(sum("total_sf_net")),
|
|
5181
|
+
lf: round23(sum("lf")),
|
|
5182
|
+
lf_net: round23(sum("lf_net")),
|
|
5125
5183
|
ea: sum("ea"),
|
|
5126
|
-
sy_net:
|
|
5184
|
+
sy_net: round23(sum("sy_net"))
|
|
5127
5185
|
};
|
|
5128
5186
|
}
|
|
5129
5187
|
function reportJson({ projectName = "", rows = [], bySheet = [], scaleInfo = [], markups = [], rfis = [], sheetLabel = null, conditionColumns = [], attrsByCond = null, shapeLabels = [], byLabel = [], displayUnits = "imperial", rollGoods = [] }) {
|
|
@@ -5621,7 +5679,7 @@ function computeRollTakeoff(conditions, shapes, dimsFor, uppFor) {
|
|
|
5621
5679
|
orderFt,
|
|
5622
5680
|
rollCount: rollLayoutRollCount(layout.strips),
|
|
5623
5681
|
oversize: layout.strips.some((s) => s.overRoll),
|
|
5624
|
-
qty:
|
|
5682
|
+
qty: round23(rollQtyForUnit(orderFt, config.rollWidthFt, unit)),
|
|
5625
5683
|
unit,
|
|
5626
5684
|
cutCount: layout.strips.length,
|
|
5627
5685
|
seamLf: seamLfForStrips(layout.strips),
|
|
@@ -5674,16 +5732,16 @@ function rollReportRows(rollByCond, rows) {
|
|
|
5674
5732
|
roll_length_ft: ri.config.rollLengthFt,
|
|
5675
5733
|
direction: ri.direction,
|
|
5676
5734
|
cuts: ri.cutCount,
|
|
5677
|
-
order_lf:
|
|
5735
|
+
order_lf: round23(ri.orderFt * mult),
|
|
5678
5736
|
rolls: ri.rollCount * mult,
|
|
5679
|
-
order_qty:
|
|
5737
|
+
order_qty: round23(ri.qty * mult),
|
|
5680
5738
|
order_unit: ri.unit,
|
|
5681
5739
|
oversize: ri.oversize,
|
|
5682
5740
|
// seam_lf APPENDS last (the additive-only convention the roll_goods
|
|
5683
5741
|
// block already follows): the figured weld-rod / seam-tape length, ×N
|
|
5684
5742
|
// like every other reported quantity. 0 is a real answer — a layout
|
|
5685
5743
|
// whose rooms each fit one strip has no seams to weld.
|
|
5686
|
-
seam_lf:
|
|
5744
|
+
seam_lf: round23((ri.seamLf || 0) * mult)
|
|
5687
5745
|
});
|
|
5688
5746
|
}
|
|
5689
5747
|
return out;
|
|
@@ -5790,6 +5848,59 @@ function drawShapes(ctx, toCanvas, shapes, sheetW, sheetH, longEdge) {
|
|
|
5790
5848
|
}
|
|
5791
5849
|
ctx.setLineDash([]);
|
|
5792
5850
|
}
|
|
5851
|
+
var MARK_QUESTION = "#ff8c00";
|
|
5852
|
+
var MARK_STRUCK = "#e10ee1";
|
|
5853
|
+
var MARK_RING = "#7a00e6";
|
|
5854
|
+
function polyCircle(ctx, x, y, r) {
|
|
5855
|
+
ctx.beginPath();
|
|
5856
|
+
for (let i = 0; i <= 16; i++) {
|
|
5857
|
+
const a = i / 16 * 2 * Math.PI;
|
|
5858
|
+
const px = x + r * Math.cos(a), py = y + r * Math.sin(a);
|
|
5859
|
+
if (i === 0) ctx.moveTo(px, py);
|
|
5860
|
+
else ctx.lineTo(px, py);
|
|
5861
|
+
}
|
|
5862
|
+
ctx.stroke();
|
|
5863
|
+
}
|
|
5864
|
+
function drawMarks(ctx, toCanvas, marks, longEdge) {
|
|
5865
|
+
const r = Math.max(7, longEdge / 110);
|
|
5866
|
+
const w = Math.max(1.6, longEdge / 600);
|
|
5867
|
+
ctx.setLineDash([]);
|
|
5868
|
+
ctx.lineWidth = w;
|
|
5869
|
+
let drawn = 0;
|
|
5870
|
+
for (const [mx, my] of marks.question ?? []) {
|
|
5871
|
+
const [x, y] = toCanvas(mx, my);
|
|
5872
|
+
ctx.strokeStyle = MARK_QUESTION;
|
|
5873
|
+
polyCircle(ctx, x, y, r);
|
|
5874
|
+
ctx.fillStyle = MARK_QUESTION;
|
|
5875
|
+
ctx.font = `bold ${Math.round(r * 1.4)}px sans-serif`;
|
|
5876
|
+
ctx.fillText("?", x - r * 0.35, y + r * 0.5);
|
|
5877
|
+
drawn++;
|
|
5878
|
+
}
|
|
5879
|
+
for (const [mx, my] of marks.struck ?? []) {
|
|
5880
|
+
const [x, y] = toCanvas(mx, my);
|
|
5881
|
+
ctx.strokeStyle = MARK_STRUCK;
|
|
5882
|
+
const m = r * 0.8;
|
|
5883
|
+
ctx.beginPath();
|
|
5884
|
+
ctx.moveTo(x - m, y - m);
|
|
5885
|
+
ctx.lineTo(x + m, y + m);
|
|
5886
|
+
ctx.moveTo(x - m, y + m);
|
|
5887
|
+
ctx.lineTo(x + m, y - m);
|
|
5888
|
+
ctx.stroke();
|
|
5889
|
+
ctx.beginPath();
|
|
5890
|
+
ctx.moveTo(x - r * 1.2, y);
|
|
5891
|
+
ctx.lineTo(x + r * 1.2, y);
|
|
5892
|
+
ctx.stroke();
|
|
5893
|
+
drawn++;
|
|
5894
|
+
}
|
|
5895
|
+
for (const [mx, my] of marks.ring ?? []) {
|
|
5896
|
+
const [x, y] = toCanvas(mx, my);
|
|
5897
|
+
ctx.strokeStyle = MARK_RING;
|
|
5898
|
+
polyCircle(ctx, x, y, r);
|
|
5899
|
+
polyCircle(ctx, x, y, r * 0.6);
|
|
5900
|
+
drawn++;
|
|
5901
|
+
}
|
|
5902
|
+
return drawn;
|
|
5903
|
+
}
|
|
5793
5904
|
|
|
5794
5905
|
// src/session.ts
|
|
5795
5906
|
var SNAP_CELL = 24;
|
|
@@ -6035,9 +6146,11 @@ var Session = class _Session {
|
|
|
6035
6146
|
}
|
|
6036
6147
|
const ppf = gridPxPerFoot(opts.grid, s.upp);
|
|
6037
6148
|
const sheetShapes = this.shapes.filter((x) => x.sheet_id === s.key);
|
|
6149
|
+
let marksDrawn = 0;
|
|
6038
6150
|
const { png, width, height, zoom } = await s.page.renderRegionPng(r, px, (ctx, toCanvas) => {
|
|
6039
6151
|
if (ppf) drawGrid(ctx, toCanvas, r, ppf);
|
|
6040
6152
|
if (opts.overlay) drawShapes(ctx, toCanvas, sheetShapes, s.widthPx, s.heightPx, px);
|
|
6153
|
+
if (opts.marks) marksDrawn = drawMarks(ctx, toCanvas, opts.marks, px);
|
|
6041
6154
|
});
|
|
6042
6155
|
return {
|
|
6043
6156
|
png,
|
|
@@ -6050,6 +6163,7 @@ var Session = class _Session {
|
|
|
6050
6163
|
zoom: +zoom.toFixed(4),
|
|
6051
6164
|
overlay: !!opts.overlay,
|
|
6052
6165
|
...opts.overlay ? { shapes_drawn: sheetShapes.length } : {},
|
|
6166
|
+
...opts.marks ? { marks_drawn: marksDrawn } : {},
|
|
6053
6167
|
grid_px_per_foot: ppf ? round2(ppf) : 0
|
|
6054
6168
|
}
|
|
6055
6169
|
};
|
|
@@ -7097,26 +7211,40 @@ var Session = class _Session {
|
|
|
7097
7211
|
const s = this.sheet(key);
|
|
7098
7212
|
if (s.upp == null) throw new UserError(`${key} has no scale \u2014 a transition is a real length, so set_scale first (${this.scaleGate(s)})`);
|
|
7099
7213
|
}
|
|
7100
|
-
const
|
|
7214
|
+
const frames = /* @__PURE__ */ new Map();
|
|
7101
7215
|
for (const key of sheetsInPlay) {
|
|
7102
7216
|
const s = this.sheet(key);
|
|
7103
|
-
|
|
7104
|
-
|
|
7105
|
-
|
|
7106
|
-
|
|
7107
|
-
|
|
7108
|
-
|
|
7109
|
-
|
|
7110
|
-
|
|
7111
|
-
|
|
7112
|
-
|
|
7113
|
-
|
|
7114
|
-
|
|
7115
|
-
|
|
7116
|
-
|
|
7117
|
-
|
|
7118
|
-
|
|
7119
|
-
|
|
7217
|
+
frames.set(key, { widthPx: s.widthPx, heightPx: s.heightPx, upp: s.upp });
|
|
7218
|
+
}
|
|
7219
|
+
const src = (sh) => ({ id: sh.id, sheet_id: sh.sheet_id, verts_norm: sh.verts_norm });
|
|
7220
|
+
const derived = deriveTransitionRuns(
|
|
7221
|
+
{ tag: a.finish_tag, shapes: fa.map(src) },
|
|
7222
|
+
{ tag: b.finish_tag, shapes: fb.map(src) },
|
|
7223
|
+
frames,
|
|
7224
|
+
{ max_gap_in: maxGapIn, min_run_in: minRunIn }
|
|
7225
|
+
);
|
|
7226
|
+
const committed = [], withheld = [];
|
|
7227
|
+
for (const w of derived.withheld) {
|
|
7228
|
+
withheld.push({
|
|
7229
|
+
sheet: w.sheet_id,
|
|
7230
|
+
between_shape_ids: w.between_shape_ids,
|
|
7231
|
+
length_lf: w.length_lf,
|
|
7232
|
+
gap_in: w.gap_in,
|
|
7233
|
+
at: w.at,
|
|
7234
|
+
reason: "wall_separated",
|
|
7235
|
+
detail: `${a.finish_tag} and ${b.finish_tag} run ${w.length_lf} LF apart across ${w.gap_in}" of wall \u2014 adjacent rooms, not a butt joint. If a door opens here the transition is a threshold at the door, which this cannot see.`
|
|
7236
|
+
});
|
|
7237
|
+
}
|
|
7238
|
+
for (const r of derived.runs) {
|
|
7239
|
+
const s = this.sheet(r.sheet_id);
|
|
7240
|
+
const pathPx = r.verts_norm.map(([x, y]) => [x * s.widthPx, y * s.heightPx]);
|
|
7241
|
+
const shape = this.commit(s, opts.condition, "linear", pathPx, { area_sf: 0, perimeter_lf: r.length_lf }, {
|
|
7242
|
+
method: "agent_v1",
|
|
7243
|
+
actor: "agent",
|
|
7244
|
+
reviewed: false,
|
|
7245
|
+
derived: { between_shape_ids: r.between_shape_ids, between: [a.finish_tag, b.finish_tag], case: "butt", gap_in: r.gap_in }
|
|
7246
|
+
});
|
|
7247
|
+
committed.push({ sheet: r.sheet_id, between_shape_ids: r.between_shape_ids, length_lf: r.length_lf, gap_in: r.gap_in, at: r.at, shape_id: shape.id });
|
|
7120
7248
|
}
|
|
7121
7249
|
if (committed.length) this.flushCommits("derive_transitions");
|
|
7122
7250
|
return {
|
|
@@ -7130,23 +7258,6 @@ var Session = class _Session {
|
|
|
7130
7258
|
note: withheld.length ? `${withheld.length} run(s) are adjacency ACROSS A WALL, not a butt joint \u2014 the transition there is a threshold in the doorway, and the trace record does not say where the doorway is. view_sheet each \`at\` and place them with measure_line / place_count.` : "Every run was a butt joint inside one open space. Verify with view_sheet overlay:true before trusting the total."
|
|
7131
7259
|
};
|
|
7132
7260
|
}
|
|
7133
|
-
/** One shared run → committed transition, or a disclosed question. */
|
|
7134
|
-
recordRun(s, r, upp, condition, aId, bId, aTag, bTag, committed, withheld) {
|
|
7135
|
-
const length_lf = round2(r.length_px * upp);
|
|
7136
|
-
const gap_in = round1(r.gap_px * upp * 12);
|
|
7137
|
-
const row = { sheet: s.key, between_shape_ids: [aId, bId], length_lf, gap_in, at: [Math.round(r.at[0]), Math.round(r.at[1])] };
|
|
7138
|
-
if (r.kind === "wall") {
|
|
7139
|
-
withheld.push({ ...row, reason: "wall_separated", detail: `${aTag} and ${bTag} run ${length_lf} LF apart across ${gap_in}" of wall \u2014 adjacent rooms, not a butt joint. If a door opens here the transition is a threshold at the door, which this cannot see.` });
|
|
7140
|
-
return;
|
|
7141
|
-
}
|
|
7142
|
-
const shape = this.commit(s, condition, "linear", r.path, { area_sf: 0, perimeter_lf: length_lf }, {
|
|
7143
|
-
method: "agent_v1",
|
|
7144
|
-
actor: "agent",
|
|
7145
|
-
reviewed: false,
|
|
7146
|
-
derived: { between_shape_ids: [aId, bId], between: [aTag, bTag], case: "butt", gap_in }
|
|
7147
|
-
});
|
|
7148
|
-
committed.push({ ...row, shape_id: shape.id });
|
|
7149
|
-
}
|
|
7150
7261
|
/** Count markers — the canvas's Count tool (commitCount): one point, one EA,
|
|
7151
7262
|
* computed {count: 1}, NO scale required (EA is scale-free; the canvas's
|
|
7152
7263
|
* recompute skips count shapes for the same reason). One shape per point,
|
|
@@ -7421,6 +7532,12 @@ var Session = class _Session {
|
|
|
7421
7532
|
if (opts.commit && !opts.condition) {
|
|
7422
7533
|
throw new UserError("commit: true needs a condition \u2014 the finish tag the match markers count under (e.g. 'FD-1').");
|
|
7423
7534
|
}
|
|
7535
|
+
if (opts.commitSeed && !opts.commit) {
|
|
7536
|
+
throw new UserError("commit_seed: true needs commit: true \u2014 the seed joins the same one-undo-step batch as the matches.");
|
|
7537
|
+
}
|
|
7538
|
+
if (opts.commitSeed && scope === "set") {
|
|
7539
|
+
throw new UserError("commit_seed applies to sheet scope only \u2014 in a set-wide sweep the seed may sit on a detail or legend sheet, where it is a reference drawing. If the seed instance is installed work, place_count it on its sheet explicitly.");
|
|
7540
|
+
}
|
|
7424
7541
|
const geo = await this.ensureGeometry(s);
|
|
7425
7542
|
if (!geo.segs.length) {
|
|
7426
7543
|
throw new UserError("This sheet has no vector linework (likely a scan) \u2014 symbol matching reads the drawn segments; raster fallback not yet available in the MCP server.");
|
|
@@ -7473,16 +7590,23 @@ var Session = class _Session {
|
|
|
7473
7590
|
if (!s.spans) s.spans = textSpans(s.page);
|
|
7474
7591
|
const lbl = this.sweepLabels(s.spans, geo, fp.center, res.matches, res.withheld);
|
|
7475
7592
|
let committed2;
|
|
7476
|
-
if (opts.commit && res.matches.length) {
|
|
7477
|
-
|
|
7593
|
+
if (opts.commit && (res.matches.length || opts.commitSeed)) {
|
|
7594
|
+
const points = [...opts.commitSeed ? [fp.center] : [], ...res.matches.map((m) => m.at)];
|
|
7595
|
+
const seedOrigin = {
|
|
7596
|
+
method: "symbol_sweep",
|
|
7597
|
+
actor: "agent",
|
|
7598
|
+
reviewed: false,
|
|
7599
|
+
symbol: { score: 1, rotation: 0, mirrored: false, seed: { source: "instance", sheet: s.key } }
|
|
7600
|
+
};
|
|
7601
|
+
committed2 = this.placeCount(name, points, {
|
|
7478
7602
|
condition: opts.condition,
|
|
7479
7603
|
tool: "symbol_sweep",
|
|
7480
|
-
origins: res.matches.map((m) => ({
|
|
7604
|
+
origins: [...opts.commitSeed ? [seedOrigin] : [], ...res.matches.map((m) => ({
|
|
7481
7605
|
method: "symbol_sweep",
|
|
7482
7606
|
actor: "agent",
|
|
7483
7607
|
reviewed: false,
|
|
7484
7608
|
symbol: { score: m.score, rotation: m.rotation, mirrored: m.mirrored, seed: { source: "instance", sheet: s.key } }
|
|
7485
|
-
}))
|
|
7609
|
+
}))]
|
|
7486
7610
|
});
|
|
7487
7611
|
}
|
|
7488
7612
|
const labelNote2 = _Session.sweepLabelNote(lbl);
|
|
@@ -7501,11 +7625,13 @@ var Session = class _Session {
|
|
|
7501
7625
|
committed: committed2.committed,
|
|
7502
7626
|
shape_ids: committed2.shape_ids,
|
|
7503
7627
|
condition: committed2.condition,
|
|
7504
|
-
ea_total: committed2.ea_total
|
|
7628
|
+
ea_total: committed2.ea_total,
|
|
7629
|
+
...opts.commitSeed ? { seed_committed: true } : {}
|
|
7505
7630
|
} : {},
|
|
7506
7631
|
...(() => {
|
|
7507
7632
|
const parts = [];
|
|
7508
|
-
if (opts.commit && !res.matches.length) parts.push("commit requested but nothing cleared the bar \u2014 no shapes were committed.");
|
|
7633
|
+
if (opts.commit && !res.matches.length && !opts.commitSeed) parts.push("commit requested but nothing cleared the bar \u2014 no shapes were committed.");
|
|
7634
|
+
if (committed2 && !opts.commitSeed) parts.push(`The seed instance at (${round1(fp.center[0])}, ${round1(fp.center[1])}) is NOT in this count \u2014 if it is installed work, re-run with commit_seed: true or place_count it.`);
|
|
7509
7635
|
if (labelNote2) parts.push(labelNote2);
|
|
7510
7636
|
return parts.length ? { note: parts.join(" ") } : {};
|
|
7511
7637
|
})(),
|
|
@@ -9188,6 +9314,7 @@ var symbolSweepOutput = {
|
|
|
9188
9314
|
rejected: z.array(sweepRejected).optional().describe("Sheet scope only. Placements the geometry accepted and a counter-example refused (#259) \u2014 NEVER counted in found, and never silent: each says which negative did it and what it saw. Reinstate one by hand with place_count at its `at` if you disagree"),
|
|
9189
9315
|
negatives: sweepNegatives.optional().describe("What each `exclude` rect was read as, in the order you passed them (#259)"),
|
|
9190
9316
|
rejected_total: z.number().int().optional().describe("Set scope: placements counter-examples rejected across every swept sheet"),
|
|
9317
|
+
seed_committed: z.boolean().optional().describe("Present when commit_seed: true minted the seed instance into the batch (#296) \u2014 ea_total then includes it"),
|
|
9191
9318
|
lum_gate: sweepLumGate.optional().describe("Sheet scope only. The stated stroke-luminance gate's accounting (#260): the tolerance, the seed's own luminance band, and every placement the geometry would have committed that the pen pulled under the bar \u2014 NEVER counted in found, never silent. Set scope accounts per sheet in sheets[]"),
|
|
9192
9319
|
candidates: sweepCandidates.optional().describe("Sheet scope only \u2014 set scope accounts per sheet in sheets[]"),
|
|
9193
9320
|
complete: z.boolean().describe("True when every proposed placement was scored (every swept sheet, in set scope) and the count is a total. FALSE MEANS THE COUNT IS A FLOOR \u2014 acknowledge it before trusting found (#261)"),
|
|
@@ -11503,12 +11630,13 @@ function registerTools(realServer, session) {
|
|
|
11503
11630
|
outputSchema: placeCountOutput
|
|
11504
11631
|
}, run("place_count", (a) => session.placeCount(a.sheet, a.points, { condition: a.condition })));
|
|
11505
11632
|
server.registerTool("symbol_sweep", {
|
|
11506
|
-
description: `Find EVERY instance of a repeated plan symbol from ONE example \u2014 drains, thresholds, fixtures, transition markers: marquee a tight seed_rect around a single instance and the vector linework is searched for every other placement of that same segment cluster. Deterministic geometry, not vision: each placement scores as the length-weighted fraction of the seed's segments reproduced within tolerance_px, under translation plus 0/90/180/270 rotation and mirroring (symbols rotate on plans \u2014 both ON by default; turn them off to pin orientation). Score \u2265 0.92 is a match; the 0.75\u20130.92 band comes back in \`withheld\` with a reason \u2014 a near-match is a question you answer by LOOKING (view_sheet at its \`at\`), never a silent commit and never a silent drop. The seed's own location is reported in \`seed\` and never double-committed. Every proposed placement is scored up to a hard work ceiling sized for pathological sheets, and the reply says which it was: complete true means the count is a total; complete false (with candidates.dropped > 0) means the count is a FLOOR \u2014 some placements were never scored \u2014 so tighten the seed rect around more distinctive geometry rather than trusting it as a total. Marquee discipline: the rect must hug ONE instance \u2014 only segments FULLY inside it define the symbol, so a loose rect that swallows wall linework fingerprints the wall, not the symbol. scope "set" sweeps the WHOLE working set, counting on PLAN-role sheets only (the sheet graph decides): a symbol drawn in a detail, legend, or schedule is a reference drawing and never counts itself \u2014 which is also how you seed from one: marquee the assembly on the detail sheet and its plan-sheet occurrences are counted while the detail stays excluded (the exclusion disclosed in \`skipped\`, per-sheet results with per-sheet caps and wall-clock in \`sheets\`). Scale across sheets: the fingerprint is size-true and is never scale-SEARCHED, so a detail drawn at 1-1/2" = 1'-0" is 12\xD7 the size of the same mark on a 1/8" plan \u2014 when BOTH sheets have a scale set, the exact ratio is computed from them and the seed is resized before matching (reported per sheet as \`scaled\`); when a scale is missing, the sweep runs at 1:1 and SAYS so (\`scale_assumed\`), because an unknown ratio plus a zero count is not evidence of absence. Seeding from a detail/legend/schedule sheet REFUSES outright until both scales are set \u2014 that is the case where an unstated ratio silently finds nothing. commit: true (requires condition) commits every match center as an EA count marker through the same path as place_count \u2014 the whole sweep (set-wide included) is ONE undo step, each marker carries origin.method "symbol_sweep" with its score, transform, and seed source, and withheld placements are NEVER committed. The COUNT is scale-free (EA), but matching across sheets of different scales is not \u2014 set_scale on the sheets involved is what turns the ratio from an assumption into arithmetic. Counter-examples (#259): drafting reuses one generic shape for different devices \u2014 a wall-mounted data outlet drawn as a plain triangle, the flush-floor variant the SAME triangle inside a square, keynote callouts a triangle with a letter in it \u2014 so the seed legitimately matches things you do not mean, and seeding more geometry only works where the drawing offers more to capture. \`exclude\` takes rects around instances you do NOT mean, marqueed exactly like the seed. You never choose a mechanism; the rect's contents decide, because both are the same gesture: a rect holding EXTRA linework beyond the seed rejects placements where that extra linework is present too (the box, the letter), and a rect holding no extra linework of its own is read as the line running THROUGH it \u2014 a bare ceiling-grid tile whose grid line a real fixture, drawn over it, would BREAK. That second mechanic is not expressible as a seed: only segments fully INSIDE a rect define a symbol, and background structure is long by nature. Every rejection is disclosed in rejected[] \u2014 which negative, what fraction of its evidence was found, and the placement \u2014 and NEVER counted in found: an exclusion is a judgement, so look at it and reinstate any you disagree with using place_count at its \`at\`. A counter-example that holds no instance of the seed, or holds the seed with nothing extra, is REFUSED rather than silently doing nothing. Stroke luminance (#260): a flattened export strips the layer tree and flattens every pen, but the file still STATES stroke color \u2014 a black fixture outline over a grey ceiling grid is unambiguous there even when the geometry is identical (two empty 2 ft grid tiles reproduce a 2\xD74 fixture's outline exactly). luminance_tolerance (0\u2013254) gates on it: a sheet segment only answers for a seed segment when their stroke luminances are within the stated tolerance (Rec. 709, 0 = black, 255 = white; 32\u201364 separates black from grey without touching anti-aliasing wobble). OPT-IN and disclosed, in the spirit of tolerance_px \u2014 omitted, sweeps score exactly as before; stated, the reply's lum_gate says the seed's own luminance band and names every placement the geometry would have committed and the pen did not, so you can LOOK at what a stated gate cost. Prefer geometry (a counter-example, a tighter seed) where the drawing offers it \u2014 color is the fallback for exports where nothing else survived. Labels (#308): for a LABELED family \u2014 fixtures, tagged equipment, keyed devices \u2014 the drawing already names every instance, and the sweep reads those names: a fixture token written beside a placement, or connected to it by a drawn leader line (leader-following arms only on multi-pen sheets, where the annotation pen separates from the work), comes back as \`label\` + \`label_via\` on the row, and the seed's own tag rides \`seed.label\`. Disclosure in both directions, never a recount: a committed match with NO label while the family is labeled was counted on shape alone (measured case: two 0.97 matches that were valve internals, not drains \u2014 LOOK at those first), a withheld row carrying the seed's own tag is the drawing vouching for a near-miss (look, then place_count), and a withheld row named a DIFFERENT tag is a sibling fixture answered, not a missed count. After any batch commit, LOOK at what landed \u2014 view_sheet {overlay: true} over the swept area \u2014 and audit the markers against the drawing before trusting the EA total. ${COORDS}`,
|
|
11633
|
+
description: `Find EVERY instance of a repeated plan symbol from ONE example \u2014 drains, thresholds, fixtures, transition markers: marquee a tight seed_rect around a single instance and the vector linework is searched for every other placement of that same segment cluster. Deterministic geometry, not vision: each placement scores as the length-weighted fraction of the seed's segments reproduced within tolerance_px, under translation plus 0/90/180/270 rotation and mirroring (symbols rotate on plans \u2014 both ON by default; turn them off to pin orientation). Score \u2265 0.92 is a match; the 0.75\u20130.92 band comes back in \`withheld\` with a reason \u2014 a near-match is a question you answer by LOOKING (view_sheet at its \`at\`), never a silent commit and never a silent drop. The seed's own location is reported in \`seed\` and never double-committed. Every proposed placement is scored up to a hard work ceiling sized for pathological sheets, and the reply says which it was: complete true means the count is a total; complete false (with candidates.dropped > 0) means the count is a FLOOR \u2014 some placements were never scored \u2014 so tighten the seed rect around more distinctive geometry rather than trusting it as a total. Marquee discipline: the rect must hug ONE instance \u2014 only segments FULLY inside it define the symbol, so a loose rect that swallows wall linework fingerprints the wall, not the symbol. scope "set" sweeps the WHOLE working set, counting on PLAN-role sheets only (the sheet graph decides): a symbol drawn in a detail, legend, or schedule is a reference drawing and never counts itself \u2014 which is also how you seed from one: marquee the assembly on the detail sheet and its plan-sheet occurrences are counted while the detail stays excluded (the exclusion disclosed in \`skipped\`, per-sheet results with per-sheet caps and wall-clock in \`sheets\`). Scale across sheets: the fingerprint is size-true and is never scale-SEARCHED, so a detail drawn at 1-1/2" = 1'-0" is 12\xD7 the size of the same mark on a 1/8" plan \u2014 when BOTH sheets have a scale set, the exact ratio is computed from them and the seed is resized before matching (reported per sheet as \`scaled\`); when a scale is missing, the sweep runs at 1:1 and SAYS so (\`scale_assumed\`), because an unknown ratio plus a zero count is not evidence of absence. Seeding from a detail/legend/schedule sheet REFUSES outright until both scales are set \u2014 that is the case where an unstated ratio silently finds nothing. commit: true (requires condition) commits every match center as an EA count marker through the same path as place_count \u2014 the whole sweep (set-wide included) is ONE undo step, each marker carries origin.method "symbol_sweep" with its score, transform, and seed source, and withheld placements are NEVER committed. The SEED instance is not in that count (#296) \u2014 in sheet scope it is almost always installed work, so pass commit_seed: true to mint it into the same batch (the reply reminds you whenever a sheet-scope commit leaves it out; ea_total one short of the hand tally is exactly this). The COUNT is scale-free (EA), but matching across sheets of different scales is not \u2014 set_scale on the sheets involved is what turns the ratio from an assumption into arithmetic. Counter-examples (#259): drafting reuses one generic shape for different devices \u2014 a wall-mounted data outlet drawn as a plain triangle, the flush-floor variant the SAME triangle inside a square, keynote callouts a triangle with a letter in it \u2014 so the seed legitimately matches things you do not mean, and seeding more geometry only works where the drawing offers more to capture. \`exclude\` takes rects around instances you do NOT mean, marqueed exactly like the seed. You never choose a mechanism; the rect's contents decide, because both are the same gesture: a rect holding EXTRA linework beyond the seed rejects placements where that extra linework is present too (the box, the letter), and a rect holding no extra linework of its own is read as the line running THROUGH it \u2014 a bare ceiling-grid tile whose grid line a real fixture, drawn over it, would BREAK. That second mechanic is not expressible as a seed: only segments fully INSIDE a rect define a symbol, and background structure is long by nature. Every rejection is disclosed in rejected[] \u2014 which negative, what fraction of its evidence was found, and the placement \u2014 and NEVER counted in found: an exclusion is a judgement, so look at it and reinstate any you disagree with using place_count at its \`at\`. A counter-example that holds no instance of the seed, or holds the seed with nothing extra, is REFUSED rather than silently doing nothing. Stroke luminance (#260): a flattened export strips the layer tree and flattens every pen, but the file still STATES stroke color \u2014 a black fixture outline over a grey ceiling grid is unambiguous there even when the geometry is identical (two empty 2 ft grid tiles reproduce a 2\xD74 fixture's outline exactly). luminance_tolerance (0\u2013254) gates on it: a sheet segment only answers for a seed segment when their stroke luminances are within the stated tolerance (Rec. 709, 0 = black, 255 = white; 32\u201364 separates black from grey without touching anti-aliasing wobble). OPT-IN and disclosed, in the spirit of tolerance_px \u2014 omitted, sweeps score exactly as before; stated, the reply's lum_gate says the seed's own luminance band and names every placement the geometry would have committed and the pen did not, so you can LOOK at what a stated gate cost. Prefer geometry (a counter-example, a tighter seed) where the drawing offers it \u2014 color is the fallback for exports where nothing else survived. Labels (#308): for a LABELED family \u2014 fixtures, tagged equipment, keyed devices \u2014 the drawing already names every instance, and the sweep reads those names: a fixture token written beside a placement, or connected to it by a drawn leader line (leader-following arms only on multi-pen sheets, where the annotation pen separates from the work), comes back as \`label\` + \`label_via\` on the row, and the seed's own tag rides \`seed.label\`. Disclosure in both directions, never a recount: a committed match with NO label while the family is labeled was counted on shape alone (measured case: two 0.97 matches that were valve internals, not drains \u2014 LOOK at those first), a withheld row carrying the seed's own tag is the drawing vouching for a near-miss (look, then place_count), and a withheld row named a DIFFERENT tag is a sibling fixture answered, not a missed count. After any batch commit, LOOK at what landed \u2014 view_sheet {overlay: true} over the swept area \u2014 and audit the markers against the drawing before trusting the EA total. ${COORDS}`,
|
|
11507
11634
|
inputSchema: {
|
|
11508
11635
|
sheet: z2.string().describe("The sheet the seed rect sits on \u2014 in scope 'set' it may be ANY sheet (a detail/legend seed sheet is fingerprint source only, never counted)"),
|
|
11509
11636
|
seed_rect: z2.tuple([pointSchema, pointSchema]).describe("Marquee around ONE example instance, [[x0,y0],[x1,y1]] in image px \u2014 tight: segments fully inside define the symbol"),
|
|
11510
11637
|
condition: z2.string().optional().describe("Finish tag to commit match markers under (minted on first use), e.g. 'FD-1'. Required when commit is true"),
|
|
11511
11638
|
commit: z2.boolean().default(false).describe("Commit every MATCH center as one EA count marker (withheld placements never commit)"),
|
|
11639
|
+
commit_seed: z2.boolean().default(false).describe("Sheet scope + commit only (#296): also commit the SEED instance \u2014 in sheet scope the seed is almost always installed work, and a count that excludes it bids one short. Joins the same one-undo-step batch, origin score 1. Refused in set scope, where a detail/legend seed is a reference drawing"),
|
|
11512
11640
|
scope: z2.enum(["sheet", "set"]).default("sheet").describe('"sheet" = this sheet only; "set" = every PLAN-role sheet in the working set (needs a text layer for the sheet graph; non-plan sheets are excluded and disclosed)'),
|
|
11513
11641
|
rotations: z2.boolean().default(true).describe("Also match 90/180/270-rotated placements"),
|
|
11514
11642
|
mirror: z2.boolean().default(true).describe("Also match mirrored placements"),
|
|
@@ -11526,7 +11654,8 @@ function registerTools(realServer, session) {
|
|
|
11526
11654
|
mirror: a.mirror,
|
|
11527
11655
|
tolerancePx: a.tolerance_px,
|
|
11528
11656
|
exclude: a.exclude,
|
|
11529
|
-
luminanceTolerance: a.luminance_tolerance
|
|
11657
|
+
luminanceTolerance: a.luminance_tolerance,
|
|
11658
|
+
commitSeed: a.commit_seed
|
|
11530
11659
|
})));
|
|
11531
11660
|
server.registerTool("sweep_schedule_row", {
|
|
11532
11661
|
description: `Take off a schedule row's mark from the row itself \u2014 the estimator's own gesture: a transition type sometimes exists only as a schedule row plus tag markers scattered across the plan sheets, and this tool mints the condition FROM the row and finds every occurrence. Pass the row's key (e.g. 'T1') and the tool (1) reads the row from the set's schedule tables (the sheet_graph/find_schedule machinery \u2014 the row is the condition's cited source), (2) anchors a geometric fingerprint on the marker the tag is DRAWN as on a plan sheet (a deterministic pad ladder around the tag text; where the tag occurs more than once the fingerprint must recur at a second occurrence before it is trusted \u2014 \`anchor.corroborated\`), and (3) sweeps every PLAN-role sheet for it. The count is geometry AND text agreeing: drafting reuses one bubble shape across many marks, so a match counts ONLY when the row's own tag sits within the marker footprint (its bbox rides the match as \`tag_at\` evidence); a match labeled with a SIBLING row's tag is excluded and says whose it is, an unlabeled match is withheld as a question, and a tag drawn with no matching marker is disclosed as text_only. REFUSAL over guessing, with the reason and the fix: no such row; the same key in two tables (ambiguous); a tag drawn on no plan sheet; no repeatable marker linework around the tag \u2014 a fingerprint is never guessed from text alone (the fallback is always: marquee one instance with symbol_sweep). commit: true commits the counted matches as EA markers under the row's own key \u2014 one undo step for the whole set-wide sweep, every marker carrying origin.assignment {source: "schedule"} plus the anchor and row citation on origin.symbol.seed. The COUNT is scale-free (EA), but matching is not: where the anchor sheet and a target sheet both carry a scale, the marker is resized by their exact ratio before matching (\`scaled\` per sheet), and where one does not, the sweep runs at 1:1 and discloses it (\`scale_assumed\`) rather than reporting a confident zero. After committing, LOOK: view_sheet {overlay: true} over each swept sheet. ${COORDS}`,
|
|
@@ -11779,19 +11908,24 @@ All-or-nothing, like derive_base: an unknown tag, a transition landing on either
|
|
|
11779
11908
|
outputSchema: findTextOutput
|
|
11780
11909
|
}, run("find_text", (a) => session.findText(a.sheet, a.q, { region: a.region, limit: a.limit })));
|
|
11781
11910
|
server.registerTool("view_sheet", {
|
|
11782
|
-
description: `SEE the sheet \u2014 render the page (or a crop of it) to a PNG image. This is your eyes on the plan, so CROP, DON'T SQUINT: the render downsamples to the px budget (\u22642000 long side), which on an E-size sheet is ~4 sheet pixels per returned pixel \u2014 a full-sheet render finds WHERE things are, and only a tight region crop can tell you what the linework and labels actually say. Never audit a trace or read a dimension off a full-sheet render. region is in image px \u2014 the same space as every other tool \u2014 so a feature at pixel (ix, iy) of the returned image sits at x = region_x0 + ix \xD7 (region_x1 \u2212 region_x0) / img_w (same for y), and those coordinates go straight into one_click, measure_polygon, or read_sheet_text. overlay:true burns the session's committed shapes into the render (human-affirmed ink solid red, unreviewed machine shapes dashed blue) \u2014 render again after committing to verify your geometry landed where you intended, and sanity-check what you see: a fixture-sized ring where a room should be means the seed landed inside a stall or casework; an outsized ring means the flood escaped through an opening. To MEASURE rather than guess, pass grid: a calibrated measuring grid is burned in \u2014 thin lines every 1 ft, heavy blue every 5 ft, foot labels along the crop edges, feet counted from the crop's top-left corner. Count grid cells between walls exactly like an estimator scaling a plan; never derive a dimension by eye when the grid can give it to you. grid "auto" uses the sheet's set scale; before set_scale, pass the drawing scale read off the title block as inches-per-foot \u2014 "1/4" for a 1/4" = 1'-0" plan, "3/16", "0.25". Rendering needs the optional native canvas (@napi-rs/canvas); where it isn't installed this tool errors cleanly and every other tool still works. ${COORDS}`,
|
|
11911
|
+
description: `SEE the sheet \u2014 render the page (or a crop of it) to a PNG image. This is your eyes on the plan, so CROP, DON'T SQUINT: the render downsamples to the px budget (\u22642000 long side), which on an E-size sheet is ~4 sheet pixels per returned pixel \u2014 a full-sheet render finds WHERE things are, and only a tight region crop can tell you what the linework and labels actually say. Never audit a trace or read a dimension off a full-sheet render. region is in image px \u2014 the same space as every other tool \u2014 so a feature at pixel (ix, iy) of the returned image sits at x = region_x0 + ix \xD7 (region_x1 \u2212 region_x0) / img_w (same for y), and those coordinates go straight into one_click, measure_polygon, or read_sheet_text. overlay:true burns the session's committed shapes into the render (human-affirmed ink solid red, unreviewed machine shapes dashed blue) \u2014 render again after committing to verify your geometry landed where you intended, and sanity-check what you see: a fixture-sized ring where a room should be means the seed landed inside a stall or casework; an outsized ring means the flood escaped through an opening. To MEASURE rather than guess, pass grid: a calibrated measuring grid is burned in \u2014 thin lines every 1 ft, heavy blue every 5 ft, foot labels along the crop edges, feet counted from the crop's top-left corner. Count grid cells between walls exactly like an estimator scaling a plan; never derive a dimension by eye when the grid can give it to you. grid "auto" uses the sheet's set scale; before set_scale, pass the drawing scale read off the title block as inches-per-foot \u2014 "1/4" for a 1/4" = 1'-0" plan, "3/16", "0.25". marks (#297) burns DISCLOSURE layers into the render, so what a reply names, the picture shows: pass the coordinate lists a tool disclosed \u2014 question: withheld placements (orange ?-circles), struck: rejections a counter-example or luminance gate refused (magenta struck \xD7), ring: reference points like the sweep's own seed (violet double ring). The colors sit deliberately off the common CAD pens so they cannot vanish into color-plotted work. An overlay audit without marks shows only committed ink \u2014 the validation trap where 37 disclosed near-misses read as "it missed them". Rendering needs the optional native canvas (@napi-rs/canvas); where it isn't installed this tool errors cleanly and every other tool still works. ${COORDS}`,
|
|
11783
11912
|
inputSchema: {
|
|
11784
11913
|
sheet: z2.string(),
|
|
11785
11914
|
region: z2.object({ x0: z2.number(), y0: z2.number(), x1: z2.number(), y1: z2.number() }).optional().describe("Crop rect in image px (origin top-left, y down); omit for the full sheet"),
|
|
11786
11915
|
px: z2.number().int().min(200).max(2e3).optional().describe("Long-side pixel budget of the returned image (default 1400) \u2014 small region + high px = readable dimension strings"),
|
|
11787
11916
|
overlay: z2.boolean().optional().describe("Burn committed shapes into the render (solid = human-affirmed, dashed = unreviewed)"),
|
|
11788
|
-
grid: z2.string().optional().describe(`Burn in a calibrated 1-ft/5-ft measuring grid: "auto" = the sheet's set scale; otherwise the drawing scale as inches-per-foot, e.g. "1/4", "3/16", "0.25"`)
|
|
11917
|
+
grid: z2.string().optional().describe(`Burn in a calibrated 1-ft/5-ft measuring grid: "auto" = the sheet's set scale; otherwise the drawing scale as inches-per-foot, e.g. "1/4", "3/16", "0.25"`),
|
|
11918
|
+
marks: z2.object({
|
|
11919
|
+
question: z2.array(pointSchema).optional().describe("Open questions \u2014 withheld placements, spots to look at. Orange ?-in-circle"),
|
|
11920
|
+
struck: z2.array(pointSchema).optional().describe("Refusals \u2014 rejected[] placements, lum_gate.at. Magenta struck \xD7"),
|
|
11921
|
+
ring: z2.array(pointSchema).optional().describe("Reference points \u2014 the sweep's seed.center, an anchor. Violet double ring")
|
|
11922
|
+
}).optional().describe("Disclosure marks to burn into the render (#297): what the reply names, the picture shows. Coordinates in image px")
|
|
11789
11923
|
}
|
|
11790
11924
|
}, async (a) => {
|
|
11791
11925
|
const startedAt = process.hrtime.bigint();
|
|
11792
11926
|
let reply;
|
|
11793
11927
|
try {
|
|
11794
|
-
const { png, meta } = await session.viewSheet(a.sheet, { region: a.region, px: a.px, overlay: a.overlay, grid: a.grid });
|
|
11928
|
+
const { png, meta } = await session.viewSheet(a.sheet, { region: a.region, px: a.px, overlay: a.overlay, grid: a.grid, marks: a.marks });
|
|
11795
11929
|
reply = okImage(png, meta);
|
|
11796
11930
|
} catch (e) {
|
|
11797
11931
|
reply = fail(e);
|
|
@@ -12077,7 +12211,7 @@ function nameTheStageInRefusals(server) {
|
|
|
12077
12211
|
// package.json
|
|
12078
12212
|
var package_default = {
|
|
12079
12213
|
name: "opentakeoff-mcp",
|
|
12080
|
-
version: "0.9.
|
|
12214
|
+
version: "0.9.63",
|
|
12081
12215
|
mcpName: "io.github.Kentucky-ai/opentakeoff",
|
|
12082
12216
|
type: "module",
|
|
12083
12217
|
description: "OpenTakeoff MCP server \u2014 drive the takeoff engine from your MCP client over stdio.",
|
package/package.json
CHANGED