@driftengine/physics 3.61.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +202 -0
- package/NOTICE +9 -0
- package/README.md +503 -0
- package/dist/bodies.d.ts +121 -0
- package/dist/bodies.js +298 -0
- package/dist/buoyancy.d.ts +46 -0
- package/dist/buoyancy.js +45 -0
- package/dist/cloth.d.ts +226 -0
- package/dist/cloth.js +587 -0
- package/dist/collide/aabb.d.ts +10 -0
- package/dist/collide/aabb.js +11 -0
- package/dist/collide/body.d.ts +202 -0
- package/dist/collide/body.js +142 -0
- package/dist/collide/eject.d.ts +39 -0
- package/dist/collide/eject.js +142 -0
- package/dist/collide/index.d.ts +18 -0
- package/dist/collide/index.js +5 -0
- package/dist/collide/scratch.d.ts +8 -0
- package/dist/collide/scratch.js +9 -0
- package/dist/collide/segment.d.ts +12 -0
- package/dist/collide/segment.js +109 -0
- package/dist/collide/sweep.d.ts +3 -0
- package/dist/collide/sweep.js +1036 -0
- package/dist/colliderSet.d.ts +239 -0
- package/dist/colliderSet.js +546 -0
- package/dist/compoundContact.d.ts +70 -0
- package/dist/compoundContact.js +221 -0
- package/dist/controller.d.ts +351 -0
- package/dist/controller.js +641 -0
- package/dist/cylinder.d.ts +78 -0
- package/dist/cylinder.js +294 -0
- package/dist/cylinderContact.d.ts +7 -0
- package/dist/cylinderContact.js +704 -0
- package/dist/decompose.d.ts +105 -0
- package/dist/decompose.js +979 -0
- package/dist/events.d.ts +43 -0
- package/dist/events.js +95 -0
- package/dist/executor.d.ts +147 -0
- package/dist/executor.js +173 -0
- package/dist/faces.d.ts +26 -0
- package/dist/faces.js +34 -0
- package/dist/fingerprint.d.ts +61 -0
- package/dist/fingerprint.js +139 -0
- package/dist/groundMotor.d.ts +34 -0
- package/dist/groundMotor.js +41 -0
- package/dist/heightfieldShape.d.ts +128 -0
- package/dist/heightfieldShape.js +328 -0
- package/dist/index.d.ts +73 -0
- package/dist/index.js +47 -0
- package/dist/island.d.ts +55 -0
- package/dist/island.js +227 -0
- package/dist/islandSolve.d.ts +70 -0
- package/dist/islandSolve.js +171 -0
- package/dist/islandWorker.d.ts +18 -0
- package/dist/islandWorker.js +125 -0
- package/dist/joints.d.ts +180 -0
- package/dist/joints.js +1094 -0
- package/dist/manifold.d.ts +89 -0
- package/dist/manifold.js +608 -0
- package/dist/mass.d.ts +53 -0
- package/dist/mass.js +353 -0
- package/dist/meshContact.d.ts +57 -0
- package/dist/meshContact.js +393 -0
- package/dist/meshShape.d.ts +76 -0
- package/dist/meshShape.js +175 -0
- package/dist/pairs.d.ts +62 -0
- package/dist/pairs.js +154 -0
- package/dist/query.d.ts +71 -0
- package/dist/query.js +683 -0
- package/dist/ragdoll.d.ts +135 -0
- package/dist/ragdoll.js +489 -0
- package/dist/shape.d.ts +177 -0
- package/dist/shape.js +533 -0
- package/dist/solver.d.ts +136 -0
- package/dist/solver.js +601 -0
- package/dist/stage.d.ts +167 -0
- package/dist/stage.js +350 -0
- package/dist/stallEscape.d.ts +141 -0
- package/dist/stallEscape.js +254 -0
- package/dist/sweptStep.d.ts +173 -0
- package/dist/sweptStep.js +472 -0
- package/dist/tree.d.ts +61 -0
- package/dist/tree.js +348 -0
- package/dist/vehicle.d.ts +154 -0
- package/dist/vehicle.js +343 -0
- package/dist/workerPool.d.ts +107 -0
- package/dist/workerPool.js +239 -0
- package/dist/workers.d.ts +37 -0
- package/dist/workers.js +35 -0
- package/dist/world.d.ts +298 -0
- package/dist/world.js +712 -0
- package/package.json +55 -0
- package/src/bodies.ts +369 -0
- package/src/buoyancy.ts +78 -0
- package/src/cloth.ts +734 -0
- package/src/collide/aabb.ts +28 -0
- package/src/collide/body.ts +336 -0
- package/src/collide/eject.ts +156 -0
- package/src/collide/index.ts +26 -0
- package/src/collide/scratch.ts +10 -0
- package/src/collide/segment.ts +127 -0
- package/src/collide/sweep.ts +1132 -0
- package/src/colliderSet.ts +637 -0
- package/src/compoundContact.ts +284 -0
- package/src/controller.ts +817 -0
- package/src/cylinder.ts +340 -0
- package/src/cylinderContact.ts +825 -0
- package/src/decompose.ts +1198 -0
- package/src/events.ts +108 -0
- package/src/executor.ts +224 -0
- package/src/faces.ts +37 -0
- package/src/fingerprint.ts +146 -0
- package/src/groundMotor.ts +53 -0
- package/src/heightfieldShape.ts +395 -0
- package/src/index.ts +164 -0
- package/src/island.ts +219 -0
- package/src/islandSolve.ts +218 -0
- package/src/islandWorker.ts +141 -0
- package/src/joints.ts +1367 -0
- package/src/manifold.ts +798 -0
- package/src/mass.ts +424 -0
- package/src/meshContact.ts +443 -0
- package/src/meshShape.ts +261 -0
- package/src/pairs.ts +169 -0
- package/src/query.ts +844 -0
- package/src/ragdoll.ts +662 -0
- package/src/shape.ts +656 -0
- package/src/solver.ts +873 -0
- package/src/stage.ts +406 -0
- package/src/stallEscape.ts +362 -0
- package/src/sweptStep.ts +609 -0
- package/src/tree.ts +366 -0
- package/src/vehicle.ts +489 -0
- package/src/workerPool.ts +283 -0
- package/src/workers.ts +38 -0
- package/src/world.ts +961 -0
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
import type { Aabb } from './collide/index.ts';
|
|
2
|
+
import type { Manifold, ShapePose } from './manifold.ts';
|
|
3
|
+
import { collideShapes, createManifold } from './manifold.ts';
|
|
4
|
+
import { MAX_MESH_MANIFOLDS, collideMesh, isMesh } from './meshContact.ts';
|
|
5
|
+
import type { ConvexShape } from './shape.ts';
|
|
6
|
+
import { shapeBounds } from './shape.ts';
|
|
7
|
+
/**
|
|
8
|
+
* The most parts one body may be made of.
|
|
9
|
+
*
|
|
10
|
+
* **It lives here rather than beside `decomposeConvex`, and that is a measurement rather than a
|
|
11
|
+
* preference.** It was declared in `decompose.ts`, which is offline and which nothing in a frame
|
|
12
|
+
* should reach; importing one integer from there put the whole decomposition — the voxeliser, the
|
|
13
|
+
* flood fill, the merge, the direction table — into every bundle that touches the physics runtime.
|
|
14
|
+
* `physics-only` moved outside its 3% band and an A/B against this one import line was what said so.
|
|
15
|
+
*
|
|
16
|
+
* A constant about how many shapes a body may hold is a fact about bodies. `decompose.ts` imports it
|
|
17
|
+
* from here to cap `maxHulls`, which is the direction the dependency should have run in from the
|
|
18
|
+
* start.
|
|
19
|
+
*/
|
|
20
|
+
export const MAX_BODY_PARTS = 32;
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* A body made of several convex shapes, against whatever it meets.
|
|
24
|
+
*
|
|
25
|
+
* **The half of convex decomposition that lives in the tick.** `decomposeConvex` turns a concave
|
|
26
|
+
* mesh into hulls offline; without this there is nowhere for them to go, because `BodySet.shape`
|
|
27
|
+
* holds one shape per body and a decomposed body would have to become several bodies held together
|
|
28
|
+
* by joints. `world.ts`'s own refusal message said the runtime already accepted hulls, and it did
|
|
29
|
+
* not.
|
|
30
|
+
*
|
|
31
|
+
* **A part carries no offset and no rotation, and that is not a simplification.** A `ConvexShape` is
|
|
32
|
+
* a point cloud in the body's frame — `shape.ts` says so where it defines `vertices` — so a
|
|
33
|
+
* decomposition's parts are already where they belong. The kinematic `Body.parts` carries `ox/oy/oz`
|
|
34
|
+
* because it wraps shapes a caller built at the origin and placed afterwards; nothing here does
|
|
35
|
+
* that, and a field that is always zero is a field that is eventually set wrong.
|
|
36
|
+
*
|
|
37
|
+
* **This fills a set of manifolds where an ordinary pair fills one**, which is the shape
|
|
38
|
+
* `meshContact.ts` established: `prepareContact` appends a constraint rather than replacing one and
|
|
39
|
+
* `storeWarm` accumulates by pair, so the solver needed nothing new.
|
|
40
|
+
*/
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* How many contact planes one pair may produce.
|
|
44
|
+
*
|
|
45
|
+
* **The same number as `MAX_BODY_PARTS`, so a compound against a single convex shape can never
|
|
46
|
+
* overflow**: every part gets a plane. Two compounds can, at up to 32 by 32 part pairs against 32
|
|
47
|
+
* slots, and the overflow rule there is the mesh path's — stop when the set is full. That is the one
|
|
48
|
+
* place in this design where a contact can be dropped without anybody being told, and what would
|
|
49
|
+
* make it matter is two 32-part bodies interpenetrating deeply.
|
|
50
|
+
*/
|
|
51
|
+
export const MAX_PAIR_MANIFOLDS = MAX_BODY_PARTS;
|
|
52
|
+
|
|
53
|
+
/** Whether a body is made of parts rather than of one shape. */
|
|
54
|
+
export function isCompound(
|
|
55
|
+
parts: readonly ConvexShape[] | undefined,
|
|
56
|
+
): parts is readonly ConvexShape[] {
|
|
57
|
+
return parts !== undefined;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** How many shapes a body presents to the narrow phase, whichever way it is built. */
|
|
61
|
+
export function partCount(
|
|
62
|
+
shape: ConvexShape | undefined,
|
|
63
|
+
parts: readonly ConvexShape[] | undefined,
|
|
64
|
+
): number {
|
|
65
|
+
if (parts !== undefined) return parts.length;
|
|
66
|
+
return shape === undefined ? 0 : 1;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** One of them, by index. */
|
|
70
|
+
export function partAt(
|
|
71
|
+
shape: ConvexShape | undefined,
|
|
72
|
+
parts: readonly ConvexShape[] | undefined,
|
|
73
|
+
index: number,
|
|
74
|
+
): ConvexShape {
|
|
75
|
+
if (parts !== undefined) return parts[index] as ConvexShape;
|
|
76
|
+
return shape as ConvexShape;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* The largest distance from a body's own origin to any point of any part.
|
|
81
|
+
*
|
|
82
|
+
* What `boundsOf` needs, and it is a maximum rather than a sum because every part is already in the
|
|
83
|
+
* body's frame: a part built off to one side carries that offset inside its own `boundRadius`.
|
|
84
|
+
*/
|
|
85
|
+
export function partsBoundRadius(parts: readonly ConvexShape[]): number {
|
|
86
|
+
let radius = 0;
|
|
87
|
+
for (const part of parts) if (part.boundRadius > radius) radius = part.boundRadius;
|
|
88
|
+
return radius;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** Scratch spheres, one per part of each side, so pruning allocates nothing. */
|
|
92
|
+
const SPHERES_A = new Float64Array(MAX_BODY_PARTS * 4);
|
|
93
|
+
const SPHERES_B = new Float64Array(MAX_BODY_PARTS * 4);
|
|
94
|
+
const LOCAL: Aabb = { minX: 0, minY: 0, minZ: 0, maxX: 0, maxY: 0, maxZ: 0 };
|
|
95
|
+
const SCRATCH: Manifold = createManifold();
|
|
96
|
+
const MESH_SCRATCH: readonly Manifold[] = Array.from(
|
|
97
|
+
{ length: MAX_MESH_MANIFOLDS },
|
|
98
|
+
createManifold,
|
|
99
|
+
);
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* A bounding sphere per part, in world, so the pair loop is a distance test and not a full collide.
|
|
103
|
+
*
|
|
104
|
+
* Computed once for each side of a pair rather than once per part pair: with 32 parts a side that is
|
|
105
|
+
* 64 bound computations instead of 1,024 of them. `shapeBounds` reads a part's own points, which are
|
|
106
|
+
* in the body frame, so the centre only has to be carried through the pose.
|
|
107
|
+
*/
|
|
108
|
+
function fillSpheres(
|
|
109
|
+
shape: ConvexShape | undefined,
|
|
110
|
+
parts: readonly ConvexShape[] | undefined,
|
|
111
|
+
pose: ShapePose,
|
|
112
|
+
out: Float64Array,
|
|
113
|
+
): number {
|
|
114
|
+
const count = partCount(shape, parts);
|
|
115
|
+
for (let i = 0; i < count; i++) {
|
|
116
|
+
const part = partAt(shape, parts, i);
|
|
117
|
+
shapeBounds(part, LOCAL);
|
|
118
|
+
const cx = (LOCAL.minX + LOCAL.maxX) * 0.5;
|
|
119
|
+
const cy = (LOCAL.minY + LOCAL.maxY) * 0.5;
|
|
120
|
+
const cz = (LOCAL.minZ + LOCAL.maxZ) * 0.5;
|
|
121
|
+
const hx = (LOCAL.maxX - LOCAL.minX) * 0.5;
|
|
122
|
+
const hy = (LOCAL.maxY - LOCAL.minY) * 0.5;
|
|
123
|
+
const hz = (LOCAL.maxZ - LOCAL.minZ) * 0.5;
|
|
124
|
+
rotate(pose, cx, cy, cz, out, i * 4);
|
|
125
|
+
out[i * 4] = (out[i * 4] as number) + pose.x;
|
|
126
|
+
out[i * 4 + 1] = (out[i * 4 + 1] as number) + pose.y;
|
|
127
|
+
out[i * 4 + 2] = (out[i * 4 + 2] as number) + pose.z;
|
|
128
|
+
out[i * 4 + 3] = Math.sqrt(hx * hx + hy * hy + hz * hz);
|
|
129
|
+
}
|
|
130
|
+
return count;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/** A vector through a pose's rotation, written into `out` at `at`. */
|
|
134
|
+
function rotate(
|
|
135
|
+
pose: ShapePose,
|
|
136
|
+
x: number,
|
|
137
|
+
y: number,
|
|
138
|
+
z: number,
|
|
139
|
+
out: Float64Array,
|
|
140
|
+
at: number,
|
|
141
|
+
): void {
|
|
142
|
+
const { qx, qy, qz, qw } = pose;
|
|
143
|
+
const tx = 2 * (qy * z - qz * y);
|
|
144
|
+
const ty = 2 * (qz * x - qx * z);
|
|
145
|
+
const tz = 2 * (qx * y - qy * x);
|
|
146
|
+
out[at] = x + qw * tx + (qy * tz - qz * ty);
|
|
147
|
+
out[at + 1] = y + qw * ty + (qz * tx - qx * tz);
|
|
148
|
+
out[at + 2] = z + qw * tz + (qx * ty - qy * tx);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Every touching pair of parts, as one manifold each.
|
|
153
|
+
*
|
|
154
|
+
* **A mesh partner is the pair this row exists for**, and it gets a rule of its own. A decomposed
|
|
155
|
+
* body falling onto a level is a dynamic compound against a static mesh, so refusing it would refuse
|
|
156
|
+
* the case. Each part goes through `collideMesh` unchanged, and the budget is divided between the
|
|
157
|
+
* parts that find candidates, **never below one each**. That is a deliberate departure from the mesh
|
|
158
|
+
* path's own first-come rule: `collideMesh` stops at eight and drops the rest, which is defensible
|
|
159
|
+
* when the ninth candidate is a triangle and indefensible when it is a whole limb of the body — one
|
|
160
|
+
* leg of a chair sinking through a floor while the other three hold.
|
|
161
|
+
*/
|
|
162
|
+
export function collideCompound(
|
|
163
|
+
shapeA: ConvexShape | undefined,
|
|
164
|
+
partsA: readonly ConvexShape[] | undefined,
|
|
165
|
+
poseA: ShapePose,
|
|
166
|
+
shapeB: ConvexShape | undefined,
|
|
167
|
+
partsB: readonly ConvexShape[] | undefined,
|
|
168
|
+
poseB: ShapePose,
|
|
169
|
+
margin: number,
|
|
170
|
+
out: readonly Manifold[],
|
|
171
|
+
): number {
|
|
172
|
+
const countA = fillSpheres(shapeA, partsA, poseA, SPHERES_A);
|
|
173
|
+
const countB = fillSpheres(shapeB, partsB, poseB, SPHERES_B);
|
|
174
|
+
if (countA === 0 || countB === 0) return 0;
|
|
175
|
+
|
|
176
|
+
/*
|
|
177
|
+
* A mesh has no parts and never moves, so it is always the other side of the pair. Its budget is
|
|
178
|
+
* shared out below rather than spent by whichever part is tried first.
|
|
179
|
+
*/
|
|
180
|
+
const meshSide =
|
|
181
|
+
countA === 1 && isMesh(partAt(shapeA, partsA, 0))
|
|
182
|
+
? 0
|
|
183
|
+
: countB === 1 && isMesh(partAt(shapeB, partsB, 0))
|
|
184
|
+
? 1
|
|
185
|
+
: -1;
|
|
186
|
+
if (meshSide >= 0) {
|
|
187
|
+
return collideCompoundMesh(
|
|
188
|
+
meshSide === 0 ? shapeB : shapeA,
|
|
189
|
+
meshSide === 0 ? partsB : partsA,
|
|
190
|
+
meshSide === 0 ? poseB : poseA,
|
|
191
|
+
partAt(meshSide === 0 ? shapeA : shapeB, meshSide === 0 ? partsA : partsB, 0),
|
|
192
|
+
meshSide === 0 ? poseA : poseB,
|
|
193
|
+
meshSide === 0,
|
|
194
|
+
margin,
|
|
195
|
+
out,
|
|
196
|
+
);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
let count = 0;
|
|
200
|
+
for (let i = 0; i < countA && count < out.length; i++) {
|
|
201
|
+
const ax = SPHERES_A[i * 4] as number;
|
|
202
|
+
const ay = SPHERES_A[i * 4 + 1] as number;
|
|
203
|
+
const az = SPHERES_A[i * 4 + 2] as number;
|
|
204
|
+
const ar = (SPHERES_A[i * 4 + 3] as number) + margin;
|
|
205
|
+
const shapeI = partAt(shapeA, partsA, i);
|
|
206
|
+
for (let j = 0; j < countB && count < out.length; j++) {
|
|
207
|
+
const dx = (SPHERES_B[j * 4] as number) - ax;
|
|
208
|
+
const dy = (SPHERES_B[j * 4 + 1] as number) - ay;
|
|
209
|
+
const dz = (SPHERES_B[j * 4 + 2] as number) - az;
|
|
210
|
+
const reach = ar + (SPHERES_B[j * 4 + 3] as number);
|
|
211
|
+
if (dx * dx + dy * dy + dz * dz > reach * reach) continue;
|
|
212
|
+
const shapeJ = partAt(shapeB, partsB, j);
|
|
213
|
+
if (!collideShapes(shapeI, poseA, shapeJ, poseB, margin, SCRATCH)) continue;
|
|
214
|
+
copyInto(SCRATCH, out[count] as Manifold, partKey(i, j));
|
|
215
|
+
count++;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
return count;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/** Every part of a compound against one static triangle mesh, sharing the manifold budget. */
|
|
222
|
+
function collideCompoundMesh(
|
|
223
|
+
shape: ConvexShape | undefined,
|
|
224
|
+
parts: readonly ConvexShape[] | undefined,
|
|
225
|
+
pose: ShapePose,
|
|
226
|
+
mesh: ConvexShape,
|
|
227
|
+
meshPose: ShapePose,
|
|
228
|
+
meshIsA: boolean,
|
|
229
|
+
margin: number,
|
|
230
|
+
out: readonly Manifold[],
|
|
231
|
+
): number {
|
|
232
|
+
const count = partCount(shape, parts);
|
|
233
|
+
/*
|
|
234
|
+
* At least one slot each, and the remainder shared. A part that touches the mesh always gets a
|
|
235
|
+
* contact plane; a part that touches it in three places gets three only if the budget allows.
|
|
236
|
+
*/
|
|
237
|
+
const share = Math.max(1, Math.floor(out.length / Math.max(1, count)));
|
|
238
|
+
let filled = 0;
|
|
239
|
+
for (let i = 0; i < count && filled < out.length; i++) {
|
|
240
|
+
const part = partAt(shape, parts, i);
|
|
241
|
+
/*
|
|
242
|
+
* The scratch set is passed whole and the *copy* is what the share limits — slicing it here
|
|
243
|
+
* would allocate an array per part per pair, in the tick, which is the one thing this package
|
|
244
|
+
* does not do.
|
|
245
|
+
*/
|
|
246
|
+
const found = meshIsA
|
|
247
|
+
? collideMesh(mesh, meshPose, part, pose, margin, MESH_SCRATCH)
|
|
248
|
+
: collideMesh(part, pose, mesh, meshPose, margin, MESH_SCRATCH);
|
|
249
|
+
const room = Math.min(found, share, out.length - filled);
|
|
250
|
+
for (let m = 0; m < room && filled < out.length; m++) {
|
|
251
|
+
copyInto(MESH_SCRATCH[m] as Manifold, out[filled] as Manifold, partKey(i, 0));
|
|
252
|
+
filled++;
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
return filled;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* The two part indices, folded into one number the feature id can carry.
|
|
260
|
+
*
|
|
261
|
+
* `meshContact.ts` folds a triangle index into every feature id for the same reason: warm starting
|
|
262
|
+
* pairs an accumulated impulse to a contact by identity, and a body resting across two parts would
|
|
263
|
+
* otherwise hand one part's impulse to the other on the tick a contact moves between them.
|
|
264
|
+
*/
|
|
265
|
+
function partKey(a: number, b: number): number {
|
|
266
|
+
return a * MAX_BODY_PARTS + b;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/** A manifold copied into the pair's set, with the part pair folded into every feature id. */
|
|
270
|
+
function copyInto(from: Manifold, to: Manifold, key: number): void {
|
|
271
|
+
to.nx = from.nx;
|
|
272
|
+
to.ny = from.ny;
|
|
273
|
+
to.nz = from.nz;
|
|
274
|
+
to.count = from.count;
|
|
275
|
+
to.curvedA = from.curvedA;
|
|
276
|
+
to.curvedB = from.curvedB;
|
|
277
|
+
for (let i = 0; i < from.count; i++) {
|
|
278
|
+
to.points[i * 3] = from.points[i * 3] ?? 0;
|
|
279
|
+
to.points[i * 3 + 1] = from.points[i * 3 + 1] ?? 0;
|
|
280
|
+
to.points[i * 3 + 2] = from.points[i * 3 + 2] ?? 0;
|
|
281
|
+
to.separations[i] = from.separations[i] ?? 0;
|
|
282
|
+
to.featureIds[i] = (((key & 0xffff) << 12) ^ (from.featureIds[i] ?? 0)) | 0;
|
|
283
|
+
}
|
|
284
|
+
}
|