forma-dsl 0.1.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.
@@ -0,0 +1,159 @@
1
+ /** @import { Dimensionality } from '../index.js' */
2
+ /** @import { VectorLike } from './vector.js' */
3
+
4
+ import { Vector } from './vector.js';
5
+ import { Angle } from './angle.js';
6
+
7
+ /**
8
+ * An affine transform, held as a column-major matrix of the size the kernel wants: 3x3 for
9
+ * 2D and 4x4 for 3D, both with the last row ignored on the way in.
10
+ *
11
+ * Column-major throughout, because that is what Manifold reads. Storing row-major here and
12
+ * transposing at the boundary would put a silent transpose in the one place a wrong model
13
+ * still renders.
14
+ */
15
+ export class Transform {
16
+ /**
17
+ * @param {Dimensionality} dim Whether this transforms 2D or 3D geometry.
18
+ * @param {ReadonlyArray<number>} m The matrix, column-major, `(dim + 1)²` entries.
19
+ */
20
+ constructor(dim, m) {
21
+ /** @type {Dimensionality} Whether this transforms 2D or 3D geometry. */
22
+ this.dim = dim;
23
+ /** @type {ReadonlyArray<number>} The matrix, column-major and frozen. */
24
+ this.m = Object.freeze(m.map(Number));
25
+ Object.freeze(this);
26
+ }
27
+
28
+ /**
29
+ * @param {Dimensionality} dim Whether this transforms 2D or 3D geometry.
30
+ * @returns {Transform} The transform that changes nothing.
31
+ */
32
+ static identity(dim) {
33
+ const n = dim + 1;
34
+ const m = new Array(n * n).fill(0);
35
+ for (let i = 0; i < n; i++) m[i * n + i] = 1;
36
+ return new Transform(dim, m);
37
+ }
38
+
39
+ /** @returns {number} The side length of the matrix, one more than the dimensionality. */
40
+ get order() { return this.dim + 1; }
41
+
42
+ /**
43
+ * @param {number} row Row index.
44
+ * @param {number} col Column index.
45
+ * @returns {number} The entry, read out of the column-major storage.
46
+ */
47
+ at(row, col) { return this.m[col * this.order + row]; }
48
+
49
+ /**
50
+ * Composes two transforms as `this ∘ other`, so `other` runs first.
51
+ *
52
+ * That order matches how the blocks nest: the transform block closer to the shape is
53
+ * applied to it first.
54
+ *
55
+ * @param {Transform} other The transform to apply before this one.
56
+ * @returns {Transform} The combined transform.
57
+ */
58
+ concat(other) {
59
+ const n = this.order;
60
+ const out = new Array(n * n).fill(0);
61
+ for (let c = 0; c < n; c++) {
62
+ for (let r = 0; r < n; r++) {
63
+ let sum = 0;
64
+ for (let k = 0; k < n; k++) sum += this.at(r, k) * other.at(k, c);
65
+ out[c * n + r] = sum;
66
+ }
67
+ }
68
+ return new Transform(this.dim, out);
69
+ }
70
+
71
+ /**
72
+ * @param {VectorLike} offset How far to move, per axis.
73
+ * @param {Dimensionality} dim Whether this transforms 2D or 3D geometry.
74
+ * @returns {Transform} The translation.
75
+ */
76
+ static translation(offset, dim) {
77
+ const v = Vector.of(offset, dim);
78
+ const t = Transform.identity(dim);
79
+ const m = [...t.m];
80
+ const n = dim + 1;
81
+ for (let i = 0; i < dim; i++) m[dim * n + i] = v.components[i];
82
+ return new Transform(dim, m);
83
+ }
84
+
85
+ /**
86
+ * @param {VectorLike} factor The scale per axis; a number scales every axis alike.
87
+ * @param {Dimensionality} dim Whether this transforms 2D or 3D geometry.
88
+ * @returns {Transform} The scaling.
89
+ */
90
+ static scaling(factor, dim) {
91
+ const v = Vector.of(factor, dim);
92
+ const n = dim + 1;
93
+ const m = new Array(n * n).fill(0);
94
+ for (let i = 0; i < dim; i++) m[i * n + i] = v.components[i];
95
+ m[dim * n + dim] = 1;
96
+ return new Transform(dim, m);
97
+ }
98
+
99
+ /**
100
+ * @param {Angle | number} angle The rotation; a bare number is read as degrees.
101
+ * @returns {Transform} The 2D rotation about the origin.
102
+ */
103
+ static rotation2D(angle) {
104
+ const a = Angle.of(angle);
105
+ const c = a.cos, s = a.sin;
106
+ return new Transform(2, [c, s, 0, -s, c, 0, 0, 0, 1]);
107
+ }
108
+
109
+ /**
110
+ * Rotates about x, then y, then z — the order OpenSCAD and Cadova both use, so a model
111
+ * ported from either lands in the same orientation.
112
+ *
113
+ * @param {Vector | ReadonlyArray<number>} angles Degrees about x, y and z.
114
+ * @returns {Transform} The 3D rotation about the origin.
115
+ */
116
+ static rotation3D(angles) {
117
+ const v = Vector.of(angles, 3);
118
+ const [x, y, z] = v.components.map((d) => Angle.of(d));
119
+ const rx = new Transform(3, [1, 0, 0, 0, 0, x.cos, x.sin, 0, 0, -x.sin, x.cos, 0, 0, 0, 0, 1]);
120
+ const ry = new Transform(3, [y.cos, 0, -y.sin, 0, 0, 1, 0, 0, y.sin, 0, y.cos, 0, 0, 0, 0, 1]);
121
+ const rz = new Transform(3, [z.cos, z.sin, 0, 0, -z.sin, z.cos, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1]);
122
+ return rz.concat(ry).concat(rx);
123
+ }
124
+
125
+ /**
126
+ * @param {Angle | number | Vector | ReadonlyArray<number>} angles Degrees. A single
127
+ * angle in 2D, three in 3D; passing the wrong shape for `dim` is the caller's error.
128
+ * @param {Dimensionality} dim Whether this transforms 2D or 3D geometry.
129
+ * @returns {Transform} The rotation about the origin.
130
+ */
131
+ static rotation(angles, dim) {
132
+ return dim === 2
133
+ ? Transform.rotation2D(/** @type {Angle | number} */ (angles))
134
+ : Transform.rotation3D(/** @type {Vector | ReadonlyArray<number>} */ (angles));
135
+ }
136
+
137
+ /**
138
+ * Reflects in the plane (or line) through the origin with this normal. The normal is
139
+ * normalised first, so its length does not change the result.
140
+ *
141
+ * @param {Vector | ReadonlyArray<number>} normal The normal of the mirror plane.
142
+ * @param {Dimensionality} dim Whether this transforms 2D or 3D geometry.
143
+ * @returns {Transform} The reflection.
144
+ */
145
+ static mirroring(normal, dim) {
146
+ const n = Vector.of(normal, dim).normalized();
147
+ const order = dim + 1;
148
+ const m = Transform.identity(dim).m.slice();
149
+ for (let c = 0; c < dim; c++) {
150
+ for (let r = 0; r < dim; r++) {
151
+ m[c * order + r] = (r === c ? 1 : 0) - 2 * n.components[r] * n.components[c];
152
+ }
153
+ }
154
+ return new Transform(dim, m);
155
+ }
156
+
157
+ /** @returns {number[]} A copy of the matrix, safe for the caller to keep. */
158
+ toArray() { return [...this.m]; }
159
+ }
@@ -0,0 +1,169 @@
1
+ /**
2
+ * Anything a model can write where a vector is wanted.
3
+ *
4
+ * A bare number is accepted as shorthand for every component, which is what lets
5
+ * `scale { factor = 2 }` read as naturally as `factor = [2, 1, 1]`.
6
+ *
7
+ * @typedef {Vector | number | ReadonlyArray<number> | Record<string, number>} VectorLike
8
+ */
9
+
10
+ /**
11
+ * A 2D or 3D vector.
12
+ *
13
+ * One class covers both, because every operation below is componentwise and a separate 3D
14
+ * class would be the same code with one more letter.
15
+ */
16
+ export class Vector {
17
+ /**
18
+ * @param {ReadonlyArray<number>} components Two or three components.
19
+ */
20
+ constructor(components) {
21
+ /** @type {ReadonlyArray<number>} The components, frozen. */
22
+ this.components = Object.freeze(components.map(Number));
23
+ Object.freeze(this);
24
+ }
25
+
26
+ /**
27
+ * Reads a vector out of whatever the caller had.
28
+ *
29
+ * @param {VectorLike} value A vector, a scalar to broadcast, an array, or an `{x, y, z}`
30
+ * object whose missing keys are read as zero.
31
+ * @param {number} dimensions How many components the result must have.
32
+ * @returns {Vector} The vector.
33
+ * @throws {TypeError} If the value is the wrong size, or not a shape a vector can be read from.
34
+ */
35
+ static of(value, dimensions) {
36
+ if (value instanceof Vector) {
37
+ if (value.size !== dimensions) {
38
+ throw new TypeError(`expected a ${dimensions}D vector, got ${value.size}D`);
39
+ }
40
+ return value;
41
+ }
42
+ if (typeof value === 'number') {
43
+ return new Vector(new Array(dimensions).fill(value));
44
+ }
45
+ if (Array.isArray(value)) {
46
+ if (value.length !== dimensions) {
47
+ throw new TypeError(`expected ${dimensions} components, got ${value.length}`);
48
+ }
49
+ return new Vector(value);
50
+ }
51
+ if (value && typeof value === 'object') {
52
+ const keys = dimensions === 2 ? ['x', 'y'] : ['x', 'y', 'z'];
53
+ return new Vector(keys.map((k) => value[k] ?? 0));
54
+ }
55
+ throw new TypeError(`cannot read a ${dimensions}D vector from ${typeof value}`);
56
+ }
57
+
58
+ /**
59
+ * @param {number} dimensions How many components.
60
+ * @returns {Vector} The origin.
61
+ */
62
+ static zero(dimensions) {
63
+ return new Vector(new Array(dimensions).fill(0));
64
+ }
65
+
66
+ /** @returns {number} How many components the vector has. */
67
+ get size() { return this.components.length; }
68
+
69
+ /** @returns {number} The first component. */
70
+ get x() { return this.components[0]; }
71
+
72
+ /** @returns {number} The second component. */
73
+ get y() { return this.components[1]; }
74
+
75
+ /** @returns {number} The third component, or `undefined` in 2D. */
76
+ get z() { return this.components[2]; }
77
+
78
+ /**
79
+ * @param {VectorLike} other The right-hand side.
80
+ * @param {(a: number, b: number) => number} f Applied to each pair of components.
81
+ * @returns {Vector} The result.
82
+ */
83
+ #zip(other, f) {
84
+ const rhs = Vector.of(other, this.size);
85
+ return new Vector(this.components.map((v, i) => f(v, rhs.components[i])));
86
+ }
87
+
88
+ /**
89
+ * @param {VectorLike} other The right-hand side.
90
+ * @returns {Vector} The componentwise sum.
91
+ */
92
+ plus(other) { return this.#zip(other, (a, b) => a + b); }
93
+
94
+ /**
95
+ * @param {VectorLike} other The right-hand side.
96
+ * @returns {Vector} The componentwise difference.
97
+ */
98
+ minus(other) { return this.#zip(other, (a, b) => a - b); }
99
+
100
+ /**
101
+ * @param {VectorLike} other The right-hand side; a number scales every component.
102
+ * @returns {Vector} The componentwise product.
103
+ */
104
+ times(other) { return this.#zip(other, (a, b) => a * b); }
105
+
106
+ /**
107
+ * @param {VectorLike} other The right-hand side; a number divides every component.
108
+ * @returns {Vector} The componentwise quotient.
109
+ */
110
+ dividedBy(other) { return this.#zip(other, (a, b) => a / b); }
111
+
112
+ /** @returns {Vector} The vector pointing the other way. */
113
+ negated() { return new Vector(this.components.map((v) => -v)); }
114
+
115
+ /**
116
+ * @param {VectorLike} other The right-hand side.
117
+ * @returns {number} The dot product.
118
+ */
119
+ dot(other) {
120
+ const rhs = Vector.of(other, this.size);
121
+ return this.components.reduce((sum, v, i) => sum + v * rhs.components[i], 0);
122
+ }
123
+
124
+ /** @returns {number} The length of the vector. */
125
+ get magnitude() { return Math.sqrt(this.dot(this)); }
126
+
127
+ /**
128
+ * A zero vector normalises to itself rather than to NaN, so a degenerate normal cannot
129
+ * quietly poison a transform downstream.
130
+ *
131
+ * @returns {Vector} The vector scaled to unit length.
132
+ */
133
+ normalized() {
134
+ const m = this.magnitude;
135
+ return m === 0 ? this : new Vector(this.components.map((v) => v / m));
136
+ }
137
+
138
+ /**
139
+ * @param {Vector | ReadonlyArray<number>} other The right-hand side; must be 3D.
140
+ * @returns {Vector} The cross product.
141
+ */
142
+ cross(other) {
143
+ const b = Vector.of(other, 3);
144
+ const [ax, ay, az] = this.components;
145
+ const [bx, by, bz] = b.components;
146
+ return new Vector([ay * bz - az * by, az * bx - ax * bz, ax * by - ay * bx]);
147
+ }
148
+
149
+ /** @returns {number[]} A copy of the components, safe for the caller to keep. */
150
+ toArray() { return [...this.components]; }
151
+
152
+ /** @returns {string} The components in parentheses, for diagnostics. */
153
+ toString() { return `(${this.components.join(', ')})`; }
154
+ }
155
+
156
+ /**
157
+ * @param {number} x The first component.
158
+ * @param {number} y The second component.
159
+ * @returns {Vector} A 2D vector.
160
+ */
161
+ export const vector2 = (x, y) => new Vector([x, y]);
162
+
163
+ /**
164
+ * @param {number} x The first component.
165
+ * @param {number} y The second component.
166
+ * @param {number} z The third component.
167
+ * @returns {Vector} A 3D vector.
168
+ */
169
+ export const vector3 = (x, y, z) => new Vector([x, y, z]);