fix: rebind imported experiments to active inputs

This commit is contained in:
2026-08-29 08:35:39 +09:00
parent 62971e07ec
commit 447f77dfa7
7 changed files with 130 additions and 47 deletions
+6 -3
View File
@@ -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에 연결합니다. 같은 이름이라도 내용이 바뀌면 거절됩니다.
+1 -1
View File
@@ -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>
+25 -11
View File
@@ -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) {