partforge 0.117.1 → 0.119.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/docs/AUTHORING-PARTS.md +52 -11
- package/docs/KERNEL-CONTRACT.md +3 -3
- package/package.json +1 -1
- package/src/framework/app.css +32 -7
- package/src/framework/geometry/contour-ops.js +41 -6
- package/types/geometry.d.ts +5 -3
- package/types/kernel.d.ts +19 -4
package/docs/AUTHORING-PARTS.md
CHANGED
|
@@ -1060,12 +1060,41 @@ export function tilePicker(host) {
|
|
|
1060
1060
|
listener you attach yourself with `addEventListener`, rather than through `host.h`,
|
|
1061
1061
|
is **not** guarded — wire listeners through `host.h`, or wrap your own in try/catch.
|
|
1062
1062
|
|
|
1063
|
-
**Looking native.** The slot inherits the rail's font,
|
|
1064
|
-
|
|
1065
|
-
|
|
1063
|
+
**Looking native.** The slot inherits the rail's font and text colour, and the
|
|
1064
|
+
built-in looks come for free: bare `<button>`, `<input>` and `<select>` elements
|
|
1065
|
+
are already styled, the built-in classes are there by name for the exact thing —
|
|
1066
1066
|
`row`, `seg` (a segmented row of buttons), `action`, `ghost`, `num`, `text-input`,
|
|
1067
|
-
`select-input
|
|
1068
|
-
|
|
1067
|
+
`select-input` — and sub-controls mounted through `host.controls` *are* the
|
|
1068
|
+
built-in widgets. For SVG there are two classes: `pf-hit` (a clickable region,
|
|
1069
|
+
with a `selected` state) and `pf-drag`.
|
|
1070
|
+
|
|
1071
|
+
**Colour only with the rail's tokens.** Inheriting the rail's colours covers the
|
|
1072
|
+
text you did not style; it does not cover anything you colour yourself. For that,
|
|
1073
|
+
use a `--pf-*` custom property and never a hex literal or a named colour — the
|
|
1074
|
+
tokens flip with the light/dark theme and a literal cannot, so a `#333` border is
|
|
1075
|
+
a widget that is unreadable in one of the two themes. The ones worth knowing:
|
|
1076
|
+
`var(--pf-text)` and `var(--pf-text-2)` for text, `var(--pf-muted)` for a
|
|
1077
|
+
secondary label, `var(--pf-border)` for a rule or an outline,
|
|
1078
|
+
`var(--pf-surface-2)` for a filled chip, `var(--pf-accent)` with
|
|
1079
|
+
`var(--pf-on-accent)` for the selected or primary thing, and `var(--pf-err)` for
|
|
1080
|
+
a problem. That palette is the whole palette: the accent blue for the one thing
|
|
1081
|
+
that is chosen or primary, the surface and border greys for everything else. A
|
|
1082
|
+
widget that reaches past it for a red or a green is a widget that has stopped
|
|
1083
|
+
looking like the rest of the panel.
|
|
1084
|
+
|
|
1085
|
+
**Nest a region's label inside the region.** The common case then needs no colour
|
|
1086
|
+
from you at all: an SVG shape you leave unfilled inherits `var(--pf-text-2)`
|
|
1087
|
+
rather than SVG's own black, a `pf-hit` region is filled and outlined at rest,
|
|
1088
|
+
and a `<text>` *inside* that region stays readable through all three states —
|
|
1089
|
+
the rail gives it `var(--pf-text-2)` at rest and `var(--pf-on-accent)` once the
|
|
1090
|
+
region is selected and has gone solid accent underneath it, and clears the
|
|
1091
|
+
stroke it would otherwise inherit from the region's outline (SVG paints that
|
|
1092
|
+
around every glyph, which at label sizes leaves a pale ghost of the number).
|
|
1093
|
+
Draw the label as a sibling instead and none of that reaches it: a dark number
|
|
1094
|
+
sitting on the selected blue is the result, and you have to colour it yourself.
|
|
1095
|
+
To colour a
|
|
1096
|
+
region from your own data, set an inline `style` — a `fill="…"` attribute loses
|
|
1097
|
+
to the rail's rule.
|
|
1069
1098
|
|
|
1070
1099
|
**Reading the part's own files.** Artwork can live beside the code (the tree is text,
|
|
1071
1100
|
so an SVG, a `partforge-vector` JSON document or a JSON data file — not a PNG).
|
|
@@ -1505,20 +1534,32 @@ its coordinates, and the max that would work — rather than silently clamping:
|
|
|
1505
1534
|
|---|---|
|
|
1506
1535
|
| `filletProfile(input, r, opts?)` | `r`: number, or an array matched positionally with `opts.corners.indices` |
|
|
1507
1536
|
| `chamferProfile(input, dist, opts?)` | symmetric setback, straight connector |
|
|
1508
|
-
| `profileCorners(input)` | `[{index, point, interiorAngleDeg, convex, segTypes}]` |
|
|
1537
|
+
| `profileCorners(input)` | `[{index, position, point, interiorAngleDeg, convex, segTypes}]` — `position` is the entry's place in this list, `index` the joint's vertex number within its contour |
|
|
1509
1538
|
|
|
1510
1539
|
`opts.corners` selects which corners an op touches (default `"all"`):
|
|
1511
1540
|
|
|
1512
1541
|
- `"all"` · `"convex"` · `"concave"`
|
|
1513
|
-
- `{indices: [...]}` —
|
|
1514
|
-
with an array `r`/`dist` for per-corner radii (the
|
|
1515
|
-
|
|
1516
|
-
|
|
1542
|
+
- `{indices: [...]}` — each corner's **`position`** in `profileCorners(input)`'s return
|
|
1543
|
+
list, never its `index`; pair with an array `r`/`dist` for per-corner radii (the
|
|
1544
|
+
`roundedProfile` pattern). Any entry out of range throws, naming the range.
|
|
1545
|
+
- `{near: [x,y], count?: 1, within?: mm}` — nearest-corner selection; the hook for a
|
|
1546
|
+
human pick or an agent resolving "the top-left corner" from bbox reasoning. **Without
|
|
1547
|
+
`within` the nearest corner is always selected, however far away** — `count: 4`
|
|
1548
|
+
applied around a small pocket rounds the four corners nearest it, which may all
|
|
1549
|
+
belong to the outer wall. Pass `within` when the pick must be local; a `near` with
|
|
1550
|
+
nothing inside `within` throws instead of reaching further.
|
|
1551
|
+
|
|
1552
|
+
**`index` and `position` are different numbers.** `index` is the vertex number within
|
|
1553
|
+
the contour; `position` is the corner's place in the returned list. They agree only
|
|
1554
|
+
while every joint is a corner — the moment a contour has a smooth joint (a collinear
|
|
1555
|
+
midpoint, a G1 arc-to-line join, a fillet already applied) the vertex numbers skip
|
|
1556
|
+
ahead of the positions, and `.map((c) => c.index)` fed to `{indices}` fillets the
|
|
1557
|
+
wrong corners or throws out of range. Always map to `c.position`.
|
|
1517
1558
|
|
|
1518
1559
|
```js
|
|
1519
1560
|
// Fillet only the two corners nearest the profile's top edge, 3mm and 1.5mm:
|
|
1520
1561
|
const corners = profileCorners(outline);
|
|
1521
|
-
const top = corners.filter((c) => c.point[1] > 20).map((c) => c.
|
|
1562
|
+
const top = corners.filter((c) => c.point[1] > 20).map((c) => c.position); // position, NOT index
|
|
1522
1563
|
const rounded = filletProfile(outline, [3, 1.5], { corners: { indices: top } });
|
|
1523
1564
|
|
|
1524
1565
|
// Fillet every convex corner of a Shape2D by the same amount:
|
package/docs/KERNEL-CONTRACT.md
CHANGED
|
@@ -614,9 +614,9 @@ backwards for the holes (see the migration note below).
|
|
|
614
614
|
| `simple()` | `toRegions()` unwrapped — throws unless the result is exactly one region. |
|
|
615
615
|
| `regions()` | Scission: each disjoint region as its own live `Shape2D[]` (each further boolean-able), vs `toRegions()` which returns raw `{outer, holes}` data. Goes through `toRegions()`, so the pieces are tessellated at the backend's LOD — curves do not survive scission. |
|
|
616
616
|
| `translate([dx,dy])` / `rotate(deg, center?)` / `scale(f\|[sx,sy], center?)` / `mirror(axis)` | Rigid/similarity transforms on the contours (curve-preserving). `center` defaults to the origin; `axis` is `"x"`, `"y"`, or `{point, dir}`. `scale`'s factor is uniform when a bare number, per-axis when `[sx,sy]`. |
|
|
617
|
-
| `fillet(r, {corners?})` / `chamfer(d, {corners?})` | Round (true arcs) or bevel (straight chords) selected corners. `corners` = `"all"` (default) / `"convex"` / `"concave"` / `{indices}` / `{near, count?}`; `r`/`d` may be an array paired positionally with `{indices}`. Throws when no corner matches. |
|
|
617
|
+
| `fillet(r, {corners?})` / `chamfer(d, {corners?})` | Round (true arcs) or bevel (straight chords) selected corners. `corners` = `"all"` (default) / `"convex"` / `"concave"` / `{indices}` / `{near, count?, within?}`; `r`/`d` may be an array paired positionally with `{indices}`. `{indices}` entries are each corner's `position` in `corners()` and any entry out of range throws (never silently dropped). `{near}` takes the `count` (default 1) nearest corners; without `within` (mm) the nearest is always selected however far away, with it a pick that finds nothing inside the radius throws. Throws when no corner matches. |
|
|
618
618
|
| `simplify(tolerance)` | Corner-preserving decimation/refit within `tolerance` mm — dense point rings become fewer segments (and refit arcs/cubics) without moving corners. |
|
|
619
|
-
| `corners()` | The corner list — `{index, point, interiorAngleDeg, convex, segTypes}[]`.
|
|
619
|
+
| `corners()` | The corner list — `{index, position, point, interiorAngleDeg, convex, segTypes}[]`. `position` is the entry's place in this list and is what `fillet`/`chamfer`'s `{indices}` selects by; `index` is the joint's vertex number within its own contour (what smooth joints are skipped from), and the two diverge past any smooth joint. |
|
|
620
620
|
| `contains([x,y])` | Point-in-shape test (inside an outer, not inside a hole). |
|
|
621
621
|
| `isEmpty()` | `true` when the shape has no regions at all — a `cut`/`intersect` legitimately removed everything. Pure JS on the stored IR, backend-identical. See "Empty shapes" below. |
|
|
622
622
|
| `extrude({h, twist?, scaleTop?})` | Sugar for `k.extrude({profile: this, …})` → `Solid`. Throws on an empty shape (see "Empty shapes"). |
|
|
@@ -766,7 +766,7 @@ being single-contour by nature, they throw on a region. The full set: `translate
|
|
|
766
766
|
| | `mirrorProfile(input, axis)` | `axis: "x" \| "y" \| {point, dir}` |
|
|
767
767
|
| Corners | `filletProfile(input, r, opts?)` | `r` may be an array paired with `{indices}` |
|
|
768
768
|
| | `chamferProfile(input, dist, opts?)` | symmetric setback, straight connector |
|
|
769
|
-
| | `profileCorners(input)` | `[{index, point, interiorAngleDeg, convex, segTypes}]` |
|
|
769
|
+
| | `profileCorners(input)` | `[{index, position, point, interiorAngleDeg, convex, segTypes}]`; `{indices}` selects by `position` |
|
|
770
770
|
| Queries | `profileLength(contour)` | mm; single contour only |
|
|
771
771
|
| | `profilePointAt(contour, {t} \| {length})` | single contour only |
|
|
772
772
|
| | `profileTangentAt(contour, {t} \| {length})` | unit vector; single contour only |
|
package/package.json
CHANGED
package/src/framework/app.css
CHANGED
|
@@ -387,10 +387,10 @@ button.action:disabled { opacity: .5; cursor: default; }
|
|
|
387
387
|
/* Custom controls (type: "custom"): a part-authored widget in the rail. The
|
|
388
388
|
slot inherits the rail's cascade (font, colours, --pf-* tokens); these rules
|
|
389
389
|
give BARE elements the built-in controls' look so a widget written with
|
|
390
|
-
plain <button>/<input>/<select> still reads as native,
|
|
391
|
-
opt-in classes for SVG: .pf-hit
|
|
392
|
-
dragged one — touch-action: none is
|
|
393
|
-
instead of scrolling the sheet). */
|
|
390
|
+
plain <button>/<input>/<select> still reads as native, supply a themed
|
|
391
|
+
floor for hand-drawn SVG, and provide two opt-in classes for SVG: .pf-hit
|
|
392
|
+
(a clickable region) and .pf-drag (a dragged one — touch-action: none is
|
|
393
|
+
what lets a finger drag it on a phone instead of scrolling the sheet). */
|
|
394
394
|
.pf-custom { margin: 9px 0; }
|
|
395
395
|
.pf-custom.hidden { display: none; }
|
|
396
396
|
.pf-custom-slot { display: flow-root; }
|
|
@@ -400,7 +400,12 @@ button.action:disabled { opacity: .5; cursor: default; }
|
|
|
400
400
|
like .text-input (0,1,0) or .row .num (0,2,0). At true zero specificity,
|
|
401
401
|
any class rule wins, so a named built-in class (.seg button, .text-input,
|
|
402
402
|
select.select-input, …) always beats these bare-element defaults. */
|
|
403
|
-
|
|
403
|
+
/* `fill` inherits in SVG, so this is the floor for everything the widget draws
|
|
404
|
+
and did not colour — including <text>. SVG's own initial fill is BLACK, which
|
|
405
|
+
is invisible on the dark theme, and that is what an author gets for free
|
|
406
|
+
unless the rail supplies a token. Inherited rather than matched, so a child
|
|
407
|
+
carrying its own fill="…" attribute still wins. */
|
|
408
|
+
:where(.pf-custom) :where(svg) { display: block; max-width: 100%; height: auto; fill: var(--pf-text-2); }
|
|
404
409
|
:where(.pf-custom) :where(button) {
|
|
405
410
|
font: inherit; font-size: 12px; padding: 5px 9px; cursor: pointer;
|
|
406
411
|
background: transparent; border: 1px solid var(--pf-border); color: var(--pf-text-2);
|
|
@@ -417,9 +422,29 @@ button.action:disabled { opacity: .5; cursor: default; }
|
|
|
417
422
|
outline: none; border-color: var(--pf-accent);
|
|
418
423
|
box-shadow: 0 0 0 3px color-mix(in oklab, var(--pf-accent) 35%, transparent);
|
|
419
424
|
}
|
|
420
|
-
|
|
425
|
+
/* A region the reader clicks. The RESTING fill and stroke are what make it read
|
|
426
|
+
as a control: without them a .pf-hit polygon is a black slab with no border
|
|
427
|
+
until it is hovered or selected. Zero specificity, so a widget that colours
|
|
428
|
+
regions from its own data still wins — with an inline `style`, since a
|
|
429
|
+
`fill="…"` attribute loses to any rule that matches the element, even this
|
|
430
|
+
one. Hover and selected below keep their own (0,2,x) weight. */
|
|
431
|
+
:where(.pf-custom) :where(.pf-hit) { cursor: pointer; fill: var(--pf-surface-2); stroke: var(--pf-border); }
|
|
421
432
|
.pf-custom .pf-hit:hover { fill: color-mix(in oklab, var(--pf-accent) 25%, transparent); }
|
|
422
|
-
|
|
433
|
+
/* Selected is the SOLID accent, not a wash of it. A translucent accent lands on a
|
|
434
|
+
different value in each theme — pale over the light background, dark over the
|
|
435
|
+
dark one — so no single label colour is readable on both. Solid accent is one
|
|
436
|
+
colour in both, which is what makes --pf-on-accent below a rule rather than a
|
|
437
|
+
guess. A widget that would rather tint its own region overrides with `style`. */
|
|
438
|
+
.pf-custom .pf-hit.selected { fill: var(--pf-accent); stroke: var(--pf-accent); }
|
|
439
|
+
/* A label NESTED inside the region — the structure to recommend, because it is the
|
|
440
|
+
only one where the rail can keep the text readable as the region changes colour
|
|
441
|
+
underneath it. `fill` inherits, so without these the label takes the region's OWN
|
|
442
|
+
fill: grey on grey at rest, and accent on accent once selected. `stroke` inherits
|
|
443
|
+
too, and that one is worse than it looks: the region's outline colour gets painted
|
|
444
|
+
around every glyph, which at label sizes is most of the glyph — pale ghosts of the
|
|
445
|
+
numbers at rest, blue ones when selected. Hence stroke: none, not just a fill. */
|
|
446
|
+
:where(.pf-custom) :where(.pf-hit) text { fill: var(--pf-text-2); stroke: none; }
|
|
447
|
+
.pf-custom .pf-hit.selected text { fill: var(--pf-on-accent); }
|
|
423
448
|
.pf-custom .pf-drag { touch-action: none; }
|
|
424
449
|
.pf-custom-error {
|
|
425
450
|
margin: 4px 0; padding: 6px 8px; border: 1px solid var(--pf-err); border-radius: var(--pf-radius-control);
|
|
@@ -188,15 +188,23 @@ function contourCorners(contour) {
|
|
|
188
188
|
return out;
|
|
189
189
|
}
|
|
190
190
|
|
|
191
|
+
// Two numbers on every corner, and they are NOT interchangeable. `index` is the joint's
|
|
192
|
+
// vertex number within its own contour (what buildCornerOpRing, simplify and the lofts
|
|
193
|
+
// key on); `position` is the corner's place in THIS returned list — the one
|
|
194
|
+
// `{corners: {indices}}` selects by. They diverge as soon as a contour has a smooth
|
|
195
|
+
// joint (a collinear midpoint, a G1 arc-line join), and on region input `index`
|
|
196
|
+
// restarts per ring while `position` runs on through the flattened order.
|
|
197
|
+
const withPositions = (corners) => corners.map((c, position) => ({ ...c, position }));
|
|
198
|
+
|
|
191
199
|
export function profileCorners(input) {
|
|
192
200
|
const { kind, regions } = liftProfile(input);
|
|
193
|
-
if (kind === "points" || kind === "contour") return contourCorners(regions[0].outer);
|
|
201
|
+
if (kind === "points" || kind === "contour") return withPositions(contourCorners(regions[0].outer));
|
|
194
202
|
const out = [];
|
|
195
203
|
regions.forEach((rg, regionIndex) => {
|
|
196
204
|
for (const c of contourCorners(rg.outer)) out.push({ regionIndex, ring: "outer", ...c });
|
|
197
205
|
rg.holes.forEach((h, hi) => { for (const c of contourCorners(h)) out.push({ regionIndex, ring: { hole: hi }, ...c }); });
|
|
198
206
|
});
|
|
199
|
-
return out;
|
|
207
|
+
return withPositions(out);
|
|
200
208
|
}
|
|
201
209
|
|
|
202
210
|
// ── Fillet / chamfer ─────────────────────────────────────────────────────────
|
|
@@ -205,6 +213,19 @@ export function profileCorners(input) {
|
|
|
205
213
|
// indexes this array POSITIONALLY, matching profileCorners' documented contract).
|
|
206
214
|
// r/dist may be an array paired positionally with {indices}; every other selector
|
|
207
215
|
// broadcasts the scalar to every match. Throws when nothing matches.
|
|
216
|
+
//
|
|
217
|
+
// {indices} REJECTS any entry outside 0…corners.length-1 rather than dropping it.
|
|
218
|
+
// It used to filter silently, and the author who wrote `.map((c) => c.index)` —
|
|
219
|
+
// a vertex number, not a position — then filleted the wrong corners with no error
|
|
220
|
+
// once the out-of-range ones fell away (partforge-cloud feedback #95: ~15 applies
|
|
221
|
+
// chasing an L-bend whose arcs would not come out concentric). A throw that names
|
|
222
|
+
// the two fields is the coaching that ends that loop on the first attempt.
|
|
223
|
+
//
|
|
224
|
+
// {near} takes an optional `within` (mm). WITHOUT it the nearest `count` corners
|
|
225
|
+
// are always selected, however far away — "round the four outer corners" applied per
|
|
226
|
+
// compartment happily rounds the four corners nearest a small pocket that touches
|
|
227
|
+
// none of them. `within` turns a far pick into the same no-match error as any
|
|
228
|
+
// other empty selector.
|
|
208
229
|
function resolveCornerSelector(corners, param, opts, label) {
|
|
209
230
|
const sel = (opts && opts.corners) ?? "all";
|
|
210
231
|
const isArrayParam = Array.isArray(param);
|
|
@@ -218,14 +239,28 @@ function resolveCornerSelector(corners, param, opts, label) {
|
|
|
218
239
|
const perCorner = isArrayParam ? param : null;
|
|
219
240
|
if (perCorner && perCorner.length !== sel.indices.length)
|
|
220
241
|
throw new Error(`${label}: per-corner radius array has ${perCorner.length} entries but {indices} has ${sel.indices.length}`);
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
.
|
|
242
|
+
const n = corners.length;
|
|
243
|
+
picked = sel.indices.map((idx, j) => {
|
|
244
|
+
if (!Number.isInteger(idx))
|
|
245
|
+
throw new Error(`${label}: {indices} entry ${idx} is not an integer — {indices} takes each corner's \`position\` from profileCorners()`);
|
|
246
|
+
if (idx < 0 || idx >= n)
|
|
247
|
+
throw new Error(
|
|
248
|
+
`${label}: {indices} entry ${idx} is out of range — profileCorners() reported ${n} corner${n === 1 ? "" : "s"} (positions 0…${Math.max(0, n - 1)}). ` +
|
|
249
|
+
"{indices} takes each corner's `position` (its place in that list), not its `index` (its vertex number within the contour)",
|
|
250
|
+
);
|
|
251
|
+
return { corner: corners[idx], param: perCorner ? perCorner[j] : param };
|
|
252
|
+
});
|
|
224
253
|
} else if (sel && Array.isArray(sel.near)) {
|
|
225
254
|
const [nx, ny] = sel.near;
|
|
226
255
|
const count = sel.count ?? 1;
|
|
256
|
+
const within = sel.within;
|
|
257
|
+
if (within !== undefined && !(Number.isFinite(within) && within > 0))
|
|
258
|
+
throw new Error(`${label}: {near} within must be a positive number of mm (got ${within})`);
|
|
227
259
|
const distSq = (c) => (c.point[0] - nx) ** 2 + (c.point[1] - ny) ** 2;
|
|
228
|
-
|
|
260
|
+
const candidates = within === undefined ? corners.slice() : corners.filter((c) => distSq(c) <= within * within);
|
|
261
|
+
if (candidates.length === 0)
|
|
262
|
+
throw new Error(`${label}: no corner within ${within}mm of (${nx}, ${ny}) — the nearest is ${Math.sqrt(Math.min(...corners.map(distSq))).toFixed(3)}mm away`);
|
|
263
|
+
picked = candidates.sort((a, b) => distSq(a) - distSq(b)).slice(0, count).map((corner) => ({ corner, param }));
|
|
229
264
|
} else picked = [];
|
|
230
265
|
if (picked.length === 0) throw new Error(`${label}: no corner matched selector ${JSON.stringify(sel)}`);
|
|
231
266
|
return picked;
|
package/types/geometry.d.ts
CHANGED
|
@@ -175,9 +175,11 @@ export function filletProfile(input: ProfileInput, r: number | number[], opts?:
|
|
|
175
175
|
export function chamferProfile(input: ProfileInput, dist: number | number[], opts?: { corners?: CornerSelector }): ProfileInput;
|
|
176
176
|
|
|
177
177
|
/**
|
|
178
|
-
* The corner list — `{index, point, interiorAngleDeg, convex,
|
|
179
|
-
* plus `{regionIndex, ring}` for region/regions input.
|
|
180
|
-
* is what `filletProfile`/`chamferProfile`'s
|
|
178
|
+
* The corner list — `{index, position, point, interiorAngleDeg, convex,
|
|
179
|
+
* segTypes}[]`, plus `{regionIndex, ring}` for region/regions input. `position`
|
|
180
|
+
* (the entry's place in this list) is what `filletProfile`/`chamferProfile`'s
|
|
181
|
+
* `{indices}` selects by; `index` is the joint's vertex number within its
|
|
182
|
+
* contour, and the two diverge past any smooth joint.
|
|
181
183
|
*/
|
|
182
184
|
export function profileCorners(input: ProfileInput): Corner2D[];
|
|
183
185
|
|
package/types/kernel.d.ts
CHANGED
|
@@ -103,8 +103,15 @@ export interface ContourRegion {
|
|
|
103
103
|
|
|
104
104
|
/** One corner of a `Shape2D`, as `Shape2D.corners()` reports it. */
|
|
105
105
|
export interface Corner2D {
|
|
106
|
-
/**
|
|
106
|
+
/**
|
|
107
|
+
* The joint's vertex number within its own contour. NOT what `fillet`/
|
|
108
|
+
* `chamfer`'s `{ indices }` takes — that is `position`. The two diverge as
|
|
109
|
+
* soon as a contour has a smooth joint (a collinear midpoint, a G1 arc-line
|
|
110
|
+
* join), and on region input `index` restarts per ring.
|
|
111
|
+
*/
|
|
107
112
|
index: number;
|
|
113
|
+
/** This corner's place in the `corners()` list — what `{ indices }` selects by. */
|
|
114
|
+
position: number;
|
|
108
115
|
point: Point2;
|
|
109
116
|
interiorAngleDeg: number;
|
|
110
117
|
convex: boolean;
|
|
@@ -121,9 +128,17 @@ export type CornerSelector =
|
|
|
121
128
|
| "all"
|
|
122
129
|
| "convex"
|
|
123
130
|
| "concave"
|
|
124
|
-
/**
|
|
131
|
+
/**
|
|
132
|
+
* Each corner's `position` in `corners()` (never its `index`); a per-corner
|
|
133
|
+
* `r`/`d` array pairs with these. Any entry out of range throws.
|
|
134
|
+
*/
|
|
125
135
|
| { indices: number[] }
|
|
126
|
-
|
|
136
|
+
/**
|
|
137
|
+
* The `count` (default 1) corners nearest `near`, optionally only those
|
|
138
|
+
* `within` mm of it. Without `within` the nearest corner is always selected,
|
|
139
|
+
* however far away it is.
|
|
140
|
+
*/
|
|
141
|
+
| { near: Point2; count?: number; within?: number };
|
|
127
142
|
|
|
128
143
|
/** A mirror line for `Shape2D.mirror`. */
|
|
129
144
|
export type MirrorAxis2 = "x" | "y" | { point: Point2; dir: Point2 };
|
|
@@ -195,7 +210,7 @@ export interface Shape2D {
|
|
|
195
210
|
chamfer(d: number | number[], opts?: { corners?: CornerSelector }): Shape2D;
|
|
196
211
|
/** Corner-preserving decimation/refit within `tolerance` mm. */
|
|
197
212
|
simplify(tolerance: number): Shape2D;
|
|
198
|
-
/** The corner list
|
|
213
|
+
/** The corner list; each entry's `position` is what `fillet`/`chamfer`'s `{ indices }` selects by. */
|
|
199
214
|
corners(): Corner2D[];
|
|
200
215
|
/** Is `[x, y]` inside the shape (inside an outer, not inside a hole)? */
|
|
201
216
|
contains(p: Point2): boolean;
|