paperlab 0.3.1 → 0.4.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,197 +30,73 @@ 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,
44
39
  PaperField: () => PaperField,
45
40
  PaperFieldMesh: () => PaperFieldMesh,
46
41
  PaperLighting: () => PaperLighting,
47
- PaperMaterial: () => PaperMaterial,
48
42
  PaperMesh: () => PaperMesh,
49
- PaperMirror: () => PaperMirror,
50
- PaperStateMachine: () => PaperStateMachine,
51
43
  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,
59
44
  behaviorConfigSchema: () => behaviorConfigSchema,
60
- bend: () => bend,
61
- bendOptionsSchema: () => bendOptionsSchema,
62
- book: () => book,
63
45
  buildAgentPayload: () => buildAgentPayload,
64
- buildDisplacementGLSL: () => buildDisplacementGLSL,
65
46
  buildFieldAgentPayload: () => buildFieldAgentPayload,
66
47
  buildFieldComponentSource: () => buildFieldComponentSource,
67
- buildFieldFragmentShader: () => buildFieldFragmentShader,
68
- buildFieldVertexShader: () => buildFieldVertexShader,
69
48
  buildJsxSnippet: () => buildJsxSnippet,
70
- cardContentSchema: () => cardContentSchema,
71
- carry: () => carry,
72
- carryDrive: () => carryDrive,
73
- carryOptionsSchema: () => carryOptionsSchema,
74
49
  clothConfigSchema: () => clothConfigSchema,
75
- colonnade: () => colonnade,
76
- composeSurface: () => composeSurface,
77
- contentSchema: () => contentSchema,
78
- contentText: () => contentText,
79
50
  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
51
  describeConfig: () => describeConfig,
91
52
  describeFieldConfig: () => describeFieldConfig,
92
53
  diffConfig: () => diffConfig,
93
54
  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
55
  getBehavior: () => getBehavior,
117
56
  getDeformer: () => getDeformer,
118
- getIdlePreset: () => getIdlePreset,
119
57
  getLayout: () => getLayout,
120
- getLightingPreset: () => getLightingPreset,
121
58
  getPreset: () => getPreset,
122
59
  getStock: () => getStock,
123
- groupFieldPapers: () => groupFieldPapers,
124
- gust: () => gust,
125
- hang: () => hang,
126
- hangOptionsSchema: () => hangOptionsSchema,
127
60
  idleNames: () => idleNames,
128
- idlePresets: () => idlePresets,
129
61
  isBuiltinPreset: () => isBuiltinPreset,
130
- letterFold: () => letterFold,
131
- letterFoldOptionsSchema: () => letterFoldOptionsSchema,
132
62
  lightAngles: () => lightAngles,
133
- lightPosition: () => lightPosition,
134
63
  lightSchema: () => lightSchema,
135
- lightenHex: () => lightenHex,
136
64
  lightingNames: () => lightingNames,
137
- lightingPresets: () => lightingPresets,
138
65
  listBehaviors: () => listBehaviors,
139
66
  listDeformers: () => listDeformers,
140
67
  listLayouts: () => listLayouts,
141
68
  listPresets: () => listPresets,
142
- makeGoboTexture: () => makeGoboTexture,
143
69
  mergeConfig: () => mergeConfig,
144
70
  mergeWithDeletes: () => mergeWithDeletes,
145
- outwardCorner: () => outwardCorner,
146
71
  paperConfigSchema: () => paperConfigSchema,
147
72
  paperEdges: () => paperEdges,
148
73
  paperStatesSchema: () => paperStatesSchema,
149
74
  parityCases: () => parityCases,
150
75
  parsePreset: () => parsePreset,
151
- peel: () => peel,
152
- peelOptionsSchema: () => peelOptionsSchema,
153
76
  physicsNames: () => physicsNames,
154
- physicsSchema: () => physicsSchema,
155
- pile: () => pile,
156
- quantizeProgress: () => quantizeProgress,
157
77
  quantizeSegments: () => quantizeSegments,
158
- quantizeTime: () => quantizeTime,
159
- rack: () => rack,
160
- receiptContentSchema: () => receiptContentSchema,
161
- receiptTotals: () => receiptTotals,
162
78
  recordStateOverride: () => recordStateOverride,
163
79
  registerBehavior: () => registerBehavior,
164
80
  registerDeformer: () => registerDeformer,
165
81
  registerLayout: () => registerLayout,
166
82
  registerPreset: () => registerPreset,
167
- resolveConfig: () => resolveConfig,
168
- resolveDeformerStack: () => resolveDeformerStack,
169
- resolveFieldSlotConfig: () => resolveFieldSlotConfig,
170
83
  resolveLighting: () => resolveLighting,
171
- resolveMode: () => resolveMode,
172
- resolveSegments: () => resolveSegments,
173
84
  resolveStateConfig: () => resolveStateConfig,
174
- ribbon: () => ribbon,
175
- ribbonOptionsSchema: () => ribbonOptionsSchema,
176
- ring: () => ring,
177
- roll: () => roll,
178
- rollOptionsSchema: () => rollOptionsSchema,
179
85
  runParityHarness: () => runParityHarness,
180
- sceneSchema: () => sceneSchema,
181
86
  segmentsForArc: () => segmentsForArc,
182
87
  segmentsForSine: () => segmentsForSine,
183
88
  serializePreset: () => serializePreset,
184
- settle: () => settle,
185
- settleOptionsSchema: () => settleOptionsSchema,
186
- sheet: () => sheet,
187
- sheetBackingSize: () => sheetBackingSize,
188
89
  sheetLayoutSchema: () => sheetLayoutSchema,
189
- sheetSchema: () => sheetSchema,
190
- sheetSlotXY: () => sheetSlotXY,
191
- silhouetteRects: () => silhouetteRects,
192
90
  spanAlong: () => spanAlong,
193
- spill: () => spill,
194
- spread: () => spread,
195
- stackAutoSegments: () => stackAutoSegments,
196
- stackMinSegments: () => stackMinSegments,
197
- stackUniformValues: () => stackUniformValues,
198
91
  stateDefSchema: () => stateDefSchema,
199
- stateEventTransitions: () => stateEventTransitions,
200
- stateTransitionSchema: () => stateTransitionSchema,
201
92
  stockNames: () => stockNames,
202
- stockSchema: () => stockSchema,
203
93
  stocks: () => stocks,
204
- stripStates: () => stripStates,
205
94
  supportsWebGL: () => supportsWebGL,
206
- surfaceSchema: () => surfaceSchema,
207
- sweep: () => sweep,
208
- tornEdgesOnDetach: () => tornEdgesOnDetach,
209
- translucencyUniforms: () => translucencyUniforms,
210
- translucencyValues: () => translucencyValues,
211
95
  uniquePresetName: () => uniquePresetName,
212
96
  unregisterPreset: () => unregisterPreset,
213
- unroll: () => unroll,
214
- unrollOptionsSchema: () => unrollOptionsSchema,
215
- useContentAtlas: () => useContentAtlas,
216
97
  usePaperStates: () => usePaperStates,
217
98
  usePrefersReducedMotion: () => usePrefersReducedMotion,
218
- wall: () => wall,
219
- wave: () => wave,
220
- waveOptionsSchema: () => waveOptionsSchema,
221
- withSheetCellFromPaper: () => withSheetCellFromPaper,
222
- wrapLines: () => wrapLines,
223
- zoneAccepts: () => zoneAccepts
99
+ wrapLines: () => wrapLines
224
100
  });
225
101
  module.exports = __toCommonJS(index_exports);
226
102
 
@@ -6975,307 +6851,6 @@ var PaperField = (0, import_react16.forwardRef)(function PaperField2({ children,
6975
6851
  ] }) });
6976
6852
  });
6977
6853
 
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
6854
  // src/config/diff.ts
7280
6855
  function isPlainObject(v) {
7281
6856
  return typeof v === "object" && v !== null && !Array.isArray(v);
@@ -7378,40 +6953,218 @@ function describeConfig(config) {
7378
6953
  `torn (deckled) ${config.surface.deckle.edges.join(" and ")} edge${config.surface.deckle.edges.length > 1 ? "s" : ""}`
7379
6954
  );
7380
6955
  }
7381
- if ((config.surface.aging ?? 0) > 0.3) parts.push("visibly aged and yellowed");
7382
- return parts.join(", ");
7383
- }
7384
- function componentName(config) {
7385
- const raw = config.meta.name === "untitled" ? "PaperlabPaper" : config.meta.name;
7386
- const pascal = raw.replace(/[^a-zA-Z0-9]+(.)/g, (_, c) => c.toUpperCase()).replace(/^./, (c) => c.toUpperCase()).replace(/[^a-zA-Z0-9]/g, "");
7387
- return /^[A-Za-z]/.test(pascal) ? pascal : `Paper${pascal}`;
6956
+ if ((config.surface.aging ?? 0) > 0.3) parts.push("visibly aged and yellowed");
6957
+ return parts.join(", ");
6958
+ }
6959
+ function componentName(config) {
6960
+ const raw = config.meta.name === "untitled" ? "PaperlabPaper" : config.meta.name;
6961
+ const pascal = raw.replace(/[^a-zA-Z0-9]+(.)/g, (_, c) => c.toUpperCase()).replace(/^./, (c) => c.toUpperCase()).replace(/[^a-zA-Z0-9]/g, "");
6962
+ return /^[A-Za-z]/.test(pascal) ? pascal : `Paper${pascal}`;
6963
+ }
6964
+ function buildAgentPayload(config) {
6965
+ const name = componentName(config);
6966
+ const preset = JSON.stringify(diffConfig(config), null, 2);
6967
+ return `Integrate a Paperlab paper component into this project. (paperlab agent-payload v${AGENT_PAYLOAD_VERSION})
6968
+
6969
+ 1. Install the dependencies:
6970
+
6971
+ npm i paperlab three @react-three/fiber gsap
6972
+
6973
+ 2. Create the component below as \`components/${name}.tsx\` (or the project's
6974
+ component convention). It is self-contained and owns its own <Canvas>:
6975
+
6976
+ \`\`\`tsx
6977
+ import { Paper, type PaperConfigInput } from 'paperlab'
6978
+
6979
+ const preset = ${preset.replace(/\n/g, "\n")} satisfies PaperConfigInput
6980
+
6981
+ export function ${name}() {
6982
+ return <Paper preset={preset} />
6983
+ }
6984
+ \`\`\`
6985
+
6986
+ 3. Sizing: the component fills its parent container. Place it where I ask;
6987
+ give the parent an explicit height.
6988
+
6989
+ 4. Verify: run the dev server. You should see ${describeConfig(config)}.
6990
+ If the canvas is blank, the parent container almost certainly has no height \u2014
6991
+ give it one (this is the classic React Three Fiber integration bug, not a
6992
+ paperlab bug).
6993
+
6994
+ Constraints: don't modify the preset values; three >= 0.160 and React 19 are
6995
+ required; the component needs no props.`;
6996
+ }
6997
+
6998
+ // src/config/field-export.ts
6999
+ var MOTION_DEFAULTS = { driver: "autoplay", speed: 0.5 };
7000
+ var ENTRANCE_DEFAULTS = { type: "rise", stagger: 0.06, duration: 0.9 };
7001
+ function camel(name) {
7002
+ const id = name.replace(/[^a-zA-Z0-9]+(.)/g, (_, c) => c.toUpperCase()).replace(/[^a-zA-Z0-9]/g, "");
7003
+ const safe = /^[A-Za-z]/.test(id) ? id : `preset${id}`;
7004
+ return safe.charAt(0).toLowerCase() + safe.slice(1);
7005
+ }
7006
+ function distinctFieldPresets(input) {
7007
+ const byKey = /* @__PURE__ */ new Map();
7008
+ const usedNames = /* @__PURE__ */ new Set();
7009
+ for (const paper of input.papers) {
7010
+ const key = JSON.stringify(paper.preset);
7011
+ if (byKey.has(key)) continue;
7012
+ let varName = camel(paper.presetName);
7013
+ while (usedNames.has(varName)) varName += "2";
7014
+ usedNames.add(varName);
7015
+ byKey.set(key, { varName, presetName: paper.presetName, preset: paper.preset });
7016
+ }
7017
+ return [...byKey.values()];
7018
+ }
7019
+ function nonDefault(value, defaults) {
7020
+ const out = {};
7021
+ for (const [k, v] of Object.entries(value ?? {})) {
7022
+ if (v !== void 0 && v !== defaults[k]) out[k] = v;
7023
+ }
7024
+ return out;
7025
+ }
7026
+ function diffFieldProps(input) {
7027
+ const out = { layout: input.layout };
7028
+ const layout = getLayout(input.layout);
7029
+ const layoutOptions = nonDefault(input.layoutOptions ?? {}, layout.defaults);
7030
+ if (Object.keys(layoutOptions).length > 0) out.layoutOptions = layoutOptions;
7031
+ const motion = nonDefault(input.motion, MOTION_DEFAULTS);
7032
+ if (Object.keys(motion).length > 0) out.motion = motion;
7033
+ const entrance = nonDefault(input.entrance, ENTRANCE_DEFAULTS);
7034
+ if (Object.keys(entrance).length > 0) out.entrance = entrance;
7035
+ return out;
7036
+ }
7037
+ var LAYOUT_PHRASES = {
7038
+ ring: "a ring you can see around",
7039
+ fan: "a fanned swatch deck hinged at one corner",
7040
+ spread: "a stack slid sideways, each sheet bowing further than the last",
7041
+ pile: "a heap on a desk, the sheets underneath pressed flat",
7042
+ wall: "a pinned studio wall of sheets, none hanging quite square",
7043
+ spill: "a dropped stack caught mid-air, each sheet bent its own way",
7044
+ sweep: "a specimen chart of one sheet at every stage of its deformation",
7045
+ book: "an open book, its pages splayed from a shared spine",
7046
+ accordion: "one continuous strip folded into a concertina",
7047
+ rack: "prints stood in a row and leaning back against a wall",
7048
+ sheet: "a stamp-block grid on a shared backing sheet"
7049
+ };
7050
+ function describeFieldConfig(input) {
7051
+ const n = input.papers.length;
7052
+ const stateful = input.papers.some((p) => p.preset.states || p.states);
7053
+ const zones = input.zones ?? [];
7054
+ const parts = [];
7055
+ if (input.layout === "sheet") {
7056
+ const rows = Number(input.layoutOptions?.rows ?? 2);
7057
+ const columns = Number(input.layoutOptions?.columns ?? 5);
7058
+ parts.push(`a ${rows}\xD7${columns} sheet of papers on a shared backing`);
7059
+ } else {
7060
+ const layoutPhrase = LAYOUT_PHRASES[input.layout] ?? `a ${input.layout} layout`;
7061
+ parts.push(`${n} papers arranged in ${layoutPhrase}`);
7062
+ }
7063
+ const driver = input.motion?.driver ?? "autoplay";
7064
+ if (stateful) {
7065
+ parts.push("hovering peels/reacts per paper (interaction states)");
7066
+ if (zones.length > 0) {
7067
+ parts.push(
7068
+ `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)`
7069
+ );
7070
+ }
7071
+ } else {
7072
+ if (driver === "autoplay") parts.push("slowly orbiting on their own");
7073
+ if (driver === "drag") parts.push("draggable sideways with the pointer");
7074
+ if (zones.length > 0) parts.push(`${zones.length} drop zone${zones.length > 1 ? "s" : ""}`);
7075
+ }
7076
+ const distinct = distinctFieldPresets(input);
7077
+ if (distinct.length > 1) {
7078
+ parts.push(`mixing ${distinct.length} paper presets (${distinct.map((d) => d.presetName).join(", ")})`);
7079
+ }
7080
+ return parts.join(", ");
7081
+ }
7082
+ function buildFieldComponentSource(input) {
7083
+ const distinct = distinctFieldPresets(input);
7084
+ const varByKey = new Map(distinct.map((d) => [JSON.stringify(d.preset), d.varName]));
7085
+ const consts = distinct.map(
7086
+ (d) => `const ${d.varName} = ${JSON.stringify(diffConfig(d.preset), null, 2)} satisfies PaperConfigInput`
7087
+ ).join("\n\n");
7088
+ const slots = input.papers.map((paper) => {
7089
+ const varName = varByKey.get(JSON.stringify(paper.preset));
7090
+ const content = paper.content ? `, content: ${JSON.stringify(paper.content)}` : "";
7091
+ const states = paper.states ? `, states: ${JSON.stringify(paper.states)}` : "";
7092
+ return ` { preset: ${varName}${content}${states} },`;
7093
+ }).join("\n");
7094
+ const props = Object.entries(diffFieldProps(input)).map(
7095
+ ([key, value]) => typeof value === "string" ? ` ${key}="${value}"` : ` ${key}={${JSON.stringify(value)}}`
7096
+ ).join("\n");
7097
+ const zones = input.zones ?? [];
7098
+ const zoneImport = zones.length > 0 ? ", DropZone" : "";
7099
+ const zoneChildren = zones.map((zone) => {
7100
+ const accept = zone.accept ? ` accept={${JSON.stringify(zone.accept)}}` : "";
7101
+ const highlight = zone.highlight && zone.highlight !== "glow" ? ` highlight="${zone.highlight}"` : "";
7102
+ return ` <DropZone
7103
+ id="${zone.id}"${accept}
7104
+ bounds={${JSON.stringify(zone.bounds)}}${highlight}
7105
+ onPlace={(paper, zone) => {
7106
+ // First-run smoke test \u2014 you should see this in the console on drop:
7107
+ console.log(paper.presetName, '\u2192', zone.id)
7108
+ // Then replace it: stamp a postmark, advance the flow, persist the placement, \u2026
7109
+ }}
7110
+ />`;
7111
+ }).join("\n");
7112
+ if (zones.length === 0) {
7113
+ return `import { PaperField, type FieldPaperSlot, type PaperConfigInput${zoneImport} } from 'paperlab'
7114
+
7115
+ ${consts}
7116
+
7117
+ const papers: FieldPaperSlot[] = [
7118
+ ${slots}
7119
+ ]
7120
+
7121
+ export function PaperGallery() {
7122
+ return (
7123
+ <PaperField
7124
+ papers={papers}
7125
+ ${props}
7126
+ />
7127
+ )
7128
+ }`;
7129
+ }
7130
+ return `import { PaperField, type FieldPaperSlot, type PaperConfigInput${zoneImport} } from 'paperlab'
7131
+
7132
+ ${consts}
7133
+
7134
+ const papers: FieldPaperSlot[] = [
7135
+ ${slots}
7136
+ ]
7137
+
7138
+ export function PaperGallery() {
7139
+ return (
7140
+ <PaperField
7141
+ papers={papers}
7142
+ ${props}
7143
+ >
7144
+ ${zoneChildren}
7145
+ </PaperField>
7146
+ )
7147
+ }`;
7388
7148
  }
7389
- function buildAgentPayload(config) {
7390
- const name = componentName(config);
7391
- const preset = JSON.stringify(diffConfig(config), null, 2);
7392
- return `Integrate a Paperlab paper component into this project. (paperlab agent-payload v${AGENT_PAYLOAD_VERSION})
7149
+ function buildFieldAgentPayload(input) {
7150
+ return `Integrate a Paperlab paper gallery into this project. (paperlab agent-payload v${AGENT_PAYLOAD_VERSION})
7393
7151
 
7394
7152
  1. Install the dependencies:
7395
7153
 
7396
7154
  npm i paperlab three @react-three/fiber gsap
7397
7155
 
7398
- 2. Create the component below as \`components/${name}.tsx\` (or the project's
7399
- component convention). It is self-contained and owns its own <Canvas>:
7156
+ 2. Create the component below as \`components/PaperGallery.tsx\` (or the
7157
+ project's component convention). It is self-contained \u2014 every preset it
7158
+ uses is inlined, and it owns its own <Canvas>:
7400
7159
 
7401
7160
  \`\`\`tsx
7402
- import { Paper, type PaperConfigInput } from 'paperlab'
7403
-
7404
- const preset = ${preset.replace(/\n/g, "\n")} satisfies PaperConfigInput
7405
-
7406
- export function ${name}() {
7407
- return <Paper preset={preset} />
7408
- }
7161
+ ${buildFieldComponentSource(input)}
7409
7162
  \`\`\`
7410
7163
 
7411
7164
  3. Sizing: the component fills its parent container. Place it where I ask;
7412
7165
  give the parent an explicit height.
7413
7166
 
7414
- 4. Verify: run the dev server. You should see ${describeConfig(config)}.
7167
+ 4. Verify: run the dev server. You should see ${describeFieldConfig(input)}.
7415
7168
  If the canvas is blank, the parent container almost certainly has no height \u2014
7416
7169
  give it one (this is the classic React Three Fiber integration bug, not a
7417
7170
  paperlab bug).
@@ -7420,382 +7173,374 @@ Constraints: don't modify the preset values; three >= 0.160 and React 19 are
7420
7173
  required; the component needs no props.`;
7421
7174
  }
7422
7175
 
7423
- // src/config/field-export.ts
7424
- var MOTION_DEFAULTS = { driver: "autoplay", speed: 0.5 };
7425
- var ENTRANCE_DEFAULTS = { type: "rise", stagger: 0.06, duration: 0.9 };
7426
- function camel(name) {
7427
- const id = name.replace(/[^a-zA-Z0-9]+(.)/g, (_, c) => c.toUpperCase()).replace(/[^a-zA-Z0-9]/g, "");
7428
- const safe = /^[A-Za-z]/.test(id) ? id : `preset${id}`;
7429
- return safe.charAt(0).toLowerCase() + safe.slice(1);
7430
- }
7431
- function distinctFieldPresets(input) {
7432
- const byKey = /* @__PURE__ */ new Map();
7433
- const usedNames = /* @__PURE__ */ new Set();
7434
- for (const paper of input.papers) {
7435
- const key = JSON.stringify(paper.preset);
7436
- if (byKey.has(key)) continue;
7437
- let varName = camel(paper.presetName);
7438
- while (usedNames.has(varName)) varName += "2";
7439
- usedNames.add(varName);
7440
- byKey.set(key, { varName, presetName: paper.presetName, preset: paper.preset });
7176
+ // src/field/parity.ts
7177
+ var THREE16 = __toESM(require("three"), 1);
7178
+ var GRID = 48;
7179
+ var PARITY_EPSILON = 5e-4;
7180
+ var parityCases = [
7181
+ {
7182
+ name: "roll: defaults",
7183
+ stack: [{ type: "roll", options: { angle: 90, boundary: 0, radius: 0.12, spiral: 0.015 } }],
7184
+ sheet: { width: 1, height: 1.4 },
7185
+ t: 0
7186
+ },
7187
+ {
7188
+ name: "roll: tight receipt roll, rolling down",
7189
+ stack: [{ type: "roll", options: { angle: 270, boundary: -0.4, radius: 0.07, spiral: 0.02 } }],
7190
+ sheet: { width: 1, height: 2.6 },
7191
+ t: 0
7192
+ },
7193
+ {
7194
+ name: "curl: bottom-right peel",
7195
+ stack: [{ type: "curl", options: { corner: "bottom-right", amount: 0.45, radius: 0.2, skew: 0 } }],
7196
+ sheet: { width: 1.5, height: 1 },
7197
+ t: 0
7198
+ },
7199
+ {
7200
+ name: "curl: skewed top-left",
7201
+ stack: [{ type: "curl", options: { corner: "top-left", amount: 0.7, radius: 0.3, skew: 15 } }],
7202
+ sheet: { width: 1, height: 1 },
7203
+ t: 0
7204
+ },
7205
+ {
7206
+ name: "bend: positive arc at an angle",
7207
+ stack: [{ type: "bend", options: { curvature: 1.2, angle: 33 } }],
7208
+ sheet: { width: 1, height: 1.4 },
7209
+ t: 0
7210
+ },
7211
+ {
7212
+ /**
7213
+ * The gentle end, which nothing used to cover: the gate only ever tried
7214
+ * |curvature| ≥ 0.6, and `photo-print` — the field starter preset — bends
7215
+ * at 0.35. That band is where the arc's float32 evaluation loses its
7216
+ * cancellation, and where the two paths were 6e-4 apart until `bend` was
7217
+ * rewritten in cancellation-free form. Keep a case down here.
7218
+ */
7219
+ name: "bend: barely-there arc (the band photo-print lives in)",
7220
+ stack: [{ type: "bend", options: { curvature: 0.35, angle: 0 } }],
7221
+ sheet: { width: 1.2, height: 0.9 },
7222
+ t: 0
7223
+ },
7224
+ {
7225
+ name: "bend: the gentlest arc the schema allows",
7226
+ stack: [{ type: "bend", options: { curvature: 0.02, angle: 61 } }],
7227
+ sheet: { width: 2, height: 2.6 },
7228
+ t: 0
7229
+ },
7230
+ {
7231
+ name: "bend: negative arc",
7232
+ stack: [{ type: "bend", options: { curvature: -0.8, angle: 0 } }],
7233
+ sheet: { width: 1, height: 1.4 },
7234
+ t: 0
7235
+ },
7236
+ {
7237
+ name: "fold: 90\xB0 hinge",
7238
+ stack: [{ type: "fold", options: { angle: 90, offset: 0, foldAngle: 90, radius: 0.06 } }],
7239
+ sheet: { width: 1, height: 1.4 },
7240
+ t: 0
7241
+ },
7242
+ {
7243
+ name: "fold: deep fold, travelling down",
7244
+ stack: [{ type: "fold", options: { angle: 270, offset: 0.2, foldAngle: 165, radius: 0.04 } }],
7245
+ sheet: { width: 1, height: 1.4 },
7246
+ t: 0
7247
+ },
7248
+ {
7249
+ name: "wave: free ripple at t=1.234",
7250
+ stack: [
7251
+ {
7252
+ type: "wave",
7253
+ options: { amplitude: 0.05, wavelength: 0.5, speed: 1, angle: 20, pinnedEdge: "none" }
7254
+ }
7255
+ ],
7256
+ sheet: { width: 1, height: 1 },
7257
+ t: 1.234
7258
+ },
7259
+ {
7260
+ name: "wave: pinned top at t=2.5",
7261
+ stack: [
7262
+ {
7263
+ type: "wave",
7264
+ options: { amplitude: 0.04, wavelength: 0.4, speed: 1.5, angle: 80, pinnedEdge: "top" }
7265
+ }
7266
+ ],
7267
+ sheet: { width: 1.2, height: 1.5 },
7268
+ t: 2.5
7269
+ },
7270
+ {
7271
+ name: "drape: banner hung from the top",
7272
+ stack: [
7273
+ {
7274
+ type: "drape",
7275
+ options: { amplitude: 0.17, folds: 5, falloff: 1.6, irregular: 0.45, gather: 0.5, pinnedEdge: "top" }
7276
+ }
7277
+ ],
7278
+ sheet: { width: 1.5, height: 8.5 },
7279
+ t: 0
7280
+ },
7281
+ {
7282
+ name: "drape: deep irregular folds pinned at the bottom",
7283
+ stack: [
7284
+ {
7285
+ type: "drape",
7286
+ options: { amplitude: 0.55, folds: 11, falloff: 0.4, irregular: 1, gather: 1, pinnedEdge: "bottom" }
7287
+ }
7288
+ ],
7289
+ sheet: { width: 2.2, height: 3 },
7290
+ t: 0
7291
+ },
7292
+ {
7293
+ name: "crumple: defaults",
7294
+ stack: [{ type: "crumple", options: { amount: 0.35, scale: 3, pull: 0.4, seed: 0 } }],
7295
+ sheet: { width: 1, height: 1.4 },
7296
+ t: 0
7297
+ },
7298
+ {
7299
+ // The adversarial one. `fract` is a sawtooth, so the two halves disagree
7300
+ // hardest where a crease lands exactly on a sample — a fine scale and a
7301
+ // seed whose fold directions are near-axis puts the most creases in
7302
+ // reach of the grid.
7303
+ name: "crumple: fully crushed, fine creases, off-axis seed",
7304
+ stack: [{ type: "crumple", options: { amount: 1, scale: 7.5, pull: 1, seed: 5 } }],
7305
+ sheet: { width: 1.3, height: 0.9 },
7306
+ t: 0
7307
+ },
7308
+ {
7309
+ // Crush then curl, which is the order the crumple BEHAVIOR stacks them:
7310
+ // the creases have to be placed on the flat sheet, not on a bent one.
7311
+ name: "stacked: crumple \u2218 bend (the crumple behavior)",
7312
+ stack: [
7313
+ { type: "crumple", options: { amount: 0.62, scale: 3.3, pull: 0.5, seed: 2 } },
7314
+ { type: "bend", options: { curvature: 0.31, angle: 35 } }
7315
+ ],
7316
+ sheet: { width: 1.1, height: 1.4 },
7317
+ t: 0
7318
+ },
7319
+ {
7320
+ name: "stacked: letter-fold pair (fold \u2218 fold)",
7321
+ stack: [
7322
+ { type: "fold", options: { angle: 270, offset: 0.2333, foldAngle: 120, radius: 0.05 } },
7323
+ { type: "fold", options: { angle: 90, offset: 0.2333, foldAngle: 100, radius: 0.08 } }
7324
+ ],
7325
+ sheet: { width: 1, height: 1.4 },
7326
+ t: 0
7327
+ },
7328
+ {
7329
+ name: "stacked: bend \u2218 roll \u2218 wave",
7330
+ stack: [
7331
+ { type: "bend", options: { curvature: 0.6, angle: 0 } },
7332
+ { type: "roll", options: { angle: 90, boundary: 0.1, radius: 0.15, spiral: 0 } },
7333
+ {
7334
+ type: "wave",
7335
+ options: { amplitude: 0.02, wavelength: 0.6, speed: 0.7, angle: 45, pinnedEdge: "none" }
7336
+ }
7337
+ ],
7338
+ sheet: { width: 1, height: 1.4 },
7339
+ t: 0.8
7441
7340
  }
7442
- return [...byKey.values()];
7341
+ ];
7342
+ function buildParityFragment(stack, sheet2) {
7343
+ const composed = buildDisplacementGLSL(stack, sheet2);
7344
+ return (
7345
+ /* glsl */
7346
+ `#version 300 es
7347
+ precision highp float;
7348
+ uniform float uPlTime;
7349
+ uniform float uPlBias;
7350
+ ${composed.functionsSrc}
7351
+ ${composed.displaceSrc}
7352
+ out vec4 outColor;
7353
+ void main() {
7354
+ vec2 uv = (gl_FragCoord.xy - 0.5) / float(${GRID - 1});
7355
+ vec3 p = vec3((uv - 0.5) * uSheet, 0.0);
7356
+ outColor = vec4(plDisplace(p, uv, uPlTime, uPlBias), 1.0);
7357
+ }
7358
+ `
7359
+ );
7443
7360
  }
7444
- function nonDefault(value, defaults) {
7445
- const out = {};
7446
- for (const [k, v] of Object.entries(value ?? {})) {
7447
- if (v !== void 0 && v !== defaults[k]) out[k] = v;
7361
+ var VERT = `#version 300 es
7362
+ in vec2 aPos;
7363
+ void main() { gl_Position = vec4(aPos, 0.0, 1.0); }
7364
+ `;
7365
+ function compile(gl, type, src) {
7366
+ const shader = gl.createShader(type);
7367
+ gl.shaderSource(shader, src);
7368
+ gl.compileShader(shader);
7369
+ if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
7370
+ throw new Error(`[paperlab parity] shader compile failed: ${gl.getShaderInfoLog(shader)}`);
7448
7371
  }
7449
- return out;
7372
+ return shader;
7450
7373
  }
7451
- function diffFieldProps(input) {
7452
- const out = { layout: input.layout };
7453
- const layout = getLayout(input.layout);
7454
- const layoutOptions = nonDefault(input.layoutOptions ?? {}, layout.defaults);
7455
- if (Object.keys(layoutOptions).length > 0) out.layoutOptions = layoutOptions;
7456
- const motion = nonDefault(input.motion, MOTION_DEFAULTS);
7457
- if (Object.keys(motion).length > 0) out.motion = motion;
7458
- const entrance = nonDefault(input.entrance, ENTRANCE_DEFAULTS);
7459
- if (Object.keys(entrance).length > 0) out.entrance = entrance;
7374
+ function runCaseOnGPU(gl, c, bias = 1) {
7375
+ const composed = buildDisplacementGLSL(c.stack, c.sheet);
7376
+ const program = gl.createProgram();
7377
+ gl.attachShader(program, compile(gl, gl.VERTEX_SHADER, VERT));
7378
+ gl.attachShader(program, compile(gl, gl.FRAGMENT_SHADER, buildParityFragment(c.stack, c.sheet)));
7379
+ gl.linkProgram(program);
7380
+ if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
7381
+ throw new Error(`[paperlab parity] link failed: ${gl.getProgramInfoLog(program)}`);
7382
+ }
7383
+ gl.useProgram(program);
7384
+ for (const [name, value] of Object.entries(composed.uniforms)) {
7385
+ const loc = gl.getUniformLocation(program, name);
7386
+ if (!loc) continue;
7387
+ if (typeof value === "number") gl.uniform1f(loc, value);
7388
+ else if (value.length === 2) gl.uniform2f(loc, value[0], value[1]);
7389
+ else if (value.length === 3) gl.uniform3f(loc, value[0], value[1], value[2]);
7390
+ else gl.uniform4f(loc, value[0], value[1], value[2], value[3]);
7391
+ }
7392
+ const timeLoc = gl.getUniformLocation(program, "uPlTime");
7393
+ if (timeLoc) gl.uniform1f(timeLoc, c.t);
7394
+ const biasLoc = gl.getUniformLocation(program, "uPlBias");
7395
+ if (biasLoc) gl.uniform1f(biasLoc, bias);
7396
+ const quad = gl.createBuffer();
7397
+ gl.bindBuffer(gl.ARRAY_BUFFER, quad);
7398
+ gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([-1, -1, 3, -1, -1, 3]), gl.STATIC_DRAW);
7399
+ const posLoc = gl.getAttribLocation(program, "aPos");
7400
+ gl.enableVertexAttribArray(posLoc);
7401
+ gl.vertexAttribPointer(posLoc, 2, gl.FLOAT, false, 0, 0);
7402
+ const tex = gl.createTexture();
7403
+ gl.bindTexture(gl.TEXTURE_2D, tex);
7404
+ gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA32F, GRID, GRID, 0, gl.RGBA, gl.FLOAT, null);
7405
+ const fbo = gl.createFramebuffer();
7406
+ gl.bindFramebuffer(gl.FRAMEBUFFER, fbo);
7407
+ gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, tex, 0);
7408
+ gl.viewport(0, 0, GRID, GRID);
7409
+ gl.drawArrays(gl.TRIANGLES, 0, 3);
7410
+ const out = new Float32Array(GRID * GRID * 4);
7411
+ gl.readPixels(0, 0, GRID, GRID, gl.RGBA, gl.FLOAT, out);
7412
+ gl.bindFramebuffer(gl.FRAMEBUFFER, null);
7460
7413
  return out;
7461
7414
  }
7462
- var LAYOUT_PHRASES = {
7463
- ring: "a ring you can see around",
7464
- fan: "a fanned swatch deck hinged at one corner",
7465
- spread: "a stack slid sideways, each sheet bowing further than the last",
7466
- pile: "a heap on a desk, the sheets underneath pressed flat",
7467
- wall: "a pinned studio wall of sheets, none hanging quite square",
7468
- spill: "a dropped stack caught mid-air, each sheet bent its own way",
7469
- sweep: "a specimen chart of one sheet at every stage of its deformation",
7470
- book: "an open book, its pages splayed from a shared spine",
7471
- accordion: "one continuous strip folded into a concertina",
7472
- rack: "prints stood in a row and leaning back against a wall",
7473
- sheet: "a stamp-block grid on a shared backing sheet"
7474
- };
7475
- function describeFieldConfig(input) {
7476
- const n = input.papers.length;
7477
- const stateful = input.papers.some((p) => p.preset.states || p.states);
7478
- const zones = input.zones ?? [];
7479
- const parts = [];
7480
- if (input.layout === "sheet") {
7481
- const rows = Number(input.layoutOptions?.rows ?? 2);
7482
- const columns = Number(input.layoutOptions?.columns ?? 5);
7483
- parts.push(`a ${rows}\xD7${columns} sheet of papers on a shared backing`);
7484
- } else {
7485
- const layoutPhrase = LAYOUT_PHRASES[input.layout] ?? `a ${input.layout} layout`;
7486
- parts.push(`${n} papers arranged in ${layoutPhrase}`);
7487
- }
7488
- const driver = input.motion?.driver ?? "autoplay";
7489
- if (stateful) {
7490
- parts.push("hovering peels/reacts per paper (interaction states)");
7491
- if (zones.length > 0) {
7492
- 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)`
7494
- );
7415
+ function runBiasCases(gl) {
7416
+ const results = [];
7417
+ for (const c of parityCases) {
7418
+ const strengths = c.stack.map((i) => getDeformer(i.type).glsl?.strength !== void 0);
7419
+ if (!strengths.every((h) => h === strengths[0])) continue;
7420
+ const scales = strengths[0];
7421
+ const at0 = runCaseOnGPU(gl, c, 0);
7422
+ const reference = scales ? null : runCaseOnGPU(gl, c, 1);
7423
+ let maxError = 0;
7424
+ for (let row = 0; row < GRID; row++) {
7425
+ for (let col = 0; col < GRID; col++) {
7426
+ const i4 = (row * GRID + col) * 4;
7427
+ const ex = reference ? reference[i4] : (col / (GRID - 1) - 0.5) * c.sheet.width;
7428
+ const ey = reference ? reference[i4 + 1] : (row / (GRID - 1) - 0.5) * c.sheet.height;
7429
+ const ez = reference ? reference[i4 + 2] : 0;
7430
+ maxError = Math.max(
7431
+ maxError,
7432
+ Math.abs(at0[i4] - ex),
7433
+ Math.abs(at0[i4 + 1] - ey),
7434
+ Math.abs(at0[i4 + 2] - ez)
7435
+ );
7436
+ }
7495
7437
  }
7496
- } else {
7497
- if (driver === "autoplay") parts.push("slowly orbiting on their own");
7498
- if (driver === "drag") parts.push("draggable sideways with the pointer");
7499
- if (zones.length > 0) parts.push(`${zones.length} drop zone${zones.length > 1 ? "s" : ""}`);
7500
- }
7501
- const distinct = distinctFieldPresets(input);
7502
- if (distinct.length > 1) {
7503
- parts.push(`mixing ${distinct.length} paper presets (${distinct.map((d) => d.presetName).join(", ")})`);
7438
+ results.push({
7439
+ name: `bias: ${c.name} \u2192 ${scales ? "flat at 0" : "ignores bias"}`,
7440
+ maxError,
7441
+ pass: maxError < PARITY_EPSILON
7442
+ });
7504
7443
  }
7505
- return parts.join(", ");
7444
+ return results;
7506
7445
  }
7507
- function buildFieldComponentSource(input) {
7508
- const distinct = distinctFieldPresets(input);
7509
- const varByKey = new Map(distinct.map((d) => [JSON.stringify(d.preset), d.varName]));
7510
- const consts = distinct.map(
7511
- (d) => `const ${d.varName} = ${JSON.stringify(diffConfig(d.preset), null, 2)} satisfies PaperConfigInput`
7512
- ).join("\n\n");
7513
- const slots = input.papers.map((paper) => {
7514
- const varName = varByKey.get(JSON.stringify(paper.preset));
7515
- const content = paper.content ? `, content: ${JSON.stringify(paper.content)}` : "";
7516
- const states = paper.states ? `, states: ${JSON.stringify(paper.states)}` : "";
7517
- return ` { preset: ${varName}${content}${states} },`;
7518
- }).join("\n");
7519
- const props = Object.entries(diffFieldProps(input)).map(
7520
- ([key, value]) => typeof value === "string" ? ` ${key}="${value}"` : ` ${key}={${JSON.stringify(value)}}`
7521
- ).join("\n");
7522
- const zones = input.zones ?? [];
7523
- const zoneImport = zones.length > 0 ? ", DropZone" : "";
7524
- const zoneChildren = zones.map((zone) => {
7525
- const accept = zone.accept ? ` accept={${JSON.stringify(zone.accept)}}` : "";
7526
- const highlight = zone.highlight && zone.highlight !== "glow" ? ` highlight="${zone.highlight}"` : "";
7527
- return ` <DropZone
7528
- id="${zone.id}"${accept}
7529
- bounds={${JSON.stringify(zone.bounds)}}${highlight}
7530
- onPlace={(paper, zone) => {
7531
- // First-run smoke test \u2014 you should see this in the console on drop:
7532
- console.log(paper.presetName, '\u2192', zone.id)
7533
- // Then replace it: stamp a postmark, advance the flow, persist the placement, \u2026
7534
- }}
7535
- />`;
7536
- }).join("\n");
7537
- if (zones.length === 0) {
7538
- return `import { PaperField, type FieldPaperSlot, type PaperConfigInput${zoneImport} } from 'paperlab'
7539
-
7540
- ${consts}
7541
-
7542
- const papers: FieldPaperSlot[] = [
7543
- ${slots}
7544
- ]
7545
-
7546
- export function PaperGallery() {
7547
- return (
7548
- <PaperField
7549
- papers={papers}
7550
- ${props}
7551
- />
7552
- )
7553
- }`;
7446
+ function runParityHarness(canvas) {
7447
+ const cnv = canvas ?? document.createElement("canvas");
7448
+ const gl = cnv.getContext("webgl2");
7449
+ if (!gl) throw new Error("[paperlab parity] WebGL2 unavailable");
7450
+ if (!gl.getExtension("EXT_color_buffer_float")) {
7451
+ throw new Error("[paperlab parity] EXT_color_buffer_float unavailable");
7554
7452
  }
7555
- return `import { PaperField, type FieldPaperSlot, type PaperConfigInput${zoneImport} } from 'paperlab'
7556
-
7557
- ${consts}
7558
-
7559
- const papers: FieldPaperSlot[] = [
7560
- ${slots}
7561
- ]
7562
-
7563
- export function PaperGallery() {
7564
- return (
7565
- <PaperField
7566
- papers={papers}
7567
- ${props}
7568
- >
7569
- ${zoneChildren}
7570
- </PaperField>
7571
- )
7572
- }`;
7573
- }
7574
- function buildFieldAgentPayload(input) {
7575
- return `Integrate a Paperlab paper gallery into this project. (paperlab agent-payload v${AGENT_PAYLOAD_VERSION})
7576
-
7577
- 1. Install the dependencies:
7578
-
7579
- npm i paperlab three @react-three/fiber gsap
7580
-
7581
- 2. Create the component below as \`components/PaperGallery.tsx\` (or the
7582
- project's component convention). It is self-contained \u2014 every preset it
7583
- uses is inlined, and it owns its own <Canvas>:
7584
-
7585
- \`\`\`tsx
7586
- ${buildFieldComponentSource(input)}
7587
- \`\`\`
7588
-
7589
- 3. Sizing: the component fills its parent container. Place it where I ask;
7590
- give the parent an explicit height.
7591
-
7592
- 4. Verify: run the dev server. You should see ${describeFieldConfig(input)}.
7593
- If the canvas is blank, the parent container almost certainly has no height \u2014
7594
- give it one (this is the classic React Three Fiber integration bug, not a
7595
- paperlab bug).
7596
-
7597
- Constraints: don't modify the preset values; three >= 0.160 and React 19 are
7598
- required; the component needs no props.`;
7599
- }
7600
-
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";
7453
+ const point = new THREE16.Vector3();
7454
+ const parity = parityCases.map((c) => {
7455
+ const gpu = runCaseOnGPU(gl, c);
7456
+ let maxError = 0;
7457
+ for (let row = 0; row < GRID; row++) {
7458
+ for (let col = 0; col < GRID; col++) {
7459
+ const u = col / (GRID - 1);
7460
+ const v = row / (GRID - 1);
7461
+ point.set((u - 0.5) * c.sheet.width, (v - 0.5) * c.sheet.height, 0);
7462
+ displacePoint(point, u, v, c.stack, { t: c.t, sheet: c.sheet });
7463
+ const i4 = (row * GRID + col) * 4;
7464
+ maxError = Math.max(
7465
+ maxError,
7466
+ Math.abs(gpu[i4] - point.x),
7467
+ Math.abs(gpu[i4 + 1] - point.y),
7468
+ Math.abs(gpu[i4 + 2] - point.z)
7469
+ );
7470
+ }
7471
+ }
7472
+ return { name: c.name, maxError, pass: maxError < PARITY_EPSILON };
7473
+ });
7474
+ return [...parity, ...runBiasCases(gl)];
7606
7475
  }
7607
7476
  // Annotate the CommonJS export names for ESM import in node:
7608
7477
  0 && (module.exports = {
7609
- AGENT_PAYLOAD_VERSION,
7610
7478
  AUTO_CEILING,
7611
- ClothSim,
7612
7479
  DropZone,
7613
- DropZoneRegistry,
7614
7480
  FLAT_SEGMENTS,
7615
7481
  LightRig,
7616
- ON_TWOS_FPS,
7617
7482
  PARITY_EPSILON,
7618
7483
  Paper,
7619
- PaperFallback,
7620
7484
  PaperField,
7621
7485
  PaperFieldMesh,
7622
7486
  PaperLighting,
7623
- PaperMaterial,
7624
7487
  PaperMesh,
7625
- PaperMirror,
7626
- PaperStateMachine,
7627
7488
  SAG_TOL,
7628
- SHEET_LIFT,
7629
- TRANSMISSION_GAIN,
7630
- accordion,
7631
- applyDeformerStack,
7632
- atlasGrid,
7633
- backContentSchema,
7634
- barcodeBars,
7635
7489
  behaviorConfigSchema,
7636
- bend,
7637
- bendOptionsSchema,
7638
- book,
7639
7490
  buildAgentPayload,
7640
- buildDisplacementGLSL,
7641
7491
  buildFieldAgentPayload,
7642
7492
  buildFieldComponentSource,
7643
- buildFieldFragmentShader,
7644
- buildFieldVertexShader,
7645
7493
  buildJsxSnippet,
7646
- cardContentSchema,
7647
- carry,
7648
- carryDrive,
7649
- carryOptionsSchema,
7650
7494
  clothConfigSchema,
7651
- colonnade,
7652
- composeSurface,
7653
- contentSchema,
7654
- contentText,
7655
7495
  coreStateNames,
7656
- cornerNames,
7657
- createSheetGeometry,
7658
- crumple,
7659
- crumpleBehavior,
7660
- crumpleBehaviorOptionsSchema,
7661
- crumpleOptionsSchema,
7662
- curl,
7663
- curlOptionsSchema,
7664
- dampTo,
7665
- deformerInstanceSchema,
7666
7496
  describeConfig,
7667
7497
  describeFieldConfig,
7668
7498
  diffConfig,
7669
7499
  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
7500
  getBehavior,
7693
7501
  getDeformer,
7694
- getIdlePreset,
7695
7502
  getLayout,
7696
- getLightingPreset,
7697
7503
  getPreset,
7698
7504
  getStock,
7699
- groupFieldPapers,
7700
- gust,
7701
- hang,
7702
- hangOptionsSchema,
7703
7505
  idleNames,
7704
- idlePresets,
7705
7506
  isBuiltinPreset,
7706
- letterFold,
7707
- letterFoldOptionsSchema,
7708
7507
  lightAngles,
7709
- lightPosition,
7710
7508
  lightSchema,
7711
- lightenHex,
7712
7509
  lightingNames,
7713
- lightingPresets,
7714
7510
  listBehaviors,
7715
7511
  listDeformers,
7716
7512
  listLayouts,
7717
7513
  listPresets,
7718
- makeGoboTexture,
7719
7514
  mergeConfig,
7720
7515
  mergeWithDeletes,
7721
- outwardCorner,
7722
7516
  paperConfigSchema,
7723
7517
  paperEdges,
7724
7518
  paperStatesSchema,
7725
7519
  parityCases,
7726
7520
  parsePreset,
7727
- peel,
7728
- peelOptionsSchema,
7729
7521
  physicsNames,
7730
- physicsSchema,
7731
- pile,
7732
- quantizeProgress,
7733
7522
  quantizeSegments,
7734
- quantizeTime,
7735
- rack,
7736
- receiptContentSchema,
7737
- receiptTotals,
7738
7523
  recordStateOverride,
7739
7524
  registerBehavior,
7740
7525
  registerDeformer,
7741
7526
  registerLayout,
7742
7527
  registerPreset,
7743
- resolveConfig,
7744
- resolveDeformerStack,
7745
- resolveFieldSlotConfig,
7746
7528
  resolveLighting,
7747
- resolveMode,
7748
- resolveSegments,
7749
7529
  resolveStateConfig,
7750
- ribbon,
7751
- ribbonOptionsSchema,
7752
- ring,
7753
- roll,
7754
- rollOptionsSchema,
7755
7530
  runParityHarness,
7756
- sceneSchema,
7757
7531
  segmentsForArc,
7758
7532
  segmentsForSine,
7759
7533
  serializePreset,
7760
- settle,
7761
- settleOptionsSchema,
7762
- sheet,
7763
- sheetBackingSize,
7764
7534
  sheetLayoutSchema,
7765
- sheetSchema,
7766
- sheetSlotXY,
7767
- silhouetteRects,
7768
7535
  spanAlong,
7769
- spill,
7770
- spread,
7771
- stackAutoSegments,
7772
- stackMinSegments,
7773
- stackUniformValues,
7774
7536
  stateDefSchema,
7775
- stateEventTransitions,
7776
- stateTransitionSchema,
7777
7537
  stockNames,
7778
- stockSchema,
7779
7538
  stocks,
7780
- stripStates,
7781
7539
  supportsWebGL,
7782
- surfaceSchema,
7783
- sweep,
7784
- tornEdgesOnDetach,
7785
- translucencyUniforms,
7786
- translucencyValues,
7787
7540
  uniquePresetName,
7788
7541
  unregisterPreset,
7789
- unroll,
7790
- unrollOptionsSchema,
7791
- useContentAtlas,
7792
7542
  usePaperStates,
7793
7543
  usePrefersReducedMotion,
7794
- wall,
7795
- wave,
7796
- waveOptionsSchema,
7797
- withSheetCellFromPaper,
7798
- wrapLines,
7799
- zoneAccepts
7544
+ wrapLines
7800
7545
  });
7801
7546
  //# sourceMappingURL=index.cjs.map