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
+47 -1
View File
@@ -42,6 +42,7 @@ const requiredIds = [
"preset-hint",
"generate-preset-button", "array-canvas", "select-tool-button",
"add-tool-button", "array-view-2d-button", "array-view-3d-button",
"array-axis-view-control", "array-axis-view",
"canvas-front-badge", "fit-view-button", "zoom-in-button", "zoom-out-button",
"snap-enabled", "snap-step", "microphone-list", "microphone-id",
"microphone-x", "microphone-y", "microphone-z", "add-microphone-button",
@@ -52,7 +53,7 @@ const requiredIds = [
"beam-pattern-samples", "beam-pattern-surface-resolution",
"beam-pattern-floor", "beam-pattern-view-cuts-button",
"beam-pattern-view-3d-button", "beam-pattern-cuts-view",
"beam-pattern-surface-view", "beam-pattern-surface",
"beam-pattern-surface-view", "beam-pattern-surface", "beam-pattern-axis-view",
"beam-pattern-plot-xz", "beam-pattern-plot-yz", "beam-pattern-plot-xy",
"beam-pattern-caption-xz", "beam-pattern-caption-yz",
"beam-pattern-caption-xy", "beam-pattern-surface-caption",
@@ -82,6 +83,12 @@ const beamPatternViews = Object.fromEntries(BEAM_PATTERN_CUTS.map((cut) => [
]));
const beamPatternSurfaceView = createReceiveBeamPatternSurfaceView(
elements["beam-pattern-surface"],
{
onViewDirectionChange(direction) {
elements["beam-pattern-axis-view"].value = direction;
document.documentElement.dataset.beamPatternViewAxis = direction;
},
},
);
const splitPane = createSplitPane({
@@ -354,6 +361,29 @@ function announceSelection(message) {
if (elements["selection-status"]) elements["selection-status"].textContent = message;
}
const VIEW_DIRECTION_LABELS = Object.freeze({
iso: "등각",
"+x": "+X에서",
"-x": "−X에서",
"+y": "+Y에서",
"-y": "−Y에서",
"+z": "+Z FRONT에서",
"-z": "−Z BACK에서",
custom: "자유 회전",
});
function syncArrayViewDirection(direction) {
const resolved = Object.hasOwn(VIEW_DIRECTION_LABELS, direction) ? direction : "iso";
elements["array-axis-view"].value = resolved;
elements["array-canvas"].dataset.viewAxis = resolved;
document.documentElement.dataset.arrayViewAxis = resolved;
if (state.arrayViewMode === "3d") {
elements["canvas-front-badge"].textContent = resolved === "custom"
? "+Z FRONT · 자유 회전"
: "시점 " + VIEW_DIRECTION_LABELS[resolved] + " 원점 보기 · +Z FRONT";
}
}
function dismissCanvasInstructions() {
elements["canvas-instructions"]?.classList.add("is-hidden");
}
@@ -382,6 +412,7 @@ function setArrayViewMode(mode, options = {}) {
elements["array-view-3d-button"].classList.toggle("is-active", resolved === "3d");
elements["array-view-2d-button"].setAttribute("aria-pressed", String(resolved === "2d"));
elements["array-view-3d-button"].setAttribute("aria-pressed", String(resolved === "3d"));
elements["array-axis-view-control"].hidden = resolved !== "3d";
elements["add-tool-button"].disabled = resolved === "3d";
elements["select-tool-button"].disabled = resolved === "3d";
const instruction = elements["canvas-instructions"];
@@ -403,6 +434,7 @@ function setArrayViewMode(mode, options = {}) {
elements["canvas-front-badge"].textContent = resolved === "3d"
? "+Z FRONT · 초록 축"
: "+Z FRONT · 화면 바깥쪽";
if (resolved === "3d") syncArrayViewDirection(elements["array-axis-view"].value);
elements["array-canvas"].setAttribute(
"aria-label",
resolved === "3d"
@@ -567,6 +599,9 @@ const canvasView = createArrayCanvas(elements["array-canvas"], {
onToolChange(tool) {
setTool(tool, { userInitiated: true });
},
onViewDirectionChange(direction) {
syncArrayViewDirection(direction);
},
onFit() {
canvasView.fit();
},
@@ -1371,6 +1406,11 @@ elements["array-view-2d-button"].addEventListener("click", () =>
setArrayViewMode("2d", { focus: true }));
elements["array-view-3d-button"].addEventListener("click", () =>
setArrayViewMode("3d", { focus: true }));
elements["array-axis-view"].addEventListener("change", () => {
const direction = elements["array-axis-view"].value;
canvasView.setViewDirection(direction);
announceSelection("3D 시점 · " + VIEW_DIRECTION_LABELS[direction] + " 원점 보기");
});
elements["fit-view-button"].addEventListener("click", () => canvasView.fit());
elements["zoom-in-button"].addEventListener("click", () => canvasView.zoomBy(1.25));
elements["zoom-out-button"].addEventListener("click", () => canvasView.zoomBy(0.8));
@@ -1399,6 +1439,9 @@ elements["beam-pattern-view-cuts-button"].addEventListener("click", () =>
setBeamPatternViewMode("cuts"));
elements["beam-pattern-view-3d-button"].addEventListener("click", () =>
setBeamPatternViewMode("surface"));
elements["beam-pattern-axis-view"].addEventListener("change", () => {
beamPatternSurfaceView.setViewDirection(elements["beam-pattern-axis-view"].value);
});
elements["beam-pattern-panel"].addEventListener("toggle", () => {
if (elements["beam-pattern-panel"].open && state.beamPatternDirty) {
scheduleBeamPatternRender();
@@ -1615,6 +1658,9 @@ window.addEventListener("unload", () => {
elements["snap-enabled"].checked = state.snapEnabled;
setNumber(elements["snap-step"], state.snapStepM * 1000);
syncArrayViewDirection("iso");
elements["beam-pattern-axis-view"].value = "iso";
document.documentElement.dataset.beamPatternViewAxis = "iso";
setArrayViewMode("2d", { announce: false });
setBeamPatternViewMode("cuts", { render: false });
syncBeamPatternFrequency();