paperlab 0.5.2 → 0.6.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.d.cts CHANGED
@@ -1,8 +1,8 @@
1
1
  import * as react from 'react';
2
2
  import { ReactNode } from 'react';
3
3
  import * as THREE from 'three';
4
- import { P as PaperConfig, a as PaperConfigInput, S as SheetConfig, b as StockName, C as ContentConfigInput, B as BehaviorConfigInput, D as DeformerInstanceConfigInput, c as SurfaceConfigInput, d as SceneConfigInput, e as PhysicsConfigInput, F as FieldPaperSlot, f as ContentConfig, g as PaperStatesInput, L as LightingName, h as FilmName, i as LightOverrides, j as BackdropConfig } from './slots-CIo7FnlY.cjs';
5
- export { k as BackContentConfig, l as BehaviorConfig, m as ClothConfig, n as CoreStateName, o as DeformerInstanceConfig, p as LightOverridesInput, q as PaperEdge, r as PaperStates, s as PhysicsConfig, t as SceneConfig, u as StateDef, v as StateName, w as StateTransitionConfig, x as SurfaceConfig, W as WashConfig, y as backdropSchema, z as behaviorConfigSchema, A as clothConfigSchema, E as contentNames, G as contentSchemaFor, H as coreStateNames, I as lightSchema, J as lightingNames, K as paperConfigSchema, M as paperEdges, N as paperStatesSchema, O as physicsNames, Q as sceneSchema, R as stateDefSchema, T as stockNames, U as washSchema } from './slots-CIo7FnlY.cjs';
4
+ import { P as PaperConfig, a as PaperConfigInput, S as SheetConfig, b as StockName, C as ContentConfigInput, B as BehaviorConfigInput, D as DeformerInstanceConfigInput, c as SurfaceConfigInput, M as MemoryConfigInput, d as SceneConfigInput, e as PhysicsConfigInput, f as CreaseConfig, F as FieldPaperSlot, g as ContentConfig, h as PaperStatesInput, L as LightingName, i as FilmName, j as LightOverrides, k as BackdropConfig } from './slots-C5PqY0Q5.cjs';
5
+ export { l as BackContentConfig, m as BehaviorConfig, n as ClothConfig, o as CoreStateName, p as CreaseConfigInput, q as DeformerInstanceConfig, r as LightOverridesInput, s as MemoryConfig, t as PaperEdge, u as PaperStates, v as PhysicsConfig, w as SceneConfig, x as StateDef, y as StateName, z as StateTransitionConfig, A as StripConfig, E as SurfaceConfig, W as WashConfig, G as backdropSchema, H as behaviorConfigSchema, I as clothConfigSchema, J as contentNames, K as contentSchemaFor, N as coreStateNames, O as creaseSchema, Q as lightSchema, R as lightingNames, T as memorySchema, U as paperConfigSchema, V as paperEdges, X as paperStatesSchema, Y as physicsNames, Z as sceneSchema, _ as stateDefSchema, $ as stockNames, a0 as stripConfigSchema, a1 as washSchema } from './slots-C5PqY0Q5.cjs';
6
6
  import { z } from 'zod';
7
7
 
8
8
  /**
@@ -133,6 +133,13 @@ interface PaperMeshProps {
133
133
  deformers?: DeformerInstanceConfigInput[];
134
134
  /** Fragment-side effects: grain, aging, deckle, creases, perforation. */
135
135
  surface?: SurfaceConfigInput;
136
+ /**
137
+ * What the sheet remembers being folded — how much of a fold this paper
138
+ * keeps, and the creases it already carries. A sheet can be handed its
139
+ * creases (a letter that arrives having been folded once) as readily as it
140
+ * can be folded into them.
141
+ */
142
+ memory?: MemoryConfigInput;
136
143
  /** Scene-level presentation that travels with the paper (lighting). */
137
144
  scene?: SceneConfigInput;
138
145
  physics?: PhysicsConfigInput | 'cloth';
@@ -149,6 +156,17 @@ interface PaperMeshProps {
149
156
  onProgress?(value: number): void;
150
157
  /** Fires when a handle drag ends, with the params the drag changed. */
151
158
  onBehaviorChange?(patch: Record<string, unknown>): void;
159
+ /**
160
+ * Fires when folding the paper leaves a crease it did not have.
161
+ *
162
+ * The sheet applies its own creases immediately — this is how they get
163
+ * PERSISTED. Recording happens in the frame loop, where a fold's peak
164
+ * actually is, and routing that through React sixty times a second would
165
+ * cost more than the rest of the feature; so the mesh holds the live truth
166
+ * and reports it, and the host writes it into config whenever it likes.
167
+ * The same split `onBehaviorChange` uses for handle drags.
168
+ */
169
+ onCrease?(creases: CreaseConfig[]): void;
152
170
  /**
153
171
  * Interaction states: when the config carries `states`, pointer triggers
154
172
  * are live by default. Set false to sculpt a stateful paper without the
@@ -280,7 +298,7 @@ interface Deformer<O = Record<string, unknown>> {
280
298
  id: string;
281
299
  label: string;
282
300
  defaults: O;
283
- optionsSchema: z.ZodType<O, z.ZodTypeDef, unknown>;
301
+ optionsSchema: z.ZodType<O, unknown>;
284
302
  /** Mutate `out` (sheet-local space; flat sheet is the XY plane facing +Z). */
285
303
  displace(out: THREE.Vector3, uv: THREE.Vector2, o: O, ctx: DeformerContext): void;
286
304
  /** GPU path — arrives with field mode. */
@@ -355,31 +373,12 @@ interface DeformerInstance<O = Record<string, unknown>> {
355
373
  declare const sheetLayoutSchema: z.ZodObject<{
356
374
  rows: z.ZodDefault<z.ZodNumber>;
357
375
  columns: z.ZodDefault<z.ZodNumber>;
358
- /** World-units gap between slots. Stamps are printed in register — no jitter. */
359
376
  gutter: z.ZodDefault<z.ZodNumber>;
360
- /** Slot footprint in world units (the paper preset should match). */
361
377
  cellWidth: z.ZodDefault<z.ZodNumber>;
362
378
  cellHeight: z.ZodDefault<z.ZodNumber>;
363
- /** Render the shared backing sheet behind the grid. */
364
379
  backing: z.ZodDefault<z.ZodBoolean>;
365
380
  backingMargin: z.ZodDefault<z.ZodNumber>;
366
- }, "strip", z.ZodTypeAny, {
367
- columns: number;
368
- rows: number;
369
- gutter: number;
370
- cellWidth: number;
371
- cellHeight: number;
372
- backing: boolean;
373
- backingMargin: number;
374
- }, {
375
- columns?: number | undefined;
376
- rows?: number | undefined;
377
- gutter?: number | undefined;
378
- cellWidth?: number | undefined;
379
- cellHeight?: number | undefined;
380
- backing?: boolean | undefined;
381
- backingMargin?: number | undefined;
382
- }>;
381
+ }, z.core.$strip>;
383
382
  type SheetLayoutOptions = z.infer<typeof sheetLayoutSchema>;
384
383
 
385
384
  /** Imperative field controls for the hidden a11y keyboard flow. */
@@ -605,7 +604,7 @@ interface Behavior<O = AnyOptions> {
605
604
  id: string;
606
605
  label: string;
607
606
  defaults: O;
608
- optionsSchema: z.ZodType<O, z.ZodTypeDef, unknown>;
607
+ optionsSchema: z.ZodType<O, unknown>;
609
608
  /** Expand human params to the underlying deformer stack. */
610
609
  stack(o: O, sheet: SheetDims): DeformerInstance[];
611
610
  /** Transient, time-varying option overrides (idle motion). Never persisted. */
@@ -613,10 +612,14 @@ interface Behavior<O = AnyOptions> {
613
612
  /**
614
613
  * Whole-sheet motion written into `pose` each frame (allocation-free),
615
614
  * composed after any idle preset's transform. Must be a pure function of
616
- * (options, t) — the field applies it per instance with a time offset, so
617
- * it has to be deterministic (flight's travel-across-the-scene).
615
+ * (options, t, sheet) — the field applies it per instance with a time
616
+ * offset, so it has to be deterministic (flight's travel-across-the-scene).
617
+ *
618
+ * `sheet` is passed because whole-sheet motion is often only meaningful
619
+ * relative to the sheet's own extent: `unroll` holds the roll still in
620
+ * space, and where "still" is depends on how long the paper is.
618
621
  */
619
- transform?(o: O, t: number, pose: AeroPose): void;
622
+ transform?(o: O, t: number, pose: AeroPose, sheet: SheetDims): void;
620
623
  handles?: HandleSpec<O>[];
621
624
  /**
622
625
  * The two or three options that ARE this behavior — the ones someone
@@ -670,7 +673,7 @@ interface Layout<O = Record<string, unknown>> {
670
673
  id: string;
671
674
  label: string;
672
675
  defaults: O;
673
- optionsSchema: z.ZodType<O, z.ZodTypeDef, unknown>;
676
+ optionsSchema: z.ZodType<O, unknown>;
674
677
  /**
675
678
  * `sheet` is the field's paper size. Layouts that arrange by CONTACT —
676
679
  * edges meeting, sheets resting on each other — cannot work without it,
@@ -724,6 +727,16 @@ interface Stock {
724
727
  showThrough: number;
725
728
  /** Glossy near-white glue underside (stickers) — forces showThrough 0. */
726
729
  adhesive: boolean;
730
+ /**
731
+ * How hard this paper holds a crease, 0..1 — the material half of
732
+ * {@link MemoryConfig}. Fibrous, thick stocks take a set and keep it;
733
+ * coated and translucent ones spring most of the way back.
734
+ *
735
+ * Not a fraction of anything on its own: `memory.ts` scales it by
736
+ * `MAX_SET`, so 1 here means "as much as paper ever remembers", not "stays
737
+ * exactly as folded".
738
+ */
739
+ takesSet: number;
727
740
  }
728
741
  declare const stocks: Record<StockName, Stock>;
729
742
  declare function getStock(name: StockName): Stock;
@@ -749,6 +762,139 @@ interface IdlePreset {
749
762
  stack?(): DeformerInstance[];
750
763
  }
751
764
 
765
+ /**
766
+ * The longest strip this chain can still draw properly, for a given
767
+ * perforation spacing.
768
+ *
769
+ * {@link stripNodeCount} is CAPPED, and the cap does not fail loudly: past it
770
+ * the node count stops growing, so `segment` (which is `length / (count - 1)`)
771
+ * grows instead, and every constant that was tuned per node quietly changes
772
+ * meaning. The first thing to break is the roll — a longer chord on the same
773
+ * radius dips further inside the wrap beneath it, so the spiral heads for the
774
+ * {@link safeSpiralRadius} threshold and comes apart into a starburst.
775
+ *
776
+ * Exported because it is a UI limit as much as a physics one: a length slider
777
+ * that runs past this hands the user a roll that visibly falls apart, and the
778
+ * editor has no other way to know where the ceiling is. The constants that set
779
+ * it live here, so the derivation does too.
780
+ */
781
+ declare function maxStripLength(perforation: number): number;
782
+
783
+ /**
784
+ * Paper memory: the sheet keeps what has been done to it.
785
+ *
786
+ * Every deformer is a pure function of its options — a sheet folded to 180°
787
+ * and back to 0° flows through the stack and comes out pristine. That is
788
+ * correct for cloth and wrong for paper, which is PLASTIC: the fibres break
789
+ * along the fold and the crease outlives it.
790
+ *
791
+ * The fix lives here rather than inside the deformers, and deliberately.
792
+ * Deformer purity is load-bearing — it is what gives the GLSL twins
793
+ * something to be identical to, what lets the field scale one instance's
794
+ * bend by a per-sheet bias, and what `test:parity` checks. So memory sits
795
+ * ABOVE the stack: it watches the stack the behavior built, and it rewrites
796
+ * that stack before it runs. Nothing below this file knows it exists.
797
+ *
798
+ * The whole layer is hero-path only. The field composes one GLSL program for
799
+ * every sheet in an instanced draw call, and a crease is per-sheet state —
800
+ * so a field of a hundred papers cannot carry a hundred crease sets without
801
+ * per-instance attributes that don't exist yet. `applyMemory` is called from
802
+ * `PaperMesh` and from nowhere in `field/`.
803
+ */
804
+ /**
805
+ * What a paper at `takesSet: 1` keeps of a fold, as a fraction of the angle
806
+ * it was folded to.
807
+ *
808
+ * Kraft folded flat (180°) comes to rest around 30° open, which is `180 ×
809
+ * 0.85 × 0.2`. Keeping this multiplier here rather than folding it into the
810
+ * stock values is what lets `memory.set` and `Stock.takesSet` both be honest
811
+ * 0..1 knobs that use their whole range, instead of designer-facing sliders
812
+ * that do everything interesting in their bottom fifth.
813
+ */
814
+ declare const MAX_SET = 0.2;
815
+ /** What the crease shader carries, and so what memory may record. */
816
+ declare const MAX_CREASES = 4;
817
+ /**
818
+ * Fold a sheet's remembered creases into the stack that is about to run.
819
+ *
820
+ * A crease is a FLOOR on the fold at its line, never an addition. Where the
821
+ * behavior is already folding that line further, the crease is invisible and
822
+ * contributes nothing; as the behavior lets go, the fold angle falls to the
823
+ * crease's depth and stops there. That is what makes the crease appear on
824
+ * the way back out of a fold without anything having to detect the release —
825
+ * and it is why the residual is written INTO the live instance rather than
826
+ * added beside it. Two folds on one line would not be a deeper crease, they
827
+ * would be two creases a hair apart, and `fold` does not commute.
828
+ *
829
+ * Creases with no live fold on their line are prepended: a sheet that is
830
+ * already creased is creased BEFORE the behavior gets hold of it, so the
831
+ * behavior bends a creased sheet rather than the crease bending a bent one.
832
+ */
833
+ declare function applyMemory(stack: DeformerInstance[], creases: CreaseConfig[]): DeformerInstance[];
834
+ /**
835
+ * Watches a running deformer stack and records the creases it leaves.
836
+ *
837
+ * One per sheet, driven from the frame loop. It is deliberately not a
838
+ * reducer over config: recording has to happen at frame rate to catch the
839
+ * peak of a fold, and routing sixty writes a second through React would cost
840
+ * more than the whole rest of the feature. Instead this holds the live truth
841
+ * and says when it has changed, and the host persists that at its own pace —
842
+ * the same split `onBehaviorChange` already uses for handle drags.
843
+ *
844
+ * Slots are keyed by POSITION in the stack, which is the only stable identity
845
+ * a fold has: a behavior's stack is a pure function of its options, so
846
+ * `stack[1]` is the same fold from frame to frame, while its angle and offset
847
+ * are exactly the things that may legitimately move.
848
+ */
849
+ declare class CreaseTracker {
850
+ private slots;
851
+ private recorded;
852
+ private authored;
853
+ /** What `observe` last handed out, so an echo can be told from an edit. */
854
+ private lastReported;
855
+ constructor(authored?: CreaseConfig[]);
856
+ /**
857
+ * Forget how the paper got here without forgetting the creases.
858
+ *
859
+ * Called when the stack is replaced wholesale (a new behavior, a new
860
+ * sheet): the slots describe folds that no longer exist, but a crease is a
861
+ * property of the paper and survives being put down and picked up.
862
+ */
863
+ reset(authored?: CreaseConfig[]): void;
864
+ /**
865
+ * Take on a crease set that came from outside, and work out which kind of
866
+ * outside it was — because the two kinds want opposite things.
867
+ *
868
+ * It is USUALLY this tracker's own recording coming back, a frame or two
869
+ * after `onCrease` handed it to the host. Then the slots must survive: the
870
+ * fold that made the crease is very likely still closing, and resetting
871
+ * its peak on the host's echo would stall the crease halfway into the
872
+ * fold that was making it.
873
+ *
874
+ * But it can also be somebody EDITING the paper — a depth dragged down in
875
+ * a panel, a shared link opened, a state's overrides settling. Then the
876
+ * slots are the wrong story to keep. `merge` takes the deeper of two
877
+ * creases on a line, so a recording of 20° would quietly outvote a human
878
+ * asking for 5° and the slider would appear not to work; and it would go
879
+ * on outvoting it, because the fold that recorded the 20 is still sitting
880
+ * in a slot. An edit means the paper is what it is now, and the next
881
+ * crease has to be earned by folding it again.
882
+ *
883
+ * The two are told apart by what we last reported. Anything else is an
884
+ * edit, which is the safe way round: mistaking an echo for an edit costs
885
+ * a crease that gets re-recorded on the next fold, while mistaking an edit
886
+ * for an echo costs a control that does not work.
887
+ */
888
+ adopt(creases: CreaseConfig[]): void;
889
+ /** Everything the sheet currently carries, authored and recorded merged. */
890
+ get creases(): CreaseConfig[];
891
+ /**
892
+ * Take one frame's reading. Returns true when the crease set changed by
893
+ * enough to be worth telling anyone about.
894
+ */
895
+ observe(stack: DeformerInstance[], set: number): boolean;
896
+ }
897
+
752
898
  /**
753
899
  * Lighting presets: each is a key light + ambient level + contact shadow +
754
900
  * optional gobo (a texture the key light projects — window blinds, foliage).
@@ -1101,4 +1247,4 @@ declare function segmentsForSine(span: number, amplitude: number, wavelength: nu
1101
1247
  */
1102
1248
  declare function wrapLines(ctx: CanvasRenderingContext2D, text: string, maxWidth: number, font: string): string[];
1103
1249
 
1104
- export { AUTO_CEILING, BackdropConfig, type Behavior, BehaviorConfigInput, ContentConfig, ContentConfigInput, type Deformer, type DeformerContext, type DeformerInstance, DeformerInstanceConfigInput, DropZone, type DropZoneConfig, type DropZoneProps, FLAT_SEGMENTS, type FieldExportInput, type FieldExportPaper, type FieldExportZone, FieldPaperSlot, FilmName, type HandleSpec, type IdleName, type IdlePreset, type Layout, type LightAngles, LightOverrides, LightRig, LightingName, type LightingPreset, PARITY_EPSILON, Paper, PaperBackdrop, PaperConfig, PaperConfigInput, PaperField, PaperFieldMesh, type PaperFieldMeshProps, type PaperFieldProps, type PaperHandle, PaperLighting, type PaperLightingProps, PaperMesh, type PaperMeshProps, type PaperPose, type PaperProps, PaperStatesInput, type ParityCase, type ParityResult, PhysicsConfigInput, type PlacedPaper, SAG_TOL, SceneConfigInput, SheetConfig, type SheetDims, type SheetLayoutOptions, type StateEvent, type Stock, StockName, SurfaceConfigInput, type UsePaperStatesResult, buildAgentPayload, buildFieldAgentPayload, buildFieldComponentSource, buildJsxSnippet, describeConfig, describeFieldConfig, diffConfig, diffFieldProps, getBehavior, getDeformer, getLayout, getPreset, getStock, idleNames, isBuiltinPreset, lightAngles, listBehaviors, listDeformers, listLayouts, listPresets, mergeConfig, mergeWithDeletes, parityCases, parsePreset, quantizeSegments, recordStateOverride, registerBehavior, registerDeformer, registerLayout, registerPreset, resolveLighting, resolveStateConfig, runParityHarness, segmentsForArc, segmentsForSine, serializePreset, sheetLayoutSchema, spanAlong, stocks, supportsWebGL, uniquePresetName, unregisterPreset, usePaperStates, usePrefersReducedMotion, wrapLines };
1250
+ export { AUTO_CEILING, BackdropConfig, type Behavior, BehaviorConfigInput, ContentConfig, ContentConfigInput, CreaseConfig, CreaseTracker, type Deformer, type DeformerContext, type DeformerInstance, DeformerInstanceConfigInput, DropZone, type DropZoneConfig, type DropZoneProps, FLAT_SEGMENTS, type FieldExportInput, type FieldExportPaper, type FieldExportZone, FieldPaperSlot, FilmName, type HandleSpec, type IdleName, type IdlePreset, type Layout, type LightAngles, LightOverrides, LightRig, LightingName, type LightingPreset, MAX_CREASES, MAX_SET, MemoryConfigInput, PARITY_EPSILON, Paper, PaperBackdrop, PaperConfig, PaperConfigInput, PaperField, PaperFieldMesh, type PaperFieldMeshProps, type PaperFieldProps, type PaperHandle, PaperLighting, type PaperLightingProps, PaperMesh, type PaperMeshProps, type PaperPose, type PaperProps, PaperStatesInput, type ParityCase, type ParityResult, PhysicsConfigInput, type PlacedPaper, SAG_TOL, SceneConfigInput, SheetConfig, type SheetDims, type SheetLayoutOptions, type StateEvent, type Stock, StockName, SurfaceConfigInput, type UsePaperStatesResult, applyMemory, buildAgentPayload, buildFieldAgentPayload, buildFieldComponentSource, buildJsxSnippet, describeConfig, describeFieldConfig, diffConfig, diffFieldProps, getBehavior, getDeformer, getLayout, getPreset, getStock, idleNames, isBuiltinPreset, lightAngles, listBehaviors, listDeformers, listLayouts, listPresets, maxStripLength, mergeConfig, mergeWithDeletes, parityCases, parsePreset, quantizeSegments, recordStateOverride, registerBehavior, registerDeformer, registerLayout, registerPreset, resolveLighting, resolveStateConfig, runParityHarness, segmentsForArc, segmentsForSine, serializePreset, sheetLayoutSchema, spanAlong, stocks, supportsWebGL, uniquePresetName, unregisterPreset, usePaperStates, usePrefersReducedMotion, wrapLines };
package/dist/index.d.ts CHANGED
@@ -1,8 +1,8 @@
1
1
  import * as react from 'react';
2
2
  import { ReactNode } from 'react';
3
3
  import * as THREE from 'three';
4
- import { P as PaperConfig, a as PaperConfigInput, S as SheetConfig, b as StockName, C as ContentConfigInput, B as BehaviorConfigInput, D as DeformerInstanceConfigInput, c as SurfaceConfigInput, d as SceneConfigInput, e as PhysicsConfigInput, F as FieldPaperSlot, f as ContentConfig, g as PaperStatesInput, L as LightingName, h as FilmName, i as LightOverrides, j as BackdropConfig } from './slots-CIo7FnlY.js';
5
- export { k as BackContentConfig, l as BehaviorConfig, m as ClothConfig, n as CoreStateName, o as DeformerInstanceConfig, p as LightOverridesInput, q as PaperEdge, r as PaperStates, s as PhysicsConfig, t as SceneConfig, u as StateDef, v as StateName, w as StateTransitionConfig, x as SurfaceConfig, W as WashConfig, y as backdropSchema, z as behaviorConfigSchema, A as clothConfigSchema, E as contentNames, G as contentSchemaFor, H as coreStateNames, I as lightSchema, J as lightingNames, K as paperConfigSchema, M as paperEdges, N as paperStatesSchema, O as physicsNames, Q as sceneSchema, R as stateDefSchema, T as stockNames, U as washSchema } from './slots-CIo7FnlY.js';
4
+ import { P as PaperConfig, a as PaperConfigInput, S as SheetConfig, b as StockName, C as ContentConfigInput, B as BehaviorConfigInput, D as DeformerInstanceConfigInput, c as SurfaceConfigInput, M as MemoryConfigInput, d as SceneConfigInput, e as PhysicsConfigInput, f as CreaseConfig, F as FieldPaperSlot, g as ContentConfig, h as PaperStatesInput, L as LightingName, i as FilmName, j as LightOverrides, k as BackdropConfig } from './slots-C5PqY0Q5.js';
5
+ export { l as BackContentConfig, m as BehaviorConfig, n as ClothConfig, o as CoreStateName, p as CreaseConfigInput, q as DeformerInstanceConfig, r as LightOverridesInput, s as MemoryConfig, t as PaperEdge, u as PaperStates, v as PhysicsConfig, w as SceneConfig, x as StateDef, y as StateName, z as StateTransitionConfig, A as StripConfig, E as SurfaceConfig, W as WashConfig, G as backdropSchema, H as behaviorConfigSchema, I as clothConfigSchema, J as contentNames, K as contentSchemaFor, N as coreStateNames, O as creaseSchema, Q as lightSchema, R as lightingNames, T as memorySchema, U as paperConfigSchema, V as paperEdges, X as paperStatesSchema, Y as physicsNames, Z as sceneSchema, _ as stateDefSchema, $ as stockNames, a0 as stripConfigSchema, a1 as washSchema } from './slots-C5PqY0Q5.js';
6
6
  import { z } from 'zod';
7
7
 
8
8
  /**
@@ -133,6 +133,13 @@ interface PaperMeshProps {
133
133
  deformers?: DeformerInstanceConfigInput[];
134
134
  /** Fragment-side effects: grain, aging, deckle, creases, perforation. */
135
135
  surface?: SurfaceConfigInput;
136
+ /**
137
+ * What the sheet remembers being folded — how much of a fold this paper
138
+ * keeps, and the creases it already carries. A sheet can be handed its
139
+ * creases (a letter that arrives having been folded once) as readily as it
140
+ * can be folded into them.
141
+ */
142
+ memory?: MemoryConfigInput;
136
143
  /** Scene-level presentation that travels with the paper (lighting). */
137
144
  scene?: SceneConfigInput;
138
145
  physics?: PhysicsConfigInput | 'cloth';
@@ -149,6 +156,17 @@ interface PaperMeshProps {
149
156
  onProgress?(value: number): void;
150
157
  /** Fires when a handle drag ends, with the params the drag changed. */
151
158
  onBehaviorChange?(patch: Record<string, unknown>): void;
159
+ /**
160
+ * Fires when folding the paper leaves a crease it did not have.
161
+ *
162
+ * The sheet applies its own creases immediately — this is how they get
163
+ * PERSISTED. Recording happens in the frame loop, where a fold's peak
164
+ * actually is, and routing that through React sixty times a second would
165
+ * cost more than the rest of the feature; so the mesh holds the live truth
166
+ * and reports it, and the host writes it into config whenever it likes.
167
+ * The same split `onBehaviorChange` uses for handle drags.
168
+ */
169
+ onCrease?(creases: CreaseConfig[]): void;
152
170
  /**
153
171
  * Interaction states: when the config carries `states`, pointer triggers
154
172
  * are live by default. Set false to sculpt a stateful paper without the
@@ -280,7 +298,7 @@ interface Deformer<O = Record<string, unknown>> {
280
298
  id: string;
281
299
  label: string;
282
300
  defaults: O;
283
- optionsSchema: z.ZodType<O, z.ZodTypeDef, unknown>;
301
+ optionsSchema: z.ZodType<O, unknown>;
284
302
  /** Mutate `out` (sheet-local space; flat sheet is the XY plane facing +Z). */
285
303
  displace(out: THREE.Vector3, uv: THREE.Vector2, o: O, ctx: DeformerContext): void;
286
304
  /** GPU path — arrives with field mode. */
@@ -355,31 +373,12 @@ interface DeformerInstance<O = Record<string, unknown>> {
355
373
  declare const sheetLayoutSchema: z.ZodObject<{
356
374
  rows: z.ZodDefault<z.ZodNumber>;
357
375
  columns: z.ZodDefault<z.ZodNumber>;
358
- /** World-units gap between slots. Stamps are printed in register — no jitter. */
359
376
  gutter: z.ZodDefault<z.ZodNumber>;
360
- /** Slot footprint in world units (the paper preset should match). */
361
377
  cellWidth: z.ZodDefault<z.ZodNumber>;
362
378
  cellHeight: z.ZodDefault<z.ZodNumber>;
363
- /** Render the shared backing sheet behind the grid. */
364
379
  backing: z.ZodDefault<z.ZodBoolean>;
365
380
  backingMargin: z.ZodDefault<z.ZodNumber>;
366
- }, "strip", z.ZodTypeAny, {
367
- columns: number;
368
- rows: number;
369
- gutter: number;
370
- cellWidth: number;
371
- cellHeight: number;
372
- backing: boolean;
373
- backingMargin: number;
374
- }, {
375
- columns?: number | undefined;
376
- rows?: number | undefined;
377
- gutter?: number | undefined;
378
- cellWidth?: number | undefined;
379
- cellHeight?: number | undefined;
380
- backing?: boolean | undefined;
381
- backingMargin?: number | undefined;
382
- }>;
381
+ }, z.core.$strip>;
383
382
  type SheetLayoutOptions = z.infer<typeof sheetLayoutSchema>;
384
383
 
385
384
  /** Imperative field controls for the hidden a11y keyboard flow. */
@@ -605,7 +604,7 @@ interface Behavior<O = AnyOptions> {
605
604
  id: string;
606
605
  label: string;
607
606
  defaults: O;
608
- optionsSchema: z.ZodType<O, z.ZodTypeDef, unknown>;
607
+ optionsSchema: z.ZodType<O, unknown>;
609
608
  /** Expand human params to the underlying deformer stack. */
610
609
  stack(o: O, sheet: SheetDims): DeformerInstance[];
611
610
  /** Transient, time-varying option overrides (idle motion). Never persisted. */
@@ -613,10 +612,14 @@ interface Behavior<O = AnyOptions> {
613
612
  /**
614
613
  * Whole-sheet motion written into `pose` each frame (allocation-free),
615
614
  * composed after any idle preset's transform. Must be a pure function of
616
- * (options, t) — the field applies it per instance with a time offset, so
617
- * it has to be deterministic (flight's travel-across-the-scene).
615
+ * (options, t, sheet) — the field applies it per instance with a time
616
+ * offset, so it has to be deterministic (flight's travel-across-the-scene).
617
+ *
618
+ * `sheet` is passed because whole-sheet motion is often only meaningful
619
+ * relative to the sheet's own extent: `unroll` holds the roll still in
620
+ * space, and where "still" is depends on how long the paper is.
618
621
  */
619
- transform?(o: O, t: number, pose: AeroPose): void;
622
+ transform?(o: O, t: number, pose: AeroPose, sheet: SheetDims): void;
620
623
  handles?: HandleSpec<O>[];
621
624
  /**
622
625
  * The two or three options that ARE this behavior — the ones someone
@@ -670,7 +673,7 @@ interface Layout<O = Record<string, unknown>> {
670
673
  id: string;
671
674
  label: string;
672
675
  defaults: O;
673
- optionsSchema: z.ZodType<O, z.ZodTypeDef, unknown>;
676
+ optionsSchema: z.ZodType<O, unknown>;
674
677
  /**
675
678
  * `sheet` is the field's paper size. Layouts that arrange by CONTACT —
676
679
  * edges meeting, sheets resting on each other — cannot work without it,
@@ -724,6 +727,16 @@ interface Stock {
724
727
  showThrough: number;
725
728
  /** Glossy near-white glue underside (stickers) — forces showThrough 0. */
726
729
  adhesive: boolean;
730
+ /**
731
+ * How hard this paper holds a crease, 0..1 — the material half of
732
+ * {@link MemoryConfig}. Fibrous, thick stocks take a set and keep it;
733
+ * coated and translucent ones spring most of the way back.
734
+ *
735
+ * Not a fraction of anything on its own: `memory.ts` scales it by
736
+ * `MAX_SET`, so 1 here means "as much as paper ever remembers", not "stays
737
+ * exactly as folded".
738
+ */
739
+ takesSet: number;
727
740
  }
728
741
  declare const stocks: Record<StockName, Stock>;
729
742
  declare function getStock(name: StockName): Stock;
@@ -749,6 +762,139 @@ interface IdlePreset {
749
762
  stack?(): DeformerInstance[];
750
763
  }
751
764
 
765
+ /**
766
+ * The longest strip this chain can still draw properly, for a given
767
+ * perforation spacing.
768
+ *
769
+ * {@link stripNodeCount} is CAPPED, and the cap does not fail loudly: past it
770
+ * the node count stops growing, so `segment` (which is `length / (count - 1)`)
771
+ * grows instead, and every constant that was tuned per node quietly changes
772
+ * meaning. The first thing to break is the roll — a longer chord on the same
773
+ * radius dips further inside the wrap beneath it, so the spiral heads for the
774
+ * {@link safeSpiralRadius} threshold and comes apart into a starburst.
775
+ *
776
+ * Exported because it is a UI limit as much as a physics one: a length slider
777
+ * that runs past this hands the user a roll that visibly falls apart, and the
778
+ * editor has no other way to know where the ceiling is. The constants that set
779
+ * it live here, so the derivation does too.
780
+ */
781
+ declare function maxStripLength(perforation: number): number;
782
+
783
+ /**
784
+ * Paper memory: the sheet keeps what has been done to it.
785
+ *
786
+ * Every deformer is a pure function of its options — a sheet folded to 180°
787
+ * and back to 0° flows through the stack and comes out pristine. That is
788
+ * correct for cloth and wrong for paper, which is PLASTIC: the fibres break
789
+ * along the fold and the crease outlives it.
790
+ *
791
+ * The fix lives here rather than inside the deformers, and deliberately.
792
+ * Deformer purity is load-bearing — it is what gives the GLSL twins
793
+ * something to be identical to, what lets the field scale one instance's
794
+ * bend by a per-sheet bias, and what `test:parity` checks. So memory sits
795
+ * ABOVE the stack: it watches the stack the behavior built, and it rewrites
796
+ * that stack before it runs. Nothing below this file knows it exists.
797
+ *
798
+ * The whole layer is hero-path only. The field composes one GLSL program for
799
+ * every sheet in an instanced draw call, and a crease is per-sheet state —
800
+ * so a field of a hundred papers cannot carry a hundred crease sets without
801
+ * per-instance attributes that don't exist yet. `applyMemory` is called from
802
+ * `PaperMesh` and from nowhere in `field/`.
803
+ */
804
+ /**
805
+ * What a paper at `takesSet: 1` keeps of a fold, as a fraction of the angle
806
+ * it was folded to.
807
+ *
808
+ * Kraft folded flat (180°) comes to rest around 30° open, which is `180 ×
809
+ * 0.85 × 0.2`. Keeping this multiplier here rather than folding it into the
810
+ * stock values is what lets `memory.set` and `Stock.takesSet` both be honest
811
+ * 0..1 knobs that use their whole range, instead of designer-facing sliders
812
+ * that do everything interesting in their bottom fifth.
813
+ */
814
+ declare const MAX_SET = 0.2;
815
+ /** What the crease shader carries, and so what memory may record. */
816
+ declare const MAX_CREASES = 4;
817
+ /**
818
+ * Fold a sheet's remembered creases into the stack that is about to run.
819
+ *
820
+ * A crease is a FLOOR on the fold at its line, never an addition. Where the
821
+ * behavior is already folding that line further, the crease is invisible and
822
+ * contributes nothing; as the behavior lets go, the fold angle falls to the
823
+ * crease's depth and stops there. That is what makes the crease appear on
824
+ * the way back out of a fold without anything having to detect the release —
825
+ * and it is why the residual is written INTO the live instance rather than
826
+ * added beside it. Two folds on one line would not be a deeper crease, they
827
+ * would be two creases a hair apart, and `fold` does not commute.
828
+ *
829
+ * Creases with no live fold on their line are prepended: a sheet that is
830
+ * already creased is creased BEFORE the behavior gets hold of it, so the
831
+ * behavior bends a creased sheet rather than the crease bending a bent one.
832
+ */
833
+ declare function applyMemory(stack: DeformerInstance[], creases: CreaseConfig[]): DeformerInstance[];
834
+ /**
835
+ * Watches a running deformer stack and records the creases it leaves.
836
+ *
837
+ * One per sheet, driven from the frame loop. It is deliberately not a
838
+ * reducer over config: recording has to happen at frame rate to catch the
839
+ * peak of a fold, and routing sixty writes a second through React would cost
840
+ * more than the whole rest of the feature. Instead this holds the live truth
841
+ * and says when it has changed, and the host persists that at its own pace —
842
+ * the same split `onBehaviorChange` already uses for handle drags.
843
+ *
844
+ * Slots are keyed by POSITION in the stack, which is the only stable identity
845
+ * a fold has: a behavior's stack is a pure function of its options, so
846
+ * `stack[1]` is the same fold from frame to frame, while its angle and offset
847
+ * are exactly the things that may legitimately move.
848
+ */
849
+ declare class CreaseTracker {
850
+ private slots;
851
+ private recorded;
852
+ private authored;
853
+ /** What `observe` last handed out, so an echo can be told from an edit. */
854
+ private lastReported;
855
+ constructor(authored?: CreaseConfig[]);
856
+ /**
857
+ * Forget how the paper got here without forgetting the creases.
858
+ *
859
+ * Called when the stack is replaced wholesale (a new behavior, a new
860
+ * sheet): the slots describe folds that no longer exist, but a crease is a
861
+ * property of the paper and survives being put down and picked up.
862
+ */
863
+ reset(authored?: CreaseConfig[]): void;
864
+ /**
865
+ * Take on a crease set that came from outside, and work out which kind of
866
+ * outside it was — because the two kinds want opposite things.
867
+ *
868
+ * It is USUALLY this tracker's own recording coming back, a frame or two
869
+ * after `onCrease` handed it to the host. Then the slots must survive: the
870
+ * fold that made the crease is very likely still closing, and resetting
871
+ * its peak on the host's echo would stall the crease halfway into the
872
+ * fold that was making it.
873
+ *
874
+ * But it can also be somebody EDITING the paper — a depth dragged down in
875
+ * a panel, a shared link opened, a state's overrides settling. Then the
876
+ * slots are the wrong story to keep. `merge` takes the deeper of two
877
+ * creases on a line, so a recording of 20° would quietly outvote a human
878
+ * asking for 5° and the slider would appear not to work; and it would go
879
+ * on outvoting it, because the fold that recorded the 20 is still sitting
880
+ * in a slot. An edit means the paper is what it is now, and the next
881
+ * crease has to be earned by folding it again.
882
+ *
883
+ * The two are told apart by what we last reported. Anything else is an
884
+ * edit, which is the safe way round: mistaking an echo for an edit costs
885
+ * a crease that gets re-recorded on the next fold, while mistaking an edit
886
+ * for an echo costs a control that does not work.
887
+ */
888
+ adopt(creases: CreaseConfig[]): void;
889
+ /** Everything the sheet currently carries, authored and recorded merged. */
890
+ get creases(): CreaseConfig[];
891
+ /**
892
+ * Take one frame's reading. Returns true when the crease set changed by
893
+ * enough to be worth telling anyone about.
894
+ */
895
+ observe(stack: DeformerInstance[], set: number): boolean;
896
+ }
897
+
752
898
  /**
753
899
  * Lighting presets: each is a key light + ambient level + contact shadow +
754
900
  * optional gobo (a texture the key light projects — window blinds, foliage).
@@ -1101,4 +1247,4 @@ declare function segmentsForSine(span: number, amplitude: number, wavelength: nu
1101
1247
  */
1102
1248
  declare function wrapLines(ctx: CanvasRenderingContext2D, text: string, maxWidth: number, font: string): string[];
1103
1249
 
1104
- export { AUTO_CEILING, BackdropConfig, type Behavior, BehaviorConfigInput, ContentConfig, ContentConfigInput, type Deformer, type DeformerContext, type DeformerInstance, DeformerInstanceConfigInput, DropZone, type DropZoneConfig, type DropZoneProps, FLAT_SEGMENTS, type FieldExportInput, type FieldExportPaper, type FieldExportZone, FieldPaperSlot, FilmName, type HandleSpec, type IdleName, type IdlePreset, type Layout, type LightAngles, LightOverrides, LightRig, LightingName, type LightingPreset, PARITY_EPSILON, Paper, PaperBackdrop, PaperConfig, PaperConfigInput, PaperField, PaperFieldMesh, type PaperFieldMeshProps, type PaperFieldProps, type PaperHandle, PaperLighting, type PaperLightingProps, PaperMesh, type PaperMeshProps, type PaperPose, type PaperProps, PaperStatesInput, type ParityCase, type ParityResult, PhysicsConfigInput, type PlacedPaper, SAG_TOL, SceneConfigInput, SheetConfig, type SheetDims, type SheetLayoutOptions, type StateEvent, type Stock, StockName, SurfaceConfigInput, type UsePaperStatesResult, buildAgentPayload, buildFieldAgentPayload, buildFieldComponentSource, buildJsxSnippet, describeConfig, describeFieldConfig, diffConfig, diffFieldProps, getBehavior, getDeformer, getLayout, getPreset, getStock, idleNames, isBuiltinPreset, lightAngles, listBehaviors, listDeformers, listLayouts, listPresets, mergeConfig, mergeWithDeletes, parityCases, parsePreset, quantizeSegments, recordStateOverride, registerBehavior, registerDeformer, registerLayout, registerPreset, resolveLighting, resolveStateConfig, runParityHarness, segmentsForArc, segmentsForSine, serializePreset, sheetLayoutSchema, spanAlong, stocks, supportsWebGL, uniquePresetName, unregisterPreset, usePaperStates, usePrefersReducedMotion, wrapLines };
1250
+ export { AUTO_CEILING, BackdropConfig, type Behavior, BehaviorConfigInput, ContentConfig, ContentConfigInput, CreaseConfig, CreaseTracker, type Deformer, type DeformerContext, type DeformerInstance, DeformerInstanceConfigInput, DropZone, type DropZoneConfig, type DropZoneProps, FLAT_SEGMENTS, type FieldExportInput, type FieldExportPaper, type FieldExportZone, FieldPaperSlot, FilmName, type HandleSpec, type IdleName, type IdlePreset, type Layout, type LightAngles, LightOverrides, LightRig, LightingName, type LightingPreset, MAX_CREASES, MAX_SET, MemoryConfigInput, PARITY_EPSILON, Paper, PaperBackdrop, PaperConfig, PaperConfigInput, PaperField, PaperFieldMesh, type PaperFieldMeshProps, type PaperFieldProps, type PaperHandle, PaperLighting, type PaperLightingProps, PaperMesh, type PaperMeshProps, type PaperPose, type PaperProps, PaperStatesInput, type ParityCase, type ParityResult, PhysicsConfigInput, type PlacedPaper, SAG_TOL, SceneConfigInput, SheetConfig, type SheetDims, type SheetLayoutOptions, type StateEvent, type Stock, StockName, SurfaceConfigInput, type UsePaperStatesResult, applyMemory, buildAgentPayload, buildFieldAgentPayload, buildFieldComponentSource, buildJsxSnippet, describeConfig, describeFieldConfig, diffConfig, diffFieldProps, getBehavior, getDeformer, getLayout, getPreset, getStock, idleNames, isBuiltinPreset, lightAngles, listBehaviors, listDeformers, listLayouts, listPresets, maxStripLength, mergeConfig, mergeWithDeletes, parityCases, parsePreset, quantizeSegments, recordStateOverride, registerBehavior, registerDeformer, registerLayout, registerPreset, resolveLighting, resolveStateConfig, runParityHarness, segmentsForArc, segmentsForSine, serializePreset, sheetLayoutSchema, spanAlong, stocks, supportsWebGL, uniquePresetName, unregisterPreset, usePaperStates, usePrefersReducedMotion, wrapLines };