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,142 @@
|
|
|
1
|
+
import * as THREE from "three";
|
|
2
|
+
import { bakeArrayInterior, type ArrayInteriorBake } from "./array-interior";
|
|
3
|
+
|
|
4
|
+
// Keep the complete shell, engraved perimeter, fasteners and printed label.
|
|
5
|
+
// Only geometry inside the substrate's front face is replaced by radiance.
|
|
6
|
+
export const SHELF_INTERIOR_SURFACES = new Set([
|
|
7
|
+
"Internal_Ceramic", "Optical_Edges", "Subsurface_Optics",
|
|
8
|
+
"Optical_Film_Edge", "Amber_Optical_Inlay", "Optical_Film",
|
|
9
|
+
"Champagne_Index", "Moulded_Lettering",
|
|
10
|
+
]);
|
|
11
|
+
// Perspective ray through the middle of the shelf, rather than the camera's
|
|
12
|
+
// optical axis (the organizer sits to the right of the frame).
|
|
13
|
+
const DIRECTION = new THREE.Vector3(-1.79, 0.71, 1).normalize();
|
|
14
|
+
type Representation = { full: THREE.Mesh[]; proxy: THREE.Mesh[]; views: THREE.Mesh[][]; view: number; detailed: boolean; reason: "motion" | "angle" | "texture"; parallaxPixels: number; cameraDepth: number; eyeSlope: [number, number] | null };
|
|
15
|
+
|
|
16
|
+
/** Shared interior image; the original meshes remain ready for extraction. */
|
|
17
|
+
export class ShelfInterior {
|
|
18
|
+
private readonly files = new Map<THREE.Group, Representation>();
|
|
19
|
+
private readonly eye = new THREE.Vector3();
|
|
20
|
+
private readonly center = new THREE.Vector3();
|
|
21
|
+
private readonly bake: ArrayInteriorBake;
|
|
22
|
+
private readonly views: { direction: THREE.Vector3; bake: ArrayInteriorBake }[] = [];
|
|
23
|
+
|
|
24
|
+
constructor(renderer: THREE.WebGLRenderer, scene: THREE.Scene, template: THREE.Group) {
|
|
25
|
+
this.bake = bakeArrayInterior(renderer, scene,
|
|
26
|
+
template.children.filter((mesh): mesh is THREE.Mesh => mesh instanceof THREE.Mesh
|
|
27
|
+
&& (SHELF_INTERIOR_SURFACES.has(mesh.userData.surface) || mesh.userData.surface === "Optical_Diffuser")), {
|
|
28
|
+
surfaces: SHELF_INTERIOR_SURFACES, direction: DIRECTION, maxSize: 1536,
|
|
29
|
+
preserveOutsideSubstrate: true, name: "Shelf_Interior",
|
|
30
|
+
// The overview's shadow-casting key is behind this front face. Its
|
|
31
|
+
// shadow cannot darken ambient radiance: the array's blanket multiplier
|
|
32
|
+
// otherwise makes the stationary shelf interiors visibly too dark.
|
|
33
|
+
shadowStrength: 0,
|
|
34
|
+
});
|
|
35
|
+
this.views.push({ direction: DIRECTION, bake: this.bake });
|
|
36
|
+
try { renderer.initTexture(this.bake.texture); }
|
|
37
|
+
catch (error) { this.bake.dispose(); throw error; }
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
hasView(index: number) { return index >= 0 && index < this.views.length; }
|
|
41
|
+
|
|
42
|
+
/** Additional front-facing views share the same original shell and error budget. */
|
|
43
|
+
addView(renderer: THREE.WebGLRenderer, scene: THREE.Scene, template: THREE.Group, direction: THREE.Vector3) {
|
|
44
|
+
const bake = bakeArrayInterior(renderer, scene,
|
|
45
|
+
template.children.filter((mesh): mesh is THREE.Mesh => mesh instanceof THREE.Mesh
|
|
46
|
+
&& (SHELF_INTERIOR_SURFACES.has(mesh.userData.surface) || mesh.userData.surface === "Optical_Diffuser")),
|
|
47
|
+
{ surfaces: SHELF_INTERIOR_SURFACES, direction, maxSize: 1536, preserveOutsideSubstrate: true, name: "Shelf_Interior", shadowStrength: 0 });
|
|
48
|
+
try { renderer.initTexture(bake.texture); } catch (error) { bake.dispose(); throw error; }
|
|
49
|
+
this.views.push({ direction: direction.clone().normalize(), bake });
|
|
50
|
+
for (const [group, file] of this.files) file.views.push(this.attachProxy(group, bake));
|
|
51
|
+
}
|
|
52
|
+
private attachProxy(group: THREE.Group, bake: ArrayInteriorBake) {
|
|
53
|
+
return bake.meshes.map(source => {
|
|
54
|
+
const mesh = source.clone(); mesh.userData.sharedShelfProxy = true;
|
|
55
|
+
mesh.visible = false; group.add(mesh); return mesh;
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
attach(group: THREE.Group) {
|
|
59
|
+
if (this.files.has(group)) return;
|
|
60
|
+
const full = group.children.filter((mesh): mesh is THREE.Mesh =>
|
|
61
|
+
mesh instanceof THREE.Mesh && SHELF_INTERIOR_SURFACES.has(mesh.userData.surface));
|
|
62
|
+
const views = (this.views ?? [{ bake: this.bake }]).map(view => this.attachProxy(group, view.bake));
|
|
63
|
+
this.files.set(group, { full, proxy: views[0], views, view: 0, detailed: true, reason: "motion", parallaxPixels: 0, cameraDepth: 0, eyeSlope: null });
|
|
64
|
+
group.userData.shelfRepresentation = "geometry";
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
update(group: THREE.Group, moving: boolean, camera: THREE.Camera, pixelHeight: number) {
|
|
68
|
+
const file = this.files.get(group);
|
|
69
|
+
if (!file) return;
|
|
70
|
+
group.updateWorldMatrix(true, false);
|
|
71
|
+
this.center.set(0, 1.85, 0).applyMatrix4(group.matrixWorld).applyMatrix4(camera.matrixWorldInverse);
|
|
72
|
+
this.eye.setFromMatrixPosition(camera.matrixWorld);
|
|
73
|
+
group.worldToLocal(this.eye); this.eye.y -= 1.85;
|
|
74
|
+
const pixelsPerUnit = Math.abs(camera.projectionMatrix.elements[5] * pixelHeight / (2 * this.center.z));
|
|
75
|
+
const views = this.views ?? [{ direction: DIRECTION, bake: this.bake }];
|
|
76
|
+
const error = (direction: THREE.Vector3) => this.eye.z > 0 ? 0.21 * pixelsPerUnit * Math.max(
|
|
77
|
+
Math.abs(this.eye.x / this.eye.z - direction.x / direction.z),
|
|
78
|
+
Math.abs(this.eye.y / this.eye.z - direction.y / direction.z),
|
|
79
|
+
) : Infinity;
|
|
80
|
+
let view = file.view, parallaxPixels = error(views[view].direction);
|
|
81
|
+
// Keep an acceptable current view. Enter another only below the lower
|
|
82
|
+
// threshold, so adjacent captures cannot flicker at their boundary.
|
|
83
|
+
if (file.detailed || parallaxPixels > 3) {
|
|
84
|
+
for (let index = 0; index < views.length; index++) {
|
|
85
|
+
const candidate = error(views[index].direction);
|
|
86
|
+
if (candidate < parallaxPixels) { parallaxPixels = candidate; view = index; }
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
// Return to the authored rack capture whenever it re-enters the lower
|
|
90
|
+
// threshold; arriving through another angle must not change the settled rack.
|
|
91
|
+
const primaryError = error(views[0].direction);
|
|
92
|
+
if (primaryError <= 2.5) { view = 0; parallaxPixels = primaryError; }
|
|
93
|
+
const changing = view !== file.view;
|
|
94
|
+
const detailed = moving || parallaxPixels > (file.detailed || changing ? 2.5 : 3);
|
|
95
|
+
file.reason = moving ? "motion" : detailed ? "angle" : "texture";
|
|
96
|
+
file.parallaxPixels = Number.isFinite(parallaxPixels) ? parallaxPixels : -1;
|
|
97
|
+
file.cameraDepth = -this.center.z;
|
|
98
|
+
// Scalars only; no per-frame arrays allocated for this diagnostic inventory.
|
|
99
|
+
if (this.eye.z > 0) {
|
|
100
|
+
file.eyeSlope ||= [0, 0]; file.eyeSlope[0] = this.eye.x / this.eye.z; file.eyeSlope[1] = this.eye.y / this.eye.z;
|
|
101
|
+
} else file.eyeSlope = null;
|
|
102
|
+
if (file.detailed === detailed && !changing) return;
|
|
103
|
+
file.detailed = detailed; file.view = view;
|
|
104
|
+
for (const mesh of file.full) mesh.visible = detailed;
|
|
105
|
+
for (let index = 0; index < file.views.length; index++)
|
|
106
|
+
for (const mesh of file.views[index]) mesh.visible = !detailed && index === view;
|
|
107
|
+
group.userData.shelfRepresentation = detailed ? "geometry" : "texture";
|
|
108
|
+
group.userData.shelfView = detailed ? -1 : view;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
detach(group: THREE.Group) {
|
|
112
|
+
const file = this.files.get(group);
|
|
113
|
+
if (!file) return;
|
|
114
|
+
for (const mesh of file.full) mesh.visible = true;
|
|
115
|
+
for (const meshes of file.views) for (const mesh of meshes) group.remove(mesh);
|
|
116
|
+
this.files.delete(group);
|
|
117
|
+
delete group.userData.shelfRepresentation;
|
|
118
|
+
delete group.userData.shelfView;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
getStats(detailedSnapshot = false) {
|
|
122
|
+
let textured = 0, detailed = 0, motion = 0, angle = 0, angleErrorMaxPixels = 0, backFacing = 0;
|
|
123
|
+
const files: { slot: number; representation: string; reason: string; view: number; errorPixels: number;
|
|
124
|
+
cameraDepth: number; eyeSlope: [number, number] | null }[] | undefined = detailedSnapshot ? [] : undefined;
|
|
125
|
+
const viewCounts = (this.views ?? [this.bake]).map(() => 0);
|
|
126
|
+
for (const [group, file] of this.files) if (group.visible && group.parent) {
|
|
127
|
+
if (file.reason === 'angle') {
|
|
128
|
+
if (file.parallaxPixels < 0) backFacing++; else angleErrorMaxPixels = Math.max(angleErrorMaxPixels, file.parallaxPixels);
|
|
129
|
+
}
|
|
130
|
+
if (files) files.push({ slot: files.length, representation: file.detailed ? 'geometry' : 'texture', reason: file.reason,
|
|
131
|
+
view: file.view, errorPixels: file.parallaxPixels, cameraDepth: file.cameraDepth, eyeSlope: file.eyeSlope ? [...file.eyeSlope] : null });
|
|
132
|
+
if (file.detailed) { detailed++; if (file.reason === "motion") motion++; else angle++; } else { textured++; viewCounts[file.view]++; }
|
|
133
|
+
}
|
|
134
|
+
return { ...this.bake.stats, angleErrorMaxPixels, backFacing,
|
|
135
|
+
...(files ? { files, enterBelowPixels: 2.5, leaveAbovePixels: 3, directions: (this.views ?? [{ direction: DIRECTION }]).map(v => v.direction.toArray()) } : {}), views: viewCounts.length, viewCounts, totalTextureBytes: (this.views ?? [{ bake: this.bake }]).reduce((sum, view) => sum + (view.bake.stats.textureBytes ?? 0), 0), textured, detailed, reasons: { motion, angle, texture: textured } };
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
dispose() {
|
|
139
|
+
for (const group of this.files.keys()) this.detach(group);
|
|
140
|
+
for (const view of this.views ?? [{ bake: this.bake }]) view.bake.dispose();
|
|
141
|
+
}
|
|
142
|
+
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
const enterEase = "cubic-bezier(0.22, 1, 0.36, 1)";
|
|
2
|
+
const exitEase = "cubic-bezier(0.4, 0, 1, 1)";
|
|
3
|
+
|
|
4
|
+
/** Owns the visible lifetime, including a close that interrupts an opening. */
|
|
5
|
+
export class SurfaceTransition {
|
|
6
|
+
private animations: Animation[] = [];
|
|
7
|
+
private revision = 0;
|
|
8
|
+
|
|
9
|
+
constructor(
|
|
10
|
+
private root: HTMLElement,
|
|
11
|
+
private panel?: HTMLElement,
|
|
12
|
+
private enterDuration = 300,
|
|
13
|
+
private exitDuration = 200,
|
|
14
|
+
private exitEasing = exitEase,
|
|
15
|
+
private enterEasing = enterEase,
|
|
16
|
+
private syncTimeline = false,
|
|
17
|
+
) {}
|
|
18
|
+
|
|
19
|
+
show(reduced: boolean) {
|
|
20
|
+
this.run(true, reduced);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
hide(reduced: boolean, finished: () => void = () => {}) {
|
|
24
|
+
this.run(false, reduced, finished);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
finish() {
|
|
28
|
+
this.animations.forEach((animation) => animation.finish());
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
dispose() {
|
|
32
|
+
this.revision++;
|
|
33
|
+
this.animations.forEach((animation) => animation.cancel());
|
|
34
|
+
this.animations = [];
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
private run(show: boolean, reduced: boolean, finished?: () => void) {
|
|
38
|
+
const revision = ++this.revision;
|
|
39
|
+
const hidden = this.root.hidden;
|
|
40
|
+
const opacity = hidden ? "0" : getComputedStyle(this.root).opacity;
|
|
41
|
+
const transform = this.panel
|
|
42
|
+
? hidden
|
|
43
|
+
? "translateY(12px)"
|
|
44
|
+
: getComputedStyle(this.panel).transform
|
|
45
|
+
: undefined;
|
|
46
|
+
this.animations.forEach((animation) => animation.cancel());
|
|
47
|
+
this.animations = [];
|
|
48
|
+
this.root.hidden = false;
|
|
49
|
+
this.root.dataset.transition = show ? "opening" : "closing";
|
|
50
|
+
const complete = () => {
|
|
51
|
+
if (revision !== this.revision) return;
|
|
52
|
+
this.root.hidden = !show;
|
|
53
|
+
this.root.dataset.transition = show ? "open" : "closed";
|
|
54
|
+
this.animations.forEach((animation) => animation.cancel());
|
|
55
|
+
this.animations = [];
|
|
56
|
+
finished?.();
|
|
57
|
+
};
|
|
58
|
+
if (reduced || (!show && hidden)) {
|
|
59
|
+
complete();
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
const options: KeyframeAnimationOptions = {
|
|
63
|
+
duration: show ? this.enterDuration : this.exitDuration,
|
|
64
|
+
easing: show ? this.enterEasing : this.exitEasing,
|
|
65
|
+
fill: "both",
|
|
66
|
+
};
|
|
67
|
+
const fade = this.root.animate(
|
|
68
|
+
[{ opacity }, { opacity: show ? 1 : 0 }],
|
|
69
|
+
options,
|
|
70
|
+
);
|
|
71
|
+
this.animations.push(fade);
|
|
72
|
+
if (this.panel) {
|
|
73
|
+
this.animations.push(
|
|
74
|
+
this.panel.animate(
|
|
75
|
+
[
|
|
76
|
+
{ transform },
|
|
77
|
+
{ transform: show ? "translateY(0)" : "translateY(8px)" },
|
|
78
|
+
],
|
|
79
|
+
options,
|
|
80
|
+
),
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
// Shared with scene animation when the render frame takes longer than a browser frame.
|
|
84
|
+
if (this.syncTimeline && document.timeline.currentTime !== null) {
|
|
85
|
+
for (const animation of this.animations) animation.startTime = document.timeline.currentTime;
|
|
86
|
+
}
|
|
87
|
+
void fade.finished.then(complete).catch(() => {});
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export class ContentTransition {
|
|
92
|
+
private animation?: Animation;
|
|
93
|
+
|
|
94
|
+
reveal(element: HTMLElement, reduced: boolean) {
|
|
95
|
+
const opacity =
|
|
96
|
+
this.animation?.playState === "running"
|
|
97
|
+
? getComputedStyle(element).opacity
|
|
98
|
+
: "0.35";
|
|
99
|
+
this.cancel();
|
|
100
|
+
if (!reduced)
|
|
101
|
+
this.animation = element.animate([{ opacity }, { opacity: 1 }], {
|
|
102
|
+
duration: 150,
|
|
103
|
+
easing: enterEase,
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
cancel() {
|
|
108
|
+
this.animation?.cancel();
|
|
109
|
+
this.animation = undefined;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import * as THREE from "three";
|
|
2
|
+
|
|
3
|
+
const shortestAngle = (angle: number) =>
|
|
4
|
+
Math.atan2(Math.sin(angle), Math.cos(angle));
|
|
5
|
+
|
|
6
|
+
// OrbitControls records the requested pose; this camera is what is rendered.
|
|
7
|
+
// Spherical interpolation avoids cutting through the model on a large reset.
|
|
8
|
+
export class ViewerCameraMotion {
|
|
9
|
+
readonly focus = new THREE.Vector3();
|
|
10
|
+
private pose = new THREE.Spherical();
|
|
11
|
+
private desired = new THREE.Spherical();
|
|
12
|
+
private offset = new THREE.Vector3();
|
|
13
|
+
private resetFrom = new THREE.Spherical();
|
|
14
|
+
private resetFocus = new THREE.Vector3();
|
|
15
|
+
private elapsed = 0;
|
|
16
|
+
resetting = false;
|
|
17
|
+
|
|
18
|
+
constructor(private camera: THREE.PerspectiveCamera) {}
|
|
19
|
+
|
|
20
|
+
snap(goal: THREE.PerspectiveCamera, target: THREE.Vector3) {
|
|
21
|
+
this.resetting = false;
|
|
22
|
+
this.focus.copy(target);
|
|
23
|
+
this.pose.setFromVector3(this.offset.copy(goal.position).sub(target));
|
|
24
|
+
this.apply();
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
reset() {
|
|
28
|
+
this.resetFrom.copy(this.pose);
|
|
29
|
+
this.resetFocus.copy(this.focus);
|
|
30
|
+
this.elapsed = 0;
|
|
31
|
+
this.resetting = true;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
interruptReset(goal: THREE.PerspectiveCamera, target: THREE.Vector3) {
|
|
35
|
+
if (!this.resetting) return;
|
|
36
|
+
this.resetting = false;
|
|
37
|
+
goal.position.copy(this.camera.position);
|
|
38
|
+
target.copy(this.focus);
|
|
39
|
+
goal.lookAt(target);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
update(
|
|
43
|
+
goal: THREE.PerspectiveCamera,
|
|
44
|
+
target: THREE.Vector3,
|
|
45
|
+
dt: number,
|
|
46
|
+
reduced = false,
|
|
47
|
+
) {
|
|
48
|
+
if (reduced) {
|
|
49
|
+
this.snap(goal, target);
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
this.desired.setFromVector3(this.offset.copy(goal.position).sub(target));
|
|
53
|
+
if (this.resetting) {
|
|
54
|
+
this.elapsed += Math.max(0, dt);
|
|
55
|
+
const t = Math.min(1, this.elapsed / 0.56);
|
|
56
|
+
const p = 1 - (1 - t) ** 3;
|
|
57
|
+
this.focus.lerpVectors(this.resetFocus, target, p);
|
|
58
|
+
this.interpolate(this.resetFrom, this.desired, p, p);
|
|
59
|
+
if (t === 1) this.resetting = false;
|
|
60
|
+
} else {
|
|
61
|
+
const pan = 1 - Math.exp(-13 * Math.max(0, dt));
|
|
62
|
+
const orbit = 1 - Math.exp(-9 * Math.max(0, dt));
|
|
63
|
+
const zoom = 1 - Math.exp(-15 * Math.max(0, dt));
|
|
64
|
+
this.focus.lerp(target, pan);
|
|
65
|
+
this.interpolate(this.pose, this.desired, orbit, zoom);
|
|
66
|
+
if (this.focus.distanceToSquared(target) < 1e-10) this.focus.copy(target);
|
|
67
|
+
}
|
|
68
|
+
this.apply();
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
private interpolate(
|
|
72
|
+
from: THREE.Spherical,
|
|
73
|
+
to: THREE.Spherical,
|
|
74
|
+
angle: number,
|
|
75
|
+
radius: number,
|
|
76
|
+
) {
|
|
77
|
+
// Interpolate logarithmic distance so zoom in and zoom out feel symmetric.
|
|
78
|
+
this.pose.radius = Math.exp(
|
|
79
|
+
THREE.MathUtils.lerp(Math.log(from.radius), Math.log(to.radius), radius),
|
|
80
|
+
);
|
|
81
|
+
this.pose.phi = THREE.MathUtils.lerp(from.phi, to.phi, angle);
|
|
82
|
+
this.pose.theta = from.theta + shortestAngle(to.theta - from.theta) * angle;
|
|
83
|
+
this.pose.makeSafe();
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
private apply() {
|
|
87
|
+
this.camera.position
|
|
88
|
+
.copy(this.offset.setFromSpherical(this.pose))
|
|
89
|
+
.add(this.focus);
|
|
90
|
+
this.camera.lookAt(this.focus);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/** Reference coordinates remain exact during the film and at 1920 × 1080. */
|
|
2
|
+
export function viewportLayout(width: number, height: number, coarse: boolean, cinematic = false) {
|
|
3
|
+
width = Math.max(1, width);
|
|
4
|
+
height = Math.max(1, height);
|
|
5
|
+
if (cinematic) return {
|
|
6
|
+
width: 1920, height: 1080, scale: Math.min(width / 1920, height / 1080),
|
|
7
|
+
kind: "cinematic" as const,
|
|
8
|
+
};
|
|
9
|
+
const portrait = width / height < 1.05;
|
|
10
|
+
const compact = portrait || width < 1100 || (coarse && height < 600);
|
|
11
|
+
const scale = compact ? 1 : height / 1080;
|
|
12
|
+
return { width: width / scale, height: height / scale, scale,
|
|
13
|
+
kind: portrait ? "portrait" as const : compact ? "compact" as const : "desktop" as const };
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** Preserve the long-lens perspective. Reframe only the camera, never the card. */
|
|
17
|
+
export function archiveFraming(width: number, height: number, span: number, detail: number, compact: boolean) {
|
|
18
|
+
const aspect = width / height;
|
|
19
|
+
const portrait = aspect < 1.05;
|
|
20
|
+
const baseSpan = span + (5.9 - span) * detail;
|
|
21
|
+
const portraitDetailSpan = Math.max(6.3 / aspect, 3.7 * height / Math.max(100, 0.54 * height - 156));
|
|
22
|
+
const viewSpan = portrait
|
|
23
|
+
? Math.max(baseSpan, 8.4 / aspect + (portraitDetailSpan - 8.4 / aspect) * detail)
|
|
24
|
+
: Math.max(baseSpan, baseSpan * (16 / 9) / aspect);
|
|
25
|
+
return {
|
|
26
|
+
span: viewSpan,
|
|
27
|
+
portrait,
|
|
28
|
+
// Portrait selection is deliberately above its title and navigation.
|
|
29
|
+
previewY: portrait ? 0.36 : 0.5,
|
|
30
|
+
detailX: portrait ? 0.5 : compact ? 0.27 : 550 / 1920,
|
|
31
|
+
detailY: portrait ? 0.27 + 34 / height : compact ? 0.49 : 560 / 1080,
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function swipeDirection(dx: number, dy: number, elapsed: number) {
|
|
36
|
+
const major = Math.max(Math.abs(dx), Math.abs(dy));
|
|
37
|
+
const minor = Math.min(Math.abs(dx), Math.abs(dy));
|
|
38
|
+
if (major < 36 || major < minor * 1.3 || elapsed > 1400) return null;
|
|
39
|
+
return { axis: Math.abs(dx) > Math.abs(dy) ? "lane" as const : "row" as const,
|
|
40
|
+
direction: (Math.abs(dx) > Math.abs(dy) ? dx : dy) < 0 ? 1 : -1 };
|
|
41
|
+
}
|