Doodle3D-Core/src/d3/ShapeMesh.js

474 lines
14 KiB
JavaScript

import { Vector } from '@doodle3d/cal';
import { applyMatrixOnPath } from '../utils/vectorUtils.js';
import { shapeToPointsCornered } from '../shape/shapeToPoints.js';
import * as THREE from 'three';
import { getPointsBounds, shapeChanged } from '../shape/shapeDataUtils.js';
import MatcapMaterial from './MatcapMaterial.js';
import { DESELECT_TRANSPARENCY, LEGACY_HEIGHT_STEP } from '../constants/d3Constants.js';
import ThreeBSP from 'three-js-csg';
const THREE_BSP = ThreeBSP(THREE);
const MAX_HEIGHT_BASE = 5;
// Legacy compensation. Compensating for the fact that we
// used to devide the twist by the fixed sculpt steps.
// TODO: move this to twist factor in interface
// and converting old files on open once
const isValidNumber = (num) => typeof num === 'number' && !isNaN(num);
class ShapeMesh extends THREE.Object3D {
constructor(shapeData, active) {
super();
this.name = shapeData.UID;
const { sculpt, rotate, twist, height, type, transform, z, color, fill, solid } = shapeData;
const material = new MatcapMaterial({ color: new THREE.Color(color) });
this._mesh = new THREE.Mesh(new THREE.BufferGeometry(), material.clone());
this._mesh.name = shapeData.UID;
this._mesh.isShapeMesh = true;
this._shapes = [];
this._shapesMap = [];
this._center = new Vector();
this._sculpt = sculpt;
this._rotate = rotate;
this._twist = twist;
this._height = height;
this._type = type;
this._transform = transform;
this._z = z;
this._shapeData = shapeData;
this._color = color;
this._fill = fill;
this.updatePoints(shapeData);
this._holeMesh = new THREE.Mesh(new THREE.Geometry().fromBufferGeometry(this._mesh.geometry), material.clone());
this._holeMesh.name = shapeData.UID;
this._holeMesh.isShapeMesh = true;
this.updateSolid(solid, active);
}
add(object) {
if (!this.children.includes(object)) super.add(object);
}
remove(object) {
if (this.children.includes(object)) super.remove(object);
}
updateHoleGeometry(holes) {
if (holes === this._holes && !this._changedGeometry) return false;
if (!this._solid) return false;
this._holeMesh.geometry.dispose();
if (holes === null || !this._fill) {
this._holeMesh.geometry = new THREE.Geometry().fromBufferGeometry(this._mesh.geometry);
return true;
}
const objectGeometry = new THREE.Geometry().fromBufferGeometry(this._mesh.geometry);
let objectBSP = new THREE_BSP(objectGeometry);
objectGeometry.dispose();
objectBSP = objectBSP.subtract(holes);
this._holeMesh.geometry = objectBSP.toMesh().geometry;
this._holes = holes;
this._changedGeometry = false;
return true;
}
update(shapeData, active) {
let changed = false;
if (shapeChanged(this._shapeData, shapeData)) {
this.updatePoints(shapeData);
changed = true;
}
if (shapeData.transform !== this._transform || shapeData.z !== this._z) {
this.updateTransform(shapeData.transform, shapeData.z);
changed = true;
}
if (shapeData.height !== this._height) {
this.updateHeight(shapeData.height);
changed = true;
}
if (shapeData.sculpt !== this._sculpt) {
this.updateSculpt(shapeData.sculpt);
changed = true;
}
if (shapeData.twist !== this._twist) {
this.updateTwist(shapeData.twist);
changed = true;
}
if (shapeData.color !== this._color) {
this.updateColor(shapeData.color);
changed = true;
}
let solidChanged = false;
if (shapeData.solid !== this._solid || active !== this._active) {
this.updateSolid(shapeData.solid, active);
changed = true;
solidChanged = true;
}
this._shapeData = shapeData;
return changed;
}
setOpaque(opaque) {
this._holeMesh.material.opacity = opaque ? 1.0 : DESELECT_TRANSPARENCY;
this._holeMesh.material.transparent = !opaque;
}
dispose() {
this._mesh.geometry.dispose();
this._holeMesh.geometry.dispose();
}
updatePoints(shapeData) {
this._fill = shapeData.fill;
this._points = shapeData.points;
this._rectSize = shapeData.rectSize;
this._circle = shapeData.circle;
this._star = shapeData.star;
this._triangleSize = shapeData.triangleSize;
const compoundPaths = shapeToPointsCornered(shapeData);
this._shapes = compoundPaths.map(({ points, holes = [], pointsMap, holesMaps = [] }) => ({
shape: [points, ...holes],
maps: [pointsMap, ...holesMaps]
}));
const { min, max } = getPointsBounds(compoundPaths);
this._center.copy(min.add(max).scale(0.5));
if (!this._heightSteps) {
this._updateSide();
} else {
this._updateFaces();
}
this._updateVertices();
}
updateSculpt(sculpt) {
if (!sculpt.every(({ pos, scale }) => isValidNumber(pos) && isValidNumber(scale))) {
throw new Error(`Cannot update object ${this.name}: sculpt contains invalid values.`);
}
this._sculpt = sculpt;
this._updateSide();
this._updateVertices();
}
updateTwist(twist) {
if (!isValidNumber(twist)) {
throw new Error(`Cannot update object ${this.name}: twist is an invalid value.`);
}
this._twist = twist;
this._updateSide();
this._updateVertices();
}
updateHeight(height) {
if (!isValidNumber(height)) {
throw new Error(`Cannot update object ${this.name}: height is an invalid value.`);
}
this._height = height;
this._updateSide();
this._updateVertices();
}
updateTransform(transform, z) {
// TODO
// transform.matrix.every could improved performance wise
if (!transform.matrix.every(isValidNumber)) {
throw new Error(`Cannot update object ${this.name}: transform contains invalid values.`);
}
this._transform = transform;
this._z = z;
this._updateVertices();
}
updateColor(color) {
if (!isValidNumber(color)) {
throw new Error(`Cannot update object ${this.name}: color is an invalid value.`);
}
this._mesh.material.color = new THREE.Color(color);
this._holeMesh.material.color = new THREE.Color(color);
this._color = color;
}
updateSolid(solid, active) {
this._mesh.material.opacity = solid ? 1.0 : 0.0;
this._mesh.material.transparent = !solid;
this.visible = solid || active;
if (active || !solid) {
this.add(this._mesh);
this.remove(this._holeMesh);
} else {
this.add(this._holeMesh);
this.remove(this._mesh);
}
this._solid = solid;
this._active = active;
}
_getPoint(point, heightStep, center) {
const { scale, pos: y } = this._heightSteps[heightStep];
if (scale !== 1 || (this._twist !== 0 && heightStep !== 0)) {
point = point.subtract(center);
if (scale !== 1) {
point = point.scale(scale);
}
if (this._twist !== 0 && heightStep !== 0) {
point = point.rotate(this._twist * y / LEGACY_HEIGHT_STEP);
}
point = point.add(center);
}
return { x: point.x, y: y + this._z, z: point.y };
}
_updateVerticesHorizontal(heightStep, paths, center, indexCounter) {
for (let pathindex = 0; pathindex < paths.length; pathindex ++) {
const path = applyMatrixOnPath(paths[pathindex], this._transform);
for (let pathIndex = 0; pathIndex < path.length; pathIndex ++) {
let point = path[pathIndex];
const { x, y, z } = this._getPoint(point, heightStep, center);
this._vertices[indexCounter ++] = x;
this._vertices[indexCounter ++] = y;
this._vertices[indexCounter ++] = z;
}
}
return indexCounter;
}
_updateVertices() {
const numHeightSteps = this._heightSteps.length;
const center = this._center.applyMatrix(this._transform);
let indexCounter = 0;
for (let i = 0; i < this._shapes.length; i ++) {
const paths = this._shapes[i].shape;
if (this._fill) {
// update positions of bottom vertices
indexCounter = this._updateVerticesHorizontal(0, paths, center, indexCounter);
// update positions of top vertices
indexCounter = this._updateVerticesHorizontal(numHeightSteps - 1, paths, center, indexCounter);
}
for (let pathsIndex = 0; pathsIndex < paths.length; pathsIndex ++) {
const path = applyMatrixOnPath(paths[pathsIndex], this._transform);
for (let pathIndex = 0; pathIndex < path.length; pathIndex ++) {
let point = path[pathIndex];
for (let heightStep = 0; heightStep < numHeightSteps; heightStep ++) {
const { x, y, z } = this._getPoint(point, heightStep, center);
this._vertices[indexCounter ++] = x;
this._vertices[indexCounter ++] = y;
this._vertices[indexCounter ++] = z;
}
}
}
}
this._vertexBuffer.needsUpdate = true;
this._mesh.geometry.boundingBox = null;
this._mesh.geometry.boundingSphere = null;
this._mesh.geometry.computeFaceNormals();
this._mesh.geometry.computeVertexNormals();
this._changedGeometry = true;
}
_updateSide() {
// TODO use higher precision for export mesh
const maxHeight = MAX_HEIGHT_BASE / Math.abs(this._twist);
const heightSteps = this._sculpt
.map(({ scale, pos }) => ({
pos: pos * this._height,
scale
}))
.reduce((_heightSteps, currentStep, i, sculptSteps) => {
_heightSteps.push(currentStep);
if (sculptSteps.length === 1 + i) return _heightSteps;
const nextStep = sculptSteps[i + 1];
const heightDifference = nextStep.pos - currentStep.pos;
const intermediateSteps = Math.floor(heightDifference / maxHeight);
const intermediateStepHeight = heightDifference / intermediateSteps;
const intermediateStepScale = (nextStep.scale - currentStep.scale) / intermediateSteps;
for (let j = 1; j < intermediateSteps; j ++) {
_heightSteps.push({
pos: currentStep.pos + intermediateStepHeight * j,
scale: currentStep.scale + intermediateStepScale * j
});
}
return _heightSteps;
}, []);
const heightStepsChanged = !this._heightSteps || heightSteps.length !== this._heightSteps.length;
this._heightSteps = heightSteps;
if (heightStepsChanged) this._updateFaces();
this._changedGeometry = true;
}
_updateFaces() {
// TODO
// find better way to update indexBuffer
// seems bit redicules to remove the whole geometry to update indexes
const numHeightSteps = this._heightSteps.length;
this._mesh.geometry.dispose();
this._mesh.geometry = new THREE.BufferGeometry();
// store total number of indexes and vertices needed
let indexBufferLength = 0;
let vertexBufferLength = 0;
// store triangulated indexes for top and bottom per shape
const triangulatedIndexes = [];
const vertexOffsets = [];
for (let i = 0; i < this._shapes.length; i ++) {
const { shape, maps } = this._shapes[i];
// shape structure is [...[...Vector]]
// map to [...Int]
// sum all values to get total number of points
const numPoints = shape
.map(({ length }) => length)
.reduce((a, b) => a + b, 0);
const vertexOffset = vertexBufferLength / 3;
if (this._fill) {
let offset = 0;
const flatMap = maps
// further flattening each shape's maps;
// [pointsMap, ...holesMaps] to one flat array.
.map((map, j) => {
map = map.map(value => value + offset);
offset += shape[j].length;
return map;
})
// because maps indexes point to points,
// update each map to be offsetted by the total number of previous points
.reduce((a, b) => a.concat(b), []);
const [points, ...holes] = maps
.map((map, j) => {
const path = shape[j];
return map.map(k => path[k]);
})
.map(path => path.map(({ x, y }) => new THREE.Vector2(x, y)));
// triangulate
const triangulatedBottom = THREE.ShapeUtils.triangulateShape(points, holes)
.reduce((a, b) => a.concat(b), [])
// // map mapped indexes back to original indexes
.map(value => flatMap[value])
.map(value => value + vertexOffset);
// reverse index order for bottom so faces are flipped
const triangulatedTop = triangulatedBottom
.map(value => value + numPoints)
.reverse();
triangulatedIndexes.push(triangulatedBottom.concat(triangulatedTop));
indexBufferLength += triangulatedBottom.length + triangulatedTop.length;
vertexBufferLength += numPoints * 6;
vertexOffsets.push(vertexOffset + numPoints * 2);
} else {
vertexOffsets.push(vertexOffset);
}
// calculate the number of indexes (faces) needed for the outside wall
indexBufferLength += (numPoints - 1) * (numHeightSteps - 1) * 6;
// number of vertices needed for the outside wall is
// (the total number of points in the shape) *
// (the number of height steps + 1) *
// (the number of dimensions, 3)
vertexBufferLength += numPoints * numHeightSteps * 3;
}
const indexes = new Uint32Array(indexBufferLength);
const indexBuffer = new THREE.BufferAttribute(indexes, 1);
this._mesh.geometry.setIndex(indexBuffer);
let indexCounter = 0;
for (let i = 0; i < this._shapes.length; i ++) {
const { shape } = this._shapes[i];
if (this._fill) {
for (let j = 0; j < triangulatedIndexes[i].length; j ++) {
indexes[indexCounter ++] = triangulatedIndexes[i][j];
}
}
let pointIndexOffset = 0;
for (let shapeIndex = 0; shapeIndex < shape.length; shapeIndex ++) {
const shapePart = shape[shapeIndex];
for (let pointIndex = 0; pointIndex < shapePart.length - 1; pointIndex ++) {
let base = (pointIndexOffset + pointIndex) * numHeightSteps + vertexOffsets[i];
for (let heightStep = 0; heightStep < (numHeightSteps - 1); heightStep ++) {
indexes[indexCounter ++] = base;
indexes[indexCounter ++] = base + numHeightSteps;
indexes[indexCounter ++] = base + 1;
indexes[indexCounter ++] = base + 1;
indexes[indexCounter ++] = base + numHeightSteps;
indexes[indexCounter ++] = base + numHeightSteps + 1;
base ++;
}
}
pointIndexOffset += shapePart.length;
}
}
this._vertices = new Float32Array(vertexBufferLength);
this._vertexBuffer = new THREE.BufferAttribute(this._vertices, 3);
this._mesh.geometry.addAttribute('position', this._vertexBuffer);
this._changedGeometry = true;
}
}
export default ShapeMesh;