paperlab 0.4.0 → 0.5.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.
@@ -909,6 +909,31 @@ var stockNames = [
909
909
  "sticker"
910
910
  ];
911
911
  var stockSchema = z14.enum(stockNames);
912
+ var washSchema = z14.object({
913
+ /** The first pigment. */
914
+ color: z14.string().default("#4a5b8c").describe("color"),
915
+ /** The second. Blooms alternate, and overlaps multiply into a third. */
916
+ secondary: z14.string().default("#b06a6a").describe("color"),
917
+ /** How many pools of colour. */
918
+ blooms: z14.number().int().min(1).max(24).default(7),
919
+ /** How far a pool runs before it dries — its size against the sheet. */
920
+ spread: z14.number().min(0.1).max(1).default(0.7),
921
+ /** Softness of the wet edge. 0 is a hard cut, 1 is a pool still moving. */
922
+ bleed: z14.number().min(0).max(1).default(0.5),
923
+ /** How much pigment is in the water. */
924
+ intensity: z14.number().min(0).max(1).default(0.55),
925
+ /**
926
+ * Edge darkening — the ring of pigment left where a pool dried.
927
+ *
928
+ * The signature of the medium, and the one thing a plain gradient cannot
929
+ * fake. Without it a wash reads as an airbrush.
930
+ */
931
+ edge: z14.number().min(0).max(1).default(0.6),
932
+ /** Pigment settling into the tooth of the paper. */
933
+ granulation: z14.number().min(0).max(1).default(0.35),
934
+ /** Fixed so a preset paints the same wash every time. */
935
+ seed: z14.number().int().min(0).max(99).default(0)
936
+ });
912
937
  var blankContentBase = z14.object({
913
938
  type: z14.literal("blank")
914
939
  });
@@ -933,7 +958,7 @@ var textContentBase = z14.object({
933
958
  /** px at texture resolution (long edge = 1024 logical px before DPR). */
934
959
  size: z14.number().min(8).max(256).default(44),
935
960
  weight: z14.number().min(100).max(900).default(400),
936
- color: z14.string().default("#2b2620"),
961
+ color: z14.string().default("#2b2620").describe("color"),
937
962
  align: z14.enum(["left", "center", "right"]).default("left"),
938
963
  /** Fraction of the short edge. */
939
964
  padding: z14.number().min(0).max(0.4).default(0.09),
@@ -983,7 +1008,7 @@ var cardContentBase = z14.object({
983
1008
  * was cropped rather than as a card that was set.
984
1009
  */
985
1010
  size: z14.number().min(8).max(256).default(58),
986
- color: z14.string().default("#2b2620"),
1011
+ color: z14.string().default("#2b2620").describe("color"),
987
1012
  align: z14.enum(["left", "center"]).default("left"),
988
1013
  padding: z14.number().min(0).max(0.4).default(0.1)
989
1014
  });
@@ -1002,14 +1027,15 @@ var receiptContentBase = z14.object({
1002
1027
  timestamp: z14.string().optional(),
1003
1028
  footer: z14.string().default("KEEP FOR YOUR RECORDS")
1004
1029
  });
1030
+ var withWash = { wash: washSchema.optional() };
1005
1031
  var backContentSchema = z14.discriminatedUnion("type", [
1006
- blankContentBase,
1007
- imageContentBase,
1008
- textContentBase,
1009
- cardContentBase,
1010
- receiptContentBase
1032
+ blankContentBase.extend(withWash),
1033
+ imageContentBase.extend(withWash),
1034
+ textContentBase.extend(withWash),
1035
+ cardContentBase.extend(withWash),
1036
+ receiptContentBase.extend(withWash)
1011
1037
  ]);
1012
- var withBack = { back: backContentSchema.optional() };
1038
+ var withBack = { back: backContentSchema.optional(), ...withWash };
1013
1039
  var blankContentSchema = blankContentBase.extend(withBack);
1014
1040
  var imageContentSchema = imageContentBase.extend(withBack);
1015
1041
  var textContentSchema = textContentBase.extend(withBack);
@@ -1022,6 +1048,14 @@ var contentSchema = z14.discriminatedUnion("type", [
1022
1048
  cardContentSchema,
1023
1049
  receiptContentSchema
1024
1050
  ]);
1051
+ var contentNames = contentSchema.options.map(
1052
+ (option) => option.shape.type.value
1053
+ );
1054
+ function contentSchemaFor(type) {
1055
+ const option = contentSchema.options.find((candidate) => candidate.shape.type.value === type);
1056
+ if (!option) throw new Error(`Unknown content type: ${type}`);
1057
+ return option;
1058
+ }
1025
1059
  var paperEdges = ["top", "right", "bottom", "left"];
1026
1060
  var surfaceSchema = z14.object({
1027
1061
  /** Paper fiber noise, 0..1. */
@@ -1109,8 +1143,70 @@ var lightingNames = [
1109
1143
  "lightbox"
1110
1144
  ];
1111
1145
  var filmNames = ["agx", "neutral", "filmic"];
1146
+ var lightSchema = z14.object({
1147
+ /** Tone-mapping exposure — the stop the whole picture is printed at. */
1148
+ exposure: z14.number().min(0.1).max(4).optional(),
1149
+ /**
1150
+ * The tone curve — the film, where `exposure` is the stop.
1151
+ *
1152
+ * `filmic` is ACES, which is what every preset used to be pinned to and is
1153
+ * kept so a scene tuned against it can say so. On near-white paper it is
1154
+ * the wrong film: it desaturates and drags bright neutrals toward
1155
+ * yellow-green, which is the sepia cast a lit sheet used to pick up.
1156
+ */
1157
+ film: z14.enum(filmNames).optional(),
1158
+ /** Key light strength. */
1159
+ key: z14.number().min(0).max(12).optional(),
1160
+ /** Key light colour. */
1161
+ color: z14.string().optional().describe("color"),
1162
+ /**
1163
+ * Where the key stands, degrees around the vertical. 0° is straight in
1164
+ * front of the paper (+Z, beside the camera), 90° is off to the right,
1165
+ * and ±180° is directly behind it — which is where `nave` puts it, and
1166
+ * why that preset is carried by light coming THROUGH the paper.
1167
+ */
1168
+ direction: z14.number().min(-180).max(180).optional(),
1169
+ /** How high the key stands, degrees above the horizon. */
1170
+ height: z14.number().min(-30).max(89).optional(),
1171
+ /** Flat fill from every direction at once. Cheap, and it kills form — reach for `studio` first. */
1172
+ ambient: z14.number().min(0).max(2).optional(),
1173
+ /** The room's own light: an environment map built from `sky`. Directional fill, and the only thing paper's sheen has to reflect. */
1174
+ studio: z14.number().min(0).max(3).optional(),
1175
+ /** Distance haze, as a multiple of the preset's. 0 clears the air entirely; 2 halves the distance you can see. */
1176
+ haze: z14.number().min(0).max(3).optional()
1177
+ });
1178
+ var backdropSchema = z14.object({
1179
+ /** Behind everything, and behind the picture where it does not reach. */
1180
+ color: z14.string().default("#171717").describe("color"),
1181
+ /** A URL, or an uploaded picture. Empty is the colour on its own. */
1182
+ image: z14.string().default(""),
1183
+ fit: z14.enum(["cover", "contain"]).default("cover"),
1184
+ /**
1185
+ * Toward the colour, so the paper stays the subject.
1186
+ *
1187
+ * A backdrop at full strength competes with the sheet in front of it —
1188
+ * which is what a real photographer solves by putting the background out
1189
+ * of the light, and what this solves by mixing it back toward the ground
1190
+ * it sits on.
1191
+ */
1192
+ fade: z14.number().min(0).max(1).default(0.25),
1193
+ /** Out of focus, for the same reason. */
1194
+ blur: z14.number().min(0).max(1).default(0.2)
1195
+ });
1112
1196
  var sceneSchema = z14.object({
1113
- lighting: z14.enum(lightingNames).default("studio")
1197
+ lighting: z14.enum(lightingNames).default("studio"),
1198
+ /** What is behind the sheet. Unset leaves the canvas alone. */
1199
+ backdrop: backdropSchema.optional(),
1200
+ /**
1201
+ * Overrides on the named preset — the same authorable half stage mode has
1202
+ * always had, and which a lone sheet had no way to reach.
1203
+ *
1204
+ * A preset was the starting point everywhere EXCEPT here: a stage could be
1205
+ * "nave, but the sun is lower", and a `<Paper>` could only be one of seven
1206
+ * rigs exactly as shipped. `<PaperLighting>` has taken these overrides all
1207
+ * along; nothing was passing them.
1208
+ */
1209
+ light: lightSchema.default({})
1114
1210
  });
1115
1211
  var coreStateNames = ["rest", "hover", "pressed", "picked", "placed"];
1116
1212
  var isStateName = (s) => coreStateNames.includes(s) || s.startsWith("custom:");
@@ -1332,6 +1428,33 @@ var builtins = {
1332
1428
  behavior: { type: "letter-fold", progress: 0.4, crease: 0.3 },
1333
1429
  surface: { creaseLines: { angle: 0, positions: [1 / 3, 2 / 3], strength: 0.5 } }
1334
1430
  },
1431
+ /**
1432
+ * The wash, shown rather than described.
1433
+ *
1434
+ * A `wash` is a field on every content type, which means it is reachable
1435
+ * from any preset and discoverable from none — a toggle three folders down
1436
+ * is not an argument for the feature. This is the argument: type set over
1437
+ * paint, on cotton, deckled, with the paper still showing through both.
1438
+ */
1439
+ "washed-letter": {
1440
+ meta: { name: "Washed letter", tags: ["wash", "text"] },
1441
+ // Printer stock, not vellum: vellum is translucent and takes the room's
1442
+ // grey through the back of the sheet, which turns a wash the colour of
1443
+ // dishwater. Pigment needs something white behind it.
1444
+ sheet: { width: 1.05, height: 1.45 },
1445
+ stock: "printer",
1446
+ content: {
1447
+ type: "text",
1448
+ text: "Painted first,\nwritten after.",
1449
+ font: 'Georgia, "Times New Roman", serif',
1450
+ size: 52,
1451
+ valign: "center",
1452
+ align: "center",
1453
+ wash: { color: "#5b6f9a", secondary: "#c08a86", blooms: 6, spread: 0.85, intensity: 0.5, seed: 3 }
1454
+ },
1455
+ behavior: { type: "peel", progress: 0.12, corner: "bottom-right", radius: 0.3 },
1456
+ surface: { grain: 0.35, deckle: { edges: ["bottom", "right"], roughness: 0.45 } }
1457
+ },
1335
1458
  "vintage-note": {
1336
1459
  meta: { name: "Vintage note", tags: ["aging", "text"] },
1337
1460
  sheet: { width: 1.1, height: 1.4 },
@@ -2266,7 +2389,6 @@ function getIdlePreset(name) {
2266
2389
  }
2267
2390
 
2268
2391
  // src/scene/lighting.ts
2269
- import { z as z22 } from "zod";
2270
2392
  var lightingPresets = {
2271
2393
  studio: {
2272
2394
  id: "studio",
@@ -2427,46 +2549,14 @@ var lightingPresets = {
2427
2549
  function getLightingPreset(name) {
2428
2550
  return lightingPresets[name];
2429
2551
  }
2430
- var lightSchema = z22.object({
2431
- /** Tone-mapping exposure — the stop the whole picture is printed at. */
2432
- exposure: z22.number().min(0.1).max(4).optional(),
2433
- /**
2434
- * The tone curve — the film, where `exposure` is the stop.
2435
- *
2436
- * `filmic` is ACES, which is what every preset used to be pinned to and is
2437
- * kept so a scene tuned against it can say so. On near-white paper it is
2438
- * the wrong film: it desaturates and drags bright neutrals toward
2439
- * yellow-green, which is the sepia cast a lit sheet used to pick up.
2440
- */
2441
- film: z22.enum(filmNames).optional(),
2442
- /** Key light strength. */
2443
- key: z22.number().min(0).max(12).optional(),
2444
- /** Key light colour. */
2445
- color: z22.string().optional(),
2446
- /**
2447
- * Where the key stands, degrees around the vertical. 0° is straight in
2448
- * front of the paper (+Z, beside the camera), 90° is off to the right,
2449
- * and ±180° is directly behind it — which is where `nave` puts it, and
2450
- * why that preset is carried by light coming THROUGH the paper.
2451
- */
2452
- direction: z22.number().min(-180).max(180).optional(),
2453
- /** How high the key stands, degrees above the horizon. */
2454
- height: z22.number().min(-30).max(89).optional(),
2455
- /** Flat fill from every direction at once. Cheap, and it kills form — reach for `studio` first. */
2456
- ambient: z22.number().min(0).max(2).optional(),
2457
- /** The room's own light: an environment map built from `sky`. Directional fill, and the only thing paper's sheen has to reflect. */
2458
- studio: z22.number().min(0).max(3).optional(),
2459
- /** Distance haze, as a multiple of the preset's. 0 clears the air entirely; 2 halves the distance you can see. */
2460
- haze: z22.number().min(0).max(3).optional()
2461
- });
2462
2552
  var DEG6 = 180 / Math.PI;
2463
2553
  function lightAngles(position) {
2464
- const [x, y, z26] = position;
2465
- const distance = Math.hypot(x, y, z26);
2554
+ const [x, y, z25] = position;
2555
+ const distance = Math.hypot(x, y, z25);
2466
2556
  if (distance < 1e-9) return { azimuth: 0, elevation: 90, distance: 0 };
2467
- const ground = Math.hypot(x, z26);
2557
+ const ground = Math.hypot(x, z25);
2468
2558
  return {
2469
- azimuth: ground < 1e-9 ? 0 : Math.atan2(x, z26) * DEG6,
2559
+ azimuth: ground < 1e-9 ? 0 : Math.atan2(x, z25) * DEG6,
2470
2560
  elevation: Math.atan2(y, ground) * DEG6,
2471
2561
  distance
2472
2562
  };
@@ -3053,11 +3143,11 @@ function computeSheetNormals(geometry) {
3053
3143
  for (let i = 0, l = nrm.length; i < l; i += 3) {
3054
3144
  const x = nrm[i];
3055
3145
  const y = nrm[i + 1];
3056
- const z26 = nrm[i + 2];
3057
- const len = Math.sqrt(x * x + y * y + z26 * z26) || 1;
3146
+ const z25 = nrm[i + 2];
3147
+ const len = Math.sqrt(x * x + y * y + z25 * z25) || 1;
3058
3148
  nrm[i] = x / len;
3059
3149
  nrm[i + 1] = y / len;
3060
- nrm[i + 2] = z26 / len;
3150
+ nrm[i + 2] = z25 / len;
3061
3151
  }
3062
3152
  normalAttr.needsUpdate = true;
3063
3153
  }
@@ -3190,6 +3280,109 @@ function paintCard(ctx, w, h, content, stock, dpr) {
3190
3280
  }
3191
3281
  }
3192
3282
 
3283
+ // src/content/wash.ts
3284
+ function mulberry32(seed) {
3285
+ let a = seed * 1831565813 + 2654435769;
3286
+ return () => {
3287
+ a |= 0;
3288
+ a = a + 1831565813 | 0;
3289
+ let t = Math.imul(a ^ a >>> 15, 1 | a);
3290
+ t = t + Math.imul(t ^ t >>> 7, 61 | t) ^ t;
3291
+ return ((t ^ t >>> 14) >>> 0) / 4294967296;
3292
+ };
3293
+ }
3294
+ function rgba(hex, alpha) {
3295
+ const m = /^#?([0-9a-f]{6})$/i.exec(hex.trim());
3296
+ const n = m ? Number.parseInt(m[1], 16) : 8421504;
3297
+ return `rgba(${n >> 16 & 255}, ${n >> 8 & 255}, ${n & 255}, ${Math.max(0, Math.min(1, alpha))})`;
3298
+ }
3299
+ function bloomShape(rng) {
3300
+ return {
3301
+ phase: [rng() * Math.PI * 2, rng() * Math.PI * 2, rng() * Math.PI * 2],
3302
+ amp: [0.18 + rng() * 0.1, 0.09 + rng() * 0.06, 0.05 + rng() * 0.04]
3303
+ };
3304
+ }
3305
+ function bloomPath(ctx, cx, cy, radius, shape, from = 0, to = 1) {
3306
+ const { phase, amp } = shape;
3307
+ const STEPS = Math.max(6, Math.round(72 * (to - from)));
3308
+ ctx.beginPath();
3309
+ for (let i = 0; i <= STEPS; i++) {
3310
+ const t = (from + (to - from) * i / STEPS) * Math.PI * 2;
3311
+ const wobble = amp[0] * Math.sin(2 * t + phase[0]) + amp[1] * Math.sin(3 * t + phase[1]) + amp[2] * Math.sin(5 * t + phase[2]);
3312
+ const r = radius * (1 + wobble);
3313
+ const x = cx + Math.cos(t) * r;
3314
+ const y = cy + Math.sin(t) * r;
3315
+ if (i === 0) ctx.moveTo(x, y);
3316
+ else ctx.lineTo(x, y);
3317
+ }
3318
+ if (from === 0 && to === 1) ctx.closePath();
3319
+ }
3320
+ function canBlur(ctx) {
3321
+ if (!("filter" in ctx)) return false;
3322
+ const before = ctx.filter;
3323
+ try {
3324
+ ctx.filter = "blur(2px)";
3325
+ const worked = ctx.filter !== "none" && ctx.filter !== before;
3326
+ ctx.filter = before;
3327
+ return worked;
3328
+ } catch {
3329
+ return false;
3330
+ }
3331
+ }
3332
+ function paintWash(ctx, w, h, wash) {
3333
+ const rng = mulberry32(wash.seed + 1);
3334
+ const short = Math.min(w, h);
3335
+ const blurs = canBlur(ctx);
3336
+ ctx.save();
3337
+ ctx.globalCompositeOperation = "multiply";
3338
+ for (let i = 0; i < wash.blooms; i++) {
3339
+ const pigment = i % 2 === 0 ? wash.color : wash.secondary;
3340
+ const cx = (rng() * 1.5 - 0.25) * w;
3341
+ const cy = (rng() * 1.5 - 0.25) * h;
3342
+ const radius = short * wash.spread * (0.28 + rng() * 0.34);
3343
+ const alpha = wash.intensity * (0.35 + rng() * 0.4);
3344
+ const shape = bloomShape(rng);
3345
+ ctx.save();
3346
+ if (blurs) ctx.filter = `blur(${(2 + wash.bleed * 26) * (0.6 + rng() * 0.8)}px)`;
3347
+ const gx = cx + (rng() - 0.5) * radius * 0.5;
3348
+ const gy = cy + (rng() - 0.5) * radius * 0.5;
3349
+ const grad = ctx.createRadialGradient(gx, gy, radius * 0.05, gx, gy, radius * 1.15);
3350
+ grad.addColorStop(0, rgba(pigment, alpha * 0.55));
3351
+ grad.addColorStop(0.62, rgba(pigment, alpha));
3352
+ grad.addColorStop(1, rgba(pigment, alpha * 0.75));
3353
+ ctx.fillStyle = grad;
3354
+ bloomPath(ctx, cx, cy, radius, shape);
3355
+ ctx.fill();
3356
+ if (wash.edge > 0) {
3357
+ if (blurs) ctx.filter = `blur(${(1.5 + wash.bleed * 9) * (0.6 + rng() * 0.6)}px)`;
3358
+ ctx.lineWidth = Math.max(1, radius * (0.03 + wash.edge * 0.05));
3359
+ const arcs = 5;
3360
+ const overlap = 0.4 / arcs;
3361
+ for (let a = 0; a < arcs; a++) {
3362
+ ctx.strokeStyle = rgba(pigment, alpha * wash.edge * (0.15 + rng() * 1.05));
3363
+ bloomPath(ctx, cx, cy, radius, shape, a / arcs, (a + 1) / arcs + overlap);
3364
+ ctx.stroke();
3365
+ }
3366
+ }
3367
+ ctx.restore();
3368
+ if (wash.granulation > 0) {
3369
+ ctx.save();
3370
+ bloomPath(ctx, cx, cy, radius, shape);
3371
+ ctx.clip();
3372
+ const grains = Math.round(wash.granulation * 900);
3373
+ ctx.fillStyle = rgba(pigment, wash.granulation * alpha * 0.5);
3374
+ for (let g = 0; g < grains; g++) {
3375
+ const a = rng() * Math.PI * 2;
3376
+ const d = Math.sqrt(rng()) * radius;
3377
+ const size = 1 + rng() * (short * 4e-3);
3378
+ ctx.fillRect(cx + Math.cos(a) * d, cy + Math.sin(a) * d, size, size);
3379
+ }
3380
+ ctx.restore();
3381
+ }
3382
+ }
3383
+ ctx.restore();
3384
+ }
3385
+
3193
3386
  // src/content/texture.ts
3194
3387
  var LONG_EDGE = 1024;
3195
3388
  var DPR = 2;
@@ -3240,6 +3433,7 @@ function renderContentToCanvas(content, sheet2, stock, image) {
3240
3433
  canvas.height = h;
3241
3434
  const ctx = canvas.getContext("2d");
3242
3435
  paintBackground(ctx, w, h, stock);
3436
+ if (content.wash) paintWash(ctx, w, h, content.wash);
3243
3437
  if (content.type === "image" && image && content.src) paintImage(ctx, w, h, image, content.fit);
3244
3438
  if (content.type === "text") paintText(ctx, w, h, content, stock);
3245
3439
  if (content.type === "receipt") paintReceipt(ctx, w, h, content, stock);
@@ -3451,13 +3645,13 @@ var ClothSim = class {
3451
3645
  this.stillFrames = 0;
3452
3646
  }
3453
3647
  /** Nearest particle to a local-space point — the grab interface. */
3454
- grabNearest(x, y, z26) {
3648
+ grabNearest(x, y, z25) {
3455
3649
  let best = -1;
3456
3650
  let bestDist = Infinity;
3457
3651
  for (let i = 0; i < this.count; i++) {
3458
3652
  const dx = this.positions[i * 3] - x;
3459
3653
  const dy = this.positions[i * 3 + 1] - y;
3460
- const dz = this.positions[i * 3 + 2] - z26;
3654
+ const dz = this.positions[i * 3 + 2] - z25;
3461
3655
  const d = dx * dx + dy * dy + dz * dz;
3462
3656
  if (d < bestDist) {
3463
3657
  bestDist = d;
@@ -3468,15 +3662,15 @@ var ClothSim = class {
3468
3662
  this.wake();
3469
3663
  return best;
3470
3664
  }
3471
- moveGrab(x, y, z26) {
3665
+ moveGrab(x, y, z25) {
3472
3666
  if (this.grabbedIndex < 0) return;
3473
3667
  const i3 = this.grabbedIndex * 3;
3474
3668
  this.positions[i3] = x;
3475
3669
  this.positions[i3 + 1] = y;
3476
- this.positions[i3 + 2] = z26;
3670
+ this.positions[i3 + 2] = z25;
3477
3671
  this.prev[i3] = x;
3478
3672
  this.prev[i3 + 1] = y;
3479
- this.prev[i3 + 2] = z26;
3673
+ this.prev[i3 + 2] = z25;
3480
3674
  this.wake();
3481
3675
  }
3482
3676
  release() {
@@ -3512,19 +3706,19 @@ var ClothSim = class {
3512
3706
  }
3513
3707
  const x = p[i3];
3514
3708
  const y = p[i3 + 1];
3515
- const z26 = p[i3 + 2];
3709
+ const z25 = p[i3 + 2];
3516
3710
  const gust2 = wind * (0.55 + 0.45 * Math.sin(this.time * 1.7 + x * 2.1 + y * 1.3)) * 0.9;
3517
3711
  const ax = gust2 * 0.25;
3518
3712
  const az = gust2;
3519
3713
  const vx = (x - this.prev[i3]) * damping;
3520
3714
  const vy = (y - this.prev[i3 + 1]) * damping;
3521
- const vz = (z26 - this.prev[i3 + 2]) * damping;
3715
+ const vz = (z25 - this.prev[i3 + 2]) * damping;
3522
3716
  this.prev[i3] = x;
3523
3717
  this.prev[i3 + 1] = y;
3524
- this.prev[i3 + 2] = z26;
3718
+ this.prev[i3 + 2] = z25;
3525
3719
  p[i3] = x + vx + ax * dt2;
3526
3720
  p[i3 + 1] = y + vy - gravity * 3.2 * dt2;
3527
- p[i3 + 2] = z26 + vz + az * dt2;
3721
+ p[i3 + 2] = z25 + vz + az * dt2;
3528
3722
  maxTravel = Math.max(maxTravel, vx * vx + vy * vy + vz * vz);
3529
3723
  }
3530
3724
  for (let iter = 0; iter < SOLVER_ITERATIONS; iter++) {
@@ -3632,8 +3826,8 @@ vec3 plTransmission(vec3 inkFilter) {
3632
3826
  );
3633
3827
  function translucencyValues(translucency, lighting) {
3634
3828
  const preset = typeof lighting === "string" ? getLightingPreset(lighting) : lighting;
3635
- const [x, y, z26] = preset.key.position;
3636
- const direction = new THREE4.Vector3(x, y, z26);
3829
+ const [x, y, z25] = preset.key.position;
3830
+ const direction = new THREE4.Vector3(x, y, z25);
3637
3831
  if (direction.lengthSq() < 1e-12) direction.set(0, 1, 0);
3638
3832
  direction.normalize();
3639
3833
  const color = new THREE4.Color(preset.key.color).multiplyScalar(preset.key.intensity * TRANSMISSION_GAIN);
@@ -4386,6 +4580,7 @@ var PaperMesh = forwardRef(function PaperMesh2(props, ref) {
4386
4580
  props.interactive && !isCloth && behavior?.handles?.map((h, i) => /* @__PURE__ */ jsxs(
4387
4581
  "mesh",
4388
4582
  {
4583
+ userData: { paperlabChrome: true },
4389
4584
  ref: (m) => {
4390
4585
  handleRefs.current[i] = m;
4391
4586
  },
@@ -4529,7 +4724,7 @@ var toneMappings = {
4529
4724
  neutral: THREE9.NeutralToneMapping,
4530
4725
  filmic: THREE9.ACESFilmicToneMapping
4531
4726
  };
4532
- function mulberry32(seed) {
4727
+ function mulberry322(seed) {
4533
4728
  let a = seed;
4534
4729
  return () => {
4535
4730
  a |= 0;
@@ -4563,7 +4758,7 @@ function makeGoboTexture(kind) {
4563
4758
  }
4564
4759
  ctx.restore();
4565
4760
  } else {
4566
- const rand = mulberry32(7);
4761
+ const rand = mulberry322(7);
4567
4762
  for (const [count, radius, alpha] of [
4568
4763
  [26, 70, 0.75],
4569
4764
  [40, 38, 0.6],
@@ -4711,9 +4906,75 @@ function PaperLighting({
4711
4906
  ] });
4712
4907
  }
4713
4908
 
4714
- // src/field/dropZones.tsx
4909
+ // src/scene/backdrop.tsx
4715
4910
  import * as THREE10 from "three";
4716
- import { createContext as createContext2, useContext as useContext2, useEffect as useEffect7, useMemo as useMemo5, useRef as useRef5, useSyncExternalStore } from "react";
4911
+ import { useEffect as useEffect7, useState as useState4 } from "react";
4912
+ import { useThree as useThree3 } from "@react-three/fiber";
4913
+ function PaperBackdrop({ backdrop }) {
4914
+ const scene = useThree3((s) => s.scene);
4915
+ const size = useThree3((s) => s.size);
4916
+ const [image, setImage] = useState4(null);
4917
+ const src = backdrop?.image ?? "";
4918
+ useEffect7(() => {
4919
+ if (!src) {
4920
+ setImage(null);
4921
+ return;
4922
+ }
4923
+ let live = true;
4924
+ const img = new Image();
4925
+ img.crossOrigin = "anonymous";
4926
+ img.onload = () => live && setImage(img);
4927
+ img.onerror = () => live && setImage(null);
4928
+ img.src = src;
4929
+ return () => {
4930
+ live = false;
4931
+ };
4932
+ }, [src]);
4933
+ const key = JSON.stringify({ backdrop: backdrop ?? null, w: size.width, h: size.height, loaded: !!image });
4934
+ useEffect7(() => {
4935
+ if (!backdrop) return;
4936
+ const previous = scene.background;
4937
+ const texture = paintBackdrop(backdrop, size.width, size.height, image);
4938
+ scene.background = texture;
4939
+ return () => {
4940
+ scene.background = previous;
4941
+ texture.dispose();
4942
+ };
4943
+ }, [key]);
4944
+ return null;
4945
+ }
4946
+ function paintBackdrop(backdrop, width, height, image) {
4947
+ const w = Math.max(2, Math.round(width / 2));
4948
+ const h = Math.max(2, Math.round(height / 2));
4949
+ const canvas = document.createElement("canvas");
4950
+ canvas.width = w;
4951
+ canvas.height = h;
4952
+ const ctx = canvas.getContext("2d");
4953
+ ctx.fillStyle = backdrop.color;
4954
+ ctx.fillRect(0, 0, w, h);
4955
+ if (image && image.width > 0 && image.height > 0) {
4956
+ const scale = backdrop.fit === "cover" ? Math.max(w / image.width, h / image.height) : Math.min(w / image.width, h / image.height);
4957
+ const dw = image.width * scale;
4958
+ const dh = image.height * scale;
4959
+ ctx.save();
4960
+ if (backdrop.blur > 0) ctx.filter = `blur(${backdrop.blur * Math.min(w, h) * 0.06}px)`;
4961
+ ctx.drawImage(image, (w - dw) / 2, (h - dh) / 2, dw, dh);
4962
+ ctx.restore();
4963
+ if (backdrop.fade > 0) {
4964
+ ctx.globalAlpha = backdrop.fade;
4965
+ ctx.fillStyle = backdrop.color;
4966
+ ctx.fillRect(0, 0, w, h);
4967
+ ctx.globalAlpha = 1;
4968
+ }
4969
+ }
4970
+ const texture = new THREE10.CanvasTexture(canvas);
4971
+ texture.colorSpace = THREE10.SRGBColorSpace;
4972
+ return texture;
4973
+ }
4974
+
4975
+ // src/field/dropZones.tsx
4976
+ import * as THREE11 from "three";
4977
+ import { createContext as createContext2, useContext as useContext2, useEffect as useEffect8, useMemo as useMemo5, useRef as useRef5, useSyncExternalStore } from "react";
4717
4978
  import { jsx as jsx6, jsxs as jsxs3 } from "react/jsx-runtime";
4718
4979
  var DropZoneRegistry = class {
4719
4980
  zones = /* @__PURE__ */ new Map();
@@ -4771,10 +5032,10 @@ function DropZone(props) {
4771
5032
  const registry4 = useContext2(DropZoneContext);
4772
5033
  const { id, accept, bounds, highlight = "glow", onPlace } = props;
4773
5034
  const place = useRef5(onPlace);
4774
- useEffect7(() => {
5035
+ useEffect8(() => {
4775
5036
  place.current = onPlace;
4776
5037
  }, [onPlace]);
4777
- useEffect7(() => {
5038
+ useEffect8(() => {
4778
5039
  if (!registry4) return;
4779
5040
  return registry4.register({
4780
5041
  id,
@@ -4793,12 +5054,12 @@ function DropZoneVisual({ registry: registry4, config }) {
4793
5054
  const style = config.highlight ?? "glow";
4794
5055
  const [w, h] = config.bounds.size;
4795
5056
  const edges = useMemo5(() => {
4796
- const plane = new THREE10.PlaneGeometry(w, h);
4797
- const geo = new THREE10.EdgesGeometry(plane);
5057
+ const plane = new THREE11.PlaneGeometry(w, h);
5058
+ const geo = new THREE11.EdgesGeometry(plane);
4798
5059
  plane.dispose();
4799
5060
  return geo;
4800
5061
  }, [w, h]);
4801
- useEffect7(() => () => edges.dispose(), [edges]);
5062
+ useEffect8(() => () => edges.dispose(), [edges]);
4802
5063
  return /* @__PURE__ */ jsxs3("group", { position: config.bounds.position, children: [
4803
5064
  style !== "outline" && /* @__PURE__ */ jsxs3("mesh", { children: [
4804
5065
  /* @__PURE__ */ jsx6("planeGeometry", { args: [w, h] }),
@@ -4817,18 +5078,18 @@ function DropZoneVisual({ registry: registry4, config }) {
4817
5078
  }
4818
5079
 
4819
5080
  // src/field/sheetGrid.ts
4820
- import { z as z23 } from "zod";
4821
- var sheetLayoutSchema = z23.object({
4822
- rows: z23.number().int().min(1).max(12).default(2),
4823
- columns: z23.number().int().min(1).max(12).default(5),
5081
+ import { z as z22 } from "zod";
5082
+ var sheetLayoutSchema = z22.object({
5083
+ rows: z22.number().int().min(1).max(12).default(2),
5084
+ columns: z22.number().int().min(1).max(12).default(5),
4824
5085
  /** World-units gap between slots. Stamps are printed in register — no jitter. */
4825
- gutter: z23.number().min(0).max(1).default(0.08),
5086
+ gutter: z22.number().min(0).max(1).default(0.08),
4826
5087
  /** Slot footprint in world units (the paper preset should match). */
4827
- cellWidth: z23.number().min(0.1).max(4).default(0.72),
4828
- cellHeight: z23.number().min(0.1).max(4).default(0.86),
5088
+ cellWidth: z22.number().min(0.1).max(4).default(0.72),
5089
+ cellHeight: z22.number().min(0.1).max(4).default(0.86),
4829
5090
  /** Render the shared backing sheet behind the grid. */
4830
- backing: z23.boolean().default(true),
4831
- backingMargin: z23.number().min(0).max(1).default(0.12)
5091
+ backing: z22.boolean().default(true),
5092
+ backingMargin: z22.number().min(0).max(1).default(0.12)
4832
5093
  });
4833
5094
  var SHEET_LIFT = 0.012;
4834
5095
  function withSheetCellFromPaper(parsed, rawOptions, paperDims) {
@@ -4875,18 +5136,18 @@ function tornEdgesOnDetach(i, o) {
4875
5136
  }
4876
5137
 
4877
5138
  // src/stage/path.ts
4878
- import { z as z24 } from "zod";
4879
- var walkPathSchema = z24.object({
5139
+ import { z as z23 } from "zod";
5140
+ var walkPathSchema = z23.object({
4880
5141
  /**
4881
5142
  * Control points on the ground plane, [x, z]. The default walks away from
4882
5143
  * the camera down -Z — the shot every reference image is composed on.
4883
5144
  */
4884
- points: z24.array(z24.tuple([z24.number(), z24.number()])).min(2).default([
5145
+ points: z23.array(z23.tuple([z23.number(), z23.number()])).min(2).default([
4885
5146
  [0, 9],
4886
5147
  [0, -9]
4887
5148
  ]),
4888
5149
  /** Join the last point back to the first: an endless walk, and the only form `phase` can slide. */
4889
- closed: z24.boolean().default(false)
5150
+ closed: z23.boolean().default(false)
4890
5151
  });
4891
5152
  var SAMPLES_PER_SEGMENT = 24;
4892
5153
  var EPSILON = 1e-6;
@@ -5001,7 +5262,7 @@ function getWalkPath(options) {
5001
5262
  }
5002
5263
 
5003
5264
  // src/field/layouts/index.ts
5004
- import { z as z25 } from "zod";
5265
+ import { z as z24 } from "zod";
5005
5266
  var DEFAULT_SHEET = { width: 1, height: 1.4 };
5006
5267
  var TAU3 = Math.PI * 2;
5007
5268
  var DEG7 = Math.PI / 180;
@@ -5013,9 +5274,9 @@ function jitter2(seed, i) {
5013
5274
  function ramp(i, n) {
5014
5275
  return n > 1 ? i / (n - 1) : 1;
5015
5276
  }
5016
- var ringSchema = z25.object({
5017
- radius: z25.number().min(0.5).max(12).default(2.6),
5018
- tiltDeg: z25.number().min(-45).max(45).default(8)
5277
+ var ringSchema = z24.object({
5278
+ radius: z24.number().min(0.5).max(12).default(2.6),
5279
+ tiltDeg: z24.number().min(-45).max(45).default(8)
5019
5280
  });
5020
5281
  var ring = {
5021
5282
  id: "ring",
@@ -5033,15 +5294,15 @@ var ring = {
5033
5294
  };
5034
5295
  }
5035
5296
  };
5036
- var fanSchema = z25.object({
5297
+ var fanSchema = z24.object({
5037
5298
  /** Total angular sweep from the first sheet to the last, degrees. */
5038
- sweep: z25.number().min(0).max(180).default(72),
5299
+ sweep: z24.number().min(0).max(180).default(72),
5039
5300
  /** Where the shared pin sits, in half-sheet-heights below center. 1 = the bottom edge. */
5040
- hinge: z25.number().min(0).max(4).default(1.15),
5301
+ hinge: z24.number().min(0).max(4).default(1.15),
5041
5302
  /** Thickness step so the sheets stack in order instead of z-fighting. */
5042
- lift: z25.number().min(2e-3).max(0.08).default(0.012),
5303
+ lift: z24.number().min(2e-3).max(0.08).default(0.012),
5043
5304
  /** How much flatter the middle of the fan sits than its outer sheets. */
5044
- bow: z25.number().min(0).max(1).default(0.7)
5305
+ bow: z24.number().min(0).max(1).default(0.7)
5045
5306
  });
5046
5307
  var fan = {
5047
5308
  id: "fan",
@@ -5061,16 +5322,16 @@ var fan = {
5061
5322
  };
5062
5323
  }
5063
5324
  };
5064
- var spreadSchema = z25.object({
5325
+ var spreadSchema = z24.object({
5065
5326
  /** How far each sheet slides past the one below it. */
5066
- slip: z25.number().min(0.02).max(2).default(0.3),
5327
+ slip: z24.number().min(0.02).max(2).default(0.3),
5067
5328
  /** Direction of the slide, degrees. 0 slides right, 90 slides up. */
5068
- angle: z25.number().min(-180).max(180).default(28),
5069
- lift: z25.number().min(2e-3).max(0.08).default(0.012),
5329
+ angle: z24.number().min(-180).max(180).default(28),
5330
+ lift: z24.number().min(2e-3).max(0.08).default(0.012),
5070
5331
  /** How much more the sheets at the far end of the slide bow. */
5071
- bow: z25.number().min(0).max(1).default(0.6),
5332
+ bow: z24.number().min(0).max(1).default(0.6),
5072
5333
  /** Nothing hand-slid is perfectly square — a touch of per-sheet rotation. */
5073
- drift: z25.number().min(0).max(1).default(0.15)
5334
+ drift: z24.number().min(0).max(1).default(0.15)
5074
5335
  });
5075
5336
  var spread = {
5076
5337
  id: "spread",
@@ -5088,15 +5349,15 @@ var spread = {
5088
5349
  };
5089
5350
  }
5090
5351
  };
5091
- var pileSchema = z25.object({
5352
+ var pileSchema = z24.object({
5092
5353
  /** How far sheets wander from the center of the heap. */
5093
- scatter: z25.number().min(0).max(2).default(0.22),
5354
+ scatter: z24.number().min(0).max(2).default(0.22),
5094
5355
  /** Widest angle a sheet sits off square, degrees. */
5095
- turn: z25.number().min(0).max(180).default(24),
5096
- lift: z25.number().min(2e-3).max(0.08).default(0.011),
5356
+ turn: z24.number().min(0).max(180).default(24),
5357
+ lift: z24.number().min(2e-3).max(0.08).default(0.011),
5097
5358
  /** How flat the sheets underneath are pressed by the ones on top. */
5098
- press: z25.number().min(0).max(1).default(0.85),
5099
- seed: z25.number().int().min(0).max(9999).default(3)
5359
+ press: z24.number().min(0).max(1).default(0.85),
5360
+ seed: z24.number().int().min(0).max(9999).default(3)
5100
5361
  });
5101
5362
  var pile = {
5102
5363
  id: "pile",
@@ -5112,12 +5373,12 @@ var pile = {
5112
5373
  };
5113
5374
  }
5114
5375
  };
5115
- var wallSchema = z25.object({
5116
- gapX: z25.number().min(0.05).max(1).default(0.22),
5117
- gapY: z25.number().min(0.05).max(1).default(0.3),
5118
- jitterAmt: z25.number().min(0).max(1).default(0.25),
5376
+ var wallSchema = z24.object({
5377
+ gapX: z24.number().min(0.05).max(1).default(0.22),
5378
+ gapY: z24.number().min(0.05).max(1).default(0.3),
5379
+ jitterAmt: z24.number().min(0).max(1).default(0.25),
5119
5380
  /** Spread of sag across the wall — no two pinned sheets hang alike. */
5120
- sag: z25.number().min(0).max(1).default(0.45)
5381
+ sag: z24.number().min(0).max(1).default(0.45)
5121
5382
  });
5122
5383
  var wall = {
5123
5384
  id: "wall",
@@ -5143,15 +5404,15 @@ var wall = {
5143
5404
  };
5144
5405
  }
5145
5406
  };
5146
- var spillSchema = z25.object({
5147
- spreadX: z25.number().min(0.5).max(8).default(2.4),
5148
- spreadY: z25.number().min(0.5).max(8).default(1.5),
5149
- depth: z25.number().min(0).max(6).default(1.6),
5407
+ var spillSchema = z24.object({
5408
+ spreadX: z24.number().min(0.5).max(8).default(2.4),
5409
+ spreadY: z24.number().min(0.5).max(8).default(1.5),
5410
+ depth: z24.number().min(0).max(6).default(1.6),
5150
5411
  /** How far sheets pitch and roll out of the picture plane. */
5151
- tumble: z25.number().min(0).max(1).default(0.5),
5412
+ tumble: z24.number().min(0).max(1).default(0.5),
5152
5413
  /** Spread of bend across the sheets — a spill does not fold them alike. */
5153
- vary: z25.number().min(0).max(1).default(0.6),
5154
- seed: z25.number().int().min(0).max(9999).default(7)
5414
+ vary: z24.number().min(0).max(1).default(0.6),
5415
+ seed: z24.number().int().min(0).max(9999).default(7)
5155
5416
  });
5156
5417
  var spill = {
5157
5418
  id: "spill",
@@ -5176,13 +5437,13 @@ var spill = {
5176
5437
  };
5177
5438
  }
5178
5439
  };
5179
- var sweepSchema = z25.object({
5180
- columns: z25.number().int().min(1).max(24).default(5),
5440
+ var sweepSchema = z24.object({
5441
+ columns: z24.number().int().min(1).max(24).default(5),
5181
5442
  /** Breathing room around each specimen. */
5182
- gap: z25.number().min(0).max(2).default(0.22),
5443
+ gap: z24.number().min(0).max(2).default(0.22),
5183
5444
  /** Deformation at the first specimen and at the last. */
5184
- from: z25.number().min(0).max(1).default(0),
5185
- to: z25.number().min(0).max(1).default(1)
5445
+ from: z24.number().min(0).max(1).default(0),
5446
+ to: z24.number().min(0).max(1).default(1)
5186
5447
  });
5187
5448
  var sweep = {
5188
5449
  id: "sweep",
@@ -5206,15 +5467,15 @@ var sweep = {
5206
5467
  };
5207
5468
  }
5208
5469
  };
5209
- var bookSchema = z25.object({
5470
+ var bookSchema = z24.object({
5210
5471
  /** How far the outermost page lifts off the block, degrees. */
5211
- spread: z25.number().min(0).max(150).default(55),
5472
+ spread: z24.number().min(0).max(150).default(55),
5212
5473
  /** Fraction of the pages bound to the left. 0 = a one-sided sample book. */
5213
- split: z25.number().min(0).max(1).default(0.5),
5474
+ split: z24.number().min(0).max(1).default(0.5),
5214
5475
  /** Page thickness — the gap between pages of one block. */
5215
- lift: z25.number().min(1e-3).max(0.05).default(8e-3),
5476
+ lift: z24.number().min(1e-3).max(0.05).default(8e-3),
5216
5477
  /** How much more a lifted page arcs than one lying flat in the block. */
5217
- gutter: z25.number().min(0).max(1).default(0.6)
5478
+ gutter: z24.number().min(0).max(1).default(0.6)
5218
5479
  });
5219
5480
  var book = {
5220
5481
  id: "book",
@@ -5241,11 +5502,11 @@ var book = {
5241
5502
  };
5242
5503
  }
5243
5504
  };
5244
- var accordionSchema = z25.object({
5505
+ var accordionSchema = z24.object({
5245
5506
  /** How far each panel tilts off the strip's line, degrees. 0 = flat, 90 = shut. */
5246
- angle: z25.number().min(0).max(89).default(55),
5507
+ angle: z24.number().min(0).max(89).default(55),
5247
5508
  /** A concertina holds its creases — how much bow the panels keep. */
5248
- slack: z25.number().min(0).max(1).default(0.15)
5509
+ slack: z24.number().min(0).max(1).default(0.15)
5249
5510
  });
5250
5511
  var accordion = {
5251
5512
  id: "accordion",
@@ -5264,16 +5525,16 @@ var accordion = {
5264
5525
  };
5265
5526
  }
5266
5527
  };
5267
- var rackSchema = z25.object({
5528
+ var rackSchema = z24.object({
5268
5529
  /** Gap along the row, as a fraction of the paper's width. Under 1 they overlap. */
5269
- spacing: z25.number().min(0.05).max(2).default(0.82),
5530
+ spacing: z24.number().min(0.05).max(2).default(0.82),
5270
5531
  /** How far a sheet leans back off vertical, degrees. */
5271
- lean: z25.number().min(0).max(70).default(16),
5532
+ lean: z24.number().min(0).max(70).default(16),
5272
5533
  /** How much that lean differs sheet to sheet — nothing propped is uniform. */
5273
- vary: z25.number().min(0).max(1).default(0.55),
5534
+ vary: z24.number().min(0).max(1).default(0.55),
5274
5535
  /** Small rotations off square. */
5275
- sway: z25.number().min(0).max(1).default(0.35),
5276
- seed: z25.number().int().min(0).max(9999).default(5)
5536
+ sway: z24.number().min(0).max(1).default(0.35),
5537
+ seed: z24.number().int().min(0).max(9999).default(5)
5277
5538
  });
5278
5539
  var rack = {
5279
5540
  id: "rack",
@@ -5297,19 +5558,19 @@ var rack = {
5297
5558
  };
5298
5559
  }
5299
5560
  };
5300
- var colonnadeSchema = z25.object({
5561
+ var colonnadeSchema = z24.object({
5301
5562
  /** The walk the colonnade is built along — see `stage/path`. */
5302
5563
  path: walkPathSchema.default({}),
5303
5564
  /** Half-width of the clear aisle: how far each banner stands off the walk line. */
5304
- aisle: z25.number().min(0.2).max(20).default(2.4),
5565
+ aisle: z24.number().min(0.2).max(20).default(2.4),
5305
5566
  /** How much that gap opens and closes along the walk. Nothing hung by hand is a corridor. */
5306
- breathe: z25.number().min(0).max(1).default(0.3),
5567
+ breathe: z24.number().min(0).max(1).default(0.3),
5307
5568
  /** Widest angle a banner turns off square to the aisle, degrees. */
5308
- twist: z25.number().min(0).max(90).default(22),
5569
+ twist: z24.number().min(0).max(90).default(22),
5309
5570
  /** Fraction of the walk left clear at each end, so the figure has somewhere to enter from. */
5310
- margin: z25.number().min(0).max(0.45).default(0.05),
5571
+ margin: z24.number().min(0).max(0.45).default(0.05),
5311
5572
  /** Spread of banner heights, 0..1. */
5312
- rise: z25.number().min(0).max(1).default(0.28),
5573
+ rise: z24.number().min(0).max(1).default(0.28),
5313
5574
  /**
5314
5575
  * How far the banners lift off the floor, as a fraction of their height.
5315
5576
  *
@@ -5323,10 +5584,10 @@ var colonnadeSchema = z25.object({
5323
5584
  * The bound used to be 0, so the one thing this option documented itself
5324
5585
  * as doing was the one thing it could not do.
5325
5586
  */
5326
- hover: z25.number().min(-0.5).max(1).default(0),
5587
+ hover: z24.number().min(-0.5).max(1).default(0),
5327
5588
  /** Spread of deformation — no two lengths of hung paper drape alike. */
5328
- drape: z25.number().min(0).max(1).default(0.5),
5329
- seed: z25.number().int().min(0).max(9999).default(2)
5589
+ drape: z24.number().min(0).max(1).default(0.5),
5590
+ seed: z24.number().int().min(0).max(9999).default(2)
5330
5591
  });
5331
5592
  function colonnadeStop(i, n, margin) {
5332
5593
  const side = i % 2 === 0 ? 1 : -1;
@@ -5401,10 +5662,10 @@ registerLayout(colonnade);
5401
5662
  registerLayout(sheet);
5402
5663
 
5403
5664
  // src/PaperField.tsx
5404
- import * as THREE15 from "three";
5665
+ import * as THREE16 from "three";
5405
5666
  import { gsap as gsap5 } from "gsap";
5406
- import { Canvas, useFrame as useFrame5, useThree as useThree4 } from "@react-three/fiber";
5407
- import { forwardRef as forwardRef2, useEffect as useEffect12, useMemo as useMemo9, useRef as useRef8 } from "react";
5667
+ import { Canvas, useFrame as useFrame5, useThree as useThree5 } from "@react-three/fiber";
5668
+ import { forwardRef as forwardRef2, useEffect as useEffect13, useMemo as useMemo9, useRef as useRef8 } from "react";
5408
5669
 
5409
5670
  // src/field/slots.ts
5410
5671
  var EMPTY_SET = /* @__PURE__ */ new Set();
@@ -5452,24 +5713,24 @@ function resolveFieldSlotConfig(slot, fallback, index, layoutId, layoutOptions)
5452
5713
  }
5453
5714
 
5454
5715
  // src/field/fieldGroup.tsx
5455
- import * as THREE12 from "three";
5716
+ import * as THREE13 from "three";
5456
5717
  import { gsap as gsap3 } from "gsap";
5457
5718
  import { useFrame as useFrame3 } from "@react-three/fiber";
5458
- import { useEffect as useEffect9, useMemo as useMemo6, useRef as useRef6 } from "react";
5719
+ import { useEffect as useEffect10, useMemo as useMemo6, useRef as useRef6 } from "react";
5459
5720
  import CustomShaderMaterial2 from "three-custom-shader-material";
5460
5721
 
5461
5722
  // src/content/atlas.ts
5462
- import * as THREE11 from "three";
5463
- import { useEffect as useEffect8, useState as useState4 } from "react";
5723
+ import * as THREE12 from "three";
5724
+ import { useEffect as useEffect9, useState as useState5 } from "react";
5464
5725
  var MAX_ATLAS = 4096;
5465
5726
  function atlasGrid(count, aspect = 1) {
5466
5727
  const cols = Math.max(1, Math.min(count, Math.ceil(Math.sqrt(count * Math.max(aspect, 0.01)))));
5467
5728
  return { cols, rows: Math.max(1, Math.ceil(count / cols)) };
5468
5729
  }
5469
5730
  function useContentAtlas(contents, sheet2, stock) {
5470
- const [atlas, setAtlas] = useState4(null);
5731
+ const [atlas, setAtlas] = useState5(null);
5471
5732
  const stableContents = useStable(contents);
5472
- useEffect8(() => {
5733
+ useEffect9(() => {
5473
5734
  let disposed = false;
5474
5735
  const aspect = sheet2.height / sheet2.width;
5475
5736
  const { cols, rows } = atlasGrid(contents.length, aspect);
@@ -5485,8 +5746,8 @@ function useContentAtlas(contents, sheet2, stock) {
5485
5746
  const ctx = canvas.getContext("2d");
5486
5747
  ctx.fillStyle = stock.color;
5487
5748
  ctx.fillRect(0, 0, canvas.width, canvas.height);
5488
- const texture = new THREE11.CanvasTexture(canvas);
5489
- texture.colorSpace = THREE11.SRGBColorSpace;
5749
+ const texture = new THREE12.CanvasTexture(canvas);
5750
+ texture.colorSpace = THREE12.SRGBColorSpace;
5490
5751
  texture.anisotropy = 4;
5491
5752
  setAtlas({ texture, cols, rows });
5492
5753
  const drawTile = (index, tile) => {
@@ -5655,7 +5916,7 @@ function fieldShapeStack(config, progress) {
5655
5916
 
5656
5917
  // src/field/fieldGroup.tsx
5657
5918
  import { jsx as jsx7 } from "react/jsx-runtime";
5658
- var scratchObj = new THREE12.Object3D();
5919
+ var scratchObj = new THREE13.Object3D();
5659
5920
  var scratchAero = { position: [0, 0, 0], rotation: [0, 0, 0] };
5660
5921
  var FIELD_SEGMENT_CAP = 48;
5661
5922
  var FIELD_AUTO_CEILING = 128;
@@ -5716,28 +5977,28 @@ function FieldGroup({
5716
5977
  atlasIdx[i] = i;
5717
5978
  phase[i] = indices[i] * 0.618034 % 1 * 4;
5718
5979
  }
5719
- geo.setAttribute("aAtlas", new THREE12.InstancedBufferAttribute(atlasIdx, 1));
5720
- geo.setAttribute("aPhase", new THREE12.InstancedBufferAttribute(phase, 1));
5721
- geo.setAttribute("aBias", new THREE12.InstancedBufferAttribute(bias, 1));
5980
+ geo.setAttribute("aAtlas", new THREE13.InstancedBufferAttribute(atlasIdx, 1));
5981
+ geo.setAttribute("aPhase", new THREE13.InstancedBufferAttribute(phase, 1));
5982
+ geo.setAttribute("aBias", new THREE13.InstancedBufferAttribute(bias, 1));
5722
5983
  return geo;
5723
5984
  }, [JSON.stringify(config.sheet), structureKey, count, autoCeiling]);
5724
- useEffect9(() => () => geometry.dispose(), [geometry]);
5985
+ useEffect10(() => () => geometry.dispose(), [geometry]);
5725
5986
  const atlas = useContentAtlas(contents, config.sheet, stock);
5726
5987
  const shader = useMemo6(() => {
5727
5988
  const composed = buildDisplacementGLSL(initialStack, config.sheet);
5728
5989
  const uniforms = {};
5729
5990
  for (const [name, value] of Object.entries(composed.uniforms)) {
5730
5991
  uniforms[name] = {
5731
- value: Array.isArray(value) && value.length === 2 ? new THREE12.Vector2(...value) : value
5992
+ value: Array.isArray(value) && value.length === 2 ? new THREE13.Vector2(...value) : value
5732
5993
  };
5733
5994
  }
5734
5995
  uniforms.uPlTime = { value: 0 };
5735
5996
  uniforms.uAtlas = { value: null };
5736
- uniforms.uAtlasGrid = { value: new THREE12.Vector2(1, 1) };
5997
+ uniforms.uAtlasGrid = { value: new THREE13.Vector2(1, 1) };
5737
5998
  uniforms.uBackDarken = {
5738
5999
  value: 1 - Math.min(0.45, 0.12 + config.sheet.thickness * 0.9) * stock.opacity
5739
6000
  };
5740
- uniforms.uStockColor = { value: new THREE12.Color(stock.color) };
6001
+ uniforms.uStockColor = { value: new THREE13.Color(stock.color) };
5741
6002
  uniforms.uShowThrough = { value: config.surface.showThrough ?? stock.showThrough };
5742
6003
  Object.assign(uniforms, translucencyUniforms(config.surface.translucency ?? stock.translucency, rig));
5743
6004
  return {
@@ -5752,19 +6013,19 @@ function FieldGroup({
5752
6013
  config.surface.showThrough,
5753
6014
  config.surface.translucency
5754
6015
  ]);
5755
- useEffect9(() => {
6016
+ useEffect10(() => {
5756
6017
  const values = translucencyValues(config.surface.translucency ?? stock.translucency, rig);
5757
6018
  shader.uniforms.uTranslucency.value = values.translucency;
5758
6019
  shader.uniforms.uBackLightDir.value.copy(values.direction);
5759
6020
  shader.uniforms.uBackLightColor.value.copy(values.color);
5760
6021
  shader.uniforms.uAmbientTransmission.value = values.ambient;
5761
6022
  }, [shader, rig, config.surface.translucency, stock.translucency]);
5762
- useEffect9(() => {
6023
+ useEffect10(() => {
5763
6024
  if (!atlas) return;
5764
6025
  shader.uniforms.uAtlas.value = atlas.texture;
5765
6026
  shader.uniforms.uAtlasGrid.value.set(atlas.cols, atlas.rows);
5766
6027
  }, [atlas, shader]);
5767
- useEffect9(() => {
6028
+ useEffect10(() => {
5768
6029
  if (!behavior || shared.reduced) return;
5769
6030
  const state = { p: progressRef.current };
5770
6031
  const tween = gsap3.to(state, {
@@ -5792,7 +6053,7 @@ function FieldGroup({
5792
6053
  for (const [name, value] of Object.entries(values)) {
5793
6054
  const uniform = shader.uniforms[name];
5794
6055
  if (!uniform) continue;
5795
- if (uniform.value instanceof THREE12.Vector2 && Array.isArray(value)) {
6056
+ if (uniform.value instanceof THREE13.Vector2 && Array.isArray(value)) {
5796
6057
  uniform.value.set(value[0], value[1]);
5797
6058
  } else {
5798
6059
  uniform.value = value;
@@ -5876,13 +6137,13 @@ function FieldGroup({
5876
6137
  children: /* @__PURE__ */ jsx7(
5877
6138
  CustomShaderMaterial2,
5878
6139
  {
5879
- baseMaterial: THREE12.MeshStandardMaterial,
6140
+ baseMaterial: THREE13.MeshStandardMaterial,
5880
6141
  vertexShader: shader.vertexShader,
5881
6142
  fragmentShader: shader.fragmentShader,
5882
6143
  uniforms: shader.uniforms,
5883
6144
  roughness: stock.roughness,
5884
6145
  metalness: 0,
5885
- side: THREE12.DoubleSide
6146
+ side: THREE13.DoubleSide
5886
6147
  },
5887
6148
  `${structureKey}:${count}`
5888
6149
  )
@@ -5928,8 +6189,8 @@ var easeOut = (t) => 1 - (1 - t) ** 3;
5928
6189
  var easeInOut = (t) => t < 0.5 ? 2 * t * t : 1 - (-2 * t + 2) ** 2 / 2;
5929
6190
 
5930
6191
  // src/field/backingSheet.tsx
5931
- import * as THREE13 from "three";
5932
- import { useEffect as useEffect10, useMemo as useMemo7 } from "react";
6192
+ import * as THREE14 from "three";
6193
+ import { useEffect as useEffect11, useMemo as useMemo7 } from "react";
5933
6194
 
5934
6195
  // src/content/backing.ts
5935
6196
  function silhouetteRects(o, count) {
@@ -6010,14 +6271,14 @@ function BackingSheet({
6010
6271
  c.height = Math.max(2, Math.round(height * scale));
6011
6272
  return c;
6012
6273
  }, [width, height]);
6013
- const texture = useMemo7(() => canvas ? new THREE13.CanvasTexture(canvas) : null, [canvas]);
6274
+ const texture = useMemo7(() => canvas ? new THREE14.CanvasTexture(canvas) : null, [canvas]);
6014
6275
  const removedKey = [...removed].sort((a, b) => a - b).join(",");
6015
- useEffect10(() => {
6276
+ useEffect11(() => {
6016
6277
  if (!canvas || !texture) return;
6017
6278
  drawBacking(canvas, { options, count, tint: BACKING_TINT, removed });
6018
6279
  texture.needsUpdate = true;
6019
6280
  }, [canvas, texture, JSON.stringify(options), count, removedKey]);
6020
- useEffect10(() => () => texture?.dispose(), [texture]);
6281
+ useEffect11(() => () => texture?.dispose(), [texture]);
6021
6282
  return /* @__PURE__ */ jsxs4("mesh", { receiveShadow: true, children: [
6022
6283
  /* @__PURE__ */ jsx8("planeGeometry", { args: [width, height] }),
6023
6284
  /* @__PURE__ */ jsx8("meshStandardMaterial", { map: texture, color: "#ffffff", roughness: 0.92, metalness: 0 })
@@ -6025,10 +6286,10 @@ function BackingSheet({
6025
6286
  }
6026
6287
 
6027
6288
  // src/field/interactiveField.tsx
6028
- import * as THREE14 from "three";
6289
+ import * as THREE15 from "three";
6029
6290
  import { gsap as gsap4 } from "gsap";
6030
- import { useFrame as useFrame4, useThree as useThree3 } from "@react-three/fiber";
6031
- import { useContext as useContext3, useEffect as useEffect11, useMemo as useMemo8, useRef as useRef7, useState as useState5 } from "react";
6291
+ import { useFrame as useFrame4, useThree as useThree4 } from "@react-three/fiber";
6292
+ import { useContext as useContext3, useEffect as useEffect12, useMemo as useMemo8, useRef as useRef7, useState as useState6 } from "react";
6032
6293
  import { jsx as jsx9, jsxs as jsxs5 } from "react/jsx-runtime";
6033
6294
  var PICK_BEHAVIORS = /* @__PURE__ */ new Set(["peel", "carry"]);
6034
6295
  function InteractiveField(props) {
@@ -6038,12 +6299,12 @@ function InteractiveField(props) {
6038
6299
  const reduced = usePrefersReducedMotion(props.reducedMotion);
6039
6300
  const contextRegistry = useContext3(DropZoneContext);
6040
6301
  const registry4 = useMemo8(() => contextRegistry ?? new DropZoneRegistry(), [contextRegistry]);
6041
- const camera = useThree3((s) => s.camera);
6042
- const gl = useThree3((s) => s.gl);
6043
- const controls = useThree3((s) => s.controls);
6044
- const [removed, setRemoved] = useState5(EMPTY_SET);
6045
- const [slotPatches, setSlotPatches] = useState5({});
6046
- const [slotStates, setSlotStates] = useState5({});
6302
+ const camera = useThree4((s) => s.camera);
6303
+ const gl = useThree4((s) => s.gl);
6304
+ const controls = useThree4((s) => s.controls);
6305
+ const [removed, setRemoved] = useState6(EMPTY_SET);
6306
+ const [slotPatches, setSlotPatches] = useState6({});
6307
+ const [slotStates, setSlotStates] = useState6({});
6047
6308
  const slotConfigs = useMemo8(
6048
6309
  () => papers.map((slot, i) => {
6049
6310
  const config = resolveFieldSlotConfig(slot, fallback, i, layoutId, layoutOptions);
@@ -6078,10 +6339,10 @@ function InteractiveField(props) {
6078
6339
  }
6079
6340
  props.onSlotStateChange?.(i, state);
6080
6341
  };
6081
- const raycaster = useMemo8(() => new THREE14.Raycaster(), []);
6082
- const planeScratch = useMemo8(() => new THREE14.Plane(new THREE14.Vector3(0, 0, 1), 0), []);
6083
- const pointScratch = useMemo8(() => new THREE14.Vector3(), []);
6084
- const ndcScratch = useMemo8(() => new THREE14.Vector2(), []);
6342
+ const raycaster = useMemo8(() => new THREE15.Raycaster(), []);
6343
+ const planeScratch = useMemo8(() => new THREE15.Plane(new THREE15.Vector3(0, 0, 1), 0), []);
6344
+ const pointScratch = useMemo8(() => new THREE15.Vector3(), []);
6345
+ const ndcScratch = useMemo8(() => new THREE15.Vector2(), []);
6085
6346
  const planePoint = (clientX, clientY, planeZ) => {
6086
6347
  const rect = gl.domElement.getBoundingClientRect();
6087
6348
  ndcScratch.set(
@@ -6244,12 +6505,12 @@ function InteractiveField(props) {
6244
6505
  if (carried.pointerId !== null && e.pointerId !== carried.pointerId) return;
6245
6506
  const name = slotName(carried.slot);
6246
6507
  const zone = zonesLive().find(
6247
- (z26) => zoneContains(z26, carried.x.value, carried.y.value) && zoneAccepts(z26, name)
6508
+ (z25) => zoneContains(z25, carried.x.value, carried.y.value) && zoneAccepts(z25, name)
6248
6509
  );
6249
6510
  if (zone) settleInto(carried, zone);
6250
6511
  else returnHome(carried);
6251
6512
  };
6252
- useEffect11(() => {
6513
+ useEffect12(() => {
6253
6514
  const move = (e) => onPointerMoveRef.current(e);
6254
6515
  const up = (e) => onPointerUpRef.current(e);
6255
6516
  window.addEventListener("pointermove", move);
@@ -6285,12 +6546,12 @@ function InteractiveField(props) {
6285
6546
  group.position.z = carried.homePose.position[2] + 0.15;
6286
6547
  handle.set("drive", carryDrive(speed));
6287
6548
  const lag = 0.25;
6288
- group.rotation.y += (THREE14.MathUtils.clamp(-vx * lag, -0.6, 0.6) - group.rotation.y) * 0.12;
6289
- group.rotation.x += (THREE14.MathUtils.clamp(vy * lag * 0.7, -0.5, 0.5) - group.rotation.x) * 0.12;
6549
+ group.rotation.y += (THREE15.MathUtils.clamp(-vx * lag, -0.6, 0.6) - group.rotation.y) * 0.12;
6550
+ group.rotation.x += (THREE15.MathUtils.clamp(vy * lag * 0.7, -0.5, 0.5) - group.rotation.x) * 0.12;
6290
6551
  }
6291
6552
  const name = slotName(carried.slot);
6292
6553
  const zone = zonesLive().find(
6293
- (z26) => zoneContains(z26, group.position.x, group.position.y) && zoneAccepts(z26, name)
6554
+ (z25) => zoneContains(z25, group.position.x, group.position.y) && zoneAccepts(z25, name)
6294
6555
  );
6295
6556
  registry4.setHovered(zone?.id ?? null);
6296
6557
  const targetScale = (zone ? 1.03 : 1) * carried.homePose.scale;
@@ -6306,7 +6567,7 @@ function InteractiveField(props) {
6306
6567
  },
6307
6568
  placeAtZone: (slot, zoneId) => {
6308
6569
  const carried = carriedRef.current;
6309
- const zone = zonesLive().find((z26) => z26.id === zoneId);
6570
+ const zone = zonesLive().find((z25) => z25.id === zoneId);
6310
6571
  const group = groupRefs.current[slot];
6311
6572
  if (!carried || carried.slot !== slot || !zone || !group) return;
6312
6573
  group.position.x = zone.bounds.position[0];
@@ -6319,10 +6580,10 @@ function InteractiveField(props) {
6319
6580
  const carried = carriedRef.current;
6320
6581
  if (carried && carried.slot === slot && !carried.settling) returnHome(carried);
6321
6582
  },
6322
- zoneIds: () => zonesLive().map((z26) => z26.id),
6583
+ zoneIds: () => zonesLive().map((z25) => z25.id),
6323
6584
  slotState: (slot) => slotStates[slot] ?? "rest"
6324
6585
  };
6325
- useEffect11(() => {
6586
+ useEffect12(() => {
6326
6587
  const ref = props.a11yRef;
6327
6588
  if (!ref) return;
6328
6589
  ref.current = {
@@ -6375,7 +6636,7 @@ function InteractiveField(props) {
6375
6636
  }
6376
6637
 
6377
6638
  // src/field/keyboardMirror.tsx
6378
- import { useState as useState6 } from "react";
6639
+ import { useState as useState7 } from "react";
6379
6640
  import { jsx as jsx10, jsxs as jsxs6 } from "react/jsx-runtime";
6380
6641
  function fieldKeyboardStep(carry2, slot, key, controller) {
6381
6642
  if (!carry2) {
@@ -6421,7 +6682,7 @@ function FieldKeyboardMirror({
6421
6682
  papers,
6422
6683
  controller
6423
6684
  }) {
6424
- const [carrying, setCarrying] = useState6(null);
6685
+ const [carrying, setCarrying] = useState7(null);
6425
6686
  const paperLabel = (slot, i) => {
6426
6687
  try {
6427
6688
  const config = resolveConfig({ preset: slot.preset });
@@ -6553,11 +6814,11 @@ var PaperFieldMesh = forwardRef2(
6553
6814
  const mountTimeRef = useRef8(-1);
6554
6815
  const morphRef = useRef8({ from: null, t: 1 });
6555
6816
  const prevLayout = useRef8({ id: layoutId, options: layoutOptions });
6556
- const gl = useThree4((s) => s.gl);
6817
+ const gl = useThree5((s) => s.gl);
6557
6818
  const driver = reduced ? "none" : props.motion?.driver ?? "autoplay";
6558
6819
  const speed = props.motion?.speed ?? 0.5;
6559
6820
  const entranceType = reduced ? "none" : props.entrance?.type ?? "rise";
6560
- useEffect12(() => {
6821
+ useEffect13(() => {
6561
6822
  const prev = prevLayout.current;
6562
6823
  if (prev.id !== layoutId || JSON.stringify(prev.options) !== JSON.stringify(layoutOptions)) {
6563
6824
  morphRef.current = { from: prev, t: 0 };
@@ -6565,7 +6826,7 @@ var PaperFieldMesh = forwardRef2(
6565
6826
  prevLayout.current = { id: layoutId, options: layoutOptions };
6566
6827
  }
6567
6828
  }, [layoutId, layoutOptions]);
6568
- useEffect12(() => {
6829
+ useEffect13(() => {
6569
6830
  if (driver !== "drag") return;
6570
6831
  const el = gl.domElement;
6571
6832
  let lastX = null;
@@ -6649,9 +6910,9 @@ var PaperFieldMesh = forwardRef2(
6649
6910
  }
6650
6911
  );
6651
6912
  function FitCamera(meshProps) {
6652
- const camera = useThree4((s) => s.camera);
6653
- const width = useThree4((s) => s.size.width);
6654
- const height = useThree4((s) => s.size.height);
6913
+ const camera = useThree5((s) => s.camera);
6914
+ const width = useThree5((s) => s.size.width);
6915
+ const height = useThree5((s) => s.size.height);
6655
6916
  const papersProp = useStable(meshProps.papers ?? null);
6656
6917
  const imagesProp = useStable(meshProps.images ?? null);
6657
6918
  const presetProp = useStable(meshProps.preset ?? null);
@@ -6664,8 +6925,8 @@ function FitCamera(meshProps) {
6664
6925
  const options = resolveLayoutOptions(layoutId, layout, optionsProp, sheet2);
6665
6926
  return { layout, n: papers.length, options, sheet: sheet2 ?? DEFAULT_SHEET };
6666
6927
  }, [papersProp, imagesProp, presetProp, meshProps.layout, optionsProp]);
6667
- useEffect12(() => {
6668
- if (!(camera instanceof THREE15.PerspectiveCamera)) return;
6928
+ useEffect13(() => {
6929
+ if (!(camera instanceof THREE16.PerspectiveCamera)) return;
6669
6930
  const { position, target } = fitCamera(
6670
6931
  field.layout,
6671
6932
  field.n,
@@ -6680,7 +6941,8 @@ function FitCamera(meshProps) {
6680
6941
  }, [camera, width, height, field]);
6681
6942
  return null;
6682
6943
  }
6683
- var PaperField = forwardRef2(function PaperField2({ children, className, style, ...meshProps }, ref) {
6944
+ var PaperField = forwardRef2(function PaperField2({ children, className, style, scene, ...meshProps }, ref) {
6945
+ const rig = sceneSchema.parse(scene ?? {});
6684
6946
  const registry4 = useMemo9(() => new DropZoneRegistry(), []);
6685
6947
  const a11yRef = useRef8(null);
6686
6948
  const papers = useMemo9(
@@ -6696,17 +6958,8 @@ var PaperField = forwardRef2(function PaperField2({ children, className, style,
6696
6958
  return /* @__PURE__ */ jsx11("div", { className, style: { width: "100%", height: "100%", ...style }, children: /* @__PURE__ */ jsxs7(DropZoneContext.Provider, { value: registry4, children: [
6697
6959
  /* @__PURE__ */ jsxs7(Canvas, { shadows: true, camera: { position: [0, 0.6, 5.2], fov: 45 }, dpr: [1, 2], children: [
6698
6960
  /* @__PURE__ */ jsx11(FitCamera, { ...meshProps }),
6699
- /* @__PURE__ */ jsx11("ambientLight", { intensity: 0.7 }),
6700
- /* @__PURE__ */ jsx11(
6701
- "directionalLight",
6702
- {
6703
- position: [3, 5, 4],
6704
- intensity: 1.4,
6705
- castShadow: true,
6706
- "shadow-mapSize": [1024, 1024],
6707
- "shadow-normalBias": 0.05
6708
- }
6709
- ),
6961
+ /* @__PURE__ */ jsx11(PaperBackdrop, { backdrop: rig.backdrop }),
6962
+ /* @__PURE__ */ jsx11(PaperLighting, { preset: rig.lighting, light: rig.light, floor: -2.4, scale: 14 }),
6710
6963
  /* @__PURE__ */ jsx11(PaperFieldMesh, { ref, a11yControllerRef: a11yRef, ...meshProps }),
6711
6964
  children
6712
6965
  ] }),
@@ -6758,7 +7011,8 @@ function diffConfig(config) {
6758
7011
  } else if (config.physics !== "none") {
6759
7012
  out.physics = config.physics;
6760
7013
  }
6761
- if (config.scene.lighting !== "studio") out.scene = { lighting: config.scene.lighting };
7014
+ const scene = diffAgainst(config.scene, sceneSchema.parse({}));
7015
+ if (Object.keys(scene).length > 0) out.scene = scene;
6762
7016
  if (config.onTwos) out.onTwos = true;
6763
7017
  if (config.states) out.states = config.states;
6764
7018
  const meta = diffAgainst(config.meta, paperConfigSchema.parse({}).meta);
@@ -6769,14 +7023,35 @@ function jsxValue(value) {
6769
7023
  if (typeof value === "string") return `"${value}"`;
6770
7024
  return `{${JSON.stringify(value)}}`;
6771
7025
  }
7026
+ function withoutUploads(value) {
7027
+ let replaced = 0;
7028
+ const walk = (node) => {
7029
+ if (typeof node === "string") {
7030
+ if (!node.startsWith("data:")) return node;
7031
+ replaced++;
7032
+ const extension = node.startsWith("data:image/png") ? "png" : "jpg";
7033
+ return `/paperlab-image-${replaced}.${extension}`;
7034
+ }
7035
+ if (Array.isArray(node)) return node.map(walk);
7036
+ if (node && typeof node === "object") {
7037
+ return Object.fromEntries(Object.entries(node).map(([k, v]) => [k, walk(v)]));
7038
+ }
7039
+ return node;
7040
+ };
7041
+ return { value: walk(value), replaced };
7042
+ }
7043
+ var UPLOAD_NOTE = "// Uploaded pictures cannot travel in a snippet \u2014 the paths below are\n// stand-ins, in order. Point them at your own files.";
6772
7044
  function buildJsxSnippet(config) {
6773
- const diff = diffConfig(config);
6774
- delete diff.meta;
7045
+ const diffed = diffConfig(config);
7046
+ delete diffed.meta;
7047
+ const { value: diff, replaced } = withoutUploads(diffed);
6775
7048
  const props = Object.entries(diff).map(([key, value]) => ` ${key}=${jsxValue(value)}`);
6776
7049
  if (props.length === 0) return "<Paper />";
6777
- return `<Paper
7050
+ const snippet = `<Paper
6778
7051
  ${props.join("\n")}
6779
7052
  />`;
7053
+ return replaced > 0 ? `${UPLOAD_NOTE}
7054
+ ${snippet}` : snippet;
6780
7055
  }
6781
7056
 
6782
7057
  // src/config/agent-payload.ts
@@ -6826,7 +7101,10 @@ function componentName(config) {
6826
7101
  }
6827
7102
  function buildAgentPayload(config) {
6828
7103
  const name = componentName(config);
6829
- const preset = JSON.stringify(diffConfig(config), null, 2);
7104
+ const { value: stripped, replaced } = withoutUploads(diffConfig(config));
7105
+ const preset = JSON.stringify(stripped, null, 2);
7106
+ const uploadNote = replaced > 0 ? `${UPLOAD_NOTE}
7107
+ ` : "";
6830
7108
  return `Integrate a Paperlab paper component into this project. (paperlab agent-payload v${AGENT_PAYLOAD_VERSION})
6831
7109
 
6832
7110
  1. Install the dependencies:
@@ -6839,7 +7117,7 @@ function buildAgentPayload(config) {
6839
7117
  \`\`\`tsx
6840
7118
  import { Paper, type PaperConfigInput } from 'paperlab'
6841
7119
 
6842
- const preset = ${preset.replace(/\n/g, "\n")} satisfies PaperConfigInput
7120
+ ${uploadNote}const preset = ${preset.replace(/\n/g, "\n")} satisfies PaperConfigInput
6843
7121
 
6844
7122
  export function ${name}() {
6845
7123
  return <Paper preset={preset} />
@@ -6869,11 +7147,17 @@ export {
6869
7147
  segmentsForArc,
6870
7148
  segmentsForSine,
6871
7149
  stockNames,
7150
+ washSchema,
7151
+ contentNames,
7152
+ contentSchemaFor,
6872
7153
  paperEdges,
6873
7154
  behaviorConfigSchema,
6874
7155
  physicsNames,
6875
7156
  clothConfigSchema,
6876
7157
  lightingNames,
7158
+ lightSchema,
7159
+ backdropSchema,
7160
+ sceneSchema,
6877
7161
  coreStateNames,
6878
7162
  stateDefSchema,
6879
7163
  paperStatesSchema,
@@ -6897,7 +7181,6 @@ export {
6897
7181
  getBehavior,
6898
7182
  listBehaviors,
6899
7183
  idleNames,
6900
- lightSchema,
6901
7184
  lightAngles,
6902
7185
  resolveLighting,
6903
7186
  LightRig,
@@ -6913,6 +7196,7 @@ export {
6913
7196
  PaperMesh,
6914
7197
  cssColorOr,
6915
7198
  PaperLighting,
7199
+ PaperBackdrop,
6916
7200
  DropZone,
6917
7201
  sheetLayoutSchema,
6918
7202
  buildDisplacementGLSL,
@@ -6930,4 +7214,4 @@ export {
6930
7214
  describeConfig,
6931
7215
  buildAgentPayload
6932
7216
  };
6933
- //# sourceMappingURL=chunk-4ZU5DZEF.js.map
7217
+ //# sourceMappingURL=chunk-6IADJZX5.js.map