Add fullscreen model view
authorLanius Trolling <lanius@laniustrolling.dev>
Thu, 26 Dec 2024 20:31:13 +0000 (15:31 -0500)
committerLanius Trolling <lanius@laniustrolling.dev>
Thu, 26 Dec 2024 20:31:52 +0000 (15:31 -0500)
12 files changed:
src/main/kotlin/info/mechyrdia/Factbooks.kt
src/main/kotlin/info/mechyrdia/data/ViewsComment.kt
src/main/kotlin/info/mechyrdia/lore/ParserHtml.kt
src/main/kotlin/info/mechyrdia/lore/ParserRaw.kt
src/main/kotlin/info/mechyrdia/lore/ViewsMesh.kt [new file with mode: 0644]
src/main/kotlin/info/mechyrdia/lore/ViewsQuote.kt
src/main/kotlin/info/mechyrdia/route/ResourceLimiter.kt [new file with mode: 0644]
src/main/kotlin/info/mechyrdia/route/ResourceTypes.kt
src/main/resources/static/admin.js
src/main/resources/static/init.js
src/main/resources/static/mesh.css [new file with mode: 0644]
src/main/resources/static/mesh.js [new file with mode: 0644]

index 20e0bbc487eea45a835917077b5d37d197e0eeb3..c4094a370f3836ea9eb4a36ae6c22161a7fa4465 100644 (file)
@@ -250,6 +250,7 @@ fun Application.factbooks() {
                get<Root.AssetFile>()
                get<Root.CustomFontsStyle>()
                get<Root.LorePage>()
+               get<Root.MeshView>()
                get<Root.GalaxyMap>()
                get<Root.RandomQuote>()
                get<Root.RobotsTxt>()
index 280fafde13f2d3a2fcd12c44e93ebe1cd31d4159..174a48ec977c8a11c578d8248d3e8751f65df2ea 100644 (file)
@@ -223,7 +223,7 @@ suspend fun ApplicationCall.commentHelpPage(): HTML.() -> Unit = page("Commentin
                                        td { +"[u]Text goes here[/u]" }
                                        td {
                                                span {
-                                                       style = "text-decoration: underline"
+                                                       style = "text-decoration:underline"
                                                        +"Underlines"
                                                }
                                                +" text"
@@ -233,7 +233,7 @@ suspend fun ApplicationCall.commentHelpPage(): HTML.() -> Unit = page("Commentin
                                        td { +"[s]Text goes here[/s]" }
                                        td {
                                                span {
-                                                       style = "text-decoration: line-through"
+                                                       style = "text-decoration:line-through"
                                                        +"Strikes out"
                                                }
                                                +" text"
@@ -306,7 +306,7 @@ suspend fun ApplicationCall.commentHelpPage(): HTML.() -> Unit = page("Commentin
                                        td { +"e.g. [align=center]Text goes here[/align]" }
                                        td {
                                                div {
-                                                       style = "text-align: center"
+                                                       style = "text-align:center"
                                                        +"Center-aligns text"
                                                }
                                        }
index fea9d1edb5562249d957f851fe3848c0345738a6..f7cddf7373cd37a66c36ac875f48366ba59dd859 100644 (file)
@@ -118,11 +118,13 @@ object HtmlLexerProcessor : LexerTagFallback<HtmlBuilderContext, HtmlBuilderSubj
                "ipa",
                "code",
                "desc",
+               "moment",
                "link",
                "extlink",
                "lang",
                "url",
                "reply",
+               "epoch",
        )
        
        override fun processInvalidTag(env: LexerTagEnvironment<HtmlBuilderContext, HtmlBuilderSubject>, tag: String, param: String?, subNodes: ParserTree): HtmlBuilderSubject {
@@ -457,6 +459,17 @@ enum class FactbookFormattingTag(val type: HtmlLexerTag) {
                                style = sizeStyle
                                attributes["data-model"] = url
                        }
+                       br()
+                       a(href = "/mesh/$url") {
+                               style = "font-size:0.8em"
+                               +"View this model in fullscreen"
+                       }
+                       span {
+                               style = "font-size:1.2em"
+                               br
+                               +Entities.nbsp
+                               br
+                       }
                })
        }),
        AUDIO(HtmlTextBodyLexerTag { _, _, content ->
index 28d9cde593d892582f4382798ed4faeb2b0e7de9..c0b5b3975ee09acd08968118699402cf7eceda6b 100644 (file)
@@ -109,7 +109,15 @@ enum class RawFactbookFormattingTag(val type: HtmlLexerTag) {
                                }
                        })
        }),
-       MODEL(HtmlNotSupportedInRawViewTag("Unfortunately, raw view does not support interactive 3D model views")),
+       MODEL(HtmlTextBodyLexerTag { _, _, content ->
+               val url = content.sanitizeLink()
+               
+               ({
+                       a(href = "/mesh/$url") {
+                               +"View interactive 3D model in separate page"
+                       }
+               })
+       }),
        QUIZ(HtmlNotSupportedInRawViewTag("Unfortunately, raw view does not support interactive quizzes")),
        MOMENT(HtmlTextBodyLexerTag { _, _, content ->
                val epochMilli = content.toLongOrNull()?.let { Instant.ofEpochMilli(it).toString() } ?: content
diff --git a/src/main/kotlin/info/mechyrdia/lore/ViewsMesh.kt b/src/main/kotlin/info/mechyrdia/lore/ViewsMesh.kt
new file mode 100644 (file)
index 0000000..c94f2d1
--- /dev/null
@@ -0,0 +1,66 @@
+package info.mechyrdia.lore
+
+import info.mechyrdia.MainDomainName
+import info.mechyrdia.Utf8
+import info.mechyrdia.data.FileStorage
+import info.mechyrdia.data.StoragePath
+import io.ktor.server.application.ApplicationCall
+import io.ktor.server.request.path
+import kotlinx.html.*
+
+private val meshDir = StoragePath.assetDir / "meshes"
+
+suspend fun ApplicationCall.meshView(mesh: String): HTML.() -> Unit {
+       val pageTitle = "Viewing Mesh $mesh"
+       
+       val meshObj = String(FileStorage.instance.readFile(meshDir / "$mesh.obj")!!, Utf8)
+       val meshMtl = String(FileStorage.instance.readFile(meshDir / "$mesh.mtl")!!, Utf8)
+       
+       return {
+               lang = "en"
+               
+               head {
+                       meta(charset = "utf-8")
+                       meta(name = "viewport", content = "width=device-width, initial-scale=1.0")
+                       
+                       meta(name = "theme-color", content = "#FFCC33")
+                       
+                       ogProperty("title", pageTitle)
+                       ogProperty("type", "website")
+                       ogProperty("url", "$MainDomainName/${request.path().removePrefix("/")}")
+                       
+                       link(rel = "icon", type = "image/png", href = "/static/images/icon.png")
+                       link(rel = "icon", type = "image/svg+xml", href = "/static/images/icon.svg")
+                       
+                       title { +pageTitle }
+                       
+                       script(src = "/static/obj-viewer/three.js") {}
+                       script(src = "/static/obj-viewer/three-examples.js") {}
+                       
+                       script(type = "text/plain") {
+                               attributes["id"] = "mesh-mtl"
+                               unsafe {
+                                       +"\n"
+                                       +meshMtl
+                                       +"\n"
+                               }
+                       }
+                       
+                       script(type = "text/plain") {
+                               attributes["id"] = "mesh-obj"
+                               unsafe {
+                                       +"\n"
+                                       +meshObj
+                                       +"\n"
+                               }
+                       }
+                       
+                       link(rel = "stylesheet", type = "text/css", href = "/static/mesh.css")
+                       
+                       script(src = "/static/mesh.js") {}
+               }
+               body {
+                       canvas {}
+               }
+       }
+}
index 26b94d2a16fb9566faeefa02497905ca5d4ca6bc..55d6e5c72bb3c35218c863e757ed665b356b4f34 100644 (file)
@@ -11,7 +11,6 @@ import info.mechyrdia.data.respondXml
 import info.mechyrdia.data.root
 import info.mechyrdia.route.KeyedEnumSerializer
 import io.ktor.http.ContentType
-import io.ktor.http.HttpStatusCode
 import io.ktor.server.application.ApplicationCall
 import io.ktor.server.html.respondHtml
 import io.ktor.server.response.respondText
@@ -55,12 +54,12 @@ suspend fun randomQuote(): Quote = getQuotesList().random()
 enum class QuoteFormat(val format: String?) {
        HTML(null) {
                override suspend fun ApplicationCall.respondQuote(quote: Quote) {
-                       respondHtml(HttpStatusCode.OK, block = quote.toHtml(RANDOM_QUOTE_HTML_TITLE, this))
+                       respondHtml(block = quote.toHtml(RANDOM_QUOTE_HTML_TITLE, this))
                }
        },
        RAW_HTML("raw") {
                override suspend fun ApplicationCall.respondQuote(quote: Quote) {
-                       respondHtml(HttpStatusCode.OK, block = quote.toRawHtml(RANDOM_QUOTE_HTML_TITLE, this))
+                       respondHtml(block = quote.toRawHtml(RANDOM_QUOTE_HTML_TITLE, this))
                }
        },
        JSON("json") {
diff --git a/src/main/kotlin/info/mechyrdia/route/ResourceLimiter.kt b/src/main/kotlin/info/mechyrdia/route/ResourceLimiter.kt
new file mode 100644 (file)
index 0000000..a47174d
--- /dev/null
@@ -0,0 +1,29 @@
+package info.mechyrdia.route
+
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.DelicateCoroutinesApi
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.GlobalScope
+import kotlinx.coroutines.channels.Channel
+import kotlinx.coroutines.channels.produce
+import kotlinx.coroutines.currentCoroutineContext
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.isActive
+import kotlin.time.Duration
+import kotlin.time.Duration.Companion.milliseconds
+
+class TickingLimiter @OptIn(DelicateCoroutinesApi::class) constructor(interval: Duration, scope: CoroutineScope = GlobalScope) {
+       @OptIn(ExperimentalCoroutinesApi::class)
+       private val producer = scope.produce(capacity = Channel.RENDEZVOUS) {
+               while (currentCoroutineContext().isActive) {
+                       send(Unit)
+                       delay(interval)
+               }
+       }
+       
+       suspend fun limit() = producer.receive()
+}
+
+object ResourceLimiters {
+       val utils = TickingLimiter(75.milliseconds)
+}
index 24da667d8542618b9b854b260a54f57890bb2853..88988953da8339050729f4df4c58af825d9b9d64 100644 (file)
@@ -49,6 +49,7 @@ import info.mechyrdia.lore.generateRecentPageEdits
 import info.mechyrdia.lore.loadFontsJson
 import info.mechyrdia.lore.loreArticlePage
 import info.mechyrdia.lore.loreIntroPage
+import info.mechyrdia.lore.meshView
 import info.mechyrdia.lore.parseAs
 import info.mechyrdia.lore.randomQuote
 import info.mechyrdia.lore.recentCommentsRssFeedGenerator
@@ -76,7 +77,6 @@ import io.ktor.server.response.respondTextWriter
 import io.ktor.server.routing.RoutingContext
 import io.ktor.server.websocket.DefaultWebSocketServerSession
 import io.ktor.util.AttributeKey
-import kotlinx.coroutines.delay
 
 const val ErrorMessageCookieName = "ERROR_MSG"
 
@@ -90,7 +90,7 @@ class Root : ResourceHandler, ResourceFilter {
        
        override suspend fun RoutingContext.handleCall() {
                call.filterCall()
-               call.respondHtml(HttpStatusCode.OK, call.loreIntroPage())
+               call.respondHtml(block = call.loreIntroPage())
        }
        
        @Resource("assets/{path...}")
@@ -119,7 +119,16 @@ class Root : ResourceHandler, ResourceFilter {
                override suspend fun RoutingContext.handleCall() {
                        with(root) { call.filterCall() }
                        
-                       call.respondHtml(HttpStatusCode.OK, call.loreArticlePage(path, format))
+                       call.respondHtml(block = call.loreArticlePage(path, format))
+               }
+       }
+       
+       @Resource("mesh/{mesh}")
+       class MeshView(val mesh: String, val root: Root = Root()) : ResourceHandler {
+               override suspend fun RoutingContext.handleCall() {
+                       with(root) { call.filterCall() }
+                       
+                       call.respondHtml(block = call.meshView(mesh))
                }
        }
        
@@ -185,7 +194,7 @@ class Root : ResourceHandler, ResourceFilter {
                override suspend fun RoutingContext.handleCall() {
                        with(root) { call.filterCall() }
                        
-                       call.respondHtml(HttpStatusCode.OK, call.clientSettingsPage())
+                       call.respondHtml(block = call.clientSettingsPage())
                }
        }
        
@@ -200,7 +209,7 @@ class Root : ResourceHandler, ResourceFilter {
                        override suspend fun RoutingContext.handleCall() {
                                with(auth) { call.filterCall() }
                                
-                               call.respondHtml(HttpStatusCode.OK, call.loginPage())
+                               call.respondHtml(block = call.loginPage())
                        }
                }
                
@@ -233,7 +242,7 @@ class Root : ResourceHandler, ResourceFilter {
                
                override suspend fun RoutingContext.handleCall() {
                        call.filterCall()
-                       call.respondHtml(HttpStatusCode.OK, call.robotPage())
+                       call.respondHtml(block = call.robotPage())
                }
                
                @Resource("ws")
@@ -257,7 +266,7 @@ class Root : ResourceHandler, ResourceFilter {
                        override suspend fun RoutingContext.handleCall() {
                                with(comments) { call.filterCall() }
                                
-                               call.respondHtml(HttpStatusCode.OK, call.commentHelpPage())
+                               call.respondHtml(block = call.commentHelpPage())
                        }
                }
                
@@ -266,7 +275,7 @@ class Root : ResourceHandler, ResourceFilter {
                        override suspend fun RoutingContext.handleCall() {
                                with(comments) { call.filterCall() }
                                
-                               call.respondHtml(HttpStatusCode.OK, call.recentCommentsPage(limit))
+                               call.respondHtml(block = call.recentCommentsPage(limit))
                        }
                }
                
@@ -304,7 +313,7 @@ class Root : ResourceHandler, ResourceFilter {
                        override suspend fun RoutingContext.handleCall() {
                                with(comments) { call.filterCall() }
                                
-                               call.respondHtml(HttpStatusCode.OK, call.deleteCommentPage(id))
+                               call.respondHtml(block = call.deleteCommentPage(id))
                        }
                }
                
@@ -335,7 +344,7 @@ class Root : ResourceHandler, ResourceFilter {
                        override suspend fun RoutingContext.handleCall() {
                                with(user) { call.filterCall() }
                                
-                               call.respondHtml(HttpStatusCode.OK, call.userPage(slug))
+                               call.respondHtml(block = call.userPage(slug))
                        }
                }
        }
@@ -375,7 +384,7 @@ class Root : ResourceHandler, ResourceFilter {
                        
                        override suspend fun RoutingContext.handleCall() {
                                call.filterCall()
-                               call.respondHtml(HttpStatusCode.OK, call.robotManagementPage())
+                               call.respondHtml(block = call.robotManagementPage())
                        }
                        
                        @Resource("update")
@@ -434,7 +443,7 @@ class Root : ResourceHandler, ResourceFilter {
                                override suspend fun RoutingContext.handleCall() {
                                        with(vfs) { call.filterCall() }
                                        
-                                       call.respondHtml(HttpStatusCode.OK, call.adminViewVfs(StoragePath(path)))
+                                       call.respondHtml(block = call.adminViewVfs(StoragePath(path)))
                                }
                        }
                        
@@ -443,7 +452,7 @@ class Root : ResourceHandler, ResourceFilter {
                                override suspend fun RoutingContext.handleCall() {
                                        with(vfs) { call.filterCall() }
                                        
-                                       call.respondHtml(HttpStatusCode.OK, call.adminRequestWebDavToken())
+                                       call.respondHtml(block = call.adminRequestWebDavToken())
                                }
                        }
                        
@@ -472,7 +481,7 @@ class Root : ResourceHandler, ResourceFilter {
                                override suspend fun RoutingContext.handleCall() {
                                        with(vfs) { call.filterCall() }
                                        
-                                       call.respondHtml(HttpStatusCode.OK, call.adminShowCopyFile(StoragePath(path)))
+                                       call.respondHtml(block = call.adminShowCopyFile(StoragePath(path)))
                                }
                        }
                        
@@ -567,7 +576,7 @@ class Root : ResourceHandler, ResourceFilter {
                override suspend fun ApplicationCall.filterCall() {
                        with(root) { filterCall() }
                        
-                       delay(250L)
+                       ResourceLimiters.utils.limit()
                }
                
                @Resource("mechyrdia-sans")
index c935f7c3976a78e0c769733fd5a3cf964be551c9..2afe40528d0e76d311519caf34d6c584dae4a06a 100644 (file)
@@ -4,7 +4,7 @@
                        .split(";")
                        .reduce((obj, entry) => {
                                const trimmed = entry.trim();
-                               const eqI = trimmed.indexOf('=');
+                               const eqI = trimmed.indexOf("=");
                                const key = trimmed.substring(0, eqI).trimEnd();
                                const value = trimmed.substring(eqI + 1).trimStart();
                                return {...obj, [key]: value};
index 253018b001acd4c8b6ad9b79e9df4338deb2f514..0ce652b9efe5ef17e36278805fd895433f70216e 100644 (file)
                        .split(";")
                        .reduce((obj, entry) => {
                                const trimmed = entry.trim();
-                               const eqI = trimmed.indexOf('=');
+                               const eqI = trimmed.indexOf("=");
                                const key = trimmed.substring(0, eqI).trimEnd();
                                const value = trimmed.substring(eqI + 1).trimStart();
                                return {...obj, [key]: value};
         */
        function loadScript(url) {
                return new Promise((resolve, reject) => {
-                       const script = document.createElement('script');
+                       const script = document.createElement("script");
                        script.addEventListener("load", () => resolve());
                        script.addEventListener("error", e => reject(e));
                        script.src = url;
                        const secondCell = document.createElement("td");
                        secondCell.style.textAlign = "center";
                        secondCell.appendChild(document.createElement("img")).src = quiz.image;
-                       for (const paragraph of quiz.intro.split('\n')) {
+                       for (const paragraph of quiz.intro.split("\n")) {
                                secondCell.appendChild(document.createElement("p")).append(paragraph);
                        }
                        secondRow.appendChild(secondCell);
                        const secondCell = document.createElement("td");
                        secondCell.style.textAlign = "center";
                        secondCell.appendChild(document.createElement("img")).src = outcome.img;
-                       for (const paragraph of outcome.desc.split('\n')) {
+                       for (const paragraph of outcome.desc.split("\n")) {
                                secondCell.appendChild(document.createElement("p")).append(paragraph);
                        }
                        secondRow.appendChild(secondCell);
                                        for (const line of inText.split("\n"))
                                                urlParams.append("lines", line.trim());
 
-                                       outBlob = await (await fetch('/utils/mechyrdia-sans', {
-                                               method: 'POST',
+                                       outBlob = await (await fetch("/utils/mechyrdia-sans", {
+                                               method: "POST",
                                                headers: {
-                                                       'Content-Type': 'application/x-www-form-urlencoded',
+                                                       "Content-Type": "application/x-www-form-urlencoded",
                                                },
                                                body: urlParams,
                                        })).blob();
                                for (const line of inText.split("\n"))
                                        urlParams.append("lines", line.trim());
 
-                               const outText = await (await fetch('/utils/tylan-lang', {
-                                       method: 'POST',
+                               const outText = await (await fetch("/utils/tylan-lang", {
+                                       method: "POST",
                                        headers: {
-                                               'Content-Type': 'application/x-www-form-urlencoded',
+                                               "Content-Type": "application/x-www-form-urlencoded",
                                        },
                                        body: urlParams,
                                })).text();
                                for (const line of inText.split("\n"))
                                        urlParams.append("lines", line.trim());
 
-                               const outText = await (await fetch('/utils/pokhwal-lang', {
-                                       method: 'POST',
+                               const outText = await (await fetch("/utils/pokhwal-lang", {
+                                       method: "POST",
                                        headers: {
-                                               'Content-Type': 'application/x-www-form-urlencoded',
+                                               "Content-Type": "application/x-www-form-urlencoded",
                                        },
                                        body: urlParams,
                                })).text();
                                                        }
 
                                                        function onResize() {
-                                                               const dim = canvas.getBoundingClientRect();
-                                                               camera.aspect = dim.width / dim.height;
+                                                               const {width, height} = canvas.getBoundingClientRect();
+
+                                                               camera.aspect = width / height;
                                                                camera.updateProjectionMatrix();
-                                                               renderer.setSize(dim.width, dim.height, false);
+                                                               renderer.setSize(width, height, false);
                                                        }
 
-                                                       window.addEventListener('resize', onResize);
+                                                       window.addEventListener("resize", onResize);
                                                        await frame();
                                                        onResize();
 
                                for (const line of inText.split("\n"))
                                        urlParams.append("lines", line.trim());
 
-                               const outText = await (await fetch('/utils/preview-comment', {
-                                       method: 'POST',
+                               const outText = await (await fetch("/utils/preview-comment", {
+                                       method: "POST",
                                        headers: {
-                                               'Content-Type': 'application/x-www-form-urlencoded',
+                                               "Content-Type": "application/x-www-form-urlencoded",
                                        },
                                        body: urlParams,
                                })).text();
                                        enterBtn.disabled = true;
                                });
 
-                               webSock.addEventListener("open", _ => {
+                               webSock.addEventListener("open", () => {
                                        webSock.send(nukeBox.getAttribute("data-ws-csrf-token"));
-                               })
+                               });
                        }
                })();
        }
diff --git a/src/main/resources/static/mesh.css b/src/main/resources/static/mesh.css
new file mode 100644 (file)
index 0000000..3eb3e83
--- /dev/null
@@ -0,0 +1,9 @@
+canvas {
+       position: fixed;
+       top: 0;
+       left: 0;
+       right: 0;
+       bottom: 0;
+
+       background: linear-gradient(to bottom, #081C30, #040608);
+}
diff --git a/src/main/resources/static/mesh.js b/src/main/resources/static/mesh.js
new file mode 100644 (file)
index 0000000..e6c0c2b
--- /dev/null
@@ -0,0 +1,74 @@
+(function () {
+       /**
+        * @return {Promise<DOMHighResTimeStamp>}
+        */
+       function frame() {
+               return new Promise(resolve => window.requestAnimationFrame(resolve));
+       }
+
+       window.addEventListener("load", () => {
+               const THREE = window.THREE;
+
+               (async () => {
+                       const modelObj = document.getElementById("mesh-obj").text;
+                       const modelMtl = document.getElementById("mesh-mtl").text;
+
+                       const canvas = document.querySelector("canvas");
+
+                       const model = (() => {
+                               const mtlLib = (new THREE.MTLLoader()).parse(modelMtl, "/assets/meshes/");
+                               mtlLib.preload();
+                               return (new THREE.OBJLoader()).setMaterials(mtlLib).parse(modelObj);
+                       })();
+
+                       const camera = new THREE.PerspectiveCamera(69, 1, 0.01, 1000.0);
+
+                       const scene = new THREE.Scene();
+                       scene.add(new THREE.AmbientLight("#555555", 1.0));
+
+                       const renderer = new THREE.WebGLRenderer({"canvas": canvas, "antialias": true});
+
+                       const controls = new THREE.OrbitControls(camera, canvas);
+
+                       function render() {
+                               controls.update();
+                               renderer.render(scene, camera);
+                               window.requestAnimationFrame(render);
+                       }
+
+                       function onResize() {
+                               const width = document.documentElement.clientWidth;
+                               const height = document.documentElement.clientHeight;
+
+                               camera.aspect = width / height;
+                               camera.updateProjectionMatrix();
+                               renderer.setSize(width, height, false);
+                       }
+
+                       window.addEventListener("resize", onResize);
+                       await frame();
+                       onResize();
+
+                       scene.add(model);
+
+                       const bbox = new THREE.Box3().setFromObject(scene);
+                       bbox.dimensions = {
+                               x: bbox.max.x - bbox.min.x,
+                               y: bbox.max.y - bbox.min.y,
+                               z: bbox.max.z - bbox.min.z
+                       };
+                       model.position.sub(new THREE.Vector3(bbox.min.x + bbox.dimensions.x / 2, bbox.min.y + bbox.dimensions.y / 2, bbox.min.z + bbox.dimensions.z / 2));
+
+                       camera.position.set(bbox.dimensions.x / 2, bbox.dimensions.y / 2, Math.max(bbox.dimensions.x, bbox.dimensions.y, bbox.dimensions.z));
+
+                       const light = new THREE.PointLight("#AAAAAA", 1.0);
+                       scene.add(camera);
+                       camera.add(light);
+                       light.position.set(0, 0, 0);
+
+                       render();
+               })().catch(reason => {
+                       console.error("Error rendering model!", reason);
+               });
+       });
+})();