paperlab 0.6.0 → 0.7.0

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.
@@ -1,3 +1,8 @@
1
+ import {
2
+ DAMAGE_CHANNELS,
3
+ DAMAGE_LOOK_DEFAULTS
4
+ } from "./chunk-7TLIWPGM.js";
5
+
1
6
  // src/config/merge.ts
2
7
  function mergeConfig(base, override) {
3
8
  if (override === void 0) return base;
@@ -4399,10 +4404,10 @@ function usePaperStates(config, enabled, instant, onAction, onStateChange) {
4399
4404
  }
4400
4405
 
4401
4406
  // src/PaperMesh.tsx
4402
- import * as THREE7 from "three";
4407
+ import * as THREE8 from "three";
4403
4408
  import { gsap as gsap2 } from "gsap";
4404
- import { useFrame, useThree } from "@react-three/fiber";
4405
- import { forwardRef, useEffect as useEffect5, useImperativeHandle, useMemo as useMemo3, useRef as useRef3 } from "react";
4409
+ import { useFrame as useFrame3, useThree } from "@react-three/fiber";
4410
+ import { forwardRef, useEffect as useEffect6, useImperativeHandle, useMemo as useMemo4, useRef as useRef3 } from "react";
4406
4411
 
4407
4412
  // src/core/normals.ts
4408
4413
  function computeSheetNormals(geometry) {
@@ -4498,6 +4503,27 @@ function spreadByAspect(sheet2, target) {
4498
4503
  if (!(long > 0)) return [target, target];
4499
4504
  return [Math.round(sheet2.width / long * target), Math.round(sheet2.height / long * target)];
4500
4505
  }
4506
+ function surfacePointAt(positions, cols, rows, u, v, out) {
4507
+ const fx = Math.min(1, Math.max(0, u)) * (cols - 1);
4508
+ const fy = (1 - Math.min(1, Math.max(0, v))) * (rows - 1);
4509
+ const ix = Math.min(Math.floor(fx), cols - 2);
4510
+ const iy = Math.min(Math.floor(fy), rows - 2);
4511
+ const tx = fx - ix;
4512
+ const ty = fy - iy;
4513
+ const a = (iy * cols + ix) * 3;
4514
+ const b = a + cols * 3;
4515
+ const c = b + 3;
4516
+ const d = a + 3;
4517
+ for (let axis = 0; axis < 3; axis++) {
4518
+ const pa = positions[a + axis];
4519
+ const pb = positions[b + axis];
4520
+ const pc = positions[c + axis];
4521
+ const pd = positions[d + axis];
4522
+ const value = tx + ty <= 1 ? pa + tx * (pd - pa) + ty * (pb - pa) : pc + (1 - tx) * (pb - pc) + (1 - ty) * (pd - pc);
4523
+ out.setComponent(axis, value);
4524
+ }
4525
+ return out;
4526
+ }
4501
4527
  function createSheetGeometry(sheet2, minSegments = 2, autoSegments = LEGACY_FLAT_SEGMENTS) {
4502
4528
  const [sx, sy] = resolveSegments(sheet2, minSegments, autoSegments);
4503
4529
  return new THREE.PlaneGeometry(sheet2.width, sheet2.height, sx, sy);
@@ -4912,7 +4938,82 @@ var ClothSim = class {
4912
4938
  prev;
4913
4939
  pinned;
4914
4940
  pinTargets;
4915
- constraints = [];
4941
+ /**
4942
+ * The constraints, one typed array per field rather than one object each.
4943
+ *
4944
+ * Because the effects layer has to vary them across the sheet, and an
4945
+ * object with a fixed `rest` cannot be varied cheaply. Paper that chars
4946
+ * SHRINKS — that is what curls it toward the flame, not softening — which
4947
+ * is a shorter rest length on exactly the constraints the heat reached.
4948
+ * Burnt-through paper has to stop pulling on what is left, which is a
4949
+ * broken flag. A wet corner is floppier than the dry sheet above it, which
4950
+ * is a per-constraint stiffness. None of that is in this file; this file
4951
+ * only makes it possible, and at the defaults below the solve is
4952
+ * bit-identical to the object version it replaces.
4953
+ */
4954
+ constraintCount;
4955
+ constraintA;
4956
+ constraintB;
4957
+ /** 0 = structural, 1 = shear, 2 = bend (scaled by `stiffness` at solve time). */
4958
+ constraintKind;
4959
+ /**
4960
+ * The length each constraint is laid out at. Never changes; the reference
4961
+ * to shrink from.
4962
+ *
4963
+ * Float64, not Float32, and that is measured rather than cautious: with
4964
+ * single-precision rest lengths every one of them rounded, and the first
4965
+ * typed-array version of this solve disagreed with the object version it
4966
+ * replaced in 476–711 of 714 coordinates after five seconds. The object
4967
+ * version held a JS number, which is a double.
4968
+ */
4969
+ naturalLength;
4970
+ /** The length it pulls toward now. `naturalLength` until something shrinks or stretches it. */
4971
+ restLength;
4972
+ /** A multiplier on the constraint's own strength. 1 is the paper as built. */
4973
+ constraintStiffness;
4974
+ /** Non-zero where the paper between two particles is gone. Skipped by the solve. */
4975
+ broken;
4976
+ /**
4977
+ * How easily each particle is moved, 0..1: one over its mass, relative to
4978
+ * dry paper.
4979
+ *
4980
+ * Mass matters in exactly two places here, and both are wired, because a
4981
+ * "heavier" that touched neither would be another knob that does nothing.
4982
+ * Gravity is an acceleration, so weight alone does not make wet paper fall
4983
+ * differently. What does: the air pushes a heavy particle less (the aero
4984
+ * term is divided by mass), and the constraint solve moves a heavy particle
4985
+ * less than a light one it is tied to. Zero is infinite mass: static, moved by nothing — not the air, not the solve, and not gravity either.
4986
+ */
4987
+ invMass;
4988
+ /**
4989
+ * How far the middle of each BEND spring sits off the line between its ends
4990
+ * when relaxed, along the sheet's normal — a signed rest curvature, as a
4991
+ * sagitta in world units. Ignored on every other kind of constraint.
4992
+ *
4993
+ * Positive curls the paper toward its FRONT, the face a flat sheet turns to
4994
+ * +z — which puts the middle of each span that far BEHIND its ends. Paper
4995
+ * curling toward you is hollow on the side you see; the first cut of this
4996
+ * had the middle in front, which is a dome, and a dome's rim goes the other
4997
+ * way.
4998
+ *
4999
+ * The only lever here with a sign, and the sign is why it exists. A bend
5000
+ * spring is a distance, and a distance cannot tell paper curled toward you
5001
+ * from paper curled away: shorten it and the sheet buckles, but which way
5002
+ * is a coin toss, and gravity can flip it through later. Measured on the
5003
+ * first version, which pushed hot paper toward the front along its normal
5004
+ * and left the char to hold it — the curl held while the heat did and
5005
+ * snapped through to the BACK the moment the front had passed. Charred
5006
+ * paper stays curled toward the side that burnt because that side shrank
5007
+ * more: a curvature built into the material, which is what this is.
5008
+ *
5009
+ * Solved as a couple — the middle moves one way, the two ends half as far
5010
+ * the other — so it bends the sheet and never pushes it anywhere. A push
5011
+ * cannot do this job: it moves paper, and a uniformly curled sheet in still
5012
+ * air has no business going anywhere.
5013
+ */
5014
+ restBend;
5015
+ /** For each bend spring, the particle between its ends; -1 for every other kind. */
5016
+ constraintMiddle;
4916
5017
  params;
4917
5018
  time = 0;
4918
5019
  accumulator = 0;
@@ -4969,10 +5070,9 @@ var ClothSim = class {
4969
5070
  }
4970
5071
  this.prev.set(this.positions);
4971
5072
  const idx = (r, c) => r * cols + c;
5073
+ const links = [];
4972
5074
  const link = (a, b, kind) => {
4973
- const dx = this.positions[a * 3] - this.positions[b * 3];
4974
- const dy = this.positions[a * 3 + 1] - this.positions[b * 3 + 1];
4975
- this.constraints.push({ a, b, rest: Math.hypot(dx, dy), kind });
5075
+ links.push(a, b, kind);
4976
5076
  };
4977
5077
  for (let r = 0; r < rows; r++) {
4978
5078
  for (let c = 0; c < cols; c++) {
@@ -4986,6 +5086,31 @@ var ClothSim = class {
4986
5086
  if (r + 2 < rows) link(idx(r, c), idx(r + 2, c), 2);
4987
5087
  }
4988
5088
  }
5089
+ const m = links.length / 3;
5090
+ this.constraintCount = m;
5091
+ this.constraintA = new Uint32Array(m);
5092
+ this.constraintB = new Uint32Array(m);
5093
+ this.constraintKind = new Uint8Array(m);
5094
+ this.naturalLength = new Float64Array(m);
5095
+ this.restLength = new Float64Array(m);
5096
+ this.constraintStiffness = new Float64Array(m).fill(1);
5097
+ this.broken = new Uint8Array(m);
5098
+ this.restBend = new Float64Array(m);
5099
+ this.constraintMiddle = new Int32Array(m).fill(-1);
5100
+ for (let k = 0; k < m; k++) {
5101
+ const a = links[k * 3];
5102
+ const b = links[k * 3 + 1];
5103
+ this.constraintA[k] = a;
5104
+ this.constraintB[k] = b;
5105
+ this.constraintKind[k] = links[k * 3 + 2];
5106
+ if (this.constraintKind[k] === 2) this.constraintMiddle[k] = (a + b) / 2;
5107
+ const dx = this.positions[a * 3] - this.positions[b * 3];
5108
+ const dy = this.positions[a * 3 + 1] - this.positions[b * 3 + 1];
5109
+ const rest = Math.hypot(dx, dy);
5110
+ this.naturalLength[k] = rest;
5111
+ this.restLength[k] = rest;
5112
+ }
5113
+ this.invMass = new Float32Array(this.count).fill(1);
4989
5114
  const pin = (r, c) => {
4990
5115
  const i = idx(r, c);
4991
5116
  this.pinned[i] = 1;
@@ -5103,7 +5228,7 @@ var ClothSim = class {
5103
5228
  for (let c = centreCol - spanCols; c <= centreCol + spanCols; c++) {
5104
5229
  if (c < 0 || c >= this.cols) continue;
5105
5230
  const i = r * this.cols + c;
5106
- if (this.pinned[i]) continue;
5231
+ if (this.pinned[i] || this.invMass[i] <= 0) continue;
5107
5232
  const distance = Math.hypot((c - centreCol) * cellX, (r - centreRow) * cellY);
5108
5233
  const weight = i === this.grabbedIndex ? 1 : grabFalloff(distance, radius);
5109
5234
  if (weight <= 0) continue;
@@ -5117,6 +5242,12 @@ var ClothSim = class {
5117
5242
  );
5118
5243
  }
5119
5244
  }
5245
+ if (this.grabbed.length === 0) {
5246
+ this.grabbedIndex = -1;
5247
+ this.grabOffsets = new Float32Array(0);
5248
+ this.wake();
5249
+ return -1;
5250
+ }
5120
5251
  this.grabOffsets = Float32Array.from(offsets);
5121
5252
  this.grabAt.set(this.positions.subarray(anchor, anchor + 3));
5122
5253
  this.grabWas.set(this.grabAt);
@@ -5277,12 +5408,19 @@ var ClothSim = class {
5277
5408
  const ax = nx * facing * (1 - AERO_TURBULENCE) + gust2 * 0.25 * AERO_TURBULENCE;
5278
5409
  const ay = ny * facing * (1 - AERO_TURBULENCE);
5279
5410
  const az = nz * facing * (1 - AERO_TURBULENCE) + gust2 * AERO_TURBULENCE;
5411
+ const im = this.invMass[i];
5412
+ if (im <= 0) {
5413
+ this.prev[i3] = x;
5414
+ this.prev[i3 + 1] = y;
5415
+ this.prev[i3 + 2] = z25;
5416
+ continue;
5417
+ }
5280
5418
  this.prev[i3] = x;
5281
5419
  this.prev[i3 + 1] = y;
5282
5420
  this.prev[i3 + 2] = z25;
5283
- p[i3] = x + vx + ax * dt2;
5284
- p[i3 + 1] = y + vy + (ay - gravity * 3.2) * dt2;
5285
- p[i3 + 2] = z25 + vz + az * dt2;
5421
+ p[i3] = x + vx + ax * im * dt2;
5422
+ p[i3 + 1] = y + vy + (ay * im - gravity * 3.2) * dt2;
5423
+ p[i3 + 2] = z25 + vz + az * im * dt2;
5286
5424
  maxTravel = Math.max(maxTravel, vx * vx + vy * vy + vz * vz);
5287
5425
  }
5288
5426
  for (let k = 0; k < this.grabbed.length; k++) {
@@ -5295,21 +5433,38 @@ var ClothSim = class {
5295
5433
  p[i3 + axis] = p[i3 + axis] + (target - p[i3 + axis]) * held;
5296
5434
  }
5297
5435
  }
5436
+ const {
5437
+ constraintA,
5438
+ constraintB,
5439
+ constraintKind,
5440
+ restLength,
5441
+ constraintStiffness,
5442
+ broken,
5443
+ invMass,
5444
+ restBend,
5445
+ constraintMiddle,
5446
+ normals
5447
+ } = this;
5448
+ const bend2 = 0.25 + stiffness * 0.7;
5298
5449
  for (let iter = 0; iter < SOLVER_ITERATIONS2; iter++) {
5299
- for (const c of this.constraints) {
5300
- const k = c.kind === 2 ? 0.25 + stiffness * 0.7 : c.kind === 1 ? 0.85 : 1;
5301
- const a3 = c.a * 3;
5302
- const b3 = c.b * 3;
5450
+ for (let c = 0; c < this.constraintCount; c++) {
5451
+ if (broken[c]) continue;
5452
+ const kind = constraintKind[c];
5453
+ const k = (kind === 2 ? bend2 : kind === 1 ? 0.85 : 1) * constraintStiffness[c];
5454
+ const ia = constraintA[c];
5455
+ const ib = constraintB[c];
5456
+ const a3 = ia * 3;
5457
+ const b3 = ib * 3;
5303
5458
  const dx = p[b3] - p[a3];
5304
5459
  const dy = p[b3 + 1] - p[a3 + 1];
5305
5460
  const dz = p[b3 + 2] - p[a3 + 2];
5306
5461
  const dist = Math.sqrt(dx * dx + dy * dy + dz * dz);
5307
5462
  if (dist === 0) continue;
5308
- const ma = this.pinned[c.a] ? 0 : 1 - this.grabWeights[c.a];
5309
- const mb = this.pinned[c.b] ? 0 : 1 - this.grabWeights[c.b];
5463
+ const ma = this.pinned[ia] ? 0 : invMass[ia] * (1 - this.grabWeights[ia]);
5464
+ const mb = this.pinned[ib] ? 0 : invMass[ib] * (1 - this.grabWeights[ib]);
5310
5465
  const total = ma + mb;
5311
5466
  if (total <= 0) continue;
5312
- const diff = (dist - c.rest) / dist / total * k;
5467
+ const diff = (dist - restLength[c]) / dist / total * k;
5313
5468
  const aw = ma;
5314
5469
  const bw = mb;
5315
5470
  p[a3] = p[a3] + dx * diff * aw;
@@ -5318,6 +5473,31 @@ var ClothSim = class {
5318
5473
  p[b3] = p[b3] - dx * diff * bw;
5319
5474
  p[b3 + 1] = p[b3 + 1] - dy * diff * bw;
5320
5475
  p[b3 + 2] = p[b3 + 2] - dz * diff * bw;
5476
+ const sag = restBend[c];
5477
+ if (sag !== 0) {
5478
+ const im = constraintMiddle[c];
5479
+ const m3 = im * 3;
5480
+ const nx = normals[m3];
5481
+ const ny = normals[m3 + 1];
5482
+ const nz = normals[m3 + 2];
5483
+ const offset = (p[m3] - (p[a3] + p[b3]) / 2) * nx + (p[m3 + 1] - (p[a3 + 1] + p[b3 + 1]) / 2) * ny + (p[m3 + 2] - (p[a3 + 2] + p[b3 + 2]) / 2) * nz;
5484
+ const mm = this.pinned[im] ? 0 : invMass[im] * (1 - this.grabWeights[im]);
5485
+ const weight = mm + (ma + mb) / 4;
5486
+ if (weight > 0) {
5487
+ const lambda = (-sag - offset) / weight * k;
5488
+ const along = lambda * mm;
5489
+ const back = lambda / 2;
5490
+ p[m3] = p[m3] + nx * along;
5491
+ p[m3 + 1] = p[m3 + 1] + ny * along;
5492
+ p[m3 + 2] = p[m3 + 2] + nz * along;
5493
+ p[a3] = p[a3] - nx * back * ma;
5494
+ p[a3 + 1] = p[a3 + 1] - ny * back * ma;
5495
+ p[a3 + 2] = p[a3 + 2] - nz * back * ma;
5496
+ p[b3] = p[b3] - nx * back * mb;
5497
+ p[b3 + 1] = p[b3 + 1] - ny * back * mb;
5498
+ p[b3 + 2] = p[b3 + 2] - nz * back * mb;
5499
+ }
5500
+ }
5321
5501
  }
5322
5502
  }
5323
5503
  for (let i = 0; i < this.count; i++) {
@@ -5338,9 +5518,220 @@ var ClothSim = class {
5338
5518
  }
5339
5519
  };
5340
5520
 
5521
+ // src/physics/damage.ts
5522
+ var { char: CHAR, saturation: SATURATION, presence: PRESENCE } = DAMAGE_CHANNELS;
5523
+ var GONE_BELOW = 128;
5524
+ var CHAR_SHRINK = 0.12;
5525
+ var CHAR_CURL = 6;
5526
+ var FRONT_GRADIENT = 16 * 16;
5527
+ var WET_MASS = 2;
5528
+ var DamageCoupling = class {
5529
+ sim;
5530
+ source = null;
5531
+ version = -1;
5532
+ size = 0;
5533
+ /** Each particle's texel, laid out for `size`. */
5534
+ particleTexel = new Int32Array(0);
5535
+ /** Gone, per particle, as of the last read. */
5536
+ gone;
5537
+ /** The gone particles with live paper beside them. See {@link follow}. */
5538
+ followers = [];
5539
+ /**
5540
+ * Per bend spring, how squarely it lies ACROSS the burn front, 0..1 — and
5541
+ * remembered once it has been.
5542
+ *
5543
+ * A burnt edge rolls about an axis along the front, like a scroll; it does
5544
+ * not dish into a bowl. Measured with the curl in every bend spring alike: a
5545
+ * burnt band settled into a saddle, its corners forward and its middle BACK
5546
+ * (+0.07, −0.06), because a surface cannot curve two ways at once without
5547
+ * stretching and paper does not stretch. Curled across the front only, the
5548
+ * same band rolled toward the viewer along its whole edge (+0.13).
5549
+ *
5550
+ * "Across the front" is the direction the char is changing fastest in, at
5551
+ * the moment the front passes. It has to be remembered: once the front has
5552
+ * gone by, the char behind it is uniform and says nothing about direction,
5553
+ * but the paper stays rolled the way it rolled.
5554
+ */
5555
+ curlWeight;
5556
+ constructor(sim) {
5557
+ this.sim = sim;
5558
+ this.gone = new Uint8Array(sim.count);
5559
+ this.curlWeight = new Float32Array(sim.constraintCount);
5560
+ }
5561
+ /**
5562
+ * Bring the sim's levers up to date with `source`. Call before stepping.
5563
+ *
5564
+ * Returns whether anything was read. Free when the source has not changed,
5565
+ * and a null source hands every lever back at its default.
5566
+ */
5567
+ update(source) {
5568
+ const next = source ?? null;
5569
+ if (next === this.source && (next === null || next.version === this.version)) return false;
5570
+ if (next !== this.source) this.curlWeight.fill(0);
5571
+ this.source = next;
5572
+ if (!next) {
5573
+ this.reset();
5574
+ return true;
5575
+ }
5576
+ if (next.size !== this.size) this.layout(next.size);
5577
+ this.version = next.version;
5578
+ this.read(next.pixels);
5579
+ this.sim.wake();
5580
+ return true;
5581
+ }
5582
+ /**
5583
+ * Carry the gone particles at the edge of a hole along with the paper beside
5584
+ * them. Call after stepping.
5585
+ *
5586
+ * Each one goes where its live neighbours say it would be if the grid were
5587
+ * laid out flat around them — their positions plus its rest offset from
5588
+ * each, averaged. The triangles that straddle the burnt edge then keep
5589
+ * roughly their shape instead of stretching back to wherever the particle
5590
+ * was when it burnt. Only a band of a single particle's width, with live
5591
+ * paper pulling it both ways, is left averaging between them.
5592
+ */
5593
+ follow() {
5594
+ if (this.followers.length === 0) return;
5595
+ const { gone, sim } = this;
5596
+ const p = sim.positions;
5597
+ const { cols, rows } = sim;
5598
+ const cellX = cols > 1 ? sim.width / (cols - 1) : 0;
5599
+ const cellY = rows > 1 ? sim.height / (rows - 1) : 0;
5600
+ for (const g of this.followers) {
5601
+ const r = g / cols | 0;
5602
+ const c = g % cols;
5603
+ let sx = 0;
5604
+ let sy = 0;
5605
+ let sz = 0;
5606
+ let n = 0;
5607
+ for (let dr = -1; dr <= 1; dr++) {
5608
+ const nr = r + dr;
5609
+ if (nr < 0 || nr >= rows) continue;
5610
+ for (let dc = -1; dc <= 1; dc++) {
5611
+ const nc = c + dc;
5612
+ if (dr === 0 && dc === 0 || nc < 0 || nc >= cols) continue;
5613
+ const j = nr * cols + nc;
5614
+ if (gone[j]) continue;
5615
+ const j3 = j * 3;
5616
+ sx += p[j3] - dc * cellX;
5617
+ sy += p[j3 + 1] + dr * cellY;
5618
+ sz += p[j3 + 2];
5619
+ n++;
5620
+ }
5621
+ }
5622
+ if (n === 0) continue;
5623
+ const g3 = g * 3;
5624
+ p[g3] = sx / n;
5625
+ p[g3 + 1] = sy / n;
5626
+ p[g3 + 2] = sz / n;
5627
+ }
5628
+ }
5629
+ /** Map each particle to its texel. The damage grid's row 0 is v = 0, the sheet's BOTTOM. */
5630
+ layout(size) {
5631
+ const { cols, rows, count } = this.sim;
5632
+ const last = size - 1;
5633
+ this.size = size;
5634
+ this.particleTexel = new Int32Array(count);
5635
+ for (let r = 0; r < rows; r++) {
5636
+ for (let c = 0; c < cols; c++) {
5637
+ const u = cols > 1 ? c / (cols - 1) : 0.5;
5638
+ const v = rows > 1 ? 1 - r / (rows - 1) : 0.5;
5639
+ this.particleTexel[r * cols + c] = Math.round(v * last) * size + Math.round(u * last);
5640
+ }
5641
+ }
5642
+ }
5643
+ read(pixels) {
5644
+ const { sim, gone, particleTexel, curlWeight, size } = this;
5645
+ const {
5646
+ invMass,
5647
+ restLength,
5648
+ naturalLength,
5649
+ restBend,
5650
+ broken,
5651
+ constraintA,
5652
+ constraintB,
5653
+ constraintKind,
5654
+ constraintMiddle
5655
+ } = sim;
5656
+ for (let i = 0; i < sim.count; i++) {
5657
+ const t = particleTexel[i] * 4;
5658
+ const isGone = pixels[t + PRESENCE] < GONE_BELOW;
5659
+ gone[i] = isGone ? 1 : 0;
5660
+ invMass[i] = isGone ? 0 : 1 / (1 + WET_MASS * pixels[t + SATURATION] / 255);
5661
+ }
5662
+ for (let k = 0; k < sim.constraintCount; k++) {
5663
+ const a = constraintA[k];
5664
+ const b = constraintB[k];
5665
+ const m = constraintMiddle[k];
5666
+ if (gone[a] || gone[b] || m >= 0 && gone[m]) {
5667
+ broken[k] = 1;
5668
+ continue;
5669
+ }
5670
+ broken[k] = 0;
5671
+ let charSum = pixels[particleTexel[a] * 4 + CHAR] + pixels[particleTexel[b] * 4 + CHAR];
5672
+ let samples = 2;
5673
+ if (m >= 0) {
5674
+ charSum += pixels[particleTexel[m] * 4 + CHAR];
5675
+ samples = 3;
5676
+ }
5677
+ const char = charSum / (samples * 255);
5678
+ restLength[k] = naturalLength[k] * (1 - CHAR_SHRINK * char);
5679
+ if (constraintKind[k] === 2) {
5680
+ const t = particleTexel[m];
5681
+ const x = t % size;
5682
+ const y = t / size | 0;
5683
+ const gx = pixels[(y * size + Math.min(size - 1, x + 2)) * 4 + CHAR] - pixels[(y * size + Math.max(0, x - 2)) * 4 + CHAR];
5684
+ const gy = pixels[(Math.min(size - 1, y + 2) * size + x) * 4 + CHAR] - pixels[(Math.max(0, y - 2) * size + x) * 4 + CHAR];
5685
+ const steep = gx * gx + gy * gy;
5686
+ if (steep > FRONT_GRADIENT) {
5687
+ const across = (b - a === 2 ? gx * gx : gy * gy) / steep;
5688
+ if (across > curlWeight[k]) curlWeight[k] = across;
5689
+ }
5690
+ const span = naturalLength[k];
5691
+ restBend[k] = CHAR_CURL * char * curlWeight[k] * span * span / 8;
5692
+ }
5693
+ }
5694
+ const followers = [];
5695
+ const { cols, rows } = sim;
5696
+ for (let i = 0; i < sim.count; i++) {
5697
+ if (!gone[i]) continue;
5698
+ const r = i / cols | 0;
5699
+ const c = i % cols;
5700
+ let beside = false;
5701
+ for (let dr = -1; dr <= 1 && !beside; dr++) {
5702
+ const nr = r + dr;
5703
+ if (nr < 0 || nr >= rows) continue;
5704
+ for (let dc = -1; dc <= 1; dc++) {
5705
+ const nc = c + dc;
5706
+ if (nc < 0 || nc >= cols) continue;
5707
+ if (!gone[nr * cols + nc]) {
5708
+ beside = true;
5709
+ break;
5710
+ }
5711
+ }
5712
+ }
5713
+ if (beside) followers.push(i);
5714
+ }
5715
+ this.followers = followers;
5716
+ }
5717
+ reset() {
5718
+ const { sim } = this;
5719
+ sim.invMass.fill(1);
5720
+ sim.restBend.fill(0);
5721
+ sim.broken.fill(0);
5722
+ sim.restLength.set(sim.naturalLength);
5723
+ this.gone.fill(0);
5724
+ this.curlWeight.fill(0);
5725
+ this.followers = [];
5726
+ this.version = -1;
5727
+ sim.wake();
5728
+ }
5729
+ };
5730
+
5341
5731
  // src/surface/PaperMaterial.tsx
5342
- import * as THREE6 from "three";
5343
- import { useEffect as useEffect4, useMemo as useMemo2 } from "react";
5732
+ import * as THREE7 from "three";
5733
+ import { useEffect as useEffect5, useMemo as useMemo3 } from "react";
5734
+ import { useFrame as useFrame2 } from "@react-three/fiber";
5344
5735
  import CustomShaderMaterial from "three-custom-shader-material";
5345
5736
 
5346
5737
  // src/surface/compose.ts
@@ -5753,6 +6144,518 @@ void plPerforation(inout vec4 color) {
5753
6144
  }
5754
6145
  `
5755
6146
  );
6147
+ var DAMAGE_CHUNK = (
6148
+ /* glsl */
6149
+ `
6150
+ uniform sampler2D uDamage;
6151
+ uniform float uDamageDetail;
6152
+ // (edgeWave mm, edgeBite mm, sparkle, \u2013) \u2014 see DamageLook.
6153
+ uniform vec4 uLook3;
6154
+
6155
+ // The damage grid's texels sit ON the sheet \u2014 texel x at u = x / (N - 1), its
6156
+ // corners on the sheet's corners, which is how the field paints and how the
6157
+ // physics reads it. A texture puts texel x at (x + 0.5) / N. Sampled straight,
6158
+ // the picture of a burn sat up to half a texel off the burn the paper feels.
6159
+ vec2 plDamageUv(vec2 uv) {
6160
+ vec2 n = vec2(textureSize(uDamage, 0));
6161
+ // Texel space, on the field's convention: texel x at u = x / (N - 1).
6162
+ vec2 t = uv * (n - 1.0) + 0.5;
6163
+ // Quintic-smoothed texel coordinates.
6164
+ //
6165
+ // Bilinear filtering is continuous but its DERIVATIVE is not: the blend
6166
+ // weight is a straight line inside each texel and turns a corner at every
6167
+ // boundary. Magnify a 64 grid to fill a macro crop \u2014 one cell is 4.9 mm of
6168
+ // A4, about 58 px there \u2014 and those corners fall on a 58 px lattice, so
6169
+ // every ramp drawn from this field can crease along it.
6170
+ //
6171
+ // Also tried against the contour rings, and also not their cause (that was
6172
+ // the scorch fingers). Kept because it costs nothing and the crease is
6173
+ // real:
6174
+ // Bending the fractional part through a quintic (6t^5 - 15t^4 + 10t^3,
6175
+ // whose first and second derivatives vanish at both ends) makes the
6176
+ // interpolation C2 without a single extra tap: the hardware still does one
6177
+ // bilinear fetch, it is just asked for a smoothed position. The sample at a
6178
+ // texel's centre is unchanged, so the half-texel alignment this function
6179
+ // exists for still holds \u2014 and a field nothing has happened to reads 1
6180
+ // everywhere whatever the position, so the untouched-sheet identity is
6181
+ // untouched too.
6182
+ vec2 i = floor(t - 0.5) + 0.5;
6183
+ vec2 f = t - i;
6184
+ f = f * f * f * (f * (f * 6.0 - 15.0) + 10.0);
6185
+ return (i + f) / n;
6186
+ }
6187
+
6188
+ /**
6189
+ * The four channels here, frayed finer than the grid.
6190
+ *
6191
+ * One cell of the field is ~3.3 mm of A4, and a burnt edge is jagged at the
6192
+ * scale of a fibre (0.3\u20132 mm) \u2014 so the grid decides roughly WHERE and this
6193
+ * decides exactly where. Stretched along the fibre (the field's default grain
6194
+ * runs along u), so a tear follows the grain. Scaled by what is already
6195
+ * there, so pristine paper (char 0, presence 1) is untouched to the bit, and
6196
+ * skipped outright at detail 0: the zones still draw on every tier, only
6197
+ * without the finest octaves.
6198
+ */
6199
+ /**
6200
+ * Where on the grid this fragment reads its damage \u2014 warped.
6201
+ *
6202
+ * A burnt hole is never a circle: paper burns unevenly, and its edge wobbles
6203
+ * in long waves with small bites in and out. The field is too coarse to hold
6204
+ * that, so the READ is displaced instead: two octaves of noise in the sheet's
6205
+ * own space, a long wave of up to \xB16 mm and short bites of \xB12 mm. Every zone \u2014
6206
+ * the cut, the lip, the ember line, the char, the scorch \u2014 reads through the
6207
+ * same warp, so they all follow one uneven edge. Warping a field nothing has
6208
+ * happened to reads the same untouched value everywhere, so the identity
6209
+ * holds.
6210
+ */
6211
+ vec2 plDamageWarpAt(vec2 paperUv) {
6212
+ vec2 p = (paperUv - 0.5) * uSheetSize;
6213
+ const float mm = 1.0 / 210.0;
6214
+ vec2 wave = vec2(plFbm(p * 26.0 + 3.1), plFbm(p * 26.0 + 17.7)) - 0.5;
6215
+ vec2 bite = vec2(plNoise(p * 95.0 + 5.3), plNoise(p * 95.0 + 41.9)) - 0.5;
6216
+ vec2 shift = (wave * 2.0 * uLook3.x + bite * 2.0 * uLook3.y) * mm;
6217
+ return plDamageUv(paperUv + shift / uSheetSize);
6218
+ }
6219
+
6220
+ vec2 plDamageWarp() {
6221
+ return plDamageWarpAt(vPaperUv);
6222
+ }
6223
+
6224
+ /** Presence softened over five taps \u2014 see \`plDamageRead\`. */
6225
+ float plSoftPresence(vec2 uv) {
6226
+ vec2 texel = 0.75 / vec2(textureSize(uDamage, 0));
6227
+ return (2.0 * texture2D(uDamage, uv).a
6228
+ + texture2D(uDamage, uv + vec2(texel.x, 0.0)).a + texture2D(uDamage, uv - vec2(texel.x, 0.0)).a
6229
+ + texture2D(uDamage, uv + vec2(0.0, texel.y)).a + texture2D(uDamage, uv - vec2(0.0, texel.y)).a) / 6.0;
6230
+ }
6231
+
6232
+ /** The fray and the fibre at a point of the sheet (local units), scaled by detail. */
6233
+ vec2 plFray(vec2 p) {
6234
+ return vec2(
6235
+ (plFbm(vec2(p.x * 22.0, p.y * 48.0)) - 0.47) * uDamageDetail,
6236
+ (plNoise(vec2(p.x * 140.0, p.y * 420.0)) - 0.5) * uDamageDetail
6237
+ );
6238
+ }
6239
+
6240
+ /**
6241
+ * The presence the cut is drawn from, at any point of the paper: warped,
6242
+ * softened and frayed exactly as \`plDamageRead\` does it for this fragment.
6243
+ */
6244
+ float plDrawnPresenceAt(vec2 paperUv) {
6245
+ float a = plSoftPresence(plDamageWarpAt(paperUv));
6246
+ if (uDamageDetail > 0.0) {
6247
+ vec2 ff = plFray((paperUv - 0.5) * uSheetSize);
6248
+ a = clamp(a + (ff.x * 0.36 + ff.y * 0.05) * (1.0 - smoothstep(0.9, 1.0, a)), 0.0, 1.0);
6249
+ }
6250
+ return a;
6251
+ }
6252
+
6253
+ // The softened presence before the fray, from the last \`plDamageRead\`: what
6254
+ // the distance to the cut takes its slope from (see \`plDamage\`).
6255
+ float plDamageSmooth;
6256
+
6257
+ vec4 plDamageRead() {
6258
+ vec2 uv = plDamageWarp();
6259
+ vec4 d = texture2D(uDamage, uv);
6260
+ // The cut runs on a SOFTENED presence. Paper burns through within a single
6261
+ // texel now, so the raw field is hard-edged and its half-presence contour
6262
+ // is the grid's own staircase \u2014 the fray below can only move a line within
6263
+ // the band it is given, and a one-texel band is not enough to hide a grid.
6264
+ // Five taps round the contour; an untouched field is 1 everywhere and
6265
+ // stays exactly 1.
6266
+ d.a = plSoftPresence(uv);
6267
+ plDamageSmooth = d.a;
6268
+ if (uDamageDetail > 0.0) {
6269
+ vec2 ff = plFray(plLocal());
6270
+ float fray = ff.x;
6271
+ float fibre = ff.y;
6272
+ float edge = 1.0 - smoothstep(0.9, 1.0, d.a);
6273
+ d.r = clamp(d.r + fray * 0.3 * smoothstep(0.0, 0.15, d.r), 0.0, 1.0);
6274
+ d.a = clamp(d.a + (fray * 0.36 + fibre * 0.05) * edge, 0.0, 1.0);
6275
+ }
6276
+ return d;
6277
+ }
6278
+
6279
+ void plDamageCut(inout vec4 color, vec4 d) {
6280
+ // Missing paper: gone at half presence, on every stock alike.
6281
+ color.a *= step(0.5, d.a);
6282
+ }
6283
+ `
6284
+ );
6285
+ var DAMAGE_SHADE_CHUNK = (
6286
+ /* glsl */
6287
+ `
6288
+ uniform float uDamageTime;
6289
+ // The look, packed \u2014 see DamageLook for what each number means.
6290
+ uniform vec4 uLook0; // emberWidth mm, emberIntensity, emberCoverage, emberFlicker
6291
+ uniform vec4 uLook1; // emberGlow, lipWidth mm, lipBrightness, charWarmth
6292
+ uniform vec4 uLook2; // charCracks, scorchReach mm, scorchDarkness, fingers
6293
+ /** The light the burn gives off from the sheet itself: the ember line, and nothing else. */
6294
+ vec3 plDamageLight;
6295
+
6296
+ // World units per millimetre: a default sheet is one unit across, and A4 is 210 mm.
6297
+ const float PL_MM = 1.0 / 210.0;
6298
+
6299
+ vec3 plLinear(vec3 srgb) {
6300
+ return pow(srgb, vec3(2.2));
6301
+ }
6302
+
6303
+ /**
6304
+ * World up, as a direction in the sheet's UV, from how this fragment's UV and
6305
+ * view position change across the screen. Must be called in uniform control
6306
+ * flow \u2014 it is all derivatives.
6307
+ */
6308
+ vec2 plUpUv() {
6309
+ vec3 dpx = dFdx(-vViewPosition);
6310
+ vec3 dpy = dFdy(-vViewPosition);
6311
+ vec2 dux = dFdx(vPaperUv);
6312
+ vec2 duy = dFdy(vPaperUv);
6313
+ float det = dux.x * duy.y - dux.y * duy.x;
6314
+ if (abs(det) < 1e-14) return vec2(0.0, 1.0);
6315
+ vec3 dPdu = (dpx * duy.y - dpy * dux.y) / det;
6316
+ vec3 dPdv = (dpy * dux.x - dpx * duy.x) / det;
6317
+ vec3 up = (viewMatrix * vec4(0.0, 1.0, 0.0, 0.0)).xyz;
6318
+ vec2 g = vec2(dot(up, dPdu) / max(dot(dPdu, dPdu), 1e-12), dot(up, dPdv) / max(dot(dPdv, dPdv), 1e-12));
6319
+ float l = length(g);
6320
+ return l > 1e-6 ? g / l : vec2(0.0, 1.0);
6321
+ }
6322
+
6323
+ /** F1 and F2 of a cell noise \u2014 the crack network in char. */
6324
+ vec2 plVoronoi(vec2 p) {
6325
+ vec2 i = floor(p);
6326
+ vec2 f = fract(p);
6327
+ float f1 = 8.0;
6328
+ float f2 = 8.0;
6329
+ for (int y = -1; y <= 1; y++) {
6330
+ for (int x = -1; x <= 1; x++) {
6331
+ vec2 o = vec2(float(x), float(y));
6332
+ vec2 r = o + vec2(plHash(i + o), plHash(i + o + 17.31)) - f;
6333
+ float dd = dot(r, r);
6334
+ if (dd < f1) {
6335
+ f2 = f1;
6336
+ f1 = dd;
6337
+ } else if (dd < f2) {
6338
+ f2 = dd;
6339
+ }
6340
+ }
6341
+ }
6342
+ return sqrt(vec2(f1, f2));
6343
+ }
6344
+
6345
+ /** The scorch ramp, sampled off Scorch.png and Hero.png, as a tint over paper. 0 is the leading edge. */
6346
+ vec3 plScorchTint(float t) {
6347
+ vec3 paper = plLinear(vec3(0.90, 0.885, 0.855));
6348
+ vec3 c0 = plLinear(vec3(0.843, 0.741, 0.569)); // #D7BD91
6349
+ vec3 c1 = plLinear(vec3(0.745, 0.514, 0.263)); // #BE8343
6350
+ vec3 c2 = plLinear(vec3(0.631, 0.384, 0.180)); // #A1622E
6351
+ vec3 c3 = plLinear(vec3(0.541, 0.310, 0.141)); // #8A4F24
6352
+ vec3 c4 = plLinear(vec3(0.471, 0.275, 0.149)); // #784626
6353
+ vec3 c5 = plLinear(vec3(0.380, 0.216, 0.133)); // #613722
6354
+ vec3 c6 = plLinear(vec3(0.310, 0.200, 0.137)); // #4F3323
6355
+ float s = clamp(t, 0.0, 1.0) * 6.0;
6356
+ // Each stop is blended with a SMOOTH step, not a linear one.
6357
+ //
6358
+ // Seven colours joined by six straight segments is a ramp with a corner at
6359
+ // every joint: the colour is continuous there but its rate of change is
6360
+ // not, and the eye reads a discontinuous gradient as a faint line (Mach
6361
+ // banding). smoothstep's derivative is zero at both ends of a segment, so
6362
+ // consecutive segments meet with matching slope and there is no corner.
6363
+ //
6364
+ // Cheap insurance, not a bug fix \u2014 this was tried first against the macro
6365
+ // crops' contour rings and did NOT remove them; the cause turned out to be
6366
+ // the scorch fingers below, sampled in a frame that follows the contours.
6367
+ float k = smoothstep(0.0, 1.0, clamp(s, 0.0, 1.0));
6368
+ vec3 c = mix(c0, c1, k);
6369
+ c = mix(c, c2, smoothstep(0.0, 1.0, clamp(s - 1.0, 0.0, 1.0)));
6370
+ c = mix(c, c3, smoothstep(0.0, 1.0, clamp(s - 2.0, 0.0, 1.0)));
6371
+ c = mix(c, c4, smoothstep(0.0, 1.0, clamp(s - 3.0, 0.0, 1.0)));
6372
+ c = mix(c, c5, smoothstep(0.0, 1.0, clamp(s - 4.0, 0.0, 1.0)));
6373
+ c = mix(c, c6, smoothstep(0.0, 1.0, clamp(s - 5.0, 0.0, 1.0)));
6374
+ return min(c / paper, vec3(1.0));
6375
+ }
6376
+
6377
+ /** The ember ramp (\xA75 zone 3): off \u2192 #870E03 \u2192 #A12108 \u2192 #F77E14 \u2192 #FEFBE0, hottest last. */
6378
+ vec3 plEmberRamp(float t) {
6379
+ vec3 c0 = plLinear(vec3(0.529, 0.055, 0.012));
6380
+ vec3 c1 = plLinear(vec3(0.631, 0.129, 0.031));
6381
+ vec3 c2 = plLinear(vec3(0.969, 0.494, 0.078));
6382
+ vec3 c3 = plLinear(vec3(0.996, 0.984, 0.878));
6383
+ vec3 c = mix(c0, c1, smoothstep(0.0, 0.3, t));
6384
+ c = mix(c, c2, smoothstep(0.3, 0.7, t));
6385
+ return mix(c, c3, smoothstep(0.75, 1.0, t));
6386
+ }
6387
+
6388
+ void plDamage(inout vec4 color, inout float roughness) {
6389
+ plDamageLight = vec3(0.0);
6390
+ vec4 d = plDamageRead();
6391
+ vec2 uv = plDamageWarp();
6392
+ vec2 texel = 1.0 / vec2(textureSize(uDamage, 0));
6393
+
6394
+ // Everything measured off screen derivatives, up front: derivatives are
6395
+ // undefined past a branch that only some fragments of a quad take.
6396
+ vec2 upUv = plUpUv();
6397
+ vec4 e = texture2D(uDamage, uv + vec2(texel.x, 0.0));
6398
+ vec4 w = texture2D(uDamage, uv - vec2(texel.x, 0.0));
6399
+ vec4 n = texture2D(uDamage, uv + vec2(0.0, texel.y));
6400
+ vec4 s = texture2D(uDamage, uv - vec2(0.0, texel.y));
6401
+ // Per world unit: one texel step is 1 / (N - 1) of the sheet.
6402
+ vec2 perWorld = (vec2(textureSize(uDamage, 0)) - 1.0) / uSheetSize;
6403
+ vec2 gradP = vec2(e.a - w.a, n.a - s.a) * 0.5 * perWorld;
6404
+ vec2 gradC = vec2(e.r - w.r, n.r - s.r) * 0.5 * perWorld;
6405
+ // Millimetres from the cut, on the paper side \u2014 measured against the field
6406
+ // the cut is DRAWN from (softened, frayed, warped), through its own screen
6407
+ // derivatives. It used to divide by the slope of the raw texels, and since
6408
+ // paper burns through within a texel that slope is a cliff while the drawn
6409
+ // edge is spread over millimetres: a point well into the char computed as a
6410
+ // hair from the cut, and the ember line drew there \u2014 found by the gate.
6411
+ // The slope, though, is the SMOOTH presence's. The fray's fibre noise
6412
+ // changes pixel to pixel, and its slope is a cliff everywhere it touches:
6413
+ // divided by that, a point deep in the char came out a hair from the cut
6414
+ // and the ember glow drew there \u2014 the gate's stray pixels. The fray still
6415
+ // moves where the edge is; it just cannot fake how steep it is.
6416
+ float smoothA = plDamageSmooth;
6417
+ vec2 cutSlope = vec2(dFdx(smoothA), dFdy(smoothA));
6418
+ float pxFromCut = (d.a - 0.5) / max(length(cutSlope), 1e-4);
6419
+ // Millimetres per pixel ALONG the way to the cut, not across the screen's
6420
+ // x: a sheet leaning away is foreshortened vertically, so above and below a
6421
+ // hole a pixel covers more paper than beside it, and a scale taken from x
6422
+ // alone drew the ember zone too many pixels deep on the top and bottom rims.
6423
+ vec2 toCutPx = cutSlope / max(length(cutSlope), 1e-6);
6424
+ float mmPerPx = length(dFdx(-vViewPosition) * toCutPx.x + dFdy(-vViewPosition) * toCutPx.y) / PL_MM;
6425
+ float mm = pxFromCut * mmPerPx;
6426
+ // About a pixel of antialiasing, never more: where the warp makes the
6427
+ // estimate jump between neighbours, fwidth(mm) is millimetres wide, and the
6428
+ // ember's soft tail \u2014 faint, but multiplied by its light \u2014 drew stray glow
6429
+ // well out into the char.
6430
+ float aa = min(fwidth(mm), 1.5 * mmPerPx);
6431
+ // Scorch reaches ahead of the char, and further UP than anywhere else:
6432
+ // the char a centimetre below this point, and the char a few millimetres
6433
+ // around it, each lent to the scorch at a discount (\xA74.2, \xA74.5).
6434
+ vec2 mmUv = vec2(PL_MM) / uSheetSize;
6435
+ float charBelow = max(texture2D(uDamage, uv - upUv * uLook2.y * mmUv).r, texture2D(uDamage, uv - upUv * 0.5 * uLook2.y * mmUv).r);
6436
+ // A ring of eight, averaged: four diagonal maxima drew the grid back in as
6437
+ // squares wherever the scorch was magnified.
6438
+ float charWide = 0.0;
6439
+ for (int k = 0; k < 8; k++) {
6440
+ float a = float(k) * 0.7853982;
6441
+ charWide += texture2D(uDamage, uv + vec2(cos(a), sin(a)) * 7.0 * mmUv).r;
6442
+ }
6443
+ charWide /= 8.0;
6444
+ // Only paper that has a CUT beside it has an edge: a half-eaten cell out
6445
+ // in the middle of the sheet reads as "half a presence" too, and without
6446
+ // this the ember line and the ash lip drew on it \u2014 islands of glow on
6447
+ // paper, the painted glow by another route.
6448
+ // Eased in, not switched: a hard threshold on the neighbouring texels drew
6449
+ // the grid back into the ember glow as small squares.
6450
+ float cutNear = 1.0 - smoothstep(0.3, 0.62, min(min(e.a, w.a), min(n.a, s.a)));
6451
+ // And the cut has to really be there. \`mm\` extrapolates this pixel's
6452
+ // slope to half presence, and the uneven edge's warp bends the field inside
6453
+ // those few millimetres \u2014 measured: with the warp off the stray ember light
6454
+ // the gate finds all but vanishes. So step across the PAPER toward where the
6455
+ // cut should be, half a millimetre past it but never beyond the ember
6456
+ // zone's own outer edge (the lip and the widest bead \u2014 2.5 mm at the
6457
+ // spec's numbers, \xA75) plus that half, and read what the cut is drawn from.
6458
+ // Still paper: this is not an edge, however close the slope said it was.
6459
+ vec2 toCutPaper = -(dFdx(vPaperUv) * toCutPx.x + dFdy(vPaperUv) * toCutPx.y);
6460
+ float reachPx = min(max(mm, 0.0) + 0.5, uLook1.y + uLook0.x + 0.5) / max(mmPerPx, 1e-6);
6461
+ float pastA = plDrawnPresenceAt(vPaperUv + toCutPaper * reachPx);
6462
+ // And a hole, not a pinhole: the fray opens specks of void finer than a
6463
+ // pixel in part-burnt paper, which the check above would take for the
6464
+ // edge. The void has to go on for another half millimetre.
6465
+ float beyondA = plDrawnPresenceAt(vPaperUv + toCutPaper * (reachPx + 0.5 / max(mmPerPx, 1e-6)));
6466
+ cutNear *= (1.0 - smoothstep(0.42, 0.55, pastA)) * (1.0 - smoothstep(0.42, 0.55, beyondA));
6467
+
6468
+ // An untouched texel is left exactly as it was \u2014 the identity the whole
6469
+ // seam is built on. Nothing here has happened to this paper.
6470
+ if (d.r <= 0.0 && d.g <= 0.0 && d.a >= 1.0 && charBelow <= 0.0 && charWide <= 0.0 && e.a >= 1.0 && w.a >= 1.0 && n.a >= 1.0 && s.a >= 1.0) {
6471
+ return;
6472
+ }
6473
+
6474
+ vec2 p = plLocal();
6475
+
6476
+ // Wet paper is darker and smoother: water fills the gaps between fibres
6477
+ // that scatter light, which is both effects from one cause.
6478
+ color.rgb *= 1.0 - 0.38 * d.g;
6479
+ roughness *= 1.0 - 0.45 * d.g;
6480
+
6481
+ // \u2500\u2500 Scorch \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
6482
+ // Fingers: irregular teeth ALONG the scorch front, plus the grain's own
6483
+ // streaks. Measured along the front's tangent so they point outward.
6484
+ vec2 tangentC = length(gradC) > 1e-4 ? normalize(vec2(-gradC.y, gradC.x)) : vec2(1.0, 0.0);
6485
+ float along = dot(p, tangentC) / (4.0 * PL_MM);
6486
+ // Only where there really IS a front.
6487
+ //
6488
+ // This noise is sampled in a frame built from the char gradient, so that
6489
+ // its teeth run across the front wherever the front happens to point. That
6490
+ // works at the front and is meaningless behind it: a few millimetres into
6491
+ // the char the gradient is almost flat, its direction is whatever the
6492
+ // eight-bit field rounded to, and the frame turns with position \u2014 which
6493
+ // makes the noise a function OF THE CONTOURS rather than of the sheet. It
6494
+ // drew closed loops following the edge, nested a cell apart: the
6495
+ // "concentric contour lines, like a topographic map or tree rings" the
6496
+ // review found at macro range and left unexplained. Turning this one term
6497
+ // off removes them completely and changes nothing else.
6498
+ //
6499
+ // Measured on the scripted burn's peak: the gradient runs to about 19 at
6500
+ // the front and under 1 a few millimetres behind it.
6501
+ float frontness = smoothstep(1.0, 6.0, length(gradC));
6502
+ float fingers = (plFbm(vec2(along, dot(p, vec2(tangentC.y, -tangentC.x)) / (12.0 * PL_MM))) - 0.5) * 0.28 * frontness
6503
+ + (plFbm(vec2(p.x * 9.0, p.y * 30.0)) - 0.5) * 0.08;
6504
+ fingers *= uLook2.w;
6505
+ // Fingers on the borrowed scorch too, so the outer edge is irregular
6506
+ // teeth rather than a smooth offset of the char.
6507
+ float reach = 0.75 + fingers * 4.0;
6508
+ // The scorch reads a softened char \u2014 its own texel and the four around it
6509
+ // \u2014 so a bilinear 64\xB2 field magnified shows no contours or corners. The
6510
+ // char zone keeps the sharp one: its edge is meant to be crisp.
6511
+ float soft = (2.0 * d.r + e.r + w.r + n.r + s.r) / 6.0;
6512
+ float self_ = clamp(mix(d.r, soft, 0.65) + fingers * smoothstep(0.0, 0.08, d.r), 0.0, 1.0);
6513
+ float c = 1.0 - (1.0 - self_) * (1.0 - clamp(0.62 * charBelow * reach, 0.0, 1.0)) * (1.0 - clamp(0.4 * charWide * reach, 0.0, 1.0));
6514
+ // Fibre-scale grain in the scorch, so a smooth field magnified never shows
6515
+ // its contours \u2014 real scorch is fibrous (Scorch.png), not banded.
6516
+ c = clamp(c + ((plNoise(p * 300.0) - 0.5) * 0.07 + (plNoise(p * 90.0) - 0.5) * 0.06) * smoothstep(0.0, 0.1, c), 0.0, 1.0);
6517
+ // A dither of about one and a half of the damage texture's own levels.
6518
+ //
6519
+ // The field arrives as eight bits a channel, so char comes in steps of
6520
+ // 1/255 \u2014 and the leading edge below spans 0.03 of it, which is seven and a
6521
+ // half steps. The fibre grain above would have hidden them, but it is
6522
+ // multiplied by smoothstep(0, 0.1, c), which is approximately zero exactly
6523
+ // where the leading edge lives. This is not: it applies everywhere, and
6524
+ // being finer than a level it can only ever move a value into the
6525
+ // neighbouring one.
6526
+ c = clamp(c + (plNoise(p * 900.0) - 0.5) * (1.5 / 255.0), 0.0, 1.0);
6527
+ // A steep leading edge \u2014 paper to straw in a sliver \u2014 then the long ramp.
6528
+ float lead = smoothstep(0.02, 0.05, c);
6529
+ float ramp = smoothstep(0.05, 0.7, c);
6530
+ color.rgb *= mix(vec3(1.0), pow(plScorchTint(ramp), vec3(uLook2.z)), lead);
6531
+
6532
+ // \u2500\u2500 Char \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
6533
+ // A wide, smooth hand-over from scorch to char \u2014 the gradient is the point.
6534
+ float charZone = smoothstep(0.5, 0.9, c);
6535
+ vec2 cells = plVoronoi(p / (3.6 * PL_MM));
6536
+ // Sparse: a real crack network is broken, not a tiled floor \u2014 most cell
6537
+ // borders never split.
6538
+ float crack = (1.0 - smoothstep(0.008, 0.03, cells.y - cells.x)) * smoothstep(0.45, 0.65, plNoise(p / (7.0 * PL_MM)));
6539
+ float plate = plHash(floor(p / (3.6 * PL_MM)) + 3.7);
6540
+ // Dark orange where the char meets the scorch, deep brown toward the cut \u2014
6541
+ // warm all the way, and fading smoothly into the scorch's lighter browns
6542
+ // rather than stopping at a grey band.
6543
+ // Warm (dark orange \u2192 deep brown) or, turned down, the sampled greys.
6544
+ vec3 body = mix(plLinear(vec3(0.255, 0.239, 0.227)), plLinear(vec3(0.165, 0.086, 0.043)), uLook1.w);
6545
+ vec3 under = mix(plLinear(vec3(0.275, 0.169, 0.090)), plLinear(vec3(0.42, 0.18, 0.06)), uLook1.w);
6546
+ vec3 cracks = plLinear(vec3(0.039, 0.024, 0.016)); // #0A0604
6547
+ vec3 charColor = mix(under, body, smoothstep(0.66, 1.0, c));
6548
+ // Mottled at the plate scale and finer, and darker than the sampled body
6549
+ // under a bright key \u2014 the reference's char reads near black in the frame.
6550
+ charColor *= (0.75 + 0.35 * plate) * (0.8 + 0.4 * plNoise(p * 260.0));
6551
+ charColor = mix(charColor, cracks, crack * uLook2.x);
6552
+ color.rgb = mix(color.rgb, charColor, charZone);
6553
+ roughness = mix(roughness, 0.6, charZone);
6554
+ // Blistered, and each plate tilted its own way, so a raking light breaks
6555
+ // the char into plates rather than lighting it as one sheet of black.
6556
+ plHeight += charZone * ((plNoise(p * 380.0) - 0.5) * 0.00008 + (plate - 0.5) * 0.00018 - crack * 0.00016);
6557
+
6558
+ // \u2500\u2500 Ash lip \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
6559
+ // Inside the spec's zones (\xA75): the lip 0.5\u20131.5 mm, then the ember line
6560
+ // 0.3\u20131 mm \u2014 2.5 mm from the cut at most, which the gate holds it to.
6561
+ // Along the rim. Both the lip and the ember line below are laid out on this,
6562
+ // and unlike the scorch fingers above it is safe to sample noise in: it is
6563
+ // used ONLY within a couple of millimetres of the cut, where the presence
6564
+ // gradient is the actual edge and points somewhere real. The fingers'
6565
+ // mistake was carrying the same idea out into flat char, where the gradient
6566
+ // is rounding error and a noise sampled in its frame draws the contours.
6567
+ vec2 tangentP = length(gradP) > 1e-4 ? normalize(vec2(-gradP.y, gradP.x)) : vec2(1.0, 0.0);
6568
+
6569
+ float lipWidth = mix(0.4, max(uLook1.y, 0.41), plFbm(p / (2.5 * PL_MM)));
6570
+ // Broken, not a hem. Ash flakes off a cooling edge in pieces, so the lip
6571
+ // comes and goes along the rim and wanders in width where it is there. It
6572
+ // used to be a continuous band of even width with a highlight down the
6573
+ // middle of it, which read as a neon tube round the hole \u2014 the single most
6574
+ // synthetic thing at wide view.
6575
+ float lipAlong = dot(p, tangentP) / (3.2 * PL_MM);
6576
+ float lipBreak = smoothstep(0.34, 0.56, plFbm(vec2(lipAlong, 0.9)));
6577
+ float lip = (1.0 - smoothstep(lipWidth - aa, lipWidth + aa, mm)) * step(d.a, 0.999) * cutNear * lipBreak;
6578
+ vec3 ash = plLinear(mix(vec3(0.525, 0.498, 0.490), vec3(0.741, 0.725, 0.725), plNoise(p * 520.0) * 0.6 + plNoise(p * 90.0) * 0.4)) * uLook1.z;
6579
+ color.rgb = mix(color.rgb, ash, lip);
6580
+ roughness = mix(roughness, 0.99, lip);
6581
+ // Barely raised. THIS is where the highlight came from: a 0.7 mm ridge
6582
+ // running round the hole is a cylinder, and a cylinder under a key light
6583
+ // has a specular line down it however rough the surface is. Ash is a
6584
+ // fragile crust a fraction of a millimetre proud of the paper, not a bead
6585
+ // of solder \u2014 and the shape of it belongs to the broken edge above, not to
6586
+ // a smooth ramp.
6587
+ plHeight += lip * (1.0 - mm / max(lipWidth, 0.1)) * 0.00018;
6588
+
6589
+ // \u2500\u2500 The ember line \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
6590
+ // Beads along the cut: position along the edge, crawling on the burn's
6591
+ // clock, with a flicker of their own. Only where it is hot \u2014 and as the
6592
+ // heat goes the threshold rises, so the line breaks into fewer, dimmer
6593
+ // beads and they go out one by one.
6594
+ float s1 = dot(p, tangentP) / (2.4 * PL_MM) - uDamageTime * 0.55 * uLook0.w;
6595
+ float bead = plNoise(vec2(s1, 0.37)) * 0.7 + plNoise(vec2(s1 * 2.3, 5.1)) * 0.3;
6596
+ float flicker = 0.62 + 0.38 * plNoise(vec2(floor(s1) * 7.13, uDamageTime * 9.0 * uLook0.w));
6597
+ // Down to a smoulder's last flicker: a spent edge keeps a little heat in
6598
+ // the texture (\`Afterglow\`), and a bead should glow dim red on it rather
6599
+ // than vanish while the edge still has something to give.
6600
+ // Heat softened like the cut (its texel and the four around it), so the
6601
+ // beads and the crimson glow follow the burn rather than the grid's cells.
6602
+ float heatSoft = (2.0 * d.b + e.b + w.b + n.b + s.b) / 6.0;
6603
+ float hot = smoothstep(0.012, 0.55, heatSoft);
6604
+ // Coverage slides the threshold: more of the edge lit, or less.
6605
+ float shift = (uLook0.z - 0.5) * 0.5;
6606
+ float lit = smoothstep(mix(0.82, 0.45, hot) - shift, mix(0.9, 0.58, hot) - shift, bead);
6607
+ // Each bead tapers at its ends \u2014 a bead, not a dash cut from a strip.
6608
+ float width = max(mix(0.4, max(uLook0.x, 0.41), plNoise(vec2(s1 * 0.7, 9.0))) * (0.35 + 0.65 * lit), 1.4 * aa);
6609
+ float band = smoothstep(lipWidth - aa, lipWidth + aa, mm) * (1.0 - smoothstep(lipWidth + width - aa, lipWidth + width + aa, mm)) * cutNear;
6610
+ // Mostly orange: a bead is red at its ends and yellow-white only at its
6611
+ // hottest core, which is what Ember_line.png shows. Mapped low and pushed
6612
+ // high by a power, so white is the exception rather than the line.
6613
+ // Orange through, yellow-white at the hottest cores. Dim red alone reads
6614
+ // salmon once the curve desaturates it, so the ramp starts at orange and
6615
+ // only the cooling (low heat) falls back into red.
6616
+ float t = hot * flicker * (0.45 + 0.55 * lit);
6617
+ color.rgb = mix(color.rgb, plLinear(vec3(0.12, 0.04, 0.02)), band * lit);
6618
+ // Saturated orange at orange brightness; only the hottest cores are pushed
6619
+ // high enough for the curve to roll them to white.
6620
+ plDamageLight = plEmberRamp(t) * mix(0.8, 6.0, t * t * t * t) * band * lit * hot * uLook0.y;
6621
+
6622
+ // The rest of a real burning edge (Ember_line.png): a dim crimson glow in
6623
+ // the char beside the beads \u2014 patches of it, drifting \u2014 and specks of
6624
+ // glowing fibre that come and go. Both stay under the bloom threshold, so
6625
+ // they read as heat in the char rather than as light; both only where the
6626
+ // edge is hot, and inside the zone the beads are held to \u2014 the lip plus
6627
+ // the widest bead, measured from the cut \u2014 so the glow never reaches out
6628
+ // over paper the gate says heat must not light.
6629
+ float zoneEnd = uLook1.y + uLook0.x;
6630
+ float seam = smoothstep(lipWidth - aa, lipWidth + aa, mm) * (1.0 - smoothstep(zoneEnd - 0.6, zoneEnd, mm)) * cutNear;
6631
+ float patch_ = smoothstep(0.35, 0.72, plFbm(p / (4.0 * PL_MM) + vec2(uDamageTime * 0.15 * uLook0.w, 0.0)));
6632
+ plDamageLight += vec3(0.55, 0.06, 0.01) * seam * patch_ * hot * uLook1.x;
6633
+ float speck = step(0.985, plHash(floor(p / (0.35 * PL_MM)) + floor(uDamageTime * 8.0 * uLook0.w)));
6634
+ plDamageLight += plEmberRamp(0.9) * 1.6 * speck * seam * hot * uLook3.z;
6635
+
6636
+ plDamageCut(color, d);
6637
+ }
6638
+ `
6639
+ );
6640
+ function carveDepthHelpers(helpers) {
6641
+ const darken = "uniform float uBackDarken;\n";
6642
+ const relief = helpers.indexOf("/**\n * The paper's relief");
6643
+ const bell = helpers.lastIndexOf("/**", helpers.indexOf("float plBell("));
6644
+ if (!helpers.includes(darken) || relief < 0 || bell <= relief) {
6645
+ throw new Error("compose: HELPERS changed shape \u2014 update carveDepthHelpers");
6646
+ }
6647
+ return (helpers.slice(0, relief) + helpers.slice(bell)).replace(darken, "");
6648
+ }
6649
+ var DEPTH_HELPERS = carveDepthHelpers(HELPERS);
6650
+ var DEPTH_VERTEX = (
6651
+ /* glsl */
6652
+ `
6653
+ varying vec2 vPaperUv;
6654
+ void main() {
6655
+ vPaperUv = uv;
6656
+ }
6657
+ `
6658
+ );
5756
6659
  var AGING_CHUNK = (
5757
6660
  /* glsl */
5758
6661
  `
@@ -5836,7 +6739,55 @@ function composeSurface(surface, stock, thickness, maps = { hasFrontMap: false,
5836
6739
  calls.push("plAging(csm_DiffuseColor);");
5837
6740
  uniforms.uAgingAmount = { value: aging };
5838
6741
  }
5839
- const relief = grain !== void 0 || creases.length > 0;
6742
+ if (maps.hasDamage) {
6743
+ chunks.push(DAMAGE_CHUNK, DAMAGE_SHADE_CHUNK);
6744
+ calls.push("plDamage(csm_DiffuseColor, csm_Roughness);");
6745
+ uniforms.uDamage = { value: null };
6746
+ uniforms.uDamageDetail = { value: 1 };
6747
+ uniforms.uDamageTime = { value: 0 };
6748
+ const look = DAMAGE_LOOK_DEFAULTS;
6749
+ uniforms.uLook0 = {
6750
+ value: new THREE5.Vector4(look.emberWidth, look.emberIntensity, look.emberCoverage, look.emberFlicker)
6751
+ };
6752
+ uniforms.uLook1 = {
6753
+ value: new THREE5.Vector4(look.emberGlow, look.lipWidth, look.lipBrightness, look.charWarmth)
6754
+ };
6755
+ uniforms.uLook2 = {
6756
+ value: new THREE5.Vector4(look.charCracks, look.scorchReach, look.scorchDarkness, look.fingers)
6757
+ };
6758
+ uniforms.uLook3 = { value: new THREE5.Vector4(look.edgeWave, look.edgeBite, look.sparkle, 0) };
6759
+ }
6760
+ const cutting = [];
6761
+ const cuts = [];
6762
+ if (deckle) {
6763
+ cutting.push(DECKLE_CHUNK);
6764
+ cuts.push("plDeckle(color);");
6765
+ }
6766
+ if (perforation) {
6767
+ cutting.push(PERFORATION_CHUNK);
6768
+ cuts.push("plPerforation(color);");
6769
+ }
6770
+ if (maps.hasDamage) {
6771
+ cutting.push(DAMAGE_CHUNK);
6772
+ cuts.push("plDamageCut(color, plDamageRead());");
6773
+ }
6774
+ const depth = cutting.length > 0 ? {
6775
+ vertexShader: DEPTH_VERTEX,
6776
+ fragmentShader: (
6777
+ /* glsl */
6778
+ `
6779
+ ${DEPTH_HELPERS}
6780
+ ${cutting.join("\n")}
6781
+ void main() {
6782
+ vec4 color = vec4(1.0);
6783
+ float roughness = 1.0;
6784
+ ${cuts.join("\n ")}
6785
+ if (color.a < 0.5) discard;
6786
+ }
6787
+ `
6788
+ )
6789
+ } : null;
6790
+ const relief = grain !== void 0 || creases.length > 0 || Boolean(maps.hasDamage);
5840
6791
  const frontExpr = maps.hasFrontMap ? "texture2D(uFrontMap, vPaperUv).rgb" : "uStockColor";
5841
6792
  const backBaseExpr = stock.adhesive ? "vec3(0.965, 0.96, 0.945)" : maps.hasBackMap ? "texture2D(uBackMap, vec2(1.0 - vPaperUv.x, vPaperUv.y)).rgb" : "uStockColor";
5842
6793
  const fragmentShader = (
@@ -5865,6 +6816,7 @@ ${relief ? " // The relief every effect above described, spent once \u2014 see
5865
6816
  ${stock.adhesive ? "// Adhesive underside: higher specular than the printed face.\n if (!gl_FrontFacing) csm_Roughness = 0.18;" : ""}
5866
6817
  // What the key light pushes through the sheet, filtered by the ink on it.
5867
6818
  csm_Emissive = plTransmission(front);
6819
+ ${maps.hasDamage ? " // The ember line \u2014 the only light a burn gives off from the sheet itself.\n csm_Emissive += plDamageLight;" : ""}
5868
6820
  }
5869
6821
  `
5870
6822
  );
@@ -5876,11 +6828,14 @@ ${relief ? " // The relief every effect above described, spent once \u2014 see
5876
6828
  aging !== void 0 ? "a" : "",
5877
6829
  perforation ? "p" : "",
5878
6830
  stock.adhesive ? "A" : ""
5879
- ].join("")}:${maps.hasFrontMap ? "F" : ""}${maps.hasBackMap ? "B" : ""}`,
6831
+ ].join("")}:${maps.hasFrontMap ? "F" : ""}${maps.hasBackMap ? "B" : ""}${maps.hasDamage ? "D" : ""}`,
5880
6832
  vertexShader: VERTEX,
5881
6833
  fragmentShader,
5882
6834
  uniforms,
5883
- alphaTest: deckle || perforation ? 0.5 : 0
6835
+ // Anything that removes paper needs fragments discarded rather than
6836
+ // blended — a hole has to cut the depth buffer and the shadow too.
6837
+ alphaTest: deckle || perforation || maps.hasDamage ? 0.5 : 0,
6838
+ depth
5884
6839
  };
5885
6840
  }
5886
6841
  function pad(values, fill = 0) {
@@ -5889,8 +6844,43 @@ function pad(values, fill = 0) {
5889
6844
  return out;
5890
6845
  }
5891
6846
 
6847
+ // src/surface/useDamageTexture.ts
6848
+ import * as THREE6 from "three";
6849
+ import { useFrame } from "@react-three/fiber";
6850
+ import { useEffect as useEffect4, useMemo as useMemo2 } from "react";
6851
+ function useDamageTexture(source) {
6852
+ const texture = useMemo2(() => {
6853
+ if (!source) return null;
6854
+ const t = new THREE6.DataTexture(
6855
+ source.pixels,
6856
+ source.size,
6857
+ source.size,
6858
+ THREE6.RGBAFormat,
6859
+ THREE6.UnsignedByteType
6860
+ );
6861
+ t.magFilter = THREE6.LinearFilter;
6862
+ t.minFilter = THREE6.LinearFilter;
6863
+ t.wrapS = THREE6.ClampToEdgeWrapping;
6864
+ t.wrapT = THREE6.ClampToEdgeWrapping;
6865
+ t.generateMipmaps = false;
6866
+ t.colorSpace = THREE6.NoColorSpace;
6867
+ t.flipY = false;
6868
+ t.userData.version = source.version;
6869
+ t.needsUpdate = true;
6870
+ return t;
6871
+ }, [source]);
6872
+ useEffect4(() => () => texture?.dispose(), [texture]);
6873
+ useFrame(() => {
6874
+ if (!source || !texture) return;
6875
+ if (texture.userData.version === source.version) return;
6876
+ texture.userData.version = source.version;
6877
+ texture.needsUpdate = true;
6878
+ });
6879
+ return texture;
6880
+ }
6881
+
5892
6882
  // src/surface/PaperMaterial.tsx
5893
- import { jsx as jsx3 } from "react/jsx-runtime";
6883
+ import { Fragment, jsx as jsx3, jsxs } from "react/jsx-runtime";
5894
6884
  function PaperMaterial({
5895
6885
  stock,
5896
6886
  texture,
@@ -5899,26 +6889,31 @@ function PaperMaterial({
5899
6889
  thickness,
5900
6890
  sheet: sheet2,
5901
6891
  lighting = "studio",
5902
- creases
6892
+ creases,
6893
+ damage
5903
6894
  }) {
5904
6895
  const rig = useLightRig(lighting);
6896
+ const damageTexture = useDamageTexture(damage);
5905
6897
  const composed = composeSurface(
5906
6898
  surface,
5907
6899
  stock,
5908
6900
  thickness,
5909
6901
  {
5910
6902
  hasFrontMap: Boolean(texture),
5911
- hasBackMap: Boolean(backTexture)
6903
+ hasBackMap: Boolean(backTexture),
6904
+ hasDamage: Boolean(damageTexture)
5912
6905
  },
5913
6906
  sheet2,
5914
6907
  rig,
5915
6908
  creases ?? resolveCreases(surface, [], sheet2 ?? { width: 1, height: 1.4 })
5916
6909
  );
5917
- const bound = useMemo2(() => composed.uniforms, [composed.structureKey]);
5918
- useEffect4(() => {
6910
+ const bound = useMemo3(() => composed.uniforms, [composed.structureKey]);
6911
+ useEffect5(() => {
5919
6912
  for (const [key, uniform] of Object.entries(composed.uniforms)) {
5920
- if (!bound[key] || key === "uFrontMap" || key === "uBackMap") continue;
5921
- if (bound[key].value instanceof THREE6.Color && uniform.value instanceof THREE6.Color) {
6913
+ if (!bound[key] || key === "uFrontMap" || key === "uBackMap" || key === "uDamage" || key === "uDamageDetail" || key === "uDamageTime" || key.startsWith("uLook")) {
6914
+ continue;
6915
+ }
6916
+ if (bound[key].value instanceof THREE7.Color && uniform.value instanceof THREE7.Color) {
5922
6917
  ;
5923
6918
  bound[key].value.copy(uniform.value);
5924
6919
  } else {
@@ -5926,27 +6921,80 @@ function PaperMaterial({
5926
6921
  }
5927
6922
  }
5928
6923
  });
5929
- useEffect4(() => {
6924
+ useEffect5(() => {
5930
6925
  if (bound.uFrontMap) bound.uFrontMap.value = texture;
5931
6926
  if (bound.uBackMap) bound.uBackMap.value = backTexture ?? null;
5932
- }, [bound, texture, backTexture]);
5933
- return /* @__PURE__ */ jsx3(
5934
- CustomShaderMaterial,
5935
- {
5936
- baseMaterial: THREE6.MeshStandardMaterial,
5937
- vertexShader: composed.vertexShader,
5938
- fragmentShader: composed.fragmentShader,
5939
- uniforms: bound,
5940
- color: "#ffffff",
5941
- roughness: stock.roughness,
5942
- metalness: 0,
5943
- transparent: stock.opacity < 1,
5944
- opacity: stock.opacity,
5945
- alphaTest: composed.alphaTest,
5946
- side: THREE6.DoubleSide
5947
- },
5948
- composed.structureKey
5949
- );
6927
+ if (bound.uDamage) bound.uDamage.value = damageTexture;
6928
+ }, [bound, texture, backTexture, damageTexture]);
6929
+ useFrame2((state) => {
6930
+ if (!bound.uDamageDetail) return;
6931
+ if (bound.uDamageTime) {
6932
+ const time = damage?.time ?? state.clock.elapsedTime;
6933
+ bound.uDamageTime.value = Number.isFinite(time) ? time : 0;
6934
+ }
6935
+ if (bound.uLook0) {
6936
+ const look = { ...DAMAGE_LOOK_DEFAULTS, ...damage?.look };
6937
+ const v = (x, d2) => Number.isFinite(x) ? x : d2;
6938
+ const d = DAMAGE_LOOK_DEFAULTS;
6939
+ bound.uLook0.value.set(
6940
+ v(look.emberWidth, d.emberWidth),
6941
+ v(look.emberIntensity, d.emberIntensity),
6942
+ v(look.emberCoverage, d.emberCoverage),
6943
+ v(look.emberFlicker, d.emberFlicker)
6944
+ );
6945
+ bound.uLook1.value.set(
6946
+ v(look.emberGlow, d.emberGlow),
6947
+ v(look.lipWidth, d.lipWidth),
6948
+ v(look.lipBrightness, d.lipBrightness),
6949
+ v(look.charWarmth, d.charWarmth)
6950
+ );
6951
+ bound.uLook2.value.set(
6952
+ v(look.charCracks, d.charCracks),
6953
+ v(look.scorchReach, d.scorchReach),
6954
+ v(look.scorchDarkness, d.scorchDarkness),
6955
+ v(look.fingers, d.fingers)
6956
+ );
6957
+ bound.uLook3.value.set(
6958
+ v(look.edgeWave, d.edgeWave),
6959
+ v(look.edgeBite, d.edgeBite),
6960
+ v(look.sparkle, d.sparkle),
6961
+ 0
6962
+ );
6963
+ }
6964
+ const detail = damage?.detail ?? 1;
6965
+ bound.uDamageDetail.value = Number.isFinite(detail) ? Math.min(1, Math.max(0, detail)) : 1;
6966
+ });
6967
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
6968
+ /* @__PURE__ */ jsx3(
6969
+ CustomShaderMaterial,
6970
+ {
6971
+ baseMaterial: THREE7.MeshStandardMaterial,
6972
+ vertexShader: composed.vertexShader,
6973
+ fragmentShader: composed.fragmentShader,
6974
+ uniforms: bound,
6975
+ color: "#ffffff",
6976
+ roughness: stock.roughness,
6977
+ metalness: 0,
6978
+ transparent: stock.opacity < 1,
6979
+ opacity: stock.opacity,
6980
+ alphaTest: composed.alphaTest,
6981
+ side: THREE7.DoubleSide
6982
+ },
6983
+ composed.structureKey
6984
+ ),
6985
+ composed.depth ? /* @__PURE__ */ jsx3(
6986
+ CustomShaderMaterial,
6987
+ {
6988
+ attach: "customDepthMaterial",
6989
+ baseMaterial: THREE7.MeshDepthMaterial,
6990
+ vertexShader: composed.depth.vertexShader,
6991
+ fragmentShader: composed.depth.fragmentShader,
6992
+ uniforms: bound,
6993
+ depthPacking: THREE7.RGBADepthPacking
6994
+ },
6995
+ `${composed.structureKey}:depth`
6996
+ ) : null
6997
+ ] });
5950
6998
  }
5951
6999
 
5952
7000
  // src/motion/onTwos.ts
@@ -5960,7 +7008,7 @@ function quantizeProgress(p, duration, fps = ON_TWOS_FPS) {
5960
7008
  }
5961
7009
 
5962
7010
  // src/PaperMesh.tsx
5963
- import { jsx as jsx4, jsxs } from "react/jsx-runtime";
7011
+ import { jsx as jsx4, jsxs as jsxs2 } from "react/jsx-runtime";
5964
7012
  function configInputs(props) {
5965
7013
  return [
5966
7014
  props.preset ?? null,
@@ -5978,7 +7026,7 @@ function configInputs(props) {
5978
7026
  }
5979
7027
  function useResolvedConfig(props) {
5980
7028
  const inputs = useStable(configInputs(props));
5981
- return useMemo3(() => resolveConfig(props), [inputs]);
7029
+ return useMemo4(() => resolveConfig(props), [inputs]);
5982
7030
  }
5983
7031
  function resolveConfig(props) {
5984
7032
  const base = props.preset ? typeof props.preset === "string" ? getPreset(props.preset) : parsePreset(props.preset) : paperConfigSchema.parse({});
@@ -5997,12 +7045,12 @@ function resolveConfig(props) {
5997
7045
  }
5998
7046
  var CLOTH_MAX_SEGMENTS = 28;
5999
7047
  var PROGRESS_SAMPLES = [0, 0.25, 0.5, 0.75, 1];
6000
- var dragPlane = new THREE7.Plane();
6001
- var dragPoint = new THREE7.Vector3();
6002
- var planeNormal = new THREE7.Vector3();
6003
- var anchorScratch = new THREE7.Vector3();
6004
- var worldScratch = new THREE7.Vector3();
6005
- var quatScratch = new THREE7.Quaternion();
7048
+ var dragPlane = new THREE8.Plane();
7049
+ var dragPoint = new THREE8.Vector3();
7050
+ var planeNormal = new THREE8.Vector3();
7051
+ var anchorScratch = new THREE8.Vector3();
7052
+ var worldScratch = new THREE8.Vector3();
7053
+ var quatScratch = new THREE8.Quaternion();
6006
7054
  var PaperMesh = forwardRef(function PaperMesh2(props, ref) {
6007
7055
  const resolved = useResolvedConfig(props);
6008
7056
  const reduced = usePrefersReducedMotion(props.reducedMotion);
@@ -6031,7 +7079,7 @@ var PaperMesh = forwardRef(function PaperMesh2(props, ref) {
6031
7079
  const draggingRef = useRef3(null);
6032
7080
  const configRef = useRef3(config);
6033
7081
  configRef.current = config;
6034
- const baseRotation = useMemo3(() => {
7082
+ const baseRotation = useMemo4(() => {
6035
7083
  const [rx, ry, rz] = props.rotation ?? [0, 0, 0];
6036
7084
  return [rx, ry + config.scene.turn * Math.PI / 180, rz];
6037
7085
  }, [props.rotation, config.scene.turn]);
@@ -6046,16 +7094,16 @@ var PaperMesh = forwardRef(function PaperMesh2(props, ref) {
6046
7094
  const creasesRef = useRef3(null);
6047
7095
  creasesRef.current ??= new CreaseTracker(config.memory.creases);
6048
7096
  const creases = creasesRef.current;
6049
- useEffect5(() => {
7097
+ useEffect6(() => {
6050
7098
  if (!draggingRef.current && !playingRef.current) overridesRef.current = {};
6051
7099
  creases.reset();
6052
7100
  dirtyRef.current = true;
6053
7101
  }, [behaviorKey, deformersKey, sheetKey, physicsKey]);
6054
- useEffect5(() => {
7102
+ useEffect6(() => {
6055
7103
  creases.adopt(configRef.current.memory.creases);
6056
7104
  dirtyRef.current = true;
6057
7105
  }, [creaseKey]);
6058
- const { minSegments, autoSegments, animatedStack } = useMemo3(() => {
7106
+ const { minSegments, autoSegments, animatedStack } = useMemo4(() => {
6059
7107
  const cfg = configRef.current;
6060
7108
  const probe2 = withMemory(buildStack(cfg, {}), cfg);
6061
7109
  if (!probe2) {
@@ -6084,16 +7132,16 @@ var PaperMesh = forwardRef(function PaperMesh2(props, ref) {
6084
7132
  animatedStack: animated
6085
7133
  };
6086
7134
  }, [behaviorKey, deformersKey, physicsKey, memoryKey]);
6087
- const geometry = useMemo3(() => {
7135
+ const geometry = useMemo4(() => {
6088
7136
  if (isStrip) {
6089
7137
  const strip = configRef.current.physics;
6090
7138
  const nodes = stripNodeCount(config.sheet.height, strip.perforation);
6091
- return new THREE7.PlaneGeometry(config.sheet.width, config.sheet.height, 1, nodes - 1);
7139
+ return new THREE8.PlaneGeometry(config.sheet.width, config.sheet.height, 1, nodes - 1);
6092
7140
  }
6093
7141
  if (!isCloth) return createSheetGeometry(config.sheet, minSegments, autoSegments);
6094
7142
  const [sx, sy] = resolveSegments(config.sheet, minSegments);
6095
7143
  const capped = Math.min(Math.max(sx, sy), CLOTH_MAX_SEGMENTS);
6096
- return new THREE7.PlaneGeometry(config.sheet.width, config.sheet.height, capped, capped);
7144
+ return new THREE8.PlaneGeometry(config.sheet.width, config.sheet.height, capped, capped);
6097
7145
  }, [
6098
7146
  sheetKey,
6099
7147
  minSegments,
@@ -6102,12 +7150,12 @@ var PaperMesh = forwardRef(function PaperMesh2(props, ref) {
6102
7150
  isStrip,
6103
7151
  isStrip ? config.physics.perforation : 0
6104
7152
  ]);
6105
- useEffect5(() => () => geometry.dispose(), [geometry]);
6106
- const basePositions = useMemo3(
7153
+ useEffect6(() => () => geometry.dispose(), [geometry]);
7154
+ const basePositions = useMemo4(
6107
7155
  () => Float32Array.from(geometry.attributes.position.array),
6108
7156
  [geometry]
6109
7157
  );
6110
- const stripSim = useMemo3(() => {
7158
+ const stripSim = useMemo4(() => {
6111
7159
  if (!isStrip) return null;
6112
7160
  const strip = configRef.current.physics;
6113
7161
  return new StripSim(config.sheet.height, config.sheet.width, {
@@ -6125,7 +7173,7 @@ var PaperMesh = forwardRef(function PaperMesh2(props, ref) {
6125
7173
  });
6126
7174
  }, [geometry, isStrip]);
6127
7175
  const lastSimRef = useRef3(null);
6128
- const sim = useMemo3(() => {
7176
+ const sim = useMemo4(() => {
6129
7177
  if (!isCloth) return null;
6130
7178
  const cloth = configRef.current.physics;
6131
7179
  const cols = geometry.parameters.widthSegments + 1;
@@ -6139,9 +7187,10 @@ var PaperMesh = forwardRef(function PaperMesh2(props, ref) {
6139
7187
  next.adopt(lastSimRef.current);
6140
7188
  return next;
6141
7189
  }, [geometry, isCloth, isCloth ? config.physics.pins : ""]);
6142
- useEffect5(() => {
7190
+ useEffect6(() => {
6143
7191
  lastSimRef.current = sim;
6144
7192
  }, [sim]);
7193
+ const coupling = useMemo4(() => sim ? new DamageCoupling(sim) : null, [sim]);
6145
7194
  const stock = getStock(config.stock);
6146
7195
  const texture = useContentTexture(config.content, config.sheet, stock);
6147
7196
  const backTexture = useContentTexture(config.content.back, config.sheet, stock);
@@ -6183,7 +7232,7 @@ var PaperMesh = forwardRef(function PaperMesh2(props, ref) {
6183
7232
  playingRef.current = false;
6184
7233
  tweenRef.current?.pause();
6185
7234
  };
6186
- useEffect5(() => {
7235
+ useEffect6(() => {
6187
7236
  if (props.autoplay && !reduced) play();
6188
7237
  return () => {
6189
7238
  tweenRef.current?.kill();
@@ -6226,7 +7275,22 @@ var PaperMesh = forwardRef(function PaperMesh2(props, ref) {
6226
7275
  const index = id ? handles.findIndex((h) => h.id === id) : 0;
6227
7276
  const mesh = handleRefs.current[index];
6228
7277
  if (!mesh) return null;
6229
- return mesh.getWorldPosition(target ?? new THREE7.Vector3());
7278
+ return mesh.getWorldPosition(target ?? new THREE8.Vector3());
7279
+ },
7280
+ surfacePoint(u, v, target) {
7281
+ const mesh = meshRef.current;
7282
+ if (!mesh) return null;
7283
+ const { widthSegments, heightSegments } = geometry.parameters;
7284
+ const out = surfacePointAt(
7285
+ geometry.attributes.position.array,
7286
+ widthSegments + 1,
7287
+ heightSegments + 1,
7288
+ u,
7289
+ v,
7290
+ target ?? new THREE8.Vector3()
7291
+ );
7292
+ mesh.updateWorldMatrix(true, false);
7293
+ return out.applyMatrix4(mesh.matrixWorld);
6230
7294
  },
6231
7295
  get state() {
6232
7296
  return machineState;
@@ -6236,12 +7300,12 @@ var PaperMesh = forwardRef(function PaperMesh2(props, ref) {
6236
7300
  placeProgrammatic: () => machineRef.current?.placeProgrammatic() ?? false,
6237
7301
  returnProgrammatic: () => machineRef.current?.returnProgrammatic() ?? false
6238
7302
  }));
6239
- const shadedCreases = useMemo3(
7303
+ const shadedCreases = useMemo4(
6240
7304
  () => resolveCreases(config.surface, config.memory.creases, config.sheet),
6241
7305
  [config.surface, config.memory.creases, config.sheet]
6242
7306
  );
6243
7307
  const idlePose = useRef3({ position: [0, 0, 0], rotation: [0, 0, 0] });
6244
- useFrame(({ clock }, delta) => {
7308
+ useFrame3(({ clock }, delta) => {
6245
7309
  const cfg = liveConfig();
6246
7310
  const now = reduced ? 0 : cfg.onTwos ? quantizeTime(clock.elapsedTime) : clock.elapsedTime;
6247
7311
  const hasBehaviorTransform = Boolean(behavior?.transform && cfg.behavior);
@@ -6325,8 +7389,10 @@ var PaperMesh = forwardRef(function PaperMesh2(props, ref) {
6325
7389
  stiffness: cloth.stiffness,
6326
7390
  floor: cloth.floor
6327
7391
  });
7392
+ coupling?.update(props.damage);
6328
7393
  sim.step(delta);
6329
7394
  const moved = !sim.asleep;
7395
+ if (moved) coupling?.follow();
6330
7396
  if (!applyShape(sim.positions, moved) && moved) {
6331
7397
  const position = geometry.attributes.position;
6332
7398
  position.array.set(sim.positions);
@@ -6358,8 +7424,8 @@ var PaperMesh = forwardRef(function PaperMesh2(props, ref) {
6358
7424
  const p = overridesRef.current[behavior.progressParam];
6359
7425
  if (typeof p === "number") props.onProgress?.(p);
6360
7426
  };
6361
- const grabAnchor = useRef3(new THREE7.Vector3());
6362
- const grabOffset = useRef3(new THREE7.Vector3());
7427
+ const grabAnchor = useRef3(new THREE8.Vector3());
7428
+ const grabOffset = useRef3(new THREE8.Vector3());
6363
7429
  const clothDown = (e) => {
6364
7430
  if (!isCloth || !sim || !props.interactive || !groupRef.current) return;
6365
7431
  e.stopPropagation();
@@ -6422,7 +7488,7 @@ var PaperMesh = forwardRef(function PaperMesh2(props, ref) {
6422
7488
  e.target.releasePointerCapture(e.pointerId);
6423
7489
  };
6424
7490
  const sendState = (event) => machineRef.current?.send(event);
6425
- return /* @__PURE__ */ jsxs("group", { ref: groupRef, position: props.position, rotation: baseRotation, children: [
7491
+ return /* @__PURE__ */ jsxs2("group", { ref: groupRef, position: props.position, rotation: baseRotation, children: [
6426
7492
  /* @__PURE__ */ jsx4(
6427
7493
  "mesh",
6428
7494
  {
@@ -6454,7 +7520,8 @@ var PaperMesh = forwardRef(function PaperMesh2(props, ref) {
6454
7520
  thickness: config.sheet.thickness,
6455
7521
  sheet: config.sheet,
6456
7522
  lighting: config.scene.lighting,
6457
- creases: shadedCreases
7523
+ creases: shadedCreases,
7524
+ damage: props.damage
6458
7525
  }
6459
7526
  )
6460
7527
  }
@@ -6463,7 +7530,7 @@ var PaperMesh = forwardRef(function PaperMesh2(props, ref) {
6463
7530
  // is no deformer stack for a handle to drive. Harmless as `!isCloth`
6464
7531
  // only because the schema makes a sim and a behavior exclusive — the
6465
7532
  // intent is what is written here.
6466
- !simKind && behavior?.handles?.map((h, i) => /* @__PURE__ */ jsxs(
7533
+ !simKind && behavior?.handles?.map((h, i) => /* @__PURE__ */ jsxs2(
6467
7534
  "mesh",
6468
7535
  {
6469
7536
  userData: { paperlabChrome: true },
@@ -6536,13 +7603,13 @@ function buildStack(config, overrides, behavior, t = 0) {
6536
7603
  }
6537
7604
 
6538
7605
  // src/scene/PaperLighting.tsx
6539
- import * as THREE9 from "three";
6540
- import { useEffect as useEffect6, useMemo as useMemo4, useRef as useRef4 } from "react";
6541
- import { useFrame as useFrame2, useThree as useThree2 } from "@react-three/fiber";
7606
+ import * as THREE10 from "three";
7607
+ import { useEffect as useEffect7, useMemo as useMemo5, useRef as useRef4 } from "react";
7608
+ import { useFrame as useFrame4, useThree as useThree2 } from "@react-three/fiber";
6542
7609
  import { ContactShadows } from "@react-three/drei";
6543
7610
 
6544
7611
  // src/scene/environment.ts
6545
- import * as THREE8 from "three";
7612
+ import * as THREE9 from "three";
6546
7613
 
6547
7614
  // src/scene/color.ts
6548
7615
  var probe;
@@ -6591,7 +7658,7 @@ function drawSky(ctx, preset) {
6591
7658
  const x = skyU(angles.azimuth) * WIDTH;
6592
7659
  const y = skyV(angles.elevation) * HEIGHT;
6593
7660
  const radius = WIDTH * 0.3;
6594
- const color = new THREE8.Color(preset.key.color);
7661
+ const color = new THREE9.Color(preset.key.color);
6595
7662
  const strength = Math.min(1, preset.key.intensity / 3);
6596
7663
  for (const offset of [-WIDTH, 0, WIDTH]) {
6597
7664
  const glow = ctx.createRadialGradient(x + offset, y, 0, x + offset, y, radius);
@@ -6610,14 +7677,14 @@ function makeSkyEquirect(preset) {
6610
7677
  canvas.height = HEIGHT;
6611
7678
  const ctx = canvas.getContext("2d");
6612
7679
  drawSky(ctx, preset);
6613
- const texture = new THREE8.CanvasTexture(canvas);
6614
- texture.mapping = THREE8.EquirectangularReflectionMapping;
6615
- texture.colorSpace = THREE8.SRGBColorSpace;
7680
+ const texture = new THREE9.CanvasTexture(canvas);
7681
+ texture.mapping = THREE9.EquirectangularReflectionMapping;
7682
+ texture.colorSpace = THREE9.SRGBColorSpace;
6616
7683
  return texture;
6617
7684
  }
6618
7685
  function buildEnvironment(renderer, preset) {
6619
7686
  const equirect = makeSkyEquirect(preset);
6620
- const pmrem = new THREE8.PMREMGenerator(renderer);
7687
+ const pmrem = new THREE9.PMREMGenerator(renderer);
6621
7688
  const target = pmrem.fromEquirectangular(equirect);
6622
7689
  pmrem.dispose();
6623
7690
  equirect.dispose();
@@ -6628,11 +7695,11 @@ function buildEnvironment(renderer, preset) {
6628
7695
  }
6629
7696
 
6630
7697
  // src/scene/PaperLighting.tsx
6631
- import { Fragment, jsx as jsx5, jsxs as jsxs2 } from "react/jsx-runtime";
7698
+ import { Fragment as Fragment2, jsx as jsx5, jsxs as jsxs3 } from "react/jsx-runtime";
6632
7699
  var toneMappings = {
6633
- agx: THREE9.AgXToneMapping,
6634
- neutral: THREE9.NeutralToneMapping,
6635
- filmic: THREE9.ACESFilmicToneMapping
7700
+ agx: THREE10.AgXToneMapping,
7701
+ neutral: THREE10.NeutralToneMapping,
7702
+ filmic: THREE10.ACESFilmicToneMapping
6636
7703
  };
6637
7704
  function mulberry322(seed) {
6638
7705
  let a = seed;
@@ -6689,15 +7756,15 @@ function makeGoboTexture(kind) {
6689
7756
  }
6690
7757
  }
6691
7758
  }
6692
- const texture = new THREE9.CanvasTexture(canvas);
6693
- texture.wrapS = texture.wrapT = THREE9.RepeatWrapping;
7759
+ const texture = new THREE10.CanvasTexture(canvas);
7760
+ texture.wrapS = texture.wrapT = THREE10.RepeatWrapping;
6694
7761
  return texture;
6695
7762
  }
6696
7763
  function StudioLight({ rig }) {
6697
7764
  const gl = useThree2((s) => s.gl);
6698
7765
  const scene = useThree2((s) => s.scene);
6699
7766
  const sky = `${rig.sky.zenith}|${rig.sky.horizon}|${rig.sky.ground}|${rig.key.color}|${rig.key.intensity}|${rig.key.position.join()}`;
6700
- useEffect6(() => {
7767
+ useEffect7(() => {
6701
7768
  const environment = buildEnvironment(gl, rig);
6702
7769
  const previous = scene.environment;
6703
7770
  scene.environment = environment.texture;
@@ -6706,7 +7773,7 @@ function StudioLight({ rig }) {
6706
7773
  environment.dispose();
6707
7774
  };
6708
7775
  }, [gl, scene, sky]);
6709
- useEffect6(() => {
7776
+ useEffect7(() => {
6710
7777
  const previous = scene.environmentIntensity;
6711
7778
  scene.environmentIntensity = rig.studio;
6712
7779
  return () => {
@@ -6727,13 +7794,13 @@ function PaperLighting({
6727
7794
  contactShadow = true,
6728
7795
  environment = true
6729
7796
  }) {
6730
- const p = useMemo4(() => rig ?? resolveLighting(preset, light), [rig, preset, light]);
7797
+ const p = useMemo5(() => rig ?? resolveLighting(preset, light), [rig, preset, light]);
6731
7798
  const mapSize = shadowMapSize ?? p.shadow.mapSize;
6732
7799
  const castShadow = mapSize > 0;
6733
7800
  const reduced = usePrefersReducedMotion(reducedMotion);
6734
7801
  const gl = useThree2((s) => s.gl);
6735
7802
  const scene = useThree2((s) => s.scene);
6736
- useEffect6(() => {
7803
+ useEffect7(() => {
6737
7804
  const previousExposure = gl.toneMappingExposure;
6738
7805
  const previousFilm = gl.toneMapping;
6739
7806
  gl.toneMappingExposure = p.exposure;
@@ -6743,23 +7810,23 @@ function PaperLighting({
6743
7810
  gl.toneMapping = previousFilm;
6744
7811
  };
6745
7812
  }, [gl, p.exposure, p.film]);
6746
- useEffect6(() => {
7813
+ useEffect7(() => {
6747
7814
  if (!p.fog) return;
6748
7815
  const previous = scene.fog;
6749
- scene.fog = new THREE9.Fog(p.fog.color, p.fog.near, p.fog.far);
7816
+ scene.fog = new THREE10.Fog(p.fog.color, p.fog.near, p.fog.far);
6750
7817
  return () => {
6751
7818
  scene.fog = previous;
6752
7819
  };
6753
7820
  }, [scene, p.fog]);
6754
- const goboMap = useMemo4(() => p.gobo ? makeGoboTexture(p.gobo.kind) : null, [p.gobo?.kind]);
6755
- useEffect6(() => () => goboMap?.dispose(), [goboMap]);
7821
+ const goboMap = useMemo5(() => p.gobo ? makeGoboTexture(p.gobo.kind) : null, [p.gobo?.kind]);
7822
+ useEffect7(() => () => goboMap?.dispose(), [goboMap]);
6756
7823
  const driftRef = useRef4(0);
6757
- useFrame2((_, delta) => {
7824
+ useFrame4((_, delta) => {
6758
7825
  if (!goboMap || !p.gobo || reduced) return;
6759
7826
  driftRef.current += delta * p.gobo.drift;
6760
7827
  goboMap.offset.set(driftRef.current, driftRef.current * 0.6);
6761
7828
  });
6762
- return /* @__PURE__ */ jsxs2(Fragment, { children: [
7829
+ return /* @__PURE__ */ jsxs3(Fragment2, { children: [
6763
7830
  p.studio > 0 && (environment ? /* @__PURE__ */ jsx5(StudioLight, { rig: p }) : (
6764
7831
  // The studio light degrades rather than disappearing. A hemisphere
6765
7832
  // is the cheap half of what the room does — light from above, a
@@ -6817,15 +7884,15 @@ function PaperLighting({
6817
7884
  }
6818
7885
 
6819
7886
  // src/scene/backdrop.tsx
6820
- import * as THREE10 from "three";
6821
- import { useEffect as useEffect7, useState as useState4 } from "react";
7887
+ import * as THREE11 from "three";
7888
+ import { useEffect as useEffect8, useState as useState4 } from "react";
6822
7889
  import { useThree as useThree3 } from "@react-three/fiber";
6823
7890
  function PaperBackdrop({ backdrop }) {
6824
7891
  const scene = useThree3((s) => s.scene);
6825
7892
  const size = useThree3((s) => s.size);
6826
7893
  const [image, setImage] = useState4(null);
6827
7894
  const src = backdrop?.image ?? "";
6828
- useEffect7(() => {
7895
+ useEffect8(() => {
6829
7896
  if (!src) {
6830
7897
  setImage(null);
6831
7898
  return;
@@ -6841,7 +7908,7 @@ function PaperBackdrop({ backdrop }) {
6841
7908
  };
6842
7909
  }, [src]);
6843
7910
  const key = JSON.stringify({ backdrop: backdrop ?? null, w: size.width, h: size.height, loaded: !!image });
6844
- useEffect7(() => {
7911
+ useEffect8(() => {
6845
7912
  if (!backdrop) return;
6846
7913
  const previous = scene.background;
6847
7914
  const texture = paintBackdrop(backdrop, size.width, size.height, image);
@@ -6877,15 +7944,15 @@ function paintBackdrop(backdrop, width, height, image) {
6877
7944
  ctx.globalAlpha = 1;
6878
7945
  }
6879
7946
  }
6880
- const texture = new THREE10.CanvasTexture(canvas);
6881
- texture.colorSpace = THREE10.SRGBColorSpace;
7947
+ const texture = new THREE11.CanvasTexture(canvas);
7948
+ texture.colorSpace = THREE11.SRGBColorSpace;
6882
7949
  return texture;
6883
7950
  }
6884
7951
 
6885
7952
  // src/field/dropZones.tsx
6886
- import * as THREE11 from "three";
6887
- import { createContext as createContext2, useContext as useContext2, useEffect as useEffect8, useMemo as useMemo5, useRef as useRef5, useSyncExternalStore } from "react";
6888
- import { jsx as jsx6, jsxs as jsxs3 } from "react/jsx-runtime";
7953
+ import * as THREE12 from "three";
7954
+ import { createContext as createContext2, useContext as useContext2, useEffect as useEffect9, useMemo as useMemo6, useRef as useRef5, useSyncExternalStore } from "react";
7955
+ import { jsx as jsx6, jsxs as jsxs4 } from "react/jsx-runtime";
6889
7956
  var DropZoneRegistry = class {
6890
7957
  zones = /* @__PURE__ */ new Map();
6891
7958
  hoveredId = null;
@@ -6942,10 +8009,10 @@ function DropZone(props) {
6942
8009
  const registry4 = useContext2(DropZoneContext);
6943
8010
  const { id, accept, bounds, highlight = "glow", onPlace } = props;
6944
8011
  const place = useRef5(onPlace);
6945
- useEffect8(() => {
8012
+ useEffect9(() => {
6946
8013
  place.current = onPlace;
6947
8014
  }, [onPlace]);
6948
- useEffect8(() => {
8015
+ useEffect9(() => {
6949
8016
  if (!registry4) return;
6950
8017
  return registry4.register({
6951
8018
  id,
@@ -6963,15 +8030,15 @@ function DropZoneVisual({ registry: registry4, config }) {
6963
8030
  const hovered = registry4.hovered === config.id;
6964
8031
  const style = config.highlight ?? "glow";
6965
8032
  const [w, h] = config.bounds.size;
6966
- const edges = useMemo5(() => {
6967
- const plane = new THREE11.PlaneGeometry(w, h);
6968
- const geo = new THREE11.EdgesGeometry(plane);
8033
+ const edges = useMemo6(() => {
8034
+ const plane = new THREE12.PlaneGeometry(w, h);
8035
+ const geo = new THREE12.EdgesGeometry(plane);
6969
8036
  plane.dispose();
6970
8037
  return geo;
6971
8038
  }, [w, h]);
6972
- useEffect8(() => () => edges.dispose(), [edges]);
6973
- return /* @__PURE__ */ jsxs3("group", { position: config.bounds.position, children: [
6974
- style !== "outline" && /* @__PURE__ */ jsxs3("mesh", { children: [
8039
+ useEffect9(() => () => edges.dispose(), [edges]);
8040
+ return /* @__PURE__ */ jsxs4("group", { position: config.bounds.position, children: [
8041
+ style !== "outline" && /* @__PURE__ */ jsxs4("mesh", { children: [
6975
8042
  /* @__PURE__ */ jsx6("planeGeometry", { args: [w, h] }),
6976
8043
  /* @__PURE__ */ jsx6(
6977
8044
  "meshBasicMaterial",
@@ -7572,17 +8639,17 @@ registerLayout(colonnade);
7572
8639
  registerLayout(sheet);
7573
8640
 
7574
8641
  // src/PaperField.tsx
7575
- import * as THREE16 from "three";
8642
+ import * as THREE17 from "three";
7576
8643
  import { gsap as gsap5 } from "gsap";
7577
- import { Canvas, useFrame as useFrame5, useThree as useThree6 } from "@react-three/fiber";
7578
- import { forwardRef as forwardRef2, useEffect as useEffect14, useMemo as useMemo9, useRef as useRef8 } from "react";
8644
+ import { Canvas, useFrame as useFrame7, useThree as useThree6 } from "@react-three/fiber";
8645
+ import { forwardRef as forwardRef2, useEffect as useEffect15, useMemo as useMemo10, useRef as useRef8 } from "react";
7579
8646
 
7580
8647
  // src/scene/release.tsx
7581
8648
  import { useThree as useThree4 } from "@react-three/fiber";
7582
- import { useEffect as useEffect9 } from "react";
8649
+ import { useEffect as useEffect10 } from "react";
7583
8650
  function ReleaseContextOnUnmount() {
7584
8651
  const gl = useThree4((s) => s.gl);
7585
- useEffect9(
8652
+ useEffect10(
7586
8653
  () => () => {
7587
8654
  gl.forceContextLoss();
7588
8655
  gl.dispose();
@@ -7638,15 +8705,15 @@ function resolveFieldSlotConfig(slot, fallback, index, layoutId, layoutOptions)
7638
8705
  }
7639
8706
 
7640
8707
  // src/field/fieldGroup.tsx
7641
- import * as THREE13 from "three";
8708
+ import * as THREE14 from "three";
7642
8709
  import { gsap as gsap3 } from "gsap";
7643
- import { useFrame as useFrame3 } from "@react-three/fiber";
7644
- import { useEffect as useEffect11, useMemo as useMemo6, useRef as useRef6 } from "react";
8710
+ import { useFrame as useFrame5 } from "@react-three/fiber";
8711
+ import { useEffect as useEffect12, useMemo as useMemo7, useRef as useRef6 } from "react";
7645
8712
  import CustomShaderMaterial2 from "three-custom-shader-material";
7646
8713
 
7647
8714
  // src/content/atlas.ts
7648
- import * as THREE12 from "three";
7649
- import { useEffect as useEffect10, useState as useState5 } from "react";
8715
+ import * as THREE13 from "three";
8716
+ import { useEffect as useEffect11, useState as useState5 } from "react";
7650
8717
  var MAX_ATLAS = 4096;
7651
8718
  function atlasGrid(count, aspect = 1) {
7652
8719
  const cols = Math.max(1, Math.min(count, Math.ceil(Math.sqrt(count * Math.max(aspect, 0.01)))));
@@ -7655,7 +8722,7 @@ function atlasGrid(count, aspect = 1) {
7655
8722
  function useContentAtlas(contents, sheet2, stock) {
7656
8723
  const [atlas, setAtlas] = useState5(null);
7657
8724
  const stableContents = useStable(contents);
7658
- useEffect10(() => {
8725
+ useEffect11(() => {
7659
8726
  let disposed = false;
7660
8727
  const aspect = sheet2.height / sheet2.width;
7661
8728
  const { cols, rows } = atlasGrid(contents.length, aspect);
@@ -7671,8 +8738,8 @@ function useContentAtlas(contents, sheet2, stock) {
7671
8738
  const ctx = canvas.getContext("2d");
7672
8739
  ctx.fillStyle = stock.color;
7673
8740
  ctx.fillRect(0, 0, canvas.width, canvas.height);
7674
- const texture = new THREE12.CanvasTexture(canvas);
7675
- texture.colorSpace = THREE12.SRGBColorSpace;
8741
+ const texture = new THREE13.CanvasTexture(canvas);
8742
+ texture.colorSpace = THREE13.SRGBColorSpace;
7676
8743
  texture.anisotropy = 4;
7677
8744
  setAtlas({ texture, cols, rows });
7678
8745
  const drawTile = (index, tile) => {
@@ -7841,7 +8908,7 @@ function fieldShapeStack(config, progress) {
7841
8908
 
7842
8909
  // src/field/fieldGroup.tsx
7843
8910
  import { jsx as jsx7 } from "react/jsx-runtime";
7844
- var scratchObj = new THREE13.Object3D();
8911
+ var scratchObj = new THREE14.Object3D();
7845
8912
  var scratchAero = { position: [0, 0, 0], rotation: [0, 0, 0] };
7846
8913
  var FIELD_SEGMENT_CAP = 48;
7847
8914
  var FIELD_AUTO_CEILING = 128;
@@ -7864,7 +8931,7 @@ function FieldGroup({
7864
8931
  behavior ? config.behavior[behavior.progressParam] : 0
7865
8932
  );
7866
8933
  const buildStackAt = (progress) => fieldShapeStack(config, progress);
7867
- const initialStack = useMemo6(
8934
+ const initialStack = useMemo7(
7868
8935
  () => buildStackAt(progressRef.current),
7869
8936
  [
7870
8937
  JSON.stringify(config.behavior ?? null),
@@ -7873,7 +8940,7 @@ function FieldGroup({
7873
8940
  ]
7874
8941
  );
7875
8942
  const structureKey = initialStack.map((i) => i.type).join("|");
7876
- const geometry = useMemo6(() => {
8943
+ const geometry = useMemo7(() => {
7877
8944
  const floor = stackMinSegments(initialStack, config.sheet);
7878
8945
  const want = [0, 0];
7879
8946
  for (const p of PROGRESS_SAMPLES2) {
@@ -7902,28 +8969,28 @@ function FieldGroup({
7902
8969
  atlasIdx[i] = i;
7903
8970
  phase[i] = indices[i] * 0.618034 % 1 * 4;
7904
8971
  }
7905
- geo.setAttribute("aAtlas", new THREE13.InstancedBufferAttribute(atlasIdx, 1));
7906
- geo.setAttribute("aPhase", new THREE13.InstancedBufferAttribute(phase, 1));
7907
- geo.setAttribute("aBias", new THREE13.InstancedBufferAttribute(bias, 1));
8972
+ geo.setAttribute("aAtlas", new THREE14.InstancedBufferAttribute(atlasIdx, 1));
8973
+ geo.setAttribute("aPhase", new THREE14.InstancedBufferAttribute(phase, 1));
8974
+ geo.setAttribute("aBias", new THREE14.InstancedBufferAttribute(bias, 1));
7908
8975
  return geo;
7909
8976
  }, [JSON.stringify(config.sheet), structureKey, count, autoCeiling]);
7910
- useEffect11(() => () => geometry.dispose(), [geometry]);
8977
+ useEffect12(() => () => geometry.dispose(), [geometry]);
7911
8978
  const atlas = useContentAtlas(contents, config.sheet, stock);
7912
- const shader = useMemo6(() => {
8979
+ const shader = useMemo7(() => {
7913
8980
  const composed = buildDisplacementGLSL(initialStack, config.sheet);
7914
8981
  const uniforms = {};
7915
8982
  for (const [name, value] of Object.entries(composed.uniforms)) {
7916
8983
  uniforms[name] = {
7917
- value: Array.isArray(value) && value.length === 2 ? new THREE13.Vector2(...value) : value
8984
+ value: Array.isArray(value) && value.length === 2 ? new THREE14.Vector2(...value) : value
7918
8985
  };
7919
8986
  }
7920
8987
  uniforms.uPlTime = { value: 0 };
7921
8988
  uniforms.uAtlas = { value: null };
7922
- uniforms.uAtlasGrid = { value: new THREE13.Vector2(1, 1) };
8989
+ uniforms.uAtlasGrid = { value: new THREE14.Vector2(1, 1) };
7923
8990
  uniforms.uBackDarken = {
7924
8991
  value: 1 - Math.min(0.45, 0.12 + config.sheet.thickness * 0.9) * stock.opacity
7925
8992
  };
7926
- uniforms.uStockColor = { value: new THREE13.Color(stock.color) };
8993
+ uniforms.uStockColor = { value: new THREE14.Color(stock.color) };
7927
8994
  uniforms.uShowThrough = { value: config.surface.showThrough ?? stock.showThrough };
7928
8995
  Object.assign(uniforms, translucencyUniforms(config.surface.translucency ?? stock.translucency, rig));
7929
8996
  return {
@@ -7938,19 +9005,19 @@ function FieldGroup({
7938
9005
  config.surface.showThrough,
7939
9006
  config.surface.translucency
7940
9007
  ]);
7941
- useEffect11(() => {
9008
+ useEffect12(() => {
7942
9009
  const values = translucencyValues(config.surface.translucency ?? stock.translucency, rig);
7943
9010
  shader.uniforms.uTranslucency.value = values.translucency;
7944
9011
  shader.uniforms.uBackLightDir.value.copy(values.direction);
7945
9012
  shader.uniforms.uBackLightColor.value.copy(values.color);
7946
9013
  shader.uniforms.uAmbientTransmission.value = values.ambient;
7947
9014
  }, [shader, rig, config.surface.translucency, stock.translucency]);
7948
- useEffect11(() => {
9015
+ useEffect12(() => {
7949
9016
  if (!atlas) return;
7950
9017
  shader.uniforms.uAtlas.value = atlas.texture;
7951
9018
  shader.uniforms.uAtlasGrid.value.set(atlas.cols, atlas.rows);
7952
9019
  }, [atlas, shader]);
7953
- useEffect11(() => {
9020
+ useEffect12(() => {
7954
9021
  if (!behavior || shared.reduced) return;
7955
9022
  const state = { p: progressRef.current };
7956
9023
  const tween = gsap3.to(state, {
@@ -7968,7 +9035,7 @@ function FieldGroup({
7968
9035
  };
7969
9036
  }, [structureKey, shared.reduced]);
7970
9037
  const meshRef = useRef6(null);
7971
- useFrame3(({ clock }) => {
9038
+ useFrame5(({ clock }) => {
7972
9039
  const mesh = meshRef.current;
7973
9040
  if (!mesh) return;
7974
9041
  const elapsed = clock.elapsedTime - Math.max(shared.mountTimeRef.current, 0);
@@ -7978,7 +9045,7 @@ function FieldGroup({
7978
9045
  for (const [name, value] of Object.entries(values)) {
7979
9046
  const uniform = shader.uniforms[name];
7980
9047
  if (!uniform) continue;
7981
- if (uniform.value instanceof THREE13.Vector2 && Array.isArray(value)) {
9048
+ if (uniform.value instanceof THREE14.Vector2 && Array.isArray(value)) {
7982
9049
  uniform.value.set(value[0], value[1]);
7983
9050
  } else {
7984
9051
  uniform.value = value;
@@ -8063,13 +9130,13 @@ function FieldGroup({
8063
9130
  children: /* @__PURE__ */ jsx7(
8064
9131
  CustomShaderMaterial2,
8065
9132
  {
8066
- baseMaterial: THREE13.MeshStandardMaterial,
9133
+ baseMaterial: THREE14.MeshStandardMaterial,
8067
9134
  vertexShader: shader.vertexShader,
8068
9135
  fragmentShader: shader.fragmentShader,
8069
9136
  uniforms: shader.uniforms,
8070
9137
  roughness: stock.roughness,
8071
9138
  metalness: 0,
8072
- side: THREE13.DoubleSide
9139
+ side: THREE14.DoubleSide
8073
9140
  },
8074
9141
  `${structureKey}:${count}`
8075
9142
  )
@@ -8115,8 +9182,8 @@ var easeOut = (t) => 1 - (1 - t) ** 3;
8115
9182
  var easeInOut = (t) => t < 0.5 ? 2 * t * t : 1 - (-2 * t + 2) ** 2 / 2;
8116
9183
 
8117
9184
  // src/field/backingSheet.tsx
8118
- import * as THREE14 from "three";
8119
- import { useEffect as useEffect12, useMemo as useMemo7 } from "react";
9185
+ import * as THREE15 from "three";
9186
+ import { useEffect as useEffect13, useMemo as useMemo8 } from "react";
8120
9187
 
8121
9188
  // src/content/backing.ts
8122
9189
  function silhouetteRects(o, count) {
@@ -8181,7 +9248,7 @@ function roundedRect(ctx, x, y, w, h, r) {
8181
9248
  }
8182
9249
 
8183
9250
  // src/field/backingSheet.tsx
8184
- import { jsx as jsx8, jsxs as jsxs4 } from "react/jsx-runtime";
9251
+ import { jsx as jsx8, jsxs as jsxs5 } from "react/jsx-runtime";
8185
9252
  var BACKING_TINT = "#f3ecdd";
8186
9253
  function BackingSheet({
8187
9254
  options,
@@ -8189,7 +9256,7 @@ function BackingSheet({
8189
9256
  removed
8190
9257
  }) {
8191
9258
  const { width, height } = sheetBackingSize(options);
8192
- const canvas = useMemo7(() => {
9259
+ const canvas = useMemo8(() => {
8193
9260
  if (typeof document === "undefined") return null;
8194
9261
  const c = document.createElement("canvas");
8195
9262
  const scale = Math.min(1024, Math.round(360 * Math.max(width, height))) / Math.max(width, height);
@@ -8197,26 +9264,26 @@ function BackingSheet({
8197
9264
  c.height = Math.max(2, Math.round(height * scale));
8198
9265
  return c;
8199
9266
  }, [width, height]);
8200
- const texture = useMemo7(() => canvas ? new THREE14.CanvasTexture(canvas) : null, [canvas]);
9267
+ const texture = useMemo8(() => canvas ? new THREE15.CanvasTexture(canvas) : null, [canvas]);
8201
9268
  const removedKey = [...removed].sort((a, b) => a - b).join(",");
8202
- useEffect12(() => {
9269
+ useEffect13(() => {
8203
9270
  if (!canvas || !texture) return;
8204
9271
  drawBacking(canvas, { options, count, tint: BACKING_TINT, removed });
8205
9272
  texture.needsUpdate = true;
8206
9273
  }, [canvas, texture, JSON.stringify(options), count, removedKey]);
8207
- useEffect12(() => () => texture?.dispose(), [texture]);
8208
- return /* @__PURE__ */ jsxs4("mesh", { receiveShadow: true, children: [
9274
+ useEffect13(() => () => texture?.dispose(), [texture]);
9275
+ return /* @__PURE__ */ jsxs5("mesh", { receiveShadow: true, children: [
8209
9276
  /* @__PURE__ */ jsx8("planeGeometry", { args: [width, height] }),
8210
9277
  /* @__PURE__ */ jsx8("meshStandardMaterial", { map: texture, color: "#ffffff", roughness: 0.92, metalness: 0 })
8211
9278
  ] });
8212
9279
  }
8213
9280
 
8214
9281
  // src/field/interactiveField.tsx
8215
- import * as THREE15 from "three";
9282
+ import * as THREE16 from "three";
8216
9283
  import { gsap as gsap4 } from "gsap";
8217
- import { useFrame as useFrame4, useThree as useThree5 } from "@react-three/fiber";
8218
- import { useContext as useContext3, useEffect as useEffect13, useMemo as useMemo8, useRef as useRef7, useState as useState6 } from "react";
8219
- import { jsx as jsx9, jsxs as jsxs5 } from "react/jsx-runtime";
9284
+ import { useFrame as useFrame6, useThree as useThree5 } from "@react-three/fiber";
9285
+ import { useContext as useContext3, useEffect as useEffect14, useMemo as useMemo9, useRef as useRef7, useState as useState6 } from "react";
9286
+ import { jsx as jsx9, jsxs as jsxs6 } from "react/jsx-runtime";
8220
9287
  var PICK_BEHAVIORS = /* @__PURE__ */ new Set(["peel", "carry"]);
8221
9288
  function InteractiveField(props) {
8222
9289
  const { papers, fallback, layoutId, layoutOptions, sheetOptions } = props;
@@ -8224,14 +9291,14 @@ function InteractiveField(props) {
8224
9291
  const layout = getLayout(layoutId);
8225
9292
  const reduced = usePrefersReducedMotion(props.reducedMotion);
8226
9293
  const contextRegistry = useContext3(DropZoneContext);
8227
- const registry4 = useMemo8(() => contextRegistry ?? new DropZoneRegistry(), [contextRegistry]);
9294
+ const registry4 = useMemo9(() => contextRegistry ?? new DropZoneRegistry(), [contextRegistry]);
8228
9295
  const camera = useThree5((s) => s.camera);
8229
9296
  const gl = useThree5((s) => s.gl);
8230
9297
  const controls = useThree5((s) => s.controls);
8231
9298
  const [removed, setRemoved] = useState6(EMPTY_SET);
8232
9299
  const [slotPatches, setSlotPatches] = useState6({});
8233
9300
  const [slotStates, setSlotStates] = useState6({});
8234
- const slotConfigs = useMemo8(
9301
+ const slotConfigs = useMemo9(
8235
9302
  () => papers.map((slot, i) => {
8236
9303
  const config = resolveFieldSlotConfig(slot, fallback, i, layoutId, layoutOptions);
8237
9304
  const patch = slotPatches[i];
@@ -8245,7 +9312,7 @@ function InteractiveField(props) {
8245
9312
  slotPatches
8246
9313
  ]
8247
9314
  );
8248
- const poses = useMemo8(
9315
+ const poses = useMemo9(
8249
9316
  () => slotConfigs.map((_, i) => layout.pose(i, total, layoutOptions, 0, props.sheet)),
8250
9317
  [layoutId, JSON.stringify(layoutOptions), total, props.sheet.width, props.sheet.height]
8251
9318
  );
@@ -8265,10 +9332,10 @@ function InteractiveField(props) {
8265
9332
  }
8266
9333
  props.onSlotStateChange?.(i, state);
8267
9334
  };
8268
- const raycaster = useMemo8(() => new THREE15.Raycaster(), []);
8269
- const planeScratch = useMemo8(() => new THREE15.Plane(new THREE15.Vector3(0, 0, 1), 0), []);
8270
- const pointScratch = useMemo8(() => new THREE15.Vector3(), []);
8271
- const ndcScratch = useMemo8(() => new THREE15.Vector2(), []);
9335
+ const raycaster = useMemo9(() => new THREE16.Raycaster(), []);
9336
+ const planeScratch = useMemo9(() => new THREE16.Plane(new THREE16.Vector3(0, 0, 1), 0), []);
9337
+ const pointScratch = useMemo9(() => new THREE16.Vector3(), []);
9338
+ const ndcScratch = useMemo9(() => new THREE16.Vector2(), []);
8272
9339
  const planePoint = (clientX, clientY, planeZ) => {
8273
9340
  const rect = gl.domElement.getBoundingClientRect();
8274
9341
  ndcScratch.set(
@@ -8436,7 +9503,7 @@ function InteractiveField(props) {
8436
9503
  if (zone) settleInto(carried, zone);
8437
9504
  else returnHome(carried);
8438
9505
  };
8439
- useEffect13(() => {
9506
+ useEffect14(() => {
8440
9507
  const move = (e) => onPointerMoveRef.current(e);
8441
9508
  const up = (e) => onPointerUpRef.current(e);
8442
9509
  window.addEventListener("pointermove", move);
@@ -8446,7 +9513,7 @@ function InteractiveField(props) {
8446
9513
  window.removeEventListener("pointerup", up);
8447
9514
  };
8448
9515
  }, []);
8449
- useFrame4((_, delta) => {
9516
+ useFrame6((_, delta) => {
8450
9517
  const carried = carriedRef.current;
8451
9518
  if (!carried || carried.settling) return;
8452
9519
  const group = groupRefs.current[carried.slot];
@@ -8472,8 +9539,8 @@ function InteractiveField(props) {
8472
9539
  group.position.z = carried.homePose.position[2] + 0.15;
8473
9540
  handle.set("drive", carryDrive(speed));
8474
9541
  const lag = 0.25;
8475
- group.rotation.y += (THREE15.MathUtils.clamp(-vx * lag, -0.6, 0.6) - group.rotation.y) * 0.12;
8476
- group.rotation.x += (THREE15.MathUtils.clamp(vy * lag * 0.7, -0.5, 0.5) - group.rotation.x) * 0.12;
9542
+ group.rotation.y += (THREE16.MathUtils.clamp(-vx * lag, -0.6, 0.6) - group.rotation.y) * 0.12;
9543
+ group.rotation.x += (THREE16.MathUtils.clamp(vy * lag * 0.7, -0.5, 0.5) - group.rotation.x) * 0.12;
8477
9544
  }
8478
9545
  const name = slotName(carried.slot);
8479
9546
  const zone = zonesLive().find(
@@ -8509,7 +9576,7 @@ function InteractiveField(props) {
8509
9576
  zoneIds: () => zonesLive().map((z25) => z25.id),
8510
9577
  slotState: (slot) => slotStates[slot] ?? "rest"
8511
9578
  };
8512
- useEffect13(() => {
9579
+ useEffect14(() => {
8513
9580
  const ref = props.a11yRef;
8514
9581
  if (!ref) return;
8515
9582
  ref.current = {
@@ -8523,7 +9590,7 @@ function InteractiveField(props) {
8523
9590
  ref.current = null;
8524
9591
  };
8525
9592
  }, [props.a11yRef]);
8526
- return /* @__PURE__ */ jsxs5("group", { children: [
9593
+ return /* @__PURE__ */ jsxs6("group", { children: [
8527
9594
  sheetOptions?.backing && /* @__PURE__ */ jsx9(BackingSheet, { options: sheetOptions, count: total, removed }),
8528
9595
  (props.zones ?? []).map((zone) => /* @__PURE__ */ jsx9(DropZoneVisual, { registry: registry4, config: zone }, zone.id)),
8529
9596
  slotConfigs.map((config, i) => {
@@ -8563,7 +9630,7 @@ function InteractiveField(props) {
8563
9630
 
8564
9631
  // src/field/keyboardMirror.tsx
8565
9632
  import { useState as useState7 } from "react";
8566
- import { jsx as jsx10, jsxs as jsxs6 } from "react/jsx-runtime";
9633
+ import { jsx as jsx10, jsxs as jsxs7 } from "react/jsx-runtime";
8567
9634
  function fieldKeyboardStep(carry2, slot, key, controller) {
8568
9635
  if (!carry2) {
8569
9636
  if (key === "Enter" || key === " ") {
@@ -8625,9 +9692,9 @@ function FieldKeyboardMirror({
8625
9692
  if (handled) e.preventDefault();
8626
9693
  if (carry2 !== carrying) setCarrying(carry2);
8627
9694
  };
8628
- return /* @__PURE__ */ jsxs6("fieldset", { style: mirrorHidden, children: [
9695
+ return /* @__PURE__ */ jsxs7("fieldset", { style: mirrorHidden, children: [
8629
9696
  /* @__PURE__ */ jsx10("legend", { children: "Interactive papers" }),
8630
- papers.map((slot, i) => /* @__PURE__ */ jsxs6(
9697
+ papers.map((slot, i) => /* @__PURE__ */ jsxs7(
8631
9698
  "button",
8632
9699
  {
8633
9700
  type: "button",
@@ -8636,7 +9703,7 @@ function FieldKeyboardMirror({
8636
9703
  "aria-pressed": carrying?.slot === i,
8637
9704
  children: [
8638
9705
  paperLabel(slot, i),
8639
- carrying?.slot === i && /* @__PURE__ */ jsxs6("span", { "aria-live": "polite", children: [
9706
+ carrying?.slot === i && /* @__PURE__ */ jsxs7("span", { "aria-live": "polite", children: [
8640
9707
  " ",
8641
9708
  "\u2014 carrying; zone ",
8642
9709
  controller.current?.zoneIds()[carrying.zoneIndex] ?? "none",
@@ -8710,17 +9777,17 @@ function fitCamera(layout, n, options, sheet2, fovDeg, aspect, margin = 1.06) {
8710
9777
  }
8711
9778
 
8712
9779
  // src/PaperField.tsx
8713
- import { jsx as jsx11, jsxs as jsxs7 } from "react/jsx-runtime";
9780
+ import { jsx as jsx11, jsxs as jsxs8 } from "react/jsx-runtime";
8714
9781
  var PaperFieldMesh = forwardRef2(
8715
9782
  function PaperFieldMesh2(props, ref) {
8716
9783
  const reduced = usePrefersReducedMotion(props.reducedMotion);
8717
- const papers = useMemo9(
9784
+ const papers = useMemo10(
8718
9785
  () => effectiveFieldPapers(props.papers, props.images),
8719
9786
  [props.papers, props.images]
8720
9787
  );
8721
9788
  const stablePapers = useStable(papers);
8722
9789
  const stablePreset = useStable(props.preset ?? null);
8723
- const groups = useMemo9(
9790
+ const groups = useMemo10(
8724
9791
  () => groupFieldPapers(stablePapers, stablePreset ?? void 0),
8725
9792
  [stablePapers, stablePreset]
8726
9793
  );
@@ -8729,7 +9796,7 @@ var PaperFieldMesh = forwardRef2(
8729
9796
  const layout = getLayout(layoutId);
8730
9797
  const firstSheet = groups[0]?.config.sheet;
8731
9798
  const stableLayoutOptions = useStable(props.layoutOptions ?? {});
8732
- const layoutOptions = useMemo9(
9799
+ const layoutOptions = useMemo10(
8733
9800
  // Sheet grids size their cells from the papers themselves — gutter is
8734
9801
  // then literally the spacing between stamps (explicit cell dims win).
8735
9802
  () => resolveLayoutOptions(layoutId, layout, stableLayoutOptions, firstSheet),
@@ -8744,7 +9811,7 @@ var PaperFieldMesh = forwardRef2(
8744
9811
  const driver = reduced ? "none" : props.motion?.driver ?? "autoplay";
8745
9812
  const speed = props.motion?.speed ?? 0.5;
8746
9813
  const entranceType = reduced ? "none" : props.entrance?.type ?? "rise";
8747
- useEffect14(() => {
9814
+ useEffect15(() => {
8748
9815
  const prev = prevLayout.current;
8749
9816
  if (prev.id !== layoutId || JSON.stringify(prev.options) !== JSON.stringify(layoutOptions)) {
8750
9817
  morphRef.current = { from: prev, t: 0 };
@@ -8752,7 +9819,7 @@ var PaperFieldMesh = forwardRef2(
8752
9819
  prevLayout.current = { id: layoutId, options: layoutOptions };
8753
9820
  }
8754
9821
  }, [layoutId, layoutOptions]);
8755
- useEffect14(() => {
9822
+ useEffect15(() => {
8756
9823
  if (driver !== "drag") return;
8757
9824
  const el = gl.domElement;
8758
9825
  let lastX = null;
@@ -8778,7 +9845,7 @@ var PaperFieldMesh = forwardRef2(
8778
9845
  window.removeEventListener("pointerup", up);
8779
9846
  };
8780
9847
  }, [driver, gl]);
8781
- useFrame5(({ clock }, delta) => {
9848
+ useFrame7(({ clock }, delta) => {
8782
9849
  if (mountTimeRef.current < 0) mountTimeRef.current = clock.elapsedTime;
8783
9850
  if (driver === "autoplay") phaseRef.current += delta * speed * 0.02;
8784
9851
  if (driver === "drag") {
@@ -8820,7 +9887,7 @@ var PaperFieldMesh = forwardRef2(
8820
9887
  }
8821
9888
  ) });
8822
9889
  }
8823
- return /* @__PURE__ */ jsxs7("group", { ref, children: [
9890
+ return /* @__PURE__ */ jsxs8("group", { ref, children: [
8824
9891
  sheetOptions?.backing && /* @__PURE__ */ jsx11(BackingSheet, { options: sheetOptions, count: total, removed: EMPTY_SET }),
8825
9892
  groups.map((group, gi) => /* @__PURE__ */ jsx11(
8826
9893
  FieldGroup,
@@ -8843,7 +9910,7 @@ function FitCamera(meshProps) {
8843
9910
  const imagesProp = useStable(meshProps.images ?? null);
8844
9911
  const presetProp = useStable(meshProps.preset ?? null);
8845
9912
  const optionsProp = useStable(meshProps.layoutOptions ?? {});
8846
- const field = useMemo9(() => {
9913
+ const field = useMemo10(() => {
8847
9914
  const papers = effectiveFieldPapers(papersProp ?? void 0, imagesProp ?? void 0);
8848
9915
  const layoutId = meshProps.layout ?? "ring";
8849
9916
  const layout = getLayout(layoutId);
@@ -8851,8 +9918,8 @@ function FitCamera(meshProps) {
8851
9918
  const options = resolveLayoutOptions(layoutId, layout, optionsProp, sheet2);
8852
9919
  return { layout, n: papers.length, options, sheet: sheet2 ?? DEFAULT_SHEET };
8853
9920
  }, [papersProp, imagesProp, presetProp, meshProps.layout, optionsProp]);
8854
- useEffect14(() => {
8855
- if (!(camera instanceof THREE16.PerspectiveCamera)) return;
9921
+ useEffect15(() => {
9922
+ if (!(camera instanceof THREE17.PerspectiveCamera)) return;
8856
9923
  const { position, target } = fitCamera(
8857
9924
  field.layout,
8858
9925
  field.n,
@@ -8869,20 +9936,20 @@ function FitCamera(meshProps) {
8869
9936
  }
8870
9937
  var PaperField = forwardRef2(function PaperField2({ children, className, style, scene, ...meshProps }, ref) {
8871
9938
  const rig = sceneSchema.parse(scene ?? {});
8872
- const registry4 = useMemo9(() => new DropZoneRegistry(), []);
9939
+ const registry4 = useMemo10(() => new DropZoneRegistry(), []);
8873
9940
  const a11yRef = useRef8(null);
8874
- const papers = useMemo9(
9941
+ const papers = useMemo10(
8875
9942
  () => effectiveFieldPapers(meshProps.papers, meshProps.images),
8876
9943
  [meshProps.papers, meshProps.images]
8877
9944
  );
8878
9945
  const stablePapers = useStable(papers);
8879
9946
  const stablePreset = useStable(meshProps.preset ?? null);
8880
- const interactive = useMemo9(
9947
+ const interactive = useMemo10(
8881
9948
  () => fieldIsInteractive(stablePapers, stablePreset ?? void 0, meshProps.interactive),
8882
9949
  [stablePapers, stablePreset, meshProps.interactive]
8883
9950
  );
8884
- return /* @__PURE__ */ jsx11("div", { className, style: { width: "100%", height: "100%", ...style }, children: /* @__PURE__ */ jsxs7(DropZoneContext.Provider, { value: registry4, children: [
8885
- /* @__PURE__ */ jsxs7(Canvas, { shadows: true, camera: { position: [0, 0.6, 5.2], fov: 45 }, dpr: [1, 2], children: [
9951
+ return /* @__PURE__ */ jsx11("div", { className, style: { width: "100%", height: "100%", ...style }, children: /* @__PURE__ */ jsxs8(DropZoneContext.Provider, { value: registry4, children: [
9952
+ /* @__PURE__ */ jsxs8(Canvas, { shadows: true, camera: { position: [0, 0.6, 5.2], fov: 45 }, dpr: [1, 2], children: [
8886
9953
  /* @__PURE__ */ jsx11(FitCamera, { ...meshProps }),
8887
9954
  /* @__PURE__ */ jsx11(PaperBackdrop, { backdrop: rig.backdrop }),
8888
9955
  /* @__PURE__ */ jsx11(PaperLighting, { preset: rig.lighting, light: rig.light, floor: -2.4, scale: 14 }),
@@ -9153,4 +10220,4 @@ export {
9153
10220
  describeConfig,
9154
10221
  buildAgentPayload
9155
10222
  };
9156
- //# sourceMappingURL=chunk-E22ILEDO.js.map
10223
+ //# sourceMappingURL=chunk-MZCAN3AR.js.map