feat: add axis-aligned 3d camera views

This commit is contained in:
2026-08-30 11:40:21 +09:00
parent 51c7bbd859
commit 2a2e192196
12 changed files with 619 additions and 61 deletions
+152 -2
View File
@@ -279,6 +279,44 @@ test("Array Builder가 preset·직접 편집·검증·표준 JSON round trip을
"3D beam caption discloses the display-only radial mapping",
);
await nextPaint(windowObject);
const beamAxisView = documentObject.querySelector("#beam-pattern-axis-view");
assertEqual(beamAxisView.value, "iso", "3D beam surface starts in isometric view");
assertEqual(beamSurface.dataset.viewAxis, "iso", "beam canvas exposes isometric provenance");
const beamComputeBeforeAxisView =
documentObject.documentElement.dataset.beamPatternComputeMs;
const beamFrequencyBeforeAxisView = beamSurface.dataset.frequencyHz;
const surfaceBeforeAxisView = beamSurface.toDataURL();
beamAxisView.focus();
dispatchValue(windowObject, beamAxisView, "+z");
await nextPaint(windowObject);
assertEqual(beamSurface.dataset.viewAxis, "+z", "beam surface aligns from +Z");
assert(
beamSurface.toDataURL() !== surfaceBeforeAxisView,
"beam surface bitmap changes for the exact +Z camera",
);
dispatchValue(windowObject, beamAxisView, "+x");
await nextPaint(windowObject);
assertEqual(beamSurface.dataset.viewAxis, "+x", "beam surface aligns from +X");
assertEqual(
documentObject.activeElement,
beamAxisView,
"beam axis selector keeps keyboard focus for consecutive comparisons",
);
assertEqual(
documentObject.documentElement.dataset.beamPatternComputeMs,
beamComputeBeforeAxisView,
"beam camera changes do not recompute the physical response",
);
assertEqual(
beamSurface.dataset.frequencyHz,
beamFrequencyBeforeAxisView,
"beam camera changes preserve the evaluated analog frequency",
);
assertEqual(
documentObject.querySelector("#save-state").textContent,
initialSaveState,
"beam camera changes stay outside Array dirty state",
);
const surfaceBeforeOrbit = beamSurface.toDataURL();
const beamSurfaceRect = beamSurface.getBoundingClientRect();
const beamOrbitPointerId = 71;
@@ -317,6 +355,21 @@ test("Array Builder가 preset·직접 편집·검증·표준 JSON round trip을
beamSurface.toDataURL() !== surfaceBeforeOrbit,
"3D beam surface bitmap changes after orbit rotation",
);
assertEqual(beamSurface.dataset.viewAxis, "custom", "beam drag enters free-orbit mode");
assertEqual(beamAxisView.value, "custom", "beam view selector reflects free orbit");
beamSurface.dispatchEvent(new windowObject.KeyboardEvent("keydown", {
bubbles: true,
cancelable: true,
key: "r",
}));
await nextPaint(windowObject);
assertEqual(beamSurface.dataset.viewAxis, "iso", "beam R shortcut restores isometric view");
assertEqual(beamAxisView.value, "iso", "beam selector follows the R shortcut");
assertEqual(
documentObject.documentElement.dataset.beamPatternComputeMs,
beamComputeBeforeAxisView,
"beam camera reset also avoids a physical-response recompute",
);
documentObject.querySelector("#beam-pattern-view-cuts-button").click();
await waitFor(
() => documentObject.documentElement.dataset.beamPatternView === "cuts" &&
@@ -364,9 +417,9 @@ test("Array Builder가 preset·직접 편집·검증·표준 JSON round trip을
);
assert(
documentObject.querySelector("#canvas-front-badge").textContent.includes(
"+Z FRONT · 초록 축",
"시점 등각",
),
"3D canvas identifies the +Z front axis",
"3D canvas identifies its isometric camera",
);
assert(
documentObject.querySelector("#canvas-instructions").textContent.includes("검사 전용"),
@@ -389,6 +442,41 @@ test("Array Builder가 preset·직접 편집·검증·표준 JSON round trip을
coordinatesBeforeArray3d.join(","),
"3D inspection leaves microphone coordinates unchanged",
);
const arrayAxisControl = documentObject.querySelector("#array-axis-view-control");
const arrayAxisView = documentObject.querySelector("#array-axis-view");
assert(!arrayAxisControl.hidden, "array axis selector appears only for 3D inspection");
assertEqual(arrayAxisView.value, "iso", "array 3D starts in isometric view");
const arraySaveStateBeforeAxisView =
documentObject.querySelector("#save-state").textContent;
const array3dBeforeAxisView = canvas.toDataURL();
arrayAxisView.focus();
dispatchValue(windowObject, arrayAxisView, "+z");
await nextPaint(windowObject);
assertEqual(canvas.dataset.viewAxis, "+z", "array canvas aligns from +Z FRONT");
dispatchValue(windowObject, arrayAxisView, "+x");
await nextPaint(windowObject);
assertEqual(canvas.dataset.viewAxis, "+x", "array canvas aligns from +X");
assertEqual(
documentObject.activeElement,
arrayAxisView,
"array axis selector keeps keyboard focus for consecutive comparisons",
);
assert(
canvas.toDataURL() !== array3dBeforeAxisView,
"array 3D bitmap changes after axis alignment",
);
assertEqual(
selectedCoordinates(documentObject).join(","),
coordinatesBeforeArray3d.join(","),
"axis camera changes leave microphone coordinates unchanged",
);
assertEqual(
documentObject.querySelector("#save-state").textContent,
arraySaveStateBeforeAxisView,
"axis camera changes do not dirty the Array document",
);
const selectionBeforeArrayOrbit = documentObject
.querySelector('#microphone-list [aria-selected="true"]')?.dataset.microphoneId;
const array3dBeforeOrbit = canvas.toDataURL();
const array3dRect = canvas.getBoundingClientRect();
const arrayOrbitPointerId = 72;
@@ -427,12 +515,34 @@ test("Array Builder가 preset·직접 편집·검증·표준 JSON round trip을
canvas.toDataURL() !== array3dBeforeOrbit,
"array 3D canvas bitmap changes after orbit rotation",
);
assertEqual(canvas.dataset.viewAxis, "custom", "array drag enters free-orbit mode");
assertEqual(arrayAxisView.value, "custom", "array view selector reflects free orbit");
assertEqual(
documentObject.querySelector('#microphone-list [aria-selected="true"]')
?.dataset.microphoneId,
selectionBeforeArrayOrbit,
"array orbit drag preserves the current channel selection",
);
canvas.dispatchEvent(new windowObject.KeyboardEvent("keydown", {
bubbles: true,
cancelable: true,
key: "r",
}));
await nextPaint(windowObject);
assertEqual(canvas.dataset.viewAxis, "iso", "array R shortcut restores isometric view");
assertEqual(arrayAxisView.value, "iso", "array selector follows the R shortcut");
assertEqual(
documentObject.querySelector("#save-state").textContent,
arraySaveStateBeforeAxisView,
"array camera reset stays outside Array dirty state",
);
documentObject.querySelector("#array-view-2d-button").click();
await waitFor(
() => canvas.dataset.viewMode === "2d" &&
canvas.getAttribute("aria-label").includes("평면 편집"),
"return to array 2D editing",
);
assert(arrayAxisControl.hidden, "array axis selector hides in 2D editing");
assert(
!documentObject.querySelector("#add-tool-button").disabled &&
!documentObject.querySelector("#select-tool-button").disabled,
@@ -1138,6 +1248,18 @@ test("Array Builder가 preset·직접 편집·검증·표준 JSON round trip을
0.03,
"compact 3D beam surface preserves its aspect ratio",
);
assertEqual(
windowObject.getComputedStyle(beamSurface).touchAction,
"pan-y",
"compact beam 3D view preserves vertical page scrolling",
);
const compactBeamAxisRect = documentObject.querySelector("#beam-pattern-axis-view")
.getBoundingClientRect();
assert(
compactBeamAxisRect.left >= compactBeamSurfaceCardRect.left - 1 &&
compactBeamAxisRect.right <= compactBeamSurfaceCardRect.right + 1,
"compact beam axis selector stays inside its analysis card",
);
assert(
documentObject.documentElement.scrollWidth <=
documentObject.documentElement.clientWidth + 1,
@@ -1147,6 +1269,34 @@ test("Array Builder가 preset·직접 편집·검증·표준 JSON round trip을
documentObject.querySelector("#array-canvas").getBoundingClientRect().height >= 350,
"Array canvas remains usable at 390px",
);
documentObject.querySelector("#array-view-3d-button").click();
await waitFor(() => canvas.dataset.viewMode === "3d", "compact array 3D mode");
const compactAxisControlRect = arrayAxisControl.getBoundingClientRect();
const compactToolbarRect = documentObject.querySelector(".workspace-toolbar")
.getBoundingClientRect();
assert(
compactAxisControlRect.left >= compactToolbarRect.left - 1 &&
compactAxisControlRect.right <= compactToolbarRect.right + 1,
"compact array axis selector stays inside its toolbar",
);
const compactFrontBadge = documentObject.querySelector("#canvas-front-badge");
assert(
compactFrontBadge.scrollWidth <= compactFrontBadge.clientWidth + 1 &&
windowObject.getComputedStyle(compactFrontBadge).whiteSpace !== "nowrap",
"compact 3D direction badge wraps without clipping axis and FRONT cues",
);
assertEqual(
windowObject.getComputedStyle(canvas).touchAction,
"pan-y",
"compact array 3D view preserves vertical page scrolling",
);
assert(
documentObject.documentElement.scrollWidth <=
documentObject.documentElement.clientWidth + 1,
"array axis selector creates no horizontal overflow at 390px",
);
documentObject.querySelector("#array-view-2d-button").click();
await waitFor(() => canvas.dataset.viewMode === "2d", "return from compact array 3D");
assertEqual(
windowObject.getComputedStyle(documentObject.querySelector("#array-canvas")).touchAction,
"pan-y",