@wandelbots/nova-js 4.2.0 → 4.3.0-pr.318.15fac7b

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,140 @@
1
+ import { bc as Pose$1, t as Nova } from "../../Nova-DdbUm145.mjs";
2
+ //#region src/lib/experimental/math/Pose.d.ts
3
+ /**
4
+ * A `Pose` (position + axis-angle orientation) with methods for composing
5
+ * and inverting transforms. Instances are plain-data compatible with the
6
+ * wire-format `PoseData` type (own `position`/`orientation` properties only,
7
+ * no enumerable methods), so they can be passed directly back into API calls
8
+ * that expect a pose.
9
+ */
10
+ declare class Pose implements Pose$1 {
11
+ readonly position: number[];
12
+ readonly orientation: number[];
13
+ constructor(position?: number[], orientation?: number[]);
14
+ static from(pose: Pose$1): Pose;
15
+ static identity(): Pose;
16
+ /**
17
+ * Compose this pose with `other`, treating `other` as being expressed in
18
+ * this pose's coordinate frame. Equivalent to the homogeneous transform
19
+ * product `this * other`: applying the result to a point is the same as
20
+ * applying `other` first, then `this`.
21
+ */
22
+ multiply(other: Pose$1): Pose;
23
+ /** The inverse transform, such that `pose.multiply(pose.inverse())` is the identity pose. */
24
+ inverse(): Pose;
25
+ /** Apply this pose's transform to a point, returning the transformed point. */
26
+ transformPoint(point: number[]): number[];
27
+ /**
28
+ * Compare to another pose using separate position/orientation tolerances,
29
+ * matching wb-robotix's `Pose::isApprox()`: Euclidean distance for
30
+ * position, and quaternion angular distance for orientation - not a naive
31
+ * per-component diff, since two rotation vectors can represent nearly
32
+ * identical rotations while differing componentwise near a
33
+ * canonicalization boundary (e.g. close to the +/-pi wraparound).
34
+ */
35
+ isApprox(other: Pose$1, deltaPosition?: number, deltaOrientation?: number): boolean;
36
+ /**
37
+ * Position + orientation as a 6-element [x, y, z, roll, pitch, yaw] vector
38
+ * (Euler angles in rad, XYZ Tait-Bryan / Rx*Ry*Rz convention), matching
39
+ * wb-robotix's `Pose::toCartesian()`.
40
+ */
41
+ toCartesian(): number[];
42
+ /** Human-readable `[x, y, z][rx, ry, rz]` representation, matching wb-robotix's `Pose::string()`. */
43
+ toString(precision?: number): string;
44
+ toJSON(): Pose$1;
45
+ }
46
+ //#endregion
47
+ //#region src/lib/experimental/math/Quaternion.d.ts
48
+ /**
49
+ * A unit quaternion representing a 3D rotation. Mirrors the subset of
50
+ * `Eigen::Quaternion`'s API (see wb-robotix's `QuaternionBasePlugin.h`) that
51
+ * `Pose` composition needs: Hamilton product, conjugate/inverse, vector
52
+ * rotation, and axis-angle rotation-vector conversion. Instances are
53
+ * immutable - every method returns a new `Quaternion`.
54
+ */
55
+ type QuaternionData = {
56
+ w: number;
57
+ x: number;
58
+ y: number;
59
+ z: number;
60
+ };
61
+ declare class Quaternion implements QuaternionData {
62
+ readonly w: number;
63
+ readonly x: number;
64
+ readonly y: number;
65
+ readonly z: number;
66
+ constructor(w?: number, x?: number, y?: number, z?: number);
67
+ static identity(): Quaternion;
68
+ /**
69
+ * Construct from an axis-angle rotation vector [rx, ry, rz] (magnitude =
70
+ * angle in rad), matching `Eigen::Quaternion::FromRotationVector` (ported
71
+ * from wb-robotix's `QuaternionBasePlugin.h`).
72
+ */
73
+ static fromRotationVector(rotationVector: number[]): Quaternion;
74
+ /**
75
+ * Rotation vector [rx, ry, rz] representation (magnitude = angle in rad),
76
+ * matching `Eigen::Quaternion::toRotationVector()` (ported from
77
+ * wb-robotix's `QuaternionBasePlugin.h`): using `atan2` rather than `acos`
78
+ * keeps the angle canonically within [0, pi] regardless of which of the
79
+ * two antipodal unit quaternions (this or its negation) represents the
80
+ * rotation, rather than acos's [0, 2*pi] range - important since composed
81
+ * poses need to match the same canonical rotation vector the robot
82
+ * controller itself would report for a pose.
83
+ */
84
+ toRotationVector(): number[];
85
+ /** Hamilton product: `a.multiply(b)` applied to a vector rotates by `b` first, then by `a`. */
86
+ multiply(other: QuaternionData): Quaternion;
87
+ /** Conjugate: negates the vector part. Equal to `inverse()` for unit quaternions. */
88
+ conjugate(): Quaternion;
89
+ /** Inverse rotation. Equivalent to `conjugate()` since a `Pose`'s orientation is always a unit quaternion. */
90
+ inverse(): Quaternion;
91
+ /**
92
+ * The angle (in rad, always within [0, pi]) of the rotation that takes
93
+ * `other` to `this`, matching Eigen's built-in `Quaternion::angularDistance()`.
94
+ */
95
+ angularDistance(other: QuaternionData): number;
96
+ /** Rotate a 3D vector by this quaternion. */
97
+ rotateVector(v: number[]): number[];
98
+ toJSON(): QuaternionData;
99
+ }
100
+ //#endregion
101
+ //#region src/lib/experimental/math/withMath.d.ts
102
+ type PoseKeys = keyof Pose$1;
103
+ type IsExactlyPoseData<T> = [Exclude<keyof T, PoseKeys>] extends [never] ? [Exclude<PoseKeys, keyof T>] extends [never] ? true : false : false;
104
+ type MaxDepth = [unknown, unknown, unknown, unknown, unknown, unknown, unknown, unknown, unknown, unknown];
105
+ type AugmentPosesCore<T, Depth extends readonly unknown[]> = Depth["length"] extends MaxDepth["length"] ? T : IsExactlyPoseData<T> extends true ? Pose : T extends readonly (infer U)[] ? DeepPoseAugmented<U, [...Depth, unknown]>[] : T extends object ? { [K in keyof T]: DeepPoseAugmented<T[K], [...Depth, unknown]>; } : T;
106
+ /**
107
+ * Type-level counterpart to `augmentPoses`: recursively replaces any
108
+ * `PoseData`-shaped field with `Pose` (distributing over unions, e.g. so
109
+ * optional `Pose | undefined` fields keep the `| undefined`), so the static
110
+ * type matches what's actually returned at runtime.
111
+ */
112
+ type DeepPoseAugmented<T, Depth extends readonly unknown[] = []> = T extends unknown ? AugmentPosesCore<T, Depth> : never;
113
+ /**
114
+ * Recursively walks a parsed API response/request body, upgrading any
115
+ * `Pose`-shaped objects in place to `Pose` instances so they gain math
116
+ * methods (`multiply`, `inverse`, ...) while remaining JSON/wire-compatible.
117
+ * Used by `withMath` for `nova.api.*` responses; exported so it can also
118
+ * be applied manually to e.g. websocket messages.
119
+ */
120
+ declare function augmentPoses<T>(value: T): DeepPoseAugmented<T>;
121
+ type WithAugmentedPosesCore<T, Depth extends readonly unknown[]> = Depth["length"] extends MaxDepth["length"] ? T : T extends ((...args: infer A) => Promise<infer R>) ? (...args: A) => Promise<DeepPoseAugmented<R>> : T extends object ? { [K in keyof T]: WithAugmentedPosesCore<T[K], [...Depth, unknown]>; } : T;
122
+ /**
123
+ * Recursively maps every method on `Nova["api"]` (and its nested API groups)
124
+ * so calls are typed as returning `DeepPoseAugmented` results, matching what
125
+ * `withMath` does at runtime.
126
+ */
127
+ type WithAugmentedPoses<T> = WithAugmentedPosesCore<T, []>;
128
+ type NovaWithMath = Omit<Nova, "api"> & {
129
+ readonly api: WithAugmentedPoses<Nova["api"]>;
130
+ };
131
+ /**
132
+ * Wraps a `Nova` instance so every `nova.api.*` call automatically upgrades
133
+ * `Pose`-shaped fields in its response to `Pose` instances (with math
134
+ * methods like `multiply`/`inverse`). Returns a new proxied view - does not
135
+ * mutate the original `nova` instance.
136
+ */
137
+ declare function withMath(nova: Nova): NovaWithMath;
138
+ //#endregion
139
+ export { type DeepPoseAugmented, type NovaWithMath, Pose, type Pose$1 as PoseData, Quaternion, type QuaternionData, augmentPoses, withMath };
140
+ //# sourceMappingURL=index.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.mts","names":[],"sources":["../../../src/lib/experimental/math/Pose.ts","../../../src/lib/experimental/math/Quaternion.ts","../../../src/lib/experimental/math/withMath.ts"],"mappings":";;;;;;;;;cA+Ba,gBAAgB;WAClB;WACA;EAGP,YAAA,qBACA;SAkBK,KAAK,MAAM,SAAW;SAUtB,YAAY;;;;;;;EAUnB,SAAS,OAAO,SAAW;;EAc3B,WAAW;;EAWX,eAAe;;;;;;;;;EAaf,SACE,OAAO,QACP,wBACA;;;;;;EA0BF;;EAWA,SAAS;EAKT,UAAU;;;;;;;;;;;KCtJA;EAAmB;EAAW;EAAW;EAAW;;cAInD,sBAAsB;WACxB;WACA;WACA;WACA;EAEG,YAAA,YAAO,YAAO,YAAO;SAO1B,YAAY;;;;;;SASZ,mBAAmB,2BAA2B;;;;;;;;;;;EAwBrD;;EAaA,SAAS,OAAO,iBAAiB;;EAYjC,aAAa;;EAKb,WAAW;;;;;EAQX,gBAAgB,OAAO;;EAgBvB,aAAa;EAmBb,UAAU;;;;KC/HP,iBAAiB;KAEjB,kBAAkB,MAAM,cAAc,GAAG,8BACzC,QAAQ,gBAAgB;KAOxB;KAaA,iBACH,GACA,oCACE,wBAAwB,qBACxB,IACA,kBAAkB,kBAChB,OACA,0BAA0B,OACxB,kBAAkB,OAAO,qBACzB,sBACK,WAAW,IAAI,kBAAkB,EAAE,QAAQ,sBAC9C;;;;;;;KAQE,kBACV,GACA,yCACE,oBAAoB,iBAAiB,GAAG;;;;;;;;iBA2D5B,aAAa,GAAG,OAAO,IAAI,kBAAkB;KAKxD,uBACH,GACA,oCACE,wBAAwB,qBACxB,IACA,eAAc,YAAY,MAAM,cAAc,UACxC,MAAM,MAAM,QAAQ,kBAAkB,MAC1C,sBACK,WAAW,IAAI,uBAAuB,EAAE,QAAQ,sBACnD;;;;;;KAOH,mBAAmB,KAAK,uBAAuB;KAExC,eAAe,KAAK;WACrB,KAAK,mBAAmB;;;;;;;;iBA6BnB,SAAS,MAAM,OAAO"}
@@ -0,0 +1,287 @@
1
+ //#region src/lib/experimental/math/Quaternion.ts
2
+ const EPSILON = 1e-12;
3
+ var Quaternion = class Quaternion {
4
+ w;
5
+ x;
6
+ y;
7
+ z;
8
+ constructor(w = 1, x = 0, y = 0, z = 0) {
9
+ this.w = w;
10
+ this.x = x;
11
+ this.y = y;
12
+ this.z = z;
13
+ }
14
+ static identity() {
15
+ return new Quaternion();
16
+ }
17
+ /**
18
+ * Construct from an axis-angle rotation vector [rx, ry, rz] (magnitude =
19
+ * angle in rad), matching `Eigen::Quaternion::FromRotationVector` (ported
20
+ * from wb-robotix's `QuaternionBasePlugin.h`).
21
+ */
22
+ static fromRotationVector(rotationVector) {
23
+ const rx = rotationVector[0] ?? 0;
24
+ const ry = rotationVector[1] ?? 0;
25
+ const rz = rotationVector[2] ?? 0;
26
+ const angle = Math.sqrt(rx * rx + ry * ry + rz * rz);
27
+ if (angle < EPSILON) return Quaternion.identity();
28
+ const half = angle / 2;
29
+ const s = Math.sin(half) / angle;
30
+ return new Quaternion(Math.cos(half), rx * s, ry * s, rz * s);
31
+ }
32
+ /**
33
+ * Rotation vector [rx, ry, rz] representation (magnitude = angle in rad),
34
+ * matching `Eigen::Quaternion::toRotationVector()` (ported from
35
+ * wb-robotix's `QuaternionBasePlugin.h`): using `atan2` rather than `acos`
36
+ * keeps the angle canonically within [0, pi] regardless of which of the
37
+ * two antipodal unit quaternions (this or its negation) represents the
38
+ * rotation, rather than acos's [0, 2*pi] range - important since composed
39
+ * poses need to match the same canonical rotation vector the robot
40
+ * controller itself would report for a pose.
41
+ */
42
+ toRotationVector() {
43
+ const vecNorm = Math.sqrt(this.x * this.x + this.y * this.y + this.z * this.z);
44
+ if (vecNorm < EPSILON) return [
45
+ 0,
46
+ 0,
47
+ 0
48
+ ];
49
+ const angle = 2 * Math.atan2(vecNorm, Math.abs(this.w));
50
+ const scale = (this.w >= 0 ? angle : -angle) / vecNorm;
51
+ return [
52
+ this.x * scale,
53
+ this.y * scale,
54
+ this.z * scale
55
+ ];
56
+ }
57
+ /** Hamilton product: `a.multiply(b)` applied to a vector rotates by `b` first, then by `a`. */
58
+ multiply(other) {
59
+ const { w: aw, x: ax, y: ay, z: az } = this;
60
+ const { w: bw, x: bx, y: by, z: bz } = other;
61
+ return new Quaternion(aw * bw - ax * bx - ay * by - az * bz, aw * bx + ax * bw + ay * bz - az * by, aw * by - ax * bz + ay * bw + az * bx, aw * bz + ax * by - ay * bx + az * bw);
62
+ }
63
+ /** Conjugate: negates the vector part. Equal to `inverse()` for unit quaternions. */
64
+ conjugate() {
65
+ return new Quaternion(this.w, -this.x, -this.y, -this.z);
66
+ }
67
+ /** Inverse rotation. Equivalent to `conjugate()` since a `Pose`'s orientation is always a unit quaternion. */
68
+ inverse() {
69
+ return this.conjugate();
70
+ }
71
+ /**
72
+ * The angle (in rad, always within [0, pi]) of the rotation that takes
73
+ * `other` to `this`, matching Eigen's built-in `Quaternion::angularDistance()`.
74
+ */
75
+ angularDistance(other) {
76
+ const relative = this.multiply({
77
+ w: other.w,
78
+ x: -other.x,
79
+ y: -other.y,
80
+ z: -other.z
81
+ });
82
+ const vecNorm = Math.sqrt(relative.x * relative.x + relative.y * relative.y + relative.z * relative.z);
83
+ return 2 * Math.atan2(vecNorm, Math.abs(relative.w));
84
+ }
85
+ /** Rotate a 3D vector by this quaternion. */
86
+ rotateVector(v) {
87
+ const vx = v[0] ?? 0;
88
+ const vy = v[1] ?? 0;
89
+ const vz = v[2] ?? 0;
90
+ const { w, x, y, z } = this;
91
+ const tx = 2 * (y * vz - z * vy);
92
+ const ty = 2 * (z * vx - x * vz);
93
+ const tz = 2 * (x * vy - y * vx);
94
+ return [
95
+ vx + w * tx + (y * tz - z * ty),
96
+ vy + w * ty + (z * tx - x * tz),
97
+ vz + w * tz + (x * ty - y * tx)
98
+ ];
99
+ }
100
+ toJSON() {
101
+ return {
102
+ w: this.w,
103
+ x: this.x,
104
+ y: this.y,
105
+ z: this.z
106
+ };
107
+ }
108
+ };
109
+ //#endregion
110
+ //#region src/lib/experimental/math/Pose.ts
111
+ const ZERO_VECTOR = [
112
+ 0,
113
+ 0,
114
+ 0
115
+ ];
116
+ const TOLERANCE_MILLI = .001;
117
+ function addVectors(a, b) {
118
+ return [
119
+ (a[0] ?? 0) + (b[0] ?? 0),
120
+ (a[1] ?? 0) + (b[1] ?? 0),
121
+ (a[2] ?? 0) + (b[2] ?? 0)
122
+ ];
123
+ }
124
+ function negateVector(a) {
125
+ return [
126
+ -(a[0] ?? 0),
127
+ -(a[1] ?? 0),
128
+ -(a[2] ?? 0)
129
+ ];
130
+ }
131
+ function isFiniteVector3(v) {
132
+ return v.length === 3 && v.every((n) => Number.isFinite(n));
133
+ }
134
+ /**
135
+ * A `Pose` (position + axis-angle orientation) with methods for composing
136
+ * and inverting transforms. Instances are plain-data compatible with the
137
+ * wire-format `PoseData` type (own `position`/`orientation` properties only,
138
+ * no enumerable methods), so they can be passed directly back into API calls
139
+ * that expect a pose.
140
+ */
141
+ var Pose = class Pose {
142
+ position;
143
+ orientation;
144
+ constructor(position = ZERO_VECTOR, orientation = ZERO_VECTOR) {
145
+ if (!isFiniteVector3(position)) throw new Error(`Pose constructor: position must be an array of 3 finite numbers, got ${JSON.stringify(position)}`);
146
+ if (!isFiniteVector3(orientation)) throw new Error(`Pose constructor: orientation must be an array of 3 finite numbers, got ${JSON.stringify(orientation)}`);
147
+ this.position = [...position];
148
+ this.orientation = [...orientation];
149
+ }
150
+ static from(pose) {
151
+ if (pose instanceof Pose) return pose;
152
+ return new Pose(pose.position ?? ZERO_VECTOR, pose.orientation ?? ZERO_VECTOR);
153
+ }
154
+ static identity() {
155
+ return new Pose();
156
+ }
157
+ /**
158
+ * Compose this pose with `other`, treating `other` as being expressed in
159
+ * this pose's coordinate frame. Equivalent to the homogeneous transform
160
+ * product `this * other`: applying the result to a point is the same as
161
+ * applying `other` first, then `this`.
162
+ */
163
+ multiply(other) {
164
+ const q1 = Quaternion.fromRotationVector(this.orientation);
165
+ const q2 = Quaternion.fromRotationVector(other.orientation ?? ZERO_VECTOR);
166
+ const position = addVectors(this.position, q1.rotateVector(other.position ?? ZERO_VECTOR));
167
+ const orientation = q1.multiply(q2).toRotationVector();
168
+ return new Pose(position, orientation);
169
+ }
170
+ /** The inverse transform, such that `pose.multiply(pose.inverse())` is the identity pose. */
171
+ inverse() {
172
+ const position = Quaternion.fromRotationVector(this.orientation).inverse().rotateVector(negateVector(this.position));
173
+ const orientation = negateVector(this.orientation);
174
+ return new Pose(position, orientation);
175
+ }
176
+ /** Apply this pose's transform to a point, returning the transformed point. */
177
+ transformPoint(point) {
178
+ const q = Quaternion.fromRotationVector(this.orientation);
179
+ return addVectors(this.position, q.rotateVector(point));
180
+ }
181
+ /**
182
+ * Compare to another pose using separate position/orientation tolerances,
183
+ * matching wb-robotix's `Pose::isApprox()`: Euclidean distance for
184
+ * position, and quaternion angular distance for orientation - not a naive
185
+ * per-component diff, since two rotation vectors can represent nearly
186
+ * identical rotations while differing componentwise near a
187
+ * canonicalization boundary (e.g. close to the +/-pi wraparound).
188
+ */
189
+ isApprox(other, deltaPosition = TOLERANCE_MILLI, deltaOrientation = TOLERANCE_MILLI) {
190
+ const otherPosition = other.position ?? ZERO_VECTOR;
191
+ const otherOrientation = other.orientation ?? ZERO_VECTOR;
192
+ const positionDistance = Math.sqrt(this.position.reduce((sum, v, i) => sum + (v - (otherPosition[i] ?? 0)) ** 2, 0));
193
+ const orientationDistance = Quaternion.fromRotationVector(this.orientation).angularDistance(Quaternion.fromRotationVector(otherOrientation));
194
+ return positionDistance <= deltaPosition && orientationDistance <= deltaOrientation;
195
+ }
196
+ /**
197
+ * Position + orientation as a 6-element [x, y, z, roll, pitch, yaw] vector
198
+ * (Euler angles in rad, XYZ Tait-Bryan / Rx*Ry*Rz convention), matching
199
+ * wb-robotix's `Pose::toCartesian()`.
200
+ */
201
+ toCartesian() {
202
+ const { w, x, y, z } = Quaternion.fromRotationVector(this.orientation);
203
+ const roll = Math.atan2(2 * (w * x - y * z), 1 - 2 * (x * x + y * y));
204
+ const pitch = Math.asin(Math.min(1, Math.max(-1, 2 * (x * z + w * y))));
205
+ const yaw = Math.atan2(2 * (w * z - x * y), 1 - 2 * (y * y + z * z));
206
+ return [
207
+ ...this.position,
208
+ roll,
209
+ pitch,
210
+ yaw
211
+ ];
212
+ }
213
+ /** Human-readable `[x, y, z][rx, ry, rz]` representation, matching wb-robotix's `Pose::string()`. */
214
+ toString(precision = 6) {
215
+ const fmt = (v) => Number(v.toPrecision(precision));
216
+ return `[${this.position.map(fmt).join(", ")}][${this.orientation.map(fmt).join(", ")}]`;
217
+ }
218
+ toJSON() {
219
+ return {
220
+ position: this.position,
221
+ orientation: this.orientation
222
+ };
223
+ }
224
+ };
225
+ //#endregion
226
+ //#region src/lib/experimental/math/withMath.ts
227
+ function isPlainObject(value) {
228
+ return typeof value === "object" && value !== null && !Array.isArray(value) && !(value instanceof Pose);
229
+ }
230
+ function isNumberTriple(value) {
231
+ return Array.isArray(value) && value.length === 3 && value.every((n) => typeof n === "number");
232
+ }
233
+ /**
234
+ * Structural check for the wire shape of `Pose` (`{ position, orientation }`,
235
+ * each a 3-number array, and nothing else). This API has no other type with
236
+ * this exact shape, so it reliably identifies `Pose` values without needing
237
+ * per-endpoint knowledge of which fields are poses.
238
+ */
239
+ function isPoseShape(value) {
240
+ return Object.keys(value).length === 2 && isNumberTriple(value.position) && isNumberTriple(value.orientation);
241
+ }
242
+ function augmentPosesInPlace(value) {
243
+ if (Array.isArray(value)) {
244
+ for (const item of value) augmentPosesInPlace(item);
245
+ return;
246
+ }
247
+ if (isPlainObject(value)) if (isPoseShape(value)) Object.setPrototypeOf(value, Pose.prototype);
248
+ else for (const key of Object.keys(value)) augmentPosesInPlace(value[key]);
249
+ }
250
+ /**
251
+ * Recursively walks a parsed API response/request body, upgrading any
252
+ * `Pose`-shaped objects in place to `Pose` instances so they gain math
253
+ * methods (`multiply`, `inverse`, ...) while remaining JSON/wire-compatible.
254
+ * Used by `withMath` for `nova.api.*` responses; exported so it can also
255
+ * be applied manually to e.g. websocket messages.
256
+ */
257
+ function augmentPoses(value) {
258
+ augmentPosesInPlace(value);
259
+ return value;
260
+ }
261
+ function wrapWithPoseAugmentation(target) {
262
+ return new Proxy(target, { get(t, prop) {
263
+ const value = Reflect.get(t, prop, t);
264
+ if (typeof value === "function") return (...args) => {
265
+ const result = value.apply(t, args);
266
+ return result instanceof Promise ? result.then((data) => augmentPoses(data)) : result;
267
+ };
268
+ if (value !== null && typeof value === "object") return wrapWithPoseAugmentation(value);
269
+ return value;
270
+ } });
271
+ }
272
+ /**
273
+ * Wraps a `Nova` instance so every `nova.api.*` call automatically upgrades
274
+ * `Pose`-shaped fields in its response to `Pose` instances (with math
275
+ * methods like `multiply`/`inverse`). Returns a new proxied view - does not
276
+ * mutate the original `nova` instance.
277
+ */
278
+ function withMath(nova) {
279
+ return new Proxy(nova, { get(target, prop) {
280
+ if (prop === "api") return wrapWithPoseAugmentation(target.api);
281
+ return Reflect.get(target, prop, target);
282
+ } });
283
+ }
284
+ //#endregion
285
+ export { Pose, Quaternion, augmentPoses, withMath };
286
+
287
+ //# sourceMappingURL=index.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.mjs","names":[],"sources":["../../../src/lib/experimental/math/Quaternion.ts","../../../src/lib/experimental/math/Pose.ts","../../../src/lib/experimental/math/withMath.ts"],"sourcesContent":["/**\n * A unit quaternion representing a 3D rotation. Mirrors the subset of\n * `Eigen::Quaternion`'s API (see wb-robotix's `QuaternionBasePlugin.h`) that\n * `Pose` composition needs: Hamilton product, conjugate/inverse, vector\n * rotation, and axis-angle rotation-vector conversion. Instances are\n * immutable - every method returns a new `Quaternion`.\n */\n\nexport type QuaternionData = { w: number; x: number; y: number; z: number }\n\nconst EPSILON = 1e-12\n\nexport class Quaternion implements QuaternionData {\n readonly w: number\n readonly x: number\n readonly y: number\n readonly z: number\n\n constructor(w = 1, x = 0, y = 0, z = 0) {\n this.w = w\n this.x = x\n this.y = y\n this.z = z\n }\n\n static identity(): Quaternion {\n return new Quaternion()\n }\n\n /**\n * Construct from an axis-angle rotation vector [rx, ry, rz] (magnitude =\n * angle in rad), matching `Eigen::Quaternion::FromRotationVector` (ported\n * from wb-robotix's `QuaternionBasePlugin.h`).\n */\n static fromRotationVector(rotationVector: number[]): Quaternion {\n const rx = rotationVector[0] ?? 0\n const ry = rotationVector[1] ?? 0\n const rz = rotationVector[2] ?? 0\n\n const angle = Math.sqrt(rx * rx + ry * ry + rz * rz)\n if (angle < EPSILON) {\n return Quaternion.identity()\n }\n const half = angle / 2\n const s = Math.sin(half) / angle\n return new Quaternion(Math.cos(half), rx * s, ry * s, rz * s)\n }\n\n /**\n * Rotation vector [rx, ry, rz] representation (magnitude = angle in rad),\n * matching `Eigen::Quaternion::toRotationVector()` (ported from\n * wb-robotix's `QuaternionBasePlugin.h`): using `atan2` rather than `acos`\n * keeps the angle canonically within [0, pi] regardless of which of the\n * two antipodal unit quaternions (this or its negation) represents the\n * rotation, rather than acos's [0, 2*pi] range - important since composed\n * poses need to match the same canonical rotation vector the robot\n * controller itself would report for a pose.\n */\n toRotationVector(): number[] {\n const vecNorm = Math.sqrt(\n this.x * this.x + this.y * this.y + this.z * this.z,\n )\n if (vecNorm < EPSILON) {\n return [0, 0, 0]\n }\n const angle = 2 * Math.atan2(vecNorm, Math.abs(this.w))\n const scale = (this.w >= 0 ? angle : -angle) / vecNorm\n return [this.x * scale, this.y * scale, this.z * scale]\n }\n\n /** Hamilton product: `a.multiply(b)` applied to a vector rotates by `b` first, then by `a`. */\n multiply(other: QuaternionData): Quaternion {\n const { w: aw, x: ax, y: ay, z: az } = this\n const { w: bw, x: bx, y: by, z: bz } = other\n return new Quaternion(\n aw * bw - ax * bx - ay * by - az * bz,\n aw * bx + ax * bw + ay * bz - az * by,\n aw * by - ax * bz + ay * bw + az * bx,\n aw * bz + ax * by - ay * bx + az * bw,\n )\n }\n\n /** Conjugate: negates the vector part. Equal to `inverse()` for unit quaternions. */\n conjugate(): Quaternion {\n return new Quaternion(this.w, -this.x, -this.y, -this.z)\n }\n\n /** Inverse rotation. Equivalent to `conjugate()` since a `Pose`'s orientation is always a unit quaternion. */\n inverse(): Quaternion {\n return this.conjugate()\n }\n\n /**\n * The angle (in rad, always within [0, pi]) of the rotation that takes\n * `other` to `this`, matching Eigen's built-in `Quaternion::angularDistance()`.\n */\n angularDistance(other: QuaternionData): number {\n const relative = this.multiply({\n w: other.w,\n x: -other.x,\n y: -other.y,\n z: -other.z,\n })\n const vecNorm = Math.sqrt(\n relative.x * relative.x +\n relative.y * relative.y +\n relative.z * relative.z,\n )\n return 2 * Math.atan2(vecNorm, Math.abs(relative.w))\n }\n\n /** Rotate a 3D vector by this quaternion. */\n rotateVector(v: number[]): number[] {\n const vx = v[0] ?? 0\n const vy = v[1] ?? 0\n const vz = v[2] ?? 0\n const { w, x, y, z } = this\n\n // t = 2 * cross(q.xyz, v)\n const tx = 2 * (y * vz - z * vy)\n const ty = 2 * (z * vx - x * vz)\n const tz = 2 * (x * vy - y * vx)\n\n // v' = v + w*t + cross(q.xyz, t)\n return [\n vx + w * tx + (y * tz - z * ty),\n vy + w * ty + (z * tx - x * tz),\n vz + w * tz + (x * ty - y * tx),\n ]\n }\n\n toJSON(): QuaternionData {\n return { w: this.w, x: this.x, y: this.y, z: this.z }\n }\n}\n","import type { Pose as PoseData } from \"@wandelbots/nova-api/v2\"\nimport { Quaternion } from \"./Quaternion.ts\"\n\nconst ZERO_VECTOR = [0, 0, 0]\n\n// Matches wb-robotix's Constants.h TOLERANCE_MILLI, the default isApprox tolerance.\nconst TOLERANCE_MILLI = 1e-3\n\nfunction addVectors(a: number[], b: number[]): number[] {\n return [\n (a[0] ?? 0) + (b[0] ?? 0),\n (a[1] ?? 0) + (b[1] ?? 0),\n (a[2] ?? 0) + (b[2] ?? 0),\n ]\n}\n\nfunction negateVector(a: number[]): number[] {\n return [-(a[0] ?? 0), -(a[1] ?? 0), -(a[2] ?? 0)]\n}\n\nfunction isFiniteVector3(v: number[]): boolean {\n return v.length === 3 && v.every((n) => Number.isFinite(n))\n}\n\n/**\n * A `Pose` (position + axis-angle orientation) with methods for composing\n * and inverting transforms. Instances are plain-data compatible with the\n * wire-format `PoseData` type (own `position`/`orientation` properties only,\n * no enumerable methods), so they can be passed directly back into API calls\n * that expect a pose.\n */\nexport class Pose implements PoseData {\n readonly position: number[]\n readonly orientation: number[]\n\n constructor(\n position: number[] = ZERO_VECTOR,\n orientation: number[] = ZERO_VECTOR,\n ) {\n if (!isFiniteVector3(position)) {\n throw new Error(\n `Pose constructor: position must be an array of 3 finite numbers, got ${JSON.stringify(position)}`,\n )\n }\n if (!isFiniteVector3(orientation)) {\n throw new Error(\n `Pose constructor: orientation must be an array of 3 finite numbers, got ${JSON.stringify(orientation)}`,\n )\n }\n // Copy defensively so mutating the caller's arrays afterward can't\n // change this (supposedly immutable) instance's state.\n this.position = [...position]\n this.orientation = [...orientation]\n }\n\n static from(pose: PoseData): Pose {\n if (pose instanceof Pose) {\n return pose\n }\n return new Pose(\n pose.position ?? ZERO_VECTOR,\n pose.orientation ?? ZERO_VECTOR,\n )\n }\n\n static identity(): Pose {\n return new Pose()\n }\n\n /**\n * Compose this pose with `other`, treating `other` as being expressed in\n * this pose's coordinate frame. Equivalent to the homogeneous transform\n * product `this * other`: applying the result to a point is the same as\n * applying `other` first, then `this`.\n */\n multiply(other: PoseData): Pose {\n const q1 = Quaternion.fromRotationVector(this.orientation)\n const q2 = Quaternion.fromRotationVector(other.orientation ?? ZERO_VECTOR)\n\n const position = addVectors(\n this.position,\n q1.rotateVector(other.position ?? ZERO_VECTOR),\n )\n const orientation = q1.multiply(q2).toRotationVector()\n\n return new Pose(position, orientation)\n }\n\n /** The inverse transform, such that `pose.multiply(pose.inverse())` is the identity pose. */\n inverse(): Pose {\n const qInverse = Quaternion.fromRotationVector(this.orientation).inverse()\n\n const position = qInverse.rotateVector(negateVector(this.position))\n // Negating an axis-angle vector gives the inverse rotation directly.\n const orientation = negateVector(this.orientation)\n\n return new Pose(position, orientation)\n }\n\n /** Apply this pose's transform to a point, returning the transformed point. */\n transformPoint(point: number[]): number[] {\n const q = Quaternion.fromRotationVector(this.orientation)\n return addVectors(this.position, q.rotateVector(point))\n }\n\n /**\n * Compare to another pose using separate position/orientation tolerances,\n * matching wb-robotix's `Pose::isApprox()`: Euclidean distance for\n * position, and quaternion angular distance for orientation - not a naive\n * per-component diff, since two rotation vectors can represent nearly\n * identical rotations while differing componentwise near a\n * canonicalization boundary (e.g. close to the +/-pi wraparound).\n */\n isApprox(\n other: PoseData,\n deltaPosition = TOLERANCE_MILLI,\n deltaOrientation = TOLERANCE_MILLI,\n ): boolean {\n const otherPosition = other.position ?? ZERO_VECTOR\n const otherOrientation = other.orientation ?? ZERO_VECTOR\n\n const positionDistance = Math.sqrt(\n this.position.reduce(\n (sum, v, i) => sum + (v - (otherPosition[i] ?? 0)) ** 2,\n 0,\n ),\n )\n const orientationDistance = Quaternion.fromRotationVector(\n this.orientation,\n ).angularDistance(Quaternion.fromRotationVector(otherOrientation))\n\n return (\n positionDistance <= deltaPosition &&\n orientationDistance <= deltaOrientation\n )\n }\n\n /**\n * Position + orientation as a 6-element [x, y, z, roll, pitch, yaw] vector\n * (Euler angles in rad, XYZ Tait-Bryan / Rx*Ry*Rz convention), matching\n * wb-robotix's `Pose::toCartesian()`.\n */\n toCartesian(): number[] {\n const { w, x, y, z } = Quaternion.fromRotationVector(this.orientation)\n\n const roll = Math.atan2(2 * (w * x - y * z), 1 - 2 * (x * x + y * y))\n const pitch = Math.asin(Math.min(1, Math.max(-1, 2 * (x * z + w * y))))\n const yaw = Math.atan2(2 * (w * z - x * y), 1 - 2 * (y * y + z * z))\n\n return [...this.position, roll, pitch, yaw]\n }\n\n /** Human-readable `[x, y, z][rx, ry, rz]` representation, matching wb-robotix's `Pose::string()`. */\n toString(precision = 6): string {\n const fmt = (v: number) => Number(v.toPrecision(precision))\n return `[${this.position.map(fmt).join(\", \")}][${this.orientation.map(fmt).join(\", \")}]`\n }\n\n toJSON(): PoseData {\n return { position: this.position, orientation: this.orientation }\n }\n}\n","import type { Pose as PoseData } from \"@wandelbots/nova-api/v2\"\nimport type { Nova } from \"../../Nova.ts\"\nimport { Pose } from \"./Pose.ts\"\n\ntype PoseKeys = keyof PoseData\n\ntype IsExactlyPoseData<T> = [Exclude<keyof T, PoseKeys>] extends [never]\n ? [Exclude<PoseKeys, keyof T>] extends [never]\n ? true\n : false\n : false\n\n// Depth cap avoids \"type instantiation is excessively deep\" on nova-api's\n// large (and possibly cyclic, e.g. compound colliders) generated type graph.\ntype MaxDepth = [\n unknown,\n unknown,\n unknown,\n unknown,\n unknown,\n unknown,\n unknown,\n unknown,\n unknown,\n unknown,\n]\n\ntype AugmentPosesCore<\n T,\n Depth extends readonly unknown[],\n> = Depth[\"length\"] extends MaxDepth[\"length\"]\n ? T\n : IsExactlyPoseData<T> extends true\n ? Pose\n : T extends readonly (infer U)[]\n ? DeepPoseAugmented<U, [...Depth, unknown]>[]\n : T extends object\n ? { [K in keyof T]: DeepPoseAugmented<T[K], [...Depth, unknown]> }\n : T\n\n/**\n * Type-level counterpart to `augmentPoses`: recursively replaces any\n * `PoseData`-shaped field with `Pose` (distributing over unions, e.g. so\n * optional `Pose | undefined` fields keep the `| undefined`), so the static\n * type matches what's actually returned at runtime.\n */\nexport type DeepPoseAugmented<\n T,\n Depth extends readonly unknown[] = [],\n> = T extends unknown ? AugmentPosesCore<T, Depth> : never\n\nfunction isPlainObject(value: unknown): value is Record<string, unknown> {\n return (\n typeof value === \"object\" &&\n value !== null &&\n !Array.isArray(value) &&\n !(value instanceof Pose)\n )\n}\n\nfunction isNumberTriple(value: unknown): value is number[] {\n return (\n Array.isArray(value) &&\n value.length === 3 &&\n value.every((n) => typeof n === \"number\")\n )\n}\n\n/**\n * Structural check for the wire shape of `Pose` (`{ position, orientation }`,\n * each a 3-number array, and nothing else). This API has no other type with\n * this exact shape, so it reliably identifies `Pose` values without needing\n * per-endpoint knowledge of which fields are poses.\n */\nfunction isPoseShape(value: Record<string, unknown>): boolean {\n return (\n Object.keys(value).length === 2 &&\n isNumberTriple(value.position) &&\n isNumberTriple(value.orientation)\n )\n}\n\nfunction augmentPosesInPlace(value: unknown): void {\n if (Array.isArray(value)) {\n for (const item of value) {\n augmentPosesInPlace(item)\n }\n return\n }\n\n if (isPlainObject(value)) {\n if (isPoseShape(value)) {\n Object.setPrototypeOf(value, Pose.prototype)\n } else {\n for (const key of Object.keys(value)) {\n augmentPosesInPlace(value[key])\n }\n }\n }\n}\n\n/**\n * Recursively walks a parsed API response/request body, upgrading any\n * `Pose`-shaped objects in place to `Pose` instances so they gain math\n * methods (`multiply`, `inverse`, ...) while remaining JSON/wire-compatible.\n * Used by `withMath` for `nova.api.*` responses; exported so it can also\n * be applied manually to e.g. websocket messages.\n */\nexport function augmentPoses<T>(value: T): DeepPoseAugmented<T> {\n augmentPosesInPlace(value)\n return value as DeepPoseAugmented<T>\n}\n\ntype WithAugmentedPosesCore<\n T,\n Depth extends readonly unknown[],\n> = Depth[\"length\"] extends MaxDepth[\"length\"]\n ? T\n : T extends (...args: infer A) => Promise<infer R>\n ? (...args: A) => Promise<DeepPoseAugmented<R>>\n : T extends object\n ? { [K in keyof T]: WithAugmentedPosesCore<T[K], [...Depth, unknown]> }\n : T\n\n/**\n * Recursively maps every method on `Nova[\"api\"]` (and its nested API groups)\n * so calls are typed as returning `DeepPoseAugmented` results, matching what\n * `withMath` does at runtime.\n */\ntype WithAugmentedPoses<T> = WithAugmentedPosesCore<T, []>\n\nexport type NovaWithMath = Omit<Nova, \"api\"> & {\n readonly api: WithAugmentedPoses<Nova[\"api\"]>\n}\n\nfunction wrapWithPoseAugmentation<T extends object>(target: T): T {\n return new Proxy(target, {\n get(t, prop) {\n const value = Reflect.get(t, prop, t)\n if (typeof value === \"function\") {\n return (...args: unknown[]) => {\n const result = (value as (...a: unknown[]) => unknown).apply(t, args)\n return result instanceof Promise\n ? result.then((data) => augmentPoses(data))\n : result\n }\n }\n if (value !== null && typeof value === \"object\") {\n return wrapWithPoseAugmentation(value)\n }\n return value\n },\n }) as T\n}\n\n/**\n * Wraps a `Nova` instance so every `nova.api.*` call automatically upgrades\n * `Pose`-shaped fields in its response to `Pose` instances (with math\n * methods like `multiply`/`inverse`). Returns a new proxied view - does not\n * mutate the original `nova` instance.\n */\nexport function withMath(nova: Nova): NovaWithMath {\n const proxy = new Proxy(nova, {\n get(target, prop) {\n if (prop === \"api\") {\n return wrapWithPoseAugmentation(target.api)\n }\n // receiver is `target`, not the proxy, so `this` inside Nova's own\n // methods stays bound to the original (un-proxied) instance.\n return Reflect.get(target, prop, target)\n },\n })\n\n return proxy as unknown as NovaWithMath\n}\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@wandelbots/nova-js",
3
3
  "type": "module",
4
- "version": "4.2.0",
4
+ "version": "4.3.0-pr.318.15fac7b",
5
5
  "description": "Official JS client for the Wandelbots API",
6
6
  "sideEffects": false,
7
7
  "packageManager": "pnpm@11.9.0+sha512.bd682d5d03fe525ef7c9fd6780c6884d1e756ac4c9c9fe00c538782824310dcf90e3ddc4f53835f06dfaebd5085e41855e0bcbb3b60de2ac5bbab89e5036f03b",
@@ -20,6 +20,10 @@
20
20
  "./experimental/nats": {
21
21
  "types": "./dist/experimental/nats/index.d.mts",
22
22
  "default": "./dist/experimental/nats/index.mjs"
23
+ },
24
+ "./experimental/math": {
25
+ "types": "./dist/experimental/math/index.d.mts",
26
+ "default": "./dist/experimental/math/index.mjs"
23
27
  }
24
28
  },
25
29
  "main": "./dist/index.mjs",
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Experimental pose math helpers (`Pose`, `Quaternion`, `withMath`) for
3
+ * working with position + axis-angle orientation values from the NOVA API.
4
+ *
5
+ * This API is experimental and may change without a major version bump.
6
+ */
7
+ export type { Pose as PoseData } from "@wandelbots/nova-api/v2"
8
+ export { Pose } from "../../lib/experimental/math/Pose.ts"
9
+ export { Quaternion } from "../../lib/experimental/math/Quaternion.ts"
10
+ export type { QuaternionData } from "../../lib/experimental/math/Quaternion.ts"
11
+ export {
12
+ augmentPoses,
13
+ withMath,
14
+ } from "../../lib/experimental/math/withMath.ts"
15
+ export type {
16
+ DeepPoseAugmented,
17
+ NovaWithMath,
18
+ } from "../../lib/experimental/math/withMath.ts"
@@ -0,0 +1,162 @@
1
+ import type { Pose as PoseData } from "@wandelbots/nova-api/v2"
2
+ import { Quaternion } from "./Quaternion.ts"
3
+
4
+ const ZERO_VECTOR = [0, 0, 0]
5
+
6
+ // Matches wb-robotix's Constants.h TOLERANCE_MILLI, the default isApprox tolerance.
7
+ const TOLERANCE_MILLI = 1e-3
8
+
9
+ function addVectors(a: number[], b: number[]): number[] {
10
+ return [
11
+ (a[0] ?? 0) + (b[0] ?? 0),
12
+ (a[1] ?? 0) + (b[1] ?? 0),
13
+ (a[2] ?? 0) + (b[2] ?? 0),
14
+ ]
15
+ }
16
+
17
+ function negateVector(a: number[]): number[] {
18
+ return [-(a[0] ?? 0), -(a[1] ?? 0), -(a[2] ?? 0)]
19
+ }
20
+
21
+ function isFiniteVector3(v: number[]): boolean {
22
+ return v.length === 3 && v.every((n) => Number.isFinite(n))
23
+ }
24
+
25
+ /**
26
+ * A `Pose` (position + axis-angle orientation) with methods for composing
27
+ * and inverting transforms. Instances are plain-data compatible with the
28
+ * wire-format `PoseData` type (own `position`/`orientation` properties only,
29
+ * no enumerable methods), so they can be passed directly back into API calls
30
+ * that expect a pose.
31
+ */
32
+ export class Pose implements PoseData {
33
+ readonly position: number[]
34
+ readonly orientation: number[]
35
+
36
+ constructor(
37
+ position: number[] = ZERO_VECTOR,
38
+ orientation: number[] = ZERO_VECTOR,
39
+ ) {
40
+ if (!isFiniteVector3(position)) {
41
+ throw new Error(
42
+ `Pose constructor: position must be an array of 3 finite numbers, got ${JSON.stringify(position)}`,
43
+ )
44
+ }
45
+ if (!isFiniteVector3(orientation)) {
46
+ throw new Error(
47
+ `Pose constructor: orientation must be an array of 3 finite numbers, got ${JSON.stringify(orientation)}`,
48
+ )
49
+ }
50
+ // Copy defensively so mutating the caller's arrays afterward can't
51
+ // change this (supposedly immutable) instance's state.
52
+ this.position = [...position]
53
+ this.orientation = [...orientation]
54
+ }
55
+
56
+ static from(pose: PoseData): Pose {
57
+ if (pose instanceof Pose) {
58
+ return pose
59
+ }
60
+ return new Pose(
61
+ pose.position ?? ZERO_VECTOR,
62
+ pose.orientation ?? ZERO_VECTOR,
63
+ )
64
+ }
65
+
66
+ static identity(): Pose {
67
+ return new Pose()
68
+ }
69
+
70
+ /**
71
+ * Compose this pose with `other`, treating `other` as being expressed in
72
+ * this pose's coordinate frame. Equivalent to the homogeneous transform
73
+ * product `this * other`: applying the result to a point is the same as
74
+ * applying `other` first, then `this`.
75
+ */
76
+ multiply(other: PoseData): Pose {
77
+ const q1 = Quaternion.fromRotationVector(this.orientation)
78
+ const q2 = Quaternion.fromRotationVector(other.orientation ?? ZERO_VECTOR)
79
+
80
+ const position = addVectors(
81
+ this.position,
82
+ q1.rotateVector(other.position ?? ZERO_VECTOR),
83
+ )
84
+ const orientation = q1.multiply(q2).toRotationVector()
85
+
86
+ return new Pose(position, orientation)
87
+ }
88
+
89
+ /** The inverse transform, such that `pose.multiply(pose.inverse())` is the identity pose. */
90
+ inverse(): Pose {
91
+ const qInverse = Quaternion.fromRotationVector(this.orientation).inverse()
92
+
93
+ const position = qInverse.rotateVector(negateVector(this.position))
94
+ // Negating an axis-angle vector gives the inverse rotation directly.
95
+ const orientation = negateVector(this.orientation)
96
+
97
+ return new Pose(position, orientation)
98
+ }
99
+
100
+ /** Apply this pose's transform to a point, returning the transformed point. */
101
+ transformPoint(point: number[]): number[] {
102
+ const q = Quaternion.fromRotationVector(this.orientation)
103
+ return addVectors(this.position, q.rotateVector(point))
104
+ }
105
+
106
+ /**
107
+ * Compare to another pose using separate position/orientation tolerances,
108
+ * matching wb-robotix's `Pose::isApprox()`: Euclidean distance for
109
+ * position, and quaternion angular distance for orientation - not a naive
110
+ * per-component diff, since two rotation vectors can represent nearly
111
+ * identical rotations while differing componentwise near a
112
+ * canonicalization boundary (e.g. close to the +/-pi wraparound).
113
+ */
114
+ isApprox(
115
+ other: PoseData,
116
+ deltaPosition = TOLERANCE_MILLI,
117
+ deltaOrientation = TOLERANCE_MILLI,
118
+ ): boolean {
119
+ const otherPosition = other.position ?? ZERO_VECTOR
120
+ const otherOrientation = other.orientation ?? ZERO_VECTOR
121
+
122
+ const positionDistance = Math.sqrt(
123
+ this.position.reduce(
124
+ (sum, v, i) => sum + (v - (otherPosition[i] ?? 0)) ** 2,
125
+ 0,
126
+ ),
127
+ )
128
+ const orientationDistance = Quaternion.fromRotationVector(
129
+ this.orientation,
130
+ ).angularDistance(Quaternion.fromRotationVector(otherOrientation))
131
+
132
+ return (
133
+ positionDistance <= deltaPosition &&
134
+ orientationDistance <= deltaOrientation
135
+ )
136
+ }
137
+
138
+ /**
139
+ * Position + orientation as a 6-element [x, y, z, roll, pitch, yaw] vector
140
+ * (Euler angles in rad, XYZ Tait-Bryan / Rx*Ry*Rz convention), matching
141
+ * wb-robotix's `Pose::toCartesian()`.
142
+ */
143
+ toCartesian(): number[] {
144
+ const { w, x, y, z } = Quaternion.fromRotationVector(this.orientation)
145
+
146
+ const roll = Math.atan2(2 * (w * x - y * z), 1 - 2 * (x * x + y * y))
147
+ const pitch = Math.asin(Math.min(1, Math.max(-1, 2 * (x * z + w * y))))
148
+ const yaw = Math.atan2(2 * (w * z - x * y), 1 - 2 * (y * y + z * z))
149
+
150
+ return [...this.position, roll, pitch, yaw]
151
+ }
152
+
153
+ /** Human-readable `[x, y, z][rx, ry, rz]` representation, matching wb-robotix's `Pose::string()`. */
154
+ toString(precision = 6): string {
155
+ const fmt = (v: number) => Number(v.toPrecision(precision))
156
+ return `[${this.position.map(fmt).join(", ")}][${this.orientation.map(fmt).join(", ")}]`
157
+ }
158
+
159
+ toJSON(): PoseData {
160
+ return { position: this.position, orientation: this.orientation }
161
+ }
162
+ }
@@ -0,0 +1,135 @@
1
+ /**
2
+ * A unit quaternion representing a 3D rotation. Mirrors the subset of
3
+ * `Eigen::Quaternion`'s API (see wb-robotix's `QuaternionBasePlugin.h`) that
4
+ * `Pose` composition needs: Hamilton product, conjugate/inverse, vector
5
+ * rotation, and axis-angle rotation-vector conversion. Instances are
6
+ * immutable - every method returns a new `Quaternion`.
7
+ */
8
+
9
+ export type QuaternionData = { w: number; x: number; y: number; z: number }
10
+
11
+ const EPSILON = 1e-12
12
+
13
+ export class Quaternion implements QuaternionData {
14
+ readonly w: number
15
+ readonly x: number
16
+ readonly y: number
17
+ readonly z: number
18
+
19
+ constructor(w = 1, x = 0, y = 0, z = 0) {
20
+ this.w = w
21
+ this.x = x
22
+ this.y = y
23
+ this.z = z
24
+ }
25
+
26
+ static identity(): Quaternion {
27
+ return new Quaternion()
28
+ }
29
+
30
+ /**
31
+ * Construct from an axis-angle rotation vector [rx, ry, rz] (magnitude =
32
+ * angle in rad), matching `Eigen::Quaternion::FromRotationVector` (ported
33
+ * from wb-robotix's `QuaternionBasePlugin.h`).
34
+ */
35
+ static fromRotationVector(rotationVector: number[]): Quaternion {
36
+ const rx = rotationVector[0] ?? 0
37
+ const ry = rotationVector[1] ?? 0
38
+ const rz = rotationVector[2] ?? 0
39
+
40
+ const angle = Math.sqrt(rx * rx + ry * ry + rz * rz)
41
+ if (angle < EPSILON) {
42
+ return Quaternion.identity()
43
+ }
44
+ const half = angle / 2
45
+ const s = Math.sin(half) / angle
46
+ return new Quaternion(Math.cos(half), rx * s, ry * s, rz * s)
47
+ }
48
+
49
+ /**
50
+ * Rotation vector [rx, ry, rz] representation (magnitude = angle in rad),
51
+ * matching `Eigen::Quaternion::toRotationVector()` (ported from
52
+ * wb-robotix's `QuaternionBasePlugin.h`): using `atan2` rather than `acos`
53
+ * keeps the angle canonically within [0, pi] regardless of which of the
54
+ * two antipodal unit quaternions (this or its negation) represents the
55
+ * rotation, rather than acos's [0, 2*pi] range - important since composed
56
+ * poses need to match the same canonical rotation vector the robot
57
+ * controller itself would report for a pose.
58
+ */
59
+ toRotationVector(): number[] {
60
+ const vecNorm = Math.sqrt(
61
+ this.x * this.x + this.y * this.y + this.z * this.z,
62
+ )
63
+ if (vecNorm < EPSILON) {
64
+ return [0, 0, 0]
65
+ }
66
+ const angle = 2 * Math.atan2(vecNorm, Math.abs(this.w))
67
+ const scale = (this.w >= 0 ? angle : -angle) / vecNorm
68
+ return [this.x * scale, this.y * scale, this.z * scale]
69
+ }
70
+
71
+ /** Hamilton product: `a.multiply(b)` applied to a vector rotates by `b` first, then by `a`. */
72
+ multiply(other: QuaternionData): Quaternion {
73
+ const { w: aw, x: ax, y: ay, z: az } = this
74
+ const { w: bw, x: bx, y: by, z: bz } = other
75
+ return new Quaternion(
76
+ aw * bw - ax * bx - ay * by - az * bz,
77
+ aw * bx + ax * bw + ay * bz - az * by,
78
+ aw * by - ax * bz + ay * bw + az * bx,
79
+ aw * bz + ax * by - ay * bx + az * bw,
80
+ )
81
+ }
82
+
83
+ /** Conjugate: negates the vector part. Equal to `inverse()` for unit quaternions. */
84
+ conjugate(): Quaternion {
85
+ return new Quaternion(this.w, -this.x, -this.y, -this.z)
86
+ }
87
+
88
+ /** Inverse rotation. Equivalent to `conjugate()` since a `Pose`'s orientation is always a unit quaternion. */
89
+ inverse(): Quaternion {
90
+ return this.conjugate()
91
+ }
92
+
93
+ /**
94
+ * The angle (in rad, always within [0, pi]) of the rotation that takes
95
+ * `other` to `this`, matching Eigen's built-in `Quaternion::angularDistance()`.
96
+ */
97
+ angularDistance(other: QuaternionData): number {
98
+ const relative = this.multiply({
99
+ w: other.w,
100
+ x: -other.x,
101
+ y: -other.y,
102
+ z: -other.z,
103
+ })
104
+ const vecNorm = Math.sqrt(
105
+ relative.x * relative.x +
106
+ relative.y * relative.y +
107
+ relative.z * relative.z,
108
+ )
109
+ return 2 * Math.atan2(vecNorm, Math.abs(relative.w))
110
+ }
111
+
112
+ /** Rotate a 3D vector by this quaternion. */
113
+ rotateVector(v: number[]): number[] {
114
+ const vx = v[0] ?? 0
115
+ const vy = v[1] ?? 0
116
+ const vz = v[2] ?? 0
117
+ const { w, x, y, z } = this
118
+
119
+ // t = 2 * cross(q.xyz, v)
120
+ const tx = 2 * (y * vz - z * vy)
121
+ const ty = 2 * (z * vx - x * vz)
122
+ const tz = 2 * (x * vy - y * vx)
123
+
124
+ // v' = v + w*t + cross(q.xyz, t)
125
+ return [
126
+ vx + w * tx + (y * tz - z * ty),
127
+ vy + w * ty + (z * tx - x * tz),
128
+ vz + w * tz + (x * ty - y * tx),
129
+ ]
130
+ }
131
+
132
+ toJSON(): QuaternionData {
133
+ return { w: this.w, x: this.x, y: this.y, z: this.z }
134
+ }
135
+ }
@@ -0,0 +1,175 @@
1
+ import type { Pose as PoseData } from "@wandelbots/nova-api/v2"
2
+ import type { Nova } from "../../Nova.ts"
3
+ import { Pose } from "./Pose.ts"
4
+
5
+ type PoseKeys = keyof PoseData
6
+
7
+ type IsExactlyPoseData<T> = [Exclude<keyof T, PoseKeys>] extends [never]
8
+ ? [Exclude<PoseKeys, keyof T>] extends [never]
9
+ ? true
10
+ : false
11
+ : false
12
+
13
+ // Depth cap avoids "type instantiation is excessively deep" on nova-api's
14
+ // large (and possibly cyclic, e.g. compound colliders) generated type graph.
15
+ type MaxDepth = [
16
+ unknown,
17
+ unknown,
18
+ unknown,
19
+ unknown,
20
+ unknown,
21
+ unknown,
22
+ unknown,
23
+ unknown,
24
+ unknown,
25
+ unknown,
26
+ ]
27
+
28
+ type AugmentPosesCore<
29
+ T,
30
+ Depth extends readonly unknown[],
31
+ > = Depth["length"] extends MaxDepth["length"]
32
+ ? T
33
+ : IsExactlyPoseData<T> extends true
34
+ ? Pose
35
+ : T extends readonly (infer U)[]
36
+ ? DeepPoseAugmented<U, [...Depth, unknown]>[]
37
+ : T extends object
38
+ ? { [K in keyof T]: DeepPoseAugmented<T[K], [...Depth, unknown]> }
39
+ : T
40
+
41
+ /**
42
+ * Type-level counterpart to `augmentPoses`: recursively replaces any
43
+ * `PoseData`-shaped field with `Pose` (distributing over unions, e.g. so
44
+ * optional `Pose | undefined` fields keep the `| undefined`), so the static
45
+ * type matches what's actually returned at runtime.
46
+ */
47
+ export type DeepPoseAugmented<
48
+ T,
49
+ Depth extends readonly unknown[] = [],
50
+ > = T extends unknown ? AugmentPosesCore<T, Depth> : never
51
+
52
+ function isPlainObject(value: unknown): value is Record<string, unknown> {
53
+ return (
54
+ typeof value === "object" &&
55
+ value !== null &&
56
+ !Array.isArray(value) &&
57
+ !(value instanceof Pose)
58
+ )
59
+ }
60
+
61
+ function isNumberTriple(value: unknown): value is number[] {
62
+ return (
63
+ Array.isArray(value) &&
64
+ value.length === 3 &&
65
+ value.every((n) => typeof n === "number")
66
+ )
67
+ }
68
+
69
+ /**
70
+ * Structural check for the wire shape of `Pose` (`{ position, orientation }`,
71
+ * each a 3-number array, and nothing else). This API has no other type with
72
+ * this exact shape, so it reliably identifies `Pose` values without needing
73
+ * per-endpoint knowledge of which fields are poses.
74
+ */
75
+ function isPoseShape(value: Record<string, unknown>): boolean {
76
+ return (
77
+ Object.keys(value).length === 2 &&
78
+ isNumberTriple(value.position) &&
79
+ isNumberTriple(value.orientation)
80
+ )
81
+ }
82
+
83
+ function augmentPosesInPlace(value: unknown): void {
84
+ if (Array.isArray(value)) {
85
+ for (const item of value) {
86
+ augmentPosesInPlace(item)
87
+ }
88
+ return
89
+ }
90
+
91
+ if (isPlainObject(value)) {
92
+ if (isPoseShape(value)) {
93
+ Object.setPrototypeOf(value, Pose.prototype)
94
+ } else {
95
+ for (const key of Object.keys(value)) {
96
+ augmentPosesInPlace(value[key])
97
+ }
98
+ }
99
+ }
100
+ }
101
+
102
+ /**
103
+ * Recursively walks a parsed API response/request body, upgrading any
104
+ * `Pose`-shaped objects in place to `Pose` instances so they gain math
105
+ * methods (`multiply`, `inverse`, ...) while remaining JSON/wire-compatible.
106
+ * Used by `withMath` for `nova.api.*` responses; exported so it can also
107
+ * be applied manually to e.g. websocket messages.
108
+ */
109
+ export function augmentPoses<T>(value: T): DeepPoseAugmented<T> {
110
+ augmentPosesInPlace(value)
111
+ return value as DeepPoseAugmented<T>
112
+ }
113
+
114
+ type WithAugmentedPosesCore<
115
+ T,
116
+ Depth extends readonly unknown[],
117
+ > = Depth["length"] extends MaxDepth["length"]
118
+ ? T
119
+ : T extends (...args: infer A) => Promise<infer R>
120
+ ? (...args: A) => Promise<DeepPoseAugmented<R>>
121
+ : T extends object
122
+ ? { [K in keyof T]: WithAugmentedPosesCore<T[K], [...Depth, unknown]> }
123
+ : T
124
+
125
+ /**
126
+ * Recursively maps every method on `Nova["api"]` (and its nested API groups)
127
+ * so calls are typed as returning `DeepPoseAugmented` results, matching what
128
+ * `withMath` does at runtime.
129
+ */
130
+ type WithAugmentedPoses<T> = WithAugmentedPosesCore<T, []>
131
+
132
+ export type NovaWithMath = Omit<Nova, "api"> & {
133
+ readonly api: WithAugmentedPoses<Nova["api"]>
134
+ }
135
+
136
+ function wrapWithPoseAugmentation<T extends object>(target: T): T {
137
+ return new Proxy(target, {
138
+ get(t, prop) {
139
+ const value = Reflect.get(t, prop, t)
140
+ if (typeof value === "function") {
141
+ return (...args: unknown[]) => {
142
+ const result = (value as (...a: unknown[]) => unknown).apply(t, args)
143
+ return result instanceof Promise
144
+ ? result.then((data) => augmentPoses(data))
145
+ : result
146
+ }
147
+ }
148
+ if (value !== null && typeof value === "object") {
149
+ return wrapWithPoseAugmentation(value)
150
+ }
151
+ return value
152
+ },
153
+ }) as T
154
+ }
155
+
156
+ /**
157
+ * Wraps a `Nova` instance so every `nova.api.*` call automatically upgrades
158
+ * `Pose`-shaped fields in its response to `Pose` instances (with math
159
+ * methods like `multiply`/`inverse`). Returns a new proxied view - does not
160
+ * mutate the original `nova` instance.
161
+ */
162
+ export function withMath(nova: Nova): NovaWithMath {
163
+ const proxy = new Proxy(nova, {
164
+ get(target, prop) {
165
+ if (prop === "api") {
166
+ return wrapWithPoseAugmentation(target.api)
167
+ }
168
+ // receiver is `target`, not the proxy, so `this` inside Nova's own
169
+ // methods stays bound to the original (un-proxied) instance.
170
+ return Reflect.get(target, prop, target)
171
+ },
172
+ })
173
+
174
+ return proxy as unknown as NovaWithMath
175
+ }