pelican-engine 0.3.25 → 0.3.26
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 +6 -1
- package/distribution.json +12 -12
- package/docs/STATUS.md +2 -0
- package/docs/api/REFERENCE.md +99 -97
- package/docs/api/index.json +105 -100
- package/docs/npm/GAME-AUTHOR.md +6 -1
- package/engine.manifest.json +1 -1
- package/lib/sdk/index.d.ts +8 -2
- package/lib/sdk/index.js +1 -1
- package/lib/simulation/cloth-plugin.js +25 -1
- package/lib/simulation/cloth.js +21 -7
- package/package.json +1 -1
package/docs/npm/GAME-AUTHOR.md
CHANGED
|
@@ -233,7 +233,12 @@ 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
|
-
An automatic cloth may bind a live dynamic sphere with `bodyColliders: [{ id: "sphere-id", entity: "body-id" }]`. The plugin samples the real sphere after physics and uses it as a one-way moving obstacle.
|
|
236
|
+
An automatic cloth may bind a live dynamic sphere with `bodyColliders: [{ id: "sphere-id", entity: "body-id" }]`. The plugin samples the real sphere after physics and uses it as a one-way moving obstacle. Set `bodyReactions: true` to submit approximate normal reaction impulses from
|
|
237
|
+
particle and triangle-surface contacts through the physics bridge. This changes
|
|
238
|
+
body velocity on the next physics step; it is delayed, projection-derived and
|
|
239
|
+
not mass-coupled with cloth velocity. The body receives no reaction when the
|
|
240
|
+
option is omitted. Destroyed bodies are removed on the next automatic step.
|
|
241
|
+
This is not complete cloth-rigid-body coupling.
|
|
237
242
|
|
|
238
243
|
New cloth also resolves detected sphere overlaps against triangle surface interiors
|
|
239
244
|
using closest-point barycentric projection. `inspect(id).surfaceContacts` reports
|
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.26";
|
|
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];
|
|
@@ -277,14 +277,18 @@ export interface ClothDefinition {
|
|
|
277
277
|
selfCollisions?: boolean;
|
|
278
278
|
/** Sphere triangle-surface contacts; defaults to true for new cloth. */
|
|
279
279
|
surfaceCollisions?: boolean;
|
|
280
|
-
/** Optional live dynamic sphere bindings; sampled after the physics phase
|
|
280
|
+
/** Optional live dynamic sphere bindings; sampled after the physics phase. */
|
|
281
281
|
bodyColliders?: readonly ClothBodyCollider[];
|
|
282
|
+
/** When true, detected contacts submit approximate reaction impulses to bound bodies. */
|
|
283
|
+
bodyReactions?: boolean;
|
|
282
284
|
}
|
|
283
285
|
export interface ClothContact {
|
|
284
286
|
tick: number;
|
|
285
287
|
particle: number;
|
|
286
288
|
colliderId: string;
|
|
287
289
|
penetration: number;
|
|
290
|
+
reactionImpulse?: Vec3;
|
|
291
|
+
reactionAngularImpulse?: Vec3;
|
|
288
292
|
}
|
|
289
293
|
export interface ClothSelfContact {
|
|
290
294
|
tick: number;
|
|
@@ -298,6 +302,8 @@ export interface ClothSurfaceContact {
|
|
|
298
302
|
colliderId: string;
|
|
299
303
|
penetration: number;
|
|
300
304
|
barycentric: [number, number, number];
|
|
305
|
+
reactionImpulse?: Vec3;
|
|
306
|
+
reactionAngularImpulse?: Vec3;
|
|
301
307
|
}
|
|
302
308
|
export interface ClothState {
|
|
303
309
|
tick: number;
|
package/lib/sdk/index.js
CHANGED
|
@@ -22,6 +22,10 @@ function createEntry(input, fixedDt, materials, restoredSelfMode, restoredSurfac
|
|
|
22
22
|
return fail("ID", "Expected a nonempty id of at most 128 characters.");
|
|
23
23
|
if (definition.autoStep !== undefined && typeof definition.autoStep !== "boolean")
|
|
24
24
|
return fail("AUTO_STEP", "autoStep must be boolean.");
|
|
25
|
+
if (definition.bodyReactions !== undefined && typeof definition.bodyReactions !== "boolean")
|
|
26
|
+
return fail("BODY_REACTION", "bodyReactions must be boolean.");
|
|
27
|
+
if (definition.bodyReactions && !definition.bodyColliders?.length)
|
|
28
|
+
return fail("BODY_REACTION", "bodyReactions require bodyColliders.");
|
|
25
29
|
if (definition.bodyColliders !== undefined) {
|
|
26
30
|
if (!Array.isArray(definition.bodyColliders) || definition.bodyColliders.length > 64)
|
|
27
31
|
return fail("BODY_COLLIDER", "bodyColliders must contain at most 64 bindings.");
|
|
@@ -37,6 +41,8 @@ function createEntry(input, fixedDt, materials, restoredSelfMode, restoredSurfac
|
|
|
37
41
|
}
|
|
38
42
|
if (definition.selfCollisions !== undefined && typeof definition.selfCollisions !== "boolean")
|
|
39
43
|
return fail("SELF_COLLISION", "selfCollisions must be boolean.");
|
|
44
|
+
if (definition.bodyReactions !== undefined && typeof definition.bodyReactions !== "boolean")
|
|
45
|
+
return fail("BODY_REACTION", "bodyReactions must be boolean.");
|
|
40
46
|
if (definition.surfaceCollisions !== undefined && typeof definition.surfaceCollisions !== "boolean")
|
|
41
47
|
return fail("SURFACE_COLLISION", "surfaceCollisions must be boolean.");
|
|
42
48
|
if (restoredSurfaceMode !== undefined && definition.surfaceCollisions !== undefined && definition.surfaceCollisions !== restoredSurfaceMode)
|
|
@@ -191,7 +197,25 @@ export const createClothPlugin = context => {
|
|
|
191
197
|
entry.definition = { ...entry.definition, bodyColliders: live };
|
|
192
198
|
entry.solver.setColliders([...manual, ...sampled]);
|
|
193
199
|
}
|
|
194
|
-
entry.solver.step();
|
|
200
|
+
const state = entry.solver.step();
|
|
201
|
+
if (entry.definition.bodyReactions && entry.definition.bodyColliders?.length) {
|
|
202
|
+
const bridge = context.getRopePhysicsBridge?.() ?? fail("BODY_REACTION", "bodyReactions require the physics plugin.");
|
|
203
|
+
const byCollider = new Map((entry.definition.bodyColliders ?? []).map(binding => [binding.id, binding.entity]));
|
|
204
|
+
const reactions = new Map();
|
|
205
|
+
for (const contact of [...state.contacts, ...state.surfaceContacts]) {
|
|
206
|
+
const entity = byCollider.get(contact.colliderId);
|
|
207
|
+
if (!entity || !contact.reactionImpulse || !contact.reactionAngularImpulse)
|
|
208
|
+
continue;
|
|
209
|
+
const current = reactions.get(entity) ?? { impulse: [0, 0, 0], moment: [0, 0, 0] };
|
|
210
|
+
for (let k = 0; k < 3; k++) {
|
|
211
|
+
current.impulse[k] += contact.reactionImpulse[k];
|
|
212
|
+
current.moment[k] += contact.reactionAngularImpulse[k];
|
|
213
|
+
}
|
|
214
|
+
reactions.set(entity, current);
|
|
215
|
+
}
|
|
216
|
+
if (reactions.size)
|
|
217
|
+
bridge.applySphereReactions([...reactions].sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0).map(([entity, value]) => ({ entity, ...value })));
|
|
218
|
+
}
|
|
195
219
|
}
|
|
196
220
|
}
|
|
197
221
|
catch (error) {
|
package/lib/simulation/cloth.js
CHANGED
|
@@ -14,6 +14,7 @@ const positive = (input, label) => typeof input === "number" && Number.isFinite(
|
|
|
14
14
|
const indexOf = (input, count) => Number.isSafeInteger(input) && input >= 0 && input < count ? input : fail("CLOTH_INDEX", "particle index is out of range");
|
|
15
15
|
const distance = (a, b) => Math.hypot(a[0] - b[0], a[1] - b[1], a[2] - b[2]);
|
|
16
16
|
const tuple = (a) => [a[0], a[1], a[2]];
|
|
17
|
+
const cross = (a, b) => [a[1] * b[2] - a[2] * b[1], a[2] * b[0] - a[0] * b[2], a[0] * b[1] - a[1] * b[0]];
|
|
17
18
|
function readAnchors(input, count) {
|
|
18
19
|
if (!Array.isArray(input) || input.length > count)
|
|
19
20
|
fail("CLOTH_INPUT", "anchors must be an array of unique particle indices");
|
|
@@ -280,11 +281,18 @@ export class ClothSolver {
|
|
|
280
281
|
continue;
|
|
281
282
|
for (let k = 0; k < 3; k++)
|
|
282
283
|
p[k] += normal[k] * depth;
|
|
283
|
-
const
|
|
284
|
-
|
|
284
|
+
const magnitude = -mass * depth / h, reactionImpulse = [normal[0] * magnitude, normal[1] * magnitude, normal[2] * magnitude];
|
|
285
|
+
const point = collider.kind === "sphere" ? [collider.center[0] + normal[0] * collider.radius, collider.center[1] + normal[1] * collider.radius, collider.center[2] + normal[2] * collider.radius] : [p[0] - normal[0] * radius, p[1] - normal[1] * radius, p[2] - normal[2] * radius];
|
|
286
|
+
const reactionAngularImpulse = cross(point, reactionImpulse), key = `${i}:${collider.id}`, old = contacts.get(key);
|
|
287
|
+
if (old) {
|
|
285
288
|
old.penetration = Math.max(old.penetration, depth);
|
|
289
|
+
for (let k = 0; k < 3; k++) {
|
|
290
|
+
old.reactionImpulse[k] += reactionImpulse[k];
|
|
291
|
+
old.reactionAngularImpulse[k] += reactionAngularImpulse[k];
|
|
292
|
+
}
|
|
293
|
+
}
|
|
286
294
|
else
|
|
287
|
-
contacts.set(key, { tick: this.tickValue + 1, particle: i, colliderId: collider.id, penetration: depth });
|
|
295
|
+
contacts.set(key, { tick: this.tickValue + 1, particle: i, colliderId: collider.id, penetration: depth, reactionImpulse, reactionAngularImpulse });
|
|
288
296
|
}
|
|
289
297
|
}
|
|
290
298
|
if (this.config.surfaceCollisions)
|
|
@@ -303,11 +311,17 @@ export class ClothSolver {
|
|
|
303
311
|
for (let vertex = 0; vertex < 3; vertex++)
|
|
304
312
|
for (let axis = 0; axis < 3; axis++)
|
|
305
313
|
positions[triangle[vertex]][axis] += normal[axis] * correction * weights[vertex];
|
|
314
|
+
const magnitude = -mass * depth / h, reactionImpulse = [normal[0] * magnitude, normal[1] * magnitude, normal[2] * magnitude], reactionAngularImpulse = cross(closest.point, reactionImpulse);
|
|
306
315
|
const key = `${triangleIndex}:${collider.id}`, old = surfaceContacts.get(key);
|
|
307
|
-
if (old)
|
|
316
|
+
if (old) {
|
|
308
317
|
old.penetration = Math.max(old.penetration, depth);
|
|
318
|
+
for (let k = 0; k < 3; k++) {
|
|
319
|
+
old.reactionImpulse[k] += reactionImpulse[k];
|
|
320
|
+
old.reactionAngularImpulse[k] += reactionAngularImpulse[k];
|
|
321
|
+
}
|
|
322
|
+
}
|
|
309
323
|
else
|
|
310
|
-
surfaceContacts.set(key, { tick: this.tickValue + 1, triangle: triangleIndex, colliderId: collider.id, penetration: depth, barycentric: closest.barycentric });
|
|
324
|
+
surfaceContacts.set(key, { tick: this.tickValue + 1, triangle: triangleIndex, colliderId: collider.id, penetration: depth, barycentric: closest.barycentric, reactionImpulse, reactionAngularImpulse });
|
|
311
325
|
}
|
|
312
326
|
}
|
|
313
327
|
if (this.config.selfCollisions)
|
|
@@ -391,7 +405,7 @@ export class ClothSolver {
|
|
|
391
405
|
const colliders = readColliders(input.colliders), keys = new Set();
|
|
392
406
|
if (!Array.isArray(input.contacts) || input.contacts.length > count * colliders.length || input.contacts.some(contact => !contact || contact.tick !== input.tick ||
|
|
393
407
|
!Number.isSafeInteger(contact.particle) || contact.particle < 0 || contact.particle >= count || !colliders.some(collider => collider.id === contact.colliderId) ||
|
|
394
|
-
typeof contact.penetration !== "number" || !Number.isFinite(contact.penetration) || contact.penetration <= 0))
|
|
408
|
+
typeof contact.penetration !== "number" || !Number.isFinite(contact.penetration) || contact.penetration <= 0 || (contact.reactionImpulse === undefined) !== (contact.reactionAngularImpulse === undefined) || (contact.reactionImpulse !== undefined && (!vec(contact.reactionImpulse) || !vec(contact.reactionAngularImpulse)))))
|
|
395
409
|
fail("CLOTH_SNAPSHOT", "invalid contact state");
|
|
396
410
|
for (const contact of input.contacts) {
|
|
397
411
|
const key = `${contact.particle}:${contact.colliderId}`;
|
|
@@ -418,7 +432,7 @@ export class ClothSolver {
|
|
|
418
432
|
surfaceContacts.some(contact => !contact || contact.tick !== input.tick || !Number.isSafeInteger(contact.triangle) || contact.triangle < 0 || contact.triangle >= this.config.triangles.length ||
|
|
419
433
|
!colliders.some(collider => collider.id === contact.colliderId && collider.kind === "sphere") || typeof contact.penetration !== "number" || !Number.isFinite(contact.penetration) || contact.penetration <= 0 ||
|
|
420
434
|
!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))
|
|
435
|
+
Math.abs(contact.barycentric[0] + contact.barycentric[1] + contact.barycentric[2] - 1) > 1e-5 || (contact.reactionImpulse === undefined) !== (contact.reactionAngularImpulse === undefined) || (contact.reactionImpulse !== undefined && (!vec(contact.reactionImpulse) || !vec(contact.reactionAngularImpulse)))))
|
|
422
436
|
fail("CLOTH_SNAPSHOT", "invalid surface contact state");
|
|
423
437
|
for (const contact of surfaceContacts) {
|
|
424
438
|
const key = `${contact.triangle}:${contact.colliderId}`;
|