pelican-engine 0.3.20 → 0.3.22
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/AGENTS.md +8 -0
- package/distribution.json +12 -12
- package/docs/STATUS.md +4 -1
- package/docs/api/REFERENCE.md +105 -95
- package/docs/api/index.json +112 -96
- package/docs/npm/GAME-AUTHOR.md +8 -0
- package/engine.manifest.json +1 -1
- package/lib/sdk/index.d.ts +12 -1
- package/lib/sdk/index.js +1 -1
- package/lib/simulation/cloth-plugin.js +10 -3
- package/lib/simulation/cloth.js +82 -12
- package/package.json +1 -1
package/docs/npm/GAME-AUTHOR.md
CHANGED
|
@@ -233,6 +233,14 @@ manual cloth uses `ctx.cloth.step`. Use `setAnchor`, `releaseAnchor`, `applyForc
|
|
|
233
233
|
(next tick), `applyImpulse`, `setColliders`, `inspect`, and `destroy` to manage it.
|
|
234
234
|
The source checkout includes `examples/cloth-sheet`.
|
|
235
235
|
|
|
236
|
+
New cloth also resolves detected sphere overlaps against triangle surface interiors
|
|
237
|
+
using closest-point barycentric projection. `inspect(id).surfaceContacts` reports
|
|
238
|
+
the triangle and barycentric position. `surfaceCollisions` defaults to true for new
|
|
239
|
+
cloth and can be disabled. This is sphere-only discrete surface contact; planes
|
|
240
|
+
remain particle-only, and there is no edge/face continuous collision or rigid-body
|
|
241
|
+
reaction. Surface contact state is included in snapshots and invalid state is
|
|
242
|
+
rejected atomically. It has not been runtime or stability validated.
|
|
243
|
+
|
|
236
244
|
New cloth also resolves discrete self-contact between non-adjacent particles by
|
|
237
245
|
projecting movable particles apart; shared triangle edges are excluded.
|
|
238
246
|
`selfCollisions` defaults to true for new cloth and is reported by
|
package/engine.manifest.json
CHANGED
package/lib/sdk/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/** Pelican Engine public API. No renderer or GPU implementation types escape this module. */
|
|
2
|
-
export declare const SDK_VERSION = "0.3.
|
|
2
|
+
export declare const SDK_VERSION = "0.3.22";
|
|
3
3
|
export type Vec2 = readonly [number, number];
|
|
4
4
|
export type Vec3 = readonly [number, number, number];
|
|
5
5
|
export type Vec4 = readonly [number, number, number, number];
|
|
@@ -271,6 +271,8 @@ export interface ClothDefinition {
|
|
|
271
271
|
visual?: ClothVisualOptions;
|
|
272
272
|
/** Discrete non-adjacent particle self-contact; defaults to true for new cloth. */
|
|
273
273
|
selfCollisions?: boolean;
|
|
274
|
+
/** Sphere triangle-surface contacts; defaults to true for new cloth. */
|
|
275
|
+
surfaceCollisions?: boolean;
|
|
274
276
|
}
|
|
275
277
|
export interface ClothContact {
|
|
276
278
|
tick: number;
|
|
@@ -284,11 +286,19 @@ export interface ClothSelfContact {
|
|
|
284
286
|
secondParticle: number;
|
|
285
287
|
penetration: number;
|
|
286
288
|
}
|
|
289
|
+
export interface ClothSurfaceContact {
|
|
290
|
+
tick: number;
|
|
291
|
+
triangle: number;
|
|
292
|
+
colliderId: string;
|
|
293
|
+
penetration: number;
|
|
294
|
+
barycentric: [number, number, number];
|
|
295
|
+
}
|
|
287
296
|
export interface ClothState {
|
|
288
297
|
tick: number;
|
|
289
298
|
simulatedSeconds: number;
|
|
290
299
|
disposed: boolean;
|
|
291
300
|
selfCollisions: boolean;
|
|
301
|
+
surfaceCollisions: boolean;
|
|
292
302
|
positions: Vec3[];
|
|
293
303
|
velocities: Vec3[];
|
|
294
304
|
triangles: ClothTriangle[];
|
|
@@ -298,6 +308,7 @@ export interface ClothState {
|
|
|
298
308
|
}[];
|
|
299
309
|
contacts: ClothContact[];
|
|
300
310
|
selfContacts: ClothSelfContact[];
|
|
311
|
+
surfaceContacts: ClothSurfaceContact[];
|
|
301
312
|
maxDistanceError: number;
|
|
302
313
|
resources: {
|
|
303
314
|
particles: number;
|
package/lib/sdk/index.js
CHANGED
|
@@ -15,7 +15,7 @@ function planarUvs(points) {
|
|
|
15
15
|
return fail("VISUAL_UV", "Initial points need two nonzero projection axes or explicit UVs.");
|
|
16
16
|
return points.map(point => [(point[first] - min[first]) / width, (point[second] - min[second]) / height]);
|
|
17
17
|
}
|
|
18
|
-
function createEntry(input, fixedDt, materials, restoredSelfMode) {
|
|
18
|
+
function createEntry(input, fixedDt, materials, restoredSelfMode, restoredSurfaceMode) {
|
|
19
19
|
const definition = cloneSerializable(input);
|
|
20
20
|
if (!definition || !validId(definition.id))
|
|
21
21
|
return fail("ID", "Expected a nonempty id of at most 128 characters.");
|
|
@@ -23,6 +23,10 @@ function createEntry(input, fixedDt, materials, restoredSelfMode) {
|
|
|
23
23
|
return fail("AUTO_STEP", "autoStep must be boolean.");
|
|
24
24
|
if (definition.selfCollisions !== undefined && typeof definition.selfCollisions !== "boolean")
|
|
25
25
|
return fail("SELF_COLLISION", "selfCollisions must be boolean.");
|
|
26
|
+
if (definition.surfaceCollisions !== undefined && typeof definition.surfaceCollisions !== "boolean")
|
|
27
|
+
return fail("SURFACE_COLLISION", "surfaceCollisions must be boolean.");
|
|
28
|
+
if (restoredSurfaceMode !== undefined && definition.surfaceCollisions !== undefined && definition.surfaceCollisions !== restoredSurfaceMode)
|
|
29
|
+
return fail("SNAPSHOT", "Surface collision configuration does not match the saved solver.");
|
|
26
30
|
if (restoredSelfMode !== undefined && definition.selfCollisions !== undefined && definition.selfCollisions !== restoredSelfMode)
|
|
27
31
|
return fail("SNAPSHOT", "Self collision configuration does not match the saved solver.");
|
|
28
32
|
if (definition.fixedDt !== undefined && definition.fixedDt !== fixedDt)
|
|
@@ -39,7 +43,7 @@ function createEntry(input, fixedDt, materials, restoredSelfMode) {
|
|
|
39
43
|
visual.uvs.some(uv => !Array.isArray(uv) || uv.length !== 2 || !uv.every(value => typeof value === "number" && Number.isFinite(value) && Math.abs(value) <= 1e6))))
|
|
40
44
|
return fail("VISUAL_UV", "UVs must contain one finite 2D coordinate per particle.");
|
|
41
45
|
}
|
|
42
|
-
const solver = createCloth({ ...definition, fixedDt, ...(restoredSelfMode === undefined ? {} : { selfCollisions: restoredSelfMode }) });
|
|
46
|
+
const solver = createCloth({ ...definition, fixedDt, ...(restoredSelfMode === undefined ? {} : { selfCollisions: restoredSelfMode }), ...(restoredSurfaceMode === undefined ? {} : { surfaceCollisions: restoredSurfaceMode }) });
|
|
43
47
|
try {
|
|
44
48
|
const uvs = definition.visual ? (definition.visual.uvs ? definition.visual.uvs.map(uv => [uv[0], uv[1]]) : planarUvs(definition.points)) : [];
|
|
45
49
|
return { definition, solver, uvs };
|
|
@@ -62,7 +66,10 @@ function parseSnapshot(value, fixedDt, materials) {
|
|
|
62
66
|
const savedSelfMode = item.state?.config?.selfCollisions;
|
|
63
67
|
if (savedSelfMode !== undefined && typeof savedSelfMode !== "boolean")
|
|
64
68
|
fail("SNAPSHOT", "Invalid self collision configuration.");
|
|
65
|
-
const
|
|
69
|
+
const savedSurfaceMode = item.state?.config?.surfaceCollisions;
|
|
70
|
+
if (savedSurfaceMode !== undefined && typeof savedSurfaceMode !== "boolean")
|
|
71
|
+
fail("SNAPSHOT", "Invalid surface collision configuration.");
|
|
72
|
+
const entry = createEntry(item.definition, fixedDt, materials, savedSelfMode ?? false, savedSurfaceMode ?? false);
|
|
66
73
|
if (entries.has(entry.definition.id)) {
|
|
67
74
|
entry.solver.dispose();
|
|
68
75
|
fail("SNAPSHOT", "Duplicate cloth id.");
|
package/lib/simulation/cloth.js
CHANGED
|
@@ -96,6 +96,35 @@ function topology(points, input, bending) {
|
|
|
96
96
|
add(edge.opposite[0], edge.opposite[1]);
|
|
97
97
|
return { triangles, constraints };
|
|
98
98
|
}
|
|
99
|
+
function closestTriangle(point, a, b, c) {
|
|
100
|
+
const ab = [b[0] - a[0], b[1] - a[1], b[2] - a[2]], ac = [c[0] - a[0], c[1] - a[1], c[2] - a[2]], ap = [point[0] - a[0], point[1] - a[1], point[2] - a[2]];
|
|
101
|
+
const d1 = ab[0] * ap[0] + ab[1] * ap[1] + ab[2] * ap[2], d2 = ac[0] * ap[0] + ac[1] * ap[1] + ac[2] * ap[2];
|
|
102
|
+
if (d1 <= 0 && d2 <= 0)
|
|
103
|
+
return { point: copy(a), barycentric: [1, 0, 0] };
|
|
104
|
+
const bp = [point[0] - b[0], point[1] - b[1], point[2] - b[2]], d3 = ab[0] * bp[0] + ab[1] * bp[1] + ab[2] * bp[2], d4 = ac[0] * bp[0] + ac[1] * bp[1] + ac[2] * bp[2];
|
|
105
|
+
if (d3 >= 0 && d4 <= d3)
|
|
106
|
+
return { point: copy(b), barycentric: [0, 1, 0] };
|
|
107
|
+
const vc = d1 * d4 - d3 * d2;
|
|
108
|
+
if (vc <= 0 && d1 >= 0 && d3 <= 0) {
|
|
109
|
+
const t = d1 / (d1 - d3);
|
|
110
|
+
return { point: [a[0] + ab[0] * t, a[1] + ab[1] * t, a[2] + ab[2] * t], barycentric: [1 - t, t, 0] };
|
|
111
|
+
}
|
|
112
|
+
const cp = [point[0] - c[0], point[1] - c[1], point[2] - c[2]], d5 = ab[0] * cp[0] + ab[1] * cp[1] + ab[2] * cp[2], d6 = ac[0] * cp[0] + ac[1] * cp[1] + ac[2] * cp[2];
|
|
113
|
+
if (d6 >= 0 && d5 <= d6)
|
|
114
|
+
return { point: copy(c), barycentric: [0, 0, 1] };
|
|
115
|
+
const vb = d5 * d2 - d1 * d6;
|
|
116
|
+
if (vb <= 0 && d2 >= 0 && d6 <= 0) {
|
|
117
|
+
const t = d2 / (d2 - d6);
|
|
118
|
+
return { point: [a[0] + ac[0] * t, a[1] + ac[1] * t, a[2] + ac[2] * t], barycentric: [1 - t, 0, t] };
|
|
119
|
+
}
|
|
120
|
+
const va = d3 * d6 - d5 * d4;
|
|
121
|
+
if (va <= 0 && (d4 - d3) >= 0 && (d5 - d6) >= 0) {
|
|
122
|
+
const edge = [c[0] - b[0], c[1] - b[1], c[2] - b[2]], t = (d4 - d3) / ((d4 - d3) + (d5 - d6));
|
|
123
|
+
return { point: [b[0] + edge[0] * t, b[1] + edge[1] * t, b[2] + edge[2] * t], barycentric: [0, 1 - t, t] };
|
|
124
|
+
}
|
|
125
|
+
const denominator = 1 / (va + vb + vc), v = vb * denominator, w = vc * denominator;
|
|
126
|
+
return { point: [a[0] + ab[0] * v + ac[0] * w, a[1] + ab[1] * v + ac[1] * w, a[2] + ab[2] * v + ac[2] * w], barycentric: [1 - v - w, v, w] };
|
|
127
|
+
}
|
|
99
128
|
export function createCloth(input) {
|
|
100
129
|
if (!input || typeof input !== "object" || !Array.isArray(input.points) || input.points.length < 3 || input.points.length > 4096)
|
|
101
130
|
fail("CLOTH_INPUT", "points must contain 3..4096 particles");
|
|
@@ -110,14 +139,14 @@ export function createCloth(input) {
|
|
|
110
139
|
fail("CLOTH_INPUT", "damping must be in [0,100]");
|
|
111
140
|
if (input.bending !== undefined && typeof input.bending !== "boolean")
|
|
112
141
|
fail("CLOTH_INPUT", "bending must be boolean");
|
|
113
|
-
const bending = input.bending ?? true, selfCollisions = input.selfCollisions ?? true, gravity = readVec(input.gravity ?? [0, -9.81, 0], "gravity");
|
|
114
|
-
if (typeof selfCollisions !== "boolean")
|
|
115
|
-
fail("CLOTH_INPUT", "
|
|
142
|
+
const bending = input.bending ?? true, selfCollisions = input.selfCollisions ?? true, surfaceCollisions = input.surfaceCollisions ?? true, gravity = readVec(input.gravity ?? [0, -9.81, 0], "gravity");
|
|
143
|
+
if (typeof selfCollisions !== "boolean" || typeof surfaceCollisions !== "boolean")
|
|
144
|
+
fail("CLOTH_INPUT", "collision options must be boolean");
|
|
116
145
|
const { triangles, constraints } = topology(positions, input.triangles, bending);
|
|
117
146
|
const anchors = readAnchors(input.anchors ?? [], positions.length);
|
|
118
147
|
for (const [index, point] of anchors)
|
|
119
148
|
positions[index] = copy(point);
|
|
120
|
-
return new ClothSolver({ mass, radius, fixedDt, substeps, iterations, damping, gravity, bending, selfCollisions, triangles, constraints }, positions, anchors);
|
|
149
|
+
return new ClothSolver({ mass, radius, fixedDt, substeps, iterations, damping, gravity, bending, selfCollisions, surfaceCollisions, triangles, constraints }, positions, anchors);
|
|
121
150
|
}
|
|
122
151
|
export class ClothSolver {
|
|
123
152
|
config;
|
|
@@ -130,6 +159,7 @@ export class ClothSolver {
|
|
|
130
159
|
colliders = [];
|
|
131
160
|
contacts = [];
|
|
132
161
|
selfContacts = [];
|
|
162
|
+
surfaceContacts = [];
|
|
133
163
|
adjacent;
|
|
134
164
|
constructor(config, positions, anchors) {
|
|
135
165
|
this.config = config;
|
|
@@ -176,7 +206,7 @@ export class ClothSolver {
|
|
|
176
206
|
this.alive();
|
|
177
207
|
if (!Number.isSafeInteger(steps) || steps < 1 || steps > 1000 || this.tickValue + steps > Number.MAX_SAFE_INTEGER)
|
|
178
208
|
fail("CLOTH_INPUT", "steps must be in [1,1000] without tick overflow");
|
|
179
|
-
const before = { positions: this.positions, velocities: this.velocities, forces: this.forces, contacts: this.contacts, selfContacts: this.selfContacts, tick: this.tickValue };
|
|
209
|
+
const before = { positions: this.positions, velocities: this.velocities, forces: this.forces, contacts: this.contacts, selfContacts: this.selfContacts, surfaceContacts: this.surfaceContacts, tick: this.tickValue };
|
|
180
210
|
try {
|
|
181
211
|
for (let i = 0; i < steps; i++)
|
|
182
212
|
this.stepOne();
|
|
@@ -187,6 +217,7 @@ export class ClothSolver {
|
|
|
187
217
|
this.forces = before.forces;
|
|
188
218
|
this.contacts = before.contacts;
|
|
189
219
|
this.selfContacts = before.selfContacts;
|
|
220
|
+
this.surfaceContacts = before.surfaceContacts;
|
|
190
221
|
this.tickValue = before.tick;
|
|
191
222
|
throw error;
|
|
192
223
|
}
|
|
@@ -195,7 +226,7 @@ export class ClothSolver {
|
|
|
195
226
|
stepOne() {
|
|
196
227
|
const { fixedDt, substeps, iterations, mass, gravity, damping, constraints, radius } = this.config;
|
|
197
228
|
const h = fixedDt / substeps, decay = Math.exp(-damping * h);
|
|
198
|
-
const positions = this.positions.map(copy), velocities = this.velocities.map(copy), contacts = new Map(), selfContacts = new Map();
|
|
229
|
+
const positions = this.positions.map(copy), velocities = this.velocities.map(copy), contacts = new Map(), selfContacts = new Map(), surfaceContacts = new Map();
|
|
199
230
|
for (let sub = 0; sub < substeps; sub++) {
|
|
200
231
|
const previous = positions.map(copy);
|
|
201
232
|
for (let i = 0; i < positions.length; i++) {
|
|
@@ -256,6 +287,29 @@ export class ClothSolver {
|
|
|
256
287
|
contacts.set(key, { tick: this.tickValue + 1, particle: i, colliderId: collider.id, penetration: depth });
|
|
257
288
|
}
|
|
258
289
|
}
|
|
290
|
+
if (this.config.surfaceCollisions)
|
|
291
|
+
for (let triangleIndex = 0; triangleIndex < this.config.triangles.length; triangleIndex++) {
|
|
292
|
+
const triangle = this.config.triangles[triangleIndex], a = positions[triangle[0]], b = positions[triangle[1]], c = positions[triangle[2]];
|
|
293
|
+
for (const collider of this.colliders) {
|
|
294
|
+
if (collider.kind !== "sphere")
|
|
295
|
+
continue;
|
|
296
|
+
const closest = closestTriangle(collider.center, a, b, c), delta = [closest.point[0] - collider.center[0], closest.point[1] - collider.center[1], closest.point[2] - collider.center[2]], length = Math.hypot(...delta), depth = collider.radius + radius - length;
|
|
297
|
+
if (depth <= 0)
|
|
298
|
+
continue;
|
|
299
|
+
const normal = length < 1e-12 ? [0, 1, 0] : [delta[0] / length, delta[1] / length, delta[2] / length], weights = [this.anchors.has(triangle[0]) ? 0 : closest.barycentric[0], this.anchors.has(triangle[1]) ? 0 : closest.barycentric[1], this.anchors.has(triangle[2]) ? 0 : closest.barycentric[2]], denominator = weights.reduce((sum, value) => sum + value * value, 0);
|
|
300
|
+
if (denominator === 0)
|
|
301
|
+
continue;
|
|
302
|
+
const correction = depth / denominator;
|
|
303
|
+
for (let vertex = 0; vertex < 3; vertex++)
|
|
304
|
+
for (let axis = 0; axis < 3; axis++)
|
|
305
|
+
positions[triangle[vertex]][axis] += normal[axis] * correction * weights[vertex];
|
|
306
|
+
const key = `${triangleIndex}:${collider.id}`, old = surfaceContacts.get(key);
|
|
307
|
+
if (old)
|
|
308
|
+
old.penetration = Math.max(old.penetration, depth);
|
|
309
|
+
else
|
|
310
|
+
surfaceContacts.set(key, { tick: this.tickValue + 1, triangle: triangleIndex, colliderId: collider.id, penetration: depth, barycentric: closest.barycentric });
|
|
311
|
+
}
|
|
312
|
+
}
|
|
259
313
|
if (this.config.selfCollisions)
|
|
260
314
|
for (let i = 0; i < positions.length; i++)
|
|
261
315
|
for (let j = i + 1; j < positions.length; j++) {
|
|
@@ -294,18 +348,19 @@ export class ClothSolver {
|
|
|
294
348
|
this.forces = positions.map(() => [0, 0, 0]);
|
|
295
349
|
this.contacts = [...contacts.values()];
|
|
296
350
|
this.selfContacts = [...selfContacts.values()];
|
|
351
|
+
this.surfaceContacts = [...surfaceContacts.values()];
|
|
297
352
|
this.tickValue++;
|
|
298
353
|
}
|
|
299
354
|
inspect() {
|
|
300
355
|
if (this.disposed)
|
|
301
|
-
return { tick: this.tickValue, simulatedSeconds: this.tickValue * this.config.fixedDt, disposed: true, selfCollisions: this.config.selfCollisions, positions: [], velocities: [], triangles: [], anchors: [], contacts: [], selfContacts: [], maxDistanceError: 0, resources: { particles: 0, triangles: 0, constraints: 0, anchors: 0, colliders: 0 } };
|
|
356
|
+
return { tick: this.tickValue, simulatedSeconds: this.tickValue * this.config.fixedDt, disposed: true, selfCollisions: this.config.selfCollisions, surfaceCollisions: this.config.surfaceCollisions, positions: [], velocities: [], triangles: [], anchors: [], contacts: [], selfContacts: [], surfaceContacts: [], maxDistanceError: 0, resources: { particles: 0, triangles: 0, constraints: 0, anchors: 0, colliders: 0 } };
|
|
302
357
|
let maxDistanceError = 0;
|
|
303
358
|
for (const [a, b, rest] of this.config.constraints)
|
|
304
359
|
maxDistanceError = Math.max(maxDistanceError, Math.abs(distance(this.positions[a], this.positions[b]) - rest));
|
|
305
|
-
return { tick: this.tickValue, simulatedSeconds: this.tickValue * this.config.fixedDt, disposed: false, selfCollisions: this.config.selfCollisions,
|
|
360
|
+
return { tick: this.tickValue, simulatedSeconds: this.tickValue * this.config.fixedDt, disposed: false, selfCollisions: this.config.selfCollisions, surfaceCollisions: this.config.surfaceCollisions,
|
|
306
361
|
positions: this.positions.map(copy), velocities: this.velocities.map(copy), triangles: this.config.triangles.map(tuple),
|
|
307
362
|
anchors: [...this.anchors].map(([index, position]) => ({ index, position: copy(position) })).sort((a, b) => a.index - b.index),
|
|
308
|
-
contacts: this.contacts.map(contact => ({ ...contact })), selfContacts: this.selfContacts.map(contact => ({ ...contact })), maxDistanceError,
|
|
363
|
+
contacts: this.contacts.map(contact => ({ ...contact })), selfContacts: this.selfContacts.map(contact => ({ ...contact })), surfaceContacts: this.surfaceContacts.map(contact => ({ ...contact })), maxDistanceError,
|
|
309
364
|
resources: { particles: this.positions.length, triangles: this.config.triangles.length, constraints: this.config.constraints.length, anchors: this.anchors.size, colliders: this.colliders.length } };
|
|
310
365
|
}
|
|
311
366
|
snapshot() {
|
|
@@ -313,7 +368,7 @@ export class ClothSolver {
|
|
|
313
368
|
const state = this.inspect();
|
|
314
369
|
return { format: "pelican-cloth-cpu", version: 1, tick: this.tickValue,
|
|
315
370
|
config: { ...this.config, gravity: copy(this.config.gravity), triangles: this.config.triangles.map(tuple), constraints: this.config.constraints.map(tuple) },
|
|
316
|
-
positions: state.positions, velocities: state.velocities, forces: this.forces.map(copy), anchors: state.anchors, colliders: readColliders(this.colliders), contacts: state.contacts, selfContacts: state.selfContacts };
|
|
371
|
+
positions: state.positions, velocities: state.velocities, forces: this.forces.map(copy), anchors: state.anchors, colliders: readColliders(this.colliders), contacts: state.contacts, selfContacts: state.selfContacts, surfaceContacts: state.surfaceContacts };
|
|
317
372
|
}
|
|
318
373
|
restore(input) {
|
|
319
374
|
this.alive();
|
|
@@ -321,7 +376,7 @@ export class ClothSolver {
|
|
|
321
376
|
fail("CLOTH_SNAPSHOT", "unsupported snapshot format or tick");
|
|
322
377
|
const config = input.config, expected = this.config;
|
|
323
378
|
if (!config || config.mass !== expected.mass || config.radius !== expected.radius || config.fixedDt !== expected.fixedDt || config.substeps !== expected.substeps ||
|
|
324
|
-
config.iterations !== expected.iterations || config.damping !== expected.damping || config.bending !== expected.bending || (config.selfCollisions ?? false) !== expected.selfCollisions ||
|
|
379
|
+
config.iterations !== expected.iterations || config.damping !== expected.damping || config.bending !== expected.bending || (config.selfCollisions ?? false) !== expected.selfCollisions || (config.surfaceCollisions ?? false) !== expected.surfaceCollisions ||
|
|
325
380
|
!vec(config.gravity) || config.gravity.some((value, i) => value !== expected.gravity[i]) ||
|
|
326
381
|
!Array.isArray(config.triangles) || JSON.stringify(config.triangles) !== JSON.stringify(expected.triangles) ||
|
|
327
382
|
!Array.isArray(config.constraints) || JSON.stringify(config.constraints) !== JSON.stringify(expected.constraints))
|
|
@@ -357,6 +412,20 @@ export class ClothSolver {
|
|
|
357
412
|
fail("CLOTH_SNAPSHOT", "duplicate self contact");
|
|
358
413
|
selfKeys.add(key);
|
|
359
414
|
}
|
|
415
|
+
const surfaceContacts = input.surfaceContacts ?? [];
|
|
416
|
+
const surfaceKeys = new Set();
|
|
417
|
+
if (!Array.isArray(surfaceContacts) || surfaceContacts.length > this.config.triangles.length * colliders.length || (!this.config.surfaceCollisions && surfaceContacts.length > 0) ||
|
|
418
|
+
surfaceContacts.some(contact => !contact || contact.tick !== input.tick || !Number.isSafeInteger(contact.triangle) || contact.triangle < 0 || contact.triangle >= this.config.triangles.length ||
|
|
419
|
+
!colliders.some(collider => collider.id === contact.colliderId && collider.kind === "sphere") || typeof contact.penetration !== "number" || !Number.isFinite(contact.penetration) || contact.penetration <= 0 ||
|
|
420
|
+
!Array.isArray(contact.barycentric) || contact.barycentric.length !== 3 || !contact.barycentric.every(value => typeof value === "number" && Number.isFinite(value) && value >= 0 && value <= 1) ||
|
|
421
|
+
Math.abs(contact.barycentric[0] + contact.barycentric[1] + contact.barycentric[2] - 1) > 1e-5))
|
|
422
|
+
fail("CLOTH_SNAPSHOT", "invalid surface contact state");
|
|
423
|
+
for (const contact of surfaceContacts) {
|
|
424
|
+
const key = `${contact.triangle}:${contact.colliderId}`;
|
|
425
|
+
if (surfaceKeys.has(key))
|
|
426
|
+
fail("CLOTH_SNAPSHOT", "duplicate surface contact");
|
|
427
|
+
surfaceKeys.add(key);
|
|
428
|
+
}
|
|
360
429
|
this.positions = input.positions.map(copy);
|
|
361
430
|
this.velocities = input.velocities.map(copy);
|
|
362
431
|
this.forces = input.forces.map(copy);
|
|
@@ -364,8 +433,9 @@ export class ClothSolver {
|
|
|
364
433
|
this.colliders = colliders;
|
|
365
434
|
this.contacts = input.contacts.map(contact => ({ ...contact }));
|
|
366
435
|
this.selfContacts = selfContacts.map(contact => ({ ...contact }));
|
|
436
|
+
this.surfaceContacts = surfaceContacts.map(contact => ({ ...contact, barycentric: [...contact.barycentric] }));
|
|
367
437
|
this.tickValue = input.tick;
|
|
368
438
|
}
|
|
369
439
|
dispose() { if (this.disposed)
|
|
370
|
-
return; this.disposed = true; this.positions = []; this.velocities = []; this.forces = []; this.colliders = []; this.contacts = []; this.selfContacts = []; this.anchors.clear(); }
|
|
440
|
+
return; this.disposed = true; this.positions = []; this.velocities = []; this.forces = []; this.colliders = []; this.contacts = []; this.selfContacts = []; this.surfaceContacts = []; this.anchors.clear(); }
|
|
371
441
|
}
|