initial commit

pull/6/head
casperlamboo 5 years ago
commit 70d5ea8010
  1. 22
      .babelrc
  2. 6
      .gitignore
  3. 0
      README.md
  4. 5446
      package-lock.json
  5. 64
      package.json
  6. 67
      src/components/DoodlePreview.js
  7. 3
      src/components/index.js
  8. 34
      src/constants/d2Tools.js
  9. 4
      src/constants/d3Tools.js
  10. 414
      src/d3/ShapeMesh.js
  11. 164
      src/d3/ToonShaderRenderChain.js
  12. 74
      src/d3/createScene.js
  13. 27
      src/d3/createSceneData.js
  14. 7
      src/d3/index.js
  15. 18
      src/d3/shaders/fragmentShaderCombineTextures.js
  16. 16
      src/d3/shaders/fragmentShaderDepth.js
  17. 28
      src/d3/shaders/fragmentShaderSobel.js
  18. 23
      src/d3/shaders/fragmentShaderSobelDepth.js
  19. 22
      src/d3/shaders/fragmentShaderSobelNormal.js
  20. 16
      src/d3/shaders/vertexShaderDepth.js
  21. 8
      src/d3/shaders/vertexShaderPostprocessing.js
  22. 106
      src/d3/shapesManager.js
  23. 7
      src/index.js
  24. 3
      src/math/index.js
  25. 13
      src/math/vectorUtils.js
  26. 78
      src/shape/JSONToSketchData.js
  27. 6
      src/shape/index.js
  28. 71
      src/shape/shapeDataUtils.js
  29. 269
      src/shape/shapeToPoints.js
  30. 108
      src/shape/shapeTypeProperties.js
  31. 25
      src/utils/async.js
  32. 91
      src/utils/binaryUtils.js
  33. 13
      src/utils/dbUtils.js
  34. 149
      src/utils/exportUtils.js
  35. 37
      src/utils/imageUtils.js
  36. 8
      src/utils/index.js
  37. 46
      src/utils/webGLSupport.js

@ -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

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

5446
package-lock.json generated

File diff suppressed because it is too large Load Diff

@ -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"
}
}

@ -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;

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

@ -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
];

@ -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

@ -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;

@ -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]);
}
}

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

@ -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

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

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

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

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

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

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

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

@ -0,0 +1,8 @@
export default `
varying vec2 vUv;
void main() {
vUv = uv;
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
}
`;

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

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

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

@ -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);

@ -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;

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

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