2017-10-24 12:33:14 +02:00
|
|
|
import * as THREE from 'three';
|
|
|
|
import memoize from 'memoizee';
|
|
|
|
import { Vector } from '@doodle3d/cal';
|
|
|
|
import ClipperShape from '@doodle3d/clipper-js';
|
2017-10-25 11:39:02 +02:00
|
|
|
import { pathToVectorPath } from '../utils/vectorUtils.js';
|
|
|
|
import { CLIPPER_PRECISION } from '../constants/d2Constants.js';
|
|
|
|
import { MAX_ANGLE } from '../constants/d3Constants.js';
|
|
|
|
import { SHAPE_CACHE_LIMIT } from '../constants/general.js';
|
2017-10-25 11:58:37 +02:00
|
|
|
import { createText } from '../utils/textUtils.js';
|
2017-11-08 22:01:31 +01:00
|
|
|
import { segmentBezierPath } from '../utils/curveUtils.js';
|
2017-10-24 12:33:14 +02:00
|
|
|
|
|
|
|
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 };
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
// 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 };
|
|
|
|
}
|