@flighthq/node 0.2.0 → 0.2.1-next.410.a23f189
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/dist/boundsRectangle.d.ts.map +1 -1
- package/dist/boundsRectangle.js +9 -9
- package/dist/boundsRectangle.js.map +1 -1
- package/dist/hasAppearance.d.ts.map +1 -1
- package/dist/hasAppearance.js +0 -1
- package/dist/hasAppearance.js.map +1 -1
- package/dist/hasBlendMode.d.ts +3 -0
- package/dist/hasBlendMode.d.ts.map +1 -0
- package/dist/hasBlendMode.js +4 -0
- package/dist/hasBlendMode.js.map +1 -0
- package/dist/hasTransform2d.js +2 -2
- package/dist/hasTransform2d.js.map +1 -1
- package/dist/hasTransform3d.d.ts.map +1 -1
- package/dist/hasTransform3d.js +7 -3
- package/dist/hasTransform3d.js.map +1 -1
- package/dist/hierarchy.js +4 -4
- package/dist/hierarchy.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/node.d.ts.map +1 -1
- package/dist/node.js +3 -0
- package/dist/node.js.map +1 -1
- package/dist/revision.d.ts.map +1 -1
- package/dist/revision.js +16 -5
- package/dist/revision.js.map +1 -1
- package/dist/transform2d.d.ts +8 -5
- package/dist/transform2d.d.ts.map +1 -1
- package/dist/transform2d.js +53 -20
- package/dist/transform2d.js.map +1 -1
- package/dist/transform3d.d.ts +10 -3
- package/dist/transform3d.d.ts.map +1 -1
- package/dist/transform3d.js +79 -13
- package/dist/transform3d.js.map +1 -1
- package/dist/viewport.d.ts.map +1 -1
- package/dist/viewport.js +3 -2
- package/dist/viewport.js.map +1 -1
- package/package.json +5 -5
- package/src/boundsRectangle.test.ts +2 -2
- package/src/hasAppearance.test.ts +0 -10
- package/src/hasBlendMode.test.ts +25 -0
- package/src/hasTransform2d.test.ts +2 -2
- package/src/hasTransform3d.test.ts +18 -19
- package/src/hierarchy.test.ts +13 -13
- package/src/transform2d.test.ts +113 -51
- package/src/transform3d.test.ts +302 -100
package/src/transform3d.test.ts
CHANGED
|
@@ -1,16 +1,31 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
composeMatrix4,
|
|
3
|
+
createMatrix4,
|
|
4
|
+
createQuaternion,
|
|
5
|
+
createTransform3D,
|
|
6
|
+
createVector3,
|
|
7
|
+
multiplyMatrix4,
|
|
8
|
+
setQuaternionFromAxisAngle,
|
|
9
|
+
} from '@flighthq/geometry';
|
|
2
10
|
import type { HasTransform3D, HasTransform3DRuntime, NodeRuntime, Transform3DNode, Vector3Like } from '@flighthq/types';
|
|
3
11
|
import { describe, expect, it } from 'vitest';
|
|
4
12
|
|
|
5
13
|
import { initTransform3DRuntimeTrait, initTransform3DTrait } from './hasTransform3d';
|
|
6
|
-
import { addNodeChild } from './hierarchy';
|
|
14
|
+
import { addNodeChild, removeNodeChild } from './hierarchy';
|
|
7
15
|
import { createNode, getNodeRuntime } from './node';
|
|
8
16
|
import { invalidateNodeLocalTransform } from './revision';
|
|
9
17
|
import {
|
|
10
18
|
convertNodeVector3GlobalToLocal,
|
|
11
19
|
convertNodeVector3LocalToGlobal,
|
|
12
|
-
|
|
13
|
-
|
|
20
|
+
ensureNodeLocalMatrix4,
|
|
21
|
+
ensureNodeWorldMatrix4,
|
|
22
|
+
getNodeLocalMatrix4,
|
|
23
|
+
getNodeTransform3D,
|
|
24
|
+
getNodeWorldMatrix4,
|
|
25
|
+
isNodeLocalMatrix4Detached,
|
|
26
|
+
setNodeLocalMatrix4,
|
|
27
|
+
setNodeTransform3D,
|
|
28
|
+
syncNodeTransform3DFromMatrix4,
|
|
14
29
|
} from './transform3d';
|
|
15
30
|
|
|
16
31
|
const TestNodeKind = 'TestNode';
|
|
@@ -25,6 +40,37 @@ function createTestNode(): TestNode {
|
|
|
25
40
|
return node as TestNode;
|
|
26
41
|
}
|
|
27
42
|
|
|
43
|
+
function setNodeTranslation(node: TestNode, x: number, y: number, z: number): void {
|
|
44
|
+
node.position.x = x;
|
|
45
|
+
node.position.y = y;
|
|
46
|
+
node.position.z = z;
|
|
47
|
+
invalidateNodeLocalTransform(node);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Rotate about +Z so a child offset along +X swings toward +Y — an order-sensitive case pure
|
|
51
|
+
// translation cannot expose.
|
|
52
|
+
function setNodeRotationZ(node: TestNode, radians: number): void {
|
|
53
|
+
setQuaternionFromAxisAngle(node.rotation, _zAxis, radians);
|
|
54
|
+
invalidateNodeLocalTransform(node);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function setNodeScale(node: TestNode, x: number, y: number, z: number): void {
|
|
58
|
+
node.scale.x = x;
|
|
59
|
+
node.scale.y = y;
|
|
60
|
+
node.scale.z = z;
|
|
61
|
+
invalidateNodeLocalTransform(node);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const _zAxis = { x: 0, y: 0, z: 1 } as Vector3Like;
|
|
65
|
+
|
|
66
|
+
function translationMatrix(x: number, y: number, z: number) {
|
|
67
|
+
const m = createMatrix4();
|
|
68
|
+
m.m[12] = x;
|
|
69
|
+
m.m[13] = y;
|
|
70
|
+
m.m[14] = z;
|
|
71
|
+
return m;
|
|
72
|
+
}
|
|
73
|
+
|
|
28
74
|
function vec(): Vector3Like {
|
|
29
75
|
return { x: 0, y: 0, z: 0 } as Vector3Like;
|
|
30
76
|
}
|
|
@@ -41,58 +87,19 @@ describe('convertNodeVector3GlobalToLocal', () => {
|
|
|
41
87
|
|
|
42
88
|
it('inverts the node translation', () => {
|
|
43
89
|
const node = createTestNode();
|
|
44
|
-
node
|
|
45
|
-
node.localMatrix.m[13] = 20;
|
|
46
|
-
node.localMatrix.m[14] = 30;
|
|
47
|
-
invalidateNodeLocalTransform(node);
|
|
48
|
-
|
|
90
|
+
setNodeTranslation(node, 10, 20, 30);
|
|
49
91
|
const out = vec();
|
|
50
92
|
convertNodeVector3GlobalToLocal(out, node, { x: 11, y: 21, z: 31 } as Vector3Like);
|
|
51
93
|
expect(out.x).toBeCloseTo(1);
|
|
52
94
|
expect(out.y).toBeCloseTo(1);
|
|
53
95
|
expect(out.z).toBeCloseTo(1);
|
|
54
96
|
});
|
|
55
|
-
|
|
56
|
-
it('round-trips with convertNodeVector3LocalToGlobal', () => {
|
|
57
|
-
const parent = createTestNode();
|
|
58
|
-
const child = createTestNode();
|
|
59
|
-
addNodeChild(parent, child);
|
|
60
|
-
|
|
61
|
-
parent.localMatrix.m[12] = 7;
|
|
62
|
-
parent.localMatrix.m[13] = -2;
|
|
63
|
-
invalidateNodeLocalTransform(parent);
|
|
64
|
-
child.localMatrix.m[14] = 4;
|
|
65
|
-
invalidateNodeLocalTransform(child);
|
|
66
|
-
|
|
67
|
-
const local = { x: 1.5, y: 2.5, z: 3.5 } as Vector3Like;
|
|
68
|
-
const world = vec();
|
|
69
|
-
convertNodeVector3LocalToGlobal(world, child, local);
|
|
70
|
-
|
|
71
|
-
const back = vec();
|
|
72
|
-
convertNodeVector3GlobalToLocal(back, child, world);
|
|
73
|
-
expect(back.x).toBeCloseTo(local.x);
|
|
74
|
-
expect(back.y).toBeCloseTo(local.y);
|
|
75
|
-
expect(back.z).toBeCloseTo(local.z);
|
|
76
|
-
});
|
|
77
97
|
});
|
|
78
98
|
|
|
79
99
|
describe('convertNodeVector3LocalToGlobal', () => {
|
|
80
|
-
it('returns the point unchanged for an identity root node', () => {
|
|
81
|
-
const node = createTestNode();
|
|
82
|
-
const out = vec();
|
|
83
|
-
convertNodeVector3LocalToGlobal(out, node, { x: 1, y: 2, z: 3 } as Vector3Like);
|
|
84
|
-
expect(out.x).toBeCloseTo(1);
|
|
85
|
-
expect(out.y).toBeCloseTo(2);
|
|
86
|
-
expect(out.z).toBeCloseTo(3);
|
|
87
|
-
});
|
|
88
|
-
|
|
89
100
|
it('applies the node translation to a local point', () => {
|
|
90
101
|
const node = createTestNode();
|
|
91
|
-
node
|
|
92
|
-
node.localMatrix.m[13] = 20;
|
|
93
|
-
node.localMatrix.m[14] = 30;
|
|
94
|
-
invalidateNodeLocalTransform(node);
|
|
95
|
-
|
|
102
|
+
setNodeTranslation(node, 10, 20, 30);
|
|
96
103
|
const out = vec();
|
|
97
104
|
convertNodeVector3LocalToGlobal(out, node, { x: 1, y: 1, z: 1 } as Vector3Like);
|
|
98
105
|
expect(out.x).toBeCloseTo(11);
|
|
@@ -104,99 +111,294 @@ describe('convertNodeVector3LocalToGlobal', () => {
|
|
|
104
111
|
const parent = createTestNode();
|
|
105
112
|
const child = createTestNode();
|
|
106
113
|
addNodeChild(parent, child);
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
invalidateNodeLocalTransform(parent);
|
|
110
|
-
child.localMatrix.m[12] = 3;
|
|
111
|
-
invalidateNodeLocalTransform(child);
|
|
112
|
-
|
|
114
|
+
setNodeTranslation(parent, 5, 0, 0);
|
|
115
|
+
setNodeTranslation(child, 3, 0, 0);
|
|
113
116
|
const out = vec();
|
|
114
117
|
convertNodeVector3LocalToGlobal(out, child, { x: 0, y: 0, z: 0 } as Vector3Like);
|
|
115
118
|
expect(out.x).toBeCloseTo(8);
|
|
116
119
|
});
|
|
117
120
|
});
|
|
118
121
|
|
|
119
|
-
describe('
|
|
120
|
-
it('
|
|
122
|
+
describe('ensureNodeLocalMatrix4', () => {
|
|
123
|
+
it('composes the local matrix from translation/rotation/scale', () => {
|
|
121
124
|
const node = createTestNode();
|
|
122
|
-
node
|
|
125
|
+
setNodeTranslation(node, 2, 3, 4);
|
|
126
|
+
node.scale.x = 5;
|
|
123
127
|
invalidateNodeLocalTransform(node);
|
|
124
|
-
|
|
128
|
+
ensureNodeLocalMatrix4(node);
|
|
129
|
+
const m = getNodeLocalMatrix4(node);
|
|
130
|
+
expect(m.m[0]).toBeCloseTo(5);
|
|
131
|
+
expect(m.m[12]).toBeCloseTo(2);
|
|
132
|
+
expect(m.m[13]).toBeCloseTo(3);
|
|
133
|
+
expect(m.m[14]).toBeCloseTo(4);
|
|
134
|
+
});
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
describe('ensureNodeWorldMatrix4', () => {
|
|
138
|
+
it('computes the world matrix for a root node', () => {
|
|
139
|
+
const node = createTestNode();
|
|
140
|
+
setNodeTranslation(node, 5, 0, 0);
|
|
141
|
+
ensureNodeWorldMatrix4(node);
|
|
125
142
|
const runtime = getNodeRuntime(node) as NodeRuntime<TestTraits> & HasTransform3DRuntime;
|
|
126
|
-
expect(runtime.
|
|
127
|
-
expect(runtime.
|
|
143
|
+
expect(runtime.worldMatrix4).not.toBeNull();
|
|
144
|
+
expect(runtime.worldMatrix4!.m[12]).toBeCloseTo(5);
|
|
128
145
|
});
|
|
129
146
|
|
|
130
147
|
it('reuses a cached world matrix when no invalidation has occurred', () => {
|
|
131
148
|
const node = createTestNode();
|
|
132
|
-
|
|
149
|
+
ensureNodeWorldMatrix4(node);
|
|
133
150
|
const runtime = getNodeRuntime(node) as NodeRuntime<TestTraits> & HasTransform3DRuntime;
|
|
134
|
-
const first = runtime.
|
|
135
|
-
|
|
136
|
-
expect(runtime.
|
|
151
|
+
const first = runtime.worldMatrix4;
|
|
152
|
+
ensureNodeWorldMatrix4(node);
|
|
153
|
+
expect(runtime.worldMatrix4).toBe(first);
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
it('propagates a mid-chain parent change down to an already-cached grandchild', () => {
|
|
157
|
+
const root = createTestNode();
|
|
158
|
+
const parent = createTestNode();
|
|
159
|
+
const child = createTestNode();
|
|
160
|
+
addNodeChild(root, parent);
|
|
161
|
+
addNodeChild(parent, child);
|
|
162
|
+
setNodeTranslation(root, 1, 0, 0);
|
|
163
|
+
setNodeTranslation(parent, 2, 0, 0);
|
|
164
|
+
setNodeTranslation(child, 3, 0, 0);
|
|
165
|
+
expect(getNodeWorldMatrix4(child).m[12]).toBeCloseTo(6);
|
|
166
|
+
// Mutating the middle node must invalidate the grandchild's cached world through the chain.
|
|
167
|
+
setNodeTranslation(parent, 20, 0, 0);
|
|
168
|
+
expect(getNodeWorldMatrix4(child).m[12]).toBeCloseTo(24);
|
|
137
169
|
});
|
|
138
170
|
});
|
|
139
171
|
|
|
140
|
-
describe('
|
|
141
|
-
it('
|
|
172
|
+
describe('getNodeLocalMatrix4', () => {
|
|
173
|
+
it('reflects the authored translation', () => {
|
|
142
174
|
const node = createTestNode();
|
|
143
|
-
node
|
|
144
|
-
|
|
145
|
-
const m = getNodeWorldTransformMatrix4(node);
|
|
146
|
-
expect(m.m[12]).toBeCloseTo(3);
|
|
175
|
+
setNodeTranslation(node, 3, 0, 0);
|
|
176
|
+
expect(getNodeLocalMatrix4(node).m[12]).toBeCloseTo(3);
|
|
147
177
|
});
|
|
178
|
+
});
|
|
148
179
|
|
|
180
|
+
describe('getNodeTransform3D', () => {
|
|
181
|
+
it('reads the node TRS fields into a carrier', () => {
|
|
182
|
+
const node = createTestNode();
|
|
183
|
+
setNodeTranslation(node, 1, 2, 3);
|
|
184
|
+
node.scale.y = 4;
|
|
185
|
+
const out = createTransform3D();
|
|
186
|
+
getNodeTransform3D(out, node);
|
|
187
|
+
expect(out.position).toMatchObject({ x: 1, y: 2, z: 3 });
|
|
188
|
+
expect(out.scale.y).toBeCloseTo(4);
|
|
189
|
+
});
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
describe('getNodeWorldMatrix4', () => {
|
|
149
193
|
it('composes parent and child local matrices', () => {
|
|
150
194
|
const parent = createTestNode();
|
|
151
195
|
const child = createTestNode();
|
|
152
196
|
addNodeChild(parent, child);
|
|
153
|
-
parent
|
|
154
|
-
|
|
155
|
-
child.
|
|
156
|
-
invalidateNodeLocalTransform(child);
|
|
157
|
-
const m = getNodeWorldTransformMatrix4(child);
|
|
158
|
-
expect(m.m[12]).toBeCloseTo(15);
|
|
197
|
+
setNodeTranslation(parent, 10, 0, 0);
|
|
198
|
+
setNodeTranslation(child, 5, 0, 0);
|
|
199
|
+
expect(getNodeWorldMatrix4(child).m[12]).toBeCloseTo(15);
|
|
159
200
|
});
|
|
160
201
|
|
|
161
|
-
it('
|
|
162
|
-
const
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
202
|
+
it('updates when the parent transform changes', () => {
|
|
203
|
+
const parent = createTestNode();
|
|
204
|
+
const child = createTestNode();
|
|
205
|
+
addNodeChild(parent, child);
|
|
206
|
+
getNodeWorldMatrix4(child);
|
|
207
|
+
setNodeTranslation(parent, 7, 0, 0);
|
|
208
|
+
expect(getNodeWorldMatrix4(child).m[12]).toBeCloseTo(7);
|
|
209
|
+
});
|
|
167
210
|
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
211
|
+
it('rotates a child offset by the parent rotation (world = parentWorld × local)', () => {
|
|
212
|
+
const parent = createTestNode();
|
|
213
|
+
const child = createTestNode();
|
|
214
|
+
addNodeChild(parent, child);
|
|
215
|
+
setNodeRotationZ(parent, Math.PI / 2); // +90° about +Z maps +X -> +Y
|
|
216
|
+
setNodeTranslation(child, 1, 0, 0);
|
|
217
|
+
// The child sits at local +X; under the parent's rotation its world position swings to +Y.
|
|
218
|
+
// A reversed multiply order (local × parentWorld) would instead leave it at +X — this pins the order.
|
|
219
|
+
const world = getNodeWorldMatrix4(child);
|
|
220
|
+
expect(world.m[12]).toBeCloseTo(0);
|
|
221
|
+
expect(world.m[13]).toBeCloseTo(1);
|
|
222
|
+
expect(world.m[14]).toBeCloseTo(0);
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
it('composes parent translation after rotation onto a child offset', () => {
|
|
226
|
+
const parent = createTestNode();
|
|
227
|
+
const child = createTestNode();
|
|
228
|
+
addNodeChild(parent, child);
|
|
229
|
+
setNodeTranslation(parent, 10, 0, 0);
|
|
230
|
+
setNodeRotationZ(parent, Math.PI / 2);
|
|
231
|
+
setNodeTranslation(child, 1, 0, 0);
|
|
232
|
+
// parentWorld = T(10)·R(90°); child offset (1,0,0) rotates to (0,1,0) then translates to (10,1,0).
|
|
233
|
+
const world = getNodeWorldMatrix4(child);
|
|
234
|
+
expect(world.m[12]).toBeCloseTo(10);
|
|
235
|
+
expect(world.m[13]).toBeCloseTo(1);
|
|
236
|
+
expect(world.m[14]).toBeCloseTo(0);
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
it('scales a child offset and scale by the parent scale', () => {
|
|
240
|
+
const parent = createTestNode();
|
|
241
|
+
const child = createTestNode();
|
|
242
|
+
addNodeChild(parent, child);
|
|
243
|
+
setNodeScale(parent, 2, 2, 2);
|
|
244
|
+
setNodeTranslation(child, 3, 0, 0);
|
|
245
|
+
const world = getNodeWorldMatrix4(child);
|
|
246
|
+
expect(world.m[12]).toBeCloseTo(6); // parent scale multiplies the child's position
|
|
247
|
+
expect(world.m[0]).toBeCloseTo(2); // and its scale
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
it('composes a three-level translation/rotation/scale chain', () => {
|
|
251
|
+
const root = createTestNode();
|
|
252
|
+
const parent = createTestNode();
|
|
253
|
+
const child = createTestNode();
|
|
254
|
+
addNodeChild(root, parent);
|
|
255
|
+
addNodeChild(parent, child);
|
|
256
|
+
|
|
257
|
+
setNodeTranslation(root, 1, 2, 3);
|
|
258
|
+
setNodeRotationZ(root, Math.PI / 6);
|
|
259
|
+
setNodeScale(root, 2, 2, 2);
|
|
260
|
+
setNodeTranslation(parent, 4, 0, 0);
|
|
261
|
+
setNodeRotationZ(parent, Math.PI / 4);
|
|
262
|
+
setNodeTranslation(child, 0, 5, 0);
|
|
263
|
+
setNodeScale(child, 3, 1, 1);
|
|
264
|
+
|
|
265
|
+
// Oracle: world = local(root) × local(parent) × local(child), built from the same geometry
|
|
266
|
+
// primitives the implementation composes, so the test pins the wiring and order, not the math.
|
|
267
|
+
const localRoot = createMatrix4();
|
|
268
|
+
composeMatrix4(localRoot, root.position, root.rotation, root.scale);
|
|
269
|
+
const localParent = createMatrix4();
|
|
270
|
+
composeMatrix4(localParent, parent.position, parent.rotation, parent.scale);
|
|
271
|
+
const localChild = createMatrix4();
|
|
272
|
+
composeMatrix4(localChild, child.position, child.rotation, child.scale);
|
|
273
|
+
const rootTimesParent = createMatrix4();
|
|
274
|
+
multiplyMatrix4(rootTimesParent, localRoot, localParent);
|
|
275
|
+
const expected = createMatrix4();
|
|
276
|
+
multiplyMatrix4(expected, rootTimesParent, localChild);
|
|
277
|
+
|
|
278
|
+
const world = getNodeWorldMatrix4(child);
|
|
279
|
+
for (let i = 0; i < 16; i++) {
|
|
280
|
+
expect(world.m[i]).toBeCloseTo(expected.m[i]);
|
|
281
|
+
}
|
|
282
|
+
});
|
|
172
283
|
|
|
173
|
-
|
|
284
|
+
it('propagates a grandparent change through an unchanged middle node to a grandchild', () => {
|
|
285
|
+
const root = createTestNode();
|
|
286
|
+
const mid = createTestNode();
|
|
287
|
+
const leaf = createTestNode();
|
|
288
|
+
addNodeChild(root, mid);
|
|
289
|
+
addNodeChild(mid, leaf);
|
|
290
|
+
setNodeTranslation(root, 1, 0, 0);
|
|
291
|
+
setNodeTranslation(leaf, 2, 0, 0);
|
|
292
|
+
// Prime the grandchild's cached world (root 1 + leaf 2 = 3), then move only the grandparent.
|
|
293
|
+
expect(getNodeWorldMatrix4(leaf).m[12]).toBeCloseTo(3);
|
|
294
|
+
setNodeTranslation(root, 100, 0, 0);
|
|
295
|
+
// The middle node does not change locally, but its world moves — the grandchild must follow.
|
|
296
|
+
expect(getNodeWorldMatrix4(leaf).m[12]).toBeCloseTo(102);
|
|
174
297
|
});
|
|
175
298
|
|
|
176
|
-
it('world matrix
|
|
299
|
+
it('recomputes the world matrix after reparenting to a different parent', () => {
|
|
300
|
+
const parentA = createTestNode();
|
|
301
|
+
const parentB = createTestNode();
|
|
302
|
+
const child = createTestNode();
|
|
303
|
+
setNodeTranslation(parentA, 10, 0, 0);
|
|
304
|
+
setNodeTranslation(parentB, 0, 20, 0);
|
|
305
|
+
setNodeTranslation(child, 1, 0, 0);
|
|
306
|
+
|
|
307
|
+
addNodeChild(parentA, child);
|
|
308
|
+
expect(getNodeWorldMatrix4(child).m[12]).toBeCloseTo(11);
|
|
309
|
+
expect(getNodeWorldMatrix4(child).m[13]).toBeCloseTo(0);
|
|
310
|
+
|
|
311
|
+
// parentA and parentB carry the same local revision, so their composite worldTransformId
|
|
312
|
+
// collides; correct reparenting relies on invalidateNodeParentReference, not the id compare.
|
|
313
|
+
removeNodeChild(parentA, child);
|
|
314
|
+
addNodeChild(parentB, child);
|
|
315
|
+
const world = getNodeWorldMatrix4(child);
|
|
316
|
+
expect(world.m[12]).toBeCloseTo(1);
|
|
317
|
+
expect(world.m[13]).toBeCloseTo(20);
|
|
318
|
+
});
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
describe('isNodeLocalMatrix4Detached', () => {
|
|
322
|
+
it('is false for a TRS-authored node and true after a direct matrix set', () => {
|
|
177
323
|
const node = createTestNode();
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
324
|
+
setNodeTranslation(node, 1, 0, 0);
|
|
325
|
+
getNodeLocalMatrix4(node);
|
|
326
|
+
expect(isNodeLocalMatrix4Detached(node)).toBe(false);
|
|
327
|
+
setNodeLocalMatrix4(node, translationMatrix(9, 0, 0));
|
|
328
|
+
expect(isNodeLocalMatrix4Detached(node)).toBe(true);
|
|
329
|
+
});
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
describe('setNodeLocalMatrix4', () => {
|
|
333
|
+
it('sets the matrix directly and survives the local recompute', () => {
|
|
334
|
+
const node = createTestNode();
|
|
335
|
+
setNodeLocalMatrix4(node, translationMatrix(9, 8, 7));
|
|
336
|
+
// ensure must NOT recompose from the (dormant) TRS fields.
|
|
337
|
+
ensureNodeLocalMatrix4(node);
|
|
338
|
+
const m = getNodeLocalMatrix4(node);
|
|
339
|
+
expect(m.m[12]).toBeCloseTo(9);
|
|
340
|
+
expect(m.m[13]).toBeCloseTo(8);
|
|
341
|
+
expect(m.m[14]).toBeCloseTo(7);
|
|
184
342
|
});
|
|
185
343
|
|
|
186
|
-
it('
|
|
344
|
+
it('propagates to the world matrix', () => {
|
|
187
345
|
const parent = createTestNode();
|
|
188
346
|
const child = createTestNode();
|
|
189
347
|
addNodeChild(parent, child);
|
|
348
|
+
setNodeLocalMatrix4(parent, translationMatrix(4, 0, 0));
|
|
349
|
+
setNodeLocalMatrix4(child, translationMatrix(3, 0, 0));
|
|
350
|
+
expect(getNodeWorldMatrix4(child).m[12]).toBeCloseTo(7);
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
it('is overridden when a TRS write reclaims authority (last writer wins)', () => {
|
|
354
|
+
const node = createTestNode();
|
|
355
|
+
setNodeLocalMatrix4(node, translationMatrix(9, 0, 0));
|
|
356
|
+
setNodeTranslation(node, 2, 0, 0);
|
|
357
|
+
expect(isNodeLocalMatrix4Detached(node)).toBe(false);
|
|
358
|
+
expect(getNodeLocalMatrix4(node).m[12]).toBeCloseTo(2);
|
|
359
|
+
});
|
|
190
360
|
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
361
|
+
it('copies the source matrix rather than aliasing it', () => {
|
|
362
|
+
const node = createTestNode();
|
|
363
|
+
const source = translationMatrix(5, 0, 0);
|
|
364
|
+
setNodeLocalMatrix4(node, source);
|
|
365
|
+
source.m[12] = 999;
|
|
366
|
+
expect(getNodeLocalMatrix4(node).m[12]).toBeCloseTo(5);
|
|
367
|
+
});
|
|
368
|
+
});
|
|
195
369
|
|
|
196
|
-
|
|
197
|
-
|
|
370
|
+
describe('setNodeTransform3D', () => {
|
|
371
|
+
it('copies carrier TRS and rebuilds the matrix', () => {
|
|
372
|
+
const node = createTestNode();
|
|
373
|
+
const t = createTransform3D();
|
|
374
|
+
t.position = createVector3(6, 0, 0);
|
|
375
|
+
t.scale = createVector3(2, 2, 2);
|
|
376
|
+
setNodeTransform3D(node, t);
|
|
377
|
+
const m = getNodeLocalMatrix4(node);
|
|
378
|
+
expect(m.m[0]).toBeCloseTo(2);
|
|
379
|
+
expect(m.m[12]).toBeCloseTo(6);
|
|
380
|
+
});
|
|
381
|
+
});
|
|
382
|
+
|
|
383
|
+
describe('syncNodeTransform3DFromMatrix4', () => {
|
|
384
|
+
it('decomposes a directly-set matrix back into the TRS fields and clears detached', () => {
|
|
385
|
+
const node = createTestNode();
|
|
386
|
+
setNodeLocalMatrix4(node, translationMatrix(3, 4, 5));
|
|
387
|
+
syncNodeTransform3DFromMatrix4(node);
|
|
388
|
+
expect(isNodeLocalMatrix4Detached(node)).toBe(false);
|
|
389
|
+
expect(node.position.x).toBeCloseTo(3);
|
|
390
|
+
expect(node.position.y).toBeCloseTo(4);
|
|
391
|
+
expect(node.position.z).toBeCloseTo(5);
|
|
392
|
+
});
|
|
198
393
|
|
|
199
|
-
|
|
200
|
-
|
|
394
|
+
it('is non-destructive: the matrix cache is unchanged', () => {
|
|
395
|
+
const node = createTestNode();
|
|
396
|
+
node.rotation = createQuaternion();
|
|
397
|
+
setNodeLocalMatrix4(node, translationMatrix(3, 4, 5));
|
|
398
|
+
syncNodeTransform3DFromMatrix4(node);
|
|
399
|
+
const m = getNodeLocalMatrix4(node);
|
|
400
|
+
expect(m.m[12]).toBeCloseTo(3);
|
|
401
|
+
expect(m.m[13]).toBeCloseTo(4);
|
|
402
|
+
expect(m.m[14]).toBeCloseTo(5);
|
|
201
403
|
});
|
|
202
404
|
});
|