mirror of
https://github.com/Doodle3D/Doodle3D-Core.git
synced 2024-11-16 19:17:56 +01:00
264 lines
8.4 KiB
JavaScript
264 lines
8.4 KiB
JavaScript
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;
|
|
}
|
|
}
|