From a44c7fe153d87dc6e0118e9bd4e7dffdf44d5fb2 Mon Sep 17 00:00:00 2001 From: Georgina Halpern Date: Tue, 21 Oct 2025 17:45:12 -0400 Subject: [PATCH] refactor --- .../src/Behaviors/Cameras/framingBehavior.ts | 42 +++++++++++++++---- .../dev/core/src/Cameras/arcRotateCamera.ts | 42 +++++++++++++++++++ 2 files changed, 77 insertions(+), 7 deletions(-) diff --git a/packages/dev/core/src/Behaviors/Cameras/framingBehavior.ts b/packages/dev/core/src/Behaviors/Cameras/framingBehavior.ts index ea3ddd23baf..7bd36197ac6 100644 --- a/packages/dev/core/src/Behaviors/Cameras/framingBehavior.ts +++ b/packages/dev/core/src/Behaviors/Cameras/framingBehavior.ts @@ -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"; @@ -310,9 +310,18 @@ export class FramingBehavior implements Behavior { * @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) { @@ -350,13 +359,12 @@ export class FramingBehavior implements Behavior { // 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; } @@ -393,20 +401,40 @@ export class FramingBehavior implements Behavior { } /** - * 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; diff --git a/packages/dev/core/src/Cameras/arcRotateCamera.ts b/packages/dev/core/src/Cameras/arcRotateCamera.ts index 4c35280bf95..39895d73d6c 100644 --- a/packages/dev/core/src/Cameras/arcRotateCamera.ts +++ b/packages/dev/core/src/Cameras/arcRotateCamera.ts @@ -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 @@ -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. */