feat: add axis-aligned 3d camera views
This commit is contained in:
@@ -1,13 +1,14 @@
|
||||
import { clampPositionToAperture, positionInsideAperture } from "./model.js";
|
||||
import { orbitForViewDirection } from "./orbit-view.js";
|
||||
|
||||
const BACKGROUND = "#07110f";
|
||||
const PLOT_BACKGROUND = "#0b1714";
|
||||
const MAX_PIXEL_RATIO = 3;
|
||||
const MIN_ZOOM = 0.25;
|
||||
const MAX_ZOOM = 16;
|
||||
const MAX_ORBIT_PITCH = Math.PI / 2;
|
||||
const TOUCH_GESTURE_THRESHOLD_PX = 6;
|
||||
const DEFAULT_MARGINS = Object.freeze({ left: 54, right: 22, top: 30, bottom: 46 });
|
||||
const DEFAULT_ORBIT = Object.freeze({ yaw: -Math.PI / 5, pitch: Math.PI / 4.5 });
|
||||
|
||||
export const ARRAY_CANVAS_TOOLS = Object.freeze(["select", "add"]);
|
||||
|
||||
@@ -704,7 +705,7 @@ function drawMicrophones3d(context, model, viewport) {
|
||||
return microphones;
|
||||
}
|
||||
|
||||
function drawArray3d(context, size, model, zoom, orbit) {
|
||||
function drawArray3d(context, size, model, zoom, orbit, viewDirection) {
|
||||
context.fillStyle = BACKGROUND;
|
||||
context.fillRect(0, 0, size.width, size.height);
|
||||
if (!model?.arrayDefinition?.aperture || !Array.isArray(model.arrayDefinition.microphones)) {
|
||||
@@ -735,7 +736,13 @@ function drawArray3d(context, size, model, zoom, orbit) {
|
||||
context.textBaseline = "top";
|
||||
context.fillStyle = "rgba(205,225,218,.72)";
|
||||
context.textAlign = "left";
|
||||
context.fillText("3D INSPECT - drag to orbit", viewport.plot.x, 8);
|
||||
context.fillText(
|
||||
viewDirection === "custom"
|
||||
? "3D INSPECT - FREE ORBIT"
|
||||
: "3D INSPECT - VIEW FROM " + viewDirection.toUpperCase(),
|
||||
viewport.plot.x,
|
||||
8,
|
||||
);
|
||||
context.textAlign = "right";
|
||||
context.fillText(Math.round(zoom * 100) + "%", viewport.plot.x + viewport.plot.width, 8);
|
||||
context.textAlign = "left";
|
||||
@@ -765,7 +772,8 @@ function normalizedTool(tool) {
|
||||
* Create the editable microphone-array canvas.
|
||||
*
|
||||
* Callbacks: onSelect(id), onAdd(position), onDragStart(id), onDrag(id, position),
|
||||
* onDragEnd(id, position), onDelete(), onNudge({dx,dy,dz}), onToolChange(tool), onFit().
|
||||
* onDragEnd(id, position), onDelete(), onNudge({dx,dy,dz}), onToolChange(tool),
|
||||
* onViewDirectionChange(direction), onFit().
|
||||
*/
|
||||
export function createArrayCanvas(canvas, callbacks = {}) {
|
||||
if (!canvas) throw new TypeError("array canvas가 필요합니다.");
|
||||
@@ -782,7 +790,8 @@ export function createArrayCanvas(canvas, callbacks = {}) {
|
||||
let zoom = 1;
|
||||
let tool = "select";
|
||||
let viewMode = "2d";
|
||||
let orbit = { ...DEFAULT_ORBIT };
|
||||
let viewDirection = "iso";
|
||||
let orbit = orbitForViewDirection(viewDirection);
|
||||
let orbitDrag = null;
|
||||
let drag = null;
|
||||
let pendingAdd = null;
|
||||
@@ -793,6 +802,7 @@ export function createArrayCanvas(canvas, callbacks = {}) {
|
||||
canvas.dataset.tool = tool;
|
||||
canvas.dataset.zoom = String(zoom);
|
||||
canvas.dataset.viewMode = viewMode;
|
||||
canvas.dataset.viewAxis = viewDirection;
|
||||
canvas.dataset.orbiting = String(Boolean(orbitDrag));
|
||||
}
|
||||
if (canvas.style) {
|
||||
@@ -807,7 +817,7 @@ export function createArrayCanvas(canvas, callbacks = {}) {
|
||||
if (destroyed) return;
|
||||
const size = prepareContext(canvas, context, view);
|
||||
viewport = viewMode === "3d"
|
||||
? drawArray3d(context, size, model, zoom, orbit)
|
||||
? drawArray3d(context, size, model, zoom, orbit, viewDirection)
|
||||
: drawArray(context, size, model, zoom, drag);
|
||||
updateCanvasAffordance();
|
||||
}
|
||||
@@ -853,9 +863,31 @@ export function createArrayCanvas(canvas, callbacks = {}) {
|
||||
return tool;
|
||||
}
|
||||
|
||||
function updateViewDirection(nextDirection) {
|
||||
if (viewDirection === nextDirection) return;
|
||||
viewDirection = nextDirection;
|
||||
updateCanvasAffordance();
|
||||
callbacks.onViewDirectionChange?.(viewDirection);
|
||||
}
|
||||
|
||||
function setViewDirection(nextDirection) {
|
||||
orbit = orbitForViewDirection(nextDirection);
|
||||
updateViewDirection(nextDirection);
|
||||
viewport = null;
|
||||
schedule();
|
||||
return viewDirection;
|
||||
}
|
||||
|
||||
function markFreeOrbit() {
|
||||
updateViewDirection("custom");
|
||||
}
|
||||
|
||||
function fit() {
|
||||
zoom = 1;
|
||||
if (viewMode === "3d") orbit = { ...DEFAULT_ORBIT };
|
||||
if (viewMode === "3d") {
|
||||
orbit = orbitForViewDirection("iso");
|
||||
updateViewDirection("iso");
|
||||
}
|
||||
viewport = null;
|
||||
schedule();
|
||||
return zoom;
|
||||
@@ -918,17 +950,21 @@ export function createArrayCanvas(canvas, callbacks = {}) {
|
||||
TOUCH_GESTURE_THRESHOLD_PX;
|
||||
}
|
||||
|
||||
function finishOrbit(event) {
|
||||
function finishOrbit(event, selectOnClick = false) {
|
||||
if (!orbitDrag || (event?.pointerId !== undefined && event.pointerId !== orbitDrag.pointerId)) {
|
||||
return false;
|
||||
}
|
||||
const pointerId = event?.pointerId ?? orbitDrag.pointerId;
|
||||
const completed = orbitDrag;
|
||||
const pointerId = event?.pointerId ?? completed.pointerId;
|
||||
orbitDrag = null;
|
||||
try {
|
||||
if (canvas.hasPointerCapture?.(pointerId)) canvas.releasePointerCapture?.(pointerId);
|
||||
} catch {
|
||||
// Capture can be released by the browser before cancellation arrives.
|
||||
}
|
||||
if (selectOnClick && !completed.moved) {
|
||||
callbacks.onSelect?.(completed.hitId);
|
||||
}
|
||||
updateCanvasAffordance();
|
||||
schedule();
|
||||
return true;
|
||||
@@ -961,10 +997,13 @@ export function createArrayCanvas(canvas, callbacks = {}) {
|
||||
const currentViewport = ensureViewport();
|
||||
if (!currentViewport) return;
|
||||
const hit = hitTestProjectedMicrophone(pointerPoint(event), model, currentViewport);
|
||||
callbacks.onSelect?.(hit?.id ?? null);
|
||||
const startClient = clientPoint(event);
|
||||
orbitDrag = {
|
||||
pointerId: event.pointerId,
|
||||
lastClient: clientPoint(event),
|
||||
hitId: hit?.id ?? null,
|
||||
startClient,
|
||||
lastClient: startClient,
|
||||
moved: false,
|
||||
};
|
||||
try {
|
||||
canvas.setPointerCapture?.(event.pointerId);
|
||||
@@ -1023,11 +1062,26 @@ export function createArrayCanvas(canvas, callbacks = {}) {
|
||||
function onPointerMove(event) {
|
||||
if (orbitDrag?.pointerId === event.pointerId) {
|
||||
const current = clientPoint(event);
|
||||
if (!orbitDrag.moved) {
|
||||
const distance = Math.hypot(
|
||||
current.x - orbitDrag.startClient.x,
|
||||
current.y - orbitDrag.startClient.y,
|
||||
);
|
||||
if (distance <= TOUCH_GESTURE_THRESHOLD_PX) return;
|
||||
orbitDrag.moved = true;
|
||||
}
|
||||
const dx = current.x - orbitDrag.lastClient.x;
|
||||
const dy = current.y - orbitDrag.lastClient.y;
|
||||
orbitDrag.lastClient = current;
|
||||
orbit.yaw += dx * 0.01;
|
||||
orbit.pitch = clamp(orbit.pitch - dy * 0.01, -Math.PI * 0.46, Math.PI * 0.46);
|
||||
const nextYaw = orbit.yaw + dx * 0.01;
|
||||
const nextPitch = clamp(
|
||||
orbit.pitch - dy * 0.01,
|
||||
-MAX_ORBIT_PITCH,
|
||||
MAX_ORBIT_PITCH,
|
||||
);
|
||||
if (nextYaw !== orbit.yaw || nextPitch !== orbit.pitch) markFreeOrbit();
|
||||
orbit.yaw = nextYaw;
|
||||
orbit.pitch = nextPitch;
|
||||
viewport = null;
|
||||
event.preventDefault();
|
||||
schedule();
|
||||
@@ -1105,6 +1159,7 @@ export function createArrayCanvas(canvas, callbacks = {}) {
|
||||
const orbitStep = event.shiftKey ? 0.18 : 0.06;
|
||||
if (event.key === "ArrowLeft" || event.key === "ArrowRight") {
|
||||
event.preventDefault();
|
||||
markFreeOrbit();
|
||||
orbit.yaw += event.key === "ArrowLeft" ? -orbitStep : orbitStep;
|
||||
viewport = null;
|
||||
schedule();
|
||||
@@ -1112,11 +1167,13 @@ export function createArrayCanvas(canvas, callbacks = {}) {
|
||||
}
|
||||
if (event.key === "ArrowUp" || event.key === "ArrowDown") {
|
||||
event.preventDefault();
|
||||
orbit.pitch = clamp(
|
||||
const nextPitch = clamp(
|
||||
orbit.pitch + (event.key === "ArrowUp" ? orbitStep : -orbitStep),
|
||||
-Math.PI * 0.46,
|
||||
Math.PI * 0.46,
|
||||
-MAX_ORBIT_PITCH,
|
||||
MAX_ORBIT_PITCH,
|
||||
);
|
||||
if (nextPitch !== orbit.pitch) markFreeOrbit();
|
||||
orbit.pitch = nextPitch;
|
||||
viewport = null;
|
||||
schedule();
|
||||
}
|
||||
@@ -1159,7 +1216,7 @@ export function createArrayCanvas(canvas, callbacks = {}) {
|
||||
}
|
||||
|
||||
function onPointerUp(event) {
|
||||
if (finishOrbit(event)) return;
|
||||
if (finishOrbit(event, true)) return;
|
||||
if (finishPendingAdd(event, false)) return;
|
||||
finishDrag(event, false);
|
||||
}
|
||||
@@ -1202,6 +1259,7 @@ export function createArrayCanvas(canvas, callbacks = {}) {
|
||||
return {
|
||||
render,
|
||||
setTool,
|
||||
setViewDirection,
|
||||
fit,
|
||||
zoomBy,
|
||||
focus,
|
||||
|
||||
Reference in New Issue
Block a user