prts-terrarchive 0.2.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/GAME_ASSETS.md +26 -0
- package/LICENSE +21 -0
- package/README.en.md +274 -0
- package/README.md +423 -0
- package/THIRD_PARTY_NOTICES.md +61 -0
- package/bin/bake-rhine-details.mjs +34 -0
- package/bin/install.js +273 -0
- package/bin/pack-map-assets.mjs +110 -0
- package/bin/preview-rhine.mjs +71 -0
- package/bin/rhine-detail-baker.ts +84 -0
- package/contracts/cloud_tools_v1.schema.json +243 -0
- package/contracts/corpus_i18n_v1.schema.json +179 -0
- package/contracts/corpus_tools_v1.schema.json +1801 -0
- package/cordis.patch.yml +32 -0
- package/docs/endfield-official-i18n.md +103 -0
- package/lib/client.js +3912 -0
- package/lib/endfield-map/map.js.br +0 -0
- package/lib/endfield-map/map.js.gz +0 -0
- package/lib/endfield-map/resources/map-00b0d0744a1b4404.json.br +0 -0
- package/lib/endfield-map/resources/map-00b0d0744a1b4404.json.gz +0 -0
- package/lib/endfield-map/resources/map-01616f534aaaeca1.png +0 -0
- package/lib/endfield-map/resources/map-1562e0dff4be3d23.png +0 -0
- package/lib/endfield-map/resources/map-160fe040f080f6a3.png +0 -0
- package/lib/endfield-map/resources/map-17b9d8be25dc2b39.json.br +0 -0
- package/lib/endfield-map/resources/map-17b9d8be25dc2b39.json.gz +0 -0
- package/lib/endfield-map/resources/map-1c4245c4e8b80c43.json.br +0 -0
- package/lib/endfield-map/resources/map-1c4245c4e8b80c43.json.gz +0 -0
- package/lib/endfield-map/resources/map-1d30baaaef3e29cc.png +0 -0
- package/lib/endfield-map/resources/map-21a61386af79cd12.json.br +0 -0
- package/lib/endfield-map/resources/map-21a61386af79cd12.json.gz +0 -0
- package/lib/endfield-map/resources/map-24b920b8dab49b44.json.br +0 -0
- package/lib/endfield-map/resources/map-24b920b8dab49b44.json.gz +0 -0
- package/lib/endfield-map/resources/map-26caf8d71247abb7.png +0 -0
- package/lib/endfield-map/resources/map-2729942c89545e73.json.br +0 -0
- package/lib/endfield-map/resources/map-2729942c89545e73.json.gz +0 -0
- package/lib/endfield-map/resources/map-29063a9802f67014.json.br +0 -0
- package/lib/endfield-map/resources/map-29063a9802f67014.json.gz +0 -0
- package/lib/endfield-map/resources/map-2f35d09a3ab60a63.json.br +0 -0
- package/lib/endfield-map/resources/map-2f35d09a3ab60a63.json.gz +0 -0
- package/lib/endfield-map/resources/map-2f98a873da81425e.json.br +0 -0
- package/lib/endfield-map/resources/map-2f98a873da81425e.json.gz +0 -0
- package/lib/endfield-map/resources/map-3ab978ae44190f80.json.br +0 -0
- package/lib/endfield-map/resources/map-3ab978ae44190f80.json.gz +0 -0
- package/lib/endfield-map/resources/map-3b62c80d14208902.json.br +0 -0
- package/lib/endfield-map/resources/map-3b62c80d14208902.json.gz +0 -0
- package/lib/endfield-map/resources/map-3d486e887ae121ac.png +0 -0
- package/lib/endfield-map/resources/map-418dc4865608fce5.png +0 -0
- package/lib/endfield-map/resources/map-4366b84607335f76.json.br +0 -0
- package/lib/endfield-map/resources/map-4366b84607335f76.json.gz +0 -0
- package/lib/endfield-map/resources/map-4371259d3ab7aac7.png +0 -0
- package/lib/endfield-map/resources/map-48dcc0fd55e13f7f.png +0 -0
- package/lib/endfield-map/resources/map-4aabda0e41197570.json.br +0 -0
- package/lib/endfield-map/resources/map-4aabda0e41197570.json.gz +0 -0
- package/lib/endfield-map/resources/map-505f5687b287043c.json.br +0 -0
- package/lib/endfield-map/resources/map-505f5687b287043c.json.gz +0 -0
- package/lib/endfield-map/resources/map-5177e9d1d46f47e2.png +0 -0
- package/lib/endfield-map/resources/map-51baeb59b7f8c28d.png +0 -0
- package/lib/endfield-map/resources/map-549561b46e621874.png +0 -0
- package/lib/endfield-map/resources/map-56c6a0bef799c8dc.json.br +0 -0
- package/lib/endfield-map/resources/map-56c6a0bef799c8dc.json.gz +0 -0
- package/lib/endfield-map/resources/map-5a6963909423b674.png +0 -0
- package/lib/endfield-map/resources/map-5a9c533c0552d7b7.png +0 -0
- package/lib/endfield-map/resources/map-5d20ae9ffa9a02b0.png +0 -0
- package/lib/endfield-map/resources/map-626c8847393f60bf.png +0 -0
- package/lib/endfield-map/resources/map-63d43cbec3bed3b8.png +0 -0
- package/lib/endfield-map/resources/map-661b6ec56e722574.png +0 -0
- package/lib/endfield-map/resources/map-6ddd384eca190828.png +0 -0
- package/lib/endfield-map/resources/map-6fc1aa9144f8156c.json.br +0 -0
- package/lib/endfield-map/resources/map-6fc1aa9144f8156c.json.gz +0 -0
- package/lib/endfield-map/resources/map-738ebaa002c33cfc.png +0 -0
- package/lib/endfield-map/resources/map-758c13be6085ec07.json.br +0 -0
- package/lib/endfield-map/resources/map-758c13be6085ec07.json.gz +0 -0
- package/lib/endfield-map/resources/map-78929f26d6844509.png +0 -0
- package/lib/endfield-map/resources/map-7f09c142950a0f74.png +0 -0
- package/lib/endfield-map/resources/map-8385e11f39e1a51c.json.br +0 -0
- package/lib/endfield-map/resources/map-8385e11f39e1a51c.json.gz +0 -0
- package/lib/endfield-map/resources/map-881343926a568f27.json.br +0 -0
- package/lib/endfield-map/resources/map-881343926a568f27.json.gz +0 -0
- package/lib/endfield-map/resources/map-8d1ade3f01d952b0.png +0 -0
- package/lib/endfield-map/resources/map-8fef3495bb7ed60f.png +0 -0
- package/lib/endfield-map/resources/map-907a7ecf8a0ce352.png +0 -0
- package/lib/endfield-map/resources/map-9113d5951c4db903.json.br +0 -0
- package/lib/endfield-map/resources/map-9113d5951c4db903.json.gz +0 -0
- package/lib/endfield-map/resources/map-94ad6634436224e3.json.br +0 -0
- package/lib/endfield-map/resources/map-94ad6634436224e3.json.gz +0 -0
- package/lib/endfield-map/resources/map-99722e61da5f3e53.png +0 -0
- package/lib/endfield-map/resources/map-9be9596cc059abe3.png +0 -0
- package/lib/endfield-map/resources/map-a4db9a4763e47fbf.png +0 -0
- package/lib/endfield-map/resources/map-a4e8ef07159997e7.json.br +0 -0
- package/lib/endfield-map/resources/map-a4e8ef07159997e7.json.gz +0 -0
- package/lib/endfield-map/resources/map-ab35d06875ee418f.png +0 -0
- package/lib/endfield-map/resources/map-ac51bb3e7623cd8b.png +0 -0
- package/lib/endfield-map/resources/map-ac830a66296bc83a.png +0 -0
- package/lib/endfield-map/resources/map-b3fd04209718e55e.json.br +0 -0
- package/lib/endfield-map/resources/map-b3fd04209718e55e.json.gz +0 -0
- package/lib/endfield-map/resources/map-b8164f1af7c5d895.png +0 -0
- package/lib/endfield-map/resources/map-b8cde93b965384d8.json.br +0 -0
- package/lib/endfield-map/resources/map-b8cde93b965384d8.json.gz +0 -0
- package/lib/endfield-map/resources/map-b8e48511502e0e8d.json.br +0 -0
- package/lib/endfield-map/resources/map-b8e48511502e0e8d.json.gz +0 -0
- package/lib/endfield-map/resources/map-bfc58277d5ba649d.png +0 -0
- package/lib/endfield-map/resources/map-c013f82048c6891a.png +0 -0
- package/lib/endfield-map/resources/map-c49c947841ae4ab5.json.br +0 -0
- package/lib/endfield-map/resources/map-c49c947841ae4ab5.json.gz +0 -0
- package/lib/endfield-map/resources/map-c4d1b1c6d6706c81.json.br +0 -0
- package/lib/endfield-map/resources/map-c4d1b1c6d6706c81.json.gz +0 -0
- package/lib/endfield-map/resources/map-c607f8b4c8ad8967.png +0 -0
- package/lib/endfield-map/resources/map-c6f20276b8781326.json.br +0 -0
- package/lib/endfield-map/resources/map-c6f20276b8781326.json.gz +0 -0
- package/lib/endfield-map/resources/map-d51adf5952af1e8e.png +0 -0
- package/lib/endfield-map/resources/map-d695eface22f8b74.json.br +0 -0
- package/lib/endfield-map/resources/map-d695eface22f8b74.json.gz +0 -0
- package/lib/endfield-map/resources/map-db11d394456a7465.png +0 -0
- package/lib/endfield-map/resources/map-dcc4d218581e3dc6.png +0 -0
- package/lib/endfield-map/resources/map-ddd43dbb8ab8a4e3.json.br +0 -0
- package/lib/endfield-map/resources/map-ddd43dbb8ab8a4e3.json.gz +0 -0
- package/lib/endfield-map/resources/map-defe5db7dabf748a.png +0 -0
- package/lib/endfield-map/resources/map-e1aeb833e22520aa.json.br +0 -0
- package/lib/endfield-map/resources/map-e1aeb833e22520aa.json.gz +0 -0
- package/lib/endfield-map/resources/map-e25ee9af6e493500.png +0 -0
- package/lib/endfield-map/resources/map-e3824082cd6f747d.json.br +0 -0
- package/lib/endfield-map/resources/map-e3824082cd6f747d.json.gz +0 -0
- package/lib/endfield-map/resources/map-e694a91e630b1bd6.png +0 -0
- package/lib/endfield-map/resources/map-eaa93af79d12a87d.png +0 -0
- package/lib/endfield-map/resources/map-ec9575ee65d7766f.json.br +0 -0
- package/lib/endfield-map/resources/map-ec9575ee65d7766f.json.gz +0 -0
- package/lib/endfield-map/resources/map-ede421a329dc78a4.png +0 -0
- package/lib/endfield-map/resources/map-eff3bce8f6888fc5.json.br +0 -0
- package/lib/endfield-map/resources/map-eff3bce8f6888fc5.json.gz +0 -0
- package/lib/endfield-map/resources/map-fb5f4a59a181452c.png +0 -0
- package/lib/endfield-map/resources/map-fdeaa17eeba90d8e.json.br +0 -0
- package/lib/endfield-map/resources/map-fdeaa17eeba90d8e.json.gz +0 -0
- package/lib/rhine/assets/archive-assembly.glb +0 -0
- package/lib/rhine/assets/archive-cassette.glb +0 -0
- package/lib/rhine/fonts/MiSans-Bold.woff2 +0 -0
- package/lib/rhine/fonts/MiSans-Demibold.woff2 +0 -0
- package/lib/rhine/fonts/MiSans-Light.woff2 +0 -0
- package/lib/rhine/fonts/MiSans-Regular.woff2 +0 -0
- package/lib/rhine/fonts/MiSans-license.pdf +0 -0
- package/lib/rhine/fonts/NOTICE.txt +6 -0
- package/lib/rhine/licenses/RhineLabUI-MIT.txt +21 -0
- package/lib/rhine/licenses/entities-BSD-2-Clause.txt +11 -0
- package/lib/rhine/licenses/linkify-it-MIT.txt +22 -0
- package/lib/rhine/licenses/markdown-it-MIT.txt +22 -0
- package/lib/rhine/licenses/mdurl-MIT.txt +45 -0
- package/lib/rhine/licenses/punycode.js-MIT.txt +20 -0
- package/lib/rhine/licenses/rolling-number-MIT.txt +21 -0
- package/lib/rhine/licenses/three-MIT.txt +21 -0
- package/lib/rhine/licenses/uc.micro-MIT.txt +20 -0
- package/lib/rhine/rhine.css +1 -0
- package/lib/rhine/rhine.js +5106 -0
- package/lib/skins/common.css +31 -0
- package/lib/skins/endfield-aic.css +141 -0
- package/lib/skins/prts-agent.css +136 -0
- package/lib/skins/rhine-lab.css +70 -0
- package/package.json +112 -0
- package/presets/definition.js +26 -0
- package/presets/prts/agent.cordis.yml +25 -0
- package/presets/prts/preset.yml +3 -0
- package/presets/register.js +86 -0
- package/resources/entity-routing.json +21 -0
- package/skills/prts-investigation/SKILL.md +121 -0
- package/skills/prts-retrieval/SKILL.md +35 -0
- package/skills/prts-retrieval/references/cloud-tools.md +13 -0
- package/skills/prts-retrieval/references/diagnostics.md +15 -0
- package/skills/prts-retrieval/references/local-tools.md +24 -0
- package/skills/prts-retrieval/references/module-arknights.md +23 -0
- package/skills/prts-retrieval/references/module-dual.md +15 -0
- package/skills/prts-retrieval/references/module-endfield.md +21 -0
- package/skills/prts-retrieval/references/retrieval-process.md +44 -0
- package/skills/prts-retrieval/references/retrieval-recipes-dual.md +34 -0
- package/skills/prts-retrieval/references/retrieval-recipes-endfield.md +37 -0
- package/skills/prts-retrieval/references/retrieval-recipes.md +39 -0
- package/skills/prts-retrieval/references/tools-runtime.md +48 -0
- package/skills/prts-retrieval/references/wiki-schema.md +38 -0
- package/src/archive-presentation.js +287 -0
- package/src/cloud-projection.js +124 -0
- package/src/cloud.js +498 -0
- package/src/entity-recognizer.js +311 -0
- package/src/entity-routing.js +82 -0
- package/src/evidence-state.js +332 -0
- package/src/i18n.js +365 -0
- package/src/index.js +1261 -0
- package/src/installer.js +1362 -0
- package/src/investigation-review.js +87 -0
- package/src/investigation-store.js +579 -0
- package/src/investigation-tools.js +113 -0
- package/src/localization-format.js +23 -0
- package/src/manual-search.js +103 -0
- package/src/read.js +1097 -0
- package/src/release-compatibility.js +73 -0
- package/src/release-watch.js +40 -0
- package/src/search-projection.js +139 -0
- package/src/search.js +1522 -0
- package/src/skill.js +98 -0
- package/src/source-map.js +348 -0
- package/src/state.js +314 -0
- package/src/store.js +1569 -0
- package/src/timeline.js +386 -0
- package/src/ui.js +1353 -0
- package/src/wiki.js +111 -0
- package/tsconfig.rhine.json +15 -0
- package/ui/rhine/archive-navigation-limit.ts +31 -0
- package/ui/rhine/archive-rack.ts +83 -0
- package/ui/rhine/assets/evidence-observatory.webp +0 -0
- package/ui/rhine/board-plane-transform.ts +24 -0
- package/ui/rhine/catalogue.ts +72 -0
- package/ui/rhine/deferred-preparation.ts +146 -0
- package/ui/rhine/evidence-board-controls.css +22 -0
- package/ui/rhine/evidence-board-history.ts +73 -0
- package/ui/rhine/evidence-board-model.ts +189 -0
- package/ui/rhine/evidence-board-panel.ts +762 -0
- package/ui/rhine/evidence-board-scene.ts +548 -0
- package/ui/rhine/evidence-board.css +191 -0
- package/ui/rhine/evidence-card-art.ts +470 -0
- package/ui/rhine/evidence-card-style.ts +17 -0
- package/ui/rhine/evidence-inbox-scene.ts +43 -0
- package/ui/rhine/evidence-inbox.css +29 -0
- package/ui/rhine/evidence-inbox.ts +104 -0
- package/ui/rhine/index.ts +12 -0
- package/ui/rhine/interaction.css +20 -0
- package/ui/rhine/investigation-board-plane.ts +5 -0
- package/ui/rhine/investigation-board.css +34 -0
- package/ui/rhine/investigation-board.ts +348 -0
- package/ui/rhine/investigation-edits.ts +46 -0
- package/ui/rhine/investigation-layout.ts +29 -0
- package/ui/rhine/label-prefetch.ts +34 -0
- package/ui/rhine/looping-title.ts +40 -0
- package/ui/rhine/manual-search.css +41 -0
- package/ui/rhine/manual-search.ts +89 -0
- package/ui/rhine/original/appearance.ts +180 -0
- package/ui/rhine/original/archive-label.ts +84 -0
- package/ui/rhine/original/archive-lighting.ts +39 -0
- package/ui/rhine/original/archive-loop.ts +82 -0
- package/ui/rhine/original/archive-refill.ts +64 -0
- package/ui/rhine/original/array-detail-data.ts +2 -0
- package/ui/rhine/original/array-detail.ts +41 -0
- package/ui/rhine/original/array-interior.ts +411 -0
- package/ui/rhine/original/array-occlusion.ts +166 -0
- package/ui/rhine/original/array-shell.ts +38 -0
- package/ui/rhine/original/array-visibility.ts +309 -0
- package/ui/rhine/original/asset-url.ts +4 -0
- package/ui/rhine/original/brand.ts +12 -0
- package/ui/rhine/original/cassette-lod.ts +81 -0
- package/ui/rhine/original/data.ts +21 -0
- package/ui/rhine/original/decryption.ts +185 -0
- package/ui/rhine/original/detail-signature.ts +18 -0
- package/ui/rhine/original/glass-reveal.ts +45 -0
- package/ui/rhine/original/internal-optics.ts +70 -0
- package/ui/rhine/original/label-renderer.ts +119 -0
- package/ui/rhine/original/label-textures.ts +117 -0
- package/ui/rhine/original/model-viewer.ts +658 -0
- package/ui/rhine/original/motion.ts +116 -0
- package/ui/rhine/original/opaque-backfaces.ts +64 -0
- package/ui/rhine/original/program-preparation.ts +111 -0
- package/ui/rhine/original/quality-renderer.ts +97 -0
- package/ui/rhine/original/quality-settings.ts +111 -0
- package/ui/rhine/original/render-quality.ts +163 -0
- package/ui/rhine/original/scene.ts +2051 -0
- package/ui/rhine/original/shelf-interior.ts +142 -0
- package/ui/rhine/original/ui-transitions.ts +111 -0
- package/ui/rhine/original/viewer-camera.ts +92 -0
- package/ui/rhine/original/viewport-layout.ts +41 -0
- package/ui/rhine/original-ui.css +2334 -0
- package/ui/rhine/readability.css +46 -0
- package/ui/rhine/reading-object.ts +96 -0
- package/ui/rhine/reading-origin.ts +28 -0
- package/ui/rhine/reading-room.css +55 -0
- package/ui/rhine/report-markdown.ts +98 -0
- package/ui/rhine/scene-edge-navigation.ts +84 -0
- package/ui/rhine/scene.ts +1726 -0
- package/ui/rhine/shelf-layout.ts +38 -0
- package/ui/rhine/snapshot-update-gate.ts +22 -0
- package/ui/rhine/source-reading.ts +15 -0
- package/ui/rhine/source-selection.css +164 -0
- package/ui/rhine/temporary-performance-observers.ts +163 -0
- package/ui/rhine/temporary-performance-panel.ts +124 -0
- package/ui/rhine/temporary-performance.css +19 -0
- package/ui/rhine/temporary-performance.ts +552 -0
- package/ui/rhine/temporary-render-diagnostics.ts +105 -0
- package/ui/rhine/tool-activity.ts +69 -0
- package/ui/rhine/types.ts +219 -0
- package/ui/rhine/visible-scene-hit.ts +13 -0
- package/ui/rhine/workbench.css +818 -0
- package/ui/rhine/workbench.ts +2245 -0
- package/ui/rhine/workspace-travel.ts +32 -0
- package/vite.rhine.config.ts +19 -0
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
// Measured against the local reference at 25 fps. Times here are ORIGINAL video
|
|
2
|
+
// seconds, unlike the camera's historical (original minus five) time convention.
|
|
3
|
+
export const DECRYPTION_START = 34.12;
|
|
4
|
+
export const DECRYPTION_END = 39.56;
|
|
5
|
+
const INTERACTIVE_RATE = 1.5;
|
|
6
|
+
const clamp = (value: number) => Math.max(0, Math.min(1, value));
|
|
7
|
+
const smooth = (value: number) => {
|
|
8
|
+
const x = clamp(value);
|
|
9
|
+
return x * x * x * (10 + x * (-15 + x * 6));
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
type Knot = readonly [number, number];
|
|
13
|
+
// Monotone cubic Hermite interpolation preserves the measured easing without
|
|
14
|
+
// overshoot or velocity jumps at a frame sample. End velocities are zero.
|
|
15
|
+
export function sampleCurve(knots: readonly Knot[], time: number) {
|
|
16
|
+
if (time <= knots[0][0]) return knots[0][1];
|
|
17
|
+
if (time >= knots[knots.length - 1][0]) return knots[knots.length - 1][1];
|
|
18
|
+
const secant = (i: number) =>
|
|
19
|
+
(knots[i + 1][1] - knots[i][1]) / (knots[i + 1][0] - knots[i][0]);
|
|
20
|
+
const slope = (i: number) => {
|
|
21
|
+
if (i === 0 || i === knots.length - 1) return 0;
|
|
22
|
+
const a = secant(i - 1),
|
|
23
|
+
b = secant(i);
|
|
24
|
+
if (a * b <= 0) return 0;
|
|
25
|
+
const left = knots[i][0] - knots[i - 1][0],
|
|
26
|
+
right = knots[i + 1][0] - knots[i][0];
|
|
27
|
+
const w1 = 2 * right + left,
|
|
28
|
+
w2 = right + 2 * left;
|
|
29
|
+
return (w1 + w2) / (w1 / a + w2 / b);
|
|
30
|
+
};
|
|
31
|
+
let i = 0;
|
|
32
|
+
while (time > knots[i + 1][0]) i++;
|
|
33
|
+
const span = knots[i + 1][0] - knots[i][0],
|
|
34
|
+
t = (time - knots[i][0]) / span,
|
|
35
|
+
t2 = t * t,
|
|
36
|
+
t3 = t2 * t;
|
|
37
|
+
return (
|
|
38
|
+
(2 * t3 - 3 * t2 + 1) * knots[i][1] +
|
|
39
|
+
(t3 - 2 * t2 + t) * span * slope(i) +
|
|
40
|
+
(-2 * t3 + 3 * t2) * knots[i + 1][1] +
|
|
41
|
+
(t3 - t2) * span * slope(i + 1)
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const GROW: readonly Knot[] = [
|
|
46
|
+
[34.24, 0],
|
|
47
|
+
[34.4, 0.19],
|
|
48
|
+
[34.64, 0.57],
|
|
49
|
+
[34.96, 0.79],
|
|
50
|
+
[35.28, 0.92],
|
|
51
|
+
[35.6, 0.973],
|
|
52
|
+
[36.04, 1],
|
|
53
|
+
];
|
|
54
|
+
// The clearing front starts when the line finishes retracting. At 39.0 s
|
|
55
|
+
// the upper half is already readable; height-dependent roughness moves with it.
|
|
56
|
+
const REVEAL: readonly Knot[] = [
|
|
57
|
+
[38.84, 0],
|
|
58
|
+
[38.92, 0.28],
|
|
59
|
+
[39.0, 0.51],
|
|
60
|
+
[39.16, 0.74],
|
|
61
|
+
[39.32, 0.94],
|
|
62
|
+
[39.56, 1],
|
|
63
|
+
];
|
|
64
|
+
const RETRACT: readonly Knot[] = [
|
|
65
|
+
[37.72, 1],
|
|
66
|
+
[37.88, 0.72],
|
|
67
|
+
[38.0, 0.38],
|
|
68
|
+
[38.12, 0.22],
|
|
69
|
+
[38.24, 0.14],
|
|
70
|
+
[38.4, 0.075],
|
|
71
|
+
[38.64, 0.024],
|
|
72
|
+
[38.84, 0],
|
|
73
|
+
];
|
|
74
|
+
|
|
75
|
+
// Endpoints measured on the face at 37 seconds. Every moving endpoint is on
|
|
76
|
+
// this ONE diagonal in model space; projection carries it with the camera.
|
|
77
|
+
export const SCAN_FROM = [-1.6, 0.5] as const;
|
|
78
|
+
export const SCAN_TO = [1.98, 3.24] as const;
|
|
79
|
+
export const SCAN_CORNERS = [
|
|
80
|
+
[-1.88, 3.2],
|
|
81
|
+
[2.14, 3.36],
|
|
82
|
+
[-1.77, 0.36],
|
|
83
|
+
[2.17, 0.65],
|
|
84
|
+
] as const;
|
|
85
|
+
|
|
86
|
+
export function decryptionFrame(time: number) {
|
|
87
|
+
const grow = sampleCurve(GROW, time);
|
|
88
|
+
const remaining = sampleCurve(RETRACT, time);
|
|
89
|
+
const intervals: [number, number][] = [];
|
|
90
|
+
if (time >= 34.24 && time < 36.04 && grow > 0)
|
|
91
|
+
intervals.push([0, grow * 0.5], [1 - grow * 0.5, 1]);
|
|
92
|
+
else if (time >= 36.04 && time < 38.84)
|
|
93
|
+
intervals.push([0.5 - remaining * 0.5, 0.5 + remaining * 0.5]);
|
|
94
|
+
const markers =
|
|
95
|
+
smooth((time - 34.2) / 0.12) * (1 - smooth((time - 37.76) / 0.56));
|
|
96
|
+
return {
|
|
97
|
+
time,
|
|
98
|
+
intervals,
|
|
99
|
+
markers,
|
|
100
|
+
point: smooth((time - 38.58) / 0.2) * (1 - smooth((time - 39.08) / 0.22)),
|
|
101
|
+
label: smooth((time - 34.32) / 0.36) * (1 - smooth((time - 37.68) / 0.24)),
|
|
102
|
+
labelValue: smooth((time - 35.64) / 0.56),
|
|
103
|
+
clarity: sampleCurve(REVEAL, time),
|
|
104
|
+
phase:
|
|
105
|
+
time < 34.24
|
|
106
|
+
? "waiting"
|
|
107
|
+
: time < 36.04
|
|
108
|
+
? "joining"
|
|
109
|
+
: time < 37.72
|
|
110
|
+
? "connected"
|
|
111
|
+
: time < 38.84
|
|
112
|
+
? "retracting"
|
|
113
|
+
: time < DECRYPTION_END
|
|
114
|
+
? "revealing"
|
|
115
|
+
: "clear",
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
export type DecryptionFrame = ReturnType<typeof decryptionFrame>;
|
|
119
|
+
|
|
120
|
+
export class DecryptionController {
|
|
121
|
+
clarity = 0;
|
|
122
|
+
private active = false;
|
|
123
|
+
private elapsed: number | null = null;
|
|
124
|
+
frame: DecryptionFrame = decryptionFrame(-1);
|
|
125
|
+
|
|
126
|
+
enter(alreadyClear = false) {
|
|
127
|
+
if (this.active) return;
|
|
128
|
+
this.active = true;
|
|
129
|
+
this.elapsed = alreadyClear
|
|
130
|
+
? (DECRYPTION_END - DECRYPTION_START) / INTERACTIVE_RATE
|
|
131
|
+
: null;
|
|
132
|
+
if (alreadyClear) this.finish();
|
|
133
|
+
}
|
|
134
|
+
leave() {
|
|
135
|
+
this.active = false;
|
|
136
|
+
this.elapsed = null;
|
|
137
|
+
this.frame = decryptionFrame(-1);
|
|
138
|
+
}
|
|
139
|
+
select(clarity = 0) {
|
|
140
|
+
this.leave();
|
|
141
|
+
this.clarity = clarity;
|
|
142
|
+
}
|
|
143
|
+
finish() {
|
|
144
|
+
this.elapsed = (DECRYPTION_END - DECRYPTION_START) / INTERACTIVE_RATE;
|
|
145
|
+
this.clarity = 1;
|
|
146
|
+
this.frame = decryptionFrame(DECRYPTION_END);
|
|
147
|
+
}
|
|
148
|
+
update(dt: number, ready: boolean, reduced: boolean, referenceTime?: number) {
|
|
149
|
+
if (referenceTime !== undefined) {
|
|
150
|
+
this.frame = decryptionFrame(referenceTime);
|
|
151
|
+
this.clarity = this.frame.clarity;
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
if (!this.active) {
|
|
155
|
+
this.clarity = reduced
|
|
156
|
+
? 0
|
|
157
|
+
: this.clarity * Math.exp(-Math.max(0, dt) * 9);
|
|
158
|
+
if (this.clarity < 0.0001) this.clarity = 0;
|
|
159
|
+
this.frame = decryptionFrame(-1);
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
if (reduced) {
|
|
163
|
+
this.finish();
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
if (this.elapsed === null && ready) this.elapsed = 0;
|
|
167
|
+
else if (this.elapsed !== null)
|
|
168
|
+
this.elapsed = Math.min(
|
|
169
|
+
this.elapsed + Math.max(0, dt),
|
|
170
|
+
(DECRYPTION_END - DECRYPTION_START) / INTERACTIVE_RATE,
|
|
171
|
+
);
|
|
172
|
+
if (this.elapsed !== null) {
|
|
173
|
+
this.frame = decryptionFrame(
|
|
174
|
+
DECRYPTION_START + this.elapsed * INTERACTIVE_RATE,
|
|
175
|
+
);
|
|
176
|
+
// Re-entry during refrosting starts from the displayed material state.
|
|
177
|
+
this.clarity =
|
|
178
|
+
this.frame.clarity > this.clarity
|
|
179
|
+
? this.frame.clarity
|
|
180
|
+
: this.frame.phase === "clear"
|
|
181
|
+
? 1
|
|
182
|
+
: this.clarity * Math.exp(-Math.max(0, dt) * 9);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type * as THREE from "three";
|
|
2
|
+
|
|
3
|
+
/** Triangle order and authored front normals bind the bake to this exact mesh.
|
|
4
|
+
* Independent of attribute compaction/index renumbering in the interior bake.
|
|
5
|
+
*/
|
|
6
|
+
export function detailSignature(geometry: THREE.BufferGeometry) {
|
|
7
|
+
if (!geometry.index || !geometry.attributes.position || !geometry.attributes.normal) return "";
|
|
8
|
+
let hash = 2166136261;
|
|
9
|
+
const buffer = new ArrayBuffer(4), value = new DataView(buffer);
|
|
10
|
+
for (let i = 0; i < geometry.index.count; i++) {
|
|
11
|
+
const index = geometry.index.getX(i);
|
|
12
|
+
for (const name of ["position", "normal"]) for (let c = 0; c < 3; c++) {
|
|
13
|
+
value.setFloat32(0, geometry.attributes[name].getComponent(index, c), true);
|
|
14
|
+
for (let b = 0; b < 4; b++) hash = Math.imul(hash ^ value.getUint8(b), 16777619);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
return (hash >>> 0).toString(16).padStart(8, "0");
|
|
18
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
// A feathered clearing front travels from the top to the bottom of the cover.
|
|
2
|
+
// Progress endpoints are wholly frosted / wholly clear, including the rim.
|
|
3
|
+
const FEATHER = 0.12;
|
|
4
|
+
export const FROSTED_ROUGHNESS = 0.42;
|
|
5
|
+
export const CLEAR_ROUGHNESS = 0.025;
|
|
6
|
+
const FROST_SPAN = 0.016;
|
|
7
|
+
|
|
8
|
+
// Three's default mip rule grows blur relative to an object as the viewport
|
|
9
|
+
// shrinks. Bound the frosted footprint to 1.6% of the projected cover height.
|
|
10
|
+
export function frostedTransmissionLod(
|
|
11
|
+
panelPixels: number,
|
|
12
|
+
textureWidth: number,
|
|
13
|
+
roughness: number,
|
|
14
|
+
quality = 1,
|
|
15
|
+
ior = 1.46,
|
|
16
|
+
) {
|
|
17
|
+
const clamp = (v: number) => Math.max(0, Math.min(1, v));
|
|
18
|
+
const strength = clamp((roughness - CLEAR_ROUGHNESS) / (FROSTED_ROUGHNESS - CLEAR_ROUGHNESS));
|
|
19
|
+
const native = Math.log2(textureWidth) * roughness * clamp(ior * 2 - 2);
|
|
20
|
+
const clearLod = Math.log2(textureWidth) * CLEAR_ROUGHNESS * clamp(ior * 2 - 2);
|
|
21
|
+
const bounded = Math.log2(Math.max(2 ** clearLod, panelPixels * FROST_SPAN * Math.pow(strength, 1.15)));
|
|
22
|
+
return native + (Math.min(native, bounded) - native) * quality;
|
|
23
|
+
}
|
|
24
|
+
export const frostedTransmissionGLSL = `
|
|
25
|
+
float archiveTransmissionLod(float roughness, float ior, vec2 samplerSize) {
|
|
26
|
+
float nativeLod = log2(samplerSize.x) * roughness * clamp(ior * 2.0 - 2.0, 0.0, 1.0);
|
|
27
|
+
float strength = clamp((roughness - ${CLEAR_ROUGHNESS}) / ${FROSTED_ROUGHNESS - CLEAR_ROUGHNESS}, 0.0, 1.0);
|
|
28
|
+
float panelPixels = length(vArchiveProjectedAxis * samplerSize);
|
|
29
|
+
float clearLod = log2(samplerSize.x) * ${CLEAR_ROUGHNESS} * clamp(ior * 2.0 - 2.0, 0.0, 1.0);
|
|
30
|
+
float boundedLod = log2(max(exp2(clearLod), panelPixels * ${FROST_SPAN} * pow(strength, 1.15)));
|
|
31
|
+
return mix(nativeLod, min(nativeLod, boundedLod), archiveQuality);
|
|
32
|
+
}`;
|
|
33
|
+
export function glassRevealAtHeight(progress: number, height: number) {
|
|
34
|
+
const edge = 1 - (1 + 2 * FEATHER) * Math.max(0, Math.min(1, progress));
|
|
35
|
+
const x = Math.max(
|
|
36
|
+
0,
|
|
37
|
+
Math.min(1, (Math.max(0, Math.min(1, height)) - edge) / (2 * FEATHER)),
|
|
38
|
+
);
|
|
39
|
+
return x * x * (3 - 2 * x);
|
|
40
|
+
}
|
|
41
|
+
export const glassRevealGLSL = `
|
|
42
|
+
float glassRevealAtHeight(float progress, float height) {
|
|
43
|
+
float edge = 1.0 - ${1 + 2 * FEATHER} * clamp(progress, 0.0, 1.0);
|
|
44
|
+
return smoothstep(edge, edge + ${2 * FEATHER}, clamp(height, 0.0, 1.0));
|
|
45
|
+
}`;
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import * as THREE from "three";
|
|
2
|
+
|
|
3
|
+
const surfaces: Record<
|
|
4
|
+
string,
|
|
5
|
+
{ color: string; roughness: number; opacity: number; order: number }
|
|
6
|
+
> = {
|
|
7
|
+
Optical_Glass_Body: {
|
|
8
|
+
color: "#929894",
|
|
9
|
+
roughness: 0.38,
|
|
10
|
+
opacity: 0.27,
|
|
11
|
+
order: 20,
|
|
12
|
+
},
|
|
13
|
+
Optical_Glass_Roof: {
|
|
14
|
+
color: "#929b94",
|
|
15
|
+
roughness: 0.34,
|
|
16
|
+
opacity: 0.42,
|
|
17
|
+
order: 24,
|
|
18
|
+
},
|
|
19
|
+
Optical_Glass_Edge: {
|
|
20
|
+
color: "#edf0e7",
|
|
21
|
+
roughness: 0.19,
|
|
22
|
+
opacity: 0.9,
|
|
23
|
+
order: 28,
|
|
24
|
+
},
|
|
25
|
+
Optical_Bridge_Glass: {
|
|
26
|
+
color: "#a0aca2",
|
|
27
|
+
roughness: 0.32,
|
|
28
|
+
opacity: 0.36,
|
|
29
|
+
order: 26,
|
|
30
|
+
},
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
export function configureInternalOptics(
|
|
34
|
+
name: string,
|
|
35
|
+
mat: THREE.MeshPhysicalMaterial,
|
|
36
|
+
) {
|
|
37
|
+
const surface = surfaces[name];
|
|
38
|
+
if (!surface) return;
|
|
39
|
+
mat.color.set(surface.color);
|
|
40
|
+
mat.roughness = surface.roughness;
|
|
41
|
+
mat.metalness = 0.015;
|
|
42
|
+
mat.clearcoat = 0.42;
|
|
43
|
+
mat.clearcoatRoughness = 0.24;
|
|
44
|
+
mat.opacity = surface.opacity;
|
|
45
|
+
// Three's outer transmission pass samples its opaque capture and omits other
|
|
46
|
+
// transmissive meshes. Composite these thin frosted layers into that capture
|
|
47
|
+
// with explicit alpha blending, after the opaque ribs/substrate. The exterior
|
|
48
|
+
// cover can then refract the complete interior in both scenes.
|
|
49
|
+
mat.transmission = 0;
|
|
50
|
+
mat.transparent = false;
|
|
51
|
+
mat.blending = THREE.CustomBlending;
|
|
52
|
+
mat.blendEquation = THREE.AddEquation;
|
|
53
|
+
mat.blendSrc = THREE.SrcAlphaFactor;
|
|
54
|
+
mat.blendDst = THREE.OneMinusSrcAlphaFactor;
|
|
55
|
+
mat.blendSrcAlpha = THREE.OneFactor;
|
|
56
|
+
mat.blendDstAlpha = THREE.OneMinusSrcAlphaFactor;
|
|
57
|
+
mat.depthWrite = false;
|
|
58
|
+
// These are closed volumes; rendering both shell faces doubles their density.
|
|
59
|
+
mat.side = THREE.FrontSide;
|
|
60
|
+
mat.userData.opticalOrder = surface.order;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function internalOpticsFragment(source: string) {
|
|
64
|
+
// Grazing edges accumulate density while the face stays lightly frosted.
|
|
65
|
+
return source.replace(
|
|
66
|
+
"#include <opaque_fragment>",
|
|
67
|
+
`diffuseColor.a = min(0.86, opacity + 0.42 * pow(1.0 - abs(dot(normal, normalize(vViewPosition))), 3.0));
|
|
68
|
+
#include <opaque_fragment>`,
|
|
69
|
+
);
|
|
70
|
+
}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import * as THREE from 'three';
|
|
2
|
+
import { Pass, FullScreenQuad } from 'three/addons/postprocessing/Pass.js';
|
|
3
|
+
import type { EffectComposer } from 'three/addons/postprocessing/EffectComposer.js';
|
|
4
|
+
|
|
5
|
+
export const ARCHIVE_LABEL_LAYER = 1;
|
|
6
|
+
|
|
7
|
+
/** Save the beauty pass's existing depth before later effects reuse its buffer. */
|
|
8
|
+
class LabelDepthCapture extends Pass {
|
|
9
|
+
readonly target = new THREE.WebGLRenderTarget(1, 1, {
|
|
10
|
+
depthBuffer: false, stencilBuffer: false,
|
|
11
|
+
minFilter: THREE.NearestFilter, magFilter: THREE.NearestFilter,
|
|
12
|
+
});
|
|
13
|
+
private readonly material = new THREE.ShaderMaterial({
|
|
14
|
+
uniforms: { sourceDepth: { value: null } },
|
|
15
|
+
vertexShader: 'varying vec2 vUv; void main(){ vUv = uv; gl_Position = vec4(position.xy, 0., 1.); }',
|
|
16
|
+
fragmentShader: `uniform sampler2D sourceDepth;
|
|
17
|
+
varying vec2 vUv;
|
|
18
|
+
#include <packing>
|
|
19
|
+
void main(){ gl_FragColor = packDepthToRGBA(texture2D(sourceDepth, vUv).x); }`,
|
|
20
|
+
depthTest: false, depthWrite: false, blending: THREE.NoBlending,
|
|
21
|
+
});
|
|
22
|
+
private readonly quad = new FullScreenQuad(this.material);
|
|
23
|
+
private disposed = false;
|
|
24
|
+
constructor() { super(); this.needsSwap = false; this.target.texture.name = 'Archive label occlusion'; }
|
|
25
|
+
render(renderer: THREE.WebGLRenderer, _write: THREE.WebGLRenderTarget, read: THREE.WebGLRenderTarget) {
|
|
26
|
+
const previous = renderer.getRenderTarget();
|
|
27
|
+
this.target.setSize(read.width, read.height);
|
|
28
|
+
this.material.uniforms.sourceDepth.value = read.depthTexture;
|
|
29
|
+
try { renderer.setRenderTarget(this.target); this.quad.render(renderer); }
|
|
30
|
+
finally { renderer.setRenderTarget(previous); }
|
|
31
|
+
}
|
|
32
|
+
dispose() {
|
|
33
|
+
if (this.disposed) return;
|
|
34
|
+
this.disposed = true; this.target.dispose(); this.material.dispose(); this.quad.dispose();
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** Sharp final-resolution print, using scene depth instead of redrawing geometry. */
|
|
39
|
+
export class ArchiveLabelRenderer {
|
|
40
|
+
readonly capture = new LabelDepthCapture();
|
|
41
|
+
private readonly uniforms = {
|
|
42
|
+
archiveSceneDepth: { value: this.capture.target.texture },
|
|
43
|
+
archiveOutputSize: { value: new THREE.Vector2(1, 1) },
|
|
44
|
+
archiveDepthSize: { value: new THREE.Vector2(1, 1) },
|
|
45
|
+
archiveCameraNear: { value: 5 }, archiveCameraFar: { value: 300 },
|
|
46
|
+
};
|
|
47
|
+
private active = false;
|
|
48
|
+
private disposed = false;
|
|
49
|
+
|
|
50
|
+
attach(composer: EffectComposer) {
|
|
51
|
+
// The normal beauty pass already computes these values. Both alternating
|
|
52
|
+
// targets need an attachment; the capture is one fullscreen GPU copy only.
|
|
53
|
+
for (const target of [composer.renderTarget1, composer.renderTarget2]) {
|
|
54
|
+
target.depthTexture = new THREE.DepthTexture(1, 1, THREE.UnsignedIntType);
|
|
55
|
+
}
|
|
56
|
+
composer.insertPass(this.capture, 1);
|
|
57
|
+
}
|
|
58
|
+
createMaterial(map: THREE.Texture, options: { alignDepthSamples?: boolean } = {}) {
|
|
59
|
+
const material = new THREE.MeshBasicMaterial({ map, toneMapped: false,
|
|
60
|
+
transparent: true, depthWrite: false, depthTest: false, fog: false });
|
|
61
|
+
material.onBeforeCompile = shader => {
|
|
62
|
+
Object.assign(shader.uniforms, this.uniforms);
|
|
63
|
+
shader.fragmentShader = `uniform sampler2D archiveSceneDepth;
|
|
64
|
+
uniform vec2 archiveOutputSize;
|
|
65
|
+
uniform vec2 archiveDepthSize;
|
|
66
|
+
uniform float archiveCameraNear;
|
|
67
|
+
uniform float archiveCameraFar;
|
|
68
|
+
#include <packing>\n` + shader.fragmentShader;
|
|
69
|
+
shader.fragmentShader = shader.fragmentShader.replace('#include <clipping_planes_fragment>',
|
|
70
|
+
`#include <clipping_planes_fragment>
|
|
71
|
+
float sceneDepth = unpackRGBAToDepth(texture2D(archiveSceneDepth, gl_FragCoord.xy / archiveOutputSize));
|
|
72
|
+
float sceneDistance = -perspectiveDepthToViewZ(sceneDepth, archiveCameraNear, archiveCameraFar);
|
|
73
|
+
float printDepth = gl_FragCoord.z;
|
|
74
|
+
${options.alignDepthSamples ? `
|
|
75
|
+
// Compare both planes at the coarse depth texel's center. Otherwise a
|
|
76
|
+
// slanted sheet intermittently occludes itself on the finer print grid.
|
|
77
|
+
vec2 depthPixel = floor(gl_FragCoord.xy / archiveOutputSize * archiveDepthSize);
|
|
78
|
+
vec2 samplePosition = (depthPixel + 0.5) / archiveDepthSize * archiveOutputSize;
|
|
79
|
+
printDepth += dot(vec2(dFdx(gl_FragCoord.z), dFdy(gl_FragCoord.z)), samplePosition - gl_FragCoord.xy);
|
|
80
|
+
` : ''}
|
|
81
|
+
float printDistance = -perspectiveDepthToViewZ(printDepth, archiveCameraNear, archiveCameraFar);
|
|
82
|
+
if (printDistance > sceneDistance + 0.006) discard;`);
|
|
83
|
+
};
|
|
84
|
+
material.customProgramCacheKey = () => options.alignDepthSamples
|
|
85
|
+
? 'archive-print-scene-depth-aligned-v1' : 'archive-print-scene-depth-v1';
|
|
86
|
+
return material;
|
|
87
|
+
}
|
|
88
|
+
prepare(scene: THREE.Scene) {
|
|
89
|
+
this.active = false;
|
|
90
|
+
scene.traverseVisible(object => {
|
|
91
|
+
if (this.active || !(object instanceof THREE.Mesh) || !object.layers.isEnabled(ARCHIVE_LABEL_LAYER)) return;
|
|
92
|
+
const materials = Array.isArray(object.material) ? object.material : [object.material];
|
|
93
|
+
this.active = materials.some(material => material.visible && material.opacity > 0);
|
|
94
|
+
});
|
|
95
|
+
this.capture.enabled = this.active;
|
|
96
|
+
}
|
|
97
|
+
render(renderer: THREE.WebGLRenderer, scene: THREE.Scene, camera: THREE.PerspectiveCamera) {
|
|
98
|
+
if (this.disposed || !this.active) return;
|
|
99
|
+
renderer.getDrawingBufferSize(this.uniforms.archiveOutputSize.value);
|
|
100
|
+
this.uniforms.archiveDepthSize.value.set(this.capture.target.width, this.capture.target.height);
|
|
101
|
+
this.uniforms.archiveCameraNear.value = camera.near;
|
|
102
|
+
this.uniforms.archiveCameraFar.value = camera.far;
|
|
103
|
+
const autoClear = renderer.autoClear, background = scene.background;
|
|
104
|
+
const override = scene.overrideMaterial, layers = camera.layers.mask;
|
|
105
|
+
const target = renderer.getRenderTarget(), face = renderer.getActiveCubeFace(), mip = renderer.getActiveMipmapLevel();
|
|
106
|
+
const shadowAuto = renderer.shadowMap.autoUpdate, shadowUpdate = renderer.shadowMap.needsUpdate;
|
|
107
|
+
try {
|
|
108
|
+
renderer.autoClear = false; scene.background = null; scene.overrideMaterial = null;
|
|
109
|
+
renderer.shadowMap.autoUpdate = false; renderer.shadowMap.needsUpdate = false;
|
|
110
|
+
renderer.setRenderTarget(null); camera.layers.set(ARCHIVE_LABEL_LAYER);
|
|
111
|
+
renderer.render(scene, camera);
|
|
112
|
+
} finally {
|
|
113
|
+
renderer.autoClear = autoClear; scene.background = background; scene.overrideMaterial = override;
|
|
114
|
+
camera.layers.mask = layers; renderer.shadowMap.autoUpdate = shadowAuto; renderer.shadowMap.needsUpdate = shadowUpdate;
|
|
115
|
+
renderer.setRenderTarget(target, face, mip);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
dispose() { if (!this.disposed) { this.disposed = true; this.capture.dispose(); } }
|
|
119
|
+
}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import * as THREE from "three";
|
|
2
|
+
import { ARCHIVE_LABEL_WIDTH, ARCHIVE_LABEL_HEIGHT, archiveLabelKey, paintArchiveLabel, type ArchiveLabel } from "./archive-label";
|
|
3
|
+
import type { RenderPerformanceCapture } from "../temporary-performance";
|
|
4
|
+
|
|
5
|
+
type Family = "hero-label" | "moving-label" | "shelf-label";
|
|
6
|
+
type Entry = { id: number; texture: THREE.CanvasTexture; refs: number; used: number; prepared: boolean };
|
|
7
|
+
export type LabelPrefetchContext = { plan: number; slot: number; intent?: 'row' | 'lane' | 'pointer' | 'activity' };
|
|
8
|
+
|
|
9
|
+
/** Immutable prints; active owners and the bounded next-navigation working set share storage. */
|
|
10
|
+
export class ArchiveLabelTextures {
|
|
11
|
+
private readonly entries = new Map<string, Entry>();
|
|
12
|
+
private readonly bindings = new Map<THREE.MeshBasicMaterial, Entry>();
|
|
13
|
+
private candidates = new Set<string>();
|
|
14
|
+
private readonly evicted = new Map<string, number>();
|
|
15
|
+
private sequence = 0;
|
|
16
|
+
private clock = 0;
|
|
17
|
+
private hits = 0;
|
|
18
|
+
private misses = 0;
|
|
19
|
+
private evictions = 0;
|
|
20
|
+
private disposed = false;
|
|
21
|
+
constructor(private readonly mark: HTMLImageElement, private readonly anisotropy: number,
|
|
22
|
+
private readonly probe: () => RenderPerformanceCapture | undefined, private readonly idleLimit = 6) {}
|
|
23
|
+
private key(content: ArchiveLabel) { return `${archiveLabelKey(content)}:${document.fonts?.status || "loaded"}`; }
|
|
24
|
+
private get(content: ArchiveLabel, family: Family, use: 'bind' | 'prefetch', context?: LabelPrefetchContext) {
|
|
25
|
+
if (this.disposed) throw new Error("Archive label cache is disposed");
|
|
26
|
+
const key = this.key(content);
|
|
27
|
+
let entry = this.entries.get(key);
|
|
28
|
+
const hit = Boolean(entry), previousEntry = this.evicted.get(key);
|
|
29
|
+
if (entry) this.hits++;
|
|
30
|
+
else {
|
|
31
|
+
this.misses++;
|
|
32
|
+
const id = ++this.sequence;
|
|
33
|
+
const canvas = document.createElement("canvas"); canvas.width = ARCHIVE_LABEL_WIDTH; canvas.height = ARCHIVE_LABEL_HEIGHT;
|
|
34
|
+
const texture = new THREE.CanvasTexture(canvas);
|
|
35
|
+
texture.colorSpace = THREE.SRGBColorSpace; texture.anisotropy = this.anisotropy;
|
|
36
|
+
texture.userData.archivePrint = true; texture.userData.sharedArchiveLabel = true;
|
|
37
|
+
const created: Entry = { id, texture, refs: 0, used: 0, prepared: false };
|
|
38
|
+
// Natural draws and explicit preloads both submit the texture. The probe
|
|
39
|
+
// chains this callback; submission does not imply GPU completion.
|
|
40
|
+
texture.onUpdate = () => { created.prepared = true; };
|
|
41
|
+
const paint = () => { paintArchiveLabel(canvas, this.mark, content); texture.needsUpdate = true; };
|
|
42
|
+
try { const probe = this.probe(); if (probe) probe.texturePaint(texture, family, paint, id); else paint(); }
|
|
43
|
+
catch (error) { texture.dispose(); throw error; }
|
|
44
|
+
entry = created; this.entries.set(key, entry);
|
|
45
|
+
}
|
|
46
|
+
this.probe()?.observeTexture(entry.texture, family, entry.id);
|
|
47
|
+
entry.used = ++this.clock;
|
|
48
|
+
this.probe()?.diagnostic('resourceUpdates', { kind: 'label-cache', family, use, hit, cacheEntry: entry.id,
|
|
49
|
+
prepared: entry.prepared, candidate: this.candidates.has(key), fontStatus: document.fonts?.status || 'loaded',
|
|
50
|
+
missReason: hit ? undefined : previousEntry ? 'evicted' : 'not-resident', previousEntry: hit ? undefined : previousEntry,
|
|
51
|
+
...context, ...this.stats() });
|
|
52
|
+
return entry;
|
|
53
|
+
}
|
|
54
|
+
/** Protect at most idleLimit candidates; never increase the idle texture budget. */
|
|
55
|
+
setPrefetchCandidates(contents: readonly ArchiveLabel[]) {
|
|
56
|
+
if (this.disposed) return;
|
|
57
|
+
this.candidates = new Set(contents.map(content => this.key(content)).slice(0, this.idleLimit));
|
|
58
|
+
this.trim();
|
|
59
|
+
}
|
|
60
|
+
bind(material: THREE.MeshBasicMaterial, content: ArchiveLabel, family: Family) {
|
|
61
|
+
const current = this.bindings.get(material);
|
|
62
|
+
if (current && this.entries.get(this.key(content)) === current) return current.texture;
|
|
63
|
+
const next = this.get(content, family, 'bind');
|
|
64
|
+
next.refs++;
|
|
65
|
+
if (current) { current.refs--; current.used = ++this.clock; }
|
|
66
|
+
this.bindings.set(material, next); material.map = next.texture; this.trim();
|
|
67
|
+
return next.texture;
|
|
68
|
+
}
|
|
69
|
+
/** Keep the exact visible print when returning, including across font readiness changes. */
|
|
70
|
+
share(source: THREE.MeshBasicMaterial, target: THREE.MeshBasicMaterial) {
|
|
71
|
+
const next = this.bindings.get(source); if (!next) return undefined;
|
|
72
|
+
const current = this.bindings.get(target);
|
|
73
|
+
this.probe()?.observeTexture(next.texture, 'moving-label', next.id);
|
|
74
|
+
if (current !== next) {
|
|
75
|
+
next.refs++; if (current) { current.refs--; current.used = ++this.clock; }
|
|
76
|
+
next.used = ++this.clock; this.bindings.set(target, next); target.map = next.texture;
|
|
77
|
+
this.hits++; this.trim();
|
|
78
|
+
this.probe()?.diagnostic('resourceUpdates', { kind: 'label-cache', family: 'moving-label', use: 'share', hit: true,
|
|
79
|
+
cacheEntry: next.id, prepared: next.prepared, reason: 'retain-visible-print', ...this.stats() });
|
|
80
|
+
}
|
|
81
|
+
return next.texture;
|
|
82
|
+
}
|
|
83
|
+
prepare(content: ArchiveLabel, renderer: THREE.WebGLRenderer, context?: LabelPrefetchContext) {
|
|
84
|
+
const entry = this.get(content, 'hero-label', 'prefetch', context);
|
|
85
|
+
renderer.initTexture(entry.texture); entry.prepared = true; this.trim();
|
|
86
|
+
this.probe()?.diagnostic('resourceUpdates', { kind: 'label-prefetch-submitted', cacheEntry: entry.id, ...context });
|
|
87
|
+
}
|
|
88
|
+
invalidateResidency() { for (const entry of this.entries.values()) entry.prepared = false; }
|
|
89
|
+
isPrepared(content: ArchiveLabel) { return this.entries.get(this.key(content))?.prepared === true; }
|
|
90
|
+
release(material: THREE.MeshBasicMaterial) {
|
|
91
|
+
const entry = this.bindings.get(material); if (!entry) return;
|
|
92
|
+
this.bindings.delete(material); entry.refs--; entry.used = ++this.clock; this.trim();
|
|
93
|
+
}
|
|
94
|
+
private trim() {
|
|
95
|
+
const idle = [...this.entries.entries()].filter(([, entry]) => entry.refs === 0).sort((a, b) =>
|
|
96
|
+
Number(this.candidates.has(a[0])) - Number(this.candidates.has(b[0])) || a[1].used - b[1].used);
|
|
97
|
+
for (const [key, entry] of idle.slice(0, Math.max(0, idle.length - this.idleLimit))) {
|
|
98
|
+
this.entries.delete(key); entry.texture.dispose(); this.evictions++;
|
|
99
|
+
this.evicted.delete(key); this.evicted.set(key, entry.id);
|
|
100
|
+
if (this.evicted.size > 32) this.evicted.delete(this.evicted.keys().next().value!);
|
|
101
|
+
this.probe()?.diagnostic('resourceUpdates', { kind: 'label-cache-evict', cacheEntry: entry.id,
|
|
102
|
+
reason: this.candidates.has(key) ? 'candidate-budget' : 'idle-budget', prepared: entry.prepared, ...this.stats() });
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
stats() {
|
|
106
|
+
let active = 0, protectedIdle = 0;
|
|
107
|
+
for (const [key, entry] of this.entries) { if (entry.refs) active++; else if (this.candidates.has(key)) protectedIdle++; }
|
|
108
|
+
return { textures: this.entries.size, active, idle: this.entries.size - active, references: this.bindings.size,
|
|
109
|
+
idleLimit: this.idleLimit, protectedIdle, candidates: this.candidates.size, hits: this.hits, misses: this.misses, evictions: this.evictions,
|
|
110
|
+
rgba8BaseBytes: this.entries.size * ARCHIVE_LABEL_WIDTH * ARCHIVE_LABEL_HEIGHT * 4 };
|
|
111
|
+
}
|
|
112
|
+
dispose() {
|
|
113
|
+
if (this.disposed) return; this.disposed = true;
|
|
114
|
+
for (const entry of this.entries.values()) entry.texture.dispose();
|
|
115
|
+
this.entries.clear(); this.bindings.clear(); this.candidates.clear(); this.evicted.clear();
|
|
116
|
+
}
|
|
117
|
+
}
|