diff --git a/package-lock.json b/package-lock.json index 140403b..06c15ef 100644 --- a/package-lock.json +++ b/package-lock.json @@ -22,6 +22,11 @@ "resolved": "https://registry.npmjs.org/@doodle3d/clipper-lib/-/clipper-lib-6.4.2-b.tgz", "integrity": "sha512-glELSijsD9b+/0d9iOdasBwqH3s+xPxD59tJ7aXkBx7klugygGOMXn7PB05AdhVyA1OYMj7GUCegaQa7nvLtmQ==" }, + "@doodle3d/potrace-js": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/@doodle3d/potrace-js/-/potrace-js-0.0.6.tgz", + "integrity": "sha512-w1+sG3ClsSaQwo3ks5wl6QLe4aWEHBe8QePq0IeAcj+lypqo770sUcWVfEZWUFBumAhSlCJg3GRc8MsycHy8LA==" + }, "@doodle3d/threejs-export-obj": { "version": "0.0.4", "resolved": "https://registry.npmjs.org/@doodle3d/threejs-export-obj/-/threejs-export-obj-0.0.4.tgz", @@ -958,6 +963,11 @@ "dev": true, "optional": true }, + "bowser": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/bowser/-/bowser-1.8.1.tgz", + "integrity": "sha512-NMPaR8ILtdLSWzxQtEs16XbxMcY8ohWGQ5V+TZSJS3fNUt/PBAGkF6YWO9B/4qWE23bK3o0moQKq8UyFEosYkA==" + }, "brace-expansion": { "version": "1.1.8", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.8.tgz", diff --git a/package.json b/package.json index 115923c..23ac6f4 100755 --- a/package.json +++ b/package.json @@ -14,9 +14,11 @@ "dependencies": { "@doodle3d/cal": "0.0.8", "@doodle3d/clipper-js": "^1.0.7", + "@doodle3d/potrace-js": "0.0.6", "@doodle3d/threejs-export-obj": "0.0.4", "@doodle3d/threejs-export-stl": "0.0.3", "babel-plugin-inline-import": "^2.0.6", + "bowser": "^1.8.1", "imports-loader": "^0.7.1", "memoizee": "^0.3.9", "proptypes": "^1.1.0", diff --git a/src/constants/contextTools.js b/src/constants/contextTools.js new file mode 100644 index 0000000..a42703d --- /dev/null +++ b/src/constants/contextTools.js @@ -0,0 +1,88 @@ +export const DUPLICATE = 'duplicate-tool'; +export const DELETE = 'delete-tool'; +export const COLOR_PICKER = 'color-picker-tool'; +export const ERASER_SIZE = 'eraser-size-tool'; +export const BRUSH_SIZE = 'brush-size-tool'; +export const FILL_TOGGLE = 'fill-toggle-tool'; +export const ALIGN = 'align-tool'; +export const ADVANCED = 'advanced-tool'; + +export const LIGHT_BLUE = 'color-light-blue'; +export const LIGHT_GREEN = 'color-light-green'; +export const LIGHT_PINK = 'color-light-pink'; +export const LIGHT_YELLOW = 'color-light-yellow'; +export const BLUE = 'color-blue'; +export const GREEN = 'color-green'; +export const PINK = 'color-pink'; +export const YELLOW = 'color-yellow'; +export const DARK_BLUE = 'color-dark-blue'; +export const DARK_GREEN = 'color-dark-green'; +export const DARK_PINK = 'color-dark-pink'; +export const DARK_YELLOW = 'color-dark-yellow'; + +export const COLORS = [ + LIGHT_BLUE, + LIGHT_GREEN, + LIGHT_PINK, + LIGHT_YELLOW, + BLUE, + GREEN, + PINK, + YELLOW, + DARK_BLUE, + DARK_GREEN, + DARK_PINK, + DARK_YELLOW +]; + +export const ERASER_SIZE_SMALL = 'eraser-size-small'; +export const ERASER_SIZE_MEDIUM = 'eraser-size-medium'; +export const ERASER_SIZE_LARGE = 'eraser-size-large'; + +export const ERASER_SIZE_TOOLS = [ + ERASER_SIZE_SMALL, + ERASER_SIZE_MEDIUM, + ERASER_SIZE_LARGE +]; + +export const BRUSH_SIZE_SMALL = 'brush-size-small'; +export const BRUSH_SIZE_MEDIUM = 'brush-size-medium'; +export const BRUSH_SIZE_LARGE = 'brush-size-large'; + +export const BRUSH_SIZE_TOOLS = [ + BRUSH_SIZE_SMALL, + BRUSH_SIZE_MEDIUM, + BRUSH_SIZE_LARGE +]; + +export const FILL_TOGGLE_FILL = 'fill-toggle-fill'; +export const FILL_TOGGLE_OUTLINE = 'fill-toggle-outline'; + +export const FILL_TOGGLE_TOOLS = [ + FILL_TOGGLE_FILL, + FILL_TOGGLE_OUTLINE +]; + +export const ALIGN_LEFT = 'align-left'; +export const ALIGN_HORIZONTAL = 'align-horizontal'; +export const ALIGN_RIGHT = 'align-right'; +export const ALIGN_TOP = 'align-top'; +export const ALIGN_VERTICAL = 'align-vertical'; +export const ALIGN_BOTTOM = 'align-bottom'; + +export const ALIGN_TOOLS = [ + ALIGN_LEFT, + ALIGN_HORIZONTAL, + ALIGN_RIGHT, + ALIGN_TOP, + ALIGN_VERTICAL, + ALIGN_BOTTOM +]; + +export const UNION = 'union-tool'; +export const INTERSECT = 'intersect-tool'; + +export const ADVANCED_TOOLS = [ + UNION, + INTERSECT +]; diff --git a/src/constants/d2Constants.js b/src/constants/d2Constants.js new file mode 100644 index 0000000..3bded5f --- /dev/null +++ b/src/constants/d2Constants.js @@ -0,0 +1,43 @@ +import * as contextTools from './contextTools.js'; + +export const LINE_WIDTH = 3; +export const LINE_COLLISION_MARGIN = 4; +export const MIN_ZOOM = 0.5; +export const MAX_ZOOM = 10; +export const CANVAS_SIZE = 100; +export const GRID_SIZE = 10; +export const IMAGE_GUIDE_TRANSPARENCY = 0.7; +export const FILL_TRANSPARENCY = 0.9; +export const LINE_TRANSPARENCY = 1.0; +export const DESELECT_TRANSPARENCY = 0.2; +// default flood fill tolereance of image trace +export const DEFAULT_TRACE_TOLERANCE = 20; +// initial scale of image inside workspace +export const INITIAL_IMAGE_SCALE = 0.8; +// big images can lead to performance penalties, images bigger then MAX_IMAGE_SIZE get scaled down +export const MAX_IMAGE_SIZE = 1000; +// TODO we want to use different snapping distances for mouse and for touch events +export const SNAPPING_DISTANCE = 7.0; +export const MAX_TRACE_TOLERANCE = 256; +export const SELECTION_VIEW_MIN_SCALE = 50; +export const SELECTION_VIEW_MIN_AXIS_SCALE = 80; +export const POTRACE_OPTIONS = { + turnpolicy: 'black', + turdsize: 5.0, + optcurve: false, + alphamax: 0.5, + opttolerance: 0.2 +}; +export const ERASER_SIZES = { + [contextTools.ERASER_SIZE_SMALL]: 10, + [contextTools.ERASER_SIZE_MEDIUM]: 30, + [contextTools.ERASER_SIZE_LARGE]: 50 +}; +// sizes are in mm +export const BRUSH_SIZES = { + [contextTools.BRUSH_SIZE_SMALL]: 5, + [contextTools.BRUSH_SIZE_MEDIUM]: 10, + [contextTools.BRUSH_SIZE_LARGE]: 15 +}; + +export const CLIPPER_PRECISION = 100; // accurate to the hundredth diff --git a/src/constants/d3Constants.js b/src/constants/d3Constants.js new file mode 100644 index 0000000..8784b46 --- /dev/null +++ b/src/constants/d3Constants.js @@ -0,0 +1,10 @@ +export const SCULPT_LIMIT = 0.6; +export const DESELECT_TRANSPARENCY = 0.8; +export const MIN_CAMERA_ZOOM = 10; +export const MAX_CAMERA_ZOOM = 600; +export const MAX_CAMERA_PAN = 150; +// 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 +export const LEGACY_HEIGHT_STEP = 10; diff --git a/src/constants/exportConstants.js b/src/constants/exportConstants.js new file mode 100644 index 0000000..dfafcd2 --- /dev/null +++ b/src/constants/exportConstants.js @@ -0,0 +1 @@ + export const LINE_WIDTH = 1.0; diff --git a/src/constants/general.js b/src/constants/general.js new file mode 100644 index 0000000..1063135 --- /dev/null +++ b/src/constants/general.js @@ -0,0 +1,48 @@ +import * as contextTools from './contextTools.js'; +import bowser from 'bowser'; + +export const SHAPE_CACHE_LIMIT = 50; +export const IS_CORDOVA = typeof cordova !== 'undefined'; +export const MAX_ANGLE = 30; // if shape has an corner sharper then MAX_ANGLE said corner will be sharp (3D) +export const PIXEL_RATIO = 1.0; +// On android and iOS autofocus means the keyboard pops up for one second and then hides +// Disable autofocus on these devices +export const AUTO_FOCUS_TEXT_FIELDS = !(bowser.mobile || bowser.tablet); + +export const COLOR_STRING_TO_HEX = { + [contextTools.LIGHT_BLUE]: 0xc8e4f7, + [contextTools.LIGHT_GREEN]: 0xcbe6c0, + [contextTools.LIGHT_PINK]: 0xf8c4d8, + [contextTools.LIGHT_YELLOW]: 0xf5f5c0, + [contextTools.BLUE]: 0x92c8ef, + [contextTools.GREEN]: 0x99cc81, + [contextTools.PINK]: 0xf28bb1, + [contextTools.YELLOW]: 0xebea7f, + [contextTools.DARK_BLUE]: 0x50a8e4, + [contextTools.DARK_GREEN]: 0x5aae31, + [contextTools.DARK_PINK]: 0xe94481, + [contextTools.DARK_YELLOW]: 0xdfde24 +}; +export const COLOR_HEX_TO_STRING = Object + .entries(COLOR_STRING_TO_HEX) + .reduce((obj, [key, value]) => { + obj[value] = key; + return obj; + }, {}); + +// LEGACY +// add old color codes to corresponding color strings +// so old doodles with old colors are previewd correctly in color picker color selector +COLOR_HEX_TO_STRING[0x96cbef] = contextTools.BLUE; +COLOR_HEX_TO_STRING[0x9bca87] = contextTools.GREEN; +COLOR_HEX_TO_STRING[0xf08eb2] = contextTools.PINK; +COLOR_HEX_TO_STRING[0xfff59a] = contextTools.YELLOW; +COLOR_HEX_TO_STRING[0x7098b3] = contextTools.DARK_BLUE; +COLOR_HEX_TO_STRING[0x7ab063] = contextTools.DARK_GREEN; +COLOR_HEX_TO_STRING[0xb36984] = contextTools.DARK_PINK; +COLOR_HEX_TO_STRING[0xf5e872] = contextTools.DARK_YELLOW; +COLOR_HEX_TO_STRING[0x00DDFF] = contextTools.BLUE; + +export const REQUEST_CONFIG = { + LARGE: { timeout: 0 } +}; diff --git a/src/constants/index.js b/src/constants/index.js new file mode 100644 index 0000000..b6fd96e --- /dev/null +++ b/src/constants/index.js @@ -0,0 +1,21 @@ +import * as contextTools from './contextTools.js'; +import * as d2Constants from './d2Constants.js'; +import * as d2Tools from './d2Tools.js'; +import * as d3Constants from './d3Constants.js'; +import * as d3Tools from './d3Tools.js'; +import * as exportConstants from './exportConstants.js'; +import * as general from './general.js'; +import * as saveConstants from './saveConstants.js'; +import * as shapeTypeProperties from './shapeTypeProperties.js'; + +export { + contextTool, + d2Constant, + d2Tool, + d3Constant, + d3Tool, + exportConstant, + genera, + saveConstant, + shapeTypePropertie +}; diff --git a/src/constants/saveConstants.js b/src/constants/saveConstants.js new file mode 100644 index 0000000..e12f66f --- /dev/null +++ b/src/constants/saveConstants.js @@ -0,0 +1,4 @@ +export const THUMBNAIL_WIDTH = 240 * 2; // multiply times 2 because retina +export const THUMBNAIL_HEIGHT = 200 * 2; +export const IMAGE_TYPE = 'image/jpeg'; +export const IMAGE_QUALITY = 1; // 0 - 1 diff --git a/src/shape/shapeTypeProperties.js b/src/constants/shapeTypeProperties.js similarity index 100% rename from src/shape/shapeTypeProperties.js rename to src/constants/shapeTypeProperties.js diff --git a/src/d3/ShapeMesh.js b/src/d3/ShapeMesh.js index a06e96b..d223d6a 100644 --- a/src/d3/ShapeMesh.js +++ b/src/d3/ShapeMesh.js @@ -1,5 +1,5 @@ import { Vector } from '@doodle3d/cal'; -import { applyMatrixOnPath } from '../math/vectorUtils.js'; +import { applyMatrixOnPath } from '../utils/vectorUtils.js'; import { shapeToPointsCornered } from '../shape/shapeToPoints.js'; import * as THREE from 'three'; import { getPointsBounds, shapeChanged } from '../shape/shapeDataUtils.js'; diff --git a/src/d3/ShapesManager.js b/src/d3/ShapesManager.js index e3ce777..062eb6e 100644 --- a/src/d3/ShapesManager.js +++ b/src/d3/ShapesManager.js @@ -1,4 +1,4 @@ -import { SHAPE_TYPE_PROPERTIES } from '../shape/shapeTypeProperties.js'; +import { SHAPE_TYPE_PROPERTIES } from '../constants/shapeTypeProperties.js'; import * as THREE from 'three'; import ShapeMesh from './ShapeMesh.js'; diff --git a/src/d3/createScene.js b/src/d3/createScene.js index 76c252e..c6e9716 100644 --- a/src/d3/createScene.js +++ b/src/d3/createScene.js @@ -2,9 +2,7 @@ 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; +import { CANVAS_SIZE } from '../constants/d2Constants.js'; const CANVAS_WIDTH = CANVAS_SIZE * 2; const CANVAS_HEIGHT = CANVAS_SIZE * 2; diff --git a/src/index.js b/src/index.js index 88d834a..da2bb3d 100644 --- a/src/index.js +++ b/src/index.js @@ -3,5 +3,6 @@ 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'; +import * as constants from './constants/index.js'; -export { math, shape, utils, d3, components }; +export { math, shape, utils, d3, components, constants }; diff --git a/src/math/index.js b/src/math/index.js deleted file mode 100644 index 8cacc2a..0000000 --- a/src/math/index.js +++ /dev/null @@ -1,3 +0,0 @@ -import * as vectorUtils from './vectorUtils'; - -export { vectorUtils }; diff --git a/src/shape/JSONToSketchData.js b/src/shape/JSONToSketchData.js index 8d00e8a..1e7aff8 100644 --- a/src/shape/JSONToSketchData.js +++ b/src/shape/JSONToSketchData.js @@ -3,9 +3,7 @@ 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; +import { LEGACY_HEIGHT_STEP } from '../constants/d3Constants.js'; async function JSONToSketchData({ data, appVersion }) { let sketchData = JSON.parse(data, (key, value) => { diff --git a/src/shape/index.js b/src/shape/index.js index b0c6955..8bda739 100644 --- a/src/shape/index.js +++ b/src/shape/index.js @@ -1,6 +1,5 @@ import shapeToPoints from './shapeToPoints.js'; import JSONToSketchData from './JSONToSketchData.js'; import ShapeDataUtils from './shapeDataUtils.js'; -import shapeTypeProperties from './shapeTypeProperties.js'; -export { shapeToPoints, JSONToSketchData, shapeTypeProperties }; +export { shapeToPoints, JSONToSketchData, ShapeDataUtils }; diff --git a/src/shape/shapeDataUtils.js b/src/shape/shapeDataUtils.js index 6b6cef6..8067816 100644 --- a/src/shape/shapeDataUtils.js +++ b/src/shape/shapeDataUtils.js @@ -1,5 +1,6 @@ import memoize from 'memoizee'; import { Vector } from '@doodle3d/cal'; +import { SHAPE_CACHE_LIMIT } from '../constants/general.js'; export function shapeChanged(oldShapeData, newShapeData) { const pointsChanged = oldShapeData.points !== newShapeData.points; @@ -17,10 +18,6 @@ export function shapeChanged(oldShapeData, newShapeData) { 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), []); diff --git a/src/shape/shapeToPoints.js b/src/shape/shapeToPoints.js index 60dfae3..faf2fa4 100644 --- a/src/shape/shapeToPoints.js +++ b/src/shape/shapeToPoints.js @@ -2,11 +2,10 @@ 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; +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'; const HEART_BEZIER_PATH = [ new Vector(0.0, -0.5), @@ -232,7 +231,6 @@ function shapeToPointsCorneredRaw(shapeData) { }); } -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) { diff --git a/src/utils/exportUtils.js b/src/utils/exportUtils.js index ce4cdd9..5a01da7 100644 --- a/src/utils/exportUtils.js +++ b/src/utils/exportUtils.js @@ -5,9 +5,9 @@ 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 { applyMatrixOnShape, pathToVectorPath } from '../utils/vectorUtils.js'; import { shapeToPoints } from '../shape/shapeToPoints.js'; -import { SHAPE_TYPE_PROPERTIES } from '../shape/shapeTypeProperties.js'; +import { SHAPE_TYPE_PROPERTIES } from '../constants/shapeTypeProperties.js'; import { bufferToBase64 } from '../utils/binaryUtils.js'; const THREE_BSP = ThreeBSP(THREE); diff --git a/src/utils/index.js b/src/utils/index.js index f045091..722b458 100644 --- a/src/utils/index.js +++ b/src/utils/index.js @@ -4,5 +4,7 @@ import * as dbUtils from './dbUtils.js'; import * as imageUtils from './imageUtils.js'; import * as exportUtils from './exportUtils.js'; import * as webGLSupport from './webGLSupport.js'; +import * as vectorUtils from './vectorUtils.js'; +import * as textUtils from './textUtils.js'; -export { dbUtils, asyncUtils, imageUtils, exportUtils, webGLSupport, binaryUtils }; +export { dbUtils, asyncUtils, imageUtils, exportUtils, webGLSupport, binaryUtils, vectorUtils, textUt }; diff --git a/src/utils/textUtils.js b/src/utils/textUtils.js new file mode 100644 index 0000000..37b1b73 --- /dev/null +++ b/src/utils/textUtils.js @@ -0,0 +1,58 @@ +import { SHAPE_CACHE_LIMIT } from '../constants/general.js'; +import { Text } from '@doodle3d/cal'; +import { POTRACE_OPTIONS } from '../constants/d2Constants.js'; +import * as POTRACE from '@doodle3d/potrace-js'; +import ClipperShape from '@doodle3d/clipper-js'; +import { shapeToVectorShape } from './vectorUtils.js'; +import memoize from 'memoizee'; + +const MARGIN = 200; + +export const createText = memoize(createTextRaw, { max: SHAPE_CACHE_LIMIT }); +export function createTextRaw(text, size, family, style, weight) { + if (text === '') return []; + + const { width, height, canvas } = createTextCanvas(text, size, family, style, weight); + + // TODO merge with potrace in flood fill trace reducer + const paths = POTRACE.getPaths(POTRACE.traceCanvas(canvas, POTRACE_OPTIONS)); + + const halfWidth = width / 2; + const halfHeight = height / 2; + const pathsOffset = paths.map(path => path.map(({ x, y }) => ({ + x: (x - halfWidth) / 10, + y: (y - halfHeight) / 10 + }))); + + const shapes = new ClipperShape(pathsOffset, true, true, false) + .fixOrientation() + .seperateShapes() + .map(shape => shape.mapToLower()) + .map(shapeToVectorShape); + + return shapes; +} + +const textContext = new Text(); +export function createTextCanvas(text, size, family, style, weight) { + textContext.size = size; + textContext.family = family; + textContext.style = style; + textContext.weight = weight; + + const canvas = document.createElement('canvas'); + const context = canvas.getContext('2d'); + + const width = Math.ceil(textContext.measure(context, text)) + 2 * MARGIN; + const height = size + 2 * MARGIN; + + canvas.width = width; + canvas.height = height; + + context.fillStyle = 'white'; + context.fillRect(0, 0, width, height); + + textContext.drawText(context, text, MARGIN, height / 2); + + return { width, height, canvas }; +} diff --git a/src/math/vectorUtils.js b/src/utils/vectorUtils.js similarity index 100% rename from src/math/vectorUtils.js rename to src/utils/vectorUtils.js