fix: rebind imported experiments to active inputs
This commit is contained in:
@@ -113,8 +113,8 @@ powershell.exe -NoLogo -NoProfile -ExecutionPolicy Bypass -File .\scripts\serve.
|
||||
### 3. Acoustic Camera에서 실행하기
|
||||
|
||||
1. 홈으로 돌아가 **Acoustic Camera**를 엽니다.
|
||||
2. 권장 순서인 **Scene → Array → Experiment → Assets**로 입력합니다. Experiment를 먼저
|
||||
열어도 모든 분석 대역이 유지됩니다.
|
||||
2. 권장 순서인 **Scene → Array → Experiment → Assets**로 입력합니다. Experiment는 어느
|
||||
시점에 열어도 모든 분석 대역을 유지하고 현재 Scene·Array에 자동으로 연결됩니다.
|
||||
3. **Scene**에서 Builder가 저장한 `.scene.json`을 선택합니다.
|
||||
4. 기본 배열을 사용하지 않을 때는 **Array**에서 Array Builder가 저장한 `.array.json`을 선택합니다.
|
||||
5. 별도 실험 문서를 사용할 때만 **Experiment**에서 `.experiment.json`을 선택합니다.
|
||||
@@ -123,10 +123,10 @@ powershell.exe -NoLogo -NoProfile -ExecutionPolicy Bypass -File .\scripts\serve.
|
||||
7. 입력 검증이 통과하면 위치 추정 알고리즘, backend와 scan grid를 정하고 실행합니다.
|
||||
8. 완료 후 **Result JSON**을 저장합니다.
|
||||
|
||||
Experiment를 먼저 불러오면 문서와 모든 `frequencyBands`를 즉시 편집할 수 있습니다. 현재
|
||||
Scene·Array와 `sceneId`·`arrayId`가 다른 동안에는 검증 카드에 연결 대기를 표시하고 실행만
|
||||
비활성화합니다. 참조하는 Scene과 Array를 이어서 불러오면 실행할 수 있습니다. Scene을 다시
|
||||
불러오면 이전 asset 연결은 해제됩니다.
|
||||
Experiment를 불러오면 Camera가 먼저 문서 자체의 계약을 검사한 뒤 `sceneId`와 `arrayId`를
|
||||
현재 열린 Scene·Array에 자동으로 연결합니다. 가져온 `frequencyBands`와 나머지 분석 설정은
|
||||
보존됩니다. 재연결한 문서는 현재 입력을 기준으로 관계 검증을 다시 수행하므로 Nyquist·scan 등
|
||||
실제 오류는 그대로 거부됩니다. Scene을 다시 불러오면 이전 asset 연결은 해제됩니다.
|
||||
|
||||
Camera 안의 **Builder 새 탭** 링크는 편집 중인 Experiment와 분석 대역을 원래 Camera 탭에
|
||||
유지합니다. Builder에서 JSON을 저장한 뒤 새 Camera를 다시 열지 말고 원래 Camera 탭으로 돌아와
|
||||
|
||||
@@ -27,7 +27,7 @@ DAS·DAS-PHAT·GCC-PHAT 위치추정으로 주파수 대역별 acoustic map과 g
|
||||
|
||||
## 사용자 입력 불러오기
|
||||
|
||||
문서는 다음 순서로 불러오는 것이 안전합니다.
|
||||
문서는 다음 순서로 불러오면 asset 연결까지 한 번에 확인하기 쉽습니다.
|
||||
|
||||
1. **Scene**: `.scene.json`
|
||||
2. **Array**: 선택 사항인 `.array.json`; 또는 **비교 배열** preset 사용
|
||||
@@ -35,8 +35,11 @@ DAS·DAS-PHAT·GCC-PHAT 위치추정으로 주파수 대역별 acoustic map과 g
|
||||
4. **Assets**: Scene이 참조하는 원본 이미지와 오디오
|
||||
|
||||
Scene을 불러오면 현재 Experiment의 `sceneId`와 scan plane을 Scene에 맞추고 기존 asset 연결을
|
||||
해제합니다. Array를 불러오면 현재 Experiment의 `arrayId`를 맞춥니다. 별도 Experiment는 현재
|
||||
Scene과 Array를 참조해야 하므로 마지막에 불러오세요.
|
||||
해제합니다. Array를 불러오면 현재 Experiment의 `arrayId`를 맞춥니다. 별도 Experiment는 어느
|
||||
시점에 불러와도 문서 자체를 먼저 검증한 뒤 `sceneId`와 `arrayId`를 현재 열린 Scene·Array에
|
||||
자동으로 연결합니다. 이후 현재 입력을 기준으로 관계 검증을 다시 수행하므로 원래 참조 ID가 달라도
|
||||
연결 대기로 실행이 막히지 않으며, 모든 분석 대역은 보존됩니다. Nyquist·scan 같은 실제 관계 오류는
|
||||
그대로 거부됩니다.
|
||||
|
||||
Scene JSON에는 원본 파일 byte가 없습니다. **Assets**에서 Builder에 사용한 파일을 다시 선택하면
|
||||
Camera가 SHA-256을 계산해 manifest에 연결합니다. 같은 이름이라도 내용이 바뀌면 거절됩니다.
|
||||
|
||||
@@ -40,7 +40,7 @@
|
||||
<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="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="Experiment를 먼저 열 수 있습니다. sceneId와 arrayId는 현재 열린 Scene·Array에 자동으로 연결됩니다."><span class="file-sequence">3</span><span><strong>Experiment</strong><small>분석 설정 · ID 자동 연결</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>
|
||||
|
||||
|
||||
@@ -1568,16 +1568,30 @@ async function importDocument(kind, input) {
|
||||
try {
|
||||
const value = await readJsonFile(file);
|
||||
if (generation !== state.importGenerations[kind]) return;
|
||||
let importedExperimentReferences = null;
|
||||
let issues;
|
||||
if (kind === "array") {
|
||||
issues = await validateArrayDefinition(value);
|
||||
} else if (kind === "scene") {
|
||||
issues = await validateScene(value);
|
||||
} else {
|
||||
// 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.
|
||||
// Validate the imported document independently before changing any
|
||||
// references. Then bind it to the open workspace and run the relationship
|
||||
// checks as well, so import convenience does not weaken the contract.
|
||||
issues = await validateExperiment(value);
|
||||
if (generation !== state.importGenerations[kind]) return;
|
||||
if (issues.length === 0) {
|
||||
importedExperimentReferences = {
|
||||
sceneId: value.sceneId,
|
||||
arrayId: value.arrayId,
|
||||
};
|
||||
value.sceneId = state.scene.id;
|
||||
value.arrayId = state.arrayDefinition.id;
|
||||
issues = await validateExperiment(value, {
|
||||
arrayDefinition: state.arrayDefinition,
|
||||
scene: state.scene,
|
||||
});
|
||||
}
|
||||
}
|
||||
if (generation !== state.importGenerations[kind]) return;
|
||||
if (issues.length > 0) throw new Error(issues.slice(0, 4).map(formatIssue).join(" · "));
|
||||
@@ -1606,14 +1620,14 @@ async function importDocument(kind, input) {
|
||||
: 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(" · ")} 연결 대기`
|
||||
const referencesChanged = kind === "experiment" && (
|
||||
importedExperimentReferences?.sceneId !== state.scene.id ||
|
||||
importedExperimentReferences?.arrayId !== state.arrayDefinition.id
|
||||
);
|
||||
const referenceStatus = kind === "experiment"
|
||||
? referencesChanged
|
||||
? ` · 현재 Scene ${state.scene.id} · Array ${state.arrayDefinition.id}로 재연결`
|
||||
: " · 현재 Scene · Array 연결 확인"
|
||||
: "";
|
||||
setRunStatus(`${file.name} ${kind} 문서를 불러왔습니다.${bandStatus}${referenceStatus}`);
|
||||
} catch (error) {
|
||||
|
||||
@@ -189,6 +189,11 @@ SHA-256을 계산해 scene의 해당 asset `sha256` 필드를 채운 상태로 c
|
||||
현재 계약 버전은 `1.0.0`이다. 브라우저는 `packages/contracts/schema-validator.js`로 저장소의
|
||||
실제 schema를 실행한 뒤 `validation.js`에서 다음 관계를 추가 검사한다.
|
||||
|
||||
Acoustic Camera에서 Experiment를 가져올 때는 문서 자체를 먼저 검증한 뒤 `sceneId`와
|
||||
`arrayId`를 현재 열린 Scene·Array의 id로 연결하고, 그 context에서 관계 검증을 다시 수행한다.
|
||||
이 가져오기 편의 동작은 계약을 완화하지 않는다. 내보내기와 실행에 쓰는 Experiment도 아래 관계를
|
||||
모두 만족해야 한다.
|
||||
|
||||
- 중복 id와 array/scene/experiment/result 참조
|
||||
- asset 종류와 안전한 상대 경로
|
||||
- Nyquist 및 시간·scan 범위
|
||||
|
||||
+6
-6
@@ -109,10 +109,10 @@
|
||||
<li><strong>Scene 저장 후 Camera 열기</strong>를 누릅니다.</li>
|
||||
<li>
|
||||
Camera에서 파일을 <strong>Scene → Array → Experiment → Assets</strong> 순서로
|
||||
가져오는 것을 권장합니다. Experiment를 먼저 열어도 분석 대역은 보존되며, 참조
|
||||
Scene·Array가 연결될 때까지 실행만 비활성화됩니다. Array Builder에서 저장한 파일이
|
||||
있다면 Array 단계에서 선택하고, 내 Array/Experiment가 없다면 내장 기본값을 그대로
|
||||
둡니다.
|
||||
가져오는 것을 권장합니다. Experiment는 어느 시점에 열어도 분석 대역을 보존하고
|
||||
<code>sceneId</code>와 <code>arrayId</code>를 현재 열린 입력에 자동으로 연결합니다.
|
||||
재연결 뒤 관계 검증도 다시 수행됩니다. Array Builder에서 저장한 파일이 있다면 Array
|
||||
단계에서 선택하고, 내 Array/Experiment가 없다면 내장 기본값을 그대로 둡니다.
|
||||
</li>
|
||||
<li>
|
||||
Assets에서 Builder에 사용한 원본 이미지와 오디오를 한 번에 다시 선택합니다.
|
||||
@@ -268,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>Experiment부터 가져와도 <code>frequencyBands</code>는 모두 표시됩니다. 검증 카드에 연결 대기가 보이면 참조하는 Scene과 Array를 이어서 가져와 <code>sceneId</code>와 <code>arrayId</code>를 맞추세요.</p>
|
||||
<summary>Experiment를 가져왔는데 검증 오류가 남습니다.</summary>
|
||||
<p>참조 ID는 현재 Scene·Array에 자동으로 연결됩니다. 실행할 수 없다면 검증 카드에서 Nyquist, scan 범위, asset처럼 재연결 뒤 확인된 실제 입력 오류를 확인하세요.</p>
|
||||
</details>
|
||||
<details>
|
||||
<summary>Assets가 준비되지 않습니다.</summary>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { validateResult } from "/packages/contracts/validation.js";
|
||||
import { validateExperiment, validateResult } from "/packages/contracts/validation.js";
|
||||
|
||||
async function waitFor(predicate, label, timeoutMs = 30000) {
|
||||
const deadline = performance.now() + timeoutMs;
|
||||
@@ -344,7 +344,7 @@ test("Acoustic Camera가 8×8 CPU 실행·결과 저장·stale·재실행 흐름
|
||||
}
|
||||
});
|
||||
|
||||
test("Acoustic Camera가 Experiment 선로드와 Scene·Array 재import에서 다중 분석 대역을 보존한다", async () => {
|
||||
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";
|
||||
@@ -393,7 +393,8 @@ test("Acoustic Camera가 Experiment 선로드와 Scene·Array 재import에서
|
||||
assert(link.relList.contains("noopener"), "Builder tab cannot replace Camera through opener");
|
||||
}
|
||||
|
||||
const scene = await loadJson("/assets/sample-scenes/single-tone.scene.json");
|
||||
const openScene = await loadJson("/assets/sample-scenes/single-tone.scene.json");
|
||||
const scene = structuredClone(openScene);
|
||||
scene.id = "two-tone-band-preservation";
|
||||
scene.name = "Two-tone band preservation";
|
||||
const secondSource = structuredClone(scene.sources[0]);
|
||||
@@ -404,7 +405,10 @@ test("Acoustic Camera가 Experiment 선로드와 Scene·Array 재import에서
|
||||
secondSource.signal.phaseDeg = 45;
|
||||
scene.sources.push(secondSource);
|
||||
|
||||
const arrayDefinition = await loadJson("/assets/array-geometries/rectangular-16x12.array.json");
|
||||
const openArrayDefinition = await loadJson(
|
||||
"/assets/array-geometries/rectangular-16x12.array.json",
|
||||
);
|
||||
const arrayDefinition = structuredClone(openArrayDefinition);
|
||||
arrayDefinition.id = "custom-array-band-preservation";
|
||||
arrayDefinition.name = "Custom array band preservation";
|
||||
|
||||
@@ -427,13 +431,15 @@ test("Acoustic Camera가 Experiment 선로드와 Scene·Array 재import에서
|
||||
);
|
||||
dispatchFiles(windowObject, documentObject.querySelector("#experiment-input"), [experimentFile]);
|
||||
await waitFor(
|
||||
() => documentObject.documentElement.dataset.inputBusy === "false",
|
||||
"clean-state Experiment-first import",
|
||||
() => documentObject.documentElement.dataset.inputBusy === "false" &&
|
||||
documentObject.documentElement.dataset.cameraValid === "true" &&
|
||||
!documentObject.querySelector("#run-button").disabled,
|
||||
"Experiment import and automatic reference binding",
|
||||
);
|
||||
assertEqual(
|
||||
documentObject.querySelector("#experiment-id").value,
|
||||
experiment.id,
|
||||
"Experiment loads before its referenced Scene",
|
||||
"Experiment document loads before its originally referenced Scene",
|
||||
);
|
||||
assertEqual(readBandEditors().length, 2, "Experiment-first import renders every analysis band");
|
||||
assertEqual(
|
||||
@@ -441,22 +447,77 @@ test("Acoustic Camera가 Experiment 선로드와 Scene·Array 재import에서
|
||||
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",
|
||||
!documentObject.querySelector("#validation-message").textContent.includes("sceneId") &&
|
||||
!documentObject.querySelector("#validation-message").textContent.includes("arrayId") &&
|
||||
!documentObject.querySelector("#run-status").textContent.includes("연결 대기"),
|
||||
"automatic binding does not leave a reference mismatch warning",
|
||||
);
|
||||
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",
|
||||
documentObject.querySelector("#run-status").textContent.includes("실행 준비 완료"),
|
||||
"Experiment import reaches a ready state with every band",
|
||||
);
|
||||
|
||||
documentObject.querySelector("#export-experiment-button").click();
|
||||
const reboundDownload = await waitFor(
|
||||
() => downloads.find((download) => download.fileName.endsWith(".experiment.json")),
|
||||
"automatically rebound Experiment export",
|
||||
);
|
||||
const reboundExperiment = await fetch(reboundDownload.url).then((response) => response.json());
|
||||
assertEqual(reboundExperiment.sceneId, openScene.id, "import binds the open Scene ID");
|
||||
assertEqual(
|
||||
reboundExperiment.arrayId,
|
||||
openArrayDefinition.id,
|
||||
"import binds the open Array ID",
|
||||
);
|
||||
assertEqual(
|
||||
JSON.stringify(reboundExperiment.frequencyBands),
|
||||
JSON.stringify(expectedBands),
|
||||
"automatic reference binding preserves every imported band",
|
||||
);
|
||||
assertEqual(
|
||||
(await validateExperiment(reboundExperiment, {
|
||||
scene: openScene,
|
||||
arrayDefinition: openArrayDefinition,
|
||||
})).length,
|
||||
0,
|
||||
"rebound Experiment passes relationship validation",
|
||||
);
|
||||
downloads.length = 0;
|
||||
|
||||
const contextInvalidExperiment = structuredClone(experiment);
|
||||
contextInvalidExperiment.id = "nyquist-invalid-after-rebind";
|
||||
contextInvalidExperiment.frequencyBands[0].maxHz = openScene.sampleRate / 2;
|
||||
assertEqual(
|
||||
(await validateExperiment(contextInvalidExperiment)).length,
|
||||
0,
|
||||
"Nyquist fixture is valid without a Scene context",
|
||||
);
|
||||
const contextInvalidFile = new windowObject.File(
|
||||
[JSON.stringify(contextInvalidExperiment)],
|
||||
"nyquist-invalid-after-rebind.experiment.json",
|
||||
{ type: "application/json" },
|
||||
);
|
||||
dispatchFiles(
|
||||
windowObject,
|
||||
documentObject.querySelector("#experiment-input"),
|
||||
[contextInvalidFile],
|
||||
);
|
||||
await waitFor(
|
||||
() => documentObject.documentElement.dataset.inputBusy === "false" &&
|
||||
documentObject.documentElement.dataset.cameraValid === "true",
|
||||
"relationship-invalid Experiment rejection",
|
||||
);
|
||||
assertEqual(
|
||||
documentObject.querySelector("#experiment-id").value,
|
||||
experiment.id,
|
||||
"context relationship validation rejects the imported Experiment before assignment",
|
||||
);
|
||||
assertEqual(
|
||||
JSON.stringify(readBandEditors()),
|
||||
JSON.stringify(expectedBands),
|
||||
"failed relationship validation leaves the current bands unchanged",
|
||||
);
|
||||
|
||||
let replacementConfirmCount = 0;
|
||||
@@ -487,7 +548,7 @@ test("Acoustic Camera가 Experiment 선로드와 Scene·Array 재import에서
|
||||
() => 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",
|
||||
documentObject.documentElement.dataset.cameraValid === "true",
|
||||
"source-added Scene reimport",
|
||||
);
|
||||
assertEqual(
|
||||
|
||||
Reference in New Issue
Block a user