opentakeoff-mcp 0.9.62 → 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.
Files changed (2) hide show
  1. package/dist/server-core.js +120 -65
  2. package/package.json +1 -1
@@ -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 round22 = (n) => Math.round((n + Number.EPSILON) * 100) / 100;
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 ? round22(qty) : Math.ceil(qty - 1e-9);
5029
- return { name: m.name, unit: m.unit || "", per, basis: m.basis || "area", round: m.round !== false, note: m.note || "", basis_qty: round22(basisVal), 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: round22(floor),
5041
- wall_sf: round22(wall),
5042
- border_sf: round22(border),
5043
- lf: round22(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: round22(total),
5103
+ total_sf: round23(total),
5046
5104
  // waste-adjusted (order quantities)
5047
- floor_sf_net: round22(floor * w),
5048
- wall_sf_net: round22(wall * w),
5049
- border_sf_net: round22(border * w),
5050
- lf_net: round22(lf * w),
5051
- total_sf_net: round22(total * w),
5052
- sy_net: round22(total * w / 9),
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: round22(r.floor_sf),
5102
- wall_sf: round22(r.wall_sf),
5103
- border_sf: round22(r.border_sf),
5104
- lf: round22(r.lf),
5105
- ea: round22(r.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: round22(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: round22(sum("total_sf")),
5122
- total_sf_net: round22(sum("total_sf_net")),
5123
- lf: round22(sum("lf")),
5124
- lf_net: round22(sum("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: round22(sum("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: round22(rollQtyForUnit(orderFt, config.rollWidthFt, unit)),
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: round22(ri.orderFt * mult),
5735
+ order_lf: round23(ri.orderFt * mult),
5678
5736
  rolls: ri.rollCount * mult,
5679
- order_qty: round22(ri.qty * mult),
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: round22((ri.seamLf || 0) * mult)
5744
+ seam_lf: round23((ri.seamLf || 0) * mult)
5687
5745
  });
5688
5746
  }
5689
5747
  return out;
@@ -7153,26 +7211,40 @@ var Session = class _Session {
7153
7211
  const s = this.sheet(key);
7154
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)})`);
7155
7213
  }
7156
- const committed = [], withheld = [];
7214
+ const frames = /* @__PURE__ */ new Map();
7157
7215
  for (const key of sheetsInPlay) {
7158
7216
  const s = this.sheet(key);
7159
- const upp = s.upp;
7160
- const pxPerFt = 1 / upp;
7161
- const toPx = (sh) => sh.verts_norm.map(([x, y]) => [x * s.widthPx, y * s.heightPx]);
7162
- const onSheetA = fa.filter((x) => x.sheet_id === key), onSheetB = fb.filter((x) => x.sheet_id === key);
7163
- for (const ra of onSheetA) {
7164
- for (const rb of onSheetB) {
7165
- const runs = sharedRuns(toPx(ra), toPx(rb), {
7166
- step_px: Math.max(1, pxPerFt * 0.25),
7167
- // a quarter-foot walk — finer than any transition matters
7168
- touch_px: pxPerFt * (1 / 12),
7169
- // within an inch: one open space, not two rooms
7170
- max_gap_px: pxPerFt * (maxGapIn / 12),
7171
- min_len_px: pxPerFt * (minRunIn / 12)
7172
- });
7173
- for (const r of runs) this.recordRun(s, r, upp, opts.condition, ra.id, rb.id, a.finish_tag, b.finish_tag, committed, withheld);
7174
- }
7175
- }
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 });
7176
7248
  }
7177
7249
  if (committed.length) this.flushCommits("derive_transitions");
7178
7250
  return {
@@ -7186,23 +7258,6 @@ var Session = class _Session {
7186
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."
7187
7259
  };
7188
7260
  }
7189
- /** One shared run → committed transition, or a disclosed question. */
7190
- recordRun(s, r, upp, condition, aId, bId, aTag, bTag, committed, withheld) {
7191
- const length_lf = round2(r.length_px * upp);
7192
- const gap_in = round1(r.gap_px * upp * 12);
7193
- 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])] };
7194
- if (r.kind === "wall") {
7195
- 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.` });
7196
- return;
7197
- }
7198
- const shape = this.commit(s, condition, "linear", r.path, { area_sf: 0, perimeter_lf: length_lf }, {
7199
- method: "agent_v1",
7200
- actor: "agent",
7201
- reviewed: false,
7202
- derived: { between_shape_ids: [aId, bId], between: [aTag, bTag], case: "butt", gap_in }
7203
- });
7204
- committed.push({ ...row, shape_id: shape.id });
7205
- }
7206
7261
  /** Count markers — the canvas's Count tool (commitCount): one point, one EA,
7207
7262
  * computed {count: 1}, NO scale required (EA is scale-free; the canvas's
7208
7263
  * recompute skips count shapes for the same reason). One shape per point,
@@ -12156,7 +12211,7 @@ function nameTheStageInRefusals(server) {
12156
12211
  // package.json
12157
12212
  var package_default = {
12158
12213
  name: "opentakeoff-mcp",
12159
- version: "0.9.62",
12214
+ version: "0.9.63",
12160
12215
  mcpName: "io.github.Kentucky-ai/opentakeoff",
12161
12216
  type: "module",
12162
12217
  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.62",
3
+ "version": "0.9.63",
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.",