Files
micarrsim/apps/array-builder/index.html
T

486 lines
27 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!doctype html>
<html lang="ko">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta
name="description"
content="마이크 위치를 직접 편집하고 격자·선형·원형·나선·랜덤 배열을 생성하는 micarrsim Array Builder"
/>
<title>Array Builder · micarrsim</title>
<link rel="stylesheet" href="/packages/ui/app-shell.css" />
<link rel="stylesheet" href="/apps/array-builder/styles.css" />
</head>
<body>
<a class="skip-link" href="#array-canvas">배열 편집 화면으로 건너뛰기</a>
<div class="app-shell array-builder-shell">
<header class="app-header array-builder-header">
<a class="app-brand" href="/">
<strong>micarrsim</strong>
<span>Array Builder</span>
</a>
<nav class="array-workflow" aria-label="마이크 어레이 제작 순서">
<ol>
<li><span>1</span>크기·형상</li>
<li><span>2</span>생성·편집</li>
<li><span>3</span>검증</li>
<li><span>4</span>저장</li>
</ol>
</nav>
<div class="header-actions">
<label class="button button--ghost file-button">
JSON 열기
<input id="array-input" type="file" accept="application/json,.json" />
</label>
<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>
<main class="array-builder-layout" id="array-builder-layout" data-split-layout>
<section class="array-workspace" id="array-builder-workspace" aria-label="마이크 배열 편집 화면">
<div class="workspace-heading">
<div>
<p class="eyebrow">마이크 배치</p>
<h1>Array 2D 편집 · 3D 검사</h1>
</div>
<p class="workspace-summary" id="workspace-summary">0.45 × 0.45 m · 12 channels</p>
</div>
<div class="workspace-toolbar" role="toolbar" aria-label="배열 편집 도구">
<div class="toolbar-group" aria-label="편집 도구">
<button class="tool-button is-active" id="select-tool-button" type="button" aria-pressed="true" aria-keyshortcuts="V">선택 <kbd>V</kbd></button>
<button class="tool-button" id="add-tool-button" type="button" aria-pressed="false" aria-keyshortcuts="A">추가 <kbd>A</kbd></button>
</div>
<div class="toolbar-group array-view-toggle" role="group" aria-label="배열 보기 방식">
<button class="tool-button is-active" id="array-view-2d-button" type="button" aria-pressed="true">2D 편집</button>
<button class="tool-button" id="array-view-3d-button" type="button" aria-pressed="false">3D 검사</button>
</div>
<label class="axis-view-control" id="array-axis-view-control" hidden>
<span>3D 시점</span>
<select id="array-axis-view" aria-label="배열 3D 시점, 선택한 축에서 원점 보기">
<option value="iso">등각</option>
<option value="+x">+X에서 보기</option>
<option value="-x">−X에서 보기</option>
<option value="+y">+Y에서 보기</option>
<option value="-y">−Y에서 보기</option>
<option value="+z">+Z FRONT에서 보기</option>
<option value="-z">−Z BACK에서 보기</option>
<option value="custom" disabled>자유 회전</option>
</select>
</label>
<div class="toolbar-group" aria-label="실행 취소와 다시 실행">
<button class="tool-button" id="undo-button" type="button" aria-keyshortcuts="Control+Z" disabled>실행 취소</button>
<button class="tool-button" id="redo-button" type="button" aria-keyshortcuts="Control+Shift+Z" disabled>다시 실행</button>
</div>
<div class="toolbar-group zoom-tools" aria-label="화면 확대와 맞춤">
<button class="icon-button" id="zoom-out-button" type="button" aria-label="축소" title="축소">−</button>
<button class="tool-button" id="fit-view-button" type="button">화면 맞춤</button>
<button class="icon-button" id="zoom-in-button" type="button" aria-label="확대" title="확대">+</button>
</div>
<div class="snap-controls">
<label class="check-field"><input id="snap-enabled" type="checkbox" checked /><span>격자 스냅</span></label>
<label class="compact-field"><span>간격</span><input id="snap-step" type="number" min="0.1" max="1000" step="0.1" value="5" inputmode="decimal" /><small>mm</small></label>
</div>
</div>
<div class="array-canvas-card">
<canvas
id="array-canvas"
tabindex="0"
aria-label="마이크 어레이 평면 편집 canvas"
aria-describedby="canvas-instructions"
></canvas>
<div class="canvas-instructions" id="canvas-instructions">
<strong>마이크를 클릭하거나 드래그해 위치를 편집하세요.</strong>
<span>추가 도구에서는 빈 공간을 클릭해 새 채널을 만듭니다.</span>
</div>
<div class="canvas-front-badge" id="canvas-front-badge">+Z FRONT · 화면 바깥쪽</div>
<div class="selection-status" id="selection-status" role="status" aria-live="polite">선택된 마이크 없음</div>
</div>
<section class="array-statistics" aria-label="배열 요약">
<article class="metric-card"><span>채널</span><strong id="microphone-count">12</strong><small>microphones</small></article>
<article class="metric-card"><span>최소 간격</span><strong id="minimum-spacing">—</strong><small>nearest pair</small></article>
<article class="metric-card"><span>무게중심</span><strong id="array-centroid">—</strong><small>x, y, z</small></article>
<article class="metric-card"><span>보정 채널</span><strong id="calibration-count">0</strong><small>sensitivity / phase / response</small></article>
</section>
<details class="beam-pattern-card" id="beam-pattern-panel" open>
<summary class="beam-pattern-heading">
<div>
<p class="eyebrow">배열 분석</p>
<h2>수신 빔 패턴</h2>
</div>
<span id="beam-pattern-summary">1 kHz · XZ/YZ/XY 동시</span>
</summary>
<div class="beam-pattern-body">
<div class="beam-pattern-controls">
<p class="beam-pattern-model">
Narrowband far-field conventional delay-and-sum
<span>geometry-only</span>
</p>
<div class="beam-pattern-view-switch" role="group" aria-label="빔 패턴 보기 방식">
<button class="tool-button is-active" id="beam-pattern-view-cuts-button" type="button" aria-pressed="true">세 평면</button>
<button class="tool-button" id="beam-pattern-view-3d-button" type="button" aria-pressed="false">3D 표면</button>
</div>
<div class="beam-pattern-fields">
<label class="field beam-frequency-field">
<span>아날로그 주파수 · 로그 슬라이더</span>
<div class="beam-frequency-control">
<input
id="beam-pattern-frequency"
type="range"
min="0"
max="6000"
step="1"
value="3000"
aria-valuemin="1"
aria-valuemax="1000000"
aria-valuenow="1000"
aria-valuetext="1.000 kHz"
/>
<output id="beam-pattern-frequency-value" for="beam-pattern-frequency">1.000 kHz</output>
</div>
<small>1 Hz–1 MHz · 이동 중 live preview</small>
</label>
<label class="field">
<span>음속 (m/s)</span>
<input
id="beam-pattern-speed-of-sound"
type="number"
min="250"
max="400"
step="0.1"
value="343"
inputmode="decimal"
/>
</label>
<label class="field" data-beam-mode="cuts">
<span>단면별 조향각 (deg)</span>
<input
id="beam-pattern-steering-angle"
type="number"
min="-360"
max="360"
step="0.5"
value="0"
inputmode="decimal"
/>
<small>각 XZ/YZ/XY 평면 안에서 독립 조향</small>
</label>
<label class="field" data-beam-mode="cuts">
<span>각도 샘플</span>
<select id="beam-pattern-samples">
<option value="auto">자동 · 위상 간격 기준</option>
<option value="360">360 · 1°</option>
<option value="720">720 · 0.5°</option>
<option value="1440">1,440 · 0.25°</option>
<option value="2880">2,880 · 0.125°</option>
<option value="5760">5,760 · 0.0625°</option>
</select>
</label>
<label class="field" data-beam-mode="surface" hidden>
<span>3D 조향 방위각 (deg)</span>
<input id="beam-pattern-steering-azimuth" type="number" min="-360" max="360" step="1" value="0" inputmode="decimal" />
<small>XY에서 +X→+Y</small>
</label>
<label class="field" data-beam-mode="surface" hidden>
<span>3D 조향 고도각 (deg)</span>
<input id="beam-pattern-steering-elevation" type="number" min="-90" max="90" step="1" value="90" inputmode="decimal" />
<small>+90°가 +Z FRONT</small>
</label>
<label class="field" data-beam-mode="surface" hidden>
<span>3D 구면 표본</span>
<select id="beam-pattern-surface-resolution">
<option value="5">5° · 72×37</option>
<option value="3" selected>3° · 120×61</option>
<option value="2.5">2.5° · 144×73</option>
</select>
</label>
<label class="field" data-beam-mode="surface" hidden>
<span>3D 표시 시점</span>
<select id="beam-pattern-axis-view" aria-label="빔 패턴 3D 시점, 선택한 축에서 원점 보기">
<option value="iso">등각</option>
<option value="+x">+X에서 보기</option>
<option value="-x">−X에서 보기</option>
<option value="+y">+Y에서 보기</option>
<option value="-y">−Y에서 보기</option>
<option value="+z">+Z FRONT에서 보기</option>
<option value="-z">−Z BACK에서 보기</option>
<option value="custom" disabled>자유 회전</option>
</select>
<small>카메라만 변경 · 조향 방향과 응답 계산은 유지</small>
</label>
<label class="field">
<span>표시 하한 (dB)</span>
<select id="beam-pattern-floor">
<option value="-30">−30 dB</option>
<option value="-40">−40 dB</option>
<option value="-60" selected>−60 dB</option>
<option value="-80">−80 dB</option>
<option value="-100">−100 dB</option>
</select>
</label>
</div>
<div class="beam-pattern-metrics" aria-label="빔 패턴 수치">
<article><span>파장</span><strong id="beam-pattern-wavelength">—</strong></article>
<article><span>각도 해상도</span><strong id="beam-pattern-resolution">—</strong></article>
<article><span>조향 응답</span><strong id="beam-pattern-steering-response">—</strong></article>
<article><span>−3.0103 dB 폭</span><strong id="beam-pattern-hpbw">—</strong></article>
</div>
<p class="beam-pattern-status" id="beam-pattern-status" role="status" aria-live="polite">
현재 배열의 수신 array factor를 계산하고 있습니다.
</p>
<p class="beam-pattern-note" id="beam-pattern-model-note">
이상적으로 보정된 무지향성 점 마이크의 자유장 평면파 응답입니다. 세 평면은 각각
독립 조향한 great-circle 단면이며 3D 모드는 실제 구면 방향을 계산합니다. 근거리,
방·하우징, 마이크 주파수 응답은 포함하지 않습니다. ADC가 없으므로 Nyquist 초과
주파수도 접지 않고 원래 파장으로 계산합니다.
</p>
</div>
<div class="beam-pattern-visuals">
<div class="beam-pattern-cut-grid" id="beam-pattern-cuts-view">
<figure class="beam-pattern-figure" data-cut="xz">
<div class="beam-pattern-figure-heading"><strong>XZ</strong><span>0° +Z · 90° +X</span></div>
<svg id="beam-pattern-plot-xz" viewBox="0 0 520 520" role="img" aria-label="XZ 수신 빔 패턴 polar plot" aria-describedby="beam-pattern-status beam-pattern-model-note beam-pattern-caption-xz"></svg>
<figcaption id="beam-pattern-caption-xz">계산 중…</figcaption>
</figure>
<figure class="beam-pattern-figure" data-cut="yz">
<div class="beam-pattern-figure-heading"><strong>YZ</strong><span>0° +Z · 90° +Y</span></div>
<svg id="beam-pattern-plot-yz" viewBox="0 0 520 520" role="img" aria-label="YZ 수신 빔 패턴 polar plot" aria-describedby="beam-pattern-status beam-pattern-model-note beam-pattern-caption-yz"></svg>
<figcaption id="beam-pattern-caption-yz">계산 중…</figcaption>
</figure>
<figure class="beam-pattern-figure" data-cut="xy">
<div class="beam-pattern-figure-heading"><strong>XY</strong><span>0° +X · 90° +Y</span></div>
<svg id="beam-pattern-plot-xy" viewBox="0 0 520 520" role="img" aria-label="XY 수신 빔 패턴 polar plot" aria-describedby="beam-pattern-status beam-pattern-model-note beam-pattern-caption-xy"></svg>
<figcaption id="beam-pattern-caption-xy">계산 중…</figcaption>
</figure>
</div>
<figure class="beam-pattern-surface-figure" id="beam-pattern-surface-view" hidden>
<div class="beam-pattern-figure-heading"><strong>3D 구면 응답</strong><span>축 시점 선택 또는 drag 회전 · +Z FRONT</span></div>
<canvas id="beam-pattern-surface" tabindex="0" role="img" aria-label="3D 수신 빔 패턴 표면" aria-describedby="beam-pattern-status beam-pattern-model-note beam-pattern-surface-caption"></canvas>
<figcaption id="beam-pattern-surface-caption">3D 표면을 준비하고 있습니다.</figcaption>
</figure>
<div class="beam-pattern-diagnostics" id="beam-pattern-peaks" aria-label="평면별 sampled peak 진단">
<p data-cut="xz"><strong>XZ</strong><span>계산 중…</span></p>
<p data-cut="yz"><strong>YZ</strong><span>계산 중…</span></p>
<p data-cut="xy"><strong>XY</strong><span>계산 중…</span></p>
</div>
</div>
</div>
</details>
<details class="shortcut-help">
<summary>빠른 편집 키</summary>
<div>
<p><kbd>V</kbd> 선택 · <kbd>A</kbd> 추가 · <kbd>Delete</kbd> 선택 삭제</p>
<p><kbd>← ↑ ↓ →</kbd> 위치 미세 조정 · <kbd>Shift</kbd>를 함께 누르면 10배 이동</p>
</div>
</details>
</section>
<aside
class="array-controls"
id="array-builder-controls"
data-split-panel
aria-label="마이크 어레이 설정"
>
<div class="runtime-line">
<span class="status-chip status-chip--neutral" data-runtime-status>런타임 확인 중…</span>
<span class="save-state" id="save-state" role="status">기본 배열</span>
</div>
<section class="validation-card" aria-live="polite" aria-atomic="true">
<span class="validation-dot" id="validation-dot" aria-hidden="true"></span>
<div>
<strong id="validation-title">검증 대기</strong>
<p id="validation-message">마이크 배열을 준비하고 있습니다.</p>
</div>
</section>
<section class="control-section array-metadata-section">
<div class="section-title"><span>01</span><h2>배열 정보</h2></div>
<label class="field field--wide">
<span>이름</span>
<input id="array-name" type="text" maxlength="120" autocomplete="off" />
</label>
<label class="field field--wide">
<span>Array ID</span>
<input
id="array-id"
type="text"
maxlength="120"
pattern="[a-z0-9][a-z0-9._-]*"
title="영문 소문자나 숫자로 시작하고 소문자, 숫자, 점, 밑줄, 하이픈만 사용할 수 있습니다."
autocomplete="off"
/>
</label>
<label class="field field--wide">
<span>설명 <small>선택 사항</small></span>
<textarea id="array-description" rows="2" maxlength="1000"></textarea>
</label>
</section>
<section class="control-section control-section--primary aperture-section">
<div class="section-title"><span>02</span><h2>크기와 형상</h2></div>
<div class="field-grid">
<label class="field">
<span>너비 (m)</span>
<input id="aperture-width" type="number" min="0.001" max="20" step="0.01" value="0.45" inputmode="decimal" />
</label>
<label class="field">
<span>높이 (m)</span>
<input id="aperture-height" type="number" min="0.001" max="20" step="0.01" value="0.45" inputmode="decimal" />
</label>
</div>
<label class="field field--wide">
<span>생성 경계</span>
<select id="aperture-shape">
<option value="rectangle">사각형</option>
<option value="ellipse">타원</option>
</select>
</label>
<p class="section-note">크기를 바꿔도 기존 좌표는 자동으로 늘어나거나 줄어들지 않습니다. 생성 경계 형상은 편집용이며 JSON에는 최종 좌표와 너비·높이만 저장됩니다.</p>
<div class="button-grid button-grid--two">
<button class="button button--ghost" id="center-microphones-button" type="button">좌표 중앙 정렬</button>
<button class="button button--ghost" id="fit-aperture-button" type="button">좌표에 크기 맞춤</button>
<button class="button button--ghost button-grid__wide" id="clamp-microphones-button" type="button">경계 밖 마이크를 안으로 이동</button>
</div>
</section>
<section class="control-section preset-section">
<div class="section-title"><span>03</span><h2>프리셋 생성</h2></div>
<label class="field field--wide">
<span>배치 방식</span>
<select id="preset-type">
<option value="grid">격자</option>
<option value="linear">선형</option>
<option value="ring">원형 링</option>
<option value="archimedean">나선 · Archimedean</option>
<option value="spiral">Fermat 나선 · 해바라기</option>
<option value="random">균일 랜덤</option>
</select>
</label>
<div class="preset-grid">
<label class="field" id="preset-count-field">
<span>마이크 수</span>
<input id="preset-count" type="number" min="1" max="1024" step="1" value="16" inputmode="numeric" />
</label>
<label class="field" id="preset-axis-field">
<span>선형 축</span>
<select id="preset-axis">
<option value="x">가로 (x)</option>
<option value="y">세로 (y)</option>
</select>
</label>
<label class="field" id="preset-turns-field">
<span>나선 회전 수</span>
<input id="preset-turns" type="number" min="0.25" max="32" step="0.25" value="2.5" inputmode="decimal" />
</label>
<label class="field" id="preset-columns-field">
<span>열</span>
<input id="preset-columns" type="number" min="1" max="1024" step="1" value="4" inputmode="numeric" />
</label>
<label class="field" id="preset-rows-field">
<span>행</span>
<input id="preset-rows" type="number" min="1" max="1024" step="1" value="3" inputmode="numeric" />
</label>
<label class="field" id="preset-seed-field">
<span>Random seed</span>
<input id="preset-seed" type="number" min="0" max="4294967295" step="1" value="1" inputmode="numeric" />
</label>
<label class="field" id="preset-minimum-spacing-field">
<span>최소 간격 (m)</span>
<input id="preset-minimum-spacing" type="number" min="0" max="20" step="0.001" value="0.015" inputmode="decimal" />
</label>
<label class="field preset-grid__wide" id="preset-edge-inset-field">
<span>경계 여백 (m)</span>
<input id="preset-edge-inset" type="number" min="0" max="10" step="0.001" value="0.01" inputmode="decimal" />
</label>
</div>
<p class="section-note" id="preset-hint">격자는 아래에서 위로, 각 행은 왼쪽에서 오른쪽 순서로 채널을 만듭니다.</p>
<button class="button button--primary button--full" id="generate-preset-button" type="button">현재 배열을 프리셋으로 교체</button>
</section>
<section class="control-section microphone-section">
<div class="section-title section-title--split">
<div><span>04</span><h2>마이크 편집</h2></div>
<button class="mini-button" id="add-microphone-button" type="button">+ 마이크</button>
</div>
<label class="field field--wide search-field">
<span>목록 검색</span>
<input id="microphone-search" type="search" placeholder="ID로 찾기" autocomplete="off" />
</label>
<div class="microphone-list" id="microphone-list" role="listbox" aria-label="마이크 채널 목록">
<p class="list-empty">마이크 목록을 준비하고 있습니다.</p>
</div>
<div class="selected-microphone-editor">
<p class="selection-empty" id="microphone-selection-empty">canvas나 목록에서 마이크를 선택하세요.</p>
<div id="microphone-fields">
<label class="field field--wide">
<span>마이크 ID</span>
<input id="microphone-id" type="text" maxlength="64" autocomplete="off" disabled />
</label>
<div class="field-grid field-grid--three">
<label class="field"><span>x (m)</span><input id="microphone-x" type="number" step="0.001" inputmode="decimal" disabled /></label>
<label class="field"><span>y (m)</span><input id="microphone-y" type="number" step="0.001" inputmode="decimal" disabled /></label>
<label class="field"><span>z (m)</span><input id="microphone-z" type="number" step="0.001" inputmode="decimal" disabled /></label>
</div>
<details class="inline-details">
<summary>보정값</summary>
<div class="field-grid calibration-fields">
<label class="field"><span>Sensitivity (dB)</span><input id="microphone-sensitivity" type="number" min="-120" max="120" step="0.1" inputmode="decimal" disabled /></label>
<label class="field"><span>Phase (deg)</span><input id="microphone-phase" type="number" min="-180" max="180" step="0.1" inputmode="decimal" disabled /></label>
</div>
<p class="section-note" id="frequency-response-summary">주파수 응답 없음</p>
</details>
<div class="button-grid reorder-actions">
<button class="button button--ghost" id="move-microphone-up-button" type="button" disabled>채널 앞으로</button>
<button class="button button--ghost" id="move-microphone-down-button" type="button" disabled>채널 뒤로</button>
<button class="button button--danger button-grid__wide" id="remove-microphone-button" type="button" disabled>선택 마이크 삭제</button>
</div>
</div>
</div>
</section>
<div class="mobile-save-actions">
<button class="button button--primary button--full" id="mobile-export-button" type="button">Array JSON 저장</button>
</div>
</aside>
<div
class="split-handle"
id="array-builder-split-handle"
role="separator"
tabindex="0"
aria-label="배열 설정 패널 너비 조절"
aria-controls="array-builder-controls"
aria-orientation="vertical"
data-split-resizer
data-storage-key="micarrsim.array-builder.control-panel-width"
title="드래그하거나 방향키로 너비 조절 · Enter/더블클릭으로 초기화"
></div>
</main>
</div>
<script type="module" src="/packages/ui/runtime-status.js"></script>
<script type="module" src="/apps/array-builder/main.js"></script>
</body>
</html>