opentakeoff-mcp 0.9.75 → 0.9.76
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 +27 -13
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -178,7 +178,7 @@ reads the tool list once. ([#230](https://github.com/Kentucky-ai/opentakeoff/iss
|
|
|
178
178
|
| `import_takeoff` | **The way back in**: load a `takeoff_canvas.v1` file (a prior `export_takeoff`, or the app's own save) through the SAME merge rules as the app's Sheet-menu import—finish-tag identity joins conditions (this session's knobs win), new ids append, duplicates skip (idempotent re-import), this session's calibration wins per sheet. Resume, extend, or audit. Correction rules (#88) ride the file too—`apply_rules` re-runs them. |
|
|
179
179
|
| `apply_rules` | **Re-run the correction rules the takeoff arrived with** (#207)—the lessons an estimator TAUGHT the canvas (#88), for example, "every room like this loses the mechanical chase". Same pure `rules.ts` engine the canvas Preview runs; committed as the ONE batch its Apply makes (`reviewed: false`, one undo step, `origin {method rule_v1, actor rule}` with the rule/seed/room citation). The reply's per-rule disclosure—produced, skipped, ids—IS the preview an agent gets. Idempotent by construction: anything an existing deduct covers is dropped by the engine, so re-running after new rooms commit is the intended workflow. Rules arrive ONLY through `import_takeoff`; minting one is an estimator's correction and stays behind the canvas's human Preview→Apply gate. |
|
|
180
180
|
| `export_dxf` | The takeoff as a **CAD drawing**—a DXF (R2000) that AutoCAD, BricsCAD, LibreCAD and Revit import as native geometry. One sheet per file, like a DWG: every committed shape becomes an `LWPOLYLINE` (floor rings closed, walls and linear runs open, counts a circle) on a layer named for its finish—`OT-<TAG>`, with `-DEDUCT` / `-HOLE` / `-WALL` / `-LINEAR` / `-COUNT` suffix layers and room labels on `OT-LABELS`. Real units in the sheet's frame (origin bottom-left, Y up; feet, or `units:"m"`), and a ring's CAD area equals its `export_report` area to rounding. Refuses without a scale; with several sheets carrying shapes, `sheet` picks the drawing. Names every shape left out and why. `path` required. |
|
|
181
|
-
| `export_marked_pdf` | The **marked-up planset**—the deliverable. Writes a distribution-ready PDF: a legend cover (per-condition totals, swatches, by-sheet breakdown) plus every sheet that carries work, vector-copied from the source with shapes, hatches, per-shape quantity chips, and annotations burned in—built by the same module as the canvas's MARKED SET button. Machine-traced shapes are disclosed as pending human review on the document itself, and the cover states where the finish tags came from (`Finish assignment: N schedule-resolved · N agent-asserted · …`, plus any rooms the last assign run withheld). Default path: `<plan> - marked set.pdf` next to the plan (see **Writing to disk** below). Works without `@napi-rs/canvas`. |
|
|
181
|
+
| `export_marked_pdf` | The **marked-up planset**—the deliverable. Writes a distribution-ready PDF: a legend cover (per-condition totals, swatches, by-sheet breakdown) plus every sheet that carries work, vector-copied from the source with shapes, hatches, per-shape quantity chips, and annotations burned in—built by the same module as the canvas's MARKED SET button. Machine-traced shapes are disclosed as pending human review on the document itself, and the cover states where the finish tags came from (`Finish assignment: N schedule-resolved · N agent-asserted · …`, plus any rooms the last assign run withheld). Default path: `<plan> - marked set.pdf` next to the plan (see **Writing to disk** below). Works without `@napi-rs/canvas`. Refuses an **encrypted** source PDF (owner password, empty user password) with the sheet named — its pages cannot be vector-copied and the server has no canvas to render them; export from the app, or supply an unencrypted PDF. |
|
|
182
182
|
| `list_shapes` | The **mid-session inventory**: every committed shape's id, sheet, condition, role, quantities, room `label`, review state, and assignment verdict (`schedule` \| `asserted`—where its finish tag came from) in one compact read—the ids `edit_shape`/`delete_shape` assume you have, without pulling the whole `export_takeoff` payload. Filters by sheet/condition narrow; empty is a result, not an error. |
|
|
183
183
|
| `undo_last` | Step back over your own last `n` mutations, newest first. Exact inverses: a commit is removed, an edit restored verbatim, a delete re-inserted where it was, a materials edit's whole array restored, a condition edit's waste/multiplier pair restored. A whole `detect_rooms` sweep is **one** step. |
|
|
184
184
|
| `annotate` | Place a note ABOUT the work—cloud/highlight (`rect`), text (`at`), callout (`at` + `target`), **arrow** (`from` + `to`—plank/seam direction), **bubble** (`at` + optional `r`—keynote circle, centered text), **dimension** (`from` + `to`—a dimension line with end ticks, labeled with the measured length at the sheet's scale; the one annotation the scale gate applies to—an unscaled sheet refuses like the measure tools). Attach to a condition and it wears that scope's color on the canvas and in the marked set. No review gate: notes are not geometry. |
|
package/dist/server-core.js
CHANGED
|
@@ -12930,6 +12930,19 @@ function boostForDark(hex2) {
|
|
|
12930
12930
|
return "#" + toHex2(nr) + toHex2(ng) + toHex2(nb);
|
|
12931
12931
|
}
|
|
12932
12932
|
|
|
12933
|
+
// ../web/src/lib/markedsetSource.js
|
|
12934
|
+
function sourcePageMode({ dark = false, encrypted = false } = {}) {
|
|
12935
|
+
if (dark) return "raster-inverted";
|
|
12936
|
+
if (encrypted) return "raster";
|
|
12937
|
+
return "vector";
|
|
12938
|
+
}
|
|
12939
|
+
function sourceStampNote(mode) {
|
|
12940
|
+
return mode === "raster" ? " \xB7 raster copy \u2014 the source PDF is encrypted, so its vector page cannot be embedded" : "";
|
|
12941
|
+
}
|
|
12942
|
+
function noCanvasForRasterMessage(label) {
|
|
12943
|
+
return `${label ? `${label}: ` : ""}the source PDF is encrypted, so its page cannot be copied as vector, and this environment has no canvas to render it as an image. Export the marked set from the app, or supply an unencrypted PDF.`;
|
|
12944
|
+
}
|
|
12945
|
+
|
|
12933
12946
|
// ../web/src/lib/markedset.js
|
|
12934
12947
|
function authorTallyLine(shapes) {
|
|
12935
12948
|
const byAuthor = /* @__PURE__ */ new Map();
|
|
@@ -13341,7 +13354,7 @@ async function buildMarkedSetPdf({ projectName, dark, sheets, shapes, markups, a
|
|
|
13341
13354
|
return src;
|
|
13342
13355
|
};
|
|
13343
13356
|
for (const sh of marked) {
|
|
13344
|
-
let pg, toPage, chipRot = degrees(0), W, H;
|
|
13357
|
+
let pg, toPage, chipRot = degrees(0), W, H, mode = "vector";
|
|
13345
13358
|
if (sh.stitch) {
|
|
13346
13359
|
const members = sh.stitch.members;
|
|
13347
13360
|
const pages2 = [];
|
|
@@ -13356,7 +13369,10 @@ async function buildMarkedSetPdf({ projectName, dark, sheets, shapes, markups, a
|
|
|
13356
13369
|
W = plan.extent.w;
|
|
13357
13370
|
H = plan.extent.h;
|
|
13358
13371
|
const pageW = W / RENDER_SCALE, pageH = H / RENDER_SCALE;
|
|
13359
|
-
|
|
13372
|
+
const encrypted = !dark && (await Promise.all(members.map((m) => srcDocFor(m.file)))).some((d) => d.isEncrypted);
|
|
13373
|
+
mode = sourcePageMode({ dark, encrypted });
|
|
13374
|
+
if (mode === "raster" && typeof document === "undefined") throw new Error(noCanvasForRasterMessage(sh.label));
|
|
13375
|
+
if (mode !== "vector") {
|
|
13360
13376
|
const s = Math.min(RASTER_MAX / Math.max(pageW, pageH), 4);
|
|
13361
13377
|
const cv = document.createElement("canvas");
|
|
13362
13378
|
cv.width = Math.ceil(pageW * s);
|
|
@@ -13379,7 +13395,7 @@ async function buildMarkedSetPdf({ projectName, dark, sheets, shapes, markups, a
|
|
|
13379
13395
|
ctx.drawImage(mc, pm.dx * k, pm.dy * k);
|
|
13380
13396
|
ctx.restore();
|
|
13381
13397
|
}
|
|
13382
|
-
invertPixels(cv);
|
|
13398
|
+
if (dark) invertPixels(cv);
|
|
13383
13399
|
const png = await doc.embedPng(cv.toDataURL("image/png"));
|
|
13384
13400
|
pg = doc.addPage([pageW, pageH]);
|
|
13385
13401
|
pg.drawImage(png, { x: 0, y: 0, width: pageW, height: pageH });
|
|
@@ -13399,7 +13415,10 @@ async function buildMarkedSetPdf({ projectName, dark, sheets, shapes, markups, a
|
|
|
13399
13415
|
const vpR = page.getViewport({ scale: RENDER_SCALE });
|
|
13400
13416
|
W = vpR.width;
|
|
13401
13417
|
H = vpR.height;
|
|
13402
|
-
|
|
13418
|
+
const src = dark ? null : await srcDocFor(sh.file);
|
|
13419
|
+
mode = sourcePageMode({ dark, encrypted: !!src?.isEncrypted });
|
|
13420
|
+
if (mode === "raster" && typeof document === "undefined") throw new Error(noCanvasForRasterMessage(sh.label));
|
|
13421
|
+
if (mode !== "vector") {
|
|
13403
13422
|
const vp1 = page.getViewport({ scale: 1 });
|
|
13404
13423
|
const s = Math.min(RASTER_MAX / Math.max(vp1.width, vp1.height), 4);
|
|
13405
13424
|
const vp = page.getViewport({ scale: s });
|
|
@@ -13407,18 +13426,13 @@ async function buildMarkedSetPdf({ projectName, dark, sheets, shapes, markups, a
|
|
|
13407
13426
|
cv.width = Math.ceil(vp.width);
|
|
13408
13427
|
cv.height = Math.ceil(vp.height);
|
|
13409
13428
|
await page.render({ canvasContext: cv.getContext("2d"), viewport: vp }).promise;
|
|
13410
|
-
invertPixels(cv);
|
|
13429
|
+
if (dark) invertPixels(cv);
|
|
13411
13430
|
const png = await doc.embedPng(cv.toDataURL("image/png"));
|
|
13412
13431
|
pg = doc.addPage([vp1.width, vp1.height]);
|
|
13413
13432
|
pg.drawImage(png, { x: 0, y: 0, width: vp1.width, height: vp1.height });
|
|
13414
13433
|
const k = vp1.width / W;
|
|
13415
13434
|
toPage = (x, y) => [x * k, vp1.height - y * k];
|
|
13416
13435
|
} else {
|
|
13417
|
-
let src = srcDocs.get(sh.file);
|
|
13418
|
-
if (!src) {
|
|
13419
|
-
src = await PDFDocument.load(await loadPdfData(sh.file), { ignoreEncryption: true });
|
|
13420
|
-
srcDocs.set(sh.file, src);
|
|
13421
|
-
}
|
|
13422
13436
|
const [copied] = await doc.copyPages(src, [sh.page - 1]);
|
|
13423
13437
|
pg = doc.addPage(copied);
|
|
13424
13438
|
const [a, b, c, d, e, f] = vpR.transform;
|
|
@@ -13663,7 +13677,7 @@ async function buildMarkedSetPdf({ projectName, dark, sheets, shapes, markups, a
|
|
|
13663
13677
|
const tw = bold.widthOfTextAtSize(label, size);
|
|
13664
13678
|
pg.drawText(label, { x: pcx - tw / 2, y: pcy - size / 2.7, size, font: bold, color: acol, rotate: chipRot });
|
|
13665
13679
|
}
|
|
13666
|
-
const stamp = sh.stitch ? `${sh.label} \xB7 stitched composite (${sh.stitch.members.map((m) => m.label || m.key).join(" + ")}) \xB7 marked set` : `${sh.label} \xB7 marked set
|
|
13680
|
+
const stamp = (sh.stitch ? `${sh.label} \xB7 stitched composite (${sh.stitch.members.map((m) => m.label || m.key).join(" + ")}) \xB7 marked set` : `${sh.label} \xB7 marked set`) + sourceStampNote(mode);
|
|
13667
13681
|
text(stamp, 14, 20, 8, muted);
|
|
13668
13682
|
}
|
|
13669
13683
|
const allPages = doc.getPages();
|
|
@@ -14373,7 +14387,7 @@ All-or-nothing, like derive_base: an unknown tag, a transition landing on either
|
|
|
14373
14387
|
outputSchema: applyRulesOutput
|
|
14374
14388
|
}, run("apply_rules", (a) => session.applyRules(a)));
|
|
14375
14389
|
server.registerTool("export_marked_pdf", {
|
|
14376
|
-
description: `The MARKED-UP PLANSET \u2014 the deliverable of every takeoff. Writes a distribution-ready PDF to disk: a legend cover (per-condition totals, swatches, a by-sheet breakdown) followed by every sheet that carries takeoff shapes or annotations, vector-copied from the source plan with the work burned in as drawn \u2014 condition colors and hatches, a quantity chip on every shape, annotation clouds/callouts/highlights, and approval marks (the estimator's APPROVED rings, the agent's AGENT diamonds \u2014 the cover tallies the split). Built by the same module as the canvas's MARKED SET button, so agent output and app output are one implementation. A construction takeoff is no good without markup: finish EVERY takeoff by writing this file and giving the user its path (export_report carries the numbers for pricing; this carries the evidence). When the shapes were machine-traced and unreviewed, the document says so on its last page \u2014 the review path is importing the export_takeoff payload into the app, where agent shapes arrive as pencil proposals. Default path: next to the loaded plan as "<plan> - marked set.pdf". Needs no native canvas \u2014 pure vector copy, so it works even where view_sheet cannot render.`,
|
|
14390
|
+
description: `The MARKED-UP PLANSET \u2014 the deliverable of every takeoff. Writes a distribution-ready PDF to disk: a legend cover (per-condition totals, swatches, a by-sheet breakdown) followed by every sheet that carries takeoff shapes or annotations, vector-copied from the source plan with the work burned in as drawn \u2014 condition colors and hatches, a quantity chip on every shape, annotation clouds/callouts/highlights, and approval marks (the estimator's APPROVED rings, the agent's AGENT diamonds \u2014 the cover tallies the split). Built by the same module as the canvas's MARKED SET button, so agent output and app output are one implementation. A construction takeoff is no good without markup: finish EVERY takeoff by writing this file and giving the user its path (export_report carries the numbers for pricing; this carries the evidence). When the shapes were machine-traced and unreviewed, the document says so on its last page \u2014 the review path is importing the export_takeoff payload into the app, where agent shapes arrive as pencil proposals. Default path: next to the loaded plan as "<plan> - marked set.pdf". Needs no native canvas \u2014 pure vector copy, so it works even where view_sheet cannot render. The one source it refuses: an ENCRYPTED plan PDF (owner password, empty user password \u2014 it opens everywhere, but its pages cannot be vector-copied and there is no canvas here to render them); the refusal names the sheet \u2014 export the marked set from the app, or supply an unencrypted PDF.`,
|
|
14377
14391
|
inputSchema: {
|
|
14378
14392
|
path: z2.string().optional().describe('Where to write the PDF (default: "<plan dir>/<plan> - marked set.pdf")'),
|
|
14379
14393
|
project_name: z2.string().optional().describe("Cover-page project name (default: the plan file's name)"),
|
|
@@ -14939,7 +14953,7 @@ function nameTheStageInRefusals(server) {
|
|
|
14939
14953
|
// package.json
|
|
14940
14954
|
var package_default = {
|
|
14941
14955
|
name: "opentakeoff-mcp",
|
|
14942
|
-
version: "0.9.
|
|
14956
|
+
version: "0.9.76",
|
|
14943
14957
|
mcpName: "io.github.Kentucky-ai/opentakeoff",
|
|
14944
14958
|
type: "module",
|
|
14945
14959
|
description: "OpenTakeoff MCP server \u2014 drive the takeoff engine from your MCP client over stdio.",
|
package/package.json
CHANGED