refactor: simplify primary workflows
This commit is contained in:
@@ -15,11 +15,24 @@
|
|||||||
<strong>micarrsim</strong>
|
<strong>micarrsim</strong>
|
||||||
<span>Dataset Builder</span>
|
<span>Dataset Builder</span>
|
||||||
</a>
|
</a>
|
||||||
|
<nav class="builder-workflow" id="builder-workflow" aria-label="Scene 제작 순서">
|
||||||
|
<ol>
|
||||||
|
<li data-workflow-step="input"><span>1</span>입력</li>
|
||||||
|
<li data-workflow-step="edit"><span>2</span>배치·편집</li>
|
||||||
|
<li data-workflow-step="validate"><span>3</span>검증</li>
|
||||||
|
<li data-workflow-step="save"><span>4</span>저장</li>
|
||||||
|
</ol>
|
||||||
|
</nav>
|
||||||
<div class="header-actions">
|
<div class="header-actions">
|
||||||
<button class="button button--ghost" id="preview-button" type="button">▶ 미리 듣기</button>
|
<button class="button button--ghost" id="preview-button" type="button">▶ 미리 듣기</button>
|
||||||
<button class="button button--ghost" id="wav-button" type="button" title="공간정보 없이 모든 음원을 합친 mono 확인용 WAV입니다.">Preview WAV 저장</button>
|
|
||||||
<button class="button button--primary" id="export-button" type="button">Scene JSON 저장</button>
|
<button class="button button--primary" id="export-button" type="button">Scene JSON 저장</button>
|
||||||
<a class="button button--ghost" href="/apps/acoustic-camera/">Scene 저장 후 Camera 열기 →</a>
|
<details class="header-more">
|
||||||
|
<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>
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
@@ -27,39 +40,40 @@
|
|||||||
<section class="scene-workspace" aria-label="장면 편집 화면">
|
<section class="scene-workspace" aria-label="장면 편집 화면">
|
||||||
<div class="workspace-heading">
|
<div class="workspace-heading">
|
||||||
<div>
|
<div>
|
||||||
<p class="eyebrow">Ground-truth canvas</p>
|
<p class="eyebrow">01 · 입력과 배치</p>
|
||||||
<h1 id="workspace-title">음향 장면</h1>
|
<h1 id="workspace-title">음향 장면</h1>
|
||||||
</div>
|
</div>
|
||||||
<div class="workspace-tools">
|
<div class="workspace-tools">
|
||||||
|
<label class="button button--ghost file-button">
|
||||||
|
Scene 불러오기
|
||||||
|
<input id="scene-input" type="file" accept="application/json,.json" />
|
||||||
|
</label>
|
||||||
<label class="button button--ghost file-button">
|
<label class="button button--ghost file-button">
|
||||||
배경 이미지
|
배경 이미지
|
||||||
<input id="image-input" type="file" accept="image/*" />
|
<input id="image-input" type="file" accept="image/*" />
|
||||||
</label>
|
</label>
|
||||||
<button class="button button--danger is-hidden" id="remove-image-button" type="button">이미지 해제</button>
|
<button class="button button--danger is-hidden" id="remove-image-button" type="button">이미지 해제</button>
|
||||||
<label class="button button--ghost file-button">
|
<label class="button button--ghost file-button" title="Scene을 다시 열었을 때 원본 파일을 SHA-256으로 재연결합니다.">
|
||||||
원본 오디오 연결
|
원본 재연결
|
||||||
<input id="audio-input" type="file" accept="audio/*,.wav,.flac,.mp3,.ogg,.m4a" multiple />
|
<input id="audio-input" type="file" accept="audio/*,.wav,.flac,.mp3,.ogg,.m4a" multiple />
|
||||||
</label>
|
</label>
|
||||||
<label class="button button--ghost file-button">
|
|
||||||
Scene 불러오기
|
|
||||||
<input id="scene-input" type="file" accept="application/json,.json" />
|
|
||||||
</label>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="canvas-card">
|
<div class="canvas-card">
|
||||||
<canvas id="scene-canvas" aria-label="이미지 평면과 음원 ground truth"></canvas>
|
<canvas id="scene-canvas" aria-label="이미지 평면과 음원 ground truth"></canvas>
|
||||||
<div class="canvas-empty" id="canvas-empty">
|
<div class="canvas-empty" id="canvas-empty">
|
||||||
<strong>이미지를 추가하거나 grid를 그대로 사용하세요.</strong>
|
<strong>음원을 선택하고 canvas를 클릭해 위치를 지정하세요.</strong>
|
||||||
<span>선택한 음원을 옮기려면 canvas를 클릭하세요.</span>
|
<span>배경 이미지는 선택 사항입니다.</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="canvas-badge" id="canvas-badge">x −0.60…0.60 m · y −0.45…0.45 m</div>
|
<div class="canvas-badge" id="canvas-badge">x −0.60…0.60 m · y −0.45…0.45 m</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="source-strip-heading">
|
<div class="source-strip-heading">
|
||||||
<div>
|
<div>
|
||||||
<h2>음원 ground truth</h2>
|
<p class="eyebrow">02 · 음원 편집</p>
|
||||||
<p>파일 음원은 선택 즉시 생성됩니다. 카드를 선택한 뒤 canvas를 클릭해 위치를 지정하세요.</p>
|
<h2>음원과 이동 경로</h2>
|
||||||
|
<p>카드를 선택한 뒤 canvas를 클릭하면 현재 위치 또는 선택한 경로점이 이동합니다.</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="source-actions">
|
<div class="source-actions">
|
||||||
<button class="button button--ghost" id="add-source-button" type="button">+ 합성 음원</button>
|
<button class="button button--ghost" id="add-source-button" type="button">+ 합성 음원</button>
|
||||||
@@ -73,8 +87,11 @@
|
|||||||
|
|
||||||
<section class="asset-shelf">
|
<section class="asset-shelf">
|
||||||
<div>
|
<div>
|
||||||
<h2>로컬 assets</h2>
|
<h2>연결된 원본 파일</h2>
|
||||||
<p>원본은 업로드되지 않고 브라우저에서만 처리됩니다. PCM WAV를 권장하며, 다른 codec 지원은 환경에 따라 다릅니다.<br />Scene을 다시 불러온 뒤에는 위의 <strong>원본 오디오 연결</strong>에서 같은 파일을 선택하세요.</p>
|
<details class="asset-help">
|
||||||
|
<summary>파일 연결 도움말</summary>
|
||||||
|
<p>원본은 업로드되지 않고 브라우저에서만 처리됩니다. Scene을 다시 열었다면 위의 <strong>원본 재연결</strong>에서 같은 파일을 선택하세요. PCM WAV를 권장합니다.</p>
|
||||||
|
</details>
|
||||||
</div>
|
</div>
|
||||||
<div class="asset-list" id="asset-list"></div>
|
<div class="asset-list" id="asset-list"></div>
|
||||||
</section>
|
</section>
|
||||||
@@ -86,40 +103,13 @@
|
|||||||
<span class="save-state" id="save-state">기본 장면</span>
|
<span class="save-state" id="save-state">기본 장면</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<section class="control-section">
|
<section class="validation-card" aria-live="polite">
|
||||||
<div class="section-title"><span>01</span><h2>장면</h2></div>
|
<div class="validation-dot" id="validation-dot"></div>
|
||||||
<label class="field field--wide">
|
<div><strong id="validation-title">검증 대기</strong><p id="validation-message">장면을 준비하고 있습니다.</p></div>
|
||||||
<span>이름</span>
|
|
||||||
<input id="scene-name" type="text" maxlength="120" />
|
|
||||||
</label>
|
|
||||||
<label class="field field--wide">
|
|
||||||
<span>Scene ID</span>
|
|
||||||
<input id="scene-id" type="text" maxlength="120" pattern="[a-z0-9][a-z0-9._-]*" />
|
|
||||||
</label>
|
|
||||||
<div class="field-grid">
|
|
||||||
<label class="field"><span>길이 (s)</span><input id="scene-duration" type="number" min="0.05" max="300" step="0.05" /></label>
|
|
||||||
<label class="field"><span>Sample rate</span><input id="sample-rate" type="number" min="8000" max="192000" step="1" /></label>
|
|
||||||
<label class="field"><span>음속 (m/s)</span><input id="speed-of-sound" type="number" min="250" max="400" step="0.1" /></label>
|
|
||||||
<label class="field"><span>Seed</span><input id="scene-seed" type="number" min="0" max="4294967295" step="1" /></label>
|
|
||||||
<label class="field"><span>Mix 정규화</span><select id="mix-normalization"><option value="none">없음</option><option value="peak">Peak</option></select></label>
|
|
||||||
<label class="field"><span>Target peak</span><input id="target-peak" type="number" min="0.01" max="1" step="0.01" /></label>
|
|
||||||
</div>
|
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="control-section">
|
<section class="control-section control-section--primary" id="source-editor-section">
|
||||||
<div class="section-title"><span>02</span><h2>표시 평면</h2></div>
|
<div class="section-title"><span>02</span><h2>선택 음원 편집</h2></div>
|
||||||
<div class="field-grid field-grid--compact">
|
|
||||||
<label class="field"><span>x min</span><input id="plane-x-min" type="number" step="0.05" /></label>
|
|
||||||
<label class="field"><span>x max</span><input id="plane-x-max" type="number" step="0.05" /></label>
|
|
||||||
<label class="field"><span>y min</span><input id="plane-y-min" type="number" step="0.05" /></label>
|
|
||||||
<label class="field"><span>y max</span><input id="plane-y-max" type="number" step="0.05" /></label>
|
|
||||||
<label class="field"><span>plane z (m)</span><input id="plane-z" type="number" min="0.01" step="0.1" /></label>
|
|
||||||
</div>
|
|
||||||
<p class="section-note">x/y는 편집 화면 범위이며, 배경 이미지가 있을 때 image plane으로 JSON에 저장됩니다.</p>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section class="control-section" id="source-editor-section">
|
|
||||||
<div class="section-title"><span>03</span><h2>선택 음원</h2></div>
|
|
||||||
<div id="no-source-message" class="inline-note">음원을 선택하세요.</div>
|
<div id="no-source-message" class="inline-note">음원을 선택하세요.</div>
|
||||||
<div id="source-fields">
|
<div id="source-fields">
|
||||||
<label class="field field--wide"><span>이름</span><input id="source-name" type="text" maxlength="120" /></label>
|
<label class="field field--wide"><span>이름</span><input id="source-name" type="text" maxlength="120" /></label>
|
||||||
@@ -135,13 +125,13 @@
|
|||||||
</div>
|
</div>
|
||||||
<section class="trajectory-editor" aria-label="음원 이동 경로">
|
<section class="trajectory-editor" aria-label="음원 이동 경로">
|
||||||
<label class="check-field trajectory-toggle"><input id="source-motion" type="checkbox" /><span>시간에 따라 음원 이동</span></label>
|
<label class="check-field trajectory-toggle"><input id="source-motion" type="checkbox" /><span>시간에 따라 음원 이동</span></label>
|
||||||
<p class="section-note">경로 시간은 Scene 시작 기준입니다. 이동을 켜면 현재 위치에서 시작해 Scene 끝까지 이어지는 두 점 경로가 만들어집니다.</p>
|
<p class="section-note">켜면 현재 위치부터 Scene 끝까지의 기본 경로가 만들어집니다.</p>
|
||||||
<div id="trajectory-fields" class="is-hidden">
|
<div id="trajectory-fields" class="is-hidden">
|
||||||
<label class="field field--wide trajectory-preview">
|
<label class="field field--wide trajectory-preview">
|
||||||
<span>경로 미리보기 · <output id="preview-time-value" for="preview-time">0.00 s</output></span>
|
<span>경로 미리보기 · <output id="preview-time-value" for="preview-time">0.00 s</output></span>
|
||||||
<input id="preview-time" type="range" min="0" max="1" step="0.01" value="0" />
|
<input id="preview-time" type="range" min="0" max="1" step="0.01" value="0" />
|
||||||
</label>
|
</label>
|
||||||
<p class="section-note">경로점을 선택하면 canvas 클릭으로 그 점의 x/y를 옮길 수 있습니다. 흰 테두리 점은 현재 선택한 경로점입니다.</p>
|
<p class="section-note">경로점을 선택하고 canvas를 클릭해 x/y를 옮기세요.</p>
|
||||||
<div class="trajectory-list" id="trajectory-list"></div>
|
<div class="trajectory-list" id="trajectory-list"></div>
|
||||||
<button class="button button--ghost button--full" id="add-keyframe-button" type="button">+ 경로점 추가</button>
|
<button class="button button--ghost button--full" id="add-keyframe-button" type="button">+ 경로점 추가</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -161,7 +151,42 @@
|
|||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="control-section">
|
<section class="control-section">
|
||||||
<div class="section-title"><span>04</span><h2>Ambient</h2></div>
|
<div class="section-title"><span>설정</span><h2>장면 기본값</h2></div>
|
||||||
|
<label class="field field--wide">
|
||||||
|
<span>이름</span>
|
||||||
|
<input id="scene-name" type="text" maxlength="120" />
|
||||||
|
</label>
|
||||||
|
<label class="field field--wide">
|
||||||
|
<span>Scene ID</span>
|
||||||
|
<input id="scene-id" type="text" maxlength="120" pattern="[a-z0-9][a-z0-9._-]*" />
|
||||||
|
</label>
|
||||||
|
<label class="field field--wide"><span>길이 (s)</span><input id="scene-duration" type="number" min="0.05" max="300" step="0.05" /></label>
|
||||||
|
<details class="inline-disclosure">
|
||||||
|
<summary>고급 음향·믹스 설정</summary>
|
||||||
|
<div class="field-grid">
|
||||||
|
<label class="field"><span>Sample rate</span><input id="sample-rate" type="number" min="8000" max="192000" step="1" /></label>
|
||||||
|
<label class="field"><span>음속 (m/s)</span><input id="speed-of-sound" type="number" min="250" max="400" step="0.1" /></label>
|
||||||
|
<label class="field"><span>Seed</span><input id="scene-seed" type="number" min="0" max="4294967295" step="1" /></label>
|
||||||
|
<label class="field"><span>Mix 정규화</span><select id="mix-normalization"><option value="none">없음</option><option value="peak">Peak</option></select></label>
|
||||||
|
<label class="field"><span>Target peak</span><input id="target-peak" type="number" min="0.01" max="1" step="0.01" /></label>
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<details class="control-section disclosure-section">
|
||||||
|
<summary class="section-summary"><span>범위</span><strong>표시 평면</strong><small>고급</small></summary>
|
||||||
|
<div class="field-grid field-grid--compact">
|
||||||
|
<label class="field"><span>x min</span><input id="plane-x-min" type="number" step="0.05" /></label>
|
||||||
|
<label class="field"><span>x max</span><input id="plane-x-max" type="number" step="0.05" /></label>
|
||||||
|
<label class="field"><span>y min</span><input id="plane-y-min" type="number" step="0.05" /></label>
|
||||||
|
<label class="field"><span>y max</span><input id="plane-y-max" type="number" step="0.05" /></label>
|
||||||
|
<label class="field"><span>plane z (m)</span><input id="plane-z" type="number" min="0.01" step="0.1" /></label>
|
||||||
|
</div>
|
||||||
|
<p class="section-note">x/y는 canvas 범위이며 배경이 있으면 image plane으로 저장됩니다.</p>
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<details class="control-section disclosure-section">
|
||||||
|
<summary class="section-summary"><span>배경음</span><strong>Ambient</strong><small>선택 사항</small></summary>
|
||||||
<label class="field field--wide"><span>종류</span><select id="ambient-type"><option value="none">없음</option><option value="white">White noise</option><option value="pink">Pink noise</option><option value="asset">Audio asset</option></select></label>
|
<label class="field field--wide"><span>종류</span><select id="ambient-type"><option value="none">없음</option><option value="white">White noise</option><option value="pink">Pink noise</option><option value="asset">Audio asset</option></select></label>
|
||||||
<label class="field field--wide" id="ambient-asset-field"><span>Audio asset</span><select id="ambient-asset"></select></label>
|
<label class="field field--wide" id="ambient-asset-field"><span>Audio asset</span><select id="ambient-asset"></select></label>
|
||||||
<div class="field-grid" id="ambient-asset-options">
|
<div class="field-grid" id="ambient-asset-options">
|
||||||
@@ -169,12 +194,7 @@
|
|||||||
<label class="check-field"><input id="ambient-loop" type="checkbox" /><span>짧은 파일 반복</span></label>
|
<label class="check-field"><input id="ambient-loop" type="checkbox" /><span>짧은 파일 반복</span></label>
|
||||||
</div>
|
</div>
|
||||||
<label class="field field--wide"><span>Gain (dB)</span><input id="ambient-gain" type="number" min="-120" max="24" step="1" /></label>
|
<label class="field field--wide"><span>Gain (dB)</span><input id="ambient-gain" type="number" min="-120" max="24" step="1" /></label>
|
||||||
</section>
|
</details>
|
||||||
|
|
||||||
<section class="validation-card" aria-live="polite">
|
|
||||||
<div class="validation-dot" id="validation-dot"></div>
|
|
||||||
<div><strong id="validation-title">검증 대기</strong><p id="validation-message">장면을 준비하고 있습니다.</p></div>
|
|
||||||
</section>
|
|
||||||
</aside>
|
</aside>
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -643,3 +643,363 @@
|
|||||||
grid-row: 2;
|
grid-row: 2;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Task-first Builder navigation and progressive disclosure. */
|
||||||
|
.builder-header {
|
||||||
|
min-height: 58px;
|
||||||
|
padding-block: 9px;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.builder-workflow {
|
||||||
|
min-width: 0;
|
||||||
|
margin-left: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.builder-workflow ol {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
list-style: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.builder-workflow li {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
color: var(--muted);
|
||||||
|
font-size: 0.68rem;
|
||||||
|
font-weight: 680;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.builder-workflow li + li::before {
|
||||||
|
width: 18px;
|
||||||
|
height: 1px;
|
||||||
|
margin: 0 7px;
|
||||||
|
background: var(--line);
|
||||||
|
content: "";
|
||||||
|
}
|
||||||
|
|
||||||
|
.builder-workflow li span {
|
||||||
|
display: grid;
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
border: 1px solid rgb(101 242 191 / 34%);
|
||||||
|
border-radius: 50%;
|
||||||
|
place-items: center;
|
||||||
|
color: var(--mint);
|
||||||
|
font: 750 0.6rem ui-monospace, SFMono-Regular, Consolas, monospace;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-more {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-more > summary {
|
||||||
|
list-style: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-more > summary::-webkit-details-marker,
|
||||||
|
.inline-disclosure > summary::-webkit-details-marker,
|
||||||
|
.section-summary::-webkit-details-marker,
|
||||||
|
.asset-help > summary::-webkit-details-marker {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-more__menu {
|
||||||
|
position: absolute;
|
||||||
|
z-index: 30;
|
||||||
|
top: calc(100% + 8px);
|
||||||
|
right: 0;
|
||||||
|
display: grid;
|
||||||
|
width: max-content;
|
||||||
|
min-width: 210px;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 10px;
|
||||||
|
border: 1px solid var(--line);
|
||||||
|
border-radius: 12px;
|
||||||
|
background: #0b1714;
|
||||||
|
box-shadow: 0 18px 48px rgb(0 0 0 / 35%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-more__menu .button {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.builder-layout {
|
||||||
|
grid-template-columns: minmax(0, 1fr) minmax(320px, 370px);
|
||||||
|
min-height: calc(100vh - 59px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.scene-workspace {
|
||||||
|
padding: clamp(20px, 2.5vw, 34px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.workspace-heading {
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.workspace-heading h1 {
|
||||||
|
font-size: clamp(1.55rem, 3vw, 2.35rem);
|
||||||
|
}
|
||||||
|
|
||||||
|
.source-strip-heading .eyebrow {
|
||||||
|
margin-bottom: 5px;
|
||||||
|
font-size: 0.64rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.source-strip-heading {
|
||||||
|
margin-top: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.asset-shelf {
|
||||||
|
align-items: center;
|
||||||
|
margin-top: 20px;
|
||||||
|
padding: 14px 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.asset-help {
|
||||||
|
color: var(--muted);
|
||||||
|
font-size: 0.7rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.asset-help summary {
|
||||||
|
width: fit-content;
|
||||||
|
margin-top: 4px;
|
||||||
|
color: var(--mint);
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.asset-help p {
|
||||||
|
max-width: 520px;
|
||||||
|
margin-top: 9px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.control-panel {
|
||||||
|
top: 59px;
|
||||||
|
max-height: calc(100vh - 59px);
|
||||||
|
padding: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.control-section--primary {
|
||||||
|
border-color: rgb(101 242 191 / 28%);
|
||||||
|
background: rgb(101 242 191 / 3%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.validation-card {
|
||||||
|
margin-top: 0;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
padding: 13px 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.control-section + .validation-card {
|
||||||
|
margin-top: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inline-disclosure {
|
||||||
|
margin-top: 12px;
|
||||||
|
border-top: 1px solid var(--line);
|
||||||
|
}
|
||||||
|
|
||||||
|
.inline-disclosure > summary {
|
||||||
|
padding: 12px 0 0;
|
||||||
|
color: var(--muted);
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 0.7rem;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inline-disclosure > summary::after {
|
||||||
|
float: right;
|
||||||
|
color: var(--mint);
|
||||||
|
content: "+";
|
||||||
|
}
|
||||||
|
|
||||||
|
.inline-disclosure[open] > summary::after {
|
||||||
|
content: "−";
|
||||||
|
}
|
||||||
|
|
||||||
|
.disclosure-section {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-summary {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: auto 1fr auto;
|
||||||
|
align-items: center;
|
||||||
|
gap: 9px;
|
||||||
|
padding: 15px 17px;
|
||||||
|
cursor: pointer;
|
||||||
|
list-style: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-summary > span {
|
||||||
|
color: var(--mint);
|
||||||
|
font: 750 0.62rem ui-monospace, SFMono-Regular, Consolas, monospace;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-summary > strong {
|
||||||
|
font-size: 0.82rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-summary > small {
|
||||||
|
color: var(--muted);
|
||||||
|
font-size: 0.62rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.disclosure-section[open] {
|
||||||
|
padding: 0 17px 17px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.disclosure-section[open] .section-summary {
|
||||||
|
margin: 0 -17px 14px;
|
||||||
|
border-bottom: 1px solid var(--line);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 1180px) {
|
||||||
|
.builder-workflow li {
|
||||||
|
font-size: 0;
|
||||||
|
gap: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.builder-workflow li + li::before {
|
||||||
|
width: 12px;
|
||||||
|
margin-inline: 5px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 1100px) {
|
||||||
|
.control-panel {
|
||||||
|
max-height: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 760px) {
|
||||||
|
.builder-header {
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.builder-workflow {
|
||||||
|
order: 3;
|
||||||
|
width: 100%;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.builder-workflow ol {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||||
|
}
|
||||||
|
|
||||||
|
.builder-workflow li {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 20px minmax(0, 1fr);
|
||||||
|
gap: 5px;
|
||||||
|
font-size: 0.64rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.builder-workflow li + li::before {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-actions {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-actions > .button,
|
||||||
|
.header-actions > .header-more,
|
||||||
|
.header-more > summary {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-more__menu {
|
||||||
|
top: calc(100% + 6px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.asset-shelf {
|
||||||
|
align-items: stretch;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 430px) {
|
||||||
|
.builder-header,
|
||||||
|
.scene-workspace,
|
||||||
|
.control-panel {
|
||||||
|
padding-inline: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.builder-workflow li {
|
||||||
|
grid-template-columns: 18px minmax(0, 1fr);
|
||||||
|
font-size: 0.59rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.builder-workflow li span {
|
||||||
|
width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-actions {
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-actions .button {
|
||||||
|
min-height: 36px;
|
||||||
|
padding-inline: 7px;
|
||||||
|
font-size: 0.72rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-more__menu {
|
||||||
|
right: -2px;
|
||||||
|
min-width: 190px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.canvas-card {
|
||||||
|
min-height: 360px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#scene-canvas {
|
||||||
|
height: 360px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.workspace-tools .button,
|
||||||
|
.source-actions .button {
|
||||||
|
font-size: 0.74rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.trajectory-row {
|
||||||
|
grid-template-columns: 34px repeat(2, minmax(0, 1fr)) 30px;
|
||||||
|
grid-template-rows: repeat(2, auto);
|
||||||
|
}
|
||||||
|
|
||||||
|
.trajectory-row__select,
|
||||||
|
.trajectory-row__remove {
|
||||||
|
grid-row: 1 / 3;
|
||||||
|
align-self: stretch;
|
||||||
|
}
|
||||||
|
|
||||||
|
.trajectory-row .field:nth-of-type(1) {
|
||||||
|
grid-column: 2;
|
||||||
|
grid-row: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.trajectory-row .field:nth-of-type(2) {
|
||||||
|
grid-column: 3;
|
||||||
|
grid-row: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.trajectory-row .field:nth-of-type(3) {
|
||||||
|
grid-column: 2;
|
||||||
|
grid-row: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.trajectory-row .field:nth-of-type(4) {
|
||||||
|
grid-column: 3;
|
||||||
|
grid-row: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.trajectory-row__remove {
|
||||||
|
grid-column: 4;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
+37
-70
@@ -11,90 +11,57 @@
|
|||||||
<link rel="stylesheet" href="/packages/ui/app-shell.css" />
|
<link rel="stylesheet" href="/packages/ui/app-shell.css" />
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
<header class="landing-header">
|
||||||
|
<a class="landing-brand" href="/" aria-label="micarrsim 홈">micarrsim</a>
|
||||||
|
<div class="status-row" aria-label="실행 환경">
|
||||||
|
<span class="status-chip" id="webgpu-status">그래픽 장치 확인 중…</span>
|
||||||
|
<span class="status-chip status-chip--neutral">파일은 이 기기에서 처리됩니다</span>
|
||||||
|
</div>
|
||||||
|
<a class="landing-docs" href="/docs/">사용 설명서</a>
|
||||||
|
</header>
|
||||||
|
|
||||||
<main class="landing-shell">
|
<main class="landing-shell">
|
||||||
<section class="hero-card">
|
<section class="landing-intro" aria-labelledby="landing-title">
|
||||||
<p class="eyebrow">192-channel acoustic camera laboratory</p>
|
<p class="eyebrow">Microphone array workbench</p>
|
||||||
<h1>micarrsim</h1>
|
<h1 id="landing-title">무엇을 하시겠어요?</h1>
|
||||||
<p class="hero-copy">
|
<p>배열 위치추정을 바로 실행하거나, 평가에 사용할 음원 장면을 먼저 만드세요.</p>
|
||||||
합성 음향 장면을 만들고, 마이크 배열의 자유음장 응답과 DAS 음향 영상을 같은 조건에서
|
|
||||||
비교합니다.
|
|
||||||
</p>
|
|
||||||
<div class="status-row" aria-label="실행 환경">
|
|
||||||
<span class="status-chip" id="webgpu-status">그래픽 장치 확인 중…</span>
|
|
||||||
<span class="status-chip status-chip--neutral">로컬 처리 · 외부 업로드 없음</span>
|
|
||||||
</div>
|
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="workflow-card" aria-labelledby="workflow-title">
|
<section class="launcher-grid" aria-label="작업 선택">
|
||||||
<div class="workflow-card__heading">
|
<a class="launcher-card launcher-card--accent" href="/apps/acoustic-camera/">
|
||||||
|
<span class="launcher-card__number" aria-hidden="true">01</span>
|
||||||
<div>
|
<div>
|
||||||
<p class="eyebrow">Local workflow</p>
|
<p class="launcher-card__kicker">바로 분석</p>
|
||||||
<h2 id="workflow-title">파일이 흐르는 방식</h2>
|
<h2>Acoustic Camera</h2>
|
||||||
|
<p>기본 예제 또는 내 Scene·배열 설정으로 위치추정 결과를 확인합니다.</p>
|
||||||
</div>
|
</div>
|
||||||
<a href="/docs/#asset-reconnect">왜 원본을 다시 선택하나요?</a>
|
<span class="launcher-card__action">Camera 열기 →</span>
|
||||||
</div>
|
</a>
|
||||||
<ol class="workflow-list">
|
|
||||||
<li><strong>Dataset Builder</strong><span>음원과 위치 만들기</span></li>
|
|
||||||
<li><strong>Scene JSON + 원본 assets</strong><span>같이 보관하기</span></li>
|
|
||||||
<li><strong>Acoustic Camera</strong><span>배열·DAS 실행하기</span></li>
|
|
||||||
<li><strong>Result JSON</strong><span>map·지표 저장하기</span></li>
|
|
||||||
</ol>
|
|
||||||
<p class="workflow-note">
|
|
||||||
원본 파일은 JSON에 포함되거나 외부로 업로드되지 않습니다. Camera에서 같은 파일을 다시
|
|
||||||
선택하면 SHA-256으로 확인한 뒤 연결합니다.
|
|
||||||
</p>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section class="launcher-grid" aria-label="도구 선택">
|
|
||||||
<a class="launcher-card" href="/apps/dataset-builder/">
|
<a class="launcher-card" href="/apps/dataset-builder/">
|
||||||
<span class="launcher-card__number">01</span>
|
<span class="launcher-card__number" aria-hidden="true">02</span>
|
||||||
<div>
|
<div>
|
||||||
|
<p class="launcher-card__kicker">평가 장면 제작</p>
|
||||||
<h2>Dataset Builder</h2>
|
<h2>Dataset Builder</h2>
|
||||||
<p>기기 이미지에 음원을 배치하고 ambient를 혼합해 재현 가능한 장면을 만듭니다.</p>
|
<p>음원 파일과 이동 경로를 배치해 정답 위치가 있는 Scene을 만듭니다.</p>
|
||||||
</div>
|
</div>
|
||||||
<span class="launcher-card__action">새 Scene 만들기 →</span>
|
<span class="launcher-card__action">새 Scene 만들기 →</span>
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
<a class="launcher-card launcher-card--accent" href="/apps/acoustic-camera/">
|
|
||||||
<span class="launcher-card__number">02</span>
|
|
||||||
<div>
|
|
||||||
<h2>Acoustic Camera</h2>
|
|
||||||
<p>배열 응답과 DAS 결과를 비교하고 이동 음원의 acoustic map을 시간축으로 재생합니다.</p>
|
|
||||||
</div>
|
|
||||||
<span class="launcher-card__action">기본 예제로 바로 실행 →</span>
|
|
||||||
</a>
|
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="quick-start-grid" aria-label="빠른 시작">
|
<details class="workflow-disclosure">
|
||||||
<article class="quick-start-card">
|
<summary>내 파일로 분석하는 순서</summary>
|
||||||
<p class="eyebrow">30초 확인</p>
|
<ol class="workflow-list">
|
||||||
<h2>설정 없이 첫 결과 만들기</h2>
|
<li><strong>Scene 제작</strong><span>Builder에서 음원과 위치 지정</span></li>
|
||||||
<p>Acoustic Camera를 열어 <strong>▶ 기본 예제 실행</strong>을 누르고, 완료되면 <strong>Result JSON</strong>을 저장하세요.</p>
|
<li><strong>파일 보관</strong><span>Scene JSON과 원본 파일을 함께 보관</span></li>
|
||||||
<a href="/apps/acoustic-camera/">기본 예제 열기 →</a>
|
<li><strong>Camera 입력</strong><span>Scene, 배열, 원본 파일 선택</span></li>
|
||||||
</article>
|
<li><strong>결과 저장</strong><span>위치 map과 지표를 Result JSON으로 저장</span></li>
|
||||||
<article class="quick-start-card">
|
</ol>
|
||||||
<p class="eyebrow">이동 음원</p>
|
<p class="workflow-note">
|
||||||
<h2>설정 없이 라이브 재생</h2>
|
Scene JSON에는 원본 파일이 포함되지 않습니다. 다시 열 때 같은 파일을 선택하면 SHA-256으로
|
||||||
<p>Camera에서 <strong>이동 예제 → 라이브 준비 → 재생</strong>만 누르면 됩니다.</p>
|
확인해 연결합니다. <a href="/docs/#asset-reconnect">자세히 보기</a>
|
||||||
<a href="/apps/acoustic-camera/">이동 예제 열기 →</a>
|
</p>
|
||||||
</article>
|
</details>
|
||||||
<article class="quick-start-card">
|
|
||||||
<p class="eyebrow">내 데이터</p>
|
|
||||||
<h2>Builder부터 끝까지</h2>
|
|
||||||
<p>Scene을 저장한 뒤 Camera에서 <strong>Scene → Array → Experiment → Assets</strong> 순서로 가져오면 참조 오류를 줄일 수 있습니다.</p>
|
|
||||||
<a href="/docs/#full-workflow">5분 안내 보기 →</a>
|
|
||||||
</article>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<footer class="landing-footer">
|
|
||||||
<a href="/docs/">사용 설명서</a>
|
|
||||||
<span aria-hidden="true">·</span>
|
|
||||||
<a href="/tests/">테스트</a>
|
|
||||||
<span aria-hidden="true">·</span>
|
|
||||||
<a href="/benchmarks/webgpu-scaling.html">성능 측정</a>
|
|
||||||
<span aria-hidden="true">·</span>
|
|
||||||
<a href="/docs/execution-plan.md">실행 계획</a>
|
|
||||||
</footer>
|
|
||||||
</main>
|
</main>
|
||||||
<script type="module" src="/packages/ui/landing.js"></script>
|
<script type="module" src="/packages/ui/landing.js"></script>
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
@@ -396,3 +396,218 @@ p {
|
|||||||
transition-duration: 0.01ms !important;
|
transition-duration: 0.01ms !important;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Compact, task-first landing page. */
|
||||||
|
.landing-header {
|
||||||
|
display: flex;
|
||||||
|
width: min(1120px, calc(100% - 40px));
|
||||||
|
min-height: 64px;
|
||||||
|
margin: 0 auto;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 20px;
|
||||||
|
border-bottom: 1px solid var(--line);
|
||||||
|
}
|
||||||
|
|
||||||
|
.landing-brand,
|
||||||
|
.landing-docs {
|
||||||
|
color: inherit;
|
||||||
|
font-weight: 760;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.landing-brand {
|
||||||
|
font-size: 1rem;
|
||||||
|
letter-spacing: -0.035em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.landing-docs {
|
||||||
|
color: var(--muted);
|
||||||
|
font-size: 0.78rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.landing-docs:hover,
|
||||||
|
.landing-docs:focus-visible {
|
||||||
|
color: var(--text);
|
||||||
|
}
|
||||||
|
|
||||||
|
.landing-header .status-row {
|
||||||
|
justify-content: center;
|
||||||
|
margin-left: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.landing-shell {
|
||||||
|
width: min(960px, calc(100% - 40px));
|
||||||
|
padding: clamp(44px, 8vw, 84px) 0 36px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.landing-intro {
|
||||||
|
max-width: 720px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.landing-intro .eyebrow {
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.landing-intro h1 {
|
||||||
|
margin-bottom: 12px;
|
||||||
|
font-size: clamp(2.25rem, 6vw, 4.2rem);
|
||||||
|
font-weight: 760;
|
||||||
|
letter-spacing: -0.065em;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.landing-intro > p:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
color: #c8d9d3;
|
||||||
|
font-size: clamp(0.95rem, 2vw, 1.1rem);
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.landing-shell .launcher-grid {
|
||||||
|
gap: 16px;
|
||||||
|
margin-top: 28px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.landing-shell .launcher-card {
|
||||||
|
grid-template-rows: auto 1fr auto;
|
||||||
|
min-height: 250px;
|
||||||
|
padding: clamp(22px, 4vw, 30px);
|
||||||
|
border-radius: 20px;
|
||||||
|
box-shadow: 0 16px 48px rgb(0 0 0 / 20%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.landing-shell .launcher-card:hover,
|
||||||
|
.landing-shell .launcher-card:focus-visible {
|
||||||
|
transform: translateY(-3px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.launcher-card__number {
|
||||||
|
font: 750 0.7rem ui-monospace, SFMono-Regular, Consolas, monospace;
|
||||||
|
}
|
||||||
|
|
||||||
|
.launcher-card__kicker {
|
||||||
|
margin: 28px 0 6px !important;
|
||||||
|
color: var(--mint) !important;
|
||||||
|
font-size: 0.7rem;
|
||||||
|
font-weight: 760;
|
||||||
|
letter-spacing: 0.08em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.launcher-card--accent .launcher-card__kicker {
|
||||||
|
color: var(--cyan) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.landing-shell .launcher-card h2 {
|
||||||
|
margin-bottom: 10px;
|
||||||
|
font-size: clamp(1.45rem, 3vw, 2rem);
|
||||||
|
}
|
||||||
|
|
||||||
|
.landing-shell .launcher-card p {
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.landing-shell .launcher-card__action {
|
||||||
|
margin-top: 24px;
|
||||||
|
font-size: 0.84rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.workflow-disclosure {
|
||||||
|
margin-top: 16px;
|
||||||
|
padding: 0 18px;
|
||||||
|
border: 1px solid var(--line);
|
||||||
|
border-radius: 14px;
|
||||||
|
background: rgb(255 255 255 / 2%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.workflow-disclosure summary {
|
||||||
|
padding: 16px 0;
|
||||||
|
color: #c8d9d3;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 0.82rem;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.workflow-disclosure[open] summary {
|
||||||
|
border-bottom: 1px solid var(--line);
|
||||||
|
}
|
||||||
|
|
||||||
|
.workflow-disclosure .workflow-list {
|
||||||
|
margin: 16px 0 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.workflow-disclosure .workflow-list li {
|
||||||
|
min-height: 78px;
|
||||||
|
padding: 12px;
|
||||||
|
border-radius: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.workflow-disclosure .workflow-list li::before {
|
||||||
|
margin-bottom: 9px;
|
||||||
|
font-size: 0.62rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.workflow-disclosure .workflow-list strong {
|
||||||
|
font-size: 0.78rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.workflow-disclosure .workflow-list span {
|
||||||
|
margin-top: 4px;
|
||||||
|
font-size: 0.68rem;
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.workflow-disclosure .workflow-note {
|
||||||
|
margin: 0 0 16px;
|
||||||
|
font-size: 0.74rem;
|
||||||
|
line-height: 1.55;
|
||||||
|
}
|
||||||
|
|
||||||
|
.workflow-disclosure .workflow-note a {
|
||||||
|
color: var(--mint);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 720px) {
|
||||||
|
.landing-header {
|
||||||
|
min-height: 58px;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.landing-header .status-chip--neutral {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.landing-shell .launcher-card {
|
||||||
|
min-height: 220px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 430px) {
|
||||||
|
.landing-header,
|
||||||
|
.landing-shell {
|
||||||
|
width: min(100% - 28px, 960px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.landing-header {
|
||||||
|
flex-wrap: wrap;
|
||||||
|
padding-top: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.landing-header .status-row {
|
||||||
|
order: 3;
|
||||||
|
width: 100%;
|
||||||
|
margin: 0 0 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.landing-shell {
|
||||||
|
padding-top: 36px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.landing-intro h1 {
|
||||||
|
font-size: 2.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.landing-shell .launcher-card {
|
||||||
|
min-height: 205px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -36,6 +36,33 @@ async function digestHex(blob) {
|
|||||||
return [...new Uint8Array(digest)].map((value) => value.toString(16).padStart(2, "0")).join("");
|
return [...new Uint8Array(digest)].map((value) => value.toString(16).padStart(2, "0")).join("");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
test("시작 화면과 Dataset Builder가 작업 우선순위 및 390px 레이아웃을 유지한다", async () => {
|
||||||
|
const landingFrame = document.createElement("iframe");
|
||||||
|
landingFrame.src = "/";
|
||||||
|
landingFrame.style.cssText = "position:fixed;left:-20000px;top:0;width:390px;height:900px;border:0";
|
||||||
|
document.body.append(landingFrame);
|
||||||
|
await new Promise((resolve, reject) => {
|
||||||
|
landingFrame.addEventListener("load", resolve, { once: true });
|
||||||
|
landingFrame.addEventListener("error", () => reject(new Error("Landing iframe load failed")), { once: true });
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
const landingDocument = landingFrame.contentDocument;
|
||||||
|
const launchers = [...landingDocument.querySelectorAll(".launcher-card")];
|
||||||
|
assertEqual(launchers.length, 2, "landing exposes only two primary tasks");
|
||||||
|
assert(launchers[0].textContent.includes("Acoustic Camera"), "analysis is the first landing task");
|
||||||
|
assert(launchers[1].textContent.includes("Dataset Builder"), "scene authoring is the second landing task");
|
||||||
|
assert(!landingDocument.querySelector(".workflow-disclosure").open, "file workflow starts collapsed");
|
||||||
|
assert(!landingDocument.querySelector('a[href="/docs/execution-plan.md"]'), "developer plan is absent from landing");
|
||||||
|
assert(
|
||||||
|
landingDocument.documentElement.scrollWidth <= landingDocument.documentElement.clientWidth + 1,
|
||||||
|
"landing has no horizontal overflow at 390px",
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
landingFrame.remove();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
test("Dataset Builder 전체 저장·재연결 흐름이 동일한 scene과 preview WAV를 재현한다", async () => {
|
test("Dataset Builder 전체 저장·재연결 흐름이 동일한 scene과 preview WAV를 재현한다", async () => {
|
||||||
const frame = document.createElement("iframe");
|
const frame = document.createElement("iframe");
|
||||||
frame.src = "/apps/dataset-builder/?integration-test=1";
|
frame.src = "/apps/dataset-builder/?integration-test=1";
|
||||||
@@ -61,6 +88,47 @@ test("Dataset Builder 전체 저장·재연결 흐름이 동일한 scene과 prev
|
|||||||
await waitFor(() => documentObject.documentElement.dataset.appReady === "true", "Builder 초기화");
|
await waitFor(() => documentObject.documentElement.dataset.appReady === "true", "Builder 초기화");
|
||||||
await waitFor(() => documentObject.documentElement.dataset.sceneValid === "true", "기본 scene 검증");
|
await waitFor(() => documentObject.documentElement.dataset.sceneValid === "true", "기본 scene 검증");
|
||||||
assert(documentObject.querySelector("#scene-id").value !== "single-tone", "new default scene gets a unique id");
|
assert(documentObject.querySelector("#scene-id").value !== "single-tone", "new default scene gets a unique id");
|
||||||
|
const workflowSteps = [...documentObject.querySelectorAll("#builder-workflow [data-workflow-step]")];
|
||||||
|
assertEqual(workflowSteps.length, 4, "four Builder workflow steps are discoverable");
|
||||||
|
assertEqual(
|
||||||
|
workflowSteps.map((step) => step.dataset.workflowStep).join(","),
|
||||||
|
"input,edit,validate,save",
|
||||||
|
"Builder workflow order",
|
||||||
|
);
|
||||||
|
assert(!documentObject.querySelector(".header-more").open, "secondary export actions start collapsed");
|
||||||
|
assert(!documentObject.querySelector(".inline-disclosure").open, "advanced scene settings start collapsed");
|
||||||
|
assert(
|
||||||
|
[...documentObject.querySelectorAll(".disclosure-section")].every((section) => !section.open),
|
||||||
|
"optional plane and ambient settings start collapsed",
|
||||||
|
);
|
||||||
|
assert(
|
||||||
|
documentObject.querySelector("#source-editor-section").compareDocumentPosition(
|
||||||
|
documentObject.querySelector("#scene-name").closest(".control-section"),
|
||||||
|
) & windowObject.Node.DOCUMENT_POSITION_FOLLOWING,
|
||||||
|
"selected source editor precedes scene metadata",
|
||||||
|
);
|
||||||
|
assert(
|
||||||
|
documentObject.querySelector("#scene-canvas").getBoundingClientRect().height >= 500,
|
||||||
|
"canvas remains the dominant Builder workspace",
|
||||||
|
);
|
||||||
|
|
||||||
|
frame.style.width = "390px";
|
||||||
|
await new Promise((resolve) => windowObject.requestAnimationFrame(() => windowObject.requestAnimationFrame(resolve)));
|
||||||
|
assert(
|
||||||
|
documentObject.documentElement.scrollWidth <= documentObject.documentElement.clientWidth + 1,
|
||||||
|
"Builder has no horizontal overflow at 390px",
|
||||||
|
);
|
||||||
|
const finalWorkflowStep = workflowSteps.at(-1).getBoundingClientRect();
|
||||||
|
assert(
|
||||||
|
finalWorkflowStep.right <= documentObject.documentElement.clientWidth + 1,
|
||||||
|
"all Builder workflow steps remain visible at 390px",
|
||||||
|
);
|
||||||
|
assert(
|
||||||
|
documentObject.querySelector("#scene-canvas").getBoundingClientRect().height >= 350,
|
||||||
|
"Builder canvas remains usable at 390px",
|
||||||
|
);
|
||||||
|
frame.style.width = "1400px";
|
||||||
|
await new Promise((resolve) => windowObject.requestAnimationFrame(() => windowObject.requestAnimationFrame(resolve)));
|
||||||
|
|
||||||
const previewSamples = new Float32Array(2000);
|
const previewSamples = new Float32Array(2000);
|
||||||
for (let index = 0; index < previewSamples.length; index += 1) {
|
for (let index = 0; index < previewSamples.length; index += 1) {
|
||||||
|
|||||||
Reference in New Issue
Block a user