diff options
Diffstat (limited to 'public/App.js')
| -rw-r--r-- | public/App.js | 392 |
1 files changed, 392 insertions, 0 deletions
diff --git a/public/App.js b/public/App.js new file mode 100644 index 0000000..262025a --- /dev/null +++ b/public/App.js @@ -0,0 +1,392 @@ +import { Record } from "./fable_modules/fable-library-js.4.24.0/Types.js"; +import { class_type, record_type, bool_type, string_type } from "./fable_modules/fable-library-js.4.24.0/Reflection.js"; +import { Replay_frames } from "./SomhairlesDream.Shared/Replay.js"; +import { createAtom } from "./fable_modules/fable-library-js.4.24.0/Util.js"; +import { tryFind, item } from "./fable_modules/fable-library-js.4.24.0/Array.js"; +import { isNullOrWhiteSpace, printf, toText } from "./fable_modules/fable-library-js.4.24.0/String.js"; +import { MeshData_triangleIndices, MeshData_positionData } from "./SomhairlesDream.Shared/Domain.js"; +import { min, max } from "./fable_modules/fable-library-js.4.24.0/Double.js"; +import { defaultArg } from "./fable_modules/fable-library-js.4.24.0/Option.js"; +import { awaitPromise, startImmediate } from "./fable_modules/fable-library-js.4.24.0/Async.js"; +import { singleton } from "./fable_modules/fable-library-js.4.24.0/AsyncBuilder.js"; + +export class RunSelection extends Record { + constructor(ProjectId, RunId, BaseId, TargetId, Render) { + super(); + this.ProjectId = ProjectId; + this.RunId = RunId; + this.BaseId = BaseId; + this.TargetId = TargetId; + this.Render = Render; + } +} + +export function RunSelection_$reflection() { + return record_type("SomhairlesDream.Frontend.App.RunSelection", [], RunSelection, () => [["ProjectId", string_type], ["RunId", string_type], ["BaseId", string_type], ["TargetId", string_type], ["Render", bool_type]]); +} + +export class ViewState extends Record { + constructor(Renderer, Scene, Camera, Group, Canvas) { + super(); + this.Renderer = Renderer; + this.Scene = Scene; + this.Camera = Camera; + this.Group = Group; + this.Canvas = Canvas; + } +} + +export function ViewState_$reflection() { + return record_type("SomhairlesDream.Frontend.App.ViewState", [], ViewState, () => [["Renderer", string_type], ["Scene", string_type], ["Camera", string_type], ["Group", string_type], ["Canvas", class_type("Browser.Types.HTMLCanvasElement", undefined)]]); +} + +export const frames = Replay_frames(); + +export let viewState = createAtom(undefined); + +export let selectedFrame = createAtom(item(0, frames)); + +export let replayTimer = createAtom(undefined); + +export let replayIndex = createAtom(0); + +export let eventSource = createAtom(undefined); + +export function element(id) { + return document.getElementById(id); +} + +export function setText(id, value) { + element(id).textContent = value; +} + +export function setWidth(id, value) { + element(id).setAttribute("style", toText(printf("width: %s"))(value)); +} + +export function setStatusClass(status) { + const host = element("status-chip"); + const array = ["idle", "queued", "running", "succeeded", "failed", "stale"]; + array.forEach((name) => { + host.classList.remove(`status-${name}`); + }); + host.classList.add(`status-${status}`); + host.setAttribute("data-status", status); +} + +export function statusName(payload) { + if (payload.isStale) { + return "stale"; + } + else { + const matchValue = payload.status; + let matchResult, status; + switch (matchValue) { + case "complete": { + matchResult = 0; + break; + } + case "idle": { + matchResult = 1; + status = matchValue; + break; + } + case "queued": { + matchResult = 1; + status = matchValue; + break; + } + case "running": { + matchResult = 1; + status = matchValue; + break; + } + case "failed": { + matchResult = 1; + status = matchValue; + break; + } + default: + matchResult = 2; + } + switch (matchResult) { + case 0: + return "succeeded"; + case 1: + return status; + default: + return "unknown"; + } + } +} + +export function statusLabel(payload) { + const matchValue = payload.status; + const matchValue_1 = payload.isStale; + if (matchValue_1) { + return "心跳超时"; + } + else { + switch (matchValue) { + case "idle": + return "待机"; + case "queued": + return "排队中"; + case "running": + return "运行中"; + case "complete": + return "已完成"; + case "failed": + return "失败"; + default: + return "未知状态"; + } + } +} + +export function addMesh(view, frame) { + const geometry = new THREE.BufferGeometry(); + const positions = MeshData_positionData(frame.Mesh); + const indices = MeshData_triangleIndices(frame.Mesh); + geometry.setAttribute("position", new THREE.Float32BufferAttribute(positions, 3)); + geometry.setIndex(new THREE.Uint32BufferAttribute(indices, 1)); + geometry.computeVertexNormals(); + let color; + const matchValue = frame.Version | 0; + color = ((matchValue === 1) ? "#d7a948" : ((matchValue === 2) ? "#efe4c4" : "#a7c7c0")); + const material = new THREE.MeshStandardMaterial({ + color: color, + roughness: 0.36, + metalness: 0.72, + }); + const mesh = new THREE.Mesh(geometry, material); + view.Group.add(mesh); +} + +export function renderFrame(frame) { + selectedFrame(frame); + if (viewState() != null) { + const view = viewState(); + view.Group.clear(); + addMesh(view, frame); + view.Renderer.render(view.Scene, view.Camera); + } +} + +export function resizeView(view) { + const width = max(1, view.Canvas.clientWidth); + const height = max(1, view.Canvas.clientHeight); + view.Renderer.setSize(width, height, false); + renderFrame(selectedFrame()); +} + +export function initializeView() { + const canvas = document.getElementById("viewport-canvas"); + const width = max(1, canvas.clientWidth); + const height = max(1, canvas.clientHeight); + const renderer = new THREE.WebGLRenderer({ + canvas: canvas, + antialias: true, + alpha: true, + }); + renderer.setPixelRatio(window.devicePixelRatio); + renderer.setSize(width, height, false); + const scene = new THREE.Scene(); + scene.background = (new THREE.Color("#11171a")); + const camera = new THREE.PerspectiveCamera(34, (width / height), 0.1, 100); + camera.position.set(6.4, 4.6, 7.2); + camera.lookAt(0, 1.2, 0); + const group = new THREE.Group(); + scene.add(group); + scene.add(new THREE.AmbientLight("#f0e6c8", 1.9)); + const keyLight = new THREE.DirectionalLight("#f2c76b", 2.8); + keyLight.position.set(4, 7, 5); + scene.add(keyLight); + const view = new ViewState(renderer, scene, camera, group, canvas); + viewState(view); + renderFrame(selectedFrame()); + window.addEventListener("resize", (_arg) => { + resizeView(view); + }); + const animate = (_arg_1) => { + group.rotation.y = (group.rotation.y + 0.003); + renderer.render(scene, camera); + requestAnimationFrame(animate); + }; + requestAnimationFrame(animate); +} + +export function fallbackView() { + setText("viewport-note", "WebGL 视图待命 · 已保留重建数据"); + element("viewport-panel").classList.add("viewport-fallback"); +} + +export function showFrameMetadata(frame) { + setText("mesh-label", frame.Label); + setText("timeline-current", toText(printf("V%02d · %s"))(frame.Version)(frame.Label)); +} + +export function setManifestLink(payload) { + const link = element("artifact-manifest-link"); + const matchValue = payload.manifestUrl; + if (isNullOrWhiteSpace(matchValue)) { + link.removeAttribute("href"); + link.classList.add("is-hidden"); + } + else { + link.setAttribute("href", matchValue); + link.classList.remove("is-hidden"); + } +} + +export function updateState(payload) { + let arg, arg_3; + setStatusClass(statusName(payload)); + setText("status-label", statusLabel(payload)); + setText("run-message", ((payload.status === "failed") && !isNullOrWhiteSpace(payload.error)) ? payload.error : payload.message); + setText("project-id", payload.projectId); + setText("run-id", payload.runId); + const currentVersion = max(0, payload.currentStepIndex + 1) | 0; + setText("target-version", (arg = (payload.totalSteps | 0), toText(printf("V%02d"))(arg))); + setText("current-version", toText(printf("V%02d"))(currentVersion)); + setText("version-readout", (arg_3 = (payload.totalSteps | 0), toText(printf("%02d / %02d"))(currentVersion)(arg_3))); + setText("updated-at", payload.updatedAt); + const progress = (payload.totalSteps <= 0) ? 0 : min(100, (currentVersion / payload.totalSteps) * 100); + setWidth("progress-fill", toText(printf("%.0f%%"))(progress)); + const frame = defaultArg(tryFind((candidate) => (candidate.Version === currentVersion), frames), selectedFrame()); + setManifestLink(payload); + showFrameMetadata(frame); + renderFrame(frame); +} + +export function updateStreamState(message) { + setText("stream-state", message); +} + +export function toggleReplay() { + if (replayTimer() == null) { + replayIndex(0); + setText("replay-button", "暂停重建"); + setText("replay-state", "重建中 · 几何逐帧替换"); + const tick = () => { + if (replayIndex() >= frames.length) { + if (replayTimer() == null) { + } + else { + clearInterval(replayTimer()); + } + replayTimer(undefined); + setText("replay-button", "重建演示"); + setText("replay-state", "重建完成 · 几何已更新"); + } + else { + const frame = item(replayIndex(), frames); + showFrameMetadata(frame); + renderFrame(frame); + setText("replay-state", toText(printf("重建中 · V%02d %s"))(frame.Version)(frame.Label)); + replayIndex(replayIndex() + 1); + } + }; + tick(); + replayTimer(setInterval(tick, 900)); + } + else { + clearInterval(replayTimer()); + replayTimer(undefined); + setText("replay-button", "重建演示"); + setText("replay-state", "演示已暂停"); + } +} + +export function disconnectFromServer() { + if (eventSource() == null) { + } + else { + const source = eventSource(); + source.close(); + eventSource(undefined); + } +} + +export function connectToServer(selection) { + disconnectFromServer(); + const projectId = encodeURIComponent(selection.ProjectId); + const runId = encodeURIComponent(selection.RunId); + const source = new EventSource(toText(printf("/api/runs/events?projectId=%s&runId=%s"))(projectId)(runId)); + eventSource(source); + source.onmessage = ((event) => { + updateState(JSON.parse(event.data)); + updateStreamState("实时链路 · SSE 已连接"); + }); + source.onerror = ((_arg) => { + updateStreamState("实时链路 · 等待自动重连"); + }); +} + +export function inputValue(id) { + return element(id).value; +} + +export function selectedRun() { + return new RunSelection(inputValue("project-id-input"), inputValue("run-id-input"), inputValue("base-id-input"), inputValue("target-id-input"), element("run-render-input").checked); +} + +export function startRun(selection) { + const requestBody = { + projectId: selection.ProjectId, + runId: selection.RunId, + baseId: selection.BaseId, + targetId: selection.TargetId, + render: selection.Render, + }; + const requestOptions = { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(requestBody), + }; + setText("run-message", "正在启动设计运行"); + updateStreamState("实时链路 · 等待服务端确认"); + startImmediate(singleton.Delay(() => singleton.TryWith(singleton.Delay(() => singleton.Bind(awaitPromise(fetch("/api/runs/start", requestOptions)), (_arg) => { + let arg; + const response = _arg; + if (response.ok) { + return singleton.Bind(awaitPromise(response.json()), (_arg_1) => { + updateState(_arg_1); + connectToServer(selection); + updateStreamState("实时链路 · SSE 已连接"); + return singleton.Zero(); + }); + } + else { + setText("run-message", (arg = (response.status | 0), toText(printf("启动失败 · HTTP %d"))(arg))); + updateStreamState("实时链路 · 启动失败"); + return singleton.Zero(); + } + })), (_arg_2) => { + setText("run-message", "启动失败 · 无法连接服务端"); + updateStreamState("实时链路 · 连接失败"); + return singleton.Zero(); + }))); +} + +export function boot() { + showFrameMetadata(item(0, frames)); + try { + initializeView(); + } + catch (matchValue) { + fallbackView(); + } + element("replay-button").addEventListener("click", (_arg) => { + toggleReplay(); + }); + element("run-controls").addEventListener("submit", (event) => { + event.preventDefault(); + startRun(selectedRun()); + }); +} + +boot(); + |
