summaryrefslogtreecommitdiff
path: root/public/App.js
diff options
context:
space:
mode:
Diffstat (limited to 'public/App.js')
-rw-r--r--public/App.js392
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();
+