dylanebert HF staff commited on
Commit
7ee5f8f
1 Parent(s): 64644c3

add solution

Browse files
viewer/.gitignore ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ .DS_Store
2
+ node_modules
3
+ /build
4
+ /.svelte-kit
5
+ /package
6
+ .env
7
+ .env.*
8
+ !.env.example
9
+ vite.config.js.timestamp-*
10
+ vite.config.ts.timestamp-*
viewer/.npmrc ADDED
@@ -0,0 +1 @@
 
 
1
+ engine-strict=true
viewer/README.md ADDED
@@ -0,0 +1,38 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # create-svelte
2
+
3
+ Everything you need to build a Svelte project, powered by [`create-svelte`](https://github.com/sveltejs/kit/tree/master/packages/create-svelte).
4
+
5
+ ## Creating a project
6
+
7
+ If you're seeing this, you've probably already done this step. Congrats!
8
+
9
+ ```bash
10
+ # create a new project in the current directory
11
+ npm create svelte@latest
12
+
13
+ # create a new project in my-app
14
+ npm create svelte@latest my-app
15
+ ```
16
+
17
+ ## Developing
18
+
19
+ Once you've created a project and installed dependencies with `npm install` (or `pnpm install` or `yarn`), start a development server:
20
+
21
+ ```bash
22
+ npm run dev
23
+
24
+ # or start the server and open the app in a new browser tab
25
+ npm run dev -- --open
26
+ ```
27
+
28
+ ## Building
29
+
30
+ To create a production version of your app:
31
+
32
+ ```bash
33
+ npm run build
34
+ ```
35
+
36
+ You can preview the production build with `npm run preview`.
37
+
38
+ > To deploy your app, you may need to install an [adapter](https://kit.svelte.dev/docs/adapters) for your target environment.
viewer/bun.lockb ADDED
Binary file (52.4 kB). View file
 
viewer/package.json ADDED
@@ -0,0 +1,26 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "viewer",
3
+ "version": "0.0.1",
4
+ "private": true,
5
+ "scripts": {
6
+ "dev": "vite dev",
7
+ "build": "vite build",
8
+ "preview": "vite preview",
9
+ "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
10
+ "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch"
11
+ },
12
+ "devDependencies": {
13
+ "@sveltejs/adapter-auto": "^2.0.0",
14
+ "@sveltejs/kit": "^1.20.4",
15
+ "svelte": "^4.0.5",
16
+ "svelte-check": "^3.4.3",
17
+ "tslib": "^2.4.1",
18
+ "typescript": "^5.0.0",
19
+ "vite": "^4.4.2"
20
+ },
21
+ "type": "module",
22
+ "dependencies": {
23
+ "@babylonjs/core": "^6.23.0",
24
+ "@babylonjs/loaders": "^6.23.0"
25
+ }
26
+ }
viewer/src/app.d.ts ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // See https://kit.svelte.dev/docs/types#app
2
+ // for information about these interfaces
3
+ declare global {
4
+ namespace App {
5
+ // interface Error {}
6
+ // interface Locals {}
7
+ // interface PageData {}
8
+ // interface Platform {}
9
+ }
10
+ }
11
+
12
+ export {};
viewer/src/app.html ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+
4
+ <head>
5
+ <meta charset="utf-8" />
6
+ <link rel="icon" href="%sveltekit.assets%/favicon.png" />
7
+ <link rel="stylesheet" href="%sveltekit.assets%/global.css" />
8
+ <link href='https://fonts.googleapis.com/css?family=Roboto' rel='stylesheet'>
9
+ <meta name="viewport" content="width=device-width" />
10
+ %sveltekit.head%
11
+ </head>
12
+
13
+ <body data-sveltekit-preload-data="hover">
14
+ <div style="display: contents">%sveltekit.body%</div>
15
+ </body>
16
+
17
+ </html>
viewer/src/lib/data/models.json ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ [
2
+ {
3
+ "slug": "mv-dream",
4
+ "title": "MVDream",
5
+ "paper": "https://huggingface.co/papers/2308.16512"
6
+ },
7
+ {
8
+ "slug": "sync-dreamer",
9
+ "title": "SyncDreamer",
10
+ "paper": "https://huggingface.co/papers/2309.03453"
11
+ }
12
+ ]
viewer/src/lib/data/scenes.json ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ [
2
+ {
3
+ "slug": "boombox",
4
+ "model": "mv-dream",
5
+ "title": "BoomBox",
6
+ "url": "https://raw.githubusercontent.com/KhronosGroup/glTF-Sample-Models/master/2.0/BoomBox/glTF-Binary/BoomBox.glb"
7
+ },
8
+ {
9
+ "slug": "duck",
10
+ "model": "mv-dream",
11
+ "title": "Duck",
12
+ "url": "https://raw.githubusercontent.com/KhronosGroup/glTF-Sample-Models/master/2.0/Duck/glTF-Binary/Duck.glb"
13
+ },
14
+ {
15
+ "slug": "toycar",
16
+ "model": "sync-dreamer",
17
+ "title": "Toy Car",
18
+ "url": "https://raw.githubusercontent.com/KhronosGroup/glTF-Sample-Models/master/2.0/ToyCar/glTF-Binary/ToyCar.glb"
19
+ }
20
+ ]
viewer/src/lib/dataLoader.ts ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ import scenes from "$lib/data/scenes.json";
2
+ import models from "$lib/data/models.json";
3
+
4
+ export async function getScenes() {
5
+ return scenes;
6
+ }
7
+
8
+ export async function getModels() {
9
+ return models;
10
+ }
viewer/src/lib/index.ts ADDED
@@ -0,0 +1 @@
 
 
1
+ // place files you want to import through the `$lib` alias in this folder.
viewer/src/lib/placeholder.png ADDED
viewer/src/routes/+page.svelte ADDED
@@ -0,0 +1,49 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <script lang="ts">
2
+ import { activeTab } from "./store.js";
3
+ import ModelsView from "./components/ModelsView.svelte";
4
+ import ScenesView from "./components/ScenesView.svelte";
5
+ </script>
6
+
7
+ <div class="container">
8
+ <div class="tabs">
9
+ <button on:click={() => activeTab.set("Models")} class={$activeTab === "Models" ? "active" : ""}>Models</button>
10
+ <button on:click={() => activeTab.set("Scenes")} class={$activeTab === "Scenes" ? "active" : ""}>Scenes</button>
11
+ </div>
12
+ {#if $activeTab === "Models"}
13
+ <ModelsView />
14
+ {:else}
15
+ <ScenesView />
16
+ {/if}
17
+ </div>
18
+
19
+ <style>
20
+ .tabs {
21
+ display: flex;
22
+ justify-content: left;
23
+ margin-top: 10px;
24
+ margin-bottom: 10px;
25
+ gap: 10px;
26
+ width: 100%;
27
+ }
28
+
29
+ .tabs button {
30
+ background-color: #1a1b1e;
31
+ color: #ddd;
32
+ border: none;
33
+ outline: none;
34
+ padding: 10px 10px 10px 10px;
35
+ font-family: "Roboto", sans-serif;
36
+ font-size: 14px;
37
+ font-weight: 600;
38
+ transition: background-color 0.2s ease;
39
+ }
40
+
41
+ .tabs button:hover {
42
+ cursor: pointer;
43
+ background-color: #555;
44
+ }
45
+
46
+ .tabs button.active {
47
+ background-color: #444;
48
+ }
49
+ </style>
viewer/src/routes/components/ModelsView.svelte ADDED
@@ -0,0 +1,40 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <script lang="ts">
2
+ import { onMount } from "svelte";
3
+ import { getModels, getScenes } from "$lib/dataLoader";
4
+ import placeholderImage from "$lib/placeholder.png";
5
+
6
+ let models: any[] = [];
7
+ let sceneMap: any = {};
8
+
9
+ onMount(async () => {
10
+ models = await getModels();
11
+ const scenes = await getScenes();
12
+ for (let model of models) {
13
+ for (let scene of scenes) {
14
+ if (scene.model === model.slug) {
15
+ sceneMap[model.slug] = scene.slug;
16
+ break;
17
+ }
18
+ }
19
+ }
20
+ });
21
+
22
+ function handleImageError(event: Event) {
23
+ const image = event.currentTarget as HTMLImageElement;
24
+ image.src = placeholderImage;
25
+ }
26
+ </script>
27
+
28
+ <div class="grid">
29
+ {#each models as model}
30
+ <a href={`/models/${model.slug}`} class="grid-item">
31
+ <img
32
+ src={`/thumbnails/${sceneMap[model.slug]}.png`}
33
+ alt={model.title}
34
+ class="thumbnail"
35
+ on:error={(event) => handleImageError(event)}
36
+ />
37
+ <div class="title">{model.title}</div>
38
+ </a>
39
+ {/each}
40
+ </div>
viewer/src/routes/components/ScenesView.svelte ADDED
@@ -0,0 +1,30 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <script lang="ts">
2
+ import { onMount } from "svelte";
3
+ import { getScenes } from "$lib/dataLoader";
4
+ import placeholderImage from "$lib/placeholder.png";
5
+
6
+ let scenes: any[] = [];
7
+
8
+ onMount(async () => {
9
+ scenes = await getScenes();
10
+ });
11
+
12
+ function handleImageError(event: Event) {
13
+ const image = event.currentTarget as HTMLImageElement;
14
+ image.src = placeholderImage;
15
+ }
16
+ </script>
17
+
18
+ <div class="grid">
19
+ {#each scenes as scene}
20
+ <a href={`/viewer/${scene.slug}`} class="grid-item">
21
+ <img
22
+ src={`/thumbnails/${scene.slug}.png`}
23
+ alt={scene.title}
24
+ class="thumbnail"
25
+ on:error={(event) => handleImageError(event)}
26
+ />
27
+ <div class="title">{scene.title}</div>
28
+ </a>
29
+ {/each}
30
+ </div>
viewer/src/routes/models/[slug]/+page.server.ts ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { error } from "@sveltejs/kit";
2
+ import { getModels, getScenes } from "$lib/dataLoader";
3
+
4
+ export async function load({ params }) {
5
+ const models = await getModels();
6
+ const scenes = await getScenes();
7
+
8
+ const model = models.find((model: any) => model.slug === params.slug);
9
+ const modelScenes = scenes.filter((scene: any) => scene.model === params.slug);
10
+
11
+ if (!modelScenes.length) throw error(404);
12
+
13
+ return {
14
+ model: model,
15
+ scenes: modelScenes
16
+ };
17
+ }
viewer/src/routes/models/[slug]/+page.svelte ADDED
@@ -0,0 +1,132 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <script lang="ts">
2
+ import placeholderImage from "$lib/placeholder.png";
3
+
4
+ export let data: {
5
+ model: {
6
+ title: string;
7
+ paper: string;
8
+ };
9
+ scenes: {
10
+ slug: string;
11
+ title: string;
12
+ }[];
13
+ };
14
+
15
+ function handleImageError(event: Event) {
16
+ const image = event.currentTarget as HTMLImageElement;
17
+ image.src = placeholderImage;
18
+ }
19
+ </script>
20
+
21
+ <div class="container">
22
+ <div class="header">{data.model.title}</div>
23
+ <div class="model-container">
24
+ <div class="grid-container">
25
+ <div class="grid">
26
+ {#each data.scenes as scene}
27
+ <a href={`/viewer/${scene.slug}`} class="grid-item">
28
+ <img
29
+ src={`/thumbnails/${scene.slug}.png`}
30
+ alt={scene.title}
31
+ class="thumbnail"
32
+ on:error={(event) => handleImageError(event)}
33
+ />
34
+ <div class="title">{scene.title}</div>
35
+ </a>
36
+ {/each}
37
+ </div>
38
+ </div>
39
+ <div class="model-info">
40
+ <p class="model-header">Info</p>
41
+ <table class="table">
42
+ {#if data.model.paper}
43
+ <tr>
44
+ <td>Paper</td>
45
+ <td><a href={data.model.paper} target="_blank">Link</a></td>
46
+ </tr>
47
+ {/if}
48
+ </table>
49
+ </div>
50
+ </div>
51
+ </div>
52
+
53
+ <style>
54
+ .model-header {
55
+ padding: 10px;
56
+ font-size: 16px;
57
+ color: #aaa;
58
+ margin: 0;
59
+ }
60
+
61
+ .table {
62
+ width: auto;
63
+ }
64
+
65
+ .table td {
66
+ margin: 0;
67
+ padding: 10px;
68
+ border-top: 1px solid #333;
69
+ white-space: nowrap;
70
+ }
71
+
72
+ .table td:first-child {
73
+ min-width: 128px;
74
+ background-color: #222;
75
+ border-right: 1px solid #333;
76
+ font-size: 16px;
77
+ font-weight: bold;
78
+ color: #aaa;
79
+ }
80
+
81
+ .table td:last-child {
82
+ width: 100%;
83
+ font-size: 16px;
84
+ }
85
+
86
+ .table a {
87
+ color: #6d90b6;
88
+ }
89
+
90
+ .model-container {
91
+ display: flex;
92
+ flex-wrap: wrap;
93
+ align-items: flex-start;
94
+ }
95
+
96
+ .grid-container {
97
+ flex: 1;
98
+ display: flex;
99
+ flex-wrap: wrap;
100
+ justify-content: center;
101
+ }
102
+
103
+ .model-info {
104
+ border: 1px solid #333;
105
+ box-sizing: border-box;
106
+ width: 100%;
107
+ margin: 10px;
108
+
109
+ @media (min-width: 576px) {
110
+ width: 384px;
111
+ margin-top: 0;
112
+ }
113
+ }
114
+
115
+ .grid-item {
116
+ @media (min-width: 576px) {
117
+ width: 100%;
118
+ }
119
+
120
+ @media (min-width: 768px) {
121
+ width: calc(50% - 10px);
122
+ }
123
+
124
+ @media (min-width: 992px) {
125
+ width: calc(33.333% - 10px);
126
+ }
127
+
128
+ @media (min-width: 1200px) {
129
+ width: calc(25% - 10px);
130
+ }
131
+ }
132
+ </style>
viewer/src/routes/store.js ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ import { writable } from 'svelte/store';
2
+
3
+ export const activeTab = writable('Models');
viewer/src/routes/viewer/[slug]/+page.server.ts ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { error } from "@sveltejs/kit";
2
+ import { getScenes } from "$lib/dataLoader";
3
+
4
+ export async function load({ params }) {
5
+ const scenes = await getScenes();
6
+ const scene = scenes.find((scene: any) => scene.slug === params.slug);
7
+
8
+ if (!scene) throw error(404);
9
+
10
+ return {
11
+ scene
12
+ };
13
+ }
viewer/src/routes/viewer/[slug]/+page.svelte ADDED
@@ -0,0 +1,348 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <script lang="ts">
2
+ import { onMount, onDestroy } from "svelte";
3
+ import * as BABYLON from "@babylonjs/core";
4
+ import "@babylonjs/loaders/glTF";
5
+
6
+ export let data: {
7
+ scene: {
8
+ url: string;
9
+ };
10
+ };
11
+
12
+ let overlay: HTMLDivElement | null = null;
13
+ let hud: HTMLDivElement | null = null;
14
+ let hudToggleBtn: HTMLButtonElement | null = null;
15
+ let triangleCount: HTMLSpanElement | null = null;
16
+ let fpsCount: HTMLSpanElement | null = null;
17
+ let engine: BABYLON.Engine | null = null;
18
+ let scene: BABYLON.Scene | null = null;
19
+ let camera: BABYLON.ArcRotateCamera | null = null;
20
+ let container: HTMLDivElement | null = null;
21
+ let canvas: HTMLCanvasElement | null = null;
22
+ let collapsed = false;
23
+
24
+ onMount(() => {
25
+ document.body.classList.add("viewer");
26
+
27
+ const isMobile = window.innerWidth < 768;
28
+ if (isMobile) {
29
+ collapsed = true;
30
+ hudToggleBtn!.textContent = ")";
31
+ container!.classList.remove("hud-expanded");
32
+ }
33
+
34
+ loadModel(data.scene.url);
35
+
36
+ hudToggleBtn!.addEventListener("click", () => {
37
+ collapsed = !collapsed;
38
+ hudToggleBtn!.textContent = collapsed ? ")" : "(";
39
+ if (collapsed) {
40
+ container!.classList.remove("hud-expanded");
41
+ } else {
42
+ container!.classList.add("hud-expanded");
43
+ }
44
+ });
45
+
46
+ const modeItems = document.querySelectorAll(".mode-item");
47
+ modeItems.forEach((item) => {
48
+ item.addEventListener("click", (event) => {
49
+ const currentTarget = event.currentTarget as HTMLElement;
50
+ const mode = currentTarget.getAttribute("data-mode");
51
+
52
+ modeItems.forEach((i) => i.classList.remove("active"));
53
+ currentTarget.classList.add("active");
54
+
55
+ switch (mode) {
56
+ case "wireframe":
57
+ scene!.forceWireframe = true;
58
+ break;
59
+ default:
60
+ scene!.forceWireframe = false;
61
+ break;
62
+ }
63
+ });
64
+ });
65
+ });
66
+
67
+ onDestroy(() => {
68
+ if (scene) {
69
+ scene.dispose();
70
+ document.body.classList.remove("viewer");
71
+ }
72
+ });
73
+
74
+ async function loadModel(url: string) {
75
+ overlay!.style.display = "flex";
76
+
77
+ engine = new BABYLON.Engine(canvas, true);
78
+
79
+ scene = new BABYLON.Scene(engine);
80
+ scene.clearColor = BABYLON.Color4.FromHexString("#1A1B1EFF");
81
+
82
+ await BABYLON.SceneLoader.AppendAsync("", url, scene);
83
+
84
+ scene.cameras.forEach((camera) => {
85
+ camera.dispose();
86
+ });
87
+
88
+ scene.lights.forEach((light) => {
89
+ light.dispose();
90
+ });
91
+
92
+ camera = new BABYLON.ArcRotateCamera("camera", Math.PI / 3, Math.PI / 3, 30, BABYLON.Vector3.Zero(), scene);
93
+ camera.angularSensibilityY = 1000;
94
+ camera.panningSensibility = 500;
95
+ camera.wheelPrecision = 5;
96
+ camera.inertia = 0.9;
97
+ camera.panningInertia = 0.9;
98
+ camera.lowerRadiusLimit = 3;
99
+ camera.upperRadiusLimit = 100;
100
+ camera.setTarget(BABYLON.Vector3.Zero());
101
+ camera.attachControl(canvas, true);
102
+
103
+ camera.onAfterCheckInputsObservable.add(() => {
104
+ camera!.wheelPrecision = 150 / camera!.radius;
105
+ camera!.panningSensibility = 10000 / camera!.radius;
106
+ });
107
+
108
+ const light = new BABYLON.HemisphericLight("hemi", new BABYLON.Vector3(-0.5, 1, 0.5), scene);
109
+ light.intensity = 0.7;
110
+ light.diffuse = new BABYLON.Color3(1, 1, 1);
111
+ light.groundColor = new BABYLON.Color3(0.3, 0.3, 0.3);
112
+
113
+ const standardSize = 10;
114
+ let scaleFactor = 1;
115
+ let center = BABYLON.Vector3.Zero();
116
+
117
+ if (scene.meshes.length > 0) {
118
+ let bounds = scene.meshes[0].getBoundingInfo().boundingBox;
119
+ let min = bounds.minimumWorld;
120
+ let max = bounds.maximumWorld;
121
+
122
+ for (let i = 1; i < scene.meshes.length; i++) {
123
+ bounds = scene.meshes[i].getBoundingInfo().boundingBox;
124
+ min = BABYLON.Vector3.Minimize(min, bounds.minimumWorld);
125
+ max = BABYLON.Vector3.Maximize(max, bounds.maximumWorld);
126
+ }
127
+
128
+ const extent = max.subtract(min).scale(0.5);
129
+ const size = extent.length();
130
+
131
+ center = BABYLON.Vector3.Center(min, max);
132
+
133
+ scaleFactor = standardSize / size;
134
+ }
135
+
136
+ const parentNode = new BABYLON.TransformNode("parent", scene);
137
+
138
+ let totalTriangles = 0;
139
+ scene.meshes.forEach((mesh) => {
140
+ mesh.setParent(parentNode);
141
+ if (mesh.getTotalVertices() > 0) {
142
+ totalTriangles += mesh.getTotalIndices() / 3;
143
+ }
144
+ });
145
+ triangleCount!.textContent = totalTriangles.toLocaleString();
146
+
147
+ parentNode.position = center.scale(-1 * scaleFactor);
148
+ parentNode.scaling.scaleInPlace(scaleFactor);
149
+
150
+ engine.runRenderLoop(() => {
151
+ scene!.render();
152
+ if (fpsCount) {
153
+ fpsCount.textContent = engine!.getFps().toFixed();
154
+ }
155
+ });
156
+
157
+ window.addEventListener("resize", () => {
158
+ updateCanvasSize();
159
+ engine!.resize();
160
+ });
161
+
162
+ updateCanvasSize();
163
+ overlay!.style.display = "none";
164
+ }
165
+
166
+ function updateCanvasSize() {
167
+ if (!canvas || !container) return;
168
+ canvas.width = container.clientWidth;
169
+ canvas.height = container.clientHeight;
170
+ }
171
+
172
+ function capture() {
173
+ if (!engine || !camera) return;
174
+ const cachedColor = scene!.clearColor;
175
+ scene!.clearColor = BABYLON.Color4.FromHexString("#00000000");
176
+ BABYLON.Tools.CreateScreenshotUsingRenderTarget(engine, camera, 512, (data) => {
177
+ const a = document.createElement("a");
178
+ a.href = data;
179
+ a.download = "screenshot.png";
180
+ a.click();
181
+ });
182
+ scene!.clearColor = cachedColor;
183
+ }
184
+ </script>
185
+
186
+ <div bind:this={container} class="canvas-container hud-expanded">
187
+ <div bind:this={overlay} class="loading-overlay" />
188
+ <canvas bind:this={canvas} width="512" height="512" />
189
+ <div bind:this={hud} class="hud" class:collapsed>
190
+ <button bind:this={hudToggleBtn} class="hud-toggle-btn">(</button>
191
+ <div class="section">
192
+ <div class="section-title">Stats</div>
193
+ <div class="info-panel">
194
+ <div>FPS: <span bind:this={fpsCount}>0</span></div>
195
+ <div>Triangles: <span bind:this={triangleCount}>0</span></div>
196
+ </div>
197
+ </div>
198
+ <div class="section">
199
+ <div class="section-title">Render Mode</div>
200
+ <div class="button-group mode-list">
201
+ <div class="hud-button mode-item active" data-mode="rendered">Rendered</div>
202
+ <div class="hud-button mode-item" data-mode="wireframe">Wireframe</div>
203
+ </div>
204
+ </div>
205
+ <div class="section">
206
+ <div class="section-title">Actions</div>
207
+ <div class="button-group">
208
+ <div class="hud-button" on:pointerdown={capture}>Capture</div>
209
+ </div>
210
+ </div>
211
+ </div>
212
+ </div>
213
+
214
+ <style>
215
+ .canvas-container {
216
+ position: relative;
217
+ box-sizing: border-box;
218
+ transition: padding-left 0.2s ease;
219
+ display: flex;
220
+ flex-direction: column;
221
+ justify-content: center;
222
+ align-items: center;
223
+ width: 100vw;
224
+ height: 100vh;
225
+ overflow: hidden;
226
+ }
227
+
228
+ .canvas-container.hud-expanded {
229
+ padding-left: 192px;
230
+ }
231
+
232
+ .loading-overlay {
233
+ position: absolute;
234
+ top: 0;
235
+ left: 0;
236
+ width: 100%;
237
+ height: 100%;
238
+ background-color: #1a1b1e;
239
+ display: flex;
240
+ justify-content: center;
241
+ align-items: center;
242
+ z-index: 100;
243
+ }
244
+
245
+ .loading-overlay::before {
246
+ content: "Loading...";
247
+ color: white;
248
+ font-size: 16px;
249
+ }
250
+
251
+ canvas {
252
+ max-width: 100%;
253
+ max-height: 100%;
254
+ }
255
+
256
+ canvas:focus {
257
+ outline: none;
258
+ }
259
+
260
+ .hud {
261
+ position: absolute;
262
+ top: 0;
263
+ left: 0;
264
+ width: 192px;
265
+ height: 100%;
266
+ box-sizing: border-box;
267
+ font-size: 14px;
268
+ border-right: 1px solid #444;
269
+ background-color: #1a1b1e;
270
+ box-shadow: 0px 2px 5px rgba(0, 0, 0, 0.3);
271
+ transition: transform 0.2s ease;
272
+ margin: 0;
273
+ padding: 0;
274
+ }
275
+
276
+ .hud-toggle-btn {
277
+ position: absolute;
278
+ right: -30px;
279
+ top: 50%;
280
+ transform: translateY(-50%);
281
+ background-color: #1a1b1e;
282
+ border: none;
283
+ color: #aaa;
284
+ font-size: 16px;
285
+ cursor: pointer;
286
+ outline: none;
287
+ width: 29px;
288
+ height: 100%;
289
+ box-sizing: border-box;
290
+ transition: background-color 0.2s ease;
291
+ }
292
+
293
+ .hud-toggle-btn:hover {
294
+ background-color: #444;
295
+ }
296
+
297
+ .hud.collapsed {
298
+ transform: translateX(-100%);
299
+ }
300
+
301
+ .section {
302
+ width: 100%;
303
+ padding: 10px;
304
+ box-sizing: border-box;
305
+ }
306
+
307
+ .section-title {
308
+ font-size: 11px;
309
+ font-weight: light;
310
+ text-transform: uppercase;
311
+ color: #aaa;
312
+ width: 100%;
313
+ padding: 4px 4px 4px 4px;
314
+ }
315
+
316
+ .info-panel {
317
+ padding: 10px 10px 0px 10px;
318
+ color: #ddd;
319
+ }
320
+
321
+ .button-group {
322
+ border: none;
323
+ background-color: transparent;
324
+ box-sizing: border-box;
325
+ }
326
+
327
+ .hud-button {
328
+ padding: 10px 15px;
329
+ cursor: pointer;
330
+ background-color: #1a1b1e;
331
+ border-bottom: 1px solid #444;
332
+ transition: background-color 0.2s ease;
333
+ box-sizing: border-box;
334
+ }
335
+
336
+ .hud-button:last-child {
337
+ border-bottom: none;
338
+ }
339
+
340
+ .hud-button:hover {
341
+ background-color: #555;
342
+ }
343
+
344
+ .hud-button.active {
345
+ background-color: #444;
346
+ color: white;
347
+ }
348
+ </style>
viewer/static/favicon.png ADDED
viewer/static/global.css ADDED
@@ -0,0 +1,121 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ html,
2
+ body {
3
+ font-family: 'Roboto', sans-serif;
4
+ background-color: #1a1b1e;
5
+ color: white;
6
+ }
7
+
8
+ .container {
9
+ padding-left: 15px;
10
+ padding-right: 15px;
11
+ margin-left: auto;
12
+ margin-right: auto;
13
+
14
+ @media (min-width: 576px) {
15
+ max-width: 540px;
16
+ }
17
+
18
+ @media (min-width: 768px) {
19
+ max-width: 720px;
20
+ }
21
+
22
+ @media (min-width: 992px) {
23
+ max-width: 960px;
24
+ }
25
+
26
+ @media (min-width: 1200px) {
27
+ max-width: 1140px;
28
+ }
29
+ }
30
+
31
+
32
+ .header {
33
+ padding: 20px 0;
34
+ font-size: 32px;
35
+ font-weight: bold;
36
+ color: #aaa;
37
+ }
38
+
39
+ .viewer {
40
+ margin: 0;
41
+ padding: 0;
42
+ overflow: hidden;
43
+ }
44
+
45
+ .grid {
46
+ display: flex;
47
+ flex-wrap: wrap;
48
+ gap: 10px;
49
+ width: 100%;
50
+ }
51
+
52
+ .grid-item {
53
+ background: #333;
54
+ position: relative;
55
+ border: 1px solid #000;
56
+ box-sizing: border-box;
57
+ overflow: hidden;
58
+ width: 100%;
59
+
60
+ @media (min-width: 576px) {
61
+ width: calc(33.333% - 10px);
62
+ }
63
+
64
+ @media (min-width: 768px) {
65
+ width: calc(25% - 10px);
66
+ }
67
+
68
+ @media (min-width: 992px) {
69
+ width: calc(20% - 10px);
70
+ }
71
+
72
+ @media (min-width: 1200px) {
73
+ width: calc(16.666% - 10px);
74
+ }
75
+ }
76
+
77
+ .grid-item::before {
78
+ content: "";
79
+ display: block;
80
+ padding-top: 100%;
81
+ }
82
+
83
+ .grid-item:hover {
84
+ cursor: pointer;
85
+ }
86
+
87
+ .grid-item .title {
88
+ position: absolute;
89
+ bottom: 0;
90
+ left: 0;
91
+ width: 100%;
92
+ background-color: rgba(0, 0, 0, 0.2);
93
+ color: #fff;
94
+ text-align: center;
95
+ padding: 10px;
96
+ box-sizing: border-box;
97
+ font-size: 14px;
98
+ height: 40px;
99
+ overflow: hidden;
100
+ transition: height 0.2s ease;
101
+ }
102
+
103
+ .grid-item:hover .title {
104
+ height: 48px;
105
+ }
106
+
107
+ .thumbnail {
108
+ position: absolute;
109
+ top: -10px;
110
+ ;
111
+ left: 0;
112
+ width: 100%;
113
+ height: auto;
114
+ overflow: hidden;
115
+ scale: 1;
116
+ transition: scale 0.2s ease;
117
+ }
118
+
119
+ .grid-item:hover .thumbnail {
120
+ scale: 1.1;
121
+ }
viewer/static/thumbnails/boombox.png ADDED
viewer/static/thumbnails/duck.png ADDED
viewer/static/thumbnails/toycar.png ADDED
viewer/svelte.config.js ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import adapter from '@sveltejs/adapter-auto';
2
+ import { vitePreprocess } from '@sveltejs/kit/vite';
3
+
4
+ /** @type {import('@sveltejs/kit').Config} */
5
+ const config = {
6
+ // Consult https://kit.svelte.dev/docs/integrations#preprocessors
7
+ // for more information about preprocessors
8
+ preprocess: vitePreprocess(),
9
+
10
+ kit: {
11
+ // adapter-auto only supports some environments, see https://kit.svelte.dev/docs/adapter-auto for a list.
12
+ // If your environment is not supported or you settled on a specific environment, switch out the adapter.
13
+ // See https://kit.svelte.dev/docs/adapters for more information about adapters.
14
+ adapter: adapter()
15
+ }
16
+ };
17
+
18
+ export default config;
viewer/tsconfig.json ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "extends": "./.svelte-kit/tsconfig.json",
3
+ "compilerOptions": {
4
+ "allowJs": true,
5
+ "checkJs": true,
6
+ "esModuleInterop": true,
7
+ "forceConsistentCasingInFileNames": true,
8
+ "resolveJsonModule": true,
9
+ "skipLibCheck": true,
10
+ "sourceMap": true,
11
+ "strict": true
12
+ }
13
+ // Path aliases are handled by https://kit.svelte.dev/docs/configuration#alias
14
+ //
15
+ // If you want to overwrite includes/excludes, make sure to copy over the relevant includes/excludes
16
+ // from the referenced tsconfig.json - TypeScript does not merge them in
17
+ }
viewer/vite.config.ts ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ import { sveltekit } from '@sveltejs/kit/vite';
2
+ import { defineConfig } from 'vite';
3
+
4
+ export default defineConfig({
5
+ plugins: [sveltekit()]
6
+ });