import { Matrix4 } from 'three'; import * as THREE from '../node_modules/three/src/Three' export class Sketcher extends THREE.Group { constructor(camera, domElement, plane) { super() this.camera = camera; this.domElement = domElement; this.scene = scene; this.plane = plane; this.matrixAutoUpdate = false; this.sketchNormal = new THREE.Vector3(0, 0, 1) this.orientSketcher(plane) this.add(new THREE.PlaneHelper(this.plane, 1, 0xffff00)); this.linesGroup = new THREE.Group() this.linesArr = this.linesGroup.children this.pointsGroup = new THREE.Group() this.ptsArr = this.pointsGroup.children this.add(this.linesGroup) this.add(this.pointsGroup) window.lg = this.linesArr window.pg = this.ptsArr this.pickThreshold = 100 this.grabbedObject = null this.lineMaterial = new THREE.LineBasicMaterial({ color: 0x555, }) this.pointMaterial = new THREE.PointsMaterial({ color: 0xAAA, size: 3, }) this.pointStart = this.pointStart.bind(this); this.pointEnd = this.pointEnd.bind(this); this.move = this.move.bind(this); this.keyHandler = this.keyHandler.bind(this); this.picker = this.picker.bind(this); this.grabbedMove = this.grabbedMove.bind(this); this.grabEnd = this.grabEnd.bind(this); this.raycaster = new THREE.Raycaster(); window.addEventListener('keydown', this.keyHandler) domElement.addEventListener('pointerdown', this.picker) this.mode = "" this.keyTable = { 'l': this.addLine, 'Escape': this.clear } } orientSketcher() { const theta = this.sketchNormal.angleTo(this.plane.normal) const axis = this.sketchNormal.clone().cross(this.plane.normal).normalize() const rot = new THREE.Matrix4().makeRotationAxis(axis, theta) const trans = new THREE.Matrix4().makeTranslation(0, 0, this.plane.constant) this.matrix = rot.multiply(trans) // world matrix will auto update in next render this.inverse = this.matrix.clone().invert() } keyHandler(e) { switch (e.key) { case 'Escape': this.clear() this.mode = "" break; case 'l': this.addLine() this.mode = "line" break; case '=': this.plane.applyMatrix4(new Matrix4().makeRotationY(0.1)) this.orientSketcher() this.dispatchEvent({ type: 'change' }) break; case '-': this.plane.applyMatrix4(new Matrix4().makeRotationY(-0.1)) this.orientSketcher() this.dispatchEvent({ type: 'change' }) break; } } picker(e) { if (this.mode || e.buttons != 1) return this.raycaster.setFromCamera( new THREE.Vector2( (e.clientX / window.innerWidth) * 2 - 1, - (e.clientY / window.innerHeight) * 2 + 1 ), this.camera ); // console.log(this.ptsArr) const candidates = this.raycaster.intersectObjects(this.ptsArr) // console.log(candidates) if (!candidates.length) return; let minDist = candidates[0].distanceToRay let idx = 0 for (let i = 1; i < candidates.length; i++) { if (candidates.distanceToRay < minDist) { minDist = candidates.distanceToRay idx = i } } if (minDist < this.pickThreshold) { this.grabPtIdx = this.ptsArr.indexOf( candidates[idx].object ) } else { return } this.domElement.addEventListener('pointermove', this.grabbedMove); this.domElement.addEventListener('pointerup', this.grabEnd); } grabbedMove(e) { const mouseLoc = this.getLocation(e); this.moveLinePt(this.grabPtIdx,mouseLoc) this.dispatchEvent({ type: 'change' }) } moveLinePt(ptIdx, absPos) { this.ptsArr[ptIdx].geometry.attributes.position.set(absPos); this.ptsArr[ptIdx].geometry.attributes.position.needsUpdate = true; const lineIdx = Math.floor(ptIdx / 2) const endPtIdx = (ptIdx % 2) * 3 this.linesArr[lineIdx].geometry.attributes.position.set(absPos, endPtIdx) this.linesArr[lineIdx].geometry.attributes.position.needsUpdate = true; } grabEnd() { this.domElement.removeEventListener('pointermove', this.grabbedMove) this.domElement.removeEventListener('pointerup', this.grabEnd) this.ptsArr[this.grabPtIdx].geometry.computeBoundingSphere() // this.grabbedObject = null } addLine() { this.domElement.addEventListener('pointerdown', this.pointStart) } clear() { if (this.mode == "") return this.domElement.removeEventListener('pointerdown', this.pointStart) this.domElement.removeEventListener('pointermove', this.move); this.domElement.removeEventListener('pointerdown', this.pointEnd); this.domElement.removeEventListener('pointerdown', this.pointEnd); const lastLine = this.linesArr[this.linesArr.length - 1] this.linesGroup.remove(lastLine) lastLine.geometry.dispose() const lastPoints = this.ptsArr.slice(this.ptsArr.length - 2) this.pointsGroup.remove(...lastPoints) lastPoints.forEach(obj => obj.geometry.dispose()) this.dispatchEvent({ type: 'change' }) } getLocation(e) { this.raycaster.setFromCamera( new THREE.Vector2( (e.clientX / window.innerWidth) * 2 - 1, - (e.clientY / window.innerHeight) * 2 + 1 ), this.camera ); // return this.worldToLocal(this.raycaster.ray.intersectPlane(this.plane)).toArray() return this.raycaster.ray.intersectPlane(this.plane).applyMatrix4(this.inverse).toArray() } pointStart(e) { if (e.buttons !== 1) return const mouseLoc = this.getLocation(e); this.lineGeom = new THREE.BufferGeometry() this.lineGeom.setAttribute('position', new THREE.BufferAttribute( new Float32Array(6), 3 ) ); this.lineGeom.attributes.position.set(mouseLoc) this.line = new THREE.LineSegments(this.lineGeom, this.lineMaterial); this.line.frustumCulled = false; this.linesGroup.add(this.line) this.p1Geom = new THREE.BufferGeometry() this.p1Geom.setAttribute('position', new THREE.BufferAttribute( new Float32Array(3), 3 ) ); this.p1Geom.attributes.position.set(mouseLoc) this.p1 = new THREE.Points(this.p1Geom, this.pointMaterial); this.pointsGroup.add(this.p1) this.p2Geom = new THREE.BufferGeometry() this.p2Geom.setAttribute('position', new THREE.BufferAttribute( new Float32Array(3), 3 ) ); this.p2 = new THREE.Points(this.p2Geom, this.pointMaterial); this.pointsGroup.add(this.p2) this.domElement.removeEventListener('pointerdown', this.pointStart) this.domElement.addEventListener('pointermove', this.move) this.domElement.addEventListener('pointerdown', this.pointEnd) } move(e) { const mouseLoc = this.getLocation(e); this.lineGeom.attributes.position.set(mouseLoc, 3) this.lineGeom.attributes.position.needsUpdate = true; this.p2Geom.attributes.position.set(mouseLoc); this.p2Geom.attributes.position.needsUpdate = true; this.p2Geom.computeBoundingSphere(); this.dispatchEvent({ type: 'change' }) } pointEnd(e) { if (e.buttons !== 1) return; this.domElement.removeEventListener('pointermove', this.move); this.domElement.removeEventListener('pointerdown', this.pointEnd); this.pointStart(e) } }