mirror of
https://github.com/Doodle3D/Doodle3D-Core.git
synced 2024-11-10 16:33:24 +01:00
415 lines
13 KiB
JavaScript
415 lines
13 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 { DESELECT_TRANSPARENCY, LEGACY_HEIGHT_STEP } from '../constants/d3Constants.js';
|
|
|
|
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.Mesh {
|
|
constructor(shapeData, toonShader) {
|
|
const { sculpt, rotate, twist, height, type, transform, z, color, fill } = shapeData;
|
|
|
|
let material;
|
|
if (toonShader) {
|
|
material = new THREE.MeshToonMaterial({
|
|
color: new THREE.Color(color),
|
|
shading: THREE.SmoothShading,
|
|
side: THREE.DoubleSide
|
|
});
|
|
} else {
|
|
material = new THREE.MeshLambertMaterial({
|
|
color: new THREE.Color(color),
|
|
side: THREE.DoubleSide
|
|
});
|
|
}
|
|
|
|
super(new THREE.BufferGeometry(), material);
|
|
|
|
this._toonShader = toonShader;
|
|
|
|
this.name = shapeData.UID;
|
|
|
|
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);
|
|
}
|
|
|
|
update(shapeData) {
|
|
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;
|
|
}
|
|
|
|
this._shapeData = shapeData;
|
|
return changed;
|
|
}
|
|
|
|
setOpaque(opaque) {
|
|
this.material.opacity = opaque ? 1.0 : 1.0 - DESELECT_TRANSPARENCY;
|
|
this.material.transparent = !opaque;
|
|
}
|
|
|
|
dispose() {
|
|
this.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.material.color.setHex(color);
|
|
this._color = color;
|
|
}
|
|
|
|
_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.geometry.boundingBox = null;
|
|
this.geometry.boundingSphere = null;
|
|
this.geometry.computeFaceNormals();
|
|
this.geometry.computeVertexNormals();
|
|
}
|
|
_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();
|
|
}
|
|
_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.geometry.dispose();
|
|
this.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.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.geometry.addAttribute('position', this._vertexBuffer);
|
|
}
|
|
}
|
|
|
|
export default ShapeMesh;
|