Files
Aislo/B04_wf1_Surface/B04_wf1_Surface_UI_Camera.ts
T
2026-07-19 13:08:34 +09:00

78 lines
2.7 KiB
TypeScript

import type { SurfaceBounds } from "./B04_wf1_Surface_Api_Fetch";
export const SURFACE_CAMERA_FOV = 50;
const LIGHT_VIEWER_BACKGROUND = 0xf5f7f9;
const DARK_VIEWER_BACKGROUND = 0x251f38;
export interface SurfaceCameraState {
direction: [number, number, number];
distanceMeters: number;
targetMeters: [number, number, number];
}
export function getReferenceCenter(bounds: SurfaceBounds): [number, number, number] {
return [
(bounds.x_min + bounds.x_max) / 2,
(bounds.y_min + bounds.y_max) / 2,
(bounds.z_min + bounds.z_max) / 2,
];
}
export function getTopFitDistance(bounds: SurfaceBounds, aspect: number): number {
const width = Math.max(bounds.x_max - bounds.x_min, 1);
const depth = Math.max(bounds.y_max - bounds.y_min, 1);
const verticalFov = (SURFACE_CAMERA_FOV * Math.PI) / 180;
const horizontalFov = 2 * Math.atan(Math.tan(verticalFov / 2) * Math.max(aspect, 0.1));
const verticalDistance = depth / (2 * Math.tan(verticalFov / 2));
const horizontalDistance = width / (2 * Math.tan(horizontalFov / 2));
return Math.max(verticalDistance, horizontalDistance, 1) * 1.12;
}
export function targetPlaneMetersPerPixel(distanceMeters: number, viewportHeight: number): number {
const verticalFov = (SURFACE_CAMERA_FOV * Math.PI) / 180;
return (
(2 * Math.tan(verticalFov / 2) * Math.max(distanceMeters, 0.001)) / Math.max(viewportHeight, 1)
);
}
export function niceScaleDistance(roughMeters: number): number {
const exponent = Math.floor(Math.log10(Math.max(roughMeters, 0.001)));
const base = 10 ** exponent;
const normalized = roughMeters / base;
const step = normalized <= 1 ? 1 : normalized <= 2 ? 2 : normalized <= 5 ? 5 : 10;
return step * base;
}
export function bindSurfaceViewerTheme(
applyBackground: (color: string | number) => void,
): () => void {
const systemDarkTheme = window.matchMedia("(prefers-color-scheme: dark)");
const update = (): void => {
const theme = document.documentElement.getAttribute("data-theme");
const dark = theme === "dark" || (theme !== "light" && systemDarkTheme.matches);
if (!dark) {
applyBackground(LIGHT_VIEWER_BACKGROUND);
return;
}
const surfaceRaised = getComputedStyle(document.documentElement)
.getPropertyValue("--color-surface-raised")
.trim();
applyBackground(
surfaceRaised && CSS.supports("color", surfaceRaised)
? surfaceRaised
: DARK_VIEWER_BACKGROUND,
);
};
const observer = new MutationObserver(update);
observer.observe(document.documentElement, {
attributes: true,
attributeFilter: ["data-theme"],
});
systemDarkTheme.addEventListener("change", update);
update();
return () => {
observer.disconnect();
systemDarkTheme.removeEventListener("change", update);
};
}