paperlab 0.7.0 → 0.7.1

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/dist/index.cjs CHANGED
@@ -3718,6 +3718,8 @@ var ClothSim = class {
3718
3718
  height;
3719
3719
  positions;
3720
3720
  prev;
3721
+ /** Where the constructor laid every particle out — what {@link restore} puts back. */
3722
+ laidOut;
3721
3723
  pinned;
3722
3724
  pinTargets;
3723
3725
  /**
@@ -3851,6 +3853,7 @@ var ClothSim = class {
3851
3853
  }
3852
3854
  }
3853
3855
  this.prev.set(this.positions);
3856
+ this.laidOut = this.positions.slice();
3854
3857
  const idx = (r2, c2) => r2 * cols + c2;
3855
3858
  const links = [];
3856
3859
  const link = (a, b2, kind) => {
@@ -3968,6 +3971,22 @@ var ClothSim = class {
3968
3971
  this.asleep = false;
3969
3972
  this.stillFrames = 0;
3970
3973
  }
3974
+ /**
3975
+ * Back to the sheet as it was laid out: flat, still, every particle where
3976
+ * the constructor put it. Pins, a grab and every lever are left alone.
3977
+ *
3978
+ * For a history that has been rewound — a burn played back from an earlier
3979
+ * moment, say, which hands back paper that had burnt away and paper that
3980
+ * had FALLEN, lying on the floor with its springs to the sheet suddenly
3981
+ * whole again. Solved from there, the solve hauls it up through the sheet
3982
+ * in a tangle. Laid out afresh, it is simply the sheet it is again.
3983
+ */
3984
+ restore() {
3985
+ this.positions.set(this.laidOut);
3986
+ this.prev.set(this.laidOut);
3987
+ this.accumulator = 0;
3988
+ this.wake();
3989
+ }
3971
3990
  /**
3972
3991
  * Take hold of the sheet at one particle.
3973
3992
  *
@@ -4338,6 +4357,7 @@ var CHAR_SHRINK = 0.12;
4338
4357
  var CHAR_CURL = 6;
4339
4358
  var FRONT_GRADIENT = 16 * 16;
4340
4359
  var WET_MASS = 2;
4360
+ var TEAR_STRETCH = 1.5;
4341
4361
  var DamageCoupling = class {
4342
4362
  sim;
4343
4363
  source = null;
@@ -4347,8 +4367,24 @@ var DamageCoupling = class {
4347
4367
  particleTexel = new Int32Array(0);
4348
4368
  /** Gone, per particle, as of the last read. */
4349
4369
  gone;
4370
+ /** Whether any particle has ever been gone since the last reset — nothing can tear before. */
4371
+ holed = false;
4350
4372
  /** The gone particles with live paper beside them. See {@link follow}. */
4351
4373
  followers = [];
4374
+ /**
4375
+ * Which piece of paper each particle belongs to, named by the lowest index
4376
+ * in it — or -1 in the middle of a hole.
4377
+ *
4378
+ * A piece is whatever the sim's unbroken springs still hold together, which
4379
+ * is exactly what moves as one: two pieces are two things the solve lets
4380
+ * part. A gone particle on a cut takes the piece it borders MOST — a cut a
4381
+ * single particle wide borders two, and a particle averaged between them
4382
+ * would be dragged across the gap as one piece fell, stretching both edges
4383
+ * into it.
4384
+ */
4385
+ piece;
4386
+ /** Union-find over the particles, reused on every read. */
4387
+ parent;
4352
4388
  /**
4353
4389
  * Per bend spring, how squarely it lies ACROSS the burn front, 0..1 — and
4354
4390
  * remembered once it has been.
@@ -4366,9 +4402,20 @@ var DamageCoupling = class {
4366
4402
  * but the paper stays rolled the way it rolled.
4367
4403
  */
4368
4404
  curlWeight;
4405
+ /** The mesh's index as it was built — what {@link tear} hides triangles from and mends them back to. */
4406
+ original = null;
4407
+ /** Per triangle of that index: torn, and hidden until the history is rewound. */
4408
+ torn = new Uint8Array(0);
4409
+ /** The triangles with a gone corner — the only ones that can tear. */
4410
+ rim = [];
4411
+ rimStale = true;
4412
+ /** Every torn triangle has to be put back: the history was rewound, or the source went. */
4413
+ mend = false;
4369
4414
  constructor(sim) {
4370
4415
  this.sim = sim;
4371
4416
  this.gone = new Uint8Array(sim.count);
4417
+ this.piece = new Int32Array(sim.count);
4418
+ this.parent = new Int32Array(sim.count);
4372
4419
  this.curlWeight = new Float32Array(sim.constraintCount);
4373
4420
  }
4374
4421
  /**
@@ -4380,7 +4427,13 @@ var DamageCoupling = class {
4380
4427
  update(source) {
4381
4428
  const next = source ?? null;
4382
4429
  if (next === this.source && (next === null || next.version === this.version)) return false;
4383
- if (next !== this.source) this.curlWeight.fill(0);
4430
+ if (next !== this.source) {
4431
+ this.curlWeight.fill(0);
4432
+ if (next && this.holed) {
4433
+ this.rewind();
4434
+ this.gone.fill(0);
4435
+ }
4436
+ }
4384
4437
  this.source = next;
4385
4438
  if (!next) {
4386
4439
  this.reset();
@@ -4400,12 +4453,13 @@ var DamageCoupling = class {
4400
4453
  * laid out flat around them — their positions plus its rest offset from
4401
4454
  * each, averaged. The triangles that straddle the burnt edge then keep
4402
4455
  * roughly their shape instead of stretching back to wherever the particle
4403
- * was when it burnt. Only a band of a single particle's width, with live
4404
- * paper pulling it both ways, is left averaging between them.
4456
+ * was when it burnt. Only the neighbours in its own {@link piece} count, so
4457
+ * on a cut between two pieces it leaves with one of them.
4405
4458
  */
4406
4459
  follow() {
4407
4460
  if (this.followers.length === 0) return;
4408
4461
  const { gone, sim } = this;
4462
+ const piece = this.piece;
4409
4463
  const p2 = sim.positions;
4410
4464
  const { cols, rows } = sim;
4411
4465
  const cellX = cols > 1 ? sim.width / (cols - 1) : 0;
@@ -4413,6 +4467,7 @@ var DamageCoupling = class {
4413
4467
  for (const g of this.followers) {
4414
4468
  const r2 = g / cols | 0;
4415
4469
  const c2 = g % cols;
4470
+ const own = piece[g];
4416
4471
  let sx = 0;
4417
4472
  let sy = 0;
4418
4473
  let sz = 0;
@@ -4424,7 +4479,7 @@ var DamageCoupling = class {
4424
4479
  const nc = c2 + dc;
4425
4480
  if (dr === 0 && dc === 0 || nc < 0 || nc >= cols) continue;
4426
4481
  const j3 = nr * cols + nc;
4427
- if (gone[j3]) continue;
4482
+ if (gone[j3] || piece[j3] !== own) continue;
4428
4483
  const j32 = j3 * 3;
4429
4484
  sx += p2[j32] - dc * cellX;
4430
4485
  sy += p2[j32 + 1] + dr * cellY;
@@ -4439,6 +4494,70 @@ var DamageCoupling = class {
4439
4494
  p2[g3 + 2] = sz / n;
4440
4495
  }
4441
4496
  }
4497
+ /**
4498
+ * Hide the triangles a piece has torn away from. Call after {@link follow},
4499
+ * with the sheet's index buffer: it is rewritten in place, and the return
4500
+ * says whether it changed, so the caller knows to upload it.
4501
+ *
4502
+ * A triangle tears when one of its edges has stretched past
4503
+ * {@link TEAR_STRETCH} — which, among triangles with a burnt-away corner,
4504
+ * only ever happens across a gap that is opening. It is then collapsed to a
4505
+ * point, which draws nothing, casts no shadow and adds nothing to its
4506
+ * corners' normals, and it stays hidden: torn paper does not mend because
4507
+ * the two sides swing close again. Only a rewound history puts it back.
4508
+ *
4509
+ * A sheet in one piece, held, never stretches one that far — so a burn that
4510
+ * cuts nothing loose leaves this index exactly as it was built.
4511
+ */
4512
+ tear(index) {
4513
+ if (!this.holed && !this.mend) return false;
4514
+ if (!this.original || this.original.length !== index.length) {
4515
+ this.original = index.slice();
4516
+ this.torn = new Uint8Array(index.length / 3);
4517
+ this.rimStale = true;
4518
+ }
4519
+ const { original, torn, gone, sim } = this;
4520
+ let changed = false;
4521
+ if (this.mend) {
4522
+ this.mend = false;
4523
+ index.set(original);
4524
+ changed = true;
4525
+ }
4526
+ if (this.rimStale) {
4527
+ this.rimStale = false;
4528
+ const rim = [];
4529
+ for (let t = 0; t < torn.length; t++) {
4530
+ if (gone[original[t * 3]] || gone[original[t * 3 + 1]] || gone[original[t * 3 + 2]]) rim.push(t);
4531
+ }
4532
+ this.rim = rim;
4533
+ }
4534
+ const p2 = sim.positions;
4535
+ const { cols, rows } = sim;
4536
+ const cellX = cols > 1 ? sim.width / (cols - 1) : 0;
4537
+ const cellY = rows > 1 ? sim.height / (rows - 1) : 0;
4538
+ const limit = TEAR_STRETCH * TEAR_STRETCH;
4539
+ const stretched = (i, j3) => {
4540
+ const dc = i % cols - j3 % cols;
4541
+ const dr = (i / cols | 0) - (j3 / cols | 0);
4542
+ const rest = (dc * cellX) ** 2 + (dr * cellY) ** 2;
4543
+ const i3 = i * 3;
4544
+ const j32 = j3 * 3;
4545
+ const now = (p2[i3] - p2[j32]) ** 2 + (p2[i3 + 1] - p2[j32 + 1]) ** 2 + (p2[i3 + 2] - p2[j32 + 2]) ** 2;
4546
+ return now > rest * limit;
4547
+ };
4548
+ for (const t of this.rim) {
4549
+ if (torn[t]) continue;
4550
+ const a = original[t * 3];
4551
+ const b2 = original[t * 3 + 1];
4552
+ const c2 = original[t * 3 + 2];
4553
+ if (!stretched(a, b2) && !stretched(b2, c2) && !stretched(c2, a)) continue;
4554
+ torn[t] = 1;
4555
+ index[t * 3 + 1] = a;
4556
+ index[t * 3 + 2] = a;
4557
+ changed = true;
4558
+ }
4559
+ return changed;
4560
+ }
4442
4561
  /** Map each particle to its texel. The damage grid's row 0 is v = 0, the sheet's BOTTOM. */
4443
4562
  layout(size) {
4444
4563
  const { cols, rows, count } = this.sim;
@@ -4466,12 +4585,19 @@ var DamageCoupling = class {
4466
4585
  constraintKind,
4467
4586
  constraintMiddle
4468
4587
  } = sim;
4588
+ let rewound = false;
4589
+ let holed = false;
4469
4590
  for (let i = 0; i < sim.count; i++) {
4470
4591
  const t = particleTexel[i] * 4;
4471
4592
  const isGone = pixels[t + PRESENCE] < GONE_BELOW;
4593
+ if (gone[i] && !isGone) rewound = true;
4594
+ if (isGone) holed = true;
4472
4595
  gone[i] = isGone ? 1 : 0;
4473
4596
  invMass[i] = isGone ? 0 : 1 / (1 + WET_MASS * pixels[t + SATURATION] / 255);
4474
4597
  }
4598
+ if (rewound) this.rewind();
4599
+ this.holed = holed;
4600
+ this.rimStale = true;
4475
4601
  for (let k2 = 0; k2 < sim.constraintCount; k2++) {
4476
4602
  const a = constraintA[k2];
4477
4603
  const b2 = constraintB[k2];
@@ -4504,31 +4630,95 @@ var DamageCoupling = class {
4504
4630
  restBend[k2] = CHAR_CURL * char * curlWeight[k2] * span * span / 8;
4505
4631
  }
4506
4632
  }
4633
+ this.findPieces();
4634
+ }
4635
+ /**
4636
+ * Which piece each particle is in ({@link piece}), and which gone ones
4637
+ * follow a piece at all ({@link followers}).
4638
+ *
4639
+ * Pieces by union-find over the springs that still hold, rooted at the
4640
+ * lowest index so the same burn names the same pieces every time. Then each
4641
+ * gone particle with live paper beside it joins the piece it has the most
4642
+ * neighbours in, the lower-named on a tie.
4643
+ */
4644
+ findPieces() {
4645
+ const { sim, gone } = this;
4646
+ const parent = this.parent;
4647
+ const piece = this.piece;
4648
+ const { broken, constraintA, constraintB } = sim;
4649
+ for (let i = 0; i < sim.count; i++) parent[i] = i;
4650
+ const find = (i) => {
4651
+ let r2 = i;
4652
+ while (parent[r2] !== r2) r2 = parent[r2];
4653
+ while (parent[i] !== r2) {
4654
+ const up = parent[i];
4655
+ parent[i] = r2;
4656
+ i = up;
4657
+ }
4658
+ return r2;
4659
+ };
4660
+ for (let k2 = 0; k2 < sim.constraintCount; k2++) {
4661
+ if (broken[k2]) continue;
4662
+ const ra = find(constraintA[k2]);
4663
+ const rb = find(constraintB[k2]);
4664
+ if (ra < rb) parent[rb] = ra;
4665
+ else if (rb < ra) parent[ra] = rb;
4666
+ }
4667
+ for (let i = 0; i < sim.count; i++) piece[i] = gone[i] ? -1 : find(i);
4507
4668
  const followers = [];
4508
4669
  const { cols, rows } = sim;
4670
+ const seen = [];
4671
+ const votes = [];
4509
4672
  for (let i = 0; i < sim.count; i++) {
4510
4673
  if (!gone[i]) continue;
4511
4674
  const r2 = i / cols | 0;
4512
4675
  const c2 = i % cols;
4513
- let beside = false;
4514
- for (let dr = -1; dr <= 1 && !beside; dr++) {
4676
+ seen.length = 0;
4677
+ votes.length = 0;
4678
+ for (let dr = -1; dr <= 1; dr++) {
4515
4679
  const nr = r2 + dr;
4516
4680
  if (nr < 0 || nr >= rows) continue;
4517
4681
  for (let dc = -1; dc <= 1; dc++) {
4518
4682
  const nc = c2 + dc;
4519
4683
  if (nc < 0 || nc >= cols) continue;
4520
- if (!gone[nr * cols + nc]) {
4521
- beside = true;
4522
- break;
4684
+ const j3 = nr * cols + nc;
4685
+ if (gone[j3]) continue;
4686
+ const at = seen.indexOf(piece[j3]);
4687
+ if (at < 0) {
4688
+ seen.push(piece[j3]);
4689
+ votes.push(1);
4690
+ } else {
4691
+ votes[at]++;
4523
4692
  }
4524
4693
  }
4525
4694
  }
4526
- if (beside) followers.push(i);
4695
+ if (seen.length === 0) continue;
4696
+ let best = 0;
4697
+ for (let k2 = 1; k2 < seen.length; k2++) {
4698
+ if (votes[k2] > votes[best] || votes[k2] === votes[best] && seen[k2] < seen[best]) best = k2;
4699
+ }
4700
+ piece[i] = seen[best];
4701
+ followers.push(i);
4527
4702
  }
4528
4703
  this.followers = followers;
4529
4704
  }
4705
+ /**
4706
+ * A rewound history: the sheet starts over.
4707
+ *
4708
+ * Paper that had fallen away is paper again, with every spring back to the
4709
+ * sheet it was cut from — solved from where it lies, the solve would haul it
4710
+ * up off the floor through the sheet in a tangle. And the curl it rolled
4711
+ * into belonged to a front that has not passed yet.
4712
+ */
4713
+ rewind() {
4714
+ this.sim.restore();
4715
+ this.curlWeight.fill(0);
4716
+ this.torn.fill(0);
4717
+ this.mend = true;
4718
+ }
4530
4719
  reset() {
4531
4720
  const { sim } = this;
4721
+ if (this.holed) this.rewind();
4532
4722
  sim.invMass.fill(1);
4533
4723
  sim.restBend.fill(0);
4534
4724
  sim.broken.fill(0);
@@ -4536,6 +4726,7 @@ var DamageCoupling = class {
4536
4726
  this.gone.fill(0);
4537
4727
  this.curlWeight.fill(0);
4538
4728
  this.followers = [];
4729
+ this.holed = false;
4539
4730
  this.version = -1;
4540
4731
  sim.wake();
4541
4732
  }
@@ -8188,7 +8379,11 @@ var PaperMesh = (0, import_react8.forwardRef)(function PaperMesh2(props, ref) {
8188
8379
  coupling?.update(props.damage);
8189
8380
  sim.step(delta);
8190
8381
  const moved = !sim.asleep;
8191
- if (moved) coupling?.follow();
8382
+ if (moved && coupling) {
8383
+ coupling.follow();
8384
+ const index = geometry.index;
8385
+ if (index && coupling.tear(index.array)) index.needsUpdate = true;
8386
+ }
8192
8387
  if (!applyShape(sim.positions, moved) && moved) {
8193
8388
  const position = geometry.attributes.position;
8194
8389
  position.array.set(sim.positions);