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 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. |
@@ -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
- if (dark) {
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
- if (dark) {
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.75",
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opentakeoff-mcp",
3
- "version": "0.9.75",
3
+ "version": "0.9.76",
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.",