/** * PathMath script * * This is a library that provides mathematical operations involving Paths. * It intended to be used by other scripts and has no stand-alone * functionality of its own. All the library's operations are exposed by the * PathMath object created by this script. */ var PathMath = (function() { /** * A vector used to define a homogeneous point or a direction. * @typedef {number[]} Vector */ /** * A line segment defined by two homogeneous 2D points. * @typedef {Vector[]} Segment */ /** * Information about a path's 2D transform. * @typedef {Object} PathTransformInfo * @property {number} angle * The path's rotation angle in radians. * @property {number} cx * The x coordinate of the center of the path's bounding box. * @property {number} cy * The y coordinate of the center of the path's bounding box. * @property {number} height * The unscaled height of the path's bounding box. * @property {number} scaleX * The path's X-scale. * @property {number} scaleY * The path's Y-scale. * @property {number} width * The unscaled width of the path's bounding box. */ /** * A simple class for BoundingBoxes. * @param {Number} left * @param {Number} top * @param {Number} width * @param {Number} height */ function BoundingBox(left, top, width, height) { this.left = left; this.top = top; this.width = width; this.height = height; }; /** * @private * Adds two bounding boxes. * @param {BoundingBox} a * @param {BoundingBox} b * @return {BoundingBox} */ function _addBoundingBoxes(a, b) { var left = Math.min(a.left, b.left); var top = Math.min(a.top, b.top); var right = Math.max(a.left + a.width, b.left + b.width); var bottom = Math.max(a.top + a.height, b.top + b.height); return new BoundingBox(left, top, right - left, bottom - top); }; /** * Calculates the bounding box for a list of paths. * @param {Path | Path[]} paths * @return {BoundingBox} */ function getBoundingBox(paths) { if(!_.isArray(paths)) paths = [paths]; var result; _.each(paths, function(p) { var pBox = _getSingleBoundingBox(p); if(result) result = _addBoundingBoxes(result, pBox); else result = pBox; }); return result; }; /** * Returns the center of the bounding box countaining a path or list * of paths. The center is returned as a 2D homongeneous point * (It has a third component which is always 1 which is helpful for * affine transformations). * @param {(Path|Path[])} paths * @return {Vector} */ function getCenter(paths) { if(!_.isArray(pathjs)) paths = [paths]; var bbox = getBoundingBox(paths); var cx = bbox.left + bbox.width/2; var cy = bbox.top + bbox.height/2; return [cx, cy, 1]; }; /** * @private * Calculates the bounding box for a single path. * @param {Path} path * @return {BoundingBox} */ function _getSingleBoundingBox(path) { var pathData = normalizePath(path); var width = pathData.width; var height = pathData.height; var left = pathData.left - width/2; var top = pathData.top - height/2; return new BoundingBox(left, top, width, height); }; /** * Gets the 2D transform information about a path. * @param {Path} path * @return {PathTransformInfo} */ function getTransformInfo(path) { var scaleX = path.get('scaleX'); var scaleY = path.get('scaleY'); var angle = path.get('rotation')/180*Math.PI; // The transformed center of the path. var cx = path.get('left'); var cy = path.get('top'); // The untransformed width and height. var width = path.get('width'); var height = path.get('height'); return { angle: angle, cx: cx, cy: cy, height: height, scaleX: scaleX, scaleY: scaleY, width: width }; }; /** * Produces a merged path string from a list of path objects. * @param {Path[]} paths * @return {String} */ function mergePathStr(paths) { var merged = []; var bbox = getBoundingBox(paths); _.each(paths, function(p) { var pbox = getBoundingBox(p); // Convert the path to a normalized polygonal path. p = normalizePath(p); var parsed = JSON.parse(p._path); _.each(parsed, function(pathTuple, index) { var dx = pbox.left - bbox.left; var dy = pbox.top - bbox.top; // Move and Line tuples var x = pathTuple[1] + dx; var y = pathTuple[2] + dy; merged.push([pathTuple[0], x, y]); }); }); return JSON.stringify(merged); }; /** * Reproduces the data for a polygonal path such that the scales are 1 and * its rotate is 0. * This can also normalize freehand paths, but they will be converted to * polygonal paths. The quatric Bezier curves used in freehand paths are * so short though, that it doesn't make much difference though. * @param {Path} * @return {PathData} */ function normalizePath(path) { var segments = toSegments(path); return segmentsToPath(segments); } /** * Produces the data for creating a path from a list of segments forming a * continuous path. * @param {Segment[]} * @return {PathData} */ function segmentsToPath(segments) { var left = segments[0][0][0]; var right = segments[0][0][0]; var top = segments[0][0][1]; var bottom = segments[0][0][1]; // Get the bounds of the segment. var pts = []; var isFirst = true; _.each(segments, function(segment) { var p1 = segment[0]; if(isFirst) { isFirst = false; pts.push(p1); } var p2 = segment[1]; left = Math.min(left, p1[0], p2[0]); right = Math.max(right, p1[0], p2[0]); top = Math.min(top, p1[1], p2[1]); bottom = Math.max(bottom, p1[1], p2[1]); pts.push(p2); }); // Get the path's left and top coordinates. var width = right-left; var height = bottom-top; var cx = left + width/2; var cy = top + height/2; // Convert the points to a _path. var _path = []; var firstPt = true; _.each(pts, function(pt) { var type = 'L'; if(firstPt) { type = 'M'; firstPt = false; } _path.push([type, pt[0]-left, pt[1]-top]); }); return { _path: JSON.stringify(_path), left: cx, top: cy, width: width, height: height }; } /** * Converts a path into a list of line segments. As the nature of this * method suggests, this does not work to convert quadratic paths * (for freehand paths) or cubic paths (for oval paths). */ function toSegments(path) { var _path = JSON.parse(path.get('_path')); var transformInfo = getTransformInfo(path); var segments = []; var prevPt; _.each(_path, function(tuple) { var type = tuple[0]; // In freehand paths, approximate the quadratic curve by just // using the endpoint of the curve. The curves are so short that // they may as well be tiny segments anyways. if(type === 'Q') { tuple[1] = tuple[3]; tuple[2] = tuple[4]; type = 'L'; } // Transform the point to 2D homogeneous map coordinates. pt = tupleToPoint(tuple, transformInfo); // If we have an 'L' type point, then add the segment. // Either way, keep track of the point we've moved to. if(type === 'L' && !(prevPt[0] == pt[0] && prevPt[1] == pt[1])) segments.push([prevPt, pt]); prevPt = pt; }); return segments; } /** * Transforms a tuple for a point in a path into a point in * homogeneous 2D map coordinates. * @param {PathTuple} tuple * @param {PathTransformInfo} transformInfo * @return {Vector} */ function tupleToPoint(tuple, transformInfo) { var width = transformInfo.width; var height = transformInfo.height; var scaleX = transformInfo.scaleX; var scaleY = transformInfo.scaleY; var angle = transformInfo.angle; var cx = transformInfo.cx; var cy = transformInfo.cy; // The point in path coordinates, relative to the path center. var x = tuple[1] - width/2; var y = tuple[2] - height/2; var pt = [x,y,1]; // The transform of the point from path coordinates to map // coordinates. var scale = MatrixMath.scale([scaleX, scaleY]); var rotate = MatrixMath.rotate(angle); var transform = MatrixMath.translate([cx, cy]); transform = MatrixMath.multiply(transform, rotate); transform = MatrixMath.multiply(transform, scale); return MatrixMath.multiply(transform, pt); } on('chat:message', function(msg) { if(msg.type === 'api' && msg.content.indexOf('!pathInfo') === 0) { log('!pathInfo'); try { var path = findObjs({ _type: 'path', _id: msg.selected[0]._id })[0]; log(path); log(path.get('_path')); var segments = toSegments(path); log('Segments: '); log(segments); var pathData = segmentsToPath(segments); log('New path data: '); log(pathData); var curPage = path.get('_pageid'); _.extend(pathData, { stroke: '#ff0000', _pageid: curPage, layer: path.get('layer') }); var newPath = createObj('path', pathData); log(newPath); } catch(err) { log('!pathInfo ERROR: '); log(err.message); } } }); return { BoundingBox: BoundingBox, getBoundingBox: getBoundingBox, getCenter: getCenter, getTransformInfo: getTransformInfo, mergePathStr: mergePathStr, normalizePath: normalizePath, segmentsToPath: segmentsToPath, toSegments: toSegments, tupleToPoint: tupleToPoint }; })();