From f3c87ea2dc73aebbf2ce9a86c6d8438a09b520e6 Mon Sep 17 00:00:00 2001 From: Felix Frank Date: Tue, 9 Jul 2024 15:48:18 +0200 Subject: [PATCH] Added first representation of cube selection state. --- src/grid/Grid.ts | 4 +- src/selections/cubeselection/CubeSelection.ts | 93 ++++++++++ .../handle/edge/CubeSelectionEdgeHandle.ts | 38 ++++ .../handle/plane/CubeSelectionPlaneHandle.ts | 44 +++++ .../cubeselection/mesh/CubeSelectionMesh.ts | 164 ++++++++++++++++++ src/toolbox/select/SelectTool.ts | 84 ++++++++- 6 files changed, 420 insertions(+), 7 deletions(-) create mode 100644 src/selections/cubeselection/CubeSelection.ts create mode 100644 src/selections/cubeselection/handle/edge/CubeSelectionEdgeHandle.ts create mode 100644 src/selections/cubeselection/handle/plane/CubeSelectionPlaneHandle.ts create mode 100644 src/selections/cubeselection/mesh/CubeSelectionMesh.ts diff --git a/src/grid/Grid.ts b/src/grid/Grid.ts index 8ddf0714..7593cfa7 100644 --- a/src/grid/Grid.ts +++ b/src/grid/Grid.ts @@ -1,5 +1,5 @@ import { GRID_SIDE_LINE_COLOR, GRID_CENTER_LINE_COLOR } from "../constant/GridColors.ts"; -import { HELPER_LAYER_MASK } from "../constant/VisibilityLayerMask.ts"; +import { UI_LAYER_MASK } from "../constant/VisibilityLayerMask.ts"; import { GridHelper, Object3D } from "three"; /** @@ -15,7 +15,7 @@ export default class DIVEGrid extends Object3D { const grid = new GridHelper(100, 100, GRID_CENTER_LINE_COLOR, GRID_SIDE_LINE_COLOR); grid.material.depthTest = false; - grid.layers.mask = HELPER_LAYER_MASK; + grid.layers.mask = UI_LAYER_MASK; this.add(grid); } diff --git a/src/selections/cubeselection/CubeSelection.ts b/src/selections/cubeselection/CubeSelection.ts new file mode 100644 index 00000000..defcf2ae --- /dev/null +++ b/src/selections/cubeselection/CubeSelection.ts @@ -0,0 +1,93 @@ +import { Box3, Object3D } from "three"; +import { DIVECubeSelectionMesh } from "./mesh/CubeSelectionMesh"; + +export class DIVECubeSelection extends Object3D { + private _parents: { [id: string]: Object3D }; + private _objectRoot: Object3D; + + private _boundingBox: Box3; + private _boxMesh: DIVECubeSelectionMesh; + + public get objects(): Object3D[] { + return this._objectRoot.children; + } + + public set objects(objects: Object3D[]) { + this._objectRoot.children = objects; + this.calculateBoundingBox(); + this._boxMesh.updateBox(this._boundingBox); + } + + constructor() { + super(); + + this._parents = {}; + this._objectRoot = new Object3D(); + this.add(this._objectRoot); + + this._boundingBox = new Box3() + this._boxMesh = new DIVECubeSelectionMesh(); + this.add(this._boxMesh); + + } + + public Attach(object: Object3D): this { + if (object.parent !== null) { + this._parents[object.uuid] = object.parent; + } + + this.addChild(object); + + return this; + } + + public Detach(object: Object3D): this; + public Detach(object: string): this; + public Detach(object: Object3D | string): this { + if (typeof object === 'string') { + const index = this.children.findIndex((child) => child.uuid === object); + if (index === -1) return this; + + if (this._parents[object]) { + this.removeChild(this.children[index]); + this._parents[object].add(this.children[index]); + delete this._parents[object]; + } else { + this.removeChild(this.children[index]); + } + } else { + if (this._parents[object.uuid]) { + this.removeChild(object); + this._parents[object.uuid].add(object); + delete this._parents[object.uuid]; + } else { + this.removeChild(object); + } + } + + return this; + } + + private addChild(object: Object3D): this { + this._objectRoot.add(object); + this.calculateBoundingBox(); + this._boxMesh.updateBox(this._boundingBox); + + return this; + } + + private removeChild(object: Object3D): this { + this._objectRoot.remove(object); + this.calculateBoundingBox(); + this._boxMesh.updateBox(this._boundingBox); + return this; + } + + private calculateBoundingBox(): void { + this._objectRoot.children.forEach((child) => { + child.updateMatrixWorld(true); + }); + + this._boundingBox.setFromObject(this._objectRoot); + } +} \ No newline at end of file diff --git a/src/selections/cubeselection/handle/edge/CubeSelectionEdgeHandle.ts b/src/selections/cubeselection/handle/edge/CubeSelectionEdgeHandle.ts new file mode 100644 index 00000000..37088134 --- /dev/null +++ b/src/selections/cubeselection/handle/edge/CubeSelectionEdgeHandle.ts @@ -0,0 +1,38 @@ +import { Line2, LineGeometry, LineMaterial } from 'three/examples/jsm/Addons'; +import { HELPER_LAYER_MASK } from '../../../../constant/VisibilityLayerMask'; +import { Color } from 'three'; + +export class DIVECubeSelectionEdgeHandle extends Line2 { + readonly isCubeSelection = true; + readonly isCubeSelectionEdge = true; + + private _color: number; + private _hoverColor: number = 0xff0000; + + constructor(material: LineMaterial) { + super(); + + this.layers.mask = HELPER_LAYER_MASK; + + this.geometry = new LineGeometry(); + this.material = material.clone(); + this._color = this.material.color.getHex(); + + const hsl = new Color(this._color).getHSL({ h: 0, s: 0, l: 0 }); + this._hoverColor = new Color(this._color).setHSL(hsl.h, hsl.s, hsl.l * 1.3).getHex(); + } + + public setPoints(points: [number, number, number, number, number, number]): this { + this.geometry.setPositions(points); + this.geometry.computeBoundingBox(); + return this; + } + + public onPointerEnter(): void { + this.material.color.setHex(this._hoverColor); + } + + public onPointerLeave(): void { + this.material.color.setHex(this._color); + } +} \ No newline at end of file diff --git a/src/selections/cubeselection/handle/plane/CubeSelectionPlaneHandle.ts b/src/selections/cubeselection/handle/plane/CubeSelectionPlaneHandle.ts new file mode 100644 index 00000000..e5a02fcd --- /dev/null +++ b/src/selections/cubeselection/handle/plane/CubeSelectionPlaneHandle.ts @@ -0,0 +1,44 @@ +import { Color, Mesh, MeshBasicMaterial, PlaneGeometry, Vector3, HSL } from 'three'; +import { HELPER_LAYER_MASK } from '../../../../constant/VisibilityLayerMask'; + +export class DIVECubeSelectionPlaneHandle extends Mesh { + readonly isCubeSelection = true; + readonly isCubeSelectionPlane = true; + + private _color: number; + private _hoverColor: number = 0xff0000; + + constructor(material: MeshBasicMaterial) { + super(); + + this.layers.mask = HELPER_LAYER_MASK; + this.geometry = new PlaneGeometry(); + + this.material = material.clone(); + this._color = material.color.getHex(); + + const hsl: HSL = { + h: 0, + s: 0, + l: 0, + }; + new Color(this._color).getHSL(hsl); + this._hoverColor = new Color(this._color).setHSL(hsl.h, hsl.s, hsl.l * 1.2).getHex(); + + + } + + public setPoints(points: [Vector3, Vector3, Vector3, Vector3]): this { + this.geometry.setFromPoints(points); + this.geometry.computeBoundingBox(); + return this; + } + + public onPointerEnter(): void { + (this.material as MeshBasicMaterial).color.setHex(this._hoverColor); + } + + public onPointerLeave(): void { + (this.material as MeshBasicMaterial).color.setHex(this._color); + } +} \ No newline at end of file diff --git a/src/selections/cubeselection/mesh/CubeSelectionMesh.ts b/src/selections/cubeselection/mesh/CubeSelectionMesh.ts new file mode 100644 index 00000000..3762ec82 --- /dev/null +++ b/src/selections/cubeselection/mesh/CubeSelectionMesh.ts @@ -0,0 +1,164 @@ +import { Box3, Color, MeshBasicMaterial, Object3D, Vector3 } from 'three'; +import { LineMaterial } from 'three/examples/jsm/Addons'; +import { DIVECubeSelectionPlaneHandle } from '../handle/plane/CubeSelectionPlaneHandle'; +import { DIVECubeSelectionEdgeHandle } from '../handle/edge/CubeSelectionEdgeHandle'; + +export class DIVECubeSelectionMesh extends Object3D { + private _planeMaterial: MeshBasicMaterial; + private _lineMaterial: LineMaterial; + + /** + * Order: posX, negX, posY, negY, posZ, negZ + */ + private _planes: DIVECubeSelectionPlaneHandle[] = []; + public get planes(): DIVECubeSelectionPlaneHandle[] { + return this._planes; + } + + private _planesNode: Object3D; + private _edgesNode: Object3D; + + constructor() { + super(); + + this._planeMaterial = new MeshBasicMaterial({ + color: 0x4e5cff, + transparent: true, + opacity: 0.2, + }); + + this._planesNode = new Object3D(); + this.add(this._planesNode); + + const hsl = this._planeMaterial.color.getHSL({ h: 0, s: 0, l: 0 }); + this._lineMaterial = new LineMaterial({ + color: new Color().setHSL(hsl.h, hsl.s, hsl.l * 1.5).getHex(), + linewidth: 5, // in world units with size attenuation, pixels otherwise + vertexColors: false, + dashed: false, + alphaToCoverage: true, + }); + + this._edgesNode = new Object3D(); + this.add(this._edgesNode); + } + + public updateBox(box: Box3): void { + this.updateBoxMesh(box); + } + + private updateBoxMesh(box: Box3): void { + this._planes = []; + this._planesNode.clear(); + this._edgesNode.clear(); + + // when there are no children, set the geometry dimensions to 0 + const size = new Vector3(); + box.getSize(size); + if (size.length() === 0) return; + + const center = new Vector3(); + box.getCenter(center); + const width = box.max.x - box.min.x; + const height = box.max.y - box.min.y; + const depth = box.max.z - box.min.z; + + this._planes = [ + new DIVECubeSelectionPlaneHandle(this._planeMaterial).setPoints([ // posX + new Vector3(width / 2, height / 2, - depth / 2), + new Vector3(width / 2, -height / 2, - depth / 2), + new Vector3(width / 2, height / 2, depth / 2), + new Vector3(width / 2, -height / 2, depth / 2), + ]).translateX(center.x).translateY(center.y).translateZ(center.z), + + new DIVECubeSelectionPlaneHandle(this._planeMaterial).setPoints([ // negX + new Vector3(-width / 2, height / 2, depth / 2), + new Vector3(-width / 2, -height / 2, depth / 2), + new Vector3(-width / 2, height / 2, - depth / 2), + new Vector3(-width / 2, -height / 2, - depth / 2), + ]).translateX(center.x).translateY(center.y).translateZ(center.z), + + new DIVECubeSelectionPlaneHandle(this._planeMaterial).setPoints([ // posY + new Vector3(-width / 2, height / 2, - depth / 2), + new Vector3(width / 2, height / 2, - depth / 2), + new Vector3(-width / 2, height / 2, depth / 2), + new Vector3(width / 2, height / 2, depth / 2), + ]).translateX(center.x).translateY(center.y).translateZ(center.z), + + new DIVECubeSelectionPlaneHandle(this._planeMaterial).setPoints([ // negY + new Vector3(-width / 2, -height / 2, - depth / 2), + new Vector3(-width / 2, -height / 2, depth / 2), + new Vector3(width / 2, -height / 2, - depth / 2), + new Vector3(width / 2, -height / 2, depth / 2), + ]).translateX(center.x).translateY(center.y).translateZ(center.z), + + new DIVECubeSelectionPlaneHandle(this._planeMaterial).setPoints([ // posZ + new Vector3(width / 2, -height / 2, depth / 2), + new Vector3(-width / 2, -height / 2, depth / 2), + new Vector3(width / 2, height / 2, depth / 2), + new Vector3(-width / 2, height / 2, depth / 2), + ]).translateX(center.x).translateY(center.y).translateZ(center.z), + + new DIVECubeSelectionPlaneHandle(this._planeMaterial).setPoints([ // negZ + new Vector3(-width / 2, height / 2, - depth / 2), + new Vector3(-width / 2, -height / 2, - depth / 2), + new Vector3(width / 2, height / 2, - depth / 2), + new Vector3(width / 2, -height / 2, - depth / 2), + ]).translateX(center.x).translateY(center.y).translateZ(center.z), + ]; + + this._planesNode.clear(); + this._planesNode.add(...this._planes); + + const points = [ + width / 2, height / 2, depth / 2, + width / 2, height / 2, - depth / 2, + + width / 2, - height / 2, depth / 2, + width / 2, - height / 2, - depth / 2, + + - width / 2, height / 2, depth / 2, + - width / 2, height / 2, - depth / 2, + + - width / 2, - height / 2, depth / 2, + - width / 2, - height / 2, - depth / 2, + + + width / 2, height / 2, depth / 2, + width / 2, - height / 2, depth / 2, + + width / 2, height / 2, - depth / 2, + width / 2, - height / 2, - depth / 2, + + - width / 2, height / 2, depth / 2, + - width / 2, - height / 2, depth / 2, + + - width / 2, height / 2, - depth / 2, + - width / 2, - height / 2, - depth / 2, + + + width / 2, height / 2, depth / 2, + - width / 2, height / 2, depth / 2, + + width / 2, height / 2, - depth / 2, + - width / 2, height / 2, - depth / 2, + + width / 2, - height / 2, depth / 2, + - width / 2, - height / 2, depth / 2, + + width / 2, - height / 2, - depth / 2, + - width / 2, - height / 2, - depth / 2, + ]; + + points.forEach((point, index) => { + if (index % 6 === 0) { + const edge = new DIVECubeSelectionEdgeHandle(this._lineMaterial); + edge.setPoints([ + points[index] + center.x, points[index + 1] + center.y, points[index + 2] + center.z, + points[index + 3] + center.x, points[index + 4] + center.y, points[index + 5] + center.z, + ]); + this._edgesNode.add(edge); + } + }); + } +} \ No newline at end of file diff --git a/src/toolbox/select/SelectTool.ts b/src/toolbox/select/SelectTool.ts index c770d15f..ec528ed9 100644 --- a/src/toolbox/select/SelectTool.ts +++ b/src/toolbox/select/SelectTool.ts @@ -1,9 +1,14 @@ -import { Object3D } from "three"; +import { Object3D, Raycaster, Vector2 } from "three"; import { DIVESelectable, isSelectable } from "../../interface/Selectable.ts"; import DIVEScene from "../../scene/Scene.ts"; import { DIVEMoveable } from "../../interface/Moveable.ts"; import DIVEOrbitControls from "../../controls/OrbitControls.ts"; import DIVETransformTool from "../transform/TransformTool.ts"; +import { DIVECubeSelection } from "../../selections/cubeselection/CubeSelection.ts"; +import { DIVECubeSelectionEdgeHandle } from "../../selections/cubeselection/handle/edge/CubeSelectionEdgeHandle.ts"; +import { DIVECubeSelectionPlaneHandle } from "../../selections/cubeselection/handle/plane/CubeSelectionPlaneHandle.ts"; +import { TransformControls } from "three/examples/jsm/Addons"; +import { PRODUCT_LAYER_MASK, HELPER_LAYER_MASK, UI_LAYER_MASK } from "../../constant/VisibilityLayerMask.ts"; export interface DIVEObjectEventMap { select: object @@ -18,15 +23,54 @@ export interface DIVEObjectEventMap { */ export default class DIVESelectTool extends DIVETransformTool { + private canvas: HTMLElement; + private scene: DIVEScene; + private controller: DIVEOrbitControls; + private raycaster: Raycaster; + private gizmo: TransformControls; + + private cube: DIVECubeSelection; constructor(scene: DIVEScene, controller: DIVEOrbitControls) { super(scene, controller); this.name = "SelectTool"; + + this.canvas = controller.domElement; + this.scene = scene; + this.controller = controller; + this.raycaster = new Raycaster(); + this.raycaster.layers.mask = PRODUCT_LAYER_MASK | HELPER_LAYER_MASK; + + this.gizmo = new TransformControls(this.controller.object, this.canvas); + + this.gizmo.layers.mask = UI_LAYER_MASK; + this.gizmo.getRaycaster().layers.mask = UI_LAYER_MASK & this.controller.object.layers.mask; + this.gizmo.traverse((child) => { + child.layers.mask = UI_LAYER_MASK; + }); + this.gizmo.addEventListener('objectChange', () => { + if (!this.gizmo.object) return; + if (!('onMove' in this.gizmo.object)) return; + if (typeof this.gizmo.object.onMove !== 'function') return; + this.gizmo.object.onMove(); + }); + + this.controller.object.onSetCameraLayer = (mask: number) => { + this.gizmo.getRaycaster().layers.mask = UI_LAYER_MASK & mask; + }; + + this.gizmo.addEventListener('dragging-changed', function (event) { + controller.enabled = !event.value; + }); + + this.scene.add(this.gizmo); + + this.cube = new DIVECubeSelection(); + this.scene.Root.add(this.cube); } public Activate(): void { } - public Select(selectable: DIVESelectable): void { if (selectable.onSelect) selectable.onSelect(); @@ -36,17 +80,45 @@ export default class DIVESelectTool extends DIVETransformTool { public Deselect(selectable: DIVESelectable): void { if (selectable.onDeselect) selectable.onDeselect(); - this.DetachGizmo(); + this.DetachGizmo(selectable); } - public DetachGizmo(): void { + public DetachGizmo(selectable: DIVESelectable): void { this._gizmo.detach(); + + this.cube.Detach(selectable as (Object3D & DIVESelectable & DIVEMoveable)); + } + + private lastHover: Object3D | null = null; + public onPointerMove(e: PointerEvent): void { + const pointerPos: Vector2 = new Vector2(e.offsetX / this.canvas.clientWidth * 2 - 1, e.offsetY / this.canvas.clientHeight * -2 + 1); + this.raycaster.setFromCamera(pointerPos, this.controller.object); + + if (this.lastHover) { + if ('isCubeSelection' in this.lastHover) { + (this.lastHover as DIVECubeSelectionEdgeHandle | DIVECubeSelectionPlaneHandle).onPointerLeave(); + } + } + this.lastHover = null; + + const first = this.raycast()[0]; + if (first) { + if ('isCubeSelection' in first.object) { + this.lastHover = first.object as DIVECubeSelectionEdgeHandle | DIVECubeSelectionPlaneHandle; + + (this.lastHover as DIVECubeSelectionEdgeHandle | DIVECubeSelectionPlaneHandle).onPointerEnter(); + + return; + } + } + } public AttachGizmo(selectable: DIVESelectable): void { if ('isMoveable' in selectable) { const movable = selectable as (Object3D & DIVESelectable & DIVEMoveable); - this._gizmo.attach(movable); + // this._gizmo.attach(movable); + this.cube.Attach(movable); } } @@ -61,6 +133,8 @@ export default class DIVESelectTool extends DIVETransformTool { if (this._gizmo.object) { this.Deselect(this._gizmo.object as Object3D & DIVESelectable); } + // if (this.gizmo.object) this.Deselect(this.gizmo.object as (Object3D & DIVESelectable)); + if (this.cube.objects.length > 0) this.cube.objects.forEach((object: Object3D) => this.Deselect(object as (Object3D & DIVESelectable))); return; }