opentakeoff-mcp 0.9.63 → 0.9.65
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 -0
- package/dist/server-core.js +991 -9
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -158,6 +158,7 @@ reads the tool list once. ([#230](https://github.com/Kentucky-ai/opentakeoff/iss
|
|
|
158
158
|
| `export_report` | The **computed Report document**—`opentakeoff.report.v1`, the same JSON the canvas Report exports: gross + waste-adjusted quantities, the computed materials **buy list** per condition plus the project-wide roll-up, per-sheet base subtotals, and scale provenance. The contract for pricing consumers—`export_takeoff` carries materials as config rows, `takeoff_summary` strips them. Inline, and to disk with `path` (see **Writing to disk** below). |
|
|
159
159
|
| `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. |
|
|
160
160
|
| `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. |
|
|
161
|
+
| `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. |
|
|
161
162
|
| `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`. |
|
|
162
163
|
| `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. |
|
|
163
164
|
| `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. |
|
package/dist/server-core.js
CHANGED
|
@@ -1432,6 +1432,306 @@ function classifyOffsetAnnotationSegs(segs, meta, ws, maxOffsetPx, minOffsetPx,
|
|
|
1432
1432
|
}
|
|
1433
1433
|
return soft;
|
|
1434
1434
|
}
|
|
1435
|
+
var DIMSTR_ANGLE_TOL = 2;
|
|
1436
|
+
var DIMSTR_CHAIN_GAP_FT = 0.6;
|
|
1437
|
+
var DIMSTR_CHAIN_GAP_PX = 3;
|
|
1438
|
+
var DIMSTR_CHAIN_OFF_FT = 0.06;
|
|
1439
|
+
var DIMSTR_CHAIN_OFF_PX = 1.25;
|
|
1440
|
+
var DIMSTR_RUN_MIN_FT = 4;
|
|
1441
|
+
var DIMSTR_ISO_OFF_FT = 1;
|
|
1442
|
+
var DIMSTR_ISO_OVERLAP = 0.4;
|
|
1443
|
+
var DIMSTR_WITNESS_MIN_FT = 0.3;
|
|
1444
|
+
var DIMSTR_WITNESS_SOFT_MAX_FT = 8;
|
|
1445
|
+
var DIMSTR_ATTACH_FT = 0.35;
|
|
1446
|
+
var DIMSTR_ATTACH_PX = 2;
|
|
1447
|
+
var DIMSTR_JUNCTION_MIN_FT = 1.5;
|
|
1448
|
+
var DIMSTR_WIT_PER_JUNCTION_MAX = 2;
|
|
1449
|
+
var DIMSTR_TICK_MIN_FT = 0.2;
|
|
1450
|
+
var DIMSTR_TICK_MAX_FT = 1.5;
|
|
1451
|
+
var DIMSTR_TICK_ANG_LO = 20;
|
|
1452
|
+
var DIMSTR_TICK_ANG_HI = 70;
|
|
1453
|
+
var DIMSTR_TICK_AT_JUNCTION_FT = 0.5;
|
|
1454
|
+
var DIMSTR_MIN_JUNCTIONS = 2;
|
|
1455
|
+
var DIMSTR_MIN_TICKED = 2;
|
|
1456
|
+
var DIMSTR_TICKED_FRAC = 0.5;
|
|
1457
|
+
var DIMSTR_OVERSHOOT_FT = 0.1;
|
|
1458
|
+
var DIMSTR_OVERSHOOT_PX = 2;
|
|
1459
|
+
var DIMSTR_TOUCH_CLUTTER_MAX = 1;
|
|
1460
|
+
var DIMTEXT_NEAR_FT = 1;
|
|
1461
|
+
var DIMTEXT_REACH_FT = 40;
|
|
1462
|
+
var DIMTEXT_MIN_LINE_X = 1.5;
|
|
1463
|
+
var DIMTEXT_STUB_MAX_FT = 2.5;
|
|
1464
|
+
var DIMSTR_LINE_MARGIN_FT = 0.5;
|
|
1465
|
+
var DIMSTR_PIECE_MAX_DIMFRAC = 0.66;
|
|
1466
|
+
var DIMSTR_PIECE_MAX_CURVEFRAC = 0.25;
|
|
1467
|
+
var DIMSTR_BAND_FT = 0.7;
|
|
1468
|
+
var DIMSTR_BAND_CLUTTER_MAX = 4;
|
|
1469
|
+
function sweepDimensionStrings(segs, meta, ws, ftPx, rejects, dimTexts) {
|
|
1470
|
+
const n = segs.length >> 2;
|
|
1471
|
+
const soft = new Uint8Array(n);
|
|
1472
|
+
const hits = [];
|
|
1473
|
+
if (!meta || !n || !(ftPx > 0)) return { soft, runs: hits };
|
|
1474
|
+
const cand = [];
|
|
1475
|
+
for (let i = 0; i < n; i++) {
|
|
1476
|
+
if (meta[i] & (SEG_CURVE | SEG_CLIP | SEG_FILLONLY)) continue;
|
|
1477
|
+
const x1 = segs[i * 4] * ws, y1 = segs[i * 4 + 1] * ws, x2 = segs[i * 4 + 2] * ws, y2 = segs[i * 4 + 3] * ws;
|
|
1478
|
+
const L = Math.hypot(x2 - x1, y2 - y1);
|
|
1479
|
+
if (!(L > 0)) continue;
|
|
1480
|
+
let ang = Math.atan2(y2 - y1, x2 - x1) * 180 / Math.PI;
|
|
1481
|
+
if (ang < 0) ang += 180;
|
|
1482
|
+
if (ang >= 180) ang -= 180;
|
|
1483
|
+
cand.push({ i, ang, x1, y1, x2, y2, L });
|
|
1484
|
+
}
|
|
1485
|
+
if (!cand.length) return { soft, runs: hits };
|
|
1486
|
+
const BIN = DIMSTR_ANGLE_TOL, NB = Math.round(180 / BIN);
|
|
1487
|
+
const gap = Math.max(DIMSTR_CHAIN_GAP_PX, DIMSTR_CHAIN_GAP_FT * ftPx);
|
|
1488
|
+
const offTol = Math.max(DIMSTR_CHAIN_OFF_PX, DIMSTR_CHAIN_OFF_FT * ftPx);
|
|
1489
|
+
const chains = [];
|
|
1490
|
+
const claimed = new Uint8Array(n);
|
|
1491
|
+
for (const shift of [0, BIN / 2]) {
|
|
1492
|
+
const bins = /* @__PURE__ */ new Map();
|
|
1493
|
+
for (const c of cand) {
|
|
1494
|
+
if (claimed[c.i]) continue;
|
|
1495
|
+
let b = Math.floor((c.ang + shift) / BIN);
|
|
1496
|
+
if (b >= NB) b -= NB;
|
|
1497
|
+
const axis = ((b * BIN - shift + BIN / 2) % 180 + 180) % 180;
|
|
1498
|
+
const th = axis * Math.PI / 180, dx = Math.cos(th), dy = Math.sin(th), nx = -dy, ny = dx;
|
|
1499
|
+
const r = {
|
|
1500
|
+
c,
|
|
1501
|
+
d: (c.x1 + c.x2) / 2 * nx + (c.y1 + c.y2) / 2 * ny,
|
|
1502
|
+
t0: Math.min(c.x1 * dx + c.y1 * dy, c.x2 * dx + c.y2 * dy),
|
|
1503
|
+
t1: Math.max(c.x1 * dx + c.y1 * dy, c.x2 * dx + c.y2 * dy)
|
|
1504
|
+
};
|
|
1505
|
+
const arr2 = bins.get(b);
|
|
1506
|
+
if (arr2) arr2.push(r);
|
|
1507
|
+
else bins.set(b, [r]);
|
|
1508
|
+
}
|
|
1509
|
+
for (const [b, rows] of bins) {
|
|
1510
|
+
const axis = ((b * BIN - shift + BIN / 2) % 180 + 180) % 180;
|
|
1511
|
+
rows.sort((a, z4) => a.d - z4.d || a.t0 - z4.t0);
|
|
1512
|
+
let g0 = 0;
|
|
1513
|
+
for (let k = 1; k <= rows.length; k++) {
|
|
1514
|
+
if (k < rows.length && rows[k].d - rows[k - 1].d <= offTol) continue;
|
|
1515
|
+
const grp = rows.slice(g0, k).sort((a, z4) => a.t0 - z4.t0 || a.t1 - z4.t1);
|
|
1516
|
+
let cur = null;
|
|
1517
|
+
const flush = () => {
|
|
1518
|
+
if (!cur) return;
|
|
1519
|
+
if (shift === 0 || cur.members.length > 1 || !claimed[cur.members[0]]) {
|
|
1520
|
+
if (shift === 0 && cur.members.length < 2) {
|
|
1521
|
+
cur = null;
|
|
1522
|
+
return;
|
|
1523
|
+
}
|
|
1524
|
+
cur.d = cur.dSum / cur.members.length;
|
|
1525
|
+
cur.len = cur.t1 - cur.t0;
|
|
1526
|
+
const th = axis * Math.PI / 180, dx = Math.cos(th), dy = Math.sin(th), nx = -dy, ny = dx;
|
|
1527
|
+
cur.p0 = [cur.t0 * dx + cur.d * nx, cur.t0 * dy + cur.d * ny];
|
|
1528
|
+
cur.p1 = [cur.t1 * dx + cur.d * nx, cur.t1 * dy + cur.d * ny];
|
|
1529
|
+
chains.push(cur);
|
|
1530
|
+
for (const m of cur.members) claimed[m] = 1;
|
|
1531
|
+
}
|
|
1532
|
+
cur = null;
|
|
1533
|
+
};
|
|
1534
|
+
for (const r of grp) {
|
|
1535
|
+
if (cur && r.t0 - cur.t1 <= gap) {
|
|
1536
|
+
cur.t1 = Math.max(cur.t1, r.t1);
|
|
1537
|
+
cur.members.push(r.c.i);
|
|
1538
|
+
cur.dSum += r.d;
|
|
1539
|
+
} else {
|
|
1540
|
+
flush();
|
|
1541
|
+
cur = { axis, d: 0, dSum: r.d, t0: r.t0, t1: r.t1, len: 0, members: [r.c.i], p0: [0, 0], p1: [0, 0] };
|
|
1542
|
+
}
|
|
1543
|
+
}
|
|
1544
|
+
flush();
|
|
1545
|
+
g0 = k;
|
|
1546
|
+
}
|
|
1547
|
+
}
|
|
1548
|
+
}
|
|
1549
|
+
const runMin = DIMSTR_RUN_MIN_FT * ftPx, isoOff = DIMSTR_ISO_OFF_FT * ftPx;
|
|
1550
|
+
const wMin = DIMSTR_WITNESS_MIN_FT * ftPx, wSoftMax = DIMSTR_WITNESS_SOFT_MAX_FT * ftPx;
|
|
1551
|
+
const attach = Math.max(DIMSTR_ATTACH_PX, DIMSTR_ATTACH_FT * ftPx);
|
|
1552
|
+
const jMin = DIMSTR_JUNCTION_MIN_FT * ftPx;
|
|
1553
|
+
const tickMin = DIMSTR_TICK_MIN_FT * ftPx, tickMax = DIMSTR_TICK_MAX_FT * ftPx;
|
|
1554
|
+
const tickAt = DIMSTR_TICK_AT_JUNCTION_FT * ftPx;
|
|
1555
|
+
const over = Math.max(DIMSTR_OVERSHOOT_PX, DIMSTR_OVERSHOOT_FT * ftPx);
|
|
1556
|
+
const band = DIMSTR_BAND_FT * ftPx;
|
|
1557
|
+
const hasParallelPartner = (C, skip) => {
|
|
1558
|
+
const th = C.axis * Math.PI / 180, dx = Math.cos(th), dy = Math.sin(th), nx = -dy, ny = dx;
|
|
1559
|
+
for (const O of chains) {
|
|
1560
|
+
if (O === C || O === skip) continue;
|
|
1561
|
+
let rel = Math.abs(O.axis - C.axis);
|
|
1562
|
+
if (rel > 90) rel = 180 - rel;
|
|
1563
|
+
if (rel > 3 * DIMSTR_ANGLE_TOL) continue;
|
|
1564
|
+
const d0 = O.p0[0] * nx + O.p0[1] * ny - C.d, d1 = O.p1[0] * nx + O.p1[1] * ny - C.d;
|
|
1565
|
+
const gapAbs = Math.min(Math.abs(d0), Math.abs(d1));
|
|
1566
|
+
if (gapAbs > isoOff || gapAbs < attach / 2) continue;
|
|
1567
|
+
const t0 = Math.min(O.p0[0] * dx + O.p0[1] * dy, O.p1[0] * dx + O.p1[1] * dy);
|
|
1568
|
+
const t1 = Math.max(O.p0[0] * dx + O.p0[1] * dy, O.p1[0] * dx + O.p1[1] * dy);
|
|
1569
|
+
if (Math.min(t1, C.t1) - Math.max(t0, C.t0) >= DIMSTR_ISO_OVERLAP * C.len) return true;
|
|
1570
|
+
}
|
|
1571
|
+
return false;
|
|
1572
|
+
};
|
|
1573
|
+
const reject = (R, why) => {
|
|
1574
|
+
rejects?.push({ axis: R.axis, d: R.d, t0: R.t0, t1: R.t1, lenFt: R.len / ftPx, why });
|
|
1575
|
+
};
|
|
1576
|
+
for (const R of chains) {
|
|
1577
|
+
if (R.len < runMin) continue;
|
|
1578
|
+
if (hasParallelPartner(R, null)) {
|
|
1579
|
+
reject(R, "iso");
|
|
1580
|
+
continue;
|
|
1581
|
+
}
|
|
1582
|
+
const th = R.axis * Math.PI / 180, dx = Math.cos(th), dy = Math.sin(th), nx = -dy, ny = dx;
|
|
1583
|
+
const wit = [], ticks = [];
|
|
1584
|
+
let touchClutter = 0;
|
|
1585
|
+
for (const C of chains) {
|
|
1586
|
+
if (C === R) continue;
|
|
1587
|
+
const d0 = C.p0[0] * nx + C.p0[1] * ny - R.d, d1 = C.p1[0] * nx + C.p1[1] * ny - R.d;
|
|
1588
|
+
const near0 = Math.abs(d0) <= attach, near1 = Math.abs(d1) <= attach;
|
|
1589
|
+
const crosses = d0 < -attach && d1 > attach || d1 < -attach && d0 > attach;
|
|
1590
|
+
if (!near0 && !near1 && !crosses) continue;
|
|
1591
|
+
const t0 = C.p0[0] * dx + C.p0[1] * dy, t1 = C.p1[0] * dx + C.p1[1] * dy;
|
|
1592
|
+
const tm = (t0 + t1) / 2;
|
|
1593
|
+
const tRef = near0 && !near1 ? t0 : near1 && !near0 ? t1 : tm;
|
|
1594
|
+
if (tRef < R.t0 - attach || tRef > R.t1 + attach) continue;
|
|
1595
|
+
let rel = Math.abs(C.axis - R.axis);
|
|
1596
|
+
if (rel > 90) rel = 180 - rel;
|
|
1597
|
+
if (rel >= 90 - 2 * DIMSTR_ANGLE_TOL && C.len >= wMin) {
|
|
1598
|
+
wit.push({ t: tRef, C, past: crosses || near0 && near1 });
|
|
1599
|
+
} else if (rel >= DIMSTR_TICK_ANG_LO && rel <= DIMSTR_TICK_ANG_HI && C.len >= tickMin && C.len <= tickMax && Math.abs((d0 + d1) / 2) <= attach) {
|
|
1600
|
+
ticks.push({ t: tm, C, past: false });
|
|
1601
|
+
} else if (++touchClutter > DIMSTR_TOUCH_CLUTTER_MAX) break;
|
|
1602
|
+
}
|
|
1603
|
+
if (touchClutter > DIMSTR_TOUCH_CLUTTER_MAX) {
|
|
1604
|
+
reject(R, "touch-clutter");
|
|
1605
|
+
continue;
|
|
1606
|
+
}
|
|
1607
|
+
const live = wit.filter((w) => w.past || w.t >= R.t0 + over && w.t <= R.t1 - over);
|
|
1608
|
+
if (live.length < DIMSTR_MIN_JUNCTIONS) {
|
|
1609
|
+
reject(R, `witnesses (raw=${wit.length} live=${live.length} ticks=${ticks.length})`);
|
|
1610
|
+
continue;
|
|
1611
|
+
}
|
|
1612
|
+
live.sort((a, z4) => a.t - z4.t);
|
|
1613
|
+
const junc = [];
|
|
1614
|
+
let dense = false;
|
|
1615
|
+
for (const w of live) {
|
|
1616
|
+
const j = junc[junc.length - 1];
|
|
1617
|
+
if (j && w.t - j.t < jMin) {
|
|
1618
|
+
if (++j.n > DIMSTR_WIT_PER_JUNCTION_MAX) {
|
|
1619
|
+
dense = true;
|
|
1620
|
+
break;
|
|
1621
|
+
}
|
|
1622
|
+
} else junc.push({ t: w.t, n: 1 });
|
|
1623
|
+
}
|
|
1624
|
+
if (dense || junc.length < DIMSTR_MIN_JUNCTIONS) {
|
|
1625
|
+
reject(R, dense ? "dense" : "junctions");
|
|
1626
|
+
continue;
|
|
1627
|
+
}
|
|
1628
|
+
let ticked = 0;
|
|
1629
|
+
const usedTicks = [];
|
|
1630
|
+
for (const j of junc) {
|
|
1631
|
+
const t = ticks.find((tk) => Math.abs(tk.t - j.t) <= tickAt);
|
|
1632
|
+
if (t) {
|
|
1633
|
+
ticked++;
|
|
1634
|
+
usedTicks.push(t);
|
|
1635
|
+
}
|
|
1636
|
+
}
|
|
1637
|
+
if (ticked < DIMSTR_MIN_TICKED || ticked < DIMSTR_TICKED_FRAC * junc.length) {
|
|
1638
|
+
reject(R, `ticked (junc=${junc.length} ticked=${ticked} ticks=${ticks.length})`);
|
|
1639
|
+
continue;
|
|
1640
|
+
}
|
|
1641
|
+
const witSet = new Set(live.map((w) => w.C)), tickSet = new Set(usedTicks.map((t) => t.C));
|
|
1642
|
+
let bandClutter = 0;
|
|
1643
|
+
for (const C of chains) {
|
|
1644
|
+
if (C === R || witSet.has(C) || tickSet.has(C)) continue;
|
|
1645
|
+
const mx = (C.p0[0] + C.p1[0]) / 2, my = (C.p0[1] + C.p1[1]) / 2;
|
|
1646
|
+
if (Math.abs(mx * nx + my * ny - R.d) > band) continue;
|
|
1647
|
+
const tt = mx * dx + my * dy;
|
|
1648
|
+
if (tt < R.t0 || tt > R.t1) continue;
|
|
1649
|
+
if (++bandClutter > DIMSTR_BAND_CLUTTER_MAX) break;
|
|
1650
|
+
}
|
|
1651
|
+
if (bandClutter > DIMSTR_BAND_CLUTTER_MAX) {
|
|
1652
|
+
reject(R, "band-clutter");
|
|
1653
|
+
continue;
|
|
1654
|
+
}
|
|
1655
|
+
hits.push(R);
|
|
1656
|
+
for (const m of R.members) soft[m] = 1;
|
|
1657
|
+
for (const t of usedTicks) for (const m of t.C.members) soft[m] = 1;
|
|
1658
|
+
for (const w of live) {
|
|
1659
|
+
if (w.C.len > wSoftMax) continue;
|
|
1660
|
+
if (hasParallelPartner(w.C, R)) continue;
|
|
1661
|
+
for (const m of w.C.members) soft[m] = 1;
|
|
1662
|
+
}
|
|
1663
|
+
}
|
|
1664
|
+
if (dimTexts && dimTexts.length) {
|
|
1665
|
+
const near = DIMTEXT_NEAR_FT * ftPx;
|
|
1666
|
+
const reach = DIMTEXT_REACH_FT * ftPx;
|
|
1667
|
+
const margin = DIMSTR_LINE_MARGIN_FT * ftPx;
|
|
1668
|
+
for (const T of dimTexts) {
|
|
1669
|
+
const ang = (T.ang % 180 + 180) % 180;
|
|
1670
|
+
const th = ang * Math.PI / 180, dx = Math.cos(th), dy = Math.sin(th), nx = -dy, ny = dx;
|
|
1671
|
+
const tx = T.x * ws, ty = T.y * ws;
|
|
1672
|
+
const dT = tx * nx + ty * ny, tT = tx * dx + ty * dy;
|
|
1673
|
+
const pieces = [];
|
|
1674
|
+
let dSum = 0, wSum = 0;
|
|
1675
|
+
for (const C of chains) {
|
|
1676
|
+
let rel = Math.abs(C.axis - ang);
|
|
1677
|
+
if (rel > 90) rel = 180 - rel;
|
|
1678
|
+
if (rel > 2 * DIMSTR_ANGLE_TOL) continue;
|
|
1679
|
+
const cd = (C.p0[0] + C.p1[0]) / 2 * nx + (C.p0[1] + C.p1[1]) / 2 * ny;
|
|
1680
|
+
if (Math.abs(cd - dT) > near) continue;
|
|
1681
|
+
const ct0 = Math.min(C.p0[0] * dx + C.p0[1] * dy, C.p1[0] * dx + C.p1[1] * dy);
|
|
1682
|
+
const ct1 = Math.max(C.p0[0] * dx + C.p0[1] * dy, C.p1[0] * dx + C.p1[1] * dy);
|
|
1683
|
+
if (ct1 < tT - reach || ct0 > tT + reach) continue;
|
|
1684
|
+
pieces.push(C);
|
|
1685
|
+
dSum += cd * C.len;
|
|
1686
|
+
wSum += C.len;
|
|
1687
|
+
}
|
|
1688
|
+
if (!wSum) continue;
|
|
1689
|
+
let t0 = Infinity, t1 = -Infinity, drawn = 0;
|
|
1690
|
+
for (const C of pieces) {
|
|
1691
|
+
t0 = Math.min(t0, Math.min(C.p0[0] * dx + C.p0[1] * dy, C.p1[0] * dx + C.p1[1] * dy));
|
|
1692
|
+
t1 = Math.max(t1, Math.max(C.p0[0] * dx + C.p0[1] * dy, C.p1[0] * dx + C.p1[1] * dy));
|
|
1693
|
+
drawn += C.len;
|
|
1694
|
+
}
|
|
1695
|
+
if (drawn < Math.max(runMin, DIMTEXT_MIN_LINE_X * T.wPx * ws)) continue;
|
|
1696
|
+
const dLine = dSum / wSum;
|
|
1697
|
+
const proxy = {
|
|
1698
|
+
axis: ang,
|
|
1699
|
+
d: dLine,
|
|
1700
|
+
t0,
|
|
1701
|
+
t1,
|
|
1702
|
+
len: t1 - t0,
|
|
1703
|
+
members: [],
|
|
1704
|
+
p0: [t0 * dx + dLine * nx, t0 * dy + dLine * ny],
|
|
1705
|
+
p1: [t1 * dx + dLine * nx, t1 * dy + dLine * ny]
|
|
1706
|
+
};
|
|
1707
|
+
if (hasParallelPartner(proxy, null)) continue;
|
|
1708
|
+
hits.push(proxy);
|
|
1709
|
+
for (const C of pieces) {
|
|
1710
|
+
if (C.len > t1 - t0 + 2 * margin) continue;
|
|
1711
|
+
if (hasParallelPartner(C, null)) continue;
|
|
1712
|
+
for (const m of C.members) soft[m] = 1;
|
|
1713
|
+
}
|
|
1714
|
+
for (const C of chains) {
|
|
1715
|
+
if (C.len < tickMin || C.len > DIMTEXT_STUB_MAX_FT * ftPx) continue;
|
|
1716
|
+
let rel = Math.abs(C.axis - ang);
|
|
1717
|
+
if (rel > 90) rel = 180 - rel;
|
|
1718
|
+
if (rel < DIMSTR_TICK_ANG_LO) continue;
|
|
1719
|
+
const d0 = C.p0[0] * nx + C.p0[1] * ny - dLine, d1 = C.p1[0] * nx + C.p1[1] * ny - dLine;
|
|
1720
|
+
const near0 = Math.abs(d0) <= attach, near1 = Math.abs(d1) <= attach;
|
|
1721
|
+
const crosses = d0 < -attach && d1 > attach || d1 < -attach && d0 > attach;
|
|
1722
|
+
if (!near0 && !near1 && !crosses) continue;
|
|
1723
|
+
const tm = (C.p0[0] + C.p1[0]) / 2 * dx + (C.p0[1] + C.p1[1]) / 2 * dy;
|
|
1724
|
+
if (tm < t0 - margin || tm > t1 + margin) continue;
|
|
1725
|
+
if (hasParallelPartner(C, null)) continue;
|
|
1726
|
+
for (const m of C.members) soft[m] = 1;
|
|
1727
|
+
}
|
|
1728
|
+
}
|
|
1729
|
+
}
|
|
1730
|
+
return { soft, runs: hits };
|
|
1731
|
+
}
|
|
1732
|
+
function classifyDimensionStringSegs(segs, meta, ws, ftPx, dimTexts) {
|
|
1733
|
+
return sweepDimensionStrings(segs, meta, ws, ftPx, void 0, dimTexts).soft;
|
|
1734
|
+
}
|
|
1435
1735
|
var HATCH_ID_ANGLE_Q = 0.5;
|
|
1436
1736
|
var HATCH_ID_PITCH_Q = 0.1;
|
|
1437
1737
|
function hatchFamilies(segs, meta) {
|
|
@@ -1452,7 +1752,8 @@ function hatchFamilies(segs, meta) {
|
|
|
1452
1752
|
});
|
|
1453
1753
|
}
|
|
1454
1754
|
var MASK_CURVE_BIT = 4;
|
|
1455
|
-
|
|
1755
|
+
var MASK_DIMSTR_BIT = 16;
|
|
1756
|
+
function buildMask(segs, imgW, imgH, maxDim = MASK_MAX_DIM, meta = null, pxPerFt = 0, basePxPerFt = 0, page = null, roles = null, dimTexts = null) {
|
|
1456
1757
|
if (pxPerFt instanceof Uint8Array || pxPerFt === null) {
|
|
1457
1758
|
if (!roles) roles = pxPerFt;
|
|
1458
1759
|
pxPerFt = 0;
|
|
@@ -1479,15 +1780,20 @@ function buildMask(segs, imgW, imgH, maxDim = MASK_MAX_DIM, meta = null, pxPerFt
|
|
|
1479
1780
|
if (soft && annot) {
|
|
1480
1781
|
for (let i = 0; i < soft.length; i++) if (annot[i]) soft[i] = 1;
|
|
1481
1782
|
}
|
|
1783
|
+
const dimstr = meta && mppf > 0 ? classifyDimensionStringSegs(segs, meta, ws, mppf, dimTexts) : null;
|
|
1784
|
+
if (soft && dimstr) {
|
|
1785
|
+
for (let i = 0; i < soft.length; i++) if (dimstr[i]) soft[i] = 1;
|
|
1786
|
+
}
|
|
1482
1787
|
const noDoor = meta ? flagNonDoorArcs(segs, meta) : null;
|
|
1483
1788
|
let softCount = 0;
|
|
1484
1789
|
for (let i = 0, si = 0; i + 3 < segs.length; i += 4, si++) {
|
|
1485
1790
|
const role = roles ? roles[si] : 0;
|
|
1486
1791
|
if (role === 2 || role === 3 || role === 5 || role === 6) continue;
|
|
1487
1792
|
let v = role === 1 || role === 4 ? 1 : soft && soft[si] ? 2 : 1;
|
|
1793
|
+
if (v === 2 && dimstr && dimstr[si]) v |= MASK_DIMSTR_BIT;
|
|
1488
1794
|
if (v === 1 && meta && meta[si] & SEG_CURVE) v = 1 | MASK_CURVE_BIT;
|
|
1489
1795
|
if (v & MASK_CURVE_BIT && noDoor && noDoor[si]) v |= MASK_NODOOR_BIT;
|
|
1490
|
-
if (v
|
|
1796
|
+
if (v & 2 && !(v & 1)) softCount++;
|
|
1491
1797
|
let x0 = Math.round(segs[i] * ws), y0 = Math.round(segs[i + 1] * ws);
|
|
1492
1798
|
const x1 = Math.round(segs[i + 2] * ws), y1 = Math.round(segs[i + 3] * ws);
|
|
1493
1799
|
const dx = Math.abs(x1 - x0), dy = -Math.abs(y1 - y0);
|
|
@@ -2190,6 +2496,112 @@ function sealAttempt(mo, ix, iy, sensitivity, radii, minPassPx = 0, given) {
|
|
|
2190
2496
|
const cy = Math.max(0, Math.min(mo.mh - 1, Math.round(iy * mo.ws)));
|
|
2191
2497
|
const cSoft = (mo.mask[cy * mo.mw + cx] & 2) !== 0;
|
|
2192
2498
|
const cdt = (s) => cSoft ? 0 : s.dt[cy * mo.mw + cx];
|
|
2499
|
+
const mergeAcrossDimStrings = (ref) => {
|
|
2500
|
+
if (!mo.softCount) return null;
|
|
2501
|
+
const { mw, mh, mask, ws } = mo;
|
|
2502
|
+
const isDim = (i) => (mask[i] & MASK_DIMSTR_BIT) !== 0 && (mask[i] & 1) === 0;
|
|
2503
|
+
const capCells = mw * mh * SEAL_MAX_SHEET_FRAC;
|
|
2504
|
+
const box = boxOf(ref.region, mw, mh);
|
|
2505
|
+
const region = ref.region.slice();
|
|
2506
|
+
let count = ref.count, mergedPieces = 0;
|
|
2507
|
+
const tried = /* @__PURE__ */ new Set();
|
|
2508
|
+
let frontier = true;
|
|
2509
|
+
for (let pass = 0; pass < 8 && frontier; pass++) {
|
|
2510
|
+
frontier = false;
|
|
2511
|
+
const b = { x0: Math.max(1, box.x0 - 2), y0: Math.max(1, box.y0 - 2), x1: Math.min(mw - 2, box.x1 + 2), y1: Math.min(mh - 2, box.y1 + 2) };
|
|
2512
|
+
for (let y = b.y0; y <= b.y1; y++) for (let x = b.x0; x <= b.x1; x++) {
|
|
2513
|
+
const i = y * mw + x;
|
|
2514
|
+
if (!isDim(i)) continue;
|
|
2515
|
+
if (!(region[i - 1] || region[i + 1] || region[i - mw] || region[i + mw])) continue;
|
|
2516
|
+
for (const o of [-1, 1, -mw, mw]) {
|
|
2517
|
+
let j = i + o, steps = 0;
|
|
2518
|
+
while (steps++ < 3 && isDim(j)) j += o;
|
|
2519
|
+
if (region[j] || mask[j] & 3 || tried.has(j)) continue;
|
|
2520
|
+
tried.add(j);
|
|
2521
|
+
const jx = j % mw, jy = j / mw | 0;
|
|
2522
|
+
const fr = floodPass(mo, (jx + 0.5) / ws, (jy + 0.5) / ws, 3);
|
|
2523
|
+
if (fr.status !== "ok") continue;
|
|
2524
|
+
if (count + fr.count > capCells) continue;
|
|
2525
|
+
{
|
|
2526
|
+
const fb2 = boxOf(fr.region, mw, mh);
|
|
2527
|
+
let bAll = 0, bDim = 0, bCurve = 0;
|
|
2528
|
+
for (let yy = Math.max(1, fb2.y0); yy <= Math.min(mh - 2, fb2.y1); yy++) for (let xx = Math.max(1, fb2.x0); xx <= Math.min(mw - 2, fb2.x1); xx++) {
|
|
2529
|
+
const k = yy * mw + xx;
|
|
2530
|
+
if (!fr.region[k]) continue;
|
|
2531
|
+
for (const oo of [-1, 1, -mw, mw]) {
|
|
2532
|
+
const mk = mask[k + oo];
|
|
2533
|
+
if (mk & 3 && !fr.region[k + oo]) {
|
|
2534
|
+
bAll++;
|
|
2535
|
+
if (isDim(k + oo)) bDim++;
|
|
2536
|
+
if (mk & MASK_CURVE_BIT) bCurve++;
|
|
2537
|
+
}
|
|
2538
|
+
}
|
|
2539
|
+
}
|
|
2540
|
+
if (bAll && bDim / bAll > DIMSTR_PIECE_MAX_DIMFRAC) continue;
|
|
2541
|
+
if (bAll && bCurve / bAll > DIMSTR_PIECE_MAX_CURVEFRAC) continue;
|
|
2542
|
+
}
|
|
2543
|
+
for (let k = 0; k < region.length; k++) if (fr.region[k] && !region[k]) {
|
|
2544
|
+
region[k] = 1;
|
|
2545
|
+
count++;
|
|
2546
|
+
}
|
|
2547
|
+
const fb = boxOf(fr.region, mw, mh);
|
|
2548
|
+
box.x0 = Math.min(box.x0, fb.x0);
|
|
2549
|
+
box.y0 = Math.min(box.y0, fb.y0);
|
|
2550
|
+
box.x1 = Math.max(box.x1, fb.x1);
|
|
2551
|
+
box.y1 = Math.max(box.y1, fb.y1);
|
|
2552
|
+
mergedPieces++;
|
|
2553
|
+
frontier = true;
|
|
2554
|
+
}
|
|
2555
|
+
}
|
|
2556
|
+
}
|
|
2557
|
+
if (!mergedPieces) return null;
|
|
2558
|
+
const final = new Uint8Array(mw * mh);
|
|
2559
|
+
const cx0 = Math.max(0, Math.min(mw - 1, Math.round(ix * ws)));
|
|
2560
|
+
const cy0 = Math.max(0, Math.min(mh - 1, Math.round(iy * ws)));
|
|
2561
|
+
let s0 = cy0 * mw + cx0;
|
|
2562
|
+
if (!region[s0]) {
|
|
2563
|
+
s0 = -1;
|
|
2564
|
+
for (let k = 0; k < region.length && s0 < 0; k++) if (ref.region[k]) s0 = k;
|
|
2565
|
+
if (s0 < 0) return null;
|
|
2566
|
+
}
|
|
2567
|
+
const stack = [s0];
|
|
2568
|
+
final[s0] = 1;
|
|
2569
|
+
let fCount = 0;
|
|
2570
|
+
const nb = { x0: mw, y0: mh, x1: 0, y1: 0 };
|
|
2571
|
+
while (stack.length) {
|
|
2572
|
+
const i = stack.pop();
|
|
2573
|
+
fCount++;
|
|
2574
|
+
const x = i % mw, y = i / mw | 0;
|
|
2575
|
+
if (x < nb.x0) nb.x0 = x;
|
|
2576
|
+
if (x > nb.x1) nb.x1 = x;
|
|
2577
|
+
if (y < nb.y0) nb.y0 = y;
|
|
2578
|
+
if (y > nb.y1) nb.y1 = y;
|
|
2579
|
+
for (const o of [-1, 1, -mw, mw]) {
|
|
2580
|
+
const j = i + o;
|
|
2581
|
+
if (j < 0 || j >= final.length || final[j]) continue;
|
|
2582
|
+
const xj = j % mw;
|
|
2583
|
+
if (Math.abs(xj - x) > 1) continue;
|
|
2584
|
+
if (region[j] || isDim(j)) {
|
|
2585
|
+
final[j] = 1;
|
|
2586
|
+
stack.push(j);
|
|
2587
|
+
}
|
|
2588
|
+
}
|
|
2589
|
+
}
|
|
2590
|
+
regionBox.set(final, nb);
|
|
2591
|
+
return {
|
|
2592
|
+
status: "ok",
|
|
2593
|
+
region: final,
|
|
2594
|
+
count: fCount,
|
|
2595
|
+
mw,
|
|
2596
|
+
mh,
|
|
2597
|
+
ws,
|
|
2598
|
+
mppf: mo.mppf,
|
|
2599
|
+
hardHits: ref.hardHits,
|
|
2600
|
+
softHits: ref.softHits,
|
|
2601
|
+
hatchFiltered: true,
|
|
2602
|
+
hatchTier: "bounded"
|
|
2603
|
+
};
|
|
2604
|
+
};
|
|
2193
2605
|
if (minPassPx > 0) {
|
|
2194
2606
|
const s = scratch();
|
|
2195
2607
|
const dm = dilatedView(mo, minPassPx, s.dt);
|
|
@@ -2205,7 +2617,7 @@ function sealAttempt(mo, ix, iy, sensitivity, radii, minPassPx = 0, given) {
|
|
|
2205
2617
|
f.minPassPx = minPassPx;
|
|
2206
2618
|
f.minPassDelta = d;
|
|
2207
2619
|
}
|
|
2208
|
-
return f;
|
|
2620
|
+
return mergeAcrossDimStrings(f) ?? f;
|
|
2209
2621
|
}
|
|
2210
2622
|
const vf = f.count > f.mw * f.mh * SEAL_MAX_SHEET_FRAC ? 1 : virtualBoundaryFrac(f, s.dt);
|
|
2211
2623
|
if (vf <= SEAL_VIRTUAL_MAX) {
|
|
@@ -2218,7 +2630,7 @@ function sealAttempt(mo, ix, iy, sensitivity, radii, minPassPx = 0, given) {
|
|
|
2218
2630
|
}
|
|
2219
2631
|
}
|
|
2220
2632
|
const base = rawFlood();
|
|
2221
|
-
if (base.status === "ok") return base;
|
|
2633
|
+
if (base.status === "ok") return mergeAcrossDimStrings(base) ?? base;
|
|
2222
2634
|
const sc = scratch();
|
|
2223
2635
|
for (const r of radii) {
|
|
2224
2636
|
if (r <= minPassPx) continue;
|
|
@@ -3680,6 +4092,483 @@ function splitFromFamily(conds, condId) {
|
|
|
3680
4092
|
});
|
|
3681
4093
|
}
|
|
3682
4094
|
|
|
4095
|
+
// ../web/src/lib/curve.js
|
|
4096
|
+
function crPoint(p0, p1, p2, p3, t) {
|
|
4097
|
+
const knot = (ti, a, b) => ti + Math.sqrt(Math.max(Math.hypot(b[0] - a[0], b[1] - a[1]), 1e-6));
|
|
4098
|
+
const t0 = 0, t1 = knot(t0, p0, p1), t2 = knot(t1, p1, p2), t3 = knot(t2, p2, p3);
|
|
4099
|
+
const u = t1 + (t2 - t1) * t;
|
|
4100
|
+
const lp = (a, b, ta, tb) => {
|
|
4101
|
+
const w = (u - ta) / (tb - ta || 1e-9);
|
|
4102
|
+
return [a[0] + (b[0] - a[0]) * w, a[1] + (b[1] - a[1]) * w];
|
|
4103
|
+
};
|
|
4104
|
+
const A1 = lp(p0, p1, t0, t1), A2 = lp(p1, p2, t1, t2), A3 = lp(p2, p3, t2, t3);
|
|
4105
|
+
const B1 = lp(A1, A2, t0, t2), B2 = lp(A2, A3, t1, t3);
|
|
4106
|
+
return lp(B1, B2, t1, t2);
|
|
4107
|
+
}
|
|
4108
|
+
function flattenCurve(pts, opts = {}) {
|
|
4109
|
+
const maxPts = opts.maxPts || 220;
|
|
4110
|
+
const n = (pts || []).length;
|
|
4111
|
+
if (n < 3) return (pts || []).map((p) => [p[0], p[1]]);
|
|
4112
|
+
const P = [pts[0], ...pts, pts[n - 1]];
|
|
4113
|
+
const want = [];
|
|
4114
|
+
let total = 0;
|
|
4115
|
+
for (let i = 1; i < P.length - 2; i++) {
|
|
4116
|
+
const chord = Math.hypot(P[i + 1][0] - P[i][0], P[i + 1][1] - P[i][1]);
|
|
4117
|
+
const steps = Math.max(6, Math.min(24, Math.round(chord / 6)));
|
|
4118
|
+
want.push(steps);
|
|
4119
|
+
total += steps;
|
|
4120
|
+
}
|
|
4121
|
+
const scale = total > maxPts ? maxPts / total : 1;
|
|
4122
|
+
const out = [[pts[0][0], pts[0][1]]];
|
|
4123
|
+
for (let i = 1; i < P.length - 2; i++) {
|
|
4124
|
+
const steps = Math.max(2, Math.round(want[i - 1] * scale));
|
|
4125
|
+
for (let j = 1; j <= steps; j++) out.push(crPoint(P[i - 1], P[i], P[i + 1], P[i + 2], j / steps));
|
|
4126
|
+
}
|
|
4127
|
+
return out;
|
|
4128
|
+
}
|
|
4129
|
+
|
|
4130
|
+
// ../web/src/lib/dxf.ts
|
|
4131
|
+
var DXF_STAMP = "OpenTakeoff DXF export";
|
|
4132
|
+
var FT_PER_M = 1 / 0.3048;
|
|
4133
|
+
var CONDITION_COLORS = [5, 3, 1, 2, 4, 6, 30, 40, 90, 150, 200, 210, 32, 92, 172, 12];
|
|
4134
|
+
var COLOR_LABELS = 7;
|
|
4135
|
+
var COLOR_HOLE = 8;
|
|
4136
|
+
function dxfLayerName(tag, suffix) {
|
|
4137
|
+
let base = String(tag ?? "").trim().replace(/[<>\/\\":;?*|,=`]+/g, "-").replace(/\s+/g, " ").replace(/-{2,}/g, "-").replace(/^-|-$/g, "").toUpperCase();
|
|
4138
|
+
if (!base) base = "UNTAGGED";
|
|
4139
|
+
if (base.length > 48) base = base.slice(0, 48).replace(/-$/, "");
|
|
4140
|
+
return suffix ? `OT-${base}-${suffix}` : `OT-${base}`;
|
|
4141
|
+
}
|
|
4142
|
+
var fmt = (v) => {
|
|
4143
|
+
const r = Math.round(v * 1e4) / 1e4;
|
|
4144
|
+
return (Object.is(r, -0) ? 0 : r).toString();
|
|
4145
|
+
};
|
|
4146
|
+
var Dxf = class {
|
|
4147
|
+
out = [];
|
|
4148
|
+
seed = 32;
|
|
4149
|
+
handle() {
|
|
4150
|
+
return (this.seed++).toString(16).toUpperCase();
|
|
4151
|
+
}
|
|
4152
|
+
peek() {
|
|
4153
|
+
return this.seed.toString(16).toUpperCase();
|
|
4154
|
+
}
|
|
4155
|
+
add(code, value) {
|
|
4156
|
+
this.out.push(String(code), String(value));
|
|
4157
|
+
}
|
|
4158
|
+
text() {
|
|
4159
|
+
return this.out.join("\n") + "\n";
|
|
4160
|
+
}
|
|
4161
|
+
};
|
|
4162
|
+
function polygonCentroid(pts) {
|
|
4163
|
+
let a = 0, cx = 0, cy = 0;
|
|
4164
|
+
const n = pts.length;
|
|
4165
|
+
for (let i = 0; i < n; i++) {
|
|
4166
|
+
const [x0, y0] = pts[i], [x1, y1] = pts[(i + 1) % n];
|
|
4167
|
+
const cross = x0 * y1 - x1 * y0;
|
|
4168
|
+
a += cross;
|
|
4169
|
+
cx += (x0 + x1) * cross;
|
|
4170
|
+
cy += (y0 + y1) * cross;
|
|
4171
|
+
}
|
|
4172
|
+
if (Math.abs(a) < 1e-12) {
|
|
4173
|
+
return [pts.reduce((s, p) => s + p[0], 0) / n, pts.reduce((s, p) => s + p[1], 0) / n];
|
|
4174
|
+
}
|
|
4175
|
+
a *= 0.5;
|
|
4176
|
+
return [cx / (6 * a), cy / (6 * a)];
|
|
4177
|
+
}
|
|
4178
|
+
function buildSheetDxf(sheet, opts = {}) {
|
|
4179
|
+
const units = opts.units === "m" ? "m" : "ft";
|
|
4180
|
+
const k = units === "m" ? 1 / FT_PER_M : 1;
|
|
4181
|
+
const textH = opts.text_height ?? (units === "m" ? 0.3 : 1);
|
|
4182
|
+
const { w, h } = sheet.dims;
|
|
4183
|
+
const upp = sheet.upp;
|
|
4184
|
+
if (!(w > 0) || !(h > 0)) throw new Error(`DXF: sheet ${sheet.sheet_id} has no image dimensions`);
|
|
4185
|
+
if (!(upp > 0)) throw new Error(`DXF: sheet ${sheet.sheet_id} has no scale \u2014 set the scale before exporting to CAD`);
|
|
4186
|
+
const toXY = ([nx, ny]) => [nx * w * upp * k, (1 - ny) * h * upp * k];
|
|
4187
|
+
const toPx = ([nx, ny]) => [nx * w, ny * h];
|
|
4188
|
+
const fromPx = ([px, py]) => [px * upp * k, (h - py) * upp * k];
|
|
4189
|
+
const condById = new Map(sheet.conditions.map((c) => [c.id, c]));
|
|
4190
|
+
const condColor = /* @__PURE__ */ new Map();
|
|
4191
|
+
sheet.conditions.forEach((c, i) => condColor.set(c.id, CONDITION_COLORS[i % CONDITION_COLORS.length]));
|
|
4192
|
+
const ents = [];
|
|
4193
|
+
const layers = /* @__PURE__ */ new Map();
|
|
4194
|
+
const useLayer = (name, color) => {
|
|
4195
|
+
if (!layers.has(name)) layers.set(name, color);
|
|
4196
|
+
return name;
|
|
4197
|
+
};
|
|
4198
|
+
const skipped = [];
|
|
4199
|
+
let shapesOut = 0;
|
|
4200
|
+
const ext = { minX: Infinity, minY: Infinity, maxX: -Infinity, maxY: -Infinity };
|
|
4201
|
+
const grow = ([x, y]) => {
|
|
4202
|
+
if (x < ext.minX) ext.minX = x;
|
|
4203
|
+
if (x > ext.maxX) ext.maxX = x;
|
|
4204
|
+
if (y < ext.minY) ext.minY = y;
|
|
4205
|
+
if (y > ext.maxY) ext.maxY = y;
|
|
4206
|
+
};
|
|
4207
|
+
for (const s of sheet.shapes) {
|
|
4208
|
+
if (s.sheet_id !== sheet.sheet_id) continue;
|
|
4209
|
+
const cond = condById.get(s.condition_id);
|
|
4210
|
+
const tag = cond?.finish_tag || cond?.name || "";
|
|
4211
|
+
const color = condColor.get(s.condition_id) ?? COLOR_LABELS;
|
|
4212
|
+
const verts = Array.isArray(s.verts_norm) ? s.verts_norm : [];
|
|
4213
|
+
const role = s.measure_role;
|
|
4214
|
+
const before = ents.length;
|
|
4215
|
+
if (role === "count") {
|
|
4216
|
+
const c = verts[0];
|
|
4217
|
+
if (!c) {
|
|
4218
|
+
skipped.push({ id: s.id, reason: "count mark without a position" });
|
|
4219
|
+
continue;
|
|
4220
|
+
}
|
|
4221
|
+
const at = toXY(c);
|
|
4222
|
+
ents.push({ kind: "circle", layer: useLayer(dxfLayerName(tag, "COUNT"), color), c: at, r: 0.5 * k });
|
|
4223
|
+
grow([at[0] - 0.5 * k, at[1] - 0.5 * k]);
|
|
4224
|
+
grow([at[0] + 0.5 * k, at[1] + 0.5 * k]);
|
|
4225
|
+
} else if (role === "surface_area" || role === "linear") {
|
|
4226
|
+
if (verts.length < 2) {
|
|
4227
|
+
skipped.push({ id: s.id, reason: `${role} shape with fewer than 2 vertices` });
|
|
4228
|
+
continue;
|
|
4229
|
+
}
|
|
4230
|
+
const px = verts.map(toPx);
|
|
4231
|
+
const run2 = role === "linear" && s.curved ? flattenCurve(px) : px;
|
|
4232
|
+
const pts = run2.map(fromPx);
|
|
4233
|
+
pts.forEach(grow);
|
|
4234
|
+
ents.push({ kind: "lwpoly", layer: useLayer(dxfLayerName(tag, role === "linear" ? "LINEAR" : "WALL"), color), pts, closed: false });
|
|
4235
|
+
} else if (role === "floor_area" || role === "deduct") {
|
|
4236
|
+
if (role === "deduct" && s.cuts_shape_id) {
|
|
4237
|
+
skipped.push({ id: s.id, reason: `deduct reconciled into ${s.cuts_shape_id} \u2014 ships as that ring's hole` });
|
|
4238
|
+
continue;
|
|
4239
|
+
}
|
|
4240
|
+
if (verts.length < 3) {
|
|
4241
|
+
skipped.push({ id: s.id, reason: `${role} ring with fewer than 3 vertices` });
|
|
4242
|
+
continue;
|
|
4243
|
+
}
|
|
4244
|
+
const pts = verts.map(toXY);
|
|
4245
|
+
pts.forEach(grow);
|
|
4246
|
+
const layer = useLayer(role === "deduct" ? dxfLayerName(tag, "DEDUCT") : dxfLayerName(tag), color);
|
|
4247
|
+
ents.push({ kind: "lwpoly", layer, pts, closed: true });
|
|
4248
|
+
for (const ring of s.verts_norm_holes || []) {
|
|
4249
|
+
if (!Array.isArray(ring) || ring.length < 3) continue;
|
|
4250
|
+
const hp = ring.map(toXY);
|
|
4251
|
+
hp.forEach(grow);
|
|
4252
|
+
ents.push({ kind: "lwpoly", layer: useLayer(dxfLayerName(tag, "HOLE"), COLOR_HOLE), pts: hp, closed: true });
|
|
4253
|
+
}
|
|
4254
|
+
if (role === "floor_area" && s.label && String(s.label).trim()) {
|
|
4255
|
+
ents.push({ kind: "text", layer: useLayer("OT-LABELS", COLOR_LABELS), at: polygonCentroid(pts), text: String(s.label).trim(), height: textH });
|
|
4256
|
+
}
|
|
4257
|
+
} else {
|
|
4258
|
+
skipped.push({ id: s.id, reason: `unknown measure_role "${role}"` });
|
|
4259
|
+
continue;
|
|
4260
|
+
}
|
|
4261
|
+
if (ents.length > before) shapesOut++;
|
|
4262
|
+
}
|
|
4263
|
+
const hasExt = Number.isFinite(ext.minX);
|
|
4264
|
+
const extents = hasExt ? { min: [ext.minX, ext.minY], max: [ext.maxX, ext.maxY] } : null;
|
|
4265
|
+
const d = new Dxf();
|
|
4266
|
+
const H = {
|
|
4267
|
+
vportTbl: d.handle(),
|
|
4268
|
+
vportActive: d.handle(),
|
|
4269
|
+
ltypeTbl: d.handle(),
|
|
4270
|
+
ltByBlock: d.handle(),
|
|
4271
|
+
ltByLayer: d.handle(),
|
|
4272
|
+
ltCont: d.handle(),
|
|
4273
|
+
layerTbl: d.handle(),
|
|
4274
|
+
layer0: d.handle(),
|
|
4275
|
+
styleTbl: d.handle(),
|
|
4276
|
+
styleStd: d.handle(),
|
|
4277
|
+
viewTbl: d.handle(),
|
|
4278
|
+
ucsTbl: d.handle(),
|
|
4279
|
+
appidTbl: d.handle(),
|
|
4280
|
+
appidAcad: d.handle(),
|
|
4281
|
+
dimTbl: d.handle(),
|
|
4282
|
+
dimStd: d.handle(),
|
|
4283
|
+
brTbl: d.handle(),
|
|
4284
|
+
brModel: d.handle(),
|
|
4285
|
+
brPaper: d.handle(),
|
|
4286
|
+
blkModel: d.handle(),
|
|
4287
|
+
endModel: d.handle(),
|
|
4288
|
+
blkPaper: d.handle(),
|
|
4289
|
+
endPaper: d.handle(),
|
|
4290
|
+
rootDict: d.handle(),
|
|
4291
|
+
groupDict: d.handle(),
|
|
4292
|
+
plotDict: d.handle(),
|
|
4293
|
+
plotPlaceholder: d.handle()
|
|
4294
|
+
};
|
|
4295
|
+
const layerHandles = /* @__PURE__ */ new Map();
|
|
4296
|
+
for (const name of layers.keys()) layerHandles.set(name, d.handle());
|
|
4297
|
+
const entHandles = ents.map(() => d.handle());
|
|
4298
|
+
d.add(999, `${DXF_STAMP} \u2014 sheet ${sheet.label || sheet.sheet_id} \u2014 units ${units === "m" ? "metres" : "feet"} \u2014 origin bottom-left of sheet, Y up`);
|
|
4299
|
+
d.add(0, "SECTION");
|
|
4300
|
+
d.add(2, "HEADER");
|
|
4301
|
+
d.add(9, "$ACADVER");
|
|
4302
|
+
d.add(1, "AC1015");
|
|
4303
|
+
d.add(9, "$HANDSEED");
|
|
4304
|
+
d.add(5, d.peek());
|
|
4305
|
+
d.add(9, "$INSUNITS");
|
|
4306
|
+
d.add(70, units === "m" ? 6 : 2);
|
|
4307
|
+
d.add(9, "$MEASUREMENT");
|
|
4308
|
+
d.add(70, units === "m" ? 1 : 0);
|
|
4309
|
+
d.add(9, "$LUNITS");
|
|
4310
|
+
d.add(70, units === "m" ? 2 : 4);
|
|
4311
|
+
d.add(9, "$EXTMIN");
|
|
4312
|
+
d.add(10, fmt(hasExt ? ext.minX : 0));
|
|
4313
|
+
d.add(20, fmt(hasExt ? ext.minY : 0));
|
|
4314
|
+
d.add(30, 0);
|
|
4315
|
+
d.add(9, "$EXTMAX");
|
|
4316
|
+
d.add(10, fmt(hasExt ? ext.maxX : w * upp * k));
|
|
4317
|
+
d.add(20, fmt(hasExt ? ext.maxY : h * upp * k));
|
|
4318
|
+
d.add(30, 0);
|
|
4319
|
+
d.add(9, "$LIMMIN");
|
|
4320
|
+
d.add(10, 0);
|
|
4321
|
+
d.add(20, 0);
|
|
4322
|
+
d.add(9, "$LIMMAX");
|
|
4323
|
+
d.add(10, fmt(w * upp * k));
|
|
4324
|
+
d.add(20, fmt(h * upp * k));
|
|
4325
|
+
d.add(9, "$CLAYER");
|
|
4326
|
+
d.add(8, "0");
|
|
4327
|
+
d.add(9, "$PDMODE");
|
|
4328
|
+
d.add(70, 0);
|
|
4329
|
+
d.add(0, "ENDSEC");
|
|
4330
|
+
d.add(0, "SECTION");
|
|
4331
|
+
d.add(2, "CLASSES");
|
|
4332
|
+
d.add(0, "ENDSEC");
|
|
4333
|
+
d.add(0, "SECTION");
|
|
4334
|
+
d.add(2, "TABLES");
|
|
4335
|
+
const table = (name, handle, count) => {
|
|
4336
|
+
d.add(0, "TABLE");
|
|
4337
|
+
d.add(2, name);
|
|
4338
|
+
d.add(5, handle);
|
|
4339
|
+
d.add(330, "0");
|
|
4340
|
+
d.add(100, "AcDbSymbolTable");
|
|
4341
|
+
d.add(70, count);
|
|
4342
|
+
};
|
|
4343
|
+
const record = (type, handle, owner, sub) => {
|
|
4344
|
+
d.add(0, type);
|
|
4345
|
+
d.add(5, handle);
|
|
4346
|
+
d.add(330, owner);
|
|
4347
|
+
d.add(100, "AcDbSymbolTableRecord");
|
|
4348
|
+
d.add(100, sub);
|
|
4349
|
+
};
|
|
4350
|
+
const endtab = () => d.add(0, "ENDTAB");
|
|
4351
|
+
table("VPORT", H.vportTbl, 1);
|
|
4352
|
+
record("VPORT", H.vportActive, H.vportTbl, "AcDbViewportTableRecord");
|
|
4353
|
+
d.add(2, "*Active");
|
|
4354
|
+
d.add(70, 0);
|
|
4355
|
+
d.add(10, 0);
|
|
4356
|
+
d.add(20, 0);
|
|
4357
|
+
d.add(11, 1);
|
|
4358
|
+
d.add(21, 1);
|
|
4359
|
+
const cx = hasExt ? (ext.minX + ext.maxX) / 2 : w * upp * k / 2;
|
|
4360
|
+
const cy = hasExt ? (ext.minY + ext.maxY) / 2 : h * upp * k / 2;
|
|
4361
|
+
const vh = hasExt ? Math.max(ext.maxY - ext.minY, (ext.maxX - ext.minX) / 1.6, 1) * 1.1 : h * upp * k;
|
|
4362
|
+
d.add(12, fmt(cx));
|
|
4363
|
+
d.add(22, fmt(cy));
|
|
4364
|
+
d.add(13, 0);
|
|
4365
|
+
d.add(23, 0);
|
|
4366
|
+
d.add(14, 10);
|
|
4367
|
+
d.add(24, 10);
|
|
4368
|
+
d.add(15, 10);
|
|
4369
|
+
d.add(25, 10);
|
|
4370
|
+
d.add(16, 0);
|
|
4371
|
+
d.add(26, 0);
|
|
4372
|
+
d.add(36, 1);
|
|
4373
|
+
d.add(17, 0);
|
|
4374
|
+
d.add(27, 0);
|
|
4375
|
+
d.add(37, 0);
|
|
4376
|
+
d.add(40, fmt(vh));
|
|
4377
|
+
d.add(41, 1.6);
|
|
4378
|
+
d.add(42, 50);
|
|
4379
|
+
d.add(43, 0);
|
|
4380
|
+
d.add(44, 0);
|
|
4381
|
+
d.add(50, 0);
|
|
4382
|
+
d.add(51, 0);
|
|
4383
|
+
d.add(71, 0);
|
|
4384
|
+
d.add(72, 100);
|
|
4385
|
+
d.add(73, 1);
|
|
4386
|
+
d.add(74, 3);
|
|
4387
|
+
d.add(75, 0);
|
|
4388
|
+
d.add(76, 0);
|
|
4389
|
+
d.add(77, 0);
|
|
4390
|
+
d.add(78, 0);
|
|
4391
|
+
endtab();
|
|
4392
|
+
table("LTYPE", H.ltypeTbl, 3);
|
|
4393
|
+
for (const [hd, name, desc] of [[H.ltByBlock, "ByBlock", ""], [H.ltByLayer, "ByLayer", ""], [H.ltCont, "Continuous", "Solid line"]]) {
|
|
4394
|
+
record("LTYPE", hd, H.ltypeTbl, "AcDbLinetypeTableRecord");
|
|
4395
|
+
d.add(2, name);
|
|
4396
|
+
d.add(70, 0);
|
|
4397
|
+
d.add(3, desc);
|
|
4398
|
+
d.add(72, 65);
|
|
4399
|
+
d.add(73, 0);
|
|
4400
|
+
d.add(40, 0);
|
|
4401
|
+
}
|
|
4402
|
+
endtab();
|
|
4403
|
+
table("LAYER", H.layerTbl, layers.size + 1);
|
|
4404
|
+
const layerRec = (hd, name, color) => {
|
|
4405
|
+
record("LAYER", hd, H.layerTbl, "AcDbLayerTableRecord");
|
|
4406
|
+
d.add(2, name);
|
|
4407
|
+
d.add(70, 0);
|
|
4408
|
+
d.add(62, color);
|
|
4409
|
+
d.add(6, "Continuous");
|
|
4410
|
+
d.add(370, -3);
|
|
4411
|
+
d.add(390, H.plotPlaceholder);
|
|
4412
|
+
};
|
|
4413
|
+
layerRec(H.layer0, "0", 7);
|
|
4414
|
+
for (const [name, color] of layers) layerRec(layerHandles.get(name), name, color);
|
|
4415
|
+
endtab();
|
|
4416
|
+
table("STYLE", H.styleTbl, 1);
|
|
4417
|
+
record("STYLE", H.styleStd, H.styleTbl, "AcDbTextStyleTableRecord");
|
|
4418
|
+
d.add(2, "Standard");
|
|
4419
|
+
d.add(70, 0);
|
|
4420
|
+
d.add(40, 0);
|
|
4421
|
+
d.add(41, 1);
|
|
4422
|
+
d.add(50, 0);
|
|
4423
|
+
d.add(71, 0);
|
|
4424
|
+
d.add(42, 0.2);
|
|
4425
|
+
d.add(3, "txt");
|
|
4426
|
+
d.add(4, "");
|
|
4427
|
+
endtab();
|
|
4428
|
+
table("VIEW", H.viewTbl, 0);
|
|
4429
|
+
endtab();
|
|
4430
|
+
table("UCS", H.ucsTbl, 0);
|
|
4431
|
+
endtab();
|
|
4432
|
+
table("APPID", H.appidTbl, 1);
|
|
4433
|
+
record("APPID", H.appidAcad, H.appidTbl, "AcDbRegAppTableRecord");
|
|
4434
|
+
d.add(2, "ACAD");
|
|
4435
|
+
d.add(70, 0);
|
|
4436
|
+
endtab();
|
|
4437
|
+
d.add(0, "TABLE");
|
|
4438
|
+
d.add(2, "DIMSTYLE");
|
|
4439
|
+
d.add(5, H.dimTbl);
|
|
4440
|
+
d.add(330, "0");
|
|
4441
|
+
d.add(100, "AcDbSymbolTable");
|
|
4442
|
+
d.add(70, 1);
|
|
4443
|
+
d.add(100, "AcDbDimStyleTable");
|
|
4444
|
+
d.add(0, "DIMSTYLE");
|
|
4445
|
+
d.add(105, H.dimStd);
|
|
4446
|
+
d.add(330, H.dimTbl);
|
|
4447
|
+
d.add(100, "AcDbSymbolTableRecord");
|
|
4448
|
+
d.add(100, "AcDbDimStyleTableRecord");
|
|
4449
|
+
d.add(2, "Standard");
|
|
4450
|
+
d.add(70, 0);
|
|
4451
|
+
d.add(340, H.styleStd);
|
|
4452
|
+
endtab();
|
|
4453
|
+
table("BLOCK_RECORD", H.brTbl, 2);
|
|
4454
|
+
for (const [hd, name] of [[H.brModel, "*Model_Space"], [H.brPaper, "*Paper_Space"]]) {
|
|
4455
|
+
record("BLOCK_RECORD", hd, H.brTbl, "AcDbBlockTableRecord");
|
|
4456
|
+
d.add(2, name);
|
|
4457
|
+
d.add(340, "0");
|
|
4458
|
+
}
|
|
4459
|
+
endtab();
|
|
4460
|
+
d.add(0, "ENDSEC");
|
|
4461
|
+
d.add(0, "SECTION");
|
|
4462
|
+
d.add(2, "BLOCKS");
|
|
4463
|
+
const block = (name, br, begin, end, paper) => {
|
|
4464
|
+
d.add(0, "BLOCK");
|
|
4465
|
+
d.add(5, begin);
|
|
4466
|
+
d.add(330, br);
|
|
4467
|
+
d.add(100, "AcDbEntity");
|
|
4468
|
+
if (paper) d.add(67, 1);
|
|
4469
|
+
d.add(8, "0");
|
|
4470
|
+
d.add(100, "AcDbBlockBegin");
|
|
4471
|
+
d.add(2, name);
|
|
4472
|
+
d.add(70, 0);
|
|
4473
|
+
d.add(10, 0);
|
|
4474
|
+
d.add(20, 0);
|
|
4475
|
+
d.add(30, 0);
|
|
4476
|
+
d.add(3, name);
|
|
4477
|
+
d.add(1, "");
|
|
4478
|
+
d.add(0, "ENDBLK");
|
|
4479
|
+
d.add(5, end);
|
|
4480
|
+
d.add(330, br);
|
|
4481
|
+
d.add(100, "AcDbEntity");
|
|
4482
|
+
if (paper) d.add(67, 1);
|
|
4483
|
+
d.add(8, "0");
|
|
4484
|
+
d.add(100, "AcDbBlockEnd");
|
|
4485
|
+
};
|
|
4486
|
+
block("*Model_Space", H.brModel, H.blkModel, H.endModel, false);
|
|
4487
|
+
block("*Paper_Space", H.brPaper, H.blkPaper, H.endPaper, true);
|
|
4488
|
+
d.add(0, "ENDSEC");
|
|
4489
|
+
d.add(0, "SECTION");
|
|
4490
|
+
d.add(2, "ENTITIES");
|
|
4491
|
+
ents.forEach((e, i) => {
|
|
4492
|
+
const hd = entHandles[i];
|
|
4493
|
+
if (e.kind === "lwpoly") {
|
|
4494
|
+
d.add(0, "LWPOLYLINE");
|
|
4495
|
+
d.add(5, hd);
|
|
4496
|
+
d.add(330, H.brModel);
|
|
4497
|
+
d.add(100, "AcDbEntity");
|
|
4498
|
+
d.add(8, e.layer);
|
|
4499
|
+
d.add(100, "AcDbPolyline");
|
|
4500
|
+
d.add(90, e.pts.length);
|
|
4501
|
+
d.add(70, e.closed ? 1 : 0);
|
|
4502
|
+
d.add(43, 0);
|
|
4503
|
+
for (const [x, y] of e.pts) {
|
|
4504
|
+
d.add(10, fmt(x));
|
|
4505
|
+
d.add(20, fmt(y));
|
|
4506
|
+
}
|
|
4507
|
+
} else if (e.kind === "circle") {
|
|
4508
|
+
d.add(0, "CIRCLE");
|
|
4509
|
+
d.add(5, hd);
|
|
4510
|
+
d.add(330, H.brModel);
|
|
4511
|
+
d.add(100, "AcDbEntity");
|
|
4512
|
+
d.add(8, e.layer);
|
|
4513
|
+
d.add(100, "AcDbCircle");
|
|
4514
|
+
d.add(10, fmt(e.c[0]));
|
|
4515
|
+
d.add(20, fmt(e.c[1]));
|
|
4516
|
+
d.add(30, 0);
|
|
4517
|
+
d.add(40, fmt(e.r));
|
|
4518
|
+
} else {
|
|
4519
|
+
d.add(0, "TEXT");
|
|
4520
|
+
d.add(5, hd);
|
|
4521
|
+
d.add(330, H.brModel);
|
|
4522
|
+
d.add(100, "AcDbEntity");
|
|
4523
|
+
d.add(8, e.layer);
|
|
4524
|
+
d.add(100, "AcDbText");
|
|
4525
|
+
d.add(10, fmt(e.at[0]));
|
|
4526
|
+
d.add(20, fmt(e.at[1]));
|
|
4527
|
+
d.add(30, 0);
|
|
4528
|
+
d.add(40, fmt(e.height));
|
|
4529
|
+
d.add(1, e.text.replace(/[\r\n]+/g, " "));
|
|
4530
|
+
d.add(72, 1);
|
|
4531
|
+
d.add(11, fmt(e.at[0]));
|
|
4532
|
+
d.add(21, fmt(e.at[1]));
|
|
4533
|
+
d.add(31, 0);
|
|
4534
|
+
d.add(100, "AcDbText");
|
|
4535
|
+
d.add(73, 2);
|
|
4536
|
+
}
|
|
4537
|
+
});
|
|
4538
|
+
d.add(0, "ENDSEC");
|
|
4539
|
+
d.add(0, "SECTION");
|
|
4540
|
+
d.add(2, "OBJECTS");
|
|
4541
|
+
d.add(0, "DICTIONARY");
|
|
4542
|
+
d.add(5, H.rootDict);
|
|
4543
|
+
d.add(330, "0");
|
|
4544
|
+
d.add(100, "AcDbDictionary");
|
|
4545
|
+
d.add(281, 1);
|
|
4546
|
+
d.add(3, "ACAD_GROUP");
|
|
4547
|
+
d.add(350, H.groupDict);
|
|
4548
|
+
d.add(3, "ACAD_PLOTSTYLENAME");
|
|
4549
|
+
d.add(350, H.plotDict);
|
|
4550
|
+
d.add(0, "DICTIONARY");
|
|
4551
|
+
d.add(5, H.groupDict);
|
|
4552
|
+
d.add(330, H.rootDict);
|
|
4553
|
+
d.add(100, "AcDbDictionary");
|
|
4554
|
+
d.add(281, 1);
|
|
4555
|
+
d.add(0, "ACDBDICTIONARYWDFLT");
|
|
4556
|
+
d.add(5, H.plotDict);
|
|
4557
|
+
d.add(330, H.rootDict);
|
|
4558
|
+
d.add(100, "AcDbDictionary");
|
|
4559
|
+
d.add(281, 1);
|
|
4560
|
+
d.add(3, "Normal");
|
|
4561
|
+
d.add(350, H.plotPlaceholder);
|
|
4562
|
+
d.add(100, "AcDbDictionaryWithDefault");
|
|
4563
|
+
d.add(340, H.plotPlaceholder);
|
|
4564
|
+
d.add(0, "ACDBPLACEHOLDER");
|
|
4565
|
+
d.add(5, H.plotPlaceholder);
|
|
4566
|
+
d.add(330, H.plotDict);
|
|
4567
|
+
d.add(0, "ENDSEC");
|
|
4568
|
+
d.add(0, "EOF");
|
|
4569
|
+
return { dxf: d.text(), layers: [...layers.keys()], entities: ents.length, shapes: shapesOut, skipped, extents };
|
|
4570
|
+
}
|
|
4571
|
+
|
|
3683
4572
|
// ../web/src/lib/confidence.ts
|
|
3684
4573
|
var CONF_RASTER = 0.9;
|
|
3685
4574
|
var CONF_HATCH = 0.95;
|
|
@@ -8844,6 +9733,37 @@ var Session = class _Session {
|
|
|
8844
9733
|
}
|
|
8845
9734
|
/** The exact browser save payload (TakeoffCanvas.jsx autosave + the schema key
|
|
8846
9735
|
* store.saveAnnotations stamps) — importable by the app. */
|
|
9736
|
+
/** The takeoff on ONE sheet as a DXF drawing (R2000, LWPOLYLINE per shape,
|
|
9737
|
+
* OT-<TAG> layer per finish, feet by default) — the CAD-native handoff.
|
|
9738
|
+
* `sheet` omitted resolves only when exactly one calibrated sheet carries
|
|
9739
|
+
* shapes; otherwise the refusal names the candidates, because two floors in
|
|
9740
|
+
* one model space at the same origin would be a lie. A sheet without a
|
|
9741
|
+
* scale refuses too (a CAD file in pixels is worse than none). */
|
|
9742
|
+
exportDxf(sheetName, units = "ft") {
|
|
9743
|
+
if (!this.docs.size) throw new UserError("No plan loaded \u2014 call load_plan first.");
|
|
9744
|
+
let s;
|
|
9745
|
+
if (sheetName != null && sheetName !== "") {
|
|
9746
|
+
s = this.sheet(sheetName);
|
|
9747
|
+
} else {
|
|
9748
|
+
const withShapes = new Set(this.shapes.map((x) => x.sheet_id));
|
|
9749
|
+
const cands = [...this.sheets.values()].filter((x) => withShapes.has(x.key));
|
|
9750
|
+
if (cands.length === 0) throw new UserError("No sheet carries committed shapes yet \u2014 measure something before exporting to CAD.");
|
|
9751
|
+
if (cands.length > 1) {
|
|
9752
|
+
throw new UserError(`Several sheets carry shapes \u2014 pass sheet to pick one drawing (a DXF is one sheet, like a DWG is): ${cands.map((x) => `"${x.key}"${x.sheetNumber ? ` (${x.sheetNumber})` : ""}`).join(", ")}.`);
|
|
9753
|
+
}
|
|
9754
|
+
s = cands[0];
|
|
9755
|
+
}
|
|
9756
|
+
if (s.upp == null) throw new UserError(`Sheet "${s.key}" has no scale \u2014 call set_scale first; a CAD file in pixels is worse than none.`);
|
|
9757
|
+
const build = buildSheetDxf({
|
|
9758
|
+
sheet_id: s.key,
|
|
9759
|
+
label: s.sheetNumber || s.key,
|
|
9760
|
+
dims: { w: s.widthPx, h: s.heightPx },
|
|
9761
|
+
upp: s.upp,
|
|
9762
|
+
shapes: this.shapes,
|
|
9763
|
+
conditions: this.conditions
|
|
9764
|
+
}, { units });
|
|
9765
|
+
return { sheet: s, build };
|
|
9766
|
+
}
|
|
8847
9767
|
exportPayload() {
|
|
8848
9768
|
if (!this.docs.size) throw new UserError("No plan loaded \u2014 call load_plan first.");
|
|
8849
9769
|
return {
|
|
@@ -9363,6 +10283,18 @@ var takeoffSummaryOutput = {
|
|
|
9363
10283
|
}).passthrough(),
|
|
9364
10284
|
scale_unconfirmed: z.array(z.string()).optional().describe("Sheets whose scale is agent-set and no human has confirmed \u2014 these totals stand on an unverified scale; verify against a stated dimension or confirm in the canvas")
|
|
9365
10285
|
};
|
|
10286
|
+
var exportDxfOutput = {
|
|
10287
|
+
path: z.string().describe("The DXF written"),
|
|
10288
|
+
sheet: z.string().describe("Sheet key the drawing was cut from"),
|
|
10289
|
+
sheet_number: z.string().nullable(),
|
|
10290
|
+
units: z.enum(["ft", "m"]),
|
|
10291
|
+
layers: z.array(z.string()).describe("Layer names in table order \u2014 OT-<TAG>, plus -DEDUCT/-HOLE/-WALL/-LINEAR/-COUNT suffix layers and OT-LABELS"),
|
|
10292
|
+
entities: z.number().int().describe("LWPOLYLINE + CIRCLE + TEXT entities in model space"),
|
|
10293
|
+
shapes: z.number().int().describe("Committed shapes that produced geometry"),
|
|
10294
|
+
skipped: z.array(z.object({ id: z.string(), reason: z.string() })).describe("Shapes on this sheet left out, each with why \u2014 never silent"),
|
|
10295
|
+
extents: z.object({ min: z.tuple([z.number(), z.number()]), max: z.tuple([z.number(), z.number()]) }).nullable().describe("Model-space bounding box in output units; origin = sheet's bottom-left, Y up"),
|
|
10296
|
+
bytes: z.number().int()
|
|
10297
|
+
};
|
|
9366
10298
|
var exportTakeoffOutput = {
|
|
9367
10299
|
schema: z.string(),
|
|
9368
10300
|
project_name: z.string(),
|
|
@@ -10174,7 +11106,7 @@ function normalize(d) {
|
|
|
10174
11106
|
}
|
|
10175
11107
|
return out;
|
|
10176
11108
|
}
|
|
10177
|
-
function
|
|
11109
|
+
function fmt2(n) {
|
|
10178
11110
|
let r = Math.round(n * 1e4) / 1e4;
|
|
10179
11111
|
if (Object.is(r, -0)) r = 0;
|
|
10180
11112
|
return String(r);
|
|
@@ -10202,7 +11134,7 @@ function transformPath(d, fn) {
|
|
|
10202
11134
|
}
|
|
10203
11135
|
if (bad) continue;
|
|
10204
11136
|
let s = seg.t;
|
|
10205
|
-
for (let k = 0; k < dst.length; k++) s += " " +
|
|
11137
|
+
for (let k = 0; k < dst.length; k++) s += " " + fmt2(dst[k]);
|
|
10206
11138
|
parts.push(s);
|
|
10207
11139
|
}
|
|
10208
11140
|
return parts.join(" ");
|
|
@@ -10464,6 +11396,16 @@ function boostForDark(hex2) {
|
|
|
10464
11396
|
}
|
|
10465
11397
|
|
|
10466
11398
|
// ../web/src/lib/markedset.js
|
|
11399
|
+
function authorTallyLine(shapes) {
|
|
11400
|
+
const byAuthor = /* @__PURE__ */ new Map();
|
|
11401
|
+
for (const s of shapes || []) {
|
|
11402
|
+
const a = typeof s.author === "string" && s.author.trim() ? s.author.trim() : "";
|
|
11403
|
+
byAuthor.set(a, (byAuthor.get(a) || 0) + 1);
|
|
11404
|
+
}
|
|
11405
|
+
if (![...byAuthor.keys()].some(Boolean)) return null;
|
|
11406
|
+
const parts = [...byAuthor.entries()].sort((a, b) => a[0] === "" ? 1 : b[0] === "" ? -1 : a[0].localeCompare(b[0])).map(([a, n]) => `${a || "unattributed"} (${n})`);
|
|
11407
|
+
return `Marks by: ${parts.join(" \xB7 ")}`;
|
|
11408
|
+
}
|
|
10467
11409
|
var COBALT = "#1f3fc7";
|
|
10468
11410
|
var DEDUCT_RED = "#b03a26";
|
|
10469
11411
|
var DARK_BG = [0.055, 0.07, 0.09];
|
|
@@ -10741,6 +11683,11 @@ async function buildMarkedSetPdf({ projectName, dark, sheets, shapes, markups, a
|
|
|
10741
11683
|
metaY -= 12;
|
|
10742
11684
|
draw(`Approval stamps: ${apCount.estimator} estimator-approved \xB7 ${apCount.agent} agent-marked`, { x: 52, y: metaY, size: 9.5, font, color: muted });
|
|
10743
11685
|
}
|
|
11686
|
+
const authorsLine = authorTallyLine(markedShapes);
|
|
11687
|
+
if (authorsLine) {
|
|
11688
|
+
metaY -= 12;
|
|
11689
|
+
draw(authorsLine, { x: 52, y: metaY, size: 9.5, font, color: muted });
|
|
11690
|
+
}
|
|
10744
11691
|
let y = metaY - 34;
|
|
10745
11692
|
const rows = conditionTotals(conditions, markedShapes).filter((r) => r.shape_count > 0);
|
|
10746
11693
|
draw("CONDITIONS", { x: 52, y, size: 9, font: bold, color: muted });
|
|
@@ -11179,6 +12126,9 @@ async function isOwnExport(outPath, kind) {
|
|
|
11179
12126
|
if (kind === "json") {
|
|
11180
12127
|
return /"schema"\s*:\s*"opentakeoff\./.test((await head(outPath, 4096)).toString("utf8"));
|
|
11181
12128
|
}
|
|
12129
|
+
if (kind === "dxf") {
|
|
12130
|
+
return /^999\r?\nOpenTakeoff DXF export/.test((await head(outPath, 64)).toString("utf8"));
|
|
12131
|
+
}
|
|
11182
12132
|
if ((await head(outPath, 5)).toString("latin1") !== "%PDF-") return false;
|
|
11183
12133
|
const { readFile: readFile4 } = await import("node:fs/promises");
|
|
11184
12134
|
const { PDFDocument } = await import("pdf-lib");
|
|
@@ -11735,6 +12685,33 @@ All-or-nothing, like derive_base: an unknown tag, a transition landing on either
|
|
|
11735
12685
|
}
|
|
11736
12686
|
return payload;
|
|
11737
12687
|
}));
|
|
12688
|
+
server.registerTool("export_dxf", {
|
|
12689
|
+
description: `The takeoff as a CAD drawing \u2014 a DXF (R2000) AutoCAD, BricsCAD, LibreCAD and Revit import as native geometry, not a picture. ONE sheet per file, like a DWG: every committed shape on that sheet becomes an LWPOLYLINE (floor rings CLOSED, walls and linear runs open, count marks a 1-ft circle), on a layer named for its finish \u2014 OT-<TAG>, with -DEDUCT / -HOLE / -WALL / -LINEAR / -COUNT suffix layers so a CAD user isolates any bucket with one layer filter, and room labels as TEXT on OT-LABELS. Coordinates are real units in the sheet's own frame: origin at the sheet's BOTTOM-left, Y up (CAD convention), feet by default ($INSUNITS 2) or metres with units:"m"; a ring's area in CAD equals its area in export_report to rounding, so the drawing IS the audit. Requires the sheet's scale (refuses otherwise \u2014 pixels in a DXF are worse than nothing); with several sheets carrying shapes, pass sheet to choose the drawing (the refusal lists them). The reply names every shape left out and why \u2014 a reconciled deduct ships as its parent's -HOLE ring, never twice. Writes to path (required \u2014 a DXF lives on disk, next to the DWG it aligns to); pair with export_marked_pdf for the reviewed planset.`,
|
|
12690
|
+
inputSchema: {
|
|
12691
|
+
path: z2.string().describe("File path to write the .dxf to"),
|
|
12692
|
+
sheet: z2.string().optional().describe('Sheet key ("plan.pdf", "plan.pdf#2") or title-block number ("A-101"). Optional only when exactly one calibrated sheet carries shapes'),
|
|
12693
|
+
units: z2.enum(["ft", "m"]).optional().describe('Output units \u2014 "ft" (default) or "m"'),
|
|
12694
|
+
overwrite: z2.boolean().optional().describe(OVERWRITE_DESC)
|
|
12695
|
+
},
|
|
12696
|
+
outputSchema: exportDxfOutput
|
|
12697
|
+
}, run("export_dxf", async ({ path: outPath, sheet, units, overwrite }) => {
|
|
12698
|
+
const { sheet: s, build } = session.exportDxf(sheet, units ?? "ft");
|
|
12699
|
+
await assertWritable(outPath, "dxf", overwrite);
|
|
12700
|
+
const { writeFile: writeFile2 } = await import("node:fs/promises");
|
|
12701
|
+
await writeFile2(outPath, build.dxf, "utf8");
|
|
12702
|
+
return {
|
|
12703
|
+
path: outPath,
|
|
12704
|
+
sheet: s.key,
|
|
12705
|
+
sheet_number: s.sheetNumber ?? null,
|
|
12706
|
+
units: units ?? "ft",
|
|
12707
|
+
layers: build.layers,
|
|
12708
|
+
entities: build.entities,
|
|
12709
|
+
shapes: build.shapes,
|
|
12710
|
+
skipped: build.skipped,
|
|
12711
|
+
extents: build.extents,
|
|
12712
|
+
bytes: Buffer.byteLength(build.dxf, "utf8")
|
|
12713
|
+
};
|
|
12714
|
+
}));
|
|
11738
12715
|
server.registerTool("export_report", {
|
|
11739
12716
|
description: `The computed Report document \u2014 "opentakeoff.report.v1", the same schema the canvas Report's JSON export writes. Everything a pricing consumer needs without re-implementing the app's math: per-condition quantities with waste and multiplier applied (gross and *_net), the computed materials BUY LIST per condition (order quantity = basis \xF7 coverage rate, rounded up to whole purchase units) plus the project-wide roll-up summed by (name, unit), per-sheet BASE subtotals, scale provenance per sheet, and annotations. Contrast: export_takeoff is the raw canvas payload (materials as CONFIG rows, no computed quantities) and takeoff_summary strips materials for a compact reply \u2014 when the numbers are leaving for pricing, consume this. A report alone is HALF the deliverable: pair it with export_marked_pdf, because a takeoff is reviewed on marked drawings, not on numbers. Returned inline; pass path to also write it to disk as JSON.`,
|
|
11740
12717
|
inputSchema: {
|
|
@@ -12122,9 +13099,13 @@ var TOOL_STAGES = {
|
|
|
12122
13099
|
"export_report",
|
|
12123
13100
|
"import_takeoff",
|
|
12124
13101
|
"apply_rules",
|
|
12125
|
-
"export_marked_pdf"
|
|
13102
|
+
"export_marked_pdf",
|
|
13103
|
+
"export_dxf"
|
|
12126
13104
|
]
|
|
12127
13105
|
};
|
|
13106
|
+
var TOOL_NAMES = Object.freeze(
|
|
13107
|
+
Object.values(TOOL_STAGES).flat().sort()
|
|
13108
|
+
);
|
|
12128
13109
|
var OPENABLE = ["measure", "revise", "handoff"];
|
|
12129
13110
|
var openToolStageOutput = {
|
|
12130
13111
|
stage: z3.string().describe("The stage that was opened"),
|
|
@@ -12211,7 +13192,7 @@ function nameTheStageInRefusals(server) {
|
|
|
12211
13192
|
// package.json
|
|
12212
13193
|
var package_default = {
|
|
12213
13194
|
name: "opentakeoff-mcp",
|
|
12214
|
-
version: "0.9.
|
|
13195
|
+
version: "0.9.65",
|
|
12215
13196
|
mcpName: "io.github.Kentucky-ai/opentakeoff",
|
|
12216
13197
|
type: "module",
|
|
12217
13198
|
description: "OpenTakeoff MCP server \u2014 drive the takeoff engine from your MCP client over stdio.",
|
|
@@ -12227,7 +13208,8 @@ var package_default = {
|
|
|
12227
13208
|
mcpb: "npm run build && node scripts/build-mcpb.mjs",
|
|
12228
13209
|
prepublishOnly: "npm run typecheck && npm test && npm run build",
|
|
12229
13210
|
typecheck: "tsc --noEmit",
|
|
12230
|
-
|
|
13211
|
+
"check:tool-count": "node scripts/check-tool-count.mjs",
|
|
13212
|
+
test: "node --import tsx --test test/conformance.test.ts test/context.test.ts test/dxf.test.ts test/e2e.test.ts test/labels.test.ts test/overlap.test.ts test/parity.test.ts test/raster.test.ts test/resources.test.ts test/safewrite.test.ts test/scalewarn.test.ts test/session.test.ts test/staging.test.ts test/tools.test.ts test/transitions.test.ts test/twins.test.ts test/view.test.ts"
|
|
12231
13213
|
},
|
|
12232
13214
|
dependencies: {
|
|
12233
13215
|
"@modelcontextprotocol/sdk": "^1.12.0",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "opentakeoff-mcp",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.65",
|
|
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.",
|
|
@@ -16,7 +16,8 @@
|
|
|
16
16
|
"mcpb": "npm run build && node scripts/build-mcpb.mjs",
|
|
17
17
|
"prepublishOnly": "npm run typecheck && npm test && npm run build",
|
|
18
18
|
"typecheck": "tsc --noEmit",
|
|
19
|
-
"
|
|
19
|
+
"check:tool-count": "node scripts/check-tool-count.mjs",
|
|
20
|
+
"test": "node --import tsx --test test/conformance.test.ts test/context.test.ts test/dxf.test.ts test/e2e.test.ts test/labels.test.ts test/overlap.test.ts test/parity.test.ts test/raster.test.ts test/resources.test.ts test/safewrite.test.ts test/scalewarn.test.ts test/session.test.ts test/staging.test.ts test/tools.test.ts test/transitions.test.ts test/twins.test.ts test/view.test.ts"
|
|
20
21
|
},
|
|
21
22
|
"dependencies": {
|
|
22
23
|
"@modelcontextprotocol/sdk": "^1.12.0",
|