diff --git a/debug/viewer.css b/debug/viewer.css index 52cd4bd64..5a0af41e6 100644 --- a/debug/viewer.css +++ b/debug/viewer.css @@ -92,4 +92,8 @@ svg text { .selectModelCode * { font-size: 18px; font-weight: 200; +} + +#crosshairs-horizontal_text, #crosshairs-vertical_text { + display: none; } \ No newline at end of file diff --git a/debug/viewer.html b/debug/viewer.html index b9d9d166f..e3227b2ab 100644 --- a/debug/viewer.html +++ b/debug/viewer.html @@ -42,6 +42,7 @@ + diff --git a/index.js b/index.js index c2414bf35..d30624d6e 100644 --- a/index.js +++ b/index.js @@ -169,10 +169,12 @@ var MakerJs; * @param b Second angle. * @returns true if angles are the same, false if they are not */ - function areEqual(angle1, angle2) { - var a1 = noRevolutions(MakerJs.round(angle1)); - var a2 = noRevolutions(MakerJs.round(angle2)); - return a1 == a2 || a1 + 360 == a2 || a1 - 360 == a2; + function areEqual(angle1, angle2, accuracy) { + if (accuracy === void 0) { accuracy = .0001; } + var a1 = noRevolutions(angle1); + var a2 = noRevolutions(angle2); + var d = noRevolutions(MakerJs.round(a2 - a1, accuracy)); + return d == 0; } angle.areEqual = areEqual; /** @@ -322,8 +324,14 @@ var MakerJs; * @param b Second point. * @returns true if points are the same, false if they are not */ - function areEqual(a, b) { - return a[0] == b[0] && a[1] == b[1]; + function areEqual(a, b, withinDistance) { + if (!withinDistance) { + return a[0] == b[0] && a[1] == b[1]; + } + else { + var distance = MakerJs.measure.pointDistance(a, b); + return distance <= withinDistance; + } } point.areEqual = areEqual; /** @@ -339,6 +347,20 @@ var MakerJs; return MakerJs.round(a[0], accuracy) == MakerJs.round(b[0], accuracy) && MakerJs.round(a[1], accuracy) == MakerJs.round(b[1], accuracy); } point.areEqualRounded = areEqualRounded; + /** + * Get the average of two points. + * + * @param a First point. + * @param b Second point. + * @returns New point object which is the average of a and b. + */ + function average(a, b) { + function avg(i) { + return (a[i] + b[i]) / 2; + } + return [avg(0), avg(1)]; + } + point.average = average; /** * Clone a point into a new point. * @@ -506,7 +528,7 @@ var MakerJs; var pointAngleInRadians = MakerJs.angle.ofPointInRadians(rotationOrigin, pointToRotate); var d = MakerJs.measure.pointDistance(rotationOrigin, pointToRotate); var rotatedPoint = fromPolar(pointAngleInRadians + MakerJs.angle.toRadians(angleInDegrees), d); - return rounded(add(rotationOrigin, rotatedPoint)); + return add(rotationOrigin, rotatedPoint); } point.rotate = rotate; /** @@ -533,7 +555,7 @@ var MakerJs; */ function serialize(pointContext, accuracy) { var roundedPoint = rounded(pointContext, accuracy); - return roundedPoint[0] + ',' + roundedPoint[1]; + return JSON.stringify(roundedPoint); } point.serialize = serialize; /** @@ -567,14 +589,15 @@ var MakerJs; * @private */ var pathAreEqualMap = {}; - pathAreEqualMap[MakerJs.pathType.Line] = function (line1, line2) { - return (MakerJs.point.areEqual(line1.origin, line2.origin) && MakerJs.point.areEqual(line1.end, line2.end)) || (MakerJs.point.areEqual(line1.origin, line2.end) && MakerJs.point.areEqual(line1.end, line2.origin)); + pathAreEqualMap[MakerJs.pathType.Line] = function (line1, line2, withinPointDistance) { + return (MakerJs.point.areEqual(line1.origin, line2.origin, withinPointDistance) && MakerJs.point.areEqual(line1.end, line2.end, withinPointDistance)) + || (MakerJs.point.areEqual(line1.origin, line2.end, withinPointDistance) && MakerJs.point.areEqual(line1.end, line2.origin, withinPointDistance)); }; - pathAreEqualMap[MakerJs.pathType.Circle] = function (circle1, circle2) { - return MakerJs.point.areEqual(circle1.origin, circle2.origin) && circle1.radius == circle2.radius; + pathAreEqualMap[MakerJs.pathType.Circle] = function (circle1, circle2, withinPointDistance) { + return MakerJs.point.areEqual(circle1.origin, circle2.origin, withinPointDistance) && circle1.radius == circle2.radius; }; - pathAreEqualMap[MakerJs.pathType.Arc] = function (arc1, arc2) { - return pathAreEqualMap[MakerJs.pathType.Circle](arc1, arc2) && MakerJs.angle.areEqual(arc1.startAngle, arc2.startAngle) && MakerJs.angle.areEqual(arc1.endAngle, arc2.endAngle); + pathAreEqualMap[MakerJs.pathType.Arc] = function (arc1, arc2, withinPointDistance) { + return pathAreEqualMap[MakerJs.pathType.Circle](arc1, arc2, withinPointDistance) && MakerJs.angle.areEqual(arc1.startAngle, arc2.startAngle) && MakerJs.angle.areEqual(arc1.endAngle, arc2.endAngle); }; /** * Find out if two paths are equal. @@ -583,12 +606,12 @@ var MakerJs; * @param b Second path. * @returns true if paths are the same, false if they are not */ - function areEqual(path1, path2) { + function areEqual(path1, path2, withinPointDistance) { var result = false; if (path1.type == path2.type) { var fn = pathAreEqualMap[path1.type]; if (fn) { - result = fn(path1, path2); + result = fn(path1, path2, withinPointDistance); } } return result; @@ -687,8 +710,8 @@ var MakerJs; line.end = MakerJs.point.rotate(line.end, angleInDegrees, rotationOrigin); }; map[MakerJs.pathType.Arc] = function (arc) { - arc.startAngle += angleInDegrees; - arc.endAngle += angleInDegrees; + arc.startAngle = MakerJs.angle.noRevolutions(arc.startAngle + angleInDegrees); + arc.endAngle = MakerJs.angle.noRevolutions(arc.endAngle + angleInDegrees); }; pathToRotate.origin = MakerJs.point.rotate(pathToRotate.origin, angleInDegrees, rotationOrigin); var fn = map[pathToRotate.type]; @@ -740,12 +763,13 @@ var MakerJs; return null; } function getAngleStrictlyBetweenArcAngles() { - var endAngle = MakerJs.angle.ofArcEnd(arc); + var startAngle = MakerJs.angle.noRevolutions(arc.startAngle); + var endAngle = startAngle + MakerJs.angle.ofArcEnd(arc) - arc.startAngle; var tries = [0, 1, -1]; for (var i = 0; i < tries.length; i++) { var add = +360 * tries[i]; - if (MakerJs.measure.isBetween(angleAtBreakPoint + add, arc.startAngle, endAngle, true)) { - return angleAtBreakPoint + add; + if (MakerJs.measure.isBetween(angleAtBreakPoint + add, startAngle, endAngle, true)) { + return arc.startAngle + angleAtBreakPoint + add - startAngle; } } return null; @@ -916,13 +940,33 @@ var MakerJs; return count; } model.countChildModels = countChildModels; + /** + * Get an unused id in the models map with the same prefix. + * + * @param modelContext The model containing the models map. + * @param modelId The id to use directly (if unused), or as a prefix. + */ + function getSimilarModelId(modelContext, modelId) { + if (!modelContext.models) + return modelId; + var i = 0; + var newModelId = modelId; + while (newModelId in modelContext.models) { + i++; + newModelId = modelId + '_' + i; + } + return newModelId; + } + model.getSimilarModelId = getSimilarModelId; /** * Get an unused id in the paths map with the same prefix. * * @param modelContext The model containing the paths map. - * @param pathId The pathId to use directly (if unused), or as a prefix. + * @param pathId The id to use directly (if unused), or as a prefix. */ function getSimilarPathId(modelContext, pathId) { + if (!modelContext.paths) + return pathId; var i = 0; var newPathId = pathId; while (newPathId in modelContext.paths) { @@ -939,6 +983,8 @@ var MakerJs; * @param origin Optional offset reference point. */ function originate(modelToOriginate, origin) { + if (!modelToOriginate) + return; var newOrigin = MakerJs.point.add(modelToOriginate.origin, origin); if (modelToOriginate.paths) { for (var id in modelToOriginate.paths) { @@ -1100,11 +1146,15 @@ var MakerJs; function walkPaths(modelContext, callback) { if (modelContext.paths) { for (var pathId in modelContext.paths) { + if (!modelContext.paths[pathId]) + continue; callback(modelContext, pathId, modelContext.paths[pathId]); } } if (modelContext.models) { for (var id in modelContext.models) { + if (!modelContext.models[id]) + continue; walkPaths(modelContext.models[id], callback); } } @@ -1115,29 +1165,33 @@ var MakerJs; var MakerJs; (function (MakerJs) { var model; - (function (model_1) { + (function (model) { /** * @private */ function getNonZeroSegments(pathToSegment, breakPoint) { + var segmentType = pathToSegment.type; var segment1 = MakerJs.cloneObject(pathToSegment); var segment2 = MakerJs.path.breakAtPoint(segment1, breakPoint); if (segment2) { var segments = [segment1, segment2]; for (var i = 2; i--;) { - if (MakerJs.round(MakerJs.measure.pathLength(segments[i]), .00001) == 0) { + if (MakerJs.round(MakerJs.measure.pathLength(segments[i]), .0001) == 0) { return null; } } return segments; } + else if (segmentType == MakerJs.pathType.Circle) { + return [segment1]; + } return null; } /** * @private */ function breakAlongForeignPath(segments, overlappedSegments, foreignPath) { - if (MakerJs.path.areEqual(segments[0].path, foreignPath)) { + if (MakerJs.path.areEqual(segments[0].path, foreignPath, .0001)) { segments[0].overlapped = true; segments[0].duplicate = true; overlappedSegments.push(segments[0]); @@ -1169,11 +1223,18 @@ var MakerJs; } if (subSegments) { segments[i].path = subSegments[0]; - var newSegment = { path: subSegments[1], overlapped: segments[i].overlapped, uniqueForeignIntersectionPoints: [] }; - if (segments[i].overlapped) { - overlappedSegments.push(newSegment); + if (subSegments[1]) { + var newSegment = { + path: subSegments[1], + pathId: segments[0].pathId, + overlapped: segments[i].overlapped, + uniqueForeignIntersectionPoints: [] + }; + if (segments[i].overlapped) { + overlappedSegments.push(newSegment); + } + segments.push(newSegment); } - segments.push(newSegment); //re-check this segment for another deep intersection i--; } @@ -1187,7 +1248,7 @@ var MakerJs; var added = 0; function addUniquePoint(pointToAdd) { for (var i = 0; i < pointArray.length; i++) { - if (MakerJs.point.areEqualRounded(pointArray[i], pointToAdd)) { + if (MakerJs.point.areEqual(pointArray[i], pointToAdd, .000000001)) { return; } } @@ -1202,7 +1263,7 @@ var MakerJs; /** * @private */ - function checkInsideForeignPath(segment, foreignPath, farPoint) { + function checkIntersectsForeignPath(segment, foreignPath, foreignPathId, farPoint) { if (farPoint === void 0) { farPoint = [7654321, 1234567]; } var origin = MakerJs.point.middle(segment.path); var lineToFarPoint = new MakerJs.paths.Line(origin, farPoint); @@ -1219,9 +1280,9 @@ var MakerJs; * @private */ function checkInsideForeignModel(segment, modelToIntersect, farPoint) { - model_1.walkPaths(modelToIntersect, function (mx, pathId2, path2) { + model.walkPaths(modelToIntersect, function (mx, pathId2, path2) { if (path2) { - checkInsideForeignPath(segment, path2, farPoint); + checkIntersectsForeignPath(segment, path2, pathId2, farPoint); } }); } @@ -1242,19 +1303,30 @@ var MakerJs; checkInsideForeignModel(segment, modelContext, farPoint); return !!segment.isInside; } - model_1.isPathInsideModel = isPathInsideModel; + model.isPathInsideModel = isPathInsideModel; + /** + * Break a model's paths everywhere they intersect with another path. + * + * @param modelToBreak The model containing paths to be broken. + * @param modelToIntersect Optional model containing paths to look for intersection, or else the modelToBreak will be used. + */ + function breakPathsAtIntersections(modelToBreak, modelToIntersect) { + breakAllPathsAtIntersections(modelToBreak, modelToIntersect || modelToBreak, false); + } + model.breakPathsAtIntersections = breakPathsAtIntersections; /** * @private */ - function breakAllPathsAtIntersections(modelToBreak, modelToIntersect, farPoint) { + function breakAllPathsAtIntersections(modelToBreak, modelToIntersect, checkIsInside, farPoint) { var crossedPaths = []; var overlappedSegments = []; - model_1.walkPaths(modelToBreak, function (modelContext, pathId1, path1) { + model.walkPaths(modelToBreak, function (modelContext, pathId1, path1) { if (!path1) return; //clone this path and make it the first segment var segment = { path: MakerJs.cloneObject(path1), + pathId: pathId1, overlapped: false, uniqueForeignIntersectionPoints: [] }; @@ -1264,14 +1336,16 @@ var MakerJs; segments: [segment] }; //keep breaking the segments anywhere they intersect with paths of the other model - model_1.walkPaths(modelToIntersect, function (mx, pathId2, path2) { - if (path2) { + model.walkPaths(modelToIntersect, function (mx, pathId2, path2) { + if (path2 && path1 !== path2) { breakAlongForeignPath(thisPath.segments, overlappedSegments, path2); } }); - //check each segment whether it is inside or outside - for (var i = 0; i < thisPath.segments.length; i++) { - checkInsideForeignModel(thisPath.segments[i], modelToIntersect); + if (checkIsInside) { + //check each segment whether it is inside or outside + for (var i = 0; i < thisPath.segments.length; i++) { + checkInsideForeignModel(thisPath.segments[i], modelToIntersect, farPoint); + } } crossedPaths.push(thisPath); }); @@ -1280,9 +1354,9 @@ var MakerJs; /** * @private */ - function checkForEqualOverlaps(crossedPathsA, crossedPathsB) { + function checkForEqualOverlaps(crossedPathsA, crossedPathsB, pointMatchingDistance) { function compareSegments(segment1, segment2) { - if (MakerJs.path.areEqual(segment1.path, segment2.path)) { + if (MakerJs.path.areEqual(segment1.path, segment2.path, pointMatchingDistance)) { segment1.duplicate = segment2.duplicate = true; } } @@ -1299,13 +1373,13 @@ var MakerJs; * @private */ function addOrDeleteSegments(crossedPath, includeInside, includeOutside, keepDuplicates) { - function addSegment(model, pathIdBase, segment) { - var id = model_1.getSimilarPathId(model, pathIdBase); - model.paths[id] = segment.path; + function addSegment(modelContext, pathIdBase, segment) { + var id = model.getSimilarPathId(modelContext, pathIdBase); + modelContext.paths[id] = segment.path; } - function checkAddSegment(model, pathIdBase, segment) { + function checkAddSegment(modelContext, pathIdBase, segment) { if (segment.isInside && includeInside || !segment.isInside && includeOutside) { - addSegment(model, pathIdBase, segment); + addSegment(modelContext, pathIdBase, segment); } } //delete the original, its segments will be added @@ -1322,7 +1396,7 @@ var MakerJs; } } /** - * Combine 2 models. The models should be originated. + * Combine 2 models. The models should be originated, and every path within each model should be part of a loop. * * @param modelA First model to combine. * @param modelB Second model to combine. @@ -1333,23 +1407,30 @@ var MakerJs; * @param keepDuplicates Flag to include paths which are duplicate in both models. * @param farPoint Optional point of reference which is outside the bounds of both models. */ - function combine(modelA, modelB, includeAInsideB, includeAOutsideB, includeBInsideA, includeBOutsideA, keepDuplicates, farPoint) { + function combine(modelA, modelB, includeAInsideB, includeAOutsideB, includeBInsideA, includeBOutsideA, options) { if (includeAInsideB === void 0) { includeAInsideB = false; } if (includeAOutsideB === void 0) { includeAOutsideB = true; } if (includeBInsideA === void 0) { includeBInsideA = false; } if (includeBOutsideA === void 0) { includeBOutsideA = true; } - if (keepDuplicates === void 0) { keepDuplicates = true; } - var pathsA = breakAllPathsAtIntersections(modelA, modelB, farPoint); - var pathsB = breakAllPathsAtIntersections(modelB, modelA, farPoint); - checkForEqualOverlaps(pathsA.overlappedSegments, pathsB.overlappedSegments); + var opts = { + trimDeadEnds: true, + pointMatchingDistance: .005 + }; + MakerJs.extendObject(opts, options); + var pathsA = breakAllPathsAtIntersections(modelA, modelB, true, opts.farPoint); + var pathsB = breakAllPathsAtIntersections(modelB, modelA, true, opts.farPoint); + checkForEqualOverlaps(pathsA.overlappedSegments, pathsB.overlappedSegments, opts.pointMatchingDistance); for (var i = 0; i < pathsA.crossedPaths.length; i++) { - addOrDeleteSegments(pathsA.crossedPaths[i], includeAInsideB, includeAOutsideB, keepDuplicates); + addOrDeleteSegments(pathsA.crossedPaths[i], includeAInsideB, includeAOutsideB, true); } for (var i = 0; i < pathsB.crossedPaths.length; i++) { - addOrDeleteSegments(pathsB.crossedPaths[i], includeBInsideA, includeBOutsideA); + addOrDeleteSegments(pathsB.crossedPaths[i], includeBInsideA, includeBOutsideA, false); + } + if (opts.trimDeadEnds) { + model.removeDeadEnds({ models: { modelA: modelA, modelB: modelB } }); } } - model_1.combine = combine; + model.combine = combine; })(model = MakerJs.model || (MakerJs.model = {})); })(MakerJs || (MakerJs = {})); var MakerJs; @@ -1487,6 +1568,10 @@ var MakerJs; function isBetweenArcAngles(angleInQuestion, arc, exclusive) { var startAngle = arc.startAngle; var endAngle = MakerJs.angle.ofArcEnd(arc); + var span = endAngle - startAngle; + startAngle = MakerJs.angle.noRevolutions(startAngle); + endAngle = startAngle + span; + angleInQuestion = MakerJs.angle.noRevolutions(angleInQuestion); //computed angles will not be negative, but the arc may have specified a negative angle, so check against one revolution forward and backward return (isBetween(angleInQuestion, startAngle, endAngle, exclusive) || isBetween(angleInQuestion, startAngle + 360, endAngle + 360, exclusive) || isBetween(angleInQuestion, startAngle - 360, endAngle - 360, exclusive)); } @@ -1501,11 +1586,11 @@ var MakerJs; */ function isBetweenPoints(pointInQuestion, line, exclusive) { for (var i = 2; i--;) { - var origin_value = MakerJs.round(line.origin[i]); - var end_value = MakerJs.round(line.end[i]); - if (origin_value == end_value) { + if (MakerJs.round(line.origin[i] - line.end[i], .000001) == 0) { continue; } + var origin_value = MakerJs.round(line.origin[i]); + var end_value = MakerJs.round(line.end[i]); if (!isBetween(MakerJs.round(pointInQuestion[i]), origin_value, end_value, exclusive)) return false; } @@ -1621,16 +1706,18 @@ var MakerJs; getExtreme(totalMeasurement.low, pathMeasurement.low, Math.min); getExtreme(totalMeasurement.high, pathMeasurement.high, Math.max); } - function measure(model, offsetOrigin) { - var newOrigin = MakerJs.point.add(model.origin, offsetOrigin); - if (model.paths) { - for (var id in model.paths) { - lowerOrHigher(newOrigin, pathExtents(model.paths[id])); + function measure(modelToMeasure, offsetOrigin) { + if (!modelToMeasure) + return; + var newOrigin = MakerJs.point.add(modelToMeasure.origin, offsetOrigin); + if (modelToMeasure.paths) { + for (var id in modelToMeasure.paths) { + lowerOrHigher(newOrigin, pathExtents(modelToMeasure.paths[id])); } } - if (model.models) { - for (var id in model.models) { - measure(model.models[id], newOrigin); + if (modelToMeasure.models) { + for (var id in modelToMeasure.models) { + measure(modelToMeasure.models[id], newOrigin); } } } @@ -1699,12 +1786,18 @@ var MakerJs; var newOffset = MakerJs.point.add((this.fixPoint ? this.fixPoint(modelToExport.origin) : modelToExport.origin), offset); if (modelToExport.paths) { for (var id in modelToExport.paths) { - this.exportPath(id, modelToExport.paths[id], newOffset, modelToExport.layer); + var currPath = modelToExport.paths[id]; + if (!currPath) + continue; + this.exportPath(id, currPath, newOffset, modelToExport.layer); } } if (modelToExport.models) { for (var id in modelToExport.models) { - this.exportModel(id, modelToExport.models[id], newOffset); + var currModel = modelToExport.models[id]; + if (!currModel) + continue; + this.exportModel(id, currModel, newOffset); } } if (this.endModel) { @@ -2086,14 +2179,14 @@ var MakerJs; * @private */ function getSlope(line) { - var dx = MakerJs.round(line.end[0] - line.origin[0]); - if (dx == 0) { + var dx = line.end[0] - line.origin[0]; + if (MakerJs.round(dx) == 0) { return { line: line, hasSlope: false }; } - var dy = MakerJs.round(line.end[1] - line.origin[1]); + var dy = line.end[1] - line.origin[1]; var slope = dy / dx; var yIntercept = line.origin[1] - slope * line.origin[0]; return { @@ -2116,13 +2209,13 @@ var MakerJs; */ function checkAngleOverlap(arc1, arc2, options) { var pointsOfIntersection = []; - function checkAngles(index, a, b) { + function checkAngles(a, b) { function checkAngle(n) { return MakerJs.measure.isBetweenArcAngles(n, a, options.excludeTangents); } return checkAngle(b.startAngle) || checkAngle(b.endAngle); } - if (checkAngles(0, arc1, arc2) || checkAngles(1, arc2, arc1) || (arc1.startAngle == arc2.startAngle && arc1.endAngle == arc2.endAngle)) { + if (checkAngles(arc1, arc2) || checkAngles(arc2, arc1) || (arc1.startAngle == arc2.startAngle && arc1.endAngle == arc2.endAngle)) { options.out_AreOverlapped = true; } } @@ -2155,15 +2248,15 @@ var MakerJs; var slope2 = getSlope(line2); if (!slope1.hasSlope && !slope2.hasSlope) { //lines are both vertical, see if x are the same - if (slope1.line.origin[0] == slope2.line.origin[0]) { + if (MakerJs.round(slope1.line.origin[0] - slope2.line.origin[0]) == 0) { //check for overlap checkLineOverlap(line1, line2, options); } return null; } - if (slope1.hasSlope && slope2.hasSlope && (slope1.slope == slope2.slope)) { + if (slope1.hasSlope && slope2.hasSlope && (MakerJs.round(slope1.slope - slope2.slope, .00001) == 0)) { //lines are parallel, but not vertical, see if y-intercept is the same - if (slope1.yIntercept == slope2.yIntercept) { + if (MakerJs.round(slope1.yIntercept - slope2.yIntercept, .00001) == 0) { //check for overlap checkLineOverlap(line1, line2, options); } @@ -2205,21 +2298,22 @@ var MakerJs; } //line is horizontal, get the y value from any point var lineY = MakerJs.round(clonedLine.origin[1]); + var lineYabs = Math.abs(lineY); //if y is greater than radius, there is no intersection - if (lineY > radius) { + if (lineYabs > radius) { return null; } var anglesOfIntersection = []; //if horizontal Y is the same as the radius, we know it's 90 degrees - if (lineY == radius) { + if (lineYabs == radius) { if (options.excludeTangents) { return null; } - anglesOfIntersection.push(unRotate(90)); + anglesOfIntersection.push(unRotate(lineY > 0 ? 90 : 270)); } else { function intersectionBetweenEndpoints(x, angleOfX) { - if (MakerJs.measure.isBetween(x, clonedLine.origin[0], clonedLine.end[0], options.excludeTangents)) { + if (MakerJs.measure.isBetween(MakerJs.round(x), MakerJs.round(clonedLine.origin[0]), MakerJs.round(clonedLine.end[0]), options.excludeTangents)) { anglesOfIntersection.push(unRotate(angleOfX)); } } @@ -2241,7 +2335,7 @@ var MakerJs; */ function circleToCircle(circle1, circle2, options) { //see if circles are the same - if (circle1.radius == circle2.radius && MakerJs.point.areEqual(circle1.origin, circle2.origin)) { + if (circle1.radius == circle2.radius && MakerJs.point.areEqual(circle1.origin, circle2.origin, .0001)) { options.out_AreOverlapped = true; return null; } @@ -2273,14 +2367,14 @@ var MakerJs; return null; } //see if circles are tangent interior - if (c2.radius - x == c1.radius) { + if (MakerJs.round(c2.radius - x - c1.radius) == 0) { if (options.excludeTangents) { return null; } return [[unRotate(180)], [unRotate(180)]]; } //see if circles are tangent exterior - if (x - c2.radius == c1.radius) { + if (MakerJs.round(x - c2.radius - c1.radius) == 0) { if (options.excludeTangents) { return null; } @@ -2327,7 +2421,7 @@ var MakerJs; /** * @private */ - function getMatchingPointProperties(path1, path2) { + function getMatchingPointProperties(path1, path2, options) { var path1Properties = getPointProperties(path1); var path2Properties = getPointProperties(path2); var result = null; @@ -2341,7 +2435,7 @@ var MakerJs; }; } function check(i1, i2) { - if (MakerJs.point.areEqualRounded(path1Properties[i1].point, path2Properties[i2].point)) { + if (MakerJs.point.areEqual(path1Properties[i1].point, path2Properties[i2].point, .0001)) { result = [ makeMatch(path1, path1Properties, i1), makeMatch(path2, path2Properties, i2) @@ -2365,7 +2459,7 @@ var MakerJs; return false; } properties[i].shardPoint = circleIntersection.intersectionPoints[0]; - if (MakerJs.point.areEqualRounded(properties[i].point, circleIntersection.intersectionPoints[0], options.accuracy)) { + if (MakerJs.point.areEqual(properties[i].point, circleIntersection.intersectionPoints[0], .0001)) { if (circleIntersection.intersectionPoints.length > 1) { properties[i].shardPoint = circleIntersection.intersectionPoints[1]; } @@ -2517,11 +2611,11 @@ var MakerJs; function dogbone(line1, line2, filletRadius, options) { if (MakerJs.isPathLine(line1) && MakerJs.isPathLine(line2) && filletRadius && filletRadius > 0) { var opts = { - accuracy: .0001 + pointMatchingDistance: .005 }; MakerJs.extendObject(opts, options); //first find the common point - var commonProperty = getMatchingPointProperties(line1, line2); + var commonProperty = getMatchingPointProperties(line1, line2, options); if (commonProperty) { //get the ratio comparison of the two lines var ratio = getLineRatio([line1, line2]); @@ -2568,11 +2662,11 @@ var MakerJs; function fillet(path1, path2, filletRadius, options) { if (path1 && path2 && filletRadius && filletRadius > 0) { var opts = { - accuracy: .0001 + pointMatchingDistance: .005 }; MakerJs.extendObject(opts, options); //first find the common point - var commonProperty = getMatchingPointProperties(path1, path2); + var commonProperty = getMatchingPointProperties(path1, path2, options); if (commonProperty) { //since arcs can curl beyond, we need a local reference point. //An intersection with a circle of the same radius as the desired fillet should suffice. @@ -2674,6 +2768,34 @@ var MakerJs; (function (MakerJs) { var model; (function (model) { + /** + * @private + */ + var PointMap = (function () { + function PointMap(matchingDistance) { + if (matchingDistance === void 0) { matchingDistance = .001; } + this.matchingDistance = matchingDistance; + this.list = []; + } + PointMap.prototype.add = function (pointToAdd, item) { + this.list.push({ averagePoint: pointToAdd, item: item }); + }; + PointMap.prototype.find = function (pointToFind, saveAverage) { + for (var i = 0; i < this.list.length; i++) { + var item = this.list[i]; + var distance = MakerJs.measure.pointDistance(pointToFind, item.averagePoint); + if (distance <= this.matchingDistance) { + if (saveAverage) { + item.averagePoint = MakerJs.point.average(item.averagePoint, pointToFind); + } + return item.item; + } + } + return null; + }; + return PointMap; + })(); + model.PointMap = PointMap; /** * @private */ @@ -2720,7 +2842,7 @@ var MakerJs; while (true) { var currPath = currLink.path; currPath.reversed = currLink.reversed; - var id = model.getSimilarPathId(loopModel, currPath.primePathId); + var id = model.getSimilarPathId(loopModel, currPath.pathId); loopModel.paths[id] = currPath; if (!connections[currLink.nextConnection]) break; @@ -2750,11 +2872,11 @@ var MakerJs; var connections = {}; var result = { models: {} }; var opts = { - accuracy: .0001 + pointMatchingDistance: .005 }; MakerJs.extendObject(opts, options); function getLinkedPathsOnConnectionPoint(p) { - var serializedPoint = MakerJs.point.serialize(p, opts.accuracy); + var serializedPoint = MakerJs.point.serialize(p, .0001); //TODO convert to pointmap if (!(serializedPoint in connections)) { connections[serializedPoint] = []; } @@ -2773,14 +2895,15 @@ var MakerJs; } return result.models[id]; } + //todo: remove dead ends first model.originate(modelContext); //find loops by looking at all paths in this model model.walkPaths(modelContext, function (modelContext, pathId, pathContext) { if (!pathContext) return; var safePath = MakerJs.cloneObject(pathContext); - safePath.primePathId = pathId; - safePath.primeModel = modelContext; + safePath.pathId = pathId; + safePath.modelContext = modelContext; //circles are loops by nature if (safePath.type == MakerJs.pathType.Circle) { var loopModel = { @@ -2796,7 +2919,7 @@ var MakerJs; for (var i = 2; i--;) { var linkedPath = { path: safePath, - nextConnection: MakerJs.point.serialize(safePath.endPoints[1 - i], opts.accuracy), + nextConnection: MakerJs.point.serialize(safePath.endPoints[1 - i], .0001), reversed: i != 0 }; getLinkedPathsOnConnectionPoint(safePath.endPoints[i]).push(linkedPath); @@ -2836,13 +2959,110 @@ var MakerJs; function detachLoop(loopToDetach) { for (var id in loopToDetach.paths) { var pathDirectionalWithOriginalContext = loopToDetach.paths[id]; - var primeModel = pathDirectionalWithOriginalContext.primeModel; - if (primeModel && primeModel.paths && pathDirectionalWithOriginalContext.primePathId) { - delete primeModel.paths[pathDirectionalWithOriginalContext.primePathId]; + var primeModel = pathDirectionalWithOriginalContext.modelContext; + if (primeModel && primeModel.paths && pathDirectionalWithOriginalContext.pathId) { + delete primeModel.paths[pathDirectionalWithOriginalContext.pathId]; } } } model.detachLoop = detachLoop; + /** + * @private + */ + var DeadEndFinder = (function () { + function DeadEndFinder(pointMatchingDistance) { + this.pointMatchingDistance = pointMatchingDistance; + this.pointMap = new PointMap(pointMatchingDistance); + } + DeadEndFinder.prototype.addPathRef = function (p, pathRef) { + var found = this.pointMap.find(p, true); + if (found) { + found.push(pathRef); + } + else { + this.pointMap.add(p, [pathRef]); + } + }; + DeadEndFinder.prototype.removeMatchingPathRefs = function (a, b) { + //see if any are the same in each array + for (var ai = 0; ai < a.length; ai++) { + for (var bi = 0; bi < b.length; bi++) { + if (a[ai] === b[bi]) { + var pathRef = a[ai]; + a.splice(ai, 1); + b.splice(bi, 1); + return pathRef; + } + } + } + return null; + }; + DeadEndFinder.prototype.removePathRef = function (pathRef) { + var _this = this; + var removePath = function (p) { + var pathRefs = _this.pointMap.find(p, false); + for (var i = 0; i < pathRefs.length; i++) { + if (pathRefs[i] === pathRef) { + pathRefs.splice(i, 1); + return; + } + } + }; + for (var i = 2; i--;) { + removePath(pathRef.endPoints[i]); + } + }; + DeadEndFinder.prototype.removeDeadEnd = function () { + var found = false; + var oddPathRefs = null; + for (var i = 0; i < this.pointMap.list.length; i++) { + var pathRefs = this.pointMap.list[i].item; + if (pathRefs.length % 2 == 0) + continue; + if (pathRefs.length == 1) { + var pathRef = pathRefs[0]; + this.removePathRef(pathRef); + delete pathRef.modelContext.paths[pathRef.pathId]; + found = true; + } + else { + if (!oddPathRefs) { + //save this for another iteration + oddPathRefs = pathRefs; + } + else { + //compare with the saved + var pathRef = this.removeMatchingPathRefs(oddPathRefs, pathRefs); + if (pathRef) { + delete pathRef.modelContext.paths[pathRef.pathId]; + found = true; + //clear the saved + oddPathRefs = null; + } + } + } + } + return found; + }; + return DeadEndFinder; + })(); + function removeDeadEnds(modelContext, pointMatchingDistance) { + if (pointMatchingDistance === void 0) { pointMatchingDistance = .005; } + var serializedPointAccuracy = .0001; + var deadEndFinder = new DeadEndFinder(pointMatchingDistance); + model.walkPaths(modelContext, function (modelContext, pathId, pathContext) { + var endPoints = MakerJs.point.fromPathEnds(pathContext); + if (!endPoints) + return; + var pathRef = { modelContext: modelContext, pathId: pathId, endPoints: endPoints }; + for (var i = 2; i--;) { + deadEndFinder.addPathRef(endPoints[i], pathRef); + } + }); + while (deadEndFinder.removeDeadEnd()) + ; + } + model.removeDeadEnds = removeDeadEnds; })(model = MakerJs.model || (MakerJs.model = {})); })(MakerJs || (MakerJs = {})); var MakerJs; @@ -3055,7 +3275,7 @@ var MakerJs; var depthModel; var opts = { extrusion: 1, - accuracy: .0001 + pointMatchingDistance: .005 }; MakerJs.extendObject(opts, options); var loops = MakerJs.model.findLoops(modelToExport, opts); diff --git a/package.json b/package.json index b80be17ce..445b637dd 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "makerjs", - "version": "0.5.3", + "version": "0.6.0", "description": "Maker.js, a Microsoft Garage project, is a JavaScript library for creating and sharing modular line drawings for CNC and laser cutters.", "main": "index.js", "scripts": { diff --git a/src/core/angle.ts b/src/core/angle.ts index c6ab4c9b8..d69d46af7 100644 --- a/src/core/angle.ts +++ b/src/core/angle.ts @@ -9,11 +9,11 @@ module MakerJs.angle { * @param b Second angle. * @returns true if angles are the same, false if they are not */ - export function areEqual(angle1: number, angle2: number) { - var a1 = noRevolutions(round(angle1)); - var a2 = noRevolutions(round(angle2)); - - return a1 == a2 || a1 + 360 == a2 || a1 - 360 == a2; + export function areEqual(angle1: number, angle2: number, accuracy: number = .0001) { + var a1 = noRevolutions(angle1); + var a2 = noRevolutions(angle2); + var d = noRevolutions(round(a2 - a1, accuracy)); + return d == 0; } /** diff --git a/src/core/break.ts b/src/core/break.ts index 52f8d9b73..481fee73a 100644 --- a/src/core/break.ts +++ b/src/core/break.ts @@ -23,12 +23,14 @@ module MakerJs.path { } function getAngleStrictlyBetweenArcAngles() { - var endAngle = angle.ofArcEnd(arc); + var startAngle = angle.noRevolutions(arc.startAngle); + var endAngle = startAngle + angle.ofArcEnd(arc) - arc.startAngle; + var tries = [0, 1, -1]; for (var i = 0; i < tries.length; i++) { var add = + 360 * tries[i]; - if (measure.isBetween(angleAtBreakPoint + add, arc.startAngle, endAngle, true)) { - return angleAtBreakPoint + add; + if (measure.isBetween(angleAtBreakPoint + add, startAngle, endAngle, true)) { + return arc.startAngle + angleAtBreakPoint + add - startAngle; } } return null; diff --git a/src/core/combine.ts b/src/core/combine.ts index b80c954f4..5bbd2915e 100644 --- a/src/core/combine.ts +++ b/src/core/combine.ts @@ -6,17 +6,20 @@ module MakerJs.model { * @private */ function getNonZeroSegments(pathToSegment: IPath, breakPoint: IPoint): IPath[] { + var segmentType = pathToSegment.type; var segment1 = cloneObject(pathToSegment); var segment2 = path.breakAtPoint(segment1, breakPoint); if (segment2) { var segments: IPath[] = [segment1, segment2]; for (var i = 2; i--;) { - if (round(measure.pathLength(segments[i]), .00001) == 0) { + if (round(measure.pathLength(segments[i]), .0001) == 0) { return null; } } return segments; + } else if (segmentType == pathType.Circle) { + return [segment1]; } return null; } @@ -26,7 +29,7 @@ module MakerJs.model { */ function breakAlongForeignPath(segments: ICrossedPathSegment[], overlappedSegments: ICrossedPathSegment[], foreignPath: IPath) { - if (path.areEqual(segments[0].path, foreignPath)) { + if (path.areEqual(segments[0].path, foreignPath, .0001)) { segments[0].overlapped = true; segments[0].duplicate = true; @@ -70,13 +73,20 @@ module MakerJs.model { if (subSegments) { segments[i].path = subSegments[0]; - var newSegment = { path: subSegments[1], overlapped: segments[i].overlapped, uniqueForeignIntersectionPoints: [] }; + if (subSegments[1]) { + var newSegment: ICrossedPathSegment = { + path: subSegments[1], + pathId: segments[0].pathId, + overlapped: segments[i].overlapped, + uniqueForeignIntersectionPoints: [] + }; - if (segments[i].overlapped) { - overlappedSegments.push(newSegment); - } + if (segments[i].overlapped) { + overlappedSegments.push(newSegment); + } - segments.push(newSegment); + segments.push(newSegment); + } //re-check this segment for another deep intersection i--; @@ -94,7 +104,7 @@ module MakerJs.model { function addUniquePoint(pointToAdd: IPoint) { for (var i = 0; i < pointArray.length; i++) { - if (point.areEqualRounded(pointArray[i], pointToAdd)) { + if (point.areEqual(pointArray[i], pointToAdd, .000000001)) { return; } } @@ -112,7 +122,7 @@ module MakerJs.model { /** * @private */ - function checkInsideForeignPath(segment: IPathInside, foreignPath: IPath, farPoint: IPoint = [7654321, 1234567]) { + function checkIntersectsForeignPath(segment: IPathInside, foreignPath: IPath, foreignPathId: string, farPoint: IPoint = [7654321, 1234567]) { var origin = point.middle(segment.path); var lineToFarPoint = new paths.Line(origin, farPoint); var farInt = path.intersection(lineToFarPoint, foreignPath); @@ -133,7 +143,7 @@ module MakerJs.model { function checkInsideForeignModel(segment: IPathInside, modelToIntersect: IModel, farPoint?: IPoint) { walkPaths(modelToIntersect, function (mx: IModel, pathId2: string, path2: IPath) { if (path2) { - checkInsideForeignPath(segment, path2, farPoint); + checkIntersectsForeignPath(segment, path2, pathId2, farPoint); } }); } @@ -171,6 +181,7 @@ module MakerJs.model { * @private */ interface ICrossedPathSegment extends IPathInside { + pathId: string; overlapped: boolean; duplicate?: boolean; } @@ -178,9 +189,7 @@ module MakerJs.model { /** * @private */ - interface ICrossedPath { - modelContext: IModel; - pathId: string; + interface ICrossedPath extends IRefPathIdInModel { segments: ICrossedPathSegment[]; } @@ -192,10 +201,20 @@ module MakerJs.model { overlappedSegments: ICrossedPathSegment[]; } + /** + * Break a model's paths everywhere they intersect with another path. + * + * @param modelToBreak The model containing paths to be broken. + * @param modelToIntersect Optional model containing paths to look for intersection, or else the modelToBreak will be used. + */ + export function breakPathsAtIntersections(modelToBreak: IModel, modelToIntersect?: IModel) { + breakAllPathsAtIntersections(modelToBreak, modelToIntersect || modelToBreak, false); + } + /** * @private */ - function breakAllPathsAtIntersections(modelToBreak: IModel, modelToIntersect: IModel, farPoint: IPoint): ICombinedModel { + function breakAllPathsAtIntersections(modelToBreak: IModel, modelToIntersect: IModel, checkIsInside: boolean, farPoint?: IPoint): ICombinedModel { var crossedPaths: ICrossedPath[] = []; var overlappedSegments: ICrossedPathSegment[] = []; @@ -207,6 +226,7 @@ module MakerJs.model { //clone this path and make it the first segment var segment: ICrossedPathSegment = { path: cloneObject(path1), + pathId: pathId1, overlapped: false, uniqueForeignIntersectionPoints: [] }; @@ -219,14 +239,16 @@ module MakerJs.model { //keep breaking the segments anywhere they intersect with paths of the other model walkPaths(modelToIntersect, function (mx: IModel, pathId2: string, path2: IPath) { - if (path2) { + if (path2 && path1 !== path2) { breakAlongForeignPath(thisPath.segments, overlappedSegments, path2); } }); - //check each segment whether it is inside or outside - for (var i = 0; i < thisPath.segments.length; i++) { - checkInsideForeignModel(thisPath.segments[i], modelToIntersect); + if (checkIsInside) { + //check each segment whether it is inside or outside + for (var i = 0; i < thisPath.segments.length; i++) { + checkInsideForeignModel(thisPath.segments[i], modelToIntersect, farPoint); + } } crossedPaths.push(thisPath); @@ -238,10 +260,10 @@ module MakerJs.model { /** * @private */ - function checkForEqualOverlaps(crossedPathsA: ICrossedPathSegment[], crossedPathsB: ICrossedPathSegment[]) { + function checkForEqualOverlaps(crossedPathsA: ICrossedPathSegment[], crossedPathsB: ICrossedPathSegment[], pointMatchingDistance: number) { function compareSegments(segment1: ICrossedPathSegment, segment2: ICrossedPathSegment) { - if (path.areEqual(segment1.path, segment2.path)) { + if (path.areEqual(segment1.path, segment2.path, pointMatchingDistance)) { segment1.duplicate = segment2.duplicate = true; } } @@ -261,16 +283,16 @@ module MakerJs.model { /** * @private */ - function addOrDeleteSegments(crossedPath: ICrossedPath, includeInside: boolean, includeOutside: boolean, keepDuplicates?: boolean) { + function addOrDeleteSegments(crossedPath: ICrossedPath, includeInside: boolean, includeOutside: boolean, keepDuplicates: boolean) { - function addSegment(model: IModel, pathIdBase: string, segment: ICrossedPathSegment) { - var id = getSimilarPathId(model, pathIdBase); - model.paths[id] = segment.path; + function addSegment(modelContext: IModel, pathIdBase: string, segment: ICrossedPathSegment) { + var id = getSimilarPathId(modelContext, pathIdBase); + modelContext.paths[id] = segment.path; } - function checkAddSegment(model: IModel, pathIdBase: string, segment: ICrossedPathSegment) { + function checkAddSegment(modelContext: IModel, pathIdBase: string, segment: ICrossedPathSegment) { if (segment.isInside && includeInside || !segment.isInside && includeOutside) { - addSegment(model, pathIdBase, segment); + addSegment(modelContext, pathIdBase, segment); } } @@ -289,7 +311,7 @@ module MakerJs.model { } /** - * Combine 2 models. The models should be originated. + * Combine 2 models. The models should be originated, and every path within each model should be part of a loop. * * @param modelA First model to combine. * @param modelB Second model to combine. @@ -300,19 +322,29 @@ module MakerJs.model { * @param keepDuplicates Flag to include paths which are duplicate in both models. * @param farPoint Optional point of reference which is outside the bounds of both models. */ - export function combine(modelA: IModel, modelB: IModel, includeAInsideB: boolean = false, includeAOutsideB: boolean = true, includeBInsideA: boolean = false, includeBOutsideA: boolean = true, keepDuplicates: boolean = true, farPoint?: IPoint) { + export function combine(modelA: IModel, modelB: IModel, includeAInsideB: boolean = false, includeAOutsideB: boolean = true, includeBInsideA: boolean = false, includeBOutsideA: boolean = true, options?: ICombineOptions) { - var pathsA = breakAllPathsAtIntersections(modelA, modelB, farPoint); - var pathsB = breakAllPathsAtIntersections(modelB, modelA, farPoint); + var opts: ICombineOptions = { + trimDeadEnds: true, + pointMatchingDistance: .005 + }; + extendObject(opts, options); - checkForEqualOverlaps(pathsA.overlappedSegments, pathsB.overlappedSegments); + var pathsA = breakAllPathsAtIntersections(modelA, modelB, true, opts.farPoint); + var pathsB = breakAllPathsAtIntersections(modelB, modelA, true, opts.farPoint); + + checkForEqualOverlaps(pathsA.overlappedSegments, pathsB.overlappedSegments, opts.pointMatchingDistance); for (var i = 0; i < pathsA.crossedPaths.length; i++) { - addOrDeleteSegments(pathsA.crossedPaths[i], includeAInsideB, includeAOutsideB, keepDuplicates); + addOrDeleteSegments(pathsA.crossedPaths[i], includeAInsideB, includeAOutsideB, true); } for (var i = 0; i < pathsB.crossedPaths.length; i++) { - addOrDeleteSegments(pathsB.crossedPaths[i], includeBInsideA, includeBOutsideA); + addOrDeleteSegments(pathsB.crossedPaths[i], includeBInsideA, includeBOutsideA, false); + } + + if (opts.trimDeadEnds) { + removeDeadEnds({ models: { modelA: modelA, modelB: modelB } }); } } diff --git a/src/core/exporter.ts b/src/core/exporter.ts index e66c11e0e..ac4177d48 100644 --- a/src/core/exporter.ts +++ b/src/core/exporter.ts @@ -76,13 +76,17 @@ module MakerJs.exporter { if (modelToExport.paths) { for (var id in modelToExport.paths) { - this.exportPath(id, modelToExport.paths[id], newOffset, modelToExport.layer); + var currPath = modelToExport.paths[id]; + if (!currPath) continue; + this.exportPath(id, currPath, newOffset, modelToExport.layer); } } if (modelToExport.models) { for (var id in modelToExport.models) { - this.exportModel(id, modelToExport.models[id], newOffset); + var currModel = modelToExport.models[id]; + if (!currModel) continue; + this.exportModel(id, currModel, newOffset); } } diff --git a/src/core/fillet.ts b/src/core/fillet.ts index 4e8d1f0b5..64e5addce 100644 --- a/src/core/fillet.ts +++ b/src/core/fillet.ts @@ -63,7 +63,7 @@ module MakerJs.path { /** * @private */ - function getMatchingPointProperties(path1: IPath, path2: IPath): IMatchPointProperty[] { + function getMatchingPointProperties(path1: IPath, path2: IPath, options?: IPointMatchOptions): IMatchPointProperty[] { var path1Properties = getPointProperties(path1); var path2Properties = getPointProperties(path2); @@ -80,7 +80,7 @@ module MakerJs.path { } function check(i1: number, i2: number) { - if (point.areEqualRounded(path1Properties[i1].point, path2Properties[i2].point)) { + if (point.areEqual(path1Properties[i1].point, path2Properties[i2].point, .0001)) { result = [ makeMatch(path1, path1Properties, i1), makeMatch(path2, path2Properties, i2) @@ -110,7 +110,7 @@ module MakerJs.path { properties[i].shardPoint = circleIntersection.intersectionPoints[0]; - if (point.areEqualRounded(properties[i].point, circleIntersection.intersectionPoints[0], options.accuracy)) { + if (point.areEqual(properties[i].point, circleIntersection.intersectionPoints[0], .0001)) { if (circleIntersection.intersectionPoints.length > 1) { properties[i].shardPoint = circleIntersection.intersectionPoints[1]; } else { @@ -296,12 +296,12 @@ module MakerJs.path { if (isPathLine(line1) && isPathLine(line2) && filletRadius && filletRadius > 0) { var opts: IPointMatchOptions = { - accuracy: .0001 + pointMatchingDistance: .005 }; extendObject(opts, options); //first find the common point - var commonProperty = getMatchingPointProperties(line1, line2); + var commonProperty = getMatchingPointProperties(line1, line2, options); if (commonProperty) { //get the ratio comparison of the two lines @@ -360,12 +360,12 @@ module MakerJs.path { if (path1 && path2 && filletRadius && filletRadius > 0) { var opts: IPointMatchOptions = { - accuracy: .0001 + pointMatchingDistance: .005 }; extendObject(opts, options); //first find the common point - var commonProperty = getMatchingPointProperties(path1, path2); + var commonProperty = getMatchingPointProperties(path1, path2, options); if (commonProperty) { //since arcs can curl beyond, we need a local reference point. diff --git a/src/core/intersect.ts b/src/core/intersect.ts index d91af9473..99e3fc483 100644 --- a/src/core/intersect.ts +++ b/src/core/intersect.ts @@ -232,15 +232,15 @@ module MakerJs.path { * @private */ function getSlope(line: IPathLine): ISlope { - var dx = round(line.end[0] - line.origin[0]); - if (dx == 0) { + var dx = line.end[0] - line.origin[0]; + if (round(dx) == 0) { return { line: line, hasSlope: false }; } - var dy = round(line.end[1] - line.origin[1]); + var dy = line.end[1] - line.origin[1]; var slope = dy / dx; var yIntercept = line.origin[1] - slope * line.origin[0]; @@ -268,7 +268,7 @@ module MakerJs.path { function checkAngleOverlap(arc1: IPathArc, arc2: IPathArc, options: IPathIntersectionOptions): void { var pointsOfIntersection: IPoint[] = []; - function checkAngles(index: number, a: IPathArc, b: IPathArc) { + function checkAngles(a: IPathArc, b: IPathArc) { function checkAngle(n: number) { return measure.isBetweenArcAngles(n, a, options.excludeTangents); @@ -277,7 +277,7 @@ module MakerJs.path { return checkAngle(b.startAngle) || checkAngle(b.endAngle); } - if (checkAngles(0, arc1, arc2) || checkAngles(1, arc2, arc1) || (arc1.startAngle == arc2.startAngle && arc1.endAngle == arc2.endAngle)) { + if (checkAngles(arc1, arc2) || checkAngles(arc2, arc1) || (arc1.startAngle == arc2.startAngle && arc1.endAngle == arc2.endAngle)) { options.out_AreOverlapped = true; } } @@ -318,7 +318,7 @@ module MakerJs.path { if (!slope1.hasSlope && !slope2.hasSlope) { //lines are both vertical, see if x are the same - if (slope1.line.origin[0] == slope2.line.origin[0]) { + if (round(slope1.line.origin[0] - slope2.line.origin[0]) == 0) { //check for overlap checkLineOverlap(line1, line2, options); @@ -327,10 +327,10 @@ module MakerJs.path { return null; } - if (slope1.hasSlope && slope2.hasSlope && (slope1.slope == slope2.slope)) { + if (slope1.hasSlope && slope2.hasSlope && (round(slope1.slope - slope2.slope, .00001) == 0)) { //lines are parallel, but not vertical, see if y-intercept is the same - if (slope1.yIntercept == slope2.yIntercept) { + if (round(slope1.yIntercept - slope2.yIntercept, .00001) == 0) { //check for overlap checkLineOverlap(line1, line2, options); @@ -382,27 +382,28 @@ module MakerJs.path { //line is horizontal, get the y value from any point var lineY = round(clonedLine.origin[1]); + var lineYabs = Math.abs(lineY); //if y is greater than radius, there is no intersection - if (lineY > radius) { + if (lineYabs > radius) { return null; } var anglesOfIntersection: number[] = []; //if horizontal Y is the same as the radius, we know it's 90 degrees - if (lineY == radius) { + if (lineYabs == radius) { if (options.excludeTangents) { return null; } - anglesOfIntersection.push(unRotate(90)); + anglesOfIntersection.push(unRotate(lineY > 0 ? 90 : 270)); } else { function intersectionBetweenEndpoints(x: number, angleOfX: number) { - if (measure.isBetween(x, clonedLine.origin[0], clonedLine.end[0], options.excludeTangents)) { + if (measure.isBetween(round(x), round(clonedLine.origin[0]), round(clonedLine.end[0]), options.excludeTangents)) { anglesOfIntersection.push(unRotate(angleOfX)); } } @@ -430,7 +431,7 @@ module MakerJs.path { function circleToCircle(circle1: IPathCircle, circle2: IPathCircle, options: IPathIntersectionOptions): number[][] { //see if circles are the same - if (circle1.radius == circle2.radius && point.areEqual(circle1.origin, circle2.origin)) { + if (circle1.radius == circle2.radius && point.areEqual(circle1.origin, circle2.origin, .0001)) { options.out_AreOverlapped = true; return null; } @@ -472,7 +473,7 @@ module MakerJs.path { } //see if circles are tangent interior - if (c2.radius - x == c1.radius) { + if (round(c2.radius - x - c1.radius) == 0) { if (options.excludeTangents) { return null; @@ -482,7 +483,7 @@ module MakerJs.path { } //see if circles are tangent exterior - if (x - c2.radius == c1.radius) { + if (round(x - c2.radius - c1.radius) == 0) { if (options.excludeTangents) { return null; diff --git a/src/core/loops.ts b/src/core/loops.ts index 88d81f621..298b21b6f 100644 --- a/src/core/loops.ts +++ b/src/core/loops.ts @@ -5,9 +5,46 @@ module MakerJs.model { /** * @private */ - interface IPathDirectionalWithPrimeContext extends IPathDirectional { - primePathId: string; - primeModel: IModel; + export interface IPointMappedItem { + averagePoint: IPoint; + item: T; + } + + /** + * @private + */ + export class PointMap { + public list: IPointMappedItem[] = []; + + constructor(public matchingDistance: number = .001) { + } + + public add(pointToAdd: IPoint, item: T) { + this.list.push({ averagePoint: pointToAdd, item: item }); + } + + public find(pointToFind: IPoint, saveAverage: boolean): T { + for (var i = 0; i < this.list.length; i++) { + var item = this.list[i]; + var distance = measure.pointDistance(pointToFind, item.averagePoint); + + if (distance <= this.matchingDistance) { + + if (saveAverage) { + item.averagePoint = point.average(item.averagePoint, pointToFind); + } + + return item.item; + } + } + return null; + } + } + + /** + * @private + */ + interface IPathDirectionalWithPrimeContext extends IPathDirectional, IRefPathIdInModel { } /** @@ -90,7 +127,7 @@ module MakerJs.model { var currPath = currLink.path; currPath.reversed = currLink.reversed; - var id = model.getSimilarPathId(loopModel, currPath.primePathId); + var id = model.getSimilarPathId(loopModel, currPath.pathId); loopModel.paths[id] = currPath; if (!connections[currLink.nextConnection]) break; @@ -126,12 +163,12 @@ module MakerJs.model { var result: IModel = { models: {} }; var opts: IFindLoopsOptions = { - accuracy: .0001 + pointMatchingDistance: .005 }; extendObject(opts, options); function getLinkedPathsOnConnectionPoint(p: IPoint) { - var serializedPoint = point.serialize(p, opts.accuracy); + var serializedPoint = point.serialize(p, .0001); //TODO convert to pointmap if (!(serializedPoint in connections)) { connections[serializedPoint] = []; @@ -157,6 +194,7 @@ module MakerJs.model { return result.models[id]; } + //todo: remove dead ends first model.originate(modelContext); //find loops by looking at all paths in this model @@ -165,8 +203,8 @@ module MakerJs.model { if (!pathContext) return; var safePath = cloneObject(pathContext); - safePath.primePathId = pathId; - safePath.primeModel = modelContext; + safePath.pathId = pathId; + safePath.modelContext = modelContext; //circles are loops by nature if (safePath.type == pathType.Circle) { @@ -185,7 +223,7 @@ module MakerJs.model { for (var i = 2; i--;) { var linkedPath: ILinkedPath = { path: safePath, - nextConnection: point.serialize(safePath.endPoints[1 - i], opts.accuracy), + nextConnection: point.serialize(safePath.endPoints[1 - i], .0001), //TODO convert to pointmap reversed: i != 0 }; getLinkedPathsOnConnectionPoint(safePath.endPoints[i]).push(linkedPath); @@ -235,10 +273,130 @@ module MakerJs.model { export function detachLoop(loopToDetach: IModel) { for (var id in loopToDetach.paths) { var pathDirectionalWithOriginalContext = loopToDetach.paths[id]; - var primeModel = pathDirectionalWithOriginalContext.primeModel; - if (primeModel && primeModel.paths && pathDirectionalWithOriginalContext.primePathId) { - delete primeModel.paths[pathDirectionalWithOriginalContext.primePathId]; + var primeModel = pathDirectionalWithOriginalContext.modelContext; + if (primeModel && primeModel.paths && pathDirectionalWithOriginalContext.pathId) { + delete primeModel.paths[pathDirectionalWithOriginalContext.pathId]; + } + } + } + + /** + * @private + */ + interface IRefPathEndpoints extends IRefPathIdInModel { + endPoints: IPoint[]; + } + + /** + * @private + */ + class DeadEndFinder { + + private pointMap: PointMap; + + constructor(public pointMatchingDistance) { + this.pointMap = new PointMap(pointMatchingDistance); + } + + public addPathRef(p: IPoint, pathRef: IRefPathEndpoints) { + var found = this.pointMap.find(p, true); + if (found) { + found.push(pathRef); + } else { + this.pointMap.add(p, [pathRef]); } } + + private removeMatchingPathRefs(a: IRefPathEndpoints[], b: IRefPathEndpoints[]) { + //see if any are the same in each array + for (var ai = 0; ai < a.length; ai++) { + for (var bi = 0; bi < b.length; bi++) { + if (a[ai] === b[bi]) { + var pathRef = a[ai]; + a.splice(ai, 1); + b.splice(bi, 1); + return pathRef; + } + } + } + return null; + } + + private removePathRef(pathRef: IRefPathEndpoints) { + + var removePath = (p: IPoint) => { + var pathRefs = this.pointMap.find(p, false); + + for (var i = 0; i < pathRefs.length; i++) { + if (pathRefs[i] === pathRef) { + pathRefs.splice(i, 1); + return; + } + } + } + + for (var i = 2; i--;) { + removePath(pathRef.endPoints[i]); + } + } + + public removeDeadEnd(): boolean { + var found = false; + var oddPathRefs: IRefPathEndpoints[] = null; + + for (var i = 0; i < this.pointMap.list.length; i++) { + + var pathRefs = this.pointMap.list[i].item; + + if (pathRefs.length % 2 == 0) continue; + + if (pathRefs.length == 1) { + var pathRef = pathRefs[0]; + this.removePathRef(pathRef); + + delete pathRef.modelContext.paths[pathRef.pathId]; + found = true; + + } else { + + if (!oddPathRefs) { + //save this for another iteration + oddPathRefs = pathRefs; + } else { + + //compare with the saved + var pathRef = this.removeMatchingPathRefs(oddPathRefs, pathRefs); + if (pathRef) { + + delete pathRef.modelContext.paths[pathRef.pathId]; + found = true; + + //clear the saved + oddPathRefs = null; + } + } + } + } + return found; + } + } + + export function removeDeadEnds(modelContext: IModel, pointMatchingDistance = .005) { + var serializedPointAccuracy = .0001; + var deadEndFinder = new DeadEndFinder(pointMatchingDistance); + + walkPaths(modelContext, function (modelContext: IModel, pathId: string, pathContext: IPath) { + var endPoints = point.fromPathEnds(pathContext); + + if (!endPoints) return; + + var pathRef: IRefPathEndpoints = { modelContext: modelContext, pathId: pathId, endPoints: endPoints }; + + for (var i = 2; i--;) { + deadEndFinder.addPathRef(endPoints[i], pathRef); + } + }); + + while (deadEndFinder.removeDeadEnd()); } } diff --git a/src/core/maker.ts b/src/core/maker.ts index cf9b52001..354834d68 100644 --- a/src/core/maker.ts +++ b/src/core/maker.ts @@ -336,9 +336,25 @@ module MakerJs { export interface IPointMatchOptions { /** - * Optional exemplar of number of decimal places. + * Max distance to consider two points as the same. */ - accuracy?: number; + pointMatchingDistance?: number; + } + + /** + * Options to pass to model.combine. + */ + export interface ICombineOptions extends IPointMatchOptions { + + /** + * Flag to remove paths which are not part of a loop. + */ + trimDeadEnds?: boolean; + + /** + * Point which is known to be outside of the model. + */ + farPoint?: IPoint; } /** @@ -449,6 +465,20 @@ module MakerJs { return item && (item.paths || item.models); } + /** + * Reference to a path id within a model. + */ + export interface IRefPathIdInModel { + modelContext: IModel; + pathId: string; + } + + /** + * Path and its reference id within a model + */ + export interface IRefPathInModel extends IRefPathIdInModel { + pathContext: IPath; + } } //CommonJs diff --git a/src/core/measure.ts b/src/core/measure.ts index 93055e9a7..998e24ade 100644 --- a/src/core/measure.ts +++ b/src/core/measure.ts @@ -76,6 +76,13 @@ module MakerJs.measure { var startAngle = arc.startAngle; var endAngle = angle.ofArcEnd(arc); + var span = endAngle - startAngle; + + startAngle = angle.noRevolutions(startAngle); + endAngle = startAngle + span; + + angleInQuestion = angle.noRevolutions(angleInQuestion); + //computed angles will not be negative, but the arc may have specified a negative angle, so check against one revolution forward and backward return (isBetween(angleInQuestion, startAngle, endAngle, exclusive) || isBetween(angleInQuestion, startAngle + 360, endAngle + 360, exclusive) || isBetween(angleInQuestion, startAngle - 360, endAngle - 360, exclusive)) } @@ -90,11 +97,11 @@ module MakerJs.measure { */ export function isBetweenPoints(pointInQuestion: IPoint, line: IPathLine, exclusive: boolean): boolean { for (var i = 2; i--;) { - var origin_value = round(line.origin[i]); - var end_value = round(line.end[i]); - if (origin_value == end_value) { + if (round(line.origin[i] - line.end[i], .000001) == 0) { continue; } + var origin_value = round(line.origin[i]); + var end_value = round(line.end[i]); if (!isBetween(round(pointInQuestion[i]), origin_value, end_value, exclusive)) return false; } return true; @@ -228,19 +235,20 @@ module MakerJs.measure { getExtreme(totalMeasurement.high, pathMeasurement.high, Math.max); } - function measure(model: IModel, offsetOrigin?: IPoint) { + function measure(modelToMeasure: IModel, offsetOrigin?: IPoint) { + if (!modelToMeasure) return; - var newOrigin = point.add(model.origin, offsetOrigin); + var newOrigin = point.add(modelToMeasure.origin, offsetOrigin); - if (model.paths) { - for (var id in model.paths) { - lowerOrHigher(newOrigin, pathExtents(model.paths[id])); + if (modelToMeasure.paths) { + for (var id in modelToMeasure.paths) { + lowerOrHigher(newOrigin, pathExtents(modelToMeasure.paths[id])); } } - if (model.models) { - for (var id in model.models) { - measure(model.models[id], newOrigin); + if (modelToMeasure.models) { + for (var id in modelToMeasure.models) { + measure(modelToMeasure.models[id], newOrigin); } } } diff --git a/src/core/model.ts b/src/core/model.ts index 05b4a6e49..055986171 100644 --- a/src/core/model.ts +++ b/src/core/model.ts @@ -20,13 +20,33 @@ module MakerJs.model { return count; } + /** + * Get an unused id in the models map with the same prefix. + * + * @param modelContext The model containing the models map. + * @param modelId The id to use directly (if unused), or as a prefix. + */ + export function getSimilarModelId(modelContext: IModel, modelId: string): string { + if (!modelContext.models) return modelId; + + var i = 0; + var newModelId = modelId; + while (newModelId in modelContext.models) { + i++; + newModelId = modelId + '_' + i; + } + return newModelId; + } + /** * Get an unused id in the paths map with the same prefix. * * @param modelContext The model containing the paths map. - * @param pathId The pathId to use directly (if unused), or as a prefix. + * @param pathId The id to use directly (if unused), or as a prefix. */ export function getSimilarPathId(modelContext: IModel, pathId: string): string { + if (!modelContext.paths) return pathId; + var i = 0; var newPathId = pathId; while (newPathId in modelContext.paths) { @@ -43,6 +63,8 @@ module MakerJs.model { * @param origin Optional offset reference point. */ export function originate(modelToOriginate: IModel, origin?: IPoint) { + if (!modelToOriginate) return; + var newOrigin = point.add(modelToOriginate.origin, origin); if (modelToOriginate.paths) { @@ -229,12 +251,14 @@ module MakerJs.model { if (modelContext.paths) { for (var pathId in modelContext.paths) { + if (!modelContext.paths[pathId]) continue; callback(modelContext, pathId, modelContext.paths[pathId]); } } if (modelContext.models) { for (var id in modelContext.models) { + if (!modelContext.models[id]) continue; walkPaths(modelContext.models[id], callback); } } diff --git a/src/core/openjscad.ts b/src/core/openjscad.ts index 247b8909d..9cb5e29dd 100644 --- a/src/core/openjscad.ts +++ b/src/core/openjscad.ts @@ -152,7 +152,7 @@ module MakerJs.exporter { var opts: IOpenJsCadOptions = { extrusion: 1, - accuracy: .0001 + pointMatchingDistance: .005 }; extendObject(opts, options); diff --git a/src/core/path.ts b/src/core/path.ts index 09d24e3de..7e7b004cf 100644 --- a/src/core/path.ts +++ b/src/core/path.ts @@ -6,7 +6,7 @@ module MakerJs.path { * @private */ interface IPathAreEqualMap { - [type: string]: (path1: IPath, path2: IPath) => boolean; + [type: string]: (path1: IPath, path2: IPath, withinPointDistance?: number) => boolean; } /** @@ -14,16 +14,17 @@ module MakerJs.path { */ var pathAreEqualMap: IPathAreEqualMap = {}; - pathAreEqualMap[pathType.Line] = function (line1: IPathLine, line2: IPathLine): boolean { - return (point.areEqual(line1.origin, line2.origin) && point.areEqual(line1.end, line2.end)) || (point.areEqual(line1.origin, line2.end) && point.areEqual(line1.end, line2.origin)); + pathAreEqualMap[pathType.Line] = function (line1: IPathLine, line2: IPathLine, withinPointDistance?: number): boolean { + return (point.areEqual(line1.origin, line2.origin, withinPointDistance) && point.areEqual(line1.end, line2.end, withinPointDistance)) + || (point.areEqual(line1.origin, line2.end, withinPointDistance) && point.areEqual(line1.end, line2.origin, withinPointDistance)); }; - pathAreEqualMap[pathType.Circle] = function (circle1: IPathCircle, circle2: IPathCircle): boolean { - return point.areEqual(circle1.origin, circle2.origin) && circle1.radius == circle2.radius; + pathAreEqualMap[pathType.Circle] = function (circle1: IPathCircle, circle2: IPathCircle, withinPointDistance): boolean { + return point.areEqual(circle1.origin, circle2.origin, withinPointDistance) && circle1.radius == circle2.radius; }; - pathAreEqualMap[pathType.Arc] = function (arc1: IPathArc, arc2: IPathArc): boolean { - return pathAreEqualMap[pathType.Circle](arc1, arc2) && angle.areEqual(arc1.startAngle, arc2.startAngle) && angle.areEqual(arc1.endAngle, arc2.endAngle); + pathAreEqualMap[pathType.Arc] = function (arc1: IPathArc, arc2: IPathArc, withinPointDistance): boolean { + return pathAreEqualMap[pathType.Circle](arc1, arc2, withinPointDistance) && angle.areEqual(arc1.startAngle, arc2.startAngle) && angle.areEqual(arc1.endAngle, arc2.endAngle); }; /** @@ -33,14 +34,14 @@ module MakerJs.path { * @param b Second path. * @returns true if paths are the same, false if they are not */ - export function areEqual(path1: IPath, path2: IPath): boolean { + export function areEqual(path1: IPath, path2: IPath, withinPointDistance?: number): boolean { var result = false; if (path1.type == path2.type) { var fn = pathAreEqualMap[path1.type]; if (fn) { - result = fn(path1, path2); + result = fn(path1, path2, withinPointDistance); } } @@ -174,8 +175,8 @@ module MakerJs.path { } map[pathType.Arc] = function (arc: IPathArc) { - arc.startAngle += angleInDegrees; - arc.endAngle += angleInDegrees; + arc.startAngle = angle.noRevolutions(arc.startAngle + angleInDegrees); + arc.endAngle = angle.noRevolutions(arc.endAngle + angleInDegrees); } pathToRotate.origin = point.rotate(pathToRotate.origin, angleInDegrees, rotationOrigin); diff --git a/src/core/point.ts b/src/core/point.ts index 2f58fb682..f5198482f 100644 --- a/src/core/point.ts +++ b/src/core/point.ts @@ -32,8 +32,13 @@ module MakerJs.point { * @param b Second point. * @returns true if points are the same, false if they are not */ - export function areEqual(a: IPoint, b: IPoint): boolean { - return a[0] == b[0] && a[1] == b[1]; + export function areEqual(a: IPoint, b: IPoint, withinDistance?: number): boolean { + if (!withinDistance) { + return a[0] == b[0] && a[1] == b[1]; + } else { + var distance = measure.pointDistance(a, b); + return distance <= withinDistance; + } } /** @@ -48,6 +53,20 @@ module MakerJs.point { return round(a[0], accuracy) == round(b[0], accuracy) && round(a[1], accuracy) == round(b[1], accuracy); } + /** + * Get the average of two points. + * + * @param a First point. + * @param b Second point. + * @returns New point object which is the average of a and b. + */ + export function average(a: IPoint, b: IPoint): IPoint{ + function avg(i): number { + return (a[i] + b[i]) / 2; + } + return [avg(0), avg(1)]; + } + /** * Clone a point into a new point. * @@ -230,7 +249,7 @@ module MakerJs.point { var d = measure.pointDistance(rotationOrigin, pointToRotate); var rotatedPoint = fromPolar(pointAngleInRadians + angle.toRadians(angleInDegrees), d); - return rounded(add(rotationOrigin, rotatedPoint)); + return add(rotationOrigin, rotatedPoint); } /** @@ -257,7 +276,7 @@ module MakerJs.point { */ export function serialize(pointContext: IPoint, accuracy?: number) { var roundedPoint = rounded(pointContext, accuracy); - return roundedPoint[0] + ',' + roundedPoint[1]; + return JSON.stringify(roundedPoint); } /** diff --git a/target/js/browser.maker.js b/target/js/browser.maker.js index 38bc67c6a..f40754711 100644 --- a/target/js/browser.maker.js +++ b/target/js/browser.maker.js @@ -170,10 +170,12 @@ var MakerJs; * @param b Second angle. * @returns true if angles are the same, false if they are not */ - function areEqual(angle1, angle2) { - var a1 = noRevolutions(MakerJs.round(angle1)); - var a2 = noRevolutions(MakerJs.round(angle2)); - return a1 == a2 || a1 + 360 == a2 || a1 - 360 == a2; + function areEqual(angle1, angle2, accuracy) { + if (accuracy === void 0) { accuracy = .0001; } + var a1 = noRevolutions(angle1); + var a2 = noRevolutions(angle2); + var d = noRevolutions(MakerJs.round(a2 - a1, accuracy)); + return d == 0; } angle.areEqual = areEqual; /** @@ -323,8 +325,14 @@ var MakerJs; * @param b Second point. * @returns true if points are the same, false if they are not */ - function areEqual(a, b) { - return a[0] == b[0] && a[1] == b[1]; + function areEqual(a, b, withinDistance) { + if (!withinDistance) { + return a[0] == b[0] && a[1] == b[1]; + } + else { + var distance = MakerJs.measure.pointDistance(a, b); + return distance <= withinDistance; + } } point.areEqual = areEqual; /** @@ -340,6 +348,20 @@ var MakerJs; return MakerJs.round(a[0], accuracy) == MakerJs.round(b[0], accuracy) && MakerJs.round(a[1], accuracy) == MakerJs.round(b[1], accuracy); } point.areEqualRounded = areEqualRounded; + /** + * Get the average of two points. + * + * @param a First point. + * @param b Second point. + * @returns New point object which is the average of a and b. + */ + function average(a, b) { + function avg(i) { + return (a[i] + b[i]) / 2; + } + return [avg(0), avg(1)]; + } + point.average = average; /** * Clone a point into a new point. * @@ -507,7 +529,7 @@ var MakerJs; var pointAngleInRadians = MakerJs.angle.ofPointInRadians(rotationOrigin, pointToRotate); var d = MakerJs.measure.pointDistance(rotationOrigin, pointToRotate); var rotatedPoint = fromPolar(pointAngleInRadians + MakerJs.angle.toRadians(angleInDegrees), d); - return rounded(add(rotationOrigin, rotatedPoint)); + return add(rotationOrigin, rotatedPoint); } point.rotate = rotate; /** @@ -534,7 +556,7 @@ var MakerJs; */ function serialize(pointContext, accuracy) { var roundedPoint = rounded(pointContext, accuracy); - return roundedPoint[0] + ',' + roundedPoint[1]; + return JSON.stringify(roundedPoint); } point.serialize = serialize; /** @@ -568,14 +590,15 @@ var MakerJs; * @private */ var pathAreEqualMap = {}; - pathAreEqualMap[MakerJs.pathType.Line] = function (line1, line2) { - return (MakerJs.point.areEqual(line1.origin, line2.origin) && MakerJs.point.areEqual(line1.end, line2.end)) || (MakerJs.point.areEqual(line1.origin, line2.end) && MakerJs.point.areEqual(line1.end, line2.origin)); + pathAreEqualMap[MakerJs.pathType.Line] = function (line1, line2, withinPointDistance) { + return (MakerJs.point.areEqual(line1.origin, line2.origin, withinPointDistance) && MakerJs.point.areEqual(line1.end, line2.end, withinPointDistance)) + || (MakerJs.point.areEqual(line1.origin, line2.end, withinPointDistance) && MakerJs.point.areEqual(line1.end, line2.origin, withinPointDistance)); }; - pathAreEqualMap[MakerJs.pathType.Circle] = function (circle1, circle2) { - return MakerJs.point.areEqual(circle1.origin, circle2.origin) && circle1.radius == circle2.radius; + pathAreEqualMap[MakerJs.pathType.Circle] = function (circle1, circle2, withinPointDistance) { + return MakerJs.point.areEqual(circle1.origin, circle2.origin, withinPointDistance) && circle1.radius == circle2.radius; }; - pathAreEqualMap[MakerJs.pathType.Arc] = function (arc1, arc2) { - return pathAreEqualMap[MakerJs.pathType.Circle](arc1, arc2) && MakerJs.angle.areEqual(arc1.startAngle, arc2.startAngle) && MakerJs.angle.areEqual(arc1.endAngle, arc2.endAngle); + pathAreEqualMap[MakerJs.pathType.Arc] = function (arc1, arc2, withinPointDistance) { + return pathAreEqualMap[MakerJs.pathType.Circle](arc1, arc2, withinPointDistance) && MakerJs.angle.areEqual(arc1.startAngle, arc2.startAngle) && MakerJs.angle.areEqual(arc1.endAngle, arc2.endAngle); }; /** * Find out if two paths are equal. @@ -584,12 +607,12 @@ var MakerJs; * @param b Second path. * @returns true if paths are the same, false if they are not */ - function areEqual(path1, path2) { + function areEqual(path1, path2, withinPointDistance) { var result = false; if (path1.type == path2.type) { var fn = pathAreEqualMap[path1.type]; if (fn) { - result = fn(path1, path2); + result = fn(path1, path2, withinPointDistance); } } return result; @@ -688,8 +711,8 @@ var MakerJs; line.end = MakerJs.point.rotate(line.end, angleInDegrees, rotationOrigin); }; map[MakerJs.pathType.Arc] = function (arc) { - arc.startAngle += angleInDegrees; - arc.endAngle += angleInDegrees; + arc.startAngle = MakerJs.angle.noRevolutions(arc.startAngle + angleInDegrees); + arc.endAngle = MakerJs.angle.noRevolutions(arc.endAngle + angleInDegrees); }; pathToRotate.origin = MakerJs.point.rotate(pathToRotate.origin, angleInDegrees, rotationOrigin); var fn = map[pathToRotate.type]; @@ -741,12 +764,13 @@ var MakerJs; return null; } function getAngleStrictlyBetweenArcAngles() { - var endAngle = MakerJs.angle.ofArcEnd(arc); + var startAngle = MakerJs.angle.noRevolutions(arc.startAngle); + var endAngle = startAngle + MakerJs.angle.ofArcEnd(arc) - arc.startAngle; var tries = [0, 1, -1]; for (var i = 0; i < tries.length; i++) { var add = +360 * tries[i]; - if (MakerJs.measure.isBetween(angleAtBreakPoint + add, arc.startAngle, endAngle, true)) { - return angleAtBreakPoint + add; + if (MakerJs.measure.isBetween(angleAtBreakPoint + add, startAngle, endAngle, true)) { + return arc.startAngle + angleAtBreakPoint + add - startAngle; } } return null; @@ -917,13 +941,33 @@ var MakerJs; return count; } model.countChildModels = countChildModels; + /** + * Get an unused id in the models map with the same prefix. + * + * @param modelContext The model containing the models map. + * @param modelId The id to use directly (if unused), or as a prefix. + */ + function getSimilarModelId(modelContext, modelId) { + if (!modelContext.models) + return modelId; + var i = 0; + var newModelId = modelId; + while (newModelId in modelContext.models) { + i++; + newModelId = modelId + '_' + i; + } + return newModelId; + } + model.getSimilarModelId = getSimilarModelId; /** * Get an unused id in the paths map with the same prefix. * * @param modelContext The model containing the paths map. - * @param pathId The pathId to use directly (if unused), or as a prefix. + * @param pathId The id to use directly (if unused), or as a prefix. */ function getSimilarPathId(modelContext, pathId) { + if (!modelContext.paths) + return pathId; var i = 0; var newPathId = pathId; while (newPathId in modelContext.paths) { @@ -940,6 +984,8 @@ var MakerJs; * @param origin Optional offset reference point. */ function originate(modelToOriginate, origin) { + if (!modelToOriginate) + return; var newOrigin = MakerJs.point.add(modelToOriginate.origin, origin); if (modelToOriginate.paths) { for (var id in modelToOriginate.paths) { @@ -1101,11 +1147,15 @@ var MakerJs; function walkPaths(modelContext, callback) { if (modelContext.paths) { for (var pathId in modelContext.paths) { + if (!modelContext.paths[pathId]) + continue; callback(modelContext, pathId, modelContext.paths[pathId]); } } if (modelContext.models) { for (var id in modelContext.models) { + if (!modelContext.models[id]) + continue; walkPaths(modelContext.models[id], callback); } } @@ -1116,29 +1166,33 @@ var MakerJs; var MakerJs; (function (MakerJs) { var model; - (function (model_1) { + (function (model) { /** * @private */ function getNonZeroSegments(pathToSegment, breakPoint) { + var segmentType = pathToSegment.type; var segment1 = MakerJs.cloneObject(pathToSegment); var segment2 = MakerJs.path.breakAtPoint(segment1, breakPoint); if (segment2) { var segments = [segment1, segment2]; for (var i = 2; i--;) { - if (MakerJs.round(MakerJs.measure.pathLength(segments[i]), .00001) == 0) { + if (MakerJs.round(MakerJs.measure.pathLength(segments[i]), .0001) == 0) { return null; } } return segments; } + else if (segmentType == MakerJs.pathType.Circle) { + return [segment1]; + } return null; } /** * @private */ function breakAlongForeignPath(segments, overlappedSegments, foreignPath) { - if (MakerJs.path.areEqual(segments[0].path, foreignPath)) { + if (MakerJs.path.areEqual(segments[0].path, foreignPath, .0001)) { segments[0].overlapped = true; segments[0].duplicate = true; overlappedSegments.push(segments[0]); @@ -1170,11 +1224,18 @@ var MakerJs; } if (subSegments) { segments[i].path = subSegments[0]; - var newSegment = { path: subSegments[1], overlapped: segments[i].overlapped, uniqueForeignIntersectionPoints: [] }; - if (segments[i].overlapped) { - overlappedSegments.push(newSegment); + if (subSegments[1]) { + var newSegment = { + path: subSegments[1], + pathId: segments[0].pathId, + overlapped: segments[i].overlapped, + uniqueForeignIntersectionPoints: [] + }; + if (segments[i].overlapped) { + overlappedSegments.push(newSegment); + } + segments.push(newSegment); } - segments.push(newSegment); //re-check this segment for another deep intersection i--; } @@ -1188,7 +1249,7 @@ var MakerJs; var added = 0; function addUniquePoint(pointToAdd) { for (var i = 0; i < pointArray.length; i++) { - if (MakerJs.point.areEqualRounded(pointArray[i], pointToAdd)) { + if (MakerJs.point.areEqual(pointArray[i], pointToAdd, .000000001)) { return; } } @@ -1203,7 +1264,7 @@ var MakerJs; /** * @private */ - function checkInsideForeignPath(segment, foreignPath, farPoint) { + function checkIntersectsForeignPath(segment, foreignPath, foreignPathId, farPoint) { if (farPoint === void 0) { farPoint = [7654321, 1234567]; } var origin = MakerJs.point.middle(segment.path); var lineToFarPoint = new MakerJs.paths.Line(origin, farPoint); @@ -1220,9 +1281,9 @@ var MakerJs; * @private */ function checkInsideForeignModel(segment, modelToIntersect, farPoint) { - model_1.walkPaths(modelToIntersect, function (mx, pathId2, path2) { + model.walkPaths(modelToIntersect, function (mx, pathId2, path2) { if (path2) { - checkInsideForeignPath(segment, path2, farPoint); + checkIntersectsForeignPath(segment, path2, pathId2, farPoint); } }); } @@ -1243,19 +1304,30 @@ var MakerJs; checkInsideForeignModel(segment, modelContext, farPoint); return !!segment.isInside; } - model_1.isPathInsideModel = isPathInsideModel; + model.isPathInsideModel = isPathInsideModel; + /** + * Break a model's paths everywhere they intersect with another path. + * + * @param modelToBreak The model containing paths to be broken. + * @param modelToIntersect Optional model containing paths to look for intersection, or else the modelToBreak will be used. + */ + function breakPathsAtIntersections(modelToBreak, modelToIntersect) { + breakAllPathsAtIntersections(modelToBreak, modelToIntersect || modelToBreak, false); + } + model.breakPathsAtIntersections = breakPathsAtIntersections; /** * @private */ - function breakAllPathsAtIntersections(modelToBreak, modelToIntersect, farPoint) { + function breakAllPathsAtIntersections(modelToBreak, modelToIntersect, checkIsInside, farPoint) { var crossedPaths = []; var overlappedSegments = []; - model_1.walkPaths(modelToBreak, function (modelContext, pathId1, path1) { + model.walkPaths(modelToBreak, function (modelContext, pathId1, path1) { if (!path1) return; //clone this path and make it the first segment var segment = { path: MakerJs.cloneObject(path1), + pathId: pathId1, overlapped: false, uniqueForeignIntersectionPoints: [] }; @@ -1265,14 +1337,16 @@ var MakerJs; segments: [segment] }; //keep breaking the segments anywhere they intersect with paths of the other model - model_1.walkPaths(modelToIntersect, function (mx, pathId2, path2) { - if (path2) { + model.walkPaths(modelToIntersect, function (mx, pathId2, path2) { + if (path2 && path1 !== path2) { breakAlongForeignPath(thisPath.segments, overlappedSegments, path2); } }); - //check each segment whether it is inside or outside - for (var i = 0; i < thisPath.segments.length; i++) { - checkInsideForeignModel(thisPath.segments[i], modelToIntersect); + if (checkIsInside) { + //check each segment whether it is inside or outside + for (var i = 0; i < thisPath.segments.length; i++) { + checkInsideForeignModel(thisPath.segments[i], modelToIntersect, farPoint); + } } crossedPaths.push(thisPath); }); @@ -1281,9 +1355,9 @@ var MakerJs; /** * @private */ - function checkForEqualOverlaps(crossedPathsA, crossedPathsB) { + function checkForEqualOverlaps(crossedPathsA, crossedPathsB, pointMatchingDistance) { function compareSegments(segment1, segment2) { - if (MakerJs.path.areEqual(segment1.path, segment2.path)) { + if (MakerJs.path.areEqual(segment1.path, segment2.path, pointMatchingDistance)) { segment1.duplicate = segment2.duplicate = true; } } @@ -1300,13 +1374,13 @@ var MakerJs; * @private */ function addOrDeleteSegments(crossedPath, includeInside, includeOutside, keepDuplicates) { - function addSegment(model, pathIdBase, segment) { - var id = model_1.getSimilarPathId(model, pathIdBase); - model.paths[id] = segment.path; + function addSegment(modelContext, pathIdBase, segment) { + var id = model.getSimilarPathId(modelContext, pathIdBase); + modelContext.paths[id] = segment.path; } - function checkAddSegment(model, pathIdBase, segment) { + function checkAddSegment(modelContext, pathIdBase, segment) { if (segment.isInside && includeInside || !segment.isInside && includeOutside) { - addSegment(model, pathIdBase, segment); + addSegment(modelContext, pathIdBase, segment); } } //delete the original, its segments will be added @@ -1323,7 +1397,7 @@ var MakerJs; } } /** - * Combine 2 models. The models should be originated. + * Combine 2 models. The models should be originated, and every path within each model should be part of a loop. * * @param modelA First model to combine. * @param modelB Second model to combine. @@ -1334,23 +1408,30 @@ var MakerJs; * @param keepDuplicates Flag to include paths which are duplicate in both models. * @param farPoint Optional point of reference which is outside the bounds of both models. */ - function combine(modelA, modelB, includeAInsideB, includeAOutsideB, includeBInsideA, includeBOutsideA, keepDuplicates, farPoint) { + function combine(modelA, modelB, includeAInsideB, includeAOutsideB, includeBInsideA, includeBOutsideA, options) { if (includeAInsideB === void 0) { includeAInsideB = false; } if (includeAOutsideB === void 0) { includeAOutsideB = true; } if (includeBInsideA === void 0) { includeBInsideA = false; } if (includeBOutsideA === void 0) { includeBOutsideA = true; } - if (keepDuplicates === void 0) { keepDuplicates = true; } - var pathsA = breakAllPathsAtIntersections(modelA, modelB, farPoint); - var pathsB = breakAllPathsAtIntersections(modelB, modelA, farPoint); - checkForEqualOverlaps(pathsA.overlappedSegments, pathsB.overlappedSegments); + var opts = { + trimDeadEnds: true, + pointMatchingDistance: .005 + }; + MakerJs.extendObject(opts, options); + var pathsA = breakAllPathsAtIntersections(modelA, modelB, true, opts.farPoint); + var pathsB = breakAllPathsAtIntersections(modelB, modelA, true, opts.farPoint); + checkForEqualOverlaps(pathsA.overlappedSegments, pathsB.overlappedSegments, opts.pointMatchingDistance); for (var i = 0; i < pathsA.crossedPaths.length; i++) { - addOrDeleteSegments(pathsA.crossedPaths[i], includeAInsideB, includeAOutsideB, keepDuplicates); + addOrDeleteSegments(pathsA.crossedPaths[i], includeAInsideB, includeAOutsideB, true); } for (var i = 0; i < pathsB.crossedPaths.length; i++) { - addOrDeleteSegments(pathsB.crossedPaths[i], includeBInsideA, includeBOutsideA); + addOrDeleteSegments(pathsB.crossedPaths[i], includeBInsideA, includeBOutsideA, false); + } + if (opts.trimDeadEnds) { + model.removeDeadEnds({ models: { modelA: modelA, modelB: modelB } }); } } - model_1.combine = combine; + model.combine = combine; })(model = MakerJs.model || (MakerJs.model = {})); })(MakerJs || (MakerJs = {})); var MakerJs; @@ -1488,6 +1569,10 @@ var MakerJs; function isBetweenArcAngles(angleInQuestion, arc, exclusive) { var startAngle = arc.startAngle; var endAngle = MakerJs.angle.ofArcEnd(arc); + var span = endAngle - startAngle; + startAngle = MakerJs.angle.noRevolutions(startAngle); + endAngle = startAngle + span; + angleInQuestion = MakerJs.angle.noRevolutions(angleInQuestion); //computed angles will not be negative, but the arc may have specified a negative angle, so check against one revolution forward and backward return (isBetween(angleInQuestion, startAngle, endAngle, exclusive) || isBetween(angleInQuestion, startAngle + 360, endAngle + 360, exclusive) || isBetween(angleInQuestion, startAngle - 360, endAngle - 360, exclusive)); } @@ -1502,11 +1587,11 @@ var MakerJs; */ function isBetweenPoints(pointInQuestion, line, exclusive) { for (var i = 2; i--;) { - var origin_value = MakerJs.round(line.origin[i]); - var end_value = MakerJs.round(line.end[i]); - if (origin_value == end_value) { + if (MakerJs.round(line.origin[i] - line.end[i], .000001) == 0) { continue; } + var origin_value = MakerJs.round(line.origin[i]); + var end_value = MakerJs.round(line.end[i]); if (!isBetween(MakerJs.round(pointInQuestion[i]), origin_value, end_value, exclusive)) return false; } @@ -1622,16 +1707,18 @@ var MakerJs; getExtreme(totalMeasurement.low, pathMeasurement.low, Math.min); getExtreme(totalMeasurement.high, pathMeasurement.high, Math.max); } - function measure(model, offsetOrigin) { - var newOrigin = MakerJs.point.add(model.origin, offsetOrigin); - if (model.paths) { - for (var id in model.paths) { - lowerOrHigher(newOrigin, pathExtents(model.paths[id])); + function measure(modelToMeasure, offsetOrigin) { + if (!modelToMeasure) + return; + var newOrigin = MakerJs.point.add(modelToMeasure.origin, offsetOrigin); + if (modelToMeasure.paths) { + for (var id in modelToMeasure.paths) { + lowerOrHigher(newOrigin, pathExtents(modelToMeasure.paths[id])); } } - if (model.models) { - for (var id in model.models) { - measure(model.models[id], newOrigin); + if (modelToMeasure.models) { + for (var id in modelToMeasure.models) { + measure(modelToMeasure.models[id], newOrigin); } } } @@ -1700,12 +1787,18 @@ var MakerJs; var newOffset = MakerJs.point.add((this.fixPoint ? this.fixPoint(modelToExport.origin) : modelToExport.origin), offset); if (modelToExport.paths) { for (var id in modelToExport.paths) { - this.exportPath(id, modelToExport.paths[id], newOffset, modelToExport.layer); + var currPath = modelToExport.paths[id]; + if (!currPath) + continue; + this.exportPath(id, currPath, newOffset, modelToExport.layer); } } if (modelToExport.models) { for (var id in modelToExport.models) { - this.exportModel(id, modelToExport.models[id], newOffset); + var currModel = modelToExport.models[id]; + if (!currModel) + continue; + this.exportModel(id, currModel, newOffset); } } if (this.endModel) { @@ -2087,14 +2180,14 @@ var MakerJs; * @private */ function getSlope(line) { - var dx = MakerJs.round(line.end[0] - line.origin[0]); - if (dx == 0) { + var dx = line.end[0] - line.origin[0]; + if (MakerJs.round(dx) == 0) { return { line: line, hasSlope: false }; } - var dy = MakerJs.round(line.end[1] - line.origin[1]); + var dy = line.end[1] - line.origin[1]; var slope = dy / dx; var yIntercept = line.origin[1] - slope * line.origin[0]; return { @@ -2117,13 +2210,13 @@ var MakerJs; */ function checkAngleOverlap(arc1, arc2, options) { var pointsOfIntersection = []; - function checkAngles(index, a, b) { + function checkAngles(a, b) { function checkAngle(n) { return MakerJs.measure.isBetweenArcAngles(n, a, options.excludeTangents); } return checkAngle(b.startAngle) || checkAngle(b.endAngle); } - if (checkAngles(0, arc1, arc2) || checkAngles(1, arc2, arc1) || (arc1.startAngle == arc2.startAngle && arc1.endAngle == arc2.endAngle)) { + if (checkAngles(arc1, arc2) || checkAngles(arc2, arc1) || (arc1.startAngle == arc2.startAngle && arc1.endAngle == arc2.endAngle)) { options.out_AreOverlapped = true; } } @@ -2156,15 +2249,15 @@ var MakerJs; var slope2 = getSlope(line2); if (!slope1.hasSlope && !slope2.hasSlope) { //lines are both vertical, see if x are the same - if (slope1.line.origin[0] == slope2.line.origin[0]) { + if (MakerJs.round(slope1.line.origin[0] - slope2.line.origin[0]) == 0) { //check for overlap checkLineOverlap(line1, line2, options); } return null; } - if (slope1.hasSlope && slope2.hasSlope && (slope1.slope == slope2.slope)) { + if (slope1.hasSlope && slope2.hasSlope && (MakerJs.round(slope1.slope - slope2.slope, .00001) == 0)) { //lines are parallel, but not vertical, see if y-intercept is the same - if (slope1.yIntercept == slope2.yIntercept) { + if (MakerJs.round(slope1.yIntercept - slope2.yIntercept, .00001) == 0) { //check for overlap checkLineOverlap(line1, line2, options); } @@ -2206,21 +2299,22 @@ var MakerJs; } //line is horizontal, get the y value from any point var lineY = MakerJs.round(clonedLine.origin[1]); + var lineYabs = Math.abs(lineY); //if y is greater than radius, there is no intersection - if (lineY > radius) { + if (lineYabs > radius) { return null; } var anglesOfIntersection = []; //if horizontal Y is the same as the radius, we know it's 90 degrees - if (lineY == radius) { + if (lineYabs == radius) { if (options.excludeTangents) { return null; } - anglesOfIntersection.push(unRotate(90)); + anglesOfIntersection.push(unRotate(lineY > 0 ? 90 : 270)); } else { function intersectionBetweenEndpoints(x, angleOfX) { - if (MakerJs.measure.isBetween(x, clonedLine.origin[0], clonedLine.end[0], options.excludeTangents)) { + if (MakerJs.measure.isBetween(MakerJs.round(x), MakerJs.round(clonedLine.origin[0]), MakerJs.round(clonedLine.end[0]), options.excludeTangents)) { anglesOfIntersection.push(unRotate(angleOfX)); } } @@ -2242,7 +2336,7 @@ var MakerJs; */ function circleToCircle(circle1, circle2, options) { //see if circles are the same - if (circle1.radius == circle2.radius && MakerJs.point.areEqual(circle1.origin, circle2.origin)) { + if (circle1.radius == circle2.radius && MakerJs.point.areEqual(circle1.origin, circle2.origin, .0001)) { options.out_AreOverlapped = true; return null; } @@ -2274,14 +2368,14 @@ var MakerJs; return null; } //see if circles are tangent interior - if (c2.radius - x == c1.radius) { + if (MakerJs.round(c2.radius - x - c1.radius) == 0) { if (options.excludeTangents) { return null; } return [[unRotate(180)], [unRotate(180)]]; } //see if circles are tangent exterior - if (x - c2.radius == c1.radius) { + if (MakerJs.round(x - c2.radius - c1.radius) == 0) { if (options.excludeTangents) { return null; } @@ -2328,7 +2422,7 @@ var MakerJs; /** * @private */ - function getMatchingPointProperties(path1, path2) { + function getMatchingPointProperties(path1, path2, options) { var path1Properties = getPointProperties(path1); var path2Properties = getPointProperties(path2); var result = null; @@ -2342,7 +2436,7 @@ var MakerJs; }; } function check(i1, i2) { - if (MakerJs.point.areEqualRounded(path1Properties[i1].point, path2Properties[i2].point)) { + if (MakerJs.point.areEqual(path1Properties[i1].point, path2Properties[i2].point, .0001)) { result = [ makeMatch(path1, path1Properties, i1), makeMatch(path2, path2Properties, i2) @@ -2366,7 +2460,7 @@ var MakerJs; return false; } properties[i].shardPoint = circleIntersection.intersectionPoints[0]; - if (MakerJs.point.areEqualRounded(properties[i].point, circleIntersection.intersectionPoints[0], options.accuracy)) { + if (MakerJs.point.areEqual(properties[i].point, circleIntersection.intersectionPoints[0], .0001)) { if (circleIntersection.intersectionPoints.length > 1) { properties[i].shardPoint = circleIntersection.intersectionPoints[1]; } @@ -2518,11 +2612,11 @@ var MakerJs; function dogbone(line1, line2, filletRadius, options) { if (MakerJs.isPathLine(line1) && MakerJs.isPathLine(line2) && filletRadius && filletRadius > 0) { var opts = { - accuracy: .0001 + pointMatchingDistance: .005 }; MakerJs.extendObject(opts, options); //first find the common point - var commonProperty = getMatchingPointProperties(line1, line2); + var commonProperty = getMatchingPointProperties(line1, line2, options); if (commonProperty) { //get the ratio comparison of the two lines var ratio = getLineRatio([line1, line2]); @@ -2569,11 +2663,11 @@ var MakerJs; function fillet(path1, path2, filletRadius, options) { if (path1 && path2 && filletRadius && filletRadius > 0) { var opts = { - accuracy: .0001 + pointMatchingDistance: .005 }; MakerJs.extendObject(opts, options); //first find the common point - var commonProperty = getMatchingPointProperties(path1, path2); + var commonProperty = getMatchingPointProperties(path1, path2, options); if (commonProperty) { //since arcs can curl beyond, we need a local reference point. //An intersection with a circle of the same radius as the desired fillet should suffice. @@ -2675,6 +2769,34 @@ var MakerJs; (function (MakerJs) { var model; (function (model) { + /** + * @private + */ + var PointMap = (function () { + function PointMap(matchingDistance) { + if (matchingDistance === void 0) { matchingDistance = .001; } + this.matchingDistance = matchingDistance; + this.list = []; + } + PointMap.prototype.add = function (pointToAdd, item) { + this.list.push({ averagePoint: pointToAdd, item: item }); + }; + PointMap.prototype.find = function (pointToFind, saveAverage) { + for (var i = 0; i < this.list.length; i++) { + var item = this.list[i]; + var distance = MakerJs.measure.pointDistance(pointToFind, item.averagePoint); + if (distance <= this.matchingDistance) { + if (saveAverage) { + item.averagePoint = MakerJs.point.average(item.averagePoint, pointToFind); + } + return item.item; + } + } + return null; + }; + return PointMap; + })(); + model.PointMap = PointMap; /** * @private */ @@ -2721,7 +2843,7 @@ var MakerJs; while (true) { var currPath = currLink.path; currPath.reversed = currLink.reversed; - var id = model.getSimilarPathId(loopModel, currPath.primePathId); + var id = model.getSimilarPathId(loopModel, currPath.pathId); loopModel.paths[id] = currPath; if (!connections[currLink.nextConnection]) break; @@ -2751,11 +2873,11 @@ var MakerJs; var connections = {}; var result = { models: {} }; var opts = { - accuracy: .0001 + pointMatchingDistance: .005 }; MakerJs.extendObject(opts, options); function getLinkedPathsOnConnectionPoint(p) { - var serializedPoint = MakerJs.point.serialize(p, opts.accuracy); + var serializedPoint = MakerJs.point.serialize(p, .0001); //TODO convert to pointmap if (!(serializedPoint in connections)) { connections[serializedPoint] = []; } @@ -2774,14 +2896,15 @@ var MakerJs; } return result.models[id]; } + //todo: remove dead ends first model.originate(modelContext); //find loops by looking at all paths in this model model.walkPaths(modelContext, function (modelContext, pathId, pathContext) { if (!pathContext) return; var safePath = MakerJs.cloneObject(pathContext); - safePath.primePathId = pathId; - safePath.primeModel = modelContext; + safePath.pathId = pathId; + safePath.modelContext = modelContext; //circles are loops by nature if (safePath.type == MakerJs.pathType.Circle) { var loopModel = { @@ -2797,7 +2920,7 @@ var MakerJs; for (var i = 2; i--;) { var linkedPath = { path: safePath, - nextConnection: MakerJs.point.serialize(safePath.endPoints[1 - i], opts.accuracy), + nextConnection: MakerJs.point.serialize(safePath.endPoints[1 - i], .0001), reversed: i != 0 }; getLinkedPathsOnConnectionPoint(safePath.endPoints[i]).push(linkedPath); @@ -2837,13 +2960,110 @@ var MakerJs; function detachLoop(loopToDetach) { for (var id in loopToDetach.paths) { var pathDirectionalWithOriginalContext = loopToDetach.paths[id]; - var primeModel = pathDirectionalWithOriginalContext.primeModel; - if (primeModel && primeModel.paths && pathDirectionalWithOriginalContext.primePathId) { - delete primeModel.paths[pathDirectionalWithOriginalContext.primePathId]; + var primeModel = pathDirectionalWithOriginalContext.modelContext; + if (primeModel && primeModel.paths && pathDirectionalWithOriginalContext.pathId) { + delete primeModel.paths[pathDirectionalWithOriginalContext.pathId]; } } } model.detachLoop = detachLoop; + /** + * @private + */ + var DeadEndFinder = (function () { + function DeadEndFinder(pointMatchingDistance) { + this.pointMatchingDistance = pointMatchingDistance; + this.pointMap = new PointMap(pointMatchingDistance); + } + DeadEndFinder.prototype.addPathRef = function (p, pathRef) { + var found = this.pointMap.find(p, true); + if (found) { + found.push(pathRef); + } + else { + this.pointMap.add(p, [pathRef]); + } + }; + DeadEndFinder.prototype.removeMatchingPathRefs = function (a, b) { + //see if any are the same in each array + for (var ai = 0; ai < a.length; ai++) { + for (var bi = 0; bi < b.length; bi++) { + if (a[ai] === b[bi]) { + var pathRef = a[ai]; + a.splice(ai, 1); + b.splice(bi, 1); + return pathRef; + } + } + } + return null; + }; + DeadEndFinder.prototype.removePathRef = function (pathRef) { + var _this = this; + var removePath = function (p) { + var pathRefs = _this.pointMap.find(p, false); + for (var i = 0; i < pathRefs.length; i++) { + if (pathRefs[i] === pathRef) { + pathRefs.splice(i, 1); + return; + } + } + }; + for (var i = 2; i--;) { + removePath(pathRef.endPoints[i]); + } + }; + DeadEndFinder.prototype.removeDeadEnd = function () { + var found = false; + var oddPathRefs = null; + for (var i = 0; i < this.pointMap.list.length; i++) { + var pathRefs = this.pointMap.list[i].item; + if (pathRefs.length % 2 == 0) + continue; + if (pathRefs.length == 1) { + var pathRef = pathRefs[0]; + this.removePathRef(pathRef); + delete pathRef.modelContext.paths[pathRef.pathId]; + found = true; + } + else { + if (!oddPathRefs) { + //save this for another iteration + oddPathRefs = pathRefs; + } + else { + //compare with the saved + var pathRef = this.removeMatchingPathRefs(oddPathRefs, pathRefs); + if (pathRef) { + delete pathRef.modelContext.paths[pathRef.pathId]; + found = true; + //clear the saved + oddPathRefs = null; + } + } + } + } + return found; + }; + return DeadEndFinder; + })(); + function removeDeadEnds(modelContext, pointMatchingDistance) { + if (pointMatchingDistance === void 0) { pointMatchingDistance = .005; } + var serializedPointAccuracy = .0001; + var deadEndFinder = new DeadEndFinder(pointMatchingDistance); + model.walkPaths(modelContext, function (modelContext, pathId, pathContext) { + var endPoints = MakerJs.point.fromPathEnds(pathContext); + if (!endPoints) + return; + var pathRef = { modelContext: modelContext, pathId: pathId, endPoints: endPoints }; + for (var i = 2; i--;) { + deadEndFinder.addPathRef(endPoints[i], pathRef); + } + }); + while (deadEndFinder.removeDeadEnd()) + ; + } + model.removeDeadEnds = removeDeadEnds; })(model = MakerJs.model || (MakerJs.model = {})); })(MakerJs || (MakerJs = {})); var MakerJs; @@ -3056,7 +3276,7 @@ var MakerJs; var depthModel; var opts = { extrusion: 1, - accuracy: .0001 + pointMatchingDistance: .005 }; MakerJs.extendObject(opts, options); var loops = MakerJs.model.findLoops(modelToExport, opts); diff --git a/target/js/node.maker.js b/target/js/node.maker.js index c2414bf35..d30624d6e 100644 --- a/target/js/node.maker.js +++ b/target/js/node.maker.js @@ -169,10 +169,12 @@ var MakerJs; * @param b Second angle. * @returns true if angles are the same, false if they are not */ - function areEqual(angle1, angle2) { - var a1 = noRevolutions(MakerJs.round(angle1)); - var a2 = noRevolutions(MakerJs.round(angle2)); - return a1 == a2 || a1 + 360 == a2 || a1 - 360 == a2; + function areEqual(angle1, angle2, accuracy) { + if (accuracy === void 0) { accuracy = .0001; } + var a1 = noRevolutions(angle1); + var a2 = noRevolutions(angle2); + var d = noRevolutions(MakerJs.round(a2 - a1, accuracy)); + return d == 0; } angle.areEqual = areEqual; /** @@ -322,8 +324,14 @@ var MakerJs; * @param b Second point. * @returns true if points are the same, false if they are not */ - function areEqual(a, b) { - return a[0] == b[0] && a[1] == b[1]; + function areEqual(a, b, withinDistance) { + if (!withinDistance) { + return a[0] == b[0] && a[1] == b[1]; + } + else { + var distance = MakerJs.measure.pointDistance(a, b); + return distance <= withinDistance; + } } point.areEqual = areEqual; /** @@ -339,6 +347,20 @@ var MakerJs; return MakerJs.round(a[0], accuracy) == MakerJs.round(b[0], accuracy) && MakerJs.round(a[1], accuracy) == MakerJs.round(b[1], accuracy); } point.areEqualRounded = areEqualRounded; + /** + * Get the average of two points. + * + * @param a First point. + * @param b Second point. + * @returns New point object which is the average of a and b. + */ + function average(a, b) { + function avg(i) { + return (a[i] + b[i]) / 2; + } + return [avg(0), avg(1)]; + } + point.average = average; /** * Clone a point into a new point. * @@ -506,7 +528,7 @@ var MakerJs; var pointAngleInRadians = MakerJs.angle.ofPointInRadians(rotationOrigin, pointToRotate); var d = MakerJs.measure.pointDistance(rotationOrigin, pointToRotate); var rotatedPoint = fromPolar(pointAngleInRadians + MakerJs.angle.toRadians(angleInDegrees), d); - return rounded(add(rotationOrigin, rotatedPoint)); + return add(rotationOrigin, rotatedPoint); } point.rotate = rotate; /** @@ -533,7 +555,7 @@ var MakerJs; */ function serialize(pointContext, accuracy) { var roundedPoint = rounded(pointContext, accuracy); - return roundedPoint[0] + ',' + roundedPoint[1]; + return JSON.stringify(roundedPoint); } point.serialize = serialize; /** @@ -567,14 +589,15 @@ var MakerJs; * @private */ var pathAreEqualMap = {}; - pathAreEqualMap[MakerJs.pathType.Line] = function (line1, line2) { - return (MakerJs.point.areEqual(line1.origin, line2.origin) && MakerJs.point.areEqual(line1.end, line2.end)) || (MakerJs.point.areEqual(line1.origin, line2.end) && MakerJs.point.areEqual(line1.end, line2.origin)); + pathAreEqualMap[MakerJs.pathType.Line] = function (line1, line2, withinPointDistance) { + return (MakerJs.point.areEqual(line1.origin, line2.origin, withinPointDistance) && MakerJs.point.areEqual(line1.end, line2.end, withinPointDistance)) + || (MakerJs.point.areEqual(line1.origin, line2.end, withinPointDistance) && MakerJs.point.areEqual(line1.end, line2.origin, withinPointDistance)); }; - pathAreEqualMap[MakerJs.pathType.Circle] = function (circle1, circle2) { - return MakerJs.point.areEqual(circle1.origin, circle2.origin) && circle1.radius == circle2.radius; + pathAreEqualMap[MakerJs.pathType.Circle] = function (circle1, circle2, withinPointDistance) { + return MakerJs.point.areEqual(circle1.origin, circle2.origin, withinPointDistance) && circle1.radius == circle2.radius; }; - pathAreEqualMap[MakerJs.pathType.Arc] = function (arc1, arc2) { - return pathAreEqualMap[MakerJs.pathType.Circle](arc1, arc2) && MakerJs.angle.areEqual(arc1.startAngle, arc2.startAngle) && MakerJs.angle.areEqual(arc1.endAngle, arc2.endAngle); + pathAreEqualMap[MakerJs.pathType.Arc] = function (arc1, arc2, withinPointDistance) { + return pathAreEqualMap[MakerJs.pathType.Circle](arc1, arc2, withinPointDistance) && MakerJs.angle.areEqual(arc1.startAngle, arc2.startAngle) && MakerJs.angle.areEqual(arc1.endAngle, arc2.endAngle); }; /** * Find out if two paths are equal. @@ -583,12 +606,12 @@ var MakerJs; * @param b Second path. * @returns true if paths are the same, false if they are not */ - function areEqual(path1, path2) { + function areEqual(path1, path2, withinPointDistance) { var result = false; if (path1.type == path2.type) { var fn = pathAreEqualMap[path1.type]; if (fn) { - result = fn(path1, path2); + result = fn(path1, path2, withinPointDistance); } } return result; @@ -687,8 +710,8 @@ var MakerJs; line.end = MakerJs.point.rotate(line.end, angleInDegrees, rotationOrigin); }; map[MakerJs.pathType.Arc] = function (arc) { - arc.startAngle += angleInDegrees; - arc.endAngle += angleInDegrees; + arc.startAngle = MakerJs.angle.noRevolutions(arc.startAngle + angleInDegrees); + arc.endAngle = MakerJs.angle.noRevolutions(arc.endAngle + angleInDegrees); }; pathToRotate.origin = MakerJs.point.rotate(pathToRotate.origin, angleInDegrees, rotationOrigin); var fn = map[pathToRotate.type]; @@ -740,12 +763,13 @@ var MakerJs; return null; } function getAngleStrictlyBetweenArcAngles() { - var endAngle = MakerJs.angle.ofArcEnd(arc); + var startAngle = MakerJs.angle.noRevolutions(arc.startAngle); + var endAngle = startAngle + MakerJs.angle.ofArcEnd(arc) - arc.startAngle; var tries = [0, 1, -1]; for (var i = 0; i < tries.length; i++) { var add = +360 * tries[i]; - if (MakerJs.measure.isBetween(angleAtBreakPoint + add, arc.startAngle, endAngle, true)) { - return angleAtBreakPoint + add; + if (MakerJs.measure.isBetween(angleAtBreakPoint + add, startAngle, endAngle, true)) { + return arc.startAngle + angleAtBreakPoint + add - startAngle; } } return null; @@ -916,13 +940,33 @@ var MakerJs; return count; } model.countChildModels = countChildModels; + /** + * Get an unused id in the models map with the same prefix. + * + * @param modelContext The model containing the models map. + * @param modelId The id to use directly (if unused), or as a prefix. + */ + function getSimilarModelId(modelContext, modelId) { + if (!modelContext.models) + return modelId; + var i = 0; + var newModelId = modelId; + while (newModelId in modelContext.models) { + i++; + newModelId = modelId + '_' + i; + } + return newModelId; + } + model.getSimilarModelId = getSimilarModelId; /** * Get an unused id in the paths map with the same prefix. * * @param modelContext The model containing the paths map. - * @param pathId The pathId to use directly (if unused), or as a prefix. + * @param pathId The id to use directly (if unused), or as a prefix. */ function getSimilarPathId(modelContext, pathId) { + if (!modelContext.paths) + return pathId; var i = 0; var newPathId = pathId; while (newPathId in modelContext.paths) { @@ -939,6 +983,8 @@ var MakerJs; * @param origin Optional offset reference point. */ function originate(modelToOriginate, origin) { + if (!modelToOriginate) + return; var newOrigin = MakerJs.point.add(modelToOriginate.origin, origin); if (modelToOriginate.paths) { for (var id in modelToOriginate.paths) { @@ -1100,11 +1146,15 @@ var MakerJs; function walkPaths(modelContext, callback) { if (modelContext.paths) { for (var pathId in modelContext.paths) { + if (!modelContext.paths[pathId]) + continue; callback(modelContext, pathId, modelContext.paths[pathId]); } } if (modelContext.models) { for (var id in modelContext.models) { + if (!modelContext.models[id]) + continue; walkPaths(modelContext.models[id], callback); } } @@ -1115,29 +1165,33 @@ var MakerJs; var MakerJs; (function (MakerJs) { var model; - (function (model_1) { + (function (model) { /** * @private */ function getNonZeroSegments(pathToSegment, breakPoint) { + var segmentType = pathToSegment.type; var segment1 = MakerJs.cloneObject(pathToSegment); var segment2 = MakerJs.path.breakAtPoint(segment1, breakPoint); if (segment2) { var segments = [segment1, segment2]; for (var i = 2; i--;) { - if (MakerJs.round(MakerJs.measure.pathLength(segments[i]), .00001) == 0) { + if (MakerJs.round(MakerJs.measure.pathLength(segments[i]), .0001) == 0) { return null; } } return segments; } + else if (segmentType == MakerJs.pathType.Circle) { + return [segment1]; + } return null; } /** * @private */ function breakAlongForeignPath(segments, overlappedSegments, foreignPath) { - if (MakerJs.path.areEqual(segments[0].path, foreignPath)) { + if (MakerJs.path.areEqual(segments[0].path, foreignPath, .0001)) { segments[0].overlapped = true; segments[0].duplicate = true; overlappedSegments.push(segments[0]); @@ -1169,11 +1223,18 @@ var MakerJs; } if (subSegments) { segments[i].path = subSegments[0]; - var newSegment = { path: subSegments[1], overlapped: segments[i].overlapped, uniqueForeignIntersectionPoints: [] }; - if (segments[i].overlapped) { - overlappedSegments.push(newSegment); + if (subSegments[1]) { + var newSegment = { + path: subSegments[1], + pathId: segments[0].pathId, + overlapped: segments[i].overlapped, + uniqueForeignIntersectionPoints: [] + }; + if (segments[i].overlapped) { + overlappedSegments.push(newSegment); + } + segments.push(newSegment); } - segments.push(newSegment); //re-check this segment for another deep intersection i--; } @@ -1187,7 +1248,7 @@ var MakerJs; var added = 0; function addUniquePoint(pointToAdd) { for (var i = 0; i < pointArray.length; i++) { - if (MakerJs.point.areEqualRounded(pointArray[i], pointToAdd)) { + if (MakerJs.point.areEqual(pointArray[i], pointToAdd, .000000001)) { return; } } @@ -1202,7 +1263,7 @@ var MakerJs; /** * @private */ - function checkInsideForeignPath(segment, foreignPath, farPoint) { + function checkIntersectsForeignPath(segment, foreignPath, foreignPathId, farPoint) { if (farPoint === void 0) { farPoint = [7654321, 1234567]; } var origin = MakerJs.point.middle(segment.path); var lineToFarPoint = new MakerJs.paths.Line(origin, farPoint); @@ -1219,9 +1280,9 @@ var MakerJs; * @private */ function checkInsideForeignModel(segment, modelToIntersect, farPoint) { - model_1.walkPaths(modelToIntersect, function (mx, pathId2, path2) { + model.walkPaths(modelToIntersect, function (mx, pathId2, path2) { if (path2) { - checkInsideForeignPath(segment, path2, farPoint); + checkIntersectsForeignPath(segment, path2, pathId2, farPoint); } }); } @@ -1242,19 +1303,30 @@ var MakerJs; checkInsideForeignModel(segment, modelContext, farPoint); return !!segment.isInside; } - model_1.isPathInsideModel = isPathInsideModel; + model.isPathInsideModel = isPathInsideModel; + /** + * Break a model's paths everywhere they intersect with another path. + * + * @param modelToBreak The model containing paths to be broken. + * @param modelToIntersect Optional model containing paths to look for intersection, or else the modelToBreak will be used. + */ + function breakPathsAtIntersections(modelToBreak, modelToIntersect) { + breakAllPathsAtIntersections(modelToBreak, modelToIntersect || modelToBreak, false); + } + model.breakPathsAtIntersections = breakPathsAtIntersections; /** * @private */ - function breakAllPathsAtIntersections(modelToBreak, modelToIntersect, farPoint) { + function breakAllPathsAtIntersections(modelToBreak, modelToIntersect, checkIsInside, farPoint) { var crossedPaths = []; var overlappedSegments = []; - model_1.walkPaths(modelToBreak, function (modelContext, pathId1, path1) { + model.walkPaths(modelToBreak, function (modelContext, pathId1, path1) { if (!path1) return; //clone this path and make it the first segment var segment = { path: MakerJs.cloneObject(path1), + pathId: pathId1, overlapped: false, uniqueForeignIntersectionPoints: [] }; @@ -1264,14 +1336,16 @@ var MakerJs; segments: [segment] }; //keep breaking the segments anywhere they intersect with paths of the other model - model_1.walkPaths(modelToIntersect, function (mx, pathId2, path2) { - if (path2) { + model.walkPaths(modelToIntersect, function (mx, pathId2, path2) { + if (path2 && path1 !== path2) { breakAlongForeignPath(thisPath.segments, overlappedSegments, path2); } }); - //check each segment whether it is inside or outside - for (var i = 0; i < thisPath.segments.length; i++) { - checkInsideForeignModel(thisPath.segments[i], modelToIntersect); + if (checkIsInside) { + //check each segment whether it is inside or outside + for (var i = 0; i < thisPath.segments.length; i++) { + checkInsideForeignModel(thisPath.segments[i], modelToIntersect, farPoint); + } } crossedPaths.push(thisPath); }); @@ -1280,9 +1354,9 @@ var MakerJs; /** * @private */ - function checkForEqualOverlaps(crossedPathsA, crossedPathsB) { + function checkForEqualOverlaps(crossedPathsA, crossedPathsB, pointMatchingDistance) { function compareSegments(segment1, segment2) { - if (MakerJs.path.areEqual(segment1.path, segment2.path)) { + if (MakerJs.path.areEqual(segment1.path, segment2.path, pointMatchingDistance)) { segment1.duplicate = segment2.duplicate = true; } } @@ -1299,13 +1373,13 @@ var MakerJs; * @private */ function addOrDeleteSegments(crossedPath, includeInside, includeOutside, keepDuplicates) { - function addSegment(model, pathIdBase, segment) { - var id = model_1.getSimilarPathId(model, pathIdBase); - model.paths[id] = segment.path; + function addSegment(modelContext, pathIdBase, segment) { + var id = model.getSimilarPathId(modelContext, pathIdBase); + modelContext.paths[id] = segment.path; } - function checkAddSegment(model, pathIdBase, segment) { + function checkAddSegment(modelContext, pathIdBase, segment) { if (segment.isInside && includeInside || !segment.isInside && includeOutside) { - addSegment(model, pathIdBase, segment); + addSegment(modelContext, pathIdBase, segment); } } //delete the original, its segments will be added @@ -1322,7 +1396,7 @@ var MakerJs; } } /** - * Combine 2 models. The models should be originated. + * Combine 2 models. The models should be originated, and every path within each model should be part of a loop. * * @param modelA First model to combine. * @param modelB Second model to combine. @@ -1333,23 +1407,30 @@ var MakerJs; * @param keepDuplicates Flag to include paths which are duplicate in both models. * @param farPoint Optional point of reference which is outside the bounds of both models. */ - function combine(modelA, modelB, includeAInsideB, includeAOutsideB, includeBInsideA, includeBOutsideA, keepDuplicates, farPoint) { + function combine(modelA, modelB, includeAInsideB, includeAOutsideB, includeBInsideA, includeBOutsideA, options) { if (includeAInsideB === void 0) { includeAInsideB = false; } if (includeAOutsideB === void 0) { includeAOutsideB = true; } if (includeBInsideA === void 0) { includeBInsideA = false; } if (includeBOutsideA === void 0) { includeBOutsideA = true; } - if (keepDuplicates === void 0) { keepDuplicates = true; } - var pathsA = breakAllPathsAtIntersections(modelA, modelB, farPoint); - var pathsB = breakAllPathsAtIntersections(modelB, modelA, farPoint); - checkForEqualOverlaps(pathsA.overlappedSegments, pathsB.overlappedSegments); + var opts = { + trimDeadEnds: true, + pointMatchingDistance: .005 + }; + MakerJs.extendObject(opts, options); + var pathsA = breakAllPathsAtIntersections(modelA, modelB, true, opts.farPoint); + var pathsB = breakAllPathsAtIntersections(modelB, modelA, true, opts.farPoint); + checkForEqualOverlaps(pathsA.overlappedSegments, pathsB.overlappedSegments, opts.pointMatchingDistance); for (var i = 0; i < pathsA.crossedPaths.length; i++) { - addOrDeleteSegments(pathsA.crossedPaths[i], includeAInsideB, includeAOutsideB, keepDuplicates); + addOrDeleteSegments(pathsA.crossedPaths[i], includeAInsideB, includeAOutsideB, true); } for (var i = 0; i < pathsB.crossedPaths.length; i++) { - addOrDeleteSegments(pathsB.crossedPaths[i], includeBInsideA, includeBOutsideA); + addOrDeleteSegments(pathsB.crossedPaths[i], includeBInsideA, includeBOutsideA, false); + } + if (opts.trimDeadEnds) { + model.removeDeadEnds({ models: { modelA: modelA, modelB: modelB } }); } } - model_1.combine = combine; + model.combine = combine; })(model = MakerJs.model || (MakerJs.model = {})); })(MakerJs || (MakerJs = {})); var MakerJs; @@ -1487,6 +1568,10 @@ var MakerJs; function isBetweenArcAngles(angleInQuestion, arc, exclusive) { var startAngle = arc.startAngle; var endAngle = MakerJs.angle.ofArcEnd(arc); + var span = endAngle - startAngle; + startAngle = MakerJs.angle.noRevolutions(startAngle); + endAngle = startAngle + span; + angleInQuestion = MakerJs.angle.noRevolutions(angleInQuestion); //computed angles will not be negative, but the arc may have specified a negative angle, so check against one revolution forward and backward return (isBetween(angleInQuestion, startAngle, endAngle, exclusive) || isBetween(angleInQuestion, startAngle + 360, endAngle + 360, exclusive) || isBetween(angleInQuestion, startAngle - 360, endAngle - 360, exclusive)); } @@ -1501,11 +1586,11 @@ var MakerJs; */ function isBetweenPoints(pointInQuestion, line, exclusive) { for (var i = 2; i--;) { - var origin_value = MakerJs.round(line.origin[i]); - var end_value = MakerJs.round(line.end[i]); - if (origin_value == end_value) { + if (MakerJs.round(line.origin[i] - line.end[i], .000001) == 0) { continue; } + var origin_value = MakerJs.round(line.origin[i]); + var end_value = MakerJs.round(line.end[i]); if (!isBetween(MakerJs.round(pointInQuestion[i]), origin_value, end_value, exclusive)) return false; } @@ -1621,16 +1706,18 @@ var MakerJs; getExtreme(totalMeasurement.low, pathMeasurement.low, Math.min); getExtreme(totalMeasurement.high, pathMeasurement.high, Math.max); } - function measure(model, offsetOrigin) { - var newOrigin = MakerJs.point.add(model.origin, offsetOrigin); - if (model.paths) { - for (var id in model.paths) { - lowerOrHigher(newOrigin, pathExtents(model.paths[id])); + function measure(modelToMeasure, offsetOrigin) { + if (!modelToMeasure) + return; + var newOrigin = MakerJs.point.add(modelToMeasure.origin, offsetOrigin); + if (modelToMeasure.paths) { + for (var id in modelToMeasure.paths) { + lowerOrHigher(newOrigin, pathExtents(modelToMeasure.paths[id])); } } - if (model.models) { - for (var id in model.models) { - measure(model.models[id], newOrigin); + if (modelToMeasure.models) { + for (var id in modelToMeasure.models) { + measure(modelToMeasure.models[id], newOrigin); } } } @@ -1699,12 +1786,18 @@ var MakerJs; var newOffset = MakerJs.point.add((this.fixPoint ? this.fixPoint(modelToExport.origin) : modelToExport.origin), offset); if (modelToExport.paths) { for (var id in modelToExport.paths) { - this.exportPath(id, modelToExport.paths[id], newOffset, modelToExport.layer); + var currPath = modelToExport.paths[id]; + if (!currPath) + continue; + this.exportPath(id, currPath, newOffset, modelToExport.layer); } } if (modelToExport.models) { for (var id in modelToExport.models) { - this.exportModel(id, modelToExport.models[id], newOffset); + var currModel = modelToExport.models[id]; + if (!currModel) + continue; + this.exportModel(id, currModel, newOffset); } } if (this.endModel) { @@ -2086,14 +2179,14 @@ var MakerJs; * @private */ function getSlope(line) { - var dx = MakerJs.round(line.end[0] - line.origin[0]); - if (dx == 0) { + var dx = line.end[0] - line.origin[0]; + if (MakerJs.round(dx) == 0) { return { line: line, hasSlope: false }; } - var dy = MakerJs.round(line.end[1] - line.origin[1]); + var dy = line.end[1] - line.origin[1]; var slope = dy / dx; var yIntercept = line.origin[1] - slope * line.origin[0]; return { @@ -2116,13 +2209,13 @@ var MakerJs; */ function checkAngleOverlap(arc1, arc2, options) { var pointsOfIntersection = []; - function checkAngles(index, a, b) { + function checkAngles(a, b) { function checkAngle(n) { return MakerJs.measure.isBetweenArcAngles(n, a, options.excludeTangents); } return checkAngle(b.startAngle) || checkAngle(b.endAngle); } - if (checkAngles(0, arc1, arc2) || checkAngles(1, arc2, arc1) || (arc1.startAngle == arc2.startAngle && arc1.endAngle == arc2.endAngle)) { + if (checkAngles(arc1, arc2) || checkAngles(arc2, arc1) || (arc1.startAngle == arc2.startAngle && arc1.endAngle == arc2.endAngle)) { options.out_AreOverlapped = true; } } @@ -2155,15 +2248,15 @@ var MakerJs; var slope2 = getSlope(line2); if (!slope1.hasSlope && !slope2.hasSlope) { //lines are both vertical, see if x are the same - if (slope1.line.origin[0] == slope2.line.origin[0]) { + if (MakerJs.round(slope1.line.origin[0] - slope2.line.origin[0]) == 0) { //check for overlap checkLineOverlap(line1, line2, options); } return null; } - if (slope1.hasSlope && slope2.hasSlope && (slope1.slope == slope2.slope)) { + if (slope1.hasSlope && slope2.hasSlope && (MakerJs.round(slope1.slope - slope2.slope, .00001) == 0)) { //lines are parallel, but not vertical, see if y-intercept is the same - if (slope1.yIntercept == slope2.yIntercept) { + if (MakerJs.round(slope1.yIntercept - slope2.yIntercept, .00001) == 0) { //check for overlap checkLineOverlap(line1, line2, options); } @@ -2205,21 +2298,22 @@ var MakerJs; } //line is horizontal, get the y value from any point var lineY = MakerJs.round(clonedLine.origin[1]); + var lineYabs = Math.abs(lineY); //if y is greater than radius, there is no intersection - if (lineY > radius) { + if (lineYabs > radius) { return null; } var anglesOfIntersection = []; //if horizontal Y is the same as the radius, we know it's 90 degrees - if (lineY == radius) { + if (lineYabs == radius) { if (options.excludeTangents) { return null; } - anglesOfIntersection.push(unRotate(90)); + anglesOfIntersection.push(unRotate(lineY > 0 ? 90 : 270)); } else { function intersectionBetweenEndpoints(x, angleOfX) { - if (MakerJs.measure.isBetween(x, clonedLine.origin[0], clonedLine.end[0], options.excludeTangents)) { + if (MakerJs.measure.isBetween(MakerJs.round(x), MakerJs.round(clonedLine.origin[0]), MakerJs.round(clonedLine.end[0]), options.excludeTangents)) { anglesOfIntersection.push(unRotate(angleOfX)); } } @@ -2241,7 +2335,7 @@ var MakerJs; */ function circleToCircle(circle1, circle2, options) { //see if circles are the same - if (circle1.radius == circle2.radius && MakerJs.point.areEqual(circle1.origin, circle2.origin)) { + if (circle1.radius == circle2.radius && MakerJs.point.areEqual(circle1.origin, circle2.origin, .0001)) { options.out_AreOverlapped = true; return null; } @@ -2273,14 +2367,14 @@ var MakerJs; return null; } //see if circles are tangent interior - if (c2.radius - x == c1.radius) { + if (MakerJs.round(c2.radius - x - c1.radius) == 0) { if (options.excludeTangents) { return null; } return [[unRotate(180)], [unRotate(180)]]; } //see if circles are tangent exterior - if (x - c2.radius == c1.radius) { + if (MakerJs.round(x - c2.radius - c1.radius) == 0) { if (options.excludeTangents) { return null; } @@ -2327,7 +2421,7 @@ var MakerJs; /** * @private */ - function getMatchingPointProperties(path1, path2) { + function getMatchingPointProperties(path1, path2, options) { var path1Properties = getPointProperties(path1); var path2Properties = getPointProperties(path2); var result = null; @@ -2341,7 +2435,7 @@ var MakerJs; }; } function check(i1, i2) { - if (MakerJs.point.areEqualRounded(path1Properties[i1].point, path2Properties[i2].point)) { + if (MakerJs.point.areEqual(path1Properties[i1].point, path2Properties[i2].point, .0001)) { result = [ makeMatch(path1, path1Properties, i1), makeMatch(path2, path2Properties, i2) @@ -2365,7 +2459,7 @@ var MakerJs; return false; } properties[i].shardPoint = circleIntersection.intersectionPoints[0]; - if (MakerJs.point.areEqualRounded(properties[i].point, circleIntersection.intersectionPoints[0], options.accuracy)) { + if (MakerJs.point.areEqual(properties[i].point, circleIntersection.intersectionPoints[0], .0001)) { if (circleIntersection.intersectionPoints.length > 1) { properties[i].shardPoint = circleIntersection.intersectionPoints[1]; } @@ -2517,11 +2611,11 @@ var MakerJs; function dogbone(line1, line2, filletRadius, options) { if (MakerJs.isPathLine(line1) && MakerJs.isPathLine(line2) && filletRadius && filletRadius > 0) { var opts = { - accuracy: .0001 + pointMatchingDistance: .005 }; MakerJs.extendObject(opts, options); //first find the common point - var commonProperty = getMatchingPointProperties(line1, line2); + var commonProperty = getMatchingPointProperties(line1, line2, options); if (commonProperty) { //get the ratio comparison of the two lines var ratio = getLineRatio([line1, line2]); @@ -2568,11 +2662,11 @@ var MakerJs; function fillet(path1, path2, filletRadius, options) { if (path1 && path2 && filletRadius && filletRadius > 0) { var opts = { - accuracy: .0001 + pointMatchingDistance: .005 }; MakerJs.extendObject(opts, options); //first find the common point - var commonProperty = getMatchingPointProperties(path1, path2); + var commonProperty = getMatchingPointProperties(path1, path2, options); if (commonProperty) { //since arcs can curl beyond, we need a local reference point. //An intersection with a circle of the same radius as the desired fillet should suffice. @@ -2674,6 +2768,34 @@ var MakerJs; (function (MakerJs) { var model; (function (model) { + /** + * @private + */ + var PointMap = (function () { + function PointMap(matchingDistance) { + if (matchingDistance === void 0) { matchingDistance = .001; } + this.matchingDistance = matchingDistance; + this.list = []; + } + PointMap.prototype.add = function (pointToAdd, item) { + this.list.push({ averagePoint: pointToAdd, item: item }); + }; + PointMap.prototype.find = function (pointToFind, saveAverage) { + for (var i = 0; i < this.list.length; i++) { + var item = this.list[i]; + var distance = MakerJs.measure.pointDistance(pointToFind, item.averagePoint); + if (distance <= this.matchingDistance) { + if (saveAverage) { + item.averagePoint = MakerJs.point.average(item.averagePoint, pointToFind); + } + return item.item; + } + } + return null; + }; + return PointMap; + })(); + model.PointMap = PointMap; /** * @private */ @@ -2720,7 +2842,7 @@ var MakerJs; while (true) { var currPath = currLink.path; currPath.reversed = currLink.reversed; - var id = model.getSimilarPathId(loopModel, currPath.primePathId); + var id = model.getSimilarPathId(loopModel, currPath.pathId); loopModel.paths[id] = currPath; if (!connections[currLink.nextConnection]) break; @@ -2750,11 +2872,11 @@ var MakerJs; var connections = {}; var result = { models: {} }; var opts = { - accuracy: .0001 + pointMatchingDistance: .005 }; MakerJs.extendObject(opts, options); function getLinkedPathsOnConnectionPoint(p) { - var serializedPoint = MakerJs.point.serialize(p, opts.accuracy); + var serializedPoint = MakerJs.point.serialize(p, .0001); //TODO convert to pointmap if (!(serializedPoint in connections)) { connections[serializedPoint] = []; } @@ -2773,14 +2895,15 @@ var MakerJs; } return result.models[id]; } + //todo: remove dead ends first model.originate(modelContext); //find loops by looking at all paths in this model model.walkPaths(modelContext, function (modelContext, pathId, pathContext) { if (!pathContext) return; var safePath = MakerJs.cloneObject(pathContext); - safePath.primePathId = pathId; - safePath.primeModel = modelContext; + safePath.pathId = pathId; + safePath.modelContext = modelContext; //circles are loops by nature if (safePath.type == MakerJs.pathType.Circle) { var loopModel = { @@ -2796,7 +2919,7 @@ var MakerJs; for (var i = 2; i--;) { var linkedPath = { path: safePath, - nextConnection: MakerJs.point.serialize(safePath.endPoints[1 - i], opts.accuracy), + nextConnection: MakerJs.point.serialize(safePath.endPoints[1 - i], .0001), reversed: i != 0 }; getLinkedPathsOnConnectionPoint(safePath.endPoints[i]).push(linkedPath); @@ -2836,13 +2959,110 @@ var MakerJs; function detachLoop(loopToDetach) { for (var id in loopToDetach.paths) { var pathDirectionalWithOriginalContext = loopToDetach.paths[id]; - var primeModel = pathDirectionalWithOriginalContext.primeModel; - if (primeModel && primeModel.paths && pathDirectionalWithOriginalContext.primePathId) { - delete primeModel.paths[pathDirectionalWithOriginalContext.primePathId]; + var primeModel = pathDirectionalWithOriginalContext.modelContext; + if (primeModel && primeModel.paths && pathDirectionalWithOriginalContext.pathId) { + delete primeModel.paths[pathDirectionalWithOriginalContext.pathId]; } } } model.detachLoop = detachLoop; + /** + * @private + */ + var DeadEndFinder = (function () { + function DeadEndFinder(pointMatchingDistance) { + this.pointMatchingDistance = pointMatchingDistance; + this.pointMap = new PointMap(pointMatchingDistance); + } + DeadEndFinder.prototype.addPathRef = function (p, pathRef) { + var found = this.pointMap.find(p, true); + if (found) { + found.push(pathRef); + } + else { + this.pointMap.add(p, [pathRef]); + } + }; + DeadEndFinder.prototype.removeMatchingPathRefs = function (a, b) { + //see if any are the same in each array + for (var ai = 0; ai < a.length; ai++) { + for (var bi = 0; bi < b.length; bi++) { + if (a[ai] === b[bi]) { + var pathRef = a[ai]; + a.splice(ai, 1); + b.splice(bi, 1); + return pathRef; + } + } + } + return null; + }; + DeadEndFinder.prototype.removePathRef = function (pathRef) { + var _this = this; + var removePath = function (p) { + var pathRefs = _this.pointMap.find(p, false); + for (var i = 0; i < pathRefs.length; i++) { + if (pathRefs[i] === pathRef) { + pathRefs.splice(i, 1); + return; + } + } + }; + for (var i = 2; i--;) { + removePath(pathRef.endPoints[i]); + } + }; + DeadEndFinder.prototype.removeDeadEnd = function () { + var found = false; + var oddPathRefs = null; + for (var i = 0; i < this.pointMap.list.length; i++) { + var pathRefs = this.pointMap.list[i].item; + if (pathRefs.length % 2 == 0) + continue; + if (pathRefs.length == 1) { + var pathRef = pathRefs[0]; + this.removePathRef(pathRef); + delete pathRef.modelContext.paths[pathRef.pathId]; + found = true; + } + else { + if (!oddPathRefs) { + //save this for another iteration + oddPathRefs = pathRefs; + } + else { + //compare with the saved + var pathRef = this.removeMatchingPathRefs(oddPathRefs, pathRefs); + if (pathRef) { + delete pathRef.modelContext.paths[pathRef.pathId]; + found = true; + //clear the saved + oddPathRefs = null; + } + } + } + } + return found; + }; + return DeadEndFinder; + })(); + function removeDeadEnds(modelContext, pointMatchingDistance) { + if (pointMatchingDistance === void 0) { pointMatchingDistance = .005; } + var serializedPointAccuracy = .0001; + var deadEndFinder = new DeadEndFinder(pointMatchingDistance); + model.walkPaths(modelContext, function (modelContext, pathId, pathContext) { + var endPoints = MakerJs.point.fromPathEnds(pathContext); + if (!endPoints) + return; + var pathRef = { modelContext: modelContext, pathId: pathId, endPoints: endPoints }; + for (var i = 2; i--;) { + deadEndFinder.addPathRef(endPoints[i], pathRef); + } + }); + while (deadEndFinder.removeDeadEnd()) + ; + } + model.removeDeadEnds = removeDeadEnds; })(model = MakerJs.model || (MakerJs.model = {})); })(MakerJs || (MakerJs = {})); var MakerJs; @@ -3055,7 +3275,7 @@ var MakerJs; var depthModel; var opts = { extrusion: 1, - accuracy: .0001 + pointMatchingDistance: .005 }; MakerJs.extendObject(opts, options); var loops = MakerJs.model.findLoops(modelToExport, opts); diff --git a/target/ts/makerjs.d.ts b/target/ts/makerjs.d.ts index 779af0534..e18c42ef6 100644 --- a/target/ts/makerjs.d.ts +++ b/target/ts/makerjs.d.ts @@ -252,9 +252,22 @@ declare module MakerJs { */ interface IPointMatchOptions { /** - * Optional exemplar of number of decimal places. + * Max distance to consider two points as the same. */ - accuracy?: number; + pointMatchingDistance?: number; + } + /** + * Options to pass to model.combine. + */ + interface ICombineOptions extends IPointMatchOptions { + /** + * Flag to remove paths which are not part of a loop. + */ + trimDeadEnds?: boolean; + /** + * Point which is known to be outside of the model. + */ + farPoint?: IPoint; } /** * Options to pass to model.findLoops. @@ -343,6 +356,19 @@ declare module MakerJs { * Test to see if an object implements the required properties of a model. */ function isModel(item: any): boolean; + /** + * Reference to a path id within a model. + */ + interface IRefPathIdInModel { + modelContext: IModel; + pathId: string; + } + /** + * Path and its reference id within a model + */ + interface IRefPathInModel extends IRefPathIdInModel { + pathContext: IPath; + } } declare module MakerJs.angle { /** @@ -352,7 +378,7 @@ declare module MakerJs.angle { * @param b Second angle. * @returns true if angles are the same, false if they are not */ - function areEqual(angle1: number, angle2: number): boolean; + function areEqual(angle1: number, angle2: number, accuracy?: number): boolean; /** * Ensures an angle is not greater than 360 * @@ -439,7 +465,7 @@ declare module MakerJs.point { * @param b Second point. * @returns true if points are the same, false if they are not */ - function areEqual(a: IPoint, b: IPoint): boolean; + function areEqual(a: IPoint, b: IPoint, withinDistance?: number): boolean; /** * Find out if two points are equal after rounding. * @@ -449,6 +475,14 @@ declare module MakerJs.point { * @returns true if points are the same, false if they are not */ function areEqualRounded(a: IPoint, b: IPoint, accuracy?: number): boolean; + /** + * Get the average of two points. + * + * @param a First point. + * @param b Second point. + * @returns New point object which is the average of a and b. + */ + function average(a: IPoint, b: IPoint): IPoint; /** * Clone a point into a new point. * @@ -567,7 +601,7 @@ declare module MakerJs.path { * @param b Second path. * @returns true if paths are the same, false if they are not */ - function areEqual(path1: IPath, path2: IPath): boolean; + function areEqual(path1: IPath, path2: IPath, withinPointDistance?: number): boolean; /** * Create a clone of a path, mirrored on either or both x and y axes. * @@ -698,11 +732,18 @@ declare module MakerJs.model { * @returns Number of child models. */ function countChildModels(modelContext: IModel): number; + /** + * Get an unused id in the models map with the same prefix. + * + * @param modelContext The model containing the models map. + * @param modelId The id to use directly (if unused), or as a prefix. + */ + function getSimilarModelId(modelContext: IModel, modelId: string): string; /** * Get an unused id in the paths map with the same prefix. * * @param modelContext The model containing the paths map. - * @param pathId The pathId to use directly (if unused), or as a prefix. + * @param pathId The id to use directly (if unused), or as a prefix. */ function getSimilarPathId(modelContext: IModel, pathId: string): string; /** @@ -782,7 +823,14 @@ declare module MakerJs.model { */ function isPathInsideModel(pathContext: IPath, modelContext: IModel, farPoint?: IPoint): boolean; /** - * Combine 2 models. The models should be originated. + * Break a model's paths everywhere they intersect with another path. + * + * @param modelToBreak The model containing paths to be broken. + * @param modelToIntersect Optional model containing paths to look for intersection, or else the modelToBreak will be used. + */ + function breakPathsAtIntersections(modelToBreak: IModel, modelToIntersect?: IModel): void; + /** + * Combine 2 models. The models should be originated, and every path within each model should be part of a loop. * * @param modelA First model to combine. * @param modelB Second model to combine. @@ -793,7 +841,7 @@ declare module MakerJs.model { * @param keepDuplicates Flag to include paths which are duplicate in both models. * @param farPoint Optional point of reference which is outside the bounds of both models. */ - function combine(modelA: IModel, modelB: IModel, includeAInsideB?: boolean, includeAOutsideB?: boolean, includeBInsideA?: boolean, includeBOutsideA?: boolean, keepDuplicates?: boolean, farPoint?: IPoint): void; + function combine(modelA: IModel, modelB: IModel, includeAInsideB?: boolean, includeAOutsideB?: boolean, includeBInsideA?: boolean, includeBOutsideA?: boolean, options?: ICombineOptions): void; } declare module MakerJs.units { /** @@ -1064,6 +1112,23 @@ declare module MakerJs.kit { function getParameterValues(ctor: IKit): any[]; } declare module MakerJs.model { + /** + * @private + */ + interface IPointMappedItem { + averagePoint: IPoint; + item: T; + } + /** + * @private + */ + class PointMap { + matchingDistance: number; + list: IPointMappedItem[]; + constructor(matchingDistance?: number); + add(pointToAdd: IPoint, item: T): void; + find(pointToFind: IPoint, saveAverage: boolean): T; + } /** * Find paths that have common endpoints and form loops. * @@ -1078,6 +1143,7 @@ declare module MakerJs.model { * @param loopToDetach The model to search for loops. */ function detachLoop(loopToDetach: IModel): void; + function removeDeadEnds(modelContext: IModel, pointMatchingDistance?: number): void; } declare module MakerJs.exporter { /**