feat: add live multi-plane and 3d array views
This commit is contained in:
@@ -32,6 +32,10 @@ function dispatchValue(windowObject, element, value, eventName = "change") {
|
||||
element.dispatchEvent(new windowObject.Event(eventName, { bubbles: true }));
|
||||
}
|
||||
|
||||
function logarithmicFrequencyPosition(frequencyHz) {
|
||||
return Math.round(Math.log10(frequencyHz) * 1000);
|
||||
}
|
||||
|
||||
function dispatchFiles(windowObject, input, files) {
|
||||
const transfer = new windowObject.DataTransfer();
|
||||
for (const file of files) transfer.items.add(file);
|
||||
@@ -102,20 +106,46 @@ test("Array Builder가 preset·직접 편집·검증·표준 JSON round trip을
|
||||
() => 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");
|
||||
const beamCuts = ["xz", "yz", "xy"];
|
||||
const beamPlots = Object.fromEntries(beamCuts.map((cut) => [
|
||||
cut,
|
||||
documentObject.querySelector("#beam-pattern-plot-" + cut),
|
||||
]));
|
||||
const initialBeamPaths = Object.fromEntries(beamCuts.map((cut) => [
|
||||
cut,
|
||||
beamPlots[cut].querySelector(".beam-pattern-response")?.getAttribute("d"),
|
||||
]));
|
||||
for (const cut of beamCuts) {
|
||||
const beamPlot = beamPlots[cut];
|
||||
assert(
|
||||
initialBeamPaths[cut]?.startsWith("M"),
|
||||
cut.toUpperCase() + " polar response path is rendered",
|
||||
);
|
||||
assertEqual(
|
||||
beamPlot.querySelectorAll(".beam-pattern-ring").length,
|
||||
5,
|
||||
cut.toUpperCase() + " polar plot has four inner dB rings and the 0 dB ring",
|
||||
);
|
||||
assertEqual(beamPlot.dataset.cut, cut, cut.toUpperCase() + " cut provenance");
|
||||
assertEqual(
|
||||
beamPlot.dataset.frequencyHz,
|
||||
"1000",
|
||||
cut.toUpperCase() + " default analog frequency",
|
||||
);
|
||||
assert(
|
||||
Number(beamPlot.dataset.sampleCount) >= 720,
|
||||
cut.toUpperCase() + " auto angular sampling uses at least 720 directions",
|
||||
);
|
||||
}
|
||||
assert(
|
||||
Number(beamPlot.dataset.sampleCount) >= 720,
|
||||
"auto angular sampling uses at least 720 directions",
|
||||
!documentObject.querySelector("#beam-pattern-cuts-view").hidden &&
|
||||
documentObject.querySelector("#beam-pattern-surface-view").hidden,
|
||||
"three polar cuts are the simultaneous default view",
|
||||
);
|
||||
assertEqual(
|
||||
documentObject.querySelectorAll("#beam-pattern-cuts-view .beam-pattern-figure").length,
|
||||
3,
|
||||
"XZ, YZ, and XY figures are present together",
|
||||
);
|
||||
assert(
|
||||
documentObject.querySelector("#beam-pattern-model-note").textContent.includes(
|
||||
@@ -125,16 +155,34 @@ test("Array Builder가 preset·직접 편집·검증·표준 JSON round trip을
|
||||
);
|
||||
|
||||
const initialSaveState = documentObject.querySelector("#save-state").textContent;
|
||||
const frequencySlider = documentObject.querySelector("#beam-pattern-frequency");
|
||||
dispatchValue(
|
||||
windowObject,
|
||||
documentObject.querySelector("#beam-pattern-frequency"),
|
||||
49_125,
|
||||
frequencySlider,
|
||||
logarithmicFrequencyPosition(49_125),
|
||||
"input",
|
||||
);
|
||||
await waitFor(
|
||||
() => beamPlot.dataset.frequencyHz === "49125" &&
|
||||
Number(beamPlot.dataset.sampleCount) > 720,
|
||||
"above-Nyquist-style analog pattern with auto angular sampling",
|
||||
() => documentObject.documentElement.dataset.beamPatternPreview === "true" &&
|
||||
beamCuts.every((cut) =>
|
||||
Number(beamPlots[cut].dataset.frequencyHz) > 48_000 &&
|
||||
beamPlots[cut].dataset.sampleCount === "720"),
|
||||
"logarithmic slider input renders a bounded live preview in every cut",
|
||||
);
|
||||
const previewFrequencyHz = Number(frequencySlider.getAttribute("aria-valuenow"));
|
||||
assert(
|
||||
previewFrequencyHz > 48_000 && previewFrequencyHz < 50_000,
|
||||
"log slider position maps to the intended above-Nyquist-style analog frequency",
|
||||
);
|
||||
assert(
|
||||
documentObject.querySelector("#beam-pattern-frequency-value").textContent.includes("kHz"),
|
||||
"log slider exposes its physical frequency value",
|
||||
);
|
||||
frequencySlider.dispatchEvent(new windowObject.Event("change", { bubbles: true }));
|
||||
await waitFor(
|
||||
() => documentObject.documentElement.dataset.beamPatternPreview === "false" &&
|
||||
beamCuts.every((cut) => Number(beamPlots[cut].dataset.sampleCount) > 720),
|
||||
"logarithmic slider change renders full auto resolution in every cut",
|
||||
);
|
||||
assert(
|
||||
documentObject.querySelector("#beam-pattern-wavelength").textContent.includes("mm"),
|
||||
@@ -146,15 +194,15 @@ test("Array Builder가 preset·직접 편집·검증·표준 JSON round trip을
|
||||
360,
|
||||
);
|
||||
await waitFor(
|
||||
() => beamPlot.dataset.sampleCount === "360" &&
|
||||
() => beamCuts.every((cut) => beamPlots[cut].dataset.sampleCount === "360") &&
|
||||
documentObject.querySelector("#beam-pattern-hpbw").textContent.includes(
|
||||
"해상도 부족",
|
||||
),
|
||||
"coarse angular grid exposes unresolved metrics",
|
||||
"coarse angular grid exposes unresolved metrics for the simultaneous cuts",
|
||||
);
|
||||
dispatchValue(
|
||||
windowObject,
|
||||
documentObject.querySelector("#beam-pattern-frequency"),
|
||||
documentObject.querySelector("#beam-pattern-speed-of-sound"),
|
||||
"",
|
||||
"input",
|
||||
);
|
||||
@@ -168,18 +216,28 @@ test("Array Builder가 preset·직접 편집·검증·표준 JSON round trip을
|
||||
);
|
||||
dispatchValue(
|
||||
windowObject,
|
||||
documentObject.querySelector("#beam-pattern-samples"),
|
||||
"auto",
|
||||
documentObject.querySelector("#beam-pattern-speed-of-sound"),
|
||||
343,
|
||||
"input",
|
||||
);
|
||||
dispatchValue(
|
||||
windowObject,
|
||||
documentObject.querySelector("#beam-pattern-frequency"),
|
||||
1_000,
|
||||
frequencySlider,
|
||||
logarithmicFrequencyPosition(1_000),
|
||||
"input",
|
||||
);
|
||||
frequencySlider.dispatchEvent(new windowObject.Event("change", { bubbles: true }));
|
||||
dispatchValue(
|
||||
windowObject,
|
||||
documentObject.querySelector("#beam-pattern-samples"),
|
||||
"auto",
|
||||
);
|
||||
await waitFor(
|
||||
() => documentObject.documentElement.dataset.beamPatternReady === "true" &&
|
||||
beamPlot.dataset.frequencyHz === "1000",
|
||||
documentObject.documentElement.dataset.beamPatternPreview === "false" &&
|
||||
beamCuts.every((cut) =>
|
||||
beamPlots[cut].dataset.frequencyHz === "1000" &&
|
||||
Number(beamPlots[cut].dataset.sampleCount) >= 720),
|
||||
"beam preview recovery",
|
||||
);
|
||||
assertEqual(
|
||||
@@ -187,6 +245,87 @@ test("Array Builder가 preset·직접 편집·검증·표준 JSON round trip을
|
||||
initialSaveState,
|
||||
"beam preview settings are outside Array dirty state",
|
||||
);
|
||||
|
||||
documentObject.querySelector("#beam-pattern-view-3d-button").click();
|
||||
const beamSurface = documentObject.querySelector("#beam-pattern-surface");
|
||||
await waitFor(
|
||||
() => documentObject.documentElement.dataset.beamPatternView === "surface" &&
|
||||
documentObject.documentElement.dataset.beamPatternReady === "true" &&
|
||||
documentObject.documentElement.dataset.beamPatternPreview === "false" &&
|
||||
!documentObject.querySelector("#beam-pattern-surface-view").hidden &&
|
||||
documentObject.querySelector("#beam-pattern-cuts-view").hidden &&
|
||||
Number(beamSurface.dataset.sampleCount) > 0,
|
||||
"3D beam surface render",
|
||||
);
|
||||
assert(
|
||||
/^\d+x\d+$/.test(beamSurface.dataset.gridShape),
|
||||
"3D beam canvas records its raw sphere grid",
|
||||
);
|
||||
assertEqual(
|
||||
documentObject.querySelector("#beam-pattern-view-3d-button").getAttribute("aria-pressed"),
|
||||
"true",
|
||||
"3D beam view toggle is pressed",
|
||||
);
|
||||
assert(
|
||||
documentObject.querySelector("#beam-pattern-surface-caption").textContent.includes(
|
||||
"raw grid samples",
|
||||
),
|
||||
"3D beam caption identifies raw sampled directions",
|
||||
);
|
||||
assert(
|
||||
documentObject.querySelector("#beam-pattern-surface-caption").textContent.includes(
|
||||
"dB-floor radial mapping",
|
||||
),
|
||||
"3D beam caption discloses the display-only radial mapping",
|
||||
);
|
||||
await nextPaint(windowObject);
|
||||
const surfaceBeforeOrbit = beamSurface.toDataURL();
|
||||
const beamSurfaceRect = beamSurface.getBoundingClientRect();
|
||||
const beamOrbitPointerId = 71;
|
||||
beamSurface.dispatchEvent(new windowObject.PointerEvent("pointerdown", {
|
||||
bubbles: true,
|
||||
cancelable: true,
|
||||
pointerId: beamOrbitPointerId,
|
||||
pointerType: "mouse",
|
||||
button: 0,
|
||||
buttons: 1,
|
||||
clientX: beamSurfaceRect.left + beamSurfaceRect.width * 0.5,
|
||||
clientY: beamSurfaceRect.top + beamSurfaceRect.height * 0.5,
|
||||
}));
|
||||
assertEqual(beamSurface.dataset.orbiting, "true", "3D beam drag begins orbiting");
|
||||
beamSurface.dispatchEvent(new windowObject.PointerEvent("pointermove", {
|
||||
bubbles: true,
|
||||
cancelable: true,
|
||||
pointerId: beamOrbitPointerId,
|
||||
pointerType: "mouse",
|
||||
button: -1,
|
||||
buttons: 1,
|
||||
clientX: beamSurfaceRect.left + beamSurfaceRect.width * 0.7,
|
||||
clientY: beamSurfaceRect.top + beamSurfaceRect.height * 0.36,
|
||||
}));
|
||||
beamSurface.dispatchEvent(new windowObject.PointerEvent("pointerup", {
|
||||
bubbles: true,
|
||||
cancelable: true,
|
||||
pointerId: beamOrbitPointerId,
|
||||
pointerType: "mouse",
|
||||
button: 0,
|
||||
buttons: 0,
|
||||
}));
|
||||
await nextPaint(windowObject);
|
||||
assert(!beamSurface.dataset.orbiting, "3D beam drag finishes orbiting");
|
||||
assert(
|
||||
beamSurface.toDataURL() !== surfaceBeforeOrbit,
|
||||
"3D beam surface bitmap changes after orbit rotation",
|
||||
);
|
||||
documentObject.querySelector("#beam-pattern-view-cuts-button").click();
|
||||
await waitFor(
|
||||
() => documentObject.documentElement.dataset.beamPatternView === "cuts" &&
|
||||
documentObject.documentElement.dataset.beamPatternReady === "true" &&
|
||||
!documentObject.querySelector("#beam-pattern-cuts-view").hidden &&
|
||||
documentObject.querySelector("#beam-pattern-surface-view").hidden,
|
||||
"return to simultaneous polar cuts",
|
||||
);
|
||||
|
||||
assertEqual(microphoneCount(documentObject), 12, "default 4x3 channels");
|
||||
assertEqual(
|
||||
documentObject.querySelector("#microphone-list [data-microphone-id]").dataset.microphoneId,
|
||||
@@ -203,6 +342,116 @@ test("Array Builder가 preset·직접 편집·검증·표준 JSON round trip을
|
||||
);
|
||||
|
||||
const canvas = documentObject.querySelector("#array-canvas");
|
||||
assertEqual(canvas.dataset.viewMode, "2d", "array canvas starts in editable 2D mode");
|
||||
assert(
|
||||
documentObject.querySelector("#canvas-front-badge").textContent.includes(
|
||||
"+Z FRONT · 화면 바깥쪽",
|
||||
),
|
||||
"2D canvas states the +Z front direction",
|
||||
);
|
||||
const coordinatesBeforeArray3d = selectedCoordinates(documentObject);
|
||||
const microphoneCountBeforeArray3d = microphoneCount(documentObject);
|
||||
documentObject.querySelector("#array-view-3d-button").click();
|
||||
await waitFor(
|
||||
() => canvas.dataset.viewMode === "3d" &&
|
||||
canvas.getAttribute("aria-label").includes("3D 검사"),
|
||||
"array 3D inspection mode",
|
||||
);
|
||||
assert(
|
||||
documentObject.querySelector("#add-tool-button").disabled &&
|
||||
documentObject.querySelector("#select-tool-button").disabled,
|
||||
"array canvas edit tools are disabled in 3D inspection",
|
||||
);
|
||||
assert(
|
||||
documentObject.querySelector("#canvas-front-badge").textContent.includes(
|
||||
"+Z FRONT · 초록 축",
|
||||
),
|
||||
"3D canvas identifies the +Z front axis",
|
||||
);
|
||||
assert(
|
||||
documentObject.querySelector("#canvas-instructions").textContent.includes("검사 전용"),
|
||||
"3D array guidance explains that coordinate editing is disabled",
|
||||
);
|
||||
canvas.dispatchEvent(new windowObject.KeyboardEvent("keydown", {
|
||||
bubbles: true,
|
||||
cancelable: true,
|
||||
key: "a",
|
||||
}));
|
||||
await nextPaint(windowObject);
|
||||
assertEqual(canvas.dataset.tool, "select", "A shortcut cannot enable add in 3D inspection");
|
||||
assertEqual(
|
||||
microphoneCount(documentObject),
|
||||
microphoneCountBeforeArray3d,
|
||||
"3D inspection cannot add a microphone",
|
||||
);
|
||||
assertEqual(
|
||||
selectedCoordinates(documentObject).join(","),
|
||||
coordinatesBeforeArray3d.join(","),
|
||||
"3D inspection leaves microphone coordinates unchanged",
|
||||
);
|
||||
const array3dBeforeOrbit = canvas.toDataURL();
|
||||
const array3dRect = canvas.getBoundingClientRect();
|
||||
const arrayOrbitPointerId = 72;
|
||||
canvas.dispatchEvent(new windowObject.PointerEvent("pointerdown", {
|
||||
bubbles: true,
|
||||
cancelable: true,
|
||||
pointerId: arrayOrbitPointerId,
|
||||
pointerType: "mouse",
|
||||
button: 0,
|
||||
buttons: 1,
|
||||
clientX: array3dRect.left + array3dRect.width * 0.5,
|
||||
clientY: array3dRect.top + array3dRect.height * 0.5,
|
||||
}));
|
||||
assertEqual(canvas.dataset.orbiting, "true", "array 3D drag begins orbiting");
|
||||
canvas.dispatchEvent(new windowObject.PointerEvent("pointermove", {
|
||||
bubbles: true,
|
||||
cancelable: true,
|
||||
pointerId: arrayOrbitPointerId,
|
||||
pointerType: "mouse",
|
||||
button: -1,
|
||||
buttons: 1,
|
||||
clientX: array3dRect.left + array3dRect.width * 0.68,
|
||||
clientY: array3dRect.top + array3dRect.height * 0.4,
|
||||
}));
|
||||
canvas.dispatchEvent(new windowObject.PointerEvent("pointerup", {
|
||||
bubbles: true,
|
||||
cancelable: true,
|
||||
pointerId: arrayOrbitPointerId,
|
||||
pointerType: "mouse",
|
||||
button: 0,
|
||||
buttons: 0,
|
||||
}));
|
||||
await nextPaint(windowObject);
|
||||
assertEqual(canvas.dataset.orbiting, "false", "array 3D drag finishes orbiting");
|
||||
assert(
|
||||
canvas.toDataURL() !== array3dBeforeOrbit,
|
||||
"array 3D canvas bitmap changes after orbit rotation",
|
||||
);
|
||||
documentObject.querySelector("#array-view-2d-button").click();
|
||||
await waitFor(
|
||||
() => canvas.dataset.viewMode === "2d" &&
|
||||
canvas.getAttribute("aria-label").includes("평면 편집"),
|
||||
"return to array 2D editing",
|
||||
);
|
||||
assert(
|
||||
!documentObject.querySelector("#add-tool-button").disabled &&
|
||||
!documentObject.querySelector("#select-tool-button").disabled,
|
||||
"array canvas edit tools are restored in 2D",
|
||||
);
|
||||
assert(
|
||||
documentObject.querySelector("#canvas-front-badge").textContent.includes(
|
||||
"+Z FRONT · 화면 바깥쪽",
|
||||
),
|
||||
"returning to 2D restores its +Z front cue",
|
||||
);
|
||||
documentObject.querySelector(
|
||||
'#microphone-list [data-microphone-id="mic-001"]',
|
||||
).click();
|
||||
assertEqual(
|
||||
selectedCoordinates(documentObject).join(","),
|
||||
coordinatesBeforeArray3d.join(","),
|
||||
"2D edit resumes from the original microphone coordinates",
|
||||
);
|
||||
const defaultArray = createArrayDefinition();
|
||||
const initialCanvasRect = canvas.getBoundingClientRect();
|
||||
const initialViewport = fitArrayViewport(
|
||||
@@ -234,9 +483,10 @@ test("Array Builder가 preset·직접 편집·검증·표준 JSON round trip을
|
||||
dispatchDragPointer("pointerup", 36, -24, 0);
|
||||
await waitFor(
|
||||
() => documentObject.documentElement.dataset.beamPatternPreview === "false" &&
|
||||
beamPlot.querySelector(".beam-pattern-response")?.getAttribute("d") !==
|
||||
initialBeamPath,
|
||||
"beam pattern follows edited microphone geometry",
|
||||
beamCuts.every((cut) =>
|
||||
beamPlots[cut].querySelector(".beam-pattern-response")?.getAttribute("d") !==
|
||||
initialBeamPaths[cut]),
|
||||
"all beam-pattern cuts follow edited microphone geometry",
|
||||
);
|
||||
const draggedPosition = selectedCoordinates(documentObject);
|
||||
assert(
|
||||
@@ -789,28 +1039,63 @@ 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();
|
||||
const compactBeamPlotRects = beamCuts.map((cut) =>
|
||||
beamPlots[cut].getBoundingClientRect());
|
||||
assert(
|
||||
compactBeamPlotRect.left >= compactBeamCardRect.left - 1 &&
|
||||
compactBeamPlotRect.right <= compactBeamCardRect.right + 1,
|
||||
"compact polar plot stays inside its analysis card",
|
||||
compactBeamPlotRects.every((rect) =>
|
||||
rect.left >= compactBeamCardRect.left - 1 &&
|
||||
rect.right <= compactBeamCardRect.right + 1),
|
||||
"all compact polar plots stay inside their analysis card",
|
||||
);
|
||||
assert(
|
||||
Math.abs(compactBeamPlotRect.width - compactBeamPlotRect.height) <= 2,
|
||||
"compact polar plot remains square",
|
||||
compactBeamPlotRects.every((rect) => Math.abs(rect.width - rect.height) <= 2),
|
||||
"all compact polar plots remain square",
|
||||
);
|
||||
assert(
|
||||
compactBeamPlotRects.every(
|
||||
(rect, index) => index === 0 || rect.top > compactBeamPlotRects[index - 1].bottom,
|
||||
),
|
||||
"compact XZ, YZ, and XY plots stack without overlap",
|
||||
);
|
||||
const compactBeamFields = [
|
||||
...documentObject.querySelectorAll(".beam-pattern-fields > .field"),
|
||||
].map((field) => field.getBoundingClientRect());
|
||||
].filter((field) => !field.hidden).map((field) => field.getBoundingClientRect());
|
||||
assert(
|
||||
compactBeamFields.every(
|
||||
(rect, index) => index === 0 || rect.top >= compactBeamFields[index - 1].bottom - 1,
|
||||
),
|
||||
"compact beam controls stack without clipping",
|
||||
);
|
||||
documentObject.querySelector("#beam-pattern-view-3d-button").click();
|
||||
await waitFor(
|
||||
() => documentObject.documentElement.dataset.beamPatternView === "surface" &&
|
||||
documentObject.documentElement.dataset.beamPatternReady === "true" &&
|
||||
!documentObject.querySelector("#beam-pattern-surface-view").hidden &&
|
||||
Number(beamSurface.dataset.sampleCount) > 0,
|
||||
"compact 3D beam surface",
|
||||
);
|
||||
await nextPaint(windowObject);
|
||||
const compactBeamSurfaceRect = beamSurface.getBoundingClientRect();
|
||||
const compactBeamSurfaceCardRect = documentObject.querySelector("#beam-pattern-panel")
|
||||
.getBoundingClientRect();
|
||||
assert(
|
||||
compactBeamSurfaceRect.left >= compactBeamSurfaceCardRect.left - 1 &&
|
||||
compactBeamSurfaceRect.right <= compactBeamSurfaceCardRect.right + 1,
|
||||
"compact 3D beam surface stays inside its analysis card",
|
||||
);
|
||||
assertNear(
|
||||
compactBeamSurfaceRect.width / compactBeamSurfaceRect.height,
|
||||
16 / 10,
|
||||
0.03,
|
||||
"compact 3D beam surface preserves its aspect ratio",
|
||||
);
|
||||
assert(
|
||||
documentObject.documentElement.scrollWidth <=
|
||||
documentObject.documentElement.clientWidth + 1,
|
||||
"3D beam mode has no horizontal overflow at 390px",
|
||||
);
|
||||
assert(
|
||||
documentObject.querySelector("#array-canvas").getBoundingClientRect().height >= 350,
|
||||
"Array canvas remains usable at 390px",
|
||||
|
||||
Reference in New Issue
Block a user