fix: preserve analysis bands across input workflows
This commit is contained in:
@@ -68,12 +68,14 @@ powershell.exe -NoLogo -NoProfile -ExecutionPolicy Bypass -File .\scripts\serve.
|
||||
|
||||
### 이동 음원 바로 보기
|
||||
|
||||
1. Acoustic Camera에서 **이동 예제 열기**를 누릅니다.
|
||||
1. Acoustic Camera에서 **이동 예제 전체 열기**를 누릅니다.
|
||||
2. Moving 카드의 **Map 준비**를 눌러 2 Hz acoustic map을 계산합니다.
|
||||
3. **재생**을 누르거나 시간 slider를 움직여 음원과 heatmap의 이동을 확인합니다.
|
||||
|
||||
이 기능은 trajectory가 있는 합성 Scene의 준정적 시간창 재생입니다. 실제 192채널 마이크 장치에서
|
||||
들어오는 스트림을 캡처하는 기능은 아닙니다.
|
||||
이 버튼은 Scene뿐 아니라 Array와 Experiment, 분석 대역까지 예제 전체로 교체합니다. 여러 대역이나
|
||||
저장하지 않은 설정이 있으면 교체 전에 확인합니다.
|
||||
|
||||
## 내 장면으로 전체 흐름 실행하기
|
||||
|
||||
@@ -111,7 +113,8 @@ powershell.exe -NoLogo -NoProfile -ExecutionPolicy Bypass -File .\scripts\serve.
|
||||
### 3. Acoustic Camera에서 실행하기
|
||||
|
||||
1. 홈으로 돌아가 **Acoustic Camera**를 엽니다.
|
||||
2. 다음 순서로 입력합니다: **Scene → Array → Experiment → Assets**.
|
||||
2. 권장 순서인 **Scene → Array → Experiment → Assets**로 입력합니다. Experiment를 먼저
|
||||
열어도 모든 분석 대역이 유지됩니다.
|
||||
3. **Scene**에서 Builder가 저장한 `.scene.json`을 선택합니다.
|
||||
4. 기본 배열을 사용하지 않을 때는 **Array**에서 Array Builder가 저장한 `.array.json`을 선택합니다.
|
||||
5. 별도 실험 문서를 사용할 때만 **Experiment**에서 `.experiment.json`을 선택합니다.
|
||||
@@ -120,8 +123,15 @@ powershell.exe -NoLogo -NoProfile -ExecutionPolicy Bypass -File .\scripts\serve.
|
||||
7. 입력 검증이 통과하면 위치 추정 알고리즘, backend와 scan grid를 정하고 실행합니다.
|
||||
8. 완료 후 **Result JSON**을 저장합니다.
|
||||
|
||||
Array와 Scene을 먼저 불러와야 Experiment의 `arrayId`와 `sceneId` 관계를 안전하게 검증할 수
|
||||
있습니다. Scene을 다시 불러오면 이전 asset 연결은 해제됩니다.
|
||||
Experiment를 먼저 불러오면 문서와 모든 `frequencyBands`를 즉시 편집할 수 있습니다. 현재
|
||||
Scene·Array와 `sceneId`·`arrayId`가 다른 동안에는 검증 카드에 연결 대기를 표시하고 실행만
|
||||
비활성화합니다. 참조하는 Scene과 Array를 이어서 불러오면 실행할 수 있습니다. Scene을 다시
|
||||
불러오면 이전 asset 연결은 해제됩니다.
|
||||
|
||||
Camera 안의 **Builder 새 탭** 링크는 편집 중인 Experiment와 분석 대역을 원래 Camera 탭에
|
||||
유지합니다. Builder에서 JSON을 저장한 뒤 새 Camera를 다시 열지 말고 원래 Camera 탭으로 돌아와
|
||||
Scene 또는 Array를 선택하세요. 같은 Camera 페이지에서 Scene을 교체하면 scan 영역과
|
||||
`sceneId`만 맞추며 기존 `frequencyBands`는 보존합니다.
|
||||
|
||||
Camera에서 수정한 Scene·Experiment, 완료 Result와 전체 배열 비교 기록은 각각 JSON으로 저장할 수
|
||||
있습니다. 저장하지 않은 항목이나 실행 중 계산이 있으면 페이지를 닫을 때 브라우저가 확인합니다.
|
||||
|
||||
@@ -34,24 +34,24 @@
|
||||
<section class="workflow-card input-card" id="input-step" data-workflow-section="1">
|
||||
<div class="step-heading">
|
||||
<span class="step-number">1</span>
|
||||
<div><h2>입력 준비</h2><p>기본 예제를 쓰거나 파일을 순서대로 교체하세요.</p></div>
|
||||
<div><h2>입력 준비</h2><p>기본 예제를 쓰거나 파일을 교체하세요. Experiment부터 열어도 대역은 유지됩니다.</p></div>
|
||||
</div>
|
||||
|
||||
<div class="workspace-tools input-file-grid" aria-label="입력 파일">
|
||||
<label class="file-button file-choice" title="Scene JSON을 선택하세요."><span class="file-sequence">1</span><span><strong>Scene</strong><small>장면·음원</small></span><input id="scene-input" type="file" accept="application/json,.json" /></label>
|
||||
<label class="file-button file-choice" title="사용자 배열이 있을 때 Array JSON을 선택하세요."><span class="file-sequence">2</span><span><strong>Array</strong><small>마이크 배열</small></span><input id="array-input" type="file" accept="application/json,.json" /></label>
|
||||
<label class="file-button file-choice" title="현재 Scene과 Array를 참조하는 Experiment JSON을 선택하세요."><span class="file-sequence">3</span><span><strong>Experiment</strong><small>분석 설정</small></span><input id="experiment-input" type="file" accept="application/json,.json" /></label>
|
||||
<label class="file-button file-choice" title="Experiment를 먼저 열 수 있습니다. 참조 ID가 다른 동안에는 실행만 비활성화됩니다."><span class="file-sequence">3</span><span><strong>Experiment</strong><small>분석 설정 · 선로드 가능</small></span><input id="experiment-input" type="file" accept="application/json,.json" /></label>
|
||||
<label class="file-button file-choice" title="Scene에 사용한 원본 파일을 다시 선택하세요."><span class="file-sequence">4</span><span><strong>Assets</strong><small>오디오·이미지</small></span><input id="asset-input" type="file" accept="audio/*,image/*,.wav,.flac,.mp3,.ogg,.m4a,.png,.jpg,.jpeg,.webp,.svg" multiple /></label>
|
||||
</div>
|
||||
|
||||
<div class="input-summary">
|
||||
<div class="compact-section-heading"><h3>현재 장면</h3><a class="builder-link" href="/apps/dataset-builder/">Builder에서 편집 →</a></div>
|
||||
<div class="compact-section-heading"><h3>현재 장면</h3><a class="builder-link" href="/apps/dataset-builder/" target="_blank" rel="noopener" title="현재 Camera의 Experiment를 유지한 채 새 탭에서 Scene을 편집합니다. 저장 후 이 Camera 탭으로 돌아와 Scene JSON을 여세요.">Builder 새 탭 ↗</a></div>
|
||||
<div class="source-summary" id="source-summary"></div>
|
||||
<div class="asset-list" id="asset-list"></div>
|
||||
</div>
|
||||
|
||||
<div class="array-card">
|
||||
<div class="array-heading"><div><span class="compact-label">MIC ARRAY</span><h3 id="array-title">배열</h3><a class="builder-link" href="/apps/array-builder/">Array Builder에서 만들기 →</a></div><strong id="microphone-count">— ch</strong></div>
|
||||
<div class="array-heading"><div><span class="compact-label">MIC ARRAY</span><h3 id="array-title">배열</h3><a class="builder-link" href="/apps/array-builder/" target="_blank" rel="noopener" title="현재 Camera의 Experiment를 유지한 채 새 탭에서 배열을 편집합니다. 저장 후 이 Camera 탭으로 돌아와 Array JSON을 여세요.">Array Builder 새 탭 ↗</a></div><strong id="microphone-count">— ch</strong></div>
|
||||
<label class="field">
|
||||
<span>배열 선택</span>
|
||||
<select id="array-preset">
|
||||
@@ -200,7 +200,7 @@
|
||||
</div>
|
||||
<p id="live-status" aria-live="polite">trajectory가 있는 Scene을 불러오거나 이동 예제를 여세요.</p>
|
||||
<div class="live-actions">
|
||||
<button class="button button--ghost" id="load-moving-example-button" type="button">이동 예제 열기</button>
|
||||
<button class="button button--ghost" id="load-moving-example-button" type="button" title="Scene·Array·Experiment와 분석 대역을 이동 예제 전체로 교체합니다.">이동 예제 전체 열기</button>
|
||||
<button class="button button--primary" id="prepare-live-button" type="button" disabled>Map 준비</button>
|
||||
<button class="button button--ghost" id="live-play-button" type="button" disabled>▶ 재생</button>
|
||||
</div>
|
||||
|
||||
@@ -981,7 +981,8 @@ function applyValidation(validation) {
|
||||
if (!state.lastRun && document.documentElement.dataset.runState === "idle") {
|
||||
setRunStatus(
|
||||
`${state.arrayDefinition.microphones.length} ch · ` +
|
||||
`${state.experiment.scan.columns}×${state.experiment.scan.rows} · 실행 준비 완료`,
|
||||
`${state.experiment.scan.columns}×${state.experiment.scan.rows} · ` +
|
||||
`분석 대역 ${state.experiment.frequencyBands.length}개 · 실행 준비 완료`,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1470,6 +1471,18 @@ function toggleLivePlayback() {
|
||||
|
||||
async function loadMovingExample() {
|
||||
if (state.active) return;
|
||||
const bandCount = state.experiment?.frequencyBands?.length ?? 0;
|
||||
if (
|
||||
(bandCount > 1 || hasUnsavedWork()) &&
|
||||
!window.confirm(
|
||||
"이동 예제는 현재 Scene·Array·Experiment 전체와 " +
|
||||
String(bandCount) +
|
||||
"개 분석 대역을 교체합니다. 계속할까요?",
|
||||
)
|
||||
) {
|
||||
setRunStatus("이동 예제를 열지 않았습니다. 현재 입력과 분석 대역을 유지합니다.");
|
||||
return;
|
||||
}
|
||||
beginInputOperation();
|
||||
try {
|
||||
setRunStatus("이동 음원 예제를 불러오고 있습니다…");
|
||||
@@ -1494,7 +1507,7 @@ async function loadMovingExample() {
|
||||
assetRuntime.reset();
|
||||
markChanged({ syncControls: true, renderBands: true, dirty: "none" });
|
||||
renderAssets();
|
||||
setRunStatus("이동 음원 예제를 열었습니다. ‘라이브 준비’를 누르세요.");
|
||||
setRunStatus("Scene·Array·Experiment를 이동 음원 예제 전체로 교체했습니다. ‘Map 준비’를 누르세요.");
|
||||
} catch (error) {
|
||||
setTransientError(error, "이동 예제 불러오기 실패");
|
||||
} finally {
|
||||
@@ -1561,10 +1574,10 @@ async function importDocument(kind, input) {
|
||||
} else if (kind === "scene") {
|
||||
issues = await validateScene(value);
|
||||
} else {
|
||||
issues = await validateExperiment(value, {
|
||||
arrayDefinition: state.arrayDefinition,
|
||||
scene: state.scene,
|
||||
});
|
||||
// An Experiment is a useful editable document even before its referenced
|
||||
// Scene or Array has been loaded. Validate its own contract here, then let
|
||||
// validateCurrent() keep Run disabled until the document IDs line up.
|
||||
issues = await validateExperiment(value);
|
||||
}
|
||||
if (generation !== state.importGenerations[kind]) return;
|
||||
if (issues.length > 0) throw new Error(issues.slice(0, 4).map(formatIssue).join(" · "));
|
||||
@@ -1588,7 +1601,21 @@ async function importDocument(kind, input) {
|
||||
dirty: kind === "experiment" ? "none" : "experiment",
|
||||
});
|
||||
renderAssets();
|
||||
setRunStatus(`${file.name} ${kind} 문서를 불러왔습니다.`);
|
||||
const bandStatus = kind === "scene"
|
||||
? ` · 분석 대역 ${state.experiment.frequencyBands.length}개 유지`
|
||||
: kind === "experiment"
|
||||
? ` · 분석 대역 ${state.experiment.frequencyBands.length}개`
|
||||
: "";
|
||||
const pendingReferences = kind === "experiment"
|
||||
? [
|
||||
value.sceneId !== state.scene?.id ? `Scene ${value.sceneId}` : null,
|
||||
value.arrayId !== state.arrayDefinition?.id ? `Array ${value.arrayId}` : null,
|
||||
].filter(Boolean)
|
||||
: [];
|
||||
const referenceStatus = pendingReferences.length > 0
|
||||
? ` · ${pendingReferences.join(" · ")} 연결 대기`
|
||||
: "";
|
||||
setRunStatus(`${file.name} ${kind} 문서를 불러왔습니다.${bandStatus}${referenceStatus}`);
|
||||
} catch (error) {
|
||||
setTransientError(error, `${file.name} 불러오기 실패`);
|
||||
} finally {
|
||||
|
||||
@@ -34,7 +34,7 @@
|
||||
JSON 열기
|
||||
<input id="array-input" type="file" accept="application/json,.json" />
|
||||
</label>
|
||||
<a class="button button--ghost" href="/apps/acoustic-camera/">Camera 열기</a>
|
||||
<a class="button button--ghost" href="/apps/acoustic-camera/" title="새 Camera 세션을 엽니다. 기존 Camera에서 편집을 시작했다면 원래 Camera 탭으로 돌아가 Array JSON을 선택하세요.">새 Camera 열기</a>
|
||||
<button class="button button--primary" id="export-button" type="button">Array JSON 저장</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
@@ -30,7 +30,7 @@
|
||||
<summary class="button button--ghost" aria-label="추가 작업">더보기</summary>
|
||||
<div class="header-more__menu">
|
||||
<button class="button button--ghost" id="wav-button" type="button" title="공간정보 없이 모든 음원을 합친 mono 확인용 WAV입니다.">Preview WAV 저장</button>
|
||||
<a class="button button--ghost" href="/apps/acoustic-camera/">Acoustic Camera 열기 →</a>
|
||||
<a class="button button--ghost" href="/apps/acoustic-camera/" title="새 Camera 세션을 엽니다. 기존 Camera에서 편집을 시작했다면 원래 Camera 탭으로 돌아가 Scene JSON을 선택하세요.">새 Acoustic Camera 열기 →</a>
|
||||
</div>
|
||||
</details>
|
||||
</div>
|
||||
|
||||
+6
-4
@@ -109,8 +109,10 @@
|
||||
<li><strong>Scene 저장 후 Camera 열기</strong>를 누릅니다.</li>
|
||||
<li>
|
||||
Camera에서 파일을 <strong>Scene → Array → Experiment → Assets</strong> 순서로
|
||||
가져옵니다. Array Builder에서 저장한 파일이 있다면 Array 단계에서 선택하고,
|
||||
내 Array/Experiment가 없다면 내장 기본값을 그대로 둡니다.
|
||||
가져오는 것을 권장합니다. Experiment를 먼저 열어도 분석 대역은 보존되며, 참조
|
||||
Scene·Array가 연결될 때까지 실행만 비활성화됩니다. Array Builder에서 저장한 파일이
|
||||
있다면 Array 단계에서 선택하고, 내 Array/Experiment가 없다면 내장 기본값을 그대로
|
||||
둡니다.
|
||||
</li>
|
||||
<li>
|
||||
Assets에서 Builder에 사용한 원본 이미지와 오디오를 한 번에 다시 선택합니다.
|
||||
@@ -266,8 +268,8 @@
|
||||
<p><code>start.cmd -Port 4174</code>처럼 다른 포트를 지정하고 <code>http://127.0.0.1:4174/</code>을 여세요.</p>
|
||||
</details>
|
||||
<details>
|
||||
<summary>Experiment를 가져올 수 없습니다.</summary>
|
||||
<p>Scene과 Array를 먼저 가져오세요. Experiment의 <code>sceneId</code>와 <code>arrayId</code>가 현재 문서와 같아야 합니다.</p>
|
||||
<summary>Experiment를 가져온 뒤 실행할 수 없습니다.</summary>
|
||||
<p>Experiment부터 가져와도 <code>frequencyBands</code>는 모두 표시됩니다. 검증 카드에 연결 대기가 보이면 참조하는 Scene과 Array를 이어서 가져와 <code>sceneId</code>와 <code>arrayId</code>를 맞추세요.</p>
|
||||
</details>
|
||||
<details>
|
||||
<summary>Assets가 준비되지 않습니다.</summary>
|
||||
|
||||
@@ -25,7 +25,8 @@ Unit과 integration test가 같은 페이지에서 실행됩니다. WebGPU 장
|
||||
- `array-builder-ui.test.js`: 프리셋·직접 추가·좌표·보정·채널 편집, undo/redo,
|
||||
중복 좌표 차단, 표준 Array JSON 저장·재가져오기와 390px layout
|
||||
- `camera-ui.test.js`: Dedicated Worker CPU/WebGPU 실행, 취소, Result schema, rectangular/Fermat와
|
||||
DAS/DAS-PHAT 비교, GCC-PHAT CPU·협대역 제약; 이동 예제 로드, 8×8 CPU·2 Hz live map의
|
||||
DAS/DAS-PHAT 비교, GCC-PHAT CPU·협대역 제약, Builder 새 탭과 Experiment 선로드·Scene
|
||||
재import의 다중 band 보존; 이동 예제 전체 교체 확인, 8×8 CPU·2 Hz live map의
|
||||
Auto→CPU fallback 뒤 공통 analysis plan 재사용, 복수 frame, scrub, 재생 시간 증가와 일시정지
|
||||
- `workflow-ui.test.js`: Dataset Builder의 Scene JSON과 원본 assets를 Acoustic Camera에 넘겨
|
||||
Result JSON을 저장하는 end-to-end 흐름
|
||||
|
||||
@@ -15,6 +15,13 @@ function dispatchValue(windowObject, element, value, eventName = "change") {
|
||||
element.dispatchEvent(new windowObject.Event(eventName, { bubbles: true }));
|
||||
}
|
||||
|
||||
function dispatchFiles(windowObject, input, files) {
|
||||
const transfer = new windowObject.DataTransfer();
|
||||
for (const file of files) transfer.items.add(file);
|
||||
input.files = transfer.files;
|
||||
input.dispatchEvent(new windowObject.Event("change", { bubbles: true }));
|
||||
}
|
||||
|
||||
async function loadJson(path) {
|
||||
const response = await fetch(path);
|
||||
assert(response.ok, `${path} load failed`);
|
||||
@@ -337,6 +344,206 @@ test("Acoustic Camera가 8×8 CPU 실행·결과 저장·stale·재실행 흐름
|
||||
}
|
||||
});
|
||||
|
||||
test("Acoustic Camera가 Experiment 선로드와 Scene·Array 재import에서 다중 분석 대역을 보존한다", async () => {
|
||||
const frame = document.createElement("iframe");
|
||||
frame.src = "/apps/acoustic-camera/?integration-test=1&band-preservation=1";
|
||||
frame.style.cssText = "position:fixed;left:-20000px;top:0;width:1400px;height:1000px;border:0";
|
||||
document.body.append(frame);
|
||||
await new Promise((resolve, reject) => {
|
||||
frame.addEventListener("load", resolve, { once: true });
|
||||
frame.addEventListener(
|
||||
"error",
|
||||
() => reject(new Error("Acoustic Camera band-preservation iframe load failed")),
|
||||
{ once: true },
|
||||
);
|
||||
});
|
||||
|
||||
const windowObject = frame.contentWindow;
|
||||
const documentObject = frame.contentDocument;
|
||||
const downloads = [];
|
||||
const objectUrls = [];
|
||||
const originalAnchorClick = windowObject.HTMLAnchorElement.prototype.click;
|
||||
const originalRevoke = windowObject.URL.revokeObjectURL.bind(windowObject.URL);
|
||||
const originalConfirm = windowObject.confirm.bind(windowObject);
|
||||
windowObject.HTMLAnchorElement.prototype.click = function captureDownload() {
|
||||
if (this.download) downloads.push({ fileName: this.download, url: this.href });
|
||||
};
|
||||
windowObject.URL.revokeObjectURL = (url) => objectUrls.push(url);
|
||||
|
||||
const readBandEditors = () =>
|
||||
[...documentObject.querySelectorAll("#band-list .band-editor")].map((editor) =>
|
||||
Object.fromEntries(
|
||||
[...editor.querySelectorAll("input[data-band-key]")].map((input) => [
|
||||
input.dataset.bandKey,
|
||||
input.type === "number" ? Number(input.value) : input.value,
|
||||
]),
|
||||
));
|
||||
|
||||
try {
|
||||
await waitFor(
|
||||
() => documentObject.documentElement.dataset.appReady === "true" &&
|
||||
documentObject.documentElement.dataset.cameraValid === "true",
|
||||
"band-preservation Camera initialization",
|
||||
);
|
||||
|
||||
const builderLinks = [...documentObject.querySelectorAll("a.builder-link")];
|
||||
assertEqual(builderLinks.length, 2, "Scene and Array Builder links");
|
||||
for (const link of builderLinks) {
|
||||
assertEqual(link.target, "_blank", "Builder opens without unloading Camera");
|
||||
assert(link.relList.contains("noopener"), "Builder tab cannot replace Camera through opener");
|
||||
}
|
||||
|
||||
const scene = await loadJson("/assets/sample-scenes/single-tone.scene.json");
|
||||
scene.id = "two-tone-band-preservation";
|
||||
scene.name = "Two-tone band preservation";
|
||||
const secondSource = structuredClone(scene.sources[0]);
|
||||
secondSource.id = "source-2";
|
||||
secondSource.name = "1.2 kHz tone";
|
||||
secondSource.position = [-0.12, 0.09, 2];
|
||||
secondSource.signal.frequency = 1200;
|
||||
secondSource.signal.phaseDeg = 45;
|
||||
scene.sources.push(secondSource);
|
||||
|
||||
const arrayDefinition = await loadJson("/assets/array-geometries/rectangular-16x12.array.json");
|
||||
arrayDefinition.id = "custom-array-band-preservation";
|
||||
arrayDefinition.name = "Custom array band preservation";
|
||||
|
||||
const experiment = await loadJson("/assets/sample-scenes/single-tone.experiment.json");
|
||||
experiment.id = "two-band-import-first";
|
||||
experiment.name = "Two-band import first";
|
||||
experiment.sceneId = scene.id;
|
||||
experiment.arrayId = arrayDefinition.id;
|
||||
experiment.frequencyBands.push({
|
||||
id: "tone-1.2k",
|
||||
minHz: 1100,
|
||||
maxHz: 1300,
|
||||
color: "#58c7ff",
|
||||
});
|
||||
const expectedBands = structuredClone(experiment.frequencyBands);
|
||||
const experimentFile = new windowObject.File(
|
||||
[JSON.stringify(experiment)],
|
||||
"two-band-import-first.experiment.json",
|
||||
{ type: "application/json" },
|
||||
);
|
||||
dispatchFiles(windowObject, documentObject.querySelector("#experiment-input"), [experimentFile]);
|
||||
await waitFor(
|
||||
() => documentObject.documentElement.dataset.inputBusy === "false",
|
||||
"clean-state Experiment-first import",
|
||||
);
|
||||
assertEqual(
|
||||
documentObject.querySelector("#experiment-id").value,
|
||||
experiment.id,
|
||||
"Experiment loads before its referenced Scene",
|
||||
);
|
||||
assertEqual(readBandEditors().length, 2, "Experiment-first import renders every analysis band");
|
||||
assertEqual(
|
||||
JSON.stringify(readBandEditors()),
|
||||
JSON.stringify(expectedBands),
|
||||
"Experiment-first import preserves band IDs, ranges, and colors",
|
||||
);
|
||||
await waitFor(
|
||||
() => documentObject.documentElement.dataset.cameraValid === "false" &&
|
||||
documentObject.querySelector("#validation-message").textContent.includes("sceneId") &&
|
||||
documentObject.querySelector("#validation-message").textContent.includes("arrayId"),
|
||||
"pending Scene and Array linkage validation",
|
||||
);
|
||||
assert(
|
||||
documentObject.querySelector("#validation-message").textContent.includes("다릅니다"),
|
||||
"Experiment-first import explains the pending document ID mismatch",
|
||||
);
|
||||
assert(
|
||||
documentObject.querySelector("#run-status").textContent.includes("분석 대역 2개") &&
|
||||
documentObject.querySelector("#run-status").textContent.includes(scene.id) &&
|
||||
documentObject.querySelector("#run-status").textContent.includes(arrayDefinition.id) &&
|
||||
documentObject.querySelector("#run-status").textContent.includes("연결 대기"),
|
||||
"Experiment-first status reports every band and pending reference",
|
||||
);
|
||||
|
||||
let replacementConfirmCount = 0;
|
||||
windowObject.confirm = () => {
|
||||
replacementConfirmCount += 1;
|
||||
return false;
|
||||
};
|
||||
documentObject.querySelector("#load-moving-example-button").click();
|
||||
assertEqual(replacementConfirmCount, 1, "full moving example asks before replacing bands");
|
||||
assertEqual(
|
||||
JSON.stringify(readBandEditors()),
|
||||
JSON.stringify(expectedBands),
|
||||
"declining the full example keeps every analysis band",
|
||||
);
|
||||
assert(
|
||||
documentObject.querySelector("#run-status").textContent.includes("유지"),
|
||||
"declined replacement explains that inputs were kept",
|
||||
);
|
||||
windowObject.confirm = originalConfirm;
|
||||
|
||||
const sceneFile = new windowObject.File(
|
||||
[JSON.stringify(scene)],
|
||||
"two-tone-band-preservation.scene.json",
|
||||
{ type: "application/json" },
|
||||
);
|
||||
dispatchFiles(windowObject, documentObject.querySelector("#scene-input"), [sceneFile]);
|
||||
await waitFor(
|
||||
() => documentObject.documentElement.dataset.inputBusy === "false" &&
|
||||
documentObject.querySelectorAll("#source-summary .source-line").length === 2 &&
|
||||
documentObject.querySelectorAll("#band-list .band-editor").length === 2 &&
|
||||
documentObject.documentElement.dataset.cameraValid === "false",
|
||||
"source-added Scene reimport",
|
||||
);
|
||||
assertEqual(
|
||||
JSON.stringify(readBandEditors()),
|
||||
JSON.stringify(expectedBands),
|
||||
"Scene reimport preserves band IDs, ranges, and colors",
|
||||
);
|
||||
assert(
|
||||
documentObject.querySelector("#run-status").textContent.includes("분석 대역 2개"),
|
||||
"Camera status keeps the preserved band count visible",
|
||||
);
|
||||
|
||||
const arrayFile = new windowObject.File(
|
||||
[JSON.stringify(arrayDefinition)],
|
||||
"custom-array-band-preservation.array.json",
|
||||
{ type: "application/json" },
|
||||
);
|
||||
dispatchFiles(windowObject, documentObject.querySelector("#array-input"), [arrayFile]);
|
||||
await waitFor(
|
||||
() => documentObject.documentElement.dataset.inputBusy === "false" &&
|
||||
documentObject.querySelectorAll("#band-list .band-editor").length === 2 &&
|
||||
documentObject.documentElement.dataset.cameraValid === "true",
|
||||
"referenced Array reimport",
|
||||
);
|
||||
assertEqual(
|
||||
JSON.stringify(readBandEditors()),
|
||||
JSON.stringify(expectedBands),
|
||||
"Array reimport also preserves every analysis band",
|
||||
);
|
||||
assert(
|
||||
documentObject.querySelector("#run-status").textContent.includes("분석 대역 2개"),
|
||||
"ready status keeps the connected Experiment band count visible",
|
||||
);
|
||||
|
||||
documentObject.querySelector("#export-experiment-button").click();
|
||||
const experimentDownload = await waitFor(
|
||||
() => downloads.find((download) => download.fileName.endsWith(".experiment.json")),
|
||||
"preserved Experiment export",
|
||||
);
|
||||
const exportedExperiment = await fetch(experimentDownload.url).then((response) => response.json());
|
||||
assertEqual(
|
||||
JSON.stringify(exportedExperiment.frequencyBands),
|
||||
JSON.stringify(expectedBands),
|
||||
"Experiment export preserves every band after Scene reimport",
|
||||
);
|
||||
assertEqual(exportedExperiment.sceneId, scene.id, "Experiment rebinds only the Scene ID");
|
||||
assertEqual(exportedExperiment.arrayId, arrayDefinition.id, "Experiment rebinds the Array ID");
|
||||
} finally {
|
||||
windowObject.confirm = originalConfirm;
|
||||
windowObject.HTMLAnchorElement.prototype.click = originalAnchorClick;
|
||||
windowObject.URL.revokeObjectURL = originalRevoke;
|
||||
for (const url of objectUrls) originalRevoke(url);
|
||||
frame.remove();
|
||||
}
|
||||
});
|
||||
|
||||
test("Acoustic Camera prepares, scrubs, plays, and pauses the moving-source example", async () => {
|
||||
const frame = document.createElement("iframe");
|
||||
frame.src = "/apps/acoustic-camera/?integration-test=1";
|
||||
|
||||
@@ -34,7 +34,8 @@ Unit과 integration module은 같은 browser runner에서 순서대로 실행됩
|
||||
1/2/5 grid와 keyboard nudge
|
||||
- `evaluation.test.js`: local peak와 검출 감도, truth matching, energy/sidelobe/main-lobe 및 비확률적
|
||||
beam-spread covariance ellipse
|
||||
- `camera.test.js`: canonical input hash, workload 추정, 이동 timeline 계획·재생 보간, Result schema 검증
|
||||
- `camera.test.js`: canonical input hash, workload 추정, Scene 정렬의 분석 band 보존,
|
||||
이동 timeline 계획·재생 보간, Result schema 검증
|
||||
- `webgpu.test.js`: WGSL buffer packing, DAS-PHAT phase normalization, bin/point batching,
|
||||
장치 한계와 typed CPU fallback 정책
|
||||
- `split-pane.test.js`: 조절식 설정 패널의 포인터 좌표, 최소·최대 폭과 양쪽 배치
|
||||
|
||||
@@ -382,7 +382,12 @@ test("Camera distance shift keeps the scene and scan on one z plane", () => {
|
||||
test("Camera aligns an experiment to an imported scene without changing grid resolution", () => {
|
||||
const experiment = {
|
||||
scan: { z: 2, xMin: -1, xMax: 1, yMin: -1, yMax: 1, columns: 32, rows: 24 },
|
||||
frequencyBands: [
|
||||
{ id: "low", minHz: 600, maxHz: 900, color: "#65f2bf" },
|
||||
{ id: "high", minHz: 1100, maxHz: 1500, color: "#58c7ff" },
|
||||
],
|
||||
};
|
||||
const originalBands = structuredClone(experiment.frequencyBands);
|
||||
const imageScene = {
|
||||
sources: [{ id: "one", position: [0.2, -0.1, 4.5] }],
|
||||
image: { plane: { z: 4.5, xMin: -2, xMax: 3, yMin: -1.5, yMax: 2.5 } },
|
||||
@@ -433,6 +438,11 @@ test("Camera aligns an experiment to an imported scene without changing grid res
|
||||
};
|
||||
assertEqual(alignExperimentToScene(experiment, movingScene), "source-bounds", "trajectory fallback");
|
||||
assertEqual(experiment.scan.z, 5, "trajectory depths contribute to median scan depth");
|
||||
assertEqual(
|
||||
JSON.stringify(experiment.frequencyBands),
|
||||
JSON.stringify(originalBands),
|
||||
"scene alignment preserves every analysis band",
|
||||
);
|
||||
assertNear(experiment.scan.xMin, -2.9, 1e-12, "trajectory x minimum with padding");
|
||||
assertNear(experiment.scan.xMax, 4.9, 1e-12, "trajectory x maximum with padding");
|
||||
assertNear(experiment.scan.yMin, -4.2, 1e-12, "trajectory y minimum with padding");
|
||||
|
||||
Reference in New Issue
Block a user