import * as THREE from 'three'; import memoize from 'memoizee'; import { Vector } from '@doodle3d/cal'; import ClipperShape from '@doodle3d/clipper-js'; 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'; import { createText } from '../utils/textUtils.js'; import { segmentBezierPath } from '../utils/curveUtils.js'; 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 }; }