playcanvas
Version:
Open-source WebGL/WebGPU 3D engine for the web
483 lines (482 loc) • 13.1 kB
JavaScript
var __defProp = Object.defineProperty;
var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
import { Vec3 } from "../../core/math/vec3.js";
import { Quat } from "../../core/math/quat.js";
import { TransformGizmo } from "./transform-gizmo.js";
import { BoxShape } from "./shape/box-shape.js";
import { PlaneShape } from "./shape/plane-shape.js";
import { BoxLineShape } from "./shape/boxline-shape.js";
const v1 = new Vec3();
const v2 = new Vec3();
const point = new Vec3();
const delta = new Vec3();
const q = new Quat();
const GLANCE_EPSILON = 0.01;
class ScaleGizmo extends TransformGizmo {
/**
* Creates a new ScaleGizmo object. Use {@link Gizmo.createLayer} to create the layer
* required to display the gizmo.
*
* @param {CameraComponent} camera - The camera component.
* @param {Layer} layer - The layer responsible for rendering the gizmo.
* @example
* const gizmo = new ScaleGizmo(camera, layer);
*/
constructor(camera, layer) {
super(camera, layer, "gizmo:scale");
__publicField(this, "_shapes", {
xyz: new BoxShape(this._device, {
axis: "xyz",
layers: [this._layer.id],
defaultColor: this._theme.shapeBase.xyz,
hoverColor: this._theme.shapeHover.xyz,
disabledColor: this._theme.disabled
}),
yz: new PlaneShape(this._device, {
axis: "x",
layers: [this._layer.id],
rotation: new Vec3(0, 0, -90),
defaultColor: this._theme.shapeBase.x,
hoverColor: this._theme.shapeHover.x,
disabledColor: this._theme.disabled,
depth: 1
}),
xz: new PlaneShape(this._device, {
axis: "y",
layers: [this._layer.id],
rotation: new Vec3(0, 0, 0),
defaultColor: this._theme.shapeBase.y,
hoverColor: this._theme.shapeHover.y,
disabledColor: this._theme.disabled,
depth: 1
}),
xy: new PlaneShape(this._device, {
axis: "z",
layers: [this._layer.id],
rotation: new Vec3(90, 0, 0),
defaultColor: this._theme.shapeBase.z,
hoverColor: this._theme.shapeHover.z,
disabledColor: this._theme.disabled,
depth: 1
}),
x: new BoxLineShape(this._device, {
axis: "x",
layers: [this._layer.id],
rotation: new Vec3(0, 0, -90),
defaultColor: this._theme.shapeBase.x,
hoverColor: this._theme.shapeHover.x,
disabledColor: this._theme.disabled
}),
y: new BoxLineShape(this._device, {
axis: "y",
layers: [this._layer.id],
rotation: new Vec3(0, 0, 0),
defaultColor: this._theme.shapeBase.y,
hoverColor: this._theme.shapeHover.y,
disabledColor: this._theme.disabled
}),
z: new BoxLineShape(this._device, {
axis: "z",
layers: [this._layer.id],
rotation: new Vec3(90, 0, 0),
defaultColor: this._theme.shapeBase.z,
hoverColor: this._theme.shapeHover.z,
disabledColor: this._theme.disabled
})
});
/**
* @type {GizmoSpace}
* @protected
*/
__publicField(this, "_coordSpace", "local");
/**
* Internal mapping from each attached node to their starting scale.
*
* @type {Map<GraphNode, Vec3>}
* @private
*/
__publicField(this, "_nodeScales", /* @__PURE__ */ new Map());
/**
* Internal state if transform should use uniform scaling.
*
* @protected
*/
__publicField(this, "_uniform", false);
/** @override */
__publicField(this, "snapIncrement", 1);
/**
* Flips the planes to face the camera.
*/
__publicField(this, "flipPlanes", true);
/**
* The lower bound for scaling.
*/
__publicField(this, "lowerBoundScale", new Vec3(-Infinity, -Infinity, -Infinity));
this._createTransform();
this.on(TransformGizmo.EVENT_TRANSFORMSTART, () => {
this._storeNodeScales();
this._drag(true);
});
this.on(TransformGizmo.EVENT_TRANSFORMMOVE, (point2) => {
const scaleDelta = delta.copy(point2).sub(this._selectionStartPoint);
if (this.snap) {
scaleDelta.mulScalar(1 / this.snapIncrement);
scaleDelta.round();
scaleDelta.mulScalar(this.snapIncrement);
}
scaleDelta.mulScalar(1 / this._scale);
this._setNodeScales(scaleDelta.add(Vec3.ONE));
});
this.on(TransformGizmo.EVENT_TRANSFORMEND, () => {
this._drag(false);
});
this.on(TransformGizmo.EVENT_NODESDETACH, () => {
this._nodeScales.clear();
});
}
set coordSpace(value) {
}
get coordSpace() {
return this._coordSpace;
}
/**
* Sets the uniform scaling state for planes.
*
* @type {boolean}
*/
set uniform(value) {
this._uniform = value ?? this._uniform;
}
/**
* Gets the uniform scaling state for planes.
*
* @type {boolean}
*/
get uniform() {
return this._uniform;
}
/**
* Sets the axis gap.
*
* @type {number}
*/
set axisGap(value) {
this._setArrowProp("gap", value);
}
/**
* Gets the axis gap.
*
* @type {number}
*/
get axisGap() {
return this._shapes.x.gap;
}
/**
* Sets the axis line thickness.
*
* @type {number}
*/
set axisLineThickness(value) {
this._setArrowProp("lineThickness", value);
}
/**
* Gets the axis line thickness.
*
* @type {number}
*/
get axisLineThickness() {
return this._shapes.x.lineThickness;
}
/**
* Sets the axis line length.
*
* @type {number}
*/
set axisLineLength(value) {
this._setArrowProp("lineLength", value);
}
/**
* Gets the axis line length.
*
* @type {number}
*/
get axisLineLength() {
return this._shapes.x.lineLength;
}
/**
* Sets the axis line tolerance.
*
* @type {number}
*/
set axisLineTolerance(value) {
this._setArrowProp("tolerance", value);
}
/**
* Gets the axis line tolerance.
*
* @type {number}
*/
get axisLineTolerance() {
return this._shapes.x.tolerance;
}
/**
* Sets the axis box size.
*
* @type {number}
*/
set axisBoxSize(value) {
this._setArrowProp("boxSize", value);
}
/**
* Gets the axis box size.
*
* @type {number}
*/
get axisBoxSize() {
return this._shapes.x.boxSize;
}
/**
* Sets the plane size.
*
* @type {number}
*/
set axisPlaneSize(value) {
this._setPlaneProp("size", value);
}
/**
* Gets the plane size.
*
* @type {number}
*/
get axisPlaneSize() {
return this._shapes.yz.size;
}
/**
* Sets the plane gap.
*
* @type {number}
*/
set axisPlaneGap(value) {
this._setPlaneProp("gap", value);
}
/**
* Gets the plane gap.
*
* @type {number}
*/
get axisPlaneGap() {
return this._shapes.yz.gap;
}
/**
* Sets the axis center size.
*
* @type {number}
*/
set axisCenterSize(value) {
this._shapes.xyz.size = value;
}
/**
* Gets the axis center size.
*
* @type {number}
*/
get axisCenterSize() {
return this._shapes.xyz.size;
}
/**
* @type {boolean}
* @deprecated Use {@link flipPlanes} instead.
* @ignore
*/
set flipShapes(value) {
this.flipPlanes = value;
}
/**
* @type {boolean}
* @deprecated Use {@link flipPlanes} instead.
* @ignore
*/
get flipShapes() {
return this.flipPlanes;
}
/**
* @param {string} prop - The property name.
* @param {any} value - The property value.
* @private
*/
_setArrowProp(prop, value) {
this._shapes.x[prop] = value;
this._shapes.y[prop] = value;
this._shapes.z[prop] = value;
}
/**
* @param {string} prop - The property name.
* @param {any} value - The property value.
* @private
*/
_setPlaneProp(prop, value) {
this._shapes.yz[prop] = value;
this._shapes.xz[prop] = value;
this._shapes.xy[prop] = value;
}
/** @private */
_shapesLookAtCamera() {
const cameraDir = this.cameraDir;
let changed = false;
let dot, enabled;
dot = cameraDir.dot(this.root.right);
enabled = 1 - Math.abs(dot) > GLANCE_EPSILON;
if (this._shapes.x.entity.enabled !== enabled) {
this._shapes.x.entity.enabled = enabled;
changed = true;
}
dot = cameraDir.dot(this.root.up);
enabled = 1 - Math.abs(dot) > GLANCE_EPSILON;
if (this._shapes.y.entity.enabled !== enabled) {
this._shapes.y.entity.enabled = enabled;
changed = true;
}
dot = cameraDir.dot(this.root.forward);
enabled = 1 - Math.abs(dot) > GLANCE_EPSILON;
if (this._shapes.z.entity.enabled !== enabled) {
this._shapes.z.entity.enabled = enabled;
changed = true;
}
let flipped;
v1.cross(cameraDir, this.root.right);
enabled = 1 - v1.length() > GLANCE_EPSILON;
if (this._shapes.yz.entity.enabled !== enabled) {
this._shapes.yz.entity.enabled = enabled;
changed = true;
}
flipped = this.flipPlanes ? v2.set(0, +(v1.dot(this.root.forward) < 0), +(v1.dot(this.root.up) < 0)) : Vec3.ZERO;
if (!this._shapes.yz.flipped.equals(flipped)) {
this._shapes.yz.flipped = flipped;
changed = true;
}
v1.cross(cameraDir, this.root.forward);
enabled = 1 - v1.length() > GLANCE_EPSILON;
if (this._shapes.xy.entity.enabled !== enabled) {
this._shapes.xy.entity.enabled = enabled;
changed = true;
}
flipped = this.flipPlanes ? v2.set(+(v1.dot(this.root.up) < 0), +(v1.dot(this.root.right) > 0), 0) : Vec3.ZERO;
if (!this._shapes.xy.flipped.equals(flipped)) {
this._shapes.xy.flipped = flipped;
changed = true;
}
v1.cross(cameraDir, this.root.up);
enabled = 1 - v1.length() > GLANCE_EPSILON;
if (this._shapes.xz.entity.enabled !== enabled) {
this._shapes.xz.entity.enabled = enabled;
changed = true;
}
flipped = this.flipPlanes ? v2.set(+(v1.dot(this.root.forward) > 0), 0, +(v1.dot(this.root.right) > 0)) : Vec3.ZERO;
if (!this._shapes.xz.flipped.equals(flipped)) {
this._shapes.xz.flipped = flipped;
changed = true;
}
if (changed) {
this._renderUpdate = true;
}
}
/**
* @param {boolean} state - The state.
* @private
*/
_drag(state) {
for (const axis in this._shapes) {
const shape = this._shapes[axis];
switch (this.dragMode) {
case "show": {
continue;
}
case "hide": {
shape.visible = !state;
continue;
}
case "selected": {
if (this._selectedAxis === "xyz") {
shape.visible = state ? axis.length === 1 : true;
continue;
}
if (this._selectedIsPlane) {
shape.visible = state ? axis.length === 1 && !axis.includes(this._selectedAxis) : true;
continue;
}
shape.visible = state ? axis === this._selectedAxis : true;
continue;
}
}
}
this._renderUpdate = true;
}
/** @private */
_storeNodeScales() {
for (let i = 0; i < this.nodes.length; i++) {
const node = this.nodes[i];
this._nodeScales.set(node, node.getLocalScale().clone());
}
}
/**
* @param {Vec3} scaleDelta - The point delta.
* @private
*/
_setNodeScales(scaleDelta) {
for (let i = 0; i < this.nodes.length; i++) {
const node = this.nodes[i];
const scale = this._nodeScales.get(node);
if (!scale) {
continue;
}
node.setLocalScale(v1.copy(scale).mul(scaleDelta).max(this.lowerBoundScale));
}
}
/**
* @param {number} x - The x coordinate.
* @param {number} y - The y coordinate.
* @returns {Vec3} The point (space is {@link TransformGizmo#coordSpace}).
* @protected
*/
_screenToPoint(x, y) {
const gizmoPos = this.root.getLocalPosition();
const mouseWPos = this._camera.screenToWorld(x, y, 1);
const axis = this._selectedAxis;
const isPlane = this._selectedIsPlane;
const ray = this._createRay(mouseWPos);
const plane = this._createPlane(axis, axis === "xyz", !isPlane);
if (!plane.intersectsRay(ray, point)) {
return point;
}
if (axis === "xyz") {
const projDir = v2.add2(this._camera.entity.up, this._camera.entity.right).normalize();
const dir = v1.sub2(point, gizmoPos);
const v = dir.length() * dir.normalize().dot(projDir);
point.set(v, v, v);
return point;
}
q.copy(this._rootStartRot).invert().transformVector(point, point);
if (!isPlane) {
this._projectToAxis(point, axis);
}
if (this._uniform && isPlane) {
v1.set(1, 1, 1);
v1[axis] = 0;
point.copy(v1.mulScalar(v1.dot(point)));
point[axis] = 0;
}
return point;
}
/** @override */
prerender() {
super.prerender();
if (!this.enabled) {
return;
}
this._shapesLookAtCamera();
}
}
export {
ScaleGizmo
};