pelican-engine 0.3.18 → 0.3.19

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.
@@ -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 discrete self-contact between non-adjacent particles by
237
+ projecting movable particles apart; shared triangle edges are excluded.
238
+ `selfCollisions` defaults to true for new cloth and is reported by
239
+ `ctx.cloth.inspect(id)`. Set it to false for particle-only cloth. Self-contact
240
+ state is included in snapshots. This is particle self-contact, not triangle
241
+ interior/edge collision, continuous collision, friction or a guarantee against
242
+ penetration. It has not been runtime or stability validated.
243
+
236
244
  The CPU solver uses edge distance constraints and an optional distance-based
237
245
  bending approximation (`bending: true` by default). Sampled planes and spheres
238
246
  only move cloth particles; they do not receive reactions. Contacts do not cover
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pelican-engine",
3
- "version": "0.3.18",
3
+ "version": "0.3.19",
4
4
  "milestone": "M3",
5
5
  "stage": "advanced-rendering-development",
6
6
  "license": "MIT",
@@ -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.18";
2
+ export declare const SDK_VERSION = "0.3.19";
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];
@@ -269,6 +269,8 @@ export interface ClothDefinition {
269
269
  position: Vec3;
270
270
  }[];
271
271
  visual?: ClothVisualOptions;
272
+ /** Discrete non-adjacent particle self-contact; defaults to true for new cloth. */
273
+ selfCollisions?: boolean;
272
274
  }
273
275
  export interface ClothContact {
274
276
  tick: number;
@@ -276,10 +278,17 @@ export interface ClothContact {
276
278
  colliderId: string;
277
279
  penetration: number;
278
280
  }
281
+ export interface ClothSelfContact {
282
+ tick: number;
283
+ firstParticle: number;
284
+ secondParticle: number;
285
+ penetration: number;
286
+ }
279
287
  export interface ClothState {
280
288
  tick: number;
281
289
  simulatedSeconds: number;
282
290
  disposed: boolean;
291
+ selfCollisions: boolean;
283
292
  positions: Vec3[];
284
293
  velocities: Vec3[];
285
294
  triangles: ClothTriangle[];
@@ -288,6 +297,7 @@ export interface ClothState {
288
297
  position: Vec3;
289
298
  }[];
290
299
  contacts: ClothContact[];
300
+ selfContacts: ClothSelfContact[];
291
301
  maxDistanceError: number;
292
302
  resources: {
293
303
  particles: number;
package/lib/sdk/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  /** Pelican Engine public API. No renderer or GPU implementation types escape this module. */
2
- export const SDK_VERSION = "0.3.18";
2
+ export const SDK_VERSION = "0.3.19";
3
3
  export const SYSTEM_PHASES = [
4
4
  "input",
5
5
  "prePhysics",
@@ -110,12 +110,14 @@ export function createCloth(input) {
110
110
  fail("CLOTH_INPUT", "damping must be in [0,100]");
111
111
  if (input.bending !== undefined && typeof input.bending !== "boolean")
112
112
  fail("CLOTH_INPUT", "bending must be boolean");
113
- const bending = input.bending ?? true, gravity = readVec(input.gravity ?? [0, -9.81, 0], "gravity");
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", "selfCollisions must be boolean");
114
116
  const { triangles, constraints } = topology(positions, input.triangles, bending);
115
117
  const anchors = readAnchors(input.anchors ?? [], positions.length);
116
118
  for (const [index, point] of anchors)
117
119
  positions[index] = copy(point);
118
- return new ClothSolver({ mass, radius, fixedDt, substeps, iterations, damping, gravity, bending, triangles, constraints }, positions, anchors);
120
+ return new ClothSolver({ mass, radius, fixedDt, substeps, iterations, damping, gravity, bending, selfCollisions, triangles, constraints }, positions, anchors);
119
121
  }
120
122
  export class ClothSolver {
121
123
  config;
@@ -127,12 +129,15 @@ export class ClothSolver {
127
129
  forces;
128
130
  colliders = [];
129
131
  contacts = [];
132
+ selfContacts = [];
133
+ adjacent;
130
134
  constructor(config, positions, anchors) {
131
135
  this.config = config;
132
136
  this.positions = positions;
133
137
  this.anchors = anchors;
134
138
  this.velocities = positions.map(() => [0, 0, 0]);
135
139
  this.forces = positions.map(() => [0, 0, 0]);
140
+ this.adjacent = new Set(config.triangles.flatMap(([a, b, c]) => [`${Math.min(a, b)}:${Math.max(a, b)}`, `${Math.min(b, c)}:${Math.max(b, c)}`, `${Math.min(c, a)}:${Math.max(c, a)}`]));
136
141
  }
137
142
  alive() { if (this.disposed)
138
143
  fail("CLOTH_DISPOSED", "cloth has been disposed"); }
@@ -171,7 +176,7 @@ export class ClothSolver {
171
176
  this.alive();
172
177
  if (!Number.isSafeInteger(steps) || steps < 1 || steps > 1000 || this.tickValue + steps > Number.MAX_SAFE_INTEGER)
173
178
  fail("CLOTH_INPUT", "steps must be in [1,1000] without tick overflow");
174
- const before = { positions: this.positions, velocities: this.velocities, forces: this.forces, contacts: this.contacts, tick: this.tickValue };
179
+ const before = { positions: this.positions, velocities: this.velocities, forces: this.forces, contacts: this.contacts, selfContacts: this.selfContacts, tick: this.tickValue };
175
180
  try {
176
181
  for (let i = 0; i < steps; i++)
177
182
  this.stepOne();
@@ -181,6 +186,7 @@ export class ClothSolver {
181
186
  this.velocities = before.velocities;
182
187
  this.forces = before.forces;
183
188
  this.contacts = before.contacts;
189
+ this.selfContacts = before.selfContacts;
184
190
  this.tickValue = before.tick;
185
191
  throw error;
186
192
  }
@@ -189,7 +195,7 @@ export class ClothSolver {
189
195
  stepOne() {
190
196
  const { fixedDt, substeps, iterations, mass, gravity, damping, constraints, radius } = this.config;
191
197
  const h = fixedDt / substeps, decay = Math.exp(-damping * h);
192
- const positions = this.positions.map(copy), velocities = this.velocities.map(copy), contacts = new Map();
198
+ const positions = this.positions.map(copy), velocities = this.velocities.map(copy), contacts = new Map(), selfContacts = new Map();
193
199
  for (let sub = 0; sub < substeps; sub++) {
194
200
  const previous = positions.map(copy);
195
201
  for (let i = 0; i < positions.length; i++) {
@@ -250,6 +256,29 @@ export class ClothSolver {
250
256
  contacts.set(key, { tick: this.tickValue + 1, particle: i, colliderId: collider.id, penetration: depth });
251
257
  }
252
258
  }
259
+ if (this.config.selfCollisions)
260
+ for (let i = 0; i < positions.length; i++)
261
+ for (let j = i + 1; j < positions.length; j++) {
262
+ if (this.adjacent.has(`${i}:${j}`))
263
+ continue;
264
+ const a = positions[i], b = positions[j], dx = a[0] - b[0], dy = a[1] - b[1], dz = a[2] - b[2], length = Math.hypot(dx, dy, dz), depth = 2 * radius - length;
265
+ if (depth <= 0)
266
+ continue;
267
+ const normal = length < 1e-12 ? [1, 0, 0] : [dx / length, dy / length, dz / length];
268
+ const wa = this.anchors.has(i) ? 0 : 1, wb = this.anchors.has(j) ? 0 : 1;
269
+ if (wa + wb === 0)
270
+ continue;
271
+ const correction = depth / (wa + wb);
272
+ for (let axis = 0; axis < 3; axis++) {
273
+ a[axis] += normal[axis] * correction * wa;
274
+ b[axis] -= normal[axis] * correction * wb;
275
+ }
276
+ const key = `${i}:${j}`, old = selfContacts.get(key);
277
+ if (old)
278
+ old.penetration = Math.max(old.penetration, depth);
279
+ else
280
+ selfContacts.set(key, { tick: this.tickValue + 1, firstParticle: i, secondParticle: j, penetration: depth });
281
+ }
253
282
  }
254
283
  for (let i = 0; i < positions.length; i++) {
255
284
  if (this.anchors.has(i))
@@ -264,18 +293,19 @@ export class ClothSolver {
264
293
  this.velocities = velocities;
265
294
  this.forces = positions.map(() => [0, 0, 0]);
266
295
  this.contacts = [...contacts.values()];
296
+ this.selfContacts = [...selfContacts.values()];
267
297
  this.tickValue++;
268
298
  }
269
299
  inspect() {
270
300
  if (this.disposed)
271
- return { tick: this.tickValue, simulatedSeconds: this.tickValue * this.config.fixedDt, disposed: true, positions: [], velocities: [], triangles: [], anchors: [], contacts: [], maxDistanceError: 0, resources: { particles: 0, triangles: 0, constraints: 0, anchors: 0, colliders: 0 } };
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 } };
272
302
  let maxDistanceError = 0;
273
303
  for (const [a, b, rest] of this.config.constraints)
274
304
  maxDistanceError = Math.max(maxDistanceError, Math.abs(distance(this.positions[a], this.positions[b]) - rest));
275
- return { tick: this.tickValue, simulatedSeconds: this.tickValue * this.config.fixedDt, disposed: false,
305
+ return { tick: this.tickValue, simulatedSeconds: this.tickValue * this.config.fixedDt, disposed: false, selfCollisions: this.config.selfCollisions,
276
306
  positions: this.positions.map(copy), velocities: this.velocities.map(copy), triangles: this.config.triangles.map(tuple),
277
307
  anchors: [...this.anchors].map(([index, position]) => ({ index, position: copy(position) })).sort((a, b) => a.index - b.index),
278
- contacts: this.contacts.map(contact => ({ ...contact })), maxDistanceError,
308
+ contacts: this.contacts.map(contact => ({ ...contact })), selfContacts: this.selfContacts.map(contact => ({ ...contact })), maxDistanceError,
279
309
  resources: { particles: this.positions.length, triangles: this.config.triangles.length, constraints: this.config.constraints.length, anchors: this.anchors.size, colliders: this.colliders.length } };
280
310
  }
281
311
  snapshot() {
@@ -283,7 +313,7 @@ export class ClothSolver {
283
313
  const state = this.inspect();
284
314
  return { format: "pelican-cloth-cpu", version: 1, tick: this.tickValue,
285
315
  config: { ...this.config, gravity: copy(this.config.gravity), triangles: this.config.triangles.map(tuple), constraints: this.config.constraints.map(tuple) },
286
- positions: state.positions, velocities: state.velocities, forces: this.forces.map(copy), anchors: state.anchors, colliders: readColliders(this.colliders), contacts: state.contacts };
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 };
287
317
  }
288
318
  restore(input) {
289
319
  this.alive();
@@ -291,7 +321,7 @@ export class ClothSolver {
291
321
  fail("CLOTH_SNAPSHOT", "unsupported snapshot format or tick");
292
322
  const config = input.config, expected = this.config;
293
323
  if (!config || config.mass !== expected.mass || config.radius !== expected.radius || config.fixedDt !== expected.fixedDt || config.substeps !== expected.substeps ||
294
- config.iterations !== expected.iterations || config.damping !== expected.damping || config.bending !== expected.bending ||
324
+ config.iterations !== expected.iterations || config.damping !== expected.damping || config.bending !== expected.bending || (config.selfCollisions ?? false) !== expected.selfCollisions ||
295
325
  !vec(config.gravity) || config.gravity.some((value, i) => value !== expected.gravity[i]) ||
296
326
  !Array.isArray(config.triangles) || JSON.stringify(config.triangles) !== JSON.stringify(expected.triangles) ||
297
327
  !Array.isArray(config.constraints) || JSON.stringify(config.constraints) !== JSON.stringify(expected.constraints))
@@ -320,8 +350,9 @@ export class ClothSolver {
320
350
  this.anchors = anchors;
321
351
  this.colliders = colliders;
322
352
  this.contacts = input.contacts.map(contact => ({ ...contact }));
353
+ this.selfContacts = (input.selfContacts ?? []).map(contact => ({ ...contact }));
323
354
  this.tickValue = input.tick;
324
355
  }
325
356
  dispose() { if (this.disposed)
326
- return; this.disposed = true; this.positions = []; this.velocities = []; this.forces = []; this.colliders = []; this.contacts = []; this.anchors.clear(); }
357
+ return; this.disposed = true; this.positions = []; this.velocities = []; this.forces = []; this.colliders = []; this.contacts = []; this.selfContacts = []; this.anchors.clear(); }
327
358
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pelican-engine",
3
- "version": "0.3.18",
3
+ "version": "0.3.19",
4
4
  "description": "Pelican Engine public SDK and offline development CLI",
5
5
  "type": "module",
6
6
  "license": "MIT",