import * as THREE from 'three'; import { Vector } from 'cal'; import { SHAPE_TYPE_PROPERTIES } from '../../constants/shapeTypeProperties.js'; import * as d3Tools from '../../constants/d3Tools'; import handleURL from '../../../img/3d/sculptHandle.png'; import { createTextureFromURL, SpriteHandle } from '../../utils/threeUtils.js'; import BaseTransformer from './BaseTransformer.js'; import { getSelectedObjectsSelector, getBoundingBox } from '../../utils/selectionUtils.js'; import * as actions from '../../actions/index.js'; // import createDebug from 'debug'; // const debug = createDebug('d3d:transformer:sculpt'); const ARROW_HELPER = false; const HANDLE_SCALE = 0.125; const LINE_HIT_DISTANCE = 5.0; const lineMaterial = new THREE.LineBasicMaterial({ color: 0x72bcd4 }); const ARROW_HELPER_OBJECT_DIR = new THREE.ArrowHelper(new THREE.Vector3(), new THREE.Vector3(), 50, 0xff0000); const ARROW_HELPER_CAMERA_DIR = new THREE.ArrowHelper(new THREE.Vector3(), new THREE.Vector3(), 50, 0x00ff00); const ARROW_HELPER_SCULPT_DIR = new THREE.ArrowHelper(new THREE.Vector3(), new THREE.Vector3(), 50, 0x0000ff); const ARROW_HELPER_MOUSE_DIR = new THREE.ArrowHelper(new THREE.Vector3(), new THREE.Vector3(), 50, 0xffff00); ARROW_HELPER_MOUSE_DIR.visible = false; const handleTexture = createTextureFromURL(handleURL); export default class SculptTransformer extends BaseTransformer { _handles = []; constructor(dispatch, scene, camera, domElement) { super(dispatch, scene, camera, domElement); this.name = 'sculpt-transformer'; this.center = new THREE.Vector2(); this.cameraRotation = new THREE.Vector3(); this.sideDistance = 0; this.enableHitDetection = true; } tap(event) { const handle = this.includedHandle(event.intersections); if (handle) { this.dispatch(actions.removeSculptHandle(handle.heightIndex)); return; } const hitLine = this.findHitLine(event); if (hitLine) { // add sculpt handle, do not store as active this.dispatch(actions.addSculptHandle(hitLine, false)); return; } super.tap(event); } dragStart(event) { const handle = this.includedHandle(event.intersections); if (handle) { this.dispatch(actions.sculptStart(handle.heightIndex, true)); return; } const hitLine = this.findHitLine(event); if (hitLine) { // add sculpt handle, store as active this.dispatch(actions.addSculptHandle(hitLine, true)); return; } super.dragStart(event); } drag(event) { if (this.sculpt && this.sculpt.activeHandle !== null) { const { normalMatrix, origin, sculptDirection } = this.calculateSculptDirection(); const mouseDelta = event.position.subtract(event.previousPosition); const mouseDirection = new THREE.Vector3() .set(mouseDelta.x, -mouseDelta.y, 0) .applyMatrix3(new THREE.Matrix3().getNormalMatrix(this._camera.object.matrix)) .applyMatrix3(normalMatrix) .normalize(); ARROW_HELPER_MOUSE_DIR.position.copy(origin); ARROW_HELPER_MOUSE_DIR.setDirection(mouseDirection); const delta = mouseDirection.dot(sculptDirection) * mouseDelta.length(); this.dispatch(actions.sculpt(delta)); } else { super.drag(event); } } dragEnd(event) { if (this.sculpt && this.sculpt.activeHandle !== null) { ARROW_HELPER_MOUSE_DIR.visible = false; this.dispatch(actions.sculptEnd()); return; } super.dragEnd(event); } update(state) { this.sculpt = state.d3.sculpt; this.visible = state.selection.objects.length > 0; // TODO: create general selector for this in a reducer const objectsById = state.objectsById; const selectedShapeDatas = getSelectedObjectsSelector(state.selection.objects, objectsById); const boundingBox = getBoundingBox(selectedShapeDatas, state.selection.transform); const editableObjects = selectedShapeDatas.some(shapeData => { return SHAPE_TYPE_PROPERTIES[shapeData.type].tools[d3Tools.SCULPT]; }); this.visible = editableObjects; if (!editableObjects) return; let { min, max, center } = boundingBox; center = center.applyMatrix(state.selection.transform); const maxSize = new Vector(max.x, max.z).applyMatrix(state.selection.transform); this.sideDistance = maxSize.subtract(center).length(); this.center.copy(center); this.maxHeight = max.y; this.minHeight = min.y; if (!this._state || state.d3.camera !== this._state.d3.camera) this._camera = state.d3.camera; this.createUI(this.sculpt.handles.length); this.updateHandlePosition(); this.updateSpriteScale(); this._state = state; } removeHandles() { const children = [...this.children]; for (let i = 0; i < children.length; i ++) { const mesh = children[i]; if (mesh.geometry) mesh.geometry.dispose(); this.remove(mesh); } } createUI(numItems) { if (this.numItems === numItems) return; this.removeHandles(); if (ARROW_HELPER) { this.add( ARROW_HELPER_OBJECT_DIR, ARROW_HELPER_SCULPT_DIR, ARROW_HELPER_CAMERA_DIR, ARROW_HELPER_MOUSE_DIR ); } this._handles = []; const lineGeometry = new THREE.Geometry(); for (let heightIndex = 0; heightIndex < numItems; heightIndex ++) { const handle = this.createHandle(heightIndex); this.add(handle); this._handles.push(handle); lineGeometry.vertices.push(new THREE.Vector3(0, 0, 0)); } this.line = new THREE.Line(lineGeometry, lineMaterial); this.add(this.line); this.numItems = numItems; } createHandle(index) { const handle = new SpriteHandle(handleTexture, HANDLE_SCALE); handle.name = `sculpt-transformer-handle-${index}`; handle.heightIndex = index; return handle; } calculateSculptDirection() { const inverseWorld = new THREE.Matrix4().getInverse(this.matrixWorld); const normalMatrix = new THREE.Matrix3().getNormalMatrix(inverseWorld); const origin = new THREE.Vector3(this.center.x, this.minHeight, this.center.y); const objectDirection = new THREE.Vector3(0, 1, 0).applyMatrix3(normalMatrix); const cameraDirection = new THREE.Vector3() .set(0, 0, -1) .applyEuler(this._camera.object.rotation) .applyMatrix3(normalMatrix); const sculptDirection = new THREE.Vector3() .crossVectors(cameraDirection, objectDirection) .setY(0) .normalize(); return { origin, objectDirection, cameraDirection, sculptDirection, normalMatrix }; } updateHandlePosition() { const { origin, objectDirection, cameraDirection, sculptDirection } = this.calculateSculptDirection(); ARROW_HELPER_OBJECT_DIR.position.copy(origin); ARROW_HELPER_OBJECT_DIR.setDirection(objectDirection); ARROW_HELPER_CAMERA_DIR.position.copy(origin); ARROW_HELPER_CAMERA_DIR.setDirection(cameraDirection); ARROW_HELPER_SCULPT_DIR.position.copy(origin); ARROW_HELPER_SCULPT_DIR.setDirection(sculptDirection); for (let i = 0; i < this.sculpt.handles.length; i ++) { const handle = this._handles[i]; const { scale, pos: y } = this.sculpt.handles[i]; const base = new THREE.Vector2(sculptDirection.x, sculptDirection.z) .multiplyScalar(this.sideDistance * scale) .add(this.center); const position = new THREE.Vector3(base.x, y, base.y); handle.position.copy(position); this.line.geometry.vertices[i].copy(position); } if (this.line) { this.line.geometry.verticesNeedUpdate = true; } } // checks for handles in objects and returns the first found includedHandle(intersections) { if (this._handles.length === 0) return null; for (const i in intersections) { const object = intersections[i].object; const handleIndex = this._handles.indexOf(object); if (handleIndex !== -1) { return this._handles[handleIndex]; } } return null; } // Check if event was close enough to line, // if it was returns y position // otherwise returns null findHitLine(event) { if (!this.line) return null; const { ray } = this.getRayCaster(event.position); for (let i = 1; i < this.line.geometry.vertices.length; i ++) { const v0 = this.line.geometry.vertices[i - 1]; const v1 = this.line.geometry.vertices[i]; const pointOnSegment = new THREE.Vector3(); const distance = Math.sqrt(ray.distanceSqToSegment(v0, v1, pointOnSegment, null)); if (distance < LINE_HIT_DISTANCE) { return pointOnSegment.y; } } return null; } }