@woosh/meep-engine
Version:
Pure JavaScript game engine. Fully featured and production ready.
314 lines (245 loc) • 8.98 kB
JavaScript
import { Frustum as ThreeFrustum } from 'three';
import AABB2 from '../../src/core/geom/2d/aabb/AABB2.js';
import Vector2 from '../../src/core/geom/Vector2.js';
import { makeScreenScissorFrustum } from "../../src/engine/graphics/camera/makeScreenScissorFrustum.js";
import { Camera } from '../../src/engine/graphics/ecs/camera/Camera.js';
import { ShadedGeometrySystem } from "../../src/engine/graphics/ecs/mesh-v2/ShadedGeometrySystem.js";
import { make_ray_from_viewport_position } from "../../src/engine/graphics/make_ray_from_viewport_position.js";
import View from '../../src/view/View.js';
import SelectionAddAction from '../actions/concrete/SelectionAddAction.js';
import SelectionClearAction from '../actions/concrete/SelectionClearAction.js';
import EditorEntity from "../ecs/EditorEntity.js";
import Tool from './engine/Tool.js';
class SelectionView extends View {
constructor() {
super();
this.el = document.createElement('div');
const style = this.el.style;
style.left = "0";
style.top = "0";
style.borderWidth = "1px";
style.borderStyle = "solid";
style.borderColor = "#02ff00";
style.background = "rgba(2,256,0,0.1)";
style.pointerEvents = "none";
style.zIndex = 1000;
style.position = "absolute";
}
}
class SelectionTool extends Tool {
constructor() {
super();
this.name = "marquee_selection";
this.settings = {};
this.anchorPoint = new Vector2();
this.targetPoint = new Vector2();
this.box = new AABB2();
this.selectionMarker = new SelectionView();
}
readPosition(target) {
const pointerPosition = this.engine.devices.pointer.position;
target.copy(pointerPosition).sub(this.engine.gameView.position);
}
start() {
//read mouse position
//set anchor point
this.readPosition(this.anchorPoint);
//run update loop once
this.update();
//add view of selection
document.body.appendChild(this.selectionMarker.el);
}
update() {
this.readPosition(this.targetPoint);
//update selection marker
const x0 = Math.min(this.targetPoint.x, this.anchorPoint.x);
const x1 = Math.max(this.targetPoint.x, this.anchorPoint.x);
const y0 = Math.min(this.targetPoint.y, this.anchorPoint.y);
const y1 = Math.max(this.targetPoint.y, this.anchorPoint.y);
this.box.set(x0, y0, x1, y1);
this.selectionMarker.position.set(x0, y0).add(this.engine.gameView.position);
this.selectionMarker.size.set(x1 - x0, y1 - y0);
}
stop() {
const self = this;
//finish selection
//remove selection view
if (this.selectionMarker.el.parentNode === document.body) {
document.body.removeChild(this.selectionMarker.el);
}
/**
*
* @type {Engine|null}
*/
const engine = this.engine;
//convert selection box to a frustum based on camera view
let camera = null;
const em = engine.entityManager;
/**
*
* @param {Camera} c
* @returns {boolean}
*/
function visitCamera(c) {
if (!c.active) {
//skip inactive camera
return true;
}
camera = c.object;
//TODO check assumption that first camera is the right one
return false;
}
em.dataset.traverseComponents(Camera, visitCamera);
if (camera === null) {
console.error("Couldn't find a camera to project selection box from");
}
let selection;
const editor = this.editor;
const box = this.box;
//if box size is 0, it leads to all planes being converged on one point, to correct this we need to ensure that box dimensions are greater than zero
const dX = Math.abs(box.x0 - box.x1);
const dY = Math.abs(box.y0 - box.y1);
if (dX < SELECTION_MIN_SIZE && dY < SELECTION_MIN_SIZE) {
selection = pickingEntitySelection(new Vector2(box.x0, box.y0), editor.engine, camera);
} else {
if (dX < SELECTION_MIN_SIZE) {
box.x0 -= (SELECTION_MIN_SIZE - dX) / 2;
box.x1 += (SELECTION_MIN_SIZE - dX) / 2;
}
if (dY < SELECTION_MIN_SIZE) {
box.y0 -= (SELECTION_MIN_SIZE - dY) / 2;
box.y1 += (SELECTION_MIN_SIZE - dY) / 2;
}
selection = marqueeSelection(box, editor, camera);
}
console.log("Selected entities", selection);
const actions = editor.actions;
actions.mark();
if (!self.modifiers.shift) {
actions.do(new SelectionClearAction());
}
actions.do(new SelectionAddAction(selection));
}
}
const SELECTION_MIN_SIZE = 0.01;
/**
*
* @param {int} entity
* @param {EntityComponentDataset} dataset
* @returns {int}
*/
function dereferenceEntity(entity, dataset) {
if (dataset.componentTypeMap.indexOf(EditorEntity) === -1) {
//EditorEntity is not registered
return entity;
}
const editorEntity = dataset.getComponent(entity, EditorEntity);
if (editorEntity === undefined) {
return entity;
}
if (editorEntity.referenceEntity !== -1) {
//reference found, deference
return editorEntity.referenceEntity;
} else {
//no reference, just ignore this one
return -1;
}
}
/**
*
* @param {LeafNode|BinaryNode} node
* @returns {number|undefined}
*/
function findEntityOfNode(node) {
const entity = node.entity;
if (typeof entity === "number") {
return entity;
} else {
const parentNode = node.parentNode;
if (parentNode !== null) {
return findEntityOfNode(parentNode);
}
}
}
/**
*
* @param {Vector2} point
* @param {Engine} engine
* @param {THREE.Camera} camera
* @returns {number[]} entities
*/
export function pickingEntitySelection(point, engine, camera) {
const em = engine.entityManager;
const dataset = em.dataset;
// push source by a small amount along the way to prevent selection of elements directly on the NEAR plane
const ray_offset = 0.0001;
const ray = make_ray_from_viewport_position(engine,point);
const ray_origin = ray.origin;
const ray_direction = ray.direction;
ray_origin._add(
ray_direction.x * ray_offset,
ray_direction.y * ray_offset,
ray_direction.z * ray_offset,
)
let bestCandidate = null;
let bestDistance = Infinity;
/**
*
* @param {number} entity
* @param {Vector3} contact
*/
function tryAddEntity(entity, contact) {
const distance = ray.origin.distanceSqrTo(contact);
if (distance < bestDistance) {
bestDistance = distance;
entity = dereferenceEntity(entity, dataset);
if (entity === -1) {
return;
}
bestCandidate = entity;
}
}
/**
*
* @type {ShadedGeometrySystem|undefined}
*/
const sgm = em.getSystem(ShadedGeometrySystem);
if (sgm !== undefined && sgm !== null) {
const hits = sgm.raycast(
ray_origin.x, ray_origin.y, ray_origin.y,
ray_direction.x, ray_direction.y, ray_direction.z
);
for (let i = 0; i < hits.length; i++) {
const hit = hits[i];
tryAddEntity(hit.entity, hit.contact.position);
}
}
return bestCandidate !== null ? [bestCandidate] : [];
}
/**
*
* @param {AABB2} box in screen-space, in pixels
* @param {Editor} editor
* @param {THREE.Camera} camera
*/
function marqueeSelection(box, editor, camera) {
const engine = editor.engine;
const dataset = engine.entityManager.dataset;
const normalizedBox = box.clone();
/**
* @type {Vector2}
*/
const viewportSize = engine.graphics.viewport.size;
/*
need to convert pixel size into normalized viewport coordinates, they range from -1 to 1 and have inverse Y axis compared to html
*/
normalizedBox.x0 = (box.x0 / viewportSize.x) * 2 - 1;
normalizedBox.x1 = (box.x1 / viewportSize.x) * 2 - 1;
normalizedBox.y0 = -(box.y0 / viewportSize.y) * 2 + 1;
normalizedBox.y1 = -(box.y1 / viewportSize.y) * 2 + 1;
const frustum = new ThreeFrustum();
makeScreenScissorFrustum(frustum, normalizedBox, camera);
const selection = [];
return selection;
}
export default SelectionTool;