summaryrefslogtreecommitdiff
path: root/public
diff options
context:
space:
mode:
Diffstat (limited to 'public')
-rw-r--r--public/App.js382
-rw-r--r--public/index.html17
-rw-r--r--public/styles.css30
3 files changed, 367 insertions, 62 deletions
diff --git a/public/App.js b/public/App.js
index 70d2377..ab4ae59 100644
--- a/public/App.js
+++ b/public/App.js
@@ -1,9 +1,9 @@
import { Union, Record } from "./fable_modules/fable-library-js.4.24.0/Types.js";
import { union_type, 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 { equals, 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 { defaultOf, equals, createAtom } from "./fable_modules/fable-library-js.4.24.0/Util.js";
+import { equalsWith, tryFind, iterateIndexed, item } from "./fable_modules/fable-library-js.4.24.0/Array.js";
+import { join, 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";
@@ -84,6 +84,10 @@ export let currentProjectId = createAtom("");
export let currentRunId = createAtom("");
+export let historicalSteps = createAtom([]);
+
+export let pinnedIndex = createAtom(undefined);
+
export let loadGeneration = createAtom(0);
export let pendingLoad = createAtom(undefined);
@@ -333,9 +337,34 @@ export function setNoteRetryable(retryable) {
}
}
+export function updateFollowButton() {
+ if ((historicalSteps().length > 0) && (pinnedIndex() != null)) {
+ element("follow-button").removeAttribute("disabled");
+ setText("follow-button", "跟随最新");
+ }
+ else {
+ element("follow-button").setAttribute("disabled", "disabled");
+ if (historicalSteps().length > 0) {
+ setText("follow-button", "跟随最新 · 已开启");
+ }
+ else {
+ setText("follow-button", "跟随最新");
+ }
+ }
+}
+
+export function stopReplay() {
+ if (replayTimer() == null) {
+ }
+ else {
+ clearInterval(replayTimer());
+ replayTimer(undefined);
+ }
+}
+
export function showFrameMetadata(frame) {
- setText("mesh-label", frame.Label);
- setText("timeline-current", toText(printf("V%02d · %s"))(frame.Version)(frame.Label));
+ setText("mesh-label", toText(printf("占位 · %s"))(frame.Label));
+ setText("timeline-current", toText(printf("V%02d · 占位 · %s"))(frame.Version)(frame.Label));
}
export function setManifestLink(payload) {
@@ -351,7 +380,7 @@ export function setManifestLink(payload) {
}
}
-export function startStepLoad(view, owner) {
+export function startStepLoad(view, owner, historical) {
const generation = (loadGeneration() + 1) | 0;
loadGeneration(generation);
pendingLoad([generation, owner]);
@@ -364,7 +393,12 @@ export function startStepLoad(view, owner) {
let url;
const arg_2 = encodeURIComponent(owner.Path);
url = toText(printf("/api/artifacts/steps?projectId=%s&runId=%s&path=%s"))(projectId)(runId)(arg_2);
- setText("viewport-note", toText(printf("管线几何加载中 · %s"))(owner.Path));
+ if (historical) {
+ setText("viewport-note", toText(printf("历史几何加载中 · %s"))(owner.Path));
+ }
+ else {
+ setText("viewport-note", toText(printf("管线几何加载中 · %s"))(owner.Path));
+ }
loader.load(url, ((gltf) => {
if (equals(pendingLoad(), [generation, owner])) {
pendingLoad(undefined);
@@ -374,7 +408,12 @@ export function startStepLoad(view, owner) {
loadedStep(owner);
frameObject(view, gltf.scene);
view.Renderer.render(view.Scene, view.Camera);
- setText("viewport-note", toText(printf("管线几何 · %s"))(owner.Path));
+ if (historical) {
+ setText("viewport-note", toText(printf("历史几何 · %s"))(owner.Path));
+ }
+ else {
+ setText("viewport-note", toText(printf("管线几何 · %s"))(owner.Path));
+ }
}
else {
gltf.scene.traverse(function (o) { if (o.geometry && o.geometry.dispose) { o.geometry.dispose(); } var mats = Array.isArray(o.material) ? o.material : (o.material ? [o.material] : []); mats.forEach(function (m) { ['map','normalMap','roughnessMap','metalnessMap','aoMap','emissiveMap'].forEach(function (key) { var t = m[key]; if (t && t.dispose) { t.dispose(); } }); if (m.dispose) { m.dispose(); } }); });
@@ -383,14 +422,19 @@ export function startStepLoad(view, owner) {
if (equals(pendingLoad(), [generation, owner])) {
pendingLoad(undefined);
loadedStep(undefined);
- failedLoad(owner);
+ failedLoad([owner, historical]);
setNoteRetryable(true);
- setText("viewport-note", toText(printf("管线几何加载失败 · %s"))(owner.Path));
+ if (historical) {
+ setText("viewport-note", toText(printf("历史几何加载失败 · %s"))(owner.Path));
+ }
+ else {
+ setText("viewport-note", toText(printf("管线几何加载失败 · %s"))(owner.Path));
+ }
}
}));
}
-export function loadStepArtifact(view, stepPath) {
+export function loadStepArtifact(view, stepPath, historical) {
const owner = new LoadOwner(currentProjectId(), currentRunId(), stepPath);
let matchResult, pending_1;
if (pendingLoad() != null) {
@@ -410,22 +454,23 @@ export function loadStepArtifact(view, stepPath) {
break;
}
case 1: {
- startStepLoad(view, owner);
+ startStepLoad(view, owner, historical);
break;
}
}
}
export function retryFailedLoad() {
- let view, owner;
+ let view, owner, historical;
const matchValue = viewState();
const matchValue_1 = failedLoad();
- let matchResult, owner_1, view_1;
+ let matchResult, historical_1, owner_1, view_1;
if (matchValue != null) {
if (matchValue_1 != null) {
- if ((view = matchValue, (owner = matchValue_1, (owner.ProjectId === currentProjectId()) && (owner.RunId === currentRunId())))) {
+ if ((view = matchValue, (owner = matchValue_1[0], (historical = matchValue_1[1], (owner.ProjectId === currentProjectId()) && (owner.RunId === currentRunId()))))) {
matchResult = 0;
- owner_1 = matchValue_1;
+ historical_1 = matchValue_1[1];
+ owner_1 = matchValue_1[0];
view_1 = matchValue;
}
else {
@@ -441,7 +486,147 @@ export function retryFailedLoad() {
}
switch (matchResult) {
case 0: {
- loadStepArtifact(view_1, owner_1.Path);
+ loadStepArtifact(view_1, owner_1.Path, historical_1);
+ break;
+ }
+ case 1: {
+ break;
+ }
+ }
+}
+
+export function applyHistoricalStep(index, duringPlayback) {
+ let arg_5, view;
+ let matchResult, view_1;
+ if (viewState() != null) {
+ if ((view = viewState(), (index >= 0) && (index < historicalSteps().length))) {
+ matchResult = 0;
+ view_1 = viewState();
+ }
+ else {
+ matchResult = 1;
+ }
+ }
+ else {
+ matchResult = 1;
+ }
+ switch (matchResult) {
+ case 0: {
+ pinnedIndex(index);
+ const path = item(index, historicalSteps());
+ const version = (index + 1) | 0;
+ setText("mesh-label", toText(printf("历史 · %s"))(path));
+ setText("timeline-current", toText(printf("V%02d · 历史回放 · %s"))(version)(path));
+ loadStepArtifact(view_1, path, true);
+ if (duringPlayback) {
+ setText("replay-state", (arg_5 = (historicalSteps().length | 0), toText(printf("历史回放中 · V%02d (%d/%d)"))(version)(version)(arg_5)));
+ }
+ else {
+ setText("replay-state", toText(printf("历史回放 · 已固定 V%02d"))(version));
+ }
+ break;
+ }
+ case 1: {
+ break;
+ }
+ }
+}
+
+export function renderTimeline() {
+ let arg;
+ const track = element("timeline-track");
+ var child; while ((child = track.firstChild)) { track.removeChild(child); };
+ const line = document.createElement("span");
+ line.className = "timeline-line";
+ line.setAttribute("aria-hidden", "true");
+ track.appendChild(line);
+ if (historicalSteps().length > 0) {
+ track.setAttribute("style", (arg = (historicalSteps().length | 0), toText(printf("grid-template-columns: repeat(%d, 1fr)"))(arg)));
+ iterateIndexed((i, path) => {
+ const active = (pinnedIndex() == null) ? (i === (historicalSteps().length - 1)) : (pinnedIndex() === i);
+ track.appendChild(createStepNode(i + 1, path, active, true, i));
+ }, historicalSteps());
+ }
+ else {
+ track.setAttribute("style", "grid-template-columns: repeat(3, 1fr)");
+ frames.forEach((frame) => {
+ track.appendChild(createStepNode(frame.Version, toText(printf("占位 · %s"))(frame.Label), false, frame.Version < frames.length, undefined));
+ });
+ }
+}
+
+export function selectStep(index) {
+ if ((index >= 0) && (index < historicalSteps().length)) {
+ stopReplay();
+ applyHistoricalStep(index, false);
+ updateFollowButton();
+ renderTimeline();
+ }
+}
+
+export function createStepNode(version, label, active, complete, onClickIndex) {
+ const classes = ["timeline-step"];
+ if (complete) {
+ void (classes.push("timeline-step-complete"));
+ }
+ if (onClickIndex != null) {
+ void (classes.push("timeline-step-real"));
+ }
+ if (active) {
+ void (classes.push("timeline-step-active"));
+ }
+ const step = document.createElement("div");
+ step.className = join(" ", classes);
+ const dot = document.createElement("span");
+ dot.className = "timeline-dot";
+ dot.setAttribute("aria-hidden", "true");
+ step.appendChild(dot);
+ const versionLabel = document.createElement("span");
+ versionLabel.className = "timeline-version";
+ versionLabel.textContent = toText(printf("V%02d"))(version);
+ step.appendChild(versionLabel);
+ const text = document.createElement("span");
+ text.className = "timeline-label";
+ text.textContent = label;
+ step.appendChild(text);
+ if (onClickIndex == null) {
+ }
+ else {
+ const index = onClickIndex | 0;
+ step.addEventListener("click", (_arg) => {
+ selectStep(index);
+ });
+ }
+ return step;
+}
+
+export function followLatestStep() {
+ let view;
+ let matchResult, view_1;
+ if (viewState() != null) {
+ if ((view = viewState(), historicalSteps().length > 0)) {
+ matchResult = 0;
+ view_1 = viewState();
+ }
+ else {
+ matchResult = 1;
+ }
+ }
+ else {
+ matchResult = 1;
+ }
+ switch (matchResult) {
+ case 0: {
+ stopReplay();
+ pinnedIndex(undefined);
+ const total = historicalSteps().length | 0;
+ const latest = item(total - 1, historicalSteps());
+ loadStepArtifact(view_1, latest, false);
+ setText("mesh-label", latest);
+ setText("timeline-current", toText(printf("V%02d · 实时跟随 · %s"))(total)(latest));
+ setText("replay-state", toText(printf("实时跟随 · 最新 V%02d"))(total));
+ updateFollowButton();
+ renderTimeline();
break;
}
case 1: {
@@ -451,7 +636,7 @@ export function retryFailedLoad() {
}
export function updateState(payload) {
- let arg, arg_3;
+ let arg, arg_3, testExpr, arg_5, arg_7;
setStatusClass(statusName(payload));
setText("status-label", statusLabel(payload));
setText("run-message", ((payload.status === "failed") && !isNullOrWhiteSpace(payload.error)) ? payload.error : payload.message);
@@ -460,6 +645,12 @@ export function updateState(payload) {
if ((currentProjectId() !== payload.projectId) ? true : (currentRunId() !== payload.runId)) {
invalidateLoads();
displayedNode(undefined);
+ stopReplay();
+ pinnedIndex(undefined);
+ historicalSteps([]);
+ setText("replay-button", "占位演示");
+ setText("replay-state", "占位示例 · 未选择历史");
+ updateFollowButton();
}
currentProjectId(payload.projectId);
currentRunId(payload.runId);
@@ -475,28 +666,65 @@ export function updateState(payload) {
const completedSteps = (payload.completedSteps == null) ? [] : payload.completedSteps;
const matchValue = viewState();
if (matchValue == null) {
- const steps_1 = completedSteps;
- if (steps_1.length > 0) {
- setText("mesh-label", item(steps_1.length - 1, steps_1));
+ if (completedSteps.length > 0) {
+ setText("mesh-label", item(completedSteps.length - 1, completedSteps));
}
else {
showFrameMetadata(frame);
}
}
- else {
+ else if ((testExpr = completedSteps, !equalsWith((x, y) => (x === y), testExpr, defaultOf()) && (testExpr.length === 0))) {
const view = matchValue;
- const steps = completedSteps;
- if (steps.length === 0) {
- showFrameMetadata(frame);
- renderFrame(frame);
+ if (historicalSteps().length > 0) {
+ stopReplay();
+ historicalSteps([]);
+ pinnedIndex(undefined);
+ setText("replay-button", "占位演示");
+ renderTimeline();
+ updateFollowButton();
}
- else {
- const latest = item(steps.length - 1, steps);
+ showFrameMetadata(frame);
+ renderFrame(frame);
+ }
+ else {
+ const view_1 = matchValue;
+ if (!equalsWith((x_1, y_1) => (x_1 === y_1), completedSteps, historicalSteps())) {
+ if (historicalSteps().length === 0) {
+ stopReplay();
+ setText("replay-button", "播放历史");
+ }
+ historicalSteps(completedSteps);
+ let matchResult, pinned_1;
+ if (pinnedIndex() != null) {
+ if (pinnedIndex() >= completedSteps.length) {
+ matchResult = 0;
+ pinned_1 = pinnedIndex();
+ }
+ else {
+ matchResult = 1;
+ }
+ }
+ else {
+ matchResult = 1;
+ }
+ switch (matchResult) {
+ case 0: {
+ pinnedIndex(undefined);
+ break;
+ }
+ }
+ renderTimeline();
+ updateFollowButton();
+ }
+ if (pinnedIndex() == null) {
+ const latest = item(completedSteps.length - 1, completedSteps);
const owner = new LoadOwner(payload.projectId, payload.runId, latest);
if (!equals(loadedStep(), owner)) {
- loadStepArtifact(view, latest);
+ loadStepArtifact(view_1, latest, false);
}
setText("mesh-label", latest);
+ setText("timeline-current", (arg_5 = (completedSteps.length | 0), toText(printf("V%02d · 实时跟随 · %s"))(arg_5)(latest)));
+ setText("replay-state", (arg_7 = (completedSteps.length | 0), toText(printf("实时跟随 · 最新 V%02d"))(arg_7)));
}
}
}
@@ -505,38 +733,78 @@ 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", "重建完成 · 几何已更新");
+export function startPlaceholderPlayback() {
+ replayIndex(0);
+ setText("replay-button", "暂停占位演示");
+ setText("replay-state", "占位演示中 · 未选择历史");
+ const tick = () => {
+ if (replayIndex() >= frames.length) {
+ stopReplay();
+ 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));
+}
+
+export function startHistoricalPlayback() {
+ if (historicalSteps().length > 0) {
+ setText("replay-button", "暂停历史回放");
+ applyHistoricalStep(0, true);
+ updateFollowButton();
+ renderTimeline();
+ const total = historicalSteps().length | 0;
+ replayTimer(setInterval(() => {
+ let arg;
+ if (pinnedIndex() == null) {
+ stopReplay();
+ setText("replay-button", "播放历史");
+ }
+ else if ((pinnedIndex() + 1) < total) {
+ const pinned_1 = pinnedIndex() | 0;
+ applyHistoricalStep(pinned_1 + 1, true);
+ renderTimeline();
}
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);
+ const pinned_2 = pinnedIndex() | 0;
+ stopReplay();
+ setText("replay-button", "播放历史");
+ setText("replay-state", (arg = ((pinned_2 + 1) | 0), toText(printf("历史回放完成 · 停在 V%02d"))(arg)));
}
- };
- tick();
- replayTimer(setInterval(tick, 900));
+ }, 1500));
+ }
+}
+
+export function toggleReplay() {
+ let arg;
+ if (replayTimer() == null) {
+ if (historicalSteps().length > 0) {
+ startHistoricalPlayback();
+ }
+ else {
+ startPlaceholderPlayback();
+ }
}
else {
clearInterval(replayTimer());
replayTimer(undefined);
- setText("replay-button", "重建演示");
- setText("replay-state", "演示已暂停");
+ if (historicalSteps().length > 0) {
+ const current = defaultArg(pinnedIndex(), 0) | 0;
+ setText("replay-button", "播放历史");
+ setText("replay-state", (arg = ((current + 1) | 0), toText(printf("历史回放 · 已暂停 · V%02d"))(arg)));
+ }
+ else {
+ setText("replay-button", "占位演示");
+ setText("replay-state", "占位演示 · 已暂停");
+ }
}
}
@@ -624,13 +892,17 @@ export function boot() {
element("replay-button").addEventListener("click", (_arg) => {
toggleReplay();
});
- element("viewport-note").addEventListener("click", (_arg_1) => {
+ element("follow-button").addEventListener("click", (_arg_1) => {
+ followLatestStep();
+ });
+ element("viewport-note").addEventListener("click", (_arg_2) => {
retryFailedLoad();
});
element("run-controls").addEventListener("submit", (event) => {
event.preventDefault();
startRun(selectedRun());
});
+ renderTimeline();
}
boot();
diff --git a/public/index.html b/public/index.html
index 2acf08e..71e9e62 100644
--- a/public/index.html
+++ b/public/index.html
@@ -162,29 +162,32 @@
<span class="eyebrow">RECONSTRUCTION TIMELINE</span>
<h2 id="timeline-title">几何版本轨迹</h2>
</div>
- <span id="replay-state" class="replay-state">可回放 · 三个确定性快照</span>
+ <span id="replay-state" class="replay-state">占位示例 · 未选择历史</span>
</div>
- <div class="timeline-track" aria-label="几何版本">
+ <div class="timeline-track" id="timeline-track" aria-label="几何版本">
<span class="timeline-line" aria-hidden="true"></span>
<div class="timeline-step timeline-step-complete">
<span class="timeline-dot" aria-hidden="true"></span>
<span class="timeline-version">V01</span>
- <span class="timeline-label">重建起点</span>
+ <span class="timeline-label">占位 · 重建起点</span>
</div>
<div class="timeline-step timeline-step-complete">
<span class="timeline-dot" aria-hidden="true"></span>
<span class="timeline-version">V02</span>
- <span class="timeline-label">主承力骨架</span>
+ <span class="timeline-label">占位 · 主承力骨架</span>
</div>
<div class="timeline-step">
<span class="timeline-dot" aria-hidden="true"></span>
<span class="timeline-version">V03</span>
- <span class="timeline-label">舱段重构</span>
+ <span class="timeline-label">占位 · 舱段重构</span>
</div>
</div>
<div class="timeline-footer">
- <span id="timeline-current">V01 · 重建起点</span>
- <button id="replay-button" class="replay-button" type="button">重建演示</button>
+ <span id="timeline-current">V01 · 占位 · 重建起点</span>
+ <div class="timeline-actions">
+ <button id="follow-button" class="replay-button replay-button-ghost" type="button" disabled>跟随最新</button>
+ <button id="replay-button" class="replay-button" type="button">占位演示</button>
+ </div>
</div>
</section>
diff --git a/public/styles.css b/public/styles.css
index 6346df5..a925e58 100644
--- a/public/styles.css
+++ b/public/styles.css
@@ -540,6 +540,31 @@ button:focus-visible {
background: var(--gold);
}
+.timeline-step-real {
+ cursor: pointer;
+}
+
+.timeline-step-real:hover .timeline-version {
+ color: var(--ivory);
+}
+
+.timeline-step-active .timeline-dot {
+ border-color: var(--ivory);
+ box-shadow: 0 0 0 3px rgba(215, 169, 72, 0.35);
+}
+
+#follow-button[disabled] {
+ border-color: var(--line-bright);
+ color: var(--muted);
+ cursor: default;
+ opacity: 0.45;
+}
+
+#follow-button[disabled]:hover {
+ background: transparent;
+ color: var(--muted);
+}
+
.timeline-version {
color: var(--gold-soft);
font-size: 0.68rem;
@@ -557,6 +582,11 @@ button:focus-visible {
font-size: 0.68rem;
}
+.timeline-actions {
+ display: flex;
+ gap: 10px;
+}
+
.replay-button {
min-height: 44px;
padding: 8px 16px;