Upgrade RFS and Saebyeok showcases
Some checks failed
ci / build (push) Has been cancelled

This commit is contained in:
Yun Chan 2026-08-25 04:41:51 +09:00
parent 9529e3747c
commit 413486958c
13 changed files with 1410 additions and 88 deletions

View file

@ -34,6 +34,101 @@
const shell = canvas.closest("[data-field-shell]");
const setState = (s) => shell && shell.setAttribute("data-field-state", s);
/* ── 데모 조작 UI ────────────────────────────────────────────────
WebGL 이 없거나 모션을 줄여도 조작의 의미는 남아야 한다.
그래서 슬라이더·장면·실물 뷰 전환은 그래픽 초기화보다 먼저 묶는다. */
const controls = {
load: Number(document.getElementById("ctlLoad")?.value ?? 70) / 100,
supply: Number(document.getElementById("ctlSupply")?.value ?? 65) / 100,
};
const presetButtons = [...document.querySelectorAll("[data-field-preset]")];
const fieldState = document.getElementById("fieldState");
const fieldScene = document.getElementById("fieldScene");
const fieldWatch = document.getElementById("fieldWatch");
const fieldAction = document.getElementById("fieldAction");
const activePreset = () => presetButtons.find((button) => button.getAttribute("aria-pressed") === "true");
function updateReadout() {
const gap = Math.round((controls.load - controls.supply) * 100);
const selected = activePreset();
if (fieldScene) fieldScene.textContent = selected?.textContent.trim() || "수동 조정";
let state = "냉기와 발열이 맞서는 기준 상태입니다.";
let watch = "랙 배면";
let action = "부하와 급기를 따로 움직여 비교";
if (gap >= 35) {
state = "발열이 급기보다 크게 앞섭니다. 배면에서 천장으로 오르는 열기둥을 확인하십시오.";
watch = "랙 배면 · 천장 배기";
action = "급기 상향과 부하 재배분을 비교";
} else if (gap >= 15) {
state = "발열 우세 구간입니다. 랙 뒤에 머무는 고온 입자의 경로를 비교하십시오.";
watch = "랙 배면";
action = "급기량을 한 단계씩 높여 비교";
} else if (gap <= -15) {
state = "급기가 발열을 앞섭니다. FWU 제트가 만드는 저온 구간을 확인하십시오.";
watch = "FWU 제트 · 냉점";
action = "과냉 구간과 팬 운전을 함께 검토";
}
if (fieldState) fieldState.textContent = state;
if (fieldWatch) fieldWatch.textContent = watch;
if (fieldAction) fieldAction.textContent = action;
}
function matchPreset() {
const load = Math.round(controls.load * 100);
const supply = Math.round(controls.supply * 100);
let matched = false;
for (const button of presetButtons) {
const same = Number(button.dataset.load) === load && Number(button.dataset.supply) === supply;
button.setAttribute("aria-pressed", String(same));
matched ||= same;
}
if (!matched) presetButtons.forEach((button) => button.setAttribute("aria-pressed", "false"));
}
function bindSlider(id, key, outId, fmt) {
const el = document.getElementById(id);
const out = document.getElementById(outId);
if (!el) return;
const sync = () => {
controls[key] = Number(el.value) / 100;
if (out) out.textContent = fmt(Number(el.value));
matchPreset();
updateReadout();
};
el.addEventListener("input", sync);
sync();
}
bindSlider("ctlLoad", "load", "outLoad", (v) => `${v}%`);
bindSlider("ctlSupply", "supply", "outSupply", (v) => `${v}%`);
for (const button of presetButtons) {
button.addEventListener("click", () => {
const load = document.getElementById("ctlLoad");
const supply = document.getElementById("ctlSupply");
if (!(load instanceof HTMLInputElement) || !(supply instanceof HTMLInputElement)) return;
load.value = button.dataset.load || load.value;
supply.value = button.dataset.supply || supply.value;
load.dispatchEvent(new Event("input", { bubbles: true }));
supply.dispatchEvent(new Event("input", { bubbles: true }));
});
}
const visTabs = [...document.querySelectorAll("[data-vis-target]")];
const visPanels = [...document.querySelectorAll("[data-vis-panel]")];
for (const tab of visTabs) {
tab.addEventListener("click", () => {
const target = tab.dataset.visTarget;
for (const button of visTabs) button.setAttribute("aria-pressed", String(button === tab));
for (const panel of visPanels) panel.hidden = panel.dataset.visPanel !== target;
});
}
const reduced = window.matchMedia("(prefers-reduced-motion: reduce)");
if (reduced.matches) {
setState("static");
@ -450,27 +545,6 @@
}
}
/* ── 조작 입력 ────────────────────────────────────────────────── */
const controls = {
load: Number(document.getElementById("ctlLoad")?.value ?? 70) / 100,
supply: Number(document.getElementById("ctlSupply")?.value ?? 65) / 100,
};
function bindSlider(id, key, outId, fmt) {
const el = document.getElementById(id);
const out = document.getElementById(outId);
if (!el) return;
const sync = () => {
controls[key] = Number(el.value) / 100;
if (out) out.textContent = fmt(Number(el.value));
};
el.addEventListener("input", sync);
sync();
}
bindSlider("ctlLoad", "load", "outLoad", (v) => `${v}%`);
bindSlider("ctlSupply", "supply", "outSupply", (v) => `${v}%`);
const pointer = { x: 0, y: 0, on: 0 };
const onMove = (e) => {
const r = canvas.getBoundingClientRect();