Skip to content
Merged
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
84 changes: 75 additions & 9 deletions packages/dev/core/src/Behaviors/Cameras/interpolatingBehavior.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ import type { Nullable } from "../../types";
import type { Animatable } from "../../Animations/animatable.core";
import { Animation } from "../../Animations/animation";
import type { Camera } from "../../Cameras/camera";
import type { IColor3Like, IColor4Like, IMatrixLike, IQuaternionLike, IVector2Like, IVector3Like } from "../../Maths/math.like";

export type AllowedAnimValue = number | IVector2Like | IVector3Like | IQuaternionLike | IMatrixLike | IColor3Like | IColor4Like | SizeLike | undefined;

/**
* Animate camera property changes with an interpolation effect
Expand Down Expand Up @@ -86,15 +89,17 @@ export class InterpolatingBehavior<C extends Camera = Camera> implements Behavio
this._promiseResolve = undefined;
}

public updateProperties(properties: Map<string, any>): void {
public updateProperties<K extends keyof C>(properties: Map<K, AllowedAnimValue>): void {
properties.forEach((value, key) => {
const animatable = this._animatables.get(key);
animatable && (animatable.target = value);
if (value !== undefined) {
const animatable = this._animatables.get(String(key));
animatable && (animatable.target = value as unknown as any);
}
});
}

public async animatePropertiesAsync(
properties: Map<string, any>,
public async animatePropertiesAsync<K extends keyof C>(
properties: Map<K, AllowedAnimValue>,
transitionDuration: number = this.transitionDuration,
easingFn: EasingFunction = this.easingFunction
): Promise<void> {
Expand All @@ -117,13 +122,74 @@ export class InterpolatingBehavior<C extends Camera = Camera> implements Behavio
};

properties.forEach((value, key) => {
const animation = Animation.CreateAnimation(key, Animation.ANIMATIONTYPE_FLOAT, 60, easingFn);
const animatable = Animation.TransitionTo(key, value, camera, scene, 60, animation, transitionDuration, () => checkClear(key));
if (animatable) {
this._animatables.set(key, animatable);
if (value !== undefined) {
const propertyName = String(key);
const animation = Animation.CreateAnimation(propertyName, GetAnimationType(value), 60, easingFn);
const animatable = Animation.TransitionTo(propertyName, value, camera, scene, 60, animation, transitionDuration, () => checkClear(propertyName));
if (animatable) {
this._animatables.set(propertyName, animatable);
}
}
});
});
return await promise;
}
}

// Structural type-guards (no instanceof)
function IsQuaternionLike(v: any): v is IQuaternionLike {
return v != null && typeof v.x === "number" && typeof v.y === "number" && typeof v.z === "number" && typeof v.w === "number";
}

function IsMatrixLike(v: any): v is IMatrixLike {
return v != null && (Array.isArray((v as any).m) || typeof (v as any).m === "object");
}

function IsVector3Like(v: any): v is IVector3Like {
return v != null && typeof v.x === "number" && typeof v.y === "number" && typeof v.z === "number";
}

function IsVector2Like(v: any): v is IVector2Like {
return v != null && typeof v.x === "number" && typeof v.y === "number";
}

function IsColor3Like(v: any): v is IColor3Like {
return v != null && typeof v.r === "number" && typeof v.g === "number" && typeof v.b === "number";
}

function IsColor4Like(v: any): v is IColor4Like {
return v != null && typeof v.r === "number" && typeof v.g === "number" && typeof v.b === "number" && typeof v.a === "number";
}

export type SizeLike = { width: number; height: number };

function IsSizeLike(v: any): v is SizeLike {
return v != null && typeof v.width === "number" && typeof v.height === "number";
}

const GetAnimationType = (value: AllowedAnimValue): number => {
if (IsQuaternionLike(value)) {
return Animation.ANIMATIONTYPE_QUATERNION;
}
if (IsMatrixLike(value)) {
return Animation.ANIMATIONTYPE_MATRIX;
}
if (IsVector3Like(value)) {
return Animation.ANIMATIONTYPE_VECTOR3;
}
if (IsVector2Like(value)) {
return Animation.ANIMATIONTYPE_VECTOR2;
}
if (IsColor3Like(value)) {
return Animation.ANIMATIONTYPE_COLOR3;
}
if (IsColor4Like(value)) {
return Animation.ANIMATIONTYPE_COLOR4;
}
if (IsSizeLike(value)) {
return Animation.ANIMATIONTYPE_SIZE;
}

// Fallback to float for numbers and unknown shapes
return Animation.ANIMATIONTYPE_FLOAT;
};
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,16 @@ export class GeospatialCameraPointersInput extends OrbitCameraPointersInput {
}
}

public override onDoubleTap(type: string): void {
const scene = this.camera.getScene();
const pickResult = scene.pick(scene.pointerX, scene.pointerY, this.camera.pickPredicate);

if (pickResult.hit && pickResult.pickedPoint) {
const newRadius = this.camera.radius * 0.5; // Zoom to 50% of current distance
void this.camera.flyToAsync(undefined, undefined, newRadius, pickResult.pickedPoint);
}
}

public override onMultiTouch(
pointA: Nullable<PointerTouch>,
pointB: Nullable<PointerTouch>,
Expand Down
68 changes: 63 additions & 5 deletions packages/dev/core/src/Cameras/geospatialCamera.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ import { ComputeLocalBasisToRefs, GeospatialCameraMovement } from "./geospatialC
import type { IVector3Like } from "../Maths/math.like";
import { Vector3CopyToRef, Vector3Dot } from "../Maths/math.vector.functions";
import { Clamp } from "../Maths/math.scalar.functions";
import type { AllowedAnimValue } from "../Behaviors/Cameras/interpolatingBehavior";
import { InterpolatingBehavior } from "../Behaviors/Cameras/interpolatingBehavior";
import type { EasingFunction } from "../Animations/easing";

type CameraOptions = {
planetRadius: number; // Radius of the planet
Expand Down Expand Up @@ -37,12 +40,19 @@ export class GeospatialCamera extends Camera {
private _isViewMatrixDirty: boolean;
private _lookAtVector: Vector3 = new Vector3();

/** Behavior used for smooth flying animations */
private _flyingBehavior: InterpolatingBehavior<GeospatialCamera>;
private _flyToTargets: Map<keyof GeospatialCamera, AllowedAnimValue> = new Map();

constructor(name: string, scene: Scene, options: CameraOptions, pickPredicate?: MeshPredicate) {
super(name, new Vector3(), scene);

this._limits = new GeospatialLimits(options.planetRadius);
this._resetToDefault(this._limits);

this._flyingBehavior = new InterpolatingBehavior();
this.addBehavior(this._flyingBehavior);

this.movement = new GeospatialCameraMovement(scene, this._limits, this.position, this.center, this._lookAtVector, pickPredicate);

this.pickPredicate = pickPredicate;
Expand All @@ -51,7 +61,7 @@ export class GeospatialCamera extends Camera {
}

private _center: Vector3 = new Vector3();
/** The point on the globe that we are anchoring around. If no alternate rotation point is present, this will represent the center of screen*/
/** The point on the globe that we are anchoring around. If no alternate rotation point is supplied, this will represent the center of screen*/
public get center(): Vector3 {
return this._center;
}
Expand Down Expand Up @@ -180,6 +190,54 @@ export class GeospatialCamera extends Camera {
return this.movement.alternateRotationPt ?? this.center;
}

/**
* If camera is actively in flight, will update the target properties and use up the remaining duration from original flyTo call
*
* To start a new flyTo curve entirely, call into flyToAsync again (it will stop the inflight animation)
* @param targetYaw
* @param targetPitch
* @param targetRadius
* @param targetCenter
*/
public updateFlyToDestination(targetYaw?: number, targetPitch?: number, targetRadius?: number, targetCenter?: Vector3): void {
this._flyToTargets.clear();

this._flyToTargets.set("yaw", targetYaw);
this._flyToTargets.set("pitch", targetPitch);
this._flyToTargets.set("radius", targetRadius);
this._flyToTargets.set("center", targetCenter);

this._flyingBehavior.updateProperties(this._flyToTargets);
}

/**
* Animate camera towards passed in property values. If undefined, will use current value
* @param targetYaw
* @param targetPitch
* @param targetRadius
* @param targetCenter
* @param flightDurationMs
* @param easingFunction
* @returns Promise that will return when the animation is complete (or interuppted by pointer input)
*/
public async flyToAsync(
targetYaw?: number,
targetPitch?: number,
targetRadius?: number,
targetCenter?: Vector3,
flightDurationMs: number = 1000,
easingFunction?: EasingFunction
): Promise<void> {
this._flyToTargets.clear();

this._flyToTargets.set("yaw", targetYaw);
this._flyToTargets.set("pitch", targetPitch);
this._flyToTargets.set("radius", targetRadius);
this._flyToTargets.set("center", targetCenter);

return await this._flyingBehavior.animatePropertiesAsync(this._flyToTargets, flightDurationMs, easingFunction);
}

private _limits: GeospatialLimits;
public get limits(): GeospatialLimits {
return this._limits;
Expand Down Expand Up @@ -250,10 +308,10 @@ export class GeospatialCamera extends Camera {
* This rotation keeps the camera oriented towards the globe as it orbits around it. This is different from cameraCentricRotation which is when the camera rotates around its own axis
*/
private _applyGeocentricRotation(): void {
const currentFrameRotationDelta = this.movement.rotationDeltaCurrentFrame;
if (currentFrameRotationDelta.x !== 0 || currentFrameRotationDelta.y !== 0) {
const pitch = currentFrameRotationDelta.x !== 0 ? Clamp(this._pitch + currentFrameRotationDelta.x, 0, 0.5 * Math.PI - Epsilon) : this._pitch;
const yaw = currentFrameRotationDelta.y !== 0 ? this._yaw + currentFrameRotationDelta.y : this._yaw;
const rotationDeltaCurrentFrame = this.movement.rotationDeltaCurrentFrame;
if (rotationDeltaCurrentFrame.x !== 0 || rotationDeltaCurrentFrame.y !== 0) {
const pitch = rotationDeltaCurrentFrame.x !== 0 ? Clamp(this._pitch + rotationDeltaCurrentFrame.x, 0, 0.5 * Math.PI - Epsilon) : this._pitch;
const yaw = rotationDeltaCurrentFrame.y !== 0 ? this._yaw + rotationDeltaCurrentFrame.y : this._yaw;

// TODO: If _geocentricRotationPt is not the center, this will need to be adjusted.
this._setOrientation(yaw, pitch, this._radius, this._geocentricRotationPt);
Expand Down