feat: visualize array receive beam patterns

This commit is contained in:
2026-08-29 14:45:47 +09:00
parent 041e0c3665
commit b68cd87e3c
5 changed files with 1044 additions and 1 deletions
+118 -1
View File
@@ -98,6 +98,95 @@ test("Array Builder가 preset·직접 편집·검증·표준 JSON round trip을
() => documentObject.querySelector("#array-canvas").width > 0,
"Array canvas 렌더",
);
await waitFor(
() => documentObject.documentElement.dataset.beamPatternReady === "true",
"기본 수신 빔 패턴 렌더",
);
const beamPlot = documentObject.querySelector("#beam-pattern-plot");
const initialBeamPath = beamPlot.querySelector(".beam-pattern-response")
?.getAttribute("d");
assert(initialBeamPath?.startsWith("M"), "polar response path is rendered");
assertEqual(
beamPlot.querySelectorAll(".beam-pattern-ring").length,
5,
"polar plot has four inner dB rings and the 0 dB ring",
);
assertEqual(beamPlot.dataset.cut, "xz", "default great-circle cut");
assertEqual(beamPlot.dataset.frequencyHz, "1000", "default analog frequency");
assert(
Number(beamPlot.dataset.sampleCount) >= 720,
"auto angular sampling uses at least 720 directions",
);
assert(
documentObject.querySelector("#beam-pattern-model-note").textContent.includes(
"Nyquist 초과",
),
"UI explains that the geometry-only pattern does not fold analog frequency",
);
const initialSaveState = documentObject.querySelector("#save-state").textContent;
dispatchValue(
windowObject,
documentObject.querySelector("#beam-pattern-frequency"),
49_125,
"input",
);
await waitFor(
() => beamPlot.dataset.frequencyHz === "49125" &&
Number(beamPlot.dataset.sampleCount) > 720,
"above-Nyquist-style analog pattern with auto angular sampling",
);
assert(
documentObject.querySelector("#beam-pattern-wavelength").textContent.includes("mm"),
"high analog frequency uses its short physical wavelength",
);
dispatchValue(
windowObject,
documentObject.querySelector("#beam-pattern-samples"),
360,
);
await waitFor(
() => beamPlot.dataset.sampleCount === "360" &&
documentObject.querySelector("#beam-pattern-hpbw").textContent.includes(
"해상도 부족",
),
"coarse angular grid exposes unresolved metrics",
);
dispatchValue(
windowObject,
documentObject.querySelector("#beam-pattern-frequency"),
"",
"input",
);
await waitFor(
() => documentObject.documentElement.dataset.beamPatternReady === "error",
"invalid beam preview setting",
);
assert(
!documentObject.querySelector("#export-button").disabled,
"invalid preview never blocks valid Array export",
);
dispatchValue(
windowObject,
documentObject.querySelector("#beam-pattern-samples"),
"auto",
);
dispatchValue(
windowObject,
documentObject.querySelector("#beam-pattern-frequency"),
1_000,
"input",
);
await waitFor(
() => documentObject.documentElement.dataset.beamPatternReady === "true" &&
beamPlot.dataset.frequencyHz === "1000",
"beam preview recovery",
);
assertEqual(
documentObject.querySelector("#save-state").textContent,
initialSaveState,
"beam preview settings are outside Array dirty state",
);
assertEqual(microphoneCount(documentObject), 12, "default 4x3 channels");
assertEqual(
documentObject.querySelector("#microphone-list [data-microphone-id]").dataset.microphoneId,
@@ -143,7 +232,12 @@ test("Array Builder가 preset·직접 편집·검증·표준 JSON round trip을
dispatchDragPointer("pointerdown", 0, 0, 1);
dispatchDragPointer("pointermove", 36, -24, 1);
dispatchDragPointer("pointerup", 36, -24, 0);
await nextPaint(windowObject);
await waitFor(
() => documentObject.documentElement.dataset.beamPatternPreview === "false" &&
beamPlot.querySelector(".beam-pattern-response")?.getAttribute("d") !==
initialBeamPath,
"beam pattern follows edited microphone geometry",
);
const draggedPosition = selectedCoordinates(documentObject);
assert(
draggedPosition[0] !== initialFirstPosition[0] &&
@@ -444,6 +538,7 @@ test("Array Builder가 preset·직접 편집·검증·표준 JSON round trip을
assert(!Object.hasOwn(exported, "shape"), "editor shape is not exported");
assert(!Object.hasOwn(exported, "preset"), "editor preset is not exported");
assert(!Object.hasOwn(exported, "seed"), "editor seed is not exported");
assert(!Object.hasOwn(exported, "beamPattern"), "beam preview is not exported");
const calibrated = exported.microphones.find(
(microphone) => microphone.id === "reference-channel",
);
@@ -694,6 +789,28 @@ test("Array Builder가 preset·직접 편집·검증·표준 JSON round trip을
documentObject.documentElement.clientWidth + 1,
"Array Builder has no horizontal overflow at 390px",
);
const compactBeamPlotRect = documentObject.querySelector("#beam-pattern-plot")
.getBoundingClientRect();
const compactBeamCardRect = documentObject.querySelector("#beam-pattern-panel")
.getBoundingClientRect();
assert(
compactBeamPlotRect.left >= compactBeamCardRect.left - 1 &&
compactBeamPlotRect.right <= compactBeamCardRect.right + 1,
"compact polar plot stays inside its analysis card",
);
assert(
Math.abs(compactBeamPlotRect.width - compactBeamPlotRect.height) <= 2,
"compact polar plot remains square",
);
const compactBeamFields = [
...documentObject.querySelectorAll(".beam-pattern-fields > .field"),
].map((field) => field.getBoundingClientRect());
assert(
compactBeamFields.every(
(rect, index) => index === 0 || rect.top >= compactBeamFields[index - 1].bottom - 1,
),
"compact beam controls stack without clipping",
);
assert(
documentObject.querySelector("#array-canvas").getBoundingClientRect().height >= 350,
"Array canvas remains usable at 390px",