control/entityGizmo/EntityGizmoScene.tstypescript
/*
* Copyright 2026 Michael Gevlich
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { createEvents, type EventsHandler } from "../../Events";
import { MAX32 } from "../../math";
import { Plane } from "../../math/Plane";
import { Planet } from "../../scene/Planet";
import { Scene } from "../../scene/Scene";
import { Vec2 } from "../../math/Vec2";
import { Vec3 } from "../../math/Vec3";
import { Quat } from "../../math/Quat";
import type { IMouseState } from "../../renderer/RendererEvents";
import { Ellipsoid } from "../../ellipsoid/Ellipsoid";
import { LonLat } from "../../LonLat";
import { Entity } from "../../entity/Entity";
import { MoveAxisEntity } from "./MoveAxisEntity";
import { MovePlaneEntity } from "./MovePlaneEntity";
import { RotateEntity } from "./RotateEntity";
import { Ray } from "../../math/Ray";
import { Sphere } from "../../bv/Sphere";
import { AxisTrackEntity } from "./AxisTrackEntity";
import { CameraLock } from "../CameraLock";
import { Control } from "../Control";
import { EntityCollection } from "../../entity/EntityCollection";
import { SHADE_UNLIT } from "../../shadeModeConstants";
export interface IEntityGizmoSceneParams {
planet?: Planet;
name?: string;
editMode?: EditModeName;
tools?: GizmoTools;
autoSelect?: boolean;
}
export const NATIVE_MODE = 0;
export const YAW_MODE = 1;
/** Rotation mode: the frame the rotation rings are built in and written back through. */
export type EditMode = typeof NATIVE_MODE | typeof YAW_MODE;
export type EditModeName = "native" | "yaw";
/**
* Gizmo tool set, i.e. which handles are shown. Orthogonal to the rotation {@link EditModeName}.
* `translate` - move axes and planes only.
* `full` - move axes, planes and rotation rings.
*/
export type GizmoTools = "translate" | "full";
/** Gizmo handle names. Each handle belongs to exactly one {@link GizmoTransformKind}. */
export type GizmoTransformHandle =
"move_x" | "move_y" | "move_z" | "move_xz" | "move_xy" | "move_zy" | "rotate_pitch" | "rotate_yaw" | "rotate_roll";
/** Kind of transformation a gesture performs. */
export type GizmoTransformKind = "translate" | "rotate";
/** Payload of `transformstart`, `transformchange`, `transformend` and `transformcancel`. */
export interface IGizmoTransformEvent {
entity: Entity;
kind: GizmoTransformKind;
handle: GizmoTransformHandle;
}
const HANDLE_KINDS: Record<GizmoTransformHandle, GizmoTransformKind> = {
move_x: "translate",
move_y: "translate",
move_z: "translate",
move_xz: "translate",
move_xy: "translate",
move_zy: "translate",
rotate_pitch: "rotate",
rotate_yaw: "rotate",
rotate_roll: "rotate"
};
const SUSPENDED_NAVIGATION_CONTROLS = ["navigation", "SimpleNavigation"];
interface IGizmoGesture {
entity: Entity;
handle: GizmoTransformHandle;
kind: GizmoTransformKind;
started: boolean;
cartesian: Vec3;
pitch: number;
yaw: number;
roll: number;
}
function dragSimpleRes(unit: Vec3, clickRay: Ray, currRay: Ray, p0: Vec3, res: Vec3) {
let p1 = p0.add(Vec3.UP),
p2 = p0.add(unit);
let px = new Vec3();
if (clickRay.hitPlaneRes(Plane.fromPoints(p0, p1, p2), px) === Ray.INSIDE) {
let clickCart = Vec3.proj_b_to_a(px, unit);
if (currRay.hitPlaneRes(Plane.fromPoints(p0, p1, p2), px) === Ray.INSIDE) {
let dragCart = Vec3.proj_b_to_a(px, unit);
let dragVec = dragCart.sub(clickCart);
res.copy(p0.add(dragVec));
}
}
}
type EntityGizmoSceneEventsList = [
"mousemove",
"mouseenter",
"mouseleave",
"lclick",
"rclick",
"mclick",
"ldblclick",
"rdblclick",
"mdblclick",
"lup",
"rup",
"mup",
"ldown",
"rdown",
"mdown",
"lhold",
"rhold",
"mhold",
"mousewheel",
"touchmove",
"touchstart",
"touchend",
"doubletouch",
"touchleave",
"touchenter",
"select",
"unselect",
"change",
"position",
"pitch",
"yaw",
"roll",
"scale",
"transformstart",
"transformchange",
"transformend",
"transformcancel"
];
/**
* Scene for translating and rotating entities with a gizmo.
* @class
* @extends {Scene}
* @param {IEntityGizmoSceneParams} [options] - Gizmo scene options.
*/
class EntityGizmoScene extends Scene {
public events: EventsHandler<EntityGizmoSceneEventsList>;
protected _planet: Planet | null;
protected _startPos: Vec2 | null;
protected _startClick: Vec2;
protected _moveLayer: EntityCollection;
protected _planeLayer: EntityCollection;
protected _rotateLayer: EntityCollection;
protected _axisTrackLayer: EntityCollection;
protected _selectedEntity: Entity | null;
protected _selectedEntityCart: Vec3;
protected _selectedEntityRotation: Quat;
protected _selectedEntityPitch: number;
protected _selectedEntityYaw: number;
protected _selectedEntityRoll: number;
protected _clickPos: Vec2;
protected _axisEntity: MoveAxisEntity;
protected _planeEntity: MovePlaneEntity;
protected _rotateEntity: RotateEntity;
protected _axisTrackEntity: AxisTrackEntity;
protected _selectedMove: GizmoTransformHandle | null;
protected _ops: Record<string, (mouseState: IMouseState) => void>;
protected _axisTrackVisibility: boolean;
protected _editMode: EditMode;
protected _tools: GizmoTools;
protected _autoSelect: boolean;
protected _visibility: boolean;
protected _isActivated: boolean;
protected _gesture: IGizmoGesture | null;
protected _suspendedNavigation: string[];
constructor(options: IEntityGizmoSceneParams = {}) {
super(options.name || "EntityGizmoScene");
this.events = createEvents(ENTITY_GIZMO_SCENE_EVENTS);
this._planet = options.planet || null;
this._editMode = this._parseEditMode(options.editMode);
this._tools = options.tools === "full" ? "full" : "translate";
this._autoSelect = options.autoSelect !== false;
this._startPos = null;
this._startClick = new Vec2();
this._axisEntity = new MoveAxisEntity();
this._planeEntity = new MovePlaneEntity();
this._rotateEntity = new RotateEntity();
this._axisTrackEntity = new AxisTrackEntity();
this._moveLayer = new EntityCollection({
scaleByDistance: [0.1, MAX32, MAX32, 0.1],
shadeMode: SHADE_UNLIT,
pickingScale: [5, 1.1, 5],
visibility: false,
depthOrder: 1000
});
this._planeLayer = new EntityCollection({
scaleByDistance: [0.1, MAX32, MAX32, 0.1],
shadeMode: SHADE_UNLIT,
visibility: false,
depthOrder: 1000
});
this._rotateLayer = new EntityCollection({
shadeMode: SHADE_UNLIT,
visibility: false,
depthOrder: 1000,
pickingScale: 5
});
this._selectedEntity = null;
this._clickPos = new Vec2();
this._selectedEntityCart = new Vec3();
this._selectedEntityRotation = Quat.IDENTITY;
this._selectedEntityPitch = 0;
this._selectedEntityYaw = 0;
this._selectedEntityRoll = 0;
this._selectedMove = null;
this._axisTrackVisibility = false;
this._visibility = false;
this._isActivated = false;
this._gesture = null;
this._suspendedNavigation = [];
this._axisTrackLayer = new EntityCollection({
shadeMode: SHADE_UNLIT,
visibility: false,
pickingScale: 5,
pickingEnabled: false,
opacity: 0.6
});
this._ops = {
move_x: this._moveX,
move_y: this._moveY,
move_z: this._moveZ,
move_xz: this._moveXZ,
move_xy: this._moveXY,
move_zy: this._moveZY,
rotate_pitch: this._rotatePitch,
rotate_yaw: this._rotateYaw,
rotate_roll: this._rotateRoll,
scale: this._scale,
scale_x: this._scaleX,
scale_y: this._scaleY,
scale_z: this._scaleZ
};
}
get ellipsoid(): Ellipsoid | undefined {
if (this._planet) {
return this._planet.ellipsoid;
}
}
get planet(): Planet | null {
return this._planet;
}
get editMode(): EditMode {
return this._editMode;
}
set editMode(mode: EditMode) {
this._editMode = mode;
if (this._selectedEntity) {
let cart = this._selectedEntity.getAbsoluteCartesian();
this._selectedEntityRotation = this._getEntityRotation(this._selectedEntity, cart);
this._setRotateEntityCartesian3v(this._selectedEntity, cart);
}
}
/** Gizmo tool set: which handles are shown. */
get tools(): GizmoTools {
return this._tools;
}
set tools(tools: GizmoTools) {
if (tools === this._tools) return;
this.cancelTransform();
this._tools = tools;
this._applyVisibility();
}
/**
* When `false`, a left click neither selects a map entity nor drops the current selection.
*/
get autoSelect(): boolean {
return this._autoSelect;
}
set autoSelect(autoSelect: boolean) {
this._autoSelect = autoSelect;
}
protected get _rotateEnabled(): boolean {
return this._tools === "full";
}
public bindPlanet(planet: Planet) {
this._planet = planet;
}
protected _parseEditMode(mode: EditModeName | undefined): EditMode {
if (mode === "native") {
return NATIVE_MODE;
} else if (mode === "yaw") {
return YAW_MODE;
}
return NATIVE_MODE;
}
public override init() {
this.activate();
}
public override onremove() {
this.deactivate();
}
protected _addAxisLayers() {
this._moveLayer.addTo(this);
this._planeLayer.addTo(this);
this._rotateLayer.addTo(this);
this._axisTrackLayer.addTo(this);
this._moveLayer.add(this._axisEntity);
this._moveLayer.events.on("mouseenter", this._onAxisLayerMouseEnter);
this._moveLayer.events.on("mouseleave", this._onAxisLayerMouseLeave);
this._moveLayer.events.on("ldown", this._onAxisLayerLDown);
this._planeLayer.add(this._planeEntity);
this._planeLayer.events.on("mouseenter", this._onPlaneLayerMouseEnter);
this._planeLayer.events.on("mouseleave", this._onPlaneLayerMouseLeave);
this._planeLayer.events.on("ldown", this._onPlaneLayerLDown);
this._rotateLayer.add(this._rotateEntity);
this._rotateLayer.events.on("mouseenter", this._onRotateLayerMouseEnter);
this._rotateLayer.events.on("mouseleave", this._onRotateLayerMouseLeave);
this._rotateLayer.events.on("ldown", this._onRotateLayerLDown);
this._axisTrackLayer.add(this._axisTrackEntity);
}
protected _onAxisLayerMouseEnter = (e: IMouseState) => {
this.renderer!.handler!.canvas!.style.cursor = "pointer";
e.pickingObject.setColorHTML(e.pickingObject.properties.style.selectColor);
};
protected _onAxisLayerMouseLeave = (e: IMouseState) => {
this.renderer!.handler!.canvas!.style.cursor = "default";
e.pickingObject.setColorHTML(e.pickingObject.properties.style.color);
};
/**
* Suspends the navigation controls that are active right now, so that a drag gesture
* does not fight with the camera. The suspended set is remembered and restored by
* {@link EntityGizmoScene._navRestore}, which keeps controls that were already
* deactivated beforehand deactivated.
* @protected
*/
protected _navSuspend() {
if (!this.renderer) return;
this._suspendedNavigation = [];
for (let i = 0; i < SUSPENDED_NAVIGATION_CONTROLS.length; i++) {
let name = SUSPENDED_NAVIGATION_CONTROLS[i];
let control = this.renderer.controls[name] as (Control & { stop?: () => void }) | undefined;
if (control && control.isActive()) {
this._suspendedNavigation.push(name);
control.deactivate();
// Drops accumulated velocity and grabbed points, so no pressed button state survives.
control.stop && control.stop();
}
}
}
protected _navRestore() {
if (this.renderer) {
for (let i = 0; i < this._suspendedNavigation.length; i++) {
let control = this.renderer.controls[this._suspendedNavigation[i]];
control && control.activate();
}
}
this._suspendedNavigation = [];
}
protected _onAxisLayerLDown = (e: IMouseState) => {
this._clickPos = e.pos.clone();
if (this._selectedEntity) {
this._selectedEntityCart = this._selectedEntity.getAbsoluteCartesian();
}
this._beginGesture(e.pickingObject.properties.opName);
};
protected _onPlaneLayerMouseEnter = (e: IMouseState) => {
this.renderer!.handler!.canvas!.style.cursor = "pointer";
e.pickingObject.geoObject.setColorHTML(e.pickingObject.properties.style.selectColor);
};
protected _onPlaneLayerMouseLeave = (e: IMouseState) => {
this.renderer!.handler!.canvas!.style.cursor = "default";
e.pickingObject.geoObject.setColorHTML(e.pickingObject.properties.style.color);
};
protected _onPlaneLayerLDown = (e: IMouseState) => {
this._clickPos = e.pos.clone();
if (this._selectedEntity) {
this._selectedEntityCart = this._selectedEntity.getAbsoluteCartesian();
}
this._beginGesture(e.pickingObject.properties.opName);
};
protected _onRotateLayerMouseEnter = (e: IMouseState) => {
this.renderer!.handler!.canvas!.style.cursor = "pointer";
e.pickingObject.polyline!.setColorHTML(e.pickingObject.properties.style.selectColor);
};
protected _onRotateLayerMouseLeave = (e: IMouseState) => {
this.renderer!.handler!.canvas!.style.cursor = "default";
e.pickingObject.polyline!.setColorHTML(e.pickingObject.properties.style.color);
};
protected _onRotateLayerLDown = (e: IMouseState) => {
this._clickPos = e.pos.clone();
if (this._selectedEntity) {
this._selectedEntityCart = this._selectedEntity.getAbsoluteCartesian();
this._selectedEntityRotation = this._getEntityRotation(this._selectedEntity, this._selectedEntityCart);
this._selectedEntityPitch = this._selectedEntity.getAbsolutePitch();
this._selectedEntityYaw = this._selectedEntity.getAbsoluteYaw();
this._selectedEntityRoll = this._selectedEntity.getAbsoluteRoll();
}
this._beginGesture(e.pickingObject.properties.opName);
};
protected _onMouseMove = (e: IMouseState) => {
if (!this._gesture) return;
if (!e.leftButtonHold && !e.leftButtonDown) {
// The pointer has been released somewhere we did not hear about.
this._endGesture();
return;
}
if (!this._isEntityAttached(this._gesture.entity)) {
this.cancelTransform();
return;
}
if (this._selectedEntity && this._selectedMove && this._ops[this._selectedMove]) {
this._ops[this._selectedMove](e);
}
};
protected _removeAxisLayers() {
this._moveLayer.events.off("mouseenter", this._onAxisLayerMouseEnter);
this._moveLayer.events.off("mouseleave", this._onAxisLayerMouseLeave);
this._moveLayer.events.off("ldown", this._onAxisLayerLDown);
this._planeLayer.events.off("mouseenter", this._onPlaneLayerMouseEnter);
this._planeLayer.events.off("mouseleave", this._onPlaneLayerMouseLeave);
this._planeLayer.events.off("ldown", this._onPlaneLayerLDown);
this._rotateLayer.events.off("mouseenter", this._onRotateLayerMouseEnter);
this._rotateLayer.events.off("mouseleave", this._onRotateLayerMouseLeave);
this._rotateLayer.events.off("ldown", this._onRotateLayerLDown);
this._moveLayer.remove();
this._planeLayer.remove();
this._rotateLayer.remove();
this._axisTrackLayer.remove();
}
public activate() {
if (this._isActivated || !this.renderer) return;
this._isActivated = true;
this.renderer.events.on("lclick", this._onLclick);
this.renderer.events.on("lup", this._onLUp);
this.renderer.events.on("mousemove", this._onMouseMove);
this.renderer.events.on("touchcancel", this._onTouchCancel);
this.renderer.events.on("forwardpass", this._onForwardpass, this);
this._addAxisLayers();
}
public deactivate() {
if (!this._isActivated) return;
this._isActivated = false;
this.cancelTransform();
this._unbindGestureListeners();
this._navRestore();
if (this._selectedEntity) {
this.unselect();
}
if (this.renderer) {
this.renderer.events.off("forwardpass", this._onForwardpass);
this.renderer.events.off("lclick", this._onLclick);
this.renderer.events.off("lup", this._onLUp);
this.renderer.events.off("mousemove", this._onMouseMove);
this.renderer.events.off("touchcancel", this._onTouchCancel);
}
this._selectedEntity = null;
this._visibility = false;
this._applyVisibility();
this._setAxisTrackVisibility(false);
this._removeAxisLayers();
}
protected _setAxisTrackVisibility(visibility: boolean) {
if (visibility !== this._axisTrackVisibility) {
this._axisTrackVisibility = visibility;
this._axisTrackLayer.setVisibility(visibility);
}
}
public setVisibility(visibility: boolean) {
this._visibility = visibility;
this._applyVisibility();
this.unlockView();
}
protected _applyVisibility() {
this._moveLayer.setVisibility(this._visibility);
this._planeLayer.setVisibility(this._visibility);
this._rotateLayer.setVisibility(this._visibility && this._rotateEnabled);
}
public readyToEdit(entity: Entity): boolean {
return !entity.properties || !entity.properties.noEdit;
}
public select(entity: Entity) {
if (
(!this._selectedEntity || (this._selectedEntity && !entity.isEqual(this._selectedEntity))) &&
this.readyToEdit(entity)
) {
if (this._selectedEntity) {
this.unselect();
}
this._selectedEntity = entity;
this.renderer && this.renderer.setRelativeCenter();
this.setVisibility(true);
this.events.dispatch(this.events.select, this._selectedEntity);
}
}
public unselect() {
this.cancelTransform();
this.setVisibility(false);
let selectedEntity = this._selectedEntity;
this._selectedEntity = null;
this.events.dispatch(this.events.unselect, selectedEntity);
}
/**
* Returns `true` when the entity is one of the gizmo handles.
* @protected
*/
protected _isGizmoEntity(entity: Entity): boolean {
let ec = entity.entityCollection;
return (
ec === this._moveLayer ||
ec === this._planeLayer ||
ec === this._rotateLayer ||
ec === this._axisTrackLayer
);
}
/**
* `lclick` fires on the left button release without dragging, so dropping the selection
* here does not interfere with the map navigation.
* @protected
*/
protected _onLclick = (e: IMouseState) => {
if (!this._autoSelect || this._gesture) return;
let entity = e.pickingObject instanceof Entity ? e.pickingObject : null;
if (entity && this.readyToEdit(entity)) {
this.select(entity);
} else if (this._selectedEntity && (!entity || !this._isGizmoEntity(entity))) {
this.unselect();
}
};
public clear() {
this.removeEntityCollection(this._moveLayer);
this.removeEntityCollection(this._planeLayer);
this.removeEntityCollection(this._rotateLayer);
this.removeEntityCollection(this._axisTrackLayer);
}
protected _onForwardpass = () => {
if (this._selectedEntity) {
if (!this._isEntityAttached(this._selectedEntity)) {
this.unselect();
return;
}
let cart = this._selectedEntity.getAbsoluteCartesian();
this._axisEntity.setCartesian3v(cart);
this._planeEntity.setCartesian3v(cart);
if (this._rotateEnabled) {
this._setRotateEntityCartesian3v(this._selectedEntity, cart);
}
this._axisTrackEntity.setCartesian3v(cart);
}
};
//
// Gesture
//
/**
* Returns `true` when a drag gesture is running right now.
* @public
*/
public isTransforming(): boolean {
return this._gesture !== null;
}
/**
* Returns the running gesture description or `null`.
* @public
*/
public getActiveTransform(): IGizmoTransformEvent | null {
return this._gesture ? this._gestureEvent(this._gesture) : null;
}
/**
* Cancels the current gesture and restores the entity transform when possible.
* @public
* @returns {boolean} Whether a gesture was cancelled.
*/
public cancelTransform(): boolean {
let gesture = this._gesture;
if (!gesture) return false;
this._gesture = null;
this._selectedMove = null;
this._unbindGestureListeners();
this._setAxisTrackVisibility(false);
this._navRestore();
if (gesture.started && this._isEntityAttached(gesture.entity)) {
this._restoreGesture(gesture);
}
if (gesture.started) {
this.events.dispatch(this.events.transformcancel, this._gestureEvent(gesture));
}
return true;
}
protected _gestureEvent(gesture: IGizmoGesture): IGizmoTransformEvent {
return {
entity: gesture.entity,
kind: gesture.kind,
handle: gesture.handle
};
}
protected _beginGesture(handle: GizmoTransformHandle) {
if (!this._selectedEntity || !HANDLE_KINDS[handle]) return;
this.cancelTransform();
let entity = this._selectedEntity;
this._selectedMove = handle;
this._gesture = {
entity,
handle,
kind: HANDLE_KINDS[handle],
started: false,
cartesian: entity.getCartesian().clone(),
pitch: entity.getPitch(),
yaw: entity.getYaw(),
roll: entity.getRoll()
};
if (this._gesture.kind === "translate") {
this._setAxisTrackVisibility(true);
}
this._bindGestureListeners();
this._navSuspend();
}
/**
* Dispatches `transformstart` right before the very first entity change of the gesture.
* @protected
*/
protected _beginChange() {
let gesture = this._gesture;
if (gesture && !gesture.started) {
gesture.started = true;
this.events.dispatch(this.events.transformstart, this._gestureEvent(gesture));
}
}
protected _afterChange() {
if (this._gesture) {
this.events.dispatch(this.events.transformchange, this._gestureEvent(this._gesture));
}
}
protected _endGesture() {
let gesture = this._gesture;
if (!gesture) return;
this._gesture = null;
this._selectedMove = null;
this._unbindGestureListeners();
this._setAxisTrackVisibility(false);
this._navRestore();
if (gesture.started) {
this.events.dispatch(this.events.transformend, this._gestureEvent(gesture));
}
}
protected _restoreGesture(gesture: IGizmoGesture) {
if (gesture.kind === "translate") {
gesture.entity.setCartesian3v(gesture.cartesian);
} else {
gesture.entity.setPitchYawRoll(gesture.pitch, gesture.yaw, gesture.roll);
}
}
protected _isEntityAttached(entity: Entity): boolean {
return Boolean(entity.entityCollection || entity._layer);
}
protected _bindGestureListeners() {
if (typeof window === "undefined") return;
window.addEventListener("pointerup", this._onWindowPointerUp, true);
window.addEventListener("pointercancel", this._onWindowPointerCancel, true);
window.addEventListener("blur", this._onWindowBlur);
}
protected _unbindGestureListeners() {
if (typeof window === "undefined") return;
window.removeEventListener("pointerup", this._onWindowPointerUp, true);
window.removeEventListener("pointercancel", this._onWindowPointerCancel, true);
window.removeEventListener("blur", this._onWindowBlur);
}
protected _onWindowPointerUp = () => {
this._endGesture();
};
protected _onWindowPointerCancel = () => {
this.cancelTransform();
};
protected _onWindowBlur = () => {
this.cancelTransform();
};
protected _onLUp = () => {
this._endGesture();
};
protected _onTouchCancel = () => {
this.cancelTransform();
};
//
// Operations
//
protected _moveX = (e: IMouseState) => {
if (!this._selectedEntity) return;
let cam = this.renderer!.activeCamera;
let p0 = this._selectedEntityCart;
let clickRay = cam.getRay2v(this._clickPos);
let currRay = cam.getRay2v(e.pos);
let px = new Vec3();
if (this.planet) {
let clickCart = clickRay.hitSphere(new Sphere(p0.length(), new Vec3()))!;
let currCart = currRay.hitSphere(new Sphere(p0.length(), new Vec3()))!;
if (!currCart) return;
let rot = Quat.getRotationBetweenVectors(clickCart.normal(), currCart.normal());
px = rot.mulVec3(p0);
if (this.ellipsoid) {
let p0_lonLat = this.ellipsoid.cartesianToLonLat(p0)!;
let px_lonLat = this.ellipsoid.cartesianToLonLat(px)!;
this.ellipsoid.lonLatToCartesianRes(new LonLat(px_lonLat.lon, p0_lonLat.lat, p0_lonLat.height), px);
}
} else {
dragSimpleRes(Vec3.UNIT_X, clickRay, currRay, p0, px);
}
this._applyPosition(px);
};
protected _moveY = (e: IMouseState) => {
if (!this._selectedEntity) return;
let cam = this.renderer!.activeCamera;
let p0 = this._selectedEntityCart;
let groundNormal = Vec3.UP;
if (this.planet) {
groundNormal = this._axisEntity.getY();
}
let p1 = p0.add(groundNormal);
let p2 = p0.add(cam.getRight());
let px = new Vec3();
let clickRay = cam.getRay2v(this._clickPos);
let currRay = cam.getRay2v(e.pos);
if (clickRay.hitPlaneRes(Plane.fromPoints(p0, p1, p2), px) === Ray.INSIDE) {
let clickCart = Vec3.proj_b_to_a(px, groundNormal);
if (currRay.hitPlaneRes(Plane.fromPoints(p0, p1, p2), px) === Ray.INSIDE) {
let dragCart = Vec3.proj_b_to_a(px, groundNormal);
let dragVec = dragCart.sub(clickCart);
let pos = this._selectedEntityCart.add(dragVec);
this._applyPosition(pos);
}
}
};
protected _moveZ = (e: IMouseState) => {
if (!this._selectedEntity) return;
let cam = this.renderer!.activeCamera;
let p0 = this._selectedEntityCart;
let clickRay = cam.getRay2v(this._clickPos);
let currRay = cam.getRay2v(e.pos);
let px = new Vec3();
if (this.planet) {
let clickCart = clickRay.hitSphere(new Sphere(p0.length(), new Vec3()))!;
let currCart = currRay.hitSphere(new Sphere(p0.length(), new Vec3()))!;
if (!currCart) return;
let rot = Quat.getRotationBetweenVectors(clickCart.normal(), currCart.normal());
px = rot.mulVec3(p0);
if (this.ellipsoid) {
let p0_lonLat = this.ellipsoid.cartesianToLonLat(p0)!;
let px_lonLat = this.ellipsoid.cartesianToLonLat(px)!;
this.ellipsoid.lonLatToCartesianRes(new LonLat(p0_lonLat.lon, px_lonLat.lat, p0_lonLat.height), px);
}
} else {
dragSimpleRes(Vec3.UNIT_Z, clickRay, currRay, p0, px);
}
this._applyPosition(px);
};
protected _moveXZ = (e: IMouseState) => {
if (!this._selectedEntity) return;
let cam = this.renderer!.activeCamera;
let p0 = this._selectedEntityCart;
let clickRay = cam.getRay2v(this._clickPos);
let currRay = cam.getRay2v(e.pos);
let px = new Vec3();
if (this.planet) {
let clickCart = clickRay.hitSphere(new Sphere(p0.length(), new Vec3()))!;
let currCart = currRay.hitSphere(new Sphere(p0.length(), new Vec3()))!;
if (!currCart) return;
let rot = Quat.getRotationBetweenVectors(clickCart.normal(), currCart.normal());
px = rot.mulVec3(p0);
if (this.ellipsoid) {
let lonLat = this.ellipsoid.cartesianToLonLat(px)!;
let height = this.ellipsoid.cartesianToLonLat(p0)!.height;
this.ellipsoid.lonLatToCartesianRes(new LonLat(lonLat.lon, lonLat.lat, height), px);
}
} else {
let p1 = p0.add(Vec3.UNIT_X),
p2 = p0.add(Vec3.UNIT_Z);
let clickCart = new Vec3(),
dragCart = new Vec3();
if (clickRay.hitPlaneRes(Plane.fromPoints(p0, p1, p2), clickCart) === Ray.INSIDE) {
if (currRay.hitPlaneRes(Plane.fromPoints(p0, p1, p2), dragCart) === Ray.INSIDE) {
let dragVec = dragCart.sub(clickCart);
px = p0.add(dragVec);
}
}
}
this._applyPosition(px);
};
protected _moveXY = (e: IMouseState) => {};
protected _moveZY = (e: IMouseState) => {};
/**
* Applies a new absolute position to the selected entity and emits the gesture events
* around the change. The `position` event receives the applied `pos` and the entity.
* @protected
* @param {Vec3} pos - New absolute cartesian position.
*/
protected _applyPosition(pos: Vec3) {
let entity = this._selectedEntity!;
this._beginChange();
entity.setAbsoluteCartesian3v(pos);
this.events.dispatch(this.events.position, pos, entity);
this.events.dispatch(this.events.change, entity);
this._afterChange();
}
public override getFrameRotation(cartesian: Vec3): Quat {
return this._planet ? this._planet.getFrameRotation(cartesian) : super.getFrameRotation(cartesian);
}
protected _isPlanetEntity(entity: Entity): boolean {
const scene = entity.entityCollection?.scene as Planet | undefined;
const hasPlanetScene = Boolean(scene?.ellipsoid);
const hasFallbackPlanet = Boolean(this._planet);
return hasPlanetScene || hasFallbackPlanet;
}
protected _isYawMode(): boolean {
return this.editMode === YAW_MODE;
}
protected _getYawModeFrameRotation(entity: Entity, cart: Vec3): Quat {
const scene = entity.entityCollection?.scene as Planet | undefined;
if (scene?.ellipsoid) {
return scene.getFrameRotation(cart);
}
if (this._planet) {
return this._planet.getFrameRotation(cart);
}
return Quat.getLookRotation(Vec3.FORWARD, Vec3.UP);
}
protected _getEntityRotation(entity: Entity, cart: Vec3): Quat {
return this._isYawMode()
? new Quat().setPitchYawRoll(0, entity.getAbsoluteYaw(), 0, this._getYawModeFrameRotation(entity, cart))
: entity.getAbsoluteRotation();
}
protected _setRotateEntityCartesian3v(entity: Entity, cart: Vec3) {
if (this._isYawMode()) {
this._rotateEntity.setYawCartesian3v(cart, entity.getAbsoluteYaw());
} else {
this._rotateEntity.setCartesian3v(cart, this._getEntityRotation(entity, cart));
}
}
protected _setEntityRotation(entity: Entity, rotation: Quat, cart: Vec3) {
if (entity.parent && entity.relativePosition) {
entity.setAbsoluteRotation(rotation);
return;
}
let rot = this._isPlanetEntity(entity)
? this.getFrameRotation(cart).conjugate().inverse().mul(rotation)
: rotation;
entity.setAbsolutePitch(rot.getPitch());
entity.setAbsoluteYaw(rot.getYaw());
entity.setAbsoluteRoll(rot.getRoll());
}
protected _rotatePitch = (e: IMouseState) => {
if (!this._selectedEntity) return;
let cam = this.renderer!.activeCamera;
let p0 = this._selectedEntityCart;
let norm = this._selectedEntityRotation.mulVec3(new Vec3(1, 0, 0)).normalize();
let pl = new Plane(p0, norm);
let clickCart = new Vec3(),
dragCart = new Vec3();
if (cam.getRay2v(this._clickPos).hitPlaneRes(pl, clickCart) === Ray.INSIDE) {
if (cam.getRay2v(e.pos).hitPlaneRes(pl, dragCart) === Ray.INSIDE) {
let c0 = clickCart.sub(p0).normalize(),
c1 = dragCart.sub(p0).normalize();
let sig = Math.sign(c0.cross(c1).dot(norm));
let angle = Math.acos(c0.dot(c1));
let deg = this._selectedEntityPitch + sig * angle;
this._beginChange();
if (this._isYawMode()) {
this._selectedEntity.setAbsolutePitch(deg);
} else {
let rot = Quat.axisAngleToQuat(norm, sig * angle).mul(this._selectedEntityRotation);
this._setEntityRotation(this._selectedEntity, rot, p0);
}
this.events.dispatch(this.events.pitch, deg, this._selectedEntity);
this.events.dispatch(this.events.change, this._selectedEntity);
this._afterChange();
}
}
};
protected _rotateYaw = (e: IMouseState) => {
if (!this._selectedEntity) return;
let cam = this.renderer!.activeCamera;
let p0 = this._selectedEntityCart;
let norm = this._selectedEntityRotation.mulVec3(new Vec3(0, 1, 0)).normalize();
let pl = new Plane(p0, norm);
let clickCart = new Vec3(),
dragCart = new Vec3();
if (cam.getRay2v(this._clickPos).hitPlaneRes(pl, clickCart) === Ray.INSIDE) {
if (cam.getRay2v(e.pos).hitPlaneRes(pl, dragCart) === Ray.INSIDE) {
let c0 = clickCart.sub(p0).normalize(),
c1 = dragCart.sub(p0).normalize();
let sig = Math.sign(c1.cross(c0).dot(norm));
let angle = Math.acos(c0.dot(c1));
let deg = this._selectedEntityYaw + sig * angle;
this._beginChange();
if (this._isYawMode()) {
this._selectedEntity.setAbsoluteYaw(deg);
} else {
let rot = Quat.axisAngleToQuat(norm, -sig * angle).mul(this._selectedEntityRotation);
this._setEntityRotation(this._selectedEntity, rot, p0);
}
this.events.dispatch(this.events.yaw, deg, this._selectedEntity);
this.events.dispatch(this.events.change, this._selectedEntity);
this._afterChange();
}
}
};
protected _rotateRoll = (e: IMouseState) => {
if (!this._selectedEntity) return;
let cam = this.renderer!.activeCamera;
let p0 = this._selectedEntityCart;
let norm = this._selectedEntityRotation.mulVec3(new Vec3(0, 0, 1)).normalize();
let pl = new Plane(p0, norm);
let clickCart = new Vec3(),
dragCart = new Vec3();
if (cam.getRay2v(this._clickPos).hitPlaneRes(pl, clickCart) === Ray.INSIDE) {
if (cam.getRay2v(e.pos).hitPlaneRes(pl, dragCart) === Ray.INSIDE) {
let c0 = clickCart.sub(p0).normalize(),
c1 = dragCart.sub(p0).normalize();
let sig = Math.sign(c0.cross(c1).dot(norm));
let angle = Math.acos(c0.dot(c1));
let deg = this._selectedEntityRoll + sig * angle;
this._beginChange();
if (this._isYawMode()) {
this._selectedEntity.setAbsoluteRoll(deg);
} else {
let rot = Quat.axisAngleToQuat(norm, sig * angle).mul(this._selectedEntityRotation);
this._setEntityRotation(this._selectedEntity, rot, p0);
}
this.events.dispatch(this.events.roll, deg, this._selectedEntity);
this.events.dispatch(this.events.change, this._selectedEntity);
this._afterChange();
}
}
};
protected _scale = (e: IMouseState) => {
let scale = 1;
this.events.dispatch(this.events.scale, scale, this._selectedEntity);
this.events.dispatch(this.events.change, this._selectedEntity);
};
protected _scaleX = (e: IMouseState) => {};
protected _scaleY = (e: IMouseState) => {};
protected _scaleZ = (e: IMouseState) => {};
public getSelectedEntity(): Entity | null {
return this._selectedEntity;
}
public lockView() {
if (this.renderer && this._selectedEntity) {
let camLock = this.renderer.controls.CameraLock as CameraLock;
camLock && camLock.lockView(this._selectedEntity);
}
}
public unlockView() {
if (this.renderer && this._selectedEntity) {
let camLock = this.renderer.controls.CameraLock as CameraLock;
if (camLock) {
camLock.unlockView();
}
}
}
}
const ENTITY_GIZMO_SCENE_EVENTS: EntityGizmoSceneEventsList = [
"mousemove",
"mouseenter",
"mouseleave",
"lclick",
"rclick",
"mclick",
"ldblclick",
"rdblclick",
"mdblclick",
"lup",
"rup",
"mup",
"ldown",
"rdown",
"mdown",
"lhold",
"rhold",
"mhold",
"mousewheel",
"touchmove",
"touchstart",
"touchend",
"doubletouch",
"touchleave",
"touchenter",
"select",
"unselect",
"change",
"position",
"pitch",
"yaw",
"roll",
"scale",
"transformstart",
"transformchange",
"transformend",
"transformcancel"
];
export { EntityGizmoScene };