@weasel-js/geom 1.10.0 → 1.10.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,19 +1,13 @@
1
1
  # @weasel-js/geom
2
2
 
3
- Pure 2D geometry kernel for @weasel-js/core: affine, box, curve, polyline, the
4
- `Path` command stream and the math over it (builders, SVG `d` parsing, bounds,
5
- transforms, hit-testing, sampling, curve fitting), and easing curves.
6
- Dependency-free core. Subpaths:
7
-
8
- - `./curves` — alternate curve representations (cubic and quadratic Bezier, NURBS, Spiro)
9
- - `./tessellate` — fill tessellation, polylines and trimming; needs the optional peer `earcut`
10
- - `./booleans` — polygon booleans and path splitting; needs the optional peer `polygon-clipping`
11
- - `./3d` — vectors, quaternions, 4x4 matrices and ray intersection
12
- - `./nd` — the port-curve rule (`portControls`, `portCurvePoints`, `PORT_REACH`) over plain number arrays, for any dimension
3
+ The 2D geometry kernel the rest of weasel is built on: affine transforms, boxes,
4
+ Bezier math, the `Path` shape type and everything that operates on it, and
5
+ easing curves. It is plain functions over numbers and typed arrays, with no
6
+ DOM, no React, and no runtime dependencies in the root entry.
13
7
 
14
8
  Part of [weasel](https://github.com/orochi235/weasel), a domain-agnostic 2D
15
9
  scene-graph canvas kit for React. See the
16
- [API reference](https://orochi235.github.io/weasel/api/).
10
+ [API reference](https://orochi235.github.io/weasel/#pkg/geom).
17
11
 
18
12
  ## Install
19
13
 
@@ -21,13 +15,135 @@ scene-graph canvas kit for React. See the
21
15
  npm install @weasel-js/geom
22
16
  ```
23
17
 
24
- ## Usage
18
+ The `./tessellate` entry uses the optional peer `earcut`, and `./booleans` uses
19
+ the optional peer `polygon-clipping`. Install a peer only if you import the
20
+ entry that needs it.
21
+
22
+ ## Value types
23
+
24
+ Most functions take loose `x, y` numbers rather than point objects, so a hot
25
+ loop allocates nothing. The types that do exist are small:
26
+
27
+ | Type | Shape | Notes |
28
+ |---|---|---|
29
+ | `Mat3` | `[a, b, c, d, e, f]` | A 2D affine in canvas and `DOMMatrix` order: `x' = a·x + c·y + e`, `y' = b·x + d·y + f`. |
30
+ | `Box` | `[minX, minY, maxX, maxY]` | An axis-aligned box as a tuple. |
31
+ | `Rect` | `{ x, y, width, height }` | The same box in object form. |
32
+ | `View` | `{ x, y, scale: { x, y } }` | A viewport: the world point drawn at the canvas's top-left corner, and pixels per world unit on each axis. |
33
+ | `GlMat3` | nine-element `Float64Array` or `Float32Array` | The same affine laid out column-major for a WebGL `uniformMatrix3fv` upload. |
34
+ | `Path` | `RectPath \| PolygonPath` | A shape. See below. |
35
+
36
+ `translate`, `scale`, `rotate`, `rotateAboutPoint`, and `boxToBox` build a
37
+ `Mat3`. `multiply(m, n)` composes two, applying `n` first. `invert` returns
38
+ `null` for a singular matrix, and `applyToPoint` maps one point.
39
+
40
+ ```ts
41
+ import { applyToPoint, invert, multiply, scale, translate } from '@weasel-js/geom';
42
+
43
+ // World to screen for a view at (10, 20), zoomed 2x: translate, then scale.
44
+ const worldToScreen = multiply(scale(2, 2), translate(-10, -20));
45
+ applyToPoint(worldToScreen, 15, 25); // [10, 10]
46
+
47
+ const screenToWorld = invert(worldToScreen); // Mat3 | null
48
+ ```
49
+
50
+ The `mat3` namespace holds the `GlMat3` operations (`mat3.fromAffine`,
51
+ `mat3.toAffine`, `mat3.multiply`, `mat3.toUniform`, and others), and
52
+ `viewToMat3(view)` builds the world-to-screen `GlMat3` for a `View`.
53
+
54
+ ## Paths
55
+
56
+ A `Path` is the shape type every weasel package shares. It is either:
57
+
58
+ - A `RectPath`, `{ kind: 'rect', x, y, width, height }`, for the common case of
59
+ an axis-aligned rectangle.
60
+ - A `PolygonPath`, an SVG-style command stream: command codes (`M`, `L`, `C`,
61
+ `Q`, `Z`) in a `Uint8Array`, their coordinates in a parallel `Float32Array`,
62
+ and a `fillRule`. It covers polylines, polygons, Bezier outlines, and shapes
63
+ with holes.
64
+
65
+ What operates on a path:
66
+
67
+ - **Building.** `PathBuilder` for move, line, curve, and close calls;
68
+ `rectPath`, `polygonFromPoints`, `polylineFromPoints`, `ellipsePath`,
69
+ `regularPolygonPath`, `starPath`, `linePath`, `circlePath`, and
70
+ `roundRectPath` for common shapes; `pathFromD` to parse an SVG `d` string;
71
+ `schneiderFit` to fit cubic curves through sampled points.
72
+ - **Measuring.** `boundsOfPath`, `unionBoundsPath`, `pathSignedArea`,
73
+ `pointAlongPath`.
74
+ - **Transforming.** `translatePath`, `transformPath`, `scalePathToBounds`,
75
+ `reversePath`.
76
+ - **Hit-testing.** `pointInPath` for the filled region, `strokeHitTest` for
77
+ the outline, `pathContainsRect`, `pathIntersectsRect`, and the polygon
78
+ variants for marquee and lasso selection, and `nearestOnPath` and
79
+ `pathDistanceToPoint` for the closest point.
80
+ - **Editing.** `pathToAnchors` and `anchorsToPath` convert between the command
81
+ stream and a list of anchors with control handles, the form a pen tool
82
+ edits. `splitSubpaths` and `splitByClosure` take a compound path apart.
83
+ - **Walking the stream.** `forEachSegment` visits each command with its
84
+ coordinate offset and the pen position before it.
85
+
86
+ ```ts
87
+ import {
88
+ boundsOfPath, pathFromD, pointInPath, rotateAboutPoint, strokeHitTest, transformPath,
89
+ } from '@weasel-js/geom';
90
+
91
+ const triangle = pathFromD('M0 0 L100 0 L50 80 Z');
92
+
93
+ boundsOfPath(triangle); // { kind: 'rect', x: 0, y: 0, width: 100, height: 80 }
94
+ pointInPath(triangle, 50, 20); // true: inside the fill
95
+ strokeHitTest(triangle, 50, 3, 4); // true: within 4 units of the outline
96
+
97
+ const turned = transformPath(triangle, rotateAboutPoint(50, 40, Math.PI / 2));
98
+ ```
99
+
100
+ Below the path level, the curve functions work on one segment at a time:
101
+ `cubicEvalAt`, `splitCubicAt`, `cubicBounds`, `flattenCubic`,
102
+ `nearestOnCubic`, and their quadratic and line counterparts.
103
+
104
+ ## Easings
105
+
106
+ An `EasingFn` maps progress `t` in 0..1 to eased progress. The package exports
107
+ the standard families by name (`easeOutCubic`, `easeInOutSine`,
108
+ `easeOutBounce`, and so on), the same set keyed by name in `EASINGS`, and
109
+ `cubicBezierEasing(x1, y1, x2, y2)` for CSS-style control points.
110
+
111
+ `EasingSpec` is an easing as a value that can be stored or serialized: a
112
+ function, a name, or `{ bezier: [x1, y1, x2, y2] }`. `resolveEasing` turns any
113
+ of them into the function.
114
+
115
+ ```ts
116
+ import { resolveEasing } from '@weasel-js/geom';
117
+
118
+ const ease = resolveEasing('easeOutCubic');
119
+ ease(0.5); // 0.875
120
+
121
+ const custom = resolveEasing({ bezier: [0.4, 0, 0.2, 1] });
122
+ ```
123
+
124
+ ## Subpath entries
125
+
126
+ | Entry | What it adds | Optional peer |
127
+ |---|---|---|
128
+ | `@weasel-js/geom/curves` | Curve representations that read one shared anchor list and each produce a `PolygonPath`: `bezierCubic`, `bezierQuadratic`, `nurbs`, and `spiro`, also keyed by kind in `CURVE_REPS`. Each can evaluate a point and the curvature at a parameter. | none |
129
+ | `@weasel-js/geom/tessellate` | `tessellate` turns a path's fill into a triangle `Mesh` (vertices and indices). `extractPolylines` flattens each contour to straight segments, and `trimPolyline` shortens one from either end. | `earcut`, used by `tessellate` |
130
+ | `@weasel-js/geom/booleans` | Boolean operations on path fills: `pathUnion`, `pathIntersect`, `pathSubtract`, `pathExclude`, `pathDivide`, `pathCrop`, `pathTrim`, and `pathMerge`. Also cutting a path with a line or polyline: `splitPathBySegment`, `splitPathByPolyline`, `snipPathByPolyline`. | `polygon-clipping` |
131
+ | `@weasel-js/geom/3d` | The same kind of math one dimension up: `Vec3` and quaternion helpers, `Mat4` with `compose`, `perspective`, and `lookAt`, and ray intersection against planes and axis-aligned boxes. | none |
132
+ | `@weasel-js/geom/nd` | The port-curve rule over plain number arrays of any length: `portControls` returns the two cubic control points that make a curve leave one port along its normal and arrive against the next port's normal, and `portCurvePoints` samples that curve. | none |
133
+
134
+ The `./3d` entry reuses names from the root (`dot`, `scale`, `invert`,
135
+ `identity`), so import from one or the other, or alias at the import.
136
+
137
+ Boolean operations work on what a fill covers, so curves are flattened first
138
+ and the result is a `PolygonPath` of straight segments.
25
139
 
26
140
  ```ts
27
- import { /* … */ } from '@weasel-js/geom';
28
- import { /* … */ } from '@weasel-js/geom/booleans';
29
- import { /* … */ } from '@weasel-js/geom/curves';
30
- import { /* … */ } from '@weasel-js/geom/tessellate';
141
+ import { ellipsePath, rectPath } from '@weasel-js/geom';
142
+ import { pathSubtract } from '@weasel-js/geom/booleans';
143
+
144
+ const plate = rectPath(0, 0, 100, 60);
145
+ const hole = ellipsePath({ x: 30, y: 10, width: 40, height: 40 });
146
+ const cut = pathSubtract(plate, hole); // the plate with the ellipse removed
31
147
  ```
32
148
 
33
149
  ## License
@@ -1,5 +1,5 @@
1
- import { P as PolygonPath, a as Path } from '../path-Bf9KeVxC.js';
2
- import { P as Point } from '../cubicMath-CFXbq6vp.js';
1
+ import { a as PolygonPath, P as Path } from '../path-ljHxobyu.js';
2
+ import { P as Point } from '../cubicMath-BW-HJrCE.js';
3
3
 
4
4
  /** Union of N paths. Commutative. Returns an empty path if all inputs are empty. */
5
5
  declare function pathUnion(...paths: Path[]): PolygonPath;
@@ -137,4 +137,23 @@ declare function splitPathByPolyline(path: Path, cut: readonly Point[], opts?: S
137
137
  */
138
138
  declare function snipPathByPolyline(path: Path, cut: readonly Point[]): Path[] | null;
139
139
 
140
- export { type BooleanPiece, type MultiPolygon, type Pair, type PathToMultiPolygonOptions, type Polygon, type Ring, type SplitBySegmentOptions, multiPolygonToPath, pathCrop, pathCropPieces, pathDivide, pathDividePieces, pathExclude, pathIntersect, pathMerge, pathSubtract, pathToMultiPolygon, pathTrim, pathUnion, snipPathByPolyline, splitPathByPolyline, splitPathBySegment, trianglesToPath };
140
+ /** Options for {@link snipPathsAtCrossings}. */
141
+ interface SnipAtCrossingsOptions {
142
+ /** How closely a cutting path's curves are followed when they are flattened
143
+ * to find where they cross. Default 0.05. */
144
+ flattenTolerance?: number;
145
+ }
146
+ /**
147
+ * Cut the stroke of every path in `paths` wherever another of them crosses
148
+ * it, as {@link snipPathByPolyline} cuts along one polyline: an open subpath
149
+ * becomes open runs between crossings, and a closed one is opened at them.
150
+ * A path is not cut where it crosses itself.
151
+ *
152
+ * Answers one entry per path, in order: its pieces, or `null` when no other
153
+ * path crosses it. The cutting paths' curves are flattened, so a crossing on
154
+ * a curve is found to within `flattenTolerance`; the pieces keep their own
155
+ * segment types.
156
+ */
157
+ declare function snipPathsAtCrossings(paths: readonly Path[], opts?: SnipAtCrossingsOptions): (Path[] | null)[];
158
+
159
+ export { type BooleanPiece, type MultiPolygon, type Pair, type PathToMultiPolygonOptions, type Polygon, type Ring, type SnipAtCrossingsOptions, type SplitBySegmentOptions, multiPolygonToPath, pathCrop, pathCropPieces, pathDivide, pathDividePieces, pathExclude, pathIntersect, pathMerge, pathSubtract, pathToMultiPolygon, pathTrim, pathUnion, snipPathByPolyline, snipPathsAtCrossings, splitPathByPolyline, splitPathBySegment, trianglesToPath };
@@ -1,5 +1,5 @@
1
1
  import { elevateQuadraticToCubic, splitQuadraticAt, splitCubicAt } from '../chunk-URQMDR6Q.js';
2
- import { DEFAULT_FLATTEN_TOLERANCE, fillsOpenSubpaths, flattenCubic, segmentsCross, pointInPolygon } from '../chunk-WQMYOCFB.js';
2
+ import { DEFAULT_FLATTEN_TOLERANCE, fillsOpenSubpaths, flattenCubic, flattenQuadratic, segmentsCross, pointInPolygon } from '../chunk-IDXY2NCA.js';
3
3
  import { EPS } from '../chunk-755Y4YZS.js';
4
4
  import { PATH_Z, PATH_Q, PATH_C, PATH_L, PATH_M } from '../chunk-OXH3NOXC.js';
5
5
  import polygonClipping from 'polygon-clipping';
@@ -994,6 +994,45 @@ function snipStations(sub, cut) {
994
994
  return groups.filter((g) => g.length % 2 === 1).map((g) => g[0]).filter((st) => sub.closed || !same(st.pt, first) && !same(st.pt, end));
995
995
  }
996
996
 
997
- export { multiPolygonToPath, pathCrop, pathCropPieces, pathDivide, pathDividePieces, pathExclude, pathIntersect, pathMerge, pathSubtract, pathToMultiPolygon, pathTrim, pathUnion, snipPathByPolyline, splitPathByPolyline, splitPathBySegment, trianglesToPath };
997
+ // src/booleans/snipAtCrossings.ts
998
+ function snipPathsAtCrossings(paths, opts = {}) {
999
+ const tol = opts.flattenTolerance ?? 0.05;
1000
+ const cuts = paths.map((p) => cutLinesOf(p, tol));
1001
+ return paths.map((path, i) => {
1002
+ let pieces = [path];
1003
+ let cut = false;
1004
+ cuts.forEach((lines, j) => {
1005
+ if (j === i) return;
1006
+ for (const line of lines) {
1007
+ pieces = pieces.flatMap((piece) => {
1008
+ const snipped = snipPathByPolyline(piece, line);
1009
+ if (!snipped) return [piece];
1010
+ cut = true;
1011
+ return snipped;
1012
+ });
1013
+ }
1014
+ });
1015
+ return cut ? pieces : null;
1016
+ });
1017
+ }
1018
+ function cutLinesOf(path, tol) {
1019
+ return parseSubpaths(path, false).map(({ segs, closed }) => {
1020
+ const flat = [segs[0][0].x, segs[0][0].y];
1021
+ for (const s of segs) {
1022
+ if (s.length === 2) flat.push(s[1].x, s[1].y);
1023
+ else if (s.length === 3) flattenQuadratic(s[0].x, s[0].y, s[1].x, s[1].y, s[2].x, s[2].y, tol, flat);
1024
+ else flattenCubic(s[0].x, s[0].y, s[1].x, s[1].y, s[2].x, s[2].y, s[3].x, s[3].y, tol, flat);
1025
+ }
1026
+ const pts = [];
1027
+ for (let k = 0; k < flat.length; k += 2) pts.push({ x: flat[k], y: flat[k + 1] });
1028
+ if (closed) {
1029
+ const end = pts[pts.length - 1];
1030
+ if (end.x !== pts[0].x || end.y !== pts[0].y) pts.push({ ...pts[0] });
1031
+ }
1032
+ return pts;
1033
+ });
1034
+ }
1035
+
1036
+ export { multiPolygonToPath, pathCrop, pathCropPieces, pathDivide, pathDividePieces, pathExclude, pathIntersect, pathMerge, pathSubtract, pathToMultiPolygon, pathTrim, pathUnion, snipPathByPolyline, snipPathsAtCrossings, splitPathByPolyline, splitPathBySegment, trianglesToPath };
998
1037
  //# sourceMappingURL=index.js.map
999
1038
  //# sourceMappingURL=index.js.map