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/index.cjs
CHANGED
|
@@ -31,9 +31,12 @@ var __toCommonJS = (mod2) => __copyProps(__defProp({}, "__esModule", { value: tr
|
|
|
31
31
|
var index_exports = {};
|
|
32
32
|
__export(index_exports, {
|
|
33
33
|
AUTO_CEILING: () => AUTO_CEILING,
|
|
34
|
+
CreaseTracker: () => CreaseTracker,
|
|
34
35
|
DropZone: () => DropZone,
|
|
35
36
|
FLAT_SEGMENTS: () => FLAT_SEGMENTS,
|
|
36
37
|
LightRig: () => LightRig,
|
|
38
|
+
MAX_CREASES: () => MAX_CREASES,
|
|
39
|
+
MAX_SET: () => MAX_SET,
|
|
37
40
|
PARITY_EPSILON: () => PARITY_EPSILON,
|
|
38
41
|
Paper: () => Paper,
|
|
39
42
|
PaperBackdrop: () => PaperBackdrop,
|
|
@@ -42,6 +45,7 @@ __export(index_exports, {
|
|
|
42
45
|
PaperLighting: () => PaperLighting,
|
|
43
46
|
PaperMesh: () => PaperMesh,
|
|
44
47
|
SAG_TOL: () => SAG_TOL,
|
|
48
|
+
applyMemory: () => applyMemory,
|
|
45
49
|
backdropSchema: () => backdropSchema,
|
|
46
50
|
behaviorConfigSchema: () => behaviorConfigSchema,
|
|
47
51
|
buildAgentPayload: () => buildAgentPayload,
|
|
@@ -52,6 +56,7 @@ __export(index_exports, {
|
|
|
52
56
|
contentNames: () => contentNames,
|
|
53
57
|
contentSchemaFor: () => contentSchemaFor,
|
|
54
58
|
coreStateNames: () => coreStateNames,
|
|
59
|
+
creaseSchema: () => creaseSchema,
|
|
55
60
|
describeConfig: () => describeConfig,
|
|
56
61
|
describeFieldConfig: () => describeFieldConfig,
|
|
57
62
|
diffConfig: () => diffConfig,
|
|
@@ -70,6 +75,8 @@ __export(index_exports, {
|
|
|
70
75
|
listDeformers: () => listDeformers,
|
|
71
76
|
listLayouts: () => listLayouts,
|
|
72
77
|
listPresets: () => listPresets,
|
|
78
|
+
maxStripLength: () => maxStripLength,
|
|
79
|
+
memorySchema: () => memorySchema,
|
|
73
80
|
mergeConfig: () => mergeConfig,
|
|
74
81
|
mergeWithDeletes: () => mergeWithDeletes,
|
|
75
82
|
paperConfigSchema: () => paperConfigSchema,
|
|
@@ -96,6 +103,7 @@ __export(index_exports, {
|
|
|
96
103
|
stateDefSchema: () => stateDefSchema,
|
|
97
104
|
stockNames: () => stockNames,
|
|
98
105
|
stocks: () => stocks,
|
|
106
|
+
stripConfigSchema: () => stripConfigSchema,
|
|
99
107
|
supportsWebGL: () => supportsWebGL,
|
|
100
108
|
uniquePresetName: () => uniquePresetName,
|
|
101
109
|
unregisterPreset: () => unregisterPreset,
|
|
@@ -117,7 +125,7 @@ var import_fiber = require("@react-three/fiber");
|
|
|
117
125
|
var import_react7 = require("react");
|
|
118
126
|
|
|
119
127
|
// src/config/schema.ts
|
|
120
|
-
var
|
|
128
|
+
var import_zod15 = require("zod");
|
|
121
129
|
|
|
122
130
|
// src/config/merge.ts
|
|
123
131
|
function mergeConfig(base, override) {
|
|
@@ -351,15 +359,169 @@ var peel = {
|
|
|
351
359
|
};
|
|
352
360
|
|
|
353
361
|
// src/behaviors/unroll.ts
|
|
362
|
+
var import_zod4 = require("zod");
|
|
363
|
+
|
|
364
|
+
// src/deformers/roll.ts
|
|
354
365
|
var import_zod3 = require("zod");
|
|
355
|
-
var
|
|
366
|
+
var rollOptionsSchema = import_zod3.z.object({
|
|
367
|
+
/** Direction of rolling in the sheet plane, degrees. 0 = +x, 90 = +y. */
|
|
368
|
+
angle: import_zod3.z.number().min(-360).max(360).default(90),
|
|
369
|
+
/** Signed distance (along the roll direction, from sheet center) where the roll begins. */
|
|
370
|
+
boundary: import_zod3.z.number().min(-20).max(20).default(0),
|
|
371
|
+
/** Radius of the OUTERMOST wrap — the one the flat sheet leaves the roll on. */
|
|
372
|
+
radius: import_zod3.z.number().min(0.01).max(2).default(0.12),
|
|
373
|
+
/** Gap between consecutive wraps, in world units. 0 = a bare cylinder. */
|
|
374
|
+
thickness: import_zod3.z.number().min(0).max(0.2).default(0.015)
|
|
375
|
+
});
|
|
376
|
+
var DEG2 = Math.PI / 180;
|
|
377
|
+
var TAU = Math.PI * 2;
|
|
378
|
+
var MIN_RADIUS_FRACTION = 0.08;
|
|
379
|
+
function windAngle(s, r0, k) {
|
|
380
|
+
if (k <= 0) return s / r0;
|
|
381
|
+
const rMin = r0 * MIN_RADIUS_FRACTION;
|
|
382
|
+
const thetaFloor = (r0 - rMin) / k;
|
|
383
|
+
const sFloor = r0 * thetaFloor - k * thetaFloor * thetaFloor / 2;
|
|
384
|
+
if (s >= sFloor) return thetaFloor + (s - sFloor) / rMin;
|
|
385
|
+
return 2 * s / (r0 + Math.sqrt(r0 * r0 - 2 * k * s));
|
|
386
|
+
}
|
|
387
|
+
function windRadius(theta, r0, k) {
|
|
388
|
+
return Math.max(r0 - k * theta, r0 * MIN_RADIUS_FRACTION);
|
|
389
|
+
}
|
|
390
|
+
function rollRadius(length, core, thickness) {
|
|
391
|
+
return Math.sqrt(core * core + Math.max(0, length) * thickness / Math.PI);
|
|
392
|
+
}
|
|
393
|
+
var roll = {
|
|
394
|
+
id: "roll",
|
|
395
|
+
label: "Roll",
|
|
396
|
+
defaults: rollOptionsSchema.parse({}),
|
|
397
|
+
optionsSchema: rollOptionsSchema,
|
|
398
|
+
geometry: {
|
|
399
|
+
minSegments: 48,
|
|
400
|
+
// The INNERMOST wrap is the tightest curvature on the sheet, so it sets
|
|
401
|
+
// the density — the outer one used to, back when the spiral grew outward.
|
|
402
|
+
// Floored at a fraction of the outer radius because segment count scales
|
|
403
|
+
// as 1/√r: a roll wound to a hair's breadth would otherwise ask for a
|
|
404
|
+
// grid nobody can afford, to resolve a few square millimetres at the core.
|
|
405
|
+
autoSegments: (o, sheet2) => {
|
|
406
|
+
const span = spanAlong(sheet2, o.angle);
|
|
407
|
+
const wound = Math.max(0, span / 2 - o.boundary);
|
|
408
|
+
const k = o.thickness / TAU;
|
|
409
|
+
return segmentsForArc(span, windRadius(windAngle(wound, o.radius, k), o.radius, k));
|
|
410
|
+
},
|
|
411
|
+
axis: (o) => o.angle
|
|
412
|
+
},
|
|
413
|
+
displace(out, _uv, o) {
|
|
414
|
+
const dirX = Math.cos(o.angle * DEG2);
|
|
415
|
+
const dirY = Math.sin(o.angle * DEG2);
|
|
416
|
+
const d = out.x * dirX + out.y * dirY;
|
|
417
|
+
const s = d - o.boundary;
|
|
418
|
+
if (s <= 0) return;
|
|
419
|
+
const k = o.thickness / TAU;
|
|
420
|
+
const theta = windAngle(s, o.radius, k);
|
|
421
|
+
const r = windRadius(theta, o.radius, k) - out.z;
|
|
422
|
+
const newD = o.boundary + r * Math.sin(theta);
|
|
423
|
+
const newZ = o.radius - r * Math.cos(theta);
|
|
424
|
+
out.x += dirX * (newD - d);
|
|
425
|
+
out.y += dirY * (newD - d);
|
|
426
|
+
out.z = newZ;
|
|
427
|
+
},
|
|
428
|
+
glsl: {
|
|
429
|
+
chunk: (
|
|
430
|
+
/* glsl */
|
|
431
|
+
`
|
|
432
|
+
void FN(inout vec3 p, vec2 uv, float t) {
|
|
433
|
+
vec2 dir = vec2(cos(U_angle), sin(U_angle));
|
|
434
|
+
float d = dot(p.xy, dir);
|
|
435
|
+
float s = d - U_boundary;
|
|
436
|
+
if (s <= 0.0) return;
|
|
437
|
+
float k = U_thickness / 6.2831853071795864;
|
|
438
|
+
float rMin = U_radius * 0.08;
|
|
439
|
+
float thetaFloor = (U_radius - rMin) / max(k, 1e-9);
|
|
440
|
+
float sFloor = U_radius * thetaFloor - 0.5 * k * thetaFloor * thetaFloor;
|
|
441
|
+
float theta = k <= 0.0
|
|
442
|
+
? s / U_radius
|
|
443
|
+
: (s >= sFloor
|
|
444
|
+
? thetaFloor + (s - sFloor) / rMin
|
|
445
|
+
: (2.0 * s) / (U_radius + sqrt(U_radius * U_radius - 2.0 * k * s)));
|
|
446
|
+
float r = max(U_radius - k * theta, rMin) - p.z;
|
|
447
|
+
float newD = U_boundary + r * sin(theta);
|
|
448
|
+
float newZ = U_radius - r * cos(theta);
|
|
449
|
+
p.xy += dir * (newD - d);
|
|
450
|
+
p.z = newZ;
|
|
451
|
+
}
|
|
452
|
+
`
|
|
453
|
+
),
|
|
454
|
+
uniforms: (o) => ({
|
|
455
|
+
angle: o.angle * DEG2,
|
|
456
|
+
boundary: o.boundary,
|
|
457
|
+
radius: o.radius,
|
|
458
|
+
thickness: o.thickness
|
|
459
|
+
})
|
|
460
|
+
}
|
|
461
|
+
};
|
|
462
|
+
|
|
463
|
+
// src/behaviors/unroll.ts
|
|
464
|
+
var unrollOptionsSchema = import_zod4.z.object({
|
|
356
465
|
/** 0 = fully rolled cylinder, 1 = flat sheet. */
|
|
357
|
-
progress:
|
|
358
|
-
/** How tightly the paper is wound. */
|
|
359
|
-
tightness:
|
|
466
|
+
progress: import_zod4.z.number().min(0).max(1).default(0.5),
|
|
467
|
+
/** How tightly the paper is wound — thin layers and many turns, or few and fat. */
|
|
468
|
+
tightness: import_zod4.z.number().min(0).max(1).default(0.5),
|
|
360
469
|
/** Idle rocking of the rolled end. */
|
|
361
|
-
sway:
|
|
470
|
+
sway: import_zod4.z.number().min(0).max(1).default(0.25),
|
|
471
|
+
/** Which end of the sheet holds the roll. `top` hangs the paper below it. */
|
|
472
|
+
from: import_zod4.z.enum(["bottom", "top"]).default("bottom"),
|
|
473
|
+
/** Radius of the tube the paper is wound onto — the roll never shrinks past it. */
|
|
474
|
+
core: import_zod4.z.number().min(5e-3).max(0.5).default(0.03),
|
|
475
|
+
/**
|
|
476
|
+
* Paper already hanging at `progress` 0, in world units.
|
|
477
|
+
*
|
|
478
|
+
* A roll on a holder is never a bare cylinder: there is always a leaf out,
|
|
479
|
+
* because that is what you take hold of. Starting from nothing showing
|
|
480
|
+
* reads as a fresh roll still in its wrapper.
|
|
481
|
+
*/
|
|
482
|
+
tail: import_zod4.z.number().min(0).max(20).default(0),
|
|
483
|
+
/**
|
|
484
|
+
* How far below the roll the paper lands, in world units. Omit and it
|
|
485
|
+
* hangs forever.
|
|
486
|
+
*
|
|
487
|
+
* Paper that reaches the ground does not stop and does not carry on
|
|
488
|
+
* through it: it creases and runs out flat. Everything past this distance
|
|
489
|
+
* turns a right angle and lies down.
|
|
490
|
+
*/
|
|
491
|
+
floor: import_zod4.z.number().min(0.1).max(50).optional(),
|
|
492
|
+
/**
|
|
493
|
+
* Hold the roll still in space and let the paper hang off it, instead of
|
|
494
|
+
* letting the roll ride along with the shrinking wound region.
|
|
495
|
+
*/
|
|
496
|
+
fixed: import_zod4.z.boolean().default(false)
|
|
362
497
|
});
|
|
498
|
+
var LOOSE = 0.1;
|
|
499
|
+
var TIGHT = 0.02;
|
|
500
|
+
function layerThickness(tightness) {
|
|
501
|
+
return LOOSE - tightness * (LOOSE - TIGHT);
|
|
502
|
+
}
|
|
503
|
+
function sweep(o, sheet2) {
|
|
504
|
+
const maxRadius = rollRadius(sheet2.height, o.core, layerThickness(o.tightness));
|
|
505
|
+
const tail = Math.min(o.tail, sheet2.height);
|
|
506
|
+
return { start: -sheet2.height / 2 + tail, end: sheet2.height / 2 + maxRadius * 2 };
|
|
507
|
+
}
|
|
508
|
+
function rollBoundary(o, sheet2) {
|
|
509
|
+
const { start, end } = sweep(o, sheet2);
|
|
510
|
+
return start + o.progress * (end - start);
|
|
511
|
+
}
|
|
512
|
+
var LANDING_RADIUS = 0.035;
|
|
513
|
+
function landing(o, sheet2, boundary, rollRadiusNow) {
|
|
514
|
+
const radius = Math.min(0.5, Math.max(0.02, sheet2.height * LANDING_RADIUS));
|
|
515
|
+
const hingeDrop = radius / (Math.PI / 2);
|
|
516
|
+
const below = Math.max(o.floor, rollRadiusNow + radius);
|
|
517
|
+
const floorLine = o.from === "top" ? boundary - below : -boundary + below;
|
|
518
|
+
return {
|
|
519
|
+
type: "fold",
|
|
520
|
+
// Travel points from the roll toward the floor, so "past the crease"
|
|
521
|
+
// means "the length that has arrived", not the drop above it.
|
|
522
|
+
options: o.from === "top" ? { angle: -90, offset: -floorLine - hingeDrop, foldAngle: 90, radius } : { angle: 90, offset: floorLine - hingeDrop, foldAngle: 90, radius }
|
|
523
|
+
};
|
|
524
|
+
}
|
|
363
525
|
var unroll = {
|
|
364
526
|
id: "unroll",
|
|
365
527
|
label: "Unroll",
|
|
@@ -370,46 +532,65 @@ var unroll = {
|
|
|
370
532
|
duration: 3,
|
|
371
533
|
loopMode: "yoyo",
|
|
372
534
|
stack(o, sheet2) {
|
|
373
|
-
const
|
|
374
|
-
const
|
|
375
|
-
const
|
|
376
|
-
|
|
535
|
+
const thickness = layerThickness(o.tightness);
|
|
536
|
+
const boundary = rollBoundary(o, sheet2);
|
|
537
|
+
const wound = Math.max(0, sheet2.height / 2 - boundary);
|
|
538
|
+
const radius = rollRadius(wound, o.core, thickness);
|
|
539
|
+
const stack = [
|
|
377
540
|
{
|
|
378
541
|
type: "roll",
|
|
379
542
|
options: {
|
|
380
|
-
|
|
381
|
-
|
|
543
|
+
// Both directions sweep the same boundary; only which half of the
|
|
544
|
+
// sheet counts as "past" it changes. 270 winds the bottom (a
|
|
545
|
+
// receipt feeding downward), 90 winds the top (paper hanging below).
|
|
546
|
+
angle: o.from === "top" ? 90 : 270,
|
|
547
|
+
boundary,
|
|
382
548
|
radius,
|
|
383
|
-
|
|
549
|
+
thickness
|
|
384
550
|
}
|
|
385
551
|
}
|
|
386
552
|
];
|
|
553
|
+
if (o.floor !== void 0) stack.push(landing(o, sheet2, boundary, radius));
|
|
554
|
+
return stack;
|
|
387
555
|
},
|
|
388
556
|
loop(o, t) {
|
|
389
557
|
if (o.sway === 0) return {};
|
|
390
558
|
const wobble = Math.sin(t * 1.5) * 0.01 * o.sway;
|
|
391
559
|
return { progress: Math.min(1, Math.max(0, o.progress + wobble)) };
|
|
392
560
|
},
|
|
561
|
+
transform(o, _t, pose, sheet2) {
|
|
562
|
+
if (!o.fixed) return;
|
|
563
|
+
const travel = sweep(o, sheet2).end - rollBoundary(o, sheet2);
|
|
564
|
+
pose.position[1] += o.from === "top" ? travel : -travel;
|
|
565
|
+
},
|
|
393
566
|
handles: [
|
|
394
567
|
{
|
|
395
568
|
id: "roll-edge",
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
569
|
+
// The grab point is the edge the paper leaves the roll on, so it tracks
|
|
570
|
+
// the boundary — which runs top-to-bottom for a roll at the bottom and
|
|
571
|
+
// bottom-to-top for one at the top.
|
|
572
|
+
anchor: (o) => {
|
|
573
|
+
const v = o.from === "top" ? o.progress : 1 - o.progress;
|
|
574
|
+
return [0.5, Math.max(0.02, Math.min(0.98, v))];
|
|
575
|
+
},
|
|
576
|
+
drag(local, o, sheet2) {
|
|
577
|
+
const along = local.y / sheet2.height;
|
|
578
|
+
const p = o.from === "top" ? 0.5 + along : 0.5 - along;
|
|
579
|
+
return { progress: Math.min(1, Math.max(0, p)) };
|
|
399
580
|
}
|
|
400
581
|
}
|
|
401
582
|
]
|
|
402
583
|
};
|
|
403
584
|
|
|
404
585
|
// src/behaviors/flip.ts
|
|
405
|
-
var
|
|
406
|
-
var flipOptionsSchema =
|
|
586
|
+
var import_zod5 = require("zod");
|
|
587
|
+
var flipOptionsSchema = import_zod5.z.object({
|
|
407
588
|
/** 0 = flat, 1 = page fully turned over the spine. */
|
|
408
|
-
progress:
|
|
589
|
+
progress: import_zod5.z.number().min(0).max(1).default(0.3),
|
|
409
590
|
/** Which edge is the spine. */
|
|
410
|
-
spine:
|
|
591
|
+
spine: import_zod5.z.enum(["left", "right"]).default("left"),
|
|
411
592
|
/** Softness of the turning curl. */
|
|
412
|
-
radius:
|
|
593
|
+
radius: import_zod5.z.number().min(0.1).max(0.8).default(0.3)
|
|
413
594
|
});
|
|
414
595
|
var flip = {
|
|
415
596
|
id: "flip",
|
|
@@ -431,7 +612,7 @@ var flip = {
|
|
|
431
612
|
angle,
|
|
432
613
|
boundary: start + o.progress * (end - start),
|
|
433
614
|
radius: o.radius,
|
|
434
|
-
|
|
615
|
+
thickness: 0
|
|
435
616
|
}
|
|
436
617
|
}
|
|
437
618
|
];
|
|
@@ -449,12 +630,12 @@ var flip = {
|
|
|
449
630
|
};
|
|
450
631
|
|
|
451
632
|
// src/behaviors/letter-fold.ts
|
|
452
|
-
var
|
|
453
|
-
var letterFoldOptionsSchema =
|
|
633
|
+
var import_zod6 = require("zod");
|
|
634
|
+
var letterFoldOptionsSchema = import_zod6.z.object({
|
|
454
635
|
/** 0 = flat letter, 1 = fully tri-folded. */
|
|
455
|
-
progress:
|
|
636
|
+
progress: import_zod6.z.number().min(0).max(1).default(0.4),
|
|
456
637
|
/** Softness of the two creases. */
|
|
457
|
-
crease:
|
|
638
|
+
crease: import_zod6.z.number().min(0).max(1).default(0.3)
|
|
458
639
|
});
|
|
459
640
|
var letterFold = {
|
|
460
641
|
id: "letter-fold",
|
|
@@ -504,12 +685,12 @@ var letterFold = {
|
|
|
504
685
|
};
|
|
505
686
|
|
|
506
687
|
// src/behaviors/hang.ts
|
|
507
|
-
var
|
|
508
|
-
var hangOptionsSchema =
|
|
688
|
+
var import_zod7 = require("zod");
|
|
689
|
+
var hangOptionsSchema = import_zod7.z.object({
|
|
509
690
|
/** Wind strength driving the ripple. */
|
|
510
|
-
wind:
|
|
691
|
+
wind: import_zod7.z.number().min(0).max(1).default(0.4),
|
|
511
692
|
/** Gravity bulge of the hanging sheet. */
|
|
512
|
-
sag:
|
|
693
|
+
sag: import_zod7.z.number().min(0).max(1).default(0.3)
|
|
513
694
|
});
|
|
514
695
|
var hang = {
|
|
515
696
|
id: "hang",
|
|
@@ -538,12 +719,12 @@ var hang = {
|
|
|
538
719
|
};
|
|
539
720
|
|
|
540
721
|
// src/behaviors/fly.ts
|
|
541
|
-
var
|
|
542
|
-
var flyOptionsSchema =
|
|
722
|
+
var import_zod8 = require("zod");
|
|
723
|
+
var flyOptionsSchema = import_zod8.z.object({
|
|
543
724
|
/** Ripple energy. */
|
|
544
|
-
flutter:
|
|
725
|
+
flutter: import_zod8.z.number().min(0).max(1).default(0.5),
|
|
545
726
|
/** Aerodynamic arc of the sheet. */
|
|
546
|
-
curve:
|
|
727
|
+
curve: import_zod8.z.number().min(0).max(1).default(0.4)
|
|
547
728
|
});
|
|
548
729
|
var fly = {
|
|
549
730
|
id: "fly",
|
|
@@ -572,12 +753,12 @@ var fly = {
|
|
|
572
753
|
};
|
|
573
754
|
|
|
574
755
|
// src/behaviors/fall.ts
|
|
575
|
-
var
|
|
576
|
-
var fallOptionsSchema =
|
|
756
|
+
var import_zod9 = require("zod");
|
|
757
|
+
var fallOptionsSchema = import_zod9.z.object({
|
|
577
758
|
/** Air resistance ripple while falling. */
|
|
578
|
-
flutter:
|
|
759
|
+
flutter: import_zod9.z.number().min(0).max(1).default(0.6),
|
|
579
760
|
/** A falling sheet always lifts a corner. */
|
|
580
|
-
curl:
|
|
761
|
+
curl: import_zod9.z.number().min(0).max(1).default(0.3)
|
|
581
762
|
});
|
|
582
763
|
var fall = {
|
|
583
764
|
id: "fall",
|
|
@@ -609,21 +790,21 @@ var fall = {
|
|
|
609
790
|
};
|
|
610
791
|
|
|
611
792
|
// src/behaviors/carry.ts
|
|
612
|
-
var
|
|
613
|
-
var carryOptionsSchema =
|
|
793
|
+
var import_zod10 = require("zod");
|
|
794
|
+
var carryOptionsSchema = import_zod10.z.object({
|
|
614
795
|
/**
|
|
615
796
|
* The grab point — where the pointer was on the paper at pick time.
|
|
616
797
|
* 'auto' is resolved by the carry controller (usually the peeled corner:
|
|
617
798
|
* continuity from peel → carry is the immersion moment).
|
|
618
799
|
*/
|
|
619
|
-
grab:
|
|
800
|
+
grab: import_zod10.z.enum([...cornerNames, "auto"]).default("auto"),
|
|
620
801
|
/** From stock feel: a stamp is stiff — it flutters, it doesn't flow. */
|
|
621
|
-
stiffness:
|
|
622
|
-
flutter:
|
|
802
|
+
stiffness: import_zod10.z.number().min(0).max(1).default(0.7),
|
|
803
|
+
flutter: import_zod10.z.number().min(0).max(1).default(0.5),
|
|
623
804
|
/** How far the paper's yaw trails the drag direction (runtime transform). */
|
|
624
|
-
lag:
|
|
805
|
+
lag: import_zod10.z.number().min(0).max(1).default(0.35),
|
|
625
806
|
/** Drag-speed drive (0..1). Written live by the carry controller. */
|
|
626
|
-
drive:
|
|
807
|
+
drive: import_zod10.z.number().min(0).max(1).default(0.25)
|
|
627
808
|
});
|
|
628
809
|
var concreteGrab = (g) => g === "auto" ? "top-left" : g;
|
|
629
810
|
var DROOP_ANGLE = {
|
|
@@ -670,7 +851,7 @@ var carry = {
|
|
|
670
851
|
};
|
|
671
852
|
|
|
672
853
|
// src/behaviors/flight.ts
|
|
673
|
-
var
|
|
854
|
+
var import_zod11 = require("zod");
|
|
674
855
|
|
|
675
856
|
// src/physics/aero.ts
|
|
676
857
|
function dampTo(state, target, smoothing, dt) {
|
|
@@ -712,17 +893,17 @@ function carryDrive(speed) {
|
|
|
712
893
|
}
|
|
713
894
|
|
|
714
895
|
// src/behaviors/flight.ts
|
|
715
|
-
var flightOptionsSchema =
|
|
896
|
+
var flightOptionsSchema = import_zod11.z.object({
|
|
716
897
|
/** Directional wind vector — paper travels ACROSS the scene, not just down. */
|
|
717
|
-
wind:
|
|
718
|
-
gustiness:
|
|
719
|
-
tumble:
|
|
898
|
+
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]),
|
|
899
|
+
gustiness: import_zod11.z.number().min(0).max(1).default(0.4),
|
|
900
|
+
tumble: import_zod11.z.number().min(0).max(1).default(0.6),
|
|
720
901
|
/** 'loop' is a seamless idle cycle; 'drift' travels along the wind. */
|
|
721
|
-
path:
|
|
902
|
+
path: import_zod11.z.enum(["drift", "loop"]).default("drift"),
|
|
722
903
|
/** Drift only: exit the scene → re-enter the opposite side. */
|
|
723
|
-
respawn:
|
|
904
|
+
respawn: import_zod11.z.boolean().default(true),
|
|
724
905
|
/** Half-extent of the travel before respawn wraps it. */
|
|
725
|
-
range:
|
|
906
|
+
range: import_zod11.z.number().min(0.5).max(12).default(3.5)
|
|
726
907
|
});
|
|
727
908
|
var flight = {
|
|
728
909
|
id: "flight",
|
|
@@ -754,16 +935,16 @@ var flight = {
|
|
|
754
935
|
};
|
|
755
936
|
|
|
756
937
|
// src/behaviors/crumple.ts
|
|
757
|
-
var
|
|
758
|
-
var crumpleBehaviorOptionsSchema =
|
|
938
|
+
var import_zod12 = require("zod");
|
|
939
|
+
var crumpleBehaviorOptionsSchema = import_zod12.z.object({
|
|
759
940
|
/** 0 = flat sheet, 1 = crushed. */
|
|
760
|
-
progress:
|
|
941
|
+
progress: import_zod12.z.number().min(0).max(1).default(0.55),
|
|
761
942
|
/** Few big facets at 0, many small ones at 1. */
|
|
762
|
-
coarseness:
|
|
943
|
+
coarseness: import_zod12.z.number().min(0).max(1).default(0.35),
|
|
763
944
|
/** How far the sheet curls in on itself as it crushes. */
|
|
764
|
-
ball:
|
|
945
|
+
ball: import_zod12.z.number().min(0).max(1).default(0.5),
|
|
765
946
|
/** A different crush of the same paper. */
|
|
766
|
-
seed:
|
|
947
|
+
seed: import_zod12.z.number().int().min(0).max(7).default(0)
|
|
767
948
|
});
|
|
768
949
|
var crumpleBehavior = {
|
|
769
950
|
id: "crumple",
|
|
@@ -796,8 +977,8 @@ var crumpleBehavior = {
|
|
|
796
977
|
};
|
|
797
978
|
|
|
798
979
|
// src/behaviors/settle.ts
|
|
799
|
-
var
|
|
800
|
-
var settleOptionsSchema =
|
|
980
|
+
var import_zod13 = require("zod");
|
|
981
|
+
var settleOptionsSchema = import_zod13.z.object({
|
|
801
982
|
/**
|
|
802
983
|
* How long ago it landed, 0..1.
|
|
803
984
|
*
|
|
@@ -805,7 +986,7 @@ var settleOptionsSchema = import_zod12.z.object({
|
|
|
805
986
|
* a sheet that has been lying there, where its own weight has flattened
|
|
806
987
|
* out everything except what its stiffness refuses to give up.
|
|
807
988
|
*/
|
|
808
|
-
relax:
|
|
989
|
+
relax: import_zod13.z.number().min(0).max(1).default(0.45),
|
|
809
990
|
/**
|
|
810
991
|
* How hard the paper resists lying flat, 0..1.
|
|
811
992
|
*
|
|
@@ -813,14 +994,14 @@ var settleOptionsSchema = import_zod12.z.object({
|
|
|
813
994
|
* never does. It is the whole reason a settled sheet reads as PAPER and
|
|
814
995
|
* not as a decal — at 0 the mesh is a rectangle painted on the floor.
|
|
815
996
|
*/
|
|
816
|
-
lift:
|
|
997
|
+
lift: import_zod13.z.number().min(0).max(1).default(0.45),
|
|
817
998
|
/** Which corner stayed up. */
|
|
818
|
-
corner:
|
|
999
|
+
corner: import_zod13.z.enum(["top-left", "top-right", "bottom-left", "bottom-right"]).default("top-right"),
|
|
819
1000
|
/**
|
|
820
1001
|
* Slack across the middle — the low, long undulation of a sheet that is
|
|
821
1002
|
* touching a floor in two places and bridging between them.
|
|
822
1003
|
*/
|
|
823
|
-
slack:
|
|
1004
|
+
slack: import_zod13.z.number().min(0).max(1).default(0.4)
|
|
824
1005
|
});
|
|
825
1006
|
var settle = {
|
|
826
1007
|
id: "settle",
|
|
@@ -873,8 +1054,8 @@ var settle = {
|
|
|
873
1054
|
};
|
|
874
1055
|
|
|
875
1056
|
// src/behaviors/ribbon.ts
|
|
876
|
-
var
|
|
877
|
-
var ribbonOptionsSchema =
|
|
1057
|
+
var import_zod14 = require("zod");
|
|
1058
|
+
var ribbonOptionsSchema = import_zod14.z.object({
|
|
878
1059
|
/**
|
|
879
1060
|
* How much of the drop is lying on the floor, as a fraction of the height.
|
|
880
1061
|
*
|
|
@@ -883,11 +1064,11 @@ var ribbonOptionsSchema = import_zod13.z.object({
|
|
|
883
1064
|
* turns over reads as paper meeting a floor, which is the thing the
|
|
884
1065
|
* reference installations are actually about.
|
|
885
1066
|
*/
|
|
886
|
-
pool:
|
|
1067
|
+
pool: import_zod14.z.number().min(0).max(0.5).default(0.16),
|
|
887
1068
|
/** How tightly it turns where it lands. Low is a soft slump, high is a curl. */
|
|
888
|
-
curl:
|
|
1069
|
+
curl: import_zod14.z.number().min(0).max(1).default(0.45),
|
|
889
1070
|
/** Folds running down the length. A printed strip is never a flat plane. */
|
|
890
|
-
drape:
|
|
1071
|
+
drape: import_zod14.z.number().min(0).max(1).default(0.5)
|
|
891
1072
|
});
|
|
892
1073
|
var ribbon = {
|
|
893
1074
|
id: "ribbon",
|
|
@@ -995,12 +1176,12 @@ var ribbon = {
|
|
|
995
1176
|
};
|
|
996
1177
|
|
|
997
1178
|
// src/config/schema.ts
|
|
998
|
-
var sheetSchema =
|
|
1179
|
+
var sheetSchema = import_zod15.z.object({
|
|
999
1180
|
/** World units. A letter sheet is ~1 × 1.4, a receipt ~1 × 2.6. */
|
|
1000
|
-
width:
|
|
1001
|
-
height:
|
|
1181
|
+
width: import_zod15.z.number().positive().max(20).default(1),
|
|
1182
|
+
height: import_zod15.z.number().positive().max(20).default(1.4),
|
|
1002
1183
|
/** Visual thickness in mm-ish units; drives edge/shadow treatment, not geometry (yet). */
|
|
1003
|
-
thickness:
|
|
1184
|
+
thickness: import_zod15.z.number().min(0).max(2).default(0.2),
|
|
1004
1185
|
/**
|
|
1005
1186
|
* `'auto'` sizes the grid from the active deformers' needs — genuinely, as
|
|
1006
1187
|
* of 0.3.0. It asks each one what these options require (a gentle bend and
|
|
@@ -1016,8 +1197,8 @@ var sheetSchema = import_zod14.z.object({
|
|
|
1016
1197
|
* still raises it, because that is a correctness floor rather than a
|
|
1017
1198
|
* preference.
|
|
1018
1199
|
*/
|
|
1019
|
-
segments:
|
|
1020
|
-
cornerRadius:
|
|
1200
|
+
segments: import_zod15.z.union([import_zod15.z.literal("auto"), import_zod15.z.number().int().min(2).max(256)]).default("auto"),
|
|
1201
|
+
cornerRadius: import_zod15.z.number().min(0).max(0.5).default(0)
|
|
1021
1202
|
});
|
|
1022
1203
|
var stockNames = [
|
|
1023
1204
|
"printer",
|
|
@@ -1028,37 +1209,37 @@ var stockNames = [
|
|
|
1028
1209
|
"photo-gloss",
|
|
1029
1210
|
"sticker"
|
|
1030
1211
|
];
|
|
1031
|
-
var stockSchema =
|
|
1032
|
-
var washSchema =
|
|
1212
|
+
var stockSchema = import_zod15.z.enum(stockNames);
|
|
1213
|
+
var washSchema = import_zod15.z.object({
|
|
1033
1214
|
/** The first pigment. */
|
|
1034
|
-
color:
|
|
1215
|
+
color: import_zod15.z.string().default("#4a5b8c").describe("color"),
|
|
1035
1216
|
/** The second. Blooms alternate, and overlaps multiply into a third. */
|
|
1036
|
-
secondary:
|
|
1217
|
+
secondary: import_zod15.z.string().default("#b06a6a").describe("color"),
|
|
1037
1218
|
/** How many pools of colour. */
|
|
1038
|
-
blooms:
|
|
1219
|
+
blooms: import_zod15.z.number().int().min(1).max(24).default(7),
|
|
1039
1220
|
/** How far a pool runs before it dries — its size against the sheet. */
|
|
1040
|
-
spread:
|
|
1221
|
+
spread: import_zod15.z.number().min(0.1).max(1).default(0.7),
|
|
1041
1222
|
/** Softness of the wet edge. 0 is a hard cut, 1 is a pool still moving. */
|
|
1042
|
-
bleed:
|
|
1223
|
+
bleed: import_zod15.z.number().min(0).max(1).default(0.5),
|
|
1043
1224
|
/** How much pigment is in the water. */
|
|
1044
|
-
intensity:
|
|
1225
|
+
intensity: import_zod15.z.number().min(0).max(1).default(0.55),
|
|
1045
1226
|
/**
|
|
1046
1227
|
* Edge darkening — the ring of pigment left where a pool dried.
|
|
1047
1228
|
*
|
|
1048
1229
|
* The signature of the medium, and the one thing a plain gradient cannot
|
|
1049
1230
|
* fake. Without it a wash reads as an airbrush.
|
|
1050
1231
|
*/
|
|
1051
|
-
edge:
|
|
1232
|
+
edge: import_zod15.z.number().min(0).max(1).default(0.6),
|
|
1052
1233
|
/** Pigment settling into the tooth of the paper. */
|
|
1053
|
-
granulation:
|
|
1234
|
+
granulation: import_zod15.z.number().min(0).max(1).default(0.35),
|
|
1054
1235
|
/** Fixed so a preset paints the same wash every time. */
|
|
1055
|
-
seed:
|
|
1236
|
+
seed: import_zod15.z.number().int().min(0).max(99).default(0)
|
|
1056
1237
|
});
|
|
1057
|
-
var blankContentBase =
|
|
1058
|
-
type:
|
|
1238
|
+
var blankContentBase = import_zod15.z.object({
|
|
1239
|
+
type: import_zod15.z.literal("blank")
|
|
1059
1240
|
});
|
|
1060
|
-
var imageContentBase =
|
|
1061
|
-
type:
|
|
1241
|
+
var imageContentBase = import_zod15.z.object({
|
|
1242
|
+
type: import_zod15.z.literal("image"),
|
|
1062
1243
|
/**
|
|
1063
1244
|
* Empty means "no picture yet", and renders as bare stock rather than as
|
|
1064
1245
|
* a failure. That is what lets a built-in preset be an image preset
|
|
@@ -1066,30 +1247,30 @@ var imageContentBase = import_zod14.z.object({
|
|
|
1066
1247
|
* `postage-stamp` are containers for the caller's own art, handed over via
|
|
1067
1248
|
* `<PaperField images={...} />` or `content.src`.
|
|
1068
1249
|
*/
|
|
1069
|
-
src:
|
|
1070
|
-
fit:
|
|
1250
|
+
src: import_zod15.z.string().default(""),
|
|
1251
|
+
fit: import_zod15.z.enum(["cover", "contain"]).default("cover"),
|
|
1071
1252
|
/** Read by the hidden DOM mirror and the no-WebGL fallback. */
|
|
1072
|
-
alt:
|
|
1253
|
+
alt: import_zod15.z.string().optional()
|
|
1073
1254
|
});
|
|
1074
|
-
var textContentBase =
|
|
1075
|
-
type:
|
|
1076
|
-
text:
|
|
1077
|
-
font:
|
|
1255
|
+
var textContentBase = import_zod15.z.object({
|
|
1256
|
+
type: import_zod15.z.literal("text"),
|
|
1257
|
+
text: import_zod15.z.string().default("Dear reader,"),
|
|
1258
|
+
font: import_zod15.z.string().default('Georgia, "Times New Roman", serif'),
|
|
1078
1259
|
/** px at texture resolution (long edge = 1024 logical px before DPR). */
|
|
1079
|
-
size:
|
|
1080
|
-
weight:
|
|
1081
|
-
color:
|
|
1082
|
-
align:
|
|
1260
|
+
size: import_zod15.z.number().min(8).max(256).default(44),
|
|
1261
|
+
weight: import_zod15.z.number().min(100).max(900).default(400),
|
|
1262
|
+
color: import_zod15.z.string().default("#2b2620").describe("color"),
|
|
1263
|
+
align: import_zod15.z.enum(["left", "center", "right"]).default("left"),
|
|
1083
1264
|
/** Fraction of the short edge. */
|
|
1084
|
-
padding:
|
|
1085
|
-
lineHeight:
|
|
1265
|
+
padding: import_zod15.z.number().min(0).max(0.4).default(0.09),
|
|
1266
|
+
lineHeight: import_zod15.z.number().min(0.8).max(3).default(1.45),
|
|
1086
1267
|
/**
|
|
1087
1268
|
* Letter-spacing, in em. The one control display type cannot do without:
|
|
1088
1269
|
* a line set large enough to be read across a room needs its tracking
|
|
1089
1270
|
* pulled IN, and a small line of uppercase small-print needs it pushed
|
|
1090
1271
|
* out, and neither is achievable by changing the size.
|
|
1091
1272
|
*/
|
|
1092
|
-
tracking:
|
|
1273
|
+
tracking: import_zod15.z.number().min(-0.1).max(0.6).default(0),
|
|
1093
1274
|
/**
|
|
1094
1275
|
* Where the block sits down the sheet.
|
|
1095
1276
|
*
|
|
@@ -1098,26 +1279,26 @@ var textContentBase = import_zod14.z.object({
|
|
|
1098
1279
|
* poster wants — a block of type optically centred in the sheet rather
|
|
1099
1280
|
* than hung from its top edge.
|
|
1100
1281
|
*/
|
|
1101
|
-
valign:
|
|
1282
|
+
valign: import_zod15.z.enum(["top", "center"]).default("top")
|
|
1102
1283
|
});
|
|
1103
|
-
var cardContentBase =
|
|
1104
|
-
type:
|
|
1284
|
+
var cardContentBase = import_zod15.z.object({
|
|
1285
|
+
type: import_zod15.z.literal("card"),
|
|
1105
1286
|
/** Small, tracked, uppercase by convention — the label at the top. */
|
|
1106
|
-
title:
|
|
1287
|
+
title: import_zod15.z.string().default(""),
|
|
1107
1288
|
/** The card's reason for existing. */
|
|
1108
|
-
body:
|
|
1289
|
+
body: import_zod15.z.string().default(""),
|
|
1109
1290
|
/** Attribution, catalogue number, date — the line in small print at the foot. */
|
|
1110
|
-
note:
|
|
1291
|
+
note: import_zod15.z.string().default(""),
|
|
1111
1292
|
/** A hairline under the title. What separates a label from a paragraph. */
|
|
1112
|
-
rule:
|
|
1293
|
+
rule: import_zod15.z.boolean().default(true),
|
|
1113
1294
|
/**
|
|
1114
1295
|
* Ruled writing lines behind the body, as on an index card.
|
|
1115
1296
|
*
|
|
1116
1297
|
* Drawn UNDER the type and in the stock's own ink at low alpha, so they
|
|
1117
1298
|
* read as printed on the card rather than as underlines on the words.
|
|
1118
1299
|
*/
|
|
1119
|
-
ruled:
|
|
1120
|
-
font:
|
|
1300
|
+
ruled: import_zod15.z.boolean().default(false),
|
|
1301
|
+
font: import_zod15.z.string().default('Georgia, "Times New Roman", serif'),
|
|
1121
1302
|
/**
|
|
1122
1303
|
* Body size, px at texture resolution. Title and note derive from it.
|
|
1123
1304
|
*
|
|
@@ -1127,28 +1308,28 @@ var cardContentBase = import_zod14.z.object({
|
|
|
1127
1308
|
* third of the stock empty above and below it, which reads as a page that
|
|
1128
1309
|
* was cropped rather than as a card that was set.
|
|
1129
1310
|
*/
|
|
1130
|
-
size:
|
|
1131
|
-
color:
|
|
1132
|
-
align:
|
|
1133
|
-
padding:
|
|
1311
|
+
size: import_zod15.z.number().min(8).max(256).default(58),
|
|
1312
|
+
color: import_zod15.z.string().default("#2b2620").describe("color"),
|
|
1313
|
+
align: import_zod15.z.enum(["left", "center"]).default("left"),
|
|
1314
|
+
padding: import_zod15.z.number().min(0).max(0.4).default(0.1)
|
|
1134
1315
|
});
|
|
1135
|
-
var receiptContentBase =
|
|
1136
|
-
type:
|
|
1137
|
-
store:
|
|
1138
|
-
address:
|
|
1139
|
-
items:
|
|
1316
|
+
var receiptContentBase = import_zod15.z.object({
|
|
1317
|
+
type: import_zod15.z.literal("receipt"),
|
|
1318
|
+
store: import_zod15.z.string().default("PAPERLAB"),
|
|
1319
|
+
address: import_zod15.z.string().default("124 PAPER ST"),
|
|
1320
|
+
items: import_zod15.z.array(import_zod15.z.object({ name: import_zod15.z.string(), price: import_zod15.z.number() })).default([
|
|
1140
1321
|
{ name: "CURL, TRUE", price: 12 },
|
|
1141
1322
|
{ name: "ROLL, TIGHT", price: 8.5 },
|
|
1142
1323
|
{ name: "SHEET, ONE", price: 0.99 }
|
|
1143
1324
|
]),
|
|
1144
|
-
taxRate:
|
|
1145
|
-
barcode:
|
|
1325
|
+
taxRate: import_zod15.z.number().min(0).max(1).default(0.08),
|
|
1326
|
+
barcode: import_zod15.z.boolean().default(true),
|
|
1146
1327
|
/** Fixed so presets render deterministically; omit for "now". */
|
|
1147
|
-
timestamp:
|
|
1148
|
-
footer:
|
|
1328
|
+
timestamp: import_zod15.z.string().optional(),
|
|
1329
|
+
footer: import_zod15.z.string().default("KEEP FOR YOUR RECORDS")
|
|
1149
1330
|
});
|
|
1150
1331
|
var withWash = { wash: washSchema.optional() };
|
|
1151
|
-
var backContentSchema =
|
|
1332
|
+
var backContentSchema = import_zod15.z.discriminatedUnion("type", [
|
|
1152
1333
|
blankContentBase.extend(withWash),
|
|
1153
1334
|
imageContentBase.extend(withWash),
|
|
1154
1335
|
textContentBase.extend(withWash),
|
|
@@ -1161,7 +1342,7 @@ var imageContentSchema = imageContentBase.extend(withBack);
|
|
|
1161
1342
|
var textContentSchema = textContentBase.extend(withBack);
|
|
1162
1343
|
var cardContentSchema = cardContentBase.extend(withBack);
|
|
1163
1344
|
var receiptContentSchema = receiptContentBase.extend(withBack);
|
|
1164
|
-
var contentSchema =
|
|
1345
|
+
var contentSchema = import_zod15.z.discriminatedUnion("type", [
|
|
1165
1346
|
blankContentSchema,
|
|
1166
1347
|
imageContentSchema,
|
|
1167
1348
|
textContentSchema,
|
|
@@ -1177,80 +1358,146 @@ function contentSchemaFor(type) {
|
|
|
1177
1358
|
return option;
|
|
1178
1359
|
}
|
|
1179
1360
|
var paperEdges = ["top", "right", "bottom", "left"];
|
|
1180
|
-
var surfaceSchema =
|
|
1361
|
+
var surfaceSchema = import_zod15.z.object({
|
|
1181
1362
|
/** Paper fiber noise, 0..1. */
|
|
1182
|
-
grain:
|
|
1363
|
+
grain: import_zod15.z.number().min(0).max(1).optional(),
|
|
1183
1364
|
/** Light passing through the sheet from behind, 0..1. Stock defaults apply. */
|
|
1184
|
-
translucency:
|
|
1365
|
+
translucency: import_zod15.z.number().min(0).max(1).optional(),
|
|
1185
1366
|
/** Torn-edge alpha with a lightened fiber band. */
|
|
1186
|
-
deckle:
|
|
1187
|
-
edges:
|
|
1188
|
-
roughness:
|
|
1367
|
+
deckle: import_zod15.z.object({
|
|
1368
|
+
edges: import_zod15.z.array(import_zod15.z.enum(paperEdges)).default(["bottom"]),
|
|
1369
|
+
roughness: import_zod15.z.number().min(0).max(1).default(0.5)
|
|
1189
1370
|
}).optional(),
|
|
1190
1371
|
/** Visual AO/highlight companion to the fold deformer. */
|
|
1191
|
-
creaseLines:
|
|
1372
|
+
creaseLines: import_zod15.z.object({
|
|
1192
1373
|
/** Crease line direction, degrees (0 = horizontal lines). */
|
|
1193
|
-
angle:
|
|
1374
|
+
angle: import_zod15.z.number().min(-360).max(360).default(0),
|
|
1194
1375
|
/** Positions across the sheet, 0..1 fractions. */
|
|
1195
|
-
positions:
|
|
1196
|
-
strength:
|
|
1376
|
+
positions: import_zod15.z.array(import_zod15.z.number().min(0).max(1)).default([1 / 3, 2 / 3]),
|
|
1377
|
+
strength: import_zod15.z.number().min(0).max(1).default(0.5)
|
|
1197
1378
|
}).optional(),
|
|
1198
1379
|
/** Yellowing + foxing spots, 0..1. */
|
|
1199
|
-
aging:
|
|
1380
|
+
aging: import_zod15.z.number().min(0).max(1).optional(),
|
|
1200
1381
|
/** Reversed front-content ghost on the backside, 0..1. Stock defaults apply. */
|
|
1201
|
-
showThrough:
|
|
1382
|
+
showThrough: import_zod15.z.number().min(0).max(1).optional(),
|
|
1202
1383
|
/**
|
|
1203
1384
|
* Postage-stamp perforation: alpha-punched semicircular holes along chosen
|
|
1204
1385
|
* edges. `state` flips an edge to a ripped-through profile (torn) — set
|
|
1205
1386
|
* automatically when a paper detaches from a `sheet` field, manual wins.
|
|
1206
1387
|
*/
|
|
1207
|
-
perforation:
|
|
1208
|
-
edges:
|
|
1388
|
+
perforation: import_zod15.z.object({
|
|
1389
|
+
edges: import_zod15.z.union([import_zod15.z.array(import_zod15.z.enum(paperEdges)), import_zod15.z.literal("all")]).default("all"),
|
|
1209
1390
|
/** World units — default tuned to stamp scale. */
|
|
1210
|
-
holeRadius:
|
|
1211
|
-
spacing:
|
|
1212
|
-
state:
|
|
1213
|
-
top:
|
|
1214
|
-
right:
|
|
1215
|
-
bottom:
|
|
1216
|
-
left:
|
|
1391
|
+
holeRadius: import_zod15.z.number().min(2e-3).max(0.1).default(0.016),
|
|
1392
|
+
spacing: import_zod15.z.number().min(0.01).max(0.5).default(0.055),
|
|
1393
|
+
state: import_zod15.z.object({
|
|
1394
|
+
top: import_zod15.z.enum(["intact", "torn"]).optional(),
|
|
1395
|
+
right: import_zod15.z.enum(["intact", "torn"]).optional(),
|
|
1396
|
+
bottom: import_zod15.z.enum(["intact", "torn"]).optional(),
|
|
1397
|
+
left: import_zod15.z.enum(["intact", "torn"]).optional()
|
|
1217
1398
|
}).default({})
|
|
1218
1399
|
}).optional()
|
|
1219
1400
|
});
|
|
1220
|
-
var
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1401
|
+
var creaseSchema = import_zod15.z.object({
|
|
1402
|
+
angle: import_zod15.z.number().min(-360).max(360).default(90),
|
|
1403
|
+
offset: import_zod15.z.number().min(-20).max(20).default(0),
|
|
1404
|
+
/**
|
|
1405
|
+
* The residual fold angle in degrees, signed — how far open the crease
|
|
1406
|
+
* still sits once nothing is holding the paper. This is the whole of what
|
|
1407
|
+
* a crease IS: geometry and shading both read it, and it is what makes the
|
|
1408
|
+
* field authorable by hand (a dog-ear is a crease with a big `depth` near
|
|
1409
|
+
* a corner) rather than only recordable.
|
|
1410
|
+
*/
|
|
1411
|
+
depth: import_zod15.z.number().min(-180).max(180).default(12)
|
|
1412
|
+
});
|
|
1413
|
+
var memorySchema = import_zod15.z.object({
|
|
1414
|
+
/**
|
|
1415
|
+
* How much of a fold this paper keeps, 0..1, over the stock's own
|
|
1416
|
+
* {@link Stock.takesSet}. Kraft holds a crease hard; vellum springs most
|
|
1417
|
+
* of the way back.
|
|
1418
|
+
*/
|
|
1419
|
+
set: import_zod15.z.number().min(0).max(1).optional(),
|
|
1420
|
+
/**
|
|
1421
|
+
* The creases themselves. Recorded by folding the paper (see
|
|
1422
|
+
* `onCrease`), or written by hand — a preset can ship already creased.
|
|
1423
|
+
*
|
|
1424
|
+
* Capped at four because that is what the crease shader carries, and a
|
|
1425
|
+
* cap the schema states is better than one the renderer applies silently.
|
|
1426
|
+
*/
|
|
1427
|
+
creases: import_zod15.z.array(creaseSchema).max(4).default([])
|
|
1428
|
+
});
|
|
1429
|
+
var behaviorConfigSchema = import_zod15.z.discriminatedUnion("type", [
|
|
1430
|
+
peelOptionsSchema.extend({ type: import_zod15.z.literal("peel") }),
|
|
1431
|
+
unrollOptionsSchema.extend({ type: import_zod15.z.literal("unroll") }),
|
|
1432
|
+
flipOptionsSchema.extend({ type: import_zod15.z.literal("flip") }),
|
|
1433
|
+
letterFoldOptionsSchema.extend({ type: import_zod15.z.literal("letter-fold") }),
|
|
1434
|
+
hangOptionsSchema.extend({ type: import_zod15.z.literal("hang") }),
|
|
1435
|
+
flyOptionsSchema.extend({ type: import_zod15.z.literal("fly") }),
|
|
1436
|
+
fallOptionsSchema.extend({ type: import_zod15.z.literal("fall") }),
|
|
1437
|
+
carryOptionsSchema.extend({ type: import_zod15.z.literal("carry") }),
|
|
1438
|
+
flightOptionsSchema.extend({ type: import_zod15.z.literal("flight") }),
|
|
1439
|
+
crumpleBehaviorOptionsSchema.extend({ type: import_zod15.z.literal("crumple") }),
|
|
1440
|
+
settleOptionsSchema.extend({ type: import_zod15.z.literal("settle") }),
|
|
1441
|
+
ribbonOptionsSchema.extend({ type: import_zod15.z.literal("ribbon") })
|
|
1233
1442
|
]);
|
|
1234
|
-
var deformerInstanceSchema =
|
|
1235
|
-
type:
|
|
1236
|
-
options:
|
|
1237
|
-
enabled:
|
|
1443
|
+
var deformerInstanceSchema = import_zod15.z.object({
|
|
1444
|
+
type: import_zod15.z.string(),
|
|
1445
|
+
options: import_zod15.z.record(import_zod15.z.string(), import_zod15.z.unknown()).default({}),
|
|
1446
|
+
enabled: import_zod15.z.boolean().default(true)
|
|
1238
1447
|
});
|
|
1239
1448
|
var physicsNames = ["none", "float", "tumble", "dangle", "taped", "breeze"];
|
|
1240
|
-
var clothConfigSchema =
|
|
1241
|
-
type:
|
|
1242
|
-
pins:
|
|
1243
|
-
wind:
|
|
1449
|
+
var clothConfigSchema = import_zod15.z.object({
|
|
1450
|
+
type: import_zod15.z.literal("cloth"),
|
|
1451
|
+
pins: import_zod15.z.enum(["top-edge", "top-corners", "corner", "none"]).default("top-edge"),
|
|
1452
|
+
wind: import_zod15.z.number().min(0).max(1).default(0.3),
|
|
1244
1453
|
/** Bend stiffness: 1 = crisp paper, 0 = silk. */
|
|
1245
|
-
stiffness:
|
|
1246
|
-
gravity:
|
|
1454
|
+
stiffness: import_zod15.z.number().min(0).max(1).default(0.8),
|
|
1455
|
+
gravity: import_zod15.z.number().min(0).max(2).default(1),
|
|
1247
1456
|
/** Local-space ground plane the sheet settles onto. */
|
|
1248
|
-
floor:
|
|
1457
|
+
floor: import_zod15.z.number().min(-5).max(0).default(-1.4)
|
|
1249
1458
|
});
|
|
1250
|
-
var
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1459
|
+
var stripConfigSchema = import_zod15.z.object({
|
|
1460
|
+
type: import_zod15.z.literal("strip"),
|
|
1461
|
+
/** How far the page has scrolled, in world units of paper asked for. */
|
|
1462
|
+
scroll: import_zod15.z.number().min(-1e3).max(1e3).default(0),
|
|
1463
|
+
/** Thin layers and many turns, or few and fat. */
|
|
1464
|
+
tightness: import_zod15.z.number().min(0).max(1).default(0.6),
|
|
1465
|
+
/** Radius of the cardboard tube — the roll never pays out past it. */
|
|
1466
|
+
core: import_zod15.z.number().min(0.01).max(0.5).default(0.09),
|
|
1467
|
+
/** Paper already hanging before the first scroll. A roll always has a leaf out. */
|
|
1468
|
+
tail: import_zod15.z.number().min(0).max(20).default(1.1),
|
|
1469
|
+
/** Spacing of the perforations: one sheet's worth of strip, in world units. */
|
|
1470
|
+
perforation: import_zod15.z.number().min(0.05).max(5).default(1),
|
|
1471
|
+
/**
|
|
1472
|
+
* How much a perforation remembers being folded. 0 = a fresh roll, 1 = one
|
|
1473
|
+
* that has been used.
|
|
1474
|
+
*
|
|
1475
|
+
* The default is high on purpose: below about 0.6 the landed paper flops
|
|
1476
|
+
* over in flat panels and spreads across the floor, and at 0.7 it holds its
|
|
1477
|
+
* folds and stacks into an accordion. The pile is the point.
|
|
1478
|
+
*/
|
|
1479
|
+
crease: import_zod15.z.number().min(0).max(1).default(0.7),
|
|
1480
|
+
/** Bend stiffness between perforations. 1 = card, 0 = cloth. */
|
|
1481
|
+
stiffness: import_zod15.z.number().min(0).max(1).default(0.55),
|
|
1482
|
+
/** Broadside air drag — what makes paper float down rather than drop. */
|
|
1483
|
+
drag: import_zod15.z.number().min(0).max(1).default(0.55),
|
|
1484
|
+
gravity: import_zod15.z.number().min(0).max(2).default(1),
|
|
1485
|
+
/**
|
|
1486
|
+
* How far below the roll the paper lands. A DISTANCE below the roll's axis,
|
|
1487
|
+
* matching `unroll.floor`, not a signed y like `cloth.floor` — the roll
|
|
1488
|
+
* family measures drops, and the composition is centred on the origin so
|
|
1489
|
+
* an absolute y would not survive the offset anyway.
|
|
1490
|
+
*/
|
|
1491
|
+
floor: import_zod15.z.number().min(0.1).max(30).default(1.2),
|
|
1492
|
+
/** How long the roll coasts after the scroll stops. */
|
|
1493
|
+
inertia: import_zod15.z.number().min(0).max(1).default(0.45)
|
|
1494
|
+
});
|
|
1495
|
+
var physicsSchema = import_zod15.z.union([
|
|
1496
|
+
import_zod15.z.enum(physicsNames),
|
|
1497
|
+
import_zod15.z.literal("cloth").transform(() => clothConfigSchema.parse({ type: "cloth" })),
|
|
1498
|
+
import_zod15.z.literal("strip").transform(() => stripConfigSchema.parse({ type: "strip" })),
|
|
1499
|
+
clothConfigSchema,
|
|
1500
|
+
stripConfigSchema
|
|
1254
1501
|
]);
|
|
1255
1502
|
var lightingNames = [
|
|
1256
1503
|
"studio",
|
|
@@ -1263,9 +1510,9 @@ var lightingNames = [
|
|
|
1263
1510
|
"lightbox"
|
|
1264
1511
|
];
|
|
1265
1512
|
var filmNames = ["agx", "neutral", "filmic"];
|
|
1266
|
-
var lightSchema =
|
|
1513
|
+
var lightSchema = import_zod15.z.object({
|
|
1267
1514
|
/** Tone-mapping exposure — the stop the whole picture is printed at. */
|
|
1268
|
-
exposure:
|
|
1515
|
+
exposure: import_zod15.z.number().min(0.1).max(4).optional(),
|
|
1269
1516
|
/**
|
|
1270
1517
|
* The tone curve — the film, where `exposure` is the stop.
|
|
1271
1518
|
*
|
|
@@ -1274,33 +1521,33 @@ var lightSchema = import_zod14.z.object({
|
|
|
1274
1521
|
* the wrong film: it desaturates and drags bright neutrals toward
|
|
1275
1522
|
* yellow-green, which is the sepia cast a lit sheet used to pick up.
|
|
1276
1523
|
*/
|
|
1277
|
-
film:
|
|
1524
|
+
film: import_zod15.z.enum(filmNames).optional(),
|
|
1278
1525
|
/** Key light strength. */
|
|
1279
|
-
key:
|
|
1526
|
+
key: import_zod15.z.number().min(0).max(12).optional(),
|
|
1280
1527
|
/** Key light colour. */
|
|
1281
|
-
color:
|
|
1528
|
+
color: import_zod15.z.string().optional().describe("color"),
|
|
1282
1529
|
/**
|
|
1283
1530
|
* Where the key stands, degrees around the vertical. 0° is straight in
|
|
1284
1531
|
* front of the paper (+Z, beside the camera), 90° is off to the right,
|
|
1285
1532
|
* and ±180° is directly behind it — which is where `nave` puts it, and
|
|
1286
1533
|
* why that preset is carried by light coming THROUGH the paper.
|
|
1287
1534
|
*/
|
|
1288
|
-
direction:
|
|
1535
|
+
direction: import_zod15.z.number().min(-180).max(180).optional(),
|
|
1289
1536
|
/** How high the key stands, degrees above the horizon. */
|
|
1290
|
-
height:
|
|
1537
|
+
height: import_zod15.z.number().min(-30).max(89).optional(),
|
|
1291
1538
|
/** Flat fill from every direction at once. Cheap, and it kills form — reach for `studio` first. */
|
|
1292
|
-
ambient:
|
|
1539
|
+
ambient: import_zod15.z.number().min(0).max(2).optional(),
|
|
1293
1540
|
/** The room's own light: an environment map built from `sky`. Directional fill, and the only thing paper's sheen has to reflect. */
|
|
1294
|
-
studio:
|
|
1541
|
+
studio: import_zod15.z.number().min(0).max(3).optional(),
|
|
1295
1542
|
/** Distance haze, as a multiple of the preset's. 0 clears the air entirely; 2 halves the distance you can see. */
|
|
1296
|
-
haze:
|
|
1543
|
+
haze: import_zod15.z.number().min(0).max(3).optional()
|
|
1297
1544
|
});
|
|
1298
|
-
var backdropSchema =
|
|
1545
|
+
var backdropSchema = import_zod15.z.object({
|
|
1299
1546
|
/** Behind everything, and behind the picture where it does not reach. */
|
|
1300
|
-
color:
|
|
1547
|
+
color: import_zod15.z.string().default("#171717").describe("color"),
|
|
1301
1548
|
/** A URL, or an uploaded picture. Empty is the colour on its own. */
|
|
1302
|
-
image:
|
|
1303
|
-
fit:
|
|
1549
|
+
image: import_zod15.z.string().default(""),
|
|
1550
|
+
fit: import_zod15.z.enum(["cover", "contain"]).default("cover"),
|
|
1304
1551
|
/**
|
|
1305
1552
|
* Toward the colour, so the paper stays the subject.
|
|
1306
1553
|
*
|
|
@@ -1309,14 +1556,35 @@ var backdropSchema = import_zod14.z.object({
|
|
|
1309
1556
|
* of the light, and what this solves by mixing it back toward the ground
|
|
1310
1557
|
* it sits on.
|
|
1311
1558
|
*/
|
|
1312
|
-
fade:
|
|
1559
|
+
fade: import_zod15.z.number().min(0).max(1).default(0.25),
|
|
1313
1560
|
/** Out of focus, for the same reason. */
|
|
1314
|
-
blur:
|
|
1561
|
+
blur: import_zod15.z.number().min(0).max(1).default(0.2)
|
|
1315
1562
|
});
|
|
1316
|
-
var sceneSchema =
|
|
1317
|
-
lighting:
|
|
1563
|
+
var sceneSchema = import_zod15.z.object({
|
|
1564
|
+
lighting: import_zod15.z.enum(lightingNames).default("studio"),
|
|
1318
1565
|
/** What is behind the sheet. Unset leaves the canvas alone. */
|
|
1319
1566
|
backdrop: backdropSchema.optional(),
|
|
1567
|
+
/**
|
|
1568
|
+
* Degrees the whole composition is turned about its vertical axis, so a
|
|
1569
|
+
* preset can choose the angle it is READ from.
|
|
1570
|
+
*
|
|
1571
|
+
* Every camera in the library is fixed and head-on — `<Paper>` sits at
|
|
1572
|
+
* `(0, 0.35, 2.4)` looking down -Z, and neither it nor the editor fits a
|
|
1573
|
+
* camera to its content. That is the right default for a sheet, which is
|
|
1574
|
+
* flat and faces you. It is the wrong one for anything whose shape lives
|
|
1575
|
+
* in DEPTH: the `strip` sim folds in z by construction, so head-on its
|
|
1576
|
+
* roll and the whole accordion of its pile are edge-on and the preset
|
|
1577
|
+
* renders as a blank white column.
|
|
1578
|
+
*
|
|
1579
|
+
* A camera field would have been the other way to fix it, and is worse: it
|
|
1580
|
+
* is meaningless inside `<PaperField>` and `<PaperMesh>`, where the caller
|
|
1581
|
+
* owns the camera and there may be a dozen papers sharing it. Turning the
|
|
1582
|
+
* paper works everywhere, because it is a property of the paper.
|
|
1583
|
+
*
|
|
1584
|
+
* Additive with the `rotation` prop rather than overriding it — the prop
|
|
1585
|
+
* is the caller's, and a preset does not get to overrule it.
|
|
1586
|
+
*/
|
|
1587
|
+
turn: import_zod15.z.number().min(-180).max(180).default(0),
|
|
1320
1588
|
/**
|
|
1321
1589
|
* Overrides on the named preset — the same authorable half stage mode has
|
|
1322
1590
|
* always had, and which a lone sheet had no way to reach.
|
|
@@ -1330,56 +1598,65 @@ var sceneSchema = import_zod14.z.object({
|
|
|
1330
1598
|
});
|
|
1331
1599
|
var coreStateNames = ["rest", "hover", "pressed", "picked", "placed"];
|
|
1332
1600
|
var isStateName = (s) => coreStateNames.includes(s) || s.startsWith("custom:");
|
|
1333
|
-
var stateNameSchema =
|
|
1601
|
+
var stateNameSchema = import_zod15.z.string().refine(isStateName, {
|
|
1334
1602
|
message: `state names are ${coreStateNames.join(", ")} or "custom:<name>"`
|
|
1335
1603
|
});
|
|
1336
|
-
var stateTransitionSchema =
|
|
1337
|
-
duration:
|
|
1604
|
+
var stateTransitionSchema = import_zod15.z.object({
|
|
1605
|
+
duration: import_zod15.z.number().min(0).max(5).default(0.35),
|
|
1338
1606
|
/** GSAP ease name. */
|
|
1339
|
-
ease:
|
|
1607
|
+
ease: import_zod15.z.string().default("power2.out")
|
|
1340
1608
|
});
|
|
1341
|
-
var stateDefSchema =
|
|
1609
|
+
var stateDefSchema = import_zod15.z.object({
|
|
1342
1610
|
/** Deep-partial override of the paper schema (behavior params, surface, …). */
|
|
1343
|
-
overrides:
|
|
1611
|
+
overrides: import_zod15.z.record(import_zod15.z.string(), import_zod15.z.unknown()).default({}),
|
|
1344
1612
|
/** Transition INTO this state. */
|
|
1345
|
-
transition: stateTransitionSchema.
|
|
1613
|
+
transition: stateTransitionSchema.prefault({}),
|
|
1346
1614
|
/** Chained actions after arriving. v1: 'emit:<event>' only. */
|
|
1347
|
-
onEnter:
|
|
1615
|
+
onEnter: import_zod15.z.array(import_zod15.z.string().regex(/^emit:[\w-]+$/, 'v1 actions are "emit:<event>"')).default([])
|
|
1348
1616
|
});
|
|
1349
|
-
var paperStatesSchema =
|
|
1617
|
+
var paperStatesSchema = import_zod15.z.object({
|
|
1350
1618
|
initial: stateNameSchema.default("rest"),
|
|
1351
|
-
states:
|
|
1619
|
+
states: import_zod15.z.record(import_zod15.z.string(), stateDefSchema).default({}).refine((rec) => Object.keys(rec).every(isStateName), {
|
|
1352
1620
|
message: `state names are ${coreStateNames.join(", ")} or "custom:<name>"`
|
|
1353
1621
|
}),
|
|
1354
1622
|
/** World-units drag distance that flips pressed → picked (pick-enabled behaviors only). */
|
|
1355
|
-
pickThreshold:
|
|
1623
|
+
pickThreshold: import_zod15.z.number().min(5e-3).max(1).default(0.1)
|
|
1356
1624
|
});
|
|
1357
|
-
var metaSchema =
|
|
1358
|
-
name:
|
|
1359
|
-
author:
|
|
1360
|
-
version:
|
|
1361
|
-
tags:
|
|
1625
|
+
var metaSchema = import_zod15.z.object({
|
|
1626
|
+
name: import_zod15.z.string().default("untitled"),
|
|
1627
|
+
author: import_zod15.z.string().optional(),
|
|
1628
|
+
version: import_zod15.z.string().default("0"),
|
|
1629
|
+
tags: import_zod15.z.array(import_zod15.z.string()).default([])
|
|
1362
1630
|
});
|
|
1363
|
-
var paperConfigSchema =
|
|
1364
|
-
meta: metaSchema.
|
|
1365
|
-
sheet: sheetSchema.
|
|
1631
|
+
var paperConfigSchema = import_zod15.z.object({
|
|
1632
|
+
meta: metaSchema.prefault({}),
|
|
1633
|
+
sheet: sheetSchema.prefault({}),
|
|
1366
1634
|
stock: stockSchema.default("printer"),
|
|
1367
1635
|
content: contentSchema.default({ type: "blank" }),
|
|
1368
1636
|
/** A behavior OR a raw deformer stack — if both are present, `deformers` wins (it's the fork). */
|
|
1369
1637
|
behavior: behaviorConfigSchema.optional(),
|
|
1370
|
-
deformers:
|
|
1638
|
+
deformers: import_zod15.z.array(deformerInstanceSchema).optional(),
|
|
1371
1639
|
surface: surfaceSchema.default({}),
|
|
1640
|
+
/**
|
|
1641
|
+
* What the sheet remembers being folded — creases outlive the fold.
|
|
1642
|
+
*
|
|
1643
|
+
* Present by default, and on: paper that forgets is the bug this exists
|
|
1644
|
+
* to fix, so remembering is not something a preset should have to ask
|
|
1645
|
+
* for. `memory: { set: 0 }` is the opt-out, and it is what every sheet in
|
|
1646
|
+
* the library did before this shipped.
|
|
1647
|
+
*/
|
|
1648
|
+
memory: memorySchema.prefault({}),
|
|
1372
1649
|
physics: physicsSchema.default("none"),
|
|
1373
|
-
scene: sceneSchema.
|
|
1374
|
-
onTwos:
|
|
1650
|
+
scene: sceneSchema.prefault({}),
|
|
1651
|
+
onTwos: import_zod15.z.boolean().default(false),
|
|
1375
1652
|
/** Interaction state machine — overrides-on-base diffs. */
|
|
1376
1653
|
states: paperStatesSchema.optional()
|
|
1377
1654
|
}).superRefine((config, ctx) => {
|
|
1378
|
-
if (typeof config.physics === "object" && (config.behavior || config.deformers)) {
|
|
1655
|
+
if (typeof config.physics === "object" && config.physics.type === "strip" && (config.behavior || config.deformers)) {
|
|
1379
1656
|
ctx.addIssue({
|
|
1380
|
-
code:
|
|
1657
|
+
code: import_zod15.z.ZodIssueCode.custom,
|
|
1381
1658
|
path: ["physics"],
|
|
1382
|
-
message: "
|
|
1659
|
+
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)"
|
|
1383
1660
|
});
|
|
1384
1661
|
}
|
|
1385
1662
|
if (config.states) {
|
|
@@ -1388,7 +1665,7 @@ var paperConfigSchema = import_zod14.z.object({
|
|
|
1388
1665
|
if (!def) continue;
|
|
1389
1666
|
if (def.overrides.states !== void 0) {
|
|
1390
1667
|
ctx.addIssue({
|
|
1391
|
-
code:
|
|
1668
|
+
code: import_zod15.z.ZodIssueCode.custom,
|
|
1392
1669
|
path: ["states", "states", name, "overrides"],
|
|
1393
1670
|
message: "state overrides cannot override `states` (no nested state machines)"
|
|
1394
1671
|
});
|
|
@@ -1399,7 +1676,7 @@ var paperConfigSchema = import_zod14.z.object({
|
|
|
1399
1676
|
if (!result.success) {
|
|
1400
1677
|
const first = result.error.issues[0];
|
|
1401
1678
|
ctx.addIssue({
|
|
1402
|
-
code:
|
|
1679
|
+
code: import_zod15.z.ZodIssueCode.custom,
|
|
1403
1680
|
path: ["states", "states", name, "overrides"],
|
|
1404
1681
|
message: `state "${name}" overrides don't validate against the paper schema: ${first ? `${first.path.join(".")} \u2014 ${first.message}` : "invalid"}`
|
|
1405
1682
|
});
|
|
@@ -1529,7 +1806,9 @@ var stocks = {
|
|
|
1529
1806
|
banding: 0,
|
|
1530
1807
|
defaultSurface: { grain: 0.12 },
|
|
1531
1808
|
showThrough: 0,
|
|
1532
|
-
adhesive: false
|
|
1809
|
+
adhesive: false,
|
|
1810
|
+
// Office bond creases cleanly and holds it — the reference paper.
|
|
1811
|
+
takesSet: 0.6
|
|
1533
1812
|
},
|
|
1534
1813
|
thermal: {
|
|
1535
1814
|
id: "thermal",
|
|
@@ -1542,7 +1821,9 @@ var stocks = {
|
|
|
1542
1821
|
banding: 0.35,
|
|
1543
1822
|
defaultSurface: { aging: 0.1 },
|
|
1544
1823
|
showThrough: 0.06,
|
|
1545
|
-
adhesive: false
|
|
1824
|
+
adhesive: false,
|
|
1825
|
+
// Thin and already curled off a roll; a fold in it stays folded.
|
|
1826
|
+
takesSet: 0.65
|
|
1546
1827
|
},
|
|
1547
1828
|
kraft: {
|
|
1548
1829
|
id: "kraft",
|
|
@@ -1555,7 +1836,9 @@ var stocks = {
|
|
|
1555
1836
|
banding: 0,
|
|
1556
1837
|
defaultSurface: { grain: 0.5 },
|
|
1557
1838
|
showThrough: 0,
|
|
1558
|
-
adhesive: false
|
|
1839
|
+
adhesive: false,
|
|
1840
|
+
// Thick and fibrous. The crease is a break, and it never comes back.
|
|
1841
|
+
takesSet: 0.85
|
|
1559
1842
|
},
|
|
1560
1843
|
newsprint: {
|
|
1561
1844
|
id: "newsprint",
|
|
@@ -1568,7 +1851,9 @@ var stocks = {
|
|
|
1568
1851
|
banding: 0,
|
|
1569
1852
|
defaultSurface: { grain: 0.7, aging: 0.15 },
|
|
1570
1853
|
showThrough: 0.06,
|
|
1571
|
-
adhesive: false
|
|
1854
|
+
adhesive: false,
|
|
1855
|
+
// Soft, short-fibred, and barely sprung — it crumples rather than resists.
|
|
1856
|
+
takesSet: 0.8
|
|
1572
1857
|
},
|
|
1573
1858
|
vellum: {
|
|
1574
1859
|
id: "vellum",
|
|
@@ -1581,7 +1866,9 @@ var stocks = {
|
|
|
1581
1866
|
banding: 0,
|
|
1582
1867
|
defaultSurface: {},
|
|
1583
1868
|
showThrough: 0.55,
|
|
1584
|
-
adhesive: false
|
|
1869
|
+
adhesive: false,
|
|
1870
|
+
// Translucent and plasticky: it fights the fold and mostly wins.
|
|
1871
|
+
takesSet: 0.25
|
|
1585
1872
|
},
|
|
1586
1873
|
"photo-gloss": {
|
|
1587
1874
|
id: "photo-gloss",
|
|
@@ -1594,7 +1881,9 @@ var stocks = {
|
|
|
1594
1881
|
banding: 0,
|
|
1595
1882
|
defaultSurface: {},
|
|
1596
1883
|
showThrough: 0,
|
|
1597
|
-
adhesive: false
|
|
1884
|
+
adhesive: false,
|
|
1885
|
+
// The coating resists, then cracks white — little angle kept, lots of mark.
|
|
1886
|
+
takesSet: 0.3
|
|
1598
1887
|
},
|
|
1599
1888
|
// Photo-gloss-like face, glossy near-white glue underside. The default
|
|
1600
1889
|
// carrier for perforated stamp sheets.
|
|
@@ -1609,7 +1898,9 @@ var stocks = {
|
|
|
1609
1898
|
banding: 0,
|
|
1610
1899
|
defaultSurface: {},
|
|
1611
1900
|
showThrough: 0,
|
|
1612
|
-
adhesive: true
|
|
1901
|
+
adhesive: true,
|
|
1902
|
+
// A face sheet on a release liner; the liner does most of the remembering.
|
|
1903
|
+
takesSet: 0.5
|
|
1613
1904
|
}
|
|
1614
1905
|
};
|
|
1615
1906
|
function getStock(name) {
|
|
@@ -1636,6 +1927,183 @@ var builtins = {
|
|
|
1636
1927
|
behavior: { type: "unroll", progress: 0.55, tightness: 0.55, sway: 0.3 },
|
|
1637
1928
|
surface: { deckle: { edges: ["bottom"], roughness: 0.6 } }
|
|
1638
1929
|
},
|
|
1930
|
+
/**
|
|
1931
|
+
* A roll on a holder with a leaf already out, meant to be driven by scroll:
|
|
1932
|
+
* bind `behavior.progress` to how far down the page you are and the paper
|
|
1933
|
+
* pays out while the roll runs down toward its tube.
|
|
1934
|
+
*
|
|
1935
|
+
* The three parts that make it read as a real roll rather than a curled
|
|
1936
|
+
* sheet: `fixed` keeps the roll on its holder and moves the paper instead,
|
|
1937
|
+
* `tail` means there is always a leaf to take hold of, and `floor` gives
|
|
1938
|
+
* the drop somewhere to land — paper that reaches the ground creases and
|
|
1939
|
+
* lies down rather than hanging into the void forever. A `core` a third of
|
|
1940
|
+
* the full radius is a real cardboard tube, so the roll still looks like a
|
|
1941
|
+
* roll after it has been used down.
|
|
1942
|
+
*/
|
|
1943
|
+
"paper-roll": {
|
|
1944
|
+
meta: { name: "Paper roll", tags: ["roll", "unroll", "scroll", "hero"] },
|
|
1945
|
+
sheet: { width: 1, height: 5 },
|
|
1946
|
+
stock: "newsprint",
|
|
1947
|
+
content: { type: "blank" },
|
|
1948
|
+
behavior: {
|
|
1949
|
+
type: "unroll",
|
|
1950
|
+
progress: 0.25,
|
|
1951
|
+
tightness: 0.8,
|
|
1952
|
+
sway: 0.15,
|
|
1953
|
+
from: "top",
|
|
1954
|
+
fixed: true,
|
|
1955
|
+
core: 0.12,
|
|
1956
|
+
tail: 0.5,
|
|
1957
|
+
floor: 2.4
|
|
1958
|
+
},
|
|
1959
|
+
surface: { deckle: { edges: ["bottom"], roughness: 0.5 } }
|
|
1960
|
+
},
|
|
1961
|
+
/**
|
|
1962
|
+
* The one that is a SIMULATION rather than a shape.
|
|
1963
|
+
*
|
|
1964
|
+
* `paper-roll` above draws this same object with a deformer stack, and for
|
|
1965
|
+
* a roll paying out against a wall that is the cheaper and better answer.
|
|
1966
|
+
* This preset exists for the half that geometry cannot reach: what happens
|
|
1967
|
+
* once the paper hits the ground. A deformer can bend a sheet along a curve
|
|
1968
|
+
* you have already chosen; it cannot discover that a strip under
|
|
1969
|
+
* compression buckles at its weakest hinge, and it cannot let one fold land
|
|
1970
|
+
* on the one beneath it. Both of those are what a pile IS.
|
|
1971
|
+
*
|
|
1972
|
+
* Bind `physics.scroll` to the page and the roll turns:
|
|
1973
|
+
*
|
|
1974
|
+
* ```tsx
|
|
1975
|
+
* const [scroll, setScroll] = useState(0)
|
|
1976
|
+
* useEffect(() => {
|
|
1977
|
+
* const onScroll = () => setScroll(window.scrollY / 120)
|
|
1978
|
+
* window.addEventListener('scroll', onScroll, { passive: true })
|
|
1979
|
+
* return () => window.removeEventListener('scroll', onScroll)
|
|
1980
|
+
* }, [])
|
|
1981
|
+
* <Paper preset="toilet-roll" physics={{ type: 'strip', scroll }} />
|
|
1982
|
+
* ```
|
|
1983
|
+
*
|
|
1984
|
+
* It is a MONOTONIC world-unit number, not a 0..1 progress — the sim
|
|
1985
|
+
* differentiates it, so scrolling back up rewinds the roll and drags the
|
|
1986
|
+
* pile taut before it lifts.
|
|
1987
|
+
*
|
|
1988
|
+
* The proportions are the real object's: a panel as wide as it is long, so
|
|
1989
|
+
* `perforation` equals the sheet width and the strip tears into squares.
|
|
1990
|
+
*/
|
|
1991
|
+
"toilet-roll": {
|
|
1992
|
+
meta: { name: "Toilet roll", tags: ["roll", "scroll", "simulation", "hero"] },
|
|
1993
|
+
// The proportions are a real roll's, at the scale the library is viewed
|
|
1994
|
+
// at: `<Paper>` and the editor both look at the origin through about two
|
|
1995
|
+
// world units and neither fits a camera to its content, so the whole
|
|
1996
|
+
// composition — roll, drop and pile — has to live inside that. A panel is
|
|
1997
|
+
// as wide as it is long and the roll is about a panel across, which is
|
|
1998
|
+
// what a toilet roll is.
|
|
1999
|
+
//
|
|
2000
|
+
// Twenty-three panels of paper. Not a real roll's several hundred, but
|
|
2001
|
+
// enough that a full page of scrolling does not empty it: one scroll unit
|
|
2002
|
+
// is about one unit of paper on a fresh roll, so this is a couple of
|
|
2003
|
+
// screens' worth before the tube shows.
|
|
2004
|
+
sheet: { width: 0.6, height: 14 },
|
|
2005
|
+
stock: "printer",
|
|
2006
|
+
content: { type: "blank" },
|
|
2007
|
+
physics: {
|
|
2008
|
+
type: "strip",
|
|
2009
|
+
scroll: 0,
|
|
2010
|
+
// Wound tightly enough to hold this much paper at a believable size: the
|
|
2011
|
+
// outer radius lands at 0.61 of the panel width against a real roll's
|
|
2012
|
+
// 0.57, over about nine visible turns.
|
|
2013
|
+
//
|
|
2014
|
+
// `tightness` is doing double duty and the trade is worth knowing about.
|
|
2015
|
+
// A layer gap IS the paper's thickness, so it also sets how far apart
|
|
2016
|
+
// self-collision holds two folds — wind tighter for a neater roll and
|
|
2017
|
+
// the pile on the floor gets flatter, looser for a fatter pile and the
|
|
2018
|
+
// roll coarsens. This is the middle of that.
|
|
2019
|
+
//
|
|
2020
|
+
// It is ALSO the roll's rim, and that is the reason not to raise it on
|
|
2021
|
+
// looks alone. A layer gap is a real space between two wound turns, so
|
|
2022
|
+
// the roll's end face is concentric rings with nothing between them,
|
|
2023
|
+
// and off head-on you see between them — a fine sawtooth around the
|
|
2024
|
+
// rim that winding tighter genuinely reduces. It was tried. Tightening
|
|
2025
|
+
// to 0.78 also moves the roll's proportion to 0.553 of a panel width,
|
|
2026
|
+
// nearer a real roll's 0.57 than this is, so it looked like a free win
|
|
2027
|
+
// — and it throws the pile 1.33 units out in x against the 0.874 a
|
|
2028
|
+
// square parent can see, spreading 5.9 panel-widths. Framing beats the
|
|
2029
|
+
// rim. The sawtooth is what a roll wound from ONE zero-thickness
|
|
2030
|
+
// ribbon costs; it is not tuned away, and a caller who wants it gone
|
|
2031
|
+
// wants a thicker `stock` or fewer, fatter turns.
|
|
2032
|
+
tightness: 0.65,
|
|
2033
|
+
// A real cardboard tube, and a floor on how tight the spiral ever winds.
|
|
2034
|
+
// The innermost wrap is the coarsest thing in the roll — the same
|
|
2035
|
+
// arc-length step spans a bigger angle the smaller the radius — so the
|
|
2036
|
+
// core is what stops a nearly-empty roll turning back into a polygon.
|
|
2037
|
+
core: 0.12,
|
|
2038
|
+
// A panel and a half already hanging. A roll on a holder always has a
|
|
2039
|
+
// leaf out; starting from a bare cylinder reads as one still wrapped.
|
|
2040
|
+
tail: 0.9,
|
|
2041
|
+
perforation: 0.6,
|
|
2042
|
+
// The four numbers below were chosen TOGETHER, and by worst case rather
|
|
2043
|
+
// than by a good-looking run. A pile is chaotic: change `crease` by
|
|
2044
|
+
// 0.05 and one 14-second scroll can spread 2.0 panel-widths of floor or
|
|
2045
|
+
// 4.2, so a single trajectory is a sample and not a measurement. These
|
|
2046
|
+
// are scored over nine — three scroll depths crossed with three feed
|
|
2047
|
+
// rates — on how far the composition ever gets from the origin.
|
|
2048
|
+
//
|
|
2049
|
+
// What that fixed: the shipped set spread 2.9 panel-widths on average
|
|
2050
|
+
// and 4.0 at worst, and threw paper 1.60 units out in z. It ran off the
|
|
2051
|
+
// side of the frame and kept going. These hold 2.2 average, 2.8 worst,
|
|
2052
|
+
// and 1.09 out — while keeping the pile's height (about ten layers)
|
|
2053
|
+
// intact, which is the thing all of this exists to show.
|
|
2054
|
+
//
|
|
2055
|
+
// The most load-bearing of them. Below about 0.6 the landed paper flops
|
|
2056
|
+
// over in flat panels and runs away across the floor instead of folding
|
|
2057
|
+
// back; high is what makes the perforations hold and the pile
|
|
2058
|
+
// accordion. A used roll remembering its creases is the whole effect.
|
|
2059
|
+
crease: 0.9,
|
|
2060
|
+
// Low enough that a panel buckles rather than steering the pile: at 0.5
|
|
2061
|
+
// the sheet was stiff enough to push the folds already down along the
|
|
2062
|
+
// floor ahead of it, which is what "spreads across four panels" was.
|
|
2063
|
+
stiffness: 0.4,
|
|
2064
|
+
// High: paper is light and broad, and this is what separates it from a
|
|
2065
|
+
// rope hanging off a drum. It also damps the sideways travel that
|
|
2066
|
+
// carried the pile out of frame.
|
|
2067
|
+
drag: 0.85,
|
|
2068
|
+
gravity: 1,
|
|
2069
|
+
// The drop, measured from the roll's axis. Together with the roll's own
|
|
2070
|
+
// radius this IS the height of the composition, which is centred on the
|
|
2071
|
+
// origin — so keep their sum under about 1.7 or the roll and the pile
|
|
2072
|
+
// fall outside the ~1.75 units `<Paper>`'s fixed camera can see.
|
|
2073
|
+
//
|
|
2074
|
+
// Shortened from 1.2, and it is the single most effective number here:
|
|
2075
|
+
// a longer fall is more airtime for the strip to pick a direction and
|
|
2076
|
+
// glide, so it landed still travelling and slid. Worst-case spread goes
|
|
2077
|
+
// 4.2 panel-widths to 3.1 and the pile keeps its full depth. It buys
|
|
2078
|
+
// vertical room as well, which is what lets the roll sit further up.
|
|
2079
|
+
floor: 0.85,
|
|
2080
|
+
inertia: 0.5
|
|
2081
|
+
},
|
|
2082
|
+
surface: { grain: 0.25 },
|
|
2083
|
+
// The preset is unreadable without this, and that is not a figure of
|
|
2084
|
+
// speech: the strip sim folds in DEPTH, and every camera in the library
|
|
2085
|
+
// is fixed and head-on, so `<Paper preset="toilet-roll" />` framed the
|
|
2086
|
+
// roll end-on and the entire accordion edge-on and rendered a blank white
|
|
2087
|
+
// column. This is the three-quarter view the pile actually reads from —
|
|
2088
|
+
// enough to see along the folds and around the roll's rim, not so much
|
|
2089
|
+
// that the strip's face turns away.
|
|
2090
|
+
//
|
|
2091
|
+
// The ceiling is framing, not taste. Turning swaps the pile's DEPTH for
|
|
2092
|
+
// WIDTH, and depth is free — the camera has plenty — while width is not.
|
|
2093
|
+
// Measured over fifteen scroll trajectories the pile reaches 1.27 units
|
|
2094
|
+
// of depth at worst, and `halfWidth·cos θ + 1.27·sin θ` crosses the 0.874
|
|
2095
|
+
// half-view a square parent gets at 28°. Twenty-five leaves real margin
|
|
2096
|
+
// (0.809) and gives up almost nothing of the angle.
|
|
2097
|
+
//
|
|
2098
|
+
// Do not raise it without re-measuring that depth: the two numbers are
|
|
2099
|
+
// coupled, and the pile's depth is not stable under small changes to the
|
|
2100
|
+
// physics — see the note on `floor`.
|
|
2101
|
+
//
|
|
2102
|
+
// A parent narrower than square still crops the pile's far edge. That is
|
|
2103
|
+
// the honest limit of a fixed camera, and the caller's answer is their
|
|
2104
|
+
// own `rotation` prop, which this composes with rather than overrides.
|
|
2105
|
+
scene: { turn: 25 }
|
|
2106
|
+
},
|
|
1639
2107
|
"letter-fold": {
|
|
1640
2108
|
meta: { name: "Letter fold", tags: ["fold", "text"] },
|
|
1641
2109
|
sheet: { width: 1, height: 1.4 },
|
|
@@ -1645,7 +2113,24 @@ var builtins = {
|
|
|
1645
2113
|
text: "Dear you,\n\nSome things are worth folding carefully.\n\nYours,\nN."
|
|
1646
2114
|
},
|
|
1647
2115
|
behavior: { type: "letter-fold", progress: 0.4, crease: 0.3 },
|
|
1648
|
-
|
|
2116
|
+
// A letter that has been folded before. These were painted-on
|
|
2117
|
+
// `creaseLines` until the paper could carry real ones — two marks in the
|
|
2118
|
+
// shader at exactly the two places the tri-fold bends, which is a good
|
|
2119
|
+
// impression of a crease right up until you unfold the letter and it
|
|
2120
|
+
// comes back perfectly flat. As memory they bend the sheet as well as
|
|
2121
|
+
// marking it, and folding the letter deepens the creases it already has
|
|
2122
|
+
// rather than drawing a second pair on top of them.
|
|
2123
|
+
//
|
|
2124
|
+
// The lines are `letterFold`'s own: travel down and up, each a sixth of
|
|
2125
|
+
// the sheet from the middle. Shallower than a fold leaves (printer keeps
|
|
2126
|
+
// about 20°) because this letter has been folded once and put away, not
|
|
2127
|
+
// folded and reopened in front of you.
|
|
2128
|
+
memory: {
|
|
2129
|
+
creases: [
|
|
2130
|
+
{ angle: 270, offset: 1.4 / 6, depth: 13 },
|
|
2131
|
+
{ angle: 90, offset: 1.4 / 6, depth: 11 }
|
|
2132
|
+
]
|
|
2133
|
+
}
|
|
1649
2134
|
},
|
|
1650
2135
|
/**
|
|
1651
2136
|
* The wash, shown rather than described.
|
|
@@ -1932,8 +2417,8 @@ function barcodeBars(seed) {
|
|
|
1932
2417
|
var money = (v) => v.toFixed(2);
|
|
1933
2418
|
function paintReceipt(ctx, w, h, content, stock) {
|
|
1934
2419
|
const ink = stock.inkColor;
|
|
1935
|
-
const
|
|
1936
|
-
const colWidth = w -
|
|
2420
|
+
const pad2 = w * 0.09;
|
|
2421
|
+
const colWidth = w - pad2 * 2;
|
|
1937
2422
|
const base = Math.round(w / 15);
|
|
1938
2423
|
const mono = (size, weight = 400) => `${weight} ${size}px ui-monospace, Menlo, Consolas, monospace`;
|
|
1939
2424
|
let y = h * 0.045;
|
|
@@ -1946,9 +2431,9 @@ function paintReceipt(ctx, w, h, content, stock) {
|
|
|
1946
2431
|
const row = (left, right, size = base) => {
|
|
1947
2432
|
ctx.font = mono(size);
|
|
1948
2433
|
ctx.textAlign = "left";
|
|
1949
|
-
ctx.fillText(left,
|
|
2434
|
+
ctx.fillText(left, pad2, y);
|
|
1950
2435
|
ctx.textAlign = "right";
|
|
1951
|
-
ctx.fillText(right, w -
|
|
2436
|
+
ctx.fillText(right, w - pad2, y);
|
|
1952
2437
|
};
|
|
1953
2438
|
const divider = () => {
|
|
1954
2439
|
ctx.font = mono(base);
|
|
@@ -2046,9 +2531,9 @@ var NOTE_TRACKING = 0.06;
|
|
|
2046
2531
|
function paintCard(ctx, w, h, content, stock, dpr) {
|
|
2047
2532
|
const ink = content.color === "#2b2620" ? stock.inkColor : content.color;
|
|
2048
2533
|
const size = content.size * dpr;
|
|
2049
|
-
const
|
|
2050
|
-
const maxWidth = w -
|
|
2051
|
-
const x = content.align === "center" ? w / 2 :
|
|
2534
|
+
const pad2 = content.padding * Math.min(w, h);
|
|
2535
|
+
const maxWidth = w - pad2 * 2;
|
|
2536
|
+
const x = content.align === "center" ? w / 2 : pad2;
|
|
2052
2537
|
ctx.textAlign = content.align === "center" ? "center" : "left";
|
|
2053
2538
|
ctx.textBaseline = "alphabetic";
|
|
2054
2539
|
const titleSize = size * TITLE_RATIO;
|
|
@@ -2060,7 +2545,7 @@ function paintCard(ctx, w, h, content, stock, dpr) {
|
|
|
2060
2545
|
const noteBlock = content.note ? noteSize * 2.4 : 0;
|
|
2061
2546
|
const bodyBlock = bodyLines.length * bodyStep;
|
|
2062
2547
|
const total = titleBlock + ruleBlock + bodyBlock + noteBlock;
|
|
2063
|
-
let y = Math.max(
|
|
2548
|
+
let y = Math.max(pad2, (h - total) / 2) + size * 0.9;
|
|
2064
2549
|
if (content.title) {
|
|
2065
2550
|
ctx.font = `${titleSize}px ${content.font}`;
|
|
2066
2551
|
ctx.letterSpacing = `${TITLE_TRACKING}em`;
|
|
@@ -2076,8 +2561,8 @@ function paintCard(ctx, w, h, content, stock, dpr) {
|
|
|
2076
2561
|
ctx.globalAlpha = 0.28;
|
|
2077
2562
|
ctx.lineWidth = Math.max(1, dpr * 0.75);
|
|
2078
2563
|
ctx.beginPath();
|
|
2079
|
-
ctx.moveTo(content.align === "center" ? w / 2 - maxWidth / 2 :
|
|
2080
|
-
ctx.lineTo(content.align === "center" ? w / 2 + maxWidth / 2 :
|
|
2564
|
+
ctx.moveTo(content.align === "center" ? w / 2 - maxWidth / 2 : pad2, y - titleSize * 0.5);
|
|
2565
|
+
ctx.lineTo(content.align === "center" ? w / 2 + maxWidth / 2 : pad2 + maxWidth, y - titleSize * 0.5);
|
|
2081
2566
|
ctx.stroke();
|
|
2082
2567
|
ctx.restore();
|
|
2083
2568
|
y += ruleBlock;
|
|
@@ -2091,8 +2576,8 @@ function paintCard(ctx, w, h, content, stock, dpr) {
|
|
|
2091
2576
|
for (let i = 0; i < bodyLines.length; i++) {
|
|
2092
2577
|
const lineY = y + i * bodyStep + size * 0.28;
|
|
2093
2578
|
ctx.beginPath();
|
|
2094
|
-
ctx.moveTo(
|
|
2095
|
-
ctx.lineTo(
|
|
2579
|
+
ctx.moveTo(pad2, lineY);
|
|
2580
|
+
ctx.lineTo(pad2 + maxWidth, lineY);
|
|
2096
2581
|
ctx.stroke();
|
|
2097
2582
|
}
|
|
2098
2583
|
ctx.restore();
|
|
@@ -2100,7 +2585,7 @@ function paintCard(ctx, w, h, content, stock, dpr) {
|
|
|
2100
2585
|
ctx.font = `${size}px ${content.font}`;
|
|
2101
2586
|
ctx.fillStyle = ink;
|
|
2102
2587
|
for (const line of bodyLines) {
|
|
2103
|
-
if (y > h -
|
|
2588
|
+
if (y > h - pad2) break;
|
|
2104
2589
|
ctx.fillText(line, x, y);
|
|
2105
2590
|
y += bodyStep;
|
|
2106
2591
|
}
|
|
@@ -2108,7 +2593,7 @@ function paintCard(ctx, w, h, content, stock, dpr) {
|
|
|
2108
2593
|
ctx.font = `${noteSize}px ${content.font}`;
|
|
2109
2594
|
ctx.letterSpacing = `${NOTE_TRACKING}em`;
|
|
2110
2595
|
ctx.globalAlpha = 0.6;
|
|
2111
|
-
ctx.fillText(content.note, x, Math.min(y + noteSize * 1.4, h -
|
|
2596
|
+
ctx.fillText(content.note, x, Math.min(y + noteSize * 1.4, h - pad2));
|
|
2112
2597
|
ctx.globalAlpha = 1;
|
|
2113
2598
|
ctx.letterSpacing = "0em";
|
|
2114
2599
|
}
|
|
@@ -2238,23 +2723,23 @@ function paintImage(ctx, w, h, img, fit) {
|
|
|
2238
2723
|
}
|
|
2239
2724
|
function paintText(ctx, w, h, content, stock) {
|
|
2240
2725
|
const size = content.size * DPR;
|
|
2241
|
-
const
|
|
2726
|
+
const pad2 = content.padding * Math.min(w, h);
|
|
2242
2727
|
const font = `${content.weight} ${size}px ${content.font}`;
|
|
2243
2728
|
ctx.font = font;
|
|
2244
2729
|
ctx.fillStyle = content.color === "#2b2620" ? stock.inkColor : content.color;
|
|
2245
2730
|
ctx.textBaseline = "top";
|
|
2246
2731
|
ctx.textAlign = content.align;
|
|
2247
2732
|
ctx.letterSpacing = `${content.tracking}em`;
|
|
2248
|
-
const maxWidth = w -
|
|
2249
|
-
const x = content.align === "left" ?
|
|
2733
|
+
const maxWidth = w - pad2 * 2;
|
|
2734
|
+
const x = content.align === "left" ? pad2 : content.align === "right" ? w - pad2 : w / 2;
|
|
2250
2735
|
const lineStep = size * content.lineHeight;
|
|
2251
2736
|
const lines = wrapLines(ctx, content.text, maxWidth, font);
|
|
2252
2737
|
ctx.font = font;
|
|
2253
2738
|
ctx.letterSpacing = `${content.tracking}em`;
|
|
2254
2739
|
const block = lines.length * lineStep;
|
|
2255
|
-
let y = content.valign === "center" ? Math.max(
|
|
2740
|
+
let y = content.valign === "center" ? Math.max(pad2, (h - block) / 2) : pad2;
|
|
2256
2741
|
for (const line of lines) {
|
|
2257
|
-
if (y > h -
|
|
2742
|
+
if (y > h - pad2) break;
|
|
2258
2743
|
ctx.fillText(line, x, y);
|
|
2259
2744
|
y += lineStep;
|
|
2260
2745
|
}
|
|
@@ -2325,77 +2810,6 @@ var THREE3 = __toESM(require("three"), 1);
|
|
|
2325
2810
|
// src/deformers/registry.ts
|
|
2326
2811
|
var import_zod21 = require("zod");
|
|
2327
2812
|
|
|
2328
|
-
// src/deformers/roll.ts
|
|
2329
|
-
var import_zod15 = require("zod");
|
|
2330
|
-
var rollOptionsSchema = import_zod15.z.object({
|
|
2331
|
-
/** Direction of rolling in the sheet plane, degrees. 0 = +x, 90 = +y. */
|
|
2332
|
-
angle: import_zod15.z.number().min(-360).max(360).default(90),
|
|
2333
|
-
/** Signed distance (along the roll direction, from sheet center) where the roll begins. */
|
|
2334
|
-
boundary: import_zod15.z.number().min(-20).max(20).default(0),
|
|
2335
|
-
/** Cylinder radius — sharpness of the roll. */
|
|
2336
|
-
radius: import_zod15.z.number().min(0.01).max(2).default(0.12),
|
|
2337
|
-
/** Radius growth per radian so multi-turn rolls spiral instead of z-fighting. */
|
|
2338
|
-
spiral: import_zod15.z.number().min(0).max(0.2).default(0.015)
|
|
2339
|
-
});
|
|
2340
|
-
var DEG2 = Math.PI / 180;
|
|
2341
|
-
var roll = {
|
|
2342
|
-
id: "roll",
|
|
2343
|
-
label: "Roll",
|
|
2344
|
-
defaults: rollOptionsSchema.parse({}),
|
|
2345
|
-
optionsSchema: rollOptionsSchema,
|
|
2346
|
-
geometry: {
|
|
2347
|
-
minSegments: 48,
|
|
2348
|
-
// The winding radius is the tightest curvature on the sheet — `spiral`
|
|
2349
|
-
// only grows it as the roll winds outward, so the first turn is the one
|
|
2350
|
-
// that sets the density.
|
|
2351
|
-
autoSegments: (o, sheet2) => segmentsForArc(spanAlong(sheet2, o.angle), o.radius),
|
|
2352
|
-
axis: (o) => o.angle
|
|
2353
|
-
},
|
|
2354
|
-
displace(out, _uv, o) {
|
|
2355
|
-
const dirX = Math.cos(o.angle * DEG2);
|
|
2356
|
-
const dirY = Math.sin(o.angle * DEG2);
|
|
2357
|
-
const d = out.x * dirX + out.y * dirY;
|
|
2358
|
-
const s = d - o.boundary;
|
|
2359
|
-
if (s <= 0) return;
|
|
2360
|
-
const theta = s / o.radius;
|
|
2361
|
-
const r = o.radius + o.spiral * theta;
|
|
2362
|
-
const sin = Math.sin(theta);
|
|
2363
|
-
const cos = Math.cos(theta);
|
|
2364
|
-
const newD = o.boundary + (r - out.z) * sin;
|
|
2365
|
-
const newZ = r * (1 - cos) + out.z * cos;
|
|
2366
|
-
out.x += dirX * (newD - d);
|
|
2367
|
-
out.y += dirY * (newD - d);
|
|
2368
|
-
out.z = newZ;
|
|
2369
|
-
},
|
|
2370
|
-
glsl: {
|
|
2371
|
-
chunk: (
|
|
2372
|
-
/* glsl */
|
|
2373
|
-
`
|
|
2374
|
-
void FN(inout vec3 p, vec2 uv, float t) {
|
|
2375
|
-
vec2 dir = vec2(cos(U_angle), sin(U_angle));
|
|
2376
|
-
float d = dot(p.xy, dir);
|
|
2377
|
-
float s = d - U_boundary;
|
|
2378
|
-
if (s <= 0.0) return;
|
|
2379
|
-
float theta = s / U_radius;
|
|
2380
|
-
float r = U_radius + U_spiral * theta;
|
|
2381
|
-
float sn = sin(theta);
|
|
2382
|
-
float cs = cos(theta);
|
|
2383
|
-
float newD = U_boundary + (r - p.z) * sn;
|
|
2384
|
-
float newZ = r * (1.0 - cs) + p.z * cs;
|
|
2385
|
-
p.xy += dir * (newD - d);
|
|
2386
|
-
p.z = newZ;
|
|
2387
|
-
}
|
|
2388
|
-
`
|
|
2389
|
-
),
|
|
2390
|
-
uniforms: (o) => ({
|
|
2391
|
-
angle: o.angle * DEG2,
|
|
2392
|
-
boundary: o.boundary,
|
|
2393
|
-
radius: o.radius,
|
|
2394
|
-
spiral: o.spiral
|
|
2395
|
-
})
|
|
2396
|
-
}
|
|
2397
|
-
};
|
|
2398
|
-
|
|
2399
2813
|
// src/deformers/bend.ts
|
|
2400
2814
|
var import_zod16 = require("zod");
|
|
2401
2815
|
var bendOptionsSchema = import_zod16.z.object({
|
|
@@ -2577,7 +2991,7 @@ var waveOptionsSchema = import_zod18.z.object({
|
|
|
2577
2991
|
pinnedEdge: import_zod18.z.enum(["none", "top", "bottom", "left", "right"]).default("none")
|
|
2578
2992
|
});
|
|
2579
2993
|
var DEG5 = Math.PI / 180;
|
|
2580
|
-
var
|
|
2994
|
+
var TAU2 = Math.PI * 2;
|
|
2581
2995
|
var wave = {
|
|
2582
2996
|
id: "wave",
|
|
2583
2997
|
label: "Wave",
|
|
@@ -2604,7 +3018,7 @@ var wave = {
|
|
|
2604
3018
|
const dirX = Math.cos(o.angle * DEG5);
|
|
2605
3019
|
const dirY = Math.sin(o.angle * DEG5);
|
|
2606
3020
|
const d = out.x * dirX + out.y * dirY;
|
|
2607
|
-
const phase = (d / o.wavelength - o.speed * ctx.t) *
|
|
3021
|
+
const phase = (d / o.wavelength - o.speed * ctx.t) * TAU2;
|
|
2608
3022
|
let env = 1;
|
|
2609
3023
|
if (o.pinnedEdge === "top") env = 1 - uv.y;
|
|
2610
3024
|
else if (o.pinnedEdge === "bottom") env = uv.y;
|
|
@@ -2660,7 +3074,7 @@ var drapeOptionsSchema = import_zod19.z.object({
|
|
|
2660
3074
|
gather: import_zod19.z.number().min(0).max(1).default(0.5),
|
|
2661
3075
|
pinnedEdge: import_zod19.z.enum(["top", "bottom"]).default("top")
|
|
2662
3076
|
});
|
|
2663
|
-
var
|
|
3077
|
+
var TAU3 = Math.PI * 2;
|
|
2664
3078
|
var drape = {
|
|
2665
3079
|
id: "drape",
|
|
2666
3080
|
label: "Drape",
|
|
@@ -2688,7 +3102,7 @@ var drape = {
|
|
|
2688
3102
|
if (o.amplitude === 0) return;
|
|
2689
3103
|
const drop = o.pinnedEdge === "top" ? 1 - uv.y : uv.y;
|
|
2690
3104
|
const depth = drop ** o.falloff;
|
|
2691
|
-
const u = uv.x *
|
|
3105
|
+
const u = uv.x * TAU3 * o.folds;
|
|
2692
3106
|
const fold2 = Math.sin(u) + o.irregular * 0.6 * Math.sin(u * 1.7 + 2.1);
|
|
2693
3107
|
out.z += o.amplitude * depth * fold2;
|
|
2694
3108
|
const pinch = o.gather * depth * Math.min(o.amplitude * o.folds * 0.8, 0.6);
|
|
@@ -2965,6 +3379,211 @@ function take(out, deformer, options, sheet2, demand) {
|
|
|
2965
3379
|
if (y > out[1]) out[1] = y;
|
|
2966
3380
|
}
|
|
2967
3381
|
|
|
3382
|
+
// src/deformers/memory.ts
|
|
3383
|
+
var MAX_SET = 0.2;
|
|
3384
|
+
var CREASE_RADIUS = 0.03;
|
|
3385
|
+
var CREASE_MIN_GROWTH = 45;
|
|
3386
|
+
var CREASE_DRIFT = 0.02;
|
|
3387
|
+
var CREASE_DRIFT_ANGLE = 2;
|
|
3388
|
+
var MIN_DEPTH = 1;
|
|
3389
|
+
var MAX_CREASES = 4;
|
|
3390
|
+
function canonicalLine(angle, offset) {
|
|
3391
|
+
let a = (angle % 360 + 360) % 360;
|
|
3392
|
+
let o = offset;
|
|
3393
|
+
if (a >= 180) {
|
|
3394
|
+
a -= 180;
|
|
3395
|
+
o = -o;
|
|
3396
|
+
}
|
|
3397
|
+
return [a, o];
|
|
3398
|
+
}
|
|
3399
|
+
function sameLine(a, b) {
|
|
3400
|
+
const [aa, ao] = canonicalLine(a.angle, a.offset);
|
|
3401
|
+
const [ba, bo] = canonicalLine(b.angle, b.offset);
|
|
3402
|
+
const d = Math.abs(aa - ba);
|
|
3403
|
+
const angleClose = Math.min(d, 180 - d) <= CREASE_DRIFT_ANGLE;
|
|
3404
|
+
return angleClose && Math.abs(ao - bo) <= CREASE_DRIFT;
|
|
3405
|
+
}
|
|
3406
|
+
function applyMemory(stack, creases) {
|
|
3407
|
+
if (creases.length === 0) return stack;
|
|
3408
|
+
let out = stack;
|
|
3409
|
+
const loose = [];
|
|
3410
|
+
for (const crease of creases) {
|
|
3411
|
+
if (Math.abs(crease.depth) < MIN_DEPTH) continue;
|
|
3412
|
+
const index = out.findIndex(
|
|
3413
|
+
(i) => i.type === "fold" && i.enabled !== false && sameLine(i.options, crease)
|
|
3414
|
+
);
|
|
3415
|
+
if (index === -1) {
|
|
3416
|
+
loose.push(crease);
|
|
3417
|
+
continue;
|
|
3418
|
+
}
|
|
3419
|
+
const live = out[index];
|
|
3420
|
+
const options = live.options;
|
|
3421
|
+
if (Math.abs(options.foldAngle) >= Math.abs(crease.depth)) continue;
|
|
3422
|
+
if (out === stack) out = [...stack];
|
|
3423
|
+
out[index] = { ...live, options: { ...options, foldAngle: crease.depth } };
|
|
3424
|
+
}
|
|
3425
|
+
if (loose.length === 0) return out;
|
|
3426
|
+
return [...loose.map(toFold), ...out];
|
|
3427
|
+
}
|
|
3428
|
+
function toFold(crease) {
|
|
3429
|
+
return {
|
|
3430
|
+
type: "fold",
|
|
3431
|
+
options: {
|
|
3432
|
+
angle: crease.angle,
|
|
3433
|
+
offset: crease.offset,
|
|
3434
|
+
foldAngle: crease.depth,
|
|
3435
|
+
radius: CREASE_RADIUS
|
|
3436
|
+
}
|
|
3437
|
+
};
|
|
3438
|
+
}
|
|
3439
|
+
var CreaseTracker = class {
|
|
3440
|
+
slots = /* @__PURE__ */ new Map();
|
|
3441
|
+
recorded = [];
|
|
3442
|
+
authored = [];
|
|
3443
|
+
/** What `observe` last handed out, so an echo can be told from an edit. */
|
|
3444
|
+
lastReported = [];
|
|
3445
|
+
constructor(authored = []) {
|
|
3446
|
+
this.authored = authored;
|
|
3447
|
+
}
|
|
3448
|
+
/**
|
|
3449
|
+
* Forget how the paper got here without forgetting the creases.
|
|
3450
|
+
*
|
|
3451
|
+
* Called when the stack is replaced wholesale (a new behavior, a new
|
|
3452
|
+
* sheet): the slots describe folds that no longer exist, but a crease is a
|
|
3453
|
+
* property of the paper and survives being put down and picked up.
|
|
3454
|
+
*/
|
|
3455
|
+
reset(authored = this.creases) {
|
|
3456
|
+
this.slots.clear();
|
|
3457
|
+
this.recorded = [];
|
|
3458
|
+
this.authored = authored;
|
|
3459
|
+
}
|
|
3460
|
+
/**
|
|
3461
|
+
* Take on a crease set that came from outside, and work out which kind of
|
|
3462
|
+
* outside it was — because the two kinds want opposite things.
|
|
3463
|
+
*
|
|
3464
|
+
* It is USUALLY this tracker's own recording coming back, a frame or two
|
|
3465
|
+
* after `onCrease` handed it to the host. Then the slots must survive: the
|
|
3466
|
+
* fold that made the crease is very likely still closing, and resetting
|
|
3467
|
+
* its peak on the host's echo would stall the crease halfway into the
|
|
3468
|
+
* fold that was making it.
|
|
3469
|
+
*
|
|
3470
|
+
* But it can also be somebody EDITING the paper — a depth dragged down in
|
|
3471
|
+
* a panel, a shared link opened, a state's overrides settling. Then the
|
|
3472
|
+
* slots are the wrong story to keep. `merge` takes the deeper of two
|
|
3473
|
+
* creases on a line, so a recording of 20° would quietly outvote a human
|
|
3474
|
+
* asking for 5° and the slider would appear not to work; and it would go
|
|
3475
|
+
* on outvoting it, because the fold that recorded the 20 is still sitting
|
|
3476
|
+
* in a slot. An edit means the paper is what it is now, and the next
|
|
3477
|
+
* crease has to be earned by folding it again.
|
|
3478
|
+
*
|
|
3479
|
+
* The two are told apart by what we last reported. Anything else is an
|
|
3480
|
+
* edit, which is the safe way round: mistaking an echo for an edit costs
|
|
3481
|
+
* a crease that gets re-recorded on the next fold, while mistaking an edit
|
|
3482
|
+
* for an echo costs a control that does not work.
|
|
3483
|
+
*/
|
|
3484
|
+
adopt(creases) {
|
|
3485
|
+
if (!same(creases, this.lastReported)) this.slots.clear();
|
|
3486
|
+
this.authored = creases;
|
|
3487
|
+
this.recorded = [];
|
|
3488
|
+
}
|
|
3489
|
+
/** Everything the sheet currently carries, authored and recorded merged. */
|
|
3490
|
+
get creases() {
|
|
3491
|
+
return merge(this.authored, this.recorded);
|
|
3492
|
+
}
|
|
3493
|
+
/**
|
|
3494
|
+
* Take one frame's reading. Returns true when the crease set changed by
|
|
3495
|
+
* enough to be worth telling anyone about.
|
|
3496
|
+
*/
|
|
3497
|
+
observe(stack, set) {
|
|
3498
|
+
if (set <= 0) return false;
|
|
3499
|
+
for (let i = 0; i < stack.length; i++) {
|
|
3500
|
+
const instance = stack[i];
|
|
3501
|
+
if (instance.type !== "fold" || instance.enabled === false) continue;
|
|
3502
|
+
const o = instance.options;
|
|
3503
|
+
const magnitude = Math.abs(o.foldAngle);
|
|
3504
|
+
const slot = this.slots.get(i);
|
|
3505
|
+
if (!slot || Math.abs(slot.angle - o.angle) > CREASE_DRIFT_ANGLE || Math.abs(slot.offset - o.offset) > CREASE_DRIFT) {
|
|
3506
|
+
this.slots.set(i, {
|
|
3507
|
+
angle: o.angle,
|
|
3508
|
+
offset: o.offset,
|
|
3509
|
+
trough: magnitude,
|
|
3510
|
+
bestGrowth: 0,
|
|
3511
|
+
bestPeak: magnitude,
|
|
3512
|
+
sign: o.foldAngle < 0 ? -1 : 1
|
|
3513
|
+
});
|
|
3514
|
+
continue;
|
|
3515
|
+
}
|
|
3516
|
+
if (magnitude < slot.trough) slot.trough = magnitude;
|
|
3517
|
+
const growth = magnitude - slot.trough;
|
|
3518
|
+
if (growth > slot.bestGrowth) {
|
|
3519
|
+
slot.bestGrowth = growth;
|
|
3520
|
+
slot.bestPeak = magnitude;
|
|
3521
|
+
slot.sign = o.foldAngle < 0 ? -1 : 1;
|
|
3522
|
+
}
|
|
3523
|
+
}
|
|
3524
|
+
const next = [];
|
|
3525
|
+
for (const slot of this.slots.values()) {
|
|
3526
|
+
if (slot.bestGrowth < CREASE_MIN_GROWTH) continue;
|
|
3527
|
+
const depth = slot.sign * slot.bestPeak * set * MAX_SET;
|
|
3528
|
+
if (Math.abs(depth) < MIN_DEPTH) continue;
|
|
3529
|
+
next.push({ angle: slot.angle, offset: slot.offset, depth });
|
|
3530
|
+
}
|
|
3531
|
+
if (same(next, this.recorded)) return false;
|
|
3532
|
+
this.recorded = next;
|
|
3533
|
+
this.lastReported = this.creases;
|
|
3534
|
+
return true;
|
|
3535
|
+
}
|
|
3536
|
+
};
|
|
3537
|
+
function merge(authored, recorded) {
|
|
3538
|
+
const out = [];
|
|
3539
|
+
for (const crease of [...recorded, ...authored]) {
|
|
3540
|
+
const existing = out.find((c) => sameLine(c, crease));
|
|
3541
|
+
if (!existing) {
|
|
3542
|
+
out.push({ ...crease });
|
|
3543
|
+
continue;
|
|
3544
|
+
}
|
|
3545
|
+
if (Math.abs(crease.depth) > Math.abs(existing.depth)) existing.depth = crease.depth;
|
|
3546
|
+
}
|
|
3547
|
+
if (out.length > MAX_CREASES) {
|
|
3548
|
+
out.sort((a, b) => Math.abs(b.depth) - Math.abs(a.depth));
|
|
3549
|
+
out.length = MAX_CREASES;
|
|
3550
|
+
}
|
|
3551
|
+
return out;
|
|
3552
|
+
}
|
|
3553
|
+
function same(a, b) {
|
|
3554
|
+
if (a.length !== b.length) return false;
|
|
3555
|
+
return a.every((crease, i) => {
|
|
3556
|
+
const other = b[i];
|
|
3557
|
+
return sameLine(crease, other) && Math.abs(crease.depth - other.depth) < 0.5;
|
|
3558
|
+
});
|
|
3559
|
+
}
|
|
3560
|
+
|
|
3561
|
+
// src/surface/creases.ts
|
|
3562
|
+
var MAX_SHADED = 4;
|
|
3563
|
+
function resolveCreases(surface, creases, sheet2) {
|
|
3564
|
+
const out = [];
|
|
3565
|
+
const lines = surface.creaseLines;
|
|
3566
|
+
if (lines) {
|
|
3567
|
+
const angle = lines.angle + 90;
|
|
3568
|
+
const span = spanAlong(sheet2, angle);
|
|
3569
|
+
for (const position of lines.positions) {
|
|
3570
|
+
out.push({ angle, offset: (position - 0.5) * span, strength: lines.strength });
|
|
3571
|
+
}
|
|
3572
|
+
}
|
|
3573
|
+
for (const crease of creases) {
|
|
3574
|
+
out.push(creaseShading(crease));
|
|
3575
|
+
}
|
|
3576
|
+
if (out.length > MAX_SHADED) out.length = MAX_SHADED;
|
|
3577
|
+
return out;
|
|
3578
|
+
}
|
|
3579
|
+
function creaseShading(crease) {
|
|
3580
|
+
return {
|
|
3581
|
+
angle: crease.angle,
|
|
3582
|
+
offset: crease.offset,
|
|
3583
|
+
strength: Math.max(-1, Math.min(1, -crease.depth / (90 * MAX_SET)))
|
|
3584
|
+
};
|
|
3585
|
+
}
|
|
3586
|
+
|
|
2968
3587
|
// src/behaviors/registry.ts
|
|
2969
3588
|
var registry2 = /* @__PURE__ */ new Map();
|
|
2970
3589
|
function registerBehavior(behavior) {
|
|
@@ -3058,12 +3677,22 @@ function getIdlePreset(name) {
|
|
|
3058
3677
|
// src/physics/cloth.ts
|
|
3059
3678
|
var FIXED_DT = 1 / 120;
|
|
3060
3679
|
var SOLVER_ITERATIONS = 5;
|
|
3680
|
+
var AERO_TURBULENCE = 0.15;
|
|
3681
|
+
var GRAB_RADIUS = 0.05;
|
|
3061
3682
|
var SLEEP_EPSILON = 1e-6;
|
|
3062
3683
|
var SLEEP_FRAMES = 45;
|
|
3684
|
+
function grabFalloff(distance, radius) {
|
|
3685
|
+
const t = Math.min(1, Math.max(0, distance / radius));
|
|
3686
|
+
const s = 1 - t;
|
|
3687
|
+
return s * s * (3 - 2 * s);
|
|
3688
|
+
}
|
|
3063
3689
|
var ClothSim = class {
|
|
3064
3690
|
cols;
|
|
3065
3691
|
rows;
|
|
3066
3692
|
count;
|
|
3693
|
+
/** The sheet this grid was laid out on. Read by {@link adopt}. */
|
|
3694
|
+
width;
|
|
3695
|
+
height;
|
|
3067
3696
|
positions;
|
|
3068
3697
|
prev;
|
|
3069
3698
|
pinned;
|
|
@@ -3074,17 +3703,47 @@ var ClothSim = class {
|
|
|
3074
3703
|
accumulator = 0;
|
|
3075
3704
|
stillFrames = 0;
|
|
3076
3705
|
grabbedIndex = -1;
|
|
3706
|
+
/**
|
|
3707
|
+
* A normal per particle, refreshed once a frame.
|
|
3708
|
+
*
|
|
3709
|
+
* The sim needs these for one thing only — how much wind each part of the
|
|
3710
|
+
* sheet is actually catching — and it needs its OWN rather than the mesh's,
|
|
3711
|
+
* because the mesh's are computed after the fact by the adapter and a
|
|
3712
|
+
* deformer may be running over the top of them by then. Once a frame rather
|
|
3713
|
+
* than once a substep: paper does not turn far in eight milliseconds, and
|
|
3714
|
+
* this is the only part of the step that is not a constraint solve.
|
|
3715
|
+
*/
|
|
3716
|
+
normals;
|
|
3717
|
+
/**
|
|
3718
|
+
* How hard each particle is held, 0..1. Zero for all but the handful under
|
|
3719
|
+
* a hand, so it doubles as the inverse mass the constraint solver wants:
|
|
3720
|
+
* a particle is as immovable as it is held.
|
|
3721
|
+
*/
|
|
3722
|
+
grabWeights;
|
|
3723
|
+
/** Just the held ones, so nothing iterates the whole sheet to find six. */
|
|
3724
|
+
grabbed = [];
|
|
3725
|
+
/** Each held particle's offset from the one under the cursor, at grab time. */
|
|
3726
|
+
grabOffsets = new Float32Array(0);
|
|
3727
|
+
/** Where the hand was at the start of the last step, and how fast it moved. */
|
|
3728
|
+
grabAt = new Float32Array(3);
|
|
3729
|
+
grabWas = new Float32Array(3);
|
|
3730
|
+
grabVelocity = new Float32Array(3);
|
|
3077
3731
|
/** True when the sim has settled and steps are skipped. */
|
|
3078
3732
|
asleep = false;
|
|
3079
3733
|
constructor(cols, rows, width, height, pins, params) {
|
|
3080
3734
|
this.cols = cols;
|
|
3081
3735
|
this.rows = rows;
|
|
3082
3736
|
this.count = cols * rows;
|
|
3737
|
+
this.width = width;
|
|
3738
|
+
this.height = height;
|
|
3083
3739
|
this.params = { ...params };
|
|
3084
3740
|
this.positions = new Float32Array(this.count * 3);
|
|
3085
3741
|
this.prev = new Float32Array(this.count * 3);
|
|
3086
3742
|
this.pinned = new Uint8Array(this.count);
|
|
3087
3743
|
this.pinTargets = new Float32Array(this.count * 3);
|
|
3744
|
+
this.grabWeights = new Float32Array(this.count);
|
|
3745
|
+
this.normals = new Float32Array(this.count * 3);
|
|
3746
|
+
for (let i = 0; i < this.count; i++) this.normals[i * 3 + 2] = 1;
|
|
3088
3747
|
for (let r = 0; r < rows; r++) {
|
|
3089
3748
|
for (let c = 0; c < cols; c++) {
|
|
3090
3749
|
const i3 = (r * cols + c) * 3;
|
|
@@ -3124,6 +3783,54 @@ var ClothSim = class {
|
|
|
3124
3783
|
}
|
|
3125
3784
|
if (pins === "corner") pin(0, 0);
|
|
3126
3785
|
}
|
|
3786
|
+
/**
|
|
3787
|
+
* Carry a previous sim's drape across a rebuild.
|
|
3788
|
+
*
|
|
3789
|
+
* Sheet dimensions are a GEOMETRY dependency, so changing them builds a new
|
|
3790
|
+
* mesh and a new sim, and a new sim starts flat — which means a draped sheet
|
|
3791
|
+
* snaps rigid the instant it is resized. Nothing about the physics requires
|
|
3792
|
+
* that; it is only that nobody had carried the state over.
|
|
3793
|
+
*
|
|
3794
|
+
* What carries is the FREE particles, scaled by how much the sheet grew:
|
|
3795
|
+
* the constraints' rest lengths are laid out afresh at the new size, so
|
|
3796
|
+
* scaling the drape by the same ratio leaves every constraint exactly as
|
|
3797
|
+
* violated as it was and the sim simply continues. Pinned particles keep
|
|
3798
|
+
* the new layout's own rest positions instead — a pin holds a CORNER, and
|
|
3799
|
+
* the corner is where the resized sheet says it is.
|
|
3800
|
+
*
|
|
3801
|
+
* Refused in one case only: a different grid, because there is no
|
|
3802
|
+
* correspondence between the two sets of particles and the nearest thing to
|
|
3803
|
+
* one would be a guess. Everything else carries — a resize, a change of
|
|
3804
|
+
* pins, a deformer appearing on top. The sheet's state belongs to the sheet,
|
|
3805
|
+
* and none of those is a reason to have never fallen.
|
|
3806
|
+
*
|
|
3807
|
+
* That matters most for the one that is not a resize at all: a shape
|
|
3808
|
+
* arriving over a simulation rebuilds the mesh (the stack has its own
|
|
3809
|
+
* opinion about tessellation) without touching a single thing the physics
|
|
3810
|
+
* knows about. Resetting there would mean the sheet snapped flat the instant
|
|
3811
|
+
* you tried to fold the sheet you were holding, which is the whole point of
|
|
3812
|
+
* being able to.
|
|
3813
|
+
*
|
|
3814
|
+
* Returns whether it took.
|
|
3815
|
+
*/
|
|
3816
|
+
adopt(previous) {
|
|
3817
|
+
if (!previous || previous.cols !== this.cols || previous.rows !== this.rows) return false;
|
|
3818
|
+
const sx = previous.width > 0 ? this.width / previous.width : 1;
|
|
3819
|
+
const sy = previous.height > 0 ? this.height / previous.height : 1;
|
|
3820
|
+
const sz = (sx + sy) / 2;
|
|
3821
|
+
for (let i = 0; i < this.count; i++) {
|
|
3822
|
+
if (this.pinned[i]) continue;
|
|
3823
|
+
const i3 = i * 3;
|
|
3824
|
+
this.positions[i3] = previous.positions[i3] * sx;
|
|
3825
|
+
this.positions[i3 + 1] = previous.positions[i3 + 1] * sy;
|
|
3826
|
+
this.positions[i3 + 2] = previous.positions[i3 + 2] * sz;
|
|
3827
|
+
this.prev[i3] = previous.prev[i3] * sx;
|
|
3828
|
+
this.prev[i3 + 1] = previous.prev[i3 + 1] * sy;
|
|
3829
|
+
this.prev[i3 + 2] = previous.prev[i3 + 2] * sz;
|
|
3830
|
+
}
|
|
3831
|
+
this.wake();
|
|
3832
|
+
return true;
|
|
3833
|
+
}
|
|
3127
3834
|
setParams(params) {
|
|
3128
3835
|
let changed = false;
|
|
3129
3836
|
for (const key of ["stiffness", "gravity", "wind", "floor"]) {
|
|
@@ -3139,6 +3846,69 @@ var ClothSim = class {
|
|
|
3139
3846
|
this.asleep = false;
|
|
3140
3847
|
this.stillFrames = 0;
|
|
3141
3848
|
}
|
|
3849
|
+
/**
|
|
3850
|
+
* Take hold of the sheet at one particle.
|
|
3851
|
+
*
|
|
3852
|
+
* Separate from {@link grabNearest} because the particle a hand grabbed is
|
|
3853
|
+
* not always the particle nearest the point it touched: with a deformer
|
|
3854
|
+
* running over the simulation, what the pointer hit was a RENDERED vertex,
|
|
3855
|
+
* and the vertex it hit is the particle of the same index — the stack maps
|
|
3856
|
+
* a point to a point and never reorders them.
|
|
3857
|
+
*
|
|
3858
|
+
* What is taken hold of is a PATCH around that particle, not the particle
|
|
3859
|
+
* alone — see {@link GRAB_RADIUS}. The patch is measured across the GRID
|
|
3860
|
+
* rather than through space, because a hand holds a piece of the sheet and
|
|
3861
|
+
* keeps holding the same piece: measured through space, a fold that brought
|
|
3862
|
+
* a far corner near the fingers would silently add it to the grip.
|
|
3863
|
+
*/
|
|
3864
|
+
grab(index) {
|
|
3865
|
+
this.grabbedIndex = index >= 0 && index < this.count ? index : -1;
|
|
3866
|
+
this.grabWeights.fill(0);
|
|
3867
|
+
this.grabbed = [];
|
|
3868
|
+
if (this.grabbedIndex < 0) {
|
|
3869
|
+
this.grabOffsets = new Float32Array(0);
|
|
3870
|
+
this.wake();
|
|
3871
|
+
return -1;
|
|
3872
|
+
}
|
|
3873
|
+
const cellX = this.cols > 1 ? this.width / (this.cols - 1) : this.width;
|
|
3874
|
+
const cellY = this.rows > 1 ? this.height / (this.rows - 1) : this.height;
|
|
3875
|
+
const cell = Math.max(cellX, cellY, 1e-6);
|
|
3876
|
+
const radius = Math.min(
|
|
3877
|
+
Math.max(GRAB_RADIUS, cell * 1.5),
|
|
3878
|
+
Math.max(cell, Math.min(this.width, this.height) * 0.2)
|
|
3879
|
+
);
|
|
3880
|
+
const centreRow = Math.floor(this.grabbedIndex / this.cols);
|
|
3881
|
+
const centreCol = this.grabbedIndex % this.cols;
|
|
3882
|
+
const spanRows = Math.ceil(radius / cellY);
|
|
3883
|
+
const spanCols = Math.ceil(radius / cellX);
|
|
3884
|
+
const offsets = [];
|
|
3885
|
+
const anchor = this.grabbedIndex * 3;
|
|
3886
|
+
for (let r = centreRow - spanRows; r <= centreRow + spanRows; r++) {
|
|
3887
|
+
if (r < 0 || r >= this.rows) continue;
|
|
3888
|
+
for (let c = centreCol - spanCols; c <= centreCol + spanCols; c++) {
|
|
3889
|
+
if (c < 0 || c >= this.cols) continue;
|
|
3890
|
+
const i = r * this.cols + c;
|
|
3891
|
+
if (this.pinned[i]) continue;
|
|
3892
|
+
const distance = Math.hypot((c - centreCol) * cellX, (r - centreRow) * cellY);
|
|
3893
|
+
const weight = i === this.grabbedIndex ? 1 : grabFalloff(distance, radius);
|
|
3894
|
+
if (weight <= 0) continue;
|
|
3895
|
+
this.grabWeights[i] = weight;
|
|
3896
|
+
this.grabbed.push(i);
|
|
3897
|
+
const i3 = i * 3;
|
|
3898
|
+
offsets.push(
|
|
3899
|
+
this.positions[i3] - this.positions[anchor],
|
|
3900
|
+
this.positions[i3 + 1] - this.positions[anchor + 1],
|
|
3901
|
+
this.positions[i3 + 2] - this.positions[anchor + 2]
|
|
3902
|
+
);
|
|
3903
|
+
}
|
|
3904
|
+
}
|
|
3905
|
+
this.grabOffsets = Float32Array.from(offsets);
|
|
3906
|
+
this.grabAt.set(this.positions.subarray(anchor, anchor + 3));
|
|
3907
|
+
this.grabWas.set(this.grabAt);
|
|
3908
|
+
this.grabVelocity.fill(0);
|
|
3909
|
+
this.wake();
|
|
3910
|
+
return this.grabbedIndex;
|
|
3911
|
+
}
|
|
3142
3912
|
/** Nearest particle to a local-space point — the grab interface. */
|
|
3143
3913
|
grabNearest(x, y, z25) {
|
|
3144
3914
|
let best = -1;
|
|
@@ -3153,32 +3923,110 @@ var ClothSim = class {
|
|
|
3153
3923
|
best = i;
|
|
3154
3924
|
}
|
|
3155
3925
|
}
|
|
3156
|
-
this.
|
|
3157
|
-
|
|
3158
|
-
|
|
3926
|
+
return this.grab(best);
|
|
3927
|
+
}
|
|
3928
|
+
/** How hard one particle is being held, 0..1 — for tests and for the adapter. */
|
|
3929
|
+
grabWeightAt(index) {
|
|
3930
|
+
return this.grabWeights[index] ?? 0;
|
|
3159
3931
|
}
|
|
3932
|
+
/** Where the fingers are now. The patch follows, each particle by its weight. */
|
|
3160
3933
|
moveGrab(x, y, z25) {
|
|
3161
3934
|
if (this.grabbedIndex < 0) return;
|
|
3162
|
-
|
|
3163
|
-
this.
|
|
3164
|
-
this.
|
|
3165
|
-
this.positions[i3 + 2] = z25;
|
|
3166
|
-
this.prev[i3] = x;
|
|
3167
|
-
this.prev[i3 + 1] = y;
|
|
3168
|
-
this.prev[i3 + 2] = z25;
|
|
3935
|
+
this.grabAt[0] = x;
|
|
3936
|
+
this.grabAt[1] = y;
|
|
3937
|
+
this.grabAt[2] = z25;
|
|
3169
3938
|
this.wake();
|
|
3170
3939
|
}
|
|
3940
|
+
/**
|
|
3941
|
+
* Let go — and let go at SPEED.
|
|
3942
|
+
*
|
|
3943
|
+
* This used to drop the paper dead. Every held particle had its previous
|
|
3944
|
+
* position overwritten with its current one on the way past, and in a verlet
|
|
3945
|
+
* integrator the gap between those two IS the velocity, so a sheet whipped
|
|
3946
|
+
* across the frame and released came to a perfect standstill and then fell
|
|
3947
|
+
* straight down. Whatever you did with your hand, the paper had never heard
|
|
3948
|
+
* of it.
|
|
3949
|
+
*
|
|
3950
|
+
* The hand's velocity is measured per second (see {@link step}) and spent
|
|
3951
|
+
* here, converted into the one-substep gap the integrator reads it back out
|
|
3952
|
+
* of. Measured per second rather than per frame because a frame is not a
|
|
3953
|
+
* fixed length and a substep is: throwing the same sheet at the same speed
|
|
3954
|
+
* must not depend on what the frame rate happened to be.
|
|
3955
|
+
*/
|
|
3171
3956
|
release() {
|
|
3957
|
+
for (const i of this.grabbed) {
|
|
3958
|
+
const i3 = i * 3;
|
|
3959
|
+
const held = this.grabWeights[i];
|
|
3960
|
+
this.prev[i3] = this.positions[i3] - this.grabVelocity[0] * FIXED_DT * held;
|
|
3961
|
+
this.prev[i3 + 1] = this.positions[i3 + 1] - this.grabVelocity[1] * FIXED_DT * held;
|
|
3962
|
+
this.prev[i3 + 2] = this.positions[i3 + 2] - this.grabVelocity[2] * FIXED_DT * held;
|
|
3963
|
+
}
|
|
3172
3964
|
this.grabbedIndex = -1;
|
|
3965
|
+
this.grabbed = [];
|
|
3966
|
+
this.grabWeights.fill(0);
|
|
3967
|
+
this.grabVelocity.fill(0);
|
|
3968
|
+
this.wake();
|
|
3173
3969
|
}
|
|
3174
3970
|
step(delta) {
|
|
3175
3971
|
if (this.asleep) return;
|
|
3972
|
+
if (this.grabbedIndex >= 0 && delta > 0) {
|
|
3973
|
+
for (let axis = 0; axis < 3; axis++) {
|
|
3974
|
+
this.grabVelocity[axis] = (this.grabAt[axis] - this.grabWas[axis]) / delta;
|
|
3975
|
+
}
|
|
3976
|
+
this.grabWas.set(this.grabAt);
|
|
3977
|
+
}
|
|
3978
|
+
this.updateNormals();
|
|
3176
3979
|
this.accumulator = Math.min(this.accumulator + delta, FIXED_DT * 4);
|
|
3177
3980
|
while (this.accumulator >= FIXED_DT) {
|
|
3178
3981
|
this.substep(FIXED_DT);
|
|
3179
3982
|
this.accumulator -= FIXED_DT;
|
|
3180
3983
|
}
|
|
3181
3984
|
}
|
|
3985
|
+
/**
|
|
3986
|
+
* Which way each part of the sheet is facing, by central difference across
|
|
3987
|
+
* the grid.
|
|
3988
|
+
*
|
|
3989
|
+
* Clamped at the edges rather than wrapped or skipped: an edge particle
|
|
3990
|
+
* takes the one-sided difference, which is the same normal its neighbour
|
|
3991
|
+
* has, and an edge with no normal is an edge the wind cannot push.
|
|
3992
|
+
*
|
|
3993
|
+
* Which SIDE the normal points at is deliberately not worried about. The
|
|
3994
|
+
* force below is `n (n·v)`, a quadratic form, and that is unchanged by
|
|
3995
|
+
* flipping n — which is exactly right for paper, a surface with no front as
|
|
3996
|
+
* far as the air is concerned.
|
|
3997
|
+
*/
|
|
3998
|
+
updateNormals() {
|
|
3999
|
+
const p = this.positions;
|
|
4000
|
+
const n = this.normals;
|
|
4001
|
+
for (let r = 0; r < this.rows; r++) {
|
|
4002
|
+
for (let c = 0; c < this.cols; c++) {
|
|
4003
|
+
const left = (r * this.cols + (c > 0 ? c - 1 : c)) * 3;
|
|
4004
|
+
const right = (r * this.cols + (c < this.cols - 1 ? c + 1 : c)) * 3;
|
|
4005
|
+
const up = ((r > 0 ? r - 1 : r) * this.cols + c) * 3;
|
|
4006
|
+
const down = ((r < this.rows - 1 ? r + 1 : r) * this.cols + c) * 3;
|
|
4007
|
+
const ux = p[right] - p[left];
|
|
4008
|
+
const uy = p[right + 1] - p[left + 1];
|
|
4009
|
+
const uz = p[right + 2] - p[left + 2];
|
|
4010
|
+
const vx = p[down] - p[up];
|
|
4011
|
+
const vy = p[down + 1] - p[up + 1];
|
|
4012
|
+
const vz = p[down + 2] - p[up + 2];
|
|
4013
|
+
const nx = vy * uz - vz * uy;
|
|
4014
|
+
const ny = vz * ux - vx * uz;
|
|
4015
|
+
const nz = vx * uy - vy * ux;
|
|
4016
|
+
const length = Math.sqrt(nx * nx + ny * ny + nz * nz);
|
|
4017
|
+
const i3 = (r * this.cols + c) * 3;
|
|
4018
|
+
if (length > 1e-12) {
|
|
4019
|
+
n[i3] = nx / length;
|
|
4020
|
+
n[i3 + 1] = ny / length;
|
|
4021
|
+
n[i3 + 2] = nz / length;
|
|
4022
|
+
} else {
|
|
4023
|
+
n[i3] = 0;
|
|
4024
|
+
n[i3 + 1] = 0;
|
|
4025
|
+
n[i3 + 2] = 0;
|
|
4026
|
+
}
|
|
4027
|
+
}
|
|
4028
|
+
}
|
|
4029
|
+
}
|
|
3182
4030
|
substep(dt) {
|
|
3183
4031
|
const { gravity, wind, stiffness, floor } = this.params;
|
|
3184
4032
|
const p = this.positions;
|
|
@@ -3188,12 +4036,10 @@ var ClothSim = class {
|
|
|
3188
4036
|
let maxTravel = 0;
|
|
3189
4037
|
for (let i = 0; i < this.count; i++) {
|
|
3190
4038
|
const i3 = i * 3;
|
|
3191
|
-
if (this.pinned[i]
|
|
3192
|
-
|
|
3193
|
-
|
|
3194
|
-
|
|
3195
|
-
p[i3 + 2] = this.pinTargets[i3 + 2];
|
|
3196
|
-
}
|
|
4039
|
+
if (this.pinned[i]) {
|
|
4040
|
+
p[i3] = this.pinTargets[i3];
|
|
4041
|
+
p[i3 + 1] = this.pinTargets[i3 + 1];
|
|
4042
|
+
p[i3 + 2] = this.pinTargets[i3 + 2];
|
|
3197
4043
|
this.prev[i3] = p[i3];
|
|
3198
4044
|
this.prev[i3 + 1] = p[i3 + 1];
|
|
3199
4045
|
this.prev[i3 + 2] = p[i3 + 2];
|
|
@@ -3202,20 +4048,38 @@ var ClothSim = class {
|
|
|
3202
4048
|
const x = p[i3];
|
|
3203
4049
|
const y = p[i3 + 1];
|
|
3204
4050
|
const z25 = p[i3 + 2];
|
|
3205
|
-
const gust2 = wind * (0.55 + 0.45 * Math.sin(this.time * 1.7 + x * 2.1 + y * 1.3)) * 0.9;
|
|
3206
|
-
const ax = gust2 * 0.25;
|
|
3207
|
-
const az = gust2;
|
|
3208
4051
|
const vx = (x - this.prev[i3]) * damping;
|
|
3209
4052
|
const vy = (y - this.prev[i3 + 1]) * damping;
|
|
3210
4053
|
const vz = (z25 - this.prev[i3 + 2]) * damping;
|
|
4054
|
+
const gust2 = wind * (0.55 + 0.45 * Math.sin(this.time * 1.7 + x * 2.1 + y * 1.3)) * 0.9;
|
|
4055
|
+
const relX = gust2 * 0.25 - vx / dt;
|
|
4056
|
+
const relY = -vy / dt;
|
|
4057
|
+
const relZ = gust2 - vz / dt;
|
|
4058
|
+
const nx = this.normals[i3];
|
|
4059
|
+
const ny = this.normals[i3 + 1];
|
|
4060
|
+
const nz = this.normals[i3 + 2];
|
|
4061
|
+
const facing = nx * relX + ny * relY + nz * relZ;
|
|
4062
|
+
const ax = nx * facing * (1 - AERO_TURBULENCE) + gust2 * 0.25 * AERO_TURBULENCE;
|
|
4063
|
+
const ay = ny * facing * (1 - AERO_TURBULENCE);
|
|
4064
|
+
const az = nz * facing * (1 - AERO_TURBULENCE) + gust2 * AERO_TURBULENCE;
|
|
3211
4065
|
this.prev[i3] = x;
|
|
3212
4066
|
this.prev[i3 + 1] = y;
|
|
3213
4067
|
this.prev[i3 + 2] = z25;
|
|
3214
4068
|
p[i3] = x + vx + ax * dt2;
|
|
3215
|
-
p[i3 + 1] = y + vy - gravity * 3.2 * dt2;
|
|
4069
|
+
p[i3 + 1] = y + vy + (ay - gravity * 3.2) * dt2;
|
|
3216
4070
|
p[i3 + 2] = z25 + vz + az * dt2;
|
|
3217
4071
|
maxTravel = Math.max(maxTravel, vx * vx + vy * vy + vz * vz);
|
|
3218
4072
|
}
|
|
4073
|
+
for (let k = 0; k < this.grabbed.length; k++) {
|
|
4074
|
+
const i = this.grabbed[k];
|
|
4075
|
+
const i3 = i * 3;
|
|
4076
|
+
const k3 = k * 3;
|
|
4077
|
+
const held = this.grabWeights[i];
|
|
4078
|
+
for (let axis = 0; axis < 3; axis++) {
|
|
4079
|
+
const target = this.grabAt[axis] + this.grabOffsets[k3 + axis];
|
|
4080
|
+
p[i3 + axis] = p[i3 + axis] + (target - p[i3 + axis]) * held;
|
|
4081
|
+
}
|
|
4082
|
+
}
|
|
3219
4083
|
for (let iter = 0; iter < SOLVER_ITERATIONS; iter++) {
|
|
3220
4084
|
for (const c of this.constraints) {
|
|
3221
4085
|
const k = c.kind === 2 ? 0.25 + stiffness * 0.7 : c.kind === 1 ? 0.85 : 1;
|
|
@@ -3226,12 +4090,13 @@ var ClothSim = class {
|
|
|
3226
4090
|
const dz = p[b3 + 2] - p[a3 + 2];
|
|
3227
4091
|
const dist = Math.sqrt(dx * dx + dy * dy + dz * dz);
|
|
3228
4092
|
if (dist === 0) continue;
|
|
3229
|
-
const
|
|
3230
|
-
const
|
|
3231
|
-
const
|
|
3232
|
-
if (
|
|
3233
|
-
const
|
|
3234
|
-
const
|
|
4093
|
+
const ma = this.pinned[c.a] ? 0 : 1 - this.grabWeights[c.a];
|
|
4094
|
+
const mb = this.pinned[c.b] ? 0 : 1 - this.grabWeights[c.b];
|
|
4095
|
+
const total = ma + mb;
|
|
4096
|
+
if (total <= 0) continue;
|
|
4097
|
+
const diff = (dist - c.rest) / dist / total * k;
|
|
4098
|
+
const aw = ma;
|
|
4099
|
+
const bw = mb;
|
|
3235
4100
|
p[a3] = p[a3] + dx * diff * aw;
|
|
3236
4101
|
p[a3 + 1] = p[a3 + 1] + dy * diff * aw;
|
|
3237
4102
|
p[a3 + 2] = p[a3 + 2] + dz * diff * aw;
|
|
@@ -3258,6 +4123,724 @@ var ClothSim = class {
|
|
|
3258
4123
|
}
|
|
3259
4124
|
};
|
|
3260
4125
|
|
|
4126
|
+
// src/physics/strip.ts
|
|
4127
|
+
var FIXED_DT2 = 1 / 120;
|
|
4128
|
+
var SOLVER_ITERATIONS2 = 8;
|
|
4129
|
+
var SLEEP_EPSILON2 = 1e-7;
|
|
4130
|
+
var SLEEP_FRAMES2 = 45;
|
|
4131
|
+
var LOOSE2 = 0.055;
|
|
4132
|
+
var TIGHT2 = 0.012;
|
|
4133
|
+
var NODES_PER_PANEL = 16;
|
|
4134
|
+
var MAX_NODES = 440;
|
|
4135
|
+
var MIN_NODES = 8;
|
|
4136
|
+
var REFERENCE_SEGMENT = 0.0375;
|
|
4137
|
+
var TANGENT_SPAN = 0.09;
|
|
4138
|
+
var PULL_STIFFNESS = 0.35;
|
|
4139
|
+
var COLLIDING_ITERATIONS = 4;
|
|
4140
|
+
var COLLISION_RELAXATION = 0.35;
|
|
4141
|
+
function safeSpiralRadius(segment, thickness) {
|
|
4142
|
+
return thickness > 0 ? segment * segment / (4 * thickness) : 0;
|
|
4143
|
+
}
|
|
4144
|
+
function layerThickness2(tightness) {
|
|
4145
|
+
return LOOSE2 - Math.min(1, Math.max(0, tightness)) * (LOOSE2 - TIGHT2);
|
|
4146
|
+
}
|
|
4147
|
+
function stripNodeCount(length, perforation) {
|
|
4148
|
+
const seg = Math.max(perforation, 1e-3) / NODES_PER_PANEL;
|
|
4149
|
+
return Math.min(MAX_NODES, Math.max(MIN_NODES, Math.round(length / seg) + 1));
|
|
4150
|
+
}
|
|
4151
|
+
function maxStripLength(perforation) {
|
|
4152
|
+
const seg = Math.max(perforation, 1e-3) / NODES_PER_PANEL;
|
|
4153
|
+
return (MAX_NODES - 1 - 0.5) * seg;
|
|
4154
|
+
}
|
|
4155
|
+
var StripSim = class {
|
|
4156
|
+
/** Chain length. Node 0 is the deepest wrap; node `count-1` is the free tip. */
|
|
4157
|
+
count;
|
|
4158
|
+
/** Rest spacing between neighbours, in world units. */
|
|
4159
|
+
segment;
|
|
4160
|
+
/** Node centreline, y/z interleaved. x is implicit: the strip never twists. */
|
|
4161
|
+
pos;
|
|
4162
|
+
prev;
|
|
4163
|
+
/** Arc distance from the tip, per node. */
|
|
4164
|
+
arc;
|
|
4165
|
+
/** 1 where a perforation hinges, and the sign of the crease it remembers. */
|
|
4166
|
+
perforated;
|
|
4167
|
+
/** Bend gain correction for this chain's node spacing. See {@link REFERENCE_SEGMENT}. */
|
|
4168
|
+
bendScale;
|
|
4169
|
+
/** Half-width, in nodes, of the window the drag tangent is measured over. */
|
|
4170
|
+
tangentNodes;
|
|
4171
|
+
halfWidth;
|
|
4172
|
+
totalLength;
|
|
4173
|
+
/**
|
|
4174
|
+
* Paper that can never be paid out, because its inner end is glued to the
|
|
4175
|
+
* tube — which is true of every roll you have ever used.
|
|
4176
|
+
*
|
|
4177
|
+
* Without it the roll pays out to nothing, `firstFreeIndex` reaches 0, and
|
|
4178
|
+
* every node the roll was made of becomes free paper and falls: the whole
|
|
4179
|
+
* roll drops off its holder and lands flat on the pile. One wrap held back
|
|
4180
|
+
* leaves a cylinder at the core radius on the holder, which is what an
|
|
4181
|
+
* empty roll looks like and the closest this library can get to drawing a
|
|
4182
|
+
* cardboard tube out of the one sheet it has.
|
|
4183
|
+
*/
|
|
4184
|
+
tubeStub;
|
|
4185
|
+
params;
|
|
4186
|
+
/** Paid-out length: how much paper is off the roll. */
|
|
4187
|
+
paid;
|
|
4188
|
+
/** Angular velocity of the roll, rad/s. The thing that coasts. */
|
|
4189
|
+
omega = 0;
|
|
4190
|
+
/**
|
|
4191
|
+
* Whether anything has turned the roll yet — a scroll delta or a hand.
|
|
4192
|
+
*
|
|
4193
|
+
* `tail` is the opening pose, and editing it should re-pose the roll; but
|
|
4194
|
+
* once the host has scrolled, `paid` is the simulation's own state and
|
|
4195
|
+
* re-posing would yank the paper back out of the pile.
|
|
4196
|
+
*/
|
|
4197
|
+
driven = false;
|
|
4198
|
+
lastScroll;
|
|
4199
|
+
primed = false;
|
|
4200
|
+
/** Node held by the pointer, or -1. Kinematic while held, like a wound node. */
|
|
4201
|
+
grabbed = -1;
|
|
4202
|
+
grabY = 0;
|
|
4203
|
+
grabZ = 0;
|
|
4204
|
+
stillFrames = 0;
|
|
4205
|
+
accumulator = 0;
|
|
4206
|
+
time = 0;
|
|
4207
|
+
asleep = false;
|
|
4208
|
+
// Spatial hash for self-collision. Sized once, refilled in place — the
|
|
4209
|
+
// per-substep collision pass allocates nothing.
|
|
4210
|
+
cellOf;
|
|
4211
|
+
bucketStart;
|
|
4212
|
+
bucketItems;
|
|
4213
|
+
bucketFill;
|
|
4214
|
+
tableSize;
|
|
4215
|
+
constructor(length, width, params) {
|
|
4216
|
+
this.params = { ...params };
|
|
4217
|
+
this.totalLength = length;
|
|
4218
|
+
this.halfWidth = width / 2;
|
|
4219
|
+
this.count = stripNodeCount(length, params.perforation);
|
|
4220
|
+
this.segment = length / (this.count - 1);
|
|
4221
|
+
this.bendScale = Math.min(1, this.segment / REFERENCE_SEGMENT);
|
|
4222
|
+
this.tangentNodes = Math.max(1, Math.round(TANGENT_SPAN / this.segment));
|
|
4223
|
+
this.pos = new Float64Array(this.count * 2);
|
|
4224
|
+
this.prev = new Float64Array(this.count * 2);
|
|
4225
|
+
this.arc = new Float64Array(this.count);
|
|
4226
|
+
this.perforated = new Int8Array(this.count);
|
|
4227
|
+
for (let i = 0; i < this.count; i++) {
|
|
4228
|
+
this.arc[i] = (this.count - 1 - i) * this.segment;
|
|
4229
|
+
}
|
|
4230
|
+
const spacing = Math.max(params.perforation, this.segment * 2);
|
|
4231
|
+
for (let i = 0; i < this.count - 1; i++) {
|
|
4232
|
+
const here = Math.floor(this.arc[i] / spacing);
|
|
4233
|
+
const next = Math.floor(this.arc[i + 1] / spacing);
|
|
4234
|
+
if (here !== next) this.perforated[i] = here % 2 === 0 ? 1 : -1;
|
|
4235
|
+
}
|
|
4236
|
+
this.tableSize = 1 << Math.ceil(Math.log2(Math.max(16, this.count * 2)));
|
|
4237
|
+
this.cellOf = new Int32Array(this.count);
|
|
4238
|
+
this.bucketStart = new Int32Array(this.tableSize + 1);
|
|
4239
|
+
this.bucketItems = new Int32Array(this.count);
|
|
4240
|
+
this.bucketFill = new Int32Array(this.tableSize);
|
|
4241
|
+
this.tubeStub = this.stubFor(params.core);
|
|
4242
|
+
this.paid = Math.min(params.tail, this.usableLength);
|
|
4243
|
+
this.lastScroll = params.scroll;
|
|
4244
|
+
this.layOut();
|
|
4245
|
+
}
|
|
4246
|
+
/**
|
|
4247
|
+
* Local-space y the pile builds on, after centring. A host placing a
|
|
4248
|
+
* shadow catcher or a contact shadow needs this; it cannot derive it,
|
|
4249
|
+
* because the composition is offset to sit on the origin.
|
|
4250
|
+
*/
|
|
4251
|
+
get floorY() {
|
|
4252
|
+
return -this.params.floor + this.centreOffset;
|
|
4253
|
+
}
|
|
4254
|
+
/**
|
|
4255
|
+
* How far the whole composition is lifted so that it straddles the origin.
|
|
4256
|
+
*
|
|
4257
|
+
* The roll's axis is the sim's origin, which would park a long drop
|
|
4258
|
+
* entirely below the frame — `<Paper>` and the editor both look at the
|
|
4259
|
+
* origin through about two world units, and neither fits a camera to the
|
|
4260
|
+
* content. `unroll` learned the same lesson and left a note about it: a
|
|
4261
|
+
* composition anchored anywhere but the origin needs a bespoke camera at
|
|
4262
|
+
* every call site.
|
|
4263
|
+
*
|
|
4264
|
+
* Measured against the FULL radius rather than the current one, so it is a
|
|
4265
|
+
* constant for a given config. Deriving it from the live radius would drift
|
|
4266
|
+
* the entire pile upward as the roll ran down.
|
|
4267
|
+
*/
|
|
4268
|
+
get centreOffset() {
|
|
4269
|
+
return (this.params.floor - this.outerRadius) / 2;
|
|
4270
|
+
}
|
|
4271
|
+
/**
|
|
4272
|
+
* The same centring, along the axis the pile actually grows in.
|
|
4273
|
+
*
|
|
4274
|
+
* Everything here is built around the DROP LINE — the z the paper leaves
|
|
4275
|
+
* the roll at — and that line is not the origin and does not stay put. It
|
|
4276
|
+
* starts at 0 on a full roll and travels back to `core - outerRadius` as
|
|
4277
|
+
* the roll runs down (see {@link tangentZ}), because the spiral's centre is
|
|
4278
|
+
* fixed and the tangent point walks in toward it. The pile builds around
|
|
4279
|
+
* wherever the line has been, so a composition that ignores this is offset
|
|
4280
|
+
* by half that travel before a single fold has landed, and z is DEPTH —
|
|
4281
|
+
* the axis a fixed head-on camera has the least room in.
|
|
4282
|
+
*
|
|
4283
|
+
* A constant for a given config, for the reason {@link centreOffset} is:
|
|
4284
|
+
* derived from the live radius it would slide the whole composition
|
|
4285
|
+
* backwards as the roll emptied, which reads far worse than sitting still
|
|
4286
|
+
* slightly off centre. Centring the line's travel splits the difference
|
|
4287
|
+
* between a full roll and an empty one.
|
|
4288
|
+
*/
|
|
4289
|
+
get centreShift() {
|
|
4290
|
+
return (this.outerRadius - this.params.core) / 2;
|
|
4291
|
+
}
|
|
4292
|
+
/** Outer radius of what is still wound. Shrinks to `core` as the roll empties. */
|
|
4293
|
+
get radius() {
|
|
4294
|
+
return rollRadius(this.totalLength - this.paid, this.params.core, layerThickness2(this.params.tightness));
|
|
4295
|
+
}
|
|
4296
|
+
/** Radius of the full roll. Fixes the spiral's centre, which must not move. */
|
|
4297
|
+
get outerRadius() {
|
|
4298
|
+
return rollRadius(this.totalLength, this.params.core, layerThickness2(this.params.tightness));
|
|
4299
|
+
}
|
|
4300
|
+
/**
|
|
4301
|
+
* The wrap that never leaves: one full turn around the core, but never so
|
|
4302
|
+
* much of a short sheet that there is nothing left to unroll.
|
|
4303
|
+
*
|
|
4304
|
+
* A method rather than a constant because `core` is a live control. It was
|
|
4305
|
+
* a constructor-time `readonly`, which meant dragging `core` moved the
|
|
4306
|
+
* radius the spiral is drawn at while `usableLength` and the end stop kept
|
|
4307
|
+
* answering for the old tube — the roll would run past its own floor, or
|
|
4308
|
+
* stop short of it, depending on which way the slider went.
|
|
4309
|
+
*/
|
|
4310
|
+
stubFor(core) {
|
|
4311
|
+
return Math.min(Math.PI * 2 * core, this.totalLength * 0.3);
|
|
4312
|
+
}
|
|
4313
|
+
/** Paper that can actually leave the roll — everything but the glued stub. */
|
|
4314
|
+
get usableLength() {
|
|
4315
|
+
return this.totalLength - this.tubeStub;
|
|
4316
|
+
}
|
|
4317
|
+
/** How much paper is left to give. `0` is down to the tube, `1` is untouched. */
|
|
4318
|
+
get remaining() {
|
|
4319
|
+
return 1 - this.paid / this.usableLength;
|
|
4320
|
+
}
|
|
4321
|
+
/**
|
|
4322
|
+
* Where the paper leaves the roll: the frontmost point of the current
|
|
4323
|
+
* outer wrap. The spiral's CENTRE is fixed (a lesson the `roll` deformer
|
|
4324
|
+
* paid for — see its notes), so as the roll runs down the tangent point
|
|
4325
|
+
* travels back toward the holder and the strip hangs closer to it. That
|
|
4326
|
+
* drift is real and worth keeping.
|
|
4327
|
+
*/
|
|
4328
|
+
get tangentZ() {
|
|
4329
|
+
return this.radius - this.outerRadius;
|
|
4330
|
+
}
|
|
4331
|
+
/** Place every node: the wound ones on the spiral, the free ones straight down. */
|
|
4332
|
+
layOut() {
|
|
4333
|
+
const firstFree = this.firstFreeIndex();
|
|
4334
|
+
for (let i = 0; i < this.count; i++) {
|
|
4335
|
+
const p = i * 2;
|
|
4336
|
+
if (i < firstFree) {
|
|
4337
|
+
this.spiralPoint(this.arc[i] - this.paid, p, this.pos);
|
|
4338
|
+
} else {
|
|
4339
|
+
this.pos[p] = -(this.paid - this.arc[i]);
|
|
4340
|
+
this.pos[p + 1] = this.tangentZ;
|
|
4341
|
+
}
|
|
4342
|
+
}
|
|
4343
|
+
this.prev.set(this.pos);
|
|
4344
|
+
}
|
|
4345
|
+
/** First node that has come off the roll. Everything below it is kinematic. */
|
|
4346
|
+
firstFreeIndex() {
|
|
4347
|
+
const free = Math.ceil(this.count - 1 - this.paid / this.segment);
|
|
4348
|
+
return Math.min(this.count - 1, Math.max(0, free));
|
|
4349
|
+
}
|
|
4350
|
+
/**
|
|
4351
|
+
* Position of a point `wound` along the spiral from the tangent point,
|
|
4352
|
+
* written into `out` at offset `o`.
|
|
4353
|
+
*
|
|
4354
|
+
* Angle 0 is the tangent point at the front of the roll; winding runs up
|
|
4355
|
+
* over the top, which is the "over" hang. The radius falls by one layer
|
|
4356
|
+
* per turn, so the wraps are concentric and exactly a thickness apart.
|
|
4357
|
+
*/
|
|
4358
|
+
spiralPoint(wound, o, out) {
|
|
4359
|
+
const r0 = this.radius;
|
|
4360
|
+
const thickness = layerThickness2(this.params.tightness);
|
|
4361
|
+
const k = thickness / (Math.PI * 2);
|
|
4362
|
+
const phi = windAngle(Math.max(0, wound), r0, k);
|
|
4363
|
+
const floor = Math.max(this.params.core, safeSpiralRadius(this.segment, thickness));
|
|
4364
|
+
const r = Math.max(floor, r0 - k * phi);
|
|
4365
|
+
out[o] = r * Math.sin(phi);
|
|
4366
|
+
out[o + 1] = -this.outerRadius + r * Math.cos(phi);
|
|
4367
|
+
}
|
|
4368
|
+
setParams(params) {
|
|
4369
|
+
let changed = false;
|
|
4370
|
+
const tailBefore = this.params.tail;
|
|
4371
|
+
for (const key of [
|
|
4372
|
+
"scroll",
|
|
4373
|
+
"tightness",
|
|
4374
|
+
"core",
|
|
4375
|
+
"tail",
|
|
4376
|
+
"crease",
|
|
4377
|
+
"stiffness",
|
|
4378
|
+
"drag",
|
|
4379
|
+
"gravity",
|
|
4380
|
+
"floor",
|
|
4381
|
+
"inertia"
|
|
4382
|
+
]) {
|
|
4383
|
+
const value = params[key];
|
|
4384
|
+
if (value !== void 0 && value !== this.params[key]) {
|
|
4385
|
+
this.params[key] = value;
|
|
4386
|
+
changed = true;
|
|
4387
|
+
}
|
|
4388
|
+
}
|
|
4389
|
+
if (params.core !== void 0) {
|
|
4390
|
+
this.tubeStub = this.stubFor(params.core);
|
|
4391
|
+
this.paid = Math.min(this.paid, this.usableLength);
|
|
4392
|
+
}
|
|
4393
|
+
if (params.tail !== void 0 && tailBefore !== this.params.tail) {
|
|
4394
|
+
if (!this.driven) {
|
|
4395
|
+
this.paid = Math.min(this.params.tail, this.usableLength);
|
|
4396
|
+
this.layOut();
|
|
4397
|
+
}
|
|
4398
|
+
}
|
|
4399
|
+
if (changed) this.wake();
|
|
4400
|
+
}
|
|
4401
|
+
wake() {
|
|
4402
|
+
this.asleep = false;
|
|
4403
|
+
this.stillFrames = 0;
|
|
4404
|
+
}
|
|
4405
|
+
/**
|
|
4406
|
+
* Take hold of the paper at a point, in the same y–z the vertex buffer is
|
|
4407
|
+
* written in — mesh-local coordinates, the lift already applied. Returns the
|
|
4408
|
+
* node caught, or -1 when there is no free paper to catch.
|
|
4409
|
+
*
|
|
4410
|
+
* Speaking the buffer's coordinates rather than the solver's is the whole
|
|
4411
|
+
* contract here: a caller has a raycast hit on the mesh and nothing else,
|
|
4412
|
+
* and the centring lift is an internal detail it has no way to know.
|
|
4413
|
+
*
|
|
4414
|
+
* Only paper that has left the roll can be caught — a wound node belongs to
|
|
4415
|
+
* the spiral, and pinning one would be pinning the roll itself.
|
|
4416
|
+
*/
|
|
4417
|
+
grabNearest(y, z25) {
|
|
4418
|
+
const firstFree = this.firstFreeIndex();
|
|
4419
|
+
const localY = y - this.centreOffset;
|
|
4420
|
+
const localZ = z25 - this.centreShift;
|
|
4421
|
+
let best = -1;
|
|
4422
|
+
let bestDist = Infinity;
|
|
4423
|
+
for (let i = firstFree; i < this.count; i++) {
|
|
4424
|
+
const dy = this.pos[i * 2] - localY;
|
|
4425
|
+
const dz = this.pos[i * 2 + 1] - localZ;
|
|
4426
|
+
const d = dy * dy + dz * dz;
|
|
4427
|
+
if (d < bestDist) {
|
|
4428
|
+
bestDist = d;
|
|
4429
|
+
best = i;
|
|
4430
|
+
}
|
|
4431
|
+
}
|
|
4432
|
+
this.grabbed = best;
|
|
4433
|
+
this.grabY = localY;
|
|
4434
|
+
this.grabZ = localZ;
|
|
4435
|
+
this.wake();
|
|
4436
|
+
return best;
|
|
4437
|
+
}
|
|
4438
|
+
moveGrab(y, z25) {
|
|
4439
|
+
if (this.grabbed < 0) return;
|
|
4440
|
+
this.grabY = y - this.centreOffset;
|
|
4441
|
+
this.grabZ = z25 - this.centreShift;
|
|
4442
|
+
this.wake();
|
|
4443
|
+
}
|
|
4444
|
+
release() {
|
|
4445
|
+
this.grabbed = -1;
|
|
4446
|
+
}
|
|
4447
|
+
/** Whether a pointer currently holds the paper. */
|
|
4448
|
+
get held() {
|
|
4449
|
+
return this.grabbed >= 0;
|
|
4450
|
+
}
|
|
4451
|
+
step(delta) {
|
|
4452
|
+
if (this.params.scroll !== this.lastScroll) this.wake();
|
|
4453
|
+
if (this.asleep) return;
|
|
4454
|
+
this.accumulator = Math.min(this.accumulator + delta, FIXED_DT2 * 4);
|
|
4455
|
+
while (this.accumulator >= FIXED_DT2) {
|
|
4456
|
+
this.substep(FIXED_DT2);
|
|
4457
|
+
this.accumulator -= FIXED_DT2;
|
|
4458
|
+
}
|
|
4459
|
+
}
|
|
4460
|
+
substep(dt) {
|
|
4461
|
+
this.time += dt;
|
|
4462
|
+
this.driveRoll(dt);
|
|
4463
|
+
this.pullFeed(dt);
|
|
4464
|
+
this.integrate(dt);
|
|
4465
|
+
const firstFree = this.firstFreeIndex();
|
|
4466
|
+
this.pinWound(firstFree);
|
|
4467
|
+
for (let iter = 0; iter < SOLVER_ITERATIONS2; iter++) {
|
|
4468
|
+
this.solveDistance(firstFree);
|
|
4469
|
+
this.solveBend(firstFree);
|
|
4470
|
+
if (iter >= SOLVER_ITERATIONS2 - COLLIDING_ITERATIONS) {
|
|
4471
|
+
this.solveSelfCollision(firstFree);
|
|
4472
|
+
this.solveFloor(firstFree);
|
|
4473
|
+
}
|
|
4474
|
+
this.pinWound(firstFree);
|
|
4475
|
+
}
|
|
4476
|
+
this.solveFloor(firstFree);
|
|
4477
|
+
}
|
|
4478
|
+
/**
|
|
4479
|
+
* Turn the roll. Scroll enters as an angle the roll OWES, which a leaky
|
|
4480
|
+
* integrator spends into angular velocity — so a steady scroll gives a
|
|
4481
|
+
* steady spin and a flick spikes and coasts down.
|
|
4482
|
+
*
|
|
4483
|
+
* `ΔL = R·Δθ` with the CURRENT radius, which is why the radius has to be
|
|
4484
|
+
* read inside the loop: a nearly-empty roll spins fast and gives up very
|
|
4485
|
+
* little paper for the same scroll, and that is the whole tell that a roll
|
|
4486
|
+
* is running out.
|
|
4487
|
+
*/
|
|
4488
|
+
driveRoll(dt) {
|
|
4489
|
+
const scroll = this.params.scroll;
|
|
4490
|
+
if (!this.primed) {
|
|
4491
|
+
this.lastScroll = scroll;
|
|
4492
|
+
this.primed = true;
|
|
4493
|
+
}
|
|
4494
|
+
const impulse = (scroll - this.lastScroll) / this.outerRadius;
|
|
4495
|
+
this.lastScroll = scroll;
|
|
4496
|
+
const tau = 0.012 + this.params.inertia * 0.55;
|
|
4497
|
+
this.omega = this.omega * Math.exp(-dt / tau) + impulse / tau;
|
|
4498
|
+
if (Math.abs(this.omega) < 1e-6) this.omega = 0;
|
|
4499
|
+
const next = this.paid + this.radius * this.omega * dt;
|
|
4500
|
+
if (next !== this.paid) this.driven = true;
|
|
4501
|
+
this.paid = Math.min(this.usableLength, Math.max(0, next));
|
|
4502
|
+
if (this.paid === 0 || this.paid === this.usableLength) this.omega = 0;
|
|
4503
|
+
}
|
|
4504
|
+
/**
|
|
4505
|
+
* Take hold of the paper and pull, and the roll turns — the interaction the
|
|
4506
|
+
* real object is famous for.
|
|
4507
|
+
*
|
|
4508
|
+
* It is driven by TENSION rather than by mapping hand travel to an angle.
|
|
4509
|
+
* Paper does not stretch, so if the hand is further from the roll than there
|
|
4510
|
+
* is paper to reach it, the only way the constraint can be satisfied is for
|
|
4511
|
+
* the roll to give up more. That one rule gets the whole behaviour for free:
|
|
4512
|
+
* a slow pull feeds smoothly, a fast yank spins the roll and it carries on
|
|
4513
|
+
* after release, and pushing the paper back toward the roll does nothing at
|
|
4514
|
+
* all — which is exactly right, because slack does not rewind a roll. Only
|
|
4515
|
+
* scrolling up does.
|
|
4516
|
+
*/
|
|
4517
|
+
pullFeed(dt) {
|
|
4518
|
+
if (this.grabbed < 0 || this.grabbed < this.firstFreeIndex()) return;
|
|
4519
|
+
const reach = Math.hypot(this.grabY, this.grabZ - this.tangentZ);
|
|
4520
|
+
const available = this.paid - this.arc[this.grabbed];
|
|
4521
|
+
const over = reach - available;
|
|
4522
|
+
if (over <= 1e-6) return;
|
|
4523
|
+
const feed = Math.min(over * PULL_STIFFNESS, this.usableLength - this.paid);
|
|
4524
|
+
if (feed <= 0) return;
|
|
4525
|
+
this.paid += feed;
|
|
4526
|
+
this.omega = feed / (this.radius * dt);
|
|
4527
|
+
}
|
|
4528
|
+
/**
|
|
4529
|
+
* Verlet with ANISOTROPIC drag. A strip of paper barely notices the air
|
|
4530
|
+
* when it moves along its own length and is stopped almost dead when it
|
|
4531
|
+
* moves broadside, which is the difference between paper that floats down
|
|
4532
|
+
* and a rope that drops. Damping the velocity uniformly — the obvious
|
|
4533
|
+
* thing, and what cloth does — gets neither.
|
|
4534
|
+
*/
|
|
4535
|
+
integrate(dt) {
|
|
4536
|
+
const { gravity, drag } = this.params;
|
|
4537
|
+
const p = this.pos;
|
|
4538
|
+
const dt2 = dt * dt;
|
|
4539
|
+
const firstFree = this.firstFreeIndex();
|
|
4540
|
+
const across = Math.exp(-(0.6 + drag * 11) * dt);
|
|
4541
|
+
const along = Math.exp(-(0.4 + drag * 0.8) * dt);
|
|
4542
|
+
let travel = 0;
|
|
4543
|
+
for (let i = firstFree; i < this.count; i++) {
|
|
4544
|
+
const o = i * 2;
|
|
4545
|
+
if (i === this.grabbed) {
|
|
4546
|
+
p[o] = this.grabY;
|
|
4547
|
+
p[o + 1] = this.grabZ;
|
|
4548
|
+
continue;
|
|
4549
|
+
}
|
|
4550
|
+
const y = p[o];
|
|
4551
|
+
const z25 = p[o + 1];
|
|
4552
|
+
let vy = y - this.prev[o];
|
|
4553
|
+
let vz = z25 - this.prev[o + 1];
|
|
4554
|
+
const a = Math.max(firstFree, i - this.tangentNodes) * 2;
|
|
4555
|
+
const b = Math.min(this.count - 1, i + this.tangentNodes) * 2;
|
|
4556
|
+
let ty = p[b] - p[a];
|
|
4557
|
+
let tz = p[b + 1] - p[a + 1];
|
|
4558
|
+
const len = Math.hypot(ty, tz);
|
|
4559
|
+
if (len > 1e-9) {
|
|
4560
|
+
ty /= len;
|
|
4561
|
+
tz /= len;
|
|
4562
|
+
const vt = vy * ty + vz * tz;
|
|
4563
|
+
const ny = vy - vt * ty;
|
|
4564
|
+
const nz = vz - vt * tz;
|
|
4565
|
+
vy = vt * ty * along + ny * across;
|
|
4566
|
+
vz = vt * tz * along + nz * across;
|
|
4567
|
+
}
|
|
4568
|
+
this.prev[o] = y;
|
|
4569
|
+
this.prev[o + 1] = z25;
|
|
4570
|
+
const noise = Math.sin(this.time * 2.3 + i * 1.7) * 6e-5;
|
|
4571
|
+
p[o] = y + vy - gravity * 3.2 * dt2;
|
|
4572
|
+
p[o + 1] = z25 + vz + noise;
|
|
4573
|
+
travel = Math.max(travel, vy * vy + vz * vz);
|
|
4574
|
+
}
|
|
4575
|
+
if (this.omega === 0 && this.grabbed < 0) {
|
|
4576
|
+
if (travel < SLEEP_EPSILON2) {
|
|
4577
|
+
if (++this.stillFrames > SLEEP_FRAMES2) this.asleep = true;
|
|
4578
|
+
} else this.stillFrames = 0;
|
|
4579
|
+
} else this.stillFrames = 0;
|
|
4580
|
+
}
|
|
4581
|
+
/** Wound nodes are the roll's, not the solver's: rewrite them every pass. */
|
|
4582
|
+
pinWound(firstFree) {
|
|
4583
|
+
for (let i = 0; i < firstFree; i++) {
|
|
4584
|
+
this.spiralPoint(this.arc[i] - this.paid, i * 2, this.pos);
|
|
4585
|
+
}
|
|
4586
|
+
const tz = this.tangentZ;
|
|
4587
|
+
for (let i = firstFree; i < Math.min(this.count, firstFree + 2); i++) {
|
|
4588
|
+
if (i === this.grabbed) continue;
|
|
4589
|
+
const o = i * 2;
|
|
4590
|
+
this.pos[o + 1] = this.pos[o + 1] + (tz - this.pos[o + 1]) * 0.5;
|
|
4591
|
+
}
|
|
4592
|
+
if (this.grabbed >= firstFree) {
|
|
4593
|
+
this.pos[this.grabbed * 2] = this.grabY;
|
|
4594
|
+
this.pos[this.grabbed * 2 + 1] = this.grabZ;
|
|
4595
|
+
}
|
|
4596
|
+
}
|
|
4597
|
+
/** Paper does not stretch. Distance constraints run at full strength. */
|
|
4598
|
+
solveDistance(firstFree) {
|
|
4599
|
+
const p = this.pos;
|
|
4600
|
+
const rest = this.segment;
|
|
4601
|
+
for (let i = Math.max(0, firstFree - 1); i < this.count - 1; i++) {
|
|
4602
|
+
const a = i * 2;
|
|
4603
|
+
const b = (i + 1) * 2;
|
|
4604
|
+
const dy = p[b] - p[a];
|
|
4605
|
+
const dz = p[b + 1] - p[a + 1];
|
|
4606
|
+
const dist = Math.hypot(dy, dz);
|
|
4607
|
+
if (dist < 1e-9) continue;
|
|
4608
|
+
const diff = (dist - rest) / dist * 0.5;
|
|
4609
|
+
const aFixed = i < firstFree || i === this.grabbed;
|
|
4610
|
+
const bFixed = i + 1 === this.grabbed;
|
|
4611
|
+
if (aFixed && bFixed) continue;
|
|
4612
|
+
const aw = aFixed ? 0 : bFixed ? 2 : 1;
|
|
4613
|
+
const bw = bFixed ? 0 : aFixed ? 2 : 1;
|
|
4614
|
+
p[a] = p[a] + dy * diff * aw;
|
|
4615
|
+
p[a + 1] = p[a + 1] + dz * diff * aw;
|
|
4616
|
+
p[b] = p[b] - dy * diff * bw;
|
|
4617
|
+
p[b + 1] = p[b + 1] - dz * diff * bw;
|
|
4618
|
+
}
|
|
4619
|
+
}
|
|
4620
|
+
/**
|
|
4621
|
+
* The bend constraint, and the reason this is not a rope preset.
|
|
4622
|
+
*
|
|
4623
|
+
* Every joint wants to be straight. A joint AT A PERFORATION wants it far
|
|
4624
|
+
* less, and may want a slight fold instead — those are the hinges the pile
|
|
4625
|
+
* folds at, and the alternating sign is what turns a heap into an
|
|
4626
|
+
* accordion. Uniform stiffness gives a coil; no stiffness gives a rope.
|
|
4627
|
+
*/
|
|
4628
|
+
solveBend(firstFree) {
|
|
4629
|
+
const p = this.pos;
|
|
4630
|
+
const { stiffness, crease } = this.params;
|
|
4631
|
+
const start = Math.max(firstFree + 1, 1);
|
|
4632
|
+
for (let i = start; i < this.count - 1; i++) {
|
|
4633
|
+
const hinge = this.perforated[i];
|
|
4634
|
+
const k = (hinge !== 0 ? stiffness * 0.06 : stiffness) * 0.45 * this.bendScale;
|
|
4635
|
+
if (k <= 0) continue;
|
|
4636
|
+
const a = (i - 1) * 2;
|
|
4637
|
+
const m = i * 2;
|
|
4638
|
+
const b = (i + 1) * 2;
|
|
4639
|
+
let ty = (p[a] + p[b]) * 0.5;
|
|
4640
|
+
let tz = (p[a + 1] + p[b + 1]) * 0.5;
|
|
4641
|
+
if (hinge !== 0 && crease > 0) {
|
|
4642
|
+
let cy = p[b] - p[a];
|
|
4643
|
+
let cz = p[b + 1] - p[a + 1];
|
|
4644
|
+
const len = Math.hypot(cy, cz);
|
|
4645
|
+
if (len > 1e-9) {
|
|
4646
|
+
cy /= len;
|
|
4647
|
+
cz /= len;
|
|
4648
|
+
const bow = crease * this.segment * 0.35 * hinge;
|
|
4649
|
+
ty += -cz * bow;
|
|
4650
|
+
tz += cy * bow;
|
|
4651
|
+
}
|
|
4652
|
+
}
|
|
4653
|
+
const dy = (ty - p[m]) * k;
|
|
4654
|
+
const dz = (tz - p[m + 1]) * k;
|
|
4655
|
+
if (i !== this.grabbed) {
|
|
4656
|
+
p[m] = p[m] + dy;
|
|
4657
|
+
p[m + 1] = p[m + 1] + dz;
|
|
4658
|
+
}
|
|
4659
|
+
if (i - 1 >= firstFree && i - 1 !== this.grabbed) {
|
|
4660
|
+
p[a] = p[a] - dy * 0.5;
|
|
4661
|
+
p[a + 1] = p[a + 1] - dz * 0.5;
|
|
4662
|
+
}
|
|
4663
|
+
if (i + 1 !== this.grabbed) {
|
|
4664
|
+
p[b] = p[b] - dy * 0.5;
|
|
4665
|
+
p[b + 1] = p[b + 1] - dz * 0.5;
|
|
4666
|
+
}
|
|
4667
|
+
}
|
|
4668
|
+
}
|
|
4669
|
+
/**
|
|
4670
|
+
* Folds stack on each other instead of passing through. Without this the
|
|
4671
|
+
* pile collapses into a single flat line on the floor and there is nothing
|
|
4672
|
+
* to look at, which is the entire reason the preset exists.
|
|
4673
|
+
*
|
|
4674
|
+
* **Segment against segment, not node against node.** The obvious version
|
|
4675
|
+
* puts a sphere on every node and pushes overlapping pairs apart, and it
|
|
4676
|
+
* leaks: paper is thinner than the chain is finely cut — the layer gap here
|
|
4677
|
+
* is 0.027 against a node spacing of 0.037 — so those spheres do not touch
|
|
4678
|
+
* each other, and the chain is a string of beads with gaps between them
|
|
4679
|
+
* rather than a continuous tube. Another fold threads straight through a
|
|
4680
|
+
* gap, and once it is through, the point test pushes it out the far side
|
|
4681
|
+
* instead of back. It shows up exactly as a sheet edge buried in a surface.
|
|
4682
|
+
*
|
|
4683
|
+
* Testing the SEGMENTS closes the gaps, because a segment is the whole
|
|
4684
|
+
* length of paper between two nodes and not just its ends. The cost is a
|
|
4685
|
+
* closest-approach solve per candidate pair instead of a subtraction, which
|
|
4686
|
+
* is why the hash cell is sized to `segment + d`: a pair that can possibly
|
|
4687
|
+
* touch has its midpoints inside that, so a 3×3 neighbourhood is a complete
|
|
4688
|
+
* search rather than a hopeful one.
|
|
4689
|
+
*
|
|
4690
|
+
* Spatial hash over preallocated arrays, refilled by counting sort — no
|
|
4691
|
+
* allocation, and it does not degrade when the whole strip lands in one
|
|
4692
|
+
* cell, which is exactly what a pile IS.
|
|
4693
|
+
*/
|
|
4694
|
+
solveSelfCollision(firstFree) {
|
|
4695
|
+
this.freeFrom = firstFree;
|
|
4696
|
+
const p = this.pos;
|
|
4697
|
+
const d = Math.max(layerThickness2(this.params.tightness), 1e-4);
|
|
4698
|
+
const n = this.count;
|
|
4699
|
+
const last = n - 2;
|
|
4700
|
+
if (last < firstFree + 2) return;
|
|
4701
|
+
const cell = this.segment + d;
|
|
4702
|
+
const inv = 1 / cell;
|
|
4703
|
+
const table = this.tableSize;
|
|
4704
|
+
const mask = table - 1;
|
|
4705
|
+
const starts = this.bucketStart;
|
|
4706
|
+
const items = this.bucketItems;
|
|
4707
|
+
const placed = this.bucketFill;
|
|
4708
|
+
starts.fill(0);
|
|
4709
|
+
placed.fill(0);
|
|
4710
|
+
for (let i = firstFree; i <= last; i++) {
|
|
4711
|
+
const my = (p[i * 2] + p[(i + 1) * 2]) * 0.5;
|
|
4712
|
+
const mz = (p[i * 2 + 1] + p[(i + 1) * 2 + 1]) * 0.5;
|
|
4713
|
+
const h = (Math.floor(my * inv) * 92837111 ^ Math.floor(mz * inv) * 689287499) >>> 0 & mask;
|
|
4714
|
+
this.cellOf[i] = h;
|
|
4715
|
+
starts[h + 1]++;
|
|
4716
|
+
}
|
|
4717
|
+
for (let c = 0; c < table; c++) starts[c + 1] += starts[c];
|
|
4718
|
+
for (let i = firstFree; i <= last; i++) {
|
|
4719
|
+
const h = this.cellOf[i];
|
|
4720
|
+
items[starts[h] + placed[h]++] = i;
|
|
4721
|
+
}
|
|
4722
|
+
for (let i = firstFree; i <= last; i++) {
|
|
4723
|
+
const my = (p[i * 2] + p[(i + 1) * 2]) * 0.5;
|
|
4724
|
+
const mz = (p[i * 2 + 1] + p[(i + 1) * 2 + 1]) * 0.5;
|
|
4725
|
+
const cy = Math.floor(my * inv);
|
|
4726
|
+
const cz = Math.floor(mz * inv);
|
|
4727
|
+
for (let oy = -1; oy <= 1; oy++) {
|
|
4728
|
+
for (let oz = -1; oz <= 1; oz++) {
|
|
4729
|
+
const h = ((cy + oy) * 92837111 ^ (cz + oz) * 689287499) >>> 0 & mask;
|
|
4730
|
+
const end = starts[h + 1];
|
|
4731
|
+
for (let e = starts[h]; e < end; e++) {
|
|
4732
|
+
const j = items[e];
|
|
4733
|
+
if (j >= i + 2) this.separate(i, j, d);
|
|
4734
|
+
}
|
|
4735
|
+
}
|
|
4736
|
+
}
|
|
4737
|
+
}
|
|
4738
|
+
}
|
|
4739
|
+
/**
|
|
4740
|
+
* Push segments `(i, i+1)` and `(j, j+1)` apart to `d`, if they are closer.
|
|
4741
|
+
*
|
|
4742
|
+
* Closest approach between two 2D segments, then the correction shared over
|
|
4743
|
+
* the four endpoints by how near each is to the touching point — so a fold
|
|
4744
|
+
* caught at its middle moves bodily, and one grazed at its tip barely
|
|
4745
|
+
* pivots. Wound and held nodes are the roll's and the hand's, so they take
|
|
4746
|
+
* none of it and their partner takes the lot.
|
|
4747
|
+
*/
|
|
4748
|
+
separate(i, j, d) {
|
|
4749
|
+
const p = this.pos;
|
|
4750
|
+
const a = i * 2;
|
|
4751
|
+
const b = (i + 1) * 2;
|
|
4752
|
+
const c = j * 2;
|
|
4753
|
+
const e = (j + 1) * 2;
|
|
4754
|
+
const uy = p[b] - p[a];
|
|
4755
|
+
const uz = p[b + 1] - p[a + 1];
|
|
4756
|
+
const vy = p[e] - p[c];
|
|
4757
|
+
const vz = p[e + 1] - p[c + 1];
|
|
4758
|
+
const wy = p[a] - p[c];
|
|
4759
|
+
const wz = p[a + 1] - p[c + 1];
|
|
4760
|
+
const uu = uy * uy + uz * uz;
|
|
4761
|
+
const vv = vy * vy + vz * vz;
|
|
4762
|
+
if (uu < 1e-12 || vv < 1e-12) return;
|
|
4763
|
+
const uv = uy * vy + uz * vz;
|
|
4764
|
+
const uw = uy * wy + uz * wz;
|
|
4765
|
+
const vw = vy * wy + vz * wz;
|
|
4766
|
+
const denom = uu * vv - uv * uv;
|
|
4767
|
+
let s = denom > 1e-12 ? (uv * vw - vv * uw) / denom : 0;
|
|
4768
|
+
s = s < 0 ? 0 : s > 1 ? 1 : s;
|
|
4769
|
+
let t = (uv * s + vw) / vv;
|
|
4770
|
+
if (t < 0) {
|
|
4771
|
+
t = 0;
|
|
4772
|
+
s = -uw / uu;
|
|
4773
|
+
} else if (t > 1) {
|
|
4774
|
+
t = 1;
|
|
4775
|
+
s = (uv - uw) / uu;
|
|
4776
|
+
}
|
|
4777
|
+
s = s < 0 ? 0 : s > 1 ? 1 : s;
|
|
4778
|
+
let ny = p[c] + vy * t - (p[a] + uy * s);
|
|
4779
|
+
let nz = p[c + 1] + vz * t - (p[a + 1] + uz * s);
|
|
4780
|
+
let dist = Math.hypot(ny, nz);
|
|
4781
|
+
if (dist >= d) return;
|
|
4782
|
+
if (dist < 1e-9) {
|
|
4783
|
+
ny = -uz;
|
|
4784
|
+
nz = uy;
|
|
4785
|
+
dist = Math.hypot(ny, nz);
|
|
4786
|
+
if (dist < 1e-12) return;
|
|
4787
|
+
ny = -ny;
|
|
4788
|
+
nz = -nz;
|
|
4789
|
+
}
|
|
4790
|
+
const push = (d - dist) / dist * COLLISION_RELAXATION;
|
|
4791
|
+
const gy = ny * push;
|
|
4792
|
+
const gz = nz * push;
|
|
4793
|
+
const ki = 0.5 / ((1 - s) * (1 - s) + s * s);
|
|
4794
|
+
const kj = 0.5 / ((1 - t) * (1 - t) + t * t);
|
|
4795
|
+
this.nudge(i, -gy * (1 - s) * ki, -gz * (1 - s) * ki);
|
|
4796
|
+
this.nudge(i + 1, -gy * s * ki, -gz * s * ki);
|
|
4797
|
+
this.nudge(j, gy * (1 - t) * kj, gz * (1 - t) * kj);
|
|
4798
|
+
this.nudge(j + 1, gy * t * kj, gz * t * kj);
|
|
4799
|
+
}
|
|
4800
|
+
/** First free node for the substep in flight — `nudge` runs in the innermost
|
|
4801
|
+
* collision loop and must not recompute it per call. */
|
|
4802
|
+
freeFrom = 0;
|
|
4803
|
+
/** Move a node, unless the roll or the hand owns it. */
|
|
4804
|
+
nudge(i, dy, dz) {
|
|
4805
|
+
if (i === this.grabbed || i < this.freeFrom) return;
|
|
4806
|
+
this.pos[i * 2] = this.pos[i * 2] + dy;
|
|
4807
|
+
this.pos[i * 2 + 1] = this.pos[i * 2 + 1] + dz;
|
|
4808
|
+
}
|
|
4809
|
+
/** Restitution 0, friction high. Paper lands and stays; it does not slide. */
|
|
4810
|
+
solveFloor(firstFree) {
|
|
4811
|
+
const p = this.pos;
|
|
4812
|
+
const floor = -this.params.floor;
|
|
4813
|
+
for (let i = firstFree; i < this.count; i++) {
|
|
4814
|
+
const o = i * 2;
|
|
4815
|
+
if (i === this.grabbed || p[o] >= floor) continue;
|
|
4816
|
+
p[o] = floor;
|
|
4817
|
+
this.prev[o] = floor;
|
|
4818
|
+
this.prev[o + 1] = this.prev[o + 1] + (p[o + 1] - this.prev[o + 1]) * 0.92;
|
|
4819
|
+
}
|
|
4820
|
+
}
|
|
4821
|
+
/**
|
|
4822
|
+
* Write the chain into a 2×N quad strip, in PlaneGeometry vertex order:
|
|
4823
|
+
* row-major, top row first, x left→right. Row `i` is node `i`, so the
|
|
4824
|
+
* content texture runs down the strip and folds with it.
|
|
4825
|
+
*/
|
|
4826
|
+
writeInto(out) {
|
|
4827
|
+
const lift = this.centreOffset;
|
|
4828
|
+
const shift = this.centreShift;
|
|
4829
|
+
for (let i = 0; i < this.count; i++) {
|
|
4830
|
+
const o = i * 2;
|
|
4831
|
+
const y = this.pos[o] + lift;
|
|
4832
|
+
const z25 = this.pos[o + 1] + shift;
|
|
4833
|
+
const v = i * 6;
|
|
4834
|
+
out[v] = -this.halfWidth;
|
|
4835
|
+
out[v + 1] = y;
|
|
4836
|
+
out[v + 2] = z25;
|
|
4837
|
+
out[v + 3] = this.halfWidth;
|
|
4838
|
+
out[v + 4] = y;
|
|
4839
|
+
out[v + 5] = z25;
|
|
4840
|
+
}
|
|
4841
|
+
}
|
|
4842
|
+
};
|
|
4843
|
+
|
|
3261
4844
|
// src/surface/PaperMaterial.tsx
|
|
3262
4845
|
var THREE6 = __toESM(require("three"), 1);
|
|
3263
4846
|
var import_react4 = require("react");
|
|
@@ -3553,6 +5136,7 @@ function translucencyUniforms(translucency, lighting) {
|
|
|
3553
5136
|
}
|
|
3554
5137
|
|
|
3555
5138
|
// src/surface/compose.ts
|
|
5139
|
+
var SHADED_CREASE = 0.35;
|
|
3556
5140
|
var VERTEX = (
|
|
3557
5141
|
/* glsl */
|
|
3558
5142
|
`
|
|
@@ -3569,6 +5153,89 @@ var HELPERS = (
|
|
|
3569
5153
|
`
|
|
3570
5154
|
varying vec2 vPaperUv;
|
|
3571
5155
|
uniform float uBackDarken;
|
|
5156
|
+
uniform vec2 uSheetSize;
|
|
5157
|
+
|
|
5158
|
+
/**
|
|
5159
|
+
* Where this fragment is on the sheet, in the sheet's OWN local space \u2014
|
|
5160
|
+
* the same coordinates the deformers displace, centred on the sheet.
|
|
5161
|
+
*
|
|
5162
|
+
* Every effect below measures in these rather than in UV, and the difference
|
|
5163
|
+
* is not cosmetic. UV divides the sheet's aspect out, so a 1.2 x 1.5 sheet is
|
|
5164
|
+
* a unit square as far as the shader is concerned: fibre drawn round comes out
|
|
5165
|
+
* stretched, a tear bites deeper into the short edge than the long one, and a
|
|
5166
|
+
* crease line scored at 45 degrees renders at 51. Worse, all three change when
|
|
5167
|
+
* the sheet is RESIZED, which makes the paper's own material a function of how
|
|
5168
|
+
* big the piece is. Grain is a property of the stock and a crease is a broken
|
|
5169
|
+
* fibre; neither knows how large a sheet it was cut from.
|
|
5170
|
+
*/
|
|
5171
|
+
vec2 plLocal() {
|
|
5172
|
+
return (vPaperUv - 0.5) * uSheetSize;
|
|
5173
|
+
}
|
|
5174
|
+
|
|
5175
|
+
/**
|
|
5176
|
+
* The paper's relief, in world units above the sheet the mesh describes.
|
|
5177
|
+
*
|
|
5178
|
+
* Accumulated by whichever effects have a shape as well as a colour, and
|
|
5179
|
+
* spent once at the end of main by {@link plPerturb}. One shared field rather
|
|
5180
|
+
* than a perturbation per effect, because two effects that both tilt the
|
|
5181
|
+
* surface tilt it TOGETHER \u2014 a crease across a grained sheet is one surface,
|
|
5182
|
+
* not a crease lit on top of a grain lit on top of the paper.
|
|
5183
|
+
*/
|
|
5184
|
+
float plHeight;
|
|
5185
|
+
|
|
5186
|
+
/**
|
|
5187
|
+
* The relief, turned into the normal the lighting actually runs on.
|
|
5188
|
+
*
|
|
5189
|
+
* This is the change that makes the surface effects respond to light at all.
|
|
5190
|
+
* They used to be painted: a crease multiplied a grey band into the albedo
|
|
5191
|
+
* and added a fixed white sheen beside it, so the mark looked identical from
|
|
5192
|
+
* every angle and under every rig, and turning the sheet under the key light
|
|
5193
|
+
* did nothing to it. Real creased paper is two facets meeting at a line \u2014
|
|
5194
|
+
* swing it and the crease flips from a dark line to a bright one. Only a
|
|
5195
|
+
* normal can do that, so now the effects describe a HEIGHT and the standard
|
|
5196
|
+
* material lights it.
|
|
5197
|
+
*
|
|
5198
|
+
* The maths is Mikkelsen's surface-gradient bump, which is what three's own
|
|
5199
|
+
* perturbNormalArb implements, with one deliberate difference: three
|
|
5200
|
+
* normalises the screen-space position derivatives, which makes a bump map
|
|
5201
|
+
* look the same at any scale and is the right call for a texture. Ours is a
|
|
5202
|
+
* real depth in world units \u2014 a crease is as deep as it is however close you
|
|
5203
|
+
* stand \u2014 so the raw derivatives stay, and the ratio between them and the
|
|
5204
|
+
* height's is a true surface slope.
|
|
5205
|
+
*
|
|
5206
|
+
* Analytic height plus screen derivatives also anti-aliases itself for free:
|
|
5207
|
+
* as a crease shrinks below a pixel the derivative flattens and the mark
|
|
5208
|
+
* fades, rather than crawling.
|
|
5209
|
+
*/
|
|
5210
|
+
vec3 plPerturb(vec3 n, float height) {
|
|
5211
|
+
vec2 dH = vec2(dFdx(height), dFdy(height));
|
|
5212
|
+
if (dH.x == 0.0 && dH.y == 0.0) return n;
|
|
5213
|
+
// View-space position: the varying is its negation, by three's convention.
|
|
5214
|
+
vec3 sigmaX = dFdx(-vViewPosition);
|
|
5215
|
+
vec3 sigmaY = dFdy(-vViewPosition);
|
|
5216
|
+
vec3 r1 = cross(sigmaY, n);
|
|
5217
|
+
vec3 r2 = cross(n, sigmaX);
|
|
5218
|
+
float det = dot(sigmaX, r1) * (gl_FrontFacing ? 1.0 : -1.0);
|
|
5219
|
+
if (abs(det) < 1e-12) return n;
|
|
5220
|
+
vec3 grad = sign(det) * (dH.x * r1 + dH.y * r2);
|
|
5221
|
+
return normalize(abs(det) * n - grad);
|
|
5222
|
+
}
|
|
5223
|
+
|
|
5224
|
+
/**
|
|
5225
|
+
* A gaussian bell of unit width, pre-filtered against this fragment's own
|
|
5226
|
+
* footprint.
|
|
5227
|
+
*
|
|
5228
|
+
* Convolving a gaussian with the pixel broadens it and flattens it by the
|
|
5229
|
+
* same factor, which conserves the integral: a crease seen from across the
|
|
5230
|
+
* room dims instead of breaking into a dotted line. s is the distance
|
|
5231
|
+
* across the feature in units of its own width, so a caller only ever has to
|
|
5232
|
+
* decide how wide the thing is.
|
|
5233
|
+
*/
|
|
5234
|
+
float plBell(float s) {
|
|
5235
|
+
float px = fwidth(s);
|
|
5236
|
+
float widen = sqrt(1.0 + px * px);
|
|
5237
|
+
return exp(-(s * s) / (widen * widen)) / widen;
|
|
5238
|
+
}
|
|
3572
5239
|
|
|
3573
5240
|
float plHash(vec2 p) {
|
|
3574
5241
|
return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453123);
|
|
@@ -3609,12 +5276,54 @@ var GRAIN_CHUNK = (
|
|
|
3609
5276
|
uniform float uGrainAmount;
|
|
3610
5277
|
uniform float uGrainBanding;
|
|
3611
5278
|
|
|
5279
|
+
/**
|
|
5280
|
+
* Fibre density, per world unit.
|
|
5281
|
+
*
|
|
5282
|
+
* Per WORLD UNIT and not per UV, which is the whole fix: the fibre in a sheet
|
|
5283
|
+
* is the stock's, and it does not get coarser because someone cut a bigger
|
|
5284
|
+
* piece or stretch oval because the piece is taller than it is wide.
|
|
5285
|
+
*
|
|
5286
|
+
* The number is carried over from when it was a UV frequency, so a sheet one
|
|
5287
|
+
* world unit wide is unchanged across its width. Its height is not, and that
|
|
5288
|
+
* is the point: a 1.4-tall sheet used to fit the same 240 cycles into a
|
|
5289
|
+
* longer span and its fibre ran visibly coarser the other way.
|
|
5290
|
+
*/
|
|
5291
|
+
const float PL_FIBRE = 240.0;
|
|
5292
|
+
|
|
5293
|
+
/**
|
|
5294
|
+
* The coarser structure underneath it \u2014 paper's tooth, the part that has a
|
|
5295
|
+
* SHAPE and not just a colour.
|
|
5296
|
+
*
|
|
5297
|
+
* Separate from the fibre, and much lower, for a reason worth writing down:
|
|
5298
|
+
* the relief is differentiated in screen space, and a field at the fibre's own
|
|
5299
|
+
* frequency is a few pixels per cycle at any sane viewing distance, so its
|
|
5300
|
+
* derivative is noise and the sheet would sparkle. The tooth is safely above
|
|
5301
|
+
* the sampling rate, and it is the scale at which paper actually catches a
|
|
5302
|
+
* raking light anyway.
|
|
5303
|
+
*/
|
|
5304
|
+
const float PL_TOOTH = 70.0;
|
|
5305
|
+
|
|
5306
|
+
/**
|
|
5307
|
+
* How far the tooth stands proud, in world units, at full grain.
|
|
5308
|
+
*
|
|
5309
|
+
* Real paper's surface relief is tens of microns. Against a sheet whose width
|
|
5310
|
+
* is one world unit \u2014 call it A4 \u2014 0.00035 is about 70 microns, and at the
|
|
5311
|
+
* tooth's wavelength that is a surface tilting by four degrees or so. Enough
|
|
5312
|
+
* to break a specular highlight into paper, not enough to look pebbled.
|
|
5313
|
+
*/
|
|
5314
|
+
const float PL_TOOTH_RELIEF = 0.00035;
|
|
5315
|
+
|
|
3612
5316
|
void plGrain(inout vec4 color, inout float rough) {
|
|
3613
|
-
|
|
3614
|
-
float
|
|
5317
|
+
vec2 local = plLocal();
|
|
5318
|
+
float fiber = plFbm(local * PL_FIBRE);
|
|
5319
|
+
float fleck = plNoise(local * (PL_FIBRE * 3.75));
|
|
3615
5320
|
float g = mix(0.5, fiber * 0.75 + fleck * 0.25, uGrainAmount);
|
|
3616
5321
|
color.rgb *= 0.92 + g * 0.16;
|
|
3617
5322
|
rough = clamp(rough + (g - 0.5) * uGrainAmount * 0.35, 0.0, 1.0);
|
|
5323
|
+
// The tooth, handed to the lighting rather than drawn. A single octave: the
|
|
5324
|
+
// relief only needs the scale the eye reads as texture, and the fbm above
|
|
5325
|
+
// is already carrying everything finer as colour.
|
|
5326
|
+
plHeight += (plNoise(local * PL_TOOTH) - 0.5) * PL_TOOTH_RELIEF * uGrainAmount;
|
|
3618
5327
|
// Thermal-printer banding: faint horizontal density stripes.
|
|
3619
5328
|
if (uGrainBanding > 0.0) {
|
|
3620
5329
|
float band = sin(vPaperUv.y * 700.0) * 0.5 + 0.5;
|
|
@@ -3629,18 +5338,33 @@ var DECKLE_CHUNK = (
|
|
|
3629
5338
|
uniform vec4 uDeckleEdges; // top, right, bottom, left
|
|
3630
5339
|
uniform float uDeckleRoughness;
|
|
3631
5340
|
|
|
5341
|
+
/** Gnaw frequency along a torn edge, per world unit \u2014 see {@link plLocal}. */
|
|
5342
|
+
const float PL_DECKLE_GNAW = 26.0;
|
|
5343
|
+
|
|
3632
5344
|
void plDeckle(inout vec4 color) {
|
|
3633
5345
|
// Distance to each selected edge, gnawed by low-frequency noise.
|
|
3634
|
-
|
|
5346
|
+
//
|
|
5347
|
+
// The depth is in world units, taken against the sheet's mean dimension.
|
|
5348
|
+
// Against the MEAN rather than each edge's own span, which is what UV
|
|
5349
|
+
// amounted to: one roughness used to bite a third deeper into the short
|
|
5350
|
+
// edges of a 1 x 1.4 sheet than the long ones, for no reason anybody chose.
|
|
5351
|
+
//
|
|
5352
|
+
// Still proportional to the sheet rather than absolute, which is a decision
|
|
5353
|
+
// and not an oversight. A real deckle is a fibre length and would be the
|
|
5354
|
+
// same depth on any size of sheet; roughness is a 0..1 knob someone types,
|
|
5355
|
+
// and an absolute one would vanish on a poster and swallow a stamp.
|
|
5356
|
+
float depth = (0.012 + uDeckleRoughness * 0.05) * (uSheetSize.x + uSheetSize.y) * 0.5;
|
|
3635
5357
|
float tear = 1.0;
|
|
3636
5358
|
float fiberBand = 0.0;
|
|
3637
5359
|
vec4 dists = vec4(1.0 - vPaperUv.y, 1.0 - vPaperUv.x, vPaperUv.y, vPaperUv.x);
|
|
3638
5360
|
vec4 alongs = vec4(vPaperUv.x, vPaperUv.y, vPaperUv.x, vPaperUv.y);
|
|
5361
|
+
vec4 distScale = vec4(uSheetSize.y, uSheetSize.x, uSheetSize.y, uSheetSize.x);
|
|
5362
|
+
vec4 alongScale = vec4(uSheetSize.x, uSheetSize.y, uSheetSize.x, uSheetSize.y);
|
|
3639
5363
|
for (int e = 0; e < 4; e++) {
|
|
3640
5364
|
if (uDeckleEdges[e] < 0.5) continue;
|
|
3641
|
-
float n = plFbm(vec2(alongs[e] *
|
|
5365
|
+
float n = plFbm(vec2(alongs[e] * alongScale[e] * PL_DECKLE_GNAW, float(e) * 7.31)) - 0.5;
|
|
3642
5366
|
float boundary = depth * (0.55 + n * 1.6);
|
|
3643
|
-
float d = dists[e] - boundary;
|
|
5367
|
+
float d = dists[e] * distScale[e] - boundary;
|
|
3644
5368
|
tear = min(tear, step(0.0, d));
|
|
3645
5369
|
// Lightened fiber band just inside the tear.
|
|
3646
5370
|
fiberBand = max(fiberBand, smoothstep(depth * 1.4, 0.0, d) * step(0.0, d));
|
|
@@ -3653,23 +5377,55 @@ void plDeckle(inout vec4 color) {
|
|
|
3653
5377
|
var CREASE_CHUNK = (
|
|
3654
5378
|
/* glsl */
|
|
3655
5379
|
`
|
|
3656
|
-
uniform float
|
|
3657
|
-
uniform float
|
|
3658
|
-
uniform float
|
|
5380
|
+
uniform float uCreaseAngles[4];
|
|
5381
|
+
uniform float uCreaseStrengths[4];
|
|
5382
|
+
uniform float uCreaseOffsets[4];
|
|
5383
|
+
uniform float uCreaseWidth;
|
|
3659
5384
|
uniform int uCreaseCount;
|
|
3660
5385
|
|
|
5386
|
+
/**
|
|
5387
|
+
* Peak tilt of a crease's own facets, as a slope.
|
|
5388
|
+
*
|
|
5389
|
+
* A gaussian groove of amplitude A and width w reaches a maximum slope of
|
|
5390
|
+
* about 0.86 A/w, so an amplitude of 0.55 w peaks near 25 degrees \u2014 steep
|
|
5391
|
+
* enough that turning the sheet visibly flips the line from dark to bright,
|
|
5392
|
+
* shallow enough that it never reads as a fold in its own right. Held as a
|
|
5393
|
+
* SLOPE rather than a depth because that is the quantity the lighting
|
|
5394
|
+
* responds to, and the only one that stays honest when the width changes.
|
|
5395
|
+
*/
|
|
5396
|
+
const float PL_CREASE_TILT = 0.55;
|
|
5397
|
+
|
|
5398
|
+
/** How much grime a crease traps, at full strength. */
|
|
5399
|
+
const float PL_CREASE_SOIL = 0.1;
|
|
5400
|
+
|
|
3661
5401
|
void plCrease(inout vec4 color, inout float rough) {
|
|
3662
|
-
vec2
|
|
3663
|
-
// Coordinate across the crease lines (0..1 over the sheet).
|
|
3664
|
-
float t = dot(vPaperUv - 0.5, vec2(-dir.y, dir.x)) + 0.5;
|
|
5402
|
+
vec2 p = plLocal();
|
|
3665
5403
|
for (int i = 0; i < 4; i++) {
|
|
3666
5404
|
if (i >= uCreaseCount) break;
|
|
3667
|
-
|
|
3668
|
-
|
|
3669
|
-
|
|
3670
|
-
|
|
3671
|
-
|
|
3672
|
-
|
|
5405
|
+
vec2 dir = vec2(cos(uCreaseAngles[i]), sin(uCreaseAngles[i]));
|
|
5406
|
+
// The identical measurement the fold deformer displaces by: signed
|
|
5407
|
+
// distance across the line, in the sheet's own space. Shading and
|
|
5408
|
+
// geometry cannot place a crease differently when the number they place
|
|
5409
|
+
// it by is the same number.
|
|
5410
|
+
float s = (dot(p, dir) - uCreaseOffsets[i]) / uCreaseWidth;
|
|
5411
|
+
float strength = uCreaseStrengths[i];
|
|
5412
|
+
float bell = plBell(s);
|
|
5413
|
+
|
|
5414
|
+
// The relief. This is the fine burnished line where the fibres broke, and
|
|
5415
|
+
// it is deliberately narrower than the hinge the fold deformer bends
|
|
5416
|
+
// over: the mesh carries the wide bend, the shader carries the crease
|
|
5417
|
+
// inside it, and the two add up instead of competing. Signed, so a
|
|
5418
|
+
// mountain stands proud and a valley cuts in \u2014 the same crease read from
|
|
5419
|
+
// the other side of the sheet is the other one.
|
|
5420
|
+
plHeight += strength * uCreaseWidth * PL_CREASE_TILT * bell;
|
|
5421
|
+
|
|
5422
|
+
// What is left for the albedo once the lighting is doing the work: a
|
|
5423
|
+
// crease collects dirt and its broken fibres scatter wider. The grey band
|
|
5424
|
+
// and the painted-on sheen that used to live here were standing in for a
|
|
5425
|
+
// normal, and there is one now.
|
|
5426
|
+
float mark = bell * abs(strength);
|
|
5427
|
+
color.rgb *= 1.0 - mark * PL_CREASE_SOIL;
|
|
5428
|
+
rough = clamp(rough + mark * 0.3, 0.0, 1.0);
|
|
3673
5429
|
}
|
|
3674
5430
|
}
|
|
3675
5431
|
`
|
|
@@ -3681,7 +5437,6 @@ uniform vec4 uPerfEdges; // top, right, bottom, left enabled
|
|
|
3681
5437
|
uniform vec4 uPerfTorn; // 1 = ripped-through profile, 0 = clean punches
|
|
3682
5438
|
uniform float uPerfRadius; // world units
|
|
3683
5439
|
uniform float uPerfSpacing;
|
|
3684
|
-
uniform vec2 uSheetSize;
|
|
3685
5440
|
|
|
3686
5441
|
void plPerforation(inout vec4 color) {
|
|
3687
5442
|
// Per-edge distance/along coordinates, converted from UV to world units so
|
|
@@ -3732,11 +5487,10 @@ void plAging(inout vec4 color) {
|
|
|
3732
5487
|
}
|
|
3733
5488
|
`
|
|
3734
5489
|
);
|
|
3735
|
-
function composeSurface(surface, stock, thickness, maps = { hasFrontMap: false, hasBackMap: false }, sheet2 = { width: 1, height: 1.4 }, lighting = "studio") {
|
|
5490
|
+
function composeSurface(surface, stock, thickness, maps = { hasFrontMap: false, hasBackMap: false }, sheet2 = { width: 1, height: 1.4 }, lighting = "studio", creases = resolveCreases(surface, [], sheet2)) {
|
|
3736
5491
|
const grain = surface.grain ?? stock.defaultSurface.grain;
|
|
3737
5492
|
const aging = surface.aging ?? stock.defaultSurface.aging;
|
|
3738
5493
|
const deckle = surface.deckle;
|
|
3739
|
-
const creases = surface.creaseLines;
|
|
3740
5494
|
const perforation = surface.perforation;
|
|
3741
5495
|
const banding = stock.banding;
|
|
3742
5496
|
const showThrough = stock.adhesive ? 0 : surface.showThrough ?? stock.showThrough;
|
|
@@ -3749,6 +5503,9 @@ function composeSurface(surface, stock, thickness, maps = { hasFrontMap: false,
|
|
|
3749
5503
|
value: stock.adhesive ? 1 : 1 - Math.min(0.45, 0.12 + thickness * 0.9) * stock.opacity
|
|
3750
5504
|
},
|
|
3751
5505
|
uStockColor: { value: new THREE5.Color(stock.color) },
|
|
5506
|
+
// Always present, not just when something asks for it: every effect that
|
|
5507
|
+
// measures anything measures in the sheet's own space now — see plLocal.
|
|
5508
|
+
uSheetSize: { value: new THREE5.Vector2(sheet2.width, sheet2.height) },
|
|
3752
5509
|
uOpacity: { value: stock.opacity },
|
|
3753
5510
|
uShowThrough: { value: showThrough },
|
|
3754
5511
|
// Always compiled in: the shader early-outs at zero translucency, which
|
|
@@ -3781,21 +5538,22 @@ function composeSurface(surface, stock, thickness, maps = { hasFrontMap: false,
|
|
|
3781
5538
|
};
|
|
3782
5539
|
uniforms.uPerfRadius = { value: perforation.holeRadius };
|
|
3783
5540
|
uniforms.uPerfSpacing = { value: perforation.spacing };
|
|
3784
|
-
uniforms.uSheetSize = { value: new THREE5.Vector2(sheet2.width, sheet2.height) };
|
|
3785
5541
|
}
|
|
3786
|
-
if (creases) {
|
|
5542
|
+
if (creases.length > 0) {
|
|
3787
5543
|
chunks.push(CREASE_CHUNK);
|
|
3788
5544
|
calls.push("plCrease(csm_DiffuseColor, csm_Roughness);");
|
|
3789
|
-
uniforms.
|
|
3790
|
-
uniforms.
|
|
3791
|
-
uniforms.
|
|
3792
|
-
uniforms.
|
|
5545
|
+
uniforms.uCreaseAngles = { value: pad(creases.map((c) => c.angle * Math.PI / 180)) };
|
|
5546
|
+
uniforms.uCreaseStrengths = { value: pad(creases.map((c) => c.strength)) };
|
|
5547
|
+
uniforms.uCreaseOffsets = { value: pad(creases.map((c) => c.offset)) };
|
|
5548
|
+
uniforms.uCreaseWidth = { value: CREASE_RADIUS * SHADED_CREASE };
|
|
5549
|
+
uniforms.uCreaseCount = { value: Math.min(creases.length, 4) };
|
|
3793
5550
|
}
|
|
3794
5551
|
if (aging !== void 0) {
|
|
3795
5552
|
chunks.push(AGING_CHUNK);
|
|
3796
5553
|
calls.push("plAging(csm_DiffuseColor);");
|
|
3797
5554
|
uniforms.uAgingAmount = { value: aging };
|
|
3798
5555
|
}
|
|
5556
|
+
const relief = grain !== void 0 || creases.length > 0;
|
|
3799
5557
|
const frontExpr = maps.hasFrontMap ? "texture2D(uFrontMap, vPaperUv).rgb" : "uStockColor";
|
|
3800
5558
|
const backBaseExpr = stock.adhesive ? "vec3(0.965, 0.96, 0.945)" : maps.hasBackMap ? "texture2D(uBackMap, vec2(1.0 - vPaperUv.x, vPaperUv.y)).rgb" : "uStockColor";
|
|
3801
5559
|
const fragmentShader = (
|
|
@@ -3810,6 +5568,7 @@ ${maps.hasBackMap && !stock.adhesive ? "uniform sampler2D uBackMap;" : ""}
|
|
|
3810
5568
|
${TRANSLUCENCY_FRAGMENT}
|
|
3811
5569
|
${chunks.join("\n")}
|
|
3812
5570
|
void main() {
|
|
5571
|
+
plHeight = 0.0;
|
|
3813
5572
|
vec3 front = ${frontExpr};
|
|
3814
5573
|
if (gl_FrontFacing) {
|
|
3815
5574
|
csm_DiffuseColor = vec4(front, uOpacity);
|
|
@@ -3818,6 +5577,7 @@ void main() {
|
|
|
3818
5577
|
csm_DiffuseColor = vec4(backBase * mix(vec3(1.0), front, uShowThrough), uOpacity);
|
|
3819
5578
|
}
|
|
3820
5579
|
${calls.join("\n ")}
|
|
5580
|
+
${relief ? " // The relief every effect above described, spent once \u2014 see plPerturb.\n csm_FragNormal = plPerturb(csm_FragNormal, plHeight);" : ""}
|
|
3821
5581
|
if (!gl_FrontFacing) csm_DiffuseColor.rgb *= uBackDarken;
|
|
3822
5582
|
${stock.adhesive ? "// Adhesive underside: higher specular than the printed face.\n if (!gl_FrontFacing) csm_Roughness = 0.18;" : ""}
|
|
3823
5583
|
// What the key light pushes through the sheet, filtered by the ink on it.
|
|
@@ -3829,7 +5589,7 @@ void main() {
|
|
|
3829
5589
|
structureKey: `${[
|
|
3830
5590
|
grain !== void 0 || banding > 0 ? "g" : "",
|
|
3831
5591
|
deckle ? "d" : "",
|
|
3832
|
-
creases ? "c" : "",
|
|
5592
|
+
creases.length > 0 ? "c" : "",
|
|
3833
5593
|
aging !== void 0 ? "a" : "",
|
|
3834
5594
|
perforation ? "p" : "",
|
|
3835
5595
|
stock.adhesive ? "A" : ""
|
|
@@ -3840,9 +5600,9 @@ void main() {
|
|
|
3840
5600
|
alphaTest: deckle || perforation ? 0.5 : 0
|
|
3841
5601
|
};
|
|
3842
5602
|
}
|
|
3843
|
-
function
|
|
3844
|
-
const out =
|
|
3845
|
-
while (out.length < 4) out.push(
|
|
5603
|
+
function pad(values, fill = 0) {
|
|
5604
|
+
const out = values.slice(0, 4);
|
|
5605
|
+
while (out.length < 4) out.push(fill);
|
|
3846
5606
|
return out;
|
|
3847
5607
|
}
|
|
3848
5608
|
|
|
@@ -3866,7 +5626,8 @@ function PaperMaterial({
|
|
|
3866
5626
|
surface,
|
|
3867
5627
|
thickness,
|
|
3868
5628
|
sheet: sheet2,
|
|
3869
|
-
lighting = "studio"
|
|
5629
|
+
lighting = "studio",
|
|
5630
|
+
creases
|
|
3870
5631
|
}) {
|
|
3871
5632
|
const rig = useLightRig(lighting);
|
|
3872
5633
|
const composed = composeSurface(
|
|
@@ -3878,7 +5639,8 @@ function PaperMaterial({
|
|
|
3878
5639
|
hasBackMap: Boolean(backTexture)
|
|
3879
5640
|
},
|
|
3880
5641
|
sheet2,
|
|
3881
|
-
rig
|
|
5642
|
+
rig,
|
|
5643
|
+
creases ?? resolveCreases(surface, [], sheet2 ?? { width: 1, height: 1.4 })
|
|
3882
5644
|
);
|
|
3883
5645
|
const bound = (0, import_react4.useMemo)(() => composed.uniforms, [composed.structureKey]);
|
|
3884
5646
|
(0, import_react4.useEffect)(() => {
|
|
@@ -4334,6 +6096,7 @@ function configInputs(props) {
|
|
|
4334
6096
|
props.behavior ?? null,
|
|
4335
6097
|
props.deformers ?? null,
|
|
4336
6098
|
props.surface ?? null,
|
|
6099
|
+
props.memory ?? null,
|
|
4337
6100
|
props.scene ?? null,
|
|
4338
6101
|
props.physics ?? null,
|
|
4339
6102
|
props.onTwos ?? null
|
|
@@ -4352,6 +6115,7 @@ function resolveConfig(props) {
|
|
|
4352
6115
|
if (props.behavior) overrides.behavior = props.behavior;
|
|
4353
6116
|
if (props.deformers) overrides.deformers = props.deformers;
|
|
4354
6117
|
if (props.surface) overrides.surface = { ...base.surface, ...props.surface };
|
|
6118
|
+
if (props.memory) overrides.memory = { ...base.memory, ...props.memory };
|
|
4355
6119
|
if (props.scene) overrides.scene = { ...base.scene, ...props.scene };
|
|
4356
6120
|
if (props.physics) overrides.physics = props.physics;
|
|
4357
6121
|
if (props.onTwos !== void 0) overrides.onTwos = props.onTwos;
|
|
@@ -4379,7 +6143,9 @@ var PaperMesh = (0, import_react7.forwardRef)(function PaperMesh2(props, ref) {
|
|
|
4379
6143
|
machineRef.current = machine;
|
|
4380
6144
|
const resolvedRef = (0, import_react7.useRef)(resolved);
|
|
4381
6145
|
resolvedRef.current = resolved;
|
|
4382
|
-
const
|
|
6146
|
+
const simKind = !reduced && typeof config.physics === "object" ? config.physics.type : null;
|
|
6147
|
+
const isCloth = simKind === "cloth";
|
|
6148
|
+
const isStrip = simKind === "strip";
|
|
4383
6149
|
const idle = !reduced && typeof config.physics === "string" && config.physics !== "none" ? getIdlePreset(config.physics) : null;
|
|
4384
6150
|
const meshRef = (0, import_react7.useRef)(null);
|
|
4385
6151
|
const groupRef = (0, import_react7.useRef)(null);
|
|
@@ -4391,19 +6157,33 @@ var PaperMesh = (0, import_react7.forwardRef)(function PaperMesh2(props, ref) {
|
|
|
4391
6157
|
const draggingRef = (0, import_react7.useRef)(null);
|
|
4392
6158
|
const configRef = (0, import_react7.useRef)(config);
|
|
4393
6159
|
configRef.current = config;
|
|
6160
|
+
const baseRotation = (0, import_react7.useMemo)(() => {
|
|
6161
|
+
const [rx, ry, rz] = props.rotation ?? [0, 0, 0];
|
|
6162
|
+
return [rx, ry + config.scene.turn * Math.PI / 180, rz];
|
|
6163
|
+
}, [props.rotation, config.scene.turn]);
|
|
4394
6164
|
const controls = (0, import_fiber.useThree)((s) => s.controls);
|
|
4395
6165
|
const camera = (0, import_fiber.useThree)((s) => s.camera);
|
|
4396
6166
|
const behaviorKey = JSON.stringify(config.behavior ?? null);
|
|
4397
6167
|
const deformersKey = JSON.stringify(config.deformers ?? null);
|
|
4398
6168
|
const sheetKey = JSON.stringify(config.sheet);
|
|
4399
|
-
const physicsKey =
|
|
6169
|
+
const physicsKey = typeof config.physics === "object" ? config.physics.type : config.physics;
|
|
6170
|
+
const memoryKey = config.memory.creases.map((c) => `${c.angle}:${c.offset}`).join("|");
|
|
6171
|
+
const creaseKey = config.memory.creases.map((c) => `${c.angle}:${c.offset}:${c.depth}`).join("|");
|
|
6172
|
+
const creasesRef = (0, import_react7.useRef)(null);
|
|
6173
|
+
creasesRef.current ??= new CreaseTracker(config.memory.creases);
|
|
6174
|
+
const creases = creasesRef.current;
|
|
4400
6175
|
(0, import_react7.useEffect)(() => {
|
|
4401
6176
|
if (!draggingRef.current && !playingRef.current) overridesRef.current = {};
|
|
6177
|
+
creases.reset();
|
|
4402
6178
|
dirtyRef.current = true;
|
|
4403
6179
|
}, [behaviorKey, deformersKey, sheetKey, physicsKey]);
|
|
6180
|
+
(0, import_react7.useEffect)(() => {
|
|
6181
|
+
creases.adopt(configRef.current.memory.creases);
|
|
6182
|
+
dirtyRef.current = true;
|
|
6183
|
+
}, [creaseKey]);
|
|
4404
6184
|
const { minSegments, autoSegments, animatedStack } = (0, import_react7.useMemo)(() => {
|
|
4405
6185
|
const cfg = configRef.current;
|
|
4406
|
-
const probe2 = buildStack(cfg, {});
|
|
6186
|
+
const probe2 = withMemory(buildStack(cfg, {}), cfg);
|
|
4407
6187
|
if (!probe2) {
|
|
4408
6188
|
return {
|
|
4409
6189
|
minSegments: [2, 2],
|
|
@@ -4416,7 +6196,7 @@ var PaperMesh = (0, import_react7.forwardRef)(function PaperMesh2(props, ref) {
|
|
|
4416
6196
|
if (cfg.behavior && !cfg.deformers) {
|
|
4417
6197
|
const param = getBehavior(cfg.behavior.type).progressParam;
|
|
4418
6198
|
for (const p of PROGRESS_SAMPLES) {
|
|
4419
|
-
const at = buildStack(cfg, { [param]: p });
|
|
6199
|
+
const at = withMemory(buildStack(cfg, { [param]: p }), cfg);
|
|
4420
6200
|
if (!at) continue;
|
|
4421
6201
|
const [x, y] = stackAutoSegments(at, cfg.sheet);
|
|
4422
6202
|
if (x > want[0]) want[0] = x;
|
|
@@ -4429,30 +6209,65 @@ var PaperMesh = (0, import_react7.forwardRef)(function PaperMesh2(props, ref) {
|
|
|
4429
6209
|
autoSegments: want,
|
|
4430
6210
|
animatedStack: animated
|
|
4431
6211
|
};
|
|
4432
|
-
}, [behaviorKey, deformersKey, physicsKey]);
|
|
6212
|
+
}, [behaviorKey, deformersKey, physicsKey, memoryKey]);
|
|
4433
6213
|
const geometry = (0, import_react7.useMemo)(() => {
|
|
6214
|
+
if (isStrip) {
|
|
6215
|
+
const strip = configRef.current.physics;
|
|
6216
|
+
const nodes = stripNodeCount(config.sheet.height, strip.perforation);
|
|
6217
|
+
return new THREE7.PlaneGeometry(config.sheet.width, config.sheet.height, 1, nodes - 1);
|
|
6218
|
+
}
|
|
4434
6219
|
if (!isCloth) return createSheetGeometry(config.sheet, minSegments, autoSegments);
|
|
4435
|
-
const [sx, sy] = resolveSegments(config.sheet,
|
|
6220
|
+
const [sx, sy] = resolveSegments(config.sheet, minSegments);
|
|
4436
6221
|
const capped = Math.min(Math.max(sx, sy), CLOTH_MAX_SEGMENTS);
|
|
4437
6222
|
return new THREE7.PlaneGeometry(config.sheet.width, config.sheet.height, capped, capped);
|
|
4438
|
-
}, [
|
|
6223
|
+
}, [
|
|
6224
|
+
sheetKey,
|
|
6225
|
+
minSegments,
|
|
6226
|
+
autoSegments,
|
|
6227
|
+
isCloth,
|
|
6228
|
+
isStrip,
|
|
6229
|
+
isStrip ? config.physics.perforation : 0
|
|
6230
|
+
]);
|
|
4439
6231
|
(0, import_react7.useEffect)(() => () => geometry.dispose(), [geometry]);
|
|
4440
6232
|
const basePositions = (0, import_react7.useMemo)(
|
|
4441
6233
|
() => Float32Array.from(geometry.attributes.position.array),
|
|
4442
6234
|
[geometry]
|
|
4443
6235
|
);
|
|
6236
|
+
const stripSim = (0, import_react7.useMemo)(() => {
|
|
6237
|
+
if (!isStrip) return null;
|
|
6238
|
+
const strip = configRef.current.physics;
|
|
6239
|
+
return new StripSim(config.sheet.height, config.sheet.width, {
|
|
6240
|
+
scroll: strip.scroll,
|
|
6241
|
+
tightness: strip.tightness,
|
|
6242
|
+
core: strip.core,
|
|
6243
|
+
tail: strip.tail,
|
|
6244
|
+
perforation: strip.perforation,
|
|
6245
|
+
crease: strip.crease,
|
|
6246
|
+
stiffness: strip.stiffness,
|
|
6247
|
+
drag: strip.drag,
|
|
6248
|
+
gravity: strip.gravity,
|
|
6249
|
+
floor: strip.floor,
|
|
6250
|
+
inertia: strip.inertia
|
|
6251
|
+
});
|
|
6252
|
+
}, [geometry, isStrip]);
|
|
6253
|
+
const lastSimRef = (0, import_react7.useRef)(null);
|
|
4444
6254
|
const sim = (0, import_react7.useMemo)(() => {
|
|
4445
6255
|
if (!isCloth) return null;
|
|
4446
6256
|
const cloth = configRef.current.physics;
|
|
4447
6257
|
const cols = geometry.parameters.widthSegments + 1;
|
|
4448
6258
|
const rows = geometry.parameters.heightSegments + 1;
|
|
4449
|
-
|
|
6259
|
+
const next = new ClothSim(cols, rows, config.sheet.width, config.sheet.height, cloth.pins, {
|
|
4450
6260
|
stiffness: cloth.stiffness,
|
|
4451
6261
|
gravity: cloth.gravity,
|
|
4452
6262
|
wind: cloth.wind,
|
|
4453
6263
|
floor: cloth.floor
|
|
4454
6264
|
});
|
|
6265
|
+
next.adopt(lastSimRef.current);
|
|
6266
|
+
return next;
|
|
4455
6267
|
}, [geometry, isCloth, isCloth ? config.physics.pins : ""]);
|
|
6268
|
+
(0, import_react7.useEffect)(() => {
|
|
6269
|
+
lastSimRef.current = sim;
|
|
6270
|
+
}, [sim]);
|
|
4456
6271
|
const stock = getStock(config.stock);
|
|
4457
6272
|
const texture = useContentTexture(config.content, config.sheet, stock);
|
|
4458
6273
|
const backTexture = useContentTexture(config.content.back, config.sheet, stock);
|
|
@@ -4547,6 +6362,10 @@ var PaperMesh = (0, import_react7.forwardRef)(function PaperMesh2(props, ref) {
|
|
|
4547
6362
|
placeProgrammatic: () => machineRef.current?.placeProgrammatic() ?? false,
|
|
4548
6363
|
returnProgrammatic: () => machineRef.current?.returnProgrammatic() ?? false
|
|
4549
6364
|
}));
|
|
6365
|
+
const shadedCreases = (0, import_react7.useMemo)(
|
|
6366
|
+
() => resolveCreases(config.surface, config.memory.creases, config.sheet),
|
|
6367
|
+
[config.surface, config.memory.creases, config.sheet]
|
|
6368
|
+
);
|
|
4550
6369
|
const idlePose = (0, import_react7.useRef)({ position: [0, 0, 0], rotation: [0, 0, 0] });
|
|
4551
6370
|
(0, import_fiber.useFrame)(({ clock }, delta) => {
|
|
4552
6371
|
const cfg = liveConfig();
|
|
@@ -4559,10 +6378,10 @@ var PaperMesh = (0, import_react7.forwardRef)(function PaperMesh2(props, ref) {
|
|
|
4559
6378
|
idle?.transform?.(now, pose);
|
|
4560
6379
|
if (hasBehaviorTransform) {
|
|
4561
6380
|
const o = effectiveOptions(now);
|
|
4562
|
-
if (o) behavior.transform(o, now, pose);
|
|
6381
|
+
if (o) behavior.transform(o, now, pose, cfg.sheet);
|
|
4563
6382
|
}
|
|
4564
6383
|
const base = props.position ?? [0, 0, 0];
|
|
4565
|
-
const baseRot =
|
|
6384
|
+
const baseRot = baseRotation;
|
|
4566
6385
|
groupRef.current.position.set(
|
|
4567
6386
|
base[0] + pose.position[0],
|
|
4568
6387
|
base[1] + pose.position[1],
|
|
@@ -4574,6 +6393,56 @@ var PaperMesh = (0, import_react7.forwardRef)(function PaperMesh2(props, ref) {
|
|
|
4574
6393
|
baseRot[2] + pose.rotation[2]
|
|
4575
6394
|
);
|
|
4576
6395
|
}
|
|
6396
|
+
if (isStrip && stripSim) {
|
|
6397
|
+
const strip = cfg.physics;
|
|
6398
|
+
stripSim.setParams({
|
|
6399
|
+
scroll: strip.scroll,
|
|
6400
|
+
tightness: strip.tightness,
|
|
6401
|
+
core: strip.core,
|
|
6402
|
+
tail: strip.tail,
|
|
6403
|
+
crease: strip.crease,
|
|
6404
|
+
stiffness: strip.stiffness,
|
|
6405
|
+
drag: strip.drag,
|
|
6406
|
+
gravity: strip.gravity,
|
|
6407
|
+
floor: strip.floor,
|
|
6408
|
+
inertia: strip.inertia
|
|
6409
|
+
});
|
|
6410
|
+
stripSim.step(delta);
|
|
6411
|
+
if (!stripSim.asleep) {
|
|
6412
|
+
const position = geometry.attributes.position;
|
|
6413
|
+
stripSim.writeInto(position.array);
|
|
6414
|
+
position.needsUpdate = true;
|
|
6415
|
+
computeSheetNormals(geometry);
|
|
6416
|
+
geometry.computeBoundingSphere();
|
|
6417
|
+
}
|
|
6418
|
+
return;
|
|
6419
|
+
}
|
|
6420
|
+
const applyShape = (base, force) => {
|
|
6421
|
+
const animated = !reduced && animatedStack;
|
|
6422
|
+
const hasLoop = !reduced && Boolean(cfg.behavior && behavior?.loop);
|
|
6423
|
+
const machineAnimating = Boolean(machineRef.current?.transitioning);
|
|
6424
|
+
if (!force && !dirtyRef.current && !hasLoop && !animated && !machineAnimating) return false;
|
|
6425
|
+
const raw = buildStack(cfg, overridesRef.current, behavior, now);
|
|
6426
|
+
const setAmount = cfg.memory.set ?? stock.takesSet;
|
|
6427
|
+
if (raw && creases.observe(raw, setAmount)) props.onCrease?.(creases.creases);
|
|
6428
|
+
const stack = withMemory(raw, cfg, creases.creases);
|
|
6429
|
+
if (!stack) return false;
|
|
6430
|
+
dirtyRef.current = false;
|
|
6431
|
+
const ctx = { t: now, sheet: cfg.sheet };
|
|
6432
|
+
applyDeformerStack(geometry, base, stack, ctx);
|
|
6433
|
+
if (props.interactive && behavior?.handles) {
|
|
6434
|
+
const o = effectiveOptions(now);
|
|
6435
|
+
behavior.handles.forEach((h, i) => {
|
|
6436
|
+
const mesh = handleRefs.current[i];
|
|
6437
|
+
if (!mesh || !o) return;
|
|
6438
|
+
const [u, v] = h.anchor(o, cfg.sheet);
|
|
6439
|
+
anchorScratch.set((u - 0.5) * cfg.sheet.width, (v - 0.5) * cfg.sheet.height, 0);
|
|
6440
|
+
displacePoint(anchorScratch, u, v, stack, ctx);
|
|
6441
|
+
mesh.position.copy(anchorScratch);
|
|
6442
|
+
});
|
|
6443
|
+
}
|
|
6444
|
+
return true;
|
|
6445
|
+
};
|
|
4577
6446
|
if (isCloth && sim) {
|
|
4578
6447
|
const cloth = cfg.physics;
|
|
4579
6448
|
sim.setParams({
|
|
@@ -4583,7 +6452,8 @@ var PaperMesh = (0, import_react7.forwardRef)(function PaperMesh2(props, ref) {
|
|
|
4583
6452
|
floor: cloth.floor
|
|
4584
6453
|
});
|
|
4585
6454
|
sim.step(delta);
|
|
4586
|
-
|
|
6455
|
+
const moved = !sim.asleep;
|
|
6456
|
+
if (!applyShape(sim.positions, moved) && moved) {
|
|
4587
6457
|
const position = geometry.attributes.position;
|
|
4588
6458
|
position.array.set(sim.positions);
|
|
4589
6459
|
position.needsUpdate = true;
|
|
@@ -4591,26 +6461,7 @@ var PaperMesh = (0, import_react7.forwardRef)(function PaperMesh2(props, ref) {
|
|
|
4591
6461
|
}
|
|
4592
6462
|
return;
|
|
4593
6463
|
}
|
|
4594
|
-
|
|
4595
|
-
const hasLoop = !reduced && Boolean(cfg.behavior && behavior?.loop);
|
|
4596
|
-
const machineAnimating = Boolean(machineRef.current?.transitioning);
|
|
4597
|
-
if (!dirtyRef.current && !hasLoop && !animated && !machineAnimating) return;
|
|
4598
|
-
const stack = buildStack(cfg, overridesRef.current, behavior, now);
|
|
4599
|
-
if (!stack) return;
|
|
4600
|
-
dirtyRef.current = false;
|
|
4601
|
-
const ctx = { t: now, sheet: cfg.sheet };
|
|
4602
|
-
applyDeformerStack(geometry, basePositions, stack, ctx);
|
|
4603
|
-
if (props.interactive && behavior?.handles) {
|
|
4604
|
-
const o = effectiveOptions(now);
|
|
4605
|
-
behavior.handles.forEach((h, i) => {
|
|
4606
|
-
const mesh = handleRefs.current[i];
|
|
4607
|
-
if (!mesh || !o) return;
|
|
4608
|
-
const [u, v] = h.anchor(o, cfg.sheet);
|
|
4609
|
-
anchorScratch.set((u - 0.5) * cfg.sheet.width, (v - 0.5) * cfg.sheet.height, 0);
|
|
4610
|
-
displacePoint(anchorScratch, u, v, stack, ctx);
|
|
4611
|
-
mesh.position.copy(anchorScratch);
|
|
4612
|
-
});
|
|
4613
|
-
}
|
|
6464
|
+
applyShape(basePositions, false);
|
|
4614
6465
|
});
|
|
4615
6466
|
const localDragPoint = (e) => {
|
|
4616
6467
|
const group = groupRef.current;
|
|
@@ -4634,13 +6485,22 @@ var PaperMesh = (0, import_react7.forwardRef)(function PaperMesh2(props, ref) {
|
|
|
4634
6485
|
if (typeof p === "number") props.onProgress?.(p);
|
|
4635
6486
|
};
|
|
4636
6487
|
const grabAnchor = (0, import_react7.useRef)(new THREE7.Vector3());
|
|
6488
|
+
const grabOffset = (0, import_react7.useRef)(new THREE7.Vector3());
|
|
4637
6489
|
const clothDown = (e) => {
|
|
4638
6490
|
if (!isCloth || !sim || !props.interactive || !groupRef.current) return;
|
|
4639
6491
|
e.stopPropagation();
|
|
4640
6492
|
if (controls) controls.enabled = false;
|
|
4641
6493
|
grabAnchor.current.copy(e.point);
|
|
4642
6494
|
const local = groupRef.current.worldToLocal(worldScratch.copy(e.point));
|
|
4643
|
-
|
|
6495
|
+
const drawn = geometry.attributes.position.array;
|
|
6496
|
+
const index = nearestVertex(drawn, sim.count, local.x, local.y, local.z);
|
|
6497
|
+
sim.grab(index);
|
|
6498
|
+
const i3 = index * 3;
|
|
6499
|
+
grabOffset.current.set(
|
|
6500
|
+
drawn[i3] - sim.positions[i3],
|
|
6501
|
+
drawn[i3 + 1] - sim.positions[i3 + 1],
|
|
6502
|
+
drawn[i3 + 2] - sim.positions[i3 + 2]
|
|
6503
|
+
);
|
|
4644
6504
|
draggingRef.current = "cloth";
|
|
4645
6505
|
e.target.setPointerCapture(e.pointerId);
|
|
4646
6506
|
};
|
|
@@ -4651,7 +6511,8 @@ var PaperMesh = (0, import_react7.forwardRef)(function PaperMesh2(props, ref) {
|
|
|
4651
6511
|
const hit = e.ray.intersectPlane(dragPlane, dragPoint);
|
|
4652
6512
|
if (!hit) return;
|
|
4653
6513
|
groupRef.current.worldToLocal(hit);
|
|
4654
|
-
|
|
6514
|
+
const offset = grabOffset.current;
|
|
6515
|
+
sim.moveGrab(hit.x - offset.x, hit.y - offset.y, hit.z - offset.z);
|
|
4655
6516
|
};
|
|
4656
6517
|
const clothUp = (e) => {
|
|
4657
6518
|
if (draggingRef.current !== "cloth" || !sim) return;
|
|
@@ -4660,8 +6521,34 @@ var PaperMesh = (0, import_react7.forwardRef)(function PaperMesh2(props, ref) {
|
|
|
4660
6521
|
sim.release();
|
|
4661
6522
|
e.target.releasePointerCapture(e.pointerId);
|
|
4662
6523
|
};
|
|
6524
|
+
const stripDown = (e) => {
|
|
6525
|
+
if (!isStrip || !stripSim || !props.interactive || !groupRef.current) return;
|
|
6526
|
+
e.stopPropagation();
|
|
6527
|
+
const local = groupRef.current.worldToLocal(worldScratch.copy(e.point));
|
|
6528
|
+
if (stripSim.grabNearest(local.y, local.z) < 0) return;
|
|
6529
|
+
if (controls) controls.enabled = false;
|
|
6530
|
+
grabAnchor.current.copy(e.point);
|
|
6531
|
+
draggingRef.current = "strip";
|
|
6532
|
+
e.target.setPointerCapture(e.pointerId);
|
|
6533
|
+
};
|
|
6534
|
+
const stripMove = (e) => {
|
|
6535
|
+
if (draggingRef.current !== "strip" || !stripSim || !groupRef.current) return;
|
|
6536
|
+
camera.getWorldDirection(planeNormal);
|
|
6537
|
+
dragPlane.setFromNormalAndCoplanarPoint(planeNormal, grabAnchor.current);
|
|
6538
|
+
const hit = e.ray.intersectPlane(dragPlane, dragPoint);
|
|
6539
|
+
if (!hit) return;
|
|
6540
|
+
groupRef.current.worldToLocal(hit);
|
|
6541
|
+
stripSim.moveGrab(hit.y, hit.z);
|
|
6542
|
+
};
|
|
6543
|
+
const stripUp = (e) => {
|
|
6544
|
+
if (draggingRef.current !== "strip" || !stripSim) return;
|
|
6545
|
+
draggingRef.current = null;
|
|
6546
|
+
if (controls) controls.enabled = true;
|
|
6547
|
+
stripSim.release();
|
|
6548
|
+
e.target.releasePointerCapture(e.pointerId);
|
|
6549
|
+
};
|
|
4663
6550
|
const sendState = (event) => machineRef.current?.send(event);
|
|
4664
|
-
return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("group", { ref: groupRef, position: props.position, rotation:
|
|
6551
|
+
return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("group", { ref: groupRef, position: props.position, rotation: baseRotation, children: [
|
|
4665
6552
|
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
|
|
4666
6553
|
"mesh",
|
|
4667
6554
|
{
|
|
@@ -4672,13 +6559,15 @@ var PaperMesh = (0, import_react7.forwardRef)(function PaperMesh2(props, ref) {
|
|
|
4672
6559
|
frustumCulled: false,
|
|
4673
6560
|
onPointerOver: statesLive ? () => sendState("enter") : void 0,
|
|
4674
6561
|
onPointerOut: statesLive ? () => sendState("leave") : void 0,
|
|
4675
|
-
onPointerDown: isCloth || statesLive ? (e) => {
|
|
6562
|
+
onPointerDown: isCloth || isStrip || statesLive ? (e) => {
|
|
4676
6563
|
if (isCloth) clothDown(e);
|
|
6564
|
+
if (isStrip) stripDown(e);
|
|
4677
6565
|
if (statesLive) sendState("down");
|
|
4678
6566
|
} : void 0,
|
|
4679
|
-
onPointerMove: isCloth ? clothMove : void 0,
|
|
4680
|
-
onPointerUp: isCloth || statesLive ? (e) => {
|
|
6567
|
+
onPointerMove: isCloth ? clothMove : isStrip ? stripMove : void 0,
|
|
6568
|
+
onPointerUp: isCloth || isStrip || statesLive ? (e) => {
|
|
4681
6569
|
if (isCloth) clothUp(e);
|
|
6570
|
+
if (isStrip) stripUp(e);
|
|
4682
6571
|
if (statesLive) sendState("up");
|
|
4683
6572
|
} : void 0,
|
|
4684
6573
|
children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
|
|
@@ -4690,12 +6579,17 @@ var PaperMesh = (0, import_react7.forwardRef)(function PaperMesh2(props, ref) {
|
|
|
4690
6579
|
surface: config.surface,
|
|
4691
6580
|
thickness: config.sheet.thickness,
|
|
4692
6581
|
sheet: config.sheet,
|
|
4693
|
-
lighting: config.scene.lighting
|
|
6582
|
+
lighting: config.scene.lighting,
|
|
6583
|
+
creases: shadedCreases
|
|
4694
6584
|
}
|
|
4695
6585
|
)
|
|
4696
6586
|
}
|
|
4697
6587
|
),
|
|
4698
|
-
props.interactive &&
|
|
6588
|
+
props.interactive && // Any simulation, not cloth alone: a sim owns the vertices, so there
|
|
6589
|
+
// is no deformer stack for a handle to drive. Harmless as `!isCloth`
|
|
6590
|
+
// only because the schema makes a sim and a behavior exclusive — the
|
|
6591
|
+
// intent is what is written here.
|
|
6592
|
+
!simKind && behavior?.handles?.map((h, i) => /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(
|
|
4699
6593
|
"mesh",
|
|
4700
6594
|
{
|
|
4701
6595
|
userData: { paperlabChrome: true },
|
|
@@ -4726,8 +6620,32 @@ var PaperMesh = (0, import_react7.forwardRef)(function PaperMesh2(props, ref) {
|
|
|
4726
6620
|
))
|
|
4727
6621
|
] });
|
|
4728
6622
|
});
|
|
6623
|
+
function withMemory(stack, config, creases = config.memory.creases) {
|
|
6624
|
+
if (isStripConfig(config.physics)) return null;
|
|
6625
|
+
const out = applyMemory(stack ?? [], creases);
|
|
6626
|
+
return out.length > 0 ? out : null;
|
|
6627
|
+
}
|
|
6628
|
+
function nearestVertex(array, count, x, y, z25) {
|
|
6629
|
+
let best = -1;
|
|
6630
|
+
let bestDist = Infinity;
|
|
6631
|
+
for (let i = 0; i < count; i++) {
|
|
6632
|
+
const i3 = i * 3;
|
|
6633
|
+
const dx = array[i3] - x;
|
|
6634
|
+
const dy = array[i3 + 1] - y;
|
|
6635
|
+
const dz = array[i3 + 2] - z25;
|
|
6636
|
+
const d = dx * dx + dy * dy + dz * dz;
|
|
6637
|
+
if (d < bestDist) {
|
|
6638
|
+
bestDist = d;
|
|
6639
|
+
best = i;
|
|
6640
|
+
}
|
|
6641
|
+
}
|
|
6642
|
+
return best;
|
|
6643
|
+
}
|
|
6644
|
+
function isStripConfig(physics) {
|
|
6645
|
+
return typeof physics === "object" && physics.type === "strip";
|
|
6646
|
+
}
|
|
4729
6647
|
function buildStack(config, overrides, behavior, t = 0) {
|
|
4730
|
-
if (
|
|
6648
|
+
if (isStripConfig(config.physics)) return null;
|
|
4731
6649
|
const idle = typeof config.physics === "string" && config.physics !== "none" ? getIdlePreset(config.physics) : null;
|
|
4732
6650
|
const idleStack = idle?.stack?.() ?? [];
|
|
4733
6651
|
let shapeStack = [];
|
|
@@ -5669,7 +7587,7 @@ function getWalkPath(options) {
|
|
|
5669
7587
|
|
|
5670
7588
|
// src/field/layouts/index.ts
|
|
5671
7589
|
var DEFAULT_SHEET = { width: 1, height: 1.4 };
|
|
5672
|
-
var
|
|
7590
|
+
var TAU4 = Math.PI * 2;
|
|
5673
7591
|
var DEG7 = Math.PI / 180;
|
|
5674
7592
|
function jitter2(seed, i) {
|
|
5675
7593
|
let h = Math.imul(seed * 1e3 + i + 1 ^ 2654435769, 2654435761);
|
|
@@ -5689,7 +7607,7 @@ var ring = {
|
|
|
5689
7607
|
defaults: ringSchema.parse({}),
|
|
5690
7608
|
optionsSchema: ringSchema,
|
|
5691
7609
|
pose(i, n, o, phase) {
|
|
5692
|
-
const theta = (i / n + phase) *
|
|
7610
|
+
const theta = (i / n + phase) * TAU4;
|
|
5693
7611
|
return {
|
|
5694
7612
|
position: [Math.sin(theta) * o.radius, 0, Math.cos(theta) * o.radius],
|
|
5695
7613
|
// Face radially OUTWARD so the papers nearest the camera show their
|
|
@@ -5850,7 +7768,7 @@ var sweepSchema = import_zod24.z.object({
|
|
|
5850
7768
|
from: import_zod24.z.number().min(0).max(1).default(0),
|
|
5851
7769
|
to: import_zod24.z.number().min(0).max(1).default(1)
|
|
5852
7770
|
});
|
|
5853
|
-
var
|
|
7771
|
+
var sweep2 = {
|
|
5854
7772
|
id: "sweep",
|
|
5855
7773
|
label: "Sweep",
|
|
5856
7774
|
defaults: sweepSchema.parse({}),
|
|
@@ -5965,7 +7883,7 @@ var rack = {
|
|
|
5965
7883
|
};
|
|
5966
7884
|
var colonnadeSchema = import_zod24.z.object({
|
|
5967
7885
|
/** The walk the colonnade is built along — see `stage/path`. */
|
|
5968
|
-
path: walkPathSchema.
|
|
7886
|
+
path: walkPathSchema.prefault({}),
|
|
5969
7887
|
/** Half-width of the clear aisle: how far each banner stands off the walk line. */
|
|
5970
7888
|
aisle: import_zod24.z.number().min(0.2).max(20).default(2.4),
|
|
5971
7889
|
/** How much that gap opens and closes along the walk. Nothing hung by hand is a corridor. */
|
|
@@ -6059,7 +7977,7 @@ registerLayout(spread);
|
|
|
6059
7977
|
registerLayout(pile);
|
|
6060
7978
|
registerLayout(wall);
|
|
6061
7979
|
registerLayout(spill);
|
|
6062
|
-
registerLayout(
|
|
7980
|
+
registerLayout(sweep2);
|
|
6063
7981
|
registerLayout(book);
|
|
6064
7982
|
registerLayout(accordion);
|
|
6065
7983
|
registerLayout(rack);
|
|
@@ -6262,7 +8180,8 @@ function FieldGroup({
|
|
|
6262
8180
|
behaviorTransform.transform(
|
|
6263
8181
|
{ ...config.behavior, [behaviorTransform.progressParam]: progressRef.current },
|
|
6264
8182
|
t,
|
|
6265
|
-
pose2
|
|
8183
|
+
pose2,
|
|
8184
|
+
shared.sheet
|
|
6266
8185
|
);
|
|
6267
8186
|
scratchObj.position.x += pose2.position[0];
|
|
6268
8187
|
scratchObj.position.y += pose2.position[1];
|
|
@@ -7167,6 +9086,8 @@ function diffConfig(config) {
|
|
|
7167
9086
|
}
|
|
7168
9087
|
if (config.deformers) out.deformers = config.deformers;
|
|
7169
9088
|
if (Object.keys(config.surface).length > 0) out.surface = config.surface;
|
|
9089
|
+
const memory = diffAgainst(config.memory, memorySchema.parse({}));
|
|
9090
|
+
if (Object.keys(memory).length > 0) out.memory = memory;
|
|
7170
9091
|
if (typeof config.physics === "object") {
|
|
7171
9092
|
const defaults = clothConfigSchema.parse({ type: "cloth" });
|
|
7172
9093
|
out.physics = { type: "cloth", ...diffAgainst(config.physics, defaults) };
|
|
@@ -7241,8 +9162,9 @@ function describeConfig(config) {
|
|
|
7241
9162
|
if (config.content.type === "receipt") contentPhrase = `a store receipt for "${config.content.store}"`;
|
|
7242
9163
|
const parts = [`${contentPhrase} on ${stock.label.toLowerCase()} paper stock (${size})`];
|
|
7243
9164
|
if (typeof config.physics === "object") {
|
|
9165
|
+
const sim = config.physics;
|
|
7244
9166
|
parts.push(
|
|
7245
|
-
|
|
9167
|
+
sim.type === "strip" ? "paying off a roll as the page scrolls, folding at its perforations into a pile on the floor" : sim.pins === "none" ? "falling and settling as cloth" : `pinned (${sim.pins}) and moving like cloth in wind`
|
|
7246
9168
|
);
|
|
7247
9169
|
} else if (config.behavior) {
|
|
7248
9170
|
const phrase = BEHAVIOR_PHRASES[config.behavior.type];
|
|
@@ -7323,8 +9245,8 @@ function nonDefault(value, defaults) {
|
|
|
7323
9245
|
const out = {};
|
|
7324
9246
|
for (const [k, v] of Object.entries(value ?? {})) {
|
|
7325
9247
|
if (v === void 0) continue;
|
|
7326
|
-
const
|
|
7327
|
-
if (!
|
|
9248
|
+
const same2 = v === defaults[k] || JSON.stringify(v) === JSON.stringify(defaults[k]);
|
|
9249
|
+
if (!same2) out[k] = v;
|
|
7328
9250
|
}
|
|
7329
9251
|
return out;
|
|
7330
9252
|
}
|
|
@@ -7490,13 +9412,13 @@ var PARITY_EPSILON = 5e-4;
|
|
|
7490
9412
|
var parityCases = [
|
|
7491
9413
|
{
|
|
7492
9414
|
name: "roll: defaults",
|
|
7493
|
-
stack: [{ type: "roll", options: { angle: 90, boundary: 0, radius: 0.12,
|
|
9415
|
+
stack: [{ type: "roll", options: { angle: 90, boundary: 0, radius: 0.12, thickness: 0.04 } }],
|
|
7494
9416
|
sheet: { width: 1, height: 1.4 },
|
|
7495
9417
|
t: 0
|
|
7496
9418
|
},
|
|
7497
9419
|
{
|
|
7498
9420
|
name: "roll: tight receipt roll, rolling down",
|
|
7499
|
-
stack: [{ type: "roll", options: { angle: 270, boundary: -0.4, radius: 0.07,
|
|
9421
|
+
stack: [{ type: "roll", options: { angle: 270, boundary: -0.4, radius: 0.07, thickness: 6e-3 } }],
|
|
7500
9422
|
sheet: { width: 1, height: 2.6 },
|
|
7501
9423
|
t: 0
|
|
7502
9424
|
},
|
|
@@ -7639,7 +9561,7 @@ var parityCases = [
|
|
|
7639
9561
|
name: "stacked: bend \u2218 roll \u2218 wave",
|
|
7640
9562
|
stack: [
|
|
7641
9563
|
{ type: "bend", options: { curvature: 0.6, angle: 0 } },
|
|
7642
|
-
{ type: "roll", options: { angle: 90, boundary: 0.1, radius: 0.15,
|
|
9564
|
+
{ type: "roll", options: { angle: 90, boundary: 0.1, radius: 0.15, thickness: 0 } },
|
|
7643
9565
|
{
|
|
7644
9566
|
type: "wave",
|
|
7645
9567
|
options: { amplitude: 0.02, wavelength: 0.6, speed: 0.7, angle: 45, pinnedEdge: "none" }
|
|
@@ -7786,9 +9708,12 @@ function runParityHarness(canvas) {
|
|
|
7786
9708
|
// Annotate the CommonJS export names for ESM import in node:
|
|
7787
9709
|
0 && (module.exports = {
|
|
7788
9710
|
AUTO_CEILING,
|
|
9711
|
+
CreaseTracker,
|
|
7789
9712
|
DropZone,
|
|
7790
9713
|
FLAT_SEGMENTS,
|
|
7791
9714
|
LightRig,
|
|
9715
|
+
MAX_CREASES,
|
|
9716
|
+
MAX_SET,
|
|
7792
9717
|
PARITY_EPSILON,
|
|
7793
9718
|
Paper,
|
|
7794
9719
|
PaperBackdrop,
|
|
@@ -7797,6 +9722,7 @@ function runParityHarness(canvas) {
|
|
|
7797
9722
|
PaperLighting,
|
|
7798
9723
|
PaperMesh,
|
|
7799
9724
|
SAG_TOL,
|
|
9725
|
+
applyMemory,
|
|
7800
9726
|
backdropSchema,
|
|
7801
9727
|
behaviorConfigSchema,
|
|
7802
9728
|
buildAgentPayload,
|
|
@@ -7807,6 +9733,7 @@ function runParityHarness(canvas) {
|
|
|
7807
9733
|
contentNames,
|
|
7808
9734
|
contentSchemaFor,
|
|
7809
9735
|
coreStateNames,
|
|
9736
|
+
creaseSchema,
|
|
7810
9737
|
describeConfig,
|
|
7811
9738
|
describeFieldConfig,
|
|
7812
9739
|
diffConfig,
|
|
@@ -7825,6 +9752,8 @@ function runParityHarness(canvas) {
|
|
|
7825
9752
|
listDeformers,
|
|
7826
9753
|
listLayouts,
|
|
7827
9754
|
listPresets,
|
|
9755
|
+
maxStripLength,
|
|
9756
|
+
memorySchema,
|
|
7828
9757
|
mergeConfig,
|
|
7829
9758
|
mergeWithDeletes,
|
|
7830
9759
|
paperConfigSchema,
|
|
@@ -7851,6 +9780,7 @@ function runParityHarness(canvas) {
|
|
|
7851
9780
|
stateDefSchema,
|
|
7852
9781
|
stockNames,
|
|
7853
9782
|
stocks,
|
|
9783
|
+
stripConfigSchema,
|
|
7854
9784
|
supportsWebGL,
|
|
7855
9785
|
uniquePresetName,
|
|
7856
9786
|
unregisterPreset,
|