UNPKG

@woosh/meep-engine

Version:

Pure JavaScript game engine. Fully featured and production ready.

314 lines (245 loc) • 8.98 kB
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;