pelican-engine 0.3.16 → 0.3.17
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 +15 -3
- package/distribution.json +13 -12
- package/docs/STATUS.md +2 -2
- package/docs/api/REFERENCE.md +112 -92
- package/docs/api/index.json +111 -94
- package/docs/npm/GAME-AUTHOR.md +15 -3
- package/engine.manifest.json +1 -1
- package/lib/sdk/index.d.ts +18 -2
- package/lib/sdk/index.js +1 -1
- package/lib/simulation/rope-plugin.js +12 -4
- package/lib/simulation/rope-self-collision.js +102 -0
- package/lib/simulation/rope.js +36 -5
- package/package.json +1 -1
package/docs/npm/GAME-AUTHOR.md
CHANGED
|
@@ -147,8 +147,20 @@ solver snapshots without this field retain particle-only behavior when migrated;
|
|
|
147
147
|
engine-version restrictions still apply. Plane contacts use particle endpoints.
|
|
148
148
|
The segment index names its first particle; `fraction` runs toward the next.
|
|
149
149
|
Work increases with segment, sphere, substep and iteration counts. This does not
|
|
150
|
-
provide continuous collision detection
|
|
151
|
-
|
|
150
|
+
provide continuous collision detection; the segment solver and snapshot migration
|
|
151
|
+
have not been runtime validated.
|
|
152
|
+
|
|
153
|
+
New ropes also resolve discrete self-contact between non-adjacent segments using
|
|
154
|
+
sweep-and-prune and endpoint projection. `inspect(id).selfContacts` reports
|
|
155
|
+
segment indices, closest-point fractions and maximum overlap; `selfCollisions`
|
|
156
|
+
reports the active mode. Set `selfCollisions: false` to disable it. Adjacent
|
|
157
|
+
segments and collisions between separate ropes are not handled. Immovable anchored
|
|
158
|
+
endpoints stay fixed, and contacts introduced by corrections can wait until the
|
|
159
|
+
next iteration. Dense overlap can approach quadratic work. Snapshots from before
|
|
160
|
+
this feature retain the disabled mode when migrated; Host engine-version rules
|
|
161
|
+
still apply. This is unverified discrete contact, not a guarantee against
|
|
162
|
+
penetration, continuous collision, friction or full rope physics. The source
|
|
163
|
+
checkout includes `examples/rope-self`.
|
|
152
164
|
|
|
153
165
|
For optional visual presentation, declare a material in `defineGame({ materials:
|
|
154
166
|
[...] })` and create the rope with `visual: { material: "your-material-id" }`.
|
|
@@ -206,7 +218,7 @@ checkpoint. It must not be represented as complete two-way rigid-body interactio
|
|
|
206
218
|
|
|
207
219
|
This is an unverified development-stage increment, not complete rope physics:
|
|
208
220
|
the optional segmented visual does not provide a continuous deformable rope mesh,
|
|
209
|
-
general rigid-body coupling,
|
|
221
|
+
general rigid-body coupling, continuous collision, cross-rope collision, or tearing. Plane normals must be unit length; `offset` is the signed
|
|
210
222
|
normal distance from the origin. Host intervals must be between 1/1000 and
|
|
211
223
|
1/30 seconds inclusive.
|
|
212
224
|
|
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.17";
|
|
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];
|
|
@@ -104,7 +104,7 @@ export interface SystemContext extends EngineFacade {
|
|
|
104
104
|
readonly ropes: RopeService;
|
|
105
105
|
}
|
|
106
106
|
/** CPU particles with sampled sphere/plane collisions. Explicit body bindings apply delayed
|
|
107
|
-
* contact impulses to dynamic spheres; there is no full rigid-body coupling
|
|
107
|
+
* contact impulses to dynamic spheres; there is no full rigid-body coupling. */
|
|
108
108
|
export type RopeCollider = {
|
|
109
109
|
kind: "sphere";
|
|
110
110
|
id: string;
|
|
@@ -153,6 +153,8 @@ export interface RopeDefinition {
|
|
|
153
153
|
bodyColliders?: readonly RopeBodyCollider[];
|
|
154
154
|
/** Resolve sphere contacts on segment interiors; defaults to true for new ropes. */
|
|
155
155
|
segmentCollisions?: boolean;
|
|
156
|
+
/** Resolve discrete non-adjacent segment self contacts; defaults to true for new ropes. */
|
|
157
|
+
selfCollisions?: boolean;
|
|
156
158
|
/** Optional segmented presentation using an already-declared material id. */
|
|
157
159
|
visual?: RopeVisualOptions;
|
|
158
160
|
substeps?: number;
|
|
@@ -187,11 +189,24 @@ export interface RopeSegmentContact {
|
|
|
187
189
|
reactionImpulse?: Vec3;
|
|
188
190
|
reactionAngularImpulse?: Vec3;
|
|
189
191
|
}
|
|
192
|
+
/** Discrete self-contact between two non-adjacent rope segments. */
|
|
193
|
+
export interface RopeSelfContact {
|
|
194
|
+
tick: number;
|
|
195
|
+
/** Zero-based first endpoint of each non-adjacent segment, ordered ascending. */
|
|
196
|
+
firstSegment: number;
|
|
197
|
+
secondSegment: number;
|
|
198
|
+
/** Last closest-point fractions along the respective segments. */
|
|
199
|
+
firstFraction: number;
|
|
200
|
+
secondFraction: number;
|
|
201
|
+
/** Maximum observed overlap in metres during this tick. */
|
|
202
|
+
penetration: number;
|
|
203
|
+
}
|
|
190
204
|
export interface RopeState {
|
|
191
205
|
tick: number;
|
|
192
206
|
simulatedSeconds: number;
|
|
193
207
|
disposed: boolean;
|
|
194
208
|
segmentCollisions: boolean;
|
|
209
|
+
selfCollisions: boolean;
|
|
195
210
|
positions: Vec3[];
|
|
196
211
|
velocities: Vec3[];
|
|
197
212
|
anchors: {
|
|
@@ -200,6 +215,7 @@ export interface RopeState {
|
|
|
200
215
|
}[];
|
|
201
216
|
contacts: RopeContact[];
|
|
202
217
|
segmentContacts: RopeSegmentContact[];
|
|
218
|
+
selfContacts: RopeSelfContact[];
|
|
203
219
|
maxDistanceError: number;
|
|
204
220
|
resources: {
|
|
205
221
|
particles: number;
|
package/lib/sdk/index.js
CHANGED
|
@@ -4,7 +4,7 @@ import { createRope } from "./rope.js";
|
|
|
4
4
|
import { createRopeBodyCoupling, VirtualRopeSphere } from "./rope-body-coupling.js";
|
|
5
5
|
const fail = (code, detail) => { throw new Error(`TC_ROPE_${code}: ${detail}`); };
|
|
6
6
|
const validId = (id) => typeof id === "string" && id.trim() === id && id.length > 0 && id.length <= 128;
|
|
7
|
-
function createEntry(input, fixedDt, materials, restoredSegmentMode) {
|
|
7
|
+
function createEntry(input, fixedDt, materials, restoredSegmentMode, restoredSelfMode) {
|
|
8
8
|
const definition = cloneSerializable(input);
|
|
9
9
|
if (!definition || !validId(definition.id))
|
|
10
10
|
return fail("ID", "Expected a nonempty id of at most 128 characters.");
|
|
@@ -12,6 +12,10 @@ function createEntry(input, fixedDt, materials, restoredSegmentMode) {
|
|
|
12
12
|
return fail("AUTO_STEP", "autoStep must be a boolean.");
|
|
13
13
|
if (definition.segmentCollisions !== undefined && typeof definition.segmentCollisions !== "boolean")
|
|
14
14
|
return fail("SEGMENT_COLLISION", "segmentCollisions must be a boolean.");
|
|
15
|
+
if (definition.selfCollisions !== undefined && typeof definition.selfCollisions !== "boolean")
|
|
16
|
+
return fail("SELF_COLLISION", "selfCollisions must be a boolean.");
|
|
17
|
+
if (restoredSelfMode !== undefined && definition.selfCollisions !== undefined && definition.selfCollisions !== restoredSelfMode)
|
|
18
|
+
return fail("SNAPSHOT", "Self collision configuration does not match the saved solver.");
|
|
15
19
|
if (restoredSegmentMode !== undefined && definition.segmentCollisions !== undefined && definition.segmentCollisions !== restoredSegmentMode)
|
|
16
20
|
return fail("SNAPSHOT", "Segment collision configuration does not match the saved solver.");
|
|
17
21
|
if (definition.fixedDt !== undefined && definition.fixedDt !== fixedDt)
|
|
@@ -44,7 +48,8 @@ function createEntry(input, fixedDt, materials, restoredSegmentMode) {
|
|
|
44
48
|
entities.add(binding.entity);
|
|
45
49
|
}
|
|
46
50
|
}
|
|
47
|
-
const solver = createRope({ ...definition, fixedDt, ...(restoredSegmentMode === undefined ? {} : { segmentCollisions: restoredSegmentMode })
|
|
51
|
+
const solver = createRope({ ...definition, fixedDt, ...(restoredSegmentMode === undefined ? {} : { segmentCollisions: restoredSegmentMode }),
|
|
52
|
+
...(restoredSelfMode === undefined ? {} : { selfCollisions: restoredSelfMode }) });
|
|
48
53
|
return { definition, solver, bodyRefs: new Map(), staleBodies: new Set() };
|
|
49
54
|
}
|
|
50
55
|
function disposeEntries(entries) {
|
|
@@ -63,8 +68,11 @@ function parseSnapshot(value, fixedDt, materials) {
|
|
|
63
68
|
const savedSegmentMode = item.state?.config?.segmentCollisions;
|
|
64
69
|
if (savedSegmentMode !== undefined && typeof savedSegmentMode !== "boolean")
|
|
65
70
|
fail("SNAPSHOT", "Invalid segment collision configuration.");
|
|
66
|
-
|
|
67
|
-
|
|
71
|
+
const savedSelfMode = item.state?.config?.selfCollisions;
|
|
72
|
+
if (savedSelfMode !== undefined && typeof savedSelfMode !== "boolean")
|
|
73
|
+
fail("SNAPSHOT", "Invalid self collision configuration.");
|
|
74
|
+
// Missing flags in historical snapshots preserve their original disabled modes.
|
|
75
|
+
const entry = createEntry(item.definition, fixedDt, materials, savedSegmentMode ?? false, savedSelfMode ?? false);
|
|
68
76
|
if (entries.has(entry.definition.id)) {
|
|
69
77
|
entry.solver.dispose();
|
|
70
78
|
fail("SNAPSHOT", "Duplicate rope id.");
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
const dot = (a, b) => a[0] * b[0] + a[1] * b[1] + a[2] * b[2];
|
|
2
|
+
const sub = (a, b) => [a[0] - b[0], a[1] - b[1], a[2] - b[2]];
|
|
3
|
+
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]];
|
|
4
|
+
const clamp = (value) => Math.max(0, Math.min(1, value));
|
|
5
|
+
const fail = (message) => { throw new Error(`ROPE_NUMERIC: ${message}`); };
|
|
6
|
+
/** Closest points of finite line segments; handles collapsed and near-parallel links. */
|
|
7
|
+
function closest(a, b, c, d) {
|
|
8
|
+
const u = sub(b, a), v = sub(d, c), w = sub(a, c);
|
|
9
|
+
const uu = dot(u, u), vv = dot(v, v), uv = dot(u, v), uw = dot(u, w), vw = dot(v, w);
|
|
10
|
+
let first = 0, second = 0;
|
|
11
|
+
if (uu <= 1e-24 && vv <= 1e-24) { /* both points */ }
|
|
12
|
+
else if (uu <= 1e-24)
|
|
13
|
+
second = clamp(vw / vv);
|
|
14
|
+
else if (vv <= 1e-24)
|
|
15
|
+
first = clamp(-uw / uu);
|
|
16
|
+
else {
|
|
17
|
+
const denominator = uu * vv - uv * uv;
|
|
18
|
+
if (denominator > 1e-12 * uu * vv)
|
|
19
|
+
first = clamp((uv * vw - uw * vv) / denominator);
|
|
20
|
+
second = (uv * first + vw) / vv;
|
|
21
|
+
if (second < 0) {
|
|
22
|
+
second = 0;
|
|
23
|
+
first = clamp(-uw / uu);
|
|
24
|
+
}
|
|
25
|
+
else if (second > 1) {
|
|
26
|
+
second = 1;
|
|
27
|
+
first = clamp((uv - uw) / uu);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
const firstPoint = [a[0] + u[0] * first, a[1] + u[1] * first, a[2] + u[2] * first];
|
|
31
|
+
const secondPoint = [c[0] + v[0] * second, c[1] + v[1] * second, c[2] + v[2] * second];
|
|
32
|
+
if (![first, second, ...firstPoint, ...secondPoint].every(Number.isFinite))
|
|
33
|
+
fail("self-contact closest point is not finite");
|
|
34
|
+
return { first, second, firstPoint, secondPoint };
|
|
35
|
+
}
|
|
36
|
+
function fallbackNormal(firstDirection, secondDirection) {
|
|
37
|
+
let normal = cross(firstDirection, secondDirection), magnitude = Math.hypot(...normal);
|
|
38
|
+
if (magnitude < 1e-12) {
|
|
39
|
+
const direction = dot(firstDirection, firstDirection) >= dot(secondDirection, secondDirection) ? firstDirection : secondDirection;
|
|
40
|
+
const [x, y, z] = direction.map(Math.abs);
|
|
41
|
+
const axis = x <= y && x <= z ? [1, 0, 0] : y <= z ? [0, 1, 0] : [0, 0, 1];
|
|
42
|
+
normal = cross(direction, axis);
|
|
43
|
+
magnitude = Math.hypot(...normal);
|
|
44
|
+
}
|
|
45
|
+
return magnitude < 1e-12 ? [1, 0, 0] : [normal[0] / magnitude, normal[1] / magnitude, normal[2] / magnitude];
|
|
46
|
+
}
|
|
47
|
+
/** Sweep-and-prune broadphase followed by discrete capsule-segment projection.
|
|
48
|
+
* Bounds are rebuilt each iteration; contacts created by a correction may wait for the next pass.
|
|
49
|
+
* Adjacent segments are excluded. The caller owns rollback for these temporary positions. */
|
|
50
|
+
export function resolveRopeSelfCollisions(positions, anchors, radius, tick, contacts) {
|
|
51
|
+
if (positions.some(point => point.some(value => !Number.isFinite(value) || Math.abs(value) > 1e6)))
|
|
52
|
+
fail("particle position exceeded finite bounds before self-contact");
|
|
53
|
+
const bounds = [];
|
|
54
|
+
for (let index = 0; index < positions.length - 1; index++) {
|
|
55
|
+
const a = positions[index], b = positions[index + 1];
|
|
56
|
+
bounds.push({ index, minX: Math.min(a[0], b[0]) - radius, maxX: Math.max(a[0], b[0]) + radius,
|
|
57
|
+
minY: Math.min(a[1], b[1]) - radius, maxY: Math.max(a[1], b[1]) + radius,
|
|
58
|
+
minZ: Math.min(a[2], b[2]) - radius, maxZ: Math.max(a[2], b[2]) + radius });
|
|
59
|
+
}
|
|
60
|
+
bounds.sort((a, b) => a.minX - b.minX || a.index - b.index);
|
|
61
|
+
for (let left = 0; left < bounds.length; left++) {
|
|
62
|
+
const x = bounds[left];
|
|
63
|
+
for (let right = left + 1; right < bounds.length && bounds[right].minX <= x.maxX; right++) {
|
|
64
|
+
const y = bounds[right];
|
|
65
|
+
if (Math.abs(x.index - y.index) <= 1 || x.minY > y.maxY || y.minY > x.maxY || x.minZ > y.maxZ || y.minZ > x.maxZ)
|
|
66
|
+
continue;
|
|
67
|
+
const firstSegment = Math.min(x.index, y.index), secondSegment = Math.max(x.index, y.index);
|
|
68
|
+
const a = positions[firstSegment], b = positions[firstSegment + 1], c = positions[secondSegment], d = positions[secondSegment + 1];
|
|
69
|
+
const contact = closest(a, b, c, d), delta = sub(contact.firstPoint, contact.secondPoint), distance = Math.hypot(...delta);
|
|
70
|
+
const penetration = 2 * radius - distance;
|
|
71
|
+
if (!Number.isFinite(penetration))
|
|
72
|
+
fail("self-contact penetration is not finite");
|
|
73
|
+
if (penetration <= 0)
|
|
74
|
+
continue;
|
|
75
|
+
const normal = distance < 1e-12 ? fallbackNormal(sub(b, a), sub(d, c)) : [delta[0] / distance, delta[1] / distance, delta[2] / distance];
|
|
76
|
+
const indices = [firstSegment, firstSegment + 1, secondSegment, secondSegment + 1];
|
|
77
|
+
const gradients = [1 - contact.first, contact.first, -(1 - contact.second), -contact.second]
|
|
78
|
+
.map((weight, index) => anchors.has(indices[index]) ? 0 : weight);
|
|
79
|
+
const weight = gradients.reduce((sum, value) => sum + value * value, 0);
|
|
80
|
+
if (weight === 0)
|
|
81
|
+
continue; // four immovable endpoints cannot be projected
|
|
82
|
+
const magnitude = penetration / weight;
|
|
83
|
+
if (!Number.isFinite(magnitude))
|
|
84
|
+
fail("self-contact projection exceeded finite bounds");
|
|
85
|
+
for (let i = 0; i < 4; i++)
|
|
86
|
+
for (let axis = 0; axis < 3; axis++) {
|
|
87
|
+
const value = positions[indices[i]][axis] + normal[axis] * magnitude * gradients[i];
|
|
88
|
+
if (!Number.isFinite(value) || Math.abs(value) > 1e6)
|
|
89
|
+
fail("self-contact correction exceeded finite bounds");
|
|
90
|
+
positions[indices[i]][axis] = value;
|
|
91
|
+
}
|
|
92
|
+
const key = `${firstSegment}:${secondSegment}`, existing = contacts.get(key);
|
|
93
|
+
if (existing) {
|
|
94
|
+
existing.penetration = Math.max(existing.penetration, penetration);
|
|
95
|
+
existing.firstFraction = contact.first;
|
|
96
|
+
existing.secondFraction = contact.second;
|
|
97
|
+
}
|
|
98
|
+
else
|
|
99
|
+
contacts.set(key, { tick, firstSegment, secondSegment, firstFraction: contact.first, secondFraction: contact.second, penetration });
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
package/lib/simulation/rope.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { resolveRopeSelfCollisions } from "./rope-self-collision.js";
|
|
1
2
|
export class RopeError extends Error {
|
|
2
3
|
code;
|
|
3
4
|
constructor(code, message) {
|
|
@@ -74,6 +75,9 @@ export function createRope(options) {
|
|
|
74
75
|
if (options.segmentCollisions !== undefined && typeof options.segmentCollisions !== "boolean")
|
|
75
76
|
fail("ROPE_INPUT", "segmentCollisions must be boolean");
|
|
76
77
|
const segmentCollisions = options.segmentCollisions ?? true;
|
|
78
|
+
if (options.selfCollisions !== undefined && typeof options.selfCollisions !== "boolean")
|
|
79
|
+
fail("ROPE_INPUT", "selfCollisions must be boolean");
|
|
80
|
+
const selfCollisions = options.selfCollisions ?? true;
|
|
77
81
|
const fixedDt = positive(options.fixedDt ?? 1 / 60, "fixedDt");
|
|
78
82
|
if (fixedDt < 1 / 1000 || fixedDt > 1 / 30)
|
|
79
83
|
fail("ROPE_INPUT", "fixedDt must be in [1/1000, 1/30]");
|
|
@@ -88,7 +92,7 @@ export function createRope(options) {
|
|
|
88
92
|
const anchors = readAnchors(options.anchors ?? [{ index: 0, position: positions[0] }], positions.length);
|
|
89
93
|
for (const [i, p] of anchors)
|
|
90
94
|
positions[i] = copy(p);
|
|
91
|
-
return new RopeSolver({ mass, radius, fixedDt, substeps, iterations, damping, gravity, restLengths, segmentCollisions }, positions, anchors);
|
|
95
|
+
return new RopeSolver({ mass, radius, fixedDt, substeps, iterations, damping, gravity, restLengths, segmentCollisions, selfCollisions }, positions, anchors);
|
|
92
96
|
}
|
|
93
97
|
export class RopeSolver {
|
|
94
98
|
config;
|
|
@@ -101,6 +105,7 @@ export class RopeSolver {
|
|
|
101
105
|
colliders = [];
|
|
102
106
|
contacts = [];
|
|
103
107
|
segmentContacts = [];
|
|
108
|
+
selfContacts = [];
|
|
104
109
|
constructor(config, positions, anchors) {
|
|
105
110
|
this.config = config;
|
|
106
111
|
this.positions = positions;
|
|
@@ -163,6 +168,7 @@ export class RopeSolver {
|
|
|
163
168
|
forces: this.forces,
|
|
164
169
|
contacts: this.contacts,
|
|
165
170
|
segmentContacts: this.segmentContacts,
|
|
171
|
+
selfContacts: this.selfContacts,
|
|
166
172
|
tick: this.tickValue,
|
|
167
173
|
};
|
|
168
174
|
try {
|
|
@@ -175,6 +181,7 @@ export class RopeSolver {
|
|
|
175
181
|
this.forces = before.forces;
|
|
176
182
|
this.contacts = before.contacts;
|
|
177
183
|
this.segmentContacts = before.segmentContacts;
|
|
184
|
+
this.selfContacts = before.selfContacts;
|
|
178
185
|
this.tickValue = before.tick;
|
|
179
186
|
throw error;
|
|
180
187
|
}
|
|
@@ -187,6 +194,7 @@ export class RopeSolver {
|
|
|
187
194
|
const positions = this.positions.map(copy), velocities = this.velocities.map(copy);
|
|
188
195
|
const contacts = new Map();
|
|
189
196
|
const segmentContacts = new Map();
|
|
197
|
+
const selfContacts = new Map();
|
|
190
198
|
const spheres = this.config.segmentCollisions ? this.colliders.filter((collider) => collider.kind === "sphere") : [];
|
|
191
199
|
for (let sub = 0; sub < substeps; sub++) {
|
|
192
200
|
const previous = positions.map(copy);
|
|
@@ -357,6 +365,8 @@ export class RopeSolver {
|
|
|
357
365
|
segmentContacts.set(key, { tick: this.tickValue + 1, segment: j, colliderId: collider.id, fraction, penetration: depth, reactionImpulse: reaction, reactionAngularImpulse: moment });
|
|
358
366
|
}
|
|
359
367
|
}
|
|
368
|
+
if (this.config.selfCollisions && positions.length >= 4)
|
|
369
|
+
resolveRopeSelfCollisions(positions, this.anchors, radius, this.tickValue + 1, selfContacts);
|
|
360
370
|
}
|
|
361
371
|
for (let i = 0; i < positions.length; i++) {
|
|
362
372
|
if (this.anchors.has(i))
|
|
@@ -372,22 +382,23 @@ export class RopeSolver {
|
|
|
372
382
|
this.forces = positions.map(() => [0, 0, 0]);
|
|
373
383
|
this.contacts = [...contacts.values()];
|
|
374
384
|
this.segmentContacts = [...segmentContacts.values()];
|
|
385
|
+
this.selfContacts = [...selfContacts.values()];
|
|
375
386
|
this.tickValue++;
|
|
376
387
|
}
|
|
377
388
|
/** Detached positions for renderer synchronization without copying contact state. */
|
|
378
389
|
particlePositions() { this.alive(); return this.positions.map(copy); }
|
|
379
390
|
inspect() {
|
|
380
391
|
if (this.disposedValue)
|
|
381
|
-
return { tick: this.tickValue, simulatedSeconds: this.tickValue * this.config.fixedDt, disposed: true, segmentCollisions: this.config.segmentCollisions, positions: [], velocities: [], anchors: [], contacts: [], segmentContacts: [], maxDistanceError: 0, resources: { particles: 0, constraints: 0, anchors: 0, colliders: 0 } };
|
|
392
|
+
return { tick: this.tickValue, simulatedSeconds: this.tickValue * this.config.fixedDt, disposed: true, segmentCollisions: this.config.segmentCollisions, selfCollisions: this.config.selfCollisions, positions: [], velocities: [], anchors: [], contacts: [], segmentContacts: [], selfContacts: [], maxDistanceError: 0, resources: { particles: 0, constraints: 0, anchors: 0, colliders: 0 } };
|
|
382
393
|
const maxDistanceError = Math.max(0, ...this.config.restLengths.map((d, i) => {
|
|
383
394
|
const a = this.positions[i], b = this.positions[i + 1];
|
|
384
395
|
return Math.abs(Math.hypot(a[0] - b[0], a[1] - b[1], a[2] - b[2]) - d);
|
|
385
396
|
}));
|
|
386
397
|
return {
|
|
387
|
-
tick: this.tickValue, simulatedSeconds: this.tickValue * this.config.fixedDt, disposed: false, segmentCollisions: this.config.segmentCollisions,
|
|
398
|
+
tick: this.tickValue, simulatedSeconds: this.tickValue * this.config.fixedDt, disposed: false, segmentCollisions: this.config.segmentCollisions, selfCollisions: this.config.selfCollisions,
|
|
388
399
|
positions: this.positions.map(copy), velocities: this.velocities.map(copy),
|
|
389
400
|
anchors: [...this.anchors].map(([index, position]) => ({ index, position: copy(position) })).sort((a, b) => a.index - b.index),
|
|
390
|
-
contacts: this.contacts.map(copyContact), segmentContacts: this.segmentContacts.map(copySegmentContact), maxDistanceError,
|
|
401
|
+
contacts: this.contacts.map(copyContact), segmentContacts: this.segmentContacts.map(copySegmentContact), selfContacts: this.selfContacts.map(contact => ({ ...contact })), maxDistanceError,
|
|
391
402
|
resources: { particles: this.positions.length, constraints: this.config.restLengths.length, anchors: this.anchors.size, colliders: this.colliders.length },
|
|
392
403
|
};
|
|
393
404
|
}
|
|
@@ -398,7 +409,7 @@ export class RopeSolver {
|
|
|
398
409
|
format: "pelican-rope-cpu", version: 1, tick: this.tickValue,
|
|
399
410
|
config: { ...this.config, gravity: copy(this.config.gravity), restLengths: [...this.config.restLengths] },
|
|
400
411
|
positions: state.positions, velocities: state.velocities, anchors: state.anchors,
|
|
401
|
-
forces: this.forces.map(copy), colliders: readColliders(this.colliders), contacts: state.contacts, segmentContacts: state.segmentContacts,
|
|
412
|
+
forces: this.forces.map(copy), colliders: readColliders(this.colliders), contacts: state.contacts, segmentContacts: state.segmentContacts, selfContacts: state.selfContacts,
|
|
402
413
|
};
|
|
403
414
|
}
|
|
404
415
|
/** Accepts a JSON-round-tripped snapshot only from the same rope configuration. Atomic on failure. */
|
|
@@ -411,6 +422,7 @@ export class RopeSolver {
|
|
|
411
422
|
if (!c || c.mass !== this.config.mass || c.radius !== this.config.radius || c.fixedDt !== this.config.fixedDt ||
|
|
412
423
|
c.substeps !== this.config.substeps || c.iterations !== this.config.iterations || c.damping !== this.config.damping ||
|
|
413
424
|
(c.segmentCollisions !== undefined && typeof c.segmentCollisions !== "boolean") || (c.segmentCollisions ?? false) !== this.config.segmentCollisions ||
|
|
425
|
+
(c.selfCollisions !== undefined && typeof c.selfCollisions !== "boolean") || (c.selfCollisions ?? false) !== this.config.selfCollisions ||
|
|
414
426
|
!vec(c.gravity) || c.gravity.some((v, i) => v !== this.config.gravity[i]) ||
|
|
415
427
|
!Array.isArray(c.restLengths) || c.restLengths.length !== this.config.restLengths.length ||
|
|
416
428
|
c.restLengths.some((v, i) => v !== this.config.restLengths[i]))
|
|
@@ -448,6 +460,23 @@ export class RopeSolver {
|
|
|
448
460
|
fail("ROPE_SNAPSHOT", "duplicate segment contact");
|
|
449
461
|
segmentKeys.add(key);
|
|
450
462
|
}
|
|
463
|
+
const self = input.selfContacts === undefined ? [] : input.selfContacts;
|
|
464
|
+
const maxSelfContacts = Math.max(0, (count - 2) * (count - 3) / 2);
|
|
465
|
+
if (!Array.isArray(self) || self.length > maxSelfContacts || (!this.config.selfCollisions && self.length > 0) ||
|
|
466
|
+
self.some(contact => !contact || contact.tick !== input.tick ||
|
|
467
|
+
!Number.isSafeInteger(contact.firstSegment) || !Number.isSafeInteger(contact.secondSegment) ||
|
|
468
|
+
contact.firstSegment < 0 || contact.secondSegment >= count - 1 || contact.secondSegment - contact.firstSegment <= 1 ||
|
|
469
|
+
typeof contact.firstFraction !== "number" || !Number.isFinite(contact.firstFraction) || contact.firstFraction < 0 || contact.firstFraction > 1 ||
|
|
470
|
+
typeof contact.secondFraction !== "number" || !Number.isFinite(contact.secondFraction) || contact.secondFraction < 0 || contact.secondFraction > 1 ||
|
|
471
|
+
typeof contact.penetration !== "number" || !Number.isFinite(contact.penetration) || contact.penetration <= 0))
|
|
472
|
+
fail("ROPE_SNAPSHOT", "invalid self contact state");
|
|
473
|
+
const selfKeys = new Set();
|
|
474
|
+
for (const contact of self) {
|
|
475
|
+
const key = `${contact.firstSegment}:${contact.secondSegment}`;
|
|
476
|
+
if (selfKeys.has(key))
|
|
477
|
+
fail("ROPE_SNAPSHOT", "duplicate self contact");
|
|
478
|
+
selfKeys.add(key);
|
|
479
|
+
}
|
|
451
480
|
this.positions = input.positions.map(copy);
|
|
452
481
|
this.velocities = input.velocities.map(copy);
|
|
453
482
|
this.forces = input.forces.map(copy);
|
|
@@ -455,6 +484,7 @@ export class RopeSolver {
|
|
|
455
484
|
this.colliders = colliders;
|
|
456
485
|
this.contacts = input.contacts.map(copyContact);
|
|
457
486
|
this.segmentContacts = segments.map(copySegmentContact);
|
|
487
|
+
this.selfContacts = self.map(contact => ({ ...contact }));
|
|
458
488
|
this.tickValue = input.tick;
|
|
459
489
|
}
|
|
460
490
|
dispose() {
|
|
@@ -467,6 +497,7 @@ export class RopeSolver {
|
|
|
467
497
|
this.colliders = [];
|
|
468
498
|
this.contacts = [];
|
|
469
499
|
this.segmentContacts = [];
|
|
500
|
+
this.selfContacts = [];
|
|
470
501
|
this.anchors.clear();
|
|
471
502
|
}
|
|
472
503
|
}
|