mirror of
https://github.com/Doodle3D/Doodle3D-Core.git
synced 2024-12-22 11:03:48 +01:00
initial commit
This commit is contained in:
commit
70d5ea8010
22
.babelrc
Normal file
22
.babelrc
Normal file
@ -0,0 +1,22 @@
|
||||
{
|
||||
"env": {
|
||||
"module": {
|
||||
"presets": [
|
||||
["env", {
|
||||
"modules": false
|
||||
}],
|
||||
"react"
|
||||
]
|
||||
},
|
||||
"main": {
|
||||
"presets": ["env", "react"]
|
||||
}
|
||||
},
|
||||
"plugins": [
|
||||
"babel-plugin-transform-object-rest-spread",
|
||||
"babel-plugin-inline-import",
|
||||
"babel-plugin-transform-class-properties",
|
||||
"babel-plugin-transform-es2015-classes",
|
||||
"babel-plugin-syntax-dynamic-import"
|
||||
]
|
||||
}
|
6
.gitignore
vendored
Normal file
6
.gitignore
vendored
Normal file
@ -0,0 +1,6 @@
|
||||
|
||||
lib
|
||||
|
||||
module
|
||||
|
||||
node_modules
|
5446
package-lock.json
generated
Normal file
5446
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
64
package.json
Executable file
64
package.json
Executable file
@ -0,0 +1,64 @@
|
||||
{
|
||||
"name": "doodle3d-core",
|
||||
"version": "0.0.1",
|
||||
"description": "Core functions of Doodle3D Transform",
|
||||
"main": "lib/index.js",
|
||||
"module": "module/index.js",
|
||||
"esnext": "src/index.js",
|
||||
"scripts": {
|
||||
"prepare": "npm run build",
|
||||
"build": "npm run build:main && npm run build:module ",
|
||||
"build:main": "BABEL_ENV=main babel src -s -d lib",
|
||||
"build:module": "BABEL_ENV=module babel src -s -d module"
|
||||
},
|
||||
"devDependencies": {
|
||||
"babel-plugin-syntax-dynamic-import": "^6.18.0",
|
||||
"babel-plugin-transform-class-properties": "^6.24.1",
|
||||
"babel-plugin-transform-es2015-classes": "^6.24.1",
|
||||
"babel-plugin-transform-object-rest-spread": "^6.26.0",
|
||||
"babel-preset-env": "^1.6.1",
|
||||
"babel-preset-react": "^6.24.1",
|
||||
"babel-cli": "6.24.1",
|
||||
"babel-core": "6.24.1",
|
||||
"babel-eslint": "7.2.3",
|
||||
"babel-loader": "7.0.0",
|
||||
"babel-plugin-add-module-exports": "0.2.1",
|
||||
"babel-preset-es2015": "6.24.1",
|
||||
"chai": "3.5.0",
|
||||
"eslint": "3.19.0",
|
||||
"eslint-loader": "1.7.1",
|
||||
"mocha": "3.3.0",
|
||||
"webpack": "3.1.0",
|
||||
"yargs": "7.1.0"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git"
|
||||
},
|
||||
"keywords": [
|
||||
"webpack",
|
||||
"es6",
|
||||
"starter",
|
||||
"library",
|
||||
"universal",
|
||||
"umd",
|
||||
"commonjs"
|
||||
],
|
||||
"author": "Casper @Doodle3D",
|
||||
"dependencies": {
|
||||
"@doodle3d/cal": "0.0.8",
|
||||
"@doodle3d/clipper-js": "^1.0.7",
|
||||
"@doodle3d/threejs-export-obj": "0.0.3",
|
||||
"@doodle3d/threejs-export-stl": "0.0.2",
|
||||
"babel-plugin-inline-import": "^2.0.6",
|
||||
"imports-loader": "^0.7.1",
|
||||
"memoizee": "^0.3.9",
|
||||
"proptypes": "^1.1.0",
|
||||
"raw-loader": "^0.5.1",
|
||||
"react": "^16.0.0",
|
||||
"regenerator-runtime": "^0.11.0",
|
||||
"semver": "^5.4.1",
|
||||
"shortid": "^2.2.8",
|
||||
"three": "^0.83.0",
|
||||
"three-js-csg": "^72.0.0"
|
||||
}
|
||||
}
|
67
src/components/DoodlePreview.js
Normal file
67
src/components/DoodlePreview.js
Normal file
@ -0,0 +1,67 @@
|
||||
import * as THREE from 'three';
|
||||
import 'three/examples/js/controls/EditorControls';
|
||||
import React from 'react';
|
||||
import PropTypes from 'proptypes';
|
||||
import JSONToSketchData from '../shape/JSONToSketchData.js';
|
||||
import createSceneData from '../d3/createSceneData.js';
|
||||
import createScene from '../d3/createScene.js';
|
||||
|
||||
class DoodlePreview extends React.Component {
|
||||
constructor() {
|
||||
super();
|
||||
this.state = {
|
||||
scene: null
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount(prevProps) {
|
||||
JSONToSketchData(this.props.docData).then((sketchData) => {
|
||||
const { canvas } = this.refs;
|
||||
const { width, height, pixelRatio } = this.props
|
||||
|
||||
const sceneData = createSceneData(sketchData);
|
||||
|
||||
const scene = createScene(sceneData, canvas);
|
||||
this.setState({ scene });
|
||||
|
||||
scene.setSize(width, height, pixelRatio);
|
||||
scene.render();
|
||||
|
||||
this.editorControls = new THREE.EditorControls(scene.camera, canvas);
|
||||
this.editorControls.addEventListener('change', () => scene.render());
|
||||
});
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
const { scene } = this.state;
|
||||
const { width, height } = this.props;
|
||||
if (scene !== null && (prevProps.width !== width || prevProps.height !== height)) {
|
||||
scene.setSize(width, height);
|
||||
scene.render();
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const { width, height } = this.props;
|
||||
return (
|
||||
<canvas width={width} height={height} ref="canvas" />
|
||||
);
|
||||
}
|
||||
}
|
||||
DoodlePreview.defaultProps = {
|
||||
width: 720,
|
||||
height: 480,
|
||||
pixelRatio: 1
|
||||
};
|
||||
DoodlePreview.propTypes = {
|
||||
width: PropTypes.number.isRequired,
|
||||
height: PropTypes.number.isRequired,
|
||||
pixelRatio: PropTypes.number.isRequired,
|
||||
sketchData: PropTypes.object, // TODO
|
||||
docData: PropTypes.shape({
|
||||
appVersion: PropTypes.string,
|
||||
data: PropTypes.string
|
||||
})
|
||||
};
|
||||
|
||||
export default DoodlePreview;
|
3
src/components/index.js
Normal file
3
src/components/index.js
Normal file
@ -0,0 +1,3 @@
|
||||
import DoodlePreview from './DoodlePreview.js';
|
||||
|
||||
export { DoodlePreview };
|
34
src/constants/d2Tools.js
Normal file
34
src/constants/d2Tools.js
Normal file
@ -0,0 +1,34 @@
|
||||
export const FREE_HAND = 'freehand-tool';
|
||||
export const POLYGON = 'polygon-tool';
|
||||
export const BRUSH = 'brush-tool';
|
||||
export const TRANSFORM = 'transform-tool';
|
||||
export const ERASER = 'eraser-tool';
|
||||
export const TEXT = 'text-tool';
|
||||
export const PHOTO_GUIDE = 'photo-guide-tool';
|
||||
|
||||
export const CIRCLE = 'circle-tool';
|
||||
export const CIRCLE_SEGMENT = 'circle-segment-tool';
|
||||
export const STAR = 'star-tool';
|
||||
export const RECT = 'rect-tool';
|
||||
export const SKEW_RECT = 'skew-rect-tool';
|
||||
export const TRIANGLE = 'triangle-tool';
|
||||
export const POLY_POINT = 'poly-point-tool';
|
||||
export const HEART = 'heart-tool';
|
||||
|
||||
export const BUCKET = 'bucket-tool';
|
||||
|
||||
export const PEN_TOOLS = [
|
||||
FREE_HAND,
|
||||
POLYGON,
|
||||
BRUSH
|
||||
];
|
||||
|
||||
export const SHAPE_TOOLS = [
|
||||
STAR,
|
||||
CIRCLE,
|
||||
CIRCLE_SEGMENT,
|
||||
RECT,
|
||||
TRIANGLE,
|
||||
POLY_POINT,
|
||||
HEART
|
||||
];
|
4
src/constants/d3Tools.js
vendored
Normal file
4
src/constants/d3Tools.js
vendored
Normal file
@ -0,0 +1,4 @@
|
||||
export const HEIGHT = 'height-tool';
|
||||
export const SCULPT = 'sculpt-tool';
|
||||
export const TWIST = 'twist-tool';
|
||||
export const STAMP = 'stamp-tool';
|
414
src/d3/ShapeMesh.js
vendored
Normal file
414
src/d3/ShapeMesh.js
vendored
Normal file
@ -0,0 +1,414 @@
|
||||
import { Vector } from '@doodle3d/cal';
|
||||
import { applyMatrixOnPath } from '../math/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, LEGACY_HEIGHT_STEP } from '../js/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;
|
164
src/d3/ToonShaderRenderChain.js
vendored
Normal file
164
src/d3/ToonShaderRenderChain.js
vendored
Normal file
@ -0,0 +1,164 @@
|
||||
import * as THREE from 'three';
|
||||
import 'three/examples/js/postprocessing/EffectComposer';
|
||||
import 'three/examples/js/postprocessing/RenderPass';
|
||||
import 'three/examples/js/postprocessing/ShaderPass';
|
||||
import 'three/examples/js/shaders/CopyShader';
|
||||
import vertexShaderPostprocessing from './shaders/vertexShaderPostprocessing.js';
|
||||
import fragmentShaderSobelDepth from './shaders/fragmentShaderSobelDepth.js';
|
||||
import fragmentShaderSobelNormal from './shaders/fragmentShaderSobelNormal.js';
|
||||
import fragmentShaderCombineTextures from './shaders/fragmentShaderCombineTextures.js';
|
||||
import fragmentShaderDepth from './shaders/fragmentShaderDepth.js';
|
||||
import vertexShaderDepth from './shaders/vertexShaderDepth.js';
|
||||
|
||||
// Based on Doodle3D/Toon-Shader
|
||||
|
||||
// initize render targets with default canvas size
|
||||
const DEFAULT_WIDTH = 300;
|
||||
const DEFAULT_HEIGHT = 200;
|
||||
|
||||
const NORMAL_MATERIAL = new THREE.MeshNormalMaterial({ side: THREE.DoubleSide });
|
||||
const DEPTH_MATERIAL = new THREE.ShaderMaterial({
|
||||
uniforms: {
|
||||
mNear: { type: 'f', value: 1.0 },
|
||||
mFar: { type: 'f', value: 3000.0 },
|
||||
opacity: { type: 'f', value: 1.0 },
|
||||
logDepthBufFC: { type: 'f', value: 2.0 }
|
||||
},
|
||||
vertexShader: vertexShaderDepth,
|
||||
fragmentShader: fragmentShaderDepth,
|
||||
side: THREE.DoubleSide
|
||||
});
|
||||
|
||||
export default class Composer {
|
||||
constructor(renderer, scene, camera, groups) {
|
||||
this._renderer = renderer;
|
||||
this._scene = scene;
|
||||
this._camera = camera;
|
||||
this._groups = groups;
|
||||
|
||||
this._aspect = new THREE.Vector2(DEFAULT_WIDTH, DEFAULT_HEIGHT);
|
||||
|
||||
const {
|
||||
composer: composerDepth,
|
||||
renderTarget: renderTargetDepth
|
||||
} = this._createSobelComposer(DEPTH_MATERIAL, 0.005, fragmentShaderSobelDepth);
|
||||
|
||||
this._composerDepth = composerDepth;
|
||||
|
||||
const {
|
||||
composer: composerNormal,
|
||||
renderTarget: renderTargetNormal
|
||||
} = this._createSobelComposer(NORMAL_MATERIAL, 0.5, fragmentShaderSobelNormal);
|
||||
|
||||
this._composerNormal = composerNormal;
|
||||
|
||||
const renderTargetUI = new THREE.WebGLRenderTarget(DEFAULT_WIDTH, DEFAULT_HEIGHT, {
|
||||
format: THREE.RGBAFormat
|
||||
});
|
||||
this._composerUI = new THREE.EffectComposer(this._renderer, renderTargetUI);
|
||||
this._composerUI.addPass(new THREE.RenderPass(scene, camera));
|
||||
this._composerUI.addPass(new THREE.ShaderPass(THREE.CopyShader));
|
||||
|
||||
this._composer = new THREE.EffectComposer(renderer);
|
||||
this._composer.addPass(new THREE.RenderPass(scene, camera, undefined, new THREE.Color(0xffffff), 1.0));
|
||||
const combineComposers = new THREE.ShaderPass(new THREE.ShaderMaterial({
|
||||
uniforms: {
|
||||
tDiffuse: { type: 't' },
|
||||
tNormal: { type: 't', value: renderTargetNormal.texture },
|
||||
tDepth: { type: 't', value: renderTargetDepth.texture },
|
||||
tUI: { type: 't', value: renderTargetUI.texture }
|
||||
},
|
||||
vertexShader: vertexShaderPostprocessing,
|
||||
fragmentShader: fragmentShaderCombineTextures
|
||||
}));
|
||||
combineComposers.renderToScreen = true;
|
||||
this._composer.addPass(combineComposers);
|
||||
}
|
||||
|
||||
_createSobelComposer(material, threshold, fragmentShader) {
|
||||
const renderTarget = new THREE.WebGLRenderTarget(DEFAULT_WIDTH, DEFAULT_HEIGHT, {
|
||||
format: THREE.RGBFormat
|
||||
});
|
||||
|
||||
const composer = new THREE.EffectComposer(this._renderer, renderTarget);
|
||||
|
||||
composer.addPass(new THREE.RenderPass(this._scene, this._camera, material));
|
||||
|
||||
const sobelShader = new THREE.ShaderPass(new THREE.ShaderMaterial({
|
||||
uniforms: {
|
||||
tDiffuse: { type: 't' },
|
||||
threshold: { type: 'f', value: threshold },
|
||||
aspect: { type: 'v2', value: this._aspect }
|
||||
},
|
||||
vertexShader: vertexShaderPostprocessing,
|
||||
fragmentShader
|
||||
}));
|
||||
composer.addPass(sobelShader);
|
||||
|
||||
composer.addPass(new THREE.ShaderPass(THREE.CopyShader));
|
||||
|
||||
return { renderTarget, composer };
|
||||
}
|
||||
|
||||
setSize(width, height, pixelRatio) {
|
||||
this._renderer.setPixelRatio(pixelRatio);
|
||||
this._renderer.setSize(width, height);
|
||||
|
||||
this._aspect.set(width, height);
|
||||
|
||||
// adjust aspect ratio of camera
|
||||
this._camera.aspect = width / height;
|
||||
this._camera.updateProjectionMatrix();
|
||||
|
||||
width *= pixelRatio;
|
||||
height *= pixelRatio;
|
||||
|
||||
this._composer.setSize(width, height);
|
||||
this._composerNormal.setSize(width, height);
|
||||
this._composerDepth.setSize(width, height);
|
||||
this._composerUI.setSize(width, height);
|
||||
}
|
||||
|
||||
getCurrentVisibleValues() {
|
||||
const visibleValues = {};
|
||||
|
||||
for (const key in this._groups) {
|
||||
visibleValues[key] = this._groups[key].visible;
|
||||
}
|
||||
|
||||
return visibleValues;
|
||||
}
|
||||
|
||||
setVisible(initalValues, visibleGroups) {
|
||||
for (const key in this._groups) {
|
||||
const group = this._groups[key];
|
||||
|
||||
if (visibleGroups.indexOf(group) !== -1) {
|
||||
group.visible = initalValues[key];
|
||||
} else {
|
||||
group.visible = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const initalValues = this.getCurrentVisibleValues();
|
||||
|
||||
const shapes = this._groups.shapes;
|
||||
const UI = this._groups.UI;
|
||||
const plane = this._groups.plane;
|
||||
const boundingBox = this._groups.boundingBox;
|
||||
|
||||
this.setVisible(initalValues, [shapes]);
|
||||
this._composerDepth.render();
|
||||
this._composerNormal.render();
|
||||
|
||||
this.setVisible(initalValues, [UI]);
|
||||
this._composerUI.render();
|
||||
|
||||
this.setVisible(initalValues, [shapes, plane, boundingBox]);
|
||||
this._composer.render();
|
||||
|
||||
this.setVisible(initalValues, [shapes, UI, plane, boundingBox]);
|
||||
}
|
||||
}
|
74
src/d3/createScene.js
vendored
Normal file
74
src/d3/createScene.js
vendored
Normal file
@ -0,0 +1,74 @@
|
||||
import * as THREE from 'three';
|
||||
import ShapesManager from './ShapesManager.js';
|
||||
import ToonShaderRenderChain from './ToonShaderRenderChain.js';
|
||||
import { hasExtensionsFor } from '../utils/webGLSupport.js';
|
||||
|
||||
// TODO move to const
|
||||
export const CANVAS_SIZE = 100;
|
||||
|
||||
const CANVAS_WIDTH = CANVAS_SIZE * 2;
|
||||
const CANVAS_HEIGHT = CANVAS_SIZE * 2;
|
||||
|
||||
export default function createScene(state, canvas) {
|
||||
const scene = new THREE.Scene();
|
||||
|
||||
// Position and zoom of the camera should be stored in constants
|
||||
const camera = new THREE.PerspectiveCamera(50, 1, 1, 10000);
|
||||
camera.position.set(0, 200, 150);
|
||||
camera.lookAt(new THREE.Vector3(0, 0, 0));
|
||||
|
||||
scene.add(camera);
|
||||
|
||||
const shapesManager = new ShapesManager({ toonShader: hasExtensionsFor.toonShaderThumbnail });
|
||||
shapesManager.update(state);
|
||||
|
||||
scene.add(shapesManager);
|
||||
|
||||
const geometry = new THREE.PlaneGeometry(CANVAS_WIDTH, CANVAS_HEIGHT);
|
||||
const material = new THREE.MeshBasicMaterial({
|
||||
color: 0xcccccc,
|
||||
side: THREE.DoubleSide,
|
||||
transparent: true,
|
||||
opacity: 0.5
|
||||
});
|
||||
const plane = new THREE.Mesh(geometry, material);
|
||||
plane.rotation.x = Math.PI / 2;
|
||||
plane.position.y = -0.1;
|
||||
plane.name = 'bed-plane';
|
||||
scene.add(plane);
|
||||
|
||||
const renderer = new THREE.WebGLRenderer({ preserveDrawingBuffer: true, canvas });
|
||||
|
||||
const directionalLight = new THREE.PointLight(0xffffff, 0.6);
|
||||
camera.add(directionalLight);
|
||||
|
||||
const light = new THREE.AmbientLight(0x505050);
|
||||
scene.add(light);
|
||||
|
||||
let render;
|
||||
let setSizeRenderer;
|
||||
if (hasExtensionsFor.toonShaderThumbnail) {
|
||||
const renderChain = new ToonShaderRenderChain(renderer, scene, camera, {
|
||||
plane,
|
||||
UI: new THREE.Object3D(),
|
||||
shapes: shapesManager,
|
||||
boundingBox: new THREE.Object3D()
|
||||
});
|
||||
setSizeRenderer = renderChain.setSize.bind(renderChain);
|
||||
render = renderChain.render.bind(renderChain);
|
||||
} else {
|
||||
renderer.setClearColor(0xffffff);
|
||||
|
||||
setSizeRenderer = renderer.setSize.bind(renderer);
|
||||
render = renderer.render.bind(renderer, scene, camera);
|
||||
}
|
||||
|
||||
const setSize = (width, height, pixelRatio) => {
|
||||
setSizeRenderer(width, height, pixelRatio);
|
||||
|
||||
camera.aspect = width / height;
|
||||
camera.updateProjectionMatrix();
|
||||
};
|
||||
|
||||
return { scene, camera, renderer, render, setSize };
|
||||
}
|
27
src/d3/createSceneData.js
vendored
Normal file
27
src/d3/createSceneData.js
vendored
Normal file
@ -0,0 +1,27 @@
|
||||
import shortid from 'shortid';
|
||||
|
||||
export default function docToShapeData(docData) {
|
||||
const sketchData = {
|
||||
spaces: {},
|
||||
objectsById: {}
|
||||
};
|
||||
|
||||
for (let i = 0; i < docData.spaces.length; i ++) {
|
||||
const spaceData = docData.spaces[i];
|
||||
const space = i === 0 ? 'world' : shortid.generate();
|
||||
const objectIds = [];
|
||||
|
||||
for (const object of spaceData.objects) {
|
||||
const UID = shortid.generate();
|
||||
objectIds.push(UID);
|
||||
sketchData.objectsById[UID] = { ...object, UID, space };
|
||||
}
|
||||
|
||||
sketchData.spaces[space] = {
|
||||
matrix: spaceData.matrix,
|
||||
objectIds
|
||||
};
|
||||
}
|
||||
|
||||
return sketchData;
|
||||
}
|
7
src/d3/index.js
vendored
Normal file
7
src/d3/index.js
vendored
Normal file
@ -0,0 +1,7 @@
|
||||
import createSceneData from './createSceneData.js';
|
||||
import createScene from './createScene.js';
|
||||
import ToonShaderRenderChain from './ToonShaderRenderChain.js';
|
||||
import ShapeMesh from './ShapeMesh.js';
|
||||
import ShapesManager from './ShapesManager.js';
|
||||
|
||||
export { createSceneData, createScene, ToonShaderRenderChain, ShapeMesh, ShapesManager };
|
18
src/d3/shaders/fragmentShaderCombineTextures.js
vendored
Normal file
18
src/d3/shaders/fragmentShaderCombineTextures.js
vendored
Normal file
@ -0,0 +1,18 @@
|
||||
export default `
|
||||
uniform sampler2D tDiffuse;
|
||||
uniform sampler2D tNormal;
|
||||
uniform sampler2D tDepth;
|
||||
uniform sampler2D tUI;
|
||||
varying vec2 vUv;
|
||||
|
||||
void main() {
|
||||
vec4 colorDiffuse = texture2D(tDiffuse, vUv); // cell shader
|
||||
vec4 colorNormal = texture2D(tNormal, vUv); // outline from normal texture
|
||||
colorNormal.w = 0.0;
|
||||
vec4 colorDepth = texture2D(tDepth, vUv); // outline from depth texture
|
||||
colorDepth.w = 0.0;
|
||||
vec4 colorUI = texture2D(tUI, vUv); // color ui's
|
||||
|
||||
gl_FragColor = mix(max(colorDiffuse - colorDepth - colorNormal, 0.0), colorUI, colorUI.w);
|
||||
}
|
||||
`;
|
16
src/d3/shaders/fragmentShaderDepth.js
vendored
Normal file
16
src/d3/shaders/fragmentShaderDepth.js
vendored
Normal file
@ -0,0 +1,16 @@
|
||||
export default `
|
||||
uniform float mNear;
|
||||
uniform float mFar;
|
||||
uniform float opacity;
|
||||
uniform float logDepthBufFC;
|
||||
varying float vFragDepth;
|
||||
|
||||
#include <common>
|
||||
|
||||
void main() {
|
||||
float fragDepthEXT = log2(vFragDepth) * logDepthBufFC * 0.5;
|
||||
float depth = fragDepthEXT / gl_FragCoord.w;
|
||||
float color = 1.0 - smoothstep( mNear, mFar, depth );
|
||||
gl_FragColor = vec4( vec3( color ), opacity );
|
||||
}
|
||||
`;
|
28
src/d3/shaders/fragmentShaderSobel.js
vendored
Normal file
28
src/d3/shaders/fragmentShaderSobel.js
vendored
Normal file
@ -0,0 +1,28 @@
|
||||
export default `
|
||||
uniform sampler2D tDiffuse;
|
||||
uniform float threshold;
|
||||
uniform vec2 aspect;
|
||||
varying vec2 vUv;
|
||||
|
||||
void main() {
|
||||
float w = (1.0 / aspect.x);
|
||||
float h = (1.0 / aspect.y);
|
||||
|
||||
vec4 n[9];
|
||||
n[0] = texture2D(tDiffuse, vUv + vec2( -w, -h));
|
||||
n[1] = texture2D(tDiffuse, vUv + vec2(0.0, -h));
|
||||
n[2] = texture2D(tDiffuse, vUv + vec2( w, -h));
|
||||
n[3] = texture2D(tDiffuse, vUv + vec2( -w, 0.0));
|
||||
n[4] = texture2D(tDiffuse, vUv);
|
||||
n[5] = texture2D(tDiffuse, vUv + vec2( w, 0.0));
|
||||
n[6] = texture2D(tDiffuse, vUv + vec2( -w, h));
|
||||
n[7] = texture2D(tDiffuse, vUv + vec2(0.0, h));
|
||||
n[8] = texture2D(tDiffuse, vUv + vec2( w, h));
|
||||
|
||||
vec4 sobel_horizEdge = n[2] + (2.0 * n[5]) + n[8] - (n[0] + (2.0 * n[3]) + n[6]);
|
||||
vec4 sobel_vertEdge = n[0] + (2.0 * n[1]) + n[2] - (n[6] + (2.0 * n[7]) + n[8]);
|
||||
vec3 sobel = sqrt((sobel_horizEdge.rgb * sobel_horizEdge.rgb) + (sobel_vertEdge.rgb * sobel_vertEdge.rgb));
|
||||
|
||||
gl_FragColor = (length(sobel) > threshold) ? vec4(vec3(1.0), 0.0) : vec4(0.0);
|
||||
}
|
||||
`;
|
23
src/d3/shaders/fragmentShaderSobelDepth.js
vendored
Normal file
23
src/d3/shaders/fragmentShaderSobelDepth.js
vendored
Normal file
@ -0,0 +1,23 @@
|
||||
export default `
|
||||
// edge detection based on http://williamchyr.com/tag/unity/page/2/
|
||||
uniform sampler2D tDiffuse;
|
||||
uniform float threshold;
|
||||
uniform vec2 aspect;
|
||||
varying vec2 vUv;
|
||||
|
||||
void main() {
|
||||
float w = (1.0 / aspect.x);
|
||||
float h = (1.0 / aspect.y);
|
||||
|
||||
vec4 a = texture2D(tDiffuse, vUv);
|
||||
vec4 b = texture2D(tDiffuse, vUv + vec2(-w, -h));
|
||||
vec4 c = texture2D(tDiffuse, vUv + vec2(w, -h));
|
||||
vec4 d = texture2D(tDiffuse, vUv + vec2(-w, h));
|
||||
vec4 e = texture2D(tDiffuse, vUv + vec2(w, h));
|
||||
|
||||
vec4 averageDepth = (b + c + d + e) / 4.0;
|
||||
float difference = length(averageDepth - a);
|
||||
|
||||
gl_FragColor = difference > threshold ? vec4(vec3(1.0), 0.0) : vec4(0.0);
|
||||
}
|
||||
`;
|
22
src/d3/shaders/fragmentShaderSobelNormal.js
vendored
Normal file
22
src/d3/shaders/fragmentShaderSobelNormal.js
vendored
Normal file
@ -0,0 +1,22 @@
|
||||
export default `
|
||||
// edge detection based on http://williamchyr.com/tag/unity/page/2/
|
||||
uniform sampler2D tDiffuse;
|
||||
uniform float threshold;
|
||||
uniform vec2 aspect;
|
||||
varying vec2 vUv;
|
||||
|
||||
void main() {
|
||||
float w = (1.0 / aspect.x);
|
||||
float h = (1.0 / aspect.y);
|
||||
|
||||
// vec4 a = texture2D(tDiffuse, vUv);
|
||||
vec4 b = texture2D(tDiffuse, vUv + vec2(-w, -h));
|
||||
vec4 c = texture2D(tDiffuse, vUv + vec2(w, -h));
|
||||
vec4 d = texture2D(tDiffuse, vUv + vec2(-w, h));
|
||||
vec4 e = texture2D(tDiffuse, vUv + vec2(w, h));
|
||||
|
||||
float difference = length(b - e) + length(c - d);
|
||||
|
||||
gl_FragColor = difference > threshold ? vec4(vec3(1.0), 0.0) : vec4(0.0);
|
||||
}
|
||||
`;
|
16
src/d3/shaders/vertexShaderDepth.js
vendored
Normal file
16
src/d3/shaders/vertexShaderDepth.js
vendored
Normal file
@ -0,0 +1,16 @@
|
||||
export default `
|
||||
varying float vFragDepth;
|
||||
uniform float logDepthBufFC;
|
||||
|
||||
#include <common>
|
||||
#include <morphtarget_pars_vertex>
|
||||
|
||||
void main() {
|
||||
#include <begin_vertex>
|
||||
#include <morphtarget_vertex>
|
||||
#include <project_vertex>
|
||||
|
||||
// gl_Position.z = log2(max( EPSILON, gl_Position.w + 1.0 )) * logDepthBufFC;
|
||||
vFragDepth = 1.0 + gl_Position.w;
|
||||
}
|
||||
`;
|
8
src/d3/shaders/vertexShaderPostprocessing.js
vendored
Normal file
8
src/d3/shaders/vertexShaderPostprocessing.js
vendored
Normal file
@ -0,0 +1,8 @@
|
||||
export default `
|
||||
varying vec2 vUv;
|
||||
|
||||
void main() {
|
||||
vUv = uv;
|
||||
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
|
||||
}
|
||||
`;
|
106
src/d3/shapesManager.js
vendored
Normal file
106
src/d3/shapesManager.js
vendored
Normal file
@ -0,0 +1,106 @@
|
||||
import { SHAPE_TYPE_PROPERTIES } from '../shape/shapeTypeProperties.js';
|
||||
import * as THREE from 'three';
|
||||
import ShapeMesh from './ShapeMesh.js';
|
||||
|
||||
export default class ShapesManager extends THREE.Object3D {
|
||||
constructor({ toonShader }) {
|
||||
super();
|
||||
|
||||
this._toonShader = toonShader;
|
||||
|
||||
this._meshes = {};
|
||||
this._spaces = {};
|
||||
this.name = 'shapes-manager';
|
||||
// this._edges = {};
|
||||
}
|
||||
|
||||
update(state) { // retruns a bool, indicating if a rerender is required
|
||||
let render = false;
|
||||
if (this._state === state) return render;
|
||||
|
||||
for (const spaceId in state.spaces) {
|
||||
if (!this._spaces[spaceId]) {
|
||||
const space = state.spaces[spaceId];
|
||||
|
||||
const container = new THREE.Object3D();
|
||||
container.matrixAutoUpdate = false;
|
||||
container.matrix.copy(space.matrix);
|
||||
|
||||
this._spaces[spaceId] = container;
|
||||
this.add(container);
|
||||
} else if (this._spaces[spaceId] !== state.spaces[spaceId]) {
|
||||
const container = this._spaces[spaceId];
|
||||
const space = state.spaces[spaceId];
|
||||
container.matrix.copy(space.matrix);
|
||||
}
|
||||
}
|
||||
|
||||
// Remove removed shapes
|
||||
if (this._state) {
|
||||
for (const id in this._state.objectsById) {
|
||||
if (!state.objectsById[id]) {
|
||||
this._handleShapeRemove(id);
|
||||
render = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const id in state.objectsById) {
|
||||
// const shapeData = this._state.objectsById[id];
|
||||
const newShapeData = state.objectsById[id];
|
||||
|
||||
if (!SHAPE_TYPE_PROPERTIES[newShapeData.type].D3Visible) continue;
|
||||
// add new shapes
|
||||
if (!this._state || !this._state.objectsById[id]) {
|
||||
this._handleShapeAdded(newShapeData);
|
||||
render = true;
|
||||
} else {
|
||||
const { mesh } = this._meshes[id];
|
||||
if (mesh.update(newShapeData)) {
|
||||
render = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this._state = state;
|
||||
|
||||
return render;
|
||||
}
|
||||
|
||||
updateTransparent(selectedUIDs) {
|
||||
for (const UID in this._meshes) {
|
||||
const { mesh } = this._meshes[UID];
|
||||
const selected = selectedUIDs.indexOf(UID) !== -1;
|
||||
const opaque = selected || selectedUIDs.length === 0;
|
||||
|
||||
mesh.setOpaque(opaque);
|
||||
}
|
||||
}
|
||||
|
||||
getMesh(id) {
|
||||
return this._meshes[id].mesh;
|
||||
}
|
||||
|
||||
_handleShapeRemove(id) {
|
||||
if (this._meshes[id] === undefined) return;
|
||||
const { mesh, space } = this._meshes[id];
|
||||
mesh.dispose();
|
||||
delete this._meshes[id];
|
||||
|
||||
this._spaces[space].remove(mesh);
|
||||
}
|
||||
|
||||
_handleShapeAdded(shapeData) {
|
||||
if (!SHAPE_TYPE_PROPERTIES[shapeData.type].D3Visible) return;
|
||||
const { space } = shapeData;
|
||||
const mesh = new ShapeMesh(shapeData, this._toonShader);
|
||||
this._meshes[shapeData.UID] = { mesh, space };
|
||||
|
||||
this._spaces[space].add(mesh);
|
||||
//
|
||||
// const edges = new THREE.VertexNormalsHelper(mesh, 10, 0x000000, 3);
|
||||
// this._edges[shapeData.UID] = edges;
|
||||
//
|
||||
// this.add(edges);
|
||||
}
|
||||
}
|
7
src/index.js
Normal file
7
src/index.js
Normal file
@ -0,0 +1,7 @@
|
||||
import * as math from './math/index.js';
|
||||
import * as shape from './shape/index.js';
|
||||
import * as utils from './utils/index.js';
|
||||
import * as d3 from './d3/index.js';
|
||||
import * as components from './components/index.js';
|
||||
|
||||
export { math, shape, utils, d3, components };
|
3
src/math/index.js
Normal file
3
src/math/index.js
Normal file
@ -0,0 +1,3 @@
|
||||
import * as vectorUtils from './vectorUtils';
|
||||
|
||||
export { vectorUtils };
|
13
src/math/vectorUtils.js
Normal file
13
src/math/vectorUtils.js
Normal file
@ -0,0 +1,13 @@
|
||||
import { Vector } from '@doodle3d/cal';
|
||||
|
||||
// Some basic util function to apply matrix on shapes and convert points to Vector
|
||||
// returns [...point] with matrix applied to each point
|
||||
export const applyMatrixOnPath = (path, matrix) => path.map(point => point.applyMatrix(matrix));
|
||||
// returns [...[...point]] with matrix applied to each point
|
||||
export const applyMatrixOnShape = (shape, matrix) => shape.map(path => applyMatrixOnPath(path, matrix));
|
||||
// converts any type object to CAL.Vector instance
|
||||
export const pointToVector = ({ x, y }) => new Vector(x, y);
|
||||
// returns [...point] with point converted to a CAL.Vector Instance
|
||||
export const pathToVectorPath = (path) => path.map(pointToVector);
|
||||
// returns [...[...point]] with point converted to a CAL.Vector Instance
|
||||
export const shapeToVectorShape = (shape) => shape.map(pathToVectorPath);
|
78
src/shape/JSONToSketchData.js
Normal file
78
src/shape/JSONToSketchData.js
Normal file
@ -0,0 +1,78 @@
|
||||
import { Color, Matrix4 } from 'three';
|
||||
import { Vector, Matrix } from '@doodle3d/cal';
|
||||
import semver from 'semver';
|
||||
import { recursivePromiseApply } from '../utils/async.js';
|
||||
import { base64ToImage, base64ToVectorArray } from '../utils/binaryUtils.js';
|
||||
|
||||
// TODO use actual const
|
||||
const LEGACY_HEIGHT_STEP = 10;
|
||||
|
||||
async function JSONToSketchData({ data, appVersion }) {
|
||||
let sketchData = JSON.parse(data, (key, value) => {
|
||||
if (semver.lt(appVersion, '0.1.2')) {
|
||||
if (key === 'imageData') {
|
||||
return base64ToImage(value);
|
||||
}
|
||||
}
|
||||
|
||||
if (value.metadata && value.metadata.type) {
|
||||
switch (value.metadata.type) {
|
||||
case 'Vector':
|
||||
return new Vector().fromJSON(value);
|
||||
|
||||
case 'Matrix':
|
||||
return new Matrix().fromJSON(value);
|
||||
|
||||
case 'VectorArray':
|
||||
return base64ToVectorArray(value);
|
||||
|
||||
case 'Image':
|
||||
return base64ToImage(value);
|
||||
|
||||
case 'Matrix4':
|
||||
return new Matrix4().copy(value);
|
||||
|
||||
case 'Color':
|
||||
return new Color(value.data).getHex();
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
// legacy, convert { r: Float, g: Float, b: Float } to hex
|
||||
if (typeof value.r === 'number' && typeof value.g === 'number' && typeof value.b === 'number') {
|
||||
return new Color(value.r, value.g, value.b).getHex();
|
||||
}
|
||||
|
||||
return value;
|
||||
});
|
||||
sketchData = await recursivePromiseApply(sketchData);
|
||||
|
||||
if (semver.lt(appVersion, '0.4.0')) {
|
||||
sketchData = {
|
||||
spaces: [{
|
||||
matrix: new Matrix4(),
|
||||
objects: sketchData
|
||||
}]
|
||||
};
|
||||
}
|
||||
|
||||
if (semver.lt(appVersion, '0.10.0')) {
|
||||
for (const space of sketchData.spaces) {
|
||||
for (const object of space.objects) {
|
||||
const { sculpt, height } = object;
|
||||
|
||||
object.sculpt = sculpt.map((scale, i) => ({
|
||||
pos: Math.min(1, (i * LEGACY_HEIGHT_STEP) / height),
|
||||
scale
|
||||
}));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return sketchData;
|
||||
}
|
||||
|
||||
export default JSONToSketchData;
|
6
src/shape/index.js
Normal file
6
src/shape/index.js
Normal file
@ -0,0 +1,6 @@
|
||||
import shapeToPoints from './shapeToPoints.js';
|
||||
import JSONToSketchData from './JSONToSketchData.js';
|
||||
import ShapeDataUtils from './ShapeDataUtils.js';
|
||||
import shapeTypeProperties from './shapeTypeProperties.js';
|
||||
|
||||
export { shapeToPoints, JSONToSketchData, shapeTypeProperties };
|
71
src/shape/shapeDataUtils.js
Normal file
71
src/shape/shapeDataUtils.js
Normal file
@ -0,0 +1,71 @@
|
||||
import memoize from 'memoizee';
|
||||
import { Vector } from '@doodle3d/cal';
|
||||
|
||||
export function shapeChanged(oldShapeData, newShapeData) {
|
||||
const pointsChanged = oldShapeData.points !== newShapeData.points;
|
||||
const holesChanged = oldShapeData.holes !== newShapeData.holes;
|
||||
const rectSizeChanged = oldShapeData.rectSize !== newShapeData.rectSize;
|
||||
const triangleSizeChanged = oldShapeData.triangleSize !== newShapeData.triangleSize;
|
||||
const circleChanged = oldShapeData.circle !== newShapeData.circle;
|
||||
const starChanged = oldShapeData.star !== newShapeData.star;
|
||||
const textChanged = oldShapeData.text !== newShapeData.text;
|
||||
const polyPoints = oldShapeData.polyPoints !== newShapeData.polyPoints;
|
||||
const fillChanged = oldShapeData.fill !== newShapeData.fill;
|
||||
const heartChanged = oldShapeData.heart !== newShapeData.heart;
|
||||
|
||||
return pointsChanged || holesChanged || rectSizeChanged || triangleSizeChanged ||
|
||||
circleChanged || starChanged || textChanged || polyPoints || fillChanged || heartChanged;
|
||||
}
|
||||
|
||||
// TODO use actual const
|
||||
const SHAPE_CACHE_LIMIT = 10;
|
||||
|
||||
|
||||
export const getPointsBounds = memoize(getPointsBoundsRaw, { max: SHAPE_CACHE_LIMIT });
|
||||
export function getPointsBoundsRaw(compoundPaths, transform) {
|
||||
let points = compoundPaths.reduce((a, { points: b }) => a.concat(b), []);
|
||||
|
||||
if (transform !== undefined) {
|
||||
points = applyMatrixOnPath(points, transform);
|
||||
}
|
||||
|
||||
const min = new Vector(
|
||||
Math.min(...points.map((point) => point.x)),
|
||||
Math.min(...points.map((point) => point.y))
|
||||
);
|
||||
const max = new Vector(
|
||||
Math.max(...points.map((point) => point.x)),
|
||||
Math.max(...points.map((point) => point.y))
|
||||
);
|
||||
|
||||
return { min, max };
|
||||
}
|
||||
|
||||
export function getPointsCenter(points) {
|
||||
const { min, max } = getPointsBounds(points);
|
||||
return min.add(max).scale(0.5);
|
||||
}
|
||||
|
||||
export function isClosed(shapeData) {
|
||||
switch (shapeData.type) {
|
||||
case 'RECT':
|
||||
case 'TRIANGLE':
|
||||
case 'STAR':
|
||||
case 'CIRCLE':
|
||||
case 'CIRCLE_SEGMENT':
|
||||
case 'COMPOUND_PATH':
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export const shapeDataToShape = memoize(shapeDataToShapeRaw, { max: SHAPE_CACHE_LIMIT });
|
||||
// export const shapeDataToShape = shapeDataToShapeRaw;
|
||||
function shapeDataToShapeRaw(shapeData) {
|
||||
if (shapeData.type === 'IMAGE_GUIDE') {
|
||||
return new ImageShape(shapeData);
|
||||
} else {
|
||||
return new Shape(shapeData);
|
||||
}
|
||||
}
|
269
src/shape/shapeToPoints.js
Normal file
269
src/shape/shapeToPoints.js
Normal file
@ -0,0 +1,269 @@
|
||||
import * as THREE from 'three';
|
||||
import memoize from 'memoizee';
|
||||
import { Vector } from '@doodle3d/cal';
|
||||
import ClipperShape from '@doodle3d/clipper-js';
|
||||
import { pathToVectorPath } from '../math/vectorUtils.js';
|
||||
|
||||
// TODO use actual const
|
||||
const SHAPE_CACHE_LIMIT = 10;
|
||||
const CLIPPER_PRECISION = 10;
|
||||
|
||||
const HEART_BEZIER_PATH = [
|
||||
new Vector(0.0, -0.5),
|
||||
new Vector(0.1, -1.1),
|
||||
new Vector(1.0, -1.1),
|
||||
new Vector(1.0, -0.4),
|
||||
new Vector(1.0, 0.3),
|
||||
new Vector(0.1, 0.5),
|
||||
new Vector(0.0, 1.0),
|
||||
new Vector(-0.1, 0.5),
|
||||
new Vector(-1.0, 0.3),
|
||||
new Vector(-1.0, -0.4),
|
||||
new Vector(-1.0, -1.1),
|
||||
new Vector(-0.1, -1.1),
|
||||
new Vector(0.0, -0.5)
|
||||
];
|
||||
|
||||
export const shapeToPoints = memoize(shapeToPointsRaw, { max: SHAPE_CACHE_LIMIT });
|
||||
function shapeToPointsRaw(shapeData) {
|
||||
const shapes = [];
|
||||
|
||||
switch (shapeData.type) {
|
||||
case 'RECT': {
|
||||
const { rectSize } = shapeData;
|
||||
const points = [
|
||||
new Vector(0, 0),
|
||||
new Vector(rectSize.x, 0),
|
||||
new Vector(rectSize.x, rectSize.y),
|
||||
new Vector(0, rectSize.y),
|
||||
new Vector(0, 0)
|
||||
];
|
||||
shapes.push({ points, holes: [] });
|
||||
break;
|
||||
}
|
||||
case 'TRIANGLE': {
|
||||
const { triangleSize } = shapeData;
|
||||
const points = [
|
||||
new Vector(0, 0),
|
||||
new Vector(triangleSize.x / 2, triangleSize.y),
|
||||
new Vector(-triangleSize.x / 2, triangleSize.y),
|
||||
new Vector(0, 0)
|
||||
];
|
||||
shapes.push({ points, holes: [] });
|
||||
break;
|
||||
}
|
||||
case 'STAR': {
|
||||
const { rays, outerRadius, innerRadius } = shapeData.star;
|
||||
const points = [];
|
||||
let even = false;
|
||||
const numLines = rays * 2;
|
||||
for (let i = 0, rad = 0; i <= numLines; i++, rad += Math.PI / rays) {
|
||||
if (i === numLines) { // last line?
|
||||
points.push(points[0].clone()); // go to first point
|
||||
} else {
|
||||
const radius = even ? innerRadius : outerRadius;
|
||||
let x = Math.sin(rad) * radius;
|
||||
let y = -Math.cos(rad) * radius;
|
||||
points.push(new Vector(x, y));
|
||||
even = !even;
|
||||
}
|
||||
}
|
||||
shapes.push({ points, holes: [] });
|
||||
break;
|
||||
}
|
||||
case 'CIRCLE':
|
||||
case 'CIRCLE_SEGMENT': {
|
||||
const { radius, segment } = shapeData.circle;
|
||||
const points = [];
|
||||
const circumference = 2 * radius * Math.PI;
|
||||
const numSegments = circumference;
|
||||
for (let rad = 0; rad <= segment; rad += Math.PI * 2 / numSegments) {
|
||||
const x = Math.sin(rad) * radius;
|
||||
const y = -Math.cos(rad) * radius;
|
||||
points.push(new Vector(x, y));
|
||||
}
|
||||
if (segment < Math.PI * 2) {
|
||||
const x = Math.sin(segment) * radius;
|
||||
const y = -Math.cos(segment) * radius;
|
||||
|
||||
points.push(
|
||||
new Vector(x, y),
|
||||
new Vector(0, 0),
|
||||
new Vector(0, -radius)
|
||||
);
|
||||
}
|
||||
if (shapeData.type === 'CIRCLE') {
|
||||
points.push(points[0].clone()); // go to first point
|
||||
}
|
||||
shapes.push({ points, holes: [] });
|
||||
break;
|
||||
}
|
||||
case 'IMAGE_GUIDE': {
|
||||
const { width, height } = shapeData.imageData;
|
||||
const maxX = width / 2;
|
||||
const maxY = height / 2;
|
||||
const points = [
|
||||
new Vector(-maxX, -maxY),
|
||||
new Vector(maxX, -maxY),
|
||||
new Vector(maxX, maxY),
|
||||
new Vector(-maxX, maxY)
|
||||
];
|
||||
shapes.push({ points, holes: [] });
|
||||
break;
|
||||
}
|
||||
case 'TEXT': {
|
||||
const { text, family, style, weight } = shapeData.text;
|
||||
const textShapes = createText(text, 400, family, style, weight)
|
||||
.map(([points, ...holes]) => ({ points, holes }));
|
||||
|
||||
shapes.push(...textShapes);
|
||||
break;
|
||||
}
|
||||
case 'COMPOUND_PATH': {
|
||||
shapes.push({ points: shapeData.points, holes: shapeData.holes });
|
||||
break;
|
||||
}
|
||||
case 'EXPORT_SHAPE': {
|
||||
shapes.push(...shapeData.shapes);
|
||||
break;
|
||||
}
|
||||
case 'POLY_POINTS': {
|
||||
const { numPoints, radius } = shapeData.polyPoints;
|
||||
const points = [];
|
||||
for (let i = 0, rad = 0; i <= numPoints; i ++, rad += Math.PI * 2 / numPoints) {
|
||||
if (i === numPoints) { // last line?
|
||||
points.push(points[0].clone()); // go to first point
|
||||
} else {
|
||||
const x = Math.sin(rad) * radius;
|
||||
const y = -Math.cos(rad) * radius;
|
||||
points.push(new Vector(x, y));
|
||||
}
|
||||
}
|
||||
shapes.push({ points, holes: [] });
|
||||
break;
|
||||
}
|
||||
case 'HEART': {
|
||||
const { width, height } = shapeData.heart;
|
||||
const bezierPath = HEART_BEZIER_PATH
|
||||
.map(({ x, y }) => (new Vector(x * width, y * height)));
|
||||
const points = segmentBezierPath(bezierPath, 0.2);
|
||||
shapes.push({ points, holes: [] });
|
||||
break;
|
||||
}
|
||||
case 'BRUSH': {
|
||||
const [points, ...holes] = new ClipperShape([shapeData.points], false, true, false, false)
|
||||
.scaleUp(CLIPPER_PRECISION)
|
||||
.round().removeDuplicates()
|
||||
.offset(shapeData.strokeWidth * CLIPPER_PRECISION, {
|
||||
jointType: 'jtRound',
|
||||
endType: 'etOpenRound',
|
||||
miterLimit: 2.0,
|
||||
roundPrecision: 0.25
|
||||
})
|
||||
.scaleDown(CLIPPER_PRECISION)
|
||||
.mapToLower()
|
||||
.map(pathToVectorPath)
|
||||
.map(path => {
|
||||
path.push(path[0]);
|
||||
return path;
|
||||
});
|
||||
|
||||
if (points) shapes.push({ points, holes });
|
||||
break;
|
||||
}
|
||||
|
||||
case 'FREE_HAND':
|
||||
case 'POLYGON': {
|
||||
const { points = [], holes = [] } = shapeData;
|
||||
if (shapeData.fill) {
|
||||
new ClipperShape([points, ...holes], true, true, false, false)
|
||||
.simplify('pftEvenOdd')
|
||||
.seperateShapes()
|
||||
.map(shape => {
|
||||
const [points, ...holes] = shape
|
||||
.mapToLower()
|
||||
.map(pathToVectorPath)
|
||||
.map(path => {
|
||||
path.push(path[0]);
|
||||
return path;
|
||||
});
|
||||
shapes.push({ points, holes });
|
||||
});
|
||||
} else {
|
||||
shapes.push({ points, holes });
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
// make sure all shapes are clockwise and all holes are counter-clockwise
|
||||
if (shapeData.fill) {
|
||||
const setDirection = (clockwise) => (path) => {
|
||||
return (THREE.ShapeUtils.isClockWise(path) === clockwise) ? path : path.reverse();
|
||||
};
|
||||
const setDirectionClockWise = setDirection(true);
|
||||
const setDirectionCounterClockWise = setDirection(false);
|
||||
for (const shape of shapes) {
|
||||
shape.points = setDirectionClockWise(shape.points);
|
||||
shape.holes = shape.holes.map(setDirectionCounterClockWise);
|
||||
}
|
||||
}
|
||||
|
||||
return shapes;
|
||||
}
|
||||
|
||||
export const shapeToPointsCornered = memoize(shapeToPointsCorneredRaw, { max: SHAPE_CACHE_LIMIT });
|
||||
function shapeToPointsCorneredRaw(shapeData) {
|
||||
return shapeToPoints(shapeData).map(({ points: oldPoints, holes: oldHoles }) => {
|
||||
const { path: points, map: pointsMap } = addCorners(oldPoints);
|
||||
const { paths: holes, maps: holesMaps } = oldHoles
|
||||
.map(hole => addCorners(hole))
|
||||
.reduce((previous, { path, map }) => {
|
||||
previous.paths.push(path);
|
||||
previous.maps.push(map);
|
||||
|
||||
return previous;
|
||||
}, { paths: [], maps: [] });
|
||||
return { points, holes, pointsMap, holesMaps };
|
||||
});
|
||||
}
|
||||
|
||||
const MAX_ANGLE = 30; // TODO Move to actual const
|
||||
// Adds point when angle between points is larger then MAX_ANGLE
|
||||
const maxAngleRad = (MAX_ANGLE / 360) * (2 * Math.PI);
|
||||
function addCorners(oldPath) {
|
||||
const normals = [];
|
||||
for (let i = 1; i < oldPath.length; i ++) {
|
||||
const pointA = oldPath[i - 1];
|
||||
const pointB = oldPath[i];
|
||||
|
||||
const normal = pointB.subtract(pointA).normalize();
|
||||
|
||||
normals.push(normal);
|
||||
}
|
||||
|
||||
const map = [0];
|
||||
const path = [oldPath[0]];
|
||||
for (let i = 1, length = oldPath.length - 1; i < length; i ++) {
|
||||
const point = oldPath[i];
|
||||
|
||||
const normalA = normals[i - 1];
|
||||
const normalB = normals[i];
|
||||
|
||||
const angle = Math.acos(normalA.dot(normalB));
|
||||
|
||||
if (angle > maxAngleRad) {
|
||||
path.push(new Vector().copy(point));
|
||||
}
|
||||
path.push(point);
|
||||
|
||||
map.push(path.length - 1);
|
||||
}
|
||||
path.push(oldPath[oldPath.length - 1]);
|
||||
|
||||
return { path, map };
|
||||
}
|
108
src/shape/shapeTypeProperties.js
Normal file
108
src/shape/shapeTypeProperties.js
Normal file
@ -0,0 +1,108 @@
|
||||
import { Vector, Matrix } from '@doodle3d/cal';
|
||||
import * as d2Tools from '../constants/d2Tools';
|
||||
import * as d3Tools from '../constants/d3Tools';
|
||||
|
||||
const SHAPE = {
|
||||
D3Visible: true,
|
||||
snapping: false,
|
||||
tools: {
|
||||
[d2Tools.BUCKET]: true,
|
||||
[d2Tools.ERASER]: true,
|
||||
[d3Tools.HEIGHT]: true,
|
||||
[d3Tools.SCULPT]: true,
|
||||
[d3Tools.TWIST]: true
|
||||
}
|
||||
};
|
||||
|
||||
const defaultProperties = {
|
||||
height: 20.0,
|
||||
transform: new Matrix(),
|
||||
z: 0.0,
|
||||
sculpt: [{ pos: 0.0, scale: 1.0 }, { pos: 1.0, scale: 1.0 }],
|
||||
twist: 0.0,
|
||||
fill : false
|
||||
};
|
||||
|
||||
export const SHAPE_TYPE_PROPERTIES = {
|
||||
RECT: {
|
||||
...SHAPE,
|
||||
defaultProperties: { ...defaultProperties, rectSize: new Vector() }
|
||||
},
|
||||
TRIANGLE: {
|
||||
...SHAPE,
|
||||
defaultProperties: { ...defaultProperties, triangleSize: new Vector() }
|
||||
},
|
||||
STAR: {
|
||||
...SHAPE,
|
||||
defaultProperties: { ...defaultProperties, star: { innerRadius: 0, outerRadius: 0, rays: 5 } }
|
||||
},
|
||||
CIRCLE: {
|
||||
...SHAPE,
|
||||
defaultProperties: { ...defaultProperties, circle: { radius: 0, segment: 0 } }
|
||||
},
|
||||
CIRCLE_SEGMENT: {
|
||||
...SHAPE,
|
||||
defaultProperties: { ...defaultProperties, circle: { radius: 0, segment: 0 } }
|
||||
},
|
||||
COMPOUND_PATH: {
|
||||
...SHAPE,
|
||||
defaultProperties: { ...defaultProperties, points: [], holes: [], fill: true }
|
||||
},
|
||||
FREE_HAND: {
|
||||
...SHAPE,
|
||||
snapping: true,
|
||||
defaultProperties: { ...defaultProperties, points: [] }
|
||||
},
|
||||
TEXT: {
|
||||
...SHAPE,
|
||||
defaultProperties: {
|
||||
...defaultProperties,
|
||||
text: { text: '', family: 'Arial', weight: 'normal', style: 'normal' },
|
||||
fill: true
|
||||
}
|
||||
},
|
||||
IMAGE_GUIDE: {
|
||||
...SHAPE,
|
||||
defaultProperties: {
|
||||
...defaultProperties,
|
||||
imageData: { width: 1, height: 1, data: '' },
|
||||
height: 1,
|
||||
fill: true
|
||||
},
|
||||
D3Visible: false,
|
||||
tools: {
|
||||
...SHAPE.tools,
|
||||
[d2Tools.BUCKET]: false,
|
||||
[d2Tools.ERASER]: false,
|
||||
[d3Tools.HEIGHT]: false,
|
||||
[d3Tools.SCULPT]: false,
|
||||
[d3Tools.TWIST]: false
|
||||
}
|
||||
},
|
||||
POLY_POINTS: {
|
||||
...SHAPE,
|
||||
defaultProperties: { ...defaultProperties, shape: { numPoints: 6, radius: 0 } }
|
||||
},
|
||||
HEART: {
|
||||
...SHAPE,
|
||||
defaultProperties: { ...defaultProperties, shape: { width: 30.0, height: 30.0 } }
|
||||
},
|
||||
POLYGON: {
|
||||
...SHAPE,
|
||||
snapping: true,
|
||||
defaultProperties: { ...defaultProperties, points: [] }
|
||||
},
|
||||
BRUSH: {
|
||||
...SHAPE,
|
||||
snapping: false,
|
||||
defaultProperties: { ...defaultProperties, points: [], strokeWidth: 10, fill: true }
|
||||
},
|
||||
EXPORT_SHAPE: {
|
||||
...SHAPE,
|
||||
defaultProperties: {
|
||||
...defaultProperties,
|
||||
shapes: [],
|
||||
fill: true
|
||||
}
|
||||
}
|
||||
};
|
25
src/utils/async.js
Normal file
25
src/utils/async.js
Normal file
@ -0,0 +1,25 @@
|
||||
export function recursivePromiseApply(object, promises = [], first = true) {
|
||||
for (const key in object) {
|
||||
const value = object[key];
|
||||
|
||||
if (value instanceof Promise) {
|
||||
promises.push(value);
|
||||
|
||||
value.then(result => object[key] = result); // eslint-disable-line no-loop-func
|
||||
} else if (value instanceof Array || typeof value === 'object') {
|
||||
recursivePromiseApply(value, promises, false);
|
||||
}
|
||||
}
|
||||
|
||||
return first && Promise.all(promises).then(() => object);
|
||||
}
|
||||
|
||||
export async function asyncIterator(array, callback) {
|
||||
const result = [];
|
||||
for (let i = 0; i < array.length; i ++) {
|
||||
const item = array[i];
|
||||
const itemResult = await callback(item, i);
|
||||
result.push(itemResult);
|
||||
}
|
||||
return result;
|
||||
}
|
91
src/utils/binaryUtils.js
Normal file
91
src/utils/binaryUtils.js
Normal file
@ -0,0 +1,91 @@
|
||||
import { loadImage, prepareImage } from './imageUtils.js';
|
||||
import { Vector } from '@doodle3d/cal';
|
||||
|
||||
// BINARY BUFFER < - > BASE64
|
||||
export function bufferToBase64(buffer) {
|
||||
const arrayBuffer = new Uint8Array(buffer);
|
||||
let binaryString = '';
|
||||
for (let i = 0; i < arrayBuffer.length; i ++) {
|
||||
binaryString += String.fromCharCode(arrayBuffer[i]);
|
||||
}
|
||||
return btoa(binaryString);
|
||||
}
|
||||
|
||||
export function base64ToBuffer(base64) {
|
||||
const binaryString = atob(base64);
|
||||
const arrayBuffer = new Uint8Array(binaryString.length);
|
||||
for (let i = 0; i < binaryString.length; i ++) {
|
||||
arrayBuffer[i] = binaryString.charCodeAt(i);
|
||||
}
|
||||
return arrayBuffer.buffer;
|
||||
}
|
||||
|
||||
// BLOB < - > JSON
|
||||
export async function blobToJSON(blob) {
|
||||
const url = URL.createObjectURL(blob);
|
||||
const json = await fetch(url).then(response => response.json());
|
||||
URL.revokeObjectURL(url);
|
||||
|
||||
return json;
|
||||
}
|
||||
|
||||
export function JSONToBlob(json) {
|
||||
const jsonString = JSON.stringify(json);
|
||||
const blob = new Blob([jsonString], { type: 'aplication/json' });
|
||||
|
||||
return blob;
|
||||
}
|
||||
|
||||
// HTML IMAGE < - > BASE64
|
||||
export function imageToBase64(image) {
|
||||
const imageData = image.toDataURL('image/jpeg');
|
||||
|
||||
return {
|
||||
metadata: { type: 'Image' },
|
||||
data: imageData
|
||||
};
|
||||
}
|
||||
|
||||
export function base64ToImage({ data }) {
|
||||
return loadImage(data).then(prepareImage);
|
||||
}
|
||||
|
||||
// VECTOR ARRAY < - > BASE64
|
||||
export function base64ToVectorArray({ data: base64, metadata: { size } }) {
|
||||
const buffer = base64ToBuffer(base64);
|
||||
|
||||
let flatArray;
|
||||
switch (size) {
|
||||
case 'Float32':
|
||||
flatArray = new Float32Array(buffer);
|
||||
break;
|
||||
case 'Float64':
|
||||
flatArray = new Float64Array(buffer);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
const vectorArray = [];
|
||||
for (let i = 0; i < flatArray.length; i += 2) {
|
||||
const x = flatArray[i];
|
||||
const y = flatArray[i + 1];
|
||||
vectorArray.push(new Vector(x, y));
|
||||
}
|
||||
return vectorArray;
|
||||
}
|
||||
|
||||
export function vectorArrayToBase64(vectorArray) {
|
||||
const flatArray = vectorArray.reduce((array, { x, y }) => {
|
||||
array.push(x, y);
|
||||
return array;
|
||||
}, []);
|
||||
|
||||
const typedArray = new Float32Array(flatArray);
|
||||
const base64String = bufferToBase64(typedArray.buffer);
|
||||
|
||||
return {
|
||||
metadata: { type: 'VectorArray', size: 'Float32' },
|
||||
data: base64String
|
||||
};
|
||||
}
|
13
src/utils/dbUtils.js
Normal file
13
src/utils/dbUtils.js
Normal file
@ -0,0 +1,13 @@
|
||||
export function getLegalDBName(input) {
|
||||
return encodeURIComponent(input.toLowerCase())
|
||||
.replace(/\./g, '%2E')
|
||||
.replace(/!/g, '%21')
|
||||
.replace(/~/g, '%7E')
|
||||
.replace(/\*/g, '%2A')
|
||||
.replace(/'/g, '%27')
|
||||
.replace(/\(/g, '%28')
|
||||
.replace(/\)/g, '%29')
|
||||
.replace(/\-/g, '%2D')
|
||||
.toLowerCase()
|
||||
.replace(/(%..)/g, esc => `(${esc.substr(1)})`);
|
||||
}
|
149
src/utils/exportUtils.js
Normal file
149
src/utils/exportUtils.js
Normal file
@ -0,0 +1,149 @@
|
||||
import { Matrix } from '@doodle3d/cal';
|
||||
import * as exportSTL from '@doodle3d/threejs-export-stl';
|
||||
import * as exportOBJ from '@doodle3d/threejs-export-obj';
|
||||
import * as THREE from 'three';
|
||||
import ThreeBSP from 'three-js-csg';
|
||||
import ClipperShape from '@doodle3d/clipper-js';
|
||||
import ShapeMesh from '../d3/ShapeMesh.js';
|
||||
import { applyMatrixOnShape, pathToVectorPath } from '../math/vectorUtils.js';
|
||||
import { shapeToPoints } from '../shape/shapeToPoints.js';
|
||||
import { SHAPE_TYPE_PROPERTIES } from '../shape/shapeTypeProperties.js';
|
||||
import { bufferToBase64 } from '../utils/binaryUtils.js';
|
||||
|
||||
const THREE_BSP = ThreeBSP(THREE);
|
||||
|
||||
// Causes y and z coord to flip so z is up
|
||||
const ROTATION_MATRIX = new THREE.Matrix4().makeRotationX(Math.PI / 2);
|
||||
const SCALE = 10.0;
|
||||
|
||||
function createExportGeometry(shapeData, offsetSingleWalls, lineWidth) {
|
||||
let shapes = shapeToPoints(shapeData).map(({ points, holes }) => {
|
||||
const shape = applyMatrixOnShape([points, ...holes], shapeData.transform);
|
||||
return new ClipperShape(shape, shapeData.fill, true, false);
|
||||
});
|
||||
|
||||
if (shapeData.fill) {
|
||||
shapes = shapes.map(shape => shape
|
||||
.fixOrientation()
|
||||
.clean(0.01)
|
||||
);
|
||||
} else if (offsetSingleWalls) {
|
||||
shapes = shapes.map(shape => shape
|
||||
.scaleUp(SCALE)
|
||||
.round()
|
||||
.offset(lineWidth * 2 * SCALE, { jointType: 'jtSquare', endType: 'etOpenButt' })
|
||||
.simplify('pftNonZero')
|
||||
.scaleDown(SCALE)
|
||||
);
|
||||
}
|
||||
|
||||
const fill = shapeData.fill || offsetSingleWalls;
|
||||
|
||||
shapes = shapes
|
||||
.map(shape => shape
|
||||
.mapToLower()
|
||||
.map(pathToVectorPath)
|
||||
)
|
||||
.map(paths => paths.filter(path => path.length > 0))
|
||||
.filter(paths => paths.length > 0)
|
||||
.map(paths => paths.map(path => {
|
||||
if (fill) path.push(path[0].clone());
|
||||
return path;
|
||||
}))
|
||||
.map(([points, ...holes]) => ({ points, holes }));
|
||||
|
||||
const objectMesh = new ShapeMesh({
|
||||
...shapeData,
|
||||
transform: new Matrix(),
|
||||
type: 'EXPORT_SHAPE',
|
||||
fill,
|
||||
shapes
|
||||
});
|
||||
|
||||
return objectMesh;
|
||||
}
|
||||
|
||||
export function generateExportMesh(state, options = {}) {
|
||||
const {
|
||||
experimentalColorUnionExport = false,
|
||||
exportLineWidth = 2,
|
||||
offsetSingleWalls = true,
|
||||
matrix = ROTATION_MATRIX
|
||||
} = options;
|
||||
|
||||
const geometries = [];
|
||||
const materials = [];
|
||||
for (const id in state.objectsById) {
|
||||
const shapeData = state.objectsById[id];
|
||||
|
||||
if (!SHAPE_TYPE_PROPERTIES[shapeData.type].D3Visible) continue;
|
||||
|
||||
const { geometry, material } = createExportGeometry(shapeData, offsetSingleWalls, exportLineWidth);
|
||||
let objectGeometry = new THREE.Geometry().fromBufferGeometry(geometry);
|
||||
objectGeometry.mergeVertices();
|
||||
objectGeometry.applyMatrix(state.spaces[shapeData.space].matrix);
|
||||
|
||||
if (experimentalColorUnionExport) objectGeometry = new THREE_BSP(objectGeometry);
|
||||
|
||||
const colorHex = material.color.getHex();
|
||||
const index = materials.findIndex(exportMaterial => exportMaterial.color.getHex() === colorHex);
|
||||
if (index !== -1) {
|
||||
if (experimentalColorUnionExport) {
|
||||
geometries[index] = geometries[index].union(objectGeometry);
|
||||
} else {
|
||||
geometries[index].merge(objectGeometry);
|
||||
}
|
||||
} else {
|
||||
geometries.push(objectGeometry);
|
||||
materials.push(material);
|
||||
}
|
||||
}
|
||||
|
||||
const exportGeometry = geometries.reduce((combinedGeometry, geometry, materialIndex) => {
|
||||
if (experimentalColorUnionExport) geometry = geometry.toMesh().geometry;
|
||||
combinedGeometry.merge(geometry, matrix, materialIndex);
|
||||
return combinedGeometry;
|
||||
}, new THREE.Geometry());
|
||||
const exportMaterial = new THREE.MultiMaterial(materials);
|
||||
|
||||
return new THREE.Mesh(exportGeometry, exportMaterial);
|
||||
}
|
||||
|
||||
export async function createFile(objectsById, type, options) {
|
||||
const exportMesh = generateExportMesh(objectsById, options);
|
||||
|
||||
switch (type) {
|
||||
case 'json-string': {
|
||||
const object = exportMesh.geometry.toJSON().data;
|
||||
const string = JSON.stringify(object);
|
||||
return string;
|
||||
}
|
||||
case 'json-blob': {
|
||||
const object = exportMesh.geometry.toJSON().data;
|
||||
const string = JSON.stringify(object);
|
||||
const blob = new Blob([string], { type: 'application/json' });
|
||||
return blob;
|
||||
}
|
||||
case 'stl-string': {
|
||||
const string = exportSTL.fromMesh(exportMesh, false);
|
||||
return string;
|
||||
}
|
||||
case 'stl-base64': {
|
||||
const buffer = exportSTL.fromMesh(exportMesh, true);
|
||||
return bufferToBase64(buffer);
|
||||
}
|
||||
case 'stl-blob': {
|
||||
const buffer = exportSTL.fromMesh(exportMesh, true);
|
||||
return new Blob([buffer], { type: 'application/vnd.ms-pki.stl' })
|
||||
}
|
||||
case 'obj-blob': {
|
||||
const buffer = await exportOBJ.fromMesh(exportMesh, true);
|
||||
return buffer;
|
||||
}
|
||||
case 'obj-base64': {
|
||||
const buffer = await exportOBJ.fromMesh(exportMesh, true);
|
||||
const base64 = bufferToBase64(buffer);
|
||||
return base64;
|
||||
}
|
||||
}
|
||||
}
|
37
src/utils/imageUtils.js
Normal file
37
src/utils/imageUtils.js
Normal file
@ -0,0 +1,37 @@
|
||||
// import { MAX_IMAGE_SIZE } from '../js/constants/d2Constants.js';
|
||||
|
||||
const MAX_IMAGE_SIZE = 100;
|
||||
|
||||
export function prepareImage(image) {
|
||||
let { width, height } = image;
|
||||
|
||||
const maxImageSize = Math.max(width, height);
|
||||
if (maxImageSize > MAX_IMAGE_SIZE) {
|
||||
const scale = MAX_IMAGE_SIZE / maxImageSize;
|
||||
width = Math.round(width * scale);
|
||||
height = Math.round(height * scale);
|
||||
}
|
||||
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.width = width;
|
||||
canvas.height = height;
|
||||
const context = canvas.getContext('2d');
|
||||
|
||||
context.fillStyle = 'white';
|
||||
context.fillRect(0, 0, width, height);
|
||||
|
||||
context.drawImage(image, 0, 0, width, height);
|
||||
|
||||
return canvas;
|
||||
}
|
||||
|
||||
export function loadImage(src) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const image = new Image();
|
||||
image.onload = () => {
|
||||
resolve(image);
|
||||
};
|
||||
image.onerror = reject;
|
||||
image.src = src;
|
||||
});
|
||||
}
|
8
src/utils/index.js
Normal file
8
src/utils/index.js
Normal file
@ -0,0 +1,8 @@
|
||||
import * as asyncUtils from './async.js';
|
||||
import * as binaryUtils from './async.js';
|
||||
import * as dbUtils from './dbUtils.js';
|
||||
import * as imageUtils from './imageUtils.js';
|
||||
import * as exportUtils from './exportUtils.js';
|
||||
import * as webGLSupport from './webGLSupport.js';
|
||||
|
||||
export { dbUtils, asyncUtils, imageUtils, exportUtils, webGLSupport, binaryUtils };
|
46
src/utils/webGLSupport.js
Normal file
46
src/utils/webGLSupport.js
Normal file
@ -0,0 +1,46 @@
|
||||
export const TOON_SHADER_PREVIEW_EXTENSIONS = ['OES_texture_float_linear'];
|
||||
export const TOON_SHADER_THUMBNAIL_EXTENSIONS = [];
|
||||
|
||||
export const isWebGLAvailable = (() => {
|
||||
const canvas = document.createElement('canvas');
|
||||
|
||||
if (!window.WebGLRenderingContext) return false;
|
||||
|
||||
const webglContext = canvas.getContext('webgl') || canvas.getContext('experimental-webgl');
|
||||
return Boolean(webglContext);
|
||||
})();
|
||||
|
||||
export const supportedExtensions = (() => {
|
||||
if (!isWebGLAvailable) return [];
|
||||
|
||||
const canvas = document.createElement('canvas');
|
||||
|
||||
const webglContext = canvas.getContext('webgl') || canvas.getContext('experimental-webgl');
|
||||
return webglContext.getSupportedExtensions();
|
||||
})();
|
||||
|
||||
export const hasExtensionsFor = ((object) => {
|
||||
const enabled = {};
|
||||
loop: for (const key in object) {
|
||||
if (!isWebGLAvailable) {
|
||||
enabled[key] = false;
|
||||
continue loop;
|
||||
}
|
||||
|
||||
const neededExtensions = object[key];
|
||||
|
||||
for (const neededExtension of neededExtensions) {
|
||||
if (supportedExtensions.indexOf(neededExtension) === -1) {
|
||||
enabled[key] = false;
|
||||
continue loop;
|
||||
}
|
||||
}
|
||||
|
||||
enabled[key] = true;
|
||||
}
|
||||
|
||||
return enabled;
|
||||
})({
|
||||
toonShaderPreview: TOON_SHADER_PREVIEW_EXTENSIONS,
|
||||
toonShaderThumbnail: TOON_SHADER_THUMBNAIL_EXTENSIONS
|
||||
});
|
Loading…
Reference in New Issue
Block a user