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.js CHANGED
@@ -79,7 +79,7 @@ import {
79
79
  useResolvedConfig,
80
80
  washSchema,
81
81
  wrapLines
82
- } from "./chunk-MZCAN3AR.js";
82
+ } from "./chunk-NTDVOOBA.js";
83
83
  import {
84
84
  DAMAGE_CHANNELS,
85
85
  DAMAGE_LOOK_DEFAULTS
package/dist/stage.cjs CHANGED
@@ -4360,6 +4360,8 @@ var ClothSim = class {
4360
4360
  height;
4361
4361
  positions;
4362
4362
  prev;
4363
+ /** Where the constructor laid every particle out — what {@link restore} puts back. */
4364
+ laidOut;
4363
4365
  pinned;
4364
4366
  pinTargets;
4365
4367
  /**
@@ -4493,6 +4495,7 @@ var ClothSim = class {
4493
4495
  }
4494
4496
  }
4495
4497
  this.prev.set(this.positions);
4498
+ this.laidOut = this.positions.slice();
4496
4499
  const idx = (r2, c2) => r2 * cols + c2;
4497
4500
  const links = [];
4498
4501
  const link = (a, b2, kind) => {
@@ -4610,6 +4613,22 @@ var ClothSim = class {
4610
4613
  this.asleep = false;
4611
4614
  this.stillFrames = 0;
4612
4615
  }
4616
+ /**
4617
+ * Back to the sheet as it was laid out: flat, still, every particle where
4618
+ * the constructor put it. Pins, a grab and every lever are left alone.
4619
+ *
4620
+ * For a history that has been rewound — a burn played back from an earlier
4621
+ * moment, say, which hands back paper that had burnt away and paper that
4622
+ * had FALLEN, lying on the floor with its springs to the sheet suddenly
4623
+ * whole again. Solved from there, the solve hauls it up through the sheet
4624
+ * in a tangle. Laid out afresh, it is simply the sheet it is again.
4625
+ */
4626
+ restore() {
4627
+ this.positions.set(this.laidOut);
4628
+ this.prev.set(this.laidOut);
4629
+ this.accumulator = 0;
4630
+ this.wake();
4631
+ }
4613
4632
  /**
4614
4633
  * Take hold of the sheet at one particle.
4615
4634
  *
@@ -4980,6 +4999,7 @@ var CHAR_SHRINK = 0.12;
4980
4999
  var CHAR_CURL = 6;
4981
5000
  var FRONT_GRADIENT = 16 * 16;
4982
5001
  var WET_MASS = 2;
5002
+ var TEAR_STRETCH = 1.5;
4983
5003
  var DamageCoupling = class {
4984
5004
  sim;
4985
5005
  source = null;
@@ -4989,8 +5009,24 @@ var DamageCoupling = class {
4989
5009
  particleTexel = new Int32Array(0);
4990
5010
  /** Gone, per particle, as of the last read. */
4991
5011
  gone;
5012
+ /** Whether any particle has ever been gone since the last reset — nothing can tear before. */
5013
+ holed = false;
4992
5014
  /** The gone particles with live paper beside them. See {@link follow}. */
4993
5015
  followers = [];
5016
+ /**
5017
+ * Which piece of paper each particle belongs to, named by the lowest index
5018
+ * in it — or -1 in the middle of a hole.
5019
+ *
5020
+ * A piece is whatever the sim's unbroken springs still hold together, which
5021
+ * is exactly what moves as one: two pieces are two things the solve lets
5022
+ * part. A gone particle on a cut takes the piece it borders MOST — a cut a
5023
+ * single particle wide borders two, and a particle averaged between them
5024
+ * would be dragged across the gap as one piece fell, stretching both edges
5025
+ * into it.
5026
+ */
5027
+ piece;
5028
+ /** Union-find over the particles, reused on every read. */
5029
+ parent;
4994
5030
  /**
4995
5031
  * Per bend spring, how squarely it lies ACROSS the burn front, 0..1 — and
4996
5032
  * remembered once it has been.
@@ -5008,9 +5044,20 @@ var DamageCoupling = class {
5008
5044
  * but the paper stays rolled the way it rolled.
5009
5045
  */
5010
5046
  curlWeight;
5047
+ /** The mesh's index as it was built — what {@link tear} hides triangles from and mends them back to. */
5048
+ original = null;
5049
+ /** Per triangle of that index: torn, and hidden until the history is rewound. */
5050
+ torn = new Uint8Array(0);
5051
+ /** The triangles with a gone corner — the only ones that can tear. */
5052
+ rim = [];
5053
+ rimStale = true;
5054
+ /** Every torn triangle has to be put back: the history was rewound, or the source went. */
5055
+ mend = false;
5011
5056
  constructor(sim) {
5012
5057
  this.sim = sim;
5013
5058
  this.gone = new Uint8Array(sim.count);
5059
+ this.piece = new Int32Array(sim.count);
5060
+ this.parent = new Int32Array(sim.count);
5014
5061
  this.curlWeight = new Float32Array(sim.constraintCount);
5015
5062
  }
5016
5063
  /**
@@ -5022,7 +5069,13 @@ var DamageCoupling = class {
5022
5069
  update(source) {
5023
5070
  const next = source ?? null;
5024
5071
  if (next === this.source && (next === null || next.version === this.version)) return false;
5025
- if (next !== this.source) this.curlWeight.fill(0);
5072
+ if (next !== this.source) {
5073
+ this.curlWeight.fill(0);
5074
+ if (next && this.holed) {
5075
+ this.rewind();
5076
+ this.gone.fill(0);
5077
+ }
5078
+ }
5026
5079
  this.source = next;
5027
5080
  if (!next) {
5028
5081
  this.reset();
@@ -5042,12 +5095,13 @@ var DamageCoupling = class {
5042
5095
  * laid out flat around them — their positions plus its rest offset from
5043
5096
  * each, averaged. The triangles that straddle the burnt edge then keep
5044
5097
  * roughly their shape instead of stretching back to wherever the particle
5045
- * was when it burnt. Only a band of a single particle's width, with live
5046
- * paper pulling it both ways, is left averaging between them.
5098
+ * was when it burnt. Only the neighbours in its own {@link piece} count, so
5099
+ * on a cut between two pieces it leaves with one of them.
5047
5100
  */
5048
5101
  follow() {
5049
5102
  if (this.followers.length === 0) return;
5050
5103
  const { gone, sim } = this;
5104
+ const piece = this.piece;
5051
5105
  const p2 = sim.positions;
5052
5106
  const { cols, rows } = sim;
5053
5107
  const cellX = cols > 1 ? sim.width / (cols - 1) : 0;
@@ -5055,6 +5109,7 @@ var DamageCoupling = class {
5055
5109
  for (const g of this.followers) {
5056
5110
  const r2 = g / cols | 0;
5057
5111
  const c2 = g % cols;
5112
+ const own = piece[g];
5058
5113
  let sx = 0;
5059
5114
  let sy = 0;
5060
5115
  let sz = 0;
@@ -5066,7 +5121,7 @@ var DamageCoupling = class {
5066
5121
  const nc = c2 + dc;
5067
5122
  if (dr === 0 && dc === 0 || nc < 0 || nc >= cols) continue;
5068
5123
  const j3 = nr * cols + nc;
5069
- if (gone[j3]) continue;
5124
+ if (gone[j3] || piece[j3] !== own) continue;
5070
5125
  const j32 = j3 * 3;
5071
5126
  sx += p2[j32] - dc * cellX;
5072
5127
  sy += p2[j32 + 1] + dr * cellY;
@@ -5081,6 +5136,70 @@ var DamageCoupling = class {
5081
5136
  p2[g3 + 2] = sz / n;
5082
5137
  }
5083
5138
  }
5139
+ /**
5140
+ * Hide the triangles a piece has torn away from. Call after {@link follow},
5141
+ * with the sheet's index buffer: it is rewritten in place, and the return
5142
+ * says whether it changed, so the caller knows to upload it.
5143
+ *
5144
+ * A triangle tears when one of its edges has stretched past
5145
+ * {@link TEAR_STRETCH} — which, among triangles with a burnt-away corner,
5146
+ * only ever happens across a gap that is opening. It is then collapsed to a
5147
+ * point, which draws nothing, casts no shadow and adds nothing to its
5148
+ * corners' normals, and it stays hidden: torn paper does not mend because
5149
+ * the two sides swing close again. Only a rewound history puts it back.
5150
+ *
5151
+ * A sheet in one piece, held, never stretches one that far — so a burn that
5152
+ * cuts nothing loose leaves this index exactly as it was built.
5153
+ */
5154
+ tear(index) {
5155
+ if (!this.holed && !this.mend) return false;
5156
+ if (!this.original || this.original.length !== index.length) {
5157
+ this.original = index.slice();
5158
+ this.torn = new Uint8Array(index.length / 3);
5159
+ this.rimStale = true;
5160
+ }
5161
+ const { original, torn, gone, sim } = this;
5162
+ let changed = false;
5163
+ if (this.mend) {
5164
+ this.mend = false;
5165
+ index.set(original);
5166
+ changed = true;
5167
+ }
5168
+ if (this.rimStale) {
5169
+ this.rimStale = false;
5170
+ const rim = [];
5171
+ for (let t = 0; t < torn.length; t++) {
5172
+ if (gone[original[t * 3]] || gone[original[t * 3 + 1]] || gone[original[t * 3 + 2]]) rim.push(t);
5173
+ }
5174
+ this.rim = rim;
5175
+ }
5176
+ const p2 = sim.positions;
5177
+ const { cols, rows } = sim;
5178
+ const cellX = cols > 1 ? sim.width / (cols - 1) : 0;
5179
+ const cellY = rows > 1 ? sim.height / (rows - 1) : 0;
5180
+ const limit = TEAR_STRETCH * TEAR_STRETCH;
5181
+ const stretched = (i, j3) => {
5182
+ const dc = i % cols - j3 % cols;
5183
+ const dr = (i / cols | 0) - (j3 / cols | 0);
5184
+ const rest = (dc * cellX) ** 2 + (dr * cellY) ** 2;
5185
+ const i3 = i * 3;
5186
+ const j32 = j3 * 3;
5187
+ const now = (p2[i3] - p2[j32]) ** 2 + (p2[i3 + 1] - p2[j32 + 1]) ** 2 + (p2[i3 + 2] - p2[j32 + 2]) ** 2;
5188
+ return now > rest * limit;
5189
+ };
5190
+ for (const t of this.rim) {
5191
+ if (torn[t]) continue;
5192
+ const a = original[t * 3];
5193
+ const b2 = original[t * 3 + 1];
5194
+ const c2 = original[t * 3 + 2];
5195
+ if (!stretched(a, b2) && !stretched(b2, c2) && !stretched(c2, a)) continue;
5196
+ torn[t] = 1;
5197
+ index[t * 3 + 1] = a;
5198
+ index[t * 3 + 2] = a;
5199
+ changed = true;
5200
+ }
5201
+ return changed;
5202
+ }
5084
5203
  /** Map each particle to its texel. The damage grid's row 0 is v = 0, the sheet's BOTTOM. */
5085
5204
  layout(size) {
5086
5205
  const { cols, rows, count } = this.sim;
@@ -5108,12 +5227,19 @@ var DamageCoupling = class {
5108
5227
  constraintKind,
5109
5228
  constraintMiddle
5110
5229
  } = sim;
5230
+ let rewound = false;
5231
+ let holed = false;
5111
5232
  for (let i = 0; i < sim.count; i++) {
5112
5233
  const t = particleTexel[i] * 4;
5113
5234
  const isGone = pixels[t + PRESENCE] < GONE_BELOW;
5235
+ if (gone[i] && !isGone) rewound = true;
5236
+ if (isGone) holed = true;
5114
5237
  gone[i] = isGone ? 1 : 0;
5115
5238
  invMass[i] = isGone ? 0 : 1 / (1 + WET_MASS * pixels[t + SATURATION] / 255);
5116
5239
  }
5240
+ if (rewound) this.rewind();
5241
+ this.holed = holed;
5242
+ this.rimStale = true;
5117
5243
  for (let k2 = 0; k2 < sim.constraintCount; k2++) {
5118
5244
  const a = constraintA[k2];
5119
5245
  const b2 = constraintB[k2];
@@ -5146,31 +5272,95 @@ var DamageCoupling = class {
5146
5272
  restBend[k2] = CHAR_CURL * char * curlWeight[k2] * span * span / 8;
5147
5273
  }
5148
5274
  }
5275
+ this.findPieces();
5276
+ }
5277
+ /**
5278
+ * Which piece each particle is in ({@link piece}), and which gone ones
5279
+ * follow a piece at all ({@link followers}).
5280
+ *
5281
+ * Pieces by union-find over the springs that still hold, rooted at the
5282
+ * lowest index so the same burn names the same pieces every time. Then each
5283
+ * gone particle with live paper beside it joins the piece it has the most
5284
+ * neighbours in, the lower-named on a tie.
5285
+ */
5286
+ findPieces() {
5287
+ const { sim, gone } = this;
5288
+ const parent = this.parent;
5289
+ const piece = this.piece;
5290
+ const { broken, constraintA, constraintB } = sim;
5291
+ for (let i = 0; i < sim.count; i++) parent[i] = i;
5292
+ const find = (i) => {
5293
+ let r2 = i;
5294
+ while (parent[r2] !== r2) r2 = parent[r2];
5295
+ while (parent[i] !== r2) {
5296
+ const up = parent[i];
5297
+ parent[i] = r2;
5298
+ i = up;
5299
+ }
5300
+ return r2;
5301
+ };
5302
+ for (let k2 = 0; k2 < sim.constraintCount; k2++) {
5303
+ if (broken[k2]) continue;
5304
+ const ra = find(constraintA[k2]);
5305
+ const rb = find(constraintB[k2]);
5306
+ if (ra < rb) parent[rb] = ra;
5307
+ else if (rb < ra) parent[ra] = rb;
5308
+ }
5309
+ for (let i = 0; i < sim.count; i++) piece[i] = gone[i] ? -1 : find(i);
5149
5310
  const followers = [];
5150
5311
  const { cols, rows } = sim;
5312
+ const seen = [];
5313
+ const votes = [];
5151
5314
  for (let i = 0; i < sim.count; i++) {
5152
5315
  if (!gone[i]) continue;
5153
5316
  const r2 = i / cols | 0;
5154
5317
  const c2 = i % cols;
5155
- let beside = false;
5156
- for (let dr = -1; dr <= 1 && !beside; dr++) {
5318
+ seen.length = 0;
5319
+ votes.length = 0;
5320
+ for (let dr = -1; dr <= 1; dr++) {
5157
5321
  const nr = r2 + dr;
5158
5322
  if (nr < 0 || nr >= rows) continue;
5159
5323
  for (let dc = -1; dc <= 1; dc++) {
5160
5324
  const nc = c2 + dc;
5161
5325
  if (nc < 0 || nc >= cols) continue;
5162
- if (!gone[nr * cols + nc]) {
5163
- beside = true;
5164
- break;
5326
+ const j3 = nr * cols + nc;
5327
+ if (gone[j3]) continue;
5328
+ const at = seen.indexOf(piece[j3]);
5329
+ if (at < 0) {
5330
+ seen.push(piece[j3]);
5331
+ votes.push(1);
5332
+ } else {
5333
+ votes[at]++;
5165
5334
  }
5166
5335
  }
5167
5336
  }
5168
- if (beside) followers.push(i);
5337
+ if (seen.length === 0) continue;
5338
+ let best = 0;
5339
+ for (let k2 = 1; k2 < seen.length; k2++) {
5340
+ if (votes[k2] > votes[best] || votes[k2] === votes[best] && seen[k2] < seen[best]) best = k2;
5341
+ }
5342
+ piece[i] = seen[best];
5343
+ followers.push(i);
5169
5344
  }
5170
5345
  this.followers = followers;
5171
5346
  }
5347
+ /**
5348
+ * A rewound history: the sheet starts over.
5349
+ *
5350
+ * Paper that had fallen away is paper again, with every spring back to the
5351
+ * sheet it was cut from — solved from where it lies, the solve would haul it
5352
+ * up off the floor through the sheet in a tangle. And the curl it rolled
5353
+ * into belonged to a front that has not passed yet.
5354
+ */
5355
+ rewind() {
5356
+ this.sim.restore();
5357
+ this.curlWeight.fill(0);
5358
+ this.torn.fill(0);
5359
+ this.mend = true;
5360
+ }
5172
5361
  reset() {
5173
5362
  const { sim } = this;
5363
+ if (this.holed) this.rewind();
5174
5364
  sim.invMass.fill(1);
5175
5365
  sim.restBend.fill(0);
5176
5366
  sim.broken.fill(0);
@@ -5178,6 +5368,7 @@ var DamageCoupling = class {
5178
5368
  this.gone.fill(0);
5179
5369
  this.curlWeight.fill(0);
5180
5370
  this.followers = [];
5371
+ this.holed = false;
5181
5372
  this.version = -1;
5182
5373
  sim.wake();
5183
5374
  }
@@ -8491,7 +8682,11 @@ var PaperMesh = (0, import_react12.forwardRef)(function PaperMesh2(props, ref) {
8491
8682
  coupling?.update(props.damage);
8492
8683
  sim.step(delta);
8493
8684
  const moved = !sim.asleep;
8494
- if (moved) coupling?.follow();
8685
+ if (moved && coupling) {
8686
+ coupling.follow();
8687
+ const index = geometry.index;
8688
+ if (index && coupling.tear(index.array)) index.needsUpdate = true;
8689
+ }
8495
8690
  if (!applyShape(sim.positions, moved) && moved) {
8496
8691
  const position = geometry.attributes.position;
8497
8692
  position.array.set(sim.positions);