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/README.md +21 -6
- package/dist/{chunk-HRXQTJFS.js → chunk-E22ILEDO.js} +2408 -486
- package/dist/chunk-E22ILEDO.js.map +1 -0
- package/dist/index.cjs +2365 -435
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +175 -29
- package/dist/index.d.ts +175 -29
- package/dist/index.js +20 -4
- package/dist/index.js.map +1 -1
- package/dist/slots-C5PqY0Q5.d.cts +2297 -0
- package/dist/slots-C5PqY0Q5.d.ts +2297 -0
- package/dist/stage.cjs +2358 -449
- package/dist/stage.cjs.map +1 -1
- package/dist/stage.d.cts +78 -636
- package/dist/stage.d.ts +78 -636
- package/dist/stage.js +11 -11
- package/dist/stage.js.map +1 -1
- package/package.json +3 -3
- package/dist/chunk-HRXQTJFS.js.map +0 -1
- package/dist/slots-CIo7FnlY.d.cts +0 -13357
- package/dist/slots-CIo7FnlY.d.ts +0 -13357
package/dist/stage.cjs
CHANGED
|
@@ -72,7 +72,9 @@ var stocks = {
|
|
|
72
72
|
banding: 0,
|
|
73
73
|
defaultSurface: { grain: 0.12 },
|
|
74
74
|
showThrough: 0,
|
|
75
|
-
adhesive: false
|
|
75
|
+
adhesive: false,
|
|
76
|
+
// Office bond creases cleanly and holds it — the reference paper.
|
|
77
|
+
takesSet: 0.6
|
|
76
78
|
},
|
|
77
79
|
thermal: {
|
|
78
80
|
id: "thermal",
|
|
@@ -85,7 +87,9 @@ var stocks = {
|
|
|
85
87
|
banding: 0.35,
|
|
86
88
|
defaultSurface: { aging: 0.1 },
|
|
87
89
|
showThrough: 0.06,
|
|
88
|
-
adhesive: false
|
|
90
|
+
adhesive: false,
|
|
91
|
+
// Thin and already curled off a roll; a fold in it stays folded.
|
|
92
|
+
takesSet: 0.65
|
|
89
93
|
},
|
|
90
94
|
kraft: {
|
|
91
95
|
id: "kraft",
|
|
@@ -98,7 +102,9 @@ var stocks = {
|
|
|
98
102
|
banding: 0,
|
|
99
103
|
defaultSurface: { grain: 0.5 },
|
|
100
104
|
showThrough: 0,
|
|
101
|
-
adhesive: false
|
|
105
|
+
adhesive: false,
|
|
106
|
+
// Thick and fibrous. The crease is a break, and it never comes back.
|
|
107
|
+
takesSet: 0.85
|
|
102
108
|
},
|
|
103
109
|
newsprint: {
|
|
104
110
|
id: "newsprint",
|
|
@@ -111,7 +117,9 @@ var stocks = {
|
|
|
111
117
|
banding: 0,
|
|
112
118
|
defaultSurface: { grain: 0.7, aging: 0.15 },
|
|
113
119
|
showThrough: 0.06,
|
|
114
|
-
adhesive: false
|
|
120
|
+
adhesive: false,
|
|
121
|
+
// Soft, short-fibred, and barely sprung — it crumples rather than resists.
|
|
122
|
+
takesSet: 0.8
|
|
115
123
|
},
|
|
116
124
|
vellum: {
|
|
117
125
|
id: "vellum",
|
|
@@ -124,7 +132,9 @@ var stocks = {
|
|
|
124
132
|
banding: 0,
|
|
125
133
|
defaultSurface: {},
|
|
126
134
|
showThrough: 0.55,
|
|
127
|
-
adhesive: false
|
|
135
|
+
adhesive: false,
|
|
136
|
+
// Translucent and plasticky: it fights the fold and mostly wins.
|
|
137
|
+
takesSet: 0.25
|
|
128
138
|
},
|
|
129
139
|
"photo-gloss": {
|
|
130
140
|
id: "photo-gloss",
|
|
@@ -137,7 +147,9 @@ var stocks = {
|
|
|
137
147
|
banding: 0,
|
|
138
148
|
defaultSurface: {},
|
|
139
149
|
showThrough: 0,
|
|
140
|
-
adhesive: false
|
|
150
|
+
adhesive: false,
|
|
151
|
+
// The coating resists, then cracks white — little angle kept, lots of mark.
|
|
152
|
+
takesSet: 0.3
|
|
141
153
|
},
|
|
142
154
|
// Photo-gloss-like face, glossy near-white glue underside. The default
|
|
143
155
|
// carrier for perforated stamp sheets.
|
|
@@ -152,7 +164,9 @@ var stocks = {
|
|
|
152
164
|
banding: 0,
|
|
153
165
|
defaultSurface: {},
|
|
154
166
|
showThrough: 0,
|
|
155
|
-
adhesive: true
|
|
167
|
+
adhesive: true,
|
|
168
|
+
// A face sheet on a release liner; the liner does most of the remembering.
|
|
169
|
+
takesSet: 0.5
|
|
156
170
|
}
|
|
157
171
|
};
|
|
158
172
|
function getStock(name) {
|
|
@@ -182,8 +196,8 @@ function barcodeBars(seed) {
|
|
|
182
196
|
var money = (v) => v.toFixed(2);
|
|
183
197
|
function paintReceipt(ctx, w, h, content, stock) {
|
|
184
198
|
const ink = stock.inkColor;
|
|
185
|
-
const
|
|
186
|
-
const colWidth = w -
|
|
199
|
+
const pad2 = w * 0.09;
|
|
200
|
+
const colWidth = w - pad2 * 2;
|
|
187
201
|
const base = Math.round(w / 15);
|
|
188
202
|
const mono = (size, weight = 400) => `${weight} ${size}px ui-monospace, Menlo, Consolas, monospace`;
|
|
189
203
|
let y = h * 0.045;
|
|
@@ -196,9 +210,9 @@ function paintReceipt(ctx, w, h, content, stock) {
|
|
|
196
210
|
const row = (left, right, size = base) => {
|
|
197
211
|
ctx.font = mono(size);
|
|
198
212
|
ctx.textAlign = "left";
|
|
199
|
-
ctx.fillText(left,
|
|
213
|
+
ctx.fillText(left, pad2, y);
|
|
200
214
|
ctx.textAlign = "right";
|
|
201
|
-
ctx.fillText(right, w -
|
|
215
|
+
ctx.fillText(right, w - pad2, y);
|
|
202
216
|
};
|
|
203
217
|
const divider = () => {
|
|
204
218
|
ctx.font = mono(base);
|
|
@@ -279,7 +293,7 @@ var import_fiber7 = require("@react-three/fiber");
|
|
|
279
293
|
var import_react17 = require("react");
|
|
280
294
|
|
|
281
295
|
// src/config/schema.ts
|
|
282
|
-
var
|
|
296
|
+
var import_zod15 = require("zod");
|
|
283
297
|
|
|
284
298
|
// src/config/merge.ts
|
|
285
299
|
function mergeConfig(base, override) {
|
|
@@ -499,15 +513,169 @@ var peel = {
|
|
|
499
513
|
};
|
|
500
514
|
|
|
501
515
|
// src/behaviors/unroll.ts
|
|
516
|
+
var import_zod4 = require("zod");
|
|
517
|
+
|
|
518
|
+
// src/deformers/roll.ts
|
|
502
519
|
var import_zod3 = require("zod");
|
|
503
|
-
var
|
|
520
|
+
var rollOptionsSchema = import_zod3.z.object({
|
|
521
|
+
/** Direction of rolling in the sheet plane, degrees. 0 = +x, 90 = +y. */
|
|
522
|
+
angle: import_zod3.z.number().min(-360).max(360).default(90),
|
|
523
|
+
/** Signed distance (along the roll direction, from sheet center) where the roll begins. */
|
|
524
|
+
boundary: import_zod3.z.number().min(-20).max(20).default(0),
|
|
525
|
+
/** Radius of the OUTERMOST wrap — the one the flat sheet leaves the roll on. */
|
|
526
|
+
radius: import_zod3.z.number().min(0.01).max(2).default(0.12),
|
|
527
|
+
/** Gap between consecutive wraps, in world units. 0 = a bare cylinder. */
|
|
528
|
+
thickness: import_zod3.z.number().min(0).max(0.2).default(0.015)
|
|
529
|
+
});
|
|
530
|
+
var DEG2 = Math.PI / 180;
|
|
531
|
+
var TAU = Math.PI * 2;
|
|
532
|
+
var MIN_RADIUS_FRACTION = 0.08;
|
|
533
|
+
function windAngle(s, r0, k) {
|
|
534
|
+
if (k <= 0) return s / r0;
|
|
535
|
+
const rMin = r0 * MIN_RADIUS_FRACTION;
|
|
536
|
+
const thetaFloor = (r0 - rMin) / k;
|
|
537
|
+
const sFloor = r0 * thetaFloor - k * thetaFloor * thetaFloor / 2;
|
|
538
|
+
if (s >= sFloor) return thetaFloor + (s - sFloor) / rMin;
|
|
539
|
+
return 2 * s / (r0 + Math.sqrt(r0 * r0 - 2 * k * s));
|
|
540
|
+
}
|
|
541
|
+
function windRadius(theta, r0, k) {
|
|
542
|
+
return Math.max(r0 - k * theta, r0 * MIN_RADIUS_FRACTION);
|
|
543
|
+
}
|
|
544
|
+
function rollRadius(length, core, thickness) {
|
|
545
|
+
return Math.sqrt(core * core + Math.max(0, length) * thickness / Math.PI);
|
|
546
|
+
}
|
|
547
|
+
var roll = {
|
|
548
|
+
id: "roll",
|
|
549
|
+
label: "Roll",
|
|
550
|
+
defaults: rollOptionsSchema.parse({}),
|
|
551
|
+
optionsSchema: rollOptionsSchema,
|
|
552
|
+
geometry: {
|
|
553
|
+
minSegments: 48,
|
|
554
|
+
// The INNERMOST wrap is the tightest curvature on the sheet, so it sets
|
|
555
|
+
// the density — the outer one used to, back when the spiral grew outward.
|
|
556
|
+
// Floored at a fraction of the outer radius because segment count scales
|
|
557
|
+
// as 1/√r: a roll wound to a hair's breadth would otherwise ask for a
|
|
558
|
+
// grid nobody can afford, to resolve a few square millimetres at the core.
|
|
559
|
+
autoSegments: (o, sheet2) => {
|
|
560
|
+
const span = spanAlong(sheet2, o.angle);
|
|
561
|
+
const wound = Math.max(0, span / 2 - o.boundary);
|
|
562
|
+
const k = o.thickness / TAU;
|
|
563
|
+
return segmentsForArc(span, windRadius(windAngle(wound, o.radius, k), o.radius, k));
|
|
564
|
+
},
|
|
565
|
+
axis: (o) => o.angle
|
|
566
|
+
},
|
|
567
|
+
displace(out, _uv, o) {
|
|
568
|
+
const dirX = Math.cos(o.angle * DEG2);
|
|
569
|
+
const dirY = Math.sin(o.angle * DEG2);
|
|
570
|
+
const d = out.x * dirX + out.y * dirY;
|
|
571
|
+
const s = d - o.boundary;
|
|
572
|
+
if (s <= 0) return;
|
|
573
|
+
const k = o.thickness / TAU;
|
|
574
|
+
const theta = windAngle(s, o.radius, k);
|
|
575
|
+
const r = windRadius(theta, o.radius, k) - out.z;
|
|
576
|
+
const newD = o.boundary + r * Math.sin(theta);
|
|
577
|
+
const newZ = o.radius - r * Math.cos(theta);
|
|
578
|
+
out.x += dirX * (newD - d);
|
|
579
|
+
out.y += dirY * (newD - d);
|
|
580
|
+
out.z = newZ;
|
|
581
|
+
},
|
|
582
|
+
glsl: {
|
|
583
|
+
chunk: (
|
|
584
|
+
/* glsl */
|
|
585
|
+
`
|
|
586
|
+
void FN(inout vec3 p, vec2 uv, float t) {
|
|
587
|
+
vec2 dir = vec2(cos(U_angle), sin(U_angle));
|
|
588
|
+
float d = dot(p.xy, dir);
|
|
589
|
+
float s = d - U_boundary;
|
|
590
|
+
if (s <= 0.0) return;
|
|
591
|
+
float k = U_thickness / 6.2831853071795864;
|
|
592
|
+
float rMin = U_radius * 0.08;
|
|
593
|
+
float thetaFloor = (U_radius - rMin) / max(k, 1e-9);
|
|
594
|
+
float sFloor = U_radius * thetaFloor - 0.5 * k * thetaFloor * thetaFloor;
|
|
595
|
+
float theta = k <= 0.0
|
|
596
|
+
? s / U_radius
|
|
597
|
+
: (s >= sFloor
|
|
598
|
+
? thetaFloor + (s - sFloor) / rMin
|
|
599
|
+
: (2.0 * s) / (U_radius + sqrt(U_radius * U_radius - 2.0 * k * s)));
|
|
600
|
+
float r = max(U_radius - k * theta, rMin) - p.z;
|
|
601
|
+
float newD = U_boundary + r * sin(theta);
|
|
602
|
+
float newZ = U_radius - r * cos(theta);
|
|
603
|
+
p.xy += dir * (newD - d);
|
|
604
|
+
p.z = newZ;
|
|
605
|
+
}
|
|
606
|
+
`
|
|
607
|
+
),
|
|
608
|
+
uniforms: (o) => ({
|
|
609
|
+
angle: o.angle * DEG2,
|
|
610
|
+
boundary: o.boundary,
|
|
611
|
+
radius: o.radius,
|
|
612
|
+
thickness: o.thickness
|
|
613
|
+
})
|
|
614
|
+
}
|
|
615
|
+
};
|
|
616
|
+
|
|
617
|
+
// src/behaviors/unroll.ts
|
|
618
|
+
var unrollOptionsSchema = import_zod4.z.object({
|
|
504
619
|
/** 0 = fully rolled cylinder, 1 = flat sheet. */
|
|
505
|
-
progress:
|
|
506
|
-
/** How tightly the paper is wound. */
|
|
507
|
-
tightness:
|
|
620
|
+
progress: import_zod4.z.number().min(0).max(1).default(0.5),
|
|
621
|
+
/** How tightly the paper is wound — thin layers and many turns, or few and fat. */
|
|
622
|
+
tightness: import_zod4.z.number().min(0).max(1).default(0.5),
|
|
508
623
|
/** Idle rocking of the rolled end. */
|
|
509
|
-
sway:
|
|
624
|
+
sway: import_zod4.z.number().min(0).max(1).default(0.25),
|
|
625
|
+
/** Which end of the sheet holds the roll. `top` hangs the paper below it. */
|
|
626
|
+
from: import_zod4.z.enum(["bottom", "top"]).default("bottom"),
|
|
627
|
+
/** Radius of the tube the paper is wound onto — the roll never shrinks past it. */
|
|
628
|
+
core: import_zod4.z.number().min(5e-3).max(0.5).default(0.03),
|
|
629
|
+
/**
|
|
630
|
+
* Paper already hanging at `progress` 0, in world units.
|
|
631
|
+
*
|
|
632
|
+
* A roll on a holder is never a bare cylinder: there is always a leaf out,
|
|
633
|
+
* because that is what you take hold of. Starting from nothing showing
|
|
634
|
+
* reads as a fresh roll still in its wrapper.
|
|
635
|
+
*/
|
|
636
|
+
tail: import_zod4.z.number().min(0).max(20).default(0),
|
|
637
|
+
/**
|
|
638
|
+
* How far below the roll the paper lands, in world units. Omit and it
|
|
639
|
+
* hangs forever.
|
|
640
|
+
*
|
|
641
|
+
* Paper that reaches the ground does not stop and does not carry on
|
|
642
|
+
* through it: it creases and runs out flat. Everything past this distance
|
|
643
|
+
* turns a right angle and lies down.
|
|
644
|
+
*/
|
|
645
|
+
floor: import_zod4.z.number().min(0.1).max(50).optional(),
|
|
646
|
+
/**
|
|
647
|
+
* Hold the roll still in space and let the paper hang off it, instead of
|
|
648
|
+
* letting the roll ride along with the shrinking wound region.
|
|
649
|
+
*/
|
|
650
|
+
fixed: import_zod4.z.boolean().default(false)
|
|
510
651
|
});
|
|
652
|
+
var LOOSE = 0.1;
|
|
653
|
+
var TIGHT = 0.02;
|
|
654
|
+
function layerThickness(tightness) {
|
|
655
|
+
return LOOSE - tightness * (LOOSE - TIGHT);
|
|
656
|
+
}
|
|
657
|
+
function sweep(o, sheet2) {
|
|
658
|
+
const maxRadius = rollRadius(sheet2.height, o.core, layerThickness(o.tightness));
|
|
659
|
+
const tail = Math.min(o.tail, sheet2.height);
|
|
660
|
+
return { start: -sheet2.height / 2 + tail, end: sheet2.height / 2 + maxRadius * 2 };
|
|
661
|
+
}
|
|
662
|
+
function rollBoundary(o, sheet2) {
|
|
663
|
+
const { start, end } = sweep(o, sheet2);
|
|
664
|
+
return start + o.progress * (end - start);
|
|
665
|
+
}
|
|
666
|
+
var LANDING_RADIUS = 0.035;
|
|
667
|
+
function landing(o, sheet2, boundary, rollRadiusNow) {
|
|
668
|
+
const radius = Math.min(0.5, Math.max(0.02, sheet2.height * LANDING_RADIUS));
|
|
669
|
+
const hingeDrop = radius / (Math.PI / 2);
|
|
670
|
+
const below = Math.max(o.floor, rollRadiusNow + radius);
|
|
671
|
+
const floorLine = o.from === "top" ? boundary - below : -boundary + below;
|
|
672
|
+
return {
|
|
673
|
+
type: "fold",
|
|
674
|
+
// Travel points from the roll toward the floor, so "past the crease"
|
|
675
|
+
// means "the length that has arrived", not the drop above it.
|
|
676
|
+
options: o.from === "top" ? { angle: -90, offset: -floorLine - hingeDrop, foldAngle: 90, radius } : { angle: 90, offset: floorLine - hingeDrop, foldAngle: 90, radius }
|
|
677
|
+
};
|
|
678
|
+
}
|
|
511
679
|
var unroll = {
|
|
512
680
|
id: "unroll",
|
|
513
681
|
label: "Unroll",
|
|
@@ -518,46 +686,65 @@ var unroll = {
|
|
|
518
686
|
duration: 3,
|
|
519
687
|
loopMode: "yoyo",
|
|
520
688
|
stack(o, sheet2) {
|
|
521
|
-
const
|
|
522
|
-
const
|
|
523
|
-
const
|
|
524
|
-
|
|
689
|
+
const thickness = layerThickness(o.tightness);
|
|
690
|
+
const boundary = rollBoundary(o, sheet2);
|
|
691
|
+
const wound = Math.max(0, sheet2.height / 2 - boundary);
|
|
692
|
+
const radius = rollRadius(wound, o.core, thickness);
|
|
693
|
+
const stack = [
|
|
525
694
|
{
|
|
526
695
|
type: "roll",
|
|
527
696
|
options: {
|
|
528
|
-
|
|
529
|
-
|
|
697
|
+
// Both directions sweep the same boundary; only which half of the
|
|
698
|
+
// sheet counts as "past" it changes. 270 winds the bottom (a
|
|
699
|
+
// receipt feeding downward), 90 winds the top (paper hanging below).
|
|
700
|
+
angle: o.from === "top" ? 90 : 270,
|
|
701
|
+
boundary,
|
|
530
702
|
radius,
|
|
531
|
-
|
|
703
|
+
thickness
|
|
532
704
|
}
|
|
533
705
|
}
|
|
534
706
|
];
|
|
707
|
+
if (o.floor !== void 0) stack.push(landing(o, sheet2, boundary, radius));
|
|
708
|
+
return stack;
|
|
535
709
|
},
|
|
536
710
|
loop(o, t) {
|
|
537
711
|
if (o.sway === 0) return {};
|
|
538
712
|
const wobble = Math.sin(t * 1.5) * 0.01 * o.sway;
|
|
539
713
|
return { progress: Math.min(1, Math.max(0, o.progress + wobble)) };
|
|
540
714
|
},
|
|
715
|
+
transform(o, _t, pose, sheet2) {
|
|
716
|
+
if (!o.fixed) return;
|
|
717
|
+
const travel = sweep(o, sheet2).end - rollBoundary(o, sheet2);
|
|
718
|
+
pose.position[1] += o.from === "top" ? travel : -travel;
|
|
719
|
+
},
|
|
541
720
|
handles: [
|
|
542
721
|
{
|
|
543
722
|
id: "roll-edge",
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
723
|
+
// The grab point is the edge the paper leaves the roll on, so it tracks
|
|
724
|
+
// the boundary — which runs top-to-bottom for a roll at the bottom and
|
|
725
|
+
// bottom-to-top for one at the top.
|
|
726
|
+
anchor: (o) => {
|
|
727
|
+
const v = o.from === "top" ? o.progress : 1 - o.progress;
|
|
728
|
+
return [0.5, Math.max(0.02, Math.min(0.98, v))];
|
|
729
|
+
},
|
|
730
|
+
drag(local, o, sheet2) {
|
|
731
|
+
const along = local.y / sheet2.height;
|
|
732
|
+
const p = o.from === "top" ? 0.5 + along : 0.5 - along;
|
|
733
|
+
return { progress: Math.min(1, Math.max(0, p)) };
|
|
547
734
|
}
|
|
548
735
|
}
|
|
549
736
|
]
|
|
550
737
|
};
|
|
551
738
|
|
|
552
739
|
// src/behaviors/flip.ts
|
|
553
|
-
var
|
|
554
|
-
var flipOptionsSchema =
|
|
740
|
+
var import_zod5 = require("zod");
|
|
741
|
+
var flipOptionsSchema = import_zod5.z.object({
|
|
555
742
|
/** 0 = flat, 1 = page fully turned over the spine. */
|
|
556
|
-
progress:
|
|
743
|
+
progress: import_zod5.z.number().min(0).max(1).default(0.3),
|
|
557
744
|
/** Which edge is the spine. */
|
|
558
|
-
spine:
|
|
745
|
+
spine: import_zod5.z.enum(["left", "right"]).default("left"),
|
|
559
746
|
/** Softness of the turning curl. */
|
|
560
|
-
radius:
|
|
747
|
+
radius: import_zod5.z.number().min(0.1).max(0.8).default(0.3)
|
|
561
748
|
});
|
|
562
749
|
var flip = {
|
|
563
750
|
id: "flip",
|
|
@@ -579,7 +766,7 @@ var flip = {
|
|
|
579
766
|
angle,
|
|
580
767
|
boundary: start + o.progress * (end - start),
|
|
581
768
|
radius: o.radius,
|
|
582
|
-
|
|
769
|
+
thickness: 0
|
|
583
770
|
}
|
|
584
771
|
}
|
|
585
772
|
];
|
|
@@ -597,12 +784,12 @@ var flip = {
|
|
|
597
784
|
};
|
|
598
785
|
|
|
599
786
|
// src/behaviors/letter-fold.ts
|
|
600
|
-
var
|
|
601
|
-
var letterFoldOptionsSchema =
|
|
787
|
+
var import_zod6 = require("zod");
|
|
788
|
+
var letterFoldOptionsSchema = import_zod6.z.object({
|
|
602
789
|
/** 0 = flat letter, 1 = fully tri-folded. */
|
|
603
|
-
progress:
|
|
790
|
+
progress: import_zod6.z.number().min(0).max(1).default(0.4),
|
|
604
791
|
/** Softness of the two creases. */
|
|
605
|
-
crease:
|
|
792
|
+
crease: import_zod6.z.number().min(0).max(1).default(0.3)
|
|
606
793
|
});
|
|
607
794
|
var letterFold = {
|
|
608
795
|
id: "letter-fold",
|
|
@@ -652,12 +839,12 @@ var letterFold = {
|
|
|
652
839
|
};
|
|
653
840
|
|
|
654
841
|
// src/behaviors/hang.ts
|
|
655
|
-
var
|
|
656
|
-
var hangOptionsSchema =
|
|
842
|
+
var import_zod7 = require("zod");
|
|
843
|
+
var hangOptionsSchema = import_zod7.z.object({
|
|
657
844
|
/** Wind strength driving the ripple. */
|
|
658
|
-
wind:
|
|
845
|
+
wind: import_zod7.z.number().min(0).max(1).default(0.4),
|
|
659
846
|
/** Gravity bulge of the hanging sheet. */
|
|
660
|
-
sag:
|
|
847
|
+
sag: import_zod7.z.number().min(0).max(1).default(0.3)
|
|
661
848
|
});
|
|
662
849
|
var hang = {
|
|
663
850
|
id: "hang",
|
|
@@ -686,12 +873,12 @@ var hang = {
|
|
|
686
873
|
};
|
|
687
874
|
|
|
688
875
|
// src/behaviors/fly.ts
|
|
689
|
-
var
|
|
690
|
-
var flyOptionsSchema =
|
|
876
|
+
var import_zod8 = require("zod");
|
|
877
|
+
var flyOptionsSchema = import_zod8.z.object({
|
|
691
878
|
/** Ripple energy. */
|
|
692
|
-
flutter:
|
|
879
|
+
flutter: import_zod8.z.number().min(0).max(1).default(0.5),
|
|
693
880
|
/** Aerodynamic arc of the sheet. */
|
|
694
|
-
curve:
|
|
881
|
+
curve: import_zod8.z.number().min(0).max(1).default(0.4)
|
|
695
882
|
});
|
|
696
883
|
var fly = {
|
|
697
884
|
id: "fly",
|
|
@@ -720,12 +907,12 @@ var fly = {
|
|
|
720
907
|
};
|
|
721
908
|
|
|
722
909
|
// src/behaviors/fall.ts
|
|
723
|
-
var
|
|
724
|
-
var fallOptionsSchema =
|
|
910
|
+
var import_zod9 = require("zod");
|
|
911
|
+
var fallOptionsSchema = import_zod9.z.object({
|
|
725
912
|
/** Air resistance ripple while falling. */
|
|
726
|
-
flutter:
|
|
913
|
+
flutter: import_zod9.z.number().min(0).max(1).default(0.6),
|
|
727
914
|
/** A falling sheet always lifts a corner. */
|
|
728
|
-
curl:
|
|
915
|
+
curl: import_zod9.z.number().min(0).max(1).default(0.3)
|
|
729
916
|
});
|
|
730
917
|
var fall = {
|
|
731
918
|
id: "fall",
|
|
@@ -757,21 +944,21 @@ var fall = {
|
|
|
757
944
|
};
|
|
758
945
|
|
|
759
946
|
// src/behaviors/carry.ts
|
|
760
|
-
var
|
|
761
|
-
var carryOptionsSchema =
|
|
947
|
+
var import_zod10 = require("zod");
|
|
948
|
+
var carryOptionsSchema = import_zod10.z.object({
|
|
762
949
|
/**
|
|
763
950
|
* The grab point — where the pointer was on the paper at pick time.
|
|
764
951
|
* 'auto' is resolved by the carry controller (usually the peeled corner:
|
|
765
952
|
* continuity from peel → carry is the immersion moment).
|
|
766
953
|
*/
|
|
767
|
-
grab:
|
|
954
|
+
grab: import_zod10.z.enum([...cornerNames, "auto"]).default("auto"),
|
|
768
955
|
/** From stock feel: a stamp is stiff — it flutters, it doesn't flow. */
|
|
769
|
-
stiffness:
|
|
770
|
-
flutter:
|
|
956
|
+
stiffness: import_zod10.z.number().min(0).max(1).default(0.7),
|
|
957
|
+
flutter: import_zod10.z.number().min(0).max(1).default(0.5),
|
|
771
958
|
/** How far the paper's yaw trails the drag direction (runtime transform). */
|
|
772
|
-
lag:
|
|
959
|
+
lag: import_zod10.z.number().min(0).max(1).default(0.35),
|
|
773
960
|
/** Drag-speed drive (0..1). Written live by the carry controller. */
|
|
774
|
-
drive:
|
|
961
|
+
drive: import_zod10.z.number().min(0).max(1).default(0.25)
|
|
775
962
|
});
|
|
776
963
|
var concreteGrab = (g) => g === "auto" ? "top-left" : g;
|
|
777
964
|
var DROOP_ANGLE = {
|
|
@@ -818,7 +1005,7 @@ var carry = {
|
|
|
818
1005
|
};
|
|
819
1006
|
|
|
820
1007
|
// src/behaviors/flight.ts
|
|
821
|
-
var
|
|
1008
|
+
var import_zod11 = require("zod");
|
|
822
1009
|
|
|
823
1010
|
// src/physics/aero.ts
|
|
824
1011
|
function dampTo(state, target, smoothing, dt) {
|
|
@@ -860,17 +1047,17 @@ function carryDrive(speed) {
|
|
|
860
1047
|
}
|
|
861
1048
|
|
|
862
1049
|
// src/behaviors/flight.ts
|
|
863
|
-
var flightOptionsSchema =
|
|
1050
|
+
var flightOptionsSchema = import_zod11.z.object({
|
|
864
1051
|
/** Directional wind vector — paper travels ACROSS the scene, not just down. */
|
|
865
|
-
wind:
|
|
866
|
-
gustiness:
|
|
867
|
-
tumble:
|
|
1052
|
+
wind: import_zod11.z.tuple([import_zod11.z.number().min(-2).max(2), import_zod11.z.number().min(-2).max(2), import_zod11.z.number().min(-2).max(2)]).default([0.6, 0.08, 0]),
|
|
1053
|
+
gustiness: import_zod11.z.number().min(0).max(1).default(0.4),
|
|
1054
|
+
tumble: import_zod11.z.number().min(0).max(1).default(0.6),
|
|
868
1055
|
/** 'loop' is a seamless idle cycle; 'drift' travels along the wind. */
|
|
869
|
-
path:
|
|
1056
|
+
path: import_zod11.z.enum(["drift", "loop"]).default("drift"),
|
|
870
1057
|
/** Drift only: exit the scene → re-enter the opposite side. */
|
|
871
|
-
respawn:
|
|
1058
|
+
respawn: import_zod11.z.boolean().default(true),
|
|
872
1059
|
/** Half-extent of the travel before respawn wraps it. */
|
|
873
|
-
range:
|
|
1060
|
+
range: import_zod11.z.number().min(0.5).max(12).default(3.5)
|
|
874
1061
|
});
|
|
875
1062
|
var flight = {
|
|
876
1063
|
id: "flight",
|
|
@@ -902,16 +1089,16 @@ var flight = {
|
|
|
902
1089
|
};
|
|
903
1090
|
|
|
904
1091
|
// src/behaviors/crumple.ts
|
|
905
|
-
var
|
|
906
|
-
var crumpleBehaviorOptionsSchema =
|
|
1092
|
+
var import_zod12 = require("zod");
|
|
1093
|
+
var crumpleBehaviorOptionsSchema = import_zod12.z.object({
|
|
907
1094
|
/** 0 = flat sheet, 1 = crushed. */
|
|
908
|
-
progress:
|
|
1095
|
+
progress: import_zod12.z.number().min(0).max(1).default(0.55),
|
|
909
1096
|
/** Few big facets at 0, many small ones at 1. */
|
|
910
|
-
coarseness:
|
|
1097
|
+
coarseness: import_zod12.z.number().min(0).max(1).default(0.35),
|
|
911
1098
|
/** How far the sheet curls in on itself as it crushes. */
|
|
912
|
-
ball:
|
|
1099
|
+
ball: import_zod12.z.number().min(0).max(1).default(0.5),
|
|
913
1100
|
/** A different crush of the same paper. */
|
|
914
|
-
seed:
|
|
1101
|
+
seed: import_zod12.z.number().int().min(0).max(7).default(0)
|
|
915
1102
|
});
|
|
916
1103
|
var crumpleBehavior = {
|
|
917
1104
|
id: "crumple",
|
|
@@ -944,8 +1131,8 @@ var crumpleBehavior = {
|
|
|
944
1131
|
};
|
|
945
1132
|
|
|
946
1133
|
// src/behaviors/settle.ts
|
|
947
|
-
var
|
|
948
|
-
var settleOptionsSchema =
|
|
1134
|
+
var import_zod13 = require("zod");
|
|
1135
|
+
var settleOptionsSchema = import_zod13.z.object({
|
|
949
1136
|
/**
|
|
950
1137
|
* How long ago it landed, 0..1.
|
|
951
1138
|
*
|
|
@@ -953,7 +1140,7 @@ var settleOptionsSchema = import_zod12.z.object({
|
|
|
953
1140
|
* a sheet that has been lying there, where its own weight has flattened
|
|
954
1141
|
* out everything except what its stiffness refuses to give up.
|
|
955
1142
|
*/
|
|
956
|
-
relax:
|
|
1143
|
+
relax: import_zod13.z.number().min(0).max(1).default(0.45),
|
|
957
1144
|
/**
|
|
958
1145
|
* How hard the paper resists lying flat, 0..1.
|
|
959
1146
|
*
|
|
@@ -961,14 +1148,14 @@ var settleOptionsSchema = import_zod12.z.object({
|
|
|
961
1148
|
* never does. It is the whole reason a settled sheet reads as PAPER and
|
|
962
1149
|
* not as a decal — at 0 the mesh is a rectangle painted on the floor.
|
|
963
1150
|
*/
|
|
964
|
-
lift:
|
|
1151
|
+
lift: import_zod13.z.number().min(0).max(1).default(0.45),
|
|
965
1152
|
/** Which corner stayed up. */
|
|
966
|
-
corner:
|
|
1153
|
+
corner: import_zod13.z.enum(["top-left", "top-right", "bottom-left", "bottom-right"]).default("top-right"),
|
|
967
1154
|
/**
|
|
968
1155
|
* Slack across the middle — the low, long undulation of a sheet that is
|
|
969
1156
|
* touching a floor in two places and bridging between them.
|
|
970
1157
|
*/
|
|
971
|
-
slack:
|
|
1158
|
+
slack: import_zod13.z.number().min(0).max(1).default(0.4)
|
|
972
1159
|
});
|
|
973
1160
|
var settle = {
|
|
974
1161
|
id: "settle",
|
|
@@ -1021,8 +1208,8 @@ var settle = {
|
|
|
1021
1208
|
};
|
|
1022
1209
|
|
|
1023
1210
|
// src/behaviors/ribbon.ts
|
|
1024
|
-
var
|
|
1025
|
-
var ribbonOptionsSchema =
|
|
1211
|
+
var import_zod14 = require("zod");
|
|
1212
|
+
var ribbonOptionsSchema = import_zod14.z.object({
|
|
1026
1213
|
/**
|
|
1027
1214
|
* How much of the drop is lying on the floor, as a fraction of the height.
|
|
1028
1215
|
*
|
|
@@ -1031,11 +1218,11 @@ var ribbonOptionsSchema = import_zod13.z.object({
|
|
|
1031
1218
|
* turns over reads as paper meeting a floor, which is the thing the
|
|
1032
1219
|
* reference installations are actually about.
|
|
1033
1220
|
*/
|
|
1034
|
-
pool:
|
|
1221
|
+
pool: import_zod14.z.number().min(0).max(0.5).default(0.16),
|
|
1035
1222
|
/** How tightly it turns where it lands. Low is a soft slump, high is a curl. */
|
|
1036
|
-
curl:
|
|
1223
|
+
curl: import_zod14.z.number().min(0).max(1).default(0.45),
|
|
1037
1224
|
/** Folds running down the length. A printed strip is never a flat plane. */
|
|
1038
|
-
drape:
|
|
1225
|
+
drape: import_zod14.z.number().min(0).max(1).default(0.5)
|
|
1039
1226
|
});
|
|
1040
1227
|
var ribbon = {
|
|
1041
1228
|
id: "ribbon",
|
|
@@ -1143,12 +1330,12 @@ var ribbon = {
|
|
|
1143
1330
|
};
|
|
1144
1331
|
|
|
1145
1332
|
// src/config/schema.ts
|
|
1146
|
-
var sheetSchema =
|
|
1333
|
+
var sheetSchema = import_zod15.z.object({
|
|
1147
1334
|
/** World units. A letter sheet is ~1 × 1.4, a receipt ~1 × 2.6. */
|
|
1148
|
-
width:
|
|
1149
|
-
height:
|
|
1335
|
+
width: import_zod15.z.number().positive().max(20).default(1),
|
|
1336
|
+
height: import_zod15.z.number().positive().max(20).default(1.4),
|
|
1150
1337
|
/** Visual thickness in mm-ish units; drives edge/shadow treatment, not geometry (yet). */
|
|
1151
|
-
thickness:
|
|
1338
|
+
thickness: import_zod15.z.number().min(0).max(2).default(0.2),
|
|
1152
1339
|
/**
|
|
1153
1340
|
* `'auto'` sizes the grid from the active deformers' needs — genuinely, as
|
|
1154
1341
|
* of 0.3.0. It asks each one what these options require (a gentle bend and
|
|
@@ -1164,8 +1351,8 @@ var sheetSchema = import_zod14.z.object({
|
|
|
1164
1351
|
* still raises it, because that is a correctness floor rather than a
|
|
1165
1352
|
* preference.
|
|
1166
1353
|
*/
|
|
1167
|
-
segments:
|
|
1168
|
-
cornerRadius:
|
|
1354
|
+
segments: import_zod15.z.union([import_zod15.z.literal("auto"), import_zod15.z.number().int().min(2).max(256)]).default("auto"),
|
|
1355
|
+
cornerRadius: import_zod15.z.number().min(0).max(0.5).default(0)
|
|
1169
1356
|
});
|
|
1170
1357
|
var stockNames = [
|
|
1171
1358
|
"printer",
|
|
@@ -1176,37 +1363,37 @@ var stockNames = [
|
|
|
1176
1363
|
"photo-gloss",
|
|
1177
1364
|
"sticker"
|
|
1178
1365
|
];
|
|
1179
|
-
var stockSchema =
|
|
1180
|
-
var washSchema =
|
|
1366
|
+
var stockSchema = import_zod15.z.enum(stockNames);
|
|
1367
|
+
var washSchema = import_zod15.z.object({
|
|
1181
1368
|
/** The first pigment. */
|
|
1182
|
-
color:
|
|
1369
|
+
color: import_zod15.z.string().default("#4a5b8c").describe("color"),
|
|
1183
1370
|
/** The second. Blooms alternate, and overlaps multiply into a third. */
|
|
1184
|
-
secondary:
|
|
1371
|
+
secondary: import_zod15.z.string().default("#b06a6a").describe("color"),
|
|
1185
1372
|
/** How many pools of colour. */
|
|
1186
|
-
blooms:
|
|
1373
|
+
blooms: import_zod15.z.number().int().min(1).max(24).default(7),
|
|
1187
1374
|
/** How far a pool runs before it dries — its size against the sheet. */
|
|
1188
|
-
spread:
|
|
1375
|
+
spread: import_zod15.z.number().min(0.1).max(1).default(0.7),
|
|
1189
1376
|
/** Softness of the wet edge. 0 is a hard cut, 1 is a pool still moving. */
|
|
1190
|
-
bleed:
|
|
1377
|
+
bleed: import_zod15.z.number().min(0).max(1).default(0.5),
|
|
1191
1378
|
/** How much pigment is in the water. */
|
|
1192
|
-
intensity:
|
|
1379
|
+
intensity: import_zod15.z.number().min(0).max(1).default(0.55),
|
|
1193
1380
|
/**
|
|
1194
1381
|
* Edge darkening — the ring of pigment left where a pool dried.
|
|
1195
1382
|
*
|
|
1196
1383
|
* The signature of the medium, and the one thing a plain gradient cannot
|
|
1197
1384
|
* fake. Without it a wash reads as an airbrush.
|
|
1198
1385
|
*/
|
|
1199
|
-
edge:
|
|
1386
|
+
edge: import_zod15.z.number().min(0).max(1).default(0.6),
|
|
1200
1387
|
/** Pigment settling into the tooth of the paper. */
|
|
1201
|
-
granulation:
|
|
1388
|
+
granulation: import_zod15.z.number().min(0).max(1).default(0.35),
|
|
1202
1389
|
/** Fixed so a preset paints the same wash every time. */
|
|
1203
|
-
seed:
|
|
1390
|
+
seed: import_zod15.z.number().int().min(0).max(99).default(0)
|
|
1204
1391
|
});
|
|
1205
|
-
var blankContentBase =
|
|
1206
|
-
type:
|
|
1392
|
+
var blankContentBase = import_zod15.z.object({
|
|
1393
|
+
type: import_zod15.z.literal("blank")
|
|
1207
1394
|
});
|
|
1208
|
-
var imageContentBase =
|
|
1209
|
-
type:
|
|
1395
|
+
var imageContentBase = import_zod15.z.object({
|
|
1396
|
+
type: import_zod15.z.literal("image"),
|
|
1210
1397
|
/**
|
|
1211
1398
|
* Empty means "no picture yet", and renders as bare stock rather than as
|
|
1212
1399
|
* a failure. That is what lets a built-in preset be an image preset
|
|
@@ -1214,30 +1401,30 @@ var imageContentBase = import_zod14.z.object({
|
|
|
1214
1401
|
* `postage-stamp` are containers for the caller's own art, handed over via
|
|
1215
1402
|
* `<PaperField images={...} />` or `content.src`.
|
|
1216
1403
|
*/
|
|
1217
|
-
src:
|
|
1218
|
-
fit:
|
|
1404
|
+
src: import_zod15.z.string().default(""),
|
|
1405
|
+
fit: import_zod15.z.enum(["cover", "contain"]).default("cover"),
|
|
1219
1406
|
/** Read by the hidden DOM mirror and the no-WebGL fallback. */
|
|
1220
|
-
alt:
|
|
1407
|
+
alt: import_zod15.z.string().optional()
|
|
1221
1408
|
});
|
|
1222
|
-
var textContentBase =
|
|
1223
|
-
type:
|
|
1224
|
-
text:
|
|
1225
|
-
font:
|
|
1409
|
+
var textContentBase = import_zod15.z.object({
|
|
1410
|
+
type: import_zod15.z.literal("text"),
|
|
1411
|
+
text: import_zod15.z.string().default("Dear reader,"),
|
|
1412
|
+
font: import_zod15.z.string().default('Georgia, "Times New Roman", serif'),
|
|
1226
1413
|
/** px at texture resolution (long edge = 1024 logical px before DPR). */
|
|
1227
|
-
size:
|
|
1228
|
-
weight:
|
|
1229
|
-
color:
|
|
1230
|
-
align:
|
|
1414
|
+
size: import_zod15.z.number().min(8).max(256).default(44),
|
|
1415
|
+
weight: import_zod15.z.number().min(100).max(900).default(400),
|
|
1416
|
+
color: import_zod15.z.string().default("#2b2620").describe("color"),
|
|
1417
|
+
align: import_zod15.z.enum(["left", "center", "right"]).default("left"),
|
|
1231
1418
|
/** Fraction of the short edge. */
|
|
1232
|
-
padding:
|
|
1233
|
-
lineHeight:
|
|
1419
|
+
padding: import_zod15.z.number().min(0).max(0.4).default(0.09),
|
|
1420
|
+
lineHeight: import_zod15.z.number().min(0.8).max(3).default(1.45),
|
|
1234
1421
|
/**
|
|
1235
1422
|
* Letter-spacing, in em. The one control display type cannot do without:
|
|
1236
1423
|
* a line set large enough to be read across a room needs its tracking
|
|
1237
1424
|
* pulled IN, and a small line of uppercase small-print needs it pushed
|
|
1238
1425
|
* out, and neither is achievable by changing the size.
|
|
1239
1426
|
*/
|
|
1240
|
-
tracking:
|
|
1427
|
+
tracking: import_zod15.z.number().min(-0.1).max(0.6).default(0),
|
|
1241
1428
|
/**
|
|
1242
1429
|
* Where the block sits down the sheet.
|
|
1243
1430
|
*
|
|
@@ -1246,26 +1433,26 @@ var textContentBase = import_zod14.z.object({
|
|
|
1246
1433
|
* poster wants — a block of type optically centred in the sheet rather
|
|
1247
1434
|
* than hung from its top edge.
|
|
1248
1435
|
*/
|
|
1249
|
-
valign:
|
|
1436
|
+
valign: import_zod15.z.enum(["top", "center"]).default("top")
|
|
1250
1437
|
});
|
|
1251
|
-
var cardContentBase =
|
|
1252
|
-
type:
|
|
1438
|
+
var cardContentBase = import_zod15.z.object({
|
|
1439
|
+
type: import_zod15.z.literal("card"),
|
|
1253
1440
|
/** Small, tracked, uppercase by convention — the label at the top. */
|
|
1254
|
-
title:
|
|
1441
|
+
title: import_zod15.z.string().default(""),
|
|
1255
1442
|
/** The card's reason for existing. */
|
|
1256
|
-
body:
|
|
1443
|
+
body: import_zod15.z.string().default(""),
|
|
1257
1444
|
/** Attribution, catalogue number, date — the line in small print at the foot. */
|
|
1258
|
-
note:
|
|
1445
|
+
note: import_zod15.z.string().default(""),
|
|
1259
1446
|
/** A hairline under the title. What separates a label from a paragraph. */
|
|
1260
|
-
rule:
|
|
1447
|
+
rule: import_zod15.z.boolean().default(true),
|
|
1261
1448
|
/**
|
|
1262
1449
|
* Ruled writing lines behind the body, as on an index card.
|
|
1263
1450
|
*
|
|
1264
1451
|
* Drawn UNDER the type and in the stock's own ink at low alpha, so they
|
|
1265
1452
|
* read as printed on the card rather than as underlines on the words.
|
|
1266
1453
|
*/
|
|
1267
|
-
ruled:
|
|
1268
|
-
font:
|
|
1454
|
+
ruled: import_zod15.z.boolean().default(false),
|
|
1455
|
+
font: import_zod15.z.string().default('Georgia, "Times New Roman", serif'),
|
|
1269
1456
|
/**
|
|
1270
1457
|
* Body size, px at texture resolution. Title and note derive from it.
|
|
1271
1458
|
*
|
|
@@ -1275,28 +1462,28 @@ var cardContentBase = import_zod14.z.object({
|
|
|
1275
1462
|
* third of the stock empty above and below it, which reads as a page that
|
|
1276
1463
|
* was cropped rather than as a card that was set.
|
|
1277
1464
|
*/
|
|
1278
|
-
size:
|
|
1279
|
-
color:
|
|
1280
|
-
align:
|
|
1281
|
-
padding:
|
|
1465
|
+
size: import_zod15.z.number().min(8).max(256).default(58),
|
|
1466
|
+
color: import_zod15.z.string().default("#2b2620").describe("color"),
|
|
1467
|
+
align: import_zod15.z.enum(["left", "center"]).default("left"),
|
|
1468
|
+
padding: import_zod15.z.number().min(0).max(0.4).default(0.1)
|
|
1282
1469
|
});
|
|
1283
|
-
var receiptContentBase =
|
|
1284
|
-
type:
|
|
1285
|
-
store:
|
|
1286
|
-
address:
|
|
1287
|
-
items:
|
|
1470
|
+
var receiptContentBase = import_zod15.z.object({
|
|
1471
|
+
type: import_zod15.z.literal("receipt"),
|
|
1472
|
+
store: import_zod15.z.string().default("PAPERLAB"),
|
|
1473
|
+
address: import_zod15.z.string().default("124 PAPER ST"),
|
|
1474
|
+
items: import_zod15.z.array(import_zod15.z.object({ name: import_zod15.z.string(), price: import_zod15.z.number() })).default([
|
|
1288
1475
|
{ name: "CURL, TRUE", price: 12 },
|
|
1289
1476
|
{ name: "ROLL, TIGHT", price: 8.5 },
|
|
1290
1477
|
{ name: "SHEET, ONE", price: 0.99 }
|
|
1291
1478
|
]),
|
|
1292
|
-
taxRate:
|
|
1293
|
-
barcode:
|
|
1479
|
+
taxRate: import_zod15.z.number().min(0).max(1).default(0.08),
|
|
1480
|
+
barcode: import_zod15.z.boolean().default(true),
|
|
1294
1481
|
/** Fixed so presets render deterministically; omit for "now". */
|
|
1295
|
-
timestamp:
|
|
1296
|
-
footer:
|
|
1482
|
+
timestamp: import_zod15.z.string().optional(),
|
|
1483
|
+
footer: import_zod15.z.string().default("KEEP FOR YOUR RECORDS")
|
|
1297
1484
|
});
|
|
1298
1485
|
var withWash = { wash: washSchema.optional() };
|
|
1299
|
-
var backContentSchema =
|
|
1486
|
+
var backContentSchema = import_zod15.z.discriminatedUnion("type", [
|
|
1300
1487
|
blankContentBase.extend(withWash),
|
|
1301
1488
|
imageContentBase.extend(withWash),
|
|
1302
1489
|
textContentBase.extend(withWash),
|
|
@@ -1309,7 +1496,7 @@ var imageContentSchema = imageContentBase.extend(withBack);
|
|
|
1309
1496
|
var textContentSchema = textContentBase.extend(withBack);
|
|
1310
1497
|
var cardContentSchema = cardContentBase.extend(withBack);
|
|
1311
1498
|
var receiptContentSchema = receiptContentBase.extend(withBack);
|
|
1312
|
-
var contentSchema =
|
|
1499
|
+
var contentSchema = import_zod15.z.discriminatedUnion("type", [
|
|
1313
1500
|
blankContentSchema,
|
|
1314
1501
|
imageContentSchema,
|
|
1315
1502
|
textContentSchema,
|
|
@@ -1320,80 +1507,146 @@ var contentNames = contentSchema.options.map(
|
|
|
1320
1507
|
(option) => option.shape.type.value
|
|
1321
1508
|
);
|
|
1322
1509
|
var paperEdges = ["top", "right", "bottom", "left"];
|
|
1323
|
-
var surfaceSchema =
|
|
1510
|
+
var surfaceSchema = import_zod15.z.object({
|
|
1324
1511
|
/** Paper fiber noise, 0..1. */
|
|
1325
|
-
grain:
|
|
1512
|
+
grain: import_zod15.z.number().min(0).max(1).optional(),
|
|
1326
1513
|
/** Light passing through the sheet from behind, 0..1. Stock defaults apply. */
|
|
1327
|
-
translucency:
|
|
1514
|
+
translucency: import_zod15.z.number().min(0).max(1).optional(),
|
|
1328
1515
|
/** Torn-edge alpha with a lightened fiber band. */
|
|
1329
|
-
deckle:
|
|
1330
|
-
edges:
|
|
1331
|
-
roughness:
|
|
1516
|
+
deckle: import_zod15.z.object({
|
|
1517
|
+
edges: import_zod15.z.array(import_zod15.z.enum(paperEdges)).default(["bottom"]),
|
|
1518
|
+
roughness: import_zod15.z.number().min(0).max(1).default(0.5)
|
|
1332
1519
|
}).optional(),
|
|
1333
1520
|
/** Visual AO/highlight companion to the fold deformer. */
|
|
1334
|
-
creaseLines:
|
|
1521
|
+
creaseLines: import_zod15.z.object({
|
|
1335
1522
|
/** Crease line direction, degrees (0 = horizontal lines). */
|
|
1336
|
-
angle:
|
|
1523
|
+
angle: import_zod15.z.number().min(-360).max(360).default(0),
|
|
1337
1524
|
/** Positions across the sheet, 0..1 fractions. */
|
|
1338
|
-
positions:
|
|
1339
|
-
strength:
|
|
1525
|
+
positions: import_zod15.z.array(import_zod15.z.number().min(0).max(1)).default([1 / 3, 2 / 3]),
|
|
1526
|
+
strength: import_zod15.z.number().min(0).max(1).default(0.5)
|
|
1340
1527
|
}).optional(),
|
|
1341
1528
|
/** Yellowing + foxing spots, 0..1. */
|
|
1342
|
-
aging:
|
|
1529
|
+
aging: import_zod15.z.number().min(0).max(1).optional(),
|
|
1343
1530
|
/** Reversed front-content ghost on the backside, 0..1. Stock defaults apply. */
|
|
1344
|
-
showThrough:
|
|
1531
|
+
showThrough: import_zod15.z.number().min(0).max(1).optional(),
|
|
1345
1532
|
/**
|
|
1346
1533
|
* Postage-stamp perforation: alpha-punched semicircular holes along chosen
|
|
1347
1534
|
* edges. `state` flips an edge to a ripped-through profile (torn) — set
|
|
1348
1535
|
* automatically when a paper detaches from a `sheet` field, manual wins.
|
|
1349
1536
|
*/
|
|
1350
|
-
perforation:
|
|
1351
|
-
edges:
|
|
1537
|
+
perforation: import_zod15.z.object({
|
|
1538
|
+
edges: import_zod15.z.union([import_zod15.z.array(import_zod15.z.enum(paperEdges)), import_zod15.z.literal("all")]).default("all"),
|
|
1352
1539
|
/** World units — default tuned to stamp scale. */
|
|
1353
|
-
holeRadius:
|
|
1354
|
-
spacing:
|
|
1355
|
-
state:
|
|
1356
|
-
top:
|
|
1357
|
-
right:
|
|
1358
|
-
bottom:
|
|
1359
|
-
left:
|
|
1540
|
+
holeRadius: import_zod15.z.number().min(2e-3).max(0.1).default(0.016),
|
|
1541
|
+
spacing: import_zod15.z.number().min(0.01).max(0.5).default(0.055),
|
|
1542
|
+
state: import_zod15.z.object({
|
|
1543
|
+
top: import_zod15.z.enum(["intact", "torn"]).optional(),
|
|
1544
|
+
right: import_zod15.z.enum(["intact", "torn"]).optional(),
|
|
1545
|
+
bottom: import_zod15.z.enum(["intact", "torn"]).optional(),
|
|
1546
|
+
left: import_zod15.z.enum(["intact", "torn"]).optional()
|
|
1360
1547
|
}).default({})
|
|
1361
1548
|
}).optional()
|
|
1362
1549
|
});
|
|
1363
|
-
var
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1550
|
+
var creaseSchema = import_zod15.z.object({
|
|
1551
|
+
angle: import_zod15.z.number().min(-360).max(360).default(90),
|
|
1552
|
+
offset: import_zod15.z.number().min(-20).max(20).default(0),
|
|
1553
|
+
/**
|
|
1554
|
+
* The residual fold angle in degrees, signed — how far open the crease
|
|
1555
|
+
* still sits once nothing is holding the paper. This is the whole of what
|
|
1556
|
+
* a crease IS: geometry and shading both read it, and it is what makes the
|
|
1557
|
+
* field authorable by hand (a dog-ear is a crease with a big `depth` near
|
|
1558
|
+
* a corner) rather than only recordable.
|
|
1559
|
+
*/
|
|
1560
|
+
depth: import_zod15.z.number().min(-180).max(180).default(12)
|
|
1561
|
+
});
|
|
1562
|
+
var memorySchema = import_zod15.z.object({
|
|
1563
|
+
/**
|
|
1564
|
+
* How much of a fold this paper keeps, 0..1, over the stock's own
|
|
1565
|
+
* {@link Stock.takesSet}. Kraft holds a crease hard; vellum springs most
|
|
1566
|
+
* of the way back.
|
|
1567
|
+
*/
|
|
1568
|
+
set: import_zod15.z.number().min(0).max(1).optional(),
|
|
1569
|
+
/**
|
|
1570
|
+
* The creases themselves. Recorded by folding the paper (see
|
|
1571
|
+
* `onCrease`), or written by hand — a preset can ship already creased.
|
|
1572
|
+
*
|
|
1573
|
+
* Capped at four because that is what the crease shader carries, and a
|
|
1574
|
+
* cap the schema states is better than one the renderer applies silently.
|
|
1575
|
+
*/
|
|
1576
|
+
creases: import_zod15.z.array(creaseSchema).max(4).default([])
|
|
1577
|
+
});
|
|
1578
|
+
var behaviorConfigSchema = import_zod15.z.discriminatedUnion("type", [
|
|
1579
|
+
peelOptionsSchema.extend({ type: import_zod15.z.literal("peel") }),
|
|
1580
|
+
unrollOptionsSchema.extend({ type: import_zod15.z.literal("unroll") }),
|
|
1581
|
+
flipOptionsSchema.extend({ type: import_zod15.z.literal("flip") }),
|
|
1582
|
+
letterFoldOptionsSchema.extend({ type: import_zod15.z.literal("letter-fold") }),
|
|
1583
|
+
hangOptionsSchema.extend({ type: import_zod15.z.literal("hang") }),
|
|
1584
|
+
flyOptionsSchema.extend({ type: import_zod15.z.literal("fly") }),
|
|
1585
|
+
fallOptionsSchema.extend({ type: import_zod15.z.literal("fall") }),
|
|
1586
|
+
carryOptionsSchema.extend({ type: import_zod15.z.literal("carry") }),
|
|
1587
|
+
flightOptionsSchema.extend({ type: import_zod15.z.literal("flight") }),
|
|
1588
|
+
crumpleBehaviorOptionsSchema.extend({ type: import_zod15.z.literal("crumple") }),
|
|
1589
|
+
settleOptionsSchema.extend({ type: import_zod15.z.literal("settle") }),
|
|
1590
|
+
ribbonOptionsSchema.extend({ type: import_zod15.z.literal("ribbon") })
|
|
1376
1591
|
]);
|
|
1377
|
-
var deformerInstanceSchema =
|
|
1378
|
-
type:
|
|
1379
|
-
options:
|
|
1380
|
-
enabled:
|
|
1592
|
+
var deformerInstanceSchema = import_zod15.z.object({
|
|
1593
|
+
type: import_zod15.z.string(),
|
|
1594
|
+
options: import_zod15.z.record(import_zod15.z.string(), import_zod15.z.unknown()).default({}),
|
|
1595
|
+
enabled: import_zod15.z.boolean().default(true)
|
|
1381
1596
|
});
|
|
1382
1597
|
var physicsNames = ["none", "float", "tumble", "dangle", "taped", "breeze"];
|
|
1383
|
-
var clothConfigSchema =
|
|
1384
|
-
type:
|
|
1385
|
-
pins:
|
|
1386
|
-
wind:
|
|
1598
|
+
var clothConfigSchema = import_zod15.z.object({
|
|
1599
|
+
type: import_zod15.z.literal("cloth"),
|
|
1600
|
+
pins: import_zod15.z.enum(["top-edge", "top-corners", "corner", "none"]).default("top-edge"),
|
|
1601
|
+
wind: import_zod15.z.number().min(0).max(1).default(0.3),
|
|
1387
1602
|
/** Bend stiffness: 1 = crisp paper, 0 = silk. */
|
|
1388
|
-
stiffness:
|
|
1389
|
-
gravity:
|
|
1603
|
+
stiffness: import_zod15.z.number().min(0).max(1).default(0.8),
|
|
1604
|
+
gravity: import_zod15.z.number().min(0).max(2).default(1),
|
|
1390
1605
|
/** Local-space ground plane the sheet settles onto. */
|
|
1391
|
-
floor:
|
|
1606
|
+
floor: import_zod15.z.number().min(-5).max(0).default(-1.4)
|
|
1607
|
+
});
|
|
1608
|
+
var stripConfigSchema = import_zod15.z.object({
|
|
1609
|
+
type: import_zod15.z.literal("strip"),
|
|
1610
|
+
/** How far the page has scrolled, in world units of paper asked for. */
|
|
1611
|
+
scroll: import_zod15.z.number().min(-1e3).max(1e3).default(0),
|
|
1612
|
+
/** Thin layers and many turns, or few and fat. */
|
|
1613
|
+
tightness: import_zod15.z.number().min(0).max(1).default(0.6),
|
|
1614
|
+
/** Radius of the cardboard tube — the roll never pays out past it. */
|
|
1615
|
+
core: import_zod15.z.number().min(0.01).max(0.5).default(0.09),
|
|
1616
|
+
/** Paper already hanging before the first scroll. A roll always has a leaf out. */
|
|
1617
|
+
tail: import_zod15.z.number().min(0).max(20).default(1.1),
|
|
1618
|
+
/** Spacing of the perforations: one sheet's worth of strip, in world units. */
|
|
1619
|
+
perforation: import_zod15.z.number().min(0.05).max(5).default(1),
|
|
1620
|
+
/**
|
|
1621
|
+
* How much a perforation remembers being folded. 0 = a fresh roll, 1 = one
|
|
1622
|
+
* that has been used.
|
|
1623
|
+
*
|
|
1624
|
+
* The default is high on purpose: below about 0.6 the landed paper flops
|
|
1625
|
+
* over in flat panels and spreads across the floor, and at 0.7 it holds its
|
|
1626
|
+
* folds and stacks into an accordion. The pile is the point.
|
|
1627
|
+
*/
|
|
1628
|
+
crease: import_zod15.z.number().min(0).max(1).default(0.7),
|
|
1629
|
+
/** Bend stiffness between perforations. 1 = card, 0 = cloth. */
|
|
1630
|
+
stiffness: import_zod15.z.number().min(0).max(1).default(0.55),
|
|
1631
|
+
/** Broadside air drag — what makes paper float down rather than drop. */
|
|
1632
|
+
drag: import_zod15.z.number().min(0).max(1).default(0.55),
|
|
1633
|
+
gravity: import_zod15.z.number().min(0).max(2).default(1),
|
|
1634
|
+
/**
|
|
1635
|
+
* How far below the roll the paper lands. A DISTANCE below the roll's axis,
|
|
1636
|
+
* matching `unroll.floor`, not a signed y like `cloth.floor` — the roll
|
|
1637
|
+
* family measures drops, and the composition is centred on the origin so
|
|
1638
|
+
* an absolute y would not survive the offset anyway.
|
|
1639
|
+
*/
|
|
1640
|
+
floor: import_zod15.z.number().min(0.1).max(30).default(1.2),
|
|
1641
|
+
/** How long the roll coasts after the scroll stops. */
|
|
1642
|
+
inertia: import_zod15.z.number().min(0).max(1).default(0.45)
|
|
1392
1643
|
});
|
|
1393
|
-
var physicsSchema =
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1644
|
+
var physicsSchema = import_zod15.z.union([
|
|
1645
|
+
import_zod15.z.enum(physicsNames),
|
|
1646
|
+
import_zod15.z.literal("cloth").transform(() => clothConfigSchema.parse({ type: "cloth" })),
|
|
1647
|
+
import_zod15.z.literal("strip").transform(() => stripConfigSchema.parse({ type: "strip" })),
|
|
1648
|
+
clothConfigSchema,
|
|
1649
|
+
stripConfigSchema
|
|
1397
1650
|
]);
|
|
1398
1651
|
var lightingNames = [
|
|
1399
1652
|
"studio",
|
|
@@ -1406,9 +1659,9 @@ var lightingNames = [
|
|
|
1406
1659
|
"lightbox"
|
|
1407
1660
|
];
|
|
1408
1661
|
var filmNames = ["agx", "neutral", "filmic"];
|
|
1409
|
-
var lightSchema =
|
|
1662
|
+
var lightSchema = import_zod15.z.object({
|
|
1410
1663
|
/** Tone-mapping exposure — the stop the whole picture is printed at. */
|
|
1411
|
-
exposure:
|
|
1664
|
+
exposure: import_zod15.z.number().min(0.1).max(4).optional(),
|
|
1412
1665
|
/**
|
|
1413
1666
|
* The tone curve — the film, where `exposure` is the stop.
|
|
1414
1667
|
*
|
|
@@ -1417,33 +1670,33 @@ var lightSchema = import_zod14.z.object({
|
|
|
1417
1670
|
* the wrong film: it desaturates and drags bright neutrals toward
|
|
1418
1671
|
* yellow-green, which is the sepia cast a lit sheet used to pick up.
|
|
1419
1672
|
*/
|
|
1420
|
-
film:
|
|
1673
|
+
film: import_zod15.z.enum(filmNames).optional(),
|
|
1421
1674
|
/** Key light strength. */
|
|
1422
|
-
key:
|
|
1675
|
+
key: import_zod15.z.number().min(0).max(12).optional(),
|
|
1423
1676
|
/** Key light colour. */
|
|
1424
|
-
color:
|
|
1677
|
+
color: import_zod15.z.string().optional().describe("color"),
|
|
1425
1678
|
/**
|
|
1426
1679
|
* Where the key stands, degrees around the vertical. 0° is straight in
|
|
1427
1680
|
* front of the paper (+Z, beside the camera), 90° is off to the right,
|
|
1428
1681
|
* and ±180° is directly behind it — which is where `nave` puts it, and
|
|
1429
1682
|
* why that preset is carried by light coming THROUGH the paper.
|
|
1430
1683
|
*/
|
|
1431
|
-
direction:
|
|
1684
|
+
direction: import_zod15.z.number().min(-180).max(180).optional(),
|
|
1432
1685
|
/** How high the key stands, degrees above the horizon. */
|
|
1433
|
-
height:
|
|
1686
|
+
height: import_zod15.z.number().min(-30).max(89).optional(),
|
|
1434
1687
|
/** Flat fill from every direction at once. Cheap, and it kills form — reach for `studio` first. */
|
|
1435
|
-
ambient:
|
|
1688
|
+
ambient: import_zod15.z.number().min(0).max(2).optional(),
|
|
1436
1689
|
/** The room's own light: an environment map built from `sky`. Directional fill, and the only thing paper's sheen has to reflect. */
|
|
1437
|
-
studio:
|
|
1690
|
+
studio: import_zod15.z.number().min(0).max(3).optional(),
|
|
1438
1691
|
/** Distance haze, as a multiple of the preset's. 0 clears the air entirely; 2 halves the distance you can see. */
|
|
1439
|
-
haze:
|
|
1692
|
+
haze: import_zod15.z.number().min(0).max(3).optional()
|
|
1440
1693
|
});
|
|
1441
|
-
var backdropSchema =
|
|
1694
|
+
var backdropSchema = import_zod15.z.object({
|
|
1442
1695
|
/** Behind everything, and behind the picture where it does not reach. */
|
|
1443
|
-
color:
|
|
1696
|
+
color: import_zod15.z.string().default("#171717").describe("color"),
|
|
1444
1697
|
/** A URL, or an uploaded picture. Empty is the colour on its own. */
|
|
1445
|
-
image:
|
|
1446
|
-
fit:
|
|
1698
|
+
image: import_zod15.z.string().default(""),
|
|
1699
|
+
fit: import_zod15.z.enum(["cover", "contain"]).default("cover"),
|
|
1447
1700
|
/**
|
|
1448
1701
|
* Toward the colour, so the paper stays the subject.
|
|
1449
1702
|
*
|
|
@@ -1452,14 +1705,35 @@ var backdropSchema = import_zod14.z.object({
|
|
|
1452
1705
|
* of the light, and what this solves by mixing it back toward the ground
|
|
1453
1706
|
* it sits on.
|
|
1454
1707
|
*/
|
|
1455
|
-
fade:
|
|
1708
|
+
fade: import_zod15.z.number().min(0).max(1).default(0.25),
|
|
1456
1709
|
/** Out of focus, for the same reason. */
|
|
1457
|
-
blur:
|
|
1710
|
+
blur: import_zod15.z.number().min(0).max(1).default(0.2)
|
|
1458
1711
|
});
|
|
1459
|
-
var sceneSchema =
|
|
1460
|
-
lighting:
|
|
1712
|
+
var sceneSchema = import_zod15.z.object({
|
|
1713
|
+
lighting: import_zod15.z.enum(lightingNames).default("studio"),
|
|
1461
1714
|
/** What is behind the sheet. Unset leaves the canvas alone. */
|
|
1462
1715
|
backdrop: backdropSchema.optional(),
|
|
1716
|
+
/**
|
|
1717
|
+
* Degrees the whole composition is turned about its vertical axis, so a
|
|
1718
|
+
* preset can choose the angle it is READ from.
|
|
1719
|
+
*
|
|
1720
|
+
* Every camera in the library is fixed and head-on — `<Paper>` sits at
|
|
1721
|
+
* `(0, 0.35, 2.4)` looking down -Z, and neither it nor the editor fits a
|
|
1722
|
+
* camera to its content. That is the right default for a sheet, which is
|
|
1723
|
+
* flat and faces you. It is the wrong one for anything whose shape lives
|
|
1724
|
+
* in DEPTH: the `strip` sim folds in z by construction, so head-on its
|
|
1725
|
+
* roll and the whole accordion of its pile are edge-on and the preset
|
|
1726
|
+
* renders as a blank white column.
|
|
1727
|
+
*
|
|
1728
|
+
* A camera field would have been the other way to fix it, and is worse: it
|
|
1729
|
+
* is meaningless inside `<PaperField>` and `<PaperMesh>`, where the caller
|
|
1730
|
+
* owns the camera and there may be a dozen papers sharing it. Turning the
|
|
1731
|
+
* paper works everywhere, because it is a property of the paper.
|
|
1732
|
+
*
|
|
1733
|
+
* Additive with the `rotation` prop rather than overriding it — the prop
|
|
1734
|
+
* is the caller's, and a preset does not get to overrule it.
|
|
1735
|
+
*/
|
|
1736
|
+
turn: import_zod15.z.number().min(-180).max(180).default(0),
|
|
1463
1737
|
/**
|
|
1464
1738
|
* Overrides on the named preset — the same authorable half stage mode has
|
|
1465
1739
|
* always had, and which a lone sheet had no way to reach.
|
|
@@ -1473,56 +1747,65 @@ var sceneSchema = import_zod14.z.object({
|
|
|
1473
1747
|
});
|
|
1474
1748
|
var coreStateNames = ["rest", "hover", "pressed", "picked", "placed"];
|
|
1475
1749
|
var isStateName = (s) => coreStateNames.includes(s) || s.startsWith("custom:");
|
|
1476
|
-
var stateNameSchema =
|
|
1750
|
+
var stateNameSchema = import_zod15.z.string().refine(isStateName, {
|
|
1477
1751
|
message: `state names are ${coreStateNames.join(", ")} or "custom:<name>"`
|
|
1478
1752
|
});
|
|
1479
|
-
var stateTransitionSchema =
|
|
1480
|
-
duration:
|
|
1753
|
+
var stateTransitionSchema = import_zod15.z.object({
|
|
1754
|
+
duration: import_zod15.z.number().min(0).max(5).default(0.35),
|
|
1481
1755
|
/** GSAP ease name. */
|
|
1482
|
-
ease:
|
|
1756
|
+
ease: import_zod15.z.string().default("power2.out")
|
|
1483
1757
|
});
|
|
1484
|
-
var stateDefSchema =
|
|
1758
|
+
var stateDefSchema = import_zod15.z.object({
|
|
1485
1759
|
/** Deep-partial override of the paper schema (behavior params, surface, …). */
|
|
1486
|
-
overrides:
|
|
1760
|
+
overrides: import_zod15.z.record(import_zod15.z.string(), import_zod15.z.unknown()).default({}),
|
|
1487
1761
|
/** Transition INTO this state. */
|
|
1488
|
-
transition: stateTransitionSchema.
|
|
1762
|
+
transition: stateTransitionSchema.prefault({}),
|
|
1489
1763
|
/** Chained actions after arriving. v1: 'emit:<event>' only. */
|
|
1490
|
-
onEnter:
|
|
1764
|
+
onEnter: import_zod15.z.array(import_zod15.z.string().regex(/^emit:[\w-]+$/, 'v1 actions are "emit:<event>"')).default([])
|
|
1491
1765
|
});
|
|
1492
|
-
var paperStatesSchema =
|
|
1766
|
+
var paperStatesSchema = import_zod15.z.object({
|
|
1493
1767
|
initial: stateNameSchema.default("rest"),
|
|
1494
|
-
states:
|
|
1768
|
+
states: import_zod15.z.record(import_zod15.z.string(), stateDefSchema).default({}).refine((rec) => Object.keys(rec).every(isStateName), {
|
|
1495
1769
|
message: `state names are ${coreStateNames.join(", ")} or "custom:<name>"`
|
|
1496
1770
|
}),
|
|
1497
1771
|
/** World-units drag distance that flips pressed → picked (pick-enabled behaviors only). */
|
|
1498
|
-
pickThreshold:
|
|
1772
|
+
pickThreshold: import_zod15.z.number().min(5e-3).max(1).default(0.1)
|
|
1499
1773
|
});
|
|
1500
|
-
var metaSchema =
|
|
1501
|
-
name:
|
|
1502
|
-
author:
|
|
1503
|
-
version:
|
|
1504
|
-
tags:
|
|
1774
|
+
var metaSchema = import_zod15.z.object({
|
|
1775
|
+
name: import_zod15.z.string().default("untitled"),
|
|
1776
|
+
author: import_zod15.z.string().optional(),
|
|
1777
|
+
version: import_zod15.z.string().default("0"),
|
|
1778
|
+
tags: import_zod15.z.array(import_zod15.z.string()).default([])
|
|
1505
1779
|
});
|
|
1506
|
-
var paperConfigSchema =
|
|
1507
|
-
meta: metaSchema.
|
|
1508
|
-
sheet: sheetSchema.
|
|
1780
|
+
var paperConfigSchema = import_zod15.z.object({
|
|
1781
|
+
meta: metaSchema.prefault({}),
|
|
1782
|
+
sheet: sheetSchema.prefault({}),
|
|
1509
1783
|
stock: stockSchema.default("printer"),
|
|
1510
1784
|
content: contentSchema.default({ type: "blank" }),
|
|
1511
1785
|
/** A behavior OR a raw deformer stack — if both are present, `deformers` wins (it's the fork). */
|
|
1512
1786
|
behavior: behaviorConfigSchema.optional(),
|
|
1513
|
-
deformers:
|
|
1787
|
+
deformers: import_zod15.z.array(deformerInstanceSchema).optional(),
|
|
1514
1788
|
surface: surfaceSchema.default({}),
|
|
1789
|
+
/**
|
|
1790
|
+
* What the sheet remembers being folded — creases outlive the fold.
|
|
1791
|
+
*
|
|
1792
|
+
* Present by default, and on: paper that forgets is the bug this exists
|
|
1793
|
+
* to fix, so remembering is not something a preset should have to ask
|
|
1794
|
+
* for. `memory: { set: 0 }` is the opt-out, and it is what every sheet in
|
|
1795
|
+
* the library did before this shipped.
|
|
1796
|
+
*/
|
|
1797
|
+
memory: memorySchema.prefault({}),
|
|
1515
1798
|
physics: physicsSchema.default("none"),
|
|
1516
|
-
scene: sceneSchema.
|
|
1517
|
-
onTwos:
|
|
1799
|
+
scene: sceneSchema.prefault({}),
|
|
1800
|
+
onTwos: import_zod15.z.boolean().default(false),
|
|
1518
1801
|
/** Interaction state machine — overrides-on-base diffs. */
|
|
1519
1802
|
states: paperStatesSchema.optional()
|
|
1520
1803
|
}).superRefine((config, ctx) => {
|
|
1521
|
-
if (typeof config.physics === "object" && (config.behavior || config.deformers)) {
|
|
1804
|
+
if (typeof config.physics === "object" && config.physics.type === "strip" && (config.behavior || config.deformers)) {
|
|
1522
1805
|
ctx.addIssue({
|
|
1523
|
-
code:
|
|
1806
|
+
code: import_zod15.z.ZodIssueCode.custom,
|
|
1524
1807
|
path: ["physics"],
|
|
1525
|
-
message: "
|
|
1808
|
+
message: "the strip simulation and behavior/deformers are exclusive \u2014 the roll owns the vertices, and its rows are chain nodes rather than the sheet's own grid (pick Shape OR Strip)"
|
|
1526
1809
|
});
|
|
1527
1810
|
}
|
|
1528
1811
|
if (config.states) {
|
|
@@ -1531,7 +1814,7 @@ var paperConfigSchema = import_zod14.z.object({
|
|
|
1531
1814
|
if (!def) continue;
|
|
1532
1815
|
if (def.overrides.states !== void 0) {
|
|
1533
1816
|
ctx.addIssue({
|
|
1534
|
-
code:
|
|
1817
|
+
code: import_zod15.z.ZodIssueCode.custom,
|
|
1535
1818
|
path: ["states", "states", name, "overrides"],
|
|
1536
1819
|
message: "state overrides cannot override `states` (no nested state machines)"
|
|
1537
1820
|
});
|
|
@@ -1542,7 +1825,7 @@ var paperConfigSchema = import_zod14.z.object({
|
|
|
1542
1825
|
if (!result.success) {
|
|
1543
1826
|
const first = result.error.issues[0];
|
|
1544
1827
|
ctx.addIssue({
|
|
1545
|
-
code:
|
|
1828
|
+
code: import_zod15.z.ZodIssueCode.custom,
|
|
1546
1829
|
path: ["states", "states", name, "overrides"],
|
|
1547
1830
|
message: `state "${name}" overrides don't validate against the paper schema: ${first ? `${first.path.join(".")} \u2014 ${first.message}` : "invalid"}`
|
|
1548
1831
|
});
|
|
@@ -1721,21 +2004,21 @@ var lightingPresets = {
|
|
|
1721
2004
|
function getLightingPreset(name) {
|
|
1722
2005
|
return lightingPresets[name];
|
|
1723
2006
|
}
|
|
1724
|
-
var
|
|
2007
|
+
var DEG3 = 180 / Math.PI;
|
|
1725
2008
|
function lightAngles(position) {
|
|
1726
2009
|
const [x, y, z30] = position;
|
|
1727
2010
|
const distance = Math.hypot(x, y, z30);
|
|
1728
2011
|
if (distance < 1e-9) return { azimuth: 0, elevation: 90, distance: 0 };
|
|
1729
2012
|
const ground = Math.hypot(x, z30);
|
|
1730
2013
|
return {
|
|
1731
|
-
azimuth: ground < 1e-9 ? 0 : Math.atan2(x, z30) *
|
|
1732
|
-
elevation: Math.atan2(y, ground) *
|
|
2014
|
+
azimuth: ground < 1e-9 ? 0 : Math.atan2(x, z30) * DEG3,
|
|
2015
|
+
elevation: Math.atan2(y, ground) * DEG3,
|
|
1733
2016
|
distance
|
|
1734
2017
|
};
|
|
1735
2018
|
}
|
|
1736
2019
|
function lightPosition(angles) {
|
|
1737
|
-
const azimuth = angles.azimuth /
|
|
1738
|
-
const elevation = angles.elevation /
|
|
2020
|
+
const azimuth = angles.azimuth / DEG3;
|
|
2021
|
+
const elevation = angles.elevation / DEG3;
|
|
1739
2022
|
const ground = Math.cos(elevation) * angles.distance;
|
|
1740
2023
|
return [Math.sin(azimuth) * ground, Math.sin(elevation) * angles.distance, Math.cos(azimuth) * ground];
|
|
1741
2024
|
}
|
|
@@ -2344,6 +2627,183 @@ var builtins = {
|
|
|
2344
2627
|
behavior: { type: "unroll", progress: 0.55, tightness: 0.55, sway: 0.3 },
|
|
2345
2628
|
surface: { deckle: { edges: ["bottom"], roughness: 0.6 } }
|
|
2346
2629
|
},
|
|
2630
|
+
/**
|
|
2631
|
+
* A roll on a holder with a leaf already out, meant to be driven by scroll:
|
|
2632
|
+
* bind `behavior.progress` to how far down the page you are and the paper
|
|
2633
|
+
* pays out while the roll runs down toward its tube.
|
|
2634
|
+
*
|
|
2635
|
+
* The three parts that make it read as a real roll rather than a curled
|
|
2636
|
+
* sheet: `fixed` keeps the roll on its holder and moves the paper instead,
|
|
2637
|
+
* `tail` means there is always a leaf to take hold of, and `floor` gives
|
|
2638
|
+
* the drop somewhere to land — paper that reaches the ground creases and
|
|
2639
|
+
* lies down rather than hanging into the void forever. A `core` a third of
|
|
2640
|
+
* the full radius is a real cardboard tube, so the roll still looks like a
|
|
2641
|
+
* roll after it has been used down.
|
|
2642
|
+
*/
|
|
2643
|
+
"paper-roll": {
|
|
2644
|
+
meta: { name: "Paper roll", tags: ["roll", "unroll", "scroll", "hero"] },
|
|
2645
|
+
sheet: { width: 1, height: 5 },
|
|
2646
|
+
stock: "newsprint",
|
|
2647
|
+
content: { type: "blank" },
|
|
2648
|
+
behavior: {
|
|
2649
|
+
type: "unroll",
|
|
2650
|
+
progress: 0.25,
|
|
2651
|
+
tightness: 0.8,
|
|
2652
|
+
sway: 0.15,
|
|
2653
|
+
from: "top",
|
|
2654
|
+
fixed: true,
|
|
2655
|
+
core: 0.12,
|
|
2656
|
+
tail: 0.5,
|
|
2657
|
+
floor: 2.4
|
|
2658
|
+
},
|
|
2659
|
+
surface: { deckle: { edges: ["bottom"], roughness: 0.5 } }
|
|
2660
|
+
},
|
|
2661
|
+
/**
|
|
2662
|
+
* The one that is a SIMULATION rather than a shape.
|
|
2663
|
+
*
|
|
2664
|
+
* `paper-roll` above draws this same object with a deformer stack, and for
|
|
2665
|
+
* a roll paying out against a wall that is the cheaper and better answer.
|
|
2666
|
+
* This preset exists for the half that geometry cannot reach: what happens
|
|
2667
|
+
* once the paper hits the ground. A deformer can bend a sheet along a curve
|
|
2668
|
+
* you have already chosen; it cannot discover that a strip under
|
|
2669
|
+
* compression buckles at its weakest hinge, and it cannot let one fold land
|
|
2670
|
+
* on the one beneath it. Both of those are what a pile IS.
|
|
2671
|
+
*
|
|
2672
|
+
* Bind `physics.scroll` to the page and the roll turns:
|
|
2673
|
+
*
|
|
2674
|
+
* ```tsx
|
|
2675
|
+
* const [scroll, setScroll] = useState(0)
|
|
2676
|
+
* useEffect(() => {
|
|
2677
|
+
* const onScroll = () => setScroll(window.scrollY / 120)
|
|
2678
|
+
* window.addEventListener('scroll', onScroll, { passive: true })
|
|
2679
|
+
* return () => window.removeEventListener('scroll', onScroll)
|
|
2680
|
+
* }, [])
|
|
2681
|
+
* <Paper preset="toilet-roll" physics={{ type: 'strip', scroll }} />
|
|
2682
|
+
* ```
|
|
2683
|
+
*
|
|
2684
|
+
* It is a MONOTONIC world-unit number, not a 0..1 progress — the sim
|
|
2685
|
+
* differentiates it, so scrolling back up rewinds the roll and drags the
|
|
2686
|
+
* pile taut before it lifts.
|
|
2687
|
+
*
|
|
2688
|
+
* The proportions are the real object's: a panel as wide as it is long, so
|
|
2689
|
+
* `perforation` equals the sheet width and the strip tears into squares.
|
|
2690
|
+
*/
|
|
2691
|
+
"toilet-roll": {
|
|
2692
|
+
meta: { name: "Toilet roll", tags: ["roll", "scroll", "simulation", "hero"] },
|
|
2693
|
+
// The proportions are a real roll's, at the scale the library is viewed
|
|
2694
|
+
// at: `<Paper>` and the editor both look at the origin through about two
|
|
2695
|
+
// world units and neither fits a camera to its content, so the whole
|
|
2696
|
+
// composition — roll, drop and pile — has to live inside that. A panel is
|
|
2697
|
+
// as wide as it is long and the roll is about a panel across, which is
|
|
2698
|
+
// what a toilet roll is.
|
|
2699
|
+
//
|
|
2700
|
+
// Twenty-three panels of paper. Not a real roll's several hundred, but
|
|
2701
|
+
// enough that a full page of scrolling does not empty it: one scroll unit
|
|
2702
|
+
// is about one unit of paper on a fresh roll, so this is a couple of
|
|
2703
|
+
// screens' worth before the tube shows.
|
|
2704
|
+
sheet: { width: 0.6, height: 14 },
|
|
2705
|
+
stock: "printer",
|
|
2706
|
+
content: { type: "blank" },
|
|
2707
|
+
physics: {
|
|
2708
|
+
type: "strip",
|
|
2709
|
+
scroll: 0,
|
|
2710
|
+
// Wound tightly enough to hold this much paper at a believable size: the
|
|
2711
|
+
// outer radius lands at 0.61 of the panel width against a real roll's
|
|
2712
|
+
// 0.57, over about nine visible turns.
|
|
2713
|
+
//
|
|
2714
|
+
// `tightness` is doing double duty and the trade is worth knowing about.
|
|
2715
|
+
// A layer gap IS the paper's thickness, so it also sets how far apart
|
|
2716
|
+
// self-collision holds two folds — wind tighter for a neater roll and
|
|
2717
|
+
// the pile on the floor gets flatter, looser for a fatter pile and the
|
|
2718
|
+
// roll coarsens. This is the middle of that.
|
|
2719
|
+
//
|
|
2720
|
+
// It is ALSO the roll's rim, and that is the reason not to raise it on
|
|
2721
|
+
// looks alone. A layer gap is a real space between two wound turns, so
|
|
2722
|
+
// the roll's end face is concentric rings with nothing between them,
|
|
2723
|
+
// and off head-on you see between them — a fine sawtooth around the
|
|
2724
|
+
// rim that winding tighter genuinely reduces. It was tried. Tightening
|
|
2725
|
+
// to 0.78 also moves the roll's proportion to 0.553 of a panel width,
|
|
2726
|
+
// nearer a real roll's 0.57 than this is, so it looked like a free win
|
|
2727
|
+
// — and it throws the pile 1.33 units out in x against the 0.874 a
|
|
2728
|
+
// square parent can see, spreading 5.9 panel-widths. Framing beats the
|
|
2729
|
+
// rim. The sawtooth is what a roll wound from ONE zero-thickness
|
|
2730
|
+
// ribbon costs; it is not tuned away, and a caller who wants it gone
|
|
2731
|
+
// wants a thicker `stock` or fewer, fatter turns.
|
|
2732
|
+
tightness: 0.65,
|
|
2733
|
+
// A real cardboard tube, and a floor on how tight the spiral ever winds.
|
|
2734
|
+
// The innermost wrap is the coarsest thing in the roll — the same
|
|
2735
|
+
// arc-length step spans a bigger angle the smaller the radius — so the
|
|
2736
|
+
// core is what stops a nearly-empty roll turning back into a polygon.
|
|
2737
|
+
core: 0.12,
|
|
2738
|
+
// A panel and a half already hanging. A roll on a holder always has a
|
|
2739
|
+
// leaf out; starting from a bare cylinder reads as one still wrapped.
|
|
2740
|
+
tail: 0.9,
|
|
2741
|
+
perforation: 0.6,
|
|
2742
|
+
// The four numbers below were chosen TOGETHER, and by worst case rather
|
|
2743
|
+
// than by a good-looking run. A pile is chaotic: change `crease` by
|
|
2744
|
+
// 0.05 and one 14-second scroll can spread 2.0 panel-widths of floor or
|
|
2745
|
+
// 4.2, so a single trajectory is a sample and not a measurement. These
|
|
2746
|
+
// are scored over nine — three scroll depths crossed with three feed
|
|
2747
|
+
// rates — on how far the composition ever gets from the origin.
|
|
2748
|
+
//
|
|
2749
|
+
// What that fixed: the shipped set spread 2.9 panel-widths on average
|
|
2750
|
+
// and 4.0 at worst, and threw paper 1.60 units out in z. It ran off the
|
|
2751
|
+
// side of the frame and kept going. These hold 2.2 average, 2.8 worst,
|
|
2752
|
+
// and 1.09 out — while keeping the pile's height (about ten layers)
|
|
2753
|
+
// intact, which is the thing all of this exists to show.
|
|
2754
|
+
//
|
|
2755
|
+
// The most load-bearing of them. Below about 0.6 the landed paper flops
|
|
2756
|
+
// over in flat panels and runs away across the floor instead of folding
|
|
2757
|
+
// back; high is what makes the perforations hold and the pile
|
|
2758
|
+
// accordion. A used roll remembering its creases is the whole effect.
|
|
2759
|
+
crease: 0.9,
|
|
2760
|
+
// Low enough that a panel buckles rather than steering the pile: at 0.5
|
|
2761
|
+
// the sheet was stiff enough to push the folds already down along the
|
|
2762
|
+
// floor ahead of it, which is what "spreads across four panels" was.
|
|
2763
|
+
stiffness: 0.4,
|
|
2764
|
+
// High: paper is light and broad, and this is what separates it from a
|
|
2765
|
+
// rope hanging off a drum. It also damps the sideways travel that
|
|
2766
|
+
// carried the pile out of frame.
|
|
2767
|
+
drag: 0.85,
|
|
2768
|
+
gravity: 1,
|
|
2769
|
+
// The drop, measured from the roll's axis. Together with the roll's own
|
|
2770
|
+
// radius this IS the height of the composition, which is centred on the
|
|
2771
|
+
// origin — so keep their sum under about 1.7 or the roll and the pile
|
|
2772
|
+
// fall outside the ~1.75 units `<Paper>`'s fixed camera can see.
|
|
2773
|
+
//
|
|
2774
|
+
// Shortened from 1.2, and it is the single most effective number here:
|
|
2775
|
+
// a longer fall is more airtime for the strip to pick a direction and
|
|
2776
|
+
// glide, so it landed still travelling and slid. Worst-case spread goes
|
|
2777
|
+
// 4.2 panel-widths to 3.1 and the pile keeps its full depth. It buys
|
|
2778
|
+
// vertical room as well, which is what lets the roll sit further up.
|
|
2779
|
+
floor: 0.85,
|
|
2780
|
+
inertia: 0.5
|
|
2781
|
+
},
|
|
2782
|
+
surface: { grain: 0.25 },
|
|
2783
|
+
// The preset is unreadable without this, and that is not a figure of
|
|
2784
|
+
// speech: the strip sim folds in DEPTH, and every camera in the library
|
|
2785
|
+
// is fixed and head-on, so `<Paper preset="toilet-roll" />` framed the
|
|
2786
|
+
// roll end-on and the entire accordion edge-on and rendered a blank white
|
|
2787
|
+
// column. This is the three-quarter view the pile actually reads from —
|
|
2788
|
+
// enough to see along the folds and around the roll's rim, not so much
|
|
2789
|
+
// that the strip's face turns away.
|
|
2790
|
+
//
|
|
2791
|
+
// The ceiling is framing, not taste. Turning swaps the pile's DEPTH for
|
|
2792
|
+
// WIDTH, and depth is free — the camera has plenty — while width is not.
|
|
2793
|
+
// Measured over fifteen scroll trajectories the pile reaches 1.27 units
|
|
2794
|
+
// of depth at worst, and `halfWidth·cos θ + 1.27·sin θ` crosses the 0.874
|
|
2795
|
+
// half-view a square parent gets at 28°. Twenty-five leaves real margin
|
|
2796
|
+
// (0.809) and gives up almost nothing of the angle.
|
|
2797
|
+
//
|
|
2798
|
+
// Do not raise it without re-measuring that depth: the two numbers are
|
|
2799
|
+
// coupled, and the pile's depth is not stable under small changes to the
|
|
2800
|
+
// physics — see the note on `floor`.
|
|
2801
|
+
//
|
|
2802
|
+
// A parent narrower than square still crops the pile's far edge. That is
|
|
2803
|
+
// the honest limit of a fixed camera, and the caller's answer is their
|
|
2804
|
+
// own `rotation` prop, which this composes with rather than overrides.
|
|
2805
|
+
scene: { turn: 25 }
|
|
2806
|
+
},
|
|
2347
2807
|
"letter-fold": {
|
|
2348
2808
|
meta: { name: "Letter fold", tags: ["fold", "text"] },
|
|
2349
2809
|
sheet: { width: 1, height: 1.4 },
|
|
@@ -2353,7 +2813,24 @@ var builtins = {
|
|
|
2353
2813
|
text: "Dear you,\n\nSome things are worth folding carefully.\n\nYours,\nN."
|
|
2354
2814
|
},
|
|
2355
2815
|
behavior: { type: "letter-fold", progress: 0.4, crease: 0.3 },
|
|
2356
|
-
|
|
2816
|
+
// A letter that has been folded before. These were painted-on
|
|
2817
|
+
// `creaseLines` until the paper could carry real ones — two marks in the
|
|
2818
|
+
// shader at exactly the two places the tri-fold bends, which is a good
|
|
2819
|
+
// impression of a crease right up until you unfold the letter and it
|
|
2820
|
+
// comes back perfectly flat. As memory they bend the sheet as well as
|
|
2821
|
+
// marking it, and folding the letter deepens the creases it already has
|
|
2822
|
+
// rather than drawing a second pair on top of them.
|
|
2823
|
+
//
|
|
2824
|
+
// The lines are `letterFold`'s own: travel down and up, each a sixth of
|
|
2825
|
+
// the sheet from the middle. Shallower than a fold leaves (printer keeps
|
|
2826
|
+
// about 20°) because this letter has been folded once and put away, not
|
|
2827
|
+
// folded and reopened in front of you.
|
|
2828
|
+
memory: {
|
|
2829
|
+
creases: [
|
|
2830
|
+
{ angle: 270, offset: 1.4 / 6, depth: 13 },
|
|
2831
|
+
{ angle: 90, offset: 1.4 / 6, depth: 11 }
|
|
2832
|
+
]
|
|
2833
|
+
}
|
|
2357
2834
|
},
|
|
2358
2835
|
/**
|
|
2359
2836
|
* The wash, shown rather than described.
|
|
@@ -2648,9 +3125,9 @@ var NOTE_TRACKING = 0.06;
|
|
|
2648
3125
|
function paintCard(ctx, w, h, content, stock, dpr) {
|
|
2649
3126
|
const ink = content.color === "#2b2620" ? stock.inkColor : content.color;
|
|
2650
3127
|
const size = content.size * dpr;
|
|
2651
|
-
const
|
|
2652
|
-
const maxWidth = w -
|
|
2653
|
-
const x = content.align === "center" ? w / 2 :
|
|
3128
|
+
const pad2 = content.padding * Math.min(w, h);
|
|
3129
|
+
const maxWidth = w - pad2 * 2;
|
|
3130
|
+
const x = content.align === "center" ? w / 2 : pad2;
|
|
2654
3131
|
ctx.textAlign = content.align === "center" ? "center" : "left";
|
|
2655
3132
|
ctx.textBaseline = "alphabetic";
|
|
2656
3133
|
const titleSize = size * TITLE_RATIO;
|
|
@@ -2662,7 +3139,7 @@ function paintCard(ctx, w, h, content, stock, dpr) {
|
|
|
2662
3139
|
const noteBlock = content.note ? noteSize * 2.4 : 0;
|
|
2663
3140
|
const bodyBlock = bodyLines.length * bodyStep;
|
|
2664
3141
|
const total = titleBlock + ruleBlock + bodyBlock + noteBlock;
|
|
2665
|
-
let y = Math.max(
|
|
3142
|
+
let y = Math.max(pad2, (h - total) / 2) + size * 0.9;
|
|
2666
3143
|
if (content.title) {
|
|
2667
3144
|
ctx.font = `${titleSize}px ${content.font}`;
|
|
2668
3145
|
ctx.letterSpacing = `${TITLE_TRACKING}em`;
|
|
@@ -2678,8 +3155,8 @@ function paintCard(ctx, w, h, content, stock, dpr) {
|
|
|
2678
3155
|
ctx.globalAlpha = 0.28;
|
|
2679
3156
|
ctx.lineWidth = Math.max(1, dpr * 0.75);
|
|
2680
3157
|
ctx.beginPath();
|
|
2681
|
-
ctx.moveTo(content.align === "center" ? w / 2 - maxWidth / 2 :
|
|
2682
|
-
ctx.lineTo(content.align === "center" ? w / 2 + maxWidth / 2 :
|
|
3158
|
+
ctx.moveTo(content.align === "center" ? w / 2 - maxWidth / 2 : pad2, y - titleSize * 0.5);
|
|
3159
|
+
ctx.lineTo(content.align === "center" ? w / 2 + maxWidth / 2 : pad2 + maxWidth, y - titleSize * 0.5);
|
|
2683
3160
|
ctx.stroke();
|
|
2684
3161
|
ctx.restore();
|
|
2685
3162
|
y += ruleBlock;
|
|
@@ -2693,8 +3170,8 @@ function paintCard(ctx, w, h, content, stock, dpr) {
|
|
|
2693
3170
|
for (let i = 0; i < bodyLines.length; i++) {
|
|
2694
3171
|
const lineY = y + i * bodyStep + size * 0.28;
|
|
2695
3172
|
ctx.beginPath();
|
|
2696
|
-
ctx.moveTo(
|
|
2697
|
-
ctx.lineTo(
|
|
3173
|
+
ctx.moveTo(pad2, lineY);
|
|
3174
|
+
ctx.lineTo(pad2 + maxWidth, lineY);
|
|
2698
3175
|
ctx.stroke();
|
|
2699
3176
|
}
|
|
2700
3177
|
ctx.restore();
|
|
@@ -2702,7 +3179,7 @@ function paintCard(ctx, w, h, content, stock, dpr) {
|
|
|
2702
3179
|
ctx.font = `${size}px ${content.font}`;
|
|
2703
3180
|
ctx.fillStyle = ink;
|
|
2704
3181
|
for (const line of bodyLines) {
|
|
2705
|
-
if (y > h -
|
|
3182
|
+
if (y > h - pad2) break;
|
|
2706
3183
|
ctx.fillText(line, x, y);
|
|
2707
3184
|
y += bodyStep;
|
|
2708
3185
|
}
|
|
@@ -2710,7 +3187,7 @@ function paintCard(ctx, w, h, content, stock, dpr) {
|
|
|
2710
3187
|
ctx.font = `${noteSize}px ${content.font}`;
|
|
2711
3188
|
ctx.letterSpacing = `${NOTE_TRACKING}em`;
|
|
2712
3189
|
ctx.globalAlpha = 0.6;
|
|
2713
|
-
ctx.fillText(content.note, x, Math.min(y + noteSize * 1.4, h -
|
|
3190
|
+
ctx.fillText(content.note, x, Math.min(y + noteSize * 1.4, h - pad2));
|
|
2714
3191
|
ctx.globalAlpha = 1;
|
|
2715
3192
|
ctx.letterSpacing = "0em";
|
|
2716
3193
|
}
|
|
@@ -2840,23 +3317,23 @@ function paintImage(ctx, w, h, img, fit) {
|
|
|
2840
3317
|
}
|
|
2841
3318
|
function paintText(ctx, w, h, content, stock) {
|
|
2842
3319
|
const size = content.size * DPR;
|
|
2843
|
-
const
|
|
3320
|
+
const pad2 = content.padding * Math.min(w, h);
|
|
2844
3321
|
const font = `${content.weight} ${size}px ${content.font}`;
|
|
2845
3322
|
ctx.font = font;
|
|
2846
3323
|
ctx.fillStyle = content.color === "#2b2620" ? stock.inkColor : content.color;
|
|
2847
3324
|
ctx.textBaseline = "top";
|
|
2848
3325
|
ctx.textAlign = content.align;
|
|
2849
3326
|
ctx.letterSpacing = `${content.tracking}em`;
|
|
2850
|
-
const maxWidth = w -
|
|
2851
|
-
const x = content.align === "left" ?
|
|
3327
|
+
const maxWidth = w - pad2 * 2;
|
|
3328
|
+
const x = content.align === "left" ? pad2 : content.align === "right" ? w - pad2 : w / 2;
|
|
2852
3329
|
const lineStep = size * content.lineHeight;
|
|
2853
3330
|
const lines = wrapLines(ctx, content.text, maxWidth, font);
|
|
2854
3331
|
ctx.font = font;
|
|
2855
3332
|
ctx.letterSpacing = `${content.tracking}em`;
|
|
2856
3333
|
const block = lines.length * lineStep;
|
|
2857
|
-
let y = content.valign === "center" ? Math.max(
|
|
3334
|
+
let y = content.valign === "center" ? Math.max(pad2, (h - block) / 2) : pad2;
|
|
2858
3335
|
for (const line of lines) {
|
|
2859
|
-
if (y > h -
|
|
3336
|
+
if (y > h - pad2) break;
|
|
2860
3337
|
ctx.fillText(line, x, y);
|
|
2861
3338
|
y += lineStep;
|
|
2862
3339
|
}
|
|
@@ -2927,77 +3404,6 @@ var THREE7 = __toESM(require("three"), 1);
|
|
|
2927
3404
|
// src/deformers/registry.ts
|
|
2928
3405
|
var import_zod21 = require("zod");
|
|
2929
3406
|
|
|
2930
|
-
// src/deformers/roll.ts
|
|
2931
|
-
var import_zod15 = require("zod");
|
|
2932
|
-
var rollOptionsSchema = import_zod15.z.object({
|
|
2933
|
-
/** Direction of rolling in the sheet plane, degrees. 0 = +x, 90 = +y. */
|
|
2934
|
-
angle: import_zod15.z.number().min(-360).max(360).default(90),
|
|
2935
|
-
/** Signed distance (along the roll direction, from sheet center) where the roll begins. */
|
|
2936
|
-
boundary: import_zod15.z.number().min(-20).max(20).default(0),
|
|
2937
|
-
/** Cylinder radius — sharpness of the roll. */
|
|
2938
|
-
radius: import_zod15.z.number().min(0.01).max(2).default(0.12),
|
|
2939
|
-
/** Radius growth per radian so multi-turn rolls spiral instead of z-fighting. */
|
|
2940
|
-
spiral: import_zod15.z.number().min(0).max(0.2).default(0.015)
|
|
2941
|
-
});
|
|
2942
|
-
var DEG3 = Math.PI / 180;
|
|
2943
|
-
var roll = {
|
|
2944
|
-
id: "roll",
|
|
2945
|
-
label: "Roll",
|
|
2946
|
-
defaults: rollOptionsSchema.parse({}),
|
|
2947
|
-
optionsSchema: rollOptionsSchema,
|
|
2948
|
-
geometry: {
|
|
2949
|
-
minSegments: 48,
|
|
2950
|
-
// The winding radius is the tightest curvature on the sheet — `spiral`
|
|
2951
|
-
// only grows it as the roll winds outward, so the first turn is the one
|
|
2952
|
-
// that sets the density.
|
|
2953
|
-
autoSegments: (o, sheet2) => segmentsForArc(spanAlong(sheet2, o.angle), o.radius),
|
|
2954
|
-
axis: (o) => o.angle
|
|
2955
|
-
},
|
|
2956
|
-
displace(out, _uv, o) {
|
|
2957
|
-
const dirX = Math.cos(o.angle * DEG3);
|
|
2958
|
-
const dirY = Math.sin(o.angle * DEG3);
|
|
2959
|
-
const d = out.x * dirX + out.y * dirY;
|
|
2960
|
-
const s = d - o.boundary;
|
|
2961
|
-
if (s <= 0) return;
|
|
2962
|
-
const theta = s / o.radius;
|
|
2963
|
-
const r = o.radius + o.spiral * theta;
|
|
2964
|
-
const sin = Math.sin(theta);
|
|
2965
|
-
const cos = Math.cos(theta);
|
|
2966
|
-
const newD = o.boundary + (r - out.z) * sin;
|
|
2967
|
-
const newZ = r * (1 - cos) + out.z * cos;
|
|
2968
|
-
out.x += dirX * (newD - d);
|
|
2969
|
-
out.y += dirY * (newD - d);
|
|
2970
|
-
out.z = newZ;
|
|
2971
|
-
},
|
|
2972
|
-
glsl: {
|
|
2973
|
-
chunk: (
|
|
2974
|
-
/* glsl */
|
|
2975
|
-
`
|
|
2976
|
-
void FN(inout vec3 p, vec2 uv, float t) {
|
|
2977
|
-
vec2 dir = vec2(cos(U_angle), sin(U_angle));
|
|
2978
|
-
float d = dot(p.xy, dir);
|
|
2979
|
-
float s = d - U_boundary;
|
|
2980
|
-
if (s <= 0.0) return;
|
|
2981
|
-
float theta = s / U_radius;
|
|
2982
|
-
float r = U_radius + U_spiral * theta;
|
|
2983
|
-
float sn = sin(theta);
|
|
2984
|
-
float cs = cos(theta);
|
|
2985
|
-
float newD = U_boundary + (r - p.z) * sn;
|
|
2986
|
-
float newZ = r * (1.0 - cs) + p.z * cs;
|
|
2987
|
-
p.xy += dir * (newD - d);
|
|
2988
|
-
p.z = newZ;
|
|
2989
|
-
}
|
|
2990
|
-
`
|
|
2991
|
-
),
|
|
2992
|
-
uniforms: (o) => ({
|
|
2993
|
-
angle: o.angle * DEG3,
|
|
2994
|
-
boundary: o.boundary,
|
|
2995
|
-
radius: o.radius,
|
|
2996
|
-
spiral: o.spiral
|
|
2997
|
-
})
|
|
2998
|
-
}
|
|
2999
|
-
};
|
|
3000
|
-
|
|
3001
3407
|
// src/deformers/bend.ts
|
|
3002
3408
|
var import_zod16 = require("zod");
|
|
3003
3409
|
var bendOptionsSchema = import_zod16.z.object({
|
|
@@ -3179,7 +3585,7 @@ var waveOptionsSchema = import_zod18.z.object({
|
|
|
3179
3585
|
pinnedEdge: import_zod18.z.enum(["none", "top", "bottom", "left", "right"]).default("none")
|
|
3180
3586
|
});
|
|
3181
3587
|
var DEG6 = Math.PI / 180;
|
|
3182
|
-
var
|
|
3588
|
+
var TAU2 = Math.PI * 2;
|
|
3183
3589
|
var wave = {
|
|
3184
3590
|
id: "wave",
|
|
3185
3591
|
label: "Wave",
|
|
@@ -3206,7 +3612,7 @@ var wave = {
|
|
|
3206
3612
|
const dirX = Math.cos(o.angle * DEG6);
|
|
3207
3613
|
const dirY = Math.sin(o.angle * DEG6);
|
|
3208
3614
|
const d = out.x * dirX + out.y * dirY;
|
|
3209
|
-
const phase = (d / o.wavelength - o.speed * ctx.t) *
|
|
3615
|
+
const phase = (d / o.wavelength - o.speed * ctx.t) * TAU2;
|
|
3210
3616
|
let env = 1;
|
|
3211
3617
|
if (o.pinnedEdge === "top") env = 1 - uv.y;
|
|
3212
3618
|
else if (o.pinnedEdge === "bottom") env = uv.y;
|
|
@@ -3262,7 +3668,7 @@ var drapeOptionsSchema = import_zod19.z.object({
|
|
|
3262
3668
|
gather: import_zod19.z.number().min(0).max(1).default(0.5),
|
|
3263
3669
|
pinnedEdge: import_zod19.z.enum(["top", "bottom"]).default("top")
|
|
3264
3670
|
});
|
|
3265
|
-
var
|
|
3671
|
+
var TAU3 = Math.PI * 2;
|
|
3266
3672
|
var drape = {
|
|
3267
3673
|
id: "drape",
|
|
3268
3674
|
label: "Drape",
|
|
@@ -3290,7 +3696,7 @@ var drape = {
|
|
|
3290
3696
|
if (o.amplitude === 0) return;
|
|
3291
3697
|
const drop = o.pinnedEdge === "top" ? 1 - uv.y : uv.y;
|
|
3292
3698
|
const depth = drop ** o.falloff;
|
|
3293
|
-
const u = uv.x *
|
|
3699
|
+
const u = uv.x * TAU3 * o.folds;
|
|
3294
3700
|
const fold2 = Math.sin(u) + o.irregular * 0.6 * Math.sin(u * 1.7 + 2.1);
|
|
3295
3701
|
out.z += o.amplitude * depth * fold2;
|
|
3296
3702
|
const pinch = o.gather * depth * Math.min(o.amplitude * o.folds * 0.8, 0.6);
|
|
@@ -3564,6 +3970,211 @@ function take(out, deformer, options, sheet2, demand) {
|
|
|
3564
3970
|
if (y > out[1]) out[1] = y;
|
|
3565
3971
|
}
|
|
3566
3972
|
|
|
3973
|
+
// src/deformers/memory.ts
|
|
3974
|
+
var MAX_SET = 0.2;
|
|
3975
|
+
var CREASE_RADIUS = 0.03;
|
|
3976
|
+
var CREASE_MIN_GROWTH = 45;
|
|
3977
|
+
var CREASE_DRIFT = 0.02;
|
|
3978
|
+
var CREASE_DRIFT_ANGLE = 2;
|
|
3979
|
+
var MIN_DEPTH = 1;
|
|
3980
|
+
var MAX_CREASES = 4;
|
|
3981
|
+
function canonicalLine(angle, offset) {
|
|
3982
|
+
let a = (angle % 360 + 360) % 360;
|
|
3983
|
+
let o = offset;
|
|
3984
|
+
if (a >= 180) {
|
|
3985
|
+
a -= 180;
|
|
3986
|
+
o = -o;
|
|
3987
|
+
}
|
|
3988
|
+
return [a, o];
|
|
3989
|
+
}
|
|
3990
|
+
function sameLine(a, b) {
|
|
3991
|
+
const [aa, ao] = canonicalLine(a.angle, a.offset);
|
|
3992
|
+
const [ba, bo] = canonicalLine(b.angle, b.offset);
|
|
3993
|
+
const d = Math.abs(aa - ba);
|
|
3994
|
+
const angleClose = Math.min(d, 180 - d) <= CREASE_DRIFT_ANGLE;
|
|
3995
|
+
return angleClose && Math.abs(ao - bo) <= CREASE_DRIFT;
|
|
3996
|
+
}
|
|
3997
|
+
function applyMemory(stack, creases) {
|
|
3998
|
+
if (creases.length === 0) return stack;
|
|
3999
|
+
let out = stack;
|
|
4000
|
+
const loose = [];
|
|
4001
|
+
for (const crease of creases) {
|
|
4002
|
+
if (Math.abs(crease.depth) < MIN_DEPTH) continue;
|
|
4003
|
+
const index = out.findIndex(
|
|
4004
|
+
(i) => i.type === "fold" && i.enabled !== false && sameLine(i.options, crease)
|
|
4005
|
+
);
|
|
4006
|
+
if (index === -1) {
|
|
4007
|
+
loose.push(crease);
|
|
4008
|
+
continue;
|
|
4009
|
+
}
|
|
4010
|
+
const live = out[index];
|
|
4011
|
+
const options = live.options;
|
|
4012
|
+
if (Math.abs(options.foldAngle) >= Math.abs(crease.depth)) continue;
|
|
4013
|
+
if (out === stack) out = [...stack];
|
|
4014
|
+
out[index] = { ...live, options: { ...options, foldAngle: crease.depth } };
|
|
4015
|
+
}
|
|
4016
|
+
if (loose.length === 0) return out;
|
|
4017
|
+
return [...loose.map(toFold), ...out];
|
|
4018
|
+
}
|
|
4019
|
+
function toFold(crease) {
|
|
4020
|
+
return {
|
|
4021
|
+
type: "fold",
|
|
4022
|
+
options: {
|
|
4023
|
+
angle: crease.angle,
|
|
4024
|
+
offset: crease.offset,
|
|
4025
|
+
foldAngle: crease.depth,
|
|
4026
|
+
radius: CREASE_RADIUS
|
|
4027
|
+
}
|
|
4028
|
+
};
|
|
4029
|
+
}
|
|
4030
|
+
var CreaseTracker = class {
|
|
4031
|
+
slots = /* @__PURE__ */ new Map();
|
|
4032
|
+
recorded = [];
|
|
4033
|
+
authored = [];
|
|
4034
|
+
/** What `observe` last handed out, so an echo can be told from an edit. */
|
|
4035
|
+
lastReported = [];
|
|
4036
|
+
constructor(authored = []) {
|
|
4037
|
+
this.authored = authored;
|
|
4038
|
+
}
|
|
4039
|
+
/**
|
|
4040
|
+
* Forget how the paper got here without forgetting the creases.
|
|
4041
|
+
*
|
|
4042
|
+
* Called when the stack is replaced wholesale (a new behavior, a new
|
|
4043
|
+
* sheet): the slots describe folds that no longer exist, but a crease is a
|
|
4044
|
+
* property of the paper and survives being put down and picked up.
|
|
4045
|
+
*/
|
|
4046
|
+
reset(authored = this.creases) {
|
|
4047
|
+
this.slots.clear();
|
|
4048
|
+
this.recorded = [];
|
|
4049
|
+
this.authored = authored;
|
|
4050
|
+
}
|
|
4051
|
+
/**
|
|
4052
|
+
* Take on a crease set that came from outside, and work out which kind of
|
|
4053
|
+
* outside it was — because the two kinds want opposite things.
|
|
4054
|
+
*
|
|
4055
|
+
* It is USUALLY this tracker's own recording coming back, a frame or two
|
|
4056
|
+
* after `onCrease` handed it to the host. Then the slots must survive: the
|
|
4057
|
+
* fold that made the crease is very likely still closing, and resetting
|
|
4058
|
+
* its peak on the host's echo would stall the crease halfway into the
|
|
4059
|
+
* fold that was making it.
|
|
4060
|
+
*
|
|
4061
|
+
* But it can also be somebody EDITING the paper — a depth dragged down in
|
|
4062
|
+
* a panel, a shared link opened, a state's overrides settling. Then the
|
|
4063
|
+
* slots are the wrong story to keep. `merge` takes the deeper of two
|
|
4064
|
+
* creases on a line, so a recording of 20° would quietly outvote a human
|
|
4065
|
+
* asking for 5° and the slider would appear not to work; and it would go
|
|
4066
|
+
* on outvoting it, because the fold that recorded the 20 is still sitting
|
|
4067
|
+
* in a slot. An edit means the paper is what it is now, and the next
|
|
4068
|
+
* crease has to be earned by folding it again.
|
|
4069
|
+
*
|
|
4070
|
+
* The two are told apart by what we last reported. Anything else is an
|
|
4071
|
+
* edit, which is the safe way round: mistaking an echo for an edit costs
|
|
4072
|
+
* a crease that gets re-recorded on the next fold, while mistaking an edit
|
|
4073
|
+
* for an echo costs a control that does not work.
|
|
4074
|
+
*/
|
|
4075
|
+
adopt(creases) {
|
|
4076
|
+
if (!same(creases, this.lastReported)) this.slots.clear();
|
|
4077
|
+
this.authored = creases;
|
|
4078
|
+
this.recorded = [];
|
|
4079
|
+
}
|
|
4080
|
+
/** Everything the sheet currently carries, authored and recorded merged. */
|
|
4081
|
+
get creases() {
|
|
4082
|
+
return merge(this.authored, this.recorded);
|
|
4083
|
+
}
|
|
4084
|
+
/**
|
|
4085
|
+
* Take one frame's reading. Returns true when the crease set changed by
|
|
4086
|
+
* enough to be worth telling anyone about.
|
|
4087
|
+
*/
|
|
4088
|
+
observe(stack, set) {
|
|
4089
|
+
if (set <= 0) return false;
|
|
4090
|
+
for (let i = 0; i < stack.length; i++) {
|
|
4091
|
+
const instance = stack[i];
|
|
4092
|
+
if (instance.type !== "fold" || instance.enabled === false) continue;
|
|
4093
|
+
const o = instance.options;
|
|
4094
|
+
const magnitude = Math.abs(o.foldAngle);
|
|
4095
|
+
const slot = this.slots.get(i);
|
|
4096
|
+
if (!slot || Math.abs(slot.angle - o.angle) > CREASE_DRIFT_ANGLE || Math.abs(slot.offset - o.offset) > CREASE_DRIFT) {
|
|
4097
|
+
this.slots.set(i, {
|
|
4098
|
+
angle: o.angle,
|
|
4099
|
+
offset: o.offset,
|
|
4100
|
+
trough: magnitude,
|
|
4101
|
+
bestGrowth: 0,
|
|
4102
|
+
bestPeak: magnitude,
|
|
4103
|
+
sign: o.foldAngle < 0 ? -1 : 1
|
|
4104
|
+
});
|
|
4105
|
+
continue;
|
|
4106
|
+
}
|
|
4107
|
+
if (magnitude < slot.trough) slot.trough = magnitude;
|
|
4108
|
+
const growth = magnitude - slot.trough;
|
|
4109
|
+
if (growth > slot.bestGrowth) {
|
|
4110
|
+
slot.bestGrowth = growth;
|
|
4111
|
+
slot.bestPeak = magnitude;
|
|
4112
|
+
slot.sign = o.foldAngle < 0 ? -1 : 1;
|
|
4113
|
+
}
|
|
4114
|
+
}
|
|
4115
|
+
const next = [];
|
|
4116
|
+
for (const slot of this.slots.values()) {
|
|
4117
|
+
if (slot.bestGrowth < CREASE_MIN_GROWTH) continue;
|
|
4118
|
+
const depth = slot.sign * slot.bestPeak * set * MAX_SET;
|
|
4119
|
+
if (Math.abs(depth) < MIN_DEPTH) continue;
|
|
4120
|
+
next.push({ angle: slot.angle, offset: slot.offset, depth });
|
|
4121
|
+
}
|
|
4122
|
+
if (same(next, this.recorded)) return false;
|
|
4123
|
+
this.recorded = next;
|
|
4124
|
+
this.lastReported = this.creases;
|
|
4125
|
+
return true;
|
|
4126
|
+
}
|
|
4127
|
+
};
|
|
4128
|
+
function merge(authored, recorded) {
|
|
4129
|
+
const out = [];
|
|
4130
|
+
for (const crease of [...recorded, ...authored]) {
|
|
4131
|
+
const existing = out.find((c) => sameLine(c, crease));
|
|
4132
|
+
if (!existing) {
|
|
4133
|
+
out.push({ ...crease });
|
|
4134
|
+
continue;
|
|
4135
|
+
}
|
|
4136
|
+
if (Math.abs(crease.depth) > Math.abs(existing.depth)) existing.depth = crease.depth;
|
|
4137
|
+
}
|
|
4138
|
+
if (out.length > MAX_CREASES) {
|
|
4139
|
+
out.sort((a, b) => Math.abs(b.depth) - Math.abs(a.depth));
|
|
4140
|
+
out.length = MAX_CREASES;
|
|
4141
|
+
}
|
|
4142
|
+
return out;
|
|
4143
|
+
}
|
|
4144
|
+
function same(a, b) {
|
|
4145
|
+
if (a.length !== b.length) return false;
|
|
4146
|
+
return a.every((crease, i) => {
|
|
4147
|
+
const other = b[i];
|
|
4148
|
+
return sameLine(crease, other) && Math.abs(crease.depth - other.depth) < 0.5;
|
|
4149
|
+
});
|
|
4150
|
+
}
|
|
4151
|
+
|
|
4152
|
+
// src/surface/creases.ts
|
|
4153
|
+
var MAX_SHADED = 4;
|
|
4154
|
+
function resolveCreases(surface, creases, sheet2) {
|
|
4155
|
+
const out = [];
|
|
4156
|
+
const lines = surface.creaseLines;
|
|
4157
|
+
if (lines) {
|
|
4158
|
+
const angle = lines.angle + 90;
|
|
4159
|
+
const span = spanAlong(sheet2, angle);
|
|
4160
|
+
for (const position of lines.positions) {
|
|
4161
|
+
out.push({ angle, offset: (position - 0.5) * span, strength: lines.strength });
|
|
4162
|
+
}
|
|
4163
|
+
}
|
|
4164
|
+
for (const crease of creases) {
|
|
4165
|
+
out.push(creaseShading(crease));
|
|
4166
|
+
}
|
|
4167
|
+
if (out.length > MAX_SHADED) out.length = MAX_SHADED;
|
|
4168
|
+
return out;
|
|
4169
|
+
}
|
|
4170
|
+
function creaseShading(crease) {
|
|
4171
|
+
return {
|
|
4172
|
+
angle: crease.angle,
|
|
4173
|
+
offset: crease.offset,
|
|
4174
|
+
strength: Math.max(-1, Math.min(1, -crease.depth / (90 * MAX_SET)))
|
|
4175
|
+
};
|
|
4176
|
+
}
|
|
4177
|
+
|
|
3567
4178
|
// src/behaviors/registry.ts
|
|
3568
4179
|
var registry2 = /* @__PURE__ */ new Map();
|
|
3569
4180
|
function registerBehavior(behavior) {
|
|
@@ -3653,12 +4264,22 @@ function getIdlePreset(name) {
|
|
|
3653
4264
|
// src/physics/cloth.ts
|
|
3654
4265
|
var FIXED_DT = 1 / 120;
|
|
3655
4266
|
var SOLVER_ITERATIONS = 5;
|
|
4267
|
+
var AERO_TURBULENCE = 0.15;
|
|
4268
|
+
var GRAB_RADIUS = 0.05;
|
|
3656
4269
|
var SLEEP_EPSILON = 1e-6;
|
|
3657
4270
|
var SLEEP_FRAMES = 45;
|
|
4271
|
+
function grabFalloff(distance, radius) {
|
|
4272
|
+
const t = Math.min(1, Math.max(0, distance / radius));
|
|
4273
|
+
const s = 1 - t;
|
|
4274
|
+
return s * s * (3 - 2 * s);
|
|
4275
|
+
}
|
|
3658
4276
|
var ClothSim = class {
|
|
3659
4277
|
cols;
|
|
3660
4278
|
rows;
|
|
3661
4279
|
count;
|
|
4280
|
+
/** The sheet this grid was laid out on. Read by {@link adopt}. */
|
|
4281
|
+
width;
|
|
4282
|
+
height;
|
|
3662
4283
|
positions;
|
|
3663
4284
|
prev;
|
|
3664
4285
|
pinned;
|
|
@@ -3669,17 +4290,47 @@ var ClothSim = class {
|
|
|
3669
4290
|
accumulator = 0;
|
|
3670
4291
|
stillFrames = 0;
|
|
3671
4292
|
grabbedIndex = -1;
|
|
4293
|
+
/**
|
|
4294
|
+
* A normal per particle, refreshed once a frame.
|
|
4295
|
+
*
|
|
4296
|
+
* The sim needs these for one thing only — how much wind each part of the
|
|
4297
|
+
* sheet is actually catching — and it needs its OWN rather than the mesh's,
|
|
4298
|
+
* because the mesh's are computed after the fact by the adapter and a
|
|
4299
|
+
* deformer may be running over the top of them by then. Once a frame rather
|
|
4300
|
+
* than once a substep: paper does not turn far in eight milliseconds, and
|
|
4301
|
+
* this is the only part of the step that is not a constraint solve.
|
|
4302
|
+
*/
|
|
4303
|
+
normals;
|
|
4304
|
+
/**
|
|
4305
|
+
* How hard each particle is held, 0..1. Zero for all but the handful under
|
|
4306
|
+
* a hand, so it doubles as the inverse mass the constraint solver wants:
|
|
4307
|
+
* a particle is as immovable as it is held.
|
|
4308
|
+
*/
|
|
4309
|
+
grabWeights;
|
|
4310
|
+
/** Just the held ones, so nothing iterates the whole sheet to find six. */
|
|
4311
|
+
grabbed = [];
|
|
4312
|
+
/** Each held particle's offset from the one under the cursor, at grab time. */
|
|
4313
|
+
grabOffsets = new Float32Array(0);
|
|
4314
|
+
/** Where the hand was at the start of the last step, and how fast it moved. */
|
|
4315
|
+
grabAt = new Float32Array(3);
|
|
4316
|
+
grabWas = new Float32Array(3);
|
|
4317
|
+
grabVelocity = new Float32Array(3);
|
|
3672
4318
|
/** True when the sim has settled and steps are skipped. */
|
|
3673
4319
|
asleep = false;
|
|
3674
4320
|
constructor(cols, rows, width, height, pins, params) {
|
|
3675
4321
|
this.cols = cols;
|
|
3676
4322
|
this.rows = rows;
|
|
3677
4323
|
this.count = cols * rows;
|
|
4324
|
+
this.width = width;
|
|
4325
|
+
this.height = height;
|
|
3678
4326
|
this.params = { ...params };
|
|
3679
4327
|
this.positions = new Float32Array(this.count * 3);
|
|
3680
4328
|
this.prev = new Float32Array(this.count * 3);
|
|
3681
4329
|
this.pinned = new Uint8Array(this.count);
|
|
3682
4330
|
this.pinTargets = new Float32Array(this.count * 3);
|
|
4331
|
+
this.grabWeights = new Float32Array(this.count);
|
|
4332
|
+
this.normals = new Float32Array(this.count * 3);
|
|
4333
|
+
for (let i = 0; i < this.count; i++) this.normals[i * 3 + 2] = 1;
|
|
3683
4334
|
for (let r = 0; r < rows; r++) {
|
|
3684
4335
|
for (let c = 0; c < cols; c++) {
|
|
3685
4336
|
const i3 = (r * cols + c) * 3;
|
|
@@ -3719,6 +4370,54 @@ var ClothSim = class {
|
|
|
3719
4370
|
}
|
|
3720
4371
|
if (pins === "corner") pin(0, 0);
|
|
3721
4372
|
}
|
|
4373
|
+
/**
|
|
4374
|
+
* Carry a previous sim's drape across a rebuild.
|
|
4375
|
+
*
|
|
4376
|
+
* Sheet dimensions are a GEOMETRY dependency, so changing them builds a new
|
|
4377
|
+
* mesh and a new sim, and a new sim starts flat — which means a draped sheet
|
|
4378
|
+
* snaps rigid the instant it is resized. Nothing about the physics requires
|
|
4379
|
+
* that; it is only that nobody had carried the state over.
|
|
4380
|
+
*
|
|
4381
|
+
* What carries is the FREE particles, scaled by how much the sheet grew:
|
|
4382
|
+
* the constraints' rest lengths are laid out afresh at the new size, so
|
|
4383
|
+
* scaling the drape by the same ratio leaves every constraint exactly as
|
|
4384
|
+
* violated as it was and the sim simply continues. Pinned particles keep
|
|
4385
|
+
* the new layout's own rest positions instead — a pin holds a CORNER, and
|
|
4386
|
+
* the corner is where the resized sheet says it is.
|
|
4387
|
+
*
|
|
4388
|
+
* Refused in one case only: a different grid, because there is no
|
|
4389
|
+
* correspondence between the two sets of particles and the nearest thing to
|
|
4390
|
+
* one would be a guess. Everything else carries — a resize, a change of
|
|
4391
|
+
* pins, a deformer appearing on top. The sheet's state belongs to the sheet,
|
|
4392
|
+
* and none of those is a reason to have never fallen.
|
|
4393
|
+
*
|
|
4394
|
+
* That matters most for the one that is not a resize at all: a shape
|
|
4395
|
+
* arriving over a simulation rebuilds the mesh (the stack has its own
|
|
4396
|
+
* opinion about tessellation) without touching a single thing the physics
|
|
4397
|
+
* knows about. Resetting there would mean the sheet snapped flat the instant
|
|
4398
|
+
* you tried to fold the sheet you were holding, which is the whole point of
|
|
4399
|
+
* being able to.
|
|
4400
|
+
*
|
|
4401
|
+
* Returns whether it took.
|
|
4402
|
+
*/
|
|
4403
|
+
adopt(previous) {
|
|
4404
|
+
if (!previous || previous.cols !== this.cols || previous.rows !== this.rows) return false;
|
|
4405
|
+
const sx = previous.width > 0 ? this.width / previous.width : 1;
|
|
4406
|
+
const sy = previous.height > 0 ? this.height / previous.height : 1;
|
|
4407
|
+
const sz = (sx + sy) / 2;
|
|
4408
|
+
for (let i = 0; i < this.count; i++) {
|
|
4409
|
+
if (this.pinned[i]) continue;
|
|
4410
|
+
const i3 = i * 3;
|
|
4411
|
+
this.positions[i3] = previous.positions[i3] * sx;
|
|
4412
|
+
this.positions[i3 + 1] = previous.positions[i3 + 1] * sy;
|
|
4413
|
+
this.positions[i3 + 2] = previous.positions[i3 + 2] * sz;
|
|
4414
|
+
this.prev[i3] = previous.prev[i3] * sx;
|
|
4415
|
+
this.prev[i3 + 1] = previous.prev[i3 + 1] * sy;
|
|
4416
|
+
this.prev[i3 + 2] = previous.prev[i3 + 2] * sz;
|
|
4417
|
+
}
|
|
4418
|
+
this.wake();
|
|
4419
|
+
return true;
|
|
4420
|
+
}
|
|
3722
4421
|
setParams(params) {
|
|
3723
4422
|
let changed = false;
|
|
3724
4423
|
for (const key of ["stiffness", "gravity", "wind", "floor"]) {
|
|
@@ -3734,6 +4433,69 @@ var ClothSim = class {
|
|
|
3734
4433
|
this.asleep = false;
|
|
3735
4434
|
this.stillFrames = 0;
|
|
3736
4435
|
}
|
|
4436
|
+
/**
|
|
4437
|
+
* Take hold of the sheet at one particle.
|
|
4438
|
+
*
|
|
4439
|
+
* Separate from {@link grabNearest} because the particle a hand grabbed is
|
|
4440
|
+
* not always the particle nearest the point it touched: with a deformer
|
|
4441
|
+
* running over the simulation, what the pointer hit was a RENDERED vertex,
|
|
4442
|
+
* and the vertex it hit is the particle of the same index — the stack maps
|
|
4443
|
+
* a point to a point and never reorders them.
|
|
4444
|
+
*
|
|
4445
|
+
* What is taken hold of is a PATCH around that particle, not the particle
|
|
4446
|
+
* alone — see {@link GRAB_RADIUS}. The patch is measured across the GRID
|
|
4447
|
+
* rather than through space, because a hand holds a piece of the sheet and
|
|
4448
|
+
* keeps holding the same piece: measured through space, a fold that brought
|
|
4449
|
+
* a far corner near the fingers would silently add it to the grip.
|
|
4450
|
+
*/
|
|
4451
|
+
grab(index) {
|
|
4452
|
+
this.grabbedIndex = index >= 0 && index < this.count ? index : -1;
|
|
4453
|
+
this.grabWeights.fill(0);
|
|
4454
|
+
this.grabbed = [];
|
|
4455
|
+
if (this.grabbedIndex < 0) {
|
|
4456
|
+
this.grabOffsets = new Float32Array(0);
|
|
4457
|
+
this.wake();
|
|
4458
|
+
return -1;
|
|
4459
|
+
}
|
|
4460
|
+
const cellX = this.cols > 1 ? this.width / (this.cols - 1) : this.width;
|
|
4461
|
+
const cellY = this.rows > 1 ? this.height / (this.rows - 1) : this.height;
|
|
4462
|
+
const cell = Math.max(cellX, cellY, 1e-6);
|
|
4463
|
+
const radius = Math.min(
|
|
4464
|
+
Math.max(GRAB_RADIUS, cell * 1.5),
|
|
4465
|
+
Math.max(cell, Math.min(this.width, this.height) * 0.2)
|
|
4466
|
+
);
|
|
4467
|
+
const centreRow = Math.floor(this.grabbedIndex / this.cols);
|
|
4468
|
+
const centreCol = this.grabbedIndex % this.cols;
|
|
4469
|
+
const spanRows = Math.ceil(radius / cellY);
|
|
4470
|
+
const spanCols = Math.ceil(radius / cellX);
|
|
4471
|
+
const offsets = [];
|
|
4472
|
+
const anchor = this.grabbedIndex * 3;
|
|
4473
|
+
for (let r = centreRow - spanRows; r <= centreRow + spanRows; r++) {
|
|
4474
|
+
if (r < 0 || r >= this.rows) continue;
|
|
4475
|
+
for (let c = centreCol - spanCols; c <= centreCol + spanCols; c++) {
|
|
4476
|
+
if (c < 0 || c >= this.cols) continue;
|
|
4477
|
+
const i = r * this.cols + c;
|
|
4478
|
+
if (this.pinned[i]) continue;
|
|
4479
|
+
const distance = Math.hypot((c - centreCol) * cellX, (r - centreRow) * cellY);
|
|
4480
|
+
const weight = i === this.grabbedIndex ? 1 : grabFalloff(distance, radius);
|
|
4481
|
+
if (weight <= 0) continue;
|
|
4482
|
+
this.grabWeights[i] = weight;
|
|
4483
|
+
this.grabbed.push(i);
|
|
4484
|
+
const i3 = i * 3;
|
|
4485
|
+
offsets.push(
|
|
4486
|
+
this.positions[i3] - this.positions[anchor],
|
|
4487
|
+
this.positions[i3 + 1] - this.positions[anchor + 1],
|
|
4488
|
+
this.positions[i3 + 2] - this.positions[anchor + 2]
|
|
4489
|
+
);
|
|
4490
|
+
}
|
|
4491
|
+
}
|
|
4492
|
+
this.grabOffsets = Float32Array.from(offsets);
|
|
4493
|
+
this.grabAt.set(this.positions.subarray(anchor, anchor + 3));
|
|
4494
|
+
this.grabWas.set(this.grabAt);
|
|
4495
|
+
this.grabVelocity.fill(0);
|
|
4496
|
+
this.wake();
|
|
4497
|
+
return this.grabbedIndex;
|
|
4498
|
+
}
|
|
3737
4499
|
/** Nearest particle to a local-space point — the grab interface. */
|
|
3738
4500
|
grabNearest(x, y, z30) {
|
|
3739
4501
|
let best = -1;
|
|
@@ -3748,32 +4510,110 @@ var ClothSim = class {
|
|
|
3748
4510
|
best = i;
|
|
3749
4511
|
}
|
|
3750
4512
|
}
|
|
3751
|
-
this.
|
|
3752
|
-
|
|
3753
|
-
|
|
4513
|
+
return this.grab(best);
|
|
4514
|
+
}
|
|
4515
|
+
/** How hard one particle is being held, 0..1 — for tests and for the adapter. */
|
|
4516
|
+
grabWeightAt(index) {
|
|
4517
|
+
return this.grabWeights[index] ?? 0;
|
|
3754
4518
|
}
|
|
4519
|
+
/** Where the fingers are now. The patch follows, each particle by its weight. */
|
|
3755
4520
|
moveGrab(x, y, z30) {
|
|
3756
4521
|
if (this.grabbedIndex < 0) return;
|
|
3757
|
-
|
|
3758
|
-
this.
|
|
3759
|
-
this.
|
|
3760
|
-
this.positions[i3 + 2] = z30;
|
|
3761
|
-
this.prev[i3] = x;
|
|
3762
|
-
this.prev[i3 + 1] = y;
|
|
3763
|
-
this.prev[i3 + 2] = z30;
|
|
4522
|
+
this.grabAt[0] = x;
|
|
4523
|
+
this.grabAt[1] = y;
|
|
4524
|
+
this.grabAt[2] = z30;
|
|
3764
4525
|
this.wake();
|
|
3765
4526
|
}
|
|
4527
|
+
/**
|
|
4528
|
+
* Let go — and let go at SPEED.
|
|
4529
|
+
*
|
|
4530
|
+
* This used to drop the paper dead. Every held particle had its previous
|
|
4531
|
+
* position overwritten with its current one on the way past, and in a verlet
|
|
4532
|
+
* integrator the gap between those two IS the velocity, so a sheet whipped
|
|
4533
|
+
* across the frame and released came to a perfect standstill and then fell
|
|
4534
|
+
* straight down. Whatever you did with your hand, the paper had never heard
|
|
4535
|
+
* of it.
|
|
4536
|
+
*
|
|
4537
|
+
* The hand's velocity is measured per second (see {@link step}) and spent
|
|
4538
|
+
* here, converted into the one-substep gap the integrator reads it back out
|
|
4539
|
+
* of. Measured per second rather than per frame because a frame is not a
|
|
4540
|
+
* fixed length and a substep is: throwing the same sheet at the same speed
|
|
4541
|
+
* must not depend on what the frame rate happened to be.
|
|
4542
|
+
*/
|
|
3766
4543
|
release() {
|
|
4544
|
+
for (const i of this.grabbed) {
|
|
4545
|
+
const i3 = i * 3;
|
|
4546
|
+
const held = this.grabWeights[i];
|
|
4547
|
+
this.prev[i3] = this.positions[i3] - this.grabVelocity[0] * FIXED_DT * held;
|
|
4548
|
+
this.prev[i3 + 1] = this.positions[i3 + 1] - this.grabVelocity[1] * FIXED_DT * held;
|
|
4549
|
+
this.prev[i3 + 2] = this.positions[i3 + 2] - this.grabVelocity[2] * FIXED_DT * held;
|
|
4550
|
+
}
|
|
3767
4551
|
this.grabbedIndex = -1;
|
|
4552
|
+
this.grabbed = [];
|
|
4553
|
+
this.grabWeights.fill(0);
|
|
4554
|
+
this.grabVelocity.fill(0);
|
|
4555
|
+
this.wake();
|
|
3768
4556
|
}
|
|
3769
4557
|
step(delta) {
|
|
3770
4558
|
if (this.asleep) return;
|
|
4559
|
+
if (this.grabbedIndex >= 0 && delta > 0) {
|
|
4560
|
+
for (let axis = 0; axis < 3; axis++) {
|
|
4561
|
+
this.grabVelocity[axis] = (this.grabAt[axis] - this.grabWas[axis]) / delta;
|
|
4562
|
+
}
|
|
4563
|
+
this.grabWas.set(this.grabAt);
|
|
4564
|
+
}
|
|
4565
|
+
this.updateNormals();
|
|
3771
4566
|
this.accumulator = Math.min(this.accumulator + delta, FIXED_DT * 4);
|
|
3772
4567
|
while (this.accumulator >= FIXED_DT) {
|
|
3773
4568
|
this.substep(FIXED_DT);
|
|
3774
4569
|
this.accumulator -= FIXED_DT;
|
|
3775
4570
|
}
|
|
3776
4571
|
}
|
|
4572
|
+
/**
|
|
4573
|
+
* Which way each part of the sheet is facing, by central difference across
|
|
4574
|
+
* the grid.
|
|
4575
|
+
*
|
|
4576
|
+
* Clamped at the edges rather than wrapped or skipped: an edge particle
|
|
4577
|
+
* takes the one-sided difference, which is the same normal its neighbour
|
|
4578
|
+
* has, and an edge with no normal is an edge the wind cannot push.
|
|
4579
|
+
*
|
|
4580
|
+
* Which SIDE the normal points at is deliberately not worried about. The
|
|
4581
|
+
* force below is `n (n·v)`, a quadratic form, and that is unchanged by
|
|
4582
|
+
* flipping n — which is exactly right for paper, a surface with no front as
|
|
4583
|
+
* far as the air is concerned.
|
|
4584
|
+
*/
|
|
4585
|
+
updateNormals() {
|
|
4586
|
+
const p = this.positions;
|
|
4587
|
+
const n = this.normals;
|
|
4588
|
+
for (let r = 0; r < this.rows; r++) {
|
|
4589
|
+
for (let c = 0; c < this.cols; c++) {
|
|
4590
|
+
const left = (r * this.cols + (c > 0 ? c - 1 : c)) * 3;
|
|
4591
|
+
const right = (r * this.cols + (c < this.cols - 1 ? c + 1 : c)) * 3;
|
|
4592
|
+
const up = ((r > 0 ? r - 1 : r) * this.cols + c) * 3;
|
|
4593
|
+
const down = ((r < this.rows - 1 ? r + 1 : r) * this.cols + c) * 3;
|
|
4594
|
+
const ux = p[right] - p[left];
|
|
4595
|
+
const uy = p[right + 1] - p[left + 1];
|
|
4596
|
+
const uz = p[right + 2] - p[left + 2];
|
|
4597
|
+
const vx = p[down] - p[up];
|
|
4598
|
+
const vy = p[down + 1] - p[up + 1];
|
|
4599
|
+
const vz = p[down + 2] - p[up + 2];
|
|
4600
|
+
const nx = vy * uz - vz * uy;
|
|
4601
|
+
const ny = vz * ux - vx * uz;
|
|
4602
|
+
const nz = vx * uy - vy * ux;
|
|
4603
|
+
const length = Math.sqrt(nx * nx + ny * ny + nz * nz);
|
|
4604
|
+
const i3 = (r * this.cols + c) * 3;
|
|
4605
|
+
if (length > 1e-12) {
|
|
4606
|
+
n[i3] = nx / length;
|
|
4607
|
+
n[i3 + 1] = ny / length;
|
|
4608
|
+
n[i3 + 2] = nz / length;
|
|
4609
|
+
} else {
|
|
4610
|
+
n[i3] = 0;
|
|
4611
|
+
n[i3 + 1] = 0;
|
|
4612
|
+
n[i3 + 2] = 0;
|
|
4613
|
+
}
|
|
4614
|
+
}
|
|
4615
|
+
}
|
|
4616
|
+
}
|
|
3777
4617
|
substep(dt) {
|
|
3778
4618
|
const { gravity, wind, stiffness, floor } = this.params;
|
|
3779
4619
|
const p = this.positions;
|
|
@@ -3783,12 +4623,10 @@ var ClothSim = class {
|
|
|
3783
4623
|
let maxTravel = 0;
|
|
3784
4624
|
for (let i = 0; i < this.count; i++) {
|
|
3785
4625
|
const i3 = i * 3;
|
|
3786
|
-
if (this.pinned[i]
|
|
3787
|
-
|
|
3788
|
-
|
|
3789
|
-
|
|
3790
|
-
p[i3 + 2] = this.pinTargets[i3 + 2];
|
|
3791
|
-
}
|
|
4626
|
+
if (this.pinned[i]) {
|
|
4627
|
+
p[i3] = this.pinTargets[i3];
|
|
4628
|
+
p[i3 + 1] = this.pinTargets[i3 + 1];
|
|
4629
|
+
p[i3 + 2] = this.pinTargets[i3 + 2];
|
|
3792
4630
|
this.prev[i3] = p[i3];
|
|
3793
4631
|
this.prev[i3 + 1] = p[i3 + 1];
|
|
3794
4632
|
this.prev[i3 + 2] = p[i3 + 2];
|
|
@@ -3797,20 +4635,38 @@ var ClothSim = class {
|
|
|
3797
4635
|
const x = p[i3];
|
|
3798
4636
|
const y = p[i3 + 1];
|
|
3799
4637
|
const z30 = p[i3 + 2];
|
|
3800
|
-
const gust2 = wind * (0.55 + 0.45 * Math.sin(this.time * 1.7 + x * 2.1 + y * 1.3)) * 0.9;
|
|
3801
|
-
const ax = gust2 * 0.25;
|
|
3802
|
-
const az = gust2;
|
|
3803
4638
|
const vx = (x - this.prev[i3]) * damping;
|
|
3804
4639
|
const vy = (y - this.prev[i3 + 1]) * damping;
|
|
3805
4640
|
const vz = (z30 - this.prev[i3 + 2]) * damping;
|
|
4641
|
+
const gust2 = wind * (0.55 + 0.45 * Math.sin(this.time * 1.7 + x * 2.1 + y * 1.3)) * 0.9;
|
|
4642
|
+
const relX = gust2 * 0.25 - vx / dt;
|
|
4643
|
+
const relY = -vy / dt;
|
|
4644
|
+
const relZ = gust2 - vz / dt;
|
|
4645
|
+
const nx = this.normals[i3];
|
|
4646
|
+
const ny = this.normals[i3 + 1];
|
|
4647
|
+
const nz = this.normals[i3 + 2];
|
|
4648
|
+
const facing = nx * relX + ny * relY + nz * relZ;
|
|
4649
|
+
const ax = nx * facing * (1 - AERO_TURBULENCE) + gust2 * 0.25 * AERO_TURBULENCE;
|
|
4650
|
+
const ay = ny * facing * (1 - AERO_TURBULENCE);
|
|
4651
|
+
const az = nz * facing * (1 - AERO_TURBULENCE) + gust2 * AERO_TURBULENCE;
|
|
3806
4652
|
this.prev[i3] = x;
|
|
3807
4653
|
this.prev[i3 + 1] = y;
|
|
3808
4654
|
this.prev[i3 + 2] = z30;
|
|
3809
4655
|
p[i3] = x + vx + ax * dt2;
|
|
3810
|
-
p[i3 + 1] = y + vy - gravity * 3.2 * dt2;
|
|
4656
|
+
p[i3 + 1] = y + vy + (ay - gravity * 3.2) * dt2;
|
|
3811
4657
|
p[i3 + 2] = z30 + vz + az * dt2;
|
|
3812
4658
|
maxTravel = Math.max(maxTravel, vx * vx + vy * vy + vz * vz);
|
|
3813
4659
|
}
|
|
4660
|
+
for (let k = 0; k < this.grabbed.length; k++) {
|
|
4661
|
+
const i = this.grabbed[k];
|
|
4662
|
+
const i3 = i * 3;
|
|
4663
|
+
const k3 = k * 3;
|
|
4664
|
+
const held = this.grabWeights[i];
|
|
4665
|
+
for (let axis = 0; axis < 3; axis++) {
|
|
4666
|
+
const target = this.grabAt[axis] + this.grabOffsets[k3 + axis];
|
|
4667
|
+
p[i3 + axis] = p[i3 + axis] + (target - p[i3 + axis]) * held;
|
|
4668
|
+
}
|
|
4669
|
+
}
|
|
3814
4670
|
for (let iter = 0; iter < SOLVER_ITERATIONS; iter++) {
|
|
3815
4671
|
for (const c of this.constraints) {
|
|
3816
4672
|
const k = c.kind === 2 ? 0.25 + stiffness * 0.7 : c.kind === 1 ? 0.85 : 1;
|
|
@@ -3821,12 +4677,13 @@ var ClothSim = class {
|
|
|
3821
4677
|
const dz = p[b3 + 2] - p[a3 + 2];
|
|
3822
4678
|
const dist = Math.sqrt(dx * dx + dy * dy + dz * dz);
|
|
3823
4679
|
if (dist === 0) continue;
|
|
3824
|
-
const
|
|
3825
|
-
const
|
|
3826
|
-
const
|
|
3827
|
-
if (
|
|
3828
|
-
const
|
|
3829
|
-
const
|
|
4680
|
+
const ma = this.pinned[c.a] ? 0 : 1 - this.grabWeights[c.a];
|
|
4681
|
+
const mb = this.pinned[c.b] ? 0 : 1 - this.grabWeights[c.b];
|
|
4682
|
+
const total = ma + mb;
|
|
4683
|
+
if (total <= 0) continue;
|
|
4684
|
+
const diff = (dist - c.rest) / dist / total * k;
|
|
4685
|
+
const aw = ma;
|
|
4686
|
+
const bw = mb;
|
|
3830
4687
|
p[a3] = p[a3] + dx * diff * aw;
|
|
3831
4688
|
p[a3 + 1] = p[a3 + 1] + dy * diff * aw;
|
|
3832
4689
|
p[a3 + 2] = p[a3 + 2] + dz * diff * aw;
|
|
@@ -3853,6 +4710,720 @@ var ClothSim = class {
|
|
|
3853
4710
|
}
|
|
3854
4711
|
};
|
|
3855
4712
|
|
|
4713
|
+
// src/physics/strip.ts
|
|
4714
|
+
var FIXED_DT2 = 1 / 120;
|
|
4715
|
+
var SOLVER_ITERATIONS2 = 8;
|
|
4716
|
+
var SLEEP_EPSILON2 = 1e-7;
|
|
4717
|
+
var SLEEP_FRAMES2 = 45;
|
|
4718
|
+
var LOOSE2 = 0.055;
|
|
4719
|
+
var TIGHT2 = 0.012;
|
|
4720
|
+
var NODES_PER_PANEL = 16;
|
|
4721
|
+
var MAX_NODES = 440;
|
|
4722
|
+
var MIN_NODES = 8;
|
|
4723
|
+
var REFERENCE_SEGMENT = 0.0375;
|
|
4724
|
+
var TANGENT_SPAN = 0.09;
|
|
4725
|
+
var PULL_STIFFNESS = 0.35;
|
|
4726
|
+
var COLLIDING_ITERATIONS = 4;
|
|
4727
|
+
var COLLISION_RELAXATION = 0.35;
|
|
4728
|
+
function safeSpiralRadius(segment, thickness) {
|
|
4729
|
+
return thickness > 0 ? segment * segment / (4 * thickness) : 0;
|
|
4730
|
+
}
|
|
4731
|
+
function layerThickness2(tightness) {
|
|
4732
|
+
return LOOSE2 - Math.min(1, Math.max(0, tightness)) * (LOOSE2 - TIGHT2);
|
|
4733
|
+
}
|
|
4734
|
+
function stripNodeCount(length, perforation) {
|
|
4735
|
+
const seg = Math.max(perforation, 1e-3) / NODES_PER_PANEL;
|
|
4736
|
+
return Math.min(MAX_NODES, Math.max(MIN_NODES, Math.round(length / seg) + 1));
|
|
4737
|
+
}
|
|
4738
|
+
var StripSim = class {
|
|
4739
|
+
/** Chain length. Node 0 is the deepest wrap; node `count-1` is the free tip. */
|
|
4740
|
+
count;
|
|
4741
|
+
/** Rest spacing between neighbours, in world units. */
|
|
4742
|
+
segment;
|
|
4743
|
+
/** Node centreline, y/z interleaved. x is implicit: the strip never twists. */
|
|
4744
|
+
pos;
|
|
4745
|
+
prev;
|
|
4746
|
+
/** Arc distance from the tip, per node. */
|
|
4747
|
+
arc;
|
|
4748
|
+
/** 1 where a perforation hinges, and the sign of the crease it remembers. */
|
|
4749
|
+
perforated;
|
|
4750
|
+
/** Bend gain correction for this chain's node spacing. See {@link REFERENCE_SEGMENT}. */
|
|
4751
|
+
bendScale;
|
|
4752
|
+
/** Half-width, in nodes, of the window the drag tangent is measured over. */
|
|
4753
|
+
tangentNodes;
|
|
4754
|
+
halfWidth;
|
|
4755
|
+
totalLength;
|
|
4756
|
+
/**
|
|
4757
|
+
* Paper that can never be paid out, because its inner end is glued to the
|
|
4758
|
+
* tube — which is true of every roll you have ever used.
|
|
4759
|
+
*
|
|
4760
|
+
* Without it the roll pays out to nothing, `firstFreeIndex` reaches 0, and
|
|
4761
|
+
* every node the roll was made of becomes free paper and falls: the whole
|
|
4762
|
+
* roll drops off its holder and lands flat on the pile. One wrap held back
|
|
4763
|
+
* leaves a cylinder at the core radius on the holder, which is what an
|
|
4764
|
+
* empty roll looks like and the closest this library can get to drawing a
|
|
4765
|
+
* cardboard tube out of the one sheet it has.
|
|
4766
|
+
*/
|
|
4767
|
+
tubeStub;
|
|
4768
|
+
params;
|
|
4769
|
+
/** Paid-out length: how much paper is off the roll. */
|
|
4770
|
+
paid;
|
|
4771
|
+
/** Angular velocity of the roll, rad/s. The thing that coasts. */
|
|
4772
|
+
omega = 0;
|
|
4773
|
+
/**
|
|
4774
|
+
* Whether anything has turned the roll yet — a scroll delta or a hand.
|
|
4775
|
+
*
|
|
4776
|
+
* `tail` is the opening pose, and editing it should re-pose the roll; but
|
|
4777
|
+
* once the host has scrolled, `paid` is the simulation's own state and
|
|
4778
|
+
* re-posing would yank the paper back out of the pile.
|
|
4779
|
+
*/
|
|
4780
|
+
driven = false;
|
|
4781
|
+
lastScroll;
|
|
4782
|
+
primed = false;
|
|
4783
|
+
/** Node held by the pointer, or -1. Kinematic while held, like a wound node. */
|
|
4784
|
+
grabbed = -1;
|
|
4785
|
+
grabY = 0;
|
|
4786
|
+
grabZ = 0;
|
|
4787
|
+
stillFrames = 0;
|
|
4788
|
+
accumulator = 0;
|
|
4789
|
+
time = 0;
|
|
4790
|
+
asleep = false;
|
|
4791
|
+
// Spatial hash for self-collision. Sized once, refilled in place — the
|
|
4792
|
+
// per-substep collision pass allocates nothing.
|
|
4793
|
+
cellOf;
|
|
4794
|
+
bucketStart;
|
|
4795
|
+
bucketItems;
|
|
4796
|
+
bucketFill;
|
|
4797
|
+
tableSize;
|
|
4798
|
+
constructor(length, width, params) {
|
|
4799
|
+
this.params = { ...params };
|
|
4800
|
+
this.totalLength = length;
|
|
4801
|
+
this.halfWidth = width / 2;
|
|
4802
|
+
this.count = stripNodeCount(length, params.perforation);
|
|
4803
|
+
this.segment = length / (this.count - 1);
|
|
4804
|
+
this.bendScale = Math.min(1, this.segment / REFERENCE_SEGMENT);
|
|
4805
|
+
this.tangentNodes = Math.max(1, Math.round(TANGENT_SPAN / this.segment));
|
|
4806
|
+
this.pos = new Float64Array(this.count * 2);
|
|
4807
|
+
this.prev = new Float64Array(this.count * 2);
|
|
4808
|
+
this.arc = new Float64Array(this.count);
|
|
4809
|
+
this.perforated = new Int8Array(this.count);
|
|
4810
|
+
for (let i = 0; i < this.count; i++) {
|
|
4811
|
+
this.arc[i] = (this.count - 1 - i) * this.segment;
|
|
4812
|
+
}
|
|
4813
|
+
const spacing = Math.max(params.perforation, this.segment * 2);
|
|
4814
|
+
for (let i = 0; i < this.count - 1; i++) {
|
|
4815
|
+
const here = Math.floor(this.arc[i] / spacing);
|
|
4816
|
+
const next = Math.floor(this.arc[i + 1] / spacing);
|
|
4817
|
+
if (here !== next) this.perforated[i] = here % 2 === 0 ? 1 : -1;
|
|
4818
|
+
}
|
|
4819
|
+
this.tableSize = 1 << Math.ceil(Math.log2(Math.max(16, this.count * 2)));
|
|
4820
|
+
this.cellOf = new Int32Array(this.count);
|
|
4821
|
+
this.bucketStart = new Int32Array(this.tableSize + 1);
|
|
4822
|
+
this.bucketItems = new Int32Array(this.count);
|
|
4823
|
+
this.bucketFill = new Int32Array(this.tableSize);
|
|
4824
|
+
this.tubeStub = this.stubFor(params.core);
|
|
4825
|
+
this.paid = Math.min(params.tail, this.usableLength);
|
|
4826
|
+
this.lastScroll = params.scroll;
|
|
4827
|
+
this.layOut();
|
|
4828
|
+
}
|
|
4829
|
+
/**
|
|
4830
|
+
* Local-space y the pile builds on, after centring. A host placing a
|
|
4831
|
+
* shadow catcher or a contact shadow needs this; it cannot derive it,
|
|
4832
|
+
* because the composition is offset to sit on the origin.
|
|
4833
|
+
*/
|
|
4834
|
+
get floorY() {
|
|
4835
|
+
return -this.params.floor + this.centreOffset;
|
|
4836
|
+
}
|
|
4837
|
+
/**
|
|
4838
|
+
* How far the whole composition is lifted so that it straddles the origin.
|
|
4839
|
+
*
|
|
4840
|
+
* The roll's axis is the sim's origin, which would park a long drop
|
|
4841
|
+
* entirely below the frame — `<Paper>` and the editor both look at the
|
|
4842
|
+
* origin through about two world units, and neither fits a camera to the
|
|
4843
|
+
* content. `unroll` learned the same lesson and left a note about it: a
|
|
4844
|
+
* composition anchored anywhere but the origin needs a bespoke camera at
|
|
4845
|
+
* every call site.
|
|
4846
|
+
*
|
|
4847
|
+
* Measured against the FULL radius rather than the current one, so it is a
|
|
4848
|
+
* constant for a given config. Deriving it from the live radius would drift
|
|
4849
|
+
* the entire pile upward as the roll ran down.
|
|
4850
|
+
*/
|
|
4851
|
+
get centreOffset() {
|
|
4852
|
+
return (this.params.floor - this.outerRadius) / 2;
|
|
4853
|
+
}
|
|
4854
|
+
/**
|
|
4855
|
+
* The same centring, along the axis the pile actually grows in.
|
|
4856
|
+
*
|
|
4857
|
+
* Everything here is built around the DROP LINE — the z the paper leaves
|
|
4858
|
+
* the roll at — and that line is not the origin and does not stay put. It
|
|
4859
|
+
* starts at 0 on a full roll and travels back to `core - outerRadius` as
|
|
4860
|
+
* the roll runs down (see {@link tangentZ}), because the spiral's centre is
|
|
4861
|
+
* fixed and the tangent point walks in toward it. The pile builds around
|
|
4862
|
+
* wherever the line has been, so a composition that ignores this is offset
|
|
4863
|
+
* by half that travel before a single fold has landed, and z is DEPTH —
|
|
4864
|
+
* the axis a fixed head-on camera has the least room in.
|
|
4865
|
+
*
|
|
4866
|
+
* A constant for a given config, for the reason {@link centreOffset} is:
|
|
4867
|
+
* derived from the live radius it would slide the whole composition
|
|
4868
|
+
* backwards as the roll emptied, which reads far worse than sitting still
|
|
4869
|
+
* slightly off centre. Centring the line's travel splits the difference
|
|
4870
|
+
* between a full roll and an empty one.
|
|
4871
|
+
*/
|
|
4872
|
+
get centreShift() {
|
|
4873
|
+
return (this.outerRadius - this.params.core) / 2;
|
|
4874
|
+
}
|
|
4875
|
+
/** Outer radius of what is still wound. Shrinks to `core` as the roll empties. */
|
|
4876
|
+
get radius() {
|
|
4877
|
+
return rollRadius(this.totalLength - this.paid, this.params.core, layerThickness2(this.params.tightness));
|
|
4878
|
+
}
|
|
4879
|
+
/** Radius of the full roll. Fixes the spiral's centre, which must not move. */
|
|
4880
|
+
get outerRadius() {
|
|
4881
|
+
return rollRadius(this.totalLength, this.params.core, layerThickness2(this.params.tightness));
|
|
4882
|
+
}
|
|
4883
|
+
/**
|
|
4884
|
+
* The wrap that never leaves: one full turn around the core, but never so
|
|
4885
|
+
* much of a short sheet that there is nothing left to unroll.
|
|
4886
|
+
*
|
|
4887
|
+
* A method rather than a constant because `core` is a live control. It was
|
|
4888
|
+
* a constructor-time `readonly`, which meant dragging `core` moved the
|
|
4889
|
+
* radius the spiral is drawn at while `usableLength` and the end stop kept
|
|
4890
|
+
* answering for the old tube — the roll would run past its own floor, or
|
|
4891
|
+
* stop short of it, depending on which way the slider went.
|
|
4892
|
+
*/
|
|
4893
|
+
stubFor(core) {
|
|
4894
|
+
return Math.min(Math.PI * 2 * core, this.totalLength * 0.3);
|
|
4895
|
+
}
|
|
4896
|
+
/** Paper that can actually leave the roll — everything but the glued stub. */
|
|
4897
|
+
get usableLength() {
|
|
4898
|
+
return this.totalLength - this.tubeStub;
|
|
4899
|
+
}
|
|
4900
|
+
/** How much paper is left to give. `0` is down to the tube, `1` is untouched. */
|
|
4901
|
+
get remaining() {
|
|
4902
|
+
return 1 - this.paid / this.usableLength;
|
|
4903
|
+
}
|
|
4904
|
+
/**
|
|
4905
|
+
* Where the paper leaves the roll: the frontmost point of the current
|
|
4906
|
+
* outer wrap. The spiral's CENTRE is fixed (a lesson the `roll` deformer
|
|
4907
|
+
* paid for — see its notes), so as the roll runs down the tangent point
|
|
4908
|
+
* travels back toward the holder and the strip hangs closer to it. That
|
|
4909
|
+
* drift is real and worth keeping.
|
|
4910
|
+
*/
|
|
4911
|
+
get tangentZ() {
|
|
4912
|
+
return this.radius - this.outerRadius;
|
|
4913
|
+
}
|
|
4914
|
+
/** Place every node: the wound ones on the spiral, the free ones straight down. */
|
|
4915
|
+
layOut() {
|
|
4916
|
+
const firstFree = this.firstFreeIndex();
|
|
4917
|
+
for (let i = 0; i < this.count; i++) {
|
|
4918
|
+
const p = i * 2;
|
|
4919
|
+
if (i < firstFree) {
|
|
4920
|
+
this.spiralPoint(this.arc[i] - this.paid, p, this.pos);
|
|
4921
|
+
} else {
|
|
4922
|
+
this.pos[p] = -(this.paid - this.arc[i]);
|
|
4923
|
+
this.pos[p + 1] = this.tangentZ;
|
|
4924
|
+
}
|
|
4925
|
+
}
|
|
4926
|
+
this.prev.set(this.pos);
|
|
4927
|
+
}
|
|
4928
|
+
/** First node that has come off the roll. Everything below it is kinematic. */
|
|
4929
|
+
firstFreeIndex() {
|
|
4930
|
+
const free = Math.ceil(this.count - 1 - this.paid / this.segment);
|
|
4931
|
+
return Math.min(this.count - 1, Math.max(0, free));
|
|
4932
|
+
}
|
|
4933
|
+
/**
|
|
4934
|
+
* Position of a point `wound` along the spiral from the tangent point,
|
|
4935
|
+
* written into `out` at offset `o`.
|
|
4936
|
+
*
|
|
4937
|
+
* Angle 0 is the tangent point at the front of the roll; winding runs up
|
|
4938
|
+
* over the top, which is the "over" hang. The radius falls by one layer
|
|
4939
|
+
* per turn, so the wraps are concentric and exactly a thickness apart.
|
|
4940
|
+
*/
|
|
4941
|
+
spiralPoint(wound, o, out) {
|
|
4942
|
+
const r0 = this.radius;
|
|
4943
|
+
const thickness = layerThickness2(this.params.tightness);
|
|
4944
|
+
const k = thickness / (Math.PI * 2);
|
|
4945
|
+
const phi = windAngle(Math.max(0, wound), r0, k);
|
|
4946
|
+
const floor = Math.max(this.params.core, safeSpiralRadius(this.segment, thickness));
|
|
4947
|
+
const r = Math.max(floor, r0 - k * phi);
|
|
4948
|
+
out[o] = r * Math.sin(phi);
|
|
4949
|
+
out[o + 1] = -this.outerRadius + r * Math.cos(phi);
|
|
4950
|
+
}
|
|
4951
|
+
setParams(params) {
|
|
4952
|
+
let changed = false;
|
|
4953
|
+
const tailBefore = this.params.tail;
|
|
4954
|
+
for (const key of [
|
|
4955
|
+
"scroll",
|
|
4956
|
+
"tightness",
|
|
4957
|
+
"core",
|
|
4958
|
+
"tail",
|
|
4959
|
+
"crease",
|
|
4960
|
+
"stiffness",
|
|
4961
|
+
"drag",
|
|
4962
|
+
"gravity",
|
|
4963
|
+
"floor",
|
|
4964
|
+
"inertia"
|
|
4965
|
+
]) {
|
|
4966
|
+
const value = params[key];
|
|
4967
|
+
if (value !== void 0 && value !== this.params[key]) {
|
|
4968
|
+
this.params[key] = value;
|
|
4969
|
+
changed = true;
|
|
4970
|
+
}
|
|
4971
|
+
}
|
|
4972
|
+
if (params.core !== void 0) {
|
|
4973
|
+
this.tubeStub = this.stubFor(params.core);
|
|
4974
|
+
this.paid = Math.min(this.paid, this.usableLength);
|
|
4975
|
+
}
|
|
4976
|
+
if (params.tail !== void 0 && tailBefore !== this.params.tail) {
|
|
4977
|
+
if (!this.driven) {
|
|
4978
|
+
this.paid = Math.min(this.params.tail, this.usableLength);
|
|
4979
|
+
this.layOut();
|
|
4980
|
+
}
|
|
4981
|
+
}
|
|
4982
|
+
if (changed) this.wake();
|
|
4983
|
+
}
|
|
4984
|
+
wake() {
|
|
4985
|
+
this.asleep = false;
|
|
4986
|
+
this.stillFrames = 0;
|
|
4987
|
+
}
|
|
4988
|
+
/**
|
|
4989
|
+
* Take hold of the paper at a point, in the same y–z the vertex buffer is
|
|
4990
|
+
* written in — mesh-local coordinates, the lift already applied. Returns the
|
|
4991
|
+
* node caught, or -1 when there is no free paper to catch.
|
|
4992
|
+
*
|
|
4993
|
+
* Speaking the buffer's coordinates rather than the solver's is the whole
|
|
4994
|
+
* contract here: a caller has a raycast hit on the mesh and nothing else,
|
|
4995
|
+
* and the centring lift is an internal detail it has no way to know.
|
|
4996
|
+
*
|
|
4997
|
+
* Only paper that has left the roll can be caught — a wound node belongs to
|
|
4998
|
+
* the spiral, and pinning one would be pinning the roll itself.
|
|
4999
|
+
*/
|
|
5000
|
+
grabNearest(y, z30) {
|
|
5001
|
+
const firstFree = this.firstFreeIndex();
|
|
5002
|
+
const localY = y - this.centreOffset;
|
|
5003
|
+
const localZ = z30 - this.centreShift;
|
|
5004
|
+
let best = -1;
|
|
5005
|
+
let bestDist = Infinity;
|
|
5006
|
+
for (let i = firstFree; i < this.count; i++) {
|
|
5007
|
+
const dy = this.pos[i * 2] - localY;
|
|
5008
|
+
const dz = this.pos[i * 2 + 1] - localZ;
|
|
5009
|
+
const d = dy * dy + dz * dz;
|
|
5010
|
+
if (d < bestDist) {
|
|
5011
|
+
bestDist = d;
|
|
5012
|
+
best = i;
|
|
5013
|
+
}
|
|
5014
|
+
}
|
|
5015
|
+
this.grabbed = best;
|
|
5016
|
+
this.grabY = localY;
|
|
5017
|
+
this.grabZ = localZ;
|
|
5018
|
+
this.wake();
|
|
5019
|
+
return best;
|
|
5020
|
+
}
|
|
5021
|
+
moveGrab(y, z30) {
|
|
5022
|
+
if (this.grabbed < 0) return;
|
|
5023
|
+
this.grabY = y - this.centreOffset;
|
|
5024
|
+
this.grabZ = z30 - this.centreShift;
|
|
5025
|
+
this.wake();
|
|
5026
|
+
}
|
|
5027
|
+
release() {
|
|
5028
|
+
this.grabbed = -1;
|
|
5029
|
+
}
|
|
5030
|
+
/** Whether a pointer currently holds the paper. */
|
|
5031
|
+
get held() {
|
|
5032
|
+
return this.grabbed >= 0;
|
|
5033
|
+
}
|
|
5034
|
+
step(delta) {
|
|
5035
|
+
if (this.params.scroll !== this.lastScroll) this.wake();
|
|
5036
|
+
if (this.asleep) return;
|
|
5037
|
+
this.accumulator = Math.min(this.accumulator + delta, FIXED_DT2 * 4);
|
|
5038
|
+
while (this.accumulator >= FIXED_DT2) {
|
|
5039
|
+
this.substep(FIXED_DT2);
|
|
5040
|
+
this.accumulator -= FIXED_DT2;
|
|
5041
|
+
}
|
|
5042
|
+
}
|
|
5043
|
+
substep(dt) {
|
|
5044
|
+
this.time += dt;
|
|
5045
|
+
this.driveRoll(dt);
|
|
5046
|
+
this.pullFeed(dt);
|
|
5047
|
+
this.integrate(dt);
|
|
5048
|
+
const firstFree = this.firstFreeIndex();
|
|
5049
|
+
this.pinWound(firstFree);
|
|
5050
|
+
for (let iter = 0; iter < SOLVER_ITERATIONS2; iter++) {
|
|
5051
|
+
this.solveDistance(firstFree);
|
|
5052
|
+
this.solveBend(firstFree);
|
|
5053
|
+
if (iter >= SOLVER_ITERATIONS2 - COLLIDING_ITERATIONS) {
|
|
5054
|
+
this.solveSelfCollision(firstFree);
|
|
5055
|
+
this.solveFloor(firstFree);
|
|
5056
|
+
}
|
|
5057
|
+
this.pinWound(firstFree);
|
|
5058
|
+
}
|
|
5059
|
+
this.solveFloor(firstFree);
|
|
5060
|
+
}
|
|
5061
|
+
/**
|
|
5062
|
+
* Turn the roll. Scroll enters as an angle the roll OWES, which a leaky
|
|
5063
|
+
* integrator spends into angular velocity — so a steady scroll gives a
|
|
5064
|
+
* steady spin and a flick spikes and coasts down.
|
|
5065
|
+
*
|
|
5066
|
+
* `ΔL = R·Δθ` with the CURRENT radius, which is why the radius has to be
|
|
5067
|
+
* read inside the loop: a nearly-empty roll spins fast and gives up very
|
|
5068
|
+
* little paper for the same scroll, and that is the whole tell that a roll
|
|
5069
|
+
* is running out.
|
|
5070
|
+
*/
|
|
5071
|
+
driveRoll(dt) {
|
|
5072
|
+
const scroll = this.params.scroll;
|
|
5073
|
+
if (!this.primed) {
|
|
5074
|
+
this.lastScroll = scroll;
|
|
5075
|
+
this.primed = true;
|
|
5076
|
+
}
|
|
5077
|
+
const impulse = (scroll - this.lastScroll) / this.outerRadius;
|
|
5078
|
+
this.lastScroll = scroll;
|
|
5079
|
+
const tau = 0.012 + this.params.inertia * 0.55;
|
|
5080
|
+
this.omega = this.omega * Math.exp(-dt / tau) + impulse / tau;
|
|
5081
|
+
if (Math.abs(this.omega) < 1e-6) this.omega = 0;
|
|
5082
|
+
const next = this.paid + this.radius * this.omega * dt;
|
|
5083
|
+
if (next !== this.paid) this.driven = true;
|
|
5084
|
+
this.paid = Math.min(this.usableLength, Math.max(0, next));
|
|
5085
|
+
if (this.paid === 0 || this.paid === this.usableLength) this.omega = 0;
|
|
5086
|
+
}
|
|
5087
|
+
/**
|
|
5088
|
+
* Take hold of the paper and pull, and the roll turns — the interaction the
|
|
5089
|
+
* real object is famous for.
|
|
5090
|
+
*
|
|
5091
|
+
* It is driven by TENSION rather than by mapping hand travel to an angle.
|
|
5092
|
+
* Paper does not stretch, so if the hand is further from the roll than there
|
|
5093
|
+
* is paper to reach it, the only way the constraint can be satisfied is for
|
|
5094
|
+
* the roll to give up more. That one rule gets the whole behaviour for free:
|
|
5095
|
+
* a slow pull feeds smoothly, a fast yank spins the roll and it carries on
|
|
5096
|
+
* after release, and pushing the paper back toward the roll does nothing at
|
|
5097
|
+
* all — which is exactly right, because slack does not rewind a roll. Only
|
|
5098
|
+
* scrolling up does.
|
|
5099
|
+
*/
|
|
5100
|
+
pullFeed(dt) {
|
|
5101
|
+
if (this.grabbed < 0 || this.grabbed < this.firstFreeIndex()) return;
|
|
5102
|
+
const reach = Math.hypot(this.grabY, this.grabZ - this.tangentZ);
|
|
5103
|
+
const available = this.paid - this.arc[this.grabbed];
|
|
5104
|
+
const over = reach - available;
|
|
5105
|
+
if (over <= 1e-6) return;
|
|
5106
|
+
const feed = Math.min(over * PULL_STIFFNESS, this.usableLength - this.paid);
|
|
5107
|
+
if (feed <= 0) return;
|
|
5108
|
+
this.paid += feed;
|
|
5109
|
+
this.omega = feed / (this.radius * dt);
|
|
5110
|
+
}
|
|
5111
|
+
/**
|
|
5112
|
+
* Verlet with ANISOTROPIC drag. A strip of paper barely notices the air
|
|
5113
|
+
* when it moves along its own length and is stopped almost dead when it
|
|
5114
|
+
* moves broadside, which is the difference between paper that floats down
|
|
5115
|
+
* and a rope that drops. Damping the velocity uniformly — the obvious
|
|
5116
|
+
* thing, and what cloth does — gets neither.
|
|
5117
|
+
*/
|
|
5118
|
+
integrate(dt) {
|
|
5119
|
+
const { gravity, drag } = this.params;
|
|
5120
|
+
const p = this.pos;
|
|
5121
|
+
const dt2 = dt * dt;
|
|
5122
|
+
const firstFree = this.firstFreeIndex();
|
|
5123
|
+
const across = Math.exp(-(0.6 + drag * 11) * dt);
|
|
5124
|
+
const along = Math.exp(-(0.4 + drag * 0.8) * dt);
|
|
5125
|
+
let travel = 0;
|
|
5126
|
+
for (let i = firstFree; i < this.count; i++) {
|
|
5127
|
+
const o = i * 2;
|
|
5128
|
+
if (i === this.grabbed) {
|
|
5129
|
+
p[o] = this.grabY;
|
|
5130
|
+
p[o + 1] = this.grabZ;
|
|
5131
|
+
continue;
|
|
5132
|
+
}
|
|
5133
|
+
const y = p[o];
|
|
5134
|
+
const z30 = p[o + 1];
|
|
5135
|
+
let vy = y - this.prev[o];
|
|
5136
|
+
let vz = z30 - this.prev[o + 1];
|
|
5137
|
+
const a = Math.max(firstFree, i - this.tangentNodes) * 2;
|
|
5138
|
+
const b = Math.min(this.count - 1, i + this.tangentNodes) * 2;
|
|
5139
|
+
let ty = p[b] - p[a];
|
|
5140
|
+
let tz = p[b + 1] - p[a + 1];
|
|
5141
|
+
const len = Math.hypot(ty, tz);
|
|
5142
|
+
if (len > 1e-9) {
|
|
5143
|
+
ty /= len;
|
|
5144
|
+
tz /= len;
|
|
5145
|
+
const vt = vy * ty + vz * tz;
|
|
5146
|
+
const ny = vy - vt * ty;
|
|
5147
|
+
const nz = vz - vt * tz;
|
|
5148
|
+
vy = vt * ty * along + ny * across;
|
|
5149
|
+
vz = vt * tz * along + nz * across;
|
|
5150
|
+
}
|
|
5151
|
+
this.prev[o] = y;
|
|
5152
|
+
this.prev[o + 1] = z30;
|
|
5153
|
+
const noise = Math.sin(this.time * 2.3 + i * 1.7) * 6e-5;
|
|
5154
|
+
p[o] = y + vy - gravity * 3.2 * dt2;
|
|
5155
|
+
p[o + 1] = z30 + vz + noise;
|
|
5156
|
+
travel = Math.max(travel, vy * vy + vz * vz);
|
|
5157
|
+
}
|
|
5158
|
+
if (this.omega === 0 && this.grabbed < 0) {
|
|
5159
|
+
if (travel < SLEEP_EPSILON2) {
|
|
5160
|
+
if (++this.stillFrames > SLEEP_FRAMES2) this.asleep = true;
|
|
5161
|
+
} else this.stillFrames = 0;
|
|
5162
|
+
} else this.stillFrames = 0;
|
|
5163
|
+
}
|
|
5164
|
+
/** Wound nodes are the roll's, not the solver's: rewrite them every pass. */
|
|
5165
|
+
pinWound(firstFree) {
|
|
5166
|
+
for (let i = 0; i < firstFree; i++) {
|
|
5167
|
+
this.spiralPoint(this.arc[i] - this.paid, i * 2, this.pos);
|
|
5168
|
+
}
|
|
5169
|
+
const tz = this.tangentZ;
|
|
5170
|
+
for (let i = firstFree; i < Math.min(this.count, firstFree + 2); i++) {
|
|
5171
|
+
if (i === this.grabbed) continue;
|
|
5172
|
+
const o = i * 2;
|
|
5173
|
+
this.pos[o + 1] = this.pos[o + 1] + (tz - this.pos[o + 1]) * 0.5;
|
|
5174
|
+
}
|
|
5175
|
+
if (this.grabbed >= firstFree) {
|
|
5176
|
+
this.pos[this.grabbed * 2] = this.grabY;
|
|
5177
|
+
this.pos[this.grabbed * 2 + 1] = this.grabZ;
|
|
5178
|
+
}
|
|
5179
|
+
}
|
|
5180
|
+
/** Paper does not stretch. Distance constraints run at full strength. */
|
|
5181
|
+
solveDistance(firstFree) {
|
|
5182
|
+
const p = this.pos;
|
|
5183
|
+
const rest = this.segment;
|
|
5184
|
+
for (let i = Math.max(0, firstFree - 1); i < this.count - 1; i++) {
|
|
5185
|
+
const a = i * 2;
|
|
5186
|
+
const b = (i + 1) * 2;
|
|
5187
|
+
const dy = p[b] - p[a];
|
|
5188
|
+
const dz = p[b + 1] - p[a + 1];
|
|
5189
|
+
const dist = Math.hypot(dy, dz);
|
|
5190
|
+
if (dist < 1e-9) continue;
|
|
5191
|
+
const diff = (dist - rest) / dist * 0.5;
|
|
5192
|
+
const aFixed = i < firstFree || i === this.grabbed;
|
|
5193
|
+
const bFixed = i + 1 === this.grabbed;
|
|
5194
|
+
if (aFixed && bFixed) continue;
|
|
5195
|
+
const aw = aFixed ? 0 : bFixed ? 2 : 1;
|
|
5196
|
+
const bw = bFixed ? 0 : aFixed ? 2 : 1;
|
|
5197
|
+
p[a] = p[a] + dy * diff * aw;
|
|
5198
|
+
p[a + 1] = p[a + 1] + dz * diff * aw;
|
|
5199
|
+
p[b] = p[b] - dy * diff * bw;
|
|
5200
|
+
p[b + 1] = p[b + 1] - dz * diff * bw;
|
|
5201
|
+
}
|
|
5202
|
+
}
|
|
5203
|
+
/**
|
|
5204
|
+
* The bend constraint, and the reason this is not a rope preset.
|
|
5205
|
+
*
|
|
5206
|
+
* Every joint wants to be straight. A joint AT A PERFORATION wants it far
|
|
5207
|
+
* less, and may want a slight fold instead — those are the hinges the pile
|
|
5208
|
+
* folds at, and the alternating sign is what turns a heap into an
|
|
5209
|
+
* accordion. Uniform stiffness gives a coil; no stiffness gives a rope.
|
|
5210
|
+
*/
|
|
5211
|
+
solveBend(firstFree) {
|
|
5212
|
+
const p = this.pos;
|
|
5213
|
+
const { stiffness, crease } = this.params;
|
|
5214
|
+
const start = Math.max(firstFree + 1, 1);
|
|
5215
|
+
for (let i = start; i < this.count - 1; i++) {
|
|
5216
|
+
const hinge = this.perforated[i];
|
|
5217
|
+
const k = (hinge !== 0 ? stiffness * 0.06 : stiffness) * 0.45 * this.bendScale;
|
|
5218
|
+
if (k <= 0) continue;
|
|
5219
|
+
const a = (i - 1) * 2;
|
|
5220
|
+
const m = i * 2;
|
|
5221
|
+
const b = (i + 1) * 2;
|
|
5222
|
+
let ty = (p[a] + p[b]) * 0.5;
|
|
5223
|
+
let tz = (p[a + 1] + p[b + 1]) * 0.5;
|
|
5224
|
+
if (hinge !== 0 && crease > 0) {
|
|
5225
|
+
let cy = p[b] - p[a];
|
|
5226
|
+
let cz = p[b + 1] - p[a + 1];
|
|
5227
|
+
const len = Math.hypot(cy, cz);
|
|
5228
|
+
if (len > 1e-9) {
|
|
5229
|
+
cy /= len;
|
|
5230
|
+
cz /= len;
|
|
5231
|
+
const bow = crease * this.segment * 0.35 * hinge;
|
|
5232
|
+
ty += -cz * bow;
|
|
5233
|
+
tz += cy * bow;
|
|
5234
|
+
}
|
|
5235
|
+
}
|
|
5236
|
+
const dy = (ty - p[m]) * k;
|
|
5237
|
+
const dz = (tz - p[m + 1]) * k;
|
|
5238
|
+
if (i !== this.grabbed) {
|
|
5239
|
+
p[m] = p[m] + dy;
|
|
5240
|
+
p[m + 1] = p[m + 1] + dz;
|
|
5241
|
+
}
|
|
5242
|
+
if (i - 1 >= firstFree && i - 1 !== this.grabbed) {
|
|
5243
|
+
p[a] = p[a] - dy * 0.5;
|
|
5244
|
+
p[a + 1] = p[a + 1] - dz * 0.5;
|
|
5245
|
+
}
|
|
5246
|
+
if (i + 1 !== this.grabbed) {
|
|
5247
|
+
p[b] = p[b] - dy * 0.5;
|
|
5248
|
+
p[b + 1] = p[b + 1] - dz * 0.5;
|
|
5249
|
+
}
|
|
5250
|
+
}
|
|
5251
|
+
}
|
|
5252
|
+
/**
|
|
5253
|
+
* Folds stack on each other instead of passing through. Without this the
|
|
5254
|
+
* pile collapses into a single flat line on the floor and there is nothing
|
|
5255
|
+
* to look at, which is the entire reason the preset exists.
|
|
5256
|
+
*
|
|
5257
|
+
* **Segment against segment, not node against node.** The obvious version
|
|
5258
|
+
* puts a sphere on every node and pushes overlapping pairs apart, and it
|
|
5259
|
+
* leaks: paper is thinner than the chain is finely cut — the layer gap here
|
|
5260
|
+
* is 0.027 against a node spacing of 0.037 — so those spheres do not touch
|
|
5261
|
+
* each other, and the chain is a string of beads with gaps between them
|
|
5262
|
+
* rather than a continuous tube. Another fold threads straight through a
|
|
5263
|
+
* gap, and once it is through, the point test pushes it out the far side
|
|
5264
|
+
* instead of back. It shows up exactly as a sheet edge buried in a surface.
|
|
5265
|
+
*
|
|
5266
|
+
* Testing the SEGMENTS closes the gaps, because a segment is the whole
|
|
5267
|
+
* length of paper between two nodes and not just its ends. The cost is a
|
|
5268
|
+
* closest-approach solve per candidate pair instead of a subtraction, which
|
|
5269
|
+
* is why the hash cell is sized to `segment + d`: a pair that can possibly
|
|
5270
|
+
* touch has its midpoints inside that, so a 3×3 neighbourhood is a complete
|
|
5271
|
+
* search rather than a hopeful one.
|
|
5272
|
+
*
|
|
5273
|
+
* Spatial hash over preallocated arrays, refilled by counting sort — no
|
|
5274
|
+
* allocation, and it does not degrade when the whole strip lands in one
|
|
5275
|
+
* cell, which is exactly what a pile IS.
|
|
5276
|
+
*/
|
|
5277
|
+
solveSelfCollision(firstFree) {
|
|
5278
|
+
this.freeFrom = firstFree;
|
|
5279
|
+
const p = this.pos;
|
|
5280
|
+
const d = Math.max(layerThickness2(this.params.tightness), 1e-4);
|
|
5281
|
+
const n = this.count;
|
|
5282
|
+
const last = n - 2;
|
|
5283
|
+
if (last < firstFree + 2) return;
|
|
5284
|
+
const cell = this.segment + d;
|
|
5285
|
+
const inv = 1 / cell;
|
|
5286
|
+
const table = this.tableSize;
|
|
5287
|
+
const mask = table - 1;
|
|
5288
|
+
const starts = this.bucketStart;
|
|
5289
|
+
const items = this.bucketItems;
|
|
5290
|
+
const placed = this.bucketFill;
|
|
5291
|
+
starts.fill(0);
|
|
5292
|
+
placed.fill(0);
|
|
5293
|
+
for (let i = firstFree; i <= last; i++) {
|
|
5294
|
+
const my = (p[i * 2] + p[(i + 1) * 2]) * 0.5;
|
|
5295
|
+
const mz = (p[i * 2 + 1] + p[(i + 1) * 2 + 1]) * 0.5;
|
|
5296
|
+
const h = (Math.floor(my * inv) * 92837111 ^ Math.floor(mz * inv) * 689287499) >>> 0 & mask;
|
|
5297
|
+
this.cellOf[i] = h;
|
|
5298
|
+
starts[h + 1]++;
|
|
5299
|
+
}
|
|
5300
|
+
for (let c = 0; c < table; c++) starts[c + 1] += starts[c];
|
|
5301
|
+
for (let i = firstFree; i <= last; i++) {
|
|
5302
|
+
const h = this.cellOf[i];
|
|
5303
|
+
items[starts[h] + placed[h]++] = i;
|
|
5304
|
+
}
|
|
5305
|
+
for (let i = firstFree; i <= last; i++) {
|
|
5306
|
+
const my = (p[i * 2] + p[(i + 1) * 2]) * 0.5;
|
|
5307
|
+
const mz = (p[i * 2 + 1] + p[(i + 1) * 2 + 1]) * 0.5;
|
|
5308
|
+
const cy = Math.floor(my * inv);
|
|
5309
|
+
const cz = Math.floor(mz * inv);
|
|
5310
|
+
for (let oy = -1; oy <= 1; oy++) {
|
|
5311
|
+
for (let oz = -1; oz <= 1; oz++) {
|
|
5312
|
+
const h = ((cy + oy) * 92837111 ^ (cz + oz) * 689287499) >>> 0 & mask;
|
|
5313
|
+
const end = starts[h + 1];
|
|
5314
|
+
for (let e = starts[h]; e < end; e++) {
|
|
5315
|
+
const j = items[e];
|
|
5316
|
+
if (j >= i + 2) this.separate(i, j, d);
|
|
5317
|
+
}
|
|
5318
|
+
}
|
|
5319
|
+
}
|
|
5320
|
+
}
|
|
5321
|
+
}
|
|
5322
|
+
/**
|
|
5323
|
+
* Push segments `(i, i+1)` and `(j, j+1)` apart to `d`, if they are closer.
|
|
5324
|
+
*
|
|
5325
|
+
* Closest approach between two 2D segments, then the correction shared over
|
|
5326
|
+
* the four endpoints by how near each is to the touching point — so a fold
|
|
5327
|
+
* caught at its middle moves bodily, and one grazed at its tip barely
|
|
5328
|
+
* pivots. Wound and held nodes are the roll's and the hand's, so they take
|
|
5329
|
+
* none of it and their partner takes the lot.
|
|
5330
|
+
*/
|
|
5331
|
+
separate(i, j, d) {
|
|
5332
|
+
const p = this.pos;
|
|
5333
|
+
const a = i * 2;
|
|
5334
|
+
const b = (i + 1) * 2;
|
|
5335
|
+
const c = j * 2;
|
|
5336
|
+
const e = (j + 1) * 2;
|
|
5337
|
+
const uy = p[b] - p[a];
|
|
5338
|
+
const uz = p[b + 1] - p[a + 1];
|
|
5339
|
+
const vy = p[e] - p[c];
|
|
5340
|
+
const vz = p[e + 1] - p[c + 1];
|
|
5341
|
+
const wy = p[a] - p[c];
|
|
5342
|
+
const wz = p[a + 1] - p[c + 1];
|
|
5343
|
+
const uu = uy * uy + uz * uz;
|
|
5344
|
+
const vv = vy * vy + vz * vz;
|
|
5345
|
+
if (uu < 1e-12 || vv < 1e-12) return;
|
|
5346
|
+
const uv = uy * vy + uz * vz;
|
|
5347
|
+
const uw = uy * wy + uz * wz;
|
|
5348
|
+
const vw = vy * wy + vz * wz;
|
|
5349
|
+
const denom = uu * vv - uv * uv;
|
|
5350
|
+
let s = denom > 1e-12 ? (uv * vw - vv * uw) / denom : 0;
|
|
5351
|
+
s = s < 0 ? 0 : s > 1 ? 1 : s;
|
|
5352
|
+
let t = (uv * s + vw) / vv;
|
|
5353
|
+
if (t < 0) {
|
|
5354
|
+
t = 0;
|
|
5355
|
+
s = -uw / uu;
|
|
5356
|
+
} else if (t > 1) {
|
|
5357
|
+
t = 1;
|
|
5358
|
+
s = (uv - uw) / uu;
|
|
5359
|
+
}
|
|
5360
|
+
s = s < 0 ? 0 : s > 1 ? 1 : s;
|
|
5361
|
+
let ny = p[c] + vy * t - (p[a] + uy * s);
|
|
5362
|
+
let nz = p[c + 1] + vz * t - (p[a + 1] + uz * s);
|
|
5363
|
+
let dist = Math.hypot(ny, nz);
|
|
5364
|
+
if (dist >= d) return;
|
|
5365
|
+
if (dist < 1e-9) {
|
|
5366
|
+
ny = -uz;
|
|
5367
|
+
nz = uy;
|
|
5368
|
+
dist = Math.hypot(ny, nz);
|
|
5369
|
+
if (dist < 1e-12) return;
|
|
5370
|
+
ny = -ny;
|
|
5371
|
+
nz = -nz;
|
|
5372
|
+
}
|
|
5373
|
+
const push = (d - dist) / dist * COLLISION_RELAXATION;
|
|
5374
|
+
const gy = ny * push;
|
|
5375
|
+
const gz = nz * push;
|
|
5376
|
+
const ki = 0.5 / ((1 - s) * (1 - s) + s * s);
|
|
5377
|
+
const kj = 0.5 / ((1 - t) * (1 - t) + t * t);
|
|
5378
|
+
this.nudge(i, -gy * (1 - s) * ki, -gz * (1 - s) * ki);
|
|
5379
|
+
this.nudge(i + 1, -gy * s * ki, -gz * s * ki);
|
|
5380
|
+
this.nudge(j, gy * (1 - t) * kj, gz * (1 - t) * kj);
|
|
5381
|
+
this.nudge(j + 1, gy * t * kj, gz * t * kj);
|
|
5382
|
+
}
|
|
5383
|
+
/** First free node for the substep in flight — `nudge` runs in the innermost
|
|
5384
|
+
* collision loop and must not recompute it per call. */
|
|
5385
|
+
freeFrom = 0;
|
|
5386
|
+
/** Move a node, unless the roll or the hand owns it. */
|
|
5387
|
+
nudge(i, dy, dz) {
|
|
5388
|
+
if (i === this.grabbed || i < this.freeFrom) return;
|
|
5389
|
+
this.pos[i * 2] = this.pos[i * 2] + dy;
|
|
5390
|
+
this.pos[i * 2 + 1] = this.pos[i * 2 + 1] + dz;
|
|
5391
|
+
}
|
|
5392
|
+
/** Restitution 0, friction high. Paper lands and stays; it does not slide. */
|
|
5393
|
+
solveFloor(firstFree) {
|
|
5394
|
+
const p = this.pos;
|
|
5395
|
+
const floor = -this.params.floor;
|
|
5396
|
+
for (let i = firstFree; i < this.count; i++) {
|
|
5397
|
+
const o = i * 2;
|
|
5398
|
+
if (i === this.grabbed || p[o] >= floor) continue;
|
|
5399
|
+
p[o] = floor;
|
|
5400
|
+
this.prev[o] = floor;
|
|
5401
|
+
this.prev[o + 1] = this.prev[o + 1] + (p[o + 1] - this.prev[o + 1]) * 0.92;
|
|
5402
|
+
}
|
|
5403
|
+
}
|
|
5404
|
+
/**
|
|
5405
|
+
* Write the chain into a 2×N quad strip, in PlaneGeometry vertex order:
|
|
5406
|
+
* row-major, top row first, x left→right. Row `i` is node `i`, so the
|
|
5407
|
+
* content texture runs down the strip and folds with it.
|
|
5408
|
+
*/
|
|
5409
|
+
writeInto(out) {
|
|
5410
|
+
const lift = this.centreOffset;
|
|
5411
|
+
const shift = this.centreShift;
|
|
5412
|
+
for (let i = 0; i < this.count; i++) {
|
|
5413
|
+
const o = i * 2;
|
|
5414
|
+
const y = this.pos[o] + lift;
|
|
5415
|
+
const z30 = this.pos[o + 1] + shift;
|
|
5416
|
+
const v = i * 6;
|
|
5417
|
+
out[v] = -this.halfWidth;
|
|
5418
|
+
out[v + 1] = y;
|
|
5419
|
+
out[v + 2] = z30;
|
|
5420
|
+
out[v + 3] = this.halfWidth;
|
|
5421
|
+
out[v + 4] = y;
|
|
5422
|
+
out[v + 5] = z30;
|
|
5423
|
+
}
|
|
5424
|
+
}
|
|
5425
|
+
};
|
|
5426
|
+
|
|
3856
5427
|
// src/surface/PaperMaterial.tsx
|
|
3857
5428
|
var THREE10 = __toESM(require("three"), 1);
|
|
3858
5429
|
var import_react9 = require("react");
|
|
@@ -3934,6 +5505,7 @@ function translucencyUniforms(translucency, lighting) {
|
|
|
3934
5505
|
}
|
|
3935
5506
|
|
|
3936
5507
|
// src/surface/compose.ts
|
|
5508
|
+
var SHADED_CREASE = 0.35;
|
|
3937
5509
|
var VERTEX = (
|
|
3938
5510
|
/* glsl */
|
|
3939
5511
|
`
|
|
@@ -3950,6 +5522,89 @@ var HELPERS = (
|
|
|
3950
5522
|
`
|
|
3951
5523
|
varying vec2 vPaperUv;
|
|
3952
5524
|
uniform float uBackDarken;
|
|
5525
|
+
uniform vec2 uSheetSize;
|
|
5526
|
+
|
|
5527
|
+
/**
|
|
5528
|
+
* Where this fragment is on the sheet, in the sheet's OWN local space \u2014
|
|
5529
|
+
* the same coordinates the deformers displace, centred on the sheet.
|
|
5530
|
+
*
|
|
5531
|
+
* Every effect below measures in these rather than in UV, and the difference
|
|
5532
|
+
* is not cosmetic. UV divides the sheet's aspect out, so a 1.2 x 1.5 sheet is
|
|
5533
|
+
* a unit square as far as the shader is concerned: fibre drawn round comes out
|
|
5534
|
+
* stretched, a tear bites deeper into the short edge than the long one, and a
|
|
5535
|
+
* crease line scored at 45 degrees renders at 51. Worse, all three change when
|
|
5536
|
+
* the sheet is RESIZED, which makes the paper's own material a function of how
|
|
5537
|
+
* big the piece is. Grain is a property of the stock and a crease is a broken
|
|
5538
|
+
* fibre; neither knows how large a sheet it was cut from.
|
|
5539
|
+
*/
|
|
5540
|
+
vec2 plLocal() {
|
|
5541
|
+
return (vPaperUv - 0.5) * uSheetSize;
|
|
5542
|
+
}
|
|
5543
|
+
|
|
5544
|
+
/**
|
|
5545
|
+
* The paper's relief, in world units above the sheet the mesh describes.
|
|
5546
|
+
*
|
|
5547
|
+
* Accumulated by whichever effects have a shape as well as a colour, and
|
|
5548
|
+
* spent once at the end of main by {@link plPerturb}. One shared field rather
|
|
5549
|
+
* than a perturbation per effect, because two effects that both tilt the
|
|
5550
|
+
* surface tilt it TOGETHER \u2014 a crease across a grained sheet is one surface,
|
|
5551
|
+
* not a crease lit on top of a grain lit on top of the paper.
|
|
5552
|
+
*/
|
|
5553
|
+
float plHeight;
|
|
5554
|
+
|
|
5555
|
+
/**
|
|
5556
|
+
* The relief, turned into the normal the lighting actually runs on.
|
|
5557
|
+
*
|
|
5558
|
+
* This is the change that makes the surface effects respond to light at all.
|
|
5559
|
+
* They used to be painted: a crease multiplied a grey band into the albedo
|
|
5560
|
+
* and added a fixed white sheen beside it, so the mark looked identical from
|
|
5561
|
+
* every angle and under every rig, and turning the sheet under the key light
|
|
5562
|
+
* did nothing to it. Real creased paper is two facets meeting at a line \u2014
|
|
5563
|
+
* swing it and the crease flips from a dark line to a bright one. Only a
|
|
5564
|
+
* normal can do that, so now the effects describe a HEIGHT and the standard
|
|
5565
|
+
* material lights it.
|
|
5566
|
+
*
|
|
5567
|
+
* The maths is Mikkelsen's surface-gradient bump, which is what three's own
|
|
5568
|
+
* perturbNormalArb implements, with one deliberate difference: three
|
|
5569
|
+
* normalises the screen-space position derivatives, which makes a bump map
|
|
5570
|
+
* look the same at any scale and is the right call for a texture. Ours is a
|
|
5571
|
+
* real depth in world units \u2014 a crease is as deep as it is however close you
|
|
5572
|
+
* stand \u2014 so the raw derivatives stay, and the ratio between them and the
|
|
5573
|
+
* height's is a true surface slope.
|
|
5574
|
+
*
|
|
5575
|
+
* Analytic height plus screen derivatives also anti-aliases itself for free:
|
|
5576
|
+
* as a crease shrinks below a pixel the derivative flattens and the mark
|
|
5577
|
+
* fades, rather than crawling.
|
|
5578
|
+
*/
|
|
5579
|
+
vec3 plPerturb(vec3 n, float height) {
|
|
5580
|
+
vec2 dH = vec2(dFdx(height), dFdy(height));
|
|
5581
|
+
if (dH.x == 0.0 && dH.y == 0.0) return n;
|
|
5582
|
+
// View-space position: the varying is its negation, by three's convention.
|
|
5583
|
+
vec3 sigmaX = dFdx(-vViewPosition);
|
|
5584
|
+
vec3 sigmaY = dFdy(-vViewPosition);
|
|
5585
|
+
vec3 r1 = cross(sigmaY, n);
|
|
5586
|
+
vec3 r2 = cross(n, sigmaX);
|
|
5587
|
+
float det = dot(sigmaX, r1) * (gl_FrontFacing ? 1.0 : -1.0);
|
|
5588
|
+
if (abs(det) < 1e-12) return n;
|
|
5589
|
+
vec3 grad = sign(det) * (dH.x * r1 + dH.y * r2);
|
|
5590
|
+
return normalize(abs(det) * n - grad);
|
|
5591
|
+
}
|
|
5592
|
+
|
|
5593
|
+
/**
|
|
5594
|
+
* A gaussian bell of unit width, pre-filtered against this fragment's own
|
|
5595
|
+
* footprint.
|
|
5596
|
+
*
|
|
5597
|
+
* Convolving a gaussian with the pixel broadens it and flattens it by the
|
|
5598
|
+
* same factor, which conserves the integral: a crease seen from across the
|
|
5599
|
+
* room dims instead of breaking into a dotted line. s is the distance
|
|
5600
|
+
* across the feature in units of its own width, so a caller only ever has to
|
|
5601
|
+
* decide how wide the thing is.
|
|
5602
|
+
*/
|
|
5603
|
+
float plBell(float s) {
|
|
5604
|
+
float px = fwidth(s);
|
|
5605
|
+
float widen = sqrt(1.0 + px * px);
|
|
5606
|
+
return exp(-(s * s) / (widen * widen)) / widen;
|
|
5607
|
+
}
|
|
3953
5608
|
|
|
3954
5609
|
float plHash(vec2 p) {
|
|
3955
5610
|
return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453123);
|
|
@@ -3990,12 +5645,54 @@ var GRAIN_CHUNK = (
|
|
|
3990
5645
|
uniform float uGrainAmount;
|
|
3991
5646
|
uniform float uGrainBanding;
|
|
3992
5647
|
|
|
5648
|
+
/**
|
|
5649
|
+
* Fibre density, per world unit.
|
|
5650
|
+
*
|
|
5651
|
+
* Per WORLD UNIT and not per UV, which is the whole fix: the fibre in a sheet
|
|
5652
|
+
* is the stock's, and it does not get coarser because someone cut a bigger
|
|
5653
|
+
* piece or stretch oval because the piece is taller than it is wide.
|
|
5654
|
+
*
|
|
5655
|
+
* The number is carried over from when it was a UV frequency, so a sheet one
|
|
5656
|
+
* world unit wide is unchanged across its width. Its height is not, and that
|
|
5657
|
+
* is the point: a 1.4-tall sheet used to fit the same 240 cycles into a
|
|
5658
|
+
* longer span and its fibre ran visibly coarser the other way.
|
|
5659
|
+
*/
|
|
5660
|
+
const float PL_FIBRE = 240.0;
|
|
5661
|
+
|
|
5662
|
+
/**
|
|
5663
|
+
* The coarser structure underneath it \u2014 paper's tooth, the part that has a
|
|
5664
|
+
* SHAPE and not just a colour.
|
|
5665
|
+
*
|
|
5666
|
+
* Separate from the fibre, and much lower, for a reason worth writing down:
|
|
5667
|
+
* the relief is differentiated in screen space, and a field at the fibre's own
|
|
5668
|
+
* frequency is a few pixels per cycle at any sane viewing distance, so its
|
|
5669
|
+
* derivative is noise and the sheet would sparkle. The tooth is safely above
|
|
5670
|
+
* the sampling rate, and it is the scale at which paper actually catches a
|
|
5671
|
+
* raking light anyway.
|
|
5672
|
+
*/
|
|
5673
|
+
const float PL_TOOTH = 70.0;
|
|
5674
|
+
|
|
5675
|
+
/**
|
|
5676
|
+
* How far the tooth stands proud, in world units, at full grain.
|
|
5677
|
+
*
|
|
5678
|
+
* Real paper's surface relief is tens of microns. Against a sheet whose width
|
|
5679
|
+
* is one world unit \u2014 call it A4 \u2014 0.00035 is about 70 microns, and at the
|
|
5680
|
+
* tooth's wavelength that is a surface tilting by four degrees or so. Enough
|
|
5681
|
+
* to break a specular highlight into paper, not enough to look pebbled.
|
|
5682
|
+
*/
|
|
5683
|
+
const float PL_TOOTH_RELIEF = 0.00035;
|
|
5684
|
+
|
|
3993
5685
|
void plGrain(inout vec4 color, inout float rough) {
|
|
3994
|
-
|
|
3995
|
-
float
|
|
5686
|
+
vec2 local = plLocal();
|
|
5687
|
+
float fiber = plFbm(local * PL_FIBRE);
|
|
5688
|
+
float fleck = plNoise(local * (PL_FIBRE * 3.75));
|
|
3996
5689
|
float g = mix(0.5, fiber * 0.75 + fleck * 0.25, uGrainAmount);
|
|
3997
5690
|
color.rgb *= 0.92 + g * 0.16;
|
|
3998
5691
|
rough = clamp(rough + (g - 0.5) * uGrainAmount * 0.35, 0.0, 1.0);
|
|
5692
|
+
// The tooth, handed to the lighting rather than drawn. A single octave: the
|
|
5693
|
+
// relief only needs the scale the eye reads as texture, and the fbm above
|
|
5694
|
+
// is already carrying everything finer as colour.
|
|
5695
|
+
plHeight += (plNoise(local * PL_TOOTH) - 0.5) * PL_TOOTH_RELIEF * uGrainAmount;
|
|
3999
5696
|
// Thermal-printer banding: faint horizontal density stripes.
|
|
4000
5697
|
if (uGrainBanding > 0.0) {
|
|
4001
5698
|
float band = sin(vPaperUv.y * 700.0) * 0.5 + 0.5;
|
|
@@ -4010,18 +5707,33 @@ var DECKLE_CHUNK = (
|
|
|
4010
5707
|
uniform vec4 uDeckleEdges; // top, right, bottom, left
|
|
4011
5708
|
uniform float uDeckleRoughness;
|
|
4012
5709
|
|
|
5710
|
+
/** Gnaw frequency along a torn edge, per world unit \u2014 see {@link plLocal}. */
|
|
5711
|
+
const float PL_DECKLE_GNAW = 26.0;
|
|
5712
|
+
|
|
4013
5713
|
void plDeckle(inout vec4 color) {
|
|
4014
5714
|
// Distance to each selected edge, gnawed by low-frequency noise.
|
|
4015
|
-
|
|
5715
|
+
//
|
|
5716
|
+
// The depth is in world units, taken against the sheet's mean dimension.
|
|
5717
|
+
// Against the MEAN rather than each edge's own span, which is what UV
|
|
5718
|
+
// amounted to: one roughness used to bite a third deeper into the short
|
|
5719
|
+
// edges of a 1 x 1.4 sheet than the long ones, for no reason anybody chose.
|
|
5720
|
+
//
|
|
5721
|
+
// Still proportional to the sheet rather than absolute, which is a decision
|
|
5722
|
+
// and not an oversight. A real deckle is a fibre length and would be the
|
|
5723
|
+
// same depth on any size of sheet; roughness is a 0..1 knob someone types,
|
|
5724
|
+
// and an absolute one would vanish on a poster and swallow a stamp.
|
|
5725
|
+
float depth = (0.012 + uDeckleRoughness * 0.05) * (uSheetSize.x + uSheetSize.y) * 0.5;
|
|
4016
5726
|
float tear = 1.0;
|
|
4017
5727
|
float fiberBand = 0.0;
|
|
4018
5728
|
vec4 dists = vec4(1.0 - vPaperUv.y, 1.0 - vPaperUv.x, vPaperUv.y, vPaperUv.x);
|
|
4019
5729
|
vec4 alongs = vec4(vPaperUv.x, vPaperUv.y, vPaperUv.x, vPaperUv.y);
|
|
5730
|
+
vec4 distScale = vec4(uSheetSize.y, uSheetSize.x, uSheetSize.y, uSheetSize.x);
|
|
5731
|
+
vec4 alongScale = vec4(uSheetSize.x, uSheetSize.y, uSheetSize.x, uSheetSize.y);
|
|
4020
5732
|
for (int e = 0; e < 4; e++) {
|
|
4021
5733
|
if (uDeckleEdges[e] < 0.5) continue;
|
|
4022
|
-
float n = plFbm(vec2(alongs[e] *
|
|
5734
|
+
float n = plFbm(vec2(alongs[e] * alongScale[e] * PL_DECKLE_GNAW, float(e) * 7.31)) - 0.5;
|
|
4023
5735
|
float boundary = depth * (0.55 + n * 1.6);
|
|
4024
|
-
float d = dists[e] - boundary;
|
|
5736
|
+
float d = dists[e] * distScale[e] - boundary;
|
|
4025
5737
|
tear = min(tear, step(0.0, d));
|
|
4026
5738
|
// Lightened fiber band just inside the tear.
|
|
4027
5739
|
fiberBand = max(fiberBand, smoothstep(depth * 1.4, 0.0, d) * step(0.0, d));
|
|
@@ -4034,23 +5746,55 @@ void plDeckle(inout vec4 color) {
|
|
|
4034
5746
|
var CREASE_CHUNK = (
|
|
4035
5747
|
/* glsl */
|
|
4036
5748
|
`
|
|
4037
|
-
uniform float
|
|
4038
|
-
uniform float
|
|
4039
|
-
uniform float
|
|
5749
|
+
uniform float uCreaseAngles[4];
|
|
5750
|
+
uniform float uCreaseStrengths[4];
|
|
5751
|
+
uniform float uCreaseOffsets[4];
|
|
5752
|
+
uniform float uCreaseWidth;
|
|
4040
5753
|
uniform int uCreaseCount;
|
|
4041
5754
|
|
|
5755
|
+
/**
|
|
5756
|
+
* Peak tilt of a crease's own facets, as a slope.
|
|
5757
|
+
*
|
|
5758
|
+
* A gaussian groove of amplitude A and width w reaches a maximum slope of
|
|
5759
|
+
* about 0.86 A/w, so an amplitude of 0.55 w peaks near 25 degrees \u2014 steep
|
|
5760
|
+
* enough that turning the sheet visibly flips the line from dark to bright,
|
|
5761
|
+
* shallow enough that it never reads as a fold in its own right. Held as a
|
|
5762
|
+
* SLOPE rather than a depth because that is the quantity the lighting
|
|
5763
|
+
* responds to, and the only one that stays honest when the width changes.
|
|
5764
|
+
*/
|
|
5765
|
+
const float PL_CREASE_TILT = 0.55;
|
|
5766
|
+
|
|
5767
|
+
/** How much grime a crease traps, at full strength. */
|
|
5768
|
+
const float PL_CREASE_SOIL = 0.1;
|
|
5769
|
+
|
|
4042
5770
|
void plCrease(inout vec4 color, inout float rough) {
|
|
4043
|
-
vec2
|
|
4044
|
-
// Coordinate across the crease lines (0..1 over the sheet).
|
|
4045
|
-
float t = dot(vPaperUv - 0.5, vec2(-dir.y, dir.x)) + 0.5;
|
|
5771
|
+
vec2 p = plLocal();
|
|
4046
5772
|
for (int i = 0; i < 4; i++) {
|
|
4047
5773
|
if (i >= uCreaseCount) break;
|
|
4048
|
-
|
|
4049
|
-
|
|
4050
|
-
|
|
4051
|
-
|
|
4052
|
-
|
|
4053
|
-
|
|
5774
|
+
vec2 dir = vec2(cos(uCreaseAngles[i]), sin(uCreaseAngles[i]));
|
|
5775
|
+
// The identical measurement the fold deformer displaces by: signed
|
|
5776
|
+
// distance across the line, in the sheet's own space. Shading and
|
|
5777
|
+
// geometry cannot place a crease differently when the number they place
|
|
5778
|
+
// it by is the same number.
|
|
5779
|
+
float s = (dot(p, dir) - uCreaseOffsets[i]) / uCreaseWidth;
|
|
5780
|
+
float strength = uCreaseStrengths[i];
|
|
5781
|
+
float bell = plBell(s);
|
|
5782
|
+
|
|
5783
|
+
// The relief. This is the fine burnished line where the fibres broke, and
|
|
5784
|
+
// it is deliberately narrower than the hinge the fold deformer bends
|
|
5785
|
+
// over: the mesh carries the wide bend, the shader carries the crease
|
|
5786
|
+
// inside it, and the two add up instead of competing. Signed, so a
|
|
5787
|
+
// mountain stands proud and a valley cuts in \u2014 the same crease read from
|
|
5788
|
+
// the other side of the sheet is the other one.
|
|
5789
|
+
plHeight += strength * uCreaseWidth * PL_CREASE_TILT * bell;
|
|
5790
|
+
|
|
5791
|
+
// What is left for the albedo once the lighting is doing the work: a
|
|
5792
|
+
// crease collects dirt and its broken fibres scatter wider. The grey band
|
|
5793
|
+
// and the painted-on sheen that used to live here were standing in for a
|
|
5794
|
+
// normal, and there is one now.
|
|
5795
|
+
float mark = bell * abs(strength);
|
|
5796
|
+
color.rgb *= 1.0 - mark * PL_CREASE_SOIL;
|
|
5797
|
+
rough = clamp(rough + mark * 0.3, 0.0, 1.0);
|
|
4054
5798
|
}
|
|
4055
5799
|
}
|
|
4056
5800
|
`
|
|
@@ -4062,7 +5806,6 @@ uniform vec4 uPerfEdges; // top, right, bottom, left enabled
|
|
|
4062
5806
|
uniform vec4 uPerfTorn; // 1 = ripped-through profile, 0 = clean punches
|
|
4063
5807
|
uniform float uPerfRadius; // world units
|
|
4064
5808
|
uniform float uPerfSpacing;
|
|
4065
|
-
uniform vec2 uSheetSize;
|
|
4066
5809
|
|
|
4067
5810
|
void plPerforation(inout vec4 color) {
|
|
4068
5811
|
// Per-edge distance/along coordinates, converted from UV to world units so
|
|
@@ -4113,11 +5856,10 @@ void plAging(inout vec4 color) {
|
|
|
4113
5856
|
}
|
|
4114
5857
|
`
|
|
4115
5858
|
);
|
|
4116
|
-
function composeSurface(surface, stock, thickness, maps = { hasFrontMap: false, hasBackMap: false }, sheet2 = { width: 1, height: 1.4 }, lighting = "studio") {
|
|
5859
|
+
function composeSurface(surface, stock, thickness, maps = { hasFrontMap: false, hasBackMap: false }, sheet2 = { width: 1, height: 1.4 }, lighting = "studio", creases = resolveCreases(surface, [], sheet2)) {
|
|
4117
5860
|
const grain = surface.grain ?? stock.defaultSurface.grain;
|
|
4118
5861
|
const aging = surface.aging ?? stock.defaultSurface.aging;
|
|
4119
5862
|
const deckle = surface.deckle;
|
|
4120
|
-
const creases = surface.creaseLines;
|
|
4121
5863
|
const perforation = surface.perforation;
|
|
4122
5864
|
const banding = stock.banding;
|
|
4123
5865
|
const showThrough = stock.adhesive ? 0 : surface.showThrough ?? stock.showThrough;
|
|
@@ -4130,6 +5872,9 @@ function composeSurface(surface, stock, thickness, maps = { hasFrontMap: false,
|
|
|
4130
5872
|
value: stock.adhesive ? 1 : 1 - Math.min(0.45, 0.12 + thickness * 0.9) * stock.opacity
|
|
4131
5873
|
},
|
|
4132
5874
|
uStockColor: { value: new THREE9.Color(stock.color) },
|
|
5875
|
+
// Always present, not just when something asks for it: every effect that
|
|
5876
|
+
// measures anything measures in the sheet's own space now — see plLocal.
|
|
5877
|
+
uSheetSize: { value: new THREE9.Vector2(sheet2.width, sheet2.height) },
|
|
4133
5878
|
uOpacity: { value: stock.opacity },
|
|
4134
5879
|
uShowThrough: { value: showThrough },
|
|
4135
5880
|
// Always compiled in: the shader early-outs at zero translucency, which
|
|
@@ -4162,21 +5907,22 @@ function composeSurface(surface, stock, thickness, maps = { hasFrontMap: false,
|
|
|
4162
5907
|
};
|
|
4163
5908
|
uniforms.uPerfRadius = { value: perforation.holeRadius };
|
|
4164
5909
|
uniforms.uPerfSpacing = { value: perforation.spacing };
|
|
4165
|
-
uniforms.uSheetSize = { value: new THREE9.Vector2(sheet2.width, sheet2.height) };
|
|
4166
5910
|
}
|
|
4167
|
-
if (creases) {
|
|
5911
|
+
if (creases.length > 0) {
|
|
4168
5912
|
chunks.push(CREASE_CHUNK);
|
|
4169
5913
|
calls.push("plCrease(csm_DiffuseColor, csm_Roughness);");
|
|
4170
|
-
uniforms.
|
|
4171
|
-
uniforms.
|
|
4172
|
-
uniforms.
|
|
4173
|
-
uniforms.
|
|
5914
|
+
uniforms.uCreaseAngles = { value: pad(creases.map((c) => c.angle * Math.PI / 180)) };
|
|
5915
|
+
uniforms.uCreaseStrengths = { value: pad(creases.map((c) => c.strength)) };
|
|
5916
|
+
uniforms.uCreaseOffsets = { value: pad(creases.map((c) => c.offset)) };
|
|
5917
|
+
uniforms.uCreaseWidth = { value: CREASE_RADIUS * SHADED_CREASE };
|
|
5918
|
+
uniforms.uCreaseCount = { value: Math.min(creases.length, 4) };
|
|
4174
5919
|
}
|
|
4175
5920
|
if (aging !== void 0) {
|
|
4176
5921
|
chunks.push(AGING_CHUNK);
|
|
4177
5922
|
calls.push("plAging(csm_DiffuseColor);");
|
|
4178
5923
|
uniforms.uAgingAmount = { value: aging };
|
|
4179
5924
|
}
|
|
5925
|
+
const relief = grain !== void 0 || creases.length > 0;
|
|
4180
5926
|
const frontExpr = maps.hasFrontMap ? "texture2D(uFrontMap, vPaperUv).rgb" : "uStockColor";
|
|
4181
5927
|
const backBaseExpr = stock.adhesive ? "vec3(0.965, 0.96, 0.945)" : maps.hasBackMap ? "texture2D(uBackMap, vec2(1.0 - vPaperUv.x, vPaperUv.y)).rgb" : "uStockColor";
|
|
4182
5928
|
const fragmentShader = (
|
|
@@ -4191,6 +5937,7 @@ ${maps.hasBackMap && !stock.adhesive ? "uniform sampler2D uBackMap;" : ""}
|
|
|
4191
5937
|
${TRANSLUCENCY_FRAGMENT}
|
|
4192
5938
|
${chunks.join("\n")}
|
|
4193
5939
|
void main() {
|
|
5940
|
+
plHeight = 0.0;
|
|
4194
5941
|
vec3 front = ${frontExpr};
|
|
4195
5942
|
if (gl_FrontFacing) {
|
|
4196
5943
|
csm_DiffuseColor = vec4(front, uOpacity);
|
|
@@ -4199,6 +5946,7 @@ void main() {
|
|
|
4199
5946
|
csm_DiffuseColor = vec4(backBase * mix(vec3(1.0), front, uShowThrough), uOpacity);
|
|
4200
5947
|
}
|
|
4201
5948
|
${calls.join("\n ")}
|
|
5949
|
+
${relief ? " // The relief every effect above described, spent once \u2014 see plPerturb.\n csm_FragNormal = plPerturb(csm_FragNormal, plHeight);" : ""}
|
|
4202
5950
|
if (!gl_FrontFacing) csm_DiffuseColor.rgb *= uBackDarken;
|
|
4203
5951
|
${stock.adhesive ? "// Adhesive underside: higher specular than the printed face.\n if (!gl_FrontFacing) csm_Roughness = 0.18;" : ""}
|
|
4204
5952
|
// What the key light pushes through the sheet, filtered by the ink on it.
|
|
@@ -4210,7 +5958,7 @@ void main() {
|
|
|
4210
5958
|
structureKey: `${[
|
|
4211
5959
|
grain !== void 0 || banding > 0 ? "g" : "",
|
|
4212
5960
|
deckle ? "d" : "",
|
|
4213
|
-
creases ? "c" : "",
|
|
5961
|
+
creases.length > 0 ? "c" : "",
|
|
4214
5962
|
aging !== void 0 ? "a" : "",
|
|
4215
5963
|
perforation ? "p" : "",
|
|
4216
5964
|
stock.adhesive ? "A" : ""
|
|
@@ -4221,9 +5969,9 @@ void main() {
|
|
|
4221
5969
|
alphaTest: deckle || perforation ? 0.5 : 0
|
|
4222
5970
|
};
|
|
4223
5971
|
}
|
|
4224
|
-
function
|
|
4225
|
-
const out =
|
|
4226
|
-
while (out.length < 4) out.push(
|
|
5972
|
+
function pad(values, fill = 0) {
|
|
5973
|
+
const out = values.slice(0, 4);
|
|
5974
|
+
while (out.length < 4) out.push(fill);
|
|
4227
5975
|
return out;
|
|
4228
5976
|
}
|
|
4229
5977
|
|
|
@@ -4247,7 +5995,8 @@ function PaperMaterial({
|
|
|
4247
5995
|
surface,
|
|
4248
5996
|
thickness,
|
|
4249
5997
|
sheet: sheet2,
|
|
4250
|
-
lighting = "studio"
|
|
5998
|
+
lighting = "studio",
|
|
5999
|
+
creases
|
|
4251
6000
|
}) {
|
|
4252
6001
|
const rig = useLightRig(lighting);
|
|
4253
6002
|
const composed = composeSurface(
|
|
@@ -4259,7 +6008,8 @@ function PaperMaterial({
|
|
|
4259
6008
|
hasBackMap: Boolean(backTexture)
|
|
4260
6009
|
},
|
|
4261
6010
|
sheet2,
|
|
4262
|
-
rig
|
|
6011
|
+
rig,
|
|
6012
|
+
creases ?? resolveCreases(surface, [], sheet2 ?? { width: 1, height: 1.4 })
|
|
4263
6013
|
);
|
|
4264
6014
|
const bound = (0, import_react9.useMemo)(() => composed.uniforms, [composed.structureKey]);
|
|
4265
6015
|
(0, import_react9.useEffect)(() => {
|
|
@@ -4594,6 +6344,7 @@ function configInputs(props) {
|
|
|
4594
6344
|
props.behavior ?? null,
|
|
4595
6345
|
props.deformers ?? null,
|
|
4596
6346
|
props.surface ?? null,
|
|
6347
|
+
props.memory ?? null,
|
|
4597
6348
|
props.scene ?? null,
|
|
4598
6349
|
props.physics ?? null,
|
|
4599
6350
|
props.onTwos ?? null
|
|
@@ -4612,6 +6363,7 @@ function resolveConfig(props) {
|
|
|
4612
6363
|
if (props.behavior) overrides.behavior = props.behavior;
|
|
4613
6364
|
if (props.deformers) overrides.deformers = props.deformers;
|
|
4614
6365
|
if (props.surface) overrides.surface = { ...base.surface, ...props.surface };
|
|
6366
|
+
if (props.memory) overrides.memory = { ...base.memory, ...props.memory };
|
|
4615
6367
|
if (props.scene) overrides.scene = { ...base.scene, ...props.scene };
|
|
4616
6368
|
if (props.physics) overrides.physics = props.physics;
|
|
4617
6369
|
if (props.onTwos !== void 0) overrides.onTwos = props.onTwos;
|
|
@@ -4639,7 +6391,9 @@ var PaperMesh = (0, import_react11.forwardRef)(function PaperMesh2(props, ref) {
|
|
|
4639
6391
|
machineRef.current = machine;
|
|
4640
6392
|
const resolvedRef = (0, import_react11.useRef)(resolved);
|
|
4641
6393
|
resolvedRef.current = resolved;
|
|
4642
|
-
const
|
|
6394
|
+
const simKind = !reduced && typeof config.physics === "object" ? config.physics.type : null;
|
|
6395
|
+
const isCloth = simKind === "cloth";
|
|
6396
|
+
const isStrip = simKind === "strip";
|
|
4643
6397
|
const idle = !reduced && typeof config.physics === "string" && config.physics !== "none" ? getIdlePreset(config.physics) : null;
|
|
4644
6398
|
const meshRef = (0, import_react11.useRef)(null);
|
|
4645
6399
|
const groupRef = (0, import_react11.useRef)(null);
|
|
@@ -4651,19 +6405,33 @@ var PaperMesh = (0, import_react11.forwardRef)(function PaperMesh2(props, ref) {
|
|
|
4651
6405
|
const draggingRef = (0, import_react11.useRef)(null);
|
|
4652
6406
|
const configRef = (0, import_react11.useRef)(config);
|
|
4653
6407
|
configRef.current = config;
|
|
6408
|
+
const baseRotation = (0, import_react11.useMemo)(() => {
|
|
6409
|
+
const [rx, ry, rz] = props.rotation ?? [0, 0, 0];
|
|
6410
|
+
return [rx, ry + config.scene.turn * Math.PI / 180, rz];
|
|
6411
|
+
}, [props.rotation, config.scene.turn]);
|
|
4654
6412
|
const controls = (0, import_fiber4.useThree)((s) => s.controls);
|
|
4655
6413
|
const camera = (0, import_fiber4.useThree)((s) => s.camera);
|
|
4656
6414
|
const behaviorKey = JSON.stringify(config.behavior ?? null);
|
|
4657
6415
|
const deformersKey = JSON.stringify(config.deformers ?? null);
|
|
4658
6416
|
const sheetKey = JSON.stringify(config.sheet);
|
|
4659
|
-
const physicsKey =
|
|
6417
|
+
const physicsKey = typeof config.physics === "object" ? config.physics.type : config.physics;
|
|
6418
|
+
const memoryKey = config.memory.creases.map((c) => `${c.angle}:${c.offset}`).join("|");
|
|
6419
|
+
const creaseKey = config.memory.creases.map((c) => `${c.angle}:${c.offset}:${c.depth}`).join("|");
|
|
6420
|
+
const creasesRef = (0, import_react11.useRef)(null);
|
|
6421
|
+
creasesRef.current ??= new CreaseTracker(config.memory.creases);
|
|
6422
|
+
const creases = creasesRef.current;
|
|
4660
6423
|
(0, import_react11.useEffect)(() => {
|
|
4661
6424
|
if (!draggingRef.current && !playingRef.current) overridesRef.current = {};
|
|
6425
|
+
creases.reset();
|
|
4662
6426
|
dirtyRef.current = true;
|
|
4663
6427
|
}, [behaviorKey, deformersKey, sheetKey, physicsKey]);
|
|
6428
|
+
(0, import_react11.useEffect)(() => {
|
|
6429
|
+
creases.adopt(configRef.current.memory.creases);
|
|
6430
|
+
dirtyRef.current = true;
|
|
6431
|
+
}, [creaseKey]);
|
|
4664
6432
|
const { minSegments, autoSegments, animatedStack } = (0, import_react11.useMemo)(() => {
|
|
4665
6433
|
const cfg = configRef.current;
|
|
4666
|
-
const probe2 = buildStack(cfg, {});
|
|
6434
|
+
const probe2 = withMemory(buildStack(cfg, {}), cfg);
|
|
4667
6435
|
if (!probe2) {
|
|
4668
6436
|
return {
|
|
4669
6437
|
minSegments: [2, 2],
|
|
@@ -4676,7 +6444,7 @@ var PaperMesh = (0, import_react11.forwardRef)(function PaperMesh2(props, ref) {
|
|
|
4676
6444
|
if (cfg.behavior && !cfg.deformers) {
|
|
4677
6445
|
const param = getBehavior(cfg.behavior.type).progressParam;
|
|
4678
6446
|
for (const p of PROGRESS_SAMPLES) {
|
|
4679
|
-
const at = buildStack(cfg, { [param]: p });
|
|
6447
|
+
const at = withMemory(buildStack(cfg, { [param]: p }), cfg);
|
|
4680
6448
|
if (!at) continue;
|
|
4681
6449
|
const [x, y] = stackAutoSegments(at, cfg.sheet);
|
|
4682
6450
|
if (x > want[0]) want[0] = x;
|
|
@@ -4689,30 +6457,65 @@ var PaperMesh = (0, import_react11.forwardRef)(function PaperMesh2(props, ref) {
|
|
|
4689
6457
|
autoSegments: want,
|
|
4690
6458
|
animatedStack: animated
|
|
4691
6459
|
};
|
|
4692
|
-
}, [behaviorKey, deformersKey, physicsKey]);
|
|
6460
|
+
}, [behaviorKey, deformersKey, physicsKey, memoryKey]);
|
|
4693
6461
|
const geometry = (0, import_react11.useMemo)(() => {
|
|
6462
|
+
if (isStrip) {
|
|
6463
|
+
const strip = configRef.current.physics;
|
|
6464
|
+
const nodes = stripNodeCount(config.sheet.height, strip.perforation);
|
|
6465
|
+
return new THREE11.PlaneGeometry(config.sheet.width, config.sheet.height, 1, nodes - 1);
|
|
6466
|
+
}
|
|
4694
6467
|
if (!isCloth) return createSheetGeometry(config.sheet, minSegments, autoSegments);
|
|
4695
|
-
const [sx, sy] = resolveSegments(config.sheet,
|
|
6468
|
+
const [sx, sy] = resolveSegments(config.sheet, minSegments);
|
|
4696
6469
|
const capped = Math.min(Math.max(sx, sy), CLOTH_MAX_SEGMENTS);
|
|
4697
6470
|
return new THREE11.PlaneGeometry(config.sheet.width, config.sheet.height, capped, capped);
|
|
4698
|
-
}, [
|
|
6471
|
+
}, [
|
|
6472
|
+
sheetKey,
|
|
6473
|
+
minSegments,
|
|
6474
|
+
autoSegments,
|
|
6475
|
+
isCloth,
|
|
6476
|
+
isStrip,
|
|
6477
|
+
isStrip ? config.physics.perforation : 0
|
|
6478
|
+
]);
|
|
4699
6479
|
(0, import_react11.useEffect)(() => () => geometry.dispose(), [geometry]);
|
|
4700
6480
|
const basePositions = (0, import_react11.useMemo)(
|
|
4701
6481
|
() => Float32Array.from(geometry.attributes.position.array),
|
|
4702
6482
|
[geometry]
|
|
4703
6483
|
);
|
|
6484
|
+
const stripSim = (0, import_react11.useMemo)(() => {
|
|
6485
|
+
if (!isStrip) return null;
|
|
6486
|
+
const strip = configRef.current.physics;
|
|
6487
|
+
return new StripSim(config.sheet.height, config.sheet.width, {
|
|
6488
|
+
scroll: strip.scroll,
|
|
6489
|
+
tightness: strip.tightness,
|
|
6490
|
+
core: strip.core,
|
|
6491
|
+
tail: strip.tail,
|
|
6492
|
+
perforation: strip.perforation,
|
|
6493
|
+
crease: strip.crease,
|
|
6494
|
+
stiffness: strip.stiffness,
|
|
6495
|
+
drag: strip.drag,
|
|
6496
|
+
gravity: strip.gravity,
|
|
6497
|
+
floor: strip.floor,
|
|
6498
|
+
inertia: strip.inertia
|
|
6499
|
+
});
|
|
6500
|
+
}, [geometry, isStrip]);
|
|
6501
|
+
const lastSimRef = (0, import_react11.useRef)(null);
|
|
4704
6502
|
const sim = (0, import_react11.useMemo)(() => {
|
|
4705
6503
|
if (!isCloth) return null;
|
|
4706
6504
|
const cloth = configRef.current.physics;
|
|
4707
6505
|
const cols = geometry.parameters.widthSegments + 1;
|
|
4708
6506
|
const rows = geometry.parameters.heightSegments + 1;
|
|
4709
|
-
|
|
6507
|
+
const next = new ClothSim(cols, rows, config.sheet.width, config.sheet.height, cloth.pins, {
|
|
4710
6508
|
stiffness: cloth.stiffness,
|
|
4711
6509
|
gravity: cloth.gravity,
|
|
4712
6510
|
wind: cloth.wind,
|
|
4713
6511
|
floor: cloth.floor
|
|
4714
6512
|
});
|
|
6513
|
+
next.adopt(lastSimRef.current);
|
|
6514
|
+
return next;
|
|
4715
6515
|
}, [geometry, isCloth, isCloth ? config.physics.pins : ""]);
|
|
6516
|
+
(0, import_react11.useEffect)(() => {
|
|
6517
|
+
lastSimRef.current = sim;
|
|
6518
|
+
}, [sim]);
|
|
4716
6519
|
const stock = getStock(config.stock);
|
|
4717
6520
|
const texture = useContentTexture(config.content, config.sheet, stock);
|
|
4718
6521
|
const backTexture = useContentTexture(config.content.back, config.sheet, stock);
|
|
@@ -4807,6 +6610,10 @@ var PaperMesh = (0, import_react11.forwardRef)(function PaperMesh2(props, ref) {
|
|
|
4807
6610
|
placeProgrammatic: () => machineRef.current?.placeProgrammatic() ?? false,
|
|
4808
6611
|
returnProgrammatic: () => machineRef.current?.returnProgrammatic() ?? false
|
|
4809
6612
|
}));
|
|
6613
|
+
const shadedCreases = (0, import_react11.useMemo)(
|
|
6614
|
+
() => resolveCreases(config.surface, config.memory.creases, config.sheet),
|
|
6615
|
+
[config.surface, config.memory.creases, config.sheet]
|
|
6616
|
+
);
|
|
4810
6617
|
const idlePose = (0, import_react11.useRef)({ position: [0, 0, 0], rotation: [0, 0, 0] });
|
|
4811
6618
|
(0, import_fiber4.useFrame)(({ clock }, delta) => {
|
|
4812
6619
|
const cfg = liveConfig();
|
|
@@ -4819,10 +6626,10 @@ var PaperMesh = (0, import_react11.forwardRef)(function PaperMesh2(props, ref) {
|
|
|
4819
6626
|
idle?.transform?.(now, pose);
|
|
4820
6627
|
if (hasBehaviorTransform) {
|
|
4821
6628
|
const o = effectiveOptions(now);
|
|
4822
|
-
if (o) behavior.transform(o, now, pose);
|
|
6629
|
+
if (o) behavior.transform(o, now, pose, cfg.sheet);
|
|
4823
6630
|
}
|
|
4824
6631
|
const base = props.position ?? [0, 0, 0];
|
|
4825
|
-
const baseRot =
|
|
6632
|
+
const baseRot = baseRotation;
|
|
4826
6633
|
groupRef.current.position.set(
|
|
4827
6634
|
base[0] + pose.position[0],
|
|
4828
6635
|
base[1] + pose.position[1],
|
|
@@ -4834,6 +6641,56 @@ var PaperMesh = (0, import_react11.forwardRef)(function PaperMesh2(props, ref) {
|
|
|
4834
6641
|
baseRot[2] + pose.rotation[2]
|
|
4835
6642
|
);
|
|
4836
6643
|
}
|
|
6644
|
+
if (isStrip && stripSim) {
|
|
6645
|
+
const strip = cfg.physics;
|
|
6646
|
+
stripSim.setParams({
|
|
6647
|
+
scroll: strip.scroll,
|
|
6648
|
+
tightness: strip.tightness,
|
|
6649
|
+
core: strip.core,
|
|
6650
|
+
tail: strip.tail,
|
|
6651
|
+
crease: strip.crease,
|
|
6652
|
+
stiffness: strip.stiffness,
|
|
6653
|
+
drag: strip.drag,
|
|
6654
|
+
gravity: strip.gravity,
|
|
6655
|
+
floor: strip.floor,
|
|
6656
|
+
inertia: strip.inertia
|
|
6657
|
+
});
|
|
6658
|
+
stripSim.step(delta);
|
|
6659
|
+
if (!stripSim.asleep) {
|
|
6660
|
+
const position = geometry.attributes.position;
|
|
6661
|
+
stripSim.writeInto(position.array);
|
|
6662
|
+
position.needsUpdate = true;
|
|
6663
|
+
computeSheetNormals(geometry);
|
|
6664
|
+
geometry.computeBoundingSphere();
|
|
6665
|
+
}
|
|
6666
|
+
return;
|
|
6667
|
+
}
|
|
6668
|
+
const applyShape = (base, force) => {
|
|
6669
|
+
const animated = !reduced && animatedStack;
|
|
6670
|
+
const hasLoop = !reduced && Boolean(cfg.behavior && behavior?.loop);
|
|
6671
|
+
const machineAnimating = Boolean(machineRef.current?.transitioning);
|
|
6672
|
+
if (!force && !dirtyRef.current && !hasLoop && !animated && !machineAnimating) return false;
|
|
6673
|
+
const raw = buildStack(cfg, overridesRef.current, behavior, now);
|
|
6674
|
+
const setAmount = cfg.memory.set ?? stock.takesSet;
|
|
6675
|
+
if (raw && creases.observe(raw, setAmount)) props.onCrease?.(creases.creases);
|
|
6676
|
+
const stack = withMemory(raw, cfg, creases.creases);
|
|
6677
|
+
if (!stack) return false;
|
|
6678
|
+
dirtyRef.current = false;
|
|
6679
|
+
const ctx = { t: now, sheet: cfg.sheet };
|
|
6680
|
+
applyDeformerStack(geometry, base, stack, ctx);
|
|
6681
|
+
if (props.interactive && behavior?.handles) {
|
|
6682
|
+
const o = effectiveOptions(now);
|
|
6683
|
+
behavior.handles.forEach((h, i) => {
|
|
6684
|
+
const mesh = handleRefs.current[i];
|
|
6685
|
+
if (!mesh || !o) return;
|
|
6686
|
+
const [u, v] = h.anchor(o, cfg.sheet);
|
|
6687
|
+
anchorScratch.set((u - 0.5) * cfg.sheet.width, (v - 0.5) * cfg.sheet.height, 0);
|
|
6688
|
+
displacePoint(anchorScratch, u, v, stack, ctx);
|
|
6689
|
+
mesh.position.copy(anchorScratch);
|
|
6690
|
+
});
|
|
6691
|
+
}
|
|
6692
|
+
return true;
|
|
6693
|
+
};
|
|
4837
6694
|
if (isCloth && sim) {
|
|
4838
6695
|
const cloth = cfg.physics;
|
|
4839
6696
|
sim.setParams({
|
|
@@ -4843,7 +6700,8 @@ var PaperMesh = (0, import_react11.forwardRef)(function PaperMesh2(props, ref) {
|
|
|
4843
6700
|
floor: cloth.floor
|
|
4844
6701
|
});
|
|
4845
6702
|
sim.step(delta);
|
|
4846
|
-
|
|
6703
|
+
const moved = !sim.asleep;
|
|
6704
|
+
if (!applyShape(sim.positions, moved) && moved) {
|
|
4847
6705
|
const position = geometry.attributes.position;
|
|
4848
6706
|
position.array.set(sim.positions);
|
|
4849
6707
|
position.needsUpdate = true;
|
|
@@ -4851,26 +6709,7 @@ var PaperMesh = (0, import_react11.forwardRef)(function PaperMesh2(props, ref) {
|
|
|
4851
6709
|
}
|
|
4852
6710
|
return;
|
|
4853
6711
|
}
|
|
4854
|
-
|
|
4855
|
-
const hasLoop = !reduced && Boolean(cfg.behavior && behavior?.loop);
|
|
4856
|
-
const machineAnimating = Boolean(machineRef.current?.transitioning);
|
|
4857
|
-
if (!dirtyRef.current && !hasLoop && !animated && !machineAnimating) return;
|
|
4858
|
-
const stack = buildStack(cfg, overridesRef.current, behavior, now);
|
|
4859
|
-
if (!stack) return;
|
|
4860
|
-
dirtyRef.current = false;
|
|
4861
|
-
const ctx = { t: now, sheet: cfg.sheet };
|
|
4862
|
-
applyDeformerStack(geometry, basePositions, stack, ctx);
|
|
4863
|
-
if (props.interactive && behavior?.handles) {
|
|
4864
|
-
const o = effectiveOptions(now);
|
|
4865
|
-
behavior.handles.forEach((h, i) => {
|
|
4866
|
-
const mesh = handleRefs.current[i];
|
|
4867
|
-
if (!mesh || !o) return;
|
|
4868
|
-
const [u, v] = h.anchor(o, cfg.sheet);
|
|
4869
|
-
anchorScratch.set((u - 0.5) * cfg.sheet.width, (v - 0.5) * cfg.sheet.height, 0);
|
|
4870
|
-
displacePoint(anchorScratch, u, v, stack, ctx);
|
|
4871
|
-
mesh.position.copy(anchorScratch);
|
|
4872
|
-
});
|
|
4873
|
-
}
|
|
6712
|
+
applyShape(basePositions, false);
|
|
4874
6713
|
});
|
|
4875
6714
|
const localDragPoint = (e) => {
|
|
4876
6715
|
const group = groupRef.current;
|
|
@@ -4894,13 +6733,22 @@ var PaperMesh = (0, import_react11.forwardRef)(function PaperMesh2(props, ref) {
|
|
|
4894
6733
|
if (typeof p === "number") props.onProgress?.(p);
|
|
4895
6734
|
};
|
|
4896
6735
|
const grabAnchor = (0, import_react11.useRef)(new THREE11.Vector3());
|
|
6736
|
+
const grabOffset = (0, import_react11.useRef)(new THREE11.Vector3());
|
|
4897
6737
|
const clothDown = (e) => {
|
|
4898
6738
|
if (!isCloth || !sim || !props.interactive || !groupRef.current) return;
|
|
4899
6739
|
e.stopPropagation();
|
|
4900
6740
|
if (controls) controls.enabled = false;
|
|
4901
6741
|
grabAnchor.current.copy(e.point);
|
|
4902
6742
|
const local = groupRef.current.worldToLocal(worldScratch.copy(e.point));
|
|
4903
|
-
|
|
6743
|
+
const drawn = geometry.attributes.position.array;
|
|
6744
|
+
const index = nearestVertex(drawn, sim.count, local.x, local.y, local.z);
|
|
6745
|
+
sim.grab(index);
|
|
6746
|
+
const i3 = index * 3;
|
|
6747
|
+
grabOffset.current.set(
|
|
6748
|
+
drawn[i3] - sim.positions[i3],
|
|
6749
|
+
drawn[i3 + 1] - sim.positions[i3 + 1],
|
|
6750
|
+
drawn[i3 + 2] - sim.positions[i3 + 2]
|
|
6751
|
+
);
|
|
4904
6752
|
draggingRef.current = "cloth";
|
|
4905
6753
|
e.target.setPointerCapture(e.pointerId);
|
|
4906
6754
|
};
|
|
@@ -4911,7 +6759,8 @@ var PaperMesh = (0, import_react11.forwardRef)(function PaperMesh2(props, ref) {
|
|
|
4911
6759
|
const hit = e.ray.intersectPlane(dragPlane, dragPoint);
|
|
4912
6760
|
if (!hit) return;
|
|
4913
6761
|
groupRef.current.worldToLocal(hit);
|
|
4914
|
-
|
|
6762
|
+
const offset = grabOffset.current;
|
|
6763
|
+
sim.moveGrab(hit.x - offset.x, hit.y - offset.y, hit.z - offset.z);
|
|
4915
6764
|
};
|
|
4916
6765
|
const clothUp = (e) => {
|
|
4917
6766
|
if (draggingRef.current !== "cloth" || !sim) return;
|
|
@@ -4920,8 +6769,34 @@ var PaperMesh = (0, import_react11.forwardRef)(function PaperMesh2(props, ref) {
|
|
|
4920
6769
|
sim.release();
|
|
4921
6770
|
e.target.releasePointerCapture(e.pointerId);
|
|
4922
6771
|
};
|
|
6772
|
+
const stripDown = (e) => {
|
|
6773
|
+
if (!isStrip || !stripSim || !props.interactive || !groupRef.current) return;
|
|
6774
|
+
e.stopPropagation();
|
|
6775
|
+
const local = groupRef.current.worldToLocal(worldScratch.copy(e.point));
|
|
6776
|
+
if (stripSim.grabNearest(local.y, local.z) < 0) return;
|
|
6777
|
+
if (controls) controls.enabled = false;
|
|
6778
|
+
grabAnchor.current.copy(e.point);
|
|
6779
|
+
draggingRef.current = "strip";
|
|
6780
|
+
e.target.setPointerCapture(e.pointerId);
|
|
6781
|
+
};
|
|
6782
|
+
const stripMove = (e) => {
|
|
6783
|
+
if (draggingRef.current !== "strip" || !stripSim || !groupRef.current) return;
|
|
6784
|
+
camera.getWorldDirection(planeNormal);
|
|
6785
|
+
dragPlane.setFromNormalAndCoplanarPoint(planeNormal, grabAnchor.current);
|
|
6786
|
+
const hit = e.ray.intersectPlane(dragPlane, dragPoint);
|
|
6787
|
+
if (!hit) return;
|
|
6788
|
+
groupRef.current.worldToLocal(hit);
|
|
6789
|
+
stripSim.moveGrab(hit.y, hit.z);
|
|
6790
|
+
};
|
|
6791
|
+
const stripUp = (e) => {
|
|
6792
|
+
if (draggingRef.current !== "strip" || !stripSim) return;
|
|
6793
|
+
draggingRef.current = null;
|
|
6794
|
+
if (controls) controls.enabled = true;
|
|
6795
|
+
stripSim.release();
|
|
6796
|
+
e.target.releasePointerCapture(e.pointerId);
|
|
6797
|
+
};
|
|
4923
6798
|
const sendState = (event) => machineRef.current?.send(event);
|
|
4924
|
-
return /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("group", { ref: groupRef, position: props.position, rotation:
|
|
6799
|
+
return /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("group", { ref: groupRef, position: props.position, rotation: baseRotation, children: [
|
|
4925
6800
|
/* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
|
|
4926
6801
|
"mesh",
|
|
4927
6802
|
{
|
|
@@ -4932,13 +6807,15 @@ var PaperMesh = (0, import_react11.forwardRef)(function PaperMesh2(props, ref) {
|
|
|
4932
6807
|
frustumCulled: false,
|
|
4933
6808
|
onPointerOver: statesLive ? () => sendState("enter") : void 0,
|
|
4934
6809
|
onPointerOut: statesLive ? () => sendState("leave") : void 0,
|
|
4935
|
-
onPointerDown: isCloth || statesLive ? (e) => {
|
|
6810
|
+
onPointerDown: isCloth || isStrip || statesLive ? (e) => {
|
|
4936
6811
|
if (isCloth) clothDown(e);
|
|
6812
|
+
if (isStrip) stripDown(e);
|
|
4937
6813
|
if (statesLive) sendState("down");
|
|
4938
6814
|
} : void 0,
|
|
4939
|
-
onPointerMove: isCloth ? clothMove : void 0,
|
|
4940
|
-
onPointerUp: isCloth || statesLive ? (e) => {
|
|
6815
|
+
onPointerMove: isCloth ? clothMove : isStrip ? stripMove : void 0,
|
|
6816
|
+
onPointerUp: isCloth || isStrip || statesLive ? (e) => {
|
|
4941
6817
|
if (isCloth) clothUp(e);
|
|
6818
|
+
if (isStrip) stripUp(e);
|
|
4942
6819
|
if (statesLive) sendState("up");
|
|
4943
6820
|
} : void 0,
|
|
4944
6821
|
children: /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
|
|
@@ -4950,12 +6827,17 @@ var PaperMesh = (0, import_react11.forwardRef)(function PaperMesh2(props, ref) {
|
|
|
4950
6827
|
surface: config.surface,
|
|
4951
6828
|
thickness: config.sheet.thickness,
|
|
4952
6829
|
sheet: config.sheet,
|
|
4953
|
-
lighting: config.scene.lighting
|
|
6830
|
+
lighting: config.scene.lighting,
|
|
6831
|
+
creases: shadedCreases
|
|
4954
6832
|
}
|
|
4955
6833
|
)
|
|
4956
6834
|
}
|
|
4957
6835
|
),
|
|
4958
|
-
props.interactive &&
|
|
6836
|
+
props.interactive && // Any simulation, not cloth alone: a sim owns the vertices, so there
|
|
6837
|
+
// is no deformer stack for a handle to drive. Harmless as `!isCloth`
|
|
6838
|
+
// only because the schema makes a sim and a behavior exclusive — the
|
|
6839
|
+
// intent is what is written here.
|
|
6840
|
+
!simKind && behavior?.handles?.map((h, i) => /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(
|
|
4959
6841
|
"mesh",
|
|
4960
6842
|
{
|
|
4961
6843
|
userData: { paperlabChrome: true },
|
|
@@ -4986,8 +6868,32 @@ var PaperMesh = (0, import_react11.forwardRef)(function PaperMesh2(props, ref) {
|
|
|
4986
6868
|
))
|
|
4987
6869
|
] });
|
|
4988
6870
|
});
|
|
6871
|
+
function withMemory(stack, config, creases = config.memory.creases) {
|
|
6872
|
+
if (isStripConfig(config.physics)) return null;
|
|
6873
|
+
const out = applyMemory(stack ?? [], creases);
|
|
6874
|
+
return out.length > 0 ? out : null;
|
|
6875
|
+
}
|
|
6876
|
+
function nearestVertex(array, count, x, y, z30) {
|
|
6877
|
+
let best = -1;
|
|
6878
|
+
let bestDist = Infinity;
|
|
6879
|
+
for (let i = 0; i < count; i++) {
|
|
6880
|
+
const i3 = i * 3;
|
|
6881
|
+
const dx = array[i3] - x;
|
|
6882
|
+
const dy = array[i3 + 1] - y;
|
|
6883
|
+
const dz = array[i3 + 2] - z30;
|
|
6884
|
+
const d = dx * dx + dy * dy + dz * dz;
|
|
6885
|
+
if (d < bestDist) {
|
|
6886
|
+
bestDist = d;
|
|
6887
|
+
best = i;
|
|
6888
|
+
}
|
|
6889
|
+
}
|
|
6890
|
+
return best;
|
|
6891
|
+
}
|
|
6892
|
+
function isStripConfig(physics) {
|
|
6893
|
+
return typeof physics === "object" && physics.type === "strip";
|
|
6894
|
+
}
|
|
4989
6895
|
function buildStack(config, overrides, behavior, t = 0) {
|
|
4990
|
-
if (
|
|
6896
|
+
if (isStripConfig(config.physics)) return null;
|
|
4991
6897
|
const idle = typeof config.physics === "string" && config.physics !== "none" ? getIdlePreset(config.physics) : null;
|
|
4992
6898
|
const idleStack = idle?.stack?.() ?? [];
|
|
4993
6899
|
let shapeStack = [];
|
|
@@ -5430,7 +7336,7 @@ function getWalkPath(options) {
|
|
|
5430
7336
|
|
|
5431
7337
|
// src/field/layouts/index.ts
|
|
5432
7338
|
var DEFAULT_SHEET = { width: 1, height: 1.4 };
|
|
5433
|
-
var
|
|
7339
|
+
var TAU4 = Math.PI * 2;
|
|
5434
7340
|
var DEG7 = Math.PI / 180;
|
|
5435
7341
|
function jitter2(seed, i) {
|
|
5436
7342
|
let h = Math.imul(seed * 1e3 + i + 1 ^ 2654435769, 2654435761);
|
|
@@ -5450,7 +7356,7 @@ var ring = {
|
|
|
5450
7356
|
defaults: ringSchema.parse({}),
|
|
5451
7357
|
optionsSchema: ringSchema,
|
|
5452
7358
|
pose(i, n, o, phase) {
|
|
5453
|
-
const theta = (i / n + phase) *
|
|
7359
|
+
const theta = (i / n + phase) * TAU4;
|
|
5454
7360
|
return {
|
|
5455
7361
|
position: [Math.sin(theta) * o.radius, 0, Math.cos(theta) * o.radius],
|
|
5456
7362
|
// Face radially OUTWARD so the papers nearest the camera show their
|
|
@@ -5611,7 +7517,7 @@ var sweepSchema = import_zod24.z.object({
|
|
|
5611
7517
|
from: import_zod24.z.number().min(0).max(1).default(0),
|
|
5612
7518
|
to: import_zod24.z.number().min(0).max(1).default(1)
|
|
5613
7519
|
});
|
|
5614
|
-
var
|
|
7520
|
+
var sweep2 = {
|
|
5615
7521
|
id: "sweep",
|
|
5616
7522
|
label: "Sweep",
|
|
5617
7523
|
defaults: sweepSchema.parse({}),
|
|
@@ -5726,7 +7632,7 @@ var rack = {
|
|
|
5726
7632
|
};
|
|
5727
7633
|
var colonnadeSchema = import_zod24.z.object({
|
|
5728
7634
|
/** The walk the colonnade is built along — see `stage/path`. */
|
|
5729
|
-
path: walkPathSchema.
|
|
7635
|
+
path: walkPathSchema.prefault({}),
|
|
5730
7636
|
/** Half-width of the clear aisle: how far each banner stands off the walk line. */
|
|
5731
7637
|
aisle: import_zod24.z.number().min(0.2).max(20).default(2.4),
|
|
5732
7638
|
/** How much that gap opens and closes along the walk. Nothing hung by hand is a corridor. */
|
|
@@ -5817,7 +7723,7 @@ registerLayout(spread);
|
|
|
5817
7723
|
registerLayout(pile);
|
|
5818
7724
|
registerLayout(wall);
|
|
5819
7725
|
registerLayout(spill);
|
|
5820
|
-
registerLayout(
|
|
7726
|
+
registerLayout(sweep2);
|
|
5821
7727
|
registerLayout(book);
|
|
5822
7728
|
registerLayout(accordion);
|
|
5823
7729
|
registerLayout(rack);
|
|
@@ -6020,7 +7926,8 @@ function FieldGroup({
|
|
|
6020
7926
|
behaviorTransform.transform(
|
|
6021
7927
|
{ ...config.behavior, [behaviorTransform.progressParam]: progressRef.current },
|
|
6022
7928
|
t,
|
|
6023
|
-
pose2
|
|
7929
|
+
pose2,
|
|
7930
|
+
shared.sheet
|
|
6024
7931
|
);
|
|
6025
7932
|
scratchObj.position.x += pose2.position[0];
|
|
6026
7933
|
scratchObj.position.y += pose2.position[1];
|
|
@@ -7072,7 +8979,7 @@ var RUN_STRIDE = 1.7;
|
|
|
7072
8979
|
var LEAN_FULL_SPEED = 1.2;
|
|
7073
8980
|
var GRAVITY = 9.81;
|
|
7074
8981
|
var FROUDE_RUN = 0.5;
|
|
7075
|
-
var
|
|
8982
|
+
var TAU5 = Math.PI * 2;
|
|
7076
8983
|
function isRunning(o) {
|
|
7077
8984
|
if (o.gait !== "auto") return o.gait === "run";
|
|
7078
8985
|
const legLength = PROPORTIONS.hip * o.height;
|
|
@@ -7088,7 +8995,7 @@ function figureGait(distance, o) {
|
|
|
7088
8995
|
const a = running ? RUN : WALK;
|
|
7089
8996
|
const cycle = cycleLength(o);
|
|
7090
8997
|
const phase = cycle > 0 ? (distance / cycle % 1 + 1) % 1 : 0;
|
|
7091
|
-
const w = phase *
|
|
8998
|
+
const w = phase * TAU5;
|
|
7092
8999
|
const leftThigh = a.thigh * Math.sin(w);
|
|
7093
9000
|
const rightThigh = a.thigh * Math.sin(w + Math.PI);
|
|
7094
9001
|
const flex = (at) => -a.knee * Math.max(0, Math.cos(w - at)) ** 1.5;
|
|
@@ -7814,9 +9721,9 @@ var stageRoomSchema = import_zod27.z.object({
|
|
|
7814
9721
|
height: import_zod27.z.number().min(1).max(6).default(2.2),
|
|
7815
9722
|
color: import_zod27.z.string().default("#171310").describe("color"),
|
|
7816
9723
|
/** Columns flanking the walk — see `stageColumnsSchema`. */
|
|
7817
|
-
columns: stageColumnsSchema.
|
|
9724
|
+
columns: stageColumnsSchema.prefault({}),
|
|
7818
9725
|
/** A wall at the end of the walk with the source in it. */
|
|
7819
|
-
doorway: stageDoorwaySchema.
|
|
9726
|
+
doorway: stageDoorwaySchema.prefault({})
|
|
7820
9727
|
});
|
|
7821
9728
|
var stageGradeSchema = import_zod27.z.object({
|
|
7822
9729
|
/**
|
|
@@ -7870,9 +9777,9 @@ var stageGradeSchema = import_zod27.z.object({
|
|
|
7870
9777
|
grain: import_zod27.z.number().min(0).max(0.5).default(0.022)
|
|
7871
9778
|
});
|
|
7872
9779
|
var stageSchema = import_zod27.z.object({
|
|
7873
|
-
path: walkPathSchema.
|
|
7874
|
-
shot: shotSchema.
|
|
7875
|
-
figure: figureSchema.
|
|
9780
|
+
path: walkPathSchema.prefault({}),
|
|
9781
|
+
shot: shotSchema.prefault({}),
|
|
9782
|
+
figure: figureSchema.prefault({}),
|
|
7876
9783
|
/** Stage mode is built for `nave`; the others are all front-lit. */
|
|
7877
9784
|
lighting: import_zod27.z.enum(lightingNames).default("nave"),
|
|
7878
9785
|
/**
|
|
@@ -7899,12 +9806,12 @@ var stageSchema = import_zod27.z.object({
|
|
|
7899
9806
|
* Still one flag away for anyone who wants it.
|
|
7900
9807
|
*/
|
|
7901
9808
|
showFigure: import_zod27.z.boolean().default(false),
|
|
7902
|
-
source: stageSourceSchema.
|
|
7903
|
-
ground: stageGroundSchema.
|
|
9809
|
+
source: stageSourceSchema.prefault({}),
|
|
9810
|
+
ground: stageGroundSchema.prefault({}),
|
|
7904
9811
|
/** Ceiling and the architecture around the walk — see `stageRoomSchema`. */
|
|
7905
|
-
room: stageRoomSchema.
|
|
9812
|
+
room: stageRoomSchema.prefault({}),
|
|
7906
9813
|
/** Thread and clips — see `stageSuspensionSchema`. */
|
|
7907
|
-
suspension: stageSuspensionSchema.
|
|
9814
|
+
suspension: stageSuspensionSchema.prefault({}),
|
|
7908
9815
|
/**
|
|
7909
9816
|
* The print — bloom, vignette, grain.
|
|
7910
9817
|
*
|
|
@@ -7916,7 +9823,7 @@ var stageSchema = import_zod27.z.object({
|
|
|
7916
9823
|
* behaviour: a stage silently losing its grade would be worse than a
|
|
7917
9824
|
* missing-module error that names the package.
|
|
7918
9825
|
*/
|
|
7919
|
-
grade: stageGradeSchema.
|
|
9826
|
+
grade: stageGradeSchema.prefault({})
|
|
7920
9827
|
});
|
|
7921
9828
|
|
|
7922
9829
|
// src/stage/navigate.ts
|
|
@@ -8884,6 +10791,8 @@ function diffConfig(config) {
|
|
|
8884
10791
|
}
|
|
8885
10792
|
if (config.deformers) out.deformers = config.deformers;
|
|
8886
10793
|
if (Object.keys(config.surface).length > 0) out.surface = config.surface;
|
|
10794
|
+
const memory = diffAgainst(config.memory, memorySchema.parse({}));
|
|
10795
|
+
if (Object.keys(memory).length > 0) out.memory = memory;
|
|
8887
10796
|
if (typeof config.physics === "object") {
|
|
8888
10797
|
const defaults = clothConfigSchema.parse({ type: "cloth" });
|
|
8889
10798
|
out.physics = { type: "cloth", ...diffAgainst(config.physics, defaults) };
|
|
@@ -8932,7 +10841,7 @@ function diffStage(stage) {
|
|
|
8932
10841
|
return diff;
|
|
8933
10842
|
}
|
|
8934
10843
|
function stringifyStage(value, indent = 0) {
|
|
8935
|
-
const
|
|
10844
|
+
const pad2 = " ".repeat(indent);
|
|
8936
10845
|
const inner = " ".repeat(indent + 1);
|
|
8937
10846
|
if (Array.isArray(value)) {
|
|
8938
10847
|
if (value.length === 0) return "[]";
|
|
@@ -8940,7 +10849,7 @@ function stringifyStage(value, indent = 0) {
|
|
|
8940
10849
|
const items = value.map((v) => `${inner}${stringifyStage(v, indent + 1)}`);
|
|
8941
10850
|
return `[
|
|
8942
10851
|
${items.join(",\n")}
|
|
8943
|
-
${
|
|
10852
|
+
${pad2}]`;
|
|
8944
10853
|
}
|
|
8945
10854
|
if (value && typeof value === "object") {
|
|
8946
10855
|
const entries = Object.entries(value);
|
|
@@ -8948,7 +10857,7 @@ ${pad}]`;
|
|
|
8948
10857
|
const items = entries.map(([k, v]) => `${inner}${JSON.stringify(k)}: ${stringifyStage(v, indent + 1)}`);
|
|
8949
10858
|
return `{
|
|
8950
10859
|
${items.join(",\n")}
|
|
8951
|
-
${
|
|
10860
|
+
${pad2}}`;
|
|
8952
10861
|
}
|
|
8953
10862
|
return JSON.stringify(value);
|
|
8954
10863
|
}
|