control/entityEditor/CameraEditorView.tstypescript
import type { Camera } from "../../camera/Camera";
import type { DepthCamera } from "../depthCamera/DepthCamera";
import { Entity } from "../../entity/Entity";
import { Object3d } from "../../Object3d";
import { Checkbox } from "../../ui/Checkbox";
import { Input } from "../../ui/Input";
import { TitleBarView } from "../../ui/TitleBarView";
import { View, type IViewParams } from "../../ui/View";
interface ICameraEditorViewParams extends IViewParams {
entity: Entity;
}
const TEMPLATE = `<div class="og-editor-panel"></div>`;
function getNumber(value: string): number | null {
const parsed = parseFloat(value);
return Number.isFinite(parsed) ? parsed : null;
}
export function hasEntityEditorDepthCamera(entity: Entity): boolean {
return !!entity.properties.depthCamera;
}
export class CameraEditorView extends View<Entity> {
protected _nearView: Input;
protected _farView: Input;
protected _leftView: Input;
protected _rightView: Input;
protected _bottomView: Input;
protected _topView: Input;
protected _verticalFovView: Input;
protected _horizontalFovView: Input;
protected _viewportWidthView: Input;
protected _viewportHeightView: Input;
protected _orthographicView: Checkbox;
protected _focusDistanceView: Input;
protected _showFrustumView: Checkbox;
protected _showFootprintView: Checkbox;
protected _biasView: Input;
protected _normalBiasView: Input;
protected _titleBarView: TitleBarView;
protected _bodyEl: HTMLElement | null;
protected _depthCamera: DepthCamera | null;
constructor(params: ICameraEditorViewParams) {
super({
template: TEMPLATE,
model: params.entity
});
this._nearView = new Input({
label: "Near",
type: "number",
min: 0,
maxFixed: 4
});
this._farView = new Input({
label: "Far",
type: "number",
min: 0,
maxFixed: 4
});
this._leftView = new Input({
label: "Bounds left",
type: "number",
maxFixed: 4
});
this._rightView = new Input({
label: "Bounds right",
type: "number",
maxFixed: 4
});
this._bottomView = new Input({
label: "Bounds bottom",
type: "number",
maxFixed: 4
});
this._topView = new Input({
label: "Bounds top",
type: "number",
maxFixed: 4
});
this._verticalFovView = new Input({
label: "Vertical FOV",
type: "number",
min: 0.1,
max: 179.9,
maxFixed: 4
});
this._horizontalFovView = new Input({
label: "Horizontal FOV",
type: "number",
min: 0.1,
max: 179.9,
maxFixed: 4
});
this._viewportWidthView = new Input({
label: "Viewport width",
type: "number",
min: 1,
step: 1,
maxFixed: 0
});
this._viewportHeightView = new Input({
label: "Viewport height",
type: "number",
min: 1,
step: 1,
maxFixed: 0
});
this._orthographicView = new Checkbox({
label: "Orthographic"
});
this._focusDistanceView = new Input({
label: "Focus distance",
type: "number",
min: 0.000001,
step: 1,
maxFixed: 4
});
this._showFrustumView = new Checkbox({
label: "Show frustum"
});
this._showFootprintView = new Checkbox({
label: "Show footprint"
});
this._biasView = new Input({
label: "Depth bias, m",
type: "number",
min: 0,
step: 0.1,
maxFixed: 3
});
this._normalBiasView = new Input({
label: "Normal bias",
type: "number",
min: 0,
step: 0.01,
maxFixed: 4
});
this._titleBarView = new TitleBarView({
title: "Camera"
});
this._bodyEl = null;
this._depthCamera = null;
this.bindEntity(params.entity);
}
public bindEntity(entity: Entity): void {
this.model = entity;
this._depthCamera = entity.properties.depthCamera;
}
public override render(params?: unknown): this {
super.render(params);
this._titleBarView.appendTo(this.el!);
this._titleBarView.events.on("change", this._onTitleBarChange);
this._bodyEl = document.createElement("div");
this._bodyEl.classList.add("og-editor-panel__body");
this.el!.appendChild(this._bodyEl);
this._nearView.appendTo(this._bodyEl);
this._farView.appendTo(this._bodyEl);
this._leftView.appendTo(this._bodyEl);
this._topView.appendTo(this._bodyEl);
this._rightView.appendTo(this._bodyEl);
this._bottomView.appendTo(this._bodyEl);
this._verticalFovView.appendTo(this._bodyEl);
this._horizontalFovView.appendTo(this._bodyEl);
this._viewportWidthView.appendTo(this._bodyEl);
this._viewportHeightView.appendTo(this._bodyEl);
this._orthographicView.appendTo(this._bodyEl);
this._focusDistanceView.appendTo(this._bodyEl);
this._showFrustumView.appendTo(this._bodyEl);
this._showFootprintView.appendTo(this._bodyEl);
this._biasView.appendTo(this._bodyEl);
this._normalBiasView.appendTo(this._bodyEl);
this._nearView.events.on("change", this._onChangeNear);
this._farView.events.on("change", this._onChangeFar);
this._leftView.events.on("change", this._onChangeLeft);
this._rightView.events.on("change", this._onChangeRight);
this._bottomView.events.on("change", this._onChangeBottom);
this._topView.events.on("change", this._onChangeTop);
this._verticalFovView.events.on("change", this._onChangeVerticalFov);
this._horizontalFovView.events.on("change", this._onChangeHorizontalFov);
this._viewportWidthView.events.on("change", this._onChangeViewportWidth);
this._viewportHeightView.events.on("change", this._onChangeViewportHeight);
this._orthographicView.events.on("change", this._onChangeOrthographic);
this._focusDistanceView.events.on("change", this._onChangeFocusDistance);
this._showFrustumView.events.on("change", this._onChangeShowFrustum);
this._showFootprintView.events.on("change", this._onChangeShowFootprint);
this._biasView.events.on("change", this._onChangeBias);
this._normalBiasView.events.on("change", this._onChangeNormalBias);
this.refresh();
return this;
}
public refresh(): void {
const camera = this._getCamera();
if (!camera) return;
const frustum = camera.frustums[0];
if (frustum) {
this._nearView.stopPropagation();
this._nearView.value = frustum.near;
this._farView.stopPropagation();
this._farView.value = frustum.far;
this._leftView.stopPropagation();
this._leftView.value = frustum.left;
this._rightView.stopPropagation();
this._rightView.value = frustum.right;
this._bottomView.stopPropagation();
this._bottomView.value = frustum.bottom;
this._topView.stopPropagation();
this._topView.value = frustum.top;
}
this._verticalFovView.stopPropagation();
this._verticalFovView.value = camera.verticalViewAngle;
this._horizontalFovView.stopPropagation();
this._horizontalFovView.value = camera.horizontalViewAngle;
this._viewportWidthView.stopPropagation();
this._viewportWidthView.value = camera.width;
this._viewportHeightView.stopPropagation();
this._viewportHeightView.value = camera.height;
const depthCamera = this._depthCamera;
this._showFrustumView.visibility = !!depthCamera;
this._showFootprintView.visibility = !!depthCamera;
this._orthographicView.visibility = !!depthCamera;
this._focusDistanceView.visibility = !!depthCamera && depthCamera.isOrthographic;
this._biasView.visibility = !!depthCamera;
this._normalBiasView.visibility = !!depthCamera;
if (depthCamera) {
if (this._orthographicView.checked !== depthCamera.isOrthographic) {
this._orthographicView.stopPropagation();
this._orthographicView.checked = depthCamera.isOrthographic;
}
this._focusDistanceView.stopPropagation();
this._focusDistanceView.value = depthCamera.focusDistance;
if (this._showFrustumView.checked !== depthCamera.showFrustum) {
this._showFrustumView.stopPropagation();
this._showFrustumView.checked = depthCamera.showFrustum;
}
if (this._showFootprintView.checked !== depthCamera.showFootprint) {
this._showFootprintView.stopPropagation();
this._showFootprintView.checked = depthCamera.showFootprint;
}
this._biasView.stopPropagation();
this._biasView.value = depthCamera.depthBiasWorld;
this._normalBiasView.stopPropagation();
this._normalBiasView.value = depthCamera.normalBias;
}
}
protected _getCamera(): Camera | null {
return this._depthCamera?.camera || null;
}
public override remove(): void {
this._nearView.remove();
this._farView.remove();
this._leftView.remove();
this._rightView.remove();
this._bottomView.remove();
this._topView.remove();
this._verticalFovView.remove();
this._horizontalFovView.remove();
this._viewportWidthView.remove();
this._viewportHeightView.remove();
this._orthographicView.remove();
this._focusDistanceView.remove();
this._showFrustumView.remove();
this._showFootprintView.remove();
this._biasView.remove();
this._normalBiasView.remove();
this._titleBarView.events.off("change", this._onTitleBarChange);
this._titleBarView.remove();
this._bodyEl = null;
super.remove();
}
protected _onTitleBarChange = (isCollapsed: boolean): void => {
this._bodyEl?.classList.toggle("og-editor-panel__body_collapsed", isCollapsed);
};
protected _setNearFar(near: number, far: number): void {
const camera = this._getCamera();
if (!camera || near <= 0 || far <= near) return;
camera.setNearFar(near, far);
camera.update();
this.refresh();
}
protected _setBounds(left: number, right: number, bottom: number, top: number): void {
const camera = this._getCamera();
const frustum = camera?.frustums[0];
if (!camera || !frustum || !camera.isOrthographic || right <= left || top <= bottom) {
this.refresh();
return;
}
frustum.setOrthoBounds(left, right, bottom, top);
camera.update();
this.refresh();
}
protected _syncFrustumEntityScale(): void {
const camera = this._getCamera();
if (!camera) return;
const tag = this.model.geoObject?.tag?.toLowerCase() || "";
if (!tag.includes("frustum")) return;
if (this._depthCamera) {
this.model.setScale3v(this._depthCamera.frustumScale);
} else {
const length = this.model.getScale().z || 1.0;
this.model.setScale3v(
Object3d.getFrustumScaleByCameraAngles(length, camera.horizontalViewAngle, camera.verticalViewAngle)
);
}
}
protected _onChangeNear = (value: string): void => {
const near = getNumber(value);
const far = this._getCamera()?.frustums[0]?.far;
if (near !== null && far != null) {
this._setNearFar(near, far);
}
};
protected _onChangeFar = (value: string): void => {
const far = getNumber(value);
const near = this._getCamera()?.frustums[0]?.near;
if (far !== null && near != null) {
this._setNearFar(near, far);
}
};
protected _onChangeLeft = (value: string): void => {
const left = getNumber(value);
const frustum = this._getCamera()?.frustums[0];
if (left !== null && frustum) {
this._setBounds(left, frustum.right, frustum.bottom, frustum.top);
}
};
protected _onChangeRight = (value: string): void => {
const right = getNumber(value);
const frustum = this._getCamera()?.frustums[0];
if (right !== null && frustum) {
this._setBounds(frustum.left, right, frustum.bottom, frustum.top);
}
};
protected _onChangeBottom = (value: string): void => {
const bottom = getNumber(value);
const frustum = this._getCamera()?.frustums[0];
if (bottom !== null && frustum) {
this._setBounds(frustum.left, frustum.right, bottom, frustum.top);
}
};
protected _onChangeTop = (value: string): void => {
const top = getNumber(value);
const frustum = this._getCamera()?.frustums[0];
if (top !== null && frustum) {
this._setBounds(frustum.left, frustum.right, frustum.bottom, top);
}
};
protected _onChangeVerticalFov = (value: string): void => {
const angle = getNumber(value);
const camera = this._getCamera();
if (!camera || angle === null || angle <= 0 || angle >= 180) return;
camera.setViewAngle(angle);
camera.update();
this._syncFrustumEntityScale();
this.refresh();
};
protected _onChangeHorizontalFov = (value: string): void => {
const angle = getNumber(value);
const camera = this._getCamera();
if (!camera || angle === null || angle <= 0 || angle >= 180) return;
camera.setHorizontalViewAngle(angle);
camera.update();
this._syncFrustumEntityScale();
this.refresh();
};
protected _setViewportSize(width: number, height: number): void {
const camera = this._getCamera();
if (!camera || width < 1 || height < 1) return;
camera.setViewportSize(width, height);
this._syncFrustumEntityScale();
this.refresh();
}
protected _onChangeViewportWidth = (value: string): void => {
const width = getNumber(value);
const camera = this._getCamera();
if (width !== null && camera) {
this._setViewportSize(width, camera.height);
}
};
protected _onChangeViewportHeight = (value: string): void => {
const height = getNumber(value);
const camera = this._getCamera();
if (height !== null && camera) {
this._setViewportSize(camera.width, height);
}
};
protected _onChangeOrthographic = (isOrthographic: boolean): void => {
if (this._depthCamera) {
this._depthCamera.isOrthographic = isOrthographic;
this._syncFrustumEntityScale();
this.refresh();
}
};
protected _onChangeFocusDistance = (value: string): void => {
const focusDistance = getNumber(value);
if (this._depthCamera && focusDistance !== null) {
this._depthCamera.focusDistance = focusDistance;
this._syncFrustumEntityScale();
this.refresh();
}
};
protected _onChangeShowFrustum = (showFrustum: boolean): void => {
if (this._depthCamera) {
this._depthCamera.showFrustum = showFrustum;
}
};
protected _onChangeShowFootprint = (showFootprint: boolean): void => {
if (this._depthCamera) {
this._depthCamera.showFootprint = showFootprint;
}
};
protected _onChangeBias = (value: string): void => {
const depthBiasWorld = getNumber(value);
if (this._depthCamera && depthBiasWorld !== null) {
this._depthCamera.depthBiasWorld = depthBiasWorld;
}
};
protected _onChangeNormalBias = (value: string): void => {
const normalBias = getNumber(value);
if (this._depthCamera && normalBias !== null) {
this._depthCamera.normalBias = normalBias;
}
};
}