@obenor/mcp 0.1.0 → 0.3.0
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/bundle/kernel-worker.mjs +1031 -148
- package/bundle/server.mjs +35240 -826
- package/package.json +1 -1
package/bundle/kernel-worker.mjs
CHANGED
|
@@ -5,13 +5,8 @@ var __glob = (map) => (path) => {
|
|
|
5
5
|
if (fn) return fn();
|
|
6
6
|
throw new Error("Module not found in bundle: " + path);
|
|
7
7
|
};
|
|
8
|
-
var __esm = (fn, res
|
|
9
|
-
|
|
10
|
-
try {
|
|
11
|
-
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
12
|
-
} catch (e) {
|
|
13
|
-
throw err = [e], e;
|
|
14
|
-
}
|
|
8
|
+
var __esm = (fn, res) => function __init() {
|
|
9
|
+
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
15
10
|
};
|
|
16
11
|
var __export = (target, all) => {
|
|
17
12
|
for (var name in all)
|
|
@@ -1498,9 +1493,9 @@ async function createObenorKernel(moduleArg = {}) {
|
|
|
1498
1493
|
var mounts = [];
|
|
1499
1494
|
var check = [mount];
|
|
1500
1495
|
while (check.length) {
|
|
1501
|
-
var
|
|
1502
|
-
mounts.push(
|
|
1503
|
-
check.push(...
|
|
1496
|
+
var m2 = check.pop();
|
|
1497
|
+
mounts.push(m2);
|
|
1498
|
+
check.push(...m2.mounts);
|
|
1504
1499
|
}
|
|
1505
1500
|
return mounts;
|
|
1506
1501
|
}, syncfs(populate, callback) {
|
|
@@ -4961,9 +4956,9 @@ async function createObenorKernel2(moduleArg = {}) {
|
|
|
4961
4956
|
var mounts = [];
|
|
4962
4957
|
var check = [mount];
|
|
4963
4958
|
while (check.length) {
|
|
4964
|
-
var
|
|
4965
|
-
mounts.push(
|
|
4966
|
-
check.push(...
|
|
4959
|
+
var m2 = check.pop();
|
|
4960
|
+
mounts.push(m2);
|
|
4961
|
+
check.push(...m2.mounts);
|
|
4967
4962
|
}
|
|
4968
4963
|
return mounts;
|
|
4969
4964
|
}, syncfs(populate, callback) {
|
|
@@ -8119,8 +8114,8 @@ var Scratch = class {
|
|
|
8119
8114
|
used = 0;
|
|
8120
8115
|
highWater = 0;
|
|
8121
8116
|
overflow = [];
|
|
8122
|
-
constructor(
|
|
8123
|
-
this.m =
|
|
8117
|
+
constructor(m2) {
|
|
8118
|
+
this.m = m2;
|
|
8124
8119
|
}
|
|
8125
8120
|
/**
|
|
8126
8121
|
* Ends a call. Only here is it safe to move the main block, because no
|
|
@@ -8216,19 +8211,19 @@ var Kernel = class _Kernel {
|
|
|
8216
8211
|
*/
|
|
8217
8212
|
static LOAD_TIMEOUT_MS = 45e3;
|
|
8218
8213
|
scratch;
|
|
8219
|
-
constructor(
|
|
8220
|
-
this.m =
|
|
8214
|
+
constructor(m2, fallbackNote = null) {
|
|
8215
|
+
this.m = m2;
|
|
8221
8216
|
this.fallbackNote = fallbackNote;
|
|
8222
|
-
this.scratch = new Scratch(
|
|
8217
|
+
this.scratch = new Scratch(m2);
|
|
8223
8218
|
const required = ["_ob_init", "_ob_version", "_ob_doc_create", "_ob_make_box"];
|
|
8224
|
-
const missing = required.filter((name) => typeof
|
|
8219
|
+
const missing = required.filter((name) => typeof m2[name] !== "function");
|
|
8225
8220
|
if (missing.length > 0) {
|
|
8226
8221
|
throw new KernelError(-1, `the loaded .wasm does not match its JavaScript glue (missing ${missing.join(", ")}). Re-stage the kernel assets: pnpm --filter @obenor/app run prepare-wasm`, "kernel", "load");
|
|
8227
8222
|
}
|
|
8228
|
-
const status =
|
|
8223
|
+
const status = m2._ob_init();
|
|
8229
8224
|
if (status !== 0)
|
|
8230
8225
|
throw this.error(status, "ob_init");
|
|
8231
|
-
const version =
|
|
8226
|
+
const version = m2.UTF8ToString(m2._ob_version());
|
|
8232
8227
|
if (!version) {
|
|
8233
8228
|
throw new KernelError(-1, "the kernel reported no version, which means the .wasm and its glue are out of step. Re-stage the kernel assets: pnpm --filter @obenor/app run prepare-wasm", "kernel", "load");
|
|
8234
8229
|
}
|
|
@@ -8882,8 +8877,8 @@ var Kernel = class _Kernel {
|
|
|
8882
8877
|
throw new KernelError(-3, "transform expects a 16-element row-major matrix");
|
|
8883
8878
|
}
|
|
8884
8879
|
this.scratch.reset();
|
|
8885
|
-
const
|
|
8886
|
-
return this.produce((out) => this.m._ob_transform(doc2, shape,
|
|
8880
|
+
const m2 = this.scratch.f64(matrixRowMajor);
|
|
8881
|
+
return this.produce((out) => this.m._ob_transform(doc2, shape, m2, out), "transform");
|
|
8887
8882
|
}
|
|
8888
8883
|
/**
|
|
8889
8884
|
* A ruled surface between two curves.
|
|
@@ -8998,8 +8993,8 @@ var Kernel = class _Kernel {
|
|
|
8998
8993
|
}
|
|
8999
8994
|
this.scratch.reset();
|
|
9000
8995
|
const id = this.m._ob_name_intern(doc2, this.scratch.cstring(face));
|
|
9001
|
-
const indices = this.scratch.i32(moves.flatMap((
|
|
9002
|
-
const positions = this.scratch.f64(moves.flatMap((
|
|
8996
|
+
const indices = this.scratch.i32(moves.flatMap((m2) => [m2.i, m2.j]));
|
|
8997
|
+
const positions = this.scratch.f64(moves.flatMap((m2) => [m2.to[0], m2.to[1], m2.to[2]]));
|
|
9003
8998
|
return this.produce((out) => this.m._ob_move_surface_poles(doc2, shape, id, indices, positions, moves.length, out), "moveSurfacePoles");
|
|
9004
8999
|
}
|
|
9005
9000
|
/** One face, carried `distance` past its edge on all four sides. */
|
|
@@ -9542,6 +9537,546 @@ var Kernel = class _Kernel {
|
|
|
9542
9537
|
}
|
|
9543
9538
|
};
|
|
9544
9539
|
|
|
9540
|
+
// ../doc/dist/arch.js
|
|
9541
|
+
var unit = (a, b) => {
|
|
9542
|
+
const dx = b[0] - a[0];
|
|
9543
|
+
const dy = b[1] - a[1];
|
|
9544
|
+
const len = Math.hypot(dx, dy);
|
|
9545
|
+
if (len < 1e-9)
|
|
9546
|
+
return null;
|
|
9547
|
+
return [dx / len, dy / len];
|
|
9548
|
+
};
|
|
9549
|
+
var leftNormal = ([x, y]) => [-y, x];
|
|
9550
|
+
function signedArea2(loop) {
|
|
9551
|
+
let sum = 0;
|
|
9552
|
+
for (let i = 0; i < loop.length; i++) {
|
|
9553
|
+
const a = loop[i];
|
|
9554
|
+
const b = loop[(i + 1) % loop.length];
|
|
9555
|
+
sum += a[0] * b[1] - b[0] * a[1];
|
|
9556
|
+
}
|
|
9557
|
+
return sum;
|
|
9558
|
+
}
|
|
9559
|
+
function offsetSide(points, d, closed, miterLimit = 4) {
|
|
9560
|
+
const n = points.length;
|
|
9561
|
+
const dirs = [];
|
|
9562
|
+
for (let i = 0; i + 1 < n; i++)
|
|
9563
|
+
dirs.push(unit(points[i], points[i + 1]));
|
|
9564
|
+
if (closed)
|
|
9565
|
+
dirs.push(unit(points[n - 1], points[0]));
|
|
9566
|
+
if (dirs.some((v) => v === null)) {
|
|
9567
|
+
throw new KernelError(-3, "a wall path repeats a point, so one of its segments has no direction and there is nothing to offset it from", "", "wall");
|
|
9568
|
+
}
|
|
9569
|
+
const out = [];
|
|
9570
|
+
for (let i = 0; i < n; i++) {
|
|
9571
|
+
const inc = i === 0 ? closed ? dirs[dirs.length - 1] : null : dirs[i - 1];
|
|
9572
|
+
const outg = i < dirs.length && !(!closed && i === n - 1) ? dirs[i] : null;
|
|
9573
|
+
if (inc === null || outg === null) {
|
|
9574
|
+
const s = outg ?? inc;
|
|
9575
|
+
const nrm = leftNormal(s);
|
|
9576
|
+
out.push([points[i][0] + nrm[0] * d, points[i][1] + nrm[1] * d]);
|
|
9577
|
+
continue;
|
|
9578
|
+
}
|
|
9579
|
+
const nA = leftNormal(inc);
|
|
9580
|
+
const nB = leftNormal(outg);
|
|
9581
|
+
const sx = nA[0] + nB[0];
|
|
9582
|
+
const sy = nA[1] + nB[1];
|
|
9583
|
+
const s2 = sx * sx + sy * sy;
|
|
9584
|
+
if (s2 < 1e-12) {
|
|
9585
|
+
const nrm = leftNormal(outg);
|
|
9586
|
+
out.push([points[i][0] + nrm[0] * d, points[i][1] + nrm[1] * d]);
|
|
9587
|
+
continue;
|
|
9588
|
+
}
|
|
9589
|
+
const mx = 2 * sx / s2;
|
|
9590
|
+
const my = 2 * sy / s2;
|
|
9591
|
+
if (Math.hypot(mx, my) > miterLimit) {
|
|
9592
|
+
const nrm = leftNormal(outg);
|
|
9593
|
+
out.push([points[i][0] + nrm[0] * d, points[i][1] + nrm[1] * d]);
|
|
9594
|
+
continue;
|
|
9595
|
+
}
|
|
9596
|
+
out.push([points[i][0] + mx * d, points[i][1] + my * d]);
|
|
9597
|
+
}
|
|
9598
|
+
return out;
|
|
9599
|
+
}
|
|
9600
|
+
function wallLoops(points, thickness, closed, justification = "centre") {
|
|
9601
|
+
const t = thickness;
|
|
9602
|
+
const [dl, dr] = justification === "left" ? [t, 0] : justification === "right" ? [0, -t] : [t / 2, -t / 2];
|
|
9603
|
+
const left = offsetSide(points, dl, closed);
|
|
9604
|
+
const right = offsetSide(points, dr, closed);
|
|
9605
|
+
if (!closed)
|
|
9606
|
+
return [[...left, ...right.slice().reverse()]];
|
|
9607
|
+
return [left, right.slice().reverse()];
|
|
9608
|
+
}
|
|
9609
|
+
function roofHeight(points, pitches, p, base = 0, sign = 1) {
|
|
9610
|
+
let z = Infinity;
|
|
9611
|
+
for (let i = 0; i < points.length; i++) {
|
|
9612
|
+
const pitch = pitches[i];
|
|
9613
|
+
if (pitch === null || pitch === void 0)
|
|
9614
|
+
continue;
|
|
9615
|
+
const a = points[i];
|
|
9616
|
+
const b = points[(i + 1) % points.length];
|
|
9617
|
+
const d = Math.max(0, sign * distanceLeftOf(p, a, b));
|
|
9618
|
+
z = Math.min(z, base + d * Math.tan(pitch));
|
|
9619
|
+
}
|
|
9620
|
+
return Number.isFinite(z) ? z : null;
|
|
9621
|
+
}
|
|
9622
|
+
function distanceLeftOf(p, a, b) {
|
|
9623
|
+
const dx = b[0] - a[0];
|
|
9624
|
+
const dy = b[1] - a[1];
|
|
9625
|
+
const len = Math.hypot(dx, dy);
|
|
9626
|
+
return (dx * (p[1] - a[1]) - dy * (p[0] - a[0])) / len;
|
|
9627
|
+
}
|
|
9628
|
+
var THIN_WALL = 75;
|
|
9629
|
+
var THIN_SLAB = 100;
|
|
9630
|
+
var TIGHT_HEAD = 150;
|
|
9631
|
+
var SHALLOW_PITCH = 2 * Math.PI / 180;
|
|
9632
|
+
var STEEP_PITCH = 60 * Math.PI / 180;
|
|
9633
|
+
var MAX_PITCH = 85 * Math.PI / 180;
|
|
9634
|
+
var DEG = 180 / Math.PI;
|
|
9635
|
+
var m = (mm) => `${(mm / 1e3).toFixed(3)} m`;
|
|
9636
|
+
function polygonCurves(loop, firstId) {
|
|
9637
|
+
return loop.map((p, i) => ({
|
|
9638
|
+
kind: "line",
|
|
9639
|
+
id: firstId + i,
|
|
9640
|
+
from: [p[0], p[1]],
|
|
9641
|
+
to: [loop[(i + 1) % loop.length][0], loop[(i + 1) % loop.length][1]]
|
|
9642
|
+
}));
|
|
9643
|
+
}
|
|
9644
|
+
var translation = (dx, dy, dz) => [
|
|
9645
|
+
1,
|
|
9646
|
+
0,
|
|
9647
|
+
0,
|
|
9648
|
+
dx,
|
|
9649
|
+
0,
|
|
9650
|
+
1,
|
|
9651
|
+
0,
|
|
9652
|
+
dy,
|
|
9653
|
+
0,
|
|
9654
|
+
0,
|
|
9655
|
+
1,
|
|
9656
|
+
dz,
|
|
9657
|
+
0,
|
|
9658
|
+
0,
|
|
9659
|
+
0,
|
|
9660
|
+
1
|
|
9661
|
+
];
|
|
9662
|
+
function planPoints(path, ctx, kind, field) {
|
|
9663
|
+
if (!Array.isArray(path) || path.length === 0) {
|
|
9664
|
+
throw new KernelError(-3, `a ${kind} needs '${field}': a list of [x, y, z] plan points`, "", kind);
|
|
9665
|
+
}
|
|
9666
|
+
const pts = path.map((p) => {
|
|
9667
|
+
const v = ctx.vec3(p, "length", [0, 0, 0]);
|
|
9668
|
+
return [v[0], v[1]];
|
|
9669
|
+
});
|
|
9670
|
+
const first = pts[0];
|
|
9671
|
+
const last = pts[pts.length - 1];
|
|
9672
|
+
if (pts.length > 2 && Math.hypot(last[0] - first[0], last[1] - first[1]) < 1e-9)
|
|
9673
|
+
pts.pop();
|
|
9674
|
+
return pts;
|
|
9675
|
+
}
|
|
9676
|
+
function pathLength(points, closed) {
|
|
9677
|
+
let total = 0;
|
|
9678
|
+
const last = closed ? points.length : points.length - 1;
|
|
9679
|
+
for (let i = 0; i < last; i++) {
|
|
9680
|
+
const a = points[i];
|
|
9681
|
+
const b = points[(i + 1) % points.length];
|
|
9682
|
+
total += Math.hypot(b[0] - a[0], b[1] - a[1]);
|
|
9683
|
+
}
|
|
9684
|
+
return total;
|
|
9685
|
+
}
|
|
9686
|
+
function buildLevel(feature, ctx) {
|
|
9687
|
+
const k = ctx.kernel;
|
|
9688
|
+
const elevation = ctx.number(feature.elevation, "length");
|
|
9689
|
+
const size = ctx.number(feature.size, "length", 5e3);
|
|
9690
|
+
if (!(size > 0)) {
|
|
9691
|
+
throw new KernelError(-3, `a level's datum square must have a positive size, not ${size}`, "", "level");
|
|
9692
|
+
}
|
|
9693
|
+
const half = size / 2;
|
|
9694
|
+
return k.buildSketch(ctx.doc, feature.id, { origin: [0, 0, elevation], normal: [0, 0, 1], xAxis: [1, 0, 0] }, {
|
|
9695
|
+
curves: [
|
|
9696
|
+
{ kind: "line", id: 1, from: [-half, -half], to: [half, -half] },
|
|
9697
|
+
{ kind: "line", id: 2, from: [half, -half], to: [half, half] },
|
|
9698
|
+
{ kind: "line", id: 3, from: [half, half], to: [-half, half] },
|
|
9699
|
+
{ kind: "line", id: 4, from: [-half, half], to: [-half, -half] }
|
|
9700
|
+
]
|
|
9701
|
+
});
|
|
9702
|
+
}
|
|
9703
|
+
function wallPlanOf(feature, ctx) {
|
|
9704
|
+
const thickness = ctx.number(feature.thickness, "length");
|
|
9705
|
+
if (!(thickness > 0)) {
|
|
9706
|
+
throw new KernelError(-3, `a wall ${thickness} mm thick encloses nothing: thickness must be positive`, "", "wall");
|
|
9707
|
+
}
|
|
9708
|
+
const base = ctx.number(feature.base, "length", 0);
|
|
9709
|
+
const top = ctx.number(feature.top, "length");
|
|
9710
|
+
if (!(top > base)) {
|
|
9711
|
+
throw new KernelError(-3, `a wall from ${m(base)} to ${m(top)} has no height to extrude. 'top' is an ELEVATION, not a height - level('first') + 2700, or an absolute number in document units`, "", "wall");
|
|
9712
|
+
}
|
|
9713
|
+
const closed = feature.closed === true;
|
|
9714
|
+
const points = planPoints(feature.path, ctx, "wall", "path");
|
|
9715
|
+
if (points.length < 2) {
|
|
9716
|
+
throw new KernelError(-3, `a wall path needs at least two points to have a direction; this one has ${points.length}`, "", "wall");
|
|
9717
|
+
}
|
|
9718
|
+
if (closed && points.length < 3) {
|
|
9719
|
+
throw new KernelError(-3, `a closed wall path needs at least three points: ${points.length} enclose no plan`, "", "wall");
|
|
9720
|
+
}
|
|
9721
|
+
return {
|
|
9722
|
+
id: feature.id,
|
|
9723
|
+
points,
|
|
9724
|
+
closed,
|
|
9725
|
+
thickness,
|
|
9726
|
+
base,
|
|
9727
|
+
top,
|
|
9728
|
+
justification: feature.justification ?? "centre"
|
|
9729
|
+
};
|
|
9730
|
+
}
|
|
9731
|
+
function buildWall(feature, ctx) {
|
|
9732
|
+
const k = ctx.kernel;
|
|
9733
|
+
const doc2 = ctx.doc;
|
|
9734
|
+
const plan2 = wallPlanOf(feature, ctx);
|
|
9735
|
+
const loops = wallLoops(plan2.points, plan2.thickness, plan2.closed, plan2.justification);
|
|
9736
|
+
const curves = [];
|
|
9737
|
+
const loopSpecs = [];
|
|
9738
|
+
const areas = loops.map((loop) => Math.abs(signedArea2(loop)));
|
|
9739
|
+
const outerIndex = areas.indexOf(Math.max(...areas));
|
|
9740
|
+
for (const [index, ring] of loops.entries()) {
|
|
9741
|
+
const loop = signedArea2(ring) < 0 ? ring.slice().reverse() : ring;
|
|
9742
|
+
const made = polygonCurves(loop, curves.length + 1);
|
|
9743
|
+
loopSpecs.push({
|
|
9744
|
+
curveIndices: made.map((_, i) => curves.length + i),
|
|
9745
|
+
outer: index === outerIndex
|
|
9746
|
+
});
|
|
9747
|
+
curves.push(...made);
|
|
9748
|
+
}
|
|
9749
|
+
const profile = k.buildSketch(doc2, `${feature.id}__plan`, { origin: [0, 0, plan2.base], normal: [0, 0, 1], xAxis: [1, 0, 0] }, { curves, loops: loopSpecs });
|
|
9750
|
+
if (plan2.thickness < THIN_WALL) {
|
|
9751
|
+
ctx.warn(`this wall is ${plan2.thickness.toFixed(0)} mm thick, under the ${THIN_WALL} mm a stud with one board comes to - fine as a partition or a screen, thin for anything holding the roof up. Built as asked`);
|
|
9752
|
+
}
|
|
9753
|
+
const centreline = pathLength(plan2.points, plan2.closed);
|
|
9754
|
+
ctx.warn(`${plan2.closed ? "closed" : "open"} wall: centreline ${m(centreline)} over ${plan2.points.length} point${plan2.points.length === 1 ? "" : "s"}, ${plan2.thickness.toFixed(0)} mm thick, ${m(plan2.base)} to ${m(plan2.top)} (${m(plan2.top - plan2.base)} high), ${plan2.justification}-justified`);
|
|
9755
|
+
return k.extrude(doc2, profile, {
|
|
9756
|
+
direction: [0, 0, 1],
|
|
9757
|
+
distance: plan2.top - plan2.base,
|
|
9758
|
+
mode: ExtrudeMode.Blind
|
|
9759
|
+
});
|
|
9760
|
+
}
|
|
9761
|
+
function hostWallOf(host, ctx) {
|
|
9762
|
+
let id = host;
|
|
9763
|
+
const seen = [];
|
|
9764
|
+
for (let hop = 0; hop < 64; hop++) {
|
|
9765
|
+
const feature = ctx.featureOf(id);
|
|
9766
|
+
if (feature.kind === "wall")
|
|
9767
|
+
return wallPlanOf(feature, ctx);
|
|
9768
|
+
if (feature.kind === "opening") {
|
|
9769
|
+
seen.push(id);
|
|
9770
|
+
id = feature.host;
|
|
9771
|
+
continue;
|
|
9772
|
+
}
|
|
9773
|
+
throw new KernelError(-3, `an opening is positioned along its host wall's own centreline, so its host must be a wall or another opening in one. '${id}' is a ${feature.kind}` + (seen.length > 0 ? `, reached through ${seen.join(" -> ")}` : "") + ". Cut the openings into the wall first, then run the other feature on the result", "", "opening");
|
|
9774
|
+
}
|
|
9775
|
+
throw new KernelError(-3, `the chain of openings starting at '${host}' never reaches a wall`, "", "opening");
|
|
9776
|
+
}
|
|
9777
|
+
function hostFrameAt(wall, along, _ctx) {
|
|
9778
|
+
const total = pathLength(wall.points, wall.closed);
|
|
9779
|
+
if (!(along >= 0) || along > total) {
|
|
9780
|
+
throw new KernelError(-3, `'${wall.id}' runs ${m(total)} along its centreline and 'along' is ${m(along)}, which is ${along < 0 ? "before the start" : `${m(along - total)} past the end`}. 'along' is measured from the first point of the wall's path, in document units`, "", "opening");
|
|
9781
|
+
}
|
|
9782
|
+
const last = wall.closed ? wall.points.length : wall.points.length - 1;
|
|
9783
|
+
let runStart = 0;
|
|
9784
|
+
let segment = 0;
|
|
9785
|
+
let runLength = 0;
|
|
9786
|
+
for (let i = 0; i < last; i++) {
|
|
9787
|
+
const a2 = wall.points[i];
|
|
9788
|
+
const b2 = wall.points[(i + 1) % wall.points.length];
|
|
9789
|
+
const length = Math.hypot(b2[0] - a2[0], b2[1] - a2[1]);
|
|
9790
|
+
segment = i;
|
|
9791
|
+
runLength = length;
|
|
9792
|
+
if (runStart + length >= along - 1e-9)
|
|
9793
|
+
break;
|
|
9794
|
+
runStart += length;
|
|
9795
|
+
}
|
|
9796
|
+
const local = along - runStart;
|
|
9797
|
+
const a = wall.points[segment];
|
|
9798
|
+
const b = wall.points[(segment + 1) % wall.points.length];
|
|
9799
|
+
const tangent = unit(a, b);
|
|
9800
|
+
if (tangent === null) {
|
|
9801
|
+
throw new KernelError(-3, `the run of '${wall.id}' at ${m(along)} has no direction: its two points are the same`, "", "opening");
|
|
9802
|
+
}
|
|
9803
|
+
const point = [a[0] + tangent[0] * local, a[1] + tangent[1] * local];
|
|
9804
|
+
const normal = [tangent[1], -tangent[0], 0];
|
|
9805
|
+
const across = wall.justification === "left" ? -wall.thickness / 2 : wall.justification === "right" ? wall.thickness / 2 : 0;
|
|
9806
|
+
return {
|
|
9807
|
+
segment,
|
|
9808
|
+
local,
|
|
9809
|
+
runLength,
|
|
9810
|
+
runStart,
|
|
9811
|
+
total,
|
|
9812
|
+
point,
|
|
9813
|
+
tangent,
|
|
9814
|
+
normal,
|
|
9815
|
+
frame: {
|
|
9816
|
+
origin: [point[0] + normal[0] * across, point[1] + normal[1] * across, wall.base],
|
|
9817
|
+
normal,
|
|
9818
|
+
xAxis: [tangent[0], tangent[1], 0]
|
|
9819
|
+
}
|
|
9820
|
+
};
|
|
9821
|
+
}
|
|
9822
|
+
function buildOpening(feature, ctx) {
|
|
9823
|
+
const k = ctx.kernel;
|
|
9824
|
+
const doc2 = ctx.doc;
|
|
9825
|
+
const body = ctx.shapeOf(feature.host);
|
|
9826
|
+
const wall = hostWallOf(feature.host, ctx);
|
|
9827
|
+
const width = ctx.number(feature.width, "length");
|
|
9828
|
+
const height = ctx.number(feature.height, "length");
|
|
9829
|
+
if (!(width > 0) || !(height > 0)) {
|
|
9830
|
+
throw new KernelError(-3, `an opening ${width} x ${height} removes nothing: width and height must both be positive`, "", "opening");
|
|
9831
|
+
}
|
|
9832
|
+
const sill = ctx.number(feature.sillHeight, "length", 0);
|
|
9833
|
+
const along = ctx.number(feature.along, "length");
|
|
9834
|
+
const at = hostFrameAt(wall, along, ctx);
|
|
9835
|
+
if (at.local - width / 2 < -1e-6 || at.local + width / 2 > at.runLength + 1e-6) {
|
|
9836
|
+
const widest = 2 * Math.min(at.local, at.runLength - at.local);
|
|
9837
|
+
throw new KernelError(-3, `a ${m(width)} opening centred ${m(along)} along '${wall.id}' runs past the end of the straight length of wall it sits on. That run goes from ${m(at.runStart)} to ${m(at.runStart + at.runLength)} and the opening wants ${m(along - width / 2)} to ${m(along + width / 2)}. An opening is cut in the plane of ONE run; past the corner it would be cutting the wall that turns away. ` + (widest > 1e-6 ? `At ${m(along)} the widest that fits is ${m(widest)}; ${m(width)} needs its centre between ${m(at.runStart + width / 2)} and ${m(at.runStart + at.runLength - width / 2)}` : "Move it off the corner"), "", "opening");
|
|
9838
|
+
}
|
|
9839
|
+
const storey = wall.top - wall.base;
|
|
9840
|
+
if (sill < -1e-6) {
|
|
9841
|
+
throw new KernelError(-3, `a sill ${m(-sill)} below the wall's base is under the floor. 'sillHeight' is measured UP from the wall's own base at ${m(wall.base)}`, "", "opening");
|
|
9842
|
+
}
|
|
9843
|
+
if (sill + height > storey + 1e-6) {
|
|
9844
|
+
throw new KernelError(-3, `a ${m(height)} opening on a ${m(sill)} sill reaches ${m(sill + height)}, and the wall is ${m(storey)} high (${m(wall.base)} to ${m(wall.top)}) - so it would breach the top, which is a gap in the roofline rather than a window. There is ${m(storey - sill)} between that sill and the top of the wall`, "", "opening");
|
|
9845
|
+
}
|
|
9846
|
+
if (storey - (sill + height) < TIGHT_HEAD && storey - (sill + height) > 1e-6) {
|
|
9847
|
+
ctx.warn(`the head lands ${(storey - sill - height).toFixed(0)} mm under the top of the wall, under the ${TIGHT_HEAD} mm a lintel wants. It builds; there is just very little wall left over the opening`);
|
|
9848
|
+
}
|
|
9849
|
+
const clearance = ctx.number(feature.clearance, "length", Math.max(1, wall.thickness * 0.02));
|
|
9850
|
+
if (!(clearance > 0)) {
|
|
9851
|
+
throw new KernelError(-3, "clearance is how far the cut runs past each face of the wall and must be positive: a cut that stops exactly on a face is the boolean that fails", "", "opening");
|
|
9852
|
+
}
|
|
9853
|
+
const halfWidth = width / 2;
|
|
9854
|
+
const reveal = k.buildSketch(doc2, `${feature.id}__reveal`, at.frame, {
|
|
9855
|
+
curves: [
|
|
9856
|
+
{ kind: "line", id: 1, from: [-halfWidth, sill], to: [halfWidth, sill] },
|
|
9857
|
+
{ kind: "line", id: 2, from: [halfWidth, sill], to: [halfWidth, sill + height] },
|
|
9858
|
+
{ kind: "line", id: 3, from: [halfWidth, sill + height], to: [-halfWidth, sill + height] },
|
|
9859
|
+
{ kind: "line", id: 4, from: [-halfWidth, sill + height], to: [-halfWidth, sill] }
|
|
9860
|
+
]
|
|
9861
|
+
});
|
|
9862
|
+
const tool = k.extrude(doc2, reveal, {
|
|
9863
|
+
direction: at.normal,
|
|
9864
|
+
distance: wall.thickness + 2 * clearance,
|
|
9865
|
+
mode: ExtrudeMode.Symmetric
|
|
9866
|
+
});
|
|
9867
|
+
ctx.warn(`opening ${m(width)} x ${m(height)} at ${m(along)} along '${wall.id}', sill ${m(wall.base + sill)}, head ${m(wall.base + sill + height)}`);
|
|
9868
|
+
return k.boolean(doc2, BooleanOp.Cut, [body], [tool]);
|
|
9869
|
+
}
|
|
9870
|
+
function buildSlab(feature, ctx) {
|
|
9871
|
+
const k = ctx.kernel;
|
|
9872
|
+
const doc2 = ctx.doc;
|
|
9873
|
+
const thickness = ctx.number(feature.thickness, "length");
|
|
9874
|
+
if (!(thickness > 0)) {
|
|
9875
|
+
throw new KernelError(-3, `a slab ${thickness} mm thick has no substance: thickness must be positive`, "", "slab");
|
|
9876
|
+
}
|
|
9877
|
+
if (thickness < THIN_SLAB) {
|
|
9878
|
+
ctx.warn(`this slab is ${thickness.toFixed(0)} mm, under the ${THIN_SLAB} mm a floor usually needs - a screed or a deck board rather than a structural floor. Built as asked`);
|
|
9879
|
+
}
|
|
9880
|
+
const level = ctx.number(feature.level, "length");
|
|
9881
|
+
const offset2 = ctx.number(feature.offset, "length", 0);
|
|
9882
|
+
const topZ = level + offset2;
|
|
9883
|
+
const hasBoundary = feature.boundary !== void 0;
|
|
9884
|
+
const hasProfile = feature.profile !== void 0;
|
|
9885
|
+
if (hasProfile && hasBoundary) {
|
|
9886
|
+
throw new KernelError(-3, `slab '${feature.id}' was given both a 'boundary' and a 'profile', which are two different plan shapes and no way to say which is meant. Keep one`, "", "slab");
|
|
9887
|
+
}
|
|
9888
|
+
if (!hasProfile && !hasBoundary) {
|
|
9889
|
+
throw new KernelError(-3, "a slab needs a shape: 'boundary' as a list of [x, y, z] plan points, or 'profile' as the id of a sketch already drawn in plan", "", "slab");
|
|
9890
|
+
}
|
|
9891
|
+
if (hasProfile) {
|
|
9892
|
+
const frame = ctx.planeOfFeature(feature.profile);
|
|
9893
|
+
if (Math.abs(frame.normal[2]) < 1 - 1e-6) {
|
|
9894
|
+
const tilt = Math.acos(Math.min(1, Math.abs(frame.normal[2]))) * DEG;
|
|
9895
|
+
ctx.warn(`'${feature.profile}' lies ${tilt.toFixed(1)} degrees off horizontal. The slab is still pushed straight DOWN from it, so it comes out as a leaning prism rather than a flat floor - which is a fine ramp and a strange floor`);
|
|
9896
|
+
}
|
|
9897
|
+
const solid = k.extrude(doc2, ctx.shapeOf(feature.profile), {
|
|
9898
|
+
direction: [0, 0, -1],
|
|
9899
|
+
distance: thickness,
|
|
9900
|
+
mode: ExtrudeMode.Blind
|
|
9901
|
+
});
|
|
9902
|
+
const dz = topZ - frame.origin[2];
|
|
9903
|
+
return Math.abs(dz) > 1e-9 ? k.transform(doc2, solid, translation(0, 0, dz)) : solid;
|
|
9904
|
+
}
|
|
9905
|
+
const points = planPoints(feature.boundary, ctx, "slab", "boundary");
|
|
9906
|
+
if (points.length < 3) {
|
|
9907
|
+
throw new KernelError(-3, `a slab boundary needs at least three points to enclose a floor; this one has ${points.length}`, "", "slab");
|
|
9908
|
+
}
|
|
9909
|
+
if (Math.abs(signedArea2(points)) < 1e-9) {
|
|
9910
|
+
throw new KernelError(-3, "the slab boundary encloses no area: its points are collinear", "", "slab");
|
|
9911
|
+
}
|
|
9912
|
+
const profile = k.buildSketch(doc2, `${feature.id}__plan`, { origin: [0, 0, topZ], normal: [0, 0, 1], xAxis: [1, 0, 0] }, { curves: polygonCurves(points, 1) });
|
|
9913
|
+
ctx.warn(`slab ${(Math.abs(signedArea2(points)) / 2e6).toFixed(2)} m2 in plan, ${thickness.toFixed(0)} mm thick, top at ${m(topZ)}, soffit at ${m(topZ - thickness)}`);
|
|
9914
|
+
return k.extrude(doc2, profile, {
|
|
9915
|
+
direction: [0, 0, -1],
|
|
9916
|
+
distance: thickness,
|
|
9917
|
+
mode: ExtrudeMode.Blind
|
|
9918
|
+
});
|
|
9919
|
+
}
|
|
9920
|
+
function buildRoof(feature, ctx) {
|
|
9921
|
+
const k = ctx.kernel;
|
|
9922
|
+
const doc2 = ctx.doc;
|
|
9923
|
+
const outline = planPoints(feature.boundary, ctx, "roof", "boundary");
|
|
9924
|
+
if (outline.length < 3) {
|
|
9925
|
+
throw new KernelError(-3, `a roof footprint needs at least three points; this one has ${outline.length}`, "", "roof");
|
|
9926
|
+
}
|
|
9927
|
+
const area2 = signedArea2(outline);
|
|
9928
|
+
if (Math.abs(area2) < 1e-9) {
|
|
9929
|
+
throw new KernelError(-3, "the roof footprint encloses no area: its points are collinear", "", "roof");
|
|
9930
|
+
}
|
|
9931
|
+
const sign = area2 > 0 ? 1 : -1;
|
|
9932
|
+
const base = ctx.number(feature.base, "length");
|
|
9933
|
+
const thickness = ctx.number(feature.thickness, "length");
|
|
9934
|
+
if (!(thickness > 0)) {
|
|
9935
|
+
throw new KernelError(-3, `a roof ${thickness} mm thick has no substance: thickness must be positive`, "", "roof");
|
|
9936
|
+
}
|
|
9937
|
+
const overhang = ctx.number(feature.overhang, "length", 0);
|
|
9938
|
+
if (overhang < 0) {
|
|
9939
|
+
ctx.warn(`the overhang is ${m(overhang)}, which pulls the eave INSIDE the footprint rather than out past it. Built that way; it is an unusual thing to want`);
|
|
9940
|
+
}
|
|
9941
|
+
const defaultPitch = ctx.number(feature.pitch, "angle");
|
|
9942
|
+
const pitches = edgePitches(feature, outline.length, defaultPitch, ctx);
|
|
9943
|
+
if (pitches.every((p) => p === null)) {
|
|
9944
|
+
throw new KernelError(-3, `a roof with no pitched edge has no top: every one of its ${outline.length} edges is a gable, so nothing rises and there is no envelope to intersect. Leave at least one edge without a 'gable' override`, "", "roof");
|
|
9945
|
+
}
|
|
9946
|
+
const eaves = Math.abs(overhang) > 1e-9 ? offsetSide(outline, -sign * overhang, true) : outline.slice();
|
|
9947
|
+
const reflex = reflexCorners(eaves, sign);
|
|
9948
|
+
if (reflex.length > 0) {
|
|
9949
|
+
ctx.warn(`this footprint is not convex - ${reflex.length} re-entrant corner${reflex.length === 1 ? "" : "s"}, at ${reflex.map((i) => `point ${i}`).join(", ")}. Every eave is extended to a full plane, so a wing can be roofed by a plane rising from an eave on the far side of the building. The solid is valid and sits inside the footprint; check the ridge lines are the ones you meant, or build the wings as separate roofs and fuse them`);
|
|
9950
|
+
}
|
|
9951
|
+
let minX = Infinity, maxX = -Infinity, minY = Infinity, maxY = -Infinity;
|
|
9952
|
+
for (const p of eaves) {
|
|
9953
|
+
minX = Math.min(minX, p[0]);
|
|
9954
|
+
maxX = Math.max(maxX, p[0]);
|
|
9955
|
+
minY = Math.min(minY, p[1]);
|
|
9956
|
+
maxY = Math.max(maxY, p[1]);
|
|
9957
|
+
}
|
|
9958
|
+
const span = Math.hypot(maxX - minX, maxY - minY);
|
|
9959
|
+
const steepest = Math.max(...pitches.filter((p) => p !== null));
|
|
9960
|
+
const headroom = span * Math.tan(steepest) + span * 0.1 + 10;
|
|
9961
|
+
const apex = highestPoint(eaves, pitches, sign);
|
|
9962
|
+
ctx.warn(`roof over ${(Math.abs(signedArea2(eaves)) / 2e6).toFixed(2)} m2, ${pitches.filter((p) => p !== null).length} pitched edge(s) and ${pitches.filter((p) => p === null).length} gable(s), default pitch ${(defaultPitch * DEG).toFixed(1)} degrees, ridge about ${m(base + apex)} (${m(apex)} above the eave), ${thickness.toFixed(0)} mm plumb thickness`);
|
|
9963
|
+
const floor = base - thickness - Math.max(1, thickness);
|
|
9964
|
+
const upper = envelopeSolid(feature.id, "top", eaves, pitches, sign, base, floor, headroom, ctx);
|
|
9965
|
+
const lower = envelopeSolid(feature.id, "soffit", eaves, pitches, sign, base - thickness, floor, headroom, ctx);
|
|
9966
|
+
return k.boolean(doc2, BooleanOp.Cut, [upper], [lower]);
|
|
9967
|
+
}
|
|
9968
|
+
function edgePitches(feature, count, fallback, ctx) {
|
|
9969
|
+
const overrides = feature.edges ?? [];
|
|
9970
|
+
if (overrides.length > count) {
|
|
9971
|
+
throw new KernelError(-3, `'edges' has ${overrides.length} entries and the footprint has ${count} edges. Entry i is the edge from boundary point i to point i+1`, "", "roof");
|
|
9972
|
+
}
|
|
9973
|
+
const check = (pitch, where) => {
|
|
9974
|
+
if (!(pitch > 0)) {
|
|
9975
|
+
throw new KernelError(-3, `${where} is ${(pitch * DEG).toFixed(1)} degrees, so it does not rise and contributes no plane to intersect. A flat roof is a slab - use 'slab' - and 'gable' is the way to say an end stands vertical`, "", "roof");
|
|
9976
|
+
}
|
|
9977
|
+
if (pitch >= MAX_PITCH) {
|
|
9978
|
+
throw new KernelError(-3, `${where} is ${(pitch * DEG).toFixed(1)} degrees. At that angle the plane climbs ${Math.tan(pitch).toFixed(1)} m for every metre inboard, which is a wall rather than a roof and needs a bounding prism kilometres tall. The limit is ${(MAX_PITCH * DEG).toFixed(0)} degrees; over about 60 it is a mansard, which is two roofs`, "", "roof");
|
|
9979
|
+
}
|
|
9980
|
+
if (pitch > STEEP_PITCH) {
|
|
9981
|
+
ctx.warn(`${where} is ${(pitch * DEG).toFixed(1)} degrees, steeper than a roof usually goes - that is mansard or spire territory. Built as asked`);
|
|
9982
|
+
} else if (pitch < SHALLOW_PITCH) {
|
|
9983
|
+
ctx.warn(`${where} is ${(pitch * DEG).toFixed(2)} degrees, which reads as flat: it rises ${(1e3 * Math.tan(pitch)).toFixed(0)} mm per metre. If you meant flat, 'slab' is the feature. Built as asked`);
|
|
9984
|
+
}
|
|
9985
|
+
return pitch;
|
|
9986
|
+
};
|
|
9987
|
+
const fallbackChecked = check(fallback, "the roof's 'pitch'");
|
|
9988
|
+
return Array.from({ length: count }, (_, i) => {
|
|
9989
|
+
const entry = overrides[i];
|
|
9990
|
+
if (entry === void 0 || entry === "")
|
|
9991
|
+
return fallbackChecked;
|
|
9992
|
+
const word = String(entry).trim().toLowerCase();
|
|
9993
|
+
if (word === "gable")
|
|
9994
|
+
return null;
|
|
9995
|
+
if (word === "pitch" || word === "pitched")
|
|
9996
|
+
return fallbackChecked;
|
|
9997
|
+
return check(ctx.number(entry, "angle"), `edge ${i}`);
|
|
9998
|
+
});
|
|
9999
|
+
}
|
|
10000
|
+
function reflexCorners(loop, sign) {
|
|
10001
|
+
const out = [];
|
|
10002
|
+
for (let i = 0; i < loop.length; i++) {
|
|
10003
|
+
const prev = loop[(i - 1 + loop.length) % loop.length];
|
|
10004
|
+
const here = loop[i];
|
|
10005
|
+
const next = loop[(i + 1) % loop.length];
|
|
10006
|
+
const cross3 = (here[0] - prev[0]) * (next[1] - here[1]) - (here[1] - prev[1]) * (next[0] - here[0]);
|
|
10007
|
+
if (sign * cross3 < -1e-9)
|
|
10008
|
+
out.push(i);
|
|
10009
|
+
}
|
|
10010
|
+
return out;
|
|
10011
|
+
}
|
|
10012
|
+
function highestPoint(loop, pitches, sign) {
|
|
10013
|
+
let best = 0;
|
|
10014
|
+
for (let i = 0; i < loop.length; i++) {
|
|
10015
|
+
for (let j = i + 1; j < loop.length; j++) {
|
|
10016
|
+
const p = [(loop[i][0] + loop[j][0]) / 2, (loop[i][1] + loop[j][1]) / 2];
|
|
10017
|
+
const z = roofHeight(loop, pitches, p, 0, sign);
|
|
10018
|
+
if (z !== null && z > best)
|
|
10019
|
+
best = z;
|
|
10020
|
+
}
|
|
10021
|
+
}
|
|
10022
|
+
return best;
|
|
10023
|
+
}
|
|
10024
|
+
function envelopeSolid(featureId, tag, loop, pitches, sign, base, floor, headroom, ctx) {
|
|
10025
|
+
const k = ctx.kernel;
|
|
10026
|
+
const doc2 = ctx.doc;
|
|
10027
|
+
const prismProfile = k.buildSketch(doc2, `${featureId}__${tag}_plan`, { origin: [0, 0, floor], normal: [0, 0, 1], xAxis: [1, 0, 0] }, { curves: polygonCurves(loop, 1) });
|
|
10028
|
+
let solid = k.extrude(doc2, prismProfile, {
|
|
10029
|
+
direction: [0, 0, 1],
|
|
10030
|
+
distance: base - floor + headroom,
|
|
10031
|
+
mode: ExtrudeMode.Blind
|
|
10032
|
+
});
|
|
10033
|
+
let reach = 0;
|
|
10034
|
+
for (const p of loop)
|
|
10035
|
+
reach = Math.max(reach, Math.hypot(p[0], p[1]));
|
|
10036
|
+
for (let i = 0; i < loop.length; i++) {
|
|
10037
|
+
const pitch = pitches[i];
|
|
10038
|
+
if (pitch === null || pitch === void 0)
|
|
10039
|
+
continue;
|
|
10040
|
+
const a = loop[i];
|
|
10041
|
+
const b = loop[(i + 1) % loop.length];
|
|
10042
|
+
const dir = unit(a, b);
|
|
10043
|
+
if (dir === null) {
|
|
10044
|
+
throw new KernelError(-3, `the roof footprint repeats point ${i}, so that edge has no direction`, "", "roof");
|
|
10045
|
+
}
|
|
10046
|
+
const inward = [sign * -dir[1], sign * dir[0]];
|
|
10047
|
+
const half = 3 * (reach + headroom) + 1e3;
|
|
10048
|
+
const rise = half * Math.tan(pitch);
|
|
10049
|
+
const drop = rise + half;
|
|
10050
|
+
const wedge = k.buildSketch(doc2, `${featureId}__${tag}_eave${i}`, {
|
|
10051
|
+
origin: [a[0], a[1], base],
|
|
10052
|
+
normal: [dir[0], dir[1], 0],
|
|
10053
|
+
xAxis: [inward[0], inward[1], 0]
|
|
10054
|
+
}, {
|
|
10055
|
+
curves: [
|
|
10056
|
+
{ kind: "line", id: 1, from: [-half, -drop], to: [half, -drop] },
|
|
10057
|
+
{ kind: "line", id: 2, from: [half, -drop], to: [half, rise] },
|
|
10058
|
+
{ kind: "line", id: 3, from: [half, rise], to: [-half, -rise] },
|
|
10059
|
+
{ kind: "line", id: 4, from: [-half, -rise], to: [-half, -drop] }
|
|
10060
|
+
]
|
|
10061
|
+
});
|
|
10062
|
+
const slab = k.extrude(doc2, wedge, {
|
|
10063
|
+
direction: [dir[0], dir[1], 0],
|
|
10064
|
+
distance: 2 * half,
|
|
10065
|
+
mode: ExtrudeMode.Symmetric
|
|
10066
|
+
});
|
|
10067
|
+
solid = k.boolean(doc2, BooleanOp.Common, [solid], [slab]);
|
|
10068
|
+
}
|
|
10069
|
+
return solid;
|
|
10070
|
+
}
|
|
10071
|
+
function elevationOfDatum(id, box) {
|
|
10072
|
+
const low = box.min[2] ?? 0;
|
|
10073
|
+
const high = box.max[2] ?? 0;
|
|
10074
|
+
if (high - low > 1e-6) {
|
|
10075
|
+
throw new Error(`level('${id}') reads the elevation of a level, and '${id}' is ${(high - low).toFixed(1)} mm tall - so it is a body, not an elevation. maxz() and minz() measure a body; level() names a storey datum`);
|
|
10076
|
+
}
|
|
10077
|
+
return low;
|
|
10078
|
+
}
|
|
10079
|
+
|
|
9545
10080
|
// ../doc/dist/expr.js
|
|
9546
10081
|
var ExpressionError = class extends Error {
|
|
9547
10082
|
expression;
|
|
@@ -9597,7 +10132,7 @@ function tokenize(src) {
|
|
|
9597
10132
|
if (!Number.isFinite(value)) {
|
|
9598
10133
|
throw new ExpressionError(`'${text}' is not a number`, src, start);
|
|
9599
10134
|
}
|
|
9600
|
-
let
|
|
10135
|
+
let unit4;
|
|
9601
10136
|
let j = i;
|
|
9602
10137
|
while (j < src.length && /\s/.test(src[j]))
|
|
9603
10138
|
j++;
|
|
@@ -9606,10 +10141,10 @@ function tokenize(src) {
|
|
|
9606
10141
|
j++;
|
|
9607
10142
|
const word = src.slice(unitStart, j);
|
|
9608
10143
|
if (word && Object.hasOwn(UNITS, word)) {
|
|
9609
|
-
|
|
10144
|
+
unit4 = word;
|
|
9610
10145
|
i = j;
|
|
9611
10146
|
}
|
|
9612
|
-
tokens.push(
|
|
10147
|
+
tokens.push(unit4 === void 0 ? { t: "num", v: value, at: start } : { t: "num", v: value, unit: unit4, at: start });
|
|
9613
10148
|
continue;
|
|
9614
10149
|
}
|
|
9615
10150
|
if (ch === "'" || ch === '"') {
|
|
@@ -9729,6 +10264,25 @@ var GEOMETRY = {
|
|
|
9729
10264
|
return from + d;
|
|
9730
10265
|
}
|
|
9731
10266
|
},
|
|
10267
|
+
/* A storey height, by name.
|
|
10268
|
+
*
|
|
10269
|
+
* `level('first')` is the whole reason the `level` feature exists: a house
|
|
10270
|
+
* built by hand declared `ffl`, `lvl1`, `deck` and `roof_z` as parameters and
|
|
10271
|
+
* typed every z in the model against one of them, which is fine until the
|
|
10272
|
+
* first floor moves and somebody has to work out which of a hundred numbers
|
|
10273
|
+
* were `lvl1` and which merely happened to equal it.
|
|
10274
|
+
*
|
|
10275
|
+
* It reads the datum face the level is realised as, which is why it is
|
|
10276
|
+
* `minz` with a name that says what it means rather than a second lookup
|
|
10277
|
+
* table beside the feature list - and why it can insist the thing it was
|
|
10278
|
+
* pointed at is FLAT. A level is flat by construction; a body is not, and
|
|
10279
|
+
* being handed one is a caller who meant `maxz`. */
|
|
10280
|
+
level: {
|
|
10281
|
+
bodies: 1,
|
|
10282
|
+
arity: 1,
|
|
10283
|
+
result: "length",
|
|
10284
|
+
fn: (g, i) => elevationOfDatum(i[0], g.bounds(i[0]))
|
|
10285
|
+
},
|
|
9732
10286
|
/** The general form: distance from a point along a direction to the surface. */
|
|
9733
10287
|
ray: {
|
|
9734
10288
|
bodies: 1,
|
|
@@ -11051,25 +11605,25 @@ function moments(values, t, periodic, ends) {
|
|
|
11051
11605
|
const h = Array.from({ length: n }, (_, i) => t[i + 1] - t[i]);
|
|
11052
11606
|
const slope = (i) => (values[i + 1] - values[i]) / h[i];
|
|
11053
11607
|
if (periodic) {
|
|
11054
|
-
const
|
|
11055
|
-
const a2 = Array.from({ length:
|
|
11056
|
-
const rhs2 = new Array(
|
|
11057
|
-
for (let i = 0; i <
|
|
11058
|
-
const prev = (i - 1 +
|
|
11608
|
+
const m3 = n;
|
|
11609
|
+
const a2 = Array.from({ length: m3 }, () => new Array(m3).fill(0));
|
|
11610
|
+
const rhs2 = new Array(m3).fill(0);
|
|
11611
|
+
for (let i = 0; i < m3; i++) {
|
|
11612
|
+
const prev = (i - 1 + m3) % m3;
|
|
11059
11613
|
const hPrev = h[prev];
|
|
11060
11614
|
const hCur = h[i];
|
|
11061
11615
|
a2[i][prev] += hPrev;
|
|
11062
11616
|
a2[i][i] += 2 * (hPrev + hCur);
|
|
11063
|
-
a2[i][(i + 1) %
|
|
11617
|
+
a2[i][(i + 1) % m3] += hCur;
|
|
11064
11618
|
const slopePrev = (values[i] - values[prev]) / hPrev;
|
|
11065
11619
|
rhs2[i] = 6 * (slope(i) - slopePrev);
|
|
11066
11620
|
}
|
|
11067
11621
|
const solved = solveDense(a2, rhs2);
|
|
11068
11622
|
return [...solved, solved[0]];
|
|
11069
11623
|
}
|
|
11070
|
-
const
|
|
11071
|
-
const a = Array.from({ length:
|
|
11072
|
-
const rhs = new Array(
|
|
11624
|
+
const m2 = n + 1;
|
|
11625
|
+
const a = Array.from({ length: m2 }, () => new Array(m2).fill(0));
|
|
11626
|
+
const rhs = new Array(m2).fill(0);
|
|
11073
11627
|
for (let i = 1; i < n; i++) {
|
|
11074
11628
|
a[i][i - 1] = h[i - 1];
|
|
11075
11629
|
a[i][i] = 2 * (h[i - 1] + h[i]);
|
|
@@ -11078,7 +11632,7 @@ function moments(values, t, periodic, ends) {
|
|
|
11078
11632
|
}
|
|
11079
11633
|
if (n === 1) {
|
|
11080
11634
|
a[0][0] = 1;
|
|
11081
|
-
a[
|
|
11635
|
+
a[m2 - 1][m2 - 1] = 1;
|
|
11082
11636
|
return solveDense(a, rhs);
|
|
11083
11637
|
}
|
|
11084
11638
|
if (ends === "parabolic") {
|
|
@@ -11113,14 +11667,14 @@ function interpolate(points, options = {}) {
|
|
|
11113
11667
|
const ends = options.ends ?? "notAKnot";
|
|
11114
11668
|
const mx = moments(xs, t, periodic, ends);
|
|
11115
11669
|
const my = moments(ys, t, periodic, ends);
|
|
11116
|
-
const evalOne = (values,
|
|
11670
|
+
const evalOne = (values, m2, u) => {
|
|
11117
11671
|
let i = 0;
|
|
11118
11672
|
while (i < t.length - 2 && u > t[i + 1])
|
|
11119
11673
|
i++;
|
|
11120
11674
|
const h = t[i + 1] - t[i];
|
|
11121
11675
|
const A = (t[i + 1] - u) / h;
|
|
11122
11676
|
const B = 1 - A;
|
|
11123
|
-
return A * values[i] + B * values[i + 1] + ((A * A * A - A) *
|
|
11677
|
+
return A * values[i] + B * values[i + 1] + ((A * A * A - A) * m2[i] + (B * B * B - B) * m2[i + 1]) * (h * h) / 6;
|
|
11124
11678
|
};
|
|
11125
11679
|
return {
|
|
11126
11680
|
knots: t.map((v) => v / span),
|
|
@@ -11286,7 +11840,7 @@ function hermiteAsBspline(points, tangents) {
|
|
|
11286
11840
|
}
|
|
11287
11841
|
|
|
11288
11842
|
// ../sketch/dist/prepare.js
|
|
11289
|
-
var
|
|
11843
|
+
var DEG2 = Math.PI / 180;
|
|
11290
11844
|
var Mode;
|
|
11291
11845
|
(function(Mode2) {
|
|
11292
11846
|
Mode2[Mode2["PointPoint"] = 0] = "PointPoint";
|
|
@@ -11521,7 +12075,7 @@ function resolveValue(value, dimension, options, fail) {
|
|
|
11521
12075
|
fail(`dimension ${value} is not a finite number`);
|
|
11522
12076
|
return 0;
|
|
11523
12077
|
}
|
|
11524
|
-
return dimension === "angle" ? value *
|
|
12078
|
+
return dimension === "angle" ? value * DEG2 : value;
|
|
11525
12079
|
}
|
|
11526
12080
|
if (!options.resolve) {
|
|
11527
12081
|
fail(`expression "${value}" needs a resolve callback`);
|
|
@@ -11532,13 +12086,13 @@ function resolveValue(value, dimension, options, fail) {
|
|
|
11532
12086
|
fail(`expression "${value}" resolved to ${String(n)}`);
|
|
11533
12087
|
return 0;
|
|
11534
12088
|
}
|
|
11535
|
-
return dimension === "angle" ? n *
|
|
12089
|
+
return dimension === "angle" ? n * DEG2 : n;
|
|
11536
12090
|
}
|
|
11537
12091
|
function prepare(sketch, options) {
|
|
11538
12092
|
const errors = [];
|
|
11539
|
-
const fail = (
|
|
11540
|
-
if (!errors.includes(
|
|
11541
|
-
errors.push(
|
|
12093
|
+
const fail = (m2) => {
|
|
12094
|
+
if (!errors.includes(m2))
|
|
12095
|
+
errors.push(m2);
|
|
11542
12096
|
};
|
|
11543
12097
|
const byId = /* @__PURE__ */ new Map();
|
|
11544
12098
|
for (const e of sketch.entities) {
|
|
@@ -11574,13 +12128,13 @@ function prepare(sketch, options) {
|
|
|
11574
12128
|
break;
|
|
11575
12129
|
case "arc":
|
|
11576
12130
|
addSlot(e.id, "r", e.radius);
|
|
11577
|
-
addSlot(e.id, "a0", e.startAngle *
|
|
11578
|
-
addSlot(e.id, "a1", e.endAngle *
|
|
12131
|
+
addSlot(e.id, "a0", e.startAngle * DEG2);
|
|
12132
|
+
addSlot(e.id, "a1", e.endAngle * DEG2);
|
|
11579
12133
|
break;
|
|
11580
12134
|
case "ellipse":
|
|
11581
12135
|
addSlot(e.id, "rx", e.majorRadius);
|
|
11582
12136
|
addSlot(e.id, "ry", e.minorRadius);
|
|
11583
|
-
addSlot(e.id, "rot", e.rotation *
|
|
12137
|
+
addSlot(e.id, "rot", e.rotation * DEG2);
|
|
11584
12138
|
break;
|
|
11585
12139
|
case "line":
|
|
11586
12140
|
case "spline":
|
|
@@ -12661,9 +13215,9 @@ function splineRows(row0, row1, res, sys, off, curve, p, slot) {
|
|
|
12661
13215
|
const degree = curve.form === "bezier" ? points.length - 1 : Math.max(1, Math.min(curve.degree, points.length - 1));
|
|
12662
13216
|
const knots = uniformKnots(points.length, degree, curve.closed && curve.form === "bspline");
|
|
12663
13217
|
for (let j = 0; j < points.length; j++) {
|
|
12664
|
-
const
|
|
13218
|
+
const unit4 = points.map((_, k) => k === j ? [1, 0] : [0, 0]);
|
|
12665
13219
|
const w = bsplinePointAt({
|
|
12666
|
-
poles:
|
|
13220
|
+
poles: unit4,
|
|
12667
13221
|
degree,
|
|
12668
13222
|
knots: knots.knots,
|
|
12669
13223
|
multiplicities: knots.multiplicities
|
|
@@ -12678,7 +13232,7 @@ function splineRows(row0, row1, res, sys, off, curve, p, slot) {
|
|
|
12678
13232
|
}
|
|
12679
13233
|
|
|
12680
13234
|
// ../sketch/dist/solver.js
|
|
12681
|
-
var
|
|
13235
|
+
var DEG3 = Math.PI / 180;
|
|
12682
13236
|
function evaluateCluster(sys, cl) {
|
|
12683
13237
|
let sum = 0;
|
|
12684
13238
|
for (const c of cl.constraints) {
|
|
@@ -12697,13 +13251,13 @@ function evaluateCluster(sys, cl) {
|
|
|
12697
13251
|
return sum;
|
|
12698
13252
|
}
|
|
12699
13253
|
function maxResidual(cl) {
|
|
12700
|
-
let
|
|
13254
|
+
let m2 = 0;
|
|
12701
13255
|
for (let i = 0; i < cl.residuals.length; i++) {
|
|
12702
13256
|
const a = Math.abs(cl.residuals[i]);
|
|
12703
|
-
if (a >
|
|
12704
|
-
|
|
13257
|
+
if (a > m2)
|
|
13258
|
+
m2 = a;
|
|
12705
13259
|
}
|
|
12706
|
-
return
|
|
13260
|
+
return m2;
|
|
12707
13261
|
}
|
|
12708
13262
|
function solveCluster(sys, cl, tolerance, maxIterations) {
|
|
12709
13263
|
let cost = evaluateCluster(sys, cl);
|
|
@@ -12939,15 +13493,15 @@ function readBack(sys) {
|
|
|
12939
13493
|
return {
|
|
12940
13494
|
...e,
|
|
12941
13495
|
radius: sys.x[slots[0]],
|
|
12942
|
-
startAngle: sys.x[slots[1]] /
|
|
12943
|
-
endAngle: sys.x[slots[2]] /
|
|
13496
|
+
startAngle: sys.x[slots[1]] / DEG3,
|
|
13497
|
+
endAngle: sys.x[slots[2]] / DEG3
|
|
12944
13498
|
};
|
|
12945
13499
|
case "ellipse":
|
|
12946
13500
|
return {
|
|
12947
13501
|
...e,
|
|
12948
13502
|
majorRadius: sys.x[slots[0]],
|
|
12949
13503
|
minorRadius: sys.x[slots[1]],
|
|
12950
|
-
rotation: sys.x[slots[2]] /
|
|
13504
|
+
rotation: sys.x[slots[2]] / DEG3
|
|
12951
13505
|
};
|
|
12952
13506
|
case "line":
|
|
12953
13507
|
case "spline":
|
|
@@ -13051,7 +13605,7 @@ function now() {
|
|
|
13051
13605
|
var TAU = Math.PI * 2;
|
|
13052
13606
|
|
|
13053
13607
|
// ../doc/dist/curvemath.js
|
|
13054
|
-
var
|
|
13608
|
+
var DEG4 = Math.PI / 180;
|
|
13055
13609
|
function conicAsBspline(id, start, apex, end, rho, construction = false) {
|
|
13056
13610
|
const r = Math.min(0.999, Math.max(1e-3, rho));
|
|
13057
13611
|
return {
|
|
@@ -13066,14 +13620,14 @@ function conicAsBspline(id, start, apex, end, rho, construction = false) {
|
|
|
13066
13620
|
};
|
|
13067
13621
|
}
|
|
13068
13622
|
function ellipseAsBspline(id, center, radiusX, radiusY, rotation, startAngle = 0, endAngle = 0, construction = false) {
|
|
13069
|
-
const from = startAngle *
|
|
13070
|
-
let sweep = (endAngle - startAngle) *
|
|
13623
|
+
const from = startAngle * DEG4;
|
|
13624
|
+
let sweep = (endAngle - startAngle) * DEG4;
|
|
13071
13625
|
if (Math.abs(sweep) < 1e-12)
|
|
13072
13626
|
sweep = Math.PI * 2;
|
|
13073
13627
|
const pieces = Math.max(1, Math.ceil(Math.abs(sweep) / (Math.PI / 2) - 1e-9));
|
|
13074
13628
|
const step = sweep / pieces;
|
|
13075
|
-
const cr = Math.cos(rotation *
|
|
13076
|
-
const sr = Math.sin(rotation *
|
|
13629
|
+
const cr = Math.cos(rotation * DEG4);
|
|
13630
|
+
const sr = Math.sin(rotation * DEG4);
|
|
13077
13631
|
const place = (x, y) => {
|
|
13078
13632
|
const px = x * radiusX;
|
|
13079
13633
|
const py = y * radiusY;
|
|
@@ -13086,13 +13640,13 @@ function ellipseAsBspline(id, center, radiusX, radiusY, rotation, startAngle = 0
|
|
|
13086
13640
|
for (let i = 0; i < pieces; i++) {
|
|
13087
13641
|
const a = from + i * step;
|
|
13088
13642
|
const b = a + step;
|
|
13089
|
-
const
|
|
13643
|
+
const m2 = (a + b) / 2;
|
|
13090
13644
|
const w = Math.cos(step / 2);
|
|
13091
13645
|
if (i === 0) {
|
|
13092
13646
|
poles.push(place(Math.cos(a), Math.sin(a)));
|
|
13093
13647
|
weights.push(1);
|
|
13094
13648
|
}
|
|
13095
|
-
poles.push(place(Math.cos(
|
|
13649
|
+
poles.push(place(Math.cos(m2) / w, Math.sin(m2) / w));
|
|
13096
13650
|
weights.push(w);
|
|
13097
13651
|
poles.push(place(Math.cos(b), Math.sin(b)));
|
|
13098
13652
|
weights.push(1);
|
|
@@ -13125,13 +13679,13 @@ function tangentSplineAsBspline(id, points, startTangent, endTangent, constructi
|
|
|
13125
13679
|
const total = t[n - 1];
|
|
13126
13680
|
for (let i = 0; i < n; i++)
|
|
13127
13681
|
t[i] = t[i] / total;
|
|
13128
|
-
const
|
|
13682
|
+
const unit4 = (v) => {
|
|
13129
13683
|
const l = Math.hypot(v[0], v[1]);
|
|
13130
13684
|
return l < 1e-12 ? [1, 0] : [v[0] / l, v[1] / l];
|
|
13131
13685
|
};
|
|
13132
13686
|
const bessel = (atStart) => {
|
|
13133
13687
|
if (n === 2)
|
|
13134
|
-
return
|
|
13688
|
+
return unit4([points[1][0] - points[0][0], points[1][1] - points[0][1]]);
|
|
13135
13689
|
const [i0, i1, i2] = atStart ? [0, 1, 2] : [n - 1, n - 2, n - 3];
|
|
13136
13690
|
const h1 = t[i1] - t[i0];
|
|
13137
13691
|
const h2 = t[i2] - t[i1];
|
|
@@ -13143,10 +13697,10 @@ function tangentSplineAsBspline(id, points, startTangent, endTangent, constructi
|
|
|
13143
13697
|
const s2 = (q2 - q1) / h2;
|
|
13144
13698
|
return s1 + (s1 - s2) * h1 / (h1 + h2);
|
|
13145
13699
|
};
|
|
13146
|
-
return
|
|
13700
|
+
return unit4([d(0), d(1)]);
|
|
13147
13701
|
};
|
|
13148
|
-
const d0 = startTangent === null ? bessel(true) :
|
|
13149
|
-
const d1 = endTangent === null ? bessel(false) :
|
|
13702
|
+
const d0 = startTangent === null ? bessel(true) : unit4(startTangent);
|
|
13703
|
+
const d1 = endTangent === null ? bessel(false) : unit4(endTangent);
|
|
13150
13704
|
const D0 = [d0[0] * total, d0[1] * total];
|
|
13151
13705
|
const D1 = [d1[0] * total, d1[1] * total];
|
|
13152
13706
|
const flat = [0, 0, 0, 0, ...t.slice(1, n - 1), 1, 1, 1, 1];
|
|
@@ -13433,15 +13987,15 @@ function threadForm(spec, fallbackDiameter) {
|
|
|
13433
13987
|
let pitch = spec.pitch ?? 0;
|
|
13434
13988
|
let designation = spec.designation ?? "";
|
|
13435
13989
|
if (spec.designation) {
|
|
13436
|
-
const
|
|
13437
|
-
if (!
|
|
13990
|
+
const m2 = /^\s*M\s*([\d.]+)\s*(?:[xX*]\s*([\d.]+))?\s*$/.exec(spec.designation);
|
|
13991
|
+
if (!m2) {
|
|
13438
13992
|
throw new Error(`'${spec.designation}' is not a metric thread designation. Use M10 for coarse or M10x1.25 for fine.`);
|
|
13439
13993
|
}
|
|
13440
|
-
const nominal = Number(
|
|
13994
|
+
const nominal = Number(m2[1]);
|
|
13441
13995
|
if (spec.majorDiameter === void 0)
|
|
13442
13996
|
major = nominal;
|
|
13443
13997
|
if (spec.pitch === void 0) {
|
|
13444
|
-
const fine =
|
|
13998
|
+
const fine = m2[2] ? Number(m2[2]) : void 0;
|
|
13445
13999
|
const coarse = COARSE[`M${nominal}`];
|
|
13446
14000
|
if (fine === void 0 && coarse === void 0) {
|
|
13447
14001
|
throw new Error(`M${nominal} has no coarse pitch in ISO 261 - give the pitch, as in M${nominal}x1.5.`);
|
|
@@ -13618,23 +14172,23 @@ var round2 = (n) => Number(n.toFixed(4)).toString();
|
|
|
13618
14172
|
var sub = (a, b) => [a[0] - b[0], a[1] - b[1], a[2] - b[2]];
|
|
13619
14173
|
var dot2 = (a, b) => a[0] * b[0] + a[1] * b[1] + a[2] * b[2];
|
|
13620
14174
|
var cross = (a, b) => [a[1] * b[2] - a[2] * b[1], a[2] * b[0] - a[0] * b[2], a[0] * b[1] - a[1] * b[0]];
|
|
13621
|
-
function
|
|
14175
|
+
function unit2(a) {
|
|
13622
14176
|
const l = Math.hypot(a[0], a[1], a[2]);
|
|
13623
14177
|
return l > 1e-12 ? [a[0] / l, a[1] / l, a[2] / l] : [1, 0, 0];
|
|
13624
14178
|
}
|
|
13625
14179
|
function frameOf(origin, normal, xAxis) {
|
|
13626
|
-
const n =
|
|
14180
|
+
const n = unit2(normal);
|
|
13627
14181
|
if (xAxis) {
|
|
13628
|
-
const x =
|
|
14182
|
+
const x = unit2(xAxis);
|
|
13629
14183
|
const inPlane = sub(x, [n[0] * dot2(x, n), n[1] * dot2(x, n), n[2] * dot2(x, n)]);
|
|
13630
|
-
return { origin, normal: n, xAxis:
|
|
14184
|
+
return { origin, normal: n, xAxis: unit2(inPlane) };
|
|
13631
14185
|
}
|
|
13632
14186
|
const seed = Math.abs(dot2([1, 0, 0], n)) > 0.9 ? [0, 1, 0] : [1, 0, 0];
|
|
13633
14187
|
const d = dot2(seed, n);
|
|
13634
14188
|
return {
|
|
13635
14189
|
origin,
|
|
13636
14190
|
normal: n,
|
|
13637
|
-
xAxis:
|
|
14191
|
+
xAxis: unit2(sub(seed, [n[0] * d, n[1] * d, n[2] * d]))
|
|
13638
14192
|
};
|
|
13639
14193
|
}
|
|
13640
14194
|
var project = (frame, p) => {
|
|
@@ -13655,7 +14209,7 @@ function projectEdges(topology, names, frame, mesh) {
|
|
|
13655
14209
|
curves.push({ kind: "line", id, from: project(frame, edge.start), to: project(frame, edge.end) });
|
|
13656
14210
|
continue;
|
|
13657
14211
|
}
|
|
13658
|
-
const square = Math.abs(Math.abs(dot2(
|
|
14212
|
+
const square = Math.abs(Math.abs(dot2(unit2(edge.axisDir), frame.normal)) - 1) < 1e-7;
|
|
13659
14213
|
if (edge.curveType === CurveType.Circle && square && edge.radius > 1e-9) {
|
|
13660
14214
|
const centre = project(frame, edge.axisOrigin);
|
|
13661
14215
|
if (edge.closed) {
|
|
@@ -14187,6 +14741,17 @@ function inputsOfUnsafe(feature) {
|
|
|
14187
14741
|
inputs.push(faceBody);
|
|
14188
14742
|
return inputs;
|
|
14189
14743
|
}
|
|
14744
|
+
/* An opening CUTS its host, so the host is a consumed input and not merely
|
|
14745
|
+
* something it reads. Left out, there is no edge in the graph: the opening
|
|
14746
|
+
* builds before the wall it is cutting, the wall can be deleted out from
|
|
14747
|
+
* under it, and an edit to the wall does not mark the opening dirty. All
|
|
14748
|
+
* three are silent. */
|
|
14749
|
+
case "opening":
|
|
14750
|
+
return typeof feature.host === "string" ? [feature.host] : [];
|
|
14751
|
+
// A slab drawn on a sketch consumes that sketch the way an extrude does.
|
|
14752
|
+
// A slab given plan points consumes nothing.
|
|
14753
|
+
case "slab":
|
|
14754
|
+
return typeof feature.profile === "string" ? [feature.profile] : [];
|
|
14190
14755
|
default:
|
|
14191
14756
|
return [];
|
|
14192
14757
|
}
|
|
@@ -14252,6 +14817,15 @@ function replaceInputUnsafe(feature, from, to) {
|
|
|
14252
14817
|
return null;
|
|
14253
14818
|
return { ...feature, targets: targets ?? feature.targets, tools: tools ?? feature.tools };
|
|
14254
14819
|
}
|
|
14820
|
+
/* Both are re-pointed for the same reason every other consumer is: insert a
|
|
14821
|
+
* feature between a wall and the opening cut into it and the opening has to
|
|
14822
|
+
* follow, or the insertion silently does nothing and two bodies come out
|
|
14823
|
+
* where one was meant. The opening still finds the wall's PATH by walking
|
|
14824
|
+
* back through whatever it now sits on. */
|
|
14825
|
+
case "opening":
|
|
14826
|
+
return feature.host === from ? { ...feature, host: to } : null;
|
|
14827
|
+
case "slab":
|
|
14828
|
+
return feature.profile === from ? { ...feature, profile: to } : null;
|
|
14255
14829
|
default:
|
|
14256
14830
|
return null;
|
|
14257
14831
|
}
|
|
@@ -14295,6 +14869,15 @@ function consumesInput(feature) {
|
|
|
14295
14869
|
case "insertIsoparm":
|
|
14296
14870
|
case "movePoles":
|
|
14297
14871
|
return true;
|
|
14872
|
+
/* An opening IS a cut: it hands back the wall with a hole in it, not a
|
|
14873
|
+
* second body beside it. Left out of this list the wall would go on being
|
|
14874
|
+
* drawn, exported and measured alongside the version with the window in it,
|
|
14875
|
+
* two solids in the same space - which is the z-fighting the comment above
|
|
14876
|
+
* describes, at building scale. `level` is not here for the opposite
|
|
14877
|
+
* reason: it consumes nothing and produces no body a person moves, so
|
|
14878
|
+
* neither half of this question applies to it. */
|
|
14879
|
+
case "opening":
|
|
14880
|
+
return true;
|
|
14298
14881
|
// A frozen body replaces the one it stores: leaving both would show the
|
|
14299
14882
|
// same part twice and re-solve the half that was frozen to avoid solving.
|
|
14300
14883
|
case "freeze":
|
|
@@ -15010,6 +15593,19 @@ function executeOne(feature, ctx) {
|
|
|
15010
15593
|
return buildHole(feature, ctx);
|
|
15011
15594
|
case "sprocket":
|
|
15012
15595
|
return buildSprocket(feature, ctx);
|
|
15596
|
+
/* The architectural family. Each returns ordinary B-rep geometry -
|
|
15597
|
+
* nothing downstream can tell a wall from an extrude - so they live in
|
|
15598
|
+
* arch.ts and arrive here as five ordinary cases. */
|
|
15599
|
+
case "level":
|
|
15600
|
+
return buildLevel(feature, ctx);
|
|
15601
|
+
case "wall":
|
|
15602
|
+
return buildWall(feature, ctx);
|
|
15603
|
+
case "opening":
|
|
15604
|
+
return buildOpening(feature, ctx);
|
|
15605
|
+
case "slab":
|
|
15606
|
+
return buildSlab(feature, ctx);
|
|
15607
|
+
case "roof":
|
|
15608
|
+
return buildRoof(feature, ctx);
|
|
15013
15609
|
case "import":
|
|
15014
15610
|
return feature.format === "obj" || feature.format === "gltf" ? k.importMesh(doc2, feature.format, feature.data) : k.import(doc2, FORMATS[feature.format], feature.data);
|
|
15015
15611
|
case "component":
|
|
@@ -15069,7 +15665,7 @@ function resolveSketchCurves(curves, ctx) {
|
|
|
15069
15665
|
case "ellipse": {
|
|
15070
15666
|
const rotation = c.rotation === void 0 ? 0 : ang(c.rotation, 0);
|
|
15071
15667
|
if (Math.abs(rotation) > 1e-12) {
|
|
15072
|
-
return ellipseAsBspline(c.id, pt(c.center), len(c.radiusX), len(c.radiusY), rotation /
|
|
15668
|
+
return ellipseAsBspline(c.id, pt(c.center), len(c.radiusX), len(c.radiusY), rotation / DEG5, ang(c.startAngle, 0) / DEG5, ang(c.endAngle, 0) / DEG5, c.construction === true);
|
|
15073
15669
|
}
|
|
15074
15670
|
return {
|
|
15075
15671
|
kind: "ellipse",
|
|
@@ -15116,7 +15712,7 @@ function resolveSketchCurves(curves, ctx) {
|
|
|
15116
15712
|
}
|
|
15117
15713
|
});
|
|
15118
15714
|
}
|
|
15119
|
-
var
|
|
15715
|
+
var DEG5 = Math.PI / 180;
|
|
15120
15716
|
function selectionFailure(op, sel) {
|
|
15121
15717
|
return new KernelError(-8, sel.note ?? `${op} selected no geometry`, "selection", op);
|
|
15122
15718
|
}
|
|
@@ -15915,10 +16511,10 @@ function wallPrism(k, doc2, id, frame, radius, angle, size) {
|
|
|
15915
16511
|
t + radius - radius * Math.cos(angle)
|
|
15916
16512
|
];
|
|
15917
16513
|
const d = [Math.cos(angle), Math.sin(angle)];
|
|
15918
|
-
const
|
|
16514
|
+
const m2 = [-Math.sin(angle), Math.cos(angle)];
|
|
15919
16515
|
const at = (alongD, alongM) => [
|
|
15920
|
-
tangent[0] + d[0] * alongD +
|
|
15921
|
-
tangent[1] + d[1] * alongD +
|
|
16516
|
+
tangent[0] + d[0] * alongD + m2[0] * alongM,
|
|
16517
|
+
tangent[1] + d[1] * alongD + m2[1] * alongM
|
|
15922
16518
|
];
|
|
15923
16519
|
const profile = k.buildSketch(doc2, id, sheetPlane(frame, -size), {
|
|
15924
16520
|
curves: [
|
|
@@ -15947,7 +16543,7 @@ function buildFlatPattern(feature, ctx) {
|
|
|
15947
16543
|
const wanted = new Set(feature.bends);
|
|
15948
16544
|
const missing = feature.bends.filter((id) => !recorded.some((b) => b.id === id));
|
|
15949
16545
|
if (missing.length > 0) {
|
|
15950
|
-
throw new KernelError(-8, `no bend called ${missing.map((
|
|
16546
|
+
throw new KernelError(-8, `no bend called ${missing.map((m2) => `'${m2}'`).join(", ")} on '${feature.input}'. Recorded bends: ${recorded.map((b) => b.id).join(", ")}.`, "", "flatPattern");
|
|
15951
16547
|
}
|
|
15952
16548
|
picked = recorded.filter((b) => wanted.has(b.id));
|
|
15953
16549
|
}
|
|
@@ -16085,35 +16681,35 @@ function composeTransforms(a, b) {
|
|
|
16085
16681
|
}
|
|
16086
16682
|
return out;
|
|
16087
16683
|
}
|
|
16088
|
-
function transformPoint(
|
|
16684
|
+
function transformPoint(m2, p) {
|
|
16089
16685
|
return [
|
|
16090
|
-
|
|
16091
|
-
|
|
16092
|
-
|
|
16686
|
+
m2[0] * p[0] + m2[1] * p[1] + m2[2] * p[2] + m2[3],
|
|
16687
|
+
m2[4] * p[0] + m2[5] * p[1] + m2[6] * p[2] + m2[7],
|
|
16688
|
+
m2[8] * p[0] + m2[9] * p[1] + m2[10] * p[2] + m2[11]
|
|
16093
16689
|
];
|
|
16094
16690
|
}
|
|
16095
|
-
function transformDirection(
|
|
16691
|
+
function transformDirection(m2, v) {
|
|
16096
16692
|
return [
|
|
16097
|
-
|
|
16098
|
-
|
|
16099
|
-
|
|
16693
|
+
m2[0] * v[0] + m2[1] * v[1] + m2[2] * v[2],
|
|
16694
|
+
m2[4] * v[0] + m2[5] * v[1] + m2[6] * v[2],
|
|
16695
|
+
m2[8] * v[0] + m2[9] * v[1] + m2[10] * v[2]
|
|
16100
16696
|
];
|
|
16101
16697
|
}
|
|
16102
|
-
function invertTransform(
|
|
16103
|
-
const tx =
|
|
16698
|
+
function invertTransform(m2) {
|
|
16699
|
+
const tx = m2[3], ty = m2[7], tz = m2[11];
|
|
16104
16700
|
return [
|
|
16105
|
-
|
|
16106
|
-
|
|
16107
|
-
|
|
16108
|
-
-(
|
|
16109
|
-
|
|
16110
|
-
|
|
16111
|
-
|
|
16112
|
-
-(
|
|
16113
|
-
|
|
16114
|
-
|
|
16115
|
-
|
|
16116
|
-
-(
|
|
16701
|
+
m2[0],
|
|
16702
|
+
m2[4],
|
|
16703
|
+
m2[8],
|
|
16704
|
+
-(m2[0] * tx + m2[4] * ty + m2[8] * tz),
|
|
16705
|
+
m2[1],
|
|
16706
|
+
m2[5],
|
|
16707
|
+
m2[9],
|
|
16708
|
+
-(m2[1] * tx + m2[5] * ty + m2[9] * tz),
|
|
16709
|
+
m2[2],
|
|
16710
|
+
m2[6],
|
|
16711
|
+
m2[10],
|
|
16712
|
+
-(m2[2] * tx + m2[6] * ty + m2[10] * tz),
|
|
16117
16713
|
0,
|
|
16118
16714
|
0,
|
|
16119
16715
|
0,
|
|
@@ -16174,9 +16770,9 @@ function frameTransform(origin, z, x) {
|
|
|
16174
16770
|
1
|
|
16175
16771
|
];
|
|
16176
16772
|
}
|
|
16177
|
-
function isIdentityTransform(
|
|
16773
|
+
function isIdentityTransform(m2, tolerance = 1e-9) {
|
|
16178
16774
|
const id = identityTransform();
|
|
16179
|
-
return
|
|
16775
|
+
return m2.length === 16 && m2.every((v, i) => Math.abs(v - id[i]) <= tolerance);
|
|
16180
16776
|
}
|
|
16181
16777
|
var dot3 = (a, b) => a[0] * b[0] + a[1] * b[1] + a[2] * b[2];
|
|
16182
16778
|
var scale = (a, s) => [a[0] * s, a[1] * s, a[2] * s];
|
|
@@ -16526,9 +17122,9 @@ function motionTransform(input2, edge) {
|
|
|
16526
17122
|
const hi = max === void 0 ? Infinity : input2.number(max, dimension, 0);
|
|
16527
17123
|
const value = Math.min(Math.max(raw, lo), hi);
|
|
16528
17124
|
if (Math.abs(value - raw) > 1e-12) {
|
|
16529
|
-
const
|
|
17125
|
+
const unit4 = dimension === "angle" ? "deg" : "mm";
|
|
16530
17126
|
const show2 = (v) => (dimension === "angle" ? v * 180 / Math.PI : v).toFixed(3);
|
|
16531
|
-
info.warnings.push(`${field} ${show2(raw)} ${
|
|
17127
|
+
info.warnings.push(`${field} ${show2(raw)} ${unit4} is outside the limits ${show2(lo === -Infinity ? raw : lo)} to ${show2(hi === Infinity ? raw : hi)} ${unit4}; held at ${show2(value)} ${unit4}`);
|
|
16532
17128
|
}
|
|
16533
17129
|
return value;
|
|
16534
17130
|
};
|
|
@@ -17662,6 +18258,102 @@ var FEATURE_SCHEMA = {
|
|
|
17662
18258
|
expr("boreDiameter", "Bore through the middle. Refused if it reaches the tooth roots, which would cut the teeth off as loose bodies.", false, "no bore")
|
|
17663
18259
|
]
|
|
17664
18260
|
},
|
|
18261
|
+
level: {
|
|
18262
|
+
kind: "level",
|
|
18263
|
+
summary: 'A named elevation - "ground", "first", "eaves" - that everything else can be built against. Write level("first") in ANY numeric field and it reads that height, so a storey is one number and the whole building follows it: move the level, rebuild, and every wall base, slab and roof on it moves too. Build these FIRST. The alternative, which is what a house built by hand here actually did, is four parameters called ffl, lvl1, deck and roof_z with every z in the model typed against one of them - and no way afterwards to tell which numbers meant "first floor" from which merely equalled it. Construction geometry: a flat datum square, never a body, never exported.',
|
|
18264
|
+
fields: [
|
|
18265
|
+
expr("elevation", "Height of this level, in document units. 0 is usually the ground floor.", true),
|
|
18266
|
+
expr("size", "Side of the datum square that shows where it is. Display only.", false, "5000 - a five-metre square, because 100 mm is invisible in a building")
|
|
18267
|
+
]
|
|
18268
|
+
},
|
|
18269
|
+
wall: {
|
|
18270
|
+
kind: "wall",
|
|
18271
|
+
summary: 'A wall from a plan centreline: mitred at every corner, extruded between two elevations. A closed path builds outer AND inner faces, so four points and one feature is a room rather than a solid block - which by hand is a sketch, an extrude, a second sketch and a boolean, and comes out with a notch on the inside of every corner because an offset that butts the corners cannot close them. Base and top are ELEVATIONS, not heights, so base: level("ground"), top: level("first") is a storey and stays one when the level moves. This is the feature that turns a building into twenty features instead of a hundred and thirty-seven.',
|
|
18272
|
+
fields: [
|
|
18273
|
+
{
|
|
18274
|
+
name: "path",
|
|
18275
|
+
type: "exprVec3[]",
|
|
18276
|
+
required: true,
|
|
18277
|
+
describe: 'The centreline in plan, as [x, y, z] points. The z of every point is IGNORED - "base" and "top" set the height. Repeating the first point at the end is fine and is dropped.'
|
|
18278
|
+
},
|
|
18279
|
+
{
|
|
18280
|
+
name: "closed",
|
|
18281
|
+
type: "boolean",
|
|
18282
|
+
describe: "Close the path back to its first point. A closed path builds two loops, an outer and an inner, so the wall encloses a room.",
|
|
18283
|
+
default: "false"
|
|
18284
|
+
},
|
|
18285
|
+
expr("base", 'Elevation of the underside. Write level("ground") to name one.', false, "0"),
|
|
18286
|
+
expr("top", 'Elevation of the top - an ELEVATION, not a height. level("first") + 2700, or an absolute number.', true),
|
|
18287
|
+
expr("thickness", "Wall thickness, across the centreline.", true),
|
|
18288
|
+
{
|
|
18289
|
+
name: "justification",
|
|
18290
|
+
type: "enum",
|
|
18291
|
+
values: ["centre", "left", "right"],
|
|
18292
|
+
describe: `Which side of the path the wall lies on. "left" is left of the direction of travel, so the path is then the wall's own face rather than its middle.`,
|
|
18293
|
+
default: "centre"
|
|
18294
|
+
}
|
|
18295
|
+
]
|
|
18296
|
+
},
|
|
18297
|
+
opening: {
|
|
18298
|
+
kind: "opening",
|
|
18299
|
+
summary: `A door or window hole, cut through a wall and positioned ALONG that wall's own centreline. There is no x, y or z to get wrong: "along" is how far up the wall it sits, and the sill is measured from the wall's base, so an opening OUTSIDE its wall is not something this feature can express. That is the point of it - a helper placing eighteen windows by coordinate here mis-parenthesised a depth, put every opening on two of four walls outside the wall, and it was found three scripts later in a render. One feature per opening rather than five (cut box, frame, ring, boolean, glass), and it re-solves: move the wall or the level and every opening in it goes too. For a second opening in the same wall, host it on the FIRST opening - a cut replaces the body it cuts.`,
|
|
18300
|
+
fields: [
|
|
18301
|
+
{
|
|
18302
|
+
name: "host",
|
|
18303
|
+
type: "featureId",
|
|
18304
|
+
required: true,
|
|
18305
|
+
describe: "The wall to cut, or the previous opening in the same wall. The path is always read from the wall at the end of that chain."
|
|
18306
|
+
},
|
|
18307
|
+
expr("along", "Distance from the FIRST point of the host wall's path, measured along its centreline. Refused past the end of the path, or past the corner of the straight run the opening sits on.", true),
|
|
18308
|
+
expr("width", "Width along the wall.", true),
|
|
18309
|
+
expr("height", "Height from the sill to the head.", true),
|
|
18310
|
+
expr("sillHeight", "Height of the sill above the WALL'S BASE. 0 is a doorway; 900 is a typical window.", false, "0 - a doorway"),
|
|
18311
|
+
expr("clearance", "How far the cut runs past each face of the wall, so the boolean does not end exactly on a face.", false, "1 mm, or 2% of the wall thickness if that is more")
|
|
18312
|
+
]
|
|
18313
|
+
},
|
|
18314
|
+
slab: {
|
|
18315
|
+
kind: "slab",
|
|
18316
|
+
summary: 'A floor, terrace or roof deck: a plan shape given a thickness, hung BELOW its level. That is deliberate and it is what makes a level worth having - the level is the finished floor, the surface you stand on and the elevation every wall on that storey takes as its base, with the structure underneath where it is on site. Give "boundary" as plan points or "profile" as a sketch already drawn in plan, not both.',
|
|
18317
|
+
fields: [
|
|
18318
|
+
{
|
|
18319
|
+
name: "boundary",
|
|
18320
|
+
type: "exprVec3[]",
|
|
18321
|
+
describe: 'The outline in plan, as [x, y, z] points; z is ignored. Give this or "profile", not both.',
|
|
18322
|
+
default: 'take the shape from "profile"'
|
|
18323
|
+
},
|
|
18324
|
+
{
|
|
18325
|
+
name: "profile",
|
|
18326
|
+
type: "featureId",
|
|
18327
|
+
describe: 'A sketch already drawn in plan, as an alternative to "boundary". It is moved to the level; its own height does not matter.',
|
|
18328
|
+
default: 'take the shape from "boundary"'
|
|
18329
|
+
},
|
|
18330
|
+
expr("level", 'The finished floor level: the TOP of the slab. Write level("first").', true),
|
|
18331
|
+
expr("thickness", "How deep the slab is.", true),
|
|
18332
|
+
expr("offset", "Moves the top face off the level. Positive is up.", false, "0 - the slab hangs down from the level, so the level is the floor you stand on")
|
|
18333
|
+
]
|
|
18334
|
+
},
|
|
18335
|
+
roof: {
|
|
18336
|
+
kind: "roof",
|
|
18337
|
+
summary: 'A pitched roof over a footprint, solved as the LOWER ENVELOPE of one plane per pitched eave: z = base + the distance in from the nearest eave, times tan(pitch). Hip, gable, shed and pyramid are all the same feature - mark an edge "gable" and it stands vertical instead of pitching. Built by hand as a subtraction ("cut everything above a plane rising from the east eave") a 17.2 degree hip on a 7.2 m wing removed the ENTIRE roof and reported success; an intersection cannot do that, because the result is a subset of every plane by construction, so too shallow a pitch gives a low roof and never an absent one. v1 solves convex footprints exactly. A non-convex one still builds a valid solid inside its footprint and warns that a wing may be roofed by a plane from the far side of the building - check the ridges, or build the wings as separate roofs and fuse them.',
|
|
18338
|
+
fields: [
|
|
18339
|
+
{
|
|
18340
|
+
name: "boundary",
|
|
18341
|
+
type: "exprVec3[]",
|
|
18342
|
+
required: true,
|
|
18343
|
+
describe: 'The footprint in plan, as [x, y, z] points; z is ignored and "base" is the eave height. Either direction round is fine.'
|
|
18344
|
+
},
|
|
18345
|
+
expr("base", 'Elevation of the eave - where the roof starts. level("eaves"), or the top of the wall it sits on.', true),
|
|
18346
|
+
expr("pitch", 'The angle every edge rises at, unless "edges" says otherwise. 30 deg is a common tiled pitch.', true),
|
|
18347
|
+
{
|
|
18348
|
+
name: "edges",
|
|
18349
|
+
type: "string[]",
|
|
18350
|
+
describe: 'Per-edge override. Entry i is the edge from boundary point i to point i+1: an angle such as "40 deg", or the word "gable" for an end that stands vertical instead of pitching. An empty string takes the default pitch, and a short list leaves the rest at the default. All four gable is refused - a roof with no pitched edge has no top.',
|
|
18351
|
+
default: 'every edge takes "pitch", which is a hip'
|
|
18352
|
+
},
|
|
18353
|
+
expr("overhang", "How far the eave stands out past the footprint. The pitch planes rise from the overhung line, which is where a real roof starts.", false, "0"),
|
|
18354
|
+
expr("thickness", "Plumb thickness, measured VERTICALLY. On a 30 degree pitch the perpendicular depth is 0.87 of this.", true)
|
|
18355
|
+
]
|
|
18356
|
+
},
|
|
17665
18357
|
freeze: {
|
|
17666
18358
|
kind: "freeze",
|
|
17667
18359
|
summary: "Stores a solved body so it is read back instead of rebuilt. The stored copy carries the persistent names its faces and edges had when it was frozen, so selections made before the freeze go on resolving; without a stored body it is a pass-through.",
|
|
@@ -18034,6 +18726,96 @@ var FEATURE_ALIASES = {
|
|
|
18034
18726
|
roller: { form: "rename", to: "rollerDiameter" },
|
|
18035
18727
|
bore: { form: "rename", to: "boreDiameter" }
|
|
18036
18728
|
},
|
|
18729
|
+
/* The architectural family. Every alias here is a name from a floor plan or
|
|
18730
|
+
* from Revit and ArchiCAD, which is where a caller reaching for these is
|
|
18731
|
+
* coming from - and the two that matter most are the two that are not
|
|
18732
|
+
* renames at all: `height` on a wall and on an opening mean different things,
|
|
18733
|
+
* and only one of them is a distance. */
|
|
18734
|
+
level: {
|
|
18735
|
+
// A level is an ELEVATION, and every other word for it is a height.
|
|
18736
|
+
height: { form: "rename", to: "elevation" },
|
|
18737
|
+
z: { form: "rename", to: "elevation" },
|
|
18738
|
+
at: { form: "rename", to: "elevation" }
|
|
18739
|
+
},
|
|
18740
|
+
wall: {
|
|
18741
|
+
/* The one that would silently build a wall from the ground to 2.7 m no
|
|
18742
|
+
* matter which storey it was meant for. `top` is an ELEVATION, so a height
|
|
18743
|
+
* given here has to be added to the base rather than renamed onto it -
|
|
18744
|
+
* there is no alias form that does arithmetic, so this one teaches. */
|
|
18745
|
+
height: {
|
|
18746
|
+
form: "rename",
|
|
18747
|
+
to: "top",
|
|
18748
|
+
note: '"top" is an ELEVATION, not a height: write base + the height you meant, or level("first") for the storey above.'
|
|
18749
|
+
},
|
|
18750
|
+
t: { form: "rename", to: "thickness" },
|
|
18751
|
+
width: { form: "rename", to: "thickness" },
|
|
18752
|
+
depth: { form: "rename", to: "thickness" },
|
|
18753
|
+
centreline: { form: "rename", to: "path" },
|
|
18754
|
+
centerline: { form: "rename", to: "path" },
|
|
18755
|
+
points: { form: "rename", to: "path" },
|
|
18756
|
+
line: { form: "rename", to: "path" },
|
|
18757
|
+
bottom: { form: "rename", to: "base" },
|
|
18758
|
+
baseLevel: { form: "rename", to: "base" },
|
|
18759
|
+
topLevel: { form: "rename", to: "top" },
|
|
18760
|
+
align: { form: "rename", to: "justification" },
|
|
18761
|
+
alignment: { form: "rename", to: "justification" },
|
|
18762
|
+
justify: { form: "rename", to: "justification" }
|
|
18763
|
+
},
|
|
18764
|
+
opening: {
|
|
18765
|
+
wall: { form: "rename", to: "host" },
|
|
18766
|
+
input: { form: "rename", to: "host" },
|
|
18767
|
+
distance: { form: "rename", to: "along" },
|
|
18768
|
+
offset: { form: "rename", to: "along" },
|
|
18769
|
+
position: {
|
|
18770
|
+
form: "rename",
|
|
18771
|
+
to: "along",
|
|
18772
|
+
note: `An opening has no x, y or z. "along" is the distance up the host wall's own centreline, which is what makes an opening outside its wall inexpressible.`
|
|
18773
|
+
},
|
|
18774
|
+
sill: { form: "rename", to: "sillHeight" },
|
|
18775
|
+
sillLevel: {
|
|
18776
|
+
form: "rename",
|
|
18777
|
+
to: "sillHeight",
|
|
18778
|
+
note: `"sillHeight" is measured UP FROM THE WALL'S BASE, not from the ground.`
|
|
18779
|
+
},
|
|
18780
|
+
headHeight: {
|
|
18781
|
+
form: "rename",
|
|
18782
|
+
to: "height",
|
|
18783
|
+
note: "The head is sillHeight + height; there is no separate field for it."
|
|
18784
|
+
}
|
|
18785
|
+
},
|
|
18786
|
+
slab: {
|
|
18787
|
+
// Named for the two features either side of it: a roof takes `base`, a
|
|
18788
|
+
// level takes `elevation`, and a slab takes `level`.
|
|
18789
|
+
elevation: { form: "rename", to: "level" },
|
|
18790
|
+
base: { form: "rename", to: "level" },
|
|
18791
|
+
top: { form: "rename", to: "level" },
|
|
18792
|
+
z: { form: "rename", to: "level" },
|
|
18793
|
+
outline: { form: "rename", to: "boundary" },
|
|
18794
|
+
points: { form: "rename", to: "boundary" },
|
|
18795
|
+
path: { form: "rename", to: "boundary" },
|
|
18796
|
+
sketch: { form: "rename", to: "profile" },
|
|
18797
|
+
t: { form: "rename", to: "thickness" },
|
|
18798
|
+
depth: { form: "rename", to: "thickness" }
|
|
18799
|
+
},
|
|
18800
|
+
roof: {
|
|
18801
|
+
pitchAngle: { form: "rename", to: "pitch" },
|
|
18802
|
+
slope: { form: "rename", to: "pitch" },
|
|
18803
|
+
angle: { form: "rename", to: "pitch" },
|
|
18804
|
+
outline: { form: "rename", to: "boundary" },
|
|
18805
|
+
footprint: { form: "rename", to: "boundary" },
|
|
18806
|
+
points: { form: "rename", to: "boundary" },
|
|
18807
|
+
eave: { form: "rename", to: "base" },
|
|
18808
|
+
eaveLevel: { form: "rename", to: "base" },
|
|
18809
|
+
eaves: { form: "rename", to: "base" },
|
|
18810
|
+
overhangs: { form: "rename", to: "overhang" },
|
|
18811
|
+
soffit: { form: "rename", to: "overhang" },
|
|
18812
|
+
t: { form: "rename", to: "thickness" },
|
|
18813
|
+
gables: {
|
|
18814
|
+
form: "rename",
|
|
18815
|
+
to: "edges",
|
|
18816
|
+
note: '"edges" is one entry per boundary edge: an angle, or the word "gable".'
|
|
18817
|
+
}
|
|
18818
|
+
},
|
|
18037
18819
|
component: {
|
|
18038
18820
|
origin: { form: "rename", to: "position" },
|
|
18039
18821
|
translate: { form: "rename", to: "position" }
|
|
@@ -18829,7 +19611,7 @@ var MATERIAL_LIBRARY = [
|
|
|
18829
19611
|
preset("carbon-fibre", "Carbon fibre", [0.05, 0.052, 0.056], 0, 0.26, "Other")
|
|
18830
19612
|
];
|
|
18831
19613
|
function materialById(id) {
|
|
18832
|
-
return MATERIAL_LIBRARY.find((
|
|
19614
|
+
return MATERIAL_LIBRARY.find((m2) => m2.id === id);
|
|
18833
19615
|
}
|
|
18834
19616
|
|
|
18835
19617
|
// ../material/dist/lzma.js
|
|
@@ -19794,18 +20576,18 @@ var PROCEDURAL_HOOK = [
|
|
|
19794
20576
|
var clamp01 = (value) => value < 0 ? 0 : value > 1 ? 1 : value;
|
|
19795
20577
|
var colour = (value, fallback) => value && value.length >= 3 ? [clamp01(value[0]), clamp01(value[1]), clamp01(value[2])] : fallback;
|
|
19796
20578
|
var range = (value, lo, hi) => typeof value === "number" && Number.isFinite(value) ? value < lo ? lo : value > hi ? hi : value : void 0;
|
|
19797
|
-
function optionalSurface(
|
|
19798
|
-
const emissive =
|
|
20579
|
+
function optionalSurface(m2) {
|
|
20580
|
+
const emissive = m2.emissive ? colour(m2.emissive, [0, 0, 0]) : void 0;
|
|
19799
20581
|
return {
|
|
19800
20582
|
...emissive ? { emissive } : {},
|
|
19801
|
-
...pick("emissiveStrength", range(
|
|
19802
|
-
...pick("transmission",
|
|
19803
|
-
...pick("ior", range(
|
|
19804
|
-
...pick("anisotropy",
|
|
19805
|
-
...pick("anisotropyRotation", range(
|
|
19806
|
-
...
|
|
19807
|
-
...pick("attenuationDistance", range(
|
|
19808
|
-
...pick("subsurface",
|
|
20583
|
+
...pick("emissiveStrength", range(m2.emissiveStrength, 0, 1e3)),
|
|
20584
|
+
...pick("transmission", m2.transmission === void 0 ? void 0 : clamp01(m2.transmission)),
|
|
20585
|
+
...pick("ior", range(m2.ior, 1.0001, 2.5)),
|
|
20586
|
+
...pick("anisotropy", m2.anisotropy === void 0 ? void 0 : clamp01(m2.anisotropy)),
|
|
20587
|
+
...pick("anisotropyRotation", range(m2.anisotropyRotation, -Math.PI * 2, Math.PI * 2)),
|
|
20588
|
+
...m2.attenuationColor ? { attenuationColor: colour(m2.attenuationColor, [1, 1, 1]) } : {},
|
|
20589
|
+
...pick("attenuationDistance", range(m2.attenuationDistance, 1e-3, 1e6)),
|
|
20590
|
+
...pick("subsurface", m2.subsurface === void 0 ? void 0 : clamp01(m2.subsurface))
|
|
19809
20591
|
};
|
|
19810
20592
|
}
|
|
19811
20593
|
var pick = (key, value) => value === void 0 ? {} : { [key]: value };
|
|
@@ -19874,7 +20656,7 @@ var sub2 = (a, b) => [a[0] - b[0], a[1] - b[1], a[2] - b[2]];
|
|
|
19874
20656
|
var dot4 = (a, b) => a[0] * b[0] + a[1] * b[1] + a[2] * b[2];
|
|
19875
20657
|
var norm = (a) => Math.hypot(a[0], a[1], a[2]);
|
|
19876
20658
|
var scale2 = (a, s) => [a[0] * s, a[1] * s, a[2] * s];
|
|
19877
|
-
function
|
|
20659
|
+
function unit3(a) {
|
|
19878
20660
|
const l = norm(a);
|
|
19879
20661
|
return l < 1e-12 || !Number.isFinite(l) ? null : [a[0] / l, a[1] / l, a[2] / l];
|
|
19880
20662
|
}
|
|
@@ -19899,7 +20681,7 @@ function axialFaces(t) {
|
|
|
19899
20681
|
const kind = SURFACE[face.surfaceType];
|
|
19900
20682
|
if (kind !== "cylinder" && kind !== "cone" && kind !== "torus")
|
|
19901
20683
|
continue;
|
|
19902
|
-
const dir =
|
|
20684
|
+
const dir = unit3(face.axisDir);
|
|
19903
20685
|
if (dir)
|
|
19904
20686
|
out.push({ face, dir });
|
|
19905
20687
|
}
|
|
@@ -19910,7 +20692,7 @@ function planarFaces(t) {
|
|
|
19910
20692
|
for (const face of t.faces) {
|
|
19911
20693
|
if (!face.planar)
|
|
19912
20694
|
continue;
|
|
19913
|
-
const normal =
|
|
20695
|
+
const normal = unit3(face.normal) ?? unit3(face.axisDir);
|
|
19914
20696
|
if (normal)
|
|
19915
20697
|
out.push({ face, normal });
|
|
19916
20698
|
}
|
|
@@ -20027,12 +20809,12 @@ var PINS = { closed: 3, tube: 2, plane: 1 };
|
|
|
20027
20809
|
var RESIDUAL_TIE = 1e-9;
|
|
20028
20810
|
function residualAt(face, p) {
|
|
20029
20811
|
if (face.planar) {
|
|
20030
|
-
const n =
|
|
20812
|
+
const n = unit3(face.normal) ?? unit3(face.axisDir);
|
|
20031
20813
|
return n ? { value: Math.abs(dot4(sub2(p, face.centroid), n)), pins: PINS.plane } : null;
|
|
20032
20814
|
}
|
|
20033
20815
|
const kind = SURFACE[face.surfaceType];
|
|
20034
20816
|
if (kind === "cylinder" && face.radius1 > 0) {
|
|
20035
|
-
const d =
|
|
20817
|
+
const d = unit3(face.axisDir);
|
|
20036
20818
|
return d ? {
|
|
20037
20819
|
value: Math.abs(norm(reject2(sub2(p, face.axisOrigin), d)) - face.radius1),
|
|
20038
20820
|
pins: PINS.tube
|
|
@@ -20042,7 +20824,7 @@ function residualAt(face, p) {
|
|
|
20042
20824
|
return { value: Math.abs(norm(sub2(p, face.axisOrigin)) - face.radius1), pins: PINS.closed };
|
|
20043
20825
|
}
|
|
20044
20826
|
if (kind === "torus" && face.radius1 > 0 && face.radius2 > 0) {
|
|
20045
|
-
const d =
|
|
20827
|
+
const d = unit3(face.axisDir);
|
|
20046
20828
|
if (!d)
|
|
20047
20829
|
return null;
|
|
20048
20830
|
const rel = sub2(p, face.axisOrigin);
|
|
@@ -20089,7 +20871,7 @@ function isFit(a, b, alignments, tolMm) {
|
|
|
20089
20871
|
for (const alignment of alignments) {
|
|
20090
20872
|
if (alignment.kind !== "concentric" || Math.abs(alignment.offset) > tolMm)
|
|
20091
20873
|
continue;
|
|
20092
|
-
const axis =
|
|
20874
|
+
const axis = unit3(faceNamed(a, alignment.faces[0])?.axisDir ?? [0, 0, 0]);
|
|
20093
20875
|
if (!axis)
|
|
20094
20876
|
continue;
|
|
20095
20877
|
const [aLow, aHigh] = spanAlong(a.bbox, axis);
|
|
@@ -20164,10 +20946,10 @@ function faceCover(face) {
|
|
|
20164
20946
|
outer = face.radius1 + face.radius2;
|
|
20165
20947
|
} else
|
|
20166
20948
|
return [face.bbox];
|
|
20167
|
-
const d =
|
|
20949
|
+
const d = unit3(face.axisDir);
|
|
20168
20950
|
if (!d)
|
|
20169
20951
|
return [face.bbox];
|
|
20170
|
-
const e1 =
|
|
20952
|
+
const e1 = unit3(reject2(Math.abs(d[0]) < 0.9 ? [1, 0, 0] : [0, 1, 0], d));
|
|
20171
20953
|
if (!e1)
|
|
20172
20954
|
return [face.bbox];
|
|
20173
20955
|
const e2 = [
|
|
@@ -22424,7 +23206,7 @@ var Document = class _Document {
|
|
|
22424
23206
|
continue;
|
|
22425
23207
|
if (state.feature.construction)
|
|
22426
23208
|
continue;
|
|
22427
|
-
if (state.feature.kind === "sketch" || state.feature.kind === "plane" || state.feature.kind === "axis")
|
|
23209
|
+
if (state.feature.kind === "sketch" || state.feature.kind === "plane" || state.feature.kind === "axis" || state.feature.kind === "level")
|
|
22428
23210
|
continue;
|
|
22429
23211
|
bodies.push({
|
|
22430
23212
|
featureId: id,
|
|
@@ -22855,6 +23637,13 @@ var Document = class _Document {
|
|
|
22855
23637
|
}
|
|
22856
23638
|
return state.shape;
|
|
22857
23639
|
},
|
|
23640
|
+
featureOf(featureId) {
|
|
23641
|
+
const state = self2.features.get(featureId);
|
|
23642
|
+
if (!state) {
|
|
23643
|
+
throw new KernelError(-3, `feature '${featureId}' does not exist`, "", "input");
|
|
23644
|
+
}
|
|
23645
|
+
return state.feature;
|
|
23646
|
+
},
|
|
22858
23647
|
// One decode path, not two. A second copy of the caching here meant every
|
|
22859
23648
|
// topology a FEATURE read skipped the signature ledger, so the one thing
|
|
22860
23649
|
// a heal report needs was recorded for queries and not for the fillets
|
|
@@ -22976,7 +23765,7 @@ var Document = class _Document {
|
|
|
22976
23765
|
throw new KernelError(-8, resolved2.note ?? `no ${entity} selected on '${bodyFeatureId}'`, "selection", "axis");
|
|
22977
23766
|
}
|
|
22978
23767
|
const wanted = new Set(resolved2.names);
|
|
22979
|
-
const
|
|
23768
|
+
const unit4 = (v) => {
|
|
22980
23769
|
const n = Math.hypot(v[0], v[1], v[2]) || 1;
|
|
22981
23770
|
return [v[0] / n, v[1] / n, v[2] / n];
|
|
22982
23771
|
};
|
|
@@ -22988,11 +23777,11 @@ var Document = class _Document {
|
|
|
22988
23777
|
if (edge.curveType === CurveType.Line) {
|
|
22989
23778
|
return {
|
|
22990
23779
|
origin: edge.midpoint,
|
|
22991
|
-
direction:
|
|
23780
|
+
direction: unit4([edge.end[0] - edge.start[0], edge.end[1] - edge.start[1], edge.end[2] - edge.start[2]])
|
|
22992
23781
|
};
|
|
22993
23782
|
}
|
|
22994
23783
|
if (edge.radius > 0)
|
|
22995
|
-
return { origin: edge.axisOrigin, direction:
|
|
23784
|
+
return { origin: edge.axisOrigin, direction: unit4(edge.axisDir) };
|
|
22996
23785
|
throw new KernelError(-10, "that edge is neither straight nor circular, so it defines no axis", "geometry", "axis");
|
|
22997
23786
|
}
|
|
22998
23787
|
const matches = topology.faces.filter((f) => wanted.has(f.name));
|
|
@@ -23001,12 +23790,12 @@ var Document = class _Document {
|
|
|
23001
23790
|
throw new KernelError(-8, `the selected face is not present on '${bodyFeatureId}'`, "selection", "axis");
|
|
23002
23791
|
}
|
|
23003
23792
|
if (face.planar)
|
|
23004
|
-
return { origin: face.centroid, direction:
|
|
23793
|
+
return { origin: face.centroid, direction: unit4(face.normal) };
|
|
23005
23794
|
const spun = Math.hypot(face.axisDir[0], face.axisDir[1], face.axisDir[2]) > 1e-9;
|
|
23006
23795
|
if (!spun) {
|
|
23007
23796
|
throw new KernelError(-10, "that face has no axis of its own - pick a cylinder, a cone or a flat face", "geometry", "axis");
|
|
23008
23797
|
}
|
|
23009
|
-
return { origin: face.axisOrigin, direction:
|
|
23798
|
+
return { origin: face.axisOrigin, direction: unit4(face.axisDir) };
|
|
23010
23799
|
},
|
|
23011
23800
|
sizeOf(featureId) {
|
|
23012
23801
|
const topology = this.topologyOf(this.shapeOf(featureId));
|
|
@@ -23081,12 +23870,12 @@ var Document = class _Document {
|
|
|
23081
23870
|
const props = this.geometryProps;
|
|
23082
23871
|
const measured = (id) => {
|
|
23083
23872
|
const shape = shapeOf(id);
|
|
23084
|
-
let
|
|
23085
|
-
if (!
|
|
23086
|
-
|
|
23087
|
-
props.set(shape,
|
|
23873
|
+
let m2 = props.get(shape);
|
|
23874
|
+
if (!m2) {
|
|
23875
|
+
m2 = this.kernel.massProperties(shape);
|
|
23876
|
+
props.set(shape, m2);
|
|
23088
23877
|
}
|
|
23089
|
-
return
|
|
23878
|
+
return m2;
|
|
23090
23879
|
};
|
|
23091
23880
|
const active = new Set(this.order.slice(0, this.rollbackIndex ?? this.order.length));
|
|
23092
23881
|
const shapeOf = (id) => {
|
|
@@ -24296,7 +25085,7 @@ var DESIGN_GUIDE = [
|
|
|
24296
25085
|
id: "index",
|
|
24297
25086
|
title: "Obenor3D design knowledge base",
|
|
24298
25087
|
when: "Start here",
|
|
24299
|
-
text: '# Obenor3D design knowledge base\n\nDistilled from five CAD textbooks and translated into Obenor3D\'s feature kinds\nand MCP tools. Written for the MCP server and the in-app agent: every recipe\nnames the tool to call, and flags where Obenor3D has no equivalent.\n\nSources, cited in each file by short name:\n\n- **Land** \u2014 K. M. Land, *Improving CAD Designs with Autodesk Fusion 360* (2023)\n- **Barr** \u2014 Barr, Juricic, Krueger, Reyes, *Design Workbook Using SOLIDWORKS 2024*\n- **Shih** \u2014 Shih & Schilling, *Parametric Modeling with SOLIDWORKS 2024*\n- **Tran** \u2014 L. Tran, *Mastering Surface Modeling with SOLIDWORKS 2024*\n- **Almattar** \u2014 T. Almattar, *Learn SOLIDWORKS 2025*, 3rd ed.\n\n## Files\n\n| File | Read it when |\n|---|---|\n| [01-design-intent.md](01-design-intent.md) | Before the first feature: planning the tree, base feature, what should flex |\n| [02-sketching.md](02-sketching.md) | Building any profile: relations, dimensions, fully defined, construction geometry |\n| [03-features-and-order.md](03-features-and-order.md) | Extrude/revolve/sweep/loft/shell/draft, end conditions, patterns, parent/child |\n| [04-fillets-and-blends.md](04-fillets-and-blends.md) | Dress-up: fillets, chamfers, order, failures |\n| [05-assemblies-and-joints.md](05-assemblies-and-joints.md) | More than one part: components, grounding, joints, interference, fits |\n| [06-sheet-metal.md](06-sheet-metal.md) | Flanges, bends, K-factor, flat pattern |\n| [07-surfacing.md](07-surfacing.md) | When a solid feature cannot make a face: continuity, loft, fill, knit, thicken |\n| [08-common-parts-recipes.md](08-common-parts-recipes.md) | Bracket, plate, pulley, housing, shaft, tray, hook, knob \u2014 step by step |\n| [09-checking-a-design.md](09-checking-a-design.md) | Before saying done: measure, mass, render, interference, sketch status |\n\n## The ten rules every book agrees on\n\n1. **Plan the tree before the first feature.** Count the features, pick the base\n feature, order them as a machinist would. (Shih ch.3-4)\n2. **Base sketch on a datum, centred on the origin.** Symmetry and later mirrors\n come free. (Barr Ex 3.1, Shih ch.7)\n3. **Shape before size.** Rough curves, then relations, then dimensions, then\n `sketch_status` says zero freedom. (Shih ch.2, 5; Almattar ch.3)\n4. **Relations say how it flexes; dimensions say how big.** A hole at a midpoint\n moves with the plate; a hole at 20 mm does not. Choose deliberately. (Land ch.3)\n5. **Name the numbers.** Parameters and expressions, not literals repeated.\n (Shih ch.5, Almattar ch.13, Land ch.9)\n6. **End conditions are intent.** `throughAll` for a hole, `toFace` for a pocket\n floor, `symmetric` for anything centred. (Shih ch.3, Land ch.10)\n7. **Features that carry meaning.** `hole`, `thread`, `flange`, `mirror`,\n `pattern*` \u2014 never a cylinder and a boolean. (Shih ch.3, Almattar ch.12)\n8. **Dress-up last**: draft \u2192 outer fillets \u2192 shell \u2192 inner features \u2192 small\n fillets/chamfers. (Barr Ex 4.1, Shih ch.12)\n9. **One component per part; ground the frame first; mate in assembly order.**\n (Land ch.3, Barr Lab 5, Shih ch.14)\n10. **Verify, then say done**: render three views, measure, mass, interference,\n every sketch fully defined. (Barr Lab 6, Tran ch.1, Almattar ch.15)\n\n## Obenor3D vocabulary map (SolidWorks / Fusion \u2192 here)\n\n| Elsewhere | Obenor3D |\n|---|---|\n| Front / Top / Right plane | `base: "xy"` / `"xz"` / `"yz"` on `add_sketch`, `add_plane` |\n| Smart Dimension, Add Relation | `add_dimension`, `add_constraint`; `sketch.constrained` on `add_sketch` |\n| Fully Defined | `sketch_status` reports 0 degrees of freedom |\n| Global variable / user parameter | `set_parameters` |\n| Extruded Boss / Cut | `add_extrude` with `operation` fuse / cut / new body |\n| Up to Surface | `mode: "toFace"` (planar faces only) |\n| Through All | `mode: "throughAll"` |\n| Mid Plane | `mode: "symmetric"` |\n| Hole Wizard | `add_hole` (simple / counterbore / countersink) + `add_thread` for tapped |\n| Mirror Feature / Body | `mirror` (bodies) |\n| Linear / Circular Pattern | `add_pattern` type linear / circular (bodies) |\n| Shell, Draft | `add_shell`, `draft` via `add_feature` |\n| Convert Entities / Project | `sketch.projected` on `add_sketch`; `project_geometry` |\n| Offset Entities | `offset_curve` (writes a new sketch) |\n| Knit, Thicken, Fill, Trim | `add_stitch`, `thicken`, `fillFace`, `add_trim_surface` |\n| Mate | `add_joint` (rigid, revolute, slider, cylindrical, pinSlot, planar, ball) |\n| Fix / ground component | `grounded: true` on `component` / `instance` |\n| Interference Detection | `check_interference`, `expect` feature |\n| Mass Properties | `measure` with `density` |\n| Edge Flange / Sketched Bend / Flatten | `add_flange`, `add_bend`, `flat_pattern` |\n| Rollback bar / Suppress | `set_timeline`, `edit.suppress`, `freeze` |\n| Rib, Dome, Wrap, Boundary Surface, Configurations, Design Table | **none** \u2014 see [textbook-gaps.md](../textbook-gaps.md) |'
|
|
25088
|
+
text: '# Obenor3D design knowledge base\n\nDistilled from five CAD textbooks and translated into Obenor3D\'s feature kinds\nand MCP tools. Written for the MCP server and the in-app agent: every recipe\nnames the tool to call, and flags where Obenor3D has no equivalent.\n\nSources, cited in each file by short name:\n\n- **Land** \u2014 K. M. Land, *Improving CAD Designs with Autodesk Fusion 360* (2023)\n- **Barr** \u2014 Barr, Juricic, Krueger, Reyes, *Design Workbook Using SOLIDWORKS 2024*\n- **Shih** \u2014 Shih & Schilling, *Parametric Modeling with SOLIDWORKS 2024*\n- **Tran** \u2014 L. Tran, *Mastering Surface Modeling with SOLIDWORKS 2024*\n- **Almattar** \u2014 T. Almattar, *Learn SOLIDWORKS 2025*, 3rd ed.\n\n## Files\n\n| File | Read it when |\n|---|---|\n| [01-design-intent.md](01-design-intent.md) | Before the first feature: planning the tree, base feature, what should flex |\n| [02-sketching.md](02-sketching.md) | Building any profile: relations, dimensions, fully defined, construction geometry |\n| [03-features-and-order.md](03-features-and-order.md) | Extrude/revolve/sweep/loft/shell/draft, end conditions, patterns, parent/child |\n| [04-fillets-and-blends.md](04-fillets-and-blends.md) | Dress-up: fillets, chamfers, order, failures |\n| [05-assemblies-and-joints.md](05-assemblies-and-joints.md) | More than one part: components, grounding, joints, interference, fits |\n| [06-sheet-metal.md](06-sheet-metal.md) | Flanges, bends, K-factor, flat pattern |\n| [07-surfacing.md](07-surfacing.md) | When a solid feature cannot make a face: continuity, loft, fill, knit, thicken |\n| [08-common-parts-recipes.md](08-common-parts-recipes.md) | Bracket, plate, pulley, housing, shaft, tray, hook, knob \u2014 step by step |\n| [09-checking-a-design.md](09-checking-a-design.md) | Before saying done: measure, mass, render, interference, sketch status |\n| [10-nurbs-and-freeform.md](10-nurbs-and-freeform.md) | Poles, degree, continuity: control nets, curves in space, matching, rebuilding |\n| [11-architecture.md](11-architecture.md) | Massing a building: levels, walls, openings, slabs, roofs \u2014 and the order they go in |\n\n## The ten rules every book agrees on\n\n1. **Plan the tree before the first feature.** Count the features, pick the base\n feature, order them as a machinist would. (Shih ch.3-4)\n2. **Base sketch on a datum, centred on the origin.** Symmetry and later mirrors\n come free. (Barr Ex 3.1, Shih ch.7)\n3. **Shape before size.** Rough curves, then relations, then dimensions, then\n `sketch_status` says zero freedom. (Shih ch.2, 5; Almattar ch.3)\n4. **Relations say how it flexes; dimensions say how big.** A hole at a midpoint\n moves with the plate; a hole at 20 mm does not. Choose deliberately. (Land ch.3)\n5. **Name the numbers.** Parameters and expressions, not literals repeated.\n (Shih ch.5, Almattar ch.13, Land ch.9)\n6. **End conditions are intent.** `throughAll` for a hole, `toFace` for a pocket\n floor, `symmetric` for anything centred. (Shih ch.3, Land ch.10)\n7. **Features that carry meaning.** `hole`, `thread`, `flange`, `mirror`,\n `pattern*` \u2014 never a cylinder and a boolean. (Shih ch.3, Almattar ch.12)\n8. **Dress-up last**: draft \u2192 outer fillets \u2192 shell \u2192 inner features \u2192 small\n fillets/chamfers. (Barr Ex 4.1, Shih ch.12)\n9. **One component per part; ground the frame first; mate in assembly order.**\n (Land ch.3, Barr Lab 5, Shih ch.14)\n10. **Verify, then say done**: render three views, measure, mass, interference,\n every sketch fully defined. (Barr Lab 6, Tran ch.1, Almattar ch.15)\n\n## Obenor3D vocabulary map (SolidWorks / Fusion \u2192 here)\n\n| Elsewhere | Obenor3D |\n|---|---|\n| Front / Top / Right plane | `base: "xy"` / `"xz"` / `"yz"` on `add_sketch`, `add_plane` |\n| Smart Dimension, Add Relation | `add_dimension`, `add_constraint`; `sketch.constrained` on `add_sketch` |\n| Fully Defined | `sketch_status` reports 0 degrees of freedom |\n| Global variable / user parameter | `set_parameters` |\n| Extruded Boss / Cut | `add_extrude` with `operation` fuse / cut / new body |\n| Up to Surface | `mode: "toFace"` (planar faces only) |\n| Through All | `mode: "throughAll"` |\n| Mid Plane | `mode: "symmetric"` |\n| Hole Wizard | `add_hole` (simple / counterbore / countersink) + `add_thread` for tapped |\n| Mirror Feature / Body | `mirror` (bodies) |\n| Linear / Circular Pattern | `add_pattern` type linear / circular (bodies) |\n| Shell, Draft | `add_shell`, `draft` via `add_feature` |\n| Convert Entities / Project | `sketch.projected` on `add_sketch`; `project_geometry` |\n| Offset Entities | `offset_curve` (writes a new sketch) |\n| Knit, Thicken, Fill, Trim | `add_stitch`, `thicken`, `fillFace`, `add_trim_surface` |\n| Mate | `add_joint` (rigid, revolute, slider, cylindrical, pinSlot, planar, ball) |\n| Fix / ground component | `grounded: true` on `component` / `instance` |\n| Interference Detection | `check_interference`, `expect` feature |\n| Mass Properties | `measure` with `density` |\n| Edge Flange / Sketched Bend / Flatten | `add_flange`, `add_bend`, `flat_pattern` |\n| Rollback bar / Suppress | `set_timeline`, `edit.suppress`, `freeze` |\n| Rib, Dome, Wrap, Boundary Surface, Configurations, Design Table | **none** \u2014 see [textbook-gaps.md](../textbook-gaps.md) |'
|
|
24300
25089
|
},
|
|
24301
25090
|
{
|
|
24302
25091
|
id: "design-intent",
|
|
@@ -24338,7 +25127,7 @@ var DESIGN_GUIDE = [
|
|
|
24338
25127
|
id: "surfacing",
|
|
24339
25128
|
title: "07 \u2014 Surfacing",
|
|
24340
25129
|
when: "When a solid feature cannot make a face: continuity, loft, fill, knit, thicken",
|
|
24341
|
-
text: '# 07 \u2014 Surfacing\n\nSource: Tran, *Mastering Surface Modeling* (ch.1 principles, ch.2-11\nrecipes); Barr Lab 4; Land ch.11-13 (FORM, no equivalent).\n\n## 1. When to use surfaces, and when not (Tran ch.1 \xA73-4, \xA77)\n\n- Shapes that extrude, sweep or loft **in one feature** should be solids.\n "Solid features take less time to make."\n- Surfaces are for the face a solid feature cannot make, built **one face at a\n time**, then knitted and thickened into a solid \u2014 "the final design should\n be a solid model" so it can carry material, be analysed, be drawn.\n- **Hybrid modelling** is the preferred method: a surface trims a solid, a\n surface replaces a face, a surface splits a solid into bodies.\n- Surfaces used only as construction are hidden when the model is complete.\n\nObenor3D reads the same way: `extrude`/`revolve` are solids;\n`sweep`/`loft { solid: false }` give surfaces; `ruledSurface`, `fillFace`,\n`surfaceFromPoints`, `projectCurve` are surface/curve makers; `stitchSurfaces`\n+ `thicken` go back to a solid; `split`, `trimSurface`, `splitFace` are the\nhybrid verbs.\n\n## 2. Continuity (Tran ch.1 \xA72, MCP instructions)\n\n| Grade | Meaning | Where it matters |\n|---|---|---|\n| C0 / G0 contact | share an edge | anything that must knit |\n| C1 / G1 tangent | tangent across the edge | fillets, most housings |\n| C2 / G2 curvature | same curvature across the edge | visible styled surfaces; a G1 join with a curvature step "draws a visible line in a reflection" |\n\n`analyze_surface` grades every join and reports `minRadius`; `query_geometry`\ncan answer G1 (a tangent edge); only `analyze_surface` answers G2. A loft\nthrough sketch stations is G1 at best across its seams. `fillFace\n{ continuity: "g1" | "g2" }` needs supporting faces to reach tangency.\n\n## 3. Check after every surface feature (Tran ch.1 \xA76)\n\nSurfaces are built face by face and "are not frequently verified like solid\nfeatures"; small gaps or overlaps stop the knit. Run `check_model` after each\nsurface feature, `inspect_body` to see whether a stitched shell closed\n(`stitchSurfaces.makeSolid` is *attempted*, not assumed \u2014 "a shell with a gap\nbehaves like a solid until the first boolean").\n\n## 4. The surface toolbox, mapped (Tran ch.2-6)\n\n| SolidWorks | Obenor3D | Notes |\n|---|---|---|\n| Extruded / Revolved surface | `sweep` or `loft` with `solid: false`; `ruledSurface` for an extrusion between two curves | no `solid` flag on extrude/revolve |\n| Swept surface | `sweep { solid: false, guide?, scaleEnd? }` | one guide rail |\n| Lofted surface + guide curves | `loft { solid: false }` | **no guide curves** \u2014 add stations |\n| Boundary surface (two-direction network, tangent to faces) |
|
|
25130
|
+
text: '# 07 \u2014 Surfacing\n\nSource: Tran, *Mastering Surface Modeling* (ch.1 principles, ch.2-11\nrecipes); Barr Lab 4; Land ch.11-13 (FORM, no equivalent).\n\n**The geometry underneath is 10-nurbs-and-freeform.md.** This file is about\nwhen to reach for a surface and how to knit one back into a solid; that one is\nabout poles, degree, continuity and the tools that move them. Two rows of the\ntable below used to read "none" and no longer do \u2014 that file is where they went.\n\n## 1. When to use surfaces, and when not (Tran ch.1 \xA73-4, \xA77)\n\n- Shapes that extrude, sweep or loft **in one feature** should be solids.\n "Solid features take less time to make."\n- Surfaces are for the face a solid feature cannot make, built **one face at a\n time**, then knitted and thickened into a solid \u2014 "the final design should\n be a solid model" so it can carry material, be analysed, be drawn.\n- **Hybrid modelling** is the preferred method: a surface trims a solid, a\n surface replaces a face, a surface splits a solid into bodies.\n- Surfaces used only as construction are hidden when the model is complete.\n\nObenor3D reads the same way: `extrude`/`revolve` are solids;\n`sweep`/`loft { solid: false }` give surfaces; `ruledSurface`, `fillFace`,\n`surfaceFromPoints`, `projectCurve` are surface/curve makers; `stitchSurfaces`\n+ `thicken` go back to a solid; `split`, `trimSurface`, `splitFace` are the\nhybrid verbs.\n\n## 2. Continuity (Tran ch.1 \xA72, MCP instructions)\n\n| Grade | Meaning | Where it matters |\n|---|---|---|\n| C0 / G0 contact | share an edge | anything that must knit |\n| C1 / G1 tangent | tangent across the edge | fillets, most housings |\n| C2 / G2 curvature | same curvature across the edge | visible styled surfaces; a G1 join with a curvature step "draws a visible line in a reflection" |\n\n`analyze_surface` grades every join and reports `minRadius`; `query_geometry`\ncan answer G1 (a tangent edge); only `analyze_surface` answers G2. A loft\nthrough sketch stations is G1 at best across its seams. `fillFace\n{ continuity: "g1" | "g2" }` needs supporting faces to reach tangency.\n\n## 3. Check after every surface feature (Tran ch.1 \xA76)\n\nSurfaces are built face by face and "are not frequently verified like solid\nfeatures"; small gaps or overlaps stop the knit. Run `check_model` after each\nsurface feature, `inspect_body` to see whether a stitched shell closed\n(`stitchSurfaces.makeSolid` is *attempted*, not assumed \u2014 "a shell with a gap\nbehaves like a solid until the first boolean").\n\n## 4. The surface toolbox, mapped (Tran ch.2-6)\n\n| SolidWorks | Obenor3D | Notes |\n|---|---|---|\n| Extruded / Revolved surface | `sweep` or `loft` with `solid: false`; `ruledSurface` for an extrusion between two curves | no `solid` flag on extrude/revolve |\n| Swept surface | `sweep { solid: false, guide?, scaleEnd? }` | one guide rail |\n| Lofted surface + guide curves | `loft { solid: false }` | **no guide curves** \u2014 add stations |\n| Boundary surface (two-direction network, tangent to faces) | `add_blend_surface { a, edgeA, b, edgeB, continuity }`, or `fillFace` with g1/g2 | see 10-nurbs-and-freeform.md \xA74 |\n| Curve through reference points, 3D sketch spline | `add_curve { poles, degree, periodic }` \u2014 a NURBS curve in world space | plus `helix`, `projectCurve`, and `trim_curve`/`extend_curve`/`offset_curve`; see 10 \xA73 |\n| Planar surface | `fillFace` over a planar loop | |\n| Filled surface (4-edge non-planar patch) | `fillFace { boundary, continuity }` | Tran ch.11 patches a raised feature with 4 edges |\n| Knit | `add_stitch { inputs, tolerance, makeSolid }` | raise tolerance for imports |\n| Thicken | `thicken` | fillets after thickening (Tran ch.3) |\n| Trim surface (with sketch / surface / mutual) | `add_trim_surface` | one tool, one side kept; mutual = two calls |\n| Split line | `add_split_face { faces, tool }` or `projectCurve` then split | raised label areas (Tran ch.4) |\n| Delete face | `delete_faces` | heals by extending neighbours |\n| Offset surface | `offset` (whole body) / `offsetFaces` | no free-standing surface offset |\n| Face fillet with hold line, Deform, Flex | **none** | |\n| Intersect, Core/Cavity, Parting line/surface | `split { input, tools }` with a surface as tool; `transform` scale | no parting-line tool |\n| Check Entity, Zebra, Curvature comb | `check_model`, `analyze_surface`; UI `model.zebra`, `model.comb` | |\n\n## 5. Recipes\n\n### A. A styled housing (Tran ch.3, ch.5, ch.6)\n\n1. Two profile sketches on `add_plane { offset }` stations; a third for the flank.\n2. `loft { profiles, solid: false }` for the main skin; add stations where the\n loft would gap (Tran ch.3 "Comparing methods": loft gaps without guides).\n3. Cap the ends: `fillFace` over each end loop.\n4. `add_stitch { makeSolid: true }` \u2192 `inspect_body` says solid.\n5. `thicken { thickness: "wall" }` **or** keep solid and `shell`.\n6. Fillets after thickening; `analyze_surface` for G2 across visible seams.\n7. Split lines for a label: `projectCurve` a sketch onto the body,\n `add_split_face`, then `emboss_faces` the label area by 0.5 mm.\n\n### B. Repairing an imported surface model (Tran ch.11)\n\nMissing hole walls: `loft` between the two edge loops of the hole, keeping the\nconnector alignment (start both loops at corresponding vertices). Missing\n4-sided patch: `fillFace` with the 4 edges. Then `add_stitch`, `check_model`.\n\n### C. Mould core and cavity (Tran ch.10)\n\n1. Finish the part as a solid with draft (`draft`, `pullDirection`).\n2. Build the parting surface as a `ruledSurface` from the part\'s silhouette\n edges outward (no parting-line tool; find silhouette edges with\n `query_geometry`).\n3. Block: `add_primitive box` around the part; `add_boolean cut` the part\n (use `copy` to keep the part); `split { tools: [partingSurface] }` the\n block into core and cavity.\n4. `check_interference` core vs cavity vs part \u2192 zero shared volume.\n\n### D. Fairing a loft (Tran ch.1 spline handles; memory: station differencing)\n\nDifference the station values twice along the loft; a rate that dips and\nrecovers is a saddle. Add or move a station rather than nudging one.\nEvaluate with `analyze_surface`; look with `render_view` under zebra\n(UI `model.zebra`).\n\n## 6. Splines (Tran ch.1 \xA75)\n\nA spline\'s quality is in its handles: few through-points, tangency weighted at\nends, watch inflection points on the curvature comb. In Obenor3D `spline`\npasses through its points and `bezier` is control-point driven; prefer bezier\nfor styled curves and spline for measured data. Both are SKETCH entities and\ntherefore planar; `add_curve` is the same idea in world space, with its poles\nand degree given directly. See 10 \xA73 for the whole curve toolbox.\n\n## 7. Surfacing checklist\n\n- [ ] Could a solid feature make it? Then make it a solid\n- [ ] `check_model` after every surface feature\n- [ ] Knit reported solid by `inspect_body` before thicken/boolean\n- [ ] G1 minimum everywhere, G2 on visible styled joins (`analyze_surface`)\n- [ ] Construction surfaces removed or absorbed once the solid exists'
|
|
24342
25131
|
},
|
|
24343
25132
|
{
|
|
24344
25133
|
id: "common-parts-recipes",
|
|
@@ -24351,10 +25140,77 @@ var DESIGN_GUIDE = [
|
|
|
24351
25140
|
title: "09 \u2014 Checking a design before saying it is done",
|
|
24352
25141
|
when: "Before saying done: measure, mass, render, interference, sketch status",
|
|
24353
25142
|
text: '# 09 \u2014 Checking a design before saying it is done\n\nEvery book ends a tutorial by evaluating: measure, mass properties, a drawing,\nan interference check, and "does it update predictably". The agent prompt\'s\n`finish_check` runs most of it in one call; this file says what to read in\nthe result and what the books add.\n\n## 1. `finish_check` sections (prompt, in order)\n\n1. **`check_model` on every body** \u2014 BUILT is not SOUND. An open shell or a\n sliver still builds; the report says so. Back off the operation.\n2. **`render_view` iso / front / right** \u2014 a shape right in one view is\n routinely wrong in another. Add a `lookAt` close-up of any blend or fit.\n3. **`measure` volume and bounding box** against expectation \u2014 a part ten\n times too big or half hollow is one number.\n4. **`check_interference` / `inspect_assembly`** on multi-body models.\n5. **`sketch_status`** on every sketch \u2014 zero freedom, no conflicts.\n\n## 2. Mass properties (Barr Lab 6, Shih ch.4, Almattar ch.7)\n\nSet the material before trusting a mass. Obenor3D\'s materials are visual;\npass `density` to `measure` (g/cm\xB3 \u2192 the tool\'s unit; state which). Typical\nvalues (Barr Table 6-1, converted): aluminium 2.70, brass 8.50, copper 8.94,\nmild steel 7.85, stainless 7.90, titanium 4.51, ABS/PLA \u2248 1.05\u20131.25 g/cm\xB3.\nReport volume, mass, bounding box. Centre of mass and inertia are not\nexposed by `measure`; say so rather than estimate.\n\nBarr\'s reminder: SolidWorks\' "density" is unit weight; mass = density \xD7\nvolume, weight = unit weight \xD7 volume. Do not mix them.\n\n## 3. Measure tool habits (Barr Lab 6, Shih 13-17)\n\n- Two faces \u2192 distance (Shih measures the inside flange distance = 1.508").\n- An edge \u2192 length; a circular edge \u2192 arc length and diameter.\n- `measure` and `measure_angle` in Obenor3D; use them to confirm a dimension\n the drawing will call out, not to discover geometry you built.\n\n## 4. Test the intent, not just the shape (Barr Ex 3.1, Shih ch.5, ch.7, Land ch.3)\n\n1. Change one driving parameter by \xB130 % with `set_parameters`.\n2. `build`, `render_view`.\n3. Did the hole stay centred? Did the mirror follow? Did the cut still go\n through? Did any fillet fail?\n4. Restore the value.\nThis is the book\'s "double-click the 2.25" dimension and change it between\n1.00" and 3.00"". A model that survives it has its intent in the tree.\n\n## 5. Assemblies (Almattar ch.8, ch.15; Barr Lab 5; MCP instructions)\n\n- `check_interference`: any shared volume is a defect unless it is a press fit\n you meant \u2014 and say so.\n- Zero overlap proves nothing about seating; `probe_ray` down the axis, or\n an `expect { contact | clearance }` feature so the next rebuild tells you.\n- Step joints through their range (`angle`/`offset` at min, mid, max) and\n re-check \u2014 the manual substitute for collision detection.\n- `inspect_assembly` for mass per component and remaining freedom.\n\n## 6. Surfaces (Tran ch.1)\n\n`check_model` after every surface feature; `inspect_body` confirms the knit is\na solid; `analyze_surface` for continuity grades and `minRadius` before any\nfillet on a curved face.\n\n## 7. Sheet metal (Shih 13-25)\n\n`flat_pattern` builds; the blank length matches L = A(R + K\xB7T) by hand;\n`bend_table` lists every bend with radius and K.\n\n## 8. Drawings as a check (Barr Lab 8-10, Shih ch.8, Almattar ch.9-10)\n\nA three-view drawing is a review tool: hidden lines show buried features,\na section shows walls. `export_drawing { views: ["front","top","right","iso"],\ndimensions: true, hiddenLines: true }` and `hidden_line_view`; a section\nthrough `add_section` or the drawing\'s section axis. Detail views, BOM,\nballoons and bend notes are not generated \u2014 describe them in the reply.\n\n## 9. What to say at the end (prompt)\n\nWhat was built, what was assumed (units, material, K-factor, tolerances),\nwhat would be changed next, and every LOOK item from `finish_check` with its\ndisposition. A finished part with stated assumptions beats an interrogation.\n\n## 10. One-page checklist\n\n- [ ] `finish_check` clean or every LOOK explained\n- [ ] Parameters named; a \xB130 % change flexes as intended and is restored\n- [ ] Material stated; mass and volume reported with the density used\n- [ ] Every external edge broken; hole mouths chamfered; draft on moulded faces\n- [ ] Multi-body: interference zero, seats proven, `expect`s in place\n- [ ] Sketches fully defined; no `fix` relations left\n- [ ] Surfaces knitted solid; G1/G2 as required\n- [ ] Sheet metal: flat pattern verified\n- [ ] Reply states assumptions and next changes'
|
|
25143
|
+
},
|
|
25144
|
+
{
|
|
25145
|
+
id: "nurbs-and-freeform",
|
|
25146
|
+
title: "10 \u2014 NURBS and freeform",
|
|
25147
|
+
when: "Poles, degree, continuity: control nets, curves in space, matching, rebuilding",
|
|
25148
|
+
text: '# 10 \u2014 NURBS and freeform\n\nSource: Tran, *Mastering Surface Modeling* (ch.1 \xA75 splines, ch.7-9 repair);\nLand ch.11-13 (FORM/T-splines, a different paradigm \u2014 noted where it differs);\nand this system\'s own kernel, which is OpenCASCADE and speaks B-spline\nnatively.\n\n**Read 07-surfacing.md first.** That file is about *when* to reach for a\nsurface and how to knit one back into a solid. This one is about the geometry\nunderneath: poles, degree, continuity, and the tools that move them. Much of\nwhat 07 calls "none" this file calls by name \u2014 it was written before these\nexisted.\n\n## 1. What a surface here actually is\n\nEvery face this kernel builds is a NURBS patch, whether it came from an\n`extrude` or from `add_blend_surface`. It has:\n\n- a **control net** of poles, `countU` by `countV`. The surface does not pass\n through them; it is pulled toward them.\n- a **degree** in each direction. Degree 1 is faceted, 2 is a conic, **3 is the\n minimum for anything a highlight will travel across**, and above 5 a surface\n is hard to control and slow to evaluate.\n- **knots**, which say where the spans meet. `add_insert_isoparm` adds one.\n\n`surface_poles { id, face }` reads the net: the count in each direction, the\ndegree, and every pole with its `[i, j]`. **i counts along u and j along v, and\nu varies fastest.** Reading it transposed gives poles that exist, in the wrong\nplaces, which is the failure that looks like a working edit.\n\n## 2. The rule that governs pole editing: natural vs trimmed\n\nA face is **natural** when it covers its whole underlying surface, and\n**trimmed** when a boundary cuts a piece out of a larger one.\n\n**Poles can only be moved on a natural face.** Move one under a trimmed face\nand the surface slides out from under the boundary that trims it \u2014 the kernel\nrefuses, and `surface_poles` says `TRIMMED` before you try, so this costs one\nread rather than a failed build.\n\n`add_untrim { input, face }` throws the boundary away and gives you the whole\nunderlying surface back. That is destructive to the trim and is the point: do\nthe pole work, then re-trim with `add_trim_surface`.\n\n## 3. Curves in space\n\n`add_curve { poles, degree?, periodic? }` is a NURBS curve in **world space**,\nnot a sketch entity \u2014 poles as xyz triples, degree defaulting sensibly to the\npole count, `periodic: true` to close it into a loop. This is the thing 07\nlists as "none (planar sketches + helix)"; it exists.\n\nBeside it:\n\n| Want | Tool |\n|---|---|\n| A curve through measured points, in a plane | sketch `spline` (interpolates) |\n| A styled curve you shape by hand, in a plane | sketch `bezier` (control points) |\n| A curve in space | `add_curve` |\n| A screw path | `add_helix` |\n| A curve lying on a body | `project_curve { curve, onto, mode }` |\n| Longer at one end | `extend_curve` |\n| Cut at a point or another curve | `trim_curve` |\n| A parallel curve | `offset_curve` |\n\n**Spline quality is in the handles, not the point count** (Tran ch.1 \xA75). Few\nthrough-points, tangency weighted at the ends, and watch the inflections on the\ncurvature comb. A spline forced through twenty points has nineteen chances to\nwobble.\n\n## 4. Making a surface\n\n| Shape | Tool | Watch for |\n|---|---|---|\n| Skin through sections | `add_loft { profiles, solid: false }` | G1 at best across seams; add stations rather than nudging one |\n| Along a path | `add_sweep { profile, path, guide?, scaleEnd? }` | one guide rail |\n| Between two curves | `add_ruled_surface { a, b }` | straight in the ruling direction, always |\n| Through a grid of points | `add_surface_from_points { points, rows, columns, degreeMin?, degreeMax?, tolerance? }` | **a grid, not a cloud** \u2014 scattered points carry no u,v order |\n| A patch inside a loop of edges | `add_fill_face { boundary, continuity }` | `g1`/`g2` need supporting faces to reach |\n| A fillet-like skin between two edges on two bodies | `add_blend_surface { a, edgeA, b, edgeB, continuity }` | this is 07\'s missing "boundary surface", by another name |\n\n## 5. Editing a surface you already have\n\n| Intent | Tool | Note |\n|---|---|---|\n| Pull the shape | `add_move_poles { input, face, moves: [{ i, j, to }] }` | read `surface_poles` first; natural faces only; poles you do not name stay put |\n| More freedom to pull with | `add_insert_isoparm { input, face, along, parameter, multiplicity }` | adds a knot, so a new row of poles; more rows means finer control and a worse-behaved surface |\n| Change the degree, or simplify | `add_rebuild_surface { input, face, tolerance?, degreeU?, degreeV?, maxSegments? }` | an approximation \u2014 always check `analyze_surface` after |\n| Make an edge tangent to a neighbour | `add_match_surface { input, face, edge, to?, toFace?, continuity }` | this is how a G1 or G2 join is *achieved*, not merely measured |\n| Reach further | `add_extend_surface { input, face, distance }` | extends the underlying surface, then re-trim |\n| Recover the untrimmed surface | `add_untrim { input, face }` | the precondition for pole work |\n| Cut it back | `add_trim_surface` | one side kept per call |\n| Close small gaps in an import | `add_heal { input, tolerance }` | before stitching, not after |\n| Join into a shell or a solid | `add_stitch { inputs, tolerance, makeSolid }` | `makeSolid` is *attempted*; confirm with `inspect_body` |\n| Give it thickness | `add_thicken { input, thickness }` | a thin wall is a wall, not a lens \u2014 the renderer measures it |\n\n## 6. Continuity, and how to reach it rather than only measure it\n\n07 \xA72 has the table. What that file could not say is how to *fix* a join:\n\n1. `analyze_surface { id, what: "continuity" }` grades every edge G0/G1/G2.\n2. A G0 edge that should be G1: `add_match_surface` with `continuity: "g1"`,\n naming the edge and the face to match to.\n3. A G1 edge that should be G2 on visible bodywork: the same call with `"g2"`.\n It needs freedom to spend \u2014 a degree-2 surface cannot be made G2 against a\n degree-5 neighbour, so `add_rebuild_surface` first with `degreeU: 3` or more.\n4. Re-measure. A match that could not reach the grade asked for reports what it\n did reach; it does not silently succeed.\n\n**A G1 join with a curvature step draws a visible line in a reflection** and no\nedge exists to explain it. That is why G2 is worth the cost on anything styled\nand worth nothing on a bracket.\n\n## 7. Recipes\n\n### A. A styled panel that meets a neighbour without a seam\n\n1. `add_loft { solid: false }` through two or three section curves\n (`add_curve` in space, or sketches).\n2. `analyze_surface` \u2014 the shared edge reads G0.\n3. `add_match_surface { face, edge, to: neighbour, toFace, continuity: "g2" }`.\n4. `analyze_surface` again: G2 across it, and `minRadius` has not collapsed.\n5. Look at it: `render_view`, and zebra in the UI (`model.zebra`). Stripes that\n break at the join mean the numbers agreed and the eye does not.\n\n### B. Pulling a highlight straight\n\n1. `surface_poles { id, face }` \u2014 confirm it says natural.\n2. If the net is a 4\xD74 there is nothing to pull with in the middle:\n `add_insert_isoparm { along: "u", parameter: 0.5 }`.\n3. `surface_poles` again for the new indices.\n4. `add_move_poles { moves: [{ i: 2, j: 1, to: [...] }] }` \u2014 a few poles, not\n the whole net.\n5. `analyze_surface` for the curvature, `render_view` for the highlight. Repeat\n from 3; the net has moved, so its numbers are stale.\n\n### C. Repairing an import\n\n`add_heal` with a tolerance a little above the worst gap \u2192 `add_stitch\n{ makeSolid: true }` \u2192 `inspect_body`. Where a face is missing entirely,\n`add_fill_face` over its loop; where the hole is four-sided and wants to be\ntangent, `add_blend_surface` from the two edges either side.\n\n## 8. Where this system has no equivalent\n\nSay so rather than searching:\n\n- **T-splines / subdivision (Fusion FORM, Land ch.11-13).** A different\n paradigm \u2014 a control cage pushed and pulled, converted to NURBS at the end.\n Nothing here does it. The nearest is `add_surface_from_points` on a sampled\n grid, then `add_move_poles`.\n- **Guide curves on a loft.** `add_loft` takes stations; add one where the loft\n gaps rather than reaching for a rail that does not exist.\n- **Face fillet with a hold line, Deform, Flex.** None.\n- **A curve network surface in two directions.** `add_fill_face` with `g2`, or\n `add_blend_surface`, is the closest.\n\n## 9. Checklist\n\n- [ ] Degree 3 or more on anything a highlight crosses\n- [ ] `surface_poles` before `add_move_poles`, and the face reads natural\n- [ ] Poles named individually \u2014 a move sends what moved, not the net\n- [ ] `analyze_surface` after every rebuild, match or pole move\n- [ ] G2 on visible styled joins, G1 everywhere else, and looked at under zebra\n- [ ] Stitched shell confirmed solid by `inspect_body` before any boolean'
|
|
25149
|
+
},
|
|
25150
|
+
{
|
|
25151
|
+
id: "architecture",
|
|
25152
|
+
title: "11 \u2014 Buildings: levels, walls, openings, slabs, roofs",
|
|
25153
|
+
when: "Massing a building: levels, walls, openings, slabs, roofs \u2014 and the order they go in",
|
|
25154
|
+
text: '# 11 \u2014 Buildings: levels, walls, openings, slabs, roofs\n\nFor massing a building fast and showing it to somebody \u2014 a client on a phone,\na planner, yourself at the end of an hour. Not a BIM authoring tool: there is no\nclassification, no fire rating, no layered construction. What there is instead\nis five feature kinds that carry the arithmetic a building is made of, so that\nmassing a house is about twenty features rather than the hundred and\nthirty-seven it took the first time it was done here by hand.\n\nEverything these produce is ordinary B-rep geometry. A wall booleans, fillets,\nsections, measures and exports exactly like an extrude, because that is all it\nis once it has built. What the feature adds is the arithmetic that RE-SOLVES.\n\n## The order. This is most of it.\n\n**`add_level` \u2192 `add_wall` \u2192 `add_opening` \u2192 `add_slab` \u2192 `add_roof`.**\n\nFollow it and the model holds together when something moves; take it out of\norder and you are back to typing z values.\n\n1. **Levels first, always.** One `add_level` per storey \u2014 `ground`, `first`,\n `eaves` \u2014 before any geometry at all. They cost nothing, they are\n construction geometry, and they are what everything else is written against.\n2. **Walls next**, `base` and `top` written as `level("ground")` and\n `level("first")`. Not numbers. A wall whose top is `2900` is a wall that\n will not move when the storey does.\n3. **Openings after the walls they are cut into**, hosted on the wall by id.\n The second opening in a wall is hosted on the FIRST opening, not on the wall\n \u2014 see below.\n4. **Slabs**, at each level, with the boundary they need.\n5. **Roof last**, its `base` at the top of the walls it sits on.\n\nThen change one level\'s `elevation` and rebuild. Every wall on that storey, every\nopening in those walls, the slab and the roof all move together. That is the whole\npromise, and it is the reason to spend the first five features on levels.\n\n## `add_level` \u2014 a named elevation\n\n```\nadd_level { id: "ground", elevation: 0 }\nadd_level { id: "first", elevation: 2900 }\nadd_level { id: "eaves", elevation: 5600 }\n```\n\nThen `level("first")` reads 2900 in ANY numeric field, exactly like `maxz()` or\n`top()` read geometry. It is realised as a flat datum square so there is one\nmechanism for "what height is this?", it never appears as a body, and it is\nnever exported.\n\nThe failure this replaces: a house built here declared `ffl`, `lvl1`, `deck` and\n`roof_z` as parameters and typed every z in the model against one of them. It\nworks \u2014 until the first floor moves and there is no way to tell which of a\nhundred numbers meant "first floor" from which merely happened to equal it.\n\n## `add_wall` \u2014 a centreline, mitred, extruded between two elevations\n\n```\nadd_wall {\n id: "outer", closed: true, thickness: 300,\n base: "level(\\"ground\\")", top: "level(\\"first\\")",\n path: [[0,0,0],[8000,0,0],[8000,6000,0],[0,6000,0]]\n}\n```\n\n- `path` is the CENTRELINE in plan. **The z of every point is ignored** \u2014 `base`\n and `top` set the height. Repeating the first point at the end is fine and is\n dropped.\n- `closed: true` builds an outer loop AND an inner one, so four points and one\n feature is a room rather than a solid block. By hand that is a sketch, an\n extrude, a second sketch and a boolean \u2014 and the corners come out with a notch\n on the inside of every bend, because an offset that butts the corners cannot\n close them. Here the corner is the intersection of the two offset lines, which\n closes exactly.\n- `top` is an **elevation**, not a height. `level("first")` or\n `level("ground") + 2700`, never `2700` on its own.\n- `justification` moves the wall off its line: `left` and `right` are relative to\n the direction of travel, so the path becomes a face of the wall rather than its\n middle. Useful when the plan dimension you have is to the inside face.\n\nA thin wall (under 75 mm) builds and says so. A wall with no thickness, fewer\nthan two points, or a repeated point is refused \u2014 there is nothing to build.\n\n## `add_opening` \u2014 placed ALONG the wall, never at a coordinate\n\n```\nadd_opening { id: "front_door", host: "outer", along: 4000,\n width: 1000, height: 2100 }\nadd_opening { id: "w1", host: "front_door", along: 12000,\n width: 1500, height: 1200, sillHeight: 900 }\n```\n\nThere is no x, y or z, and that is the feature. `along` is the distance from the\nfirst point of the host wall\'s path, measured up its centreline; `sillHeight` is\nmeasured up from the wall\'s own base. **An opening outside its wall is not\nsomething this can express.**\n\nThat is not a nicety. A helper that placed eighteen windows here by coordinate\nmis-parenthesised a depth, and every opening on two of the four walls landed\noutside the wall it belonged to. Nothing failed. Frames and glass were built\ninside solid render, invisible, and it was found three scripts later in a render.\n\n**Host the second opening on the first.** A cut REPLACES the body it cuts, so two\nopenings both hosted on `outer` would be two overlapping walls each missing a\ndifferent hole. Chain them \u2014 `host: "front_door"` \u2014 the way a second `add_hole`\ntakes the first hole\'s id. The path is always read from the wall at the end of\nthe chain, so `along` still means the same thing.\n\nThree refusals, and they stay refusals because they are the bug this exists to\nend: `along` past the end of the path; an opening whose jambs run past the corner\nof the straight run it sits on (it would be cutting the wall that turns away);\nand a head that breaches the top of the wall. Each names the measurement. A head\nwithin 150 mm of the wall top warns and builds.\n\n## `add_slab` \u2014 floors, decks, terraces\n\n```\nadd_slab { id: "gf", boundary: [[0,0,0],[8000,0,0],[8000,6000,0],[0,6000,0]],\n level: "level(\\"ground\\")", thickness: 200 }\n```\n\nThe slab hangs **below** its level by default. That is what makes a level worth\nhaving: the level is the FINISHED FLOOR, the surface you stand on and the\nelevation every wall on that storey takes as its `base`, with the structure\nunderneath where it is on site. `offset` moves the top face off the level if you\nneed it elsewhere.\n\nGive `boundary` as plan points OR `profile` as the id of a sketch already drawn\nin plan \u2014 not both. A sketch is moved to the level; its own height does not\nmatter.\n\n## `add_roof` \u2014 the lower envelope, which is why it cannot vanish\n\n```\nadd_roof { id: "roof", boundary: [[0,0,0],[7200,0,0],[7200,10400,0],[0,10400,0]],\n base: "level(\\"eaves\\")", pitch: "30 deg",\n overhang: 400, thickness: 200 }\n```\n\nThe roof surface is\n\n z(p) = base + min over the pitched edges of distance(p, edge) * tan(pitch)\n\n\u2014 the lower envelope of one plane per pitched eave. Hip, gable, shed and pyramid\nare all this one feature:\n\n| Roof | `edges` |\n|---|---|\n| Hip | omit \u2014 every edge takes `pitch` |\n| Gable | `["30 deg", "gable", "30 deg", "gable"]` \u2014 pitch the long sides |\n| Shed / lean-to | one pitched edge, the rest `"gable"` |\n| Pyramid | omit, on a footprint too narrow to form a ridge |\n| Mixed | any angle per edge: `["40 deg", "gable", "25 deg", ""]` |\n\nAn empty string takes the default `pitch`; a short list leaves the rest at the\ndefault. Entry *i* is the edge from boundary point *i* to point *i+1*.\n\n**Why an envelope and not a cut.** A hip roof built by hand here as "cut away\neverything above a plane rising from the east eave" removed the ENTIRE roof and\nreported success \u2014 at 17.2\xB0 that plane reaches the ridge 10.4 m inboard and the\nwing was only 7.2 m wide. A subtraction that takes everything is still a\nsuccessful subtraction. An intersection cannot do that: the result is a subset of\nevery plane by construction, so too shallow a pitch gives a LOW roof and never an\nabsent one. The only way to get nothing is to pitch no edge at all, and that is\nrefused with a sentence.\n\n`thickness` is a **plumb** thickness, measured vertically; the perpendicular\ndepth of the slab is `thickness * cos(pitch)`. `overhang` pushes the eave line\nout past the footprint, and the planes rise from the overhung line, which is\nwhere a real roof starts.\n\nv1 solves **convex** footprints exactly. A non-convex one \u2014 an L-plan \u2014 still\nbuilds a valid closed solid inside its footprint, and warns: every eave is\nextended to a full plane, so a wing can end up roofed by a plane rising from an\neave on the far side of the building. Check the ridges, or build the wings as\nseparate roofs and fuse them.\n\n## A house in about twenty features\n\n1. `add_level` ground / first / eaves \u2014 3.\n2. `add_slab` ground floor \u2014 1.\n3. `add_wall` closed outer, `base: level("ground")`, `top: level("first")` \u2014 1.\n4. `add_opening` \xD7 the doors and windows, chained \u2014 1 each.\n5. `add_slab` first floor \u2014 1.\n6. `add_wall` closed outer, `base: level("first")`, `top: level("eaves")` \u2014 1.\n7. `add_opening` \xD7 upstairs \u2014 1 each.\n8. `add_wall` open internal partitions, thinner \u2014 1 each.\n9. `add_roof` at `level("eaves")` \u2014 1.\n10. `set_material` on the walls and the roof, `render_view` from three angles.\n\nThen move `level("first")` and rebuild. Everything follows.\n\n## What is NOT here, and what to use instead\n\n| Wanted | Here |\n|---|---|\n| Stair | Not a feature yet. A `patternLinear` of one tread box up a going/rise vector. |\n| Window frame and glazing | Not yet \u2014 `window` and `door` are coming. Today: `add_opening`, then a thin box or two in the reveal. |\n| Curtain wall / mullion grid | `add_wall` thin, plus a `patternLinear` of mullion boxes. |\n| Curved wall in plan | `add_wall` takes straight segments. Approximate with segments, or `add_sketch` an arc and `add_extrude`. |\n| Sloping wall | `add_wall` then `add_boolean cut` with a tilted box. A wall stands between two elevations by definition. |\n| Cavity wall / layers | Two walls on the same path, different `justification`. |\n| Terrain, site | `add_surface_from_points` and `add_thicken`. |\n| Room areas, schedules | `measure`; `inspect_body` for volumes. There is no room object. |\n\n## Checks worth running before you say done\n\n- `inspect_body` the wall: a closed wall is ONE solid with a hole through it \u2014\n if the volume is footprint \xD7 height it came out as a block and the plan is not\n a room.\n- `probe_ray` through an opening: it should pass clean through. An opening that\n built outside its wall leaves the wall solid and the ray stops.\n- Change a level\'s `elevation`, `build`, and check the walls, openings and roof\n all moved. If something stayed put, it was written with a number.\n- `render_view` from outside and from an interior camera, then 09\'s checklist.'
|
|
24354
25155
|
}
|
|
24355
25156
|
];
|
|
24356
25157
|
var DESIGN_GUIDE_TOPICS = DESIGN_GUIDE.map((topic) => topic.id);
|
|
24357
25158
|
|
|
25159
|
+
// ../doc/dist/collapse.js
|
|
25160
|
+
function collapseDocument(kernel2, json) {
|
|
25161
|
+
const started = Date.now();
|
|
25162
|
+
const doc2 = Document.fromJSON(kernel2, json);
|
|
25163
|
+
const built = doc2.rebuild();
|
|
25164
|
+
if (!built.ok) {
|
|
25165
|
+
const failed = built.features.find((feature) => !feature.ok);
|
|
25166
|
+
throw new Error(`This model does not build, so there is no solid to collapse${failed === void 0 ? "" : `: ${failed.error?.message ?? failed.id}`}`);
|
|
25167
|
+
}
|
|
25168
|
+
const absorbed = /* @__PURE__ */ new Set();
|
|
25169
|
+
let bodies = 0;
|
|
25170
|
+
for (const body of built.bodies) {
|
|
25171
|
+
try {
|
|
25172
|
+
const frozen = doc2.freeze(body.featureId);
|
|
25173
|
+
bodies += 1;
|
|
25174
|
+
for (const id of frozen.absorbed)
|
|
25175
|
+
absorbed.add(id);
|
|
25176
|
+
} catch {
|
|
25177
|
+
continue;
|
|
25178
|
+
}
|
|
25179
|
+
}
|
|
25180
|
+
const collapsed = doc2.toJSON();
|
|
25181
|
+
const idOf = (feature) => typeof feature === "object" && feature !== null && "id" in feature && typeof feature.id === "string" ? feature.id : null;
|
|
25182
|
+
const kept = collapsed.features.filter((feature) => {
|
|
25183
|
+
const id = idOf(feature);
|
|
25184
|
+
return id === null || !absorbed.has(id);
|
|
25185
|
+
}).map((feature) => {
|
|
25186
|
+
if (typeof feature !== "object" || feature === null)
|
|
25187
|
+
return feature;
|
|
25188
|
+
const { absorbed: _dropped, ...rest } = feature;
|
|
25189
|
+
return rest;
|
|
25190
|
+
});
|
|
25191
|
+
const parameters = collapsed.parameters?.length ?? 0;
|
|
25192
|
+
const { references: _drawings, ...display } = collapsed.display ?? {};
|
|
25193
|
+
return {
|
|
25194
|
+
document: {
|
|
25195
|
+
...collapsed,
|
|
25196
|
+
display,
|
|
25197
|
+
features: kept,
|
|
25198
|
+
/* EVERY parameter, and the emptiness is the point rather than a tidy-up.
|
|
25199
|
+
* A parameter is the most direct statement of design intent in the whole
|
|
25200
|
+
* document - `wheel_d / 2 + 62` says what somebody decided and why - and
|
|
25201
|
+
* none of them is referenced by a `freeze` feature, which carries bytes
|
|
25202
|
+
* and not an expression. So they are removed because they can be, and the
|
|
25203
|
+
* role's promise is kept by their absence rather than by a rule about
|
|
25204
|
+
* who may read them. */
|
|
25205
|
+
parameters: []
|
|
25206
|
+
},
|
|
25207
|
+
bodies,
|
|
25208
|
+
removed: collapsed.features.length - kept.length,
|
|
25209
|
+
parameters,
|
|
25210
|
+
ms: Date.now() - started
|
|
25211
|
+
};
|
|
25212
|
+
}
|
|
25213
|
+
|
|
24358
25214
|
// ../doc/dist/agentTools.js
|
|
24359
25215
|
var MEASURES = `
|
|
24360
25216
|
|
|
@@ -24428,10 +25284,16 @@ var ENTRIES = [
|
|
|
24428
25284
|
description: 'Applies an ordered list of add / update / remove operations, builds ONCE, checks whatever you assert about the result, and then keeps all of it or none of it.\n\nREPEAT is a LOOP, and it is what makes forty of a thing one call rather than forty. `{op:"repeat", count:8, as:"i", do:[ ... ]}` runs the operations in `do` eight times, with {i} substituted anywhere in them - ids, names and expressions alike - and {#} for the iteration number. `over:[10,25,40]` runs over VALUES instead of a count, which is what most real repetition is: hole diameters, station offsets, a bolt circle.\n\nPUT THE WHOLE ITERATION IN `do`, not one feature. A rib is a sketch AND an extrude AND a fillet, and each can name the one before it by its interpolated id - `{"id":"rib{i}", "kind":"extrude", "profile":"sk{i}"}`. That is the point of the loop: splitting it into one loop per stage does not work, because a feature can only measure something built BEFORE it and the fillet of iteration one would come after the extrude of iteration forty.\n\nEVERY REPEATED `add` NEEDS AN ID THAT VARIES - "rib{i}", not "rib". Forty features with one name cannot be told apart afterwards by an assertion, an update or a person, and the batch is refused rather than applied. An expression keeps its parameters: `"x":"i * pitch"` becomes `"3 * pitch"`, still tied to `pitch`, not flattened to a number.\n\nREACH FOR THIS whenever the next thing you are about to build is more than one feature and you already know what all of them are: a sketch, its extrude and the two fillets on it; a bracket and the four holes through it; a pattern and the boolean that merges it back. What it saves is round trips, which are the expensive thing here - a chainring cost eight calls and a bicycle over two hundred, each one carrying back a full report about a part that was not finished yet.\n\nASSERT what you expect rather than reading it back afterwards. `solid` catches the open shell or the sliver the moment the boolean makes it, which is the only moment the cause is still obvious; `clear` catches the part you placed from numbers read off geometry you did not build, which is where parts end up passing through bodywork; `volume` and `bbox` catch the extrude that went the wrong way or took the wrong profile, and cost nothing to state.\n\nALWAYS ASSERT VOLUME, and do not let bbox stand in for it. A pedal was asserted with `bbox`, `solid` AND `clear`; all three passed, and the pedal had been sliced nearly in half by a bore wider than the plate was thick. It was still a closed solid, still exactly the right size on the outside, still clear of everything - and 8% light. A bounding box is blind to every interior mistake there is. Volume is not, and `components` catches the other half of the same story: a body that should be one solid and is quietly two.\n\nASSERT WHAT TOUCHES WHAT once a part is placed against another. `touching`, `clearance` and `concentric` are what overlap cannot say: a bolted joint and a part floating 2 mm off its mount share no volume either way, so `clear` passes on both. A chainring hung 2 mm off its spider through an entire model that every other check called clean.\n\nALL OR NOTHING, and that is the point. If any operation is rejected, or the model will not build, or any assertion fails, the whole batch is rolled back and the model is exactly what it was before the call - so a batch that fails leaves you nothing to clean up and nothing to guess about. The reply names the operation or assertion that failed and what the model looked like at that point.\n\n`build` is the older, narrower form of this: it only ADDS, and it cannot assert. Use it when a batch is a run of new features and nothing else; use this one to edit or delete in the same unit, or whenever you have something to assert.',
|
|
24429
25285
|
declaredBy: ["mcp", "app", "web"]
|
|
24430
25286
|
},
|
|
25287
|
+
{
|
|
25288
|
+
name: "name_model",
|
|
25289
|
+
summary: "Name the model, if nobody has named it already",
|
|
25290
|
+
description: 'Gives the model a name in the person\u2019s library. Call it once you know what you are building - after the first few features, not before - and give it the name a person would use to find this part again.\n\nWHY YOU AND NOT THE APP. A new model is called "Untitled" and stays that way: the person who asked for it is describing a part, not filing it, and a library of eleven "Untitled" cards is what that produces. You are the only party that knows what was asked for and what got built, so you are the one that can name it.\n\nNAME IT FROM THE THING, NOT FROM THE OPERATIONS. "Solar commuter frame", "Hub motor housing", "Dropout jig" - what the part IS. Not "Extrude and 6 fillets", not "Model 1", not the date. Short enough to read on a card: two or three words.\n\nIT WILL NOT OVERWRITE A PERSON\u2019S NAME. The server refuses if the model has ever been named by hand - including if somebody deliberately called it "Untitled" - and says so instead of failing. That refusal is a normal outcome: do not retry it, do not work around it, and do not ask the person to rename it back.\n\nIT NEEDS A CONNECTED MODEL. This writes to the person\u2019s library through the Edit link this session is attached to, so it does nothing for a model that lives only in this process. connect_model is what attaches one.',
|
|
25291
|
+
declaredBy: ["mcp"]
|
|
25292
|
+
},
|
|
24431
25293
|
{
|
|
24432
25294
|
name: "connect_model",
|
|
24433
25295
|
summary: "Point this session at a model in the app, from a pasted link",
|
|
24434
|
-
description: "Takes a share link the person copied out of the app and connects this session to the model behind it. From then on every change built here is published to that model, and a window with Follow set to Live shows it as it happens.\n\nTHE LINK IS THE WHOLE CONFIGURATION. It says which app and which model, so there is nothing else to ask for and nothing to set up: paste https://3d.obenor.com/s/XXXXXXXX and the connection is made.\n\nIT MUST BE AN EDIT LINK. A View, Measure or Comment link can read a model and cannot write one, and the refusal says which kind was given. Ask the person to use the Share sheet\u2019s Edit role.\n\nTHE CONNECTION LASTS AS LONG AS THIS PROCESS, and the link is never written to disk. Restarting the server means pasting again - which is the right trade for a credential, and the alternative is the OBENOR_LINK environment variable, which survives a restart because the person put it there themselves.\n\nA link is revoked and expires like any other. When one stops working the reply carries the app\u2019s own sentence - revoked, expired, or a model that is gone - so the fix is to make a new link rather than to debug this tool.",
|
|
25296
|
+
description: "Takes a share link the person copied out of the app and connects this session to the model behind it. From then on every change built here is published to that model, and a window with Follow set to Live shows it as it happens.\n\nConnecting loads the linked model into this session WITHOUT publishing the previous document into it. Save local work before switching models. Reconnecting the same link reuses the session. Switching releases only this process\u2019s previous agent slot. Use mirror status for connection/publication status, and mirror push to await publication.\n\nTHE LINK IS THE WHOLE CONFIGURATION. It says which app and which model, so there is nothing else to ask for and nothing to set up: paste https://3d.obenor.com/s/XXXXXXXX and the connection is made.\n\nIT MUST BE AN EDIT LINK. A View, Measure or Comment link can read a model and cannot write one, and the refusal says which kind was given. Ask the person to use the Share sheet\u2019s Edit role.\n\nTHE CONNECTION LASTS AS LONG AS THIS PROCESS, and the link is never written to disk. Restarting the server means pasting again - which is the right trade for a credential, and the alternative is the OBENOR_LINK environment variable, which survives a restart because the person put it there themselves.\n\nA link is revoked and expires like any other. When one stops working the reply carries the app\u2019s own sentence - revoked, expired, or a model that is gone - so the fix is to make a new link rather than to debug this tool.",
|
|
24435
25297
|
declaredBy: ["mcp"]
|
|
24436
25298
|
},
|
|
24437
25299
|
{
|
|
@@ -24503,6 +25365,25 @@ var ENTRIES = [
|
|
|
24503
25365
|
description: "Measures how a body actually bends, which is the one thing topology and a bounding box cannot tell you.\n\nCURVATURE. For each face, the smallest sphere that fits its surface (minRadius) and where that is. This is what says whether a shape can be pressed, whether a fillet of a given size will fit without eating itself, and whether a panel meant to read as convex has gone saddle-shaped somewhere - which is where a highlight breaks on a real car.\n\nCONTINUITY. For each edge shared by two faces, how smoothly they meet: G0 is a crease, G1 means the tangent planes agree, G2 means the curvature agrees too. query_geometry already answers G1 - a tangent edge is one. G2 is only available here, and it is the level that decides whether a body reads as one surface or as panels stuck together: a tangent join with a curvature step has no crease you can feel and still draws a visible line in a reflection.\n\nUSE IT ON LOFTED BODYWORK. A loft through sketch stations is G1 at best across its seams, and nothing in the model says so until you measure. Also use it before filleting a curved face: a fillet larger than the local minRadius will fail, and the error will blame the fillet rather than the surface.",
|
|
24504
25366
|
declaredBy: ["mcp", "app", "web"]
|
|
24505
25367
|
},
|
|
25368
|
+
{
|
|
25369
|
+
name: "surface_poles",
|
|
25370
|
+
summary: "A face's control net",
|
|
25371
|
+
description: `Reads the poles a NURBS face is DEFINED by - the control net a surface modeller pulls, with each pole's (i, j) and position. This is the read that makes add_move_poles aimable: that feature takes moves as {i, j, to}, and without the net there is nothing to compute an i, a j or a "to" from.
|
|
25372
|
+
|
|
25373
|
+
i counts along u and j along v, both from zero. u varies fastest, which is the one thing worth getting right: the net is a grid and reading it transposed gives poles that exist, in the wrong places.
|
|
25374
|
+
|
|
25375
|
+
TRIMMED FACES ARE REFUSED, and this says so before you try. A face that covers only part of its surface cannot have its poles moved - the surface would slide out from under the boundary trimming it - so the answer names that here rather than letting the build fail a step later. add_untrim makes such a face natural.
|
|
25376
|
+
|
|
25377
|
+
Degree and periodicity come back with it, because both decide what a move does: a move on a degree-3 net moves four spans, and a periodic net wraps.`,
|
|
25378
|
+
/* Not the V1 app. That surface is deliberately behind - `design_guide`,
|
|
25379
|
+
* `name_model` and eight others are not on it either - and this is a read
|
|
25380
|
+
* for the NURBS tools, none of which the V1 agent can add. Adding it there
|
|
25381
|
+
* would be a tool with nothing to aim.
|
|
25382
|
+
*
|
|
25383
|
+
* `web` reaches the hosted relay as well: `packages/api/src/agent/mcp.ts`
|
|
25384
|
+
* declares nothing of its own and asks the tab what it has. */
|
|
25385
|
+
declaredBy: ["mcp", "web"]
|
|
25386
|
+
},
|
|
24506
25387
|
{
|
|
24507
25388
|
name: "probe_ray",
|
|
24508
25389
|
summary: "Fire a ray through the model",
|
|
@@ -24644,7 +25525,7 @@ var ENTRIES = [
|
|
|
24644
25525
|
{
|
|
24645
25526
|
name: "design_guide",
|
|
24646
25527
|
summary: "How to design a part here",
|
|
24647
|
-
description: `The design knowledge base, distilled from five CAD textbooks into this system's feature kinds and tools. Call it with no topic for the index and the ten rules every book agrees on; call it with a topic before the work that topic covers: "design-intent" before the first feature (plan the tree, pick the base feature, decide what should flex), "sketching" for relations, dimensions and a fully defined profile, "features-and-order" for end conditions, sweep and loft rules and the order of draft, fillets and shell, "fillets-and-blends", "assemblies-and-joints", "sheet-metal", "surfacing", "common-parts-recipes" for a bracket, plate, pulley, housing, shaft, tray, hook or knob step by step, and "checking-a-design" before you say it is done. Each recipe names the tool to call and says where this system has no equivalent, so you do not go looking for a rib feature or a hole wizard that is not there. It is text, costs no build, and is cheaper than a wrong first feature.`,
|
|
25528
|
+
description: `The design knowledge base, distilled from five CAD textbooks into this system's feature kinds and tools. Call it with no topic for the index and the ten rules every book agrees on; call it with a topic before the work that topic covers: "design-intent" before the first feature (plan the tree, pick the base feature, decide what should flex), "sketching" for relations, dimensions and a fully defined profile, "features-and-order" for end conditions, sweep and loft rules and the order of draft, fillets and shell, "fillets-and-blends", "assemblies-and-joints", "sheet-metal", "surfacing", "nurbs-and-freeform" for poles, degree, continuity and the tools that move them, "architecture" BEFORE the first wall of a building - levels, walls, openings, slabs and roofs, and the order they go in, which is most of what makes a building hold together when a storey height changes - "common-parts-recipes" for a bracket, plate, pulley, housing, shaft, tray, hook or knob step by step, and "checking-a-design" before you say it is done. Each recipe names the tool to call and says where this system has no equivalent, so you do not go looking for a rib feature or a hole wizard that is not there. It is text, costs no build, and is cheaper than a wrong first feature.`,
|
|
24648
25529
|
declaredBy: ["mcp", "web"]
|
|
24649
25530
|
},
|
|
24650
25531
|
{
|
|
@@ -24784,6 +25665,8 @@ for (; ; ) {
|
|
|
24784
25665
|
let result;
|
|
24785
25666
|
if (method === "__reset") {
|
|
24786
25667
|
result = reset2(args[0]);
|
|
25668
|
+
} else if (method === "__collapse") {
|
|
25669
|
+
result = collapseDocument(kernel, args[0]);
|
|
24787
25670
|
} else if (method === "__info") {
|
|
24788
25671
|
result = {
|
|
24789
25672
|
version: kernel.version,
|