control/entityGizmo/EntityGizmo.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 { Control, type IControlParams } from "../Control";
import { createEvents, type EventsHandler } from "../../Events";
import { Entity } from "../../entity/Entity";
import {
EntityGizmoScene,
NATIVE_MODE,
YAW_MODE,
type EditModeName,
type GizmoTools,
type IGizmoTransformEvent
} from "./EntityGizmoScene";
export interface IEntityGizmoParams extends IControlParams {
tools?: GizmoTools;
editMode?: EditModeName;
autoSelect?: boolean;
}
type EntityGizmoEventsList = [
"select",
"unselect",
"transformstart",
"transformchange",
"transformend",
"transformcancel"
];
const ENTITY_GIZMO_EVENTS: EntityGizmoEventsList = [
"select",
"unselect",
"transformstart",
"transformchange",
"transformend",
"transformcancel"
];
/**
* On-map gizmo for translating and rotating entities.
* @class
* @extends {Control}
* @param {IEntityGizmoParams} [options] - Gizmo options:
* @param {GizmoTools} [options.tools] - Gizmo tool set, i.e., which handles are shown. `translate`
* shows move axes and planes only, `full` adds rotation rings. Orthogonal to `editMode`.
* @param {EditModeName} [options.editMode] - Rotation mode. `native` rotates in the entity's own
* frame, `yaw` rotates in the local horizon frame. Affects the rotation rings only, so it does
* nothing with `tools: "translate"`.
* @param {boolean} [options.autoSelect] - When `true`, a left click on a map entity selects it and
* a left click anywhere else drops the selection. When `false`, clicks do not
* change the gizmo selection, so the host application stays the only source of the selection.
* Default is `true`.
*/
export class EntityGizmo extends Control {
public events: EventsHandler<EntityGizmoEventsList>;
protected _scene: EntityGizmoScene;
constructor(options: IEntityGizmoParams = {}) {
super({
name: "EntityGizmo",
autoActivate: true,
...options
});
this.events = createEvents(ENTITY_GIZMO_EVENTS, this);
this._scene = new EntityGizmoScene({
name: `entityGizmoScene:${this.__id}`,
tools: options.tools,
editMode: options.editMode,
autoSelect: options.autoSelect
});
this._bindSceneEvents();
}
public override oninit() {
if (this.planet) {
this._scene.bindPlanet(this.planet);
}
}
public override onactivate() {
this.renderer && this.renderer.addScene(this._scene);
}
public override ondeactivate() {
this._scene.deactivate();
this.renderer && this.renderer.removeNode(this._scene);
}
public override onremove() {
this._unbindSceneEvents();
}
/**
* Gizmo tool set: which handles are shown.
* @public
*/
public get tools(): GizmoTools {
return this._scene.tools;
}
public set tools(tools: GizmoTools) {
this._scene.tools = tools;
}
/**
* Rotation mode of the rotation rings. Does nothing with `tools: "translate"`.
* @public
*/
public get editMode(): EditModeName {
return this._scene.editMode === YAW_MODE ? "yaw" : "native";
}
public set editMode(editMode: EditModeName) {
this._scene.editMode = editMode === "yaw" ? YAW_MODE : NATIVE_MODE;
}
/**
* When `false`, a left click neither selects a map entity nor drops the current selection.
* @public
*/
public get autoSelect(): boolean {
return this._scene.autoSelect;
}
public set autoSelect(autoSelect: boolean) {
this._scene.autoSelect = autoSelect;
}
/**
* Shows the gizmo on the given entity. Activates the control when it is inactive.
* Changing the selection cancels any running gesture on the previously selected entity.
* @public
* @param {Entity} entity - Entity to transform.
*/
public selectEntity(entity: Entity): void {
if (!this.isActive()) {
this.activate();
}
this._scene.select(entity);
}
/**
* Hides the gizmo and drops the selection. Cancels a running gesture.
* @public
*/
public unselectEntity(): void {
this._scene.unselect();
}
/**
* Returns the selected entity or `null`.
* @public
* @returns {Entity | null}
*/
public getSelectedEntity(): Entity | null {
return this._scene.getSelectedEntity();
}
/**
* Returns `true` while a drag gesture is running.
* @public
* @returns {boolean}
*/
public isTransforming(): boolean {
return this._scene.isTransforming();
}
/**
* Returns the running gesture description or `null`.
* @public
* @returns {IGizmoTransformEvent | null}
*/
public getActiveTransform(): IGizmoTransformEvent | null {
return this._scene.getActiveTransform();
}
/**
* Cancels the current gesture and restores the entity transform when possible.
* @public
* @returns {boolean} Whether a gesture was cancelled.
*/
public cancelTransform(): boolean {
return this._scene.cancelTransform();
}
protected _bindSceneEvents() {
this._scene.events.on("select", this._onSelect);
this._scene.events.on("unselect", this._onUnselect);
this._scene.events.on("transformstart", this._onTransformStart);
this._scene.events.on("transformchange", this._onTransformChange);
this._scene.events.on("transformend", this._onTransformEnd);
this._scene.events.on("transformcancel", this._onTransformCancel);
}
protected _unbindSceneEvents() {
this._scene.events.off("select", this._onSelect);
this._scene.events.off("unselect", this._onUnselect);
this._scene.events.off("transformstart", this._onTransformStart);
this._scene.events.off("transformchange", this._onTransformChange);
this._scene.events.off("transformend", this._onTransformEnd);
this._scene.events.off("transformcancel", this._onTransformCancel);
}
protected _onSelect = (entity: Entity) => {
this.events.dispatch(this.events.select, entity);
};
protected _onUnselect = (entity: Entity | null) => {
this.events.dispatch(this.events.unselect, entity);
};
protected _onTransformStart = (e: IGizmoTransformEvent) => {
this.events.dispatch(this.events.transformstart, e);
};
protected _onTransformChange = (e: IGizmoTransformEvent) => {
this.events.dispatch(this.events.transformchange, e);
};
protected _onTransformEnd = (e: IGizmoTransformEvent) => {
this.events.dispatch(this.events.transformend, e);
};
protected _onTransformCancel = (e: IGizmoTransformEvent) => {
this.events.dispatch(this.events.transformcancel, e);
};
}