initial commit

This commit is contained in:
casperlamboo 2017-10-24 12:33:14 +02:00
commit 70d5ea8010
37 changed files with 7493 additions and 0 deletions

22
.babelrc Normal file
View 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
View File

@ -0,0 +1,6 @@
lib
module
node_modules

0
README.md Executable file
View File

5446
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

64
package.json Executable file
View 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"
}
}

View 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
View File

@ -0,0 +1,3 @@
import DoodlePreview from './DoodlePreview.js';
export { DoodlePreview };

34
src/constants/d2Tools.js Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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 };

View 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
View 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
View 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);
}
`;

View 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);
}
`;

View 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
View 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;
}
`;

View 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
View 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
View 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
View File

@ -0,0 +1,3 @@
import * as vectorUtils from './vectorUtils';
export { vectorUtils };

13
src/math/vectorUtils.js Normal file
View 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);

View 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
View 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 };

View 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
View 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 };
}

View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
});