get<Root.AssetFile>()
get<Root.CustomFontsStyle>()
get<Root.LorePage>()
+ get<Root.MeshView>()
get<Root.GalaxyMap>()
get<Root.RandomQuote>()
get<Root.RobotsTxt>()
td { +"[u]Text goes here[/u]" }
td {
span {
- style = "text-decoration: underline"
+ style = "text-decoration:underline"
+"Underlines"
}
+" text"
td { +"[s]Text goes here[/s]" }
td {
span {
- style = "text-decoration: line-through"
+ style = "text-decoration:line-through"
+"Strikes out"
}
+" text"
td { +"e.g. [align=center]Text goes here[/align]" }
td {
div {
- style = "text-align: center"
+ style = "text-align:center"
+"Center-aligns text"
}
}
"ipa",
"code",
"desc",
+ "moment",
"link",
"extlink",
"lang",
"url",
"reply",
+ "epoch",
)
override fun processInvalidTag(env: LexerTagEnvironment<HtmlBuilderContext, HtmlBuilderSubject>, tag: String, param: String?, subNodes: ParserTree): HtmlBuilderSubject {
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 ->
}
})
}),
- 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
--- /dev/null
+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 {}
+ }
+ }
+}
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
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") {
--- /dev/null
+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)
+}
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
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"
override suspend fun RoutingContext.handleCall() {
call.filterCall()
- call.respondHtml(HttpStatusCode.OK, call.loreIntroPage())
+ call.respondHtml(block = call.loreIntroPage())
}
@Resource("assets/{path...}")
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))
}
}
override suspend fun RoutingContext.handleCall() {
with(root) { call.filterCall() }
- call.respondHtml(HttpStatusCode.OK, call.clientSettingsPage())
+ call.respondHtml(block = call.clientSettingsPage())
}
}
override suspend fun RoutingContext.handleCall() {
with(auth) { call.filterCall() }
- call.respondHtml(HttpStatusCode.OK, call.loginPage())
+ call.respondHtml(block = call.loginPage())
}
}
override suspend fun RoutingContext.handleCall() {
call.filterCall()
- call.respondHtml(HttpStatusCode.OK, call.robotPage())
+ call.respondHtml(block = call.robotPage())
}
@Resource("ws")
override suspend fun RoutingContext.handleCall() {
with(comments) { call.filterCall() }
- call.respondHtml(HttpStatusCode.OK, call.commentHelpPage())
+ call.respondHtml(block = call.commentHelpPage())
}
}
override suspend fun RoutingContext.handleCall() {
with(comments) { call.filterCall() }
- call.respondHtml(HttpStatusCode.OK, call.recentCommentsPage(limit))
+ call.respondHtml(block = call.recentCommentsPage(limit))
}
}
override suspend fun RoutingContext.handleCall() {
with(comments) { call.filterCall() }
- call.respondHtml(HttpStatusCode.OK, call.deleteCommentPage(id))
+ call.respondHtml(block = call.deleteCommentPage(id))
}
}
override suspend fun RoutingContext.handleCall() {
with(user) { call.filterCall() }
- call.respondHtml(HttpStatusCode.OK, call.userPage(slug))
+ call.respondHtml(block = call.userPage(slug))
}
}
}
override suspend fun RoutingContext.handleCall() {
call.filterCall()
- call.respondHtml(HttpStatusCode.OK, call.robotManagementPage())
+ call.respondHtml(block = call.robotManagementPage())
}
@Resource("update")
override suspend fun RoutingContext.handleCall() {
with(vfs) { call.filterCall() }
- call.respondHtml(HttpStatusCode.OK, call.adminViewVfs(StoragePath(path)))
+ call.respondHtml(block = call.adminViewVfs(StoragePath(path)))
}
}
override suspend fun RoutingContext.handleCall() {
with(vfs) { call.filterCall() }
- call.respondHtml(HttpStatusCode.OK, call.adminRequestWebDavToken())
+ call.respondHtml(block = call.adminRequestWebDavToken())
}
}
override suspend fun RoutingContext.handleCall() {
with(vfs) { call.filterCall() }
- call.respondHtml(HttpStatusCode.OK, call.adminShowCopyFile(StoragePath(path)))
+ call.respondHtml(block = call.adminShowCopyFile(StoragePath(path)))
}
}
override suspend fun ApplicationCall.filterCall() {
with(root) { filterCall() }
- delay(250L)
+ ResourceLimiters.utils.limit()
}
@Resource("mechyrdia-sans")
.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};
.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"));
- })
+ });
}
})();
}
--- /dev/null
+canvas {
+ position: fixed;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+
+ background: linear-gradient(to bottom, #081C30, #040608);
+}
--- /dev/null
+(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);
+ });
+ });
+})();