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
+62 -21
View File
@@ -1,8 +1,8 @@
import { orbitForViewDirection } from "./orbit-view.js";
const MAX_PIXEL_RATIO = 3;
const MAX_SURFACE_VIEW_SAMPLES = 20_000;
const MAX_PITCH_RAD = Math.PI * 0.46;
const DEFAULT_YAW_RAD = -Math.PI / 5;
const DEFAULT_PITCH_RAD = Math.PI / 4.5;
const MAX_PITCH_RAD = Math.PI / 2;
function clamp(value, minimum, maximum) {
return Math.min(maximum, Math.max(minimum, value));
@@ -114,11 +114,10 @@ function rotatedPoint(point, yaw, pitch) {
function projectedPoint(point, yaw, pitch, size) {
const rotated = rotatedPoint(point, yaw, pitch);
const perspective = 1 / Math.max(0.68, 1 - rotated[2] * 0.18);
const scale = Math.min(size.width, size.height) * 0.39;
return {
x: size.width / 2 + rotated[0] * scale * perspective,
y: size.height / 2 - rotated[1] * scale * perspective,
x: size.width / 2 + rotated[0] * scale,
y: size.height / 2 - rotated[1] * scale,
depth: rotated[2],
};
}
@@ -133,7 +132,7 @@ function polygonArea(points) {
return Math.abs(area) / 2;
}
export function createReceiveBeamPatternSurfaceView(canvas) {
export function createReceiveBeamPatternSurfaceView(canvas, callbacks = {}) {
if (!canvas?.getContext) {
throw new TypeError("3D 빔 패턴 canvas가 필요합니다.");
}
@@ -146,8 +145,8 @@ export function createReceiveBeamPatternSurfaceView(canvas) {
let surface = null;
let validation = null;
let errorMessage = "";
let yaw = DEFAULT_YAW_RAD;
let pitch = DEFAULT_PITCH_RAD;
let viewDirection = "iso";
let { yaw, pitch } = orbitForViewDirection(viewDirection);
let orbit = null;
let frameId = 0;
let destroyed = false;
@@ -324,6 +323,26 @@ export function createReceiveBeamPatternSurfaceView(canvas) {
schedule();
}
function updateViewDirection(nextDirection) {
if (viewDirection === nextDirection) return;
viewDirection = nextDirection;
canvas.dataset.viewAxis = viewDirection;
callbacks.onViewDirectionChange?.(viewDirection);
}
function setViewDirection(nextDirection) {
const nextOrbit = orbitForViewDirection(nextDirection);
yaw = nextOrbit.yaw;
pitch = nextOrbit.pitch;
updateViewDirection(nextDirection);
schedule();
return viewDirection;
}
function markFreeOrbit() {
updateViewDirection("custom");
}
function finishOrbit(event) {
if (!orbit || (event?.pointerId !== undefined && event.pointerId !== orbit.pointerId)) return;
const pointerId = orbit.pointerId;
@@ -356,12 +375,17 @@ export function createReceiveBeamPatternSurfaceView(canvas) {
function onPointerMove(event) {
if (!orbit || orbit.pointerId !== event.pointerId) return;
event.preventDefault();
yaw += (event.clientX - orbit.x) * 0.011;
pitch = clamp(
pitch - (event.clientY - orbit.y) * 0.011,
const deltaX = event.clientX - orbit.x;
const deltaY = event.clientY - orbit.y;
const nextYaw = yaw + deltaX * 0.011;
const nextPitch = clamp(
pitch - deltaY * 0.011,
-MAX_PITCH_RAD,
MAX_PITCH_RAD,
);
if (nextYaw !== yaw || nextPitch !== pitch) markFreeOrbit();
yaw = nextYaw;
pitch = nextPitch;
orbit.x = event.clientX;
orbit.y = event.clientY;
schedule();
@@ -369,16 +393,33 @@ export function createReceiveBeamPatternSurfaceView(canvas) {
function onKeyDown(event) {
const step = event.shiftKey ? 15 : 5;
if (event.key === "ArrowLeft") yaw -= step * Math.PI / 180;
else if (event.key === "ArrowRight") yaw += step * Math.PI / 180;
if (event.key === "ArrowLeft") {
markFreeOrbit();
yaw -= step * Math.PI / 180;
}
else if (event.key === "ArrowRight") {
markFreeOrbit();
yaw += step * Math.PI / 180;
}
else if (event.key === "ArrowUp") {
pitch = clamp(pitch + step * Math.PI / 180, -MAX_PITCH_RAD, MAX_PITCH_RAD);
const nextPitch = clamp(
pitch + step * Math.PI / 180,
-MAX_PITCH_RAD,
MAX_PITCH_RAD,
);
if (nextPitch !== pitch) markFreeOrbit();
pitch = nextPitch;
} else if (event.key === "ArrowDown") {
pitch = clamp(pitch - step * Math.PI / 180, -MAX_PITCH_RAD, MAX_PITCH_RAD);
const nextPitch = clamp(
pitch - step * Math.PI / 180,
-MAX_PITCH_RAD,
MAX_PITCH_RAD,
);
if (nextPitch !== pitch) markFreeOrbit();
pitch = nextPitch;
}
else if (event.key.toLowerCase() === "r") {
yaw = DEFAULT_YAW_RAD;
pitch = DEFAULT_PITCH_RAD;
setViewDirection("iso");
} else return;
event.preventDefault();
schedule();
@@ -399,15 +440,15 @@ export function createReceiveBeamPatternSurfaceView(canvas) {
} else {
view.addEventListener?.("resize", onResize);
}
canvas.dataset.viewAxis = viewDirection;
schedule();
return {
render,
renderError,
setViewDirection,
resetOrbit() {
yaw = DEFAULT_YAW_RAD;
pitch = DEFAULT_PITCH_RAD;
schedule();
return setViewDirection("iso");
},
destroy() {
if (destroyed) return;