UNPKG

@joint/core

Version:

JavaScript diagramming library

1,337 lines (996 loc) 72.7 kB
// Accepts path data string, array of segments, array of Curves and/or Lines, or a Polyline. // Path created is not guaranteed to be a valid (serializable) path (might not start with an M). import { Polyline } from './polyline.mjs'; import { Rect } from './rect.mjs'; import { Point } from './point.mjs'; import { Line } from './line.mjs'; import { Curve } from './curve.mjs'; import { types } from './types.mjs'; import { extend } from './extend.mjs'; export const Path = function(arg) { if (!(this instanceof Path)) { return new Path(arg); } if (typeof arg === 'string') { // create from a path data string return new Path.parse(arg); } this.segments = []; var i; var n; if (!arg) { // don't do anything } else if (Array.isArray(arg) && arg.length !== 0) { // if arg is a non-empty array // flatten one level deep // so we can chain arbitrary Path.createSegment results arg = arg.reduce(function(acc, val) { return acc.concat(val); }, []); n = arg.length; if (arg[0].isSegment) { // create from an array of segments for (i = 0; i < n; i++) { var segment = arg[i]; this.appendSegment(segment); } } else { // create from an array of Curves and/or Lines var previousObj = null; for (i = 0; i < n; i++) { var obj = arg[i]; if (!((obj instanceof Line) || (obj instanceof Curve))) { throw new Error('Cannot construct a path segment from the provided object.'); } if (i === 0) this.appendSegment(Path.createSegment('M', obj.start)); // if objects do not link up, moveto segments are inserted to cover the gaps if (previousObj && !previousObj.end.equals(obj.start)) this.appendSegment(Path.createSegment('M', obj.start)); if (obj instanceof Line) { this.appendSegment(Path.createSegment('L', obj.end)); } else if (obj instanceof Curve) { this.appendSegment(Path.createSegment('C', obj.controlPoint1, obj.controlPoint2, obj.end)); } previousObj = obj; } } } else if (arg.isSegment) { // create from a single segment this.appendSegment(arg); } else if (arg instanceof Line) { // create from a single Line this.appendSegment(Path.createSegment('M', arg.start)); this.appendSegment(Path.createSegment('L', arg.end)); } else if (arg instanceof Curve) { // create from a single Curve this.appendSegment(Path.createSegment('M', arg.start)); this.appendSegment(Path.createSegment('C', arg.controlPoint1, arg.controlPoint2, arg.end)); } else if (arg instanceof Polyline) { // create from a Polyline if (!(arg.points && (arg.points.length !== 0))) return; // if Polyline has no points, leave Path empty n = arg.points.length; for (i = 0; i < n; i++) { var point = arg.points[i]; if (i === 0) this.appendSegment(Path.createSegment('M', point)); else this.appendSegment(Path.createSegment('L', point)); } } else { // unknown object throw new Error('Cannot construct a path from the provided object.'); } }; // More permissive than V.normalizePathData and Path.prototype.serialize. // Allows path data strings that do not start with a Moveto command (unlike SVG specification). // Does not require spaces between elements; commas are allowed, separators may be omitted when unambiguous (e.g. 'ZM10,10', 'L1.6.8', 'M100-200'). // Allows for command argument chaining. // Throws an error if wrong number of arguments is provided with a command. // Throws an error if an unrecognized path command is provided (according to Path.segmentTypes). Only a subset of SVG commands is currently supported (L, C, M, Z). Path.parse = function(pathData) { if (!pathData) return new Path(); var path = new Path(); var commandRe = /(?:[a-zA-Z] *)(?:(?:-?\d+(?:\.\d+)?(?:e[-+]?\d+)? *,? *)|(?:-?\.\d+ *,? *))+|(?:[a-zA-Z] *)(?! |\d|-|\.)/g; var commands = pathData.match(commandRe); var numCommands = commands.length; for (var i = 0; i < numCommands; i++) { var command = commands[i]; var argRe = /(?:[a-zA-Z])|(?:(?:-?\d+(?:\.\d+)?(?:e[-+]?\d+)?))|(?:(?:-?\.\d+))/g; var args = command.match(argRe); var segment = Path.createSegment.apply(this, args); // args = [type, coordinate1, coordinate2...] path.appendSegment(segment); } return path; }; // Create a segment or an array of segments. // Accepts unlimited points/coords arguments after `type`. Path.createSegment = function(type) { if (!type) throw new Error('Type must be provided.'); var segmentConstructor = Path.segmentTypes[type]; if (!segmentConstructor) throw new Error(type + ' is not a recognized path segment type.'); var args = []; var n = arguments.length; for (var i = 1; i < n; i++) { // do not add first element (`type`) to args array args.push(arguments[i]); } return applyToNew(segmentConstructor, args); }; Path.prototype = { type: types.Path, // Accepts one segment or an array of segments as argument. // Throws an error if argument is not a segment or an array of segments. appendSegment: function(arg) { var segments = this.segments; var numSegments = segments.length; // works even if path has no segments var currentSegment; var previousSegment = ((numSegments !== 0) ? segments[numSegments - 1] : null); // if we are appending to an empty path, previousSegment is null var nextSegment = null; if (!Array.isArray(arg)) { // arg is a segment if (!arg || !arg.isSegment) throw new Error('Segment required.'); currentSegment = this.prepareSegment(arg, previousSegment, nextSegment); segments.push(currentSegment); } else { // arg is an array of segments // flatten one level deep // so we can chain arbitrary Path.createSegment results arg = arg.reduce(function(acc, val) { return acc.concat(val); }, []); if (!arg[0].isSegment) throw new Error('Segments required.'); var n = arg.length; for (var i = 0; i < n; i++) { var currentArg = arg[i]; currentSegment = this.prepareSegment(currentArg, previousSegment, nextSegment); segments.push(currentSegment); previousSegment = currentSegment; } } }, // Returns the bbox of the path. // If path has no segments, returns null. // If path has only invisible segments, returns bbox of the end point of last segment. bbox: function() { var segments = this.segments; var numSegments = segments.length; if (numSegments === 0) return null; // if segments is an empty array var bbox; for (var i = 0; i < numSegments; i++) { var segment = segments[i]; if (segment.isVisible) { var segmentBBox = segment.bbox(); bbox = bbox ? bbox.union(segmentBBox) : segmentBBox; } } if (bbox) return bbox; // if the path has only invisible elements, return end point of last segment var lastSegment = segments[numSegments - 1]; return new Rect(lastSegment.end.x, lastSegment.end.y, 0, 0); }, // Returns a new path that is a clone of this path. clone: function() { var segments = this.segments; var numSegments = segments.length; // works even if path has no segments var path = new Path(); for (var i = 0; i < numSegments; i++) { var segment = segments[i].clone(); path.appendSegment(segment); } return path; }, closestPoint: function(p, opt) { var t = this.closestPointT(p, opt); if (!t) return null; return this.pointAtT(t); }, closestPointLength: function(p, opt) { opt = opt || {}; var precision = (opt.precision === undefined) ? this.PRECISION : opt.precision; var segmentSubdivisions = (opt.segmentSubdivisions === undefined) ? this.getSegmentSubdivisions({ precision: precision }) : opt.segmentSubdivisions; var localOpt = { precision: precision, segmentSubdivisions: segmentSubdivisions }; var t = this.closestPointT(p, localOpt); if (!t) return 0; return this.lengthAtT(t, localOpt); }, closestPointNormalizedLength: function(p, opt) { opt = opt || {}; var precision = (opt.precision === undefined) ? this.PRECISION : opt.precision; var segmentSubdivisions = (opt.segmentSubdivisions === undefined) ? this.getSegmentSubdivisions({ precision: precision }) : opt.segmentSubdivisions; var localOpt = { precision: precision, segmentSubdivisions: segmentSubdivisions }; var cpLength = this.closestPointLength(p, localOpt); if (cpLength === 0) return 0; // shortcut var length = this.length(localOpt); if (length === 0) return 0; // prevents division by zero return cpLength / length; }, // Private function. closestPointT: function(p, opt) { var segments = this.segments; var numSegments = segments.length; if (numSegments === 0) return null; // if segments is an empty array opt = opt || {}; var precision = (opt.precision === undefined) ? this.PRECISION : opt.precision; var segmentSubdivisions = (opt.segmentSubdivisions === undefined) ? this.getSegmentSubdivisions({ precision: precision }) : opt.segmentSubdivisions; // not using localOpt var closestPointT; var minSquaredDistance = Infinity; for (var i = 0; i < numSegments; i++) { var segment = segments[i]; var subdivisions = segmentSubdivisions[i]; if (segment.isVisible) { var segmentClosestPointT = segment.closestPointT(p, { precision: precision, subdivisions: subdivisions }); var segmentClosestPoint = segment.pointAtT(segmentClosestPointT); var squaredDistance = (new Line(segmentClosestPoint, p)).squaredLength(); if (squaredDistance < minSquaredDistance) { closestPointT = { segmentIndex: i, value: segmentClosestPointT }; minSquaredDistance = squaredDistance; } } } if (closestPointT) return closestPointT; // if no visible segment, return end of last segment return { segmentIndex: numSegments - 1, value: 1 }; }, closestPointTangent: function(p, opt) { var segments = this.segments; var numSegments = segments.length; if (numSegments === 0) return null; // if segments is an empty array opt = opt || {}; var precision = (opt.precision === undefined) ? this.PRECISION : opt.precision; var segmentSubdivisions = (opt.segmentSubdivisions === undefined) ? this.getSegmentSubdivisions({ precision: precision }) : opt.segmentSubdivisions; // not using localOpt var closestPointTangent; var minSquaredDistance = Infinity; for (var i = 0; i < numSegments; i++) { var segment = segments[i]; var subdivisions = segmentSubdivisions[i]; if (segment.isDifferentiable()) { var segmentClosestPointT = segment.closestPointT(p, { precision: precision, subdivisions: subdivisions }); var segmentClosestPoint = segment.pointAtT(segmentClosestPointT); var squaredDistance = (new Line(segmentClosestPoint, p)).squaredLength(); if (squaredDistance < minSquaredDistance) { closestPointTangent = segment.tangentAtT(segmentClosestPointT); minSquaredDistance = squaredDistance; } } } if (closestPointTangent) return closestPointTangent; // if no valid segment, return null return null; }, // Returns `true` if the area surrounded by the path contains the point `p`. // Implements the even-odd algorithm (self-intersections are "outside"). // Closes open paths (always imagines a final closing segment). // Precision may be adjusted by passing an `opt` object. containsPoint: function(p, opt) { var polylines = this.toPolylines(opt); if (!polylines) return false; // shortcut (this path has no polylines) var numPolylines = polylines.length; // how many component polylines does `p` lie within? var numIntersections = 0; for (var i = 0; i < numPolylines; i++) { var polyline = polylines[i]; if (polyline.containsPoint(p)) { // `p` lies within this polyline numIntersections++; } } // returns `true` for odd numbers of intersections (even-odd algorithm) return ((numIntersections % 2) === 1); }, // Divides the path into two at requested `ratio` between 0 and 1 with precision better than `opt.precision`; optionally using `opt.subdivisions` provided. divideAt: function(ratio, opt) { var segments = this.segments; var numSegments = segments.length; if (numSegments === 0) return null; // if segments is an empty array if (ratio < 0) ratio = 0; if (ratio > 1) ratio = 1; opt = opt || {}; var precision = (opt.precision === undefined) ? this.PRECISION : opt.precision; var segmentSubdivisions = (opt.segmentSubdivisions === undefined) ? this.getSegmentSubdivisions({ precision: precision }) : opt.segmentSubdivisions; var localOpt = { precision: precision, segmentSubdivisions: segmentSubdivisions }; var pathLength = this.length(localOpt); var length = pathLength * ratio; return this.divideAtLength(length, localOpt); }, // Divides the path into two at requested `length` with precision better than requested `opt.precision`; optionally using `opt.subdivisions` provided. divideAtLength: function(length, opt) { var numSegments = this.segments.length; if (numSegments === 0) return null; // if segments is an empty array var fromStart = true; if (length < 0) { fromStart = false; // negative lengths mean start calculation from end point length = -length; // absolute value } opt = opt || {}; var precision = (opt.precision === undefined) ? this.PRECISION : opt.precision; var segmentSubdivisions = (opt.segmentSubdivisions === undefined) ? this.getSegmentSubdivisions({ precision: precision }) : opt.segmentSubdivisions; // not using localOpt var i; var segment; // identify the segment to divide: var l = 0; // length so far var divided; var dividedSegmentIndex; var lastValidSegment; // visible AND differentiable var lastValidSegmentIndex; var t; for (i = 0; i < numSegments; i++) { var index = (fromStart ? i : (numSegments - 1 - i)); segment = this.getSegment(index); var subdivisions = segmentSubdivisions[index]; var d = segment.length({ precision: precision, subdivisions: subdivisions }); if (segment.isDifferentiable()) { // segment is not just a point lastValidSegment = segment; lastValidSegmentIndex = index; if (length <= (l + d)) { dividedSegmentIndex = index; divided = segment.divideAtLength(((fromStart ? 1 : -1) * (length - l)), { precision: precision, subdivisions: subdivisions }); break; } } l += d; } if (!lastValidSegment) { // no valid segment found return null; } // else: the path contains at least one valid segment if (!divided) { // the desired length is greater than the length of the path dividedSegmentIndex = lastValidSegmentIndex; t = (fromStart ? 1 : 0); divided = lastValidSegment.divideAtT(t); } // create a copy of this path and replace the identified segment with its two divided parts: var pathCopy = this.clone(); pathCopy.replaceSegment(dividedSegmentIndex, divided); var divisionStartIndex = dividedSegmentIndex; var divisionMidIndex = dividedSegmentIndex + 1; var divisionEndIndex = dividedSegmentIndex + 2; // do not insert the part if it looks like a point if (!divided[0].isDifferentiable()) { pathCopy.removeSegment(divisionStartIndex); divisionMidIndex -= 1; divisionEndIndex -= 1; } // insert a Moveto segment to ensure secondPath will be valid: var movetoEnd = pathCopy.getSegment(divisionMidIndex).start; pathCopy.insertSegment(divisionMidIndex, Path.createSegment('M', movetoEnd)); divisionEndIndex += 1; // do not insert the part if it looks like a point if (!divided[1].isDifferentiable()) { pathCopy.removeSegment(divisionEndIndex - 1); divisionEndIndex -= 1; } // ensure that Closepath segments in secondPath will be assigned correct subpathStartSegment: var secondPathSegmentIndexConversion = divisionEndIndex - divisionStartIndex - 1; for (i = divisionEndIndex; i < pathCopy.segments.length; i++) { var originalSegment = this.getSegment(i - secondPathSegmentIndexConversion); segment = pathCopy.getSegment(i); if ((segment.type === 'Z') && !originalSegment.subpathStartSegment.end.equals(segment.subpathStartSegment.end)) { // pathCopy segment's subpathStartSegment is different from original segment's one // convert this Closepath segment to a Lineto and replace it in pathCopy var convertedSegment = Path.createSegment('L', originalSegment.end); pathCopy.replaceSegment(i, convertedSegment); } } // distribute pathCopy segments into two paths and return those: var firstPath = new Path(pathCopy.segments.slice(0, divisionMidIndex)); var secondPath = new Path(pathCopy.segments.slice(divisionMidIndex)); return [firstPath, secondPath]; }, // Checks whether two paths are exactly the same. // If `p` is undefined or null, returns false. equals: function(p) { if (!p) return false; var segments = this.segments; var otherSegments = p.segments; var numSegments = segments.length; if (otherSegments.length !== numSegments) return false; // if the two paths have different number of segments, they cannot be equal for (var i = 0; i < numSegments; i++) { var segment = segments[i]; var otherSegment = otherSegments[i]; // as soon as an inequality is found in segments, return false if ((segment.type !== otherSegment.type) || (!segment.equals(otherSegment))) return false; } // if no inequality found in segments, return true return true; }, // Accepts negative indices. // Throws an error if path has no segments. // Throws an error if index is out of range. getSegment: function(index) { var segments = this.segments; var numSegments = segments.length; if (numSegments === 0) throw new Error('Path has no segments.'); if (index < 0) index = numSegments + index; // convert negative indices to positive if (index >= numSegments || index < 0) throw new Error('Index out of range.'); return segments[index]; }, // Returns an array of segment subdivisions, with precision better than requested `opt.precision`. getSegmentSubdivisions: function(opt) { var segments = this.segments; var numSegments = segments.length; // works even if path has no segments opt = opt || {}; var precision = (opt.precision === undefined) ? this.PRECISION : opt.precision; // not using opt.segmentSubdivisions // not using localOpt var segmentSubdivisions = []; for (var i = 0; i < numSegments; i++) { var segment = segments[i]; var subdivisions = segment.getSubdivisions({ precision: precision }); segmentSubdivisions.push(subdivisions); } return segmentSubdivisions; }, // Returns an array of subpaths of this path. // Invalid paths are validated first. // Returns `[]` if path has no segments. getSubpaths: function() { const validatedPath = this.clone().validate(); const segments = validatedPath.segments; const numSegments = segments.length; const subpaths = []; for (let i = 0; i < numSegments; i++) { const segment = segments[i]; if (segment.isSubpathStart) { // we encountered a subpath start segment // create a new path for segment, and push it to list of subpaths subpaths.push(new Path(segment)); } else { // append current segment to the last subpath subpaths[subpaths.length - 1].appendSegment(segment); } } return subpaths; }, // Insert `arg` at given `index`. // `index = 0` means insert at the beginning. // `index = segments.length` means insert at the end. // Accepts negative indices, from `-1` to `-(segments.length + 1)`. // Accepts one segment or an array of segments as argument. // Throws an error if index is out of range. // Throws an error if argument is not a segment or an array of segments. insertSegment: function(index, arg) { var segments = this.segments; var numSegments = segments.length; // works even if path has no segments // note that these are incremented compared to getSegments() // we can insert after last element (note that this changes the meaning of index -1) if (index < 0) index = numSegments + index + 1; // convert negative indices to positive if (index > numSegments || index < 0) throw new Error('Index out of range.'); var currentSegment; var previousSegment = null; var nextSegment = null; if (numSegments !== 0) { if (index >= 1) { previousSegment = segments[index - 1]; nextSegment = previousSegment.nextSegment; // if we are inserting at end, nextSegment is null } else { // if index === 0 // previousSegment is null nextSegment = segments[0]; } } if (!Array.isArray(arg)) { if (!arg || !arg.isSegment) throw new Error('Segment required.'); currentSegment = this.prepareSegment(arg, previousSegment, nextSegment); segments.splice(index, 0, currentSegment); } else { // flatten one level deep // so we can chain arbitrary Path.createSegment results arg = arg.reduce(function(acc, val) { return acc.concat(val); }, []); if (!arg[0].isSegment) throw new Error('Segments required.'); var n = arg.length; for (var i = 0; i < n; i++) { var currentArg = arg[i]; currentSegment = this.prepareSegment(currentArg, previousSegment, nextSegment); segments.splice((index + i), 0, currentSegment); // incrementing index to insert subsequent segments after inserted segments previousSegment = currentSegment; } } }, intersectionWithLine: function(line, opt) { var intersection = null; var polylines = this.toPolylines(opt); if (!polylines) return null; for (var i = 0, n = polylines.length; i < n; i++) { var polyline = polylines[i]; var polylineIntersection = line.intersect(polyline); if (polylineIntersection) { intersection || (intersection = []); if (Array.isArray(polylineIntersection)) { Array.prototype.push.apply(intersection, polylineIntersection); } else { intersection.push(polylineIntersection); } } } return intersection; }, isDifferentiable: function() { var segments = this.segments; var numSegments = segments.length; for (var i = 0; i < numSegments; i++) { var segment = segments[i]; // as soon as a differentiable segment is found in segments, return true if (segment.isDifferentiable()) return true; } // if no differentiable segment is found in segments, return false return false; }, // Checks whether current path segments are valid. // Note that d is allowed to be empty - should disable rendering of the path. isValid: function() { var segments = this.segments; var isValid = (segments.length === 0) || (segments[0].type === 'M'); // either empty or first segment is a Moveto return isValid; }, // Returns length of the path, with precision better than requested `opt.precision`; or using `opt.segmentSubdivisions` provided. // If path has no segments, returns 0. length: function(opt) { var segments = this.segments; var numSegments = segments.length; if (numSegments === 0) return 0; // if segments is an empty array opt = opt || {}; var precision = (opt.precision === undefined) ? this.PRECISION : opt.precision; // opt.precision only used in getSegmentSubdivisions() call var segmentSubdivisions = (opt.segmentSubdivisions === undefined) ? this.getSegmentSubdivisions({ precision: precision }) : opt.segmentSubdivisions; // not using localOpt var length = 0; for (var i = 0; i < numSegments; i++) { var segment = segments[i]; var subdivisions = segmentSubdivisions[i]; length += segment.length({ subdivisions: subdivisions }); } return length; }, // Private function. lengthAtT: function(t, opt) { var segments = this.segments; var numSegments = segments.length; if (numSegments === 0) return 0; // if segments is an empty array var segmentIndex = t.segmentIndex; if (segmentIndex < 0) return 0; // regardless of t.value var tValue = t.value; if (segmentIndex >= numSegments) { segmentIndex = numSegments - 1; tValue = 1; } else if (tValue < 0) tValue = 0; else if (tValue > 1) tValue = 1; opt = opt || {}; var precision = (opt.precision === undefined) ? this.PRECISION : opt.precision; var segmentSubdivisions = (opt.segmentSubdivisions === undefined) ? this.getSegmentSubdivisions({ precision: precision }) : opt.segmentSubdivisions; // not using localOpt var subdivisions; var length = 0; for (var i = 0; i < segmentIndex; i++) { var segment = segments[i]; subdivisions = segmentSubdivisions[i]; length += segment.length({ precisison: precision, subdivisions: subdivisions }); } segment = segments[segmentIndex]; subdivisions = segmentSubdivisions[segmentIndex]; length += segment.lengthAtT(tValue, { precisison: precision, subdivisions: subdivisions }); return length; }, // Returns point at requested `ratio` between 0 and 1, with precision better than requested `opt.precision`; optionally using `opt.segmentSubdivisions` provided. pointAt: function(ratio, opt) { var segments = this.segments; var numSegments = segments.length; if (numSegments === 0) return null; // if segments is an empty array if (ratio <= 0) return this.start.clone(); if (ratio >= 1) return this.end.clone(); opt = opt || {}; var precision = (opt.precision === undefined) ? this.PRECISION : opt.precision; var segmentSubdivisions = (opt.segmentSubdivisions === undefined) ? this.getSegmentSubdivisions({ precision: precision }) : opt.segmentSubdivisions; var localOpt = { precision: precision, segmentSubdivisions: segmentSubdivisions }; var pathLength = this.length(localOpt); var length = pathLength * ratio; return this.pointAtLength(length, localOpt); }, // Returns point at requested `length`, with precision better than requested `opt.precision`; optionally using `opt.segmentSubdivisions` provided. // Accepts negative length. pointAtLength: function(length, opt) { var segments = this.segments; var numSegments = segments.length; if (numSegments === 0) return null; // if segments is an empty array if (length === 0) return this.start.clone(); var fromStart = true; if (length < 0) { fromStart = false; // negative lengths mean start calculation from end point length = -length; // absolute value } opt = opt || {}; var precision = (opt.precision === undefined) ? this.PRECISION : opt.precision; var segmentSubdivisions = (opt.segmentSubdivisions === undefined) ? this.getSegmentSubdivisions({ precision: precision }) : opt.segmentSubdivisions; // not using localOpt var lastVisibleSegment; var l = 0; // length so far for (var i = 0; i < numSegments; i++) { var index = (fromStart ? i : (numSegments - 1 - i)); var segment = segments[index]; var subdivisions = segmentSubdivisions[index]; var d = segment.length({ precision: precision, subdivisions: subdivisions }); if (segment.isVisible) { if (length <= (l + d)) { return segment.pointAtLength(((fromStart ? 1 : -1) * (length - l)), { precision: precision, subdivisions: subdivisions }); } lastVisibleSegment = segment; } l += d; } // if length requested is higher than the length of the path, return last visible segment endpoint if (lastVisibleSegment) return (fromStart ? lastVisibleSegment.end : lastVisibleSegment.start); // if no visible segment, return last segment end point (no matter if fromStart or no) var lastSegment = segments[numSegments - 1]; return lastSegment.end.clone(); }, // Private function. pointAtT: function(t) { var segments = this.segments; var numSegments = segments.length; if (numSegments === 0) return null; // if segments is an empty array var segmentIndex = t.segmentIndex; if (segmentIndex < 0) return segments[0].pointAtT(0); if (segmentIndex >= numSegments) return segments[numSegments - 1].pointAtT(1); var tValue = t.value; if (tValue < 0) tValue = 0; else if (tValue > 1) tValue = 1; return segments[segmentIndex].pointAtT(tValue); }, // Default precision PRECISION: 3, // Helper method for adding segments. prepareSegment: function(segment, previousSegment, nextSegment) { // insert after previous segment and before previous segment's next segment segment.previousSegment = previousSegment; segment.nextSegment = nextSegment; if (previousSegment) previousSegment.nextSegment = segment; if (nextSegment) nextSegment.previousSegment = segment; var updateSubpathStart = segment; if (segment.isSubpathStart) { segment.subpathStartSegment = segment; // assign self as subpath start segment updateSubpathStart = nextSegment; // start updating from next segment } // assign previous segment's subpath start (or self if it is a subpath start) to subsequent segments if (updateSubpathStart) this.updateSubpathStartSegment(updateSubpathStart); return segment; }, // Remove the segment at `index`. // Accepts negative indices, from `-1` to `-segments.length`. // Throws an error if path has no segments. // Throws an error if index is out of range. removeSegment: function(index) { var segments = this.segments; var numSegments = segments.length; if (numSegments === 0) throw new Error('Path has no segments.'); if (index < 0) index = numSegments + index; // convert negative indices to positive if (index >= numSegments || index < 0) throw new Error('Index out of range.'); var removedSegment = segments.splice(index, 1)[0]; var previousSegment = removedSegment.previousSegment; var nextSegment = removedSegment.nextSegment; // link the previous and next segments together (if present) if (previousSegment) previousSegment.nextSegment = nextSegment; // may be null if (nextSegment) nextSegment.previousSegment = previousSegment; // may be null // if removed segment used to start a subpath, update all subsequent segments until another subpath start segment is reached if (removedSegment.isSubpathStart && nextSegment) this.updateSubpathStartSegment(nextSegment); }, // Replace the segment at `index` with `arg`. // Accepts negative indices, from `-1` to `-segments.length`. // Accepts one segment or an array of segments as argument. // Throws an error if path has no segments. // Throws an error if index is out of range. // Throws an error if argument is not a segment or an array of segments. replaceSegment: function(index, arg) { var segments = this.segments; var numSegments = segments.length; if (numSegments === 0) throw new Error('Path has no segments.'); if (index < 0) index = numSegments + index; // convert negative indices to positive if (index >= numSegments || index < 0) throw new Error('Index out of range.'); var currentSegment; var replacedSegment = segments[index]; var previousSegment = replacedSegment.previousSegment; var nextSegment = replacedSegment.nextSegment; var updateSubpathStart = replacedSegment.isSubpathStart; // boolean: is an update of subpath starts necessary? if (!Array.isArray(arg)) { if (!arg || !arg.isSegment) throw new Error('Segment required.'); currentSegment = this.prepareSegment(arg, previousSegment, nextSegment); segments.splice(index, 1, currentSegment); // directly replace if (updateSubpathStart && currentSegment.isSubpathStart) updateSubpathStart = false; // already updated by `prepareSegment` } else { // flatten one level deep // so we can chain arbitrary Path.createSegment results arg = arg.reduce(function(acc, val) { return acc.concat(val); }, []); if (!arg[0].isSegment) throw new Error('Segments required.'); segments.splice(index, 1); var n = arg.length; for (var i = 0; i < n; i++) { var currentArg = arg[i]; currentSegment = this.prepareSegment(currentArg, previousSegment, nextSegment); segments.splice((index + i), 0, currentSegment); // incrementing index to insert subsequent segments after inserted segments previousSegment = currentSegment; if (updateSubpathStart && currentSegment.isSubpathStart) updateSubpathStart = false; // already updated by `prepareSegment` } } // if replaced segment used to start a subpath and no new subpath start was added, update all subsequent segments until another subpath start segment is reached if (updateSubpathStart && nextSegment) this.updateSubpathStartSegment(nextSegment); }, round: function(precision) { var segments = this.segments; var numSegments = segments.length; for (var i = 0; i < numSegments; i++) { var segment = segments[i]; segment.round(precision); } return this; }, scale: function(sx, sy, origin) { var segments = this.segments; var numSegments = segments.length; for (var i = 0; i < numSegments; i++) { var segment = segments[i]; segment.scale(sx, sy, origin); } return this; }, segmentAt: function(ratio, opt) { var index = this.segmentIndexAt(ratio, opt); if (!index) return null; return this.getSegment(index); }, // Accepts negative length. segmentAtLength: function(length, opt) { var index = this.segmentIndexAtLength(length, opt); if (!index) return null; return this.getSegment(index); }, segmentIndexAt: function(ratio, opt) { var segments = this.segments; var numSegments = segments.length; if (numSegments === 0) return null; // if segments is an empty array if (ratio < 0) ratio = 0; if (ratio > 1) ratio = 1; opt = opt || {}; var precision = (opt.precision === undefined) ? this.PRECISION : opt.precision; var segmentSubdivisions = (opt.segmentSubdivisions === undefined) ? this.getSegmentSubdivisions({ precision: precision }) : opt.segmentSubdivisions; var localOpt = { precision: precision, segmentSubdivisions: segmentSubdivisions }; var pathLength = this.length(localOpt); var length = pathLength * ratio; return this.segmentIndexAtLength(length, localOpt); }, // Accepts negative length. segmentIndexAtLength: function(length, opt) { var segments = this.segments; var numSegments = segments.length; if (numSegments === 0) return null; // if segments is an empty array var fromStart = true; if (length < 0) { fromStart = false; // negative lengths mean start calculation from end point length = -length; // absolute value } opt = opt || {}; var precision = (opt.precision === undefined) ? this.PRECISION : opt.precision; var segmentSubdivisions = (opt.segmentSubdivisions === undefined) ? this.getSegmentSubdivisions({ precision: precision }) : opt.segmentSubdivisions; // not using localOpt var lastVisibleSegmentIndex = null; var l = 0; // length so far for (var i = 0; i < numSegments; i++) { var index = (fromStart ? i : (numSegments - 1 - i)); var segment = segments[index]; var subdivisions = segmentSubdivisions[index]; var d = segment.length({ precision: precision, subdivisions: subdivisions }); if (segment.isVisible) { if (length <= (l + d)) return index; lastVisibleSegmentIndex = index; } l += d; } // if length requested is higher than the length of the path, return last visible segment index // if no visible segment, return null return lastVisibleSegmentIndex; }, // Returns a string that can be used to reconstruct the path. // Additional error checking compared to toString (must start with M segment). serialize: function() { if (!this.isValid()) throw new Error('Invalid path segments.'); return this.toString(); }, // Returns tangent line at requested `ratio` between 0 and 1, with precision better than requested `opt.precision`; optionally using `opt.segmentSubdivisions` provided. tangentAt: function(ratio, opt) { var segments = this.segments; var numSegments = segments.length; if (numSegments === 0) return null; // if segments is an empty array if (ratio < 0) ratio = 0; if (ratio > 1) ratio = 1; opt = opt || {}; var precision = (opt.precision === undefined) ? this.PRECISION : opt.precision; var segmentSubdivisions = (opt.segmentSubdivisions === undefined) ? this.getSegmentSubdivisions({ precision: precision }) : opt.segmentSubdivisions; var localOpt = { precision: precision, segmentSubdivisions: segmentSubdivisions }; var pathLength = this.length(localOpt); var length = pathLength * ratio; return this.tangentAtLength(length, localOpt); }, // Returns tangent line at requested `length`, with precision better than requested `opt.precision`; optionally using `opt.segmentSubdivisions` provided. // Accepts negative length. tangentAtLength: function(length, opt) { var segments = this.segments; var numSegments = segments.length; if (numSegments === 0) return null; // if segments is an empty array var fromStart = true; if (length < 0) { fromStart = false; // negative lengths mean start calculation from end point length = -length; // absolute value } opt = opt || {}; var precision = (opt.precision === undefined) ? this.PRECISION : opt.precision; var segmentSubdivisions = (opt.segmentSubdivisions === undefined) ? this.getSegmentSubdivisions({ precision: precision }) : opt.segmentSubdivisions; // not using localOpt var lastValidSegment; // visible AND differentiable (with a tangent) var l = 0; // length so far for (var i = 0; i < numSegments; i++) { var index = (fromStart ? i : (numSegments - 1 - i)); var segment = segments[index]; var subdivisions = segmentSubdivisions[index]; var d = segment.length({ precision: precision, subdivisions: subdivisions }); if (segment.isDifferentiable()) { if (length <= (l + d)) { return segment.tangentAtLength(((fromStart ? 1 : -1) * (length - l)), { precision: precision, subdivisions: subdivisions }); } lastValidSegment = segment; } l += d; } // if length requested is higher than the length of the path, return tangent of endpoint of last valid segment if (lastValidSegment) { var t = (fromStart ? 1 : 0); return lastValidSegment.tangentAtT(t); } // if no valid segment, return null return null; }, // Private function. tangentAtT: function(t) { var segments = this.segments; var numSegments = segments.length; if (numSegments === 0) return null; // if segments is an empty array var segmentIndex = t.segmentIndex; if (segmentIndex < 0) return segments[0].tangentAtT(0); if (segmentIndex >= numSegments) return segments[numSegments - 1].tangentAtT(1); var tValue = t.value; if (tValue < 0) tValue = 0; else if (tValue > 1) tValue = 1; return segments[segmentIndex].tangentAtT(tValue); }, toPoints: function(opt) { var segments = this.segments; var numSegments = segments.length; if (numSegments === 0) return null; // if segments is an empty array opt = opt || {}; var precision = (opt.precision === undefined) ? this.PRECISION : opt.precision; var segmentSubdivisions = (opt.segmentSubdivisions === undefined) ? this.getSegmentSubdivisions({ precision: precision }) : opt.segmentSubdivisions; var points = []; var partialPoints = []; for (var i = 0; i < numSegments; i++) { var segment = segments[i]; if (segment.isVisible) { var currentSegmentSubdivisions = segmentSubdivisions[i]; if (currentSegmentSubdivisions.length > 0) { var subdivisionPoints = currentSegmentSubdivisions.map(function(curve) { return curve.start; }); Array.prototype.push.apply(partialPoints, subdivisionPoints); } else { partialPoints.push(segment.start); } } else if (partialPoints.length > 0) { partialPoints.push(segments[i - 1].end); points.push(partialPoints); partialPoints = []; } } if (partialPoints.length > 0) { partialPoints.push(this.end); points.push(partialPoints); } return points; }, toPolylines: function(opt) { var polylines = []; var points = this.toPoints(opt); if (!points) return null; for (var i = 0, n = points.length; i < n; i++) { polylines.push(new Polyline(points[i])); } return polylines; }, toString: function() { var segments = this.segments; var numSegments = segments.length; var pathData = ''; for (var i = 0; i < numSegments; i++) { var segment = segments[i]; pathData += segment.serialize() + ' '; } return pathData.trim(); }, translate: function(tx, ty) { var segments = this.segments; var numSegments = segments.length; for (var i = 0; i < numSegments; i++) { var segment = segments[i]; segment.translate(tx, ty); } return this; }, // Helper method for updating subpath start of segments, starting with the one provided. updateSubpathStartSegment: function(segment) { var previousSegment = segment.previousSegment; // may be null while (segment && !segment.isSubpathStart) { // assign previous segment's subpath start segment to this segment if (previousSegment) segment.subpathStartSegment = previousSegment.subpathStartSegment; // may be null else segment.subpathStartSegment = null; // if segment had no previous segment, assign null - creates an invalid path! previousSegment = segment; segment = segment.nextSegment; // move on to the segment after etc. } }, // If the path is not valid, insert M 0 0 at the beginning. // Path with no segments is considered valid, so nothing is inserted. validate: function() { if (!this.isValid()) this.insertSegment(0, Path.createSegment('M', 0, 0)); return this; } }; Object.defineProperty(Path.prototype, 'start', { // Getter for the first visible endpoint of the path. configurable: true, enumerable: true, get: function() { var segments = this.segments; var numSegments = segments.length; if (numSegments === 0) return null; for (var i = 0; i < numSegments; i++) { var segment = segments[i]; if (segment.isVisible) return segment.start; } // if no visible segment, return last segment end point return segments[numSegments - 1].end; } }); Object.defineProperty(Path.prototype, 'end', { // Getter for the last visible endpoint of the path. configurable: true, enumerable: true, get: function() { var segments = this.segments; var numSegments = segments.length; if (numSegments === 0) return null; for (var i = numSegments - 1; i >= 0; i--) { var segment = segments[i]; if (segment.isVisible) return segment.end; } // if no visible segment, return last segment end point return segments[numSegments - 1].end; } }); // Local helper function. // Use an array of arguments to call a constructor (function called with `new`). // Adapted from https://stackoverflow.com/a/8843181/2263595 // It is not necessary to use this function if the arguments can be passed separately (i.e. if the number of arguments is limited). // - If that is the case, use `new constructor(arg1, arg2)`, for example. // It is not necessary to use this function if the function that needs an array of arguments is not supposed to be used as a constructor. // - If that is the case, use `f.apply(thisArg, [arg1, arg2...])`, for example. function applyToNew(constructor, argsArray) { // The `new` keyword can only be applied to functions that take a limited number of arguments. // - We can fake that with .bind(). // - It calls a function (`constructor`, here) with the arguments that were provided to it - effectively transforming an unlimited number of arguments into limited. // - So `new (constructor.bind(thisArg, arg1, arg2...))` // - `thisArg` can be anything (e.g. null) because `new` keyword resets context to the constructor object. // We need to pass in a variable number of arguments to the bind() call. // - We can use .apply(). // - So `new (constructor.bind.apply(constructor, [thisArg, arg1, a