Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 35 additions & 7 deletions packages/dev/core/src/Behaviors/Cameras/framingBehavior.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type { Behavior } from "../../Behaviors/behavior";
import type { Camera } from "../../Cameras/camera";
import type { ArcRotateCamera } from "../../Cameras/arcRotateCamera";
import type { ArcRotateCamera, BoundingInfoMode } from "../../Cameras/arcRotateCamera";
import { ExponentialEase, EasingFunction } from "../../Animations/easing";
import type { Observer } from "../../Misc/observable";
import { Observable } from "../../Misc/observable";
Expand Down Expand Up @@ -310,9 +310,18 @@ export class FramingBehavior implements Behavior<ArcRotateCamera> {
* @param maximumWorld Determines the bigger position of the bounding box extend
* @param focusOnOriginXZ Determines if the camera should focus on 0 in the X and Z axis instead of the mesh
* @param onAnimationEnd Callback triggered at the end of the framing animation
* @param mode Defines the method used to compute the lower radius ("sphere" or "box"). Default is 'sphere. Pass explicit mode if you want orthographic framing
* @param radiusScaling Defines a scaling factor to apply to the computed radius. Defaults to this._radiusScale
* @returns true if the zoom was done
*/
public zoomOnBoundingInfo(minimumWorld: Vector3, maximumWorld: Vector3, focusOnOriginXZ: boolean = false, onAnimationEnd: Nullable<() => void> = null): boolean {
public zoomOnBoundingInfo(
minimumWorld: Vector3,
maximumWorld: Vector3,
focusOnOriginXZ: boolean = false,
onAnimationEnd: Nullable<() => void> = null,
mode?: BoundingInfoMode,
radiusScaling?: number
): boolean {
let zoomTarget: Vector3;

if (!this._attachedCamera) {
Expand Down Expand Up @@ -350,13 +359,12 @@ export class FramingBehavior implements Behavior<ArcRotateCamera> {
// Small delta ensures camera is not always at lower zoom limit.
let radius = 0;
if (this._mode === FramingBehavior.FitFrustumSidesMode) {
const position = this._calculateLowerRadiusFromModelBoundingSphere(minimumWorld, maximumWorld);
radius = this._calculateLowerRadiusFromModelBoundingInfo(minimumWorld, maximumWorld, mode, radiusScaling);
if (this.autoCorrectCameraLimitsAndSensibility) {
this._attachedCamera.lowerRadiusLimit = radiusWorld.length() + this._attachedCamera.minZ;
}
radius = position;
} else if (this._mode === FramingBehavior.IgnoreBoundsSizeMode) {
radius = this._calculateLowerRadiusFromModelBoundingSphere(minimumWorld, maximumWorld);
radius = this._calculateLowerRadiusFromModelBoundingInfo(minimumWorld, maximumWorld, mode, radiusScaling);
if (this.autoCorrectCameraLimitsAndSensibility && this._attachedCamera.lowerRadiusLimit === null) {
this._attachedCamera.lowerRadiusLimit = this._attachedCamera.minZ;
}
Expand Down Expand Up @@ -393,20 +401,40 @@ export class FramingBehavior implements Behavior<ArcRotateCamera> {
}

/**
* Calculates the lowest radius for the camera based on the bounding box of the mesh.
* Calculates the lowest radius for the camera based on the bounding sphere of the mesh.
* @param minimumWorld
* @param maximumWorld
* @returns The minimum distance from the primary mesh's center point at which the camera must be kept in order
* to fully enclose the mesh in the viewing frustum.
*/
protected _calculateLowerRadiusFromModelBoundingSphere(minimumWorld: Vector3, maximumWorld: Vector3): number {
return this._calculateLowerRadiusFromModelBoundingInfo(minimumWorld, maximumWorld);
}

/**
* Calculates the lowest radius for the camera based on the bounding sphere or box of the mesh.
* @param minimumWorld
* @param maximumWorld
* @param mode defines whether we calculate lowerradius from bounding sphere or box. If undefined, defaults to old spherical behavior. If defined, uses cameras boundingInfo fn
* @param radiusScale Defines a scaling factor to apply to the computed radius. Defaults to this._radiusScale
* @returns The minimum distance from the primary mesh's center point at which the camera must be kept in order
* to fully enclose the mesh in the viewing frustum.
*/
protected _calculateLowerRadiusFromModelBoundingInfo(minimumWorld: Vector3, maximumWorld: Vector3, mode?: BoundingInfoMode, radiusScale?: number): number {
const camera = this._attachedCamera;

if (!camera) {
return 0;
}

let distance = camera._calculateLowerRadiusFromModelBoundingSphere(minimumWorld, maximumWorld, this._radiusScale);
let distance = 0;
if (mode === undefined) {
// For backcompat, call directly into the boundingsphere function if mode is undefined
distance = camera._calculateLowerRadiusFromModelBoundingSphere(minimumWorld, maximumWorld, radiusScale ?? this._radiusScale);
} else {
// If mode is explicitly defined, use new camera function which includes orthographic framing
distance = camera._calculateLowerRadiusFromModelBoundingInfo(minimumWorld, maximumWorld, mode, radiusScale ?? this._radiusScale);
}
if (camera.lowerRadiusLimit && this._mode === FramingBehavior.IgnoreBoundsSizeMode) {
// Don't exceed the requested limit
distance = distance < camera.lowerRadiusLimit ? camera.lowerRadiusLimit : distance;
Expand Down
42 changes: 42 additions & 0 deletions packages/dev/core/src/Cameras/arcRotateCamera.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,9 @@ Node.AddNodeConstructor("ArcRotateCamera", (name, scene) => {
return () => new ArcRotateCamera(name, 0, 0, 1.0, Vector3.Zero(), scene);
});

// Used to indicate boundingInfo for the calculateLowerRadius methods
export type BoundingInfoMode = "sphere" | "box";

/**
* Computes the alpha angle based on the source position and the target position.
* @param offset The directional offset between the source position and the target position
Expand Down Expand Up @@ -1581,6 +1584,45 @@ export class ArcRotateCamera extends TargetCamera {
return Math.max(distanceForHorizontalFrustum, distanceForVerticalFrustum);
}

/**
* @internal
* This expands functionality beyond what _calculateLowerRadiusFromModelBoundingSphere does by
* 1. Offering boundingBox mode which calculates distance according to the bounding box
* 2. Setting orthographic extents on the class
*/
public _calculateLowerRadiusFromModelBoundingInfo(minimumWorld: Vector3, maximumWorld: Vector3, mode: BoundingInfoMode = "sphere", radiusScale: number = 1): number {
// Setting orthographic extents -- this is done regardless of mode
const height = (maximumWorld.y - minimumWorld.y) * 0.5;
const width = (maximumWorld.x - minimumWorld.x) * 0.5;
const depth = (maximumWorld.z - minimumWorld.z) * 0.5;
const aspectRatio = this.getScene().getEngine().getAspectRatio(this);

if (this.mode === Camera.ORTHOGRAPHIC_CAMERA) {
if (aspectRatio < width / height) {
this.orthoRight = width * radiusScale;
this.orthoLeft = -this.orthoRight;
this.orthoTop = this.orthoRight / aspectRatio;
this.orthoBottom = this.orthoLeft / aspectRatio;
} else {
this.orthoRight = height * aspectRatio * radiusScale;
this.orthoLeft = -this.orthoRight * radiusScale;
this.orthoTop = height;
this.orthoBottom = -this.orthoTop;
}
}

if (mode === "box") {
// Formula for setting distance according to the bounding box (centring the first face of the bounding box)
const frustumSlopeY = Math.tan(this.fov / 2);
const frustumSlopeX = frustumSlopeY * aspectRatio;
const distanceForHorizontalFrustum = (radiusScale * width) / frustumSlopeX + depth;
const distanceForVerticalFrustum = (radiusScale * height) / frustumSlopeY + depth;
return Math.max(distanceForHorizontalFrustum, distanceForVerticalFrustum);
} else {
return this._calculateLowerRadiusFromModelBoundingSphere(minimumWorld, maximumWorld, radiusScale);
}
}

/**
* Destroy the camera and release the current resources hold by it.
*/
Expand Down