paperlab 0.3.1 → 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.
package/dist/index.cjs CHANGED
@@ -30,203 +30,85 @@ var __toCommonJS = (mod2) => __copyProps(__defProp({}, "__esModule", { value: tr
30
30
  // src/index.ts
31
31
  var index_exports = {};
32
32
  __export(index_exports, {
33
- AGENT_PAYLOAD_VERSION: () => AGENT_PAYLOAD_VERSION,
34
33
  AUTO_CEILING: () => AUTO_CEILING,
35
- ClothSim: () => ClothSim,
36
34
  DropZone: () => DropZone,
37
- DropZoneRegistry: () => DropZoneRegistry,
38
35
  FLAT_SEGMENTS: () => FLAT_SEGMENTS,
39
36
  LightRig: () => LightRig,
40
- ON_TWOS_FPS: () => ON_TWOS_FPS,
41
37
  PARITY_EPSILON: () => PARITY_EPSILON,
42
38
  Paper: () => Paper,
43
- PaperFallback: () => PaperFallback,
39
+ PaperBackdrop: () => PaperBackdrop,
44
40
  PaperField: () => PaperField,
45
41
  PaperFieldMesh: () => PaperFieldMesh,
46
42
  PaperLighting: () => PaperLighting,
47
- PaperMaterial: () => PaperMaterial,
48
43
  PaperMesh: () => PaperMesh,
49
- PaperMirror: () => PaperMirror,
50
- PaperStateMachine: () => PaperStateMachine,
51
44
  SAG_TOL: () => SAG_TOL,
52
- SHEET_LIFT: () => SHEET_LIFT,
53
- TRANSMISSION_GAIN: () => TRANSMISSION_GAIN,
54
- accordion: () => accordion,
55
- applyDeformerStack: () => applyDeformerStack,
56
- atlasGrid: () => atlasGrid,
57
- backContentSchema: () => backContentSchema,
58
- barcodeBars: () => barcodeBars,
45
+ backdropSchema: () => backdropSchema,
59
46
  behaviorConfigSchema: () => behaviorConfigSchema,
60
- bend: () => bend,
61
- bendOptionsSchema: () => bendOptionsSchema,
62
- book: () => book,
63
47
  buildAgentPayload: () => buildAgentPayload,
64
- buildDisplacementGLSL: () => buildDisplacementGLSL,
65
48
  buildFieldAgentPayload: () => buildFieldAgentPayload,
66
49
  buildFieldComponentSource: () => buildFieldComponentSource,
67
- buildFieldFragmentShader: () => buildFieldFragmentShader,
68
- buildFieldVertexShader: () => buildFieldVertexShader,
69
50
  buildJsxSnippet: () => buildJsxSnippet,
70
- cardContentSchema: () => cardContentSchema,
71
- carry: () => carry,
72
- carryDrive: () => carryDrive,
73
- carryOptionsSchema: () => carryOptionsSchema,
74
51
  clothConfigSchema: () => clothConfigSchema,
75
- colonnade: () => colonnade,
76
- composeSurface: () => composeSurface,
77
- contentSchema: () => contentSchema,
78
- contentText: () => contentText,
52
+ contentNames: () => contentNames,
53
+ contentSchemaFor: () => contentSchemaFor,
79
54
  coreStateNames: () => coreStateNames,
80
- cornerNames: () => cornerNames,
81
- createSheetGeometry: () => createSheetGeometry,
82
- crumple: () => crumple,
83
- crumpleBehavior: () => crumpleBehavior,
84
- crumpleBehaviorOptionsSchema: () => crumpleBehaviorOptionsSchema,
85
- crumpleOptionsSchema: () => crumpleOptionsSchema,
86
- curl: () => curl,
87
- curlOptionsSchema: () => curlOptionsSchema,
88
- dampTo: () => dampTo,
89
- deformerInstanceSchema: () => deformerInstanceSchema,
90
55
  describeConfig: () => describeConfig,
91
56
  describeFieldConfig: () => describeFieldConfig,
92
57
  diffConfig: () => diffConfig,
93
58
  diffFieldProps: () => diffFieldProps,
94
- displacePoint: () => displacePoint,
95
- distinctFieldPresets: () => distinctFieldPresets,
96
- drape: () => drape,
97
- drapeOptionsSchema: () => drapeOptionsSchema,
98
- drawBacking: () => drawBacking,
99
- fall: () => fall,
100
- fallOptionsSchema: () => fallOptionsSchema,
101
- fan: () => fan,
102
- fieldBounds: () => fieldBounds,
103
- fieldKeyboardStep: () => fieldKeyboardStep,
104
- filmNames: () => filmNames,
105
- fitCamera: () => fitCamera,
106
- flattenNumeric: () => flattenNumeric,
107
- flight: () => flight,
108
- flightOptionsSchema: () => flightOptionsSchema,
109
- flightPose: () => flightPose,
110
- flip: () => flip,
111
- flipOptionsSchema: () => flipOptionsSchema,
112
- fly: () => fly,
113
- flyOptionsSchema: () => flyOptionsSchema,
114
- fold: () => fold,
115
- foldOptionsSchema: () => foldOptionsSchema,
116
59
  getBehavior: () => getBehavior,
117
60
  getDeformer: () => getDeformer,
118
- getIdlePreset: () => getIdlePreset,
119
61
  getLayout: () => getLayout,
120
- getLightingPreset: () => getLightingPreset,
121
62
  getPreset: () => getPreset,
122
63
  getStock: () => getStock,
123
- groupFieldPapers: () => groupFieldPapers,
124
- gust: () => gust,
125
- hang: () => hang,
126
- hangOptionsSchema: () => hangOptionsSchema,
127
64
  idleNames: () => idleNames,
128
- idlePresets: () => idlePresets,
129
65
  isBuiltinPreset: () => isBuiltinPreset,
130
- letterFold: () => letterFold,
131
- letterFoldOptionsSchema: () => letterFoldOptionsSchema,
132
66
  lightAngles: () => lightAngles,
133
- lightPosition: () => lightPosition,
134
67
  lightSchema: () => lightSchema,
135
- lightenHex: () => lightenHex,
136
68
  lightingNames: () => lightingNames,
137
- lightingPresets: () => lightingPresets,
138
69
  listBehaviors: () => listBehaviors,
139
70
  listDeformers: () => listDeformers,
140
71
  listLayouts: () => listLayouts,
141
72
  listPresets: () => listPresets,
142
- makeGoboTexture: () => makeGoboTexture,
143
73
  mergeConfig: () => mergeConfig,
144
74
  mergeWithDeletes: () => mergeWithDeletes,
145
- outwardCorner: () => outwardCorner,
146
75
  paperConfigSchema: () => paperConfigSchema,
147
76
  paperEdges: () => paperEdges,
148
77
  paperStatesSchema: () => paperStatesSchema,
149
78
  parityCases: () => parityCases,
150
79
  parsePreset: () => parsePreset,
151
- peel: () => peel,
152
- peelOptionsSchema: () => peelOptionsSchema,
153
80
  physicsNames: () => physicsNames,
154
- physicsSchema: () => physicsSchema,
155
- pile: () => pile,
156
- quantizeProgress: () => quantizeProgress,
157
81
  quantizeSegments: () => quantizeSegments,
158
- quantizeTime: () => quantizeTime,
159
- rack: () => rack,
160
- receiptContentSchema: () => receiptContentSchema,
161
- receiptTotals: () => receiptTotals,
162
82
  recordStateOverride: () => recordStateOverride,
163
83
  registerBehavior: () => registerBehavior,
164
84
  registerDeformer: () => registerDeformer,
165
85
  registerLayout: () => registerLayout,
166
86
  registerPreset: () => registerPreset,
167
- resolveConfig: () => resolveConfig,
168
- resolveDeformerStack: () => resolveDeformerStack,
169
- resolveFieldSlotConfig: () => resolveFieldSlotConfig,
170
87
  resolveLighting: () => resolveLighting,
171
- resolveMode: () => resolveMode,
172
- resolveSegments: () => resolveSegments,
173
88
  resolveStateConfig: () => resolveStateConfig,
174
- ribbon: () => ribbon,
175
- ribbonOptionsSchema: () => ribbonOptionsSchema,
176
- ring: () => ring,
177
- roll: () => roll,
178
- rollOptionsSchema: () => rollOptionsSchema,
179
89
  runParityHarness: () => runParityHarness,
180
90
  sceneSchema: () => sceneSchema,
181
91
  segmentsForArc: () => segmentsForArc,
182
92
  segmentsForSine: () => segmentsForSine,
183
93
  serializePreset: () => serializePreset,
184
- settle: () => settle,
185
- settleOptionsSchema: () => settleOptionsSchema,
186
- sheet: () => sheet,
187
- sheetBackingSize: () => sheetBackingSize,
188
94
  sheetLayoutSchema: () => sheetLayoutSchema,
189
- sheetSchema: () => sheetSchema,
190
- sheetSlotXY: () => sheetSlotXY,
191
- silhouetteRects: () => silhouetteRects,
192
95
  spanAlong: () => spanAlong,
193
- spill: () => spill,
194
- spread: () => spread,
195
- stackAutoSegments: () => stackAutoSegments,
196
- stackMinSegments: () => stackMinSegments,
197
- stackUniformValues: () => stackUniformValues,
198
96
  stateDefSchema: () => stateDefSchema,
199
- stateEventTransitions: () => stateEventTransitions,
200
- stateTransitionSchema: () => stateTransitionSchema,
201
97
  stockNames: () => stockNames,
202
- stockSchema: () => stockSchema,
203
98
  stocks: () => stocks,
204
- stripStates: () => stripStates,
205
99
  supportsWebGL: () => supportsWebGL,
206
- surfaceSchema: () => surfaceSchema,
207
- sweep: () => sweep,
208
- tornEdgesOnDetach: () => tornEdgesOnDetach,
209
- translucencyUniforms: () => translucencyUniforms,
210
- translucencyValues: () => translucencyValues,
211
100
  uniquePresetName: () => uniquePresetName,
212
101
  unregisterPreset: () => unregisterPreset,
213
- unroll: () => unroll,
214
- unrollOptionsSchema: () => unrollOptionsSchema,
215
- useContentAtlas: () => useContentAtlas,
216
102
  usePaperStates: () => usePaperStates,
217
103
  usePrefersReducedMotion: () => usePrefersReducedMotion,
218
- wall: () => wall,
219
- wave: () => wave,
220
- waveOptionsSchema: () => waveOptionsSchema,
221
- withSheetCellFromPaper: () => withSheetCellFromPaper,
222
- wrapLines: () => wrapLines,
223
- zoneAccepts: () => zoneAccepts
104
+ washSchema: () => washSchema,
105
+ wrapLines: () => wrapLines
224
106
  });
225
107
  module.exports = __toCommonJS(index_exports);
226
108
 
227
109
  // src/Paper.tsx
228
- var import_fiber3 = require("@react-three/fiber");
229
- var import_react9 = require("react");
110
+ var import_fiber4 = require("@react-three/fiber");
111
+ var import_react10 = require("react");
230
112
 
231
113
  // src/PaperMesh.tsx
232
114
  var THREE7 = __toESM(require("three"), 1);
@@ -1147,6 +1029,31 @@ var stockNames = [
1147
1029
  "sticker"
1148
1030
  ];
1149
1031
  var stockSchema = import_zod14.z.enum(stockNames);
1032
+ var washSchema = import_zod14.z.object({
1033
+ /** The first pigment. */
1034
+ color: import_zod14.z.string().default("#4a5b8c").describe("color"),
1035
+ /** The second. Blooms alternate, and overlaps multiply into a third. */
1036
+ secondary: import_zod14.z.string().default("#b06a6a").describe("color"),
1037
+ /** How many pools of colour. */
1038
+ blooms: import_zod14.z.number().int().min(1).max(24).default(7),
1039
+ /** How far a pool runs before it dries — its size against the sheet. */
1040
+ spread: import_zod14.z.number().min(0.1).max(1).default(0.7),
1041
+ /** Softness of the wet edge. 0 is a hard cut, 1 is a pool still moving. */
1042
+ bleed: import_zod14.z.number().min(0).max(1).default(0.5),
1043
+ /** How much pigment is in the water. */
1044
+ intensity: import_zod14.z.number().min(0).max(1).default(0.55),
1045
+ /**
1046
+ * Edge darkening — the ring of pigment left where a pool dried.
1047
+ *
1048
+ * The signature of the medium, and the one thing a plain gradient cannot
1049
+ * fake. Without it a wash reads as an airbrush.
1050
+ */
1051
+ edge: import_zod14.z.number().min(0).max(1).default(0.6),
1052
+ /** Pigment settling into the tooth of the paper. */
1053
+ granulation: import_zod14.z.number().min(0).max(1).default(0.35),
1054
+ /** Fixed so a preset paints the same wash every time. */
1055
+ seed: import_zod14.z.number().int().min(0).max(99).default(0)
1056
+ });
1150
1057
  var blankContentBase = import_zod14.z.object({
1151
1058
  type: import_zod14.z.literal("blank")
1152
1059
  });
@@ -1171,7 +1078,7 @@ var textContentBase = import_zod14.z.object({
1171
1078
  /** px at texture resolution (long edge = 1024 logical px before DPR). */
1172
1079
  size: import_zod14.z.number().min(8).max(256).default(44),
1173
1080
  weight: import_zod14.z.number().min(100).max(900).default(400),
1174
- color: import_zod14.z.string().default("#2b2620"),
1081
+ color: import_zod14.z.string().default("#2b2620").describe("color"),
1175
1082
  align: import_zod14.z.enum(["left", "center", "right"]).default("left"),
1176
1083
  /** Fraction of the short edge. */
1177
1084
  padding: import_zod14.z.number().min(0).max(0.4).default(0.09),
@@ -1221,7 +1128,7 @@ var cardContentBase = import_zod14.z.object({
1221
1128
  * was cropped rather than as a card that was set.
1222
1129
  */
1223
1130
  size: import_zod14.z.number().min(8).max(256).default(58),
1224
- color: import_zod14.z.string().default("#2b2620"),
1131
+ color: import_zod14.z.string().default("#2b2620").describe("color"),
1225
1132
  align: import_zod14.z.enum(["left", "center"]).default("left"),
1226
1133
  padding: import_zod14.z.number().min(0).max(0.4).default(0.1)
1227
1134
  });
@@ -1240,14 +1147,15 @@ var receiptContentBase = import_zod14.z.object({
1240
1147
  timestamp: import_zod14.z.string().optional(),
1241
1148
  footer: import_zod14.z.string().default("KEEP FOR YOUR RECORDS")
1242
1149
  });
1150
+ var withWash = { wash: washSchema.optional() };
1243
1151
  var backContentSchema = import_zod14.z.discriminatedUnion("type", [
1244
- blankContentBase,
1245
- imageContentBase,
1246
- textContentBase,
1247
- cardContentBase,
1248
- receiptContentBase
1152
+ blankContentBase.extend(withWash),
1153
+ imageContentBase.extend(withWash),
1154
+ textContentBase.extend(withWash),
1155
+ cardContentBase.extend(withWash),
1156
+ receiptContentBase.extend(withWash)
1249
1157
  ]);
1250
- var withBack = { back: backContentSchema.optional() };
1158
+ var withBack = { back: backContentSchema.optional(), ...withWash };
1251
1159
  var blankContentSchema = blankContentBase.extend(withBack);
1252
1160
  var imageContentSchema = imageContentBase.extend(withBack);
1253
1161
  var textContentSchema = textContentBase.extend(withBack);
@@ -1260,6 +1168,14 @@ var contentSchema = import_zod14.z.discriminatedUnion("type", [
1260
1168
  cardContentSchema,
1261
1169
  receiptContentSchema
1262
1170
  ]);
1171
+ var contentNames = contentSchema.options.map(
1172
+ (option) => option.shape.type.value
1173
+ );
1174
+ function contentSchemaFor(type) {
1175
+ const option = contentSchema.options.find((candidate) => candidate.shape.type.value === type);
1176
+ if (!option) throw new Error(`Unknown content type: ${type}`);
1177
+ return option;
1178
+ }
1263
1179
  var paperEdges = ["top", "right", "bottom", "left"];
1264
1180
  var surfaceSchema = import_zod14.z.object({
1265
1181
  /** Paper fiber noise, 0..1. */
@@ -1347,8 +1263,70 @@ var lightingNames = [
1347
1263
  "lightbox"
1348
1264
  ];
1349
1265
  var filmNames = ["agx", "neutral", "filmic"];
1266
+ var lightSchema = import_zod14.z.object({
1267
+ /** Tone-mapping exposure — the stop the whole picture is printed at. */
1268
+ exposure: import_zod14.z.number().min(0.1).max(4).optional(),
1269
+ /**
1270
+ * The tone curve — the film, where `exposure` is the stop.
1271
+ *
1272
+ * `filmic` is ACES, which is what every preset used to be pinned to and is
1273
+ * kept so a scene tuned against it can say so. On near-white paper it is
1274
+ * the wrong film: it desaturates and drags bright neutrals toward
1275
+ * yellow-green, which is the sepia cast a lit sheet used to pick up.
1276
+ */
1277
+ film: import_zod14.z.enum(filmNames).optional(),
1278
+ /** Key light strength. */
1279
+ key: import_zod14.z.number().min(0).max(12).optional(),
1280
+ /** Key light colour. */
1281
+ color: import_zod14.z.string().optional().describe("color"),
1282
+ /**
1283
+ * Where the key stands, degrees around the vertical. 0° is straight in
1284
+ * front of the paper (+Z, beside the camera), 90° is off to the right,
1285
+ * and ±180° is directly behind it — which is where `nave` puts it, and
1286
+ * why that preset is carried by light coming THROUGH the paper.
1287
+ */
1288
+ direction: import_zod14.z.number().min(-180).max(180).optional(),
1289
+ /** How high the key stands, degrees above the horizon. */
1290
+ height: import_zod14.z.number().min(-30).max(89).optional(),
1291
+ /** Flat fill from every direction at once. Cheap, and it kills form — reach for `studio` first. */
1292
+ ambient: import_zod14.z.number().min(0).max(2).optional(),
1293
+ /** The room's own light: an environment map built from `sky`. Directional fill, and the only thing paper's sheen has to reflect. */
1294
+ studio: import_zod14.z.number().min(0).max(3).optional(),
1295
+ /** Distance haze, as a multiple of the preset's. 0 clears the air entirely; 2 halves the distance you can see. */
1296
+ haze: import_zod14.z.number().min(0).max(3).optional()
1297
+ });
1298
+ var backdropSchema = import_zod14.z.object({
1299
+ /** Behind everything, and behind the picture where it does not reach. */
1300
+ color: import_zod14.z.string().default("#171717").describe("color"),
1301
+ /** A URL, or an uploaded picture. Empty is the colour on its own. */
1302
+ image: import_zod14.z.string().default(""),
1303
+ fit: import_zod14.z.enum(["cover", "contain"]).default("cover"),
1304
+ /**
1305
+ * Toward the colour, so the paper stays the subject.
1306
+ *
1307
+ * A backdrop at full strength competes with the sheet in front of it —
1308
+ * which is what a real photographer solves by putting the background out
1309
+ * of the light, and what this solves by mixing it back toward the ground
1310
+ * it sits on.
1311
+ */
1312
+ fade: import_zod14.z.number().min(0).max(1).default(0.25),
1313
+ /** Out of focus, for the same reason. */
1314
+ blur: import_zod14.z.number().min(0).max(1).default(0.2)
1315
+ });
1350
1316
  var sceneSchema = import_zod14.z.object({
1351
- lighting: import_zod14.z.enum(lightingNames).default("studio")
1317
+ lighting: import_zod14.z.enum(lightingNames).default("studio"),
1318
+ /** What is behind the sheet. Unset leaves the canvas alone. */
1319
+ backdrop: backdropSchema.optional(),
1320
+ /**
1321
+ * Overrides on the named preset — the same authorable half stage mode has
1322
+ * always had, and which a lone sheet had no way to reach.
1323
+ *
1324
+ * A preset was the starting point everywhere EXCEPT here: a stage could be
1325
+ * "nave, but the sun is lower", and a `<Paper>` could only be one of seven
1326
+ * rigs exactly as shipped. `<PaperLighting>` has taken these overrides all
1327
+ * along; nothing was passing them.
1328
+ */
1329
+ light: lightSchema.default({})
1352
1330
  });
1353
1331
  var coreStateNames = ["rest", "hover", "pressed", "picked", "placed"];
1354
1332
  var isStateName = (s) => coreStateNames.includes(s) || s.startsWith("custom:");
@@ -1481,11 +1459,11 @@ function computeSheetNormals(geometry) {
1481
1459
  for (let i = 0, l = nrm.length; i < l; i += 3) {
1482
1460
  const x = nrm[i];
1483
1461
  const y = nrm[i + 1];
1484
- const z26 = nrm[i + 2];
1485
- const len = Math.sqrt(x * x + y * y + z26 * z26) || 1;
1462
+ const z25 = nrm[i + 2];
1463
+ const len = Math.sqrt(x * x + y * y + z25 * z25) || 1;
1486
1464
  nrm[i] = x / len;
1487
1465
  nrm[i + 1] = y / len;
1488
- nrm[i + 2] = z26 / len;
1466
+ nrm[i + 2] = z25 / len;
1489
1467
  }
1490
1468
  normalAttr.needsUpdate = true;
1491
1469
  }
@@ -1669,6 +1647,33 @@ var builtins = {
1669
1647
  behavior: { type: "letter-fold", progress: 0.4, crease: 0.3 },
1670
1648
  surface: { creaseLines: { angle: 0, positions: [1 / 3, 2 / 3], strength: 0.5 } }
1671
1649
  },
1650
+ /**
1651
+ * The wash, shown rather than described.
1652
+ *
1653
+ * A `wash` is a field on every content type, which means it is reachable
1654
+ * from any preset and discoverable from none — a toggle three folders down
1655
+ * is not an argument for the feature. This is the argument: type set over
1656
+ * paint, on cotton, deckled, with the paper still showing through both.
1657
+ */
1658
+ "washed-letter": {
1659
+ meta: { name: "Washed letter", tags: ["wash", "text"] },
1660
+ // Printer stock, not vellum: vellum is translucent and takes the room's
1661
+ // grey through the back of the sheet, which turns a wash the colour of
1662
+ // dishwater. Pigment needs something white behind it.
1663
+ sheet: { width: 1.05, height: 1.45 },
1664
+ stock: "printer",
1665
+ content: {
1666
+ type: "text",
1667
+ text: "Painted first,\nwritten after.",
1668
+ font: 'Georgia, "Times New Roman", serif',
1669
+ size: 52,
1670
+ valign: "center",
1671
+ align: "center",
1672
+ wash: { color: "#5b6f9a", secondary: "#c08a86", blooms: 6, spread: 0.85, intensity: 0.5, seed: 3 }
1673
+ },
1674
+ behavior: { type: "peel", progress: 0.12, corner: "bottom-right", radius: 0.3 },
1675
+ surface: { grain: 0.35, deckle: { edges: ["bottom", "right"], roughness: 0.45 } }
1676
+ },
1672
1677
  "vintage-note": {
1673
1678
  meta: { name: "Vintage note", tags: ["aging", "text"] },
1674
1679
  sheet: { width: 1.1, height: 1.4 },
@@ -2109,6 +2114,109 @@ function paintCard(ctx, w, h, content, stock, dpr) {
2109
2114
  }
2110
2115
  }
2111
2116
 
2117
+ // src/content/wash.ts
2118
+ function mulberry32(seed) {
2119
+ let a = seed * 1831565813 + 2654435769;
2120
+ return () => {
2121
+ a |= 0;
2122
+ a = a + 1831565813 | 0;
2123
+ let t = Math.imul(a ^ a >>> 15, 1 | a);
2124
+ t = t + Math.imul(t ^ t >>> 7, 61 | t) ^ t;
2125
+ return ((t ^ t >>> 14) >>> 0) / 4294967296;
2126
+ };
2127
+ }
2128
+ function rgba(hex, alpha) {
2129
+ const m = /^#?([0-9a-f]{6})$/i.exec(hex.trim());
2130
+ const n = m ? Number.parseInt(m[1], 16) : 8421504;
2131
+ return `rgba(${n >> 16 & 255}, ${n >> 8 & 255}, ${n & 255}, ${Math.max(0, Math.min(1, alpha))})`;
2132
+ }
2133
+ function bloomShape(rng) {
2134
+ return {
2135
+ phase: [rng() * Math.PI * 2, rng() * Math.PI * 2, rng() * Math.PI * 2],
2136
+ amp: [0.18 + rng() * 0.1, 0.09 + rng() * 0.06, 0.05 + rng() * 0.04]
2137
+ };
2138
+ }
2139
+ function bloomPath(ctx, cx, cy, radius, shape, from = 0, to = 1) {
2140
+ const { phase, amp } = shape;
2141
+ const STEPS = Math.max(6, Math.round(72 * (to - from)));
2142
+ ctx.beginPath();
2143
+ for (let i = 0; i <= STEPS; i++) {
2144
+ const t = (from + (to - from) * i / STEPS) * Math.PI * 2;
2145
+ 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]);
2146
+ const r = radius * (1 + wobble);
2147
+ const x = cx + Math.cos(t) * r;
2148
+ const y = cy + Math.sin(t) * r;
2149
+ if (i === 0) ctx.moveTo(x, y);
2150
+ else ctx.lineTo(x, y);
2151
+ }
2152
+ if (from === 0 && to === 1) ctx.closePath();
2153
+ }
2154
+ function canBlur(ctx) {
2155
+ if (!("filter" in ctx)) return false;
2156
+ const before = ctx.filter;
2157
+ try {
2158
+ ctx.filter = "blur(2px)";
2159
+ const worked = ctx.filter !== "none" && ctx.filter !== before;
2160
+ ctx.filter = before;
2161
+ return worked;
2162
+ } catch {
2163
+ return false;
2164
+ }
2165
+ }
2166
+ function paintWash(ctx, w, h, wash) {
2167
+ const rng = mulberry32(wash.seed + 1);
2168
+ const short = Math.min(w, h);
2169
+ const blurs = canBlur(ctx);
2170
+ ctx.save();
2171
+ ctx.globalCompositeOperation = "multiply";
2172
+ for (let i = 0; i < wash.blooms; i++) {
2173
+ const pigment = i % 2 === 0 ? wash.color : wash.secondary;
2174
+ const cx = (rng() * 1.5 - 0.25) * w;
2175
+ const cy = (rng() * 1.5 - 0.25) * h;
2176
+ const radius = short * wash.spread * (0.28 + rng() * 0.34);
2177
+ const alpha = wash.intensity * (0.35 + rng() * 0.4);
2178
+ const shape = bloomShape(rng);
2179
+ ctx.save();
2180
+ if (blurs) ctx.filter = `blur(${(2 + wash.bleed * 26) * (0.6 + rng() * 0.8)}px)`;
2181
+ const gx = cx + (rng() - 0.5) * radius * 0.5;
2182
+ const gy = cy + (rng() - 0.5) * radius * 0.5;
2183
+ const grad = ctx.createRadialGradient(gx, gy, radius * 0.05, gx, gy, radius * 1.15);
2184
+ grad.addColorStop(0, rgba(pigment, alpha * 0.55));
2185
+ grad.addColorStop(0.62, rgba(pigment, alpha));
2186
+ grad.addColorStop(1, rgba(pigment, alpha * 0.75));
2187
+ ctx.fillStyle = grad;
2188
+ bloomPath(ctx, cx, cy, radius, shape);
2189
+ ctx.fill();
2190
+ if (wash.edge > 0) {
2191
+ if (blurs) ctx.filter = `blur(${(1.5 + wash.bleed * 9) * (0.6 + rng() * 0.6)}px)`;
2192
+ ctx.lineWidth = Math.max(1, radius * (0.03 + wash.edge * 0.05));
2193
+ const arcs = 5;
2194
+ const overlap = 0.4 / arcs;
2195
+ for (let a = 0; a < arcs; a++) {
2196
+ ctx.strokeStyle = rgba(pigment, alpha * wash.edge * (0.15 + rng() * 1.05));
2197
+ bloomPath(ctx, cx, cy, radius, shape, a / arcs, (a + 1) / arcs + overlap);
2198
+ ctx.stroke();
2199
+ }
2200
+ }
2201
+ ctx.restore();
2202
+ if (wash.granulation > 0) {
2203
+ ctx.save();
2204
+ bloomPath(ctx, cx, cy, radius, shape);
2205
+ ctx.clip();
2206
+ const grains = Math.round(wash.granulation * 900);
2207
+ ctx.fillStyle = rgba(pigment, wash.granulation * alpha * 0.5);
2208
+ for (let g = 0; g < grains; g++) {
2209
+ const a = rng() * Math.PI * 2;
2210
+ const d = Math.sqrt(rng()) * radius;
2211
+ const size = 1 + rng() * (short * 4e-3);
2212
+ ctx.fillRect(cx + Math.cos(a) * d, cy + Math.sin(a) * d, size, size);
2213
+ }
2214
+ ctx.restore();
2215
+ }
2216
+ }
2217
+ ctx.restore();
2218
+ }
2219
+
2112
2220
  // src/content/texture.ts
2113
2221
  var LONG_EDGE = 1024;
2114
2222
  var DPR = 2;
@@ -2159,6 +2267,7 @@ function renderContentToCanvas(content, sheet2, stock, image) {
2159
2267
  canvas.height = h;
2160
2268
  const ctx = canvas.getContext("2d");
2161
2269
  paintBackground(ctx, w, h, stock);
2270
+ if (content.wash) paintWash(ctx, w, h, content.wash);
2162
2271
  if (content.type === "image" && image && content.src) paintImage(ctx, w, h, image, content.fit);
2163
2272
  if (content.type === "text") paintText(ctx, w, h, content, stock);
2164
2273
  if (content.type === "receipt") paintReceipt(ctx, w, h, content, stock);
@@ -3031,13 +3140,13 @@ var ClothSim = class {
3031
3140
  this.stillFrames = 0;
3032
3141
  }
3033
3142
  /** Nearest particle to a local-space point — the grab interface. */
3034
- grabNearest(x, y, z26) {
3143
+ grabNearest(x, y, z25) {
3035
3144
  let best = -1;
3036
3145
  let bestDist = Infinity;
3037
3146
  for (let i = 0; i < this.count; i++) {
3038
3147
  const dx = this.positions[i * 3] - x;
3039
3148
  const dy = this.positions[i * 3 + 1] - y;
3040
- const dz = this.positions[i * 3 + 2] - z26;
3149
+ const dz = this.positions[i * 3 + 2] - z25;
3041
3150
  const d = dx * dx + dy * dy + dz * dz;
3042
3151
  if (d < bestDist) {
3043
3152
  bestDist = d;
@@ -3048,15 +3157,15 @@ var ClothSim = class {
3048
3157
  this.wake();
3049
3158
  return best;
3050
3159
  }
3051
- moveGrab(x, y, z26) {
3160
+ moveGrab(x, y, z25) {
3052
3161
  if (this.grabbedIndex < 0) return;
3053
3162
  const i3 = this.grabbedIndex * 3;
3054
3163
  this.positions[i3] = x;
3055
3164
  this.positions[i3 + 1] = y;
3056
- this.positions[i3 + 2] = z26;
3165
+ this.positions[i3 + 2] = z25;
3057
3166
  this.prev[i3] = x;
3058
3167
  this.prev[i3 + 1] = y;
3059
- this.prev[i3 + 2] = z26;
3168
+ this.prev[i3 + 2] = z25;
3060
3169
  this.wake();
3061
3170
  }
3062
3171
  release() {
@@ -3092,19 +3201,19 @@ var ClothSim = class {
3092
3201
  }
3093
3202
  const x = p[i3];
3094
3203
  const y = p[i3 + 1];
3095
- const z26 = p[i3 + 2];
3204
+ const z25 = p[i3 + 2];
3096
3205
  const gust2 = wind * (0.55 + 0.45 * Math.sin(this.time * 1.7 + x * 2.1 + y * 1.3)) * 0.9;
3097
3206
  const ax = gust2 * 0.25;
3098
3207
  const az = gust2;
3099
3208
  const vx = (x - this.prev[i3]) * damping;
3100
3209
  const vy = (y - this.prev[i3 + 1]) * damping;
3101
- const vz = (z26 - this.prev[i3 + 2]) * damping;
3210
+ const vz = (z25 - this.prev[i3 + 2]) * damping;
3102
3211
  this.prev[i3] = x;
3103
3212
  this.prev[i3 + 1] = y;
3104
- this.prev[i3 + 2] = z26;
3213
+ this.prev[i3 + 2] = z25;
3105
3214
  p[i3] = x + vx + ax * dt2;
3106
3215
  p[i3 + 1] = y + vy - gravity * 3.2 * dt2;
3107
- p[i3 + 2] = z26 + vz + az * dt2;
3216
+ p[i3 + 2] = z25 + vz + az * dt2;
3108
3217
  maxTravel = Math.max(maxTravel, vx * vx + vy * vy + vz * vz);
3109
3218
  }
3110
3219
  for (let iter = 0; iter < SOLVER_ITERATIONS; iter++) {
@@ -3161,7 +3270,6 @@ var THREE5 = __toESM(require("three"), 1);
3161
3270
  var THREE4 = __toESM(require("three"), 1);
3162
3271
 
3163
3272
  // src/scene/lighting.ts
3164
- var import_zod22 = require("zod");
3165
3273
  var lightingPresets = {
3166
3274
  studio: {
3167
3275
  id: "studio",
@@ -3322,46 +3430,14 @@ var lightingPresets = {
3322
3430
  function getLightingPreset(name) {
3323
3431
  return lightingPresets[name];
3324
3432
  }
3325
- var lightSchema = import_zod22.z.object({
3326
- /** Tone-mapping exposure — the stop the whole picture is printed at. */
3327
- exposure: import_zod22.z.number().min(0.1).max(4).optional(),
3328
- /**
3329
- * The tone curve — the film, where `exposure` is the stop.
3330
- *
3331
- * `filmic` is ACES, which is what every preset used to be pinned to and is
3332
- * kept so a scene tuned against it can say so. On near-white paper it is
3333
- * the wrong film: it desaturates and drags bright neutrals toward
3334
- * yellow-green, which is the sepia cast a lit sheet used to pick up.
3335
- */
3336
- film: import_zod22.z.enum(filmNames).optional(),
3337
- /** Key light strength. */
3338
- key: import_zod22.z.number().min(0).max(12).optional(),
3339
- /** Key light colour. */
3340
- color: import_zod22.z.string().optional(),
3341
- /**
3342
- * Where the key stands, degrees around the vertical. 0° is straight in
3343
- * front of the paper (+Z, beside the camera), 90° is off to the right,
3344
- * and ±180° is directly behind it — which is where `nave` puts it, and
3345
- * why that preset is carried by light coming THROUGH the paper.
3346
- */
3347
- direction: import_zod22.z.number().min(-180).max(180).optional(),
3348
- /** How high the key stands, degrees above the horizon. */
3349
- height: import_zod22.z.number().min(-30).max(89).optional(),
3350
- /** Flat fill from every direction at once. Cheap, and it kills form — reach for `studio` first. */
3351
- ambient: import_zod22.z.number().min(0).max(2).optional(),
3352
- /** The room's own light: an environment map built from `sky`. Directional fill, and the only thing paper's sheen has to reflect. */
3353
- studio: import_zod22.z.number().min(0).max(3).optional(),
3354
- /** Distance haze, as a multiple of the preset's. 0 clears the air entirely; 2 halves the distance you can see. */
3355
- haze: import_zod22.z.number().min(0).max(3).optional()
3356
- });
3357
3433
  var DEG6 = 180 / Math.PI;
3358
3434
  function lightAngles(position) {
3359
- const [x, y, z26] = position;
3360
- const distance = Math.hypot(x, y, z26);
3435
+ const [x, y, z25] = position;
3436
+ const distance = Math.hypot(x, y, z25);
3361
3437
  if (distance < 1e-9) return { azimuth: 0, elevation: 90, distance: 0 };
3362
- const ground = Math.hypot(x, z26);
3438
+ const ground = Math.hypot(x, z25);
3363
3439
  return {
3364
- azimuth: ground < 1e-9 ? 0 : Math.atan2(x, z26) * DEG6,
3440
+ azimuth: ground < 1e-9 ? 0 : Math.atan2(x, z25) * DEG6,
3365
3441
  elevation: Math.atan2(y, ground) * DEG6,
3366
3442
  distance
3367
3443
  };
@@ -3459,8 +3535,8 @@ vec3 plTransmission(vec3 inkFilter) {
3459
3535
  );
3460
3536
  function translucencyValues(translucency, lighting) {
3461
3537
  const preset = typeof lighting === "string" ? getLightingPreset(lighting) : lighting;
3462
- const [x, y, z26] = preset.key.position;
3463
- const direction = new THREE4.Vector3(x, y, z26);
3538
+ const [x, y, z25] = preset.key.position;
3539
+ const direction = new THREE4.Vector3(x, y, z25);
3464
3540
  if (direction.lengthSq() < 1e-12) direction.set(0, 1, 0);
3465
3541
  direction.normalize();
3466
3542
  const color = new THREE4.Color(preset.key.color).multiplyScalar(preset.key.intensity * TRANSMISSION_GAIN);
@@ -4622,6 +4698,7 @@ var PaperMesh = (0, import_react7.forwardRef)(function PaperMesh2(props, ref) {
4622
4698
  props.interactive && !isCloth && behavior?.handles?.map((h, i) => /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(
4623
4699
  "mesh",
4624
4700
  {
4701
+ userData: { paperlabChrome: true },
4625
4702
  ref: (m) => {
4626
4703
  handleRefs.current[i] = m;
4627
4704
  },
@@ -4765,7 +4842,7 @@ var toneMappings = {
4765
4842
  neutral: THREE9.NeutralToneMapping,
4766
4843
  filmic: THREE9.ACESFilmicToneMapping
4767
4844
  };
4768
- function mulberry32(seed) {
4845
+ function mulberry322(seed) {
4769
4846
  let a = seed;
4770
4847
  return () => {
4771
4848
  a |= 0;
@@ -4799,7 +4876,7 @@ function makeGoboTexture(kind) {
4799
4876
  }
4800
4877
  ctx.restore();
4801
4878
  } else {
4802
- const rand = mulberry32(7);
4879
+ const rand = mulberry322(7);
4803
4880
  for (const [count, radius, alpha] of [
4804
4881
  [26, 70, 0.75],
4805
4882
  [40, 38, 0.6],
@@ -4947,17 +5024,85 @@ function PaperLighting({
4947
5024
  ] });
4948
5025
  }
4949
5026
 
5027
+ // src/scene/backdrop.tsx
5028
+ var THREE10 = __toESM(require("three"), 1);
5029
+ var import_react9 = require("react");
5030
+ var import_fiber3 = require("@react-three/fiber");
5031
+ function PaperBackdrop({ backdrop }) {
5032
+ const scene = (0, import_fiber3.useThree)((s) => s.scene);
5033
+ const size = (0, import_fiber3.useThree)((s) => s.size);
5034
+ const [image, setImage] = (0, import_react9.useState)(null);
5035
+ const src = backdrop?.image ?? "";
5036
+ (0, import_react9.useEffect)(() => {
5037
+ if (!src) {
5038
+ setImage(null);
5039
+ return;
5040
+ }
5041
+ let live = true;
5042
+ const img = new Image();
5043
+ img.crossOrigin = "anonymous";
5044
+ img.onload = () => live && setImage(img);
5045
+ img.onerror = () => live && setImage(null);
5046
+ img.src = src;
5047
+ return () => {
5048
+ live = false;
5049
+ };
5050
+ }, [src]);
5051
+ const key = JSON.stringify({ backdrop: backdrop ?? null, w: size.width, h: size.height, loaded: !!image });
5052
+ (0, import_react9.useEffect)(() => {
5053
+ if (!backdrop) return;
5054
+ const previous = scene.background;
5055
+ const texture = paintBackdrop(backdrop, size.width, size.height, image);
5056
+ scene.background = texture;
5057
+ return () => {
5058
+ scene.background = previous;
5059
+ texture.dispose();
5060
+ };
5061
+ }, [key]);
5062
+ return null;
5063
+ }
5064
+ function paintBackdrop(backdrop, width, height, image) {
5065
+ const w = Math.max(2, Math.round(width / 2));
5066
+ const h = Math.max(2, Math.round(height / 2));
5067
+ const canvas = document.createElement("canvas");
5068
+ canvas.width = w;
5069
+ canvas.height = h;
5070
+ const ctx = canvas.getContext("2d");
5071
+ ctx.fillStyle = backdrop.color;
5072
+ ctx.fillRect(0, 0, w, h);
5073
+ if (image && image.width > 0 && image.height > 0) {
5074
+ const scale = backdrop.fit === "cover" ? Math.max(w / image.width, h / image.height) : Math.min(w / image.width, h / image.height);
5075
+ const dw = image.width * scale;
5076
+ const dh = image.height * scale;
5077
+ ctx.save();
5078
+ if (backdrop.blur > 0) ctx.filter = `blur(${backdrop.blur * Math.min(w, h) * 0.06}px)`;
5079
+ ctx.drawImage(image, (w - dw) / 2, (h - dh) / 2, dw, dh);
5080
+ ctx.restore();
5081
+ if (backdrop.fade > 0) {
5082
+ ctx.globalAlpha = backdrop.fade;
5083
+ ctx.fillStyle = backdrop.color;
5084
+ ctx.fillRect(0, 0, w, h);
5085
+ ctx.globalAlpha = 1;
5086
+ }
5087
+ }
5088
+ const texture = new THREE10.CanvasTexture(canvas);
5089
+ texture.colorSpace = THREE10.SRGBColorSpace;
5090
+ return texture;
5091
+ }
5092
+
4950
5093
  // src/Paper.tsx
4951
5094
  var import_jsx_runtime6 = require("react/jsx-runtime");
4952
- var Paper = (0, import_react9.forwardRef)(function Paper2({ children, className, style, ...meshProps }, ref) {
5095
+ var Paper = (0, import_react10.forwardRef)(function Paper2({ children, className, style, ...meshProps }, ref) {
4953
5096
  const config = useResolvedConfig(meshProps);
4954
- const webgl = (0, import_react9.useMemo)(() => typeof window === "undefined" ? true : supportsWebGL(), []);
5097
+ const webgl = (0, import_react10.useMemo)(() => typeof window === "undefined" ? true : supportsWebGL(), []);
4955
5098
  return /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { className, style: { width: "100%", height: "100%", ...style }, children: [
4956
- webgl ? /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(import_fiber3.Canvas, { shadows: true, camera: { position: [0, 0.35, 2.4], fov: 40 }, dpr: [1, 2], children: [
5099
+ webgl ? /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(import_fiber4.Canvas, { shadows: true, camera: { position: [0, 0.35, 2.4], fov: 40 }, dpr: [1, 2], children: [
5100
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(PaperBackdrop, { backdrop: config.scene.backdrop }),
4957
5101
  /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
4958
5102
  PaperLighting,
4959
5103
  {
4960
5104
  preset: config.scene.lighting,
5105
+ light: config.scene.light,
4961
5106
  floor: -1.05,
4962
5107
  scale: 8,
4963
5108
  reducedMotion: meshProps.reducedMotion
@@ -4971,14 +5116,14 @@ var Paper = (0, import_react9.forwardRef)(function Paper2({ children, className,
4971
5116
  });
4972
5117
 
4973
5118
  // src/PaperField.tsx
4974
- var THREE15 = __toESM(require("three"), 1);
5119
+ var THREE16 = __toESM(require("three"), 1);
4975
5120
  var import_gsap5 = require("gsap");
4976
- var import_fiber6 = require("@react-three/fiber");
4977
- var import_react16 = require("react");
5121
+ var import_fiber7 = require("@react-three/fiber");
5122
+ var import_react17 = require("react");
4978
5123
 
4979
5124
  // src/field/dropZones.tsx
4980
- var THREE10 = __toESM(require("three"), 1);
4981
- var import_react10 = require("react");
5125
+ var THREE11 = __toESM(require("three"), 1);
5126
+ var import_react11 = require("react");
4982
5127
  var import_jsx_runtime7 = require("react/jsx-runtime");
4983
5128
  var DropZoneRegistry = class {
4984
5129
  zones = /* @__PURE__ */ new Map();
@@ -5017,7 +5162,7 @@ var DropZoneRegistry = class {
5017
5162
  for (const fn of this.listeners) fn();
5018
5163
  }
5019
5164
  };
5020
- var DropZoneContext = (0, import_react10.createContext)(null);
5165
+ var DropZoneContext = (0, import_react11.createContext)(null);
5021
5166
  var globCache = /* @__PURE__ */ new Map();
5022
5167
  var globRegExp = (glob) => {
5023
5168
  let re = globCache.get(glob);
@@ -5033,13 +5178,13 @@ function zoneAccepts(zone, name) {
5033
5178
  }
5034
5179
  var zoneContains = (zone, x, y) => Math.abs(x - zone.bounds.position[0]) <= zone.bounds.size[0] / 2 && Math.abs(y - zone.bounds.position[1]) <= zone.bounds.size[1] / 2;
5035
5180
  function DropZone(props) {
5036
- const registry4 = (0, import_react10.useContext)(DropZoneContext);
5181
+ const registry4 = (0, import_react11.useContext)(DropZoneContext);
5037
5182
  const { id, accept, bounds, highlight = "glow", onPlace } = props;
5038
- const place = (0, import_react10.useRef)(onPlace);
5039
- (0, import_react10.useEffect)(() => {
5183
+ const place = (0, import_react11.useRef)(onPlace);
5184
+ (0, import_react11.useEffect)(() => {
5040
5185
  place.current = onPlace;
5041
5186
  }, [onPlace]);
5042
- (0, import_react10.useEffect)(() => {
5187
+ (0, import_react11.useEffect)(() => {
5043
5188
  if (!registry4) return;
5044
5189
  return registry4.register({
5045
5190
  id,
@@ -5053,17 +5198,17 @@ function DropZone(props) {
5053
5198
  return /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(DropZoneVisual, { registry: registry4, config: { id, accept, bounds, highlight } });
5054
5199
  }
5055
5200
  function DropZoneVisual({ registry: registry4, config }) {
5056
- (0, import_react10.useSyncExternalStore)(registry4.subscribe, registry4.getVersion, registry4.getVersion);
5201
+ (0, import_react11.useSyncExternalStore)(registry4.subscribe, registry4.getVersion, registry4.getVersion);
5057
5202
  const hovered = registry4.hovered === config.id;
5058
5203
  const style = config.highlight ?? "glow";
5059
5204
  const [w, h] = config.bounds.size;
5060
- const edges = (0, import_react10.useMemo)(() => {
5061
- const plane = new THREE10.PlaneGeometry(w, h);
5062
- const geo = new THREE10.EdgesGeometry(plane);
5205
+ const edges = (0, import_react11.useMemo)(() => {
5206
+ const plane = new THREE11.PlaneGeometry(w, h);
5207
+ const geo = new THREE11.EdgesGeometry(plane);
5063
5208
  plane.dispose();
5064
5209
  return geo;
5065
5210
  }, [w, h]);
5066
- (0, import_react10.useEffect)(() => () => edges.dispose(), [edges]);
5211
+ (0, import_react11.useEffect)(() => () => edges.dispose(), [edges]);
5067
5212
  return /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("group", { position: config.bounds.position, children: [
5068
5213
  style !== "outline" && /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("mesh", { children: [
5069
5214
  /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("planeGeometry", { args: [w, h] }),
@@ -5082,18 +5227,18 @@ function DropZoneVisual({ registry: registry4, config }) {
5082
5227
  }
5083
5228
 
5084
5229
  // src/field/sheetGrid.ts
5085
- var import_zod23 = require("zod");
5086
- var sheetLayoutSchema = import_zod23.z.object({
5087
- rows: import_zod23.z.number().int().min(1).max(12).default(2),
5088
- columns: import_zod23.z.number().int().min(1).max(12).default(5),
5230
+ var import_zod22 = require("zod");
5231
+ var sheetLayoutSchema = import_zod22.z.object({
5232
+ rows: import_zod22.z.number().int().min(1).max(12).default(2),
5233
+ columns: import_zod22.z.number().int().min(1).max(12).default(5),
5089
5234
  /** World-units gap between slots. Stamps are printed in register — no jitter. */
5090
- gutter: import_zod23.z.number().min(0).max(1).default(0.08),
5235
+ gutter: import_zod22.z.number().min(0).max(1).default(0.08),
5091
5236
  /** Slot footprint in world units (the paper preset should match). */
5092
- cellWidth: import_zod23.z.number().min(0.1).max(4).default(0.72),
5093
- cellHeight: import_zod23.z.number().min(0.1).max(4).default(0.86),
5237
+ cellWidth: import_zod22.z.number().min(0.1).max(4).default(0.72),
5238
+ cellHeight: import_zod22.z.number().min(0.1).max(4).default(0.86),
5094
5239
  /** Render the shared backing sheet behind the grid. */
5095
- backing: import_zod23.z.boolean().default(true),
5096
- backingMargin: import_zod23.z.number().min(0).max(1).default(0.12)
5240
+ backing: import_zod22.z.boolean().default(true),
5241
+ backingMargin: import_zod22.z.number().min(0).max(1).default(0.12)
5097
5242
  });
5098
5243
  var SHEET_LIFT = 0.012;
5099
5244
  function withSheetCellFromPaper(parsed, rawOptions, paperDims) {
@@ -5185,24 +5330,24 @@ function resolveFieldSlotConfig(slot, fallback, index, layoutId, layoutOptions)
5185
5330
  }
5186
5331
 
5187
5332
  // src/field/fieldGroup.tsx
5188
- var THREE12 = __toESM(require("three"), 1);
5333
+ var THREE13 = __toESM(require("three"), 1);
5189
5334
  var import_gsap3 = require("gsap");
5190
- var import_fiber4 = require("@react-three/fiber");
5191
- var import_react12 = require("react");
5335
+ var import_fiber5 = require("@react-three/fiber");
5336
+ var import_react13 = require("react");
5192
5337
  var import_three_custom_shader_material2 = __toESM(require("three-custom-shader-material"), 1);
5193
5338
 
5194
5339
  // src/content/atlas.ts
5195
- var THREE11 = __toESM(require("three"), 1);
5196
- var import_react11 = require("react");
5340
+ var THREE12 = __toESM(require("three"), 1);
5341
+ var import_react12 = require("react");
5197
5342
  var MAX_ATLAS = 4096;
5198
5343
  function atlasGrid(count, aspect = 1) {
5199
5344
  const cols = Math.max(1, Math.min(count, Math.ceil(Math.sqrt(count * Math.max(aspect, 0.01)))));
5200
5345
  return { cols, rows: Math.max(1, Math.ceil(count / cols)) };
5201
5346
  }
5202
5347
  function useContentAtlas(contents, sheet2, stock) {
5203
- const [atlas, setAtlas] = (0, import_react11.useState)(null);
5348
+ const [atlas, setAtlas] = (0, import_react12.useState)(null);
5204
5349
  const stableContents = useStable(contents);
5205
- (0, import_react11.useEffect)(() => {
5350
+ (0, import_react12.useEffect)(() => {
5206
5351
  let disposed = false;
5207
5352
  const aspect = sheet2.height / sheet2.width;
5208
5353
  const { cols, rows } = atlasGrid(contents.length, aspect);
@@ -5218,8 +5363,8 @@ function useContentAtlas(contents, sheet2, stock) {
5218
5363
  const ctx = canvas.getContext("2d");
5219
5364
  ctx.fillStyle = stock.color;
5220
5365
  ctx.fillRect(0, 0, canvas.width, canvas.height);
5221
- const texture = new THREE11.CanvasTexture(canvas);
5222
- texture.colorSpace = THREE11.SRGBColorSpace;
5366
+ const texture = new THREE12.CanvasTexture(canvas);
5367
+ texture.colorSpace = THREE12.SRGBColorSpace;
5223
5368
  texture.anisotropy = 4;
5224
5369
  setAtlas({ texture, cols, rows });
5225
5370
  const drawTile = (index, tile) => {
@@ -5378,21 +5523,21 @@ function cap(s) {
5378
5523
  }
5379
5524
 
5380
5525
  // src/field/layouts/index.ts
5381
- var import_zod25 = require("zod");
5526
+ var import_zod24 = require("zod");
5382
5527
 
5383
5528
  // src/stage/path.ts
5384
- var import_zod24 = require("zod");
5385
- var walkPathSchema = import_zod24.z.object({
5529
+ var import_zod23 = require("zod");
5530
+ var walkPathSchema = import_zod23.z.object({
5386
5531
  /**
5387
5532
  * Control points on the ground plane, [x, z]. The default walks away from
5388
5533
  * the camera down -Z — the shot every reference image is composed on.
5389
5534
  */
5390
- points: import_zod24.z.array(import_zod24.z.tuple([import_zod24.z.number(), import_zod24.z.number()])).min(2).default([
5535
+ points: import_zod23.z.array(import_zod23.z.tuple([import_zod23.z.number(), import_zod23.z.number()])).min(2).default([
5391
5536
  [0, 9],
5392
5537
  [0, -9]
5393
5538
  ]),
5394
5539
  /** Join the last point back to the first: an endless walk, and the only form `phase` can slide. */
5395
- closed: import_zod24.z.boolean().default(false)
5540
+ closed: import_zod23.z.boolean().default(false)
5396
5541
  });
5397
5542
  var SAMPLES_PER_SEGMENT = 24;
5398
5543
  var EPSILON = 1e-6;
@@ -5518,9 +5663,9 @@ function jitter2(seed, i) {
5518
5663
  function ramp(i, n) {
5519
5664
  return n > 1 ? i / (n - 1) : 1;
5520
5665
  }
5521
- var ringSchema = import_zod25.z.object({
5522
- radius: import_zod25.z.number().min(0.5).max(12).default(2.6),
5523
- tiltDeg: import_zod25.z.number().min(-45).max(45).default(8)
5666
+ var ringSchema = import_zod24.z.object({
5667
+ radius: import_zod24.z.number().min(0.5).max(12).default(2.6),
5668
+ tiltDeg: import_zod24.z.number().min(-45).max(45).default(8)
5524
5669
  });
5525
5670
  var ring = {
5526
5671
  id: "ring",
@@ -5538,15 +5683,15 @@ var ring = {
5538
5683
  };
5539
5684
  }
5540
5685
  };
5541
- var fanSchema = import_zod25.z.object({
5686
+ var fanSchema = import_zod24.z.object({
5542
5687
  /** Total angular sweep from the first sheet to the last, degrees. */
5543
- sweep: import_zod25.z.number().min(0).max(180).default(72),
5688
+ sweep: import_zod24.z.number().min(0).max(180).default(72),
5544
5689
  /** Where the shared pin sits, in half-sheet-heights below center. 1 = the bottom edge. */
5545
- hinge: import_zod25.z.number().min(0).max(4).default(1.15),
5690
+ hinge: import_zod24.z.number().min(0).max(4).default(1.15),
5546
5691
  /** Thickness step so the sheets stack in order instead of z-fighting. */
5547
- lift: import_zod25.z.number().min(2e-3).max(0.08).default(0.012),
5692
+ lift: import_zod24.z.number().min(2e-3).max(0.08).default(0.012),
5548
5693
  /** How much flatter the middle of the fan sits than its outer sheets. */
5549
- bow: import_zod25.z.number().min(0).max(1).default(0.7)
5694
+ bow: import_zod24.z.number().min(0).max(1).default(0.7)
5550
5695
  });
5551
5696
  var fan = {
5552
5697
  id: "fan",
@@ -5566,16 +5711,16 @@ var fan = {
5566
5711
  };
5567
5712
  }
5568
5713
  };
5569
- var spreadSchema = import_zod25.z.object({
5714
+ var spreadSchema = import_zod24.z.object({
5570
5715
  /** How far each sheet slides past the one below it. */
5571
- slip: import_zod25.z.number().min(0.02).max(2).default(0.3),
5716
+ slip: import_zod24.z.number().min(0.02).max(2).default(0.3),
5572
5717
  /** Direction of the slide, degrees. 0 slides right, 90 slides up. */
5573
- angle: import_zod25.z.number().min(-180).max(180).default(28),
5574
- lift: import_zod25.z.number().min(2e-3).max(0.08).default(0.012),
5718
+ angle: import_zod24.z.number().min(-180).max(180).default(28),
5719
+ lift: import_zod24.z.number().min(2e-3).max(0.08).default(0.012),
5575
5720
  /** How much more the sheets at the far end of the slide bow. */
5576
- bow: import_zod25.z.number().min(0).max(1).default(0.6),
5721
+ bow: import_zod24.z.number().min(0).max(1).default(0.6),
5577
5722
  /** Nothing hand-slid is perfectly square — a touch of per-sheet rotation. */
5578
- drift: import_zod25.z.number().min(0).max(1).default(0.15)
5723
+ drift: import_zod24.z.number().min(0).max(1).default(0.15)
5579
5724
  });
5580
5725
  var spread = {
5581
5726
  id: "spread",
@@ -5593,15 +5738,15 @@ var spread = {
5593
5738
  };
5594
5739
  }
5595
5740
  };
5596
- var pileSchema = import_zod25.z.object({
5741
+ var pileSchema = import_zod24.z.object({
5597
5742
  /** How far sheets wander from the center of the heap. */
5598
- scatter: import_zod25.z.number().min(0).max(2).default(0.22),
5743
+ scatter: import_zod24.z.number().min(0).max(2).default(0.22),
5599
5744
  /** Widest angle a sheet sits off square, degrees. */
5600
- turn: import_zod25.z.number().min(0).max(180).default(24),
5601
- lift: import_zod25.z.number().min(2e-3).max(0.08).default(0.011),
5745
+ turn: import_zod24.z.number().min(0).max(180).default(24),
5746
+ lift: import_zod24.z.number().min(2e-3).max(0.08).default(0.011),
5602
5747
  /** How flat the sheets underneath are pressed by the ones on top. */
5603
- press: import_zod25.z.number().min(0).max(1).default(0.85),
5604
- seed: import_zod25.z.number().int().min(0).max(9999).default(3)
5748
+ press: import_zod24.z.number().min(0).max(1).default(0.85),
5749
+ seed: import_zod24.z.number().int().min(0).max(9999).default(3)
5605
5750
  });
5606
5751
  var pile = {
5607
5752
  id: "pile",
@@ -5617,12 +5762,12 @@ var pile = {
5617
5762
  };
5618
5763
  }
5619
5764
  };
5620
- var wallSchema = import_zod25.z.object({
5621
- gapX: import_zod25.z.number().min(0.05).max(1).default(0.22),
5622
- gapY: import_zod25.z.number().min(0.05).max(1).default(0.3),
5623
- jitterAmt: import_zod25.z.number().min(0).max(1).default(0.25),
5765
+ var wallSchema = import_zod24.z.object({
5766
+ gapX: import_zod24.z.number().min(0.05).max(1).default(0.22),
5767
+ gapY: import_zod24.z.number().min(0.05).max(1).default(0.3),
5768
+ jitterAmt: import_zod24.z.number().min(0).max(1).default(0.25),
5624
5769
  /** Spread of sag across the wall — no two pinned sheets hang alike. */
5625
- sag: import_zod25.z.number().min(0).max(1).default(0.45)
5770
+ sag: import_zod24.z.number().min(0).max(1).default(0.45)
5626
5771
  });
5627
5772
  var wall = {
5628
5773
  id: "wall",
@@ -5648,15 +5793,15 @@ var wall = {
5648
5793
  };
5649
5794
  }
5650
5795
  };
5651
- var spillSchema = import_zod25.z.object({
5652
- spreadX: import_zod25.z.number().min(0.5).max(8).default(2.4),
5653
- spreadY: import_zod25.z.number().min(0.5).max(8).default(1.5),
5654
- depth: import_zod25.z.number().min(0).max(6).default(1.6),
5796
+ var spillSchema = import_zod24.z.object({
5797
+ spreadX: import_zod24.z.number().min(0.5).max(8).default(2.4),
5798
+ spreadY: import_zod24.z.number().min(0.5).max(8).default(1.5),
5799
+ depth: import_zod24.z.number().min(0).max(6).default(1.6),
5655
5800
  /** How far sheets pitch and roll out of the picture plane. */
5656
- tumble: import_zod25.z.number().min(0).max(1).default(0.5),
5801
+ tumble: import_zod24.z.number().min(0).max(1).default(0.5),
5657
5802
  /** Spread of bend across the sheets — a spill does not fold them alike. */
5658
- vary: import_zod25.z.number().min(0).max(1).default(0.6),
5659
- seed: import_zod25.z.number().int().min(0).max(9999).default(7)
5803
+ vary: import_zod24.z.number().min(0).max(1).default(0.6),
5804
+ seed: import_zod24.z.number().int().min(0).max(9999).default(7)
5660
5805
  });
5661
5806
  var spill = {
5662
5807
  id: "spill",
@@ -5681,13 +5826,13 @@ var spill = {
5681
5826
  };
5682
5827
  }
5683
5828
  };
5684
- var sweepSchema = import_zod25.z.object({
5685
- columns: import_zod25.z.number().int().min(1).max(24).default(5),
5829
+ var sweepSchema = import_zod24.z.object({
5830
+ columns: import_zod24.z.number().int().min(1).max(24).default(5),
5686
5831
  /** Breathing room around each specimen. */
5687
- gap: import_zod25.z.number().min(0).max(2).default(0.22),
5832
+ gap: import_zod24.z.number().min(0).max(2).default(0.22),
5688
5833
  /** Deformation at the first specimen and at the last. */
5689
- from: import_zod25.z.number().min(0).max(1).default(0),
5690
- to: import_zod25.z.number().min(0).max(1).default(1)
5834
+ from: import_zod24.z.number().min(0).max(1).default(0),
5835
+ to: import_zod24.z.number().min(0).max(1).default(1)
5691
5836
  });
5692
5837
  var sweep = {
5693
5838
  id: "sweep",
@@ -5711,15 +5856,15 @@ var sweep = {
5711
5856
  };
5712
5857
  }
5713
5858
  };
5714
- var bookSchema = import_zod25.z.object({
5859
+ var bookSchema = import_zod24.z.object({
5715
5860
  /** How far the outermost page lifts off the block, degrees. */
5716
- spread: import_zod25.z.number().min(0).max(150).default(55),
5861
+ spread: import_zod24.z.number().min(0).max(150).default(55),
5717
5862
  /** Fraction of the pages bound to the left. 0 = a one-sided sample book. */
5718
- split: import_zod25.z.number().min(0).max(1).default(0.5),
5863
+ split: import_zod24.z.number().min(0).max(1).default(0.5),
5719
5864
  /** Page thickness — the gap between pages of one block. */
5720
- lift: import_zod25.z.number().min(1e-3).max(0.05).default(8e-3),
5865
+ lift: import_zod24.z.number().min(1e-3).max(0.05).default(8e-3),
5721
5866
  /** How much more a lifted page arcs than one lying flat in the block. */
5722
- gutter: import_zod25.z.number().min(0).max(1).default(0.6)
5867
+ gutter: import_zod24.z.number().min(0).max(1).default(0.6)
5723
5868
  });
5724
5869
  var book = {
5725
5870
  id: "book",
@@ -5746,11 +5891,11 @@ var book = {
5746
5891
  };
5747
5892
  }
5748
5893
  };
5749
- var accordionSchema = import_zod25.z.object({
5894
+ var accordionSchema = import_zod24.z.object({
5750
5895
  /** How far each panel tilts off the strip's line, degrees. 0 = flat, 90 = shut. */
5751
- angle: import_zod25.z.number().min(0).max(89).default(55),
5896
+ angle: import_zod24.z.number().min(0).max(89).default(55),
5752
5897
  /** A concertina holds its creases — how much bow the panels keep. */
5753
- slack: import_zod25.z.number().min(0).max(1).default(0.15)
5898
+ slack: import_zod24.z.number().min(0).max(1).default(0.15)
5754
5899
  });
5755
5900
  var accordion = {
5756
5901
  id: "accordion",
@@ -5769,16 +5914,16 @@ var accordion = {
5769
5914
  };
5770
5915
  }
5771
5916
  };
5772
- var rackSchema = import_zod25.z.object({
5917
+ var rackSchema = import_zod24.z.object({
5773
5918
  /** Gap along the row, as a fraction of the paper's width. Under 1 they overlap. */
5774
- spacing: import_zod25.z.number().min(0.05).max(2).default(0.82),
5919
+ spacing: import_zod24.z.number().min(0.05).max(2).default(0.82),
5775
5920
  /** How far a sheet leans back off vertical, degrees. */
5776
- lean: import_zod25.z.number().min(0).max(70).default(16),
5921
+ lean: import_zod24.z.number().min(0).max(70).default(16),
5777
5922
  /** How much that lean differs sheet to sheet — nothing propped is uniform. */
5778
- vary: import_zod25.z.number().min(0).max(1).default(0.55),
5923
+ vary: import_zod24.z.number().min(0).max(1).default(0.55),
5779
5924
  /** Small rotations off square. */
5780
- sway: import_zod25.z.number().min(0).max(1).default(0.35),
5781
- seed: import_zod25.z.number().int().min(0).max(9999).default(5)
5925
+ sway: import_zod24.z.number().min(0).max(1).default(0.35),
5926
+ seed: import_zod24.z.number().int().min(0).max(9999).default(5)
5782
5927
  });
5783
5928
  var rack = {
5784
5929
  id: "rack",
@@ -5802,19 +5947,19 @@ var rack = {
5802
5947
  };
5803
5948
  }
5804
5949
  };
5805
- var colonnadeSchema = import_zod25.z.object({
5950
+ var colonnadeSchema = import_zod24.z.object({
5806
5951
  /** The walk the colonnade is built along — see `stage/path`. */
5807
5952
  path: walkPathSchema.default({}),
5808
5953
  /** Half-width of the clear aisle: how far each banner stands off the walk line. */
5809
- aisle: import_zod25.z.number().min(0.2).max(20).default(2.4),
5954
+ aisle: import_zod24.z.number().min(0.2).max(20).default(2.4),
5810
5955
  /** How much that gap opens and closes along the walk. Nothing hung by hand is a corridor. */
5811
- breathe: import_zod25.z.number().min(0).max(1).default(0.3),
5956
+ breathe: import_zod24.z.number().min(0).max(1).default(0.3),
5812
5957
  /** Widest angle a banner turns off square to the aisle, degrees. */
5813
- twist: import_zod25.z.number().min(0).max(90).default(22),
5958
+ twist: import_zod24.z.number().min(0).max(90).default(22),
5814
5959
  /** Fraction of the walk left clear at each end, so the figure has somewhere to enter from. */
5815
- margin: import_zod25.z.number().min(0).max(0.45).default(0.05),
5960
+ margin: import_zod24.z.number().min(0).max(0.45).default(0.05),
5816
5961
  /** Spread of banner heights, 0..1. */
5817
- rise: import_zod25.z.number().min(0).max(1).default(0.28),
5962
+ rise: import_zod24.z.number().min(0).max(1).default(0.28),
5818
5963
  /**
5819
5964
  * How far the banners lift off the floor, as a fraction of their height.
5820
5965
  *
@@ -5828,10 +5973,10 @@ var colonnadeSchema = import_zod25.z.object({
5828
5973
  * The bound used to be 0, so the one thing this option documented itself
5829
5974
  * as doing was the one thing it could not do.
5830
5975
  */
5831
- hover: import_zod25.z.number().min(-0.5).max(1).default(0),
5976
+ hover: import_zod24.z.number().min(-0.5).max(1).default(0),
5832
5977
  /** Spread of deformation — no two lengths of hung paper drape alike. */
5833
- drape: import_zod25.z.number().min(0).max(1).default(0.5),
5834
- seed: import_zod25.z.number().int().min(0).max(9999).default(2)
5978
+ drape: import_zod24.z.number().min(0).max(1).default(0.5),
5979
+ seed: import_zod24.z.number().int().min(0).max(9999).default(2)
5835
5980
  });
5836
5981
  function colonnadeStop(i, n, margin) {
5837
5982
  const side = i % 2 === 0 ? 1 : -1;
@@ -5916,7 +6061,7 @@ function fieldShapeStack(config, progress) {
5916
6061
 
5917
6062
  // src/field/fieldGroup.tsx
5918
6063
  var import_jsx_runtime8 = require("react/jsx-runtime");
5919
- var scratchObj = new THREE12.Object3D();
6064
+ var scratchObj = new THREE13.Object3D();
5920
6065
  var scratchAero = { position: [0, 0, 0], rotation: [0, 0, 0] };
5921
6066
  var FIELD_SEGMENT_CAP = 48;
5922
6067
  var FIELD_AUTO_CEILING = 128;
@@ -5935,11 +6080,11 @@ function FieldGroup({
5935
6080
  const stock = getStock(config.stock);
5936
6081
  const rig = useLightRig(config.scene.lighting);
5937
6082
  const behavior = config.behavior && !config.deformers ? getBehavior(config.behavior.type) : null;
5938
- const progressRef = (0, import_react12.useRef)(
6083
+ const progressRef = (0, import_react13.useRef)(
5939
6084
  behavior ? config.behavior[behavior.progressParam] : 0
5940
6085
  );
5941
6086
  const buildStackAt = (progress) => fieldShapeStack(config, progress);
5942
- const initialStack = (0, import_react12.useMemo)(
6087
+ const initialStack = (0, import_react13.useMemo)(
5943
6088
  () => buildStackAt(progressRef.current),
5944
6089
  [
5945
6090
  JSON.stringify(config.behavior ?? null),
@@ -5948,7 +6093,7 @@ function FieldGroup({
5948
6093
  ]
5949
6094
  );
5950
6095
  const structureKey = initialStack.map((i) => i.type).join("|");
5951
- const geometry = (0, import_react12.useMemo)(() => {
6096
+ const geometry = (0, import_react13.useMemo)(() => {
5952
6097
  const floor = stackMinSegments(initialStack, config.sheet);
5953
6098
  const want = [0, 0];
5954
6099
  for (const p of PROGRESS_SAMPLES2) {
@@ -5977,28 +6122,28 @@ function FieldGroup({
5977
6122
  atlasIdx[i] = i;
5978
6123
  phase[i] = indices[i] * 0.618034 % 1 * 4;
5979
6124
  }
5980
- geo.setAttribute("aAtlas", new THREE12.InstancedBufferAttribute(atlasIdx, 1));
5981
- geo.setAttribute("aPhase", new THREE12.InstancedBufferAttribute(phase, 1));
5982
- geo.setAttribute("aBias", new THREE12.InstancedBufferAttribute(bias, 1));
6125
+ geo.setAttribute("aAtlas", new THREE13.InstancedBufferAttribute(atlasIdx, 1));
6126
+ geo.setAttribute("aPhase", new THREE13.InstancedBufferAttribute(phase, 1));
6127
+ geo.setAttribute("aBias", new THREE13.InstancedBufferAttribute(bias, 1));
5983
6128
  return geo;
5984
6129
  }, [JSON.stringify(config.sheet), structureKey, count, autoCeiling]);
5985
- (0, import_react12.useEffect)(() => () => geometry.dispose(), [geometry]);
6130
+ (0, import_react13.useEffect)(() => () => geometry.dispose(), [geometry]);
5986
6131
  const atlas = useContentAtlas(contents, config.sheet, stock);
5987
- const shader = (0, import_react12.useMemo)(() => {
6132
+ const shader = (0, import_react13.useMemo)(() => {
5988
6133
  const composed = buildDisplacementGLSL(initialStack, config.sheet);
5989
6134
  const uniforms = {};
5990
6135
  for (const [name, value] of Object.entries(composed.uniforms)) {
5991
6136
  uniforms[name] = {
5992
- value: Array.isArray(value) && value.length === 2 ? new THREE12.Vector2(...value) : value
6137
+ value: Array.isArray(value) && value.length === 2 ? new THREE13.Vector2(...value) : value
5993
6138
  };
5994
6139
  }
5995
6140
  uniforms.uPlTime = { value: 0 };
5996
6141
  uniforms.uAtlas = { value: null };
5997
- uniforms.uAtlasGrid = { value: new THREE12.Vector2(1, 1) };
6142
+ uniforms.uAtlasGrid = { value: new THREE13.Vector2(1, 1) };
5998
6143
  uniforms.uBackDarken = {
5999
6144
  value: 1 - Math.min(0.45, 0.12 + config.sheet.thickness * 0.9) * stock.opacity
6000
6145
  };
6001
- uniforms.uStockColor = { value: new THREE12.Color(stock.color) };
6146
+ uniforms.uStockColor = { value: new THREE13.Color(stock.color) };
6002
6147
  uniforms.uShowThrough = { value: config.surface.showThrough ?? stock.showThrough };
6003
6148
  Object.assign(uniforms, translucencyUniforms(config.surface.translucency ?? stock.translucency, rig));
6004
6149
  return {
@@ -6013,19 +6158,19 @@ function FieldGroup({
6013
6158
  config.surface.showThrough,
6014
6159
  config.surface.translucency
6015
6160
  ]);
6016
- (0, import_react12.useEffect)(() => {
6161
+ (0, import_react13.useEffect)(() => {
6017
6162
  const values = translucencyValues(config.surface.translucency ?? stock.translucency, rig);
6018
6163
  shader.uniforms.uTranslucency.value = values.translucency;
6019
6164
  shader.uniforms.uBackLightDir.value.copy(values.direction);
6020
6165
  shader.uniforms.uBackLightColor.value.copy(values.color);
6021
6166
  shader.uniforms.uAmbientTransmission.value = values.ambient;
6022
6167
  }, [shader, rig, config.surface.translucency, stock.translucency]);
6023
- (0, import_react12.useEffect)(() => {
6168
+ (0, import_react13.useEffect)(() => {
6024
6169
  if (!atlas) return;
6025
6170
  shader.uniforms.uAtlas.value = atlas.texture;
6026
6171
  shader.uniforms.uAtlasGrid.value.set(atlas.cols, atlas.rows);
6027
6172
  }, [atlas, shader]);
6028
- (0, import_react12.useEffect)(() => {
6173
+ (0, import_react13.useEffect)(() => {
6029
6174
  if (!behavior || shared.reduced) return;
6030
6175
  const state = { p: progressRef.current };
6031
6176
  const tween = import_gsap3.gsap.to(state, {
@@ -6042,8 +6187,8 @@ function FieldGroup({
6042
6187
  tween.kill();
6043
6188
  };
6044
6189
  }, [structureKey, shared.reduced]);
6045
- const meshRef = (0, import_react12.useRef)(null);
6046
- (0, import_fiber4.useFrame)(({ clock }) => {
6190
+ const meshRef = (0, import_react13.useRef)(null);
6191
+ (0, import_fiber5.useFrame)(({ clock }) => {
6047
6192
  const mesh = meshRef.current;
6048
6193
  if (!mesh) return;
6049
6194
  const elapsed = clock.elapsedTime - Math.max(shared.mountTimeRef.current, 0);
@@ -6053,7 +6198,7 @@ function FieldGroup({
6053
6198
  for (const [name, value] of Object.entries(values)) {
6054
6199
  const uniform = shader.uniforms[name];
6055
6200
  if (!uniform) continue;
6056
- if (uniform.value instanceof THREE12.Vector2 && Array.isArray(value)) {
6201
+ if (uniform.value instanceof THREE13.Vector2 && Array.isArray(value)) {
6057
6202
  uniform.value.set(value[0], value[1]);
6058
6203
  } else {
6059
6204
  uniform.value = value;
@@ -6137,13 +6282,13 @@ function FieldGroup({
6137
6282
  children: /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
6138
6283
  import_three_custom_shader_material2.default,
6139
6284
  {
6140
- baseMaterial: THREE12.MeshStandardMaterial,
6285
+ baseMaterial: THREE13.MeshStandardMaterial,
6141
6286
  vertexShader: shader.vertexShader,
6142
6287
  fragmentShader: shader.fragmentShader,
6143
6288
  uniforms: shader.uniforms,
6144
6289
  roughness: stock.roughness,
6145
6290
  metalness: 0,
6146
- side: THREE12.DoubleSide
6291
+ side: THREE13.DoubleSide
6147
6292
  },
6148
6293
  `${structureKey}:${count}`
6149
6294
  )
@@ -6189,8 +6334,8 @@ var easeOut = (t) => 1 - (1 - t) ** 3;
6189
6334
  var easeInOut = (t) => t < 0.5 ? 2 * t * t : 1 - (-2 * t + 2) ** 2 / 2;
6190
6335
 
6191
6336
  // src/field/backingSheet.tsx
6192
- var THREE13 = __toESM(require("three"), 1);
6193
- var import_react13 = require("react");
6337
+ var THREE14 = __toESM(require("three"), 1);
6338
+ var import_react14 = require("react");
6194
6339
 
6195
6340
  // src/content/backing.ts
6196
6341
  function silhouetteRects(o, count) {
@@ -6263,7 +6408,7 @@ function BackingSheet({
6263
6408
  removed
6264
6409
  }) {
6265
6410
  const { width, height } = sheetBackingSize(options);
6266
- const canvas = (0, import_react13.useMemo)(() => {
6411
+ const canvas = (0, import_react14.useMemo)(() => {
6267
6412
  if (typeof document === "undefined") return null;
6268
6413
  const c = document.createElement("canvas");
6269
6414
  const scale = Math.min(1024, Math.round(360 * Math.max(width, height))) / Math.max(width, height);
@@ -6271,14 +6416,14 @@ function BackingSheet({
6271
6416
  c.height = Math.max(2, Math.round(height * scale));
6272
6417
  return c;
6273
6418
  }, [width, height]);
6274
- const texture = (0, import_react13.useMemo)(() => canvas ? new THREE13.CanvasTexture(canvas) : null, [canvas]);
6419
+ const texture = (0, import_react14.useMemo)(() => canvas ? new THREE14.CanvasTexture(canvas) : null, [canvas]);
6275
6420
  const removedKey = [...removed].sort((a, b) => a - b).join(",");
6276
- (0, import_react13.useEffect)(() => {
6421
+ (0, import_react14.useEffect)(() => {
6277
6422
  if (!canvas || !texture) return;
6278
6423
  drawBacking(canvas, { options, count, tint: BACKING_TINT, removed });
6279
6424
  texture.needsUpdate = true;
6280
6425
  }, [canvas, texture, JSON.stringify(options), count, removedKey]);
6281
- (0, import_react13.useEffect)(() => () => texture?.dispose(), [texture]);
6426
+ (0, import_react14.useEffect)(() => () => texture?.dispose(), [texture]);
6282
6427
  return /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("mesh", { receiveShadow: true, children: [
6283
6428
  /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("planeGeometry", { args: [width, height] }),
6284
6429
  /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("meshStandardMaterial", { map: texture, color: "#ffffff", roughness: 0.92, metalness: 0 })
@@ -6286,10 +6431,10 @@ function BackingSheet({
6286
6431
  }
6287
6432
 
6288
6433
  // src/field/interactiveField.tsx
6289
- var THREE14 = __toESM(require("three"), 1);
6434
+ var THREE15 = __toESM(require("three"), 1);
6290
6435
  var import_gsap4 = require("gsap");
6291
- var import_fiber5 = require("@react-three/fiber");
6292
- var import_react14 = require("react");
6436
+ var import_fiber6 = require("@react-three/fiber");
6437
+ var import_react15 = require("react");
6293
6438
  var import_jsx_runtime10 = require("react/jsx-runtime");
6294
6439
  var PICK_BEHAVIORS = /* @__PURE__ */ new Set(["peel", "carry"]);
6295
6440
  function InteractiveField(props) {
@@ -6297,15 +6442,15 @@ function InteractiveField(props) {
6297
6442
  const total = papers.length;
6298
6443
  const layout = getLayout(layoutId);
6299
6444
  const reduced = usePrefersReducedMotion(props.reducedMotion);
6300
- const contextRegistry = (0, import_react14.useContext)(DropZoneContext);
6301
- const registry4 = (0, import_react14.useMemo)(() => contextRegistry ?? new DropZoneRegistry(), [contextRegistry]);
6302
- const camera = (0, import_fiber5.useThree)((s) => s.camera);
6303
- const gl = (0, import_fiber5.useThree)((s) => s.gl);
6304
- const controls = (0, import_fiber5.useThree)((s) => s.controls);
6305
- const [removed, setRemoved] = (0, import_react14.useState)(EMPTY_SET);
6306
- const [slotPatches, setSlotPatches] = (0, import_react14.useState)({});
6307
- const [slotStates, setSlotStates] = (0, import_react14.useState)({});
6308
- const slotConfigs = (0, import_react14.useMemo)(
6445
+ const contextRegistry = (0, import_react15.useContext)(DropZoneContext);
6446
+ const registry4 = (0, import_react15.useMemo)(() => contextRegistry ?? new DropZoneRegistry(), [contextRegistry]);
6447
+ const camera = (0, import_fiber6.useThree)((s) => s.camera);
6448
+ const gl = (0, import_fiber6.useThree)((s) => s.gl);
6449
+ const controls = (0, import_fiber6.useThree)((s) => s.controls);
6450
+ const [removed, setRemoved] = (0, import_react15.useState)(EMPTY_SET);
6451
+ const [slotPatches, setSlotPatches] = (0, import_react15.useState)({});
6452
+ const [slotStates, setSlotStates] = (0, import_react15.useState)({});
6453
+ const slotConfigs = (0, import_react15.useMemo)(
6309
6454
  () => papers.map((slot, i) => {
6310
6455
  const config = resolveFieldSlotConfig(slot, fallback, i, layoutId, layoutOptions);
6311
6456
  const patch = slotPatches[i];
@@ -6319,14 +6464,14 @@ function InteractiveField(props) {
6319
6464
  slotPatches
6320
6465
  ]
6321
6466
  );
6322
- const poses = (0, import_react14.useMemo)(
6467
+ const poses = (0, import_react15.useMemo)(
6323
6468
  () => slotConfigs.map((_, i) => layout.pose(i, total, layoutOptions, 0, props.sheet)),
6324
6469
  [layoutId, JSON.stringify(layoutOptions), total, props.sheet.width, props.sheet.height]
6325
6470
  );
6326
- const groupRefs = (0, import_react14.useRef)([]);
6327
- const handleRefs = (0, import_react14.useRef)([]);
6328
- const pressRef = (0, import_react14.useRef)(null);
6329
- const carriedRef = (0, import_react14.useRef)(null);
6471
+ const groupRefs = (0, import_react15.useRef)([]);
6472
+ const handleRefs = (0, import_react15.useRef)([]);
6473
+ const pressRef = (0, import_react15.useRef)(null);
6474
+ const carriedRef = (0, import_react15.useRef)(null);
6330
6475
  const zonesLive = () => [...props.zones ?? [], ...registry4.list()];
6331
6476
  const slotName = (i) => {
6332
6477
  const preset = papers[i]?.preset ?? fallback;
@@ -6339,10 +6484,10 @@ function InteractiveField(props) {
6339
6484
  }
6340
6485
  props.onSlotStateChange?.(i, state);
6341
6486
  };
6342
- const raycaster = (0, import_react14.useMemo)(() => new THREE14.Raycaster(), []);
6343
- const planeScratch = (0, import_react14.useMemo)(() => new THREE14.Plane(new THREE14.Vector3(0, 0, 1), 0), []);
6344
- const pointScratch = (0, import_react14.useMemo)(() => new THREE14.Vector3(), []);
6345
- const ndcScratch = (0, import_react14.useMemo)(() => new THREE14.Vector2(), []);
6487
+ const raycaster = (0, import_react15.useMemo)(() => new THREE15.Raycaster(), []);
6488
+ const planeScratch = (0, import_react15.useMemo)(() => new THREE15.Plane(new THREE15.Vector3(0, 0, 1), 0), []);
6489
+ const pointScratch = (0, import_react15.useMemo)(() => new THREE15.Vector3(), []);
6490
+ const ndcScratch = (0, import_react15.useMemo)(() => new THREE15.Vector2(), []);
6346
6491
  const planePoint = (clientX, clientY, planeZ) => {
6347
6492
  const rect = gl.domElement.getBoundingClientRect();
6348
6493
  ndcScratch.set(
@@ -6472,7 +6617,7 @@ function InteractiveField(props) {
6472
6617
  import_gsap4.gsap.to(group.rotation, { x: home.rotation[0], y: home.rotation[1], z: home.rotation[2], duration });
6473
6618
  import_gsap4.gsap.to(group.scale, { x: home.scale, y: home.scale, z: home.scale, duration: duration * 0.5 });
6474
6619
  };
6475
- const onPointerMoveRef = (0, import_react14.useRef)(() => {
6620
+ const onPointerMoveRef = (0, import_react15.useRef)(() => {
6476
6621
  });
6477
6622
  onPointerMoveRef.current = (e) => {
6478
6623
  const press = pressRef.current;
@@ -6496,7 +6641,7 @@ function InteractiveField(props) {
6496
6641
  pick(slot, hit[0], hit[1], pointerId);
6497
6642
  }
6498
6643
  };
6499
- const onPointerUpRef = (0, import_react14.useRef)(() => {
6644
+ const onPointerUpRef = (0, import_react15.useRef)(() => {
6500
6645
  });
6501
6646
  onPointerUpRef.current = (e) => {
6502
6647
  pressRef.current = null;
@@ -6505,12 +6650,12 @@ function InteractiveField(props) {
6505
6650
  if (carried.pointerId !== null && e.pointerId !== carried.pointerId) return;
6506
6651
  const name = slotName(carried.slot);
6507
6652
  const zone = zonesLive().find(
6508
- (z26) => zoneContains(z26, carried.x.value, carried.y.value) && zoneAccepts(z26, name)
6653
+ (z25) => zoneContains(z25, carried.x.value, carried.y.value) && zoneAccepts(z25, name)
6509
6654
  );
6510
6655
  if (zone) settleInto(carried, zone);
6511
6656
  else returnHome(carried);
6512
6657
  };
6513
- (0, import_react14.useEffect)(() => {
6658
+ (0, import_react15.useEffect)(() => {
6514
6659
  const move = (e) => onPointerMoveRef.current(e);
6515
6660
  const up = (e) => onPointerUpRef.current(e);
6516
6661
  window.addEventListener("pointermove", move);
@@ -6520,7 +6665,7 @@ function InteractiveField(props) {
6520
6665
  window.removeEventListener("pointerup", up);
6521
6666
  };
6522
6667
  }, []);
6523
- (0, import_fiber5.useFrame)((_, delta) => {
6668
+ (0, import_fiber6.useFrame)((_, delta) => {
6524
6669
  const carried = carriedRef.current;
6525
6670
  if (!carried || carried.settling) return;
6526
6671
  const group = groupRefs.current[carried.slot];
@@ -6546,19 +6691,19 @@ function InteractiveField(props) {
6546
6691
  group.position.z = carried.homePose.position[2] + 0.15;
6547
6692
  handle.set("drive", carryDrive(speed));
6548
6693
  const lag = 0.25;
6549
- group.rotation.y += (THREE14.MathUtils.clamp(-vx * lag, -0.6, 0.6) - group.rotation.y) * 0.12;
6550
- group.rotation.x += (THREE14.MathUtils.clamp(vy * lag * 0.7, -0.5, 0.5) - group.rotation.x) * 0.12;
6694
+ group.rotation.y += (THREE15.MathUtils.clamp(-vx * lag, -0.6, 0.6) - group.rotation.y) * 0.12;
6695
+ group.rotation.x += (THREE15.MathUtils.clamp(vy * lag * 0.7, -0.5, 0.5) - group.rotation.x) * 0.12;
6551
6696
  }
6552
6697
  const name = slotName(carried.slot);
6553
6698
  const zone = zonesLive().find(
6554
- (z26) => zoneContains(z26, group.position.x, group.position.y) && zoneAccepts(z26, name)
6699
+ (z25) => zoneContains(z25, group.position.x, group.position.y) && zoneAccepts(z25, name)
6555
6700
  );
6556
6701
  registry4.setHovered(zone?.id ?? null);
6557
6702
  const targetScale = (zone ? 1.03 : 1) * carried.homePose.scale;
6558
6703
  group.scale.x += (targetScale - group.scale.x) * 0.15;
6559
6704
  group.scale.y = group.scale.z = group.scale.x;
6560
6705
  });
6561
- const a11yImplRef = (0, import_react14.useRef)(null);
6706
+ const a11yImplRef = (0, import_react15.useRef)(null);
6562
6707
  a11yImplRef.current = {
6563
6708
  pick: (slot) => {
6564
6709
  const home = poses[slot];
@@ -6567,7 +6712,7 @@ function InteractiveField(props) {
6567
6712
  },
6568
6713
  placeAtZone: (slot, zoneId) => {
6569
6714
  const carried = carriedRef.current;
6570
- const zone = zonesLive().find((z26) => z26.id === zoneId);
6715
+ const zone = zonesLive().find((z25) => z25.id === zoneId);
6571
6716
  const group = groupRefs.current[slot];
6572
6717
  if (!carried || carried.slot !== slot || !zone || !group) return;
6573
6718
  group.position.x = zone.bounds.position[0];
@@ -6580,10 +6725,10 @@ function InteractiveField(props) {
6580
6725
  const carried = carriedRef.current;
6581
6726
  if (carried && carried.slot === slot && !carried.settling) returnHome(carried);
6582
6727
  },
6583
- zoneIds: () => zonesLive().map((z26) => z26.id),
6728
+ zoneIds: () => zonesLive().map((z25) => z25.id),
6584
6729
  slotState: (slot) => slotStates[slot] ?? "rest"
6585
6730
  };
6586
- (0, import_react14.useEffect)(() => {
6731
+ (0, import_react15.useEffect)(() => {
6587
6732
  const ref = props.a11yRef;
6588
6733
  if (!ref) return;
6589
6734
  ref.current = {
@@ -6636,7 +6781,7 @@ function InteractiveField(props) {
6636
6781
  }
6637
6782
 
6638
6783
  // src/field/keyboardMirror.tsx
6639
- var import_react15 = require("react");
6784
+ var import_react16 = require("react");
6640
6785
  var import_jsx_runtime11 = require("react/jsx-runtime");
6641
6786
  function fieldKeyboardStep(carry2, slot, key, controller) {
6642
6787
  if (!carry2) {
@@ -6682,7 +6827,7 @@ function FieldKeyboardMirror({
6682
6827
  papers,
6683
6828
  controller
6684
6829
  }) {
6685
- const [carrying, setCarrying] = (0, import_react15.useState)(null);
6830
+ const [carrying, setCarrying] = (0, import_react16.useState)(null);
6686
6831
  const paperLabel = (slot, i) => {
6687
6832
  try {
6688
6833
  const config = resolveConfig({ preset: slot.preset });
@@ -6785,16 +6930,16 @@ function fitCamera(layout, n, options, sheet2, fovDeg, aspect, margin = 1.06) {
6785
6930
 
6786
6931
  // src/PaperField.tsx
6787
6932
  var import_jsx_runtime12 = require("react/jsx-runtime");
6788
- var PaperFieldMesh = (0, import_react16.forwardRef)(
6933
+ var PaperFieldMesh = (0, import_react17.forwardRef)(
6789
6934
  function PaperFieldMesh2(props, ref) {
6790
6935
  const reduced = usePrefersReducedMotion(props.reducedMotion);
6791
- const papers = (0, import_react16.useMemo)(
6936
+ const papers = (0, import_react17.useMemo)(
6792
6937
  () => effectiveFieldPapers(props.papers, props.images),
6793
6938
  [props.papers, props.images]
6794
6939
  );
6795
6940
  const stablePapers = useStable(papers);
6796
6941
  const stablePreset = useStable(props.preset ?? null);
6797
- const groups = (0, import_react16.useMemo)(
6942
+ const groups = (0, import_react17.useMemo)(
6798
6943
  () => groupFieldPapers(stablePapers, stablePreset ?? void 0),
6799
6944
  [stablePapers, stablePreset]
6800
6945
  );
@@ -6803,22 +6948,22 @@ var PaperFieldMesh = (0, import_react16.forwardRef)(
6803
6948
  const layout = getLayout(layoutId);
6804
6949
  const firstSheet = groups[0]?.config.sheet;
6805
6950
  const stableLayoutOptions = useStable(props.layoutOptions ?? {});
6806
- const layoutOptions = (0, import_react16.useMemo)(
6951
+ const layoutOptions = (0, import_react17.useMemo)(
6807
6952
  // Sheet grids size their cells from the papers themselves — gutter is
6808
6953
  // then literally the spacing between stamps (explicit cell dims win).
6809
6954
  () => resolveLayoutOptions(layoutId, layout, stableLayoutOptions, firstSheet),
6810
6955
  [layoutId, stableLayoutOptions, firstSheet?.width, firstSheet?.height]
6811
6956
  );
6812
- const phaseRef = (0, import_react16.useRef)(0);
6813
- const dragVelRef = (0, import_react16.useRef)(0);
6814
- const mountTimeRef = (0, import_react16.useRef)(-1);
6815
- const morphRef = (0, import_react16.useRef)({ from: null, t: 1 });
6816
- const prevLayout = (0, import_react16.useRef)({ id: layoutId, options: layoutOptions });
6817
- const gl = (0, import_fiber6.useThree)((s) => s.gl);
6957
+ const phaseRef = (0, import_react17.useRef)(0);
6958
+ const dragVelRef = (0, import_react17.useRef)(0);
6959
+ const mountTimeRef = (0, import_react17.useRef)(-1);
6960
+ const morphRef = (0, import_react17.useRef)({ from: null, t: 1 });
6961
+ const prevLayout = (0, import_react17.useRef)({ id: layoutId, options: layoutOptions });
6962
+ const gl = (0, import_fiber7.useThree)((s) => s.gl);
6818
6963
  const driver = reduced ? "none" : props.motion?.driver ?? "autoplay";
6819
6964
  const speed = props.motion?.speed ?? 0.5;
6820
6965
  const entranceType = reduced ? "none" : props.entrance?.type ?? "rise";
6821
- (0, import_react16.useEffect)(() => {
6966
+ (0, import_react17.useEffect)(() => {
6822
6967
  const prev = prevLayout.current;
6823
6968
  if (prev.id !== layoutId || JSON.stringify(prev.options) !== JSON.stringify(layoutOptions)) {
6824
6969
  morphRef.current = { from: prev, t: 0 };
@@ -6826,7 +6971,7 @@ var PaperFieldMesh = (0, import_react16.forwardRef)(
6826
6971
  prevLayout.current = { id: layoutId, options: layoutOptions };
6827
6972
  }
6828
6973
  }, [layoutId, layoutOptions]);
6829
- (0, import_react16.useEffect)(() => {
6974
+ (0, import_react17.useEffect)(() => {
6830
6975
  if (driver !== "drag") return;
6831
6976
  const el = gl.domElement;
6832
6977
  let lastX = null;
@@ -6852,7 +6997,7 @@ var PaperFieldMesh = (0, import_react16.forwardRef)(
6852
6997
  window.removeEventListener("pointerup", up);
6853
6998
  };
6854
6999
  }, [driver, gl]);
6855
- (0, import_fiber6.useFrame)(({ clock }, delta) => {
7000
+ (0, import_fiber7.useFrame)(({ clock }, delta) => {
6856
7001
  if (mountTimeRef.current < 0) mountTimeRef.current = clock.elapsedTime;
6857
7002
  if (driver === "autoplay") phaseRef.current += delta * speed * 0.02;
6858
7003
  if (driver === "drag") {
@@ -6910,14 +7055,14 @@ var PaperFieldMesh = (0, import_react16.forwardRef)(
6910
7055
  }
6911
7056
  );
6912
7057
  function FitCamera(meshProps) {
6913
- const camera = (0, import_fiber6.useThree)((s) => s.camera);
6914
- const width = (0, import_fiber6.useThree)((s) => s.size.width);
6915
- const height = (0, import_fiber6.useThree)((s) => s.size.height);
7058
+ const camera = (0, import_fiber7.useThree)((s) => s.camera);
7059
+ const width = (0, import_fiber7.useThree)((s) => s.size.width);
7060
+ const height = (0, import_fiber7.useThree)((s) => s.size.height);
6916
7061
  const papersProp = useStable(meshProps.papers ?? null);
6917
7062
  const imagesProp = useStable(meshProps.images ?? null);
6918
7063
  const presetProp = useStable(meshProps.preset ?? null);
6919
7064
  const optionsProp = useStable(meshProps.layoutOptions ?? {});
6920
- const field = (0, import_react16.useMemo)(() => {
7065
+ const field = (0, import_react17.useMemo)(() => {
6921
7066
  const papers = effectiveFieldPapers(papersProp ?? void 0, imagesProp ?? void 0);
6922
7067
  const layoutId = meshProps.layout ?? "ring";
6923
7068
  const layout = getLayout(layoutId);
@@ -6925,8 +7070,8 @@ function FitCamera(meshProps) {
6925
7070
  const options = resolveLayoutOptions(layoutId, layout, optionsProp, sheet2);
6926
7071
  return { layout, n: papers.length, options, sheet: sheet2 ?? DEFAULT_SHEET };
6927
7072
  }, [papersProp, imagesProp, presetProp, meshProps.layout, optionsProp]);
6928
- (0, import_react16.useEffect)(() => {
6929
- if (!(camera instanceof THREE15.PerspectiveCamera)) return;
7073
+ (0, import_react17.useEffect)(() => {
7074
+ if (!(camera instanceof THREE16.PerspectiveCamera)) return;
6930
7075
  const { position, target } = fitCamera(
6931
7076
  field.layout,
6932
7077
  field.n,
@@ -6941,33 +7086,25 @@ function FitCamera(meshProps) {
6941
7086
  }, [camera, width, height, field]);
6942
7087
  return null;
6943
7088
  }
6944
- var PaperField = (0, import_react16.forwardRef)(function PaperField2({ children, className, style, ...meshProps }, ref) {
6945
- const registry4 = (0, import_react16.useMemo)(() => new DropZoneRegistry(), []);
6946
- const a11yRef = (0, import_react16.useRef)(null);
6947
- const papers = (0, import_react16.useMemo)(
7089
+ var PaperField = (0, import_react17.forwardRef)(function PaperField2({ children, className, style, scene, ...meshProps }, ref) {
7090
+ const rig = sceneSchema.parse(scene ?? {});
7091
+ const registry4 = (0, import_react17.useMemo)(() => new DropZoneRegistry(), []);
7092
+ const a11yRef = (0, import_react17.useRef)(null);
7093
+ const papers = (0, import_react17.useMemo)(
6948
7094
  () => effectiveFieldPapers(meshProps.papers, meshProps.images),
6949
7095
  [meshProps.papers, meshProps.images]
6950
7096
  );
6951
7097
  const stablePapers = useStable(papers);
6952
7098
  const stablePreset = useStable(meshProps.preset ?? null);
6953
- const interactive = (0, import_react16.useMemo)(
7099
+ const interactive = (0, import_react17.useMemo)(
6954
7100
  () => fieldIsInteractive(stablePapers, stablePreset ?? void 0, meshProps.interactive),
6955
7101
  [stablePapers, stablePreset, meshProps.interactive]
6956
7102
  );
6957
7103
  return /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("div", { className, style: { width: "100%", height: "100%", ...style }, children: /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)(DropZoneContext.Provider, { value: registry4, children: [
6958
- /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)(import_fiber6.Canvas, { shadows: true, camera: { position: [0, 0.6, 5.2], fov: 45 }, dpr: [1, 2], children: [
7104
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)(import_fiber7.Canvas, { shadows: true, camera: { position: [0, 0.6, 5.2], fov: 45 }, dpr: [1, 2], children: [
6959
7105
  /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(FitCamera, { ...meshProps }),
6960
- /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("ambientLight", { intensity: 0.7 }),
6961
- /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(
6962
- "directionalLight",
6963
- {
6964
- position: [3, 5, 4],
6965
- intensity: 1.4,
6966
- castShadow: true,
6967
- "shadow-mapSize": [1024, 1024],
6968
- "shadow-normalBias": 0.05
6969
- }
6970
- ),
7106
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(PaperBackdrop, { backdrop: rig.backdrop }),
7107
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(PaperLighting, { preset: rig.lighting, light: rig.light, floor: -2.4, scale: 14 }),
6971
7108
  /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(PaperFieldMesh, { ref, a11yControllerRef: a11yRef, ...meshProps }),
6972
7109
  children
6973
7110
  ] }),
@@ -6975,307 +7112,6 @@ var PaperField = (0, import_react16.forwardRef)(function PaperField2({ children,
6975
7112
  ] }) });
6976
7113
  });
6977
7114
 
6978
- // src/field/parity.ts
6979
- var THREE16 = __toESM(require("three"), 1);
6980
- var GRID = 48;
6981
- var PARITY_EPSILON = 5e-4;
6982
- var parityCases = [
6983
- {
6984
- name: "roll: defaults",
6985
- stack: [{ type: "roll", options: { angle: 90, boundary: 0, radius: 0.12, spiral: 0.015 } }],
6986
- sheet: { width: 1, height: 1.4 },
6987
- t: 0
6988
- },
6989
- {
6990
- name: "roll: tight receipt roll, rolling down",
6991
- stack: [{ type: "roll", options: { angle: 270, boundary: -0.4, radius: 0.07, spiral: 0.02 } }],
6992
- sheet: { width: 1, height: 2.6 },
6993
- t: 0
6994
- },
6995
- {
6996
- name: "curl: bottom-right peel",
6997
- stack: [{ type: "curl", options: { corner: "bottom-right", amount: 0.45, radius: 0.2, skew: 0 } }],
6998
- sheet: { width: 1.5, height: 1 },
6999
- t: 0
7000
- },
7001
- {
7002
- name: "curl: skewed top-left",
7003
- stack: [{ type: "curl", options: { corner: "top-left", amount: 0.7, radius: 0.3, skew: 15 } }],
7004
- sheet: { width: 1, height: 1 },
7005
- t: 0
7006
- },
7007
- {
7008
- name: "bend: positive arc at an angle",
7009
- stack: [{ type: "bend", options: { curvature: 1.2, angle: 33 } }],
7010
- sheet: { width: 1, height: 1.4 },
7011
- t: 0
7012
- },
7013
- {
7014
- /**
7015
- * The gentle end, which nothing used to cover: the gate only ever tried
7016
- * |curvature| ≥ 0.6, and `photo-print` — the field starter preset — bends
7017
- * at 0.35. That band is where the arc's float32 evaluation loses its
7018
- * cancellation, and where the two paths were 6e-4 apart until `bend` was
7019
- * rewritten in cancellation-free form. Keep a case down here.
7020
- */
7021
- name: "bend: barely-there arc (the band photo-print lives in)",
7022
- stack: [{ type: "bend", options: { curvature: 0.35, angle: 0 } }],
7023
- sheet: { width: 1.2, height: 0.9 },
7024
- t: 0
7025
- },
7026
- {
7027
- name: "bend: the gentlest arc the schema allows",
7028
- stack: [{ type: "bend", options: { curvature: 0.02, angle: 61 } }],
7029
- sheet: { width: 2, height: 2.6 },
7030
- t: 0
7031
- },
7032
- {
7033
- name: "bend: negative arc",
7034
- stack: [{ type: "bend", options: { curvature: -0.8, angle: 0 } }],
7035
- sheet: { width: 1, height: 1.4 },
7036
- t: 0
7037
- },
7038
- {
7039
- name: "fold: 90\xB0 hinge",
7040
- stack: [{ type: "fold", options: { angle: 90, offset: 0, foldAngle: 90, radius: 0.06 } }],
7041
- sheet: { width: 1, height: 1.4 },
7042
- t: 0
7043
- },
7044
- {
7045
- name: "fold: deep fold, travelling down",
7046
- stack: [{ type: "fold", options: { angle: 270, offset: 0.2, foldAngle: 165, radius: 0.04 } }],
7047
- sheet: { width: 1, height: 1.4 },
7048
- t: 0
7049
- },
7050
- {
7051
- name: "wave: free ripple at t=1.234",
7052
- stack: [
7053
- {
7054
- type: "wave",
7055
- options: { amplitude: 0.05, wavelength: 0.5, speed: 1, angle: 20, pinnedEdge: "none" }
7056
- }
7057
- ],
7058
- sheet: { width: 1, height: 1 },
7059
- t: 1.234
7060
- },
7061
- {
7062
- name: "wave: pinned top at t=2.5",
7063
- stack: [
7064
- {
7065
- type: "wave",
7066
- options: { amplitude: 0.04, wavelength: 0.4, speed: 1.5, angle: 80, pinnedEdge: "top" }
7067
- }
7068
- ],
7069
- sheet: { width: 1.2, height: 1.5 },
7070
- t: 2.5
7071
- },
7072
- {
7073
- name: "drape: banner hung from the top",
7074
- stack: [
7075
- {
7076
- type: "drape",
7077
- options: { amplitude: 0.17, folds: 5, falloff: 1.6, irregular: 0.45, gather: 0.5, pinnedEdge: "top" }
7078
- }
7079
- ],
7080
- sheet: { width: 1.5, height: 8.5 },
7081
- t: 0
7082
- },
7083
- {
7084
- name: "drape: deep irregular folds pinned at the bottom",
7085
- stack: [
7086
- {
7087
- type: "drape",
7088
- options: { amplitude: 0.55, folds: 11, falloff: 0.4, irregular: 1, gather: 1, pinnedEdge: "bottom" }
7089
- }
7090
- ],
7091
- sheet: { width: 2.2, height: 3 },
7092
- t: 0
7093
- },
7094
- {
7095
- name: "crumple: defaults",
7096
- stack: [{ type: "crumple", options: { amount: 0.35, scale: 3, pull: 0.4, seed: 0 } }],
7097
- sheet: { width: 1, height: 1.4 },
7098
- t: 0
7099
- },
7100
- {
7101
- // The adversarial one. `fract` is a sawtooth, so the two halves disagree
7102
- // hardest where a crease lands exactly on a sample — a fine scale and a
7103
- // seed whose fold directions are near-axis puts the most creases in
7104
- // reach of the grid.
7105
- name: "crumple: fully crushed, fine creases, off-axis seed",
7106
- stack: [{ type: "crumple", options: { amount: 1, scale: 7.5, pull: 1, seed: 5 } }],
7107
- sheet: { width: 1.3, height: 0.9 },
7108
- t: 0
7109
- },
7110
- {
7111
- // Crush then curl, which is the order the crumple BEHAVIOR stacks them:
7112
- // the creases have to be placed on the flat sheet, not on a bent one.
7113
- name: "stacked: crumple \u2218 bend (the crumple behavior)",
7114
- stack: [
7115
- { type: "crumple", options: { amount: 0.62, scale: 3.3, pull: 0.5, seed: 2 } },
7116
- { type: "bend", options: { curvature: 0.31, angle: 35 } }
7117
- ],
7118
- sheet: { width: 1.1, height: 1.4 },
7119
- t: 0
7120
- },
7121
- {
7122
- name: "stacked: letter-fold pair (fold \u2218 fold)",
7123
- stack: [
7124
- { type: "fold", options: { angle: 270, offset: 0.2333, foldAngle: 120, radius: 0.05 } },
7125
- { type: "fold", options: { angle: 90, offset: 0.2333, foldAngle: 100, radius: 0.08 } }
7126
- ],
7127
- sheet: { width: 1, height: 1.4 },
7128
- t: 0
7129
- },
7130
- {
7131
- name: "stacked: bend \u2218 roll \u2218 wave",
7132
- stack: [
7133
- { type: "bend", options: { curvature: 0.6, angle: 0 } },
7134
- { type: "roll", options: { angle: 90, boundary: 0.1, radius: 0.15, spiral: 0 } },
7135
- {
7136
- type: "wave",
7137
- options: { amplitude: 0.02, wavelength: 0.6, speed: 0.7, angle: 45, pinnedEdge: "none" }
7138
- }
7139
- ],
7140
- sheet: { width: 1, height: 1.4 },
7141
- t: 0.8
7142
- }
7143
- ];
7144
- function buildParityFragment(stack, sheet2) {
7145
- const composed = buildDisplacementGLSL(stack, sheet2);
7146
- return (
7147
- /* glsl */
7148
- `#version 300 es
7149
- precision highp float;
7150
- uniform float uPlTime;
7151
- uniform float uPlBias;
7152
- ${composed.functionsSrc}
7153
- ${composed.displaceSrc}
7154
- out vec4 outColor;
7155
- void main() {
7156
- vec2 uv = (gl_FragCoord.xy - 0.5) / float(${GRID - 1});
7157
- vec3 p = vec3((uv - 0.5) * uSheet, 0.0);
7158
- outColor = vec4(plDisplace(p, uv, uPlTime, uPlBias), 1.0);
7159
- }
7160
- `
7161
- );
7162
- }
7163
- var VERT = `#version 300 es
7164
- in vec2 aPos;
7165
- void main() { gl_Position = vec4(aPos, 0.0, 1.0); }
7166
- `;
7167
- function compile(gl, type, src) {
7168
- const shader = gl.createShader(type);
7169
- gl.shaderSource(shader, src);
7170
- gl.compileShader(shader);
7171
- if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
7172
- throw new Error(`[paperlab parity] shader compile failed: ${gl.getShaderInfoLog(shader)}`);
7173
- }
7174
- return shader;
7175
- }
7176
- function runCaseOnGPU(gl, c, bias = 1) {
7177
- const composed = buildDisplacementGLSL(c.stack, c.sheet);
7178
- const program = gl.createProgram();
7179
- gl.attachShader(program, compile(gl, gl.VERTEX_SHADER, VERT));
7180
- gl.attachShader(program, compile(gl, gl.FRAGMENT_SHADER, buildParityFragment(c.stack, c.sheet)));
7181
- gl.linkProgram(program);
7182
- if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
7183
- throw new Error(`[paperlab parity] link failed: ${gl.getProgramInfoLog(program)}`);
7184
- }
7185
- gl.useProgram(program);
7186
- for (const [name, value] of Object.entries(composed.uniforms)) {
7187
- const loc = gl.getUniformLocation(program, name);
7188
- if (!loc) continue;
7189
- if (typeof value === "number") gl.uniform1f(loc, value);
7190
- else if (value.length === 2) gl.uniform2f(loc, value[0], value[1]);
7191
- else if (value.length === 3) gl.uniform3f(loc, value[0], value[1], value[2]);
7192
- else gl.uniform4f(loc, value[0], value[1], value[2], value[3]);
7193
- }
7194
- const timeLoc = gl.getUniformLocation(program, "uPlTime");
7195
- if (timeLoc) gl.uniform1f(timeLoc, c.t);
7196
- const biasLoc = gl.getUniformLocation(program, "uPlBias");
7197
- if (biasLoc) gl.uniform1f(biasLoc, bias);
7198
- const quad = gl.createBuffer();
7199
- gl.bindBuffer(gl.ARRAY_BUFFER, quad);
7200
- gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([-1, -1, 3, -1, -1, 3]), gl.STATIC_DRAW);
7201
- const posLoc = gl.getAttribLocation(program, "aPos");
7202
- gl.enableVertexAttribArray(posLoc);
7203
- gl.vertexAttribPointer(posLoc, 2, gl.FLOAT, false, 0, 0);
7204
- const tex = gl.createTexture();
7205
- gl.bindTexture(gl.TEXTURE_2D, tex);
7206
- gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA32F, GRID, GRID, 0, gl.RGBA, gl.FLOAT, null);
7207
- const fbo = gl.createFramebuffer();
7208
- gl.bindFramebuffer(gl.FRAMEBUFFER, fbo);
7209
- gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, tex, 0);
7210
- gl.viewport(0, 0, GRID, GRID);
7211
- gl.drawArrays(gl.TRIANGLES, 0, 3);
7212
- const out = new Float32Array(GRID * GRID * 4);
7213
- gl.readPixels(0, 0, GRID, GRID, gl.RGBA, gl.FLOAT, out);
7214
- gl.bindFramebuffer(gl.FRAMEBUFFER, null);
7215
- return out;
7216
- }
7217
- function runBiasCases(gl) {
7218
- const results = [];
7219
- for (const c of parityCases) {
7220
- const strengths = c.stack.map((i) => getDeformer(i.type).glsl?.strength !== void 0);
7221
- if (!strengths.every((h) => h === strengths[0])) continue;
7222
- const scales = strengths[0];
7223
- const at0 = runCaseOnGPU(gl, c, 0);
7224
- const reference = scales ? null : runCaseOnGPU(gl, c, 1);
7225
- let maxError = 0;
7226
- for (let row = 0; row < GRID; row++) {
7227
- for (let col = 0; col < GRID; col++) {
7228
- const i4 = (row * GRID + col) * 4;
7229
- const ex = reference ? reference[i4] : (col / (GRID - 1) - 0.5) * c.sheet.width;
7230
- const ey = reference ? reference[i4 + 1] : (row / (GRID - 1) - 0.5) * c.sheet.height;
7231
- const ez = reference ? reference[i4 + 2] : 0;
7232
- maxError = Math.max(
7233
- maxError,
7234
- Math.abs(at0[i4] - ex),
7235
- Math.abs(at0[i4 + 1] - ey),
7236
- Math.abs(at0[i4 + 2] - ez)
7237
- );
7238
- }
7239
- }
7240
- results.push({
7241
- name: `bias: ${c.name} \u2192 ${scales ? "flat at 0" : "ignores bias"}`,
7242
- maxError,
7243
- pass: maxError < PARITY_EPSILON
7244
- });
7245
- }
7246
- return results;
7247
- }
7248
- function runParityHarness(canvas) {
7249
- const cnv = canvas ?? document.createElement("canvas");
7250
- const gl = cnv.getContext("webgl2");
7251
- if (!gl) throw new Error("[paperlab parity] WebGL2 unavailable");
7252
- if (!gl.getExtension("EXT_color_buffer_float")) {
7253
- throw new Error("[paperlab parity] EXT_color_buffer_float unavailable");
7254
- }
7255
- const point = new THREE16.Vector3();
7256
- const parity = parityCases.map((c) => {
7257
- const gpu = runCaseOnGPU(gl, c);
7258
- let maxError = 0;
7259
- for (let row = 0; row < GRID; row++) {
7260
- for (let col = 0; col < GRID; col++) {
7261
- const u = col / (GRID - 1);
7262
- const v = row / (GRID - 1);
7263
- point.set((u - 0.5) * c.sheet.width, (v - 0.5) * c.sheet.height, 0);
7264
- displacePoint(point, u, v, c.stack, { t: c.t, sheet: c.sheet });
7265
- const i4 = (row * GRID + col) * 4;
7266
- maxError = Math.max(
7267
- maxError,
7268
- Math.abs(gpu[i4] - point.x),
7269
- Math.abs(gpu[i4 + 1] - point.y),
7270
- Math.abs(gpu[i4 + 2] - point.z)
7271
- );
7272
- }
7273
- }
7274
- return { name: c.name, maxError, pass: maxError < PARITY_EPSILON };
7275
- });
7276
- return [...parity, ...runBiasCases(gl)];
7277
- }
7278
-
7279
7115
  // src/config/diff.ts
7280
7116
  function isPlainObject(v) {
7281
7117
  return typeof v === "object" && v !== null && !Array.isArray(v);
@@ -7320,7 +7156,8 @@ function diffConfig(config) {
7320
7156
  } else if (config.physics !== "none") {
7321
7157
  out.physics = config.physics;
7322
7158
  }
7323
- if (config.scene.lighting !== "studio") out.scene = { lighting: config.scene.lighting };
7159
+ const scene = diffAgainst(config.scene, sceneSchema.parse({}));
7160
+ if (Object.keys(scene).length > 0) out.scene = scene;
7324
7161
  if (config.onTwos) out.onTwos = true;
7325
7162
  if (config.states) out.states = config.states;
7326
7163
  const meta = diffAgainst(config.meta, paperConfigSchema.parse({}).meta);
@@ -7331,14 +7168,35 @@ function jsxValue(value) {
7331
7168
  if (typeof value === "string") return `"${value}"`;
7332
7169
  return `{${JSON.stringify(value)}}`;
7333
7170
  }
7171
+ function withoutUploads(value) {
7172
+ let replaced = 0;
7173
+ const walk = (node) => {
7174
+ if (typeof node === "string") {
7175
+ if (!node.startsWith("data:")) return node;
7176
+ replaced++;
7177
+ const extension = node.startsWith("data:image/png") ? "png" : "jpg";
7178
+ return `/paperlab-image-${replaced}.${extension}`;
7179
+ }
7180
+ if (Array.isArray(node)) return node.map(walk);
7181
+ if (node && typeof node === "object") {
7182
+ return Object.fromEntries(Object.entries(node).map(([k, v]) => [k, walk(v)]));
7183
+ }
7184
+ return node;
7185
+ };
7186
+ return { value: walk(value), replaced };
7187
+ }
7188
+ 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.";
7334
7189
  function buildJsxSnippet(config) {
7335
- const diff = diffConfig(config);
7336
- delete diff.meta;
7190
+ const diffed = diffConfig(config);
7191
+ delete diffed.meta;
7192
+ const { value: diff, replaced } = withoutUploads(diffed);
7337
7193
  const props = Object.entries(diff).map(([key, value]) => ` ${key}=${jsxValue(value)}`);
7338
7194
  if (props.length === 0) return "<Paper />";
7339
- return `<Paper
7195
+ const snippet = `<Paper
7340
7196
  ${props.join("\n")}
7341
7197
  />`;
7198
+ return replaced > 0 ? `${UPLOAD_NOTE}
7199
+ ${snippet}` : snippet;
7342
7200
  }
7343
7201
 
7344
7202
  // src/config/agent-payload.ts
@@ -7388,7 +7246,10 @@ function componentName(config) {
7388
7246
  }
7389
7247
  function buildAgentPayload(config) {
7390
7248
  const name = componentName(config);
7391
- const preset = JSON.stringify(diffConfig(config), null, 2);
7249
+ const { value: stripped, replaced } = withoutUploads(diffConfig(config));
7250
+ const preset = JSON.stringify(stripped, null, 2);
7251
+ const uploadNote = replaced > 0 ? `${UPLOAD_NOTE}
7252
+ ` : "";
7392
7253
  return `Integrate a Paperlab paper component into this project. (paperlab agent-payload v${AGENT_PAYLOAD_VERSION})
7393
7254
 
7394
7255
  1. Install the dependencies:
@@ -7401,7 +7262,7 @@ function buildAgentPayload(config) {
7401
7262
  \`\`\`tsx
7402
7263
  import { Paper, type PaperConfigInput } from 'paperlab'
7403
7264
 
7404
- const preset = ${preset.replace(/\n/g, "\n")} satisfies PaperConfigInput
7265
+ ${uploadNote}const preset = ${preset.replace(/\n/g, "\n")} satisfies PaperConfigInput
7405
7266
 
7406
7267
  export function ${name}() {
7407
7268
  return <Paper preset={preset} />
@@ -7444,7 +7305,9 @@ function distinctFieldPresets(input) {
7444
7305
  function nonDefault(value, defaults) {
7445
7306
  const out = {};
7446
7307
  for (const [k, v] of Object.entries(value ?? {})) {
7447
- if (v !== void 0 && v !== defaults[k]) out[k] = v;
7308
+ if (v === void 0) continue;
7309
+ const same = v === defaults[k] || JSON.stringify(v) === JSON.stringify(defaults[k]);
7310
+ if (!same) out[k] = v;
7448
7311
  }
7449
7312
  return out;
7450
7313
  }
@@ -7457,6 +7320,11 @@ function diffFieldProps(input) {
7457
7320
  if (Object.keys(motion).length > 0) out.motion = motion;
7458
7321
  const entrance = nonDefault(input.entrance, ENTRANCE_DEFAULTS);
7459
7322
  if (Object.keys(entrance).length > 0) out.entrance = entrance;
7323
+ const scene = nonDefault(
7324
+ sceneSchema.parse(input.scene ?? {}),
7325
+ sceneSchema.parse({})
7326
+ );
7327
+ if (Object.keys(scene).length > 0) out.scene = scene;
7460
7328
  return out;
7461
7329
  }
7462
7330
  var LAYOUT_PHRASES = {
@@ -7490,7 +7358,7 @@ function describeFieldConfig(input) {
7490
7358
  parts.push("hovering peels/reacts per paper (interaction states)");
7491
7359
  if (zones.length > 0) {
7492
7360
  parts.push(
7493
- `dragging one past its tear threshold detaches it to carry to the ${zones.map((z26) => z26.id).join(" or ")} zone (release elsewhere flutters it back)`
7361
+ `dragging one past its tear threshold detaches it to carry to the ${zones.map((z25) => z25.id).join(" or ")} zone (release elsewhere flutters it back)`
7494
7362
  );
7495
7363
  }
7496
7364
  } else {
@@ -7598,204 +7466,380 @@ Constraints: don't modify the preset values; three >= 0.160 and React 19 are
7598
7466
  required; the component needs no props.`;
7599
7467
  }
7600
7468
 
7601
- // src/core/modes.ts
7602
- function resolveMode(requested, ctx) {
7603
- if (requested !== "auto") return requested;
7604
- if (ctx.interactive || ctx.physics !== "none" || ctx.count <= 10) return "hero";
7605
- return "field";
7606
- }
7607
- // Annotate the CommonJS export names for ESM import in node:
7608
- 0 && (module.exports = {
7609
- AGENT_PAYLOAD_VERSION,
7610
- AUTO_CEILING,
7611
- ClothSim,
7612
- DropZone,
7613
- DropZoneRegistry,
7614
- FLAT_SEGMENTS,
7615
- LightRig,
7616
- ON_TWOS_FPS,
7617
- PARITY_EPSILON,
7618
- Paper,
7619
- PaperFallback,
7620
- PaperField,
7621
- PaperFieldMesh,
7622
- PaperLighting,
7623
- PaperMaterial,
7624
- PaperMesh,
7625
- PaperMirror,
7626
- PaperStateMachine,
7627
- SAG_TOL,
7628
- SHEET_LIFT,
7629
- TRANSMISSION_GAIN,
7630
- accordion,
7631
- applyDeformerStack,
7632
- atlasGrid,
7633
- backContentSchema,
7634
- barcodeBars,
7635
- behaviorConfigSchema,
7636
- bend,
7637
- bendOptionsSchema,
7638
- book,
7639
- buildAgentPayload,
7640
- buildDisplacementGLSL,
7641
- buildFieldAgentPayload,
7642
- buildFieldComponentSource,
7643
- buildFieldFragmentShader,
7644
- buildFieldVertexShader,
7645
- buildJsxSnippet,
7646
- cardContentSchema,
7647
- carry,
7648
- carryDrive,
7649
- carryOptionsSchema,
7469
+ // src/field/parity.ts
7470
+ var THREE17 = __toESM(require("three"), 1);
7471
+ var GRID = 48;
7472
+ var PARITY_EPSILON = 5e-4;
7473
+ var parityCases = [
7474
+ {
7475
+ name: "roll: defaults",
7476
+ stack: [{ type: "roll", options: { angle: 90, boundary: 0, radius: 0.12, spiral: 0.015 } }],
7477
+ sheet: { width: 1, height: 1.4 },
7478
+ t: 0
7479
+ },
7480
+ {
7481
+ name: "roll: tight receipt roll, rolling down",
7482
+ stack: [{ type: "roll", options: { angle: 270, boundary: -0.4, radius: 0.07, spiral: 0.02 } }],
7483
+ sheet: { width: 1, height: 2.6 },
7484
+ t: 0
7485
+ },
7486
+ {
7487
+ name: "curl: bottom-right peel",
7488
+ stack: [{ type: "curl", options: { corner: "bottom-right", amount: 0.45, radius: 0.2, skew: 0 } }],
7489
+ sheet: { width: 1.5, height: 1 },
7490
+ t: 0
7491
+ },
7492
+ {
7493
+ name: "curl: skewed top-left",
7494
+ stack: [{ type: "curl", options: { corner: "top-left", amount: 0.7, radius: 0.3, skew: 15 } }],
7495
+ sheet: { width: 1, height: 1 },
7496
+ t: 0
7497
+ },
7498
+ {
7499
+ name: "bend: positive arc at an angle",
7500
+ stack: [{ type: "bend", options: { curvature: 1.2, angle: 33 } }],
7501
+ sheet: { width: 1, height: 1.4 },
7502
+ t: 0
7503
+ },
7504
+ {
7505
+ /**
7506
+ * The gentle end, which nothing used to cover: the gate only ever tried
7507
+ * |curvature| ≥ 0.6, and `photo-print` — the field starter preset — bends
7508
+ * at 0.35. That band is where the arc's float32 evaluation loses its
7509
+ * cancellation, and where the two paths were 6e-4 apart until `bend` was
7510
+ * rewritten in cancellation-free form. Keep a case down here.
7511
+ */
7512
+ name: "bend: barely-there arc (the band photo-print lives in)",
7513
+ stack: [{ type: "bend", options: { curvature: 0.35, angle: 0 } }],
7514
+ sheet: { width: 1.2, height: 0.9 },
7515
+ t: 0
7516
+ },
7517
+ {
7518
+ name: "bend: the gentlest arc the schema allows",
7519
+ stack: [{ type: "bend", options: { curvature: 0.02, angle: 61 } }],
7520
+ sheet: { width: 2, height: 2.6 },
7521
+ t: 0
7522
+ },
7523
+ {
7524
+ name: "bend: negative arc",
7525
+ stack: [{ type: "bend", options: { curvature: -0.8, angle: 0 } }],
7526
+ sheet: { width: 1, height: 1.4 },
7527
+ t: 0
7528
+ },
7529
+ {
7530
+ name: "fold: 90\xB0 hinge",
7531
+ stack: [{ type: "fold", options: { angle: 90, offset: 0, foldAngle: 90, radius: 0.06 } }],
7532
+ sheet: { width: 1, height: 1.4 },
7533
+ t: 0
7534
+ },
7535
+ {
7536
+ name: "fold: deep fold, travelling down",
7537
+ stack: [{ type: "fold", options: { angle: 270, offset: 0.2, foldAngle: 165, radius: 0.04 } }],
7538
+ sheet: { width: 1, height: 1.4 },
7539
+ t: 0
7540
+ },
7541
+ {
7542
+ name: "wave: free ripple at t=1.234",
7543
+ stack: [
7544
+ {
7545
+ type: "wave",
7546
+ options: { amplitude: 0.05, wavelength: 0.5, speed: 1, angle: 20, pinnedEdge: "none" }
7547
+ }
7548
+ ],
7549
+ sheet: { width: 1, height: 1 },
7550
+ t: 1.234
7551
+ },
7552
+ {
7553
+ name: "wave: pinned top at t=2.5",
7554
+ stack: [
7555
+ {
7556
+ type: "wave",
7557
+ options: { amplitude: 0.04, wavelength: 0.4, speed: 1.5, angle: 80, pinnedEdge: "top" }
7558
+ }
7559
+ ],
7560
+ sheet: { width: 1.2, height: 1.5 },
7561
+ t: 2.5
7562
+ },
7563
+ {
7564
+ name: "drape: banner hung from the top",
7565
+ stack: [
7566
+ {
7567
+ type: "drape",
7568
+ options: { amplitude: 0.17, folds: 5, falloff: 1.6, irregular: 0.45, gather: 0.5, pinnedEdge: "top" }
7569
+ }
7570
+ ],
7571
+ sheet: { width: 1.5, height: 8.5 },
7572
+ t: 0
7573
+ },
7574
+ {
7575
+ name: "drape: deep irregular folds pinned at the bottom",
7576
+ stack: [
7577
+ {
7578
+ type: "drape",
7579
+ options: { amplitude: 0.55, folds: 11, falloff: 0.4, irregular: 1, gather: 1, pinnedEdge: "bottom" }
7580
+ }
7581
+ ],
7582
+ sheet: { width: 2.2, height: 3 },
7583
+ t: 0
7584
+ },
7585
+ {
7586
+ name: "crumple: defaults",
7587
+ stack: [{ type: "crumple", options: { amount: 0.35, scale: 3, pull: 0.4, seed: 0 } }],
7588
+ sheet: { width: 1, height: 1.4 },
7589
+ t: 0
7590
+ },
7591
+ {
7592
+ // The adversarial one. `fract` is a sawtooth, so the two halves disagree
7593
+ // hardest where a crease lands exactly on a sample — a fine scale and a
7594
+ // seed whose fold directions are near-axis puts the most creases in
7595
+ // reach of the grid.
7596
+ name: "crumple: fully crushed, fine creases, off-axis seed",
7597
+ stack: [{ type: "crumple", options: { amount: 1, scale: 7.5, pull: 1, seed: 5 } }],
7598
+ sheet: { width: 1.3, height: 0.9 },
7599
+ t: 0
7600
+ },
7601
+ {
7602
+ // Crush then curl, which is the order the crumple BEHAVIOR stacks them:
7603
+ // the creases have to be placed on the flat sheet, not on a bent one.
7604
+ name: "stacked: crumple \u2218 bend (the crumple behavior)",
7605
+ stack: [
7606
+ { type: "crumple", options: { amount: 0.62, scale: 3.3, pull: 0.5, seed: 2 } },
7607
+ { type: "bend", options: { curvature: 0.31, angle: 35 } }
7608
+ ],
7609
+ sheet: { width: 1.1, height: 1.4 },
7610
+ t: 0
7611
+ },
7612
+ {
7613
+ name: "stacked: letter-fold pair (fold \u2218 fold)",
7614
+ stack: [
7615
+ { type: "fold", options: { angle: 270, offset: 0.2333, foldAngle: 120, radius: 0.05 } },
7616
+ { type: "fold", options: { angle: 90, offset: 0.2333, foldAngle: 100, radius: 0.08 } }
7617
+ ],
7618
+ sheet: { width: 1, height: 1.4 },
7619
+ t: 0
7620
+ },
7621
+ {
7622
+ name: "stacked: bend \u2218 roll \u2218 wave",
7623
+ stack: [
7624
+ { type: "bend", options: { curvature: 0.6, angle: 0 } },
7625
+ { type: "roll", options: { angle: 90, boundary: 0.1, radius: 0.15, spiral: 0 } },
7626
+ {
7627
+ type: "wave",
7628
+ options: { amplitude: 0.02, wavelength: 0.6, speed: 0.7, angle: 45, pinnedEdge: "none" }
7629
+ }
7630
+ ],
7631
+ sheet: { width: 1, height: 1.4 },
7632
+ t: 0.8
7633
+ }
7634
+ ];
7635
+ function buildParityFragment(stack, sheet2) {
7636
+ const composed = buildDisplacementGLSL(stack, sheet2);
7637
+ return (
7638
+ /* glsl */
7639
+ `#version 300 es
7640
+ precision highp float;
7641
+ uniform float uPlTime;
7642
+ uniform float uPlBias;
7643
+ ${composed.functionsSrc}
7644
+ ${composed.displaceSrc}
7645
+ out vec4 outColor;
7646
+ void main() {
7647
+ vec2 uv = (gl_FragCoord.xy - 0.5) / float(${GRID - 1});
7648
+ vec3 p = vec3((uv - 0.5) * uSheet, 0.0);
7649
+ outColor = vec4(plDisplace(p, uv, uPlTime, uPlBias), 1.0);
7650
+ }
7651
+ `
7652
+ );
7653
+ }
7654
+ var VERT = `#version 300 es
7655
+ in vec2 aPos;
7656
+ void main() { gl_Position = vec4(aPos, 0.0, 1.0); }
7657
+ `;
7658
+ function compile(gl, type, src) {
7659
+ const shader = gl.createShader(type);
7660
+ gl.shaderSource(shader, src);
7661
+ gl.compileShader(shader);
7662
+ if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
7663
+ throw new Error(`[paperlab parity] shader compile failed: ${gl.getShaderInfoLog(shader)}`);
7664
+ }
7665
+ return shader;
7666
+ }
7667
+ function runCaseOnGPU(gl, c, bias = 1) {
7668
+ const composed = buildDisplacementGLSL(c.stack, c.sheet);
7669
+ const program = gl.createProgram();
7670
+ gl.attachShader(program, compile(gl, gl.VERTEX_SHADER, VERT));
7671
+ gl.attachShader(program, compile(gl, gl.FRAGMENT_SHADER, buildParityFragment(c.stack, c.sheet)));
7672
+ gl.linkProgram(program);
7673
+ if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
7674
+ throw new Error(`[paperlab parity] link failed: ${gl.getProgramInfoLog(program)}`);
7675
+ }
7676
+ gl.useProgram(program);
7677
+ for (const [name, value] of Object.entries(composed.uniforms)) {
7678
+ const loc = gl.getUniformLocation(program, name);
7679
+ if (!loc) continue;
7680
+ if (typeof value === "number") gl.uniform1f(loc, value);
7681
+ else if (value.length === 2) gl.uniform2f(loc, value[0], value[1]);
7682
+ else if (value.length === 3) gl.uniform3f(loc, value[0], value[1], value[2]);
7683
+ else gl.uniform4f(loc, value[0], value[1], value[2], value[3]);
7684
+ }
7685
+ const timeLoc = gl.getUniformLocation(program, "uPlTime");
7686
+ if (timeLoc) gl.uniform1f(timeLoc, c.t);
7687
+ const biasLoc = gl.getUniformLocation(program, "uPlBias");
7688
+ if (biasLoc) gl.uniform1f(biasLoc, bias);
7689
+ const quad = gl.createBuffer();
7690
+ gl.bindBuffer(gl.ARRAY_BUFFER, quad);
7691
+ gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([-1, -1, 3, -1, -1, 3]), gl.STATIC_DRAW);
7692
+ const posLoc = gl.getAttribLocation(program, "aPos");
7693
+ gl.enableVertexAttribArray(posLoc);
7694
+ gl.vertexAttribPointer(posLoc, 2, gl.FLOAT, false, 0, 0);
7695
+ const tex = gl.createTexture();
7696
+ gl.bindTexture(gl.TEXTURE_2D, tex);
7697
+ gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA32F, GRID, GRID, 0, gl.RGBA, gl.FLOAT, null);
7698
+ const fbo = gl.createFramebuffer();
7699
+ gl.bindFramebuffer(gl.FRAMEBUFFER, fbo);
7700
+ gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, tex, 0);
7701
+ gl.viewport(0, 0, GRID, GRID);
7702
+ gl.drawArrays(gl.TRIANGLES, 0, 3);
7703
+ const out = new Float32Array(GRID * GRID * 4);
7704
+ gl.readPixels(0, 0, GRID, GRID, gl.RGBA, gl.FLOAT, out);
7705
+ gl.bindFramebuffer(gl.FRAMEBUFFER, null);
7706
+ return out;
7707
+ }
7708
+ function runBiasCases(gl) {
7709
+ const results = [];
7710
+ for (const c of parityCases) {
7711
+ const strengths = c.stack.map((i) => getDeformer(i.type).glsl?.strength !== void 0);
7712
+ if (!strengths.every((h) => h === strengths[0])) continue;
7713
+ const scales = strengths[0];
7714
+ const at0 = runCaseOnGPU(gl, c, 0);
7715
+ const reference = scales ? null : runCaseOnGPU(gl, c, 1);
7716
+ let maxError = 0;
7717
+ for (let row = 0; row < GRID; row++) {
7718
+ for (let col = 0; col < GRID; col++) {
7719
+ const i4 = (row * GRID + col) * 4;
7720
+ const ex = reference ? reference[i4] : (col / (GRID - 1) - 0.5) * c.sheet.width;
7721
+ const ey = reference ? reference[i4 + 1] : (row / (GRID - 1) - 0.5) * c.sheet.height;
7722
+ const ez = reference ? reference[i4 + 2] : 0;
7723
+ maxError = Math.max(
7724
+ maxError,
7725
+ Math.abs(at0[i4] - ex),
7726
+ Math.abs(at0[i4 + 1] - ey),
7727
+ Math.abs(at0[i4 + 2] - ez)
7728
+ );
7729
+ }
7730
+ }
7731
+ results.push({
7732
+ name: `bias: ${c.name} \u2192 ${scales ? "flat at 0" : "ignores bias"}`,
7733
+ maxError,
7734
+ pass: maxError < PARITY_EPSILON
7735
+ });
7736
+ }
7737
+ return results;
7738
+ }
7739
+ function runParityHarness(canvas) {
7740
+ const cnv = canvas ?? document.createElement("canvas");
7741
+ const gl = cnv.getContext("webgl2");
7742
+ if (!gl) throw new Error("[paperlab parity] WebGL2 unavailable");
7743
+ if (!gl.getExtension("EXT_color_buffer_float")) {
7744
+ throw new Error("[paperlab parity] EXT_color_buffer_float unavailable");
7745
+ }
7746
+ const point = new THREE17.Vector3();
7747
+ const parity = parityCases.map((c) => {
7748
+ const gpu = runCaseOnGPU(gl, c);
7749
+ let maxError = 0;
7750
+ for (let row = 0; row < GRID; row++) {
7751
+ for (let col = 0; col < GRID; col++) {
7752
+ const u = col / (GRID - 1);
7753
+ const v = row / (GRID - 1);
7754
+ point.set((u - 0.5) * c.sheet.width, (v - 0.5) * c.sheet.height, 0);
7755
+ displacePoint(point, u, v, c.stack, { t: c.t, sheet: c.sheet });
7756
+ const i4 = (row * GRID + col) * 4;
7757
+ maxError = Math.max(
7758
+ maxError,
7759
+ Math.abs(gpu[i4] - point.x),
7760
+ Math.abs(gpu[i4 + 1] - point.y),
7761
+ Math.abs(gpu[i4 + 2] - point.z)
7762
+ );
7763
+ }
7764
+ }
7765
+ return { name: c.name, maxError, pass: maxError < PARITY_EPSILON };
7766
+ });
7767
+ return [...parity, ...runBiasCases(gl)];
7768
+ }
7769
+ // Annotate the CommonJS export names for ESM import in node:
7770
+ 0 && (module.exports = {
7771
+ AUTO_CEILING,
7772
+ DropZone,
7773
+ FLAT_SEGMENTS,
7774
+ LightRig,
7775
+ PARITY_EPSILON,
7776
+ Paper,
7777
+ PaperBackdrop,
7778
+ PaperField,
7779
+ PaperFieldMesh,
7780
+ PaperLighting,
7781
+ PaperMesh,
7782
+ SAG_TOL,
7783
+ backdropSchema,
7784
+ behaviorConfigSchema,
7785
+ buildAgentPayload,
7786
+ buildFieldAgentPayload,
7787
+ buildFieldComponentSource,
7788
+ buildJsxSnippet,
7650
7789
  clothConfigSchema,
7651
- colonnade,
7652
- composeSurface,
7653
- contentSchema,
7654
- contentText,
7790
+ contentNames,
7791
+ contentSchemaFor,
7655
7792
  coreStateNames,
7656
- cornerNames,
7657
- createSheetGeometry,
7658
- crumple,
7659
- crumpleBehavior,
7660
- crumpleBehaviorOptionsSchema,
7661
- crumpleOptionsSchema,
7662
- curl,
7663
- curlOptionsSchema,
7664
- dampTo,
7665
- deformerInstanceSchema,
7666
7793
  describeConfig,
7667
7794
  describeFieldConfig,
7668
7795
  diffConfig,
7669
7796
  diffFieldProps,
7670
- displacePoint,
7671
- distinctFieldPresets,
7672
- drape,
7673
- drapeOptionsSchema,
7674
- drawBacking,
7675
- fall,
7676
- fallOptionsSchema,
7677
- fan,
7678
- fieldBounds,
7679
- fieldKeyboardStep,
7680
- filmNames,
7681
- fitCamera,
7682
- flattenNumeric,
7683
- flight,
7684
- flightOptionsSchema,
7685
- flightPose,
7686
- flip,
7687
- flipOptionsSchema,
7688
- fly,
7689
- flyOptionsSchema,
7690
- fold,
7691
- foldOptionsSchema,
7692
7797
  getBehavior,
7693
7798
  getDeformer,
7694
- getIdlePreset,
7695
7799
  getLayout,
7696
- getLightingPreset,
7697
7800
  getPreset,
7698
7801
  getStock,
7699
- groupFieldPapers,
7700
- gust,
7701
- hang,
7702
- hangOptionsSchema,
7703
7802
  idleNames,
7704
- idlePresets,
7705
7803
  isBuiltinPreset,
7706
- letterFold,
7707
- letterFoldOptionsSchema,
7708
7804
  lightAngles,
7709
- lightPosition,
7710
7805
  lightSchema,
7711
- lightenHex,
7712
7806
  lightingNames,
7713
- lightingPresets,
7714
7807
  listBehaviors,
7715
7808
  listDeformers,
7716
7809
  listLayouts,
7717
7810
  listPresets,
7718
- makeGoboTexture,
7719
7811
  mergeConfig,
7720
7812
  mergeWithDeletes,
7721
- outwardCorner,
7722
7813
  paperConfigSchema,
7723
7814
  paperEdges,
7724
7815
  paperStatesSchema,
7725
7816
  parityCases,
7726
7817
  parsePreset,
7727
- peel,
7728
- peelOptionsSchema,
7729
7818
  physicsNames,
7730
- physicsSchema,
7731
- pile,
7732
- quantizeProgress,
7733
7819
  quantizeSegments,
7734
- quantizeTime,
7735
- rack,
7736
- receiptContentSchema,
7737
- receiptTotals,
7738
7820
  recordStateOverride,
7739
7821
  registerBehavior,
7740
7822
  registerDeformer,
7741
7823
  registerLayout,
7742
7824
  registerPreset,
7743
- resolveConfig,
7744
- resolveDeformerStack,
7745
- resolveFieldSlotConfig,
7746
7825
  resolveLighting,
7747
- resolveMode,
7748
- resolveSegments,
7749
7826
  resolveStateConfig,
7750
- ribbon,
7751
- ribbonOptionsSchema,
7752
- ring,
7753
- roll,
7754
- rollOptionsSchema,
7755
7827
  runParityHarness,
7756
7828
  sceneSchema,
7757
7829
  segmentsForArc,
7758
7830
  segmentsForSine,
7759
7831
  serializePreset,
7760
- settle,
7761
- settleOptionsSchema,
7762
- sheet,
7763
- sheetBackingSize,
7764
7832
  sheetLayoutSchema,
7765
- sheetSchema,
7766
- sheetSlotXY,
7767
- silhouetteRects,
7768
7833
  spanAlong,
7769
- spill,
7770
- spread,
7771
- stackAutoSegments,
7772
- stackMinSegments,
7773
- stackUniformValues,
7774
7834
  stateDefSchema,
7775
- stateEventTransitions,
7776
- stateTransitionSchema,
7777
7835
  stockNames,
7778
- stockSchema,
7779
7836
  stocks,
7780
- stripStates,
7781
7837
  supportsWebGL,
7782
- surfaceSchema,
7783
- sweep,
7784
- tornEdgesOnDetach,
7785
- translucencyUniforms,
7786
- translucencyValues,
7787
7838
  uniquePresetName,
7788
7839
  unregisterPreset,
7789
- unroll,
7790
- unrollOptionsSchema,
7791
- useContentAtlas,
7792
7840
  usePaperStates,
7793
7841
  usePrefersReducedMotion,
7794
- wall,
7795
- wave,
7796
- waveOptionsSchema,
7797
- withSheetCellFromPaper,
7798
- wrapLines,
7799
- zoneAccepts
7842
+ washSchema,
7843
+ wrapLines
7800
7844
  });
7801
7845
  //# sourceMappingURL=index.cjs.map