From 73fdc146587b328806caac87aeae03093c668147 Mon Sep 17 00:00:00 2001 From: Dan Marshall Date: Sat, 14 Nov 2015 23:42:47 -0800 Subject: [PATCH 01/29] added nullref checks --- src/core/measure.ts | 17 +++++++++-------- src/core/model.ts | 4 ++++ 2 files changed, 13 insertions(+), 8 deletions(-) diff --git a/src/core/measure.ts b/src/core/measure.ts index 93055e9a7..4e658679d 100644 --- a/src/core/measure.ts +++ b/src/core/measure.ts @@ -228,19 +228,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..f1adb4f86 100644 --- a/src/core/model.ts +++ b/src/core/model.ts @@ -43,6 +43,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 +231,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); } } From 9e0808617b451b31bc1677ca1e451bd6ed11723e Mon Sep 17 00:00:00 2001 From: Dan Marshall Date: Sat, 14 Nov 2015 23:43:20 -0800 Subject: [PATCH 02/29] removed unused parameter --- src/core/intersect.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/core/intersect.ts b/src/core/intersect.ts index d91af9473..5ac56f6d7 100644 --- a/src/core/intersect.ts +++ b/src/core/intersect.ts @@ -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; } } From b24fe7d17834069428de795c8758582ddf21ccff Mon Sep 17 00:00:00 2001 From: Dan Marshall Date: Sat, 14 Nov 2015 23:45:16 -0800 Subject: [PATCH 03/29] added Ring --- debug/viewer.html | 1 + 1 file changed, 1 insertion(+) 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 @@ + From 805d5926fc631963de397c50219a972474d85fb4 Mon Sep 17 00:00:00 2001 From: Dan Marshall Date: Sun, 15 Nov 2015 13:41:17 -0800 Subject: [PATCH 04/29] reversed order of rounding operation, added accuracy to ange.areEqual() --- src/core/angle.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/core/angle.ts b/src/core/angle.ts index c6ab4c9b8..04ea98e88 100644 --- a/src/core/angle.ts +++ b/src/core/angle.ts @@ -9,9 +9,9 @@ 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)); + export function areEqual(angle1: number, angle2: number, accuracy?: number) { + var a1 = round(noRevolutions(angle1), accuracy); + var a2 = round(noRevolutions(angle2), accuracy); return a1 == a2 || a1 + 360 == a2 || a1 - 360 == a2; } From f521b5fbd96c4b78812918dc31d8c288a4de4439 Mon Sep 17 00:00:00 2001 From: Dan Marshall Date: Sun, 15 Nov 2015 13:42:02 -0800 Subject: [PATCH 05/29] added getSimilarModelId --- src/core/model.ts | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/src/core/model.ts b/src/core/model.ts index f1adb4f86..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) { From 706129d22f7277966347e0ea74f77a599fa68360 Mon Sep 17 00:00:00 2001 From: Dan Marshall Date: Mon, 23 Nov 2015 09:12:33 -0800 Subject: [PATCH 06/29] exposed breakPathsAtIntersections --- src/core/combine.ts | 48 ++++++++++++++++++++++++++++++--------------- 1 file changed, 32 insertions(+), 16 deletions(-) diff --git a/src/core/combine.ts b/src/core/combine.ts index b80c954f4..f6e024af3 100644 --- a/src/core/combine.ts +++ b/src/core/combine.ts @@ -70,7 +70,11 @@ module MakerJs.model { if (subSegments) { segments[i].path = subSegments[0]; - var newSegment = { path: subSegments[1], overlapped: segments[i].overlapped, uniqueForeignIntersectionPoints: [] }; + var newSegment: ICrossedPathSegment = { + path: subSegments[1], + overlapped: segments[i].overlapped, + uniqueForeignIntersectionPoints: [] + }; if (segments[i].overlapped) { overlappedSegments.push(newSegment); @@ -112,7 +116,7 @@ module MakerJs.model { /** * @private */ - function checkInsideForeignPath(segment: IPathInside, foreignPath: IPath, farPoint: IPoint = [7654321, 1234567]) { + function checkIntersectsForeignPath(segment: IPathInside, foreignPath: IPath, 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 +137,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, farPoint); } }); } @@ -192,10 +196,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[] = []; @@ -219,14 +233,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); @@ -263,14 +279,14 @@ module MakerJs.model { */ 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 +305,7 @@ module MakerJs.model { } /** - * Combine 2 models. The models should be originated. + * Combine 2 models. The models should be originated, and every paths within each model should be part of a loop. * * @param modelA First model to combine. * @param modelB Second model to combine. @@ -302,8 +318,8 @@ module MakerJs.model { */ export function combine(modelA: IModel, modelB: IModel, includeAInsideB: boolean = false, includeAOutsideB: boolean = true, includeBInsideA: boolean = false, includeBOutsideA: boolean = true, keepDuplicates: boolean = true, farPoint?: IPoint) { - var pathsA = breakAllPathsAtIntersections(modelA, modelB, farPoint); - var pathsB = breakAllPathsAtIntersections(modelB, modelA, farPoint); + var pathsA = breakAllPathsAtIntersections(modelA, modelB, true, farPoint); + var pathsB = breakAllPathsAtIntersections(modelB, modelA, true, farPoint); checkForEqualOverlaps(pathsA.overlappedSegments, pathsB.overlappedSegments); From 311c8b7dff77b8b9d29540d2603f1bba11ca2d24 Mon Sep 17 00:00:00 2001 From: Dan Marshall Date: Mon, 23 Nov 2015 09:13:12 -0800 Subject: [PATCH 07/29] added nullref checks --- src/core/exporter.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) 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); } } From 680c13573061a3d35c91b895e6f5f109d77bb380 Mon Sep 17 00:00:00 2001 From: Dan Marshall Date: Tue, 24 Nov 2015 09:28:53 -0800 Subject: [PATCH 08/29] use rounding for point comparisons --- src/core/intersect.ts | 2 +- src/core/path.ts | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/core/intersect.ts b/src/core/intersect.ts index 5ac56f6d7..2c5352ce1 100644 --- a/src/core/intersect.ts +++ b/src/core/intersect.ts @@ -430,7 +430,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.areEqualRounded(circle1.origin, circle2.origin)) { options.out_AreOverlapped = true; return null; } diff --git a/src/core/path.ts b/src/core/path.ts index 09d24e3de..d36a9b085 100644 --- a/src/core/path.ts +++ b/src/core/path.ts @@ -15,11 +15,12 @@ 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)); + return (point.areEqualRounded(line1.origin, line2.origin) && point.areEqualRounded(line1.end, line2.end)) + || (point.areEqualRounded(line1.origin, line2.end) && point.areEqualRounded(line1.end, line2.origin)); }; pathAreEqualMap[pathType.Circle] = function (circle1: IPathCircle, circle2: IPathCircle): boolean { - return point.areEqual(circle1.origin, circle2.origin) && circle1.radius == circle2.radius; + return point.areEqualRounded(circle1.origin, circle2.origin) && circle1.radius == circle2.radius; }; pathAreEqualMap[pathType.Arc] = function (arc1: IPathArc, arc2: IPathArc): boolean { From 7faf6d6f5b05b91bece3778b0bbb66b52c07b87f Mon Sep 17 00:00:00 2001 From: Dan Marshall Date: Thu, 26 Nov 2015 09:45:45 -0800 Subject: [PATCH 09/29] crop excessive revolutions when rotating --- src/core/path.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/core/path.ts b/src/core/path.ts index d36a9b085..c81d61944 100644 --- a/src/core/path.ts +++ b/src/core/path.ts @@ -175,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); From bd9b894836bce1977f6c0b0e7b5624cd3a964d59 Mon Sep 17 00:00:00 2001 From: Dan Marshall Date: Sun, 6 Dec 2015 12:22:45 -0800 Subject: [PATCH 10/29] added withinDistance to point equality. using stringify for serialization. --- src/core/point.ts | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/core/point.ts b/src/core/point.ts index 2f58fb682..9b21c7d6d 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; + } } /** @@ -257,7 +262,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); } /** From 7a8049bfeab3e6343e1a2bb9e614b61528c5459e Mon Sep 17 00:00:00 2001 From: Dan Marshall Date: Sun, 6 Dec 2015 12:28:30 -0800 Subject: [PATCH 11/29] default angle accuracy to ten thousandths --- src/core/angle.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core/angle.ts b/src/core/angle.ts index 04ea98e88..d4b621d14 100644 --- a/src/core/angle.ts +++ b/src/core/angle.ts @@ -9,7 +9,7 @@ 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, accuracy?: number) { + export function areEqual(angle1: number, angle2: number, accuracy: number = .0001) { var a1 = round(noRevolutions(angle1), accuracy); var a2 = round(noRevolutions(angle2), accuracy); From 46209954f193079d044279f5e41019543089ff10 Mon Sep 17 00:00:00 2001 From: Dan Marshall Date: Sun, 6 Dec 2015 12:30:10 -0800 Subject: [PATCH 12/29] using withinPointDistance for point equality --- src/core/path.ts | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/src/core/path.ts b/src/core/path.ts index c81d61944..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,17 +14,17 @@ module MakerJs.path { */ var pathAreEqualMap: IPathAreEqualMap = {}; - pathAreEqualMap[pathType.Line] = function (line1: IPathLine, line2: IPathLine): boolean { - return (point.areEqualRounded(line1.origin, line2.origin) && point.areEqualRounded(line1.end, line2.end)) - || (point.areEqualRounded(line1.origin, line2.end) && point.areEqualRounded(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.areEqualRounded(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); }; /** @@ -34,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); } } From d7b70afc3a23d3b116d0710ab048e4a8fdd16f9b Mon Sep 17 00:00:00 2001 From: Dan Marshall Date: Sun, 6 Dec 2015 12:31:33 -0800 Subject: [PATCH 13/29] using pointDistance instead of rounding for point equality --- src/core/intersect.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core/intersect.ts b/src/core/intersect.ts index 2c5352ce1..ad55ba00c 100644 --- a/src/core/intersect.ts +++ b/src/core/intersect.ts @@ -430,7 +430,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.areEqualRounded(circle1.origin, circle2.origin)) { + if (circle1.radius == circle2.radius && point.areEqual(circle1.origin, circle2.origin, .0001)) { options.out_AreOverlapped = true; return null; } From bf94df51ff98e542700dd996ce0e528537411c92 Mon Sep 17 00:00:00 2001 From: Dan Marshall Date: Mon, 7 Dec 2015 09:59:40 -0800 Subject: [PATCH 14/29] use rounding only once --- src/core/angle.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/core/angle.ts b/src/core/angle.ts index d4b621d14..d69d46af7 100644 --- a/src/core/angle.ts +++ b/src/core/angle.ts @@ -10,10 +10,10 @@ module MakerJs.angle { * @returns true if angles are the same, false if they are not */ export function areEqual(angle1: number, angle2: number, accuracy: number = .0001) { - var a1 = round(noRevolutions(angle1), accuracy); - var a2 = round(noRevolutions(angle2), accuracy); - - return a1 == a2 || a1 + 360 == a2 || a1 - 360 == a2; + var a1 = noRevolutions(angle1); + var a2 = noRevolutions(angle2); + var d = noRevolutions(round(a2 - a1, accuracy)); + return d == 0; } /** From 9fa8999fe7dc28aac7b2f7eec2285c15ccdc84bb Mon Sep 17 00:00:00 2001 From: Dan Marshall Date: Mon, 7 Dec 2015 10:03:11 -0800 Subject: [PATCH 15/29] using rounded difference for equality --- src/core/intersect.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/core/intersect.ts b/src/core/intersect.ts index ad55ba00c..550f502a9 100644 --- a/src/core/intersect.ts +++ b/src/core/intersect.ts @@ -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); From d118a7d3822ae1263c48d7c35a0cd230034335c2 Mon Sep 17 00:00:00 2001 From: Dan Marshall Date: Mon, 7 Dec 2015 10:05:09 -0800 Subject: [PATCH 16/29] using rounded difference for equality. Remove revolutions for angle comparison. --- src/core/measure.ts | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/src/core/measure.ts b/src/core/measure.ts index 4e658679d..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; From 262a4d0680e12aa74449b8ea7224e2eb94effcba Mon Sep 17 00:00:00 2001 From: Dan Marshall Date: Tue, 8 Dec 2015 14:15:42 -0800 Subject: [PATCH 17/29] Added IRefPathIdInModel interface --- src/core/loops.ts | 16 +++++++--------- src/core/maker.ts | 14 ++++++++++++++ 2 files changed, 21 insertions(+), 9 deletions(-) diff --git a/src/core/loops.ts b/src/core/loops.ts index 88d81f621..1902954a4 100644 --- a/src/core/loops.ts +++ b/src/core/loops.ts @@ -5,9 +5,7 @@ module MakerJs.model { /** * @private */ - interface IPathDirectionalWithPrimeContext extends IPathDirectional { - primePathId: string; - primeModel: IModel; + interface IPathDirectionalWithPrimeContext extends IPathDirectional, IRefPathIdInModel { } /** @@ -90,7 +88,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; @@ -165,8 +163,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) { @@ -235,9 +233,9 @@ 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]; } } } diff --git a/src/core/maker.ts b/src/core/maker.ts index cf9b52001..1f9e79ca2 100644 --- a/src/core/maker.ts +++ b/src/core/maker.ts @@ -449,6 +449,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 From 5b3f423369deb11a6bf47c6e65305c80efd5fd67 Mon Sep 17 00:00:00 2001 From: Dan Marshall Date: Tue, 8 Dec 2015 14:17:09 -0800 Subject: [PATCH 18/29] added detector for dead ends --- src/core/deadends.ts | 137 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 137 insertions(+) create mode 100644 src/core/deadends.ts diff --git a/src/core/deadends.ts b/src/core/deadends.ts new file mode 100644 index 000000000..c14ea91ed --- /dev/null +++ b/src/core/deadends.ts @@ -0,0 +1,137 @@ +/// + +module MakerJs.model { + + /** + * @private + */ + interface IRefPathEndpoints extends IRefPathIdInModel { + endPoints: string[]; + } + + /** + * @private + */ + interface IRefPathEndpointsMap { + [serializedPoint: string]: IRefPathEndpoints; + } + + /** + * @private + */ + interface IRefPathEndpointsArrayMap { + [serializedPoint: string]: IRefPathEndpoints[]; + } + + /** + * @private + */ + class DeadEndFinder { + + private pointMap: IRefPathEndpointsArrayMap = {}; + + constructor(public pointMatchingDistance) { + } + + public addPathRef(serializedPoint: string, pathRef: IRefPathEndpoints) { + if (!(serializedPoint in this.pointMap)) { + this.pointMap[serializedPoint] = []; + } + + this.pointMap[serializedPoint].push(pathRef); + } + + private removePathRef(pathRef: IRefPathEndpoints) { + + var removePath = (serializedPoint: string) => { + var endpointArray = this.pointMap[serializedPoint]; + + for (var i = 0; i < endpointArray.length; i++) { + if (endpointArray[i] === pathRef) { + endpointArray.splice(i, 1); + return; + } + } + } + + for (var i = 2; i--;) { + removePath(pathRef.endPoints[i]); + } + } + + public removeDeadEnd(): boolean { + + var singlePoints: IRefPathEndpointsMap = {}; + + for (var p in this.pointMap) { + if (this.pointMap[p].length == 1) { + singlePoints[p] = this.pointMap[p][0]; + } + } + + for (var p1 in singlePoints) { + var merge = false; + + for (var p2 in singlePoints) { + if (p1 == p2) continue; + + //compare the distance + var d = measure.pointDistance(JSON.parse(p1), JSON.parse(p2)); + if (d > this.pointMatchingDistance) continue; + + merge = true; + break; + } + + if (merge) { + + this.removePathRef(singlePoints[p2]); + delete this.pointMap[p2]; + + for (var i = 2; i--;) { + if (singlePoints[p2].endPoints[i] == p2) { + singlePoints[p2].endPoints[i] = p1 + } + this.pointMap[singlePoints[p2].endPoints[i]].push(singlePoints[p2]); + } + + } else { + var pathRef = singlePoints[p1]; + + this.removePathRef(pathRef); + + delete pathRef.modelContext.paths[pathRef.pathId]; + } + + //only do the first point + return true; + } + + //no single points found + return false; + } + } + + 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: [] }; + + for (var i = 2; i--;) { + var serializedPoint = point.serialize(endPoints[i], serializedPointAccuracy); + + pathRef.endPoints.push(serializedPoint); + + deadEndFinder.addPathRef(serializedPoint, pathRef); + } + }); + + while(deadEndFinder.removeDeadEnd()); + } +} From 3fcffc730345a94987f22dbf588644947f3e1a75 Mon Sep 17 00:00:00 2001 From: Dan Marshall Date: Tue, 8 Dec 2015 14:18:48 -0800 Subject: [PATCH 19/29] tuned combine accuracy, added deadend detection --- src/core/combine.ts | 26 ++++++++++++++------------ 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/src/core/combine.ts b/src/core/combine.ts index f6e024af3..c59449f6b 100644 --- a/src/core/combine.ts +++ b/src/core/combine.ts @@ -1,4 +1,4 @@ -/// +/// module MakerJs.model { @@ -12,7 +12,7 @@ module MakerJs.model { 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; } } @@ -26,7 +26,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; @@ -98,7 +98,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, .00001)) { return; } } @@ -182,9 +182,7 @@ module MakerJs.model { /** * @private */ - interface ICrossedPath { - modelContext: IModel; - pathId: string; + interface ICrossedPath extends IRefPathIdInModel { segments: ICrossedPathSegment[]; } @@ -257,7 +255,7 @@ module MakerJs.model { function checkForEqualOverlaps(crossedPathsA: ICrossedPathSegment[], crossedPathsB: ICrossedPathSegment[]) { function compareSegments(segment1: ICrossedPathSegment, segment2: ICrossedPathSegment) { - if (path.areEqual(segment1.path, segment2.path)) { + if (path.areEqual(segment1.path, segment2.path, .0001)) { segment1.duplicate = segment2.duplicate = true; } } @@ -277,7 +275,7 @@ 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(modelContext: IModel, pathIdBase: string, segment: ICrossedPathSegment) { var id = getSimilarPathId(modelContext, pathIdBase); @@ -316,7 +314,7 @@ 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, trimDeadends: boolean = true, farPoint?: IPoint) { var pathsA = breakAllPathsAtIntersections(modelA, modelB, true, farPoint); var pathsB = breakAllPathsAtIntersections(modelB, modelA, true, farPoint); @@ -324,11 +322,15 @@ module MakerJs.model { checkForEqualOverlaps(pathsA.overlappedSegments, pathsB.overlappedSegments); 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 (trimDeadends) { + removeDeadEnds({ models: { modelA: modelA, modelB: modelB } }); } } From d52b3fc2a6a61302898d62b8c0c2e5df5e2388f2 Mon Sep 17 00:00:00 2001 From: Dan Marshall Date: Fri, 11 Dec 2015 19:03:53 -0800 Subject: [PATCH 20/29] added average. not rounding in rotate. --- src/core/point.ts | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/src/core/point.ts b/src/core/point.ts index 9b21c7d6d..f5198482f 100644 --- a/src/core/point.ts +++ b/src/core/point.ts @@ -53,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. * @@ -235,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); } /** From 3c8410a99165829e91bc8fd6178fc2ac36651383 Mon Sep 17 00:00:00 2001 From: Dan Marshall Date: Sun, 13 Dec 2015 15:24:11 -0800 Subject: [PATCH 21/29] using rounding in comparisons, not in variables --- src/core/intersect.ts | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/src/core/intersect.ts b/src/core/intersect.ts index 550f502a9..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]; @@ -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)); } } @@ -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; From 49b4acf886bf596a5a5d7c8a7decbc8496f89d70 Mon Sep 17 00:00:00 2001 From: Dan Marshall Date: Sun, 13 Dec 2015 15:29:28 -0800 Subject: [PATCH 22/29] using IPointMatchOptions. moved deadEnd code into loops --- src/core/combine.ts | 31 +++++--- src/core/deadends.ts | 137 --------------------------------- src/core/fillet.ts | 14 ++-- src/core/loops.ts | 174 +++++++++++++++++++++++++++++++++++++++++- src/core/maker.ts | 20 ++++- src/core/openjscad.ts | 2 +- 6 files changed, 217 insertions(+), 161 deletions(-) delete mode 100644 src/core/deadends.ts diff --git a/src/core/combine.ts b/src/core/combine.ts index c59449f6b..f16f1ddbb 100644 --- a/src/core/combine.ts +++ b/src/core/combine.ts @@ -1,4 +1,4 @@ -/// +/// module MakerJs.model { @@ -72,6 +72,7 @@ module MakerJs.model { var newSegment: ICrossedPathSegment = { path: subSegments[1], + pathId: segments[0].pathId, overlapped: segments[i].overlapped, uniqueForeignIntersectionPoints: [] }; @@ -98,7 +99,7 @@ module MakerJs.model { function addUniquePoint(pointToAdd: IPoint) { for (var i = 0; i < pointArray.length; i++) { - if (point.areEqual(pointArray[i], pointToAdd, .00001)) { + if (point.areEqual(pointArray[i], pointToAdd, .0000001)) { return; } } @@ -116,7 +117,7 @@ module MakerJs.model { /** * @private */ - function checkIntersectsForeignPath(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); @@ -137,7 +138,7 @@ module MakerJs.model { function checkInsideForeignModel(segment: IPathInside, modelToIntersect: IModel, farPoint?: IPoint) { walkPaths(modelToIntersect, function (mx: IModel, pathId2: string, path2: IPath) { if (path2) { - checkIntersectsForeignPath(segment, path2, farPoint); + checkIntersectsForeignPath(segment, path2, pathId2, farPoint); } }); } @@ -175,6 +176,7 @@ module MakerJs.model { * @private */ interface ICrossedPathSegment extends IPathInside { + pathId: string; overlapped: boolean; duplicate?: boolean; } @@ -219,6 +221,7 @@ module MakerJs.model { //clone this path and make it the first segment var segment: ICrossedPathSegment = { path: cloneObject(path1), + pathId: pathId1, overlapped: false, uniqueForeignIntersectionPoints: [] }; @@ -252,10 +255,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, .0001)) { + if (path.areEqual(segment1.path, segment2.path, pointMatchingDistance)) { segment1.duplicate = segment2.duplicate = true; } } @@ -314,12 +317,18 @@ 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, trimDeadends: 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 opts: ICombineOptions = { + trimDeadEnds: true, + pointMatchingDistance: .005 + }; + extendObject(opts, options); - var pathsA = breakAllPathsAtIntersections(modelA, modelB, true, farPoint); - var pathsB = breakAllPathsAtIntersections(modelB, modelA, true, farPoint); + var pathsA = breakAllPathsAtIntersections(modelA, modelB, true, opts.farPoint); + var pathsB = breakAllPathsAtIntersections(modelB, modelA, true, opts.farPoint); - checkForEqualOverlaps(pathsA.overlappedSegments, pathsB.overlappedSegments); + checkForEqualOverlaps(pathsA.overlappedSegments, pathsB.overlappedSegments, opts.pointMatchingDistance); for (var i = 0; i < pathsA.crossedPaths.length; i++) { addOrDeleteSegments(pathsA.crossedPaths[i], includeAInsideB, includeAOutsideB, true); @@ -329,7 +338,7 @@ module MakerJs.model { addOrDeleteSegments(pathsB.crossedPaths[i], includeBInsideA, includeBOutsideA, false); } - if (trimDeadends) { + if (opts.trimDeadEnds) { removeDeadEnds({ models: { modelA: modelA, modelB: modelB } }); } } diff --git a/src/core/deadends.ts b/src/core/deadends.ts deleted file mode 100644 index c14ea91ed..000000000 --- a/src/core/deadends.ts +++ /dev/null @@ -1,137 +0,0 @@ -/// - -module MakerJs.model { - - /** - * @private - */ - interface IRefPathEndpoints extends IRefPathIdInModel { - endPoints: string[]; - } - - /** - * @private - */ - interface IRefPathEndpointsMap { - [serializedPoint: string]: IRefPathEndpoints; - } - - /** - * @private - */ - interface IRefPathEndpointsArrayMap { - [serializedPoint: string]: IRefPathEndpoints[]; - } - - /** - * @private - */ - class DeadEndFinder { - - private pointMap: IRefPathEndpointsArrayMap = {}; - - constructor(public pointMatchingDistance) { - } - - public addPathRef(serializedPoint: string, pathRef: IRefPathEndpoints) { - if (!(serializedPoint in this.pointMap)) { - this.pointMap[serializedPoint] = []; - } - - this.pointMap[serializedPoint].push(pathRef); - } - - private removePathRef(pathRef: IRefPathEndpoints) { - - var removePath = (serializedPoint: string) => { - var endpointArray = this.pointMap[serializedPoint]; - - for (var i = 0; i < endpointArray.length; i++) { - if (endpointArray[i] === pathRef) { - endpointArray.splice(i, 1); - return; - } - } - } - - for (var i = 2; i--;) { - removePath(pathRef.endPoints[i]); - } - } - - public removeDeadEnd(): boolean { - - var singlePoints: IRefPathEndpointsMap = {}; - - for (var p in this.pointMap) { - if (this.pointMap[p].length == 1) { - singlePoints[p] = this.pointMap[p][0]; - } - } - - for (var p1 in singlePoints) { - var merge = false; - - for (var p2 in singlePoints) { - if (p1 == p2) continue; - - //compare the distance - var d = measure.pointDistance(JSON.parse(p1), JSON.parse(p2)); - if (d > this.pointMatchingDistance) continue; - - merge = true; - break; - } - - if (merge) { - - this.removePathRef(singlePoints[p2]); - delete this.pointMap[p2]; - - for (var i = 2; i--;) { - if (singlePoints[p2].endPoints[i] == p2) { - singlePoints[p2].endPoints[i] = p1 - } - this.pointMap[singlePoints[p2].endPoints[i]].push(singlePoints[p2]); - } - - } else { - var pathRef = singlePoints[p1]; - - this.removePathRef(pathRef); - - delete pathRef.modelContext.paths[pathRef.pathId]; - } - - //only do the first point - return true; - } - - //no single points found - return false; - } - } - - 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: [] }; - - for (var i = 2; i--;) { - var serializedPoint = point.serialize(endPoints[i], serializedPointAccuracy); - - pathRef.endPoints.push(serializedPoint); - - deadEndFinder.addPathRef(serializedPoint, pathRef); - } - }); - - while(deadEndFinder.removeDeadEnd()); - } -} 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/loops.ts b/src/core/loops.ts index 1902954a4..aa1ccfda4 100644 --- a/src/core/loops.ts +++ b/src/core/loops.ts @@ -2,6 +2,45 @@ module MakerJs.model { + /** + * @private + */ + 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 */ @@ -124,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] = []; @@ -155,6 +194,8 @@ module MakerJs.model { return result.models[id]; } + //todo: clone the original before originating + //todo: remove dead ends first model.originate(modelContext); //find loops by looking at all paths in this model @@ -162,6 +203,7 @@ module MakerJs.model { if (!pathContext) return; + //todo - don't clone var safePath = cloneObject(pathContext); safePath.pathId = pathId; safePath.modelContext = modelContext; @@ -183,7 +225,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); @@ -239,4 +281,130 @@ module MakerJs.model { } } } + + /** + * @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(a1: IRefPathEndpoints[], a2: IRefPathEndpoints[]) { + //see if any are the same in each array + for (var i = 0; i < a1.length; i++) { + for (var j = 0; j < a2.length; j++) { + if (a1[i] === a2[j]) { + + var pathRef = a1[i]; + delete pathRef.modelContext.paths[pathRef.pathId]; + + a1.splice(i, 1); + a2.splice(j, 1); + + return true; + } + } + } + return false; + } + + 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 threeIndex: IRefPathEndpoints[][] = []; + + for (var i = 0; i < this.pointMap.list.length; i++) { + + var pathRefs = this.pointMap.list[i].item; + + if (pathRefs.length == 1) { + var pathRef = pathRefs[0]; + + delete pathRef.modelContext.paths[pathRef.pathId]; + + this.removePathRef(pathRef); + + found = true; + } else if (pathRefs.length == 3) { + threeIndex.push(pathRefs); + } + } + + if (threeIndex.length) { + //find the matching singles hiding within triples + for (var a = 0; a < threeIndex.length; a++) { + var pathRefs_a = threeIndex[a]; + if (pathRefs_a.length != 3) continue; + for (var b = 0; b < threeIndex.length; b++) { + if (a == b) continue; + var pathRefs_b = threeIndex[b]; + if (pathRefs_b.length != 3) continue; + if (this.removeMatchingPathRefs(pathRefs_a, pathRefs_b)) { + + //if a matching triple was found then our index is no longer valid, so exit. + return true; + } + } + } + } + + 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 1f9e79ca2..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; } /** 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); From 08eadda3b4602371b549596944a0c30fe39db8c0 Mon Sep 17 00:00:00 2001 From: Dan Marshall Date: Sun, 13 Dec 2015 15:29:51 -0800 Subject: [PATCH 23/29] hide crosshair text --- debug/viewer.css | 4 ++++ 1 file changed, 4 insertions(+) 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 From 8b9f00a54ea57ba9d27ee1aebca1179926ff2290 Mon Sep 17 00:00:00 2001 From: Dan Marshall Date: Mon, 14 Dec 2015 21:53:32 -0800 Subject: [PATCH 24/29] handling breaking of a circle --- src/core/combine.ts | 25 +++++++++++++++---------- 1 file changed, 15 insertions(+), 10 deletions(-) diff --git a/src/core/combine.ts b/src/core/combine.ts index f16f1ddbb..52674aaca 100644 --- a/src/core/combine.ts +++ b/src/core/combine.ts @@ -6,6 +6,7 @@ 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); @@ -17,6 +18,8 @@ module MakerJs.model { } } return segments; + } else if (segmentType == pathType.Circle) { + return [segment1]; } return null; } @@ -70,18 +73,20 @@ module MakerJs.model { if (subSegments) { segments[i].path = subSegments[0]; - var newSegment: ICrossedPathSegment = { - path: subSegments[1], - pathId: segments[0].pathId, - 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--; From 385afb0e3052ecbe4c7c16a6d57399cf68c799a8 Mon Sep 17 00:00:00 2001 From: Dan Marshall Date: Mon, 14 Dec 2015 22:37:21 -0800 Subject: [PATCH 25/29] handling large rotations --- src/core/break.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) 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; From ffef148ba18ddbf7516fb1d7a233a327bb63150a Mon Sep 17 00:00:00 2001 From: Dan Marshall Date: Mon, 14 Dec 2015 23:47:37 -0800 Subject: [PATCH 26/29] simplified odd deadend matching --- src/core/loops.ts | 62 +++++++++++++++++++++-------------------------- 1 file changed, 28 insertions(+), 34 deletions(-) diff --git a/src/core/loops.ts b/src/core/loops.ts index aa1ccfda4..f6f571a52 100644 --- a/src/core/loops.ts +++ b/src/core/loops.ts @@ -309,23 +309,19 @@ module MakerJs.model { } } - private removeMatchingPathRefs(a1: IRefPathEndpoints[], a2: IRefPathEndpoints[]) { + private removeMatchingPathRefs(a: IRefPathEndpoints[], b: IRefPathEndpoints[]) { //see if any are the same in each array - for (var i = 0; i < a1.length; i++) { - for (var j = 0; j < a2.length; j++) { - if (a1[i] === a2[j]) { - - var pathRef = a1[i]; - delete pathRef.modelContext.paths[pathRef.pathId]; - - a1.splice(i, 1); - a2.splice(j, 1); - - return true; + 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 false; + return null; } private removePathRef(pathRef: IRefPathEndpoints) { @@ -348,43 +344,41 @@ module MakerJs.model { public removeDeadEnd(): boolean { var found = false; - var threeIndex: IRefPathEndpoints[][] = []; + 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; - this.removePathRef(pathRef); + } else { - found = true; - } else if (pathRefs.length == 3) { - threeIndex.push(pathRefs); - } - } + if (!oddPathRefs) { + //save this for another iteration + oddPathRefs = pathRefs; + } else { - if (threeIndex.length) { - //find the matching singles hiding within triples - for (var a = 0; a < threeIndex.length; a++) { - var pathRefs_a = threeIndex[a]; - if (pathRefs_a.length != 3) continue; - for (var b = 0; b < threeIndex.length; b++) { - if (a == b) continue; - var pathRefs_b = threeIndex[b]; - if (pathRefs_b.length != 3) continue; - if (this.removeMatchingPathRefs(pathRefs_a, pathRefs_b)) { - - //if a matching triple was found then our index is no longer valid, so exit. - return true; + //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; } } From 62bac8f967338b8bfa26e4ccb91d5e77422d3559 Mon Sep 17 00:00:00 2001 From: Dan Marshall Date: Mon, 14 Dec 2015 23:48:28 -0800 Subject: [PATCH 27/29] tightened difference checking between very close paths --- src/core/combine.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core/combine.ts b/src/core/combine.ts index 52674aaca..fa339d218 100644 --- a/src/core/combine.ts +++ b/src/core/combine.ts @@ -104,7 +104,7 @@ module MakerJs.model { function addUniquePoint(pointToAdd: IPoint) { for (var i = 0; i < pointArray.length; i++) { - if (point.areEqual(pointArray[i], pointToAdd, .0000001)) { + if (point.areEqual(pointArray[i], pointToAdd, .000000001)) { return; } } From 75970c2b9f6ee842863740968b16191f2fcb9d04 Mon Sep 17 00:00:00 2001 From: Dan Marshall Date: Tue, 15 Dec 2015 00:01:48 -0800 Subject: [PATCH 28/29] new build --- index.js | 424 ++++++++++++++++++++++++++++--------- src/core/combine.ts | 2 +- src/core/loops.ts | 2 - target/js/browser.maker.js | 424 ++++++++++++++++++++++++++++--------- target/js/node.maker.js | 424 ++++++++++++++++++++++++++++--------- target/ts/makerjs.d.ts | 82 ++++++- 6 files changed, 1041 insertions(+), 317 deletions(-) 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/src/core/combine.ts b/src/core/combine.ts index fa339d218..5bbd2915e 100644 --- a/src/core/combine.ts +++ b/src/core/combine.ts @@ -311,7 +311,7 @@ module MakerJs.model { } /** - * Combine 2 models. The models should be originated, and every paths within each model should be part of a loop. + * 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. diff --git a/src/core/loops.ts b/src/core/loops.ts index f6f571a52..298b21b6f 100644 --- a/src/core/loops.ts +++ b/src/core/loops.ts @@ -194,7 +194,6 @@ module MakerJs.model { return result.models[id]; } - //todo: clone the original before originating //todo: remove dead ends first model.originate(modelContext); @@ -203,7 +202,6 @@ module MakerJs.model { if (!pathContext) return; - //todo - don't clone var safePath = cloneObject(pathContext); safePath.pathId = pathId; safePath.modelContext = modelContext; 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 { /** From 61f010116f819677c48d57f7b0f96c58f6404eea Mon Sep 17 00:00:00 2001 From: Dan Marshall Date: Tue, 15 Dec 2015 00:02:24 -0800 Subject: [PATCH 29/29] version bump --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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": {