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,38 @@
|
|
|
1
|
+
// One bounded page spans both physical tiers; source order fills bottom first.
|
|
2
|
+
export const SHELF_LEVELS = 2;
|
|
3
|
+
export const SHELF_SLOTS_PER_LEVEL = 12;
|
|
4
|
+
export const SHELF_PAGE_SIZE = SHELF_LEVELS * SHELF_SLOTS_PER_LEVEL;
|
|
5
|
+
export const SHELF_SLOT_SPACING = 0.78;
|
|
6
|
+
export const SHELF_LEVEL_SPACING = 4.6;
|
|
7
|
+
export const SHELF_WIDTH = 5.35;
|
|
8
|
+
export const SHELF_DEPTH = (SHELF_SLOTS_PER_LEVEL - 1) * SHELF_SLOT_SPACING + 1.05;
|
|
9
|
+
export const SHELF_HEIGHT = (SHELF_LEVELS - 1) * SHELF_LEVEL_SPACING + 4.3;
|
|
10
|
+
// Clear the entire cassette width before turning or changing tiers.
|
|
11
|
+
export const SHELF_EXIT_DISTANCE = SHELF_WIDTH + 0.65;
|
|
12
|
+
|
|
13
|
+
// The reading station stays outside the rack's full rotation envelope and in
|
|
14
|
+
// front of its nearest row. Moving the camera cannot make an internal target safe.
|
|
15
|
+
export const SHELF_READING_POSITION = {
|
|
16
|
+
x: -SHELF_EXIT_DISTANCE, y: SHELF_HEIGHT / 2 - 1.85, z: SHELF_DEPTH / 2 + 3.2,
|
|
17
|
+
};
|
|
18
|
+
export const SHELF_WITHDRAW_END = 0.38;
|
|
19
|
+
export function shelfReadingPath(index: number, progress: number, focus = 0) {
|
|
20
|
+
const slot = shelfSlot(index);
|
|
21
|
+
const smooth = (v: number) => { const t = Math.max(0, Math.min(1, v)); return t * t * t * (t * (t * 6 - 15) + 10); };
|
|
22
|
+
const turn = smooth((progress - SHELF_WITHDRAW_END) / (1 - SHELF_WITHDRAW_END));
|
|
23
|
+
if (progress < SHELF_WITHDRAW_END) return {
|
|
24
|
+
x: -focus * 0.5 + (-SHELF_EXIT_DISTANCE + focus * 0.5) * smooth(progress / SHELF_WITHDRAW_END),
|
|
25
|
+
y: slot.y, z: slot.z, turn: 0,
|
|
26
|
+
};
|
|
27
|
+
return { x: SHELF_READING_POSITION.x,
|
|
28
|
+
y: slot.y + (SHELF_READING_POSITION.y - slot.y) * turn,
|
|
29
|
+
z: slot.z + (SHELF_READING_POSITION.z - slot.z) * turn, turn };
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function shelfSlot(index: number) {
|
|
33
|
+
const pageSlot = ((Math.trunc(index) % SHELF_PAGE_SIZE) + SHELF_PAGE_SIZE) % SHELF_PAGE_SIZE;
|
|
34
|
+
const level = Math.floor(pageSlot / SHELF_SLOTS_PER_LEVEL);
|
|
35
|
+
const slot = pageSlot % SHELF_SLOTS_PER_LEVEL;
|
|
36
|
+
return { level, slot, x: 0, y: 0.28 + level * SHELF_LEVEL_SPACING,
|
|
37
|
+
z: (slot - (SHELF_SLOTS_PER_LEVEL - 1) / 2) * SHELF_SLOT_SPACING };
|
|
38
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { InvestigationSnapshot } from './types';
|
|
2
|
+
|
|
3
|
+
/** Keep value signatures: snapshots may mutate in place or contain equal new objects. */
|
|
4
|
+
export class SnapshotUpdateGate {
|
|
5
|
+
private session = '';
|
|
6
|
+
private sources = '';
|
|
7
|
+
private status = '';
|
|
8
|
+
constructor(snapshot: InvestigationSnapshot) { this.read(snapshot); }
|
|
9
|
+
read(snapshot: InvestigationSnapshot) {
|
|
10
|
+
// Records and answer text belong to the reading panels. Answer presence still
|
|
11
|
+
// affects the status/report entry; every other field remains significant.
|
|
12
|
+
const { sources, answer, records: _records, ...status } = snapshot;
|
|
13
|
+
const nextSources = JSON.stringify([snapshot.sessionId, sources]);
|
|
14
|
+
const nextStatus = JSON.stringify({ ...status, hasAnswer: Boolean(answer) });
|
|
15
|
+
const sessionChanged = snapshot.sessionId !== this.session;
|
|
16
|
+
this.session = snapshot.sessionId;
|
|
17
|
+
const sourcesChanged = nextSources !== this.sources;
|
|
18
|
+
const statusChanged = sourcesChanged || nextStatus !== this.status;
|
|
19
|
+
this.sources = nextSources; this.status = nextStatus;
|
|
20
|
+
return { sessionChanged, sourcesChanged, statusChanged };
|
|
21
|
+
}
|
|
22
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { ArchiveSource } from './types';
|
|
2
|
+
|
|
3
|
+
/** A citation's reading position is independent of cumulative search/read receipts. */
|
|
4
|
+
export function referenceSource(source: ArchiveSource, ref: { line_start?: number; line_end?: number }): ArchiveSource {
|
|
5
|
+
return ref.line_start ? { ...source, lineStart: ref.line_start, lineEnd: ref.line_end ?? ref.line_start,
|
|
6
|
+
readingRange: { start: ref.line_start, end: ref.line_end ?? ref.line_start } } : source;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function resolveReadingSource(source: ArchiveSource, latest?: ArchiveSource): ArchiveSource {
|
|
10
|
+
return latest ? { ...latest, ...(source.readingRange ? { readingRange: source.readingRange } : {}) } : source;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function readerStartLine(source: ArchiveSource) {
|
|
14
|
+
return Math.max(1, (source.readingRange?.start || source.readRanges?.[0]?.start || source.lineStart || 1) - 3);
|
|
15
|
+
}
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
/* Title-first selectors remain on the HUD plane; the physical archive stays behind them. */
|
|
2
|
+
.rhine-workbench .rhine-original-stage{--choice-ink:#30382d;--choice-muted:#707461;--choice-rule:#7c81642e;--choice-font:max(17px,calc(14px / var(--stage-scale,1)))}
|
|
3
|
+
.rhine-workbench [data-layout] .rhine-source-choice{display:grid;grid-template-columns:33px minmax(0,1fr) 16px;gap:10px;align-items:start;width:100%;height:auto;min-height:70px;margin:0;padding:10px 10px 10px 2px;border:0;border-bottom:1px solid var(--choice-rule);text-align:left;color:var(--choice-ink);background:transparent;transform:none;transition:background .18s,color .18s}
|
|
4
|
+
.rhine-workbench [data-layout] .rhine-choice-number{font-size:11px;font-variant-numeric:tabular-nums;color:var(--choice-muted);padding-top:4px}
|
|
5
|
+
.rhine-workbench [data-layout] .rhine-choice-copy{min-width:0;display:grid;gap:4px}
|
|
6
|
+
.rhine-workbench [data-layout] .rhine-choice-copy strong{font-size:var(--choice-font);font-weight:500;line-height:1.45;overflow-wrap:anywhere;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden}
|
|
7
|
+
.rhine-workbench [data-layout] .rhine-choice-copy small{font-size:11px;line-height:1.5;color:var(--choice-muted)}
|
|
8
|
+
.rhine-workbench [data-layout] .rhine-choice-mark{font-size:16px;opacity:0;padding-top:2px;color:#62714e}
|
|
9
|
+
.rhine-workbench [data-layout] .rhine-source-choice:is(:hover,:focus-visible,.selected,.is-selected){background:transparent;color:#283222;transform:none;border-bottom:1px solid #90967d70}
|
|
10
|
+
.rhine-workbench [data-layout] .rhine-source-choice:is(.selected,.is-selected){box-shadow:inset 2px 0 #78835c;padding-left:9px}
|
|
11
|
+
.rhine-workbench [data-layout] .rhine-source-choice:is(:hover,:focus-visible,.selected,.is-selected) .rhine-choice-mark{opacity:1}
|
|
12
|
+
.rhine-workbench [data-layout] .rhine-source-choice:focus-visible{outline:1px solid #7a845e;outline-offset:-1px}
|
|
13
|
+
.rhine-workbench [data-layout] .rhine-source-choice:is([data-state=read],[data-state=cited]) .rhine-choice-number{color:#52683e}
|
|
14
|
+
.rhine-workbench :is(.file-ticks,.rhine-desk-list,.rhine-rack-index-list,.rhine-result-list){scrollbar-width:thin;scrollbar-color:#82877270 transparent;overscroll-behavior:contain;scroll-padding-block:5px}
|
|
15
|
+
.rhine-workbench :is(.file-ticks,.rhine-desk-list,.rhine-rack-index-list,.rhine-result-list)::-webkit-scrollbar{width:4px}
|
|
16
|
+
.rhine-workbench :is(.file-ticks,.rhine-desk-list,.rhine-rack-index-list,.rhine-result-list)::-webkit-scrollbar-track{background:transparent}
|
|
17
|
+
.rhine-workbench :is(.file-ticks,.rhine-desk-list,.rhine-rack-index-list,.rhine-result-list)::-webkit-scrollbar-thumb{background:#82877270;border-radius:2px}
|
|
18
|
+
.rhine-workbench :is(.file-ticks,.rhine-desk-list,.rhine-rack-index-list,.rhine-result-list)::-webkit-scrollbar-button{display:none}
|
|
19
|
+
|
|
20
|
+
/* The array's former anonymous ticks are now a bounded title directory. */
|
|
21
|
+
.rhine-workbench [data-layout] .archive-navigation{left:var(--edge);top:24%;bottom:235px;width:min(440px,32%);display:flex;flex-direction:column;align-items:stretch;gap:0;transform:none;background:transparent;padding:0 16px 0 0}
|
|
22
|
+
.rhine-workbench [data-layout] .rhine-choice-heading{display:flex;align-items:center;justify-content:space-between;gap:10px;color:var(--choice-muted);font-size:10px;letter-spacing:1px;min-height:28px}
|
|
23
|
+
.rhine-workbench [data-layout] .rhine-choice-heading button{font-size:12px;letter-spacing:0;min-height:32px;white-space:nowrap;color:var(--choice-ink)}
|
|
24
|
+
.rhine-workbench [data-layout] .archive-navigation .column-navigation{position:static;display:flex;flex:none;align-items:center;justify-content:space-between;gap:15px;border-bottom:1px solid #7c816461;padding:10px 0 14px;transform:none}
|
|
25
|
+
.rhine-workbench .archive-navigation .column-navigation>div{flex:1;min-width:0;gap:4px}
|
|
26
|
+
.rhine-workbench .archive-navigation .column-navigation strong{font-size:24px}
|
|
27
|
+
.rhine-workbench .archive-navigation .column-navigation button{height:38px;width:30px;font-size:22px}
|
|
28
|
+
.rhine-workbench [data-layout] .file-ticks{display:flex;flex-direction:column;align-items:stretch;gap:0;min-height:0;max-width:none;flex:1;overflow:auto}
|
|
29
|
+
.rhine-workbench [data-layout] .file-ticks button{flex:none;width:100%;height:auto;min-height:70px}
|
|
30
|
+
.rhine-workbench [data-layout] .file-ticks button:before{display:none}
|
|
31
|
+
.rhine-workbench [data-layout] .rhine-choice-footer{flex:none;display:flex;justify-content:space-between;align-items:center;gap:12px;padding:12px 0;border-top:1px solid var(--choice-rule);font-size:11px;color:var(--choice-muted)}
|
|
32
|
+
.rhine-workbench [data-layout] .rhine-choice-footer button{font-size:12px;min-height:30px;color:var(--choice-ink)}
|
|
33
|
+
.rhine-workbench [data-layout] .rhine-choice-footer button:disabled{opacity:.35}
|
|
34
|
+
.rhine-workbench [data-layout] :is(.archive-counter,.archive-hint){display:none}
|
|
35
|
+
|
|
36
|
+
/* A persistent header and reading action frame the scrollable shelf directory. */
|
|
37
|
+
.rhine-workbench [data-layout] .rhine-desk-panel{left:var(--edge);top:21%;bottom:190px;width:min(460px,35%);max-height:none;overflow:visible;gap:0;background:transparent;padding-right:16px}
|
|
38
|
+
.rhine-workbench [data-layout] .rhine-desk-panel h2{font-size:30px;margin:10px 0 5px}
|
|
39
|
+
.rhine-workbench [data-layout] .rhine-desk-panel>.rhine-rack-intro{font-size:12px;line-height:1.6;margin:0 0 10px;color:var(--choice-muted)}
|
|
40
|
+
.rhine-workbench [data-layout] .rhine-desk-panel>:not(.rhine-shelf-navigation){flex:none}
|
|
41
|
+
.rhine-workbench [data-layout] .rhine-shelf-search{display:flex;align-items:center;gap:9px;border-bottom:1px solid #7c816478;min-height:40px}
|
|
42
|
+
.rhine-workbench [data-layout] .rhine-shelf-search>span{font-size:23px;color:#697153}
|
|
43
|
+
.rhine-workbench [data-layout] .rhine-shelf-filter{background:transparent;border:0;outline:none;box-shadow:none;font:inherit;font-size:13px;min-width:0;flex:1;padding:10px 0;color:var(--choice-ink)}
|
|
44
|
+
.rhine-workbench [data-layout] .rhine-shelf-filter::placeholder{color:#737665}
|
|
45
|
+
.rhine-workbench [data-layout] .rhine-shelf-filter::-webkit-search-cancel-button{display:none}
|
|
46
|
+
.rhine-workbench [data-layout] .rhine-shelf-search:focus-within{border-color:#65734c}
|
|
47
|
+
.rhine-workbench [data-layout] .rhine-shelf-clear{width:30px;min-height:32px;font-size:19px}
|
|
48
|
+
.rhine-workbench [data-layout] .rhine-rack-range{font-size:11px;color:var(--choice-muted);margin:10px 0 5px}
|
|
49
|
+
.rhine-workbench [data-layout] .rhine-shelf-navigation{display:flex;flex:1;min-height:80px;gap:0;margin:0;align-items:stretch;order:1}
|
|
50
|
+
.rhine-workbench [data-layout] .rhine-shelf-navigation>button{display:none}
|
|
51
|
+
.rhine-workbench [data-layout] .rhine-desk-list{display:flex;flex-direction:column;min-height:0;overflow:auto;gap:0;padding-right:5px;border-bottom:1px solid var(--choice-rule)}
|
|
52
|
+
.rhine-workbench [data-layout] .rhine-desk-item{flex:none;display:grid;font-size:inherit;text-align:left;border-bottom-width:1px;min-height:70px}
|
|
53
|
+
.rhine-workbench [data-layout] .rhine-desk-pagination{order:2;display:flex;justify-content:space-between;align-items:center;padding:3px 0;margin:0;min-height:32px;color:var(--choice-muted);font-size:11px}
|
|
54
|
+
.rhine-workbench [data-layout] .rhine-desk-pagination button{font-size:12px;min-height:30px}
|
|
55
|
+
.rhine-workbench [data-layout] .rhine-desk-pagination[hidden]{display:none}
|
|
56
|
+
.rhine-workbench [data-layout] .rhine-shelf-current{order:3;position:relative;border-block:0;border-top:1px solid #7c816461;padding:12px 0 0;margin:0}
|
|
57
|
+
.rhine-workbench [data-layout] .rhine-shelf-number{font-size:11px;display:block;padding-right:100px}
|
|
58
|
+
.rhine-workbench [data-layout] .rhine-shelf-title{font-size:17px;line-height:1.5;margin:10px 0 6px;-webkit-line-clamp:2}
|
|
59
|
+
.rhine-workbench [data-layout] .rhine-shelf-meta{font-size:11px;color:var(--choice-muted)}
|
|
60
|
+
.rhine-workbench [data-layout] .rhine-shelf-excerpt{font-size:12px;line-height:1.6;-webkit-line-clamp:2;margin:6px 0;color:var(--choice-muted)}
|
|
61
|
+
.rhine-workbench [data-layout] .rhine-shelf-open{position:absolute;right:0;top:3px;width:auto;gap:12px;min-height:34px;margin:0;font-size:13px;color:#39482c}
|
|
62
|
+
.rhine-workbench [data-layout] .rhine-rack-directory{order:4;min-height:34px;margin-top:5px;font-size:12px;border-top:1px solid var(--choice-rule)}
|
|
63
|
+
.rhine-workbench [data-layout] .rhine-rack-legend,.rhine-workbench [data-layout] .rhine-shelf-report{display:none}
|
|
64
|
+
|
|
65
|
+
/* Directory, extracts and receipts use the same thin, translucent reading surface. */
|
|
66
|
+
.rhine-workbench [data-layout] :is(.rhine-results,.rhine-rack-index,.rhine-basket,.rhine-log).modal-backdrop{background:linear-gradient(90deg,#eeece57a,#eeece5c9 28%,#eeece5b3 75%,#eeece573);backdrop-filter:blur(7px);padding:0}
|
|
67
|
+
.rhine-workbench [data-layout] :is(.rhine-index-modal,.rhine-rack-index-modal,.rhine-basket-modal,.rhine-log-modal){width:min(1250px,calc(100% - 2 * var(--edge)));max-width:none;height:78%;max-height:880px;min-height:0;padding:24px 28px 18px;background:linear-gradient(115deg,#f1efe668,#f1efe624);border:0;border-block:1px solid #83896e70;box-shadow:none;color:var(--choice-ink)}
|
|
68
|
+
.rhine-workbench [data-layout] :is(.rhine-index-modal,.rhine-rack-index-modal,.rhine-basket-modal,.rhine-log-modal) .modal-top{padding-bottom:10px;min-height:40px;font-size:11px;border-bottom:1px solid var(--choice-rule)}
|
|
69
|
+
.rhine-workbench [data-layout] :is(.rhine-index-modal,.rhine-rack-index-modal,.rhine-basket-modal,.rhine-log-modal) .modal-top button{font-size:12px;gap:12px;color:#4a553c}
|
|
70
|
+
.rhine-workbench [data-layout] :is(.rhine-index-modal,.rhine-rack-index-modal,.rhine-basket-modal,.rhine-log-modal) h2{font-size:30px;font-weight:500;letter-spacing:0;margin:20px 0 18px;gap:18px}
|
|
71
|
+
.rhine-workbench [data-layout] :is(.rhine-index-modal,.rhine-rack-index-modal,.rhine-basket-modal,.rhine-log-modal) h2 small{font-size:11px;letter-spacing:1.7px;color:var(--choice-muted)}
|
|
72
|
+
.rhine-workbench [data-layout] :is(.rhine-index-modal,.rhine-rack-index-modal) .search-field{height:48px;gap:16px;border-bottom:1px solid #6f7959}
|
|
73
|
+
.rhine-workbench [data-layout] :is(.rhine-index-modal,.rhine-rack-index-modal) .search-field input{font-size:16px;background:transparent;border:0;box-shadow:none;outline:none}
|
|
74
|
+
.rhine-workbench [data-layout] :is(.rhine-index-modal,.rhine-rack-index-modal) .search-field:focus-within{box-shadow:0 1px #6f7959}
|
|
75
|
+
.rhine-workbench [data-layout] .rhine-index-modal .category-filters{gap:20px;padding:13px 0 9px}
|
|
76
|
+
.rhine-workbench [data-layout] .rhine-index-modal .category-filters button{min-height:32px;font-size:13px}
|
|
77
|
+
.rhine-workbench [data-layout] .rhine-index-modal .rhine-query-actions{font-size:12px;margin-top:10px}
|
|
78
|
+
.rhine-workbench [data-layout] .rhine-index-modal .rhine-search-submit{font-size:13px;min-height:38px}
|
|
79
|
+
.rhine-workbench [data-layout] .rhine-index-modal .result-header{display:flex;justify-content:space-between;color:var(--choice-muted);font-size:10px;padding:10px 0;border-block:1px solid var(--choice-rule)}
|
|
80
|
+
.rhine-workbench [data-layout] .rhine-result-summary{position:static;margin:0;padding:9px 0;font-size:11px;color:var(--choice-muted)}
|
|
81
|
+
.rhine-workbench [data-layout] .rhine-result-item{display:grid;grid-template-columns:minmax(0,1fr) 175px;gap:30px;padding:16px 10px 18px 0;border-bottom:1px solid var(--choice-rule)}
|
|
82
|
+
.rhine-workbench [data-layout] .rhine-result-open strong{font-size:20px;line-height:1.5;font-weight:500;margin-top:6px;overflow-wrap:anywhere}
|
|
83
|
+
.rhine-workbench [data-layout] .rhine-result-open p{font-size:13px;line-height:1.75;color:var(--choice-muted)}
|
|
84
|
+
.rhine-workbench [data-layout] .rhine-result-item:hover{background:#eff0e138}
|
|
85
|
+
.rhine-workbench [data-layout] .rhine-result-meta{font-size:11px;gap:12px;align-items:flex-start}
|
|
86
|
+
.rhine-workbench [data-layout] .rhine-result-meta button{font-size:12px;min-height:26px}
|
|
87
|
+
.rhine-workbench [data-layout] .rhine-rack-index-item{grid-template-columns:60px minmax(0,1fr) 195px;gap:18px;padding:20px 4px;border-bottom:1px solid var(--choice-rule);align-items:baseline}
|
|
88
|
+
.rhine-workbench [data-layout] .rhine-rack-index-item strong{font-size:19px;line-height:1.5;font-weight:500;overflow-wrap:anywhere}
|
|
89
|
+
.rhine-workbench [data-layout] .rhine-rack-index-item span{font-size:11px;line-height:1.6;color:var(--choice-muted)}
|
|
90
|
+
.rhine-workbench [data-layout] .rhine-rack-index-list{margin-top:15px}
|
|
91
|
+
.rhine-workbench [data-layout] .rhine-index-modal .modal-bottom{position:static;margin-top:12px;padding:12px 0 0;font-size:9px;border-top:1px solid var(--choice-rule)}
|
|
92
|
+
|
|
93
|
+
.rhine-workbench [data-layout=compact] .archive-navigation{top:22%;bottom:174px;width:34%;padding-right:10px}
|
|
94
|
+
.rhine-workbench [data-layout=compact] .rhine-desk-panel{top:19%;bottom:160px;width:38%;padding-right:12px}
|
|
95
|
+
.rhine-workbench :is([data-layout=compact],[data-layout=portrait]){--choice-font:15px}
|
|
96
|
+
.rhine-workbench :is([data-layout=compact],[data-layout=portrait]) .rhine-choice-copy small{font-size:10px}
|
|
97
|
+
.rhine-workbench :is([data-layout=compact],[data-layout=portrait]) .rhine-choice-heading>span{font-size:9px;letter-spacing:.5px}
|
|
98
|
+
.rhine-workbench :is([data-layout=compact],[data-layout=portrait]) .rhine-choice-heading button{font-size:11px}
|
|
99
|
+
.rhine-workbench :is([data-layout=compact],[data-layout=portrait]) .rhine-shelf-excerpt{display:none}
|
|
100
|
+
.rhine-workbench :is([data-layout=compact],[data-layout=portrait]) .rhine-desk-panel h2{font-size:24px;margin:6px 0}
|
|
101
|
+
.rhine-workbench :is([data-layout=compact],[data-layout=portrait]) .rhine-shelf-title{font-size:15px;margin:7px 0 4px;-webkit-line-clamp:1}
|
|
102
|
+
.rhine-workbench :is([data-layout=compact],[data-layout=portrait]) .rhine-source-choice{gap:7px;grid-template-columns:26px minmax(0,1fr) 12px;padding-block:10px;min-height:60px}
|
|
103
|
+
.rhine-workbench :is([data-layout=compact],[data-layout=portrait]) :is(.rhine-index-modal,.rhine-rack-index-modal,.rhine-basket-modal,.rhine-log-modal){height:90%;padding:12px 18px;overflow:hidden}
|
|
104
|
+
.rhine-workbench [data-layout=portrait] .archive-navigation{top:48%;bottom:183px;left:var(--edge);width:calc(100% - 2 * var(--edge));padding:0;background:transparent}
|
|
105
|
+
.rhine-workbench [data-layout=portrait] .archive-navigation .column-navigation{padding:3px 0 6px;gap:8px}
|
|
106
|
+
.rhine-workbench [data-layout=portrait] .archive-navigation .column-navigation>div{display:flex;align-items:baseline;justify-content:space-between}
|
|
107
|
+
.rhine-workbench [data-layout=portrait] .archive-navigation .column-navigation strong{font-size:18px}
|
|
108
|
+
.rhine-workbench [data-layout=portrait] .rhine-choice-heading{min-height:24px}
|
|
109
|
+
.rhine-workbench [data-layout=portrait] .rhine-choice-footer{padding:4px 0}
|
|
110
|
+
.rhine-workbench [data-layout=portrait] .rhine-desk-panel{left:var(--edge);top:31%;bottom:175px;width:calc(100% - 2 * var(--edge));padding:0;background:transparent}
|
|
111
|
+
.rhine-workbench [data-layout=portrait] .rhine-desk-panel>.rhine-rack-intro{display:none}
|
|
112
|
+
.rhine-workbench [data-layout=portrait] .rhine-shelf-current{padding-top:8px}
|
|
113
|
+
.rhine-workbench [data-layout=portrait] .rhine-desk-pagination{padding:2px 0;min-height:28px}
|
|
114
|
+
.rhine-workbench [data-layout=portrait] :is(.rhine-index-modal,.rhine-rack-index-modal,.rhine-basket-modal,.rhine-log-modal) h2{font-size:24px;margin:12px 0}
|
|
115
|
+
.rhine-workbench [data-layout=portrait] .rhine-index-modal .category-filters{gap:6px 15px;padding:8px 0}
|
|
116
|
+
.rhine-workbench [data-layout=portrait] .rhine-index-modal .category-filters button{font-size:12px;min-height:26px}
|
|
117
|
+
.rhine-workbench [data-layout=portrait] .rhine-result-item{grid-template-columns:minmax(0,1fr);gap:12px;padding:15px 0}
|
|
118
|
+
.rhine-workbench [data-layout=portrait] .rhine-result-meta{flex-direction:row;flex-wrap:wrap;gap:6px 14px;align-items:center}
|
|
119
|
+
.rhine-workbench [data-layout=portrait] .rhine-result-meta>span{width:100%}
|
|
120
|
+
.rhine-workbench [data-layout=portrait] .rhine-result-open strong{font-size:17px}
|
|
121
|
+
.rhine-workbench [data-layout=portrait] .rhine-rack-index-item{grid-template-columns:38px minmax(0,1fr);gap:8px;padding:14px 0}
|
|
122
|
+
.rhine-workbench [data-layout=portrait] .rhine-rack-index-item strong{font-size:16px}
|
|
123
|
+
.rhine-workbench [data-layout=portrait] .rhine-rack-index-item span{grid-column:2}
|
|
124
|
+
.rhine-workbench [data-layout=portrait] .rhine-result-summary{font-size:10px}
|
|
125
|
+
@media(max-height:600px){
|
|
126
|
+
.rhine-workbench [data-layout=compact] :is(.archive-navigation,.rhine-desk-panel){top:110px;bottom:128px}
|
|
127
|
+
.rhine-workbench [data-layout=compact] .rhine-desk-panel>.rhine-rack-intro{display:none}
|
|
128
|
+
.rhine-workbench [data-layout=compact] .rhine-shelf-meta{display:none}
|
|
129
|
+
.rhine-workbench [data-layout=compact] .rhine-shelf-title{-webkit-line-clamp:1;margin:5px 0}
|
|
130
|
+
.rhine-workbench [data-layout=compact] .rhine-shelf-current{padding-top:6px}
|
|
131
|
+
}
|
|
132
|
+
/* Leave the list more room than the already selected item's summary. */
|
|
133
|
+
.rhine-workbench [data-layout] .rhine-shelf-excerpt{display:none}
|
|
134
|
+
.rhine-workbench [data-layout] .rhine-shelf-current{padding:10px 0 8px}
|
|
135
|
+
.rhine-workbench [data-layout] .rhine-shelf-meta{display:none}
|
|
136
|
+
.rhine-workbench [data-layout] .rhine-shelf-title{margin:8px 0 0}
|
|
137
|
+
.rhine-workbench [data-layout] .rhine-rack-directory{padding:6px 0;margin-top:0}
|
|
138
|
+
.rhine-workbench [data-layout] .rhine-desk-list{gap:0}
|
|
139
|
+
.rhine-workbench [data-layout] .rhine-array-read{display:none}
|
|
140
|
+
.rhine-workbench [data-layout=portrait] .rhine-array-read{display:block}
|
|
141
|
+
.rhine-workbench [data-layout=portrait] .rhine-choice-heading>span{display:none}
|
|
142
|
+
.rhine-workbench [data-layout=portrait] .rhine-investigation-brief{top:175px;bottom:auto;max-height:calc(43% - 195px)}
|
|
143
|
+
.rhine-workbench [data-layout=portrait] .rhine-investigation-brief .rhine-current-source{display:none}
|
|
144
|
+
.rhine-workbench [data-layout=portrait] .archive-navigation{top:43%;bottom:218px}
|
|
145
|
+
.rhine-workbench [data-layout=portrait] .rhine-desk-panel{top:29%;bottom:218px}
|
|
146
|
+
.rhine-workbench [data-layout=portrait] .rhine-desk-panel>.tiny-label{font-size:10px}
|
|
147
|
+
.rhine-workbench [data-layout=portrait] .rhine-shelf-navigation{margin-top:0}
|
|
148
|
+
.rhine-workbench [data-layout=portrait] .rhine-shelf-title{font-size:14px}
|
|
149
|
+
|
|
150
|
+
/* The directory is an on-demand control, with the same unfilled HUD as the brief. */
|
|
151
|
+
.rhine-workbench [data-layout] .archive-navigation[hidden],
|
|
152
|
+
.rhine-workbench [data-layout] .rhine-array-toggle[hidden]{display:none}
|
|
153
|
+
.rhine-workbench [data-layout] .rhine-array-toggle{position:absolute;left:var(--edge);bottom:235px;display:flex;align-items:center;gap:12px;min-height:38px;font-size:13px;color:#414b36;background:none;border:0;border-bottom:1px solid #7c81644a;padding:6px 0}
|
|
154
|
+
.rhine-workbench [data-layout] .rhine-array-toggle>span{font-size:18px}
|
|
155
|
+
.rhine-workbench [data-layout] .rhine-array-toggle small{font-size:10px;color:#747863;font-weight:400}
|
|
156
|
+
.rhine-workbench [data-layout] .rhine-array-toggle:hover{color:#25371f;border-color:#78835c}
|
|
157
|
+
.rhine-workbench [data-layout] .rhine-array-toggle:focus-visible{outline:1px solid #78835c;outline-offset:4px}
|
|
158
|
+
.rhine-workbench [data-layout] .rhine-array-collapse{color:#646e54}
|
|
159
|
+
.rhine-workbench [data-layout] .archive-navigation[inert]{pointer-events:none}
|
|
160
|
+
.rhine-workbench [data-layout=compact] .rhine-array-toggle{bottom:174px;font-size:12px}
|
|
161
|
+
.rhine-workbench [data-layout=portrait] .rhine-array-toggle{bottom:218px;font-size:12px}
|
|
162
|
+
.rhine-workbench [data-layout=portrait][data-array-list=closed] .rhine-investigation-brief{max-height:calc(100% - 455px)}
|
|
163
|
+
.rhine-workbench [data-layout=portrait][data-array-list=closed] .rhine-investigation-brief .rhine-current-source{display:block}
|
|
164
|
+
@media(max-height:600px){.rhine-workbench [data-layout=compact] .rhine-array-toggle{bottom:128px}}
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
// TEMPORARY RHINE PROFILER: observers exist only during an explicitly started capture.
|
|
2
|
+
import { RenderPerformanceCapture } from './temporary-performance.ts';
|
|
3
|
+
import type { DiagnosticChannel } from './temporary-performance.ts';
|
|
4
|
+
|
|
5
|
+
type Entry = Record<string, unknown>;
|
|
6
|
+
const number = (value: unknown) => typeof value === 'number' && Number.isFinite(value) ? value : null;
|
|
7
|
+
const fields = (entry: Entry, keys: string[]) => Object.fromEntries(keys.map(key => [key, number(entry[key])]));
|
|
8
|
+
const relative = (value: unknown, origin: number) => typeof value === 'number' && value > 0 ? value - origin : null;
|
|
9
|
+
const allowed = (value: unknown, list: string[]) => typeof value === 'string' && list.includes(value) ? value : 'other';
|
|
10
|
+
|
|
11
|
+
/** Only authored public asset names; never export arbitrary URLs or document IDs. */
|
|
12
|
+
export function resourceIdentity(value: unknown) {
|
|
13
|
+
if (typeof value !== 'string') return null;
|
|
14
|
+
try {
|
|
15
|
+
const name = new URL(value, 'https://local.invalid').pathname.split('/').pop() || '';
|
|
16
|
+
return ['archive-cassette.glb', 'archive-assembly.glb', 'rhine.js', 'rhine.css', 'client.js', 'evidence-observatory.webp'].includes(name)
|
|
17
|
+
? name : /^MiSans-(Light|Regular|Medium|Demibold|Semibold|SemiBold|Bold)\.woff2?$/.test(name) ? name : null;
|
|
18
|
+
} catch { return null; }
|
|
19
|
+
}
|
|
20
|
+
function surface(node: unknown) {
|
|
21
|
+
if (typeof Element === 'undefined' || !(node instanceof Element)) return 'unknown';
|
|
22
|
+
for (const [selector, name] of [['.rhine-perf-tools', 'profiler'], ['.rhine-reader', 'reader'],
|
|
23
|
+
['.rhine-report', 'report'], ['.model-viewer', 'viewer'], ['.rhine-case', 'status'], ['canvas', 'canvas']]) {
|
|
24
|
+
if (node.closest(selector)) return name;
|
|
25
|
+
}
|
|
26
|
+
return 'other-ui';
|
|
27
|
+
}
|
|
28
|
+
export function resourceKind(value: unknown) {
|
|
29
|
+
if (typeof value !== 'string') return 'unknown';
|
|
30
|
+
try {
|
|
31
|
+
const path = new URL(value, 'https://local.invalid').pathname;
|
|
32
|
+
if (/\/rhine\.js$/.test(path)) return 'rhine-bundle';
|
|
33
|
+
if (/\/client\.js$/.test(path)) return 'host-client';
|
|
34
|
+
if (/\.(woff2?|ttf|otf)$/i.test(path)) return 'font';
|
|
35
|
+
if (/\.(glb|gltf)$/i.test(path)) return 'model';
|
|
36
|
+
if (/\.(png|jpg|jpeg|webp|avif|svg)$/i.test(path)) return 'image';
|
|
37
|
+
if (/\.css$/i.test(path)) return 'stylesheet';
|
|
38
|
+
if (/\.(m?js)$/i.test(path)) return 'other-script';
|
|
39
|
+
return 'other';
|
|
40
|
+
} catch { return 'unknown'; }
|
|
41
|
+
}
|
|
42
|
+
export function sanitizePerformanceEntry(type: string, entry: Entry, origin: number): Record<string, unknown> {
|
|
43
|
+
if (type === 'long-animation-frame') {
|
|
44
|
+
const scripts = Array.isArray(entry.scripts) ? entry.scripts : [];
|
|
45
|
+
return { ...fields(entry, ['duration', 'blockingDuration']),
|
|
46
|
+
renderStart: relative(entry.renderStart, origin), styleAndLayoutStart: relative(entry.styleAndLayoutStart, origin),
|
|
47
|
+
firstUIEventTimestamp: relative(entry.firstUIEventTimestamp, origin),
|
|
48
|
+
scriptsTotal: scripts.length, scriptsOmitted: Math.max(0, scripts.length - 16),
|
|
49
|
+
scripts: scripts.slice(0, 16).map((value: Entry) => ({
|
|
50
|
+
source: resourceKind(value.sourceURL), asset: resourceIdentity(value.sourceURL),
|
|
51
|
+
invokerType: allowed(value.invokerType, ['event-listener', 'user-callback', 'resolve-promise', 'reject-promise', 'classic-script', 'module-script']),
|
|
52
|
+
windowAttribution: allowed(value.windowAttribution, ['self', 'same-origin-ancestor', 'same-origin-descendant', 'same-origin', 'cross-origin-ancestor', 'cross-origin-descendant', 'cross-origin', 'other']),
|
|
53
|
+
executionStart: relative(value.executionStart, origin),
|
|
54
|
+
...fields(value, ['duration', 'pauseDuration', 'forcedStyleAndLayoutDuration', 'sourceCharPosition']),
|
|
55
|
+
})) };
|
|
56
|
+
}
|
|
57
|
+
if (type === 'event') return {
|
|
58
|
+
event: allowed(entry.name, ['click', 'pointerdown', 'pointerup', 'mousedown', 'mouseup', 'keydown', 'keyup', 'touchstart', 'touchend', 'input', 'change']),
|
|
59
|
+
...fields(entry, ['duration', 'interactionId']),
|
|
60
|
+
inputDelayMs: typeof entry.processingStart === 'number' && typeof entry.startTime === 'number' ? entry.processingStart - entry.startTime : null,
|
|
61
|
+
processingMs: typeof entry.processingStart === 'number' && typeof entry.processingEnd === 'number' ? entry.processingEnd - entry.processingStart : null,
|
|
62
|
+
};
|
|
63
|
+
if (type === 'resource') return {
|
|
64
|
+
resource: resourceKind(entry.name), asset: resourceIdentity(entry.name),
|
|
65
|
+
initiator: allowed(entry.initiatorType, ['fetch', 'xmlhttprequest', 'script', 'css', 'img', 'link', 'other']),
|
|
66
|
+
...fields(entry, ['duration', 'transferSize', 'encodedBodySize', 'decodedBodySize', 'responseStatus']),
|
|
67
|
+
responseStart: relative(entry.responseStart, origin), responseEnd: relative(entry.responseEnd, origin),
|
|
68
|
+
};
|
|
69
|
+
if (type === 'layout-shift') return { value: number(entry.value), hadRecentInput: entry.hadRecentInput === true,
|
|
70
|
+
surfaces: [...new Set((Array.isArray(entry.sources) ? entry.sources : []).map((source: Entry) => surface(source.node)))] };
|
|
71
|
+
return { duration: number(entry.duration) };
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function observePerformance(capture: RenderPerformanceCapture, root: HTMLElement, state: () => unknown = () => null) {
|
|
75
|
+
const observers: { observer: PerformanceObserver; consume: (entries: PerformanceEntry[]) => void }[] = [];
|
|
76
|
+
const events = new AbortController();
|
|
77
|
+
const types: [string, DiagnosticChannel | 'longtask'][] = [
|
|
78
|
+
['longtask', 'longtask'], ['long-animation-frame', 'longAnimationFrames'],
|
|
79
|
+
['event', 'interactions'], ['resource', 'resources'], ['layout-shift', 'layoutShifts'],
|
|
80
|
+
];
|
|
81
|
+
for (const [type, channel] of types) {
|
|
82
|
+
const supported = typeof PerformanceObserver !== 'undefined' && PerformanceObserver.supportedEntryTypes?.includes(type) === true;
|
|
83
|
+
const capability: { supported: boolean; active: boolean; reason: string } = capture.capabilities[type] = { supported, active: false, reason: supported ? '' : 'unsupported' };
|
|
84
|
+
if (type === 'longtask') capture.longTasksSupported = supported;
|
|
85
|
+
if (!supported) continue;
|
|
86
|
+
const consume = (entries: PerformanceEntry[]) => {
|
|
87
|
+
if (!entries.length) return;
|
|
88
|
+
const start = performance.now();
|
|
89
|
+
for (const entry of entries) {
|
|
90
|
+
if (channel === 'longtask') capture.addLongTask(entry.startTime, entry.duration);
|
|
91
|
+
else {
|
|
92
|
+
const detail = sanitizePerformanceEntry(type, entry as unknown as Entry, capture.startedAt);
|
|
93
|
+
if (type === 'event') {
|
|
94
|
+
capture.diagnostic('interactionSummary', { kind: detail.event, durationMs: detail.duration,
|
|
95
|
+
inputDelayMs: detail.inputDelayMs, processingMs: detail.processingMs }, entry.startTime);
|
|
96
|
+
// Hover/movement entries have interactionId=0 and formerly exhausted
|
|
97
|
+
// the raw channel. Keep real interactions and slow anonymous events.
|
|
98
|
+
if (!(Number(detail.interactionId) > 0 || Number(detail.duration) >= 48)) continue;
|
|
99
|
+
}
|
|
100
|
+
capture.diagnostic(channel, detail, entry.startTime);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
capture.diagnostic('overhead', { kind: 'observer', entryType: type, entries: entries.length, durationMs: performance.now() - start }, start);
|
|
104
|
+
};
|
|
105
|
+
let observer: PerformanceObserver | undefined;
|
|
106
|
+
try {
|
|
107
|
+
observer = new PerformanceObserver(list => consume(list.getEntries()));
|
|
108
|
+
// Event Timing has a browser minimum threshold (16ms), and rounds durations.
|
|
109
|
+
observer.observe({ type, buffered: false, ...(type === 'event' ? { durationThreshold: 16 } : {}) } as PerformanceObserverInit);
|
|
110
|
+
observers.push({ observer, consume }); capability.active = true; capability.reason = '';
|
|
111
|
+
} catch {
|
|
112
|
+
observer?.disconnect(); capability.reason = 'observe-failed';
|
|
113
|
+
if (type === 'longtask') capture.longTasksSupported = false;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
const memory = () => {
|
|
117
|
+
const heap = (performance as unknown as { memory?: { usedJSHeapSize: number; totalJSHeapSize: number; jsHeapSizeLimit: number } }).memory;
|
|
118
|
+
return heap ? { usedJSHeapSize: heap.usedJSHeapSize, totalJSHeapSize: heap.totalJSHeapSize, jsHeapSizeLimit: heap.jsHeapSizeLimit } : null;
|
|
119
|
+
};
|
|
120
|
+
capture.capabilities['js-heap'] = { supported: memory() !== null, active: memory() !== null, reason: 'Chromium estimate; may be shared/rounded; no GC attribution' };
|
|
121
|
+
let expected = performance.now() + 1000;
|
|
122
|
+
const heartbeat = (initial = false) => {
|
|
123
|
+
const start = performance.now();
|
|
124
|
+
if (!capture.running) return;
|
|
125
|
+
capture.diagnostic('heartbeat', {
|
|
126
|
+
timerDelayMs: initial ? null : Math.max(0, start - expected),
|
|
127
|
+
hidden: document.hidden, focused: document.hasFocus(), visibility: document.visibilityState,
|
|
128
|
+
viewport: { width: innerWidth, height: innerHeight, dpr: devicePixelRatio,
|
|
129
|
+
visualWidth: visualViewport?.width ?? null, visualHeight: visualViewport?.height ?? null, visualScale: visualViewport?.scale ?? null },
|
|
130
|
+
fonts: document.fonts?.status ?? null, heap: memory(), pendingGpu: capture.pendingGpu,
|
|
131
|
+
renderers: capture.rendererStates(), workbench: state(), loading: capture.loadingState(),
|
|
132
|
+
}, start);
|
|
133
|
+
expected = start + 1000;
|
|
134
|
+
capture.diagnostic('overhead', { kind: 'heartbeat', durationMs: performance.now() - start }, start);
|
|
135
|
+
};
|
|
136
|
+
heartbeat(true);
|
|
137
|
+
const interval = window.setInterval(heartbeat, 1000);
|
|
138
|
+
const lifecycle = (kind: string, detail?: unknown) => capture.mark(kind, detail);
|
|
139
|
+
for (const name of ['focus', 'blur']) window.addEventListener(name, () => lifecycle('window-' + name), { signal: events.signal });
|
|
140
|
+
for (const name of ['pagehide', 'pageshow']) window.addEventListener(name, event => {
|
|
141
|
+
capture.breakTimeline(name); lifecycle(name, { persisted: (event as PageTransitionEvent).persisted });
|
|
142
|
+
}, { signal: events.signal });
|
|
143
|
+
for (const name of ['freeze', 'resume']) document.addEventListener(name, () => capture.breakTimeline(name), { signal: events.signal });
|
|
144
|
+
window.addEventListener('resize', () => lifecycle('viewport-resize', { width: innerWidth, height: innerHeight, dpr: devicePixelRatio }), { signal: events.signal });
|
|
145
|
+
let lastWheel = -Infinity;
|
|
146
|
+
for (const name of ['pointerdown', 'wheel', 'keydown']) root.addEventListener(name, event => {
|
|
147
|
+
if (!capture.running) return;
|
|
148
|
+
if (name === 'wheel') { if (performance.now() - lastWheel < 150) return; lastWheel = performance.now(); }
|
|
149
|
+
const target = event.target instanceof Element ? event.target : null;
|
|
150
|
+
const surface = target?.closest('.rhine-perf-tools') ? 'profiler' : target?.closest('.model-viewer') ? 'viewer' : target?.tagName === 'CANVAS' ? 'scene' : 'ui';
|
|
151
|
+
capture.mark('input', { type: name, surface, trusted: event.isTrusted,
|
|
152
|
+
...(name === 'keydown' ? { key: allowed((event as KeyboardEvent).key, ['ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown', 'Enter', 'Escape', 'Tab', 'Home', 'End', 'PageUp', 'PageDown', ' ']) } : {}) });
|
|
153
|
+
}, { signal: events.signal, capture: true, passive: true });
|
|
154
|
+
window.addEventListener('error', event => lifecycle('runtime-error', {
|
|
155
|
+
kind: event instanceof ErrorEvent ? 'script' : 'resource',
|
|
156
|
+
...(event instanceof ErrorEvent ? { source: resourceKind(event.filename), line: event.lineno, column: event.colno } : {}),
|
|
157
|
+
}), { signal: events.signal, capture: true });
|
|
158
|
+
window.addEventListener('unhandledrejection', () => lifecycle('unhandled-rejection'), { signal: events.signal });
|
|
159
|
+
return () => {
|
|
160
|
+
clearInterval(interval); events.abort();
|
|
161
|
+
for (const { observer, consume } of observers) { consume(observer.takeRecords()); observer.disconnect(); }
|
|
162
|
+
};
|
|
163
|
+
}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
// TEMPORARY RHINE PROFILER — remove this module and its marked workbench hooks after testing.
|
|
2
|
+
import { RenderPerformanceCapture, liveFrameSummary } from './temporary-performance';
|
|
3
|
+
import { observePerformance } from './temporary-performance-observers';
|
|
4
|
+
import './temporary-performance.css';
|
|
5
|
+
|
|
6
|
+
const ms = (n: number | null | undefined) => n == null ? '—' : n.toFixed(2);
|
|
7
|
+
const count = (n: number | null | undefined) => n == null ? '—' : Math.round(n).toLocaleString();
|
|
8
|
+
const states: Record<string, string> = { array: '阵列', rack: '档案架', detail: '抽出 / 阅读', travel: '移动到档案架', viewer: '360° 模型' };
|
|
9
|
+
const stateLabel = (state: string) => state.split(':').map(s => states[s] || ({ search: '检索', activity: '资料动画' }[s]) || s).join(' · ');
|
|
10
|
+
|
|
11
|
+
export function mountTemporaryPerformancePanel(root: HTMLElement, metadata: () => unknown, state: () => unknown = () => null) {
|
|
12
|
+
const capture = new RenderPerformanceCapture();
|
|
13
|
+
capture.longTasksSupported = typeof PerformanceObserver !== 'undefined' && PerformanceObserver.supportedEntryTypes?.includes('longtask') === true;
|
|
14
|
+
const container = document.createElement('div');
|
|
15
|
+
container.className = 'rhine-perf-tools';
|
|
16
|
+
container.innerHTML = `<button type="button" class="rhine-perf-toggle" aria-expanded="false">性能测试 · 临时</button>
|
|
17
|
+
<section class="rhine-perf-panel" aria-label="临时渲染性能测试" hidden>
|
|
18
|
+
<header><strong>性能诊断 / v3.10</strong><button type="button" data-perf="close" aria-label="收起性能面板">收起 ×</button></header>
|
|
19
|
+
<p>保持日常使用即可,收起面板仍采集完整日志。可先录制 120 秒静置,再单独录制操作过程。</p>
|
|
20
|
+
<div class="rhine-perf-controls"><label>时长 <select aria-label="性能测试时长"><option value="30">30 秒</option><option value="60" selected>60 秒</option><option value="120">120 秒</option></select></label><button type="button" data-perf="start">开始测试</button><button type="button" data-perf="stop" disabled>停止</button><button type="button" data-perf="export" disabled>导出 JSON</button></div>
|
|
21
|
+
<div class="rhine-perf-controls"><label><input type="checkbox" data-perf="gpu" checked> GPU 计时</label><label><input type="checkbox" data-perf="detailed" checked> 详细诊断(抽样)</label><button type="button" data-perf="mark" disabled>标记刚才的卡顿</button><span>关闭详细诊断或 GPU 计时可对照采集开销</span></div>
|
|
22
|
+
<div class="rhine-perf-status" role="status">尚未开始</div>
|
|
23
|
+
<div class="rhine-perf-current">请等模型载入后开始。</div>
|
|
24
|
+
<canvas width="680" height="110" class="rhine-perf-chart" aria-label="最近 120 帧的实际回调间隔曲线,横线为 16.7 与 33.3 毫秒"></canvas>
|
|
25
|
+
<table><caption>最近 2 秒(最多 512 帧)· 单位 ms</caption><thead><tr><th>测量项</th><th>平均</th><th>P95</th><th>P99</th><th>最高</th></tr></thead><tbody></tbody></table>
|
|
26
|
+
<div class="rhine-perf-resources"></div><div class="rhine-perf-gpu"></div><div class="rhine-perf-tasks"></div>
|
|
27
|
+
<details><summary>如何读数</summary><p>实际回调间隔越长,停顿越明显;RAF 时间戳差单独保留在日志中。P95 表示 95% 的样本不超过此值。60 帧的单帧预算是 16.7 ms。FPS 是渲染回调频率,不保证屏幕实际呈现帧数。</p><p>CPU 是场景更新和绘制提交耗时;GPU 包含阴影、透射与后处理。两者不能相加,也不代表占用率。表格只统计最近窗口,全程数据和精确累计分位数保留在导出中。日志还记录绘制阶段、场景构成、资源加载生命周期、界面更新、长动画帧、交互延迟和后台档案准备。浏览器不支持的项目会明确注明。</p><p>资源显示数量,无法读取整卡显存或 GPU 占用率。切到后台时断开帧间隔统计,恢复后继续。首次出现的卡顿会保留;可先预热,再另开一次测试。记录只导出到本机;不采集正文或输入内容。JS 堆是浏览器估值,不能据此直接判断泄漏。新测试会覆盖未导出的记录。</p></details>
|
|
28
|
+
</section>`;
|
|
29
|
+
root.append(container); // Outside the scaled stage and viewer's inert siblings.
|
|
30
|
+
const get = <T extends HTMLElement = HTMLElement>(selector: string) => container.querySelector<T>(selector)!;
|
|
31
|
+
const toggle = get<HTMLButtonElement>('.rhine-perf-toggle'), panel = get('.rhine-perf-panel');
|
|
32
|
+
const start = get<HTMLButtonElement>('[data-perf=start]'), stop = get<HTMLButtonElement>('[data-perf=stop]');
|
|
33
|
+
const download = get<HTMLButtonElement>('[data-perf=export]');
|
|
34
|
+
const labels = ['实际回调间隔', 'RAF 调度滞后', '帧外空档', 'CPU 场景帧', 'GPU 帧区间 / 分段合计', '逐帧采集开销'];
|
|
35
|
+
const tbody = get('tbody');
|
|
36
|
+
const cells = labels.map(label => {
|
|
37
|
+
const row = document.createElement('tr'), heading = document.createElement('th');
|
|
38
|
+
heading.textContent = label; row.append(heading);
|
|
39
|
+
const values = Array.from({ length: 4 }, () => document.createElement('td'));
|
|
40
|
+
row.append(...values); tbody.append(row); return values;
|
|
41
|
+
});
|
|
42
|
+
const textNodes = new Map<string, HTMLElement>();
|
|
43
|
+
const setText = (selector: string, text: string) => {
|
|
44
|
+
let node = textNodes.get(selector);
|
|
45
|
+
if (!node) { node = get(selector); textNodes.set(selector, node); }
|
|
46
|
+
if (node.textContent !== text) node.textContent = text;
|
|
47
|
+
};
|
|
48
|
+
const graph = get<HTMLCanvasElement>('canvas'), graphContext = graph.getContext('2d');
|
|
49
|
+
const detailed = get<HTMLInputElement>('[data-perf=detailed]');
|
|
50
|
+
const gpu = get<HTMLInputElement>('[data-perf=gpu]'), marker = get<HTMLButtonElement>('[data-perf=mark]');
|
|
51
|
+
let interval = 0, active = true, disconnect: (() => void) | undefined, collecting = false;
|
|
52
|
+
const events = new AbortController();
|
|
53
|
+
const finishObserver = () => {
|
|
54
|
+
disconnect?.(); disconnect = undefined;
|
|
55
|
+
if (collecting) { capture.endMetadata = metadata(); collecting = false; }
|
|
56
|
+
};
|
|
57
|
+
const paint = () => {
|
|
58
|
+
const paintStart = performance.now();
|
|
59
|
+
capture.poll();
|
|
60
|
+
if (!capture.running) finishObserver();
|
|
61
|
+
toggle.textContent = capture.running ? `● 测试中 ${Math.floor(capture.elapsedMs / 1000)}s` : '性能测试 · 临时';
|
|
62
|
+
if (!panel.hidden) {
|
|
63
|
+
const samples = capture.samples, latest = samples.at(-1);
|
|
64
|
+
const recentEnd = capture.running ? capture.elapsedMs : latest?.at || 0;
|
|
65
|
+
const summary = liveFrameSummary(samples, recentEnd);
|
|
66
|
+
const recentMean = summary.callbackMs.mean;
|
|
67
|
+
const fps = recentMean ? 1000 / recentMean : null;
|
|
68
|
+
start.disabled = capture.running || capture.pendingGpu > 0;
|
|
69
|
+
stop.disabled = !capture.running; download.disabled = !capture.startedISO || capture.running || capture.pendingGpu > 0;
|
|
70
|
+
gpu.disabled = capture.running || capture.pendingGpu > 0; detailed.disabled = gpu.disabled; marker.disabled = !capture.running;
|
|
71
|
+
start.textContent = samples.length ? '开始新测试' : '开始测试';
|
|
72
|
+
setText(".rhine-perf-status", `${capture.running ? !active || document.hidden ? '采样中 · 页面未激活' : '采样中' : samples.length ? '已停止' : '尚未开始'} · ${(capture.elapsedMs / 1000).toFixed(1)}s · ${samples.length} 帧${capture.pendingGpu ? ` · GPU 待回读 ${capture.pendingGpu}` : ''}`);
|
|
73
|
+
setText(".rhine-perf-current", latest ? `${stateLabel(latest.state)} · 最近 2 秒 ${ms(fps)} 回调/秒 · ${latest.width} × ${latest.height}${summary.truncated ? ' · 已达 512 帧窗口上限' : ''} · 加载中 ${capture.loadingState().active.length}` : '请等模型载入后开始。');
|
|
74
|
+
[summary.callbackMs, summary.rafLagMs, summary.outsideFrameMs, summary.cpuMs, summary.gpuMs, summary.probeMs]
|
|
75
|
+
.forEach((d, row) => [d.mean, d.p95, d.p99, d.max].forEach((value, col) => {
|
|
76
|
+
const text = ms(value); if (cells[row][col].textContent !== text) cells[row][col].textContent = text;
|
|
77
|
+
}));
|
|
78
|
+
setText(".rhine-perf-resources", latest ? `最近一帧:${count(latest.triangles)} 三角形 / ${latest.calls} 次绘制(含多遍)\n资源:${latest.geometries} 几何 / ${latest.textures} 纹理 / ${latest.programs} 着色程序` : '');
|
|
79
|
+
const adapter = capture.adapters.find(a => a.name === latest?.renderer);
|
|
80
|
+
setText(".rhine-perf-gpu", adapter ? `${adapter.software ? '软件渲染器 · 不能代表硬件表现\n' : ''}${adapter.renderer}\n${!capture.options.gpuTiming ? '本次已关闭 GPU 计时,仍记录 CPU 和浏览器诊断。' : adapter.gpuTimerSupported ? `窗口内 GPU 有效样本 ${summary.gpuMs.count}/${summary.frames};全程统计见导出` : '浏览器不支持 GPU 计时;GPU 耗时不作推算。'}` : '');
|
|
81
|
+
setText(".rhine-perf-tasks", `窗口内实际回调间隔 >50ms:${summary.callbackOver50ms} 次;RAF >50ms:${summary.over50ms} 次\n${capture.longTasksSupported ? `页面长任务:${capture.longTasks.length} 次,最长 ${ms(capture.longTaskMaxMs)} ms` : '浏览器不支持页面长任务观测'}\n长动画帧:${capture.capabilities['long-animation-frame']?.active ? capture.diagnostics.longAnimationFrames.length + ' 次' : '不可用'};交互记录:${capture.diagnostics.interactions.length};心跳:${capture.diagnostics.heartbeat.length}`);
|
|
82
|
+
if (graphContext) {
|
|
83
|
+
const ctx = graphContext, tail = samples.slice(-120);
|
|
84
|
+
const ceiling = Math.max(50, ...tail.map(s => s.callbackIntervalMs || 0));
|
|
85
|
+
ctx.clearRect(0, 0, graph.width, graph.height);
|
|
86
|
+
ctx.font = '18px sans-serif';
|
|
87
|
+
for (const line of [16.67, 33.34]) { const y = 108 - line / ceiling * 98; ctx.strokeStyle = '#bbc0b3'; ctx.beginPath(); ctx.moveTo(0, y); ctx.lineTo(680, y); ctx.stroke(); ctx.fillStyle = '#676e5f'; if (ceiling < 90) ctx.fillText(`${line.toFixed(1)} ms`, 6, y - 2); }
|
|
88
|
+
if (ceiling >= 90) ctx.fillText('16.7 / 33.3 ms', 6, 100);
|
|
89
|
+
ctx.fillText(`${Math.ceil(ceiling)} ms`, 565, 20);
|
|
90
|
+
ctx.strokeStyle = '#96734d'; ctx.lineWidth = 2; ctx.beginPath();
|
|
91
|
+
let move = true;
|
|
92
|
+
tail.forEach((s, i) => { if (s.callbackIntervalMs === null) { move = true; return; } const x = i / 119 * 680, y = 108 - s.callbackIntervalMs / ceiling * 98; if (move) ctx.moveTo(x, y); else ctx.lineTo(x, y); move = false; }); ctx.stroke();
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
capture.diagnostic('overhead', { kind: 'panel', visible: !panel.hidden, durationMs: performance.now() - paintStart }, paintStart);
|
|
96
|
+
if (!capture.running && !capture.pendingGpu) { clearInterval(interval); interval = 0; }
|
|
97
|
+
};
|
|
98
|
+
const wake = () => { if (!interval) interval = window.setInterval(paint, 1000); paint(); };
|
|
99
|
+
const show = (visible: boolean) => { capture.mark('profiler-panel', { visible }); panel.hidden = !visible; toggle.setAttribute('aria-expanded', String(visible)); wake(); };
|
|
100
|
+
toggle.addEventListener('click', () => show(panel.hidden));
|
|
101
|
+
get('[data-perf=close]').addEventListener('click', () => { show(false); toggle.focus(); });
|
|
102
|
+
start.addEventListener('click', () => {
|
|
103
|
+
finishObserver(); capture.start(metadata(), Number(get<HTMLSelectElement>('select').value), { gpuTiming: gpu.checked, detailed: detailed.checked }); collecting = true;
|
|
104
|
+
disconnect = observePerformance(capture, root, () => ({ active, state: state() }));
|
|
105
|
+
capture.mark('profiler-panel', { visible: !panel.hidden });
|
|
106
|
+
wake();
|
|
107
|
+
});
|
|
108
|
+
marker.addEventListener('click', () => capture.mark('user-hitch-marker'));
|
|
109
|
+
stop.addEventListener('click', () => { capture.stop(); paint(); });
|
|
110
|
+
download.addEventListener('click', () => {
|
|
111
|
+
if (capture.running || capture.pendingGpu) return;
|
|
112
|
+
const blob = new Blob([JSON.stringify(capture.report(), null, 2)], { type: 'application/json' });
|
|
113
|
+
const url = URL.createObjectURL(blob), link = document.createElement('a');
|
|
114
|
+
link.href = url; link.download = `rhine-performance-${capture.startedISO.replace(/[:.]/g, '-')}.json`; link.click();
|
|
115
|
+
window.setTimeout(() => URL.revokeObjectURL(url), 1000);
|
|
116
|
+
});
|
|
117
|
+
// Local keyboard/wheel interaction must not rotate or navigate the model behind the panel.
|
|
118
|
+
for (const name of ['keydown', 'wheel', 'pointerdown']) container.addEventListener(name, e => e.stopPropagation());
|
|
119
|
+
document.addEventListener('visibilitychange', () => capture.breakTimeline(document.hidden ? 'hidden' : 'visible'), { signal: events.signal });
|
|
120
|
+
return { capture,
|
|
121
|
+
setActive(value: boolean) { if (active !== value) capture.breakTimeline(value ? 'workbench-active' : 'workbench-inactive'); active = value; },
|
|
122
|
+
dispose() { capture.stop('dispose'); finishObserver(); clearInterval(interval); events.abort(); capture.dispose(); container.remove(); },
|
|
123
|
+
};
|
|
124
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/* TEMPORARY RHINE PROFILER: no blur/filter, animations, or full-screen compositing layer. */
|
|
2
|
+
.rhine-workbench .rhine-perf-tools{position:absolute;right:12px;bottom:10px;z-index:300;font:12px/1.5 system-ui,sans-serif;color:#262b24;text-align:left}
|
|
3
|
+
.rhine-workbench .rhine-perf-tools button,.rhine-workbench .rhine-perf-tools select{font:inherit;color:inherit;background:#f1f1e9;border:1px solid #b8bdad;border-radius:3px;padding:5px 8px;cursor:pointer;letter-spacing:0}
|
|
4
|
+
.rhine-workbench .rhine-perf-tools button:disabled{opacity:.45;cursor:default}
|
|
5
|
+
.rhine-workbench .rhine-perf-tools button:focus-visible,.rhine-workbench .rhine-perf-tools select:focus-visible{outline:2px solid #647749;outline-offset:2px}
|
|
6
|
+
.rhine-workbench .rhine-perf-toggle{float:right}
|
|
7
|
+
.rhine-workbench .rhine-perf-panel{position:absolute;bottom:38px;right:0;width:min(380px,calc(100vw - 24px));max-height:calc(100dvh - 76px);overflow:auto;box-sizing:border-box;background:#f4f4ed;border:1px solid #abb29f;padding:14px;border-radius:5px;box-shadow:0 3px 16px #0002}
|
|
8
|
+
.rhine-workbench .rhine-perf-panel header,.rhine-workbench .rhine-perf-controls{display:flex;align-items:center;justify-content:space-between;gap:6px;flex-wrap:wrap}
|
|
9
|
+
.rhine-workbench .rhine-perf-panel p{margin:8px 0;color:#656c60;font-size:11px}
|
|
10
|
+
.rhine-workbench .rhine-perf-status{margin-top:10px;color:#536440}
|
|
11
|
+
.rhine-workbench .rhine-perf-current{font-variant-numeric:tabular-nums;margin-top:6px}
|
|
12
|
+
.rhine-workbench .rhine-perf-chart{width:100%;height:55px;margin-top:10px}
|
|
13
|
+
.rhine-workbench .rhine-perf-panel table{width:100%;border-collapse:collapse;font-variant-numeric:tabular-nums}
|
|
14
|
+
.rhine-workbench .rhine-perf-panel caption{text-align:left;font-size:10px;color:#656c60}
|
|
15
|
+
.rhine-workbench .rhine-perf-panel th,.rhine-workbench .rhine-perf-panel td{padding:4px;text-align:right;border-bottom:1px solid #d8dccf}
|
|
16
|
+
.rhine-workbench .rhine-perf-panel th:first-child{text-align:left;font-weight:400}
|
|
17
|
+
.rhine-workbench .rhine-perf-resources,.rhine-workbench .rhine-perf-gpu,.rhine-workbench .rhine-perf-tasks{font-size:11px;margin-top:8px;white-space:pre-line;overflow-wrap:anywhere}
|
|
18
|
+
.rhine-workbench .rhine-perf-panel details{border-top:1px solid #d8dccf;margin-top:10px;padding-top:8px;font-size:11px}
|
|
19
|
+
.rhine-workbench .rhine-perf-panel summary{cursor:pointer}
|