mirror of
https://github.com/revarbat/BOSL2.git
synced 2025-08-12 22:14:04 +02:00
offset() returns [] if offset path is degenerate and error=false
This commit is contained in:
144
regions.scad
144
regions.scad
@@ -3,7 +3,7 @@
|
|||||||
// This file provides 2D Boolean set operations on polygons, where you can
|
// This file provides 2D Boolean set operations on polygons, where you can
|
||||||
// compute, for example, the intersection or union of the shape defined by point lists, producing
|
// compute, for example, the intersection or union of the shape defined by point lists, producing
|
||||||
// a new point list. Of course, such operations may produce shapes with multiple
|
// a new point list. Of course, such operations may produce shapes with multiple
|
||||||
// components. To handle that, we use "regions" which are lists of paths representing the polygons.
|
// components. To handle that, we use "regions", which are lists of paths representing the polygons.
|
||||||
// In addition to set operations, you can calculate offsets, determine whether a point is in a
|
// In addition to set operations, you can calculate offsets, determine whether a point is in a
|
||||||
// region and you can decompose a region into parts.
|
// region and you can decompose a region into parts.
|
||||||
// Includes:
|
// Includes:
|
||||||
@@ -25,10 +25,10 @@
|
|||||||
// - Two polygons on the list do not cross each other
|
// - Two polygons on the list do not cross each other
|
||||||
// - A vertex of one polygon never meets the edge of another one except at a vertex
|
// - A vertex of one polygon never meets the edge of another one except at a vertex
|
||||||
// .
|
// .
|
||||||
// Note that this means vertex-vertex touching between two polygons is acceptable
|
// This means vertex-vertex touching between two polygons is acceptable
|
||||||
// to define a region. Note, however, that regions with vertex-vertex contact usually
|
// to define a region. Note, however, that regions with vertex-vertex contact usually
|
||||||
// cannot be rendered with CGAL. See {{is_valid_region()}} for examples of valid regions and
|
// cannot be rendered with CGAL. See {{is_valid_region()}} for examples of valid regions and
|
||||||
// lists of polygons that are not regions. Note that {{is_region_simple()}} will identify
|
// lists of polygons that are not regions. Note that {{is_region_simple()}} identifies
|
||||||
// regions with no polygon intersections at all, which should render successfully witih CGAL.
|
// regions with no polygon intersections at all, which should render successfully witih CGAL.
|
||||||
// .
|
// .
|
||||||
// The actual geometry of the region is defined by XORing together
|
// The actual geometry of the region is defined by XORing together
|
||||||
@@ -38,8 +38,8 @@
|
|||||||
// above, can be a time consuming test, so it is not done automatically. It is your responsibility to ensure that your regions are
|
// above, can be a time consuming test, so it is not done automatically. It is your responsibility to ensure that your regions are
|
||||||
// compliant. You can construct regions by making a suitable list of polygons, or by using
|
// compliant. You can construct regions by making a suitable list of polygons, or by using
|
||||||
// set operation function such as union() or difference(), which all acccept polygons, as
|
// set operation function such as union() or difference(), which all acccept polygons, as
|
||||||
// well as regions, as their inputs. And if you must you can clean up an ill-formed region using make_region(),
|
// well as regions, as their inputs. If you must, you can clean up an ill-formed region using
|
||||||
// which will break up self-intersecting polygons and polygons that cross each other.
|
// {{make_region()}}, which breaks up self-intersecting polygons and polygons that cross each other.
|
||||||
|
|
||||||
|
|
||||||
// Function: is_region()
|
// Function: is_region()
|
||||||
@@ -63,9 +63,9 @@ function is_region(x) = is_list(x) && is_path(x.x);
|
|||||||
// Description:
|
// Description:
|
||||||
// Returns true if the input is a valid region, meaning that it is a list of simple polygons whose segments do not cross each other.
|
// Returns true if the input is a valid region, meaning that it is a list of simple polygons whose segments do not cross each other.
|
||||||
// This test can be time consuming with regions that contain many points.
|
// This test can be time consuming with regions that contain many points.
|
||||||
// It differs from `is_region()` which simply checks that the object is a list whose first entry is a path
|
// It differs from `is_region()`, which simply checks that the object is a list whose first entry is a path
|
||||||
// because it searches all the list polygons for any self-intersections or intersections with each other.
|
// because it searches all the list polygons for any self-intersections or intersections with each other.
|
||||||
// Will also return true if given a single simple polygon. Use {{make_region()}} to convert sets of self-intersecting polygons into
|
// Also returns true if given a single simple polygon. Use {{make_region()}} to convert sets of self-intersecting polygons into
|
||||||
// a region.
|
// a region.
|
||||||
// Arguments:
|
// Arguments:
|
||||||
// region = region to check
|
// region = region to check
|
||||||
@@ -86,7 +86,7 @@ function is_region(x) = is_list(x) && is_path(x.x);
|
|||||||
// object = [square(10), move([8,8], square(8))];
|
// object = [square(10), move([8,8], square(8))];
|
||||||
// rainbow(object)stroke($item, width=.2,closed=true);
|
// rainbow(object)stroke($item, width=.2,closed=true);
|
||||||
// back(17)text(is_valid_region(object) ? "region" : "non-region", size=2);
|
// back(17)text(is_valid_region(object) ? "region" : "non-region", size=2);
|
||||||
// Example(2D,NoAxes): A union is one way to fix the above example and get a region. (Note that union is run here on two simple polygons, which are valid regions themselves and hence acceptable inputs to union.
|
// Example(2D,NoAxes): A union is one way to fix the above example and get a region. Union is run here on two simple polygons, which are valid regions themselves and hence acceptable inputs to union.
|
||||||
// region = union([square(10), move([8,8], square(8))]);
|
// region = union([square(10), move([8,8], square(8))]);
|
||||||
// rainbow(region)stroke($item, width=.25,closed=true);
|
// rainbow(region)stroke($item, width=.25,closed=true);
|
||||||
// back(12)text(is_valid_region(region) ? "region" : "non-region", size=2);
|
// back(12)text(is_valid_region(region) ? "region" : "non-region", size=2);
|
||||||
@@ -170,7 +170,7 @@ function is_region(x) = is_list(x) && is_path(x.x);
|
|||||||
// move([-5,11.4])text(is_valid_region(region) ? "region" : "non-region", size=3);
|
// move([-5,11.4])text(is_valid_region(region) ? "region" : "non-region", size=3);
|
||||||
function is_valid_region(region, eps=EPSILON) =
|
function is_valid_region(region, eps=EPSILON) =
|
||||||
let(region=force_region(region))
|
let(region=force_region(region))
|
||||||
assert(is_region(region), "Input is not a region")
|
assert(is_region(region), "\nInput is not a region.")
|
||||||
// no short paths
|
// no short paths
|
||||||
[for(p=region) if (len(p)<3) 1] == []
|
[for(p=region) if (len(p)<3) 1] == []
|
||||||
&&
|
&&
|
||||||
@@ -238,7 +238,7 @@ function _polygon_crosses_region(region, poly, eps=EPSILON) =
|
|||||||
// move([1,13])text(is_region_simple(region) ? "simple" : "not-simple", size=2);
|
// move([1,13])text(is_region_simple(region) ? "simple" : "not-simple", size=2);
|
||||||
function is_region_simple(region, eps=EPSILON) =
|
function is_region_simple(region, eps=EPSILON) =
|
||||||
let(region=force_region(region))
|
let(region=force_region(region))
|
||||||
assert(is_region(region), "Input is not a region")
|
assert(is_region(region), "\nInput is not a region.")
|
||||||
[for(p=region) if (!is_path_simple(p,closed=true,eps=eps)) 1] == []
|
[for(p=region) if (!is_path_simple(p,closed=true,eps=eps)) 1] == []
|
||||||
&&
|
&&
|
||||||
[for(i=[0:1:len(region)-2])
|
[for(i=[0:1:len(region)-2])
|
||||||
@@ -275,7 +275,7 @@ function is_region_simple(region, eps=EPSILON) =
|
|||||||
|
|
||||||
function make_region(polys,nonzero=false,eps=EPSILON) =
|
function make_region(polys,nonzero=false,eps=EPSILON) =
|
||||||
let(polys=force_region(polys))
|
let(polys=force_region(polys))
|
||||||
assert(is_region(polys), "Input is not a region")
|
assert(is_region(polys), "\nInput is not a region.")
|
||||||
exclusive_or(
|
exclusive_or(
|
||||||
[for(poly=polys) each polygon_parts(poly,nonzero,eps)],
|
[for(poly=polys) each polygon_parts(poly,nonzero,eps)],
|
||||||
eps=eps);
|
eps=eps);
|
||||||
@@ -321,7 +321,7 @@ function force_region(poly) = is_path(poly) ? [poly] : poly;
|
|||||||
// "intersect" = Anchors to the outer edge of the region.
|
// "intersect" = Anchors to the outer edge of the region.
|
||||||
// Example(2D): Displaying a region
|
// Example(2D): Displaying a region
|
||||||
// region([circle(d=50), square(25,center=true)]);
|
// region([circle(d=50), square(25,center=true)]);
|
||||||
// Example(2D): Displaying a list of polygons that intersect each other, which is not a region
|
// Example(2D): Displaying a list of polygons that intersect each other, which is not a region.
|
||||||
// rgn = concat(
|
// rgn = concat(
|
||||||
// [for (d=[50:-10:10]) circle(d=d-5)],
|
// [for (d=[50:-10:10]) circle(d=d-5)],
|
||||||
// [square([60,10], center=true)]
|
// [square([60,10], center=true)]
|
||||||
@@ -329,9 +329,9 @@ function force_region(poly) = is_path(poly) ? [poly] : poly;
|
|||||||
// region(rgn);
|
// region(rgn);
|
||||||
module region(r, anchor="origin", spin=0, cp="centroid", atype="hull")
|
module region(r, anchor="origin", spin=0, cp="centroid", atype="hull")
|
||||||
{
|
{
|
||||||
assert(in_list(atype, _ANCHOR_TYPES), "Anchor type must be \"hull\" or \"intersect\"");
|
assert(in_list(atype, _ANCHOR_TYPES), "\nAnchor type must be \"hull\" or \"intersect\".");
|
||||||
r = force_region(r);
|
r = force_region(r);
|
||||||
dummy=assert(is_region(r), "Input is not a region");
|
dummy=assert(is_region(r), "\nInput is not a region.");
|
||||||
points = flatten(r);
|
points = flatten(r);
|
||||||
lengths = [for(path=r) len(path)];
|
lengths = [for(path=r) len(path)];
|
||||||
starts = [0,each cumsum(lengths)];
|
starts = [0,each cumsum(lengths)];
|
||||||
@@ -416,8 +416,8 @@ module debug_region(region, vertices=true, edges=true, convexity=2, size=1)
|
|||||||
// move([x,y]) color("#ddf") circle(0.1, $fn=12);
|
// move([x,y]) color("#ddf") circle(0.1, $fn=12);
|
||||||
function point_in_region(point, region, eps=EPSILON) =
|
function point_in_region(point, region, eps=EPSILON) =
|
||||||
let(region=force_region(region))
|
let(region=force_region(region))
|
||||||
assert(is_region(region), "Region given to point_in_region is not a region")
|
assert(is_region(region), "\nRegion given to point_in_region is not a region.")
|
||||||
assert(is_vector(point,2), "Point must be a 2D point in point_in_region")
|
assert(is_vector(point,2), "\nPoint must be a 2D point in point_in_region.")
|
||||||
_point_in_region(point, region, eps);
|
_point_in_region(point, region, eps);
|
||||||
|
|
||||||
function _point_in_region(point, region, eps=EPSILON, i=0, cnt=0) =
|
function _point_in_region(point, region, eps=EPSILON, i=0, cnt=0) =
|
||||||
@@ -442,7 +442,7 @@ function _point_in_region(point, region, eps=EPSILON, i=0, cnt=0) =
|
|||||||
// Examples:
|
// Examples:
|
||||||
// area = region_area([square(10), right(20,square(8))]); // Returns 164
|
// area = region_area([square(10), right(20,square(8))]); // Returns 164
|
||||||
function region_area(region) =
|
function region_area(region) =
|
||||||
assert(is_region(region), "Input must be a region")
|
assert(is_region(region), "\nInput must be a region.")
|
||||||
let(
|
let(
|
||||||
parts = region_parts(region)
|
parts = region_parts(region)
|
||||||
)
|
)
|
||||||
@@ -468,7 +468,7 @@ function are_regions_equal(region1, region2, either_winding=false) =
|
|||||||
region1=force_region(region1),
|
region1=force_region(region1),
|
||||||
region2=force_region(region2)
|
region2=force_region(region2)
|
||||||
)
|
)
|
||||||
assert(is_region(region1) && is_region(region2), "One of the inputs is not a region")
|
assert(is_region(region1) && is_region(region2), "\nOne of the inputs is not a region.")
|
||||||
len(region1) != len(region2)? false :
|
len(region1) != len(region2)? false :
|
||||||
__are_regions_equal(either_winding?_clockwise_region(region1):region1,
|
__are_regions_equal(either_winding?_clockwise_region(region1):region1,
|
||||||
either_winding?_clockwise_region(region2):region2,
|
either_winding?_clockwise_region(region2):region2,
|
||||||
@@ -577,7 +577,7 @@ function _region_region_intersections(region1, region2, closed1=true,closed2=tru
|
|||||||
// the same list, but for the polygons in region2.
|
// the same list, but for the polygons in region2.
|
||||||
// You can pass a single polygon in for either region, but the output will be a singleton list, as if
|
// You can pass a single polygon in for either region, but the output will be a singleton list, as if
|
||||||
// you passed in a singleton region. If you set the closed parameters to false then the region components
|
// you passed in a singleton region. If you set the closed parameters to false then the region components
|
||||||
// will be treated as open paths instead of polygons.
|
// are treated as open paths instead of polygons.
|
||||||
// Arguments:
|
// Arguments:
|
||||||
// region1 = first region
|
// region1 = first region
|
||||||
// region2 = second region
|
// region2 = second region
|
||||||
@@ -599,7 +599,7 @@ function split_region_at_region_crossings(region1, region2, closed1=true, closed
|
|||||||
region1=force_region(region1),
|
region1=force_region(region1),
|
||||||
region2=force_region(region2)
|
region2=force_region(region2)
|
||||||
)
|
)
|
||||||
assert(is_region(region1) && is_region(region2),"One of the inputs is not a region")
|
assert(is_region(region1) && is_region(region2),"\nOne of the inputs is not a region.")
|
||||||
let(
|
let(
|
||||||
xings = _region_region_intersections(region1, region2, closed1, closed2, eps),
|
xings = _region_region_intersections(region1, region2, closed1, closed2, eps),
|
||||||
regions = [region1,region2],
|
regions = [region1,region2],
|
||||||
@@ -636,7 +636,7 @@ function split_region_at_region_crossings(region1, region2, closed1=true, closed
|
|||||||
// rgns = region_parts(region);
|
// rgns = region_parts(region);
|
||||||
// Description:
|
// Description:
|
||||||
// Divides a region into a list of connected regions. Each connected region has exactly one clockwise outside boundary
|
// Divides a region into a list of connected regions. Each connected region has exactly one clockwise outside boundary
|
||||||
// and zero or more counter-clockwise outlines defining internal holes. Note that behavior is undefined on invalid regions whose
|
// and zero or more counter-clockwise outlines defining internal holes. Behavior is undefined on invalid regions whose
|
||||||
// components cross each other.
|
// components cross each other.
|
||||||
// Example(2D,NoAxes):
|
// Example(2D,NoAxes):
|
||||||
// R = [for(i=[1:7]) square(i,center=true)];
|
// R = [for(i=[1:7]) square(i,center=true)];
|
||||||
@@ -654,7 +654,7 @@ function region_parts(region) =
|
|||||||
let(
|
let(
|
||||||
region = force_region(region)
|
region = force_region(region)
|
||||||
)
|
)
|
||||||
assert(is_region(region), "Input is not a region")
|
assert(is_region(region), "\nInput is not a region.")
|
||||||
let(
|
let(
|
||||||
inside = [for(i=idx(region))
|
inside = [for(i=idx(region))
|
||||||
let(pt = mean([region[i][0], region[i][1]]))
|
let(pt = mean([region[i][0], region[i][1]]))
|
||||||
@@ -705,7 +705,7 @@ function _offset_chamfer(center, points, delta) =
|
|||||||
|
|
||||||
|
|
||||||
function _shift_segment(segment, d) =
|
function _shift_segment(segment, d) =
|
||||||
assert(!approx(segment[0],segment[1]),"Path has repeated points")
|
assert(!approx(segment[0],segment[1]),"\nPath has repeated points.")
|
||||||
move(d*line_normal(segment),segment);
|
move(d*line_normal(segment),segment);
|
||||||
|
|
||||||
|
|
||||||
@@ -781,7 +781,7 @@ function _good_segments(path, d, shiftsegs, closed, quality) =
|
|||||||
// we want to quit as soon as we find a point with distance > d, hence the recursive code structure.
|
// we want to quit as soon as we find a point with distance > d, hence the recursive code structure.
|
||||||
//
|
//
|
||||||
// This test is approximate because it only samples the points listed in alpha. Listing more points
|
// This test is approximate because it only samples the points listed in alpha. Listing more points
|
||||||
// will make the test more accurate, but slower.
|
// makes the test more accurate, but slower.
|
||||||
function _segment_good(path,pathseg_unit,pathseg_len, d, seg,alpha ,index=0) =
|
function _segment_good(path,pathseg_unit,pathseg_len, d, seg,alpha ,index=0) =
|
||||||
index == len(alpha) ? false :
|
index == len(alpha) ? false :
|
||||||
_point_dist(path,pathseg_unit,pathseg_len, alpha[index]*seg[0]+(1-alpha[index])*seg[1]) > d ? true :
|
_point_dist(path,pathseg_unit,pathseg_len, alpha[index]*seg[0]+(1-alpha[index])*seg[1]) > d ? true :
|
||||||
@@ -808,8 +808,8 @@ function _point_dist(path,pathseg_unit,pathseg_len,pt) =
|
|||||||
// SynTags: Path, Region, Ext
|
// SynTags: Path, Region, Ext
|
||||||
// Topics: Paths, Polygons, Regions
|
// Topics: Paths, Polygons, Regions
|
||||||
// Usage:
|
// Usage:
|
||||||
// offsetpath = offset(path, [r=|delta=], [chamfer=], [closed=], [check_valid=], [quality=], [same_length=])
|
// offsetpath = offset(path, [r=|delta=], [chamfer=], [closed=], [check_valid=], [quality=], [error=], [same_length=])
|
||||||
// path_faces = offset(path, return_faces=true, [r=|delta=], [chamfer=], [closed=], [check_valid=], [quality=], [firstface_index=], [flip_faces=])
|
// path_faces = offset(path, return_faces=true, [r=|delta=], [chamfer=], [closed=], [check_valid=], [quality=], [error=], [firstface_index=], [flip_faces=])
|
||||||
// Description:
|
// Description:
|
||||||
// Takes a 2D input path, polygon or region and returns a path offset by the specified amount. As with the built-in
|
// Takes a 2D input path, polygon or region and returns a path offset by the specified amount. As with the built-in
|
||||||
// offset() module, you can use `r` to specify rounded offset and `delta` to specify offset with
|
// offset() module, you can use `r` to specify rounded offset and `delta` to specify offset with
|
||||||
@@ -817,12 +817,12 @@ function _point_dist(path,pathseg_unit,pathseg_len,pt) =
|
|||||||
// When `closed=true` (the default), the input is treated as a polygon. If the input is a region it is treated as a collection
|
// When `closed=true` (the default), the input is treated as a polygon. If the input is a region it is treated as a collection
|
||||||
// of polygons. In this case, positive offset values make the shape larger. If you set `closed=false` then the input is treated as a path
|
// of polygons. In this case, positive offset values make the shape larger. If you set `closed=false` then the input is treated as a path
|
||||||
// with distinct start and end points. For paths, positive offsets shifts the path to the left, relative to the direction of the path.
|
// with distinct start and end points. For paths, positive offsets shifts the path to the left, relative to the direction of the path.
|
||||||
// Note that a path that happens to end at its starting point is not the same as a polygon and the offset result may differ and the ends.
|
// Note that a path that happens to end at its starting point is not the same as a polygon, and the offset result may differ at the ends.
|
||||||
// .
|
// .
|
||||||
// If you use `delta` without chamfers, the path must not include any 180 degree turns, where the path
|
// If you use `delta` without chamfers, the path must not include any 180 degree turns, where the path
|
||||||
// reverses direction. Such reversals result in an offset with two parallel segments, so they cannot be
|
// reverses direction. Such reversals result in an offset with two parallel segments, so they cannot be
|
||||||
// extended to an intersection point. If you select chamfering the reversals are permitted and will result
|
// extended to an intersection point. If you select chamfering, the reversals are permitted and result
|
||||||
// in a single segment connecting the parallel segments. With rounding, a semi-circle will connect the two offset segments.
|
// in a single segment connecting the parallel segments. With rounding, a semi-circle connects the two offset segments.
|
||||||
// Note also that repeated points are always illegal in the input; remove them first with {{deduplicate()}}.
|
// Note also that repeated points are always illegal in the input; remove them first with {{deduplicate()}}.
|
||||||
// .
|
// .
|
||||||
// When offsets shrink the path, segments cross and become invalid. By default `offset()` checks
|
// When offsets shrink the path, segments cross and become invalid. By default `offset()` checks
|
||||||
@@ -834,12 +834,12 @@ function _point_dist(path,pathseg_unit,pathseg_len,pt) =
|
|||||||
// The erroneous removal of segments is more common when your input
|
// The erroneous removal of segments is more common when your input
|
||||||
// contains very small segments and in this case can result in an invalid situation where the remaining
|
// contains very small segments and in this case can result in an invalid situation where the remaining
|
||||||
// valid segments are parallel and cannot be connected to form an offset curve. If this happens, you
|
// valid segments are parallel and cannot be connected to form an offset curve. If this happens, you
|
||||||
// will get an error message to this effect. The only solutions are to either remove the small segments with {{deduplicate()}},
|
// get an error message to this effect. The only solutions are either to remove the small segments with {{deduplicate()}},
|
||||||
// or if your path permits it, to set check_valid to false.
|
// or if your path permits it, to set check_valid to false.
|
||||||
// .
|
// .
|
||||||
// Another situation that can arise with validity testing is that the test is not sufficiently thorough and some
|
// Another situation that can arise with validity testing is that the test is not sufficiently thorough and some
|
||||||
// segments persist that should be eliminated. In this case, increase `quality` from its default of 1 to a value of 2 or 3.
|
// segments persist that should be eliminated. In this case, increase `quality` from its default of 1 to a value of 2 or 3.
|
||||||
// This increases the number of samples on the segment that are checked, so it will increase run time. In
|
// This increases the number of samples on the segment that are checked, so it also increase run time. In
|
||||||
// some situations you may be able to decrease run time by setting quality to 0, which causes only
|
// some situations you may be able to decrease run time by setting quality to 0, which causes only
|
||||||
// segment ends to be checked.
|
// segment ends to be checked.
|
||||||
// .
|
// .
|
||||||
@@ -849,12 +849,12 @@ function _point_dist(path,pathseg_unit,pathseg_len,pt) =
|
|||||||
// difficult to associate with the input. If you want to maintain alignment between the points you
|
// difficult to associate with the input. If you want to maintain alignment between the points you
|
||||||
// can use the `same_length` option. This option requires that you use `delta=` with `chamfer=false` to ensure
|
// can use the `same_length` option. This option requires that you use `delta=` with `chamfer=false` to ensure
|
||||||
// that no points are added. with `same_length`, when points collapse to a single point in the offset, the output includes
|
// that no points are added. with `same_length`, when points collapse to a single point in the offset, the output includes
|
||||||
// that point repeated to preserve the correct length. Generally repeated points will not appear in the offset output
|
// that point repeated to preserve the correct length. Generally, repeated points do not appear in the offset output
|
||||||
// unless you set `same_length` to true, but in some rare circumstances involving very short segments, it is possible for the
|
// unless you set `same_length=true`, but in some rare circumstances involving short segments, it is possible for the
|
||||||
// repeated points to occur in the output, even when `same_length=false`.
|
// repeated points to occur in the output, even when `same_length=false`.
|
||||||
// .
|
// .
|
||||||
// Another way to obtain alignment information is to use the return_faces option, which can
|
// Another way to obtain alignment information is to use the return_faces option, which can
|
||||||
// provide alignment information for all offset parameters: it returns a face list which lists faces between
|
// provide alignment information for all offset parameters: it returns a face list that lists faces between
|
||||||
// the original path and the offset path where the vertices are ordered with the original path
|
// the original path and the offset path where the vertices are ordered with the original path
|
||||||
// first, starting at `firstface_index` and the offset path vertices appearing afterwords. The
|
// first, starting at `firstface_index` and the offset path vertices appearing afterwords. The
|
||||||
// direction of the faces can be flipped using `flip_faces`. When you request faces the return
|
// direction of the faces can be flipped using `flip_faces`. When you request faces the return
|
||||||
@@ -862,20 +862,22 @@ function _point_dist(path,pathseg_unit,pathseg_len,pt) =
|
|||||||
// Arguments:
|
// Arguments:
|
||||||
// path = the path to process. A list of 2d points.
|
// path = the path to process. A list of 2d points.
|
||||||
// ---
|
// ---
|
||||||
// r = offset radius. Distance to offset. Will round over corners.
|
// r = offset radius. Distance to offset, rounds over corners.
|
||||||
// delta = offset distance. Distance to offset with pointed corners.
|
// delta = Distance to offset with pointed corners.
|
||||||
// chamfer = chamfer corners when you specify `delta`. Default: false
|
// chamfer = Chamfer corners when you specify `delta`. Default: false
|
||||||
// closed = if true path is treated as a polygon. Default: True.
|
// closed = If true, path is treated as a polygon. Default: True.
|
||||||
// check_valid = perform segment validity check. Default: True.
|
// check_valid = Perform segment validity check. Default: True.
|
||||||
// quality = validity check quality parameter, a small integer. Default: 1.
|
// quality = Validity check quality parameter, a small integer. Default: 1.
|
||||||
// same_length = return a path with the same length as the input. Only compatible with `delta=`. Default: false
|
// error = If true, assert an error if offset path is degenerate. If false, return an empty list `[]` for a degenerate path. Default: true
|
||||||
// return_faces = return face list. Default: False.
|
// same_length = Return a path with the same length as the input. Only compatible with `delta=`. Default: false
|
||||||
// firstface_index = starting index for face list. Default: 0.
|
// return_faces = Return face list. Default: False.
|
||||||
// flip_faces = flip face direction. Default: false
|
// firstface_index = Starting index for face list. Default: 0.
|
||||||
|
// flip_faces = Flip face direction. Default: false
|
||||||
// Example(2D,NoAxes): Offset the red star out by 10 units.
|
// Example(2D,NoAxes): Offset the red star out by 10 units.
|
||||||
// star = star(5, r=100, ir=30);
|
// star = star(5, r=100, ir=30);
|
||||||
// stroke(closed=true, star, width=3, color="red");
|
// stroke(closed=true, star, width=3, color="red");
|
||||||
// stroke(closed=true, width=3, offset(star, delta=10, closed=true));
|
// stroke(closed=true, width=3,
|
||||||
|
// offset(star, delta=10, closed=true));
|
||||||
// Example(2D,NoAxes): Offset the star with chamfering
|
// Example(2D,NoAxes): Offset the star with chamfering
|
||||||
// star = star(5, r=100, ir=30);
|
// star = star(5, r=100, ir=30);
|
||||||
// stroke(closed=true, star, width=3, color="red");
|
// stroke(closed=true, star, width=3, color="red");
|
||||||
@@ -938,7 +940,7 @@ function _point_dist(path,pathseg_unit,pathseg_len,pt) =
|
|||||||
// op=offset(path, r=1.5,closed=true);
|
// op=offset(path, r=1.5,closed=true);
|
||||||
// stroke([path],width=.1,color="red");
|
// stroke([path],width=.1,color="red");
|
||||||
// stroke([op],width=.1);
|
// stroke([op],width=.1);
|
||||||
// Example(2D,NoAxes): With the default quality value, this case produces the wrong answer. This happens because the offset edge corresponding to the long left edge (shown in green) is erroneously flagged as invalid. If you use `r=` instead of `delta=` then this will fail with an error.
|
// Example(2D,NoAxes): With the default quality value, this case produces the wrong answer. This happens because the offset edge corresponding to the long left edge (shown in green) is erroneously flagged as invalid. If you use `r=` instead of `delta=` then this fails with an error.
|
||||||
// test = [[0,0],[10,0],[10,7],[0,7], [-1,-3]];
|
// test = [[0,0],[10,0],[10,7],[0,7], [-1,-3]];
|
||||||
// polygon(offset(test,delta=-1.9, closed=true));
|
// polygon(offset(test,delta=-1.9, closed=true));
|
||||||
// stroke([test],width=.1,color="red");
|
// stroke([test],width=.1,color="red");
|
||||||
@@ -955,7 +957,7 @@ function _point_dist(path,pathseg_unit,pathseg_len,pt) =
|
|||||||
// color("red")
|
// color("red")
|
||||||
// stroke(offset(star, delta=-10, closed=true, check_valid=false), // Fails if check_valid=true
|
// stroke(offset(star, delta=-10, closed=true, check_valid=false), // Fails if check_valid=true
|
||||||
// width=.3,closed=true);
|
// width=.3,closed=true);
|
||||||
// Example(2D): But if you use rounding with offset then you need `check_valid=true` when `r` is big enough. It works without the validity check as long as the offset shape retains a some of the straight edges at the star tip, but once the shape shrinks smaller than that, it fails. There is no simple way to get a correct result for the case with `r=10`, because as in the previous example, it will fail if you turn on validity checks.
|
// Example(2D): But if you use rounding with offset then you need `check_valid=true` when `r` is big enough. It works without the validity check as long as the offset shape retains a some of the straight edges at the star tip, but once the shape shrinks smaller than that, it fails. There is no simple way to get a correct result for the case with `r=10`, because as in the previous example, it fails if you turn on validity checks.
|
||||||
// star = star(5, r=22, ir=13);
|
// star = star(5, r=22, ir=13);
|
||||||
// color("green")
|
// color("green")
|
||||||
// stroke(offset(star, r=-8, closed=true,check_valid=false), width=.1, closed=true);
|
// stroke(offset(star, r=-8, closed=true,check_valid=false), width=.1, closed=true);
|
||||||
@@ -977,24 +979,25 @@ function _point_dist(path,pathseg_unit,pathseg_len,pt) =
|
|||||||
// square([40,20], center=true)));
|
// square([40,20], center=true)));
|
||||||
// stroke(rgn, width=1, color="red");
|
// stroke(rgn, width=1, color="red");
|
||||||
// region(offset(rgn, r=-5));
|
// region(offset(rgn, r=-5));
|
||||||
// Example(2D,NoAxes): Using `same_length=true` to align the original curve to the offset. Note that lots of points map to the corner at the top.
|
// Example(2D,NoAxes): Using `same_length=true` to align the original curve to the offset. Many points map to the corner at the top.
|
||||||
// closed=false;
|
// closed=false;
|
||||||
// path = [for(angle=[0:5:180]) 10*[angle/100,2*sin(angle)]];
|
// path = [for(angle=[0:5:180]) 10*[angle/100,2*sin(angle)]];
|
||||||
// opath = offset(path,delta=-3,same_length=true,closed=closed);
|
// opath = offset(path,delta=-3,same_length=true,closed=closed);
|
||||||
// stroke(path,closed=closed,width=.3);
|
// stroke(path,closed=closed,width=.3);
|
||||||
// stroke(opath,closed=closed,width=.3);
|
// stroke(opath,closed=closed,width=.3);
|
||||||
// color("red") for(i=idx(path)) stroke([path[i],opath[i]],width=.3);
|
// for(i=idx(path))
|
||||||
|
// stroke([path[i],opath[i]],width=.3,color="red");
|
||||||
|
|
||||||
function offset(
|
function offset(
|
||||||
path, r=undef, delta=undef, chamfer=false,
|
path, r=undef, delta=undef, chamfer=false,
|
||||||
closed=true, check_valid=true,
|
closed=true, check_valid=true,
|
||||||
quality=1, return_faces=false, firstface_index=0,
|
quality=1, error=true, return_faces=false, firstface_index=0,
|
||||||
flip_faces=false, same_length=false
|
flip_faces=false, same_length=false
|
||||||
) =
|
) =
|
||||||
assert(!(same_length && return_faces), "Cannot combine return_faces with same_length")
|
assert(!(same_length && return_faces), "\nCannot combine return_faces with same_length.")
|
||||||
is_region(path)?
|
is_region(path)?
|
||||||
assert(closed, "cannot set closed=false for a region")
|
assert(closed, "\nCannot set closed=false for a region.")
|
||||||
assert(!return_faces, "return_faces not supported for regions.")
|
assert(!return_faces, "\nParameter return_faces is not supported for regions.")
|
||||||
let(
|
let(
|
||||||
ofsregs = [for(R=region_parts(path))
|
ofsregs = [for(R=region_parts(path))
|
||||||
difference([for(i=idx(R)) offset(R[i], r=u_mul(i>0?-1:1,r), delta=u_mul(i>0?-1:1,delta),
|
difference([for(i=idx(R)) offset(R[i], r=u_mul(i>0?-1:1,r), delta=u_mul(i>0?-1:1,delta),
|
||||||
@@ -1003,9 +1006,9 @@ function offset(
|
|||||||
union(ofsregs)
|
union(ofsregs)
|
||||||
:
|
:
|
||||||
let(rcount = num_defined([r,delta]))
|
let(rcount = num_defined([r,delta]))
|
||||||
assert(rcount==1,"Must define exactly one of 'delta' and 'r'")
|
assert(rcount==1,"\nMust define exactly one of 'delta' and 'r'.")
|
||||||
assert(!same_length || (is_def(delta) && !chamfer), "Must specify delta, with chamfer=false, when same_length=true")
|
assert(!same_length || (is_def(delta) && !chamfer), "\nMust specify delta with chamfer=false, when same_length=true.")
|
||||||
assert(is_path(path), "Input must be a path or region")
|
assert(is_path(path), "\nInput must be a path or region.")
|
||||||
let(
|
let(
|
||||||
chamfer = is_def(r) ? false : chamfer,
|
chamfer = is_def(r) ? false : chamfer,
|
||||||
quality = max(0,round(quality)),
|
quality = max(0,round(quality)),
|
||||||
@@ -1022,10 +1025,12 @@ function offset(
|
|||||||
good = check_valid ? _good_segments(path, abs(d), shiftsegs, closed, quality)
|
good = check_valid ? _good_segments(path, abs(d), shiftsegs, closed, quality)
|
||||||
: repeat(true,len(shiftsegs)),
|
: repeat(true,len(shiftsegs)),
|
||||||
goodsegs = bselect(shiftsegs, good),
|
goodsegs = bselect(shiftsegs, good),
|
||||||
goodpath = bselect(path,good)
|
goodpath = bselect(path,good),
|
||||||
|
degenerate = (len(goodsegs)-(!closed && select(good,-1)?1:0) <= 0)
|
||||||
)
|
)
|
||||||
assert(len(goodsegs)-(!closed && select(good,-1)?1:0)>0,"Offset of path is degenerate")
|
degenerate && error ? assert(false, "\nOffset of path is degenerate.")
|
||||||
let(
|
: degenerate && !error ? [] // return empty path
|
||||||
|
: let(
|
||||||
// Extend the shifted segments to their intersection points. For open curves the endpoints
|
// Extend the shifted segments to their intersection points. For open curves the endpoints
|
||||||
// are simply the endpoints of the shifted segments. If segments are parallel then the intersection
|
// are simply the endpoints of the shifted segments. If segments are parallel then the intersection
|
||||||
// points will be undef
|
// points will be undef
|
||||||
@@ -1038,12 +1043,12 @@ function offset(
|
|||||||
cornercheck = [for(i=idx(goodsegs)) (!closed && (i==0 || i==len(goodsegs)-1))
|
cornercheck = [for(i=idx(goodsegs)) (!closed && (i==0 || i==len(goodsegs)-1))
|
||||||
|| is_def(sharpcorners[i])
|
|| is_def(sharpcorners[i])
|
||||||
|| approx(unit(deltas(select(goodsegs,i-1))[0]) * unit(deltas(goodsegs[i])[0]),-1)],
|
|| approx(unit(deltas(select(goodsegs,i-1))[0]) * unit(deltas(goodsegs[i])[0]),-1)],
|
||||||
dummyA = assert(len(sharpcorners)==2 || all(cornercheck),"Two consecutive valid offset segments are parallel but do not meet at their ends, maybe because path contains very short segments that were mistakenly flagged as invalid; unable to compute offset. If you get this error from offset_sweep() try setting ofset=\"delta\""),
|
dummyA = assert(len(sharpcorners)==2 || all(cornercheck),"\nTwo consecutive valid offset segments are parallel but do not meet at their ends, maybe because path contains very short segments that were mistakenly flagged as invalid; unable to compute offset. If you get this error from offset_sweep() try setting ofset=\"delta\"."),
|
||||||
reversecheck =
|
reversecheck =
|
||||||
!same_length
|
!same_length
|
||||||
|| !(is_def(delta) && !chamfer) // Reversals only a problem in delta mode without chamfers
|
|| !(is_def(delta) && !chamfer) // Reversals only a problem in delta mode without chamfers
|
||||||
|| all_defined(sharpcorners),
|
|| all_defined(sharpcorners),
|
||||||
dummyB = assert(reversecheck, "Either validity check failed and removed a valid segment or the input 'path' contains a segment that reverses direction (180 deg turn). Path reversals are not allowed when same_length is true because they increase path length."),
|
dummyB = assert(reversecheck, "\nEither validity check failed and removed a valid segment or the input 'path' contains a segment that reverses direction (180 deg turn). Path reversals are not allowed when same_length is true because they increase path length."),
|
||||||
// This is a Boolean array that indicates whether a corner is an outside or inside corner
|
// This is a Boolean array that indicates whether a corner is an outside or inside corner
|
||||||
// For outside corners, the new corner is an extension (angle 0), for inside corners, it turns backward (angle 180)
|
// For outside corners, the new corner is an extension (angle 0), for inside corners, it turns backward (angle 180)
|
||||||
// If either side turns back it is an inside corner---must check both.
|
// If either side turns back it is an inside corner---must check both.
|
||||||
@@ -1067,7 +1072,7 @@ function offset(
|
|||||||
: let(vang = vector_angle(select(goodsegs,i-1)[1]-goodpath[i],
|
: let(vang = vector_angle(select(goodsegs,i-1)[1]-goodpath[i],
|
||||||
goodsegs[i][0]-goodpath[i]))
|
goodsegs[i][0]-goodpath[i]))
|
||||||
assert(!outsidecorner[i] || vang!=0, // If outsidecorner[i] is true then vang>0 needed to give valid step count
|
assert(!outsidecorner[i] || vang!=0, // If outsidecorner[i] is true then vang>0 needed to give valid step count
|
||||||
"Offset computation failed, probably because validity check mistakenly removed a valid segment. Increasing quality might fix this.")
|
"\nOffset computation failed, probably because validity check mistakenly removed a valid segment. Increasing quality might fix this.")
|
||||||
1+floor(segs(r)*vang/360)
|
1+floor(segs(r)*vang/360)
|
||||||
],
|
],
|
||||||
// newcorners is a list where each entry is a list of the points that correspond to a single point in the sharpcorners
|
// newcorners is a list where each entry is a list of the points that correspond to a single point in the sharpcorners
|
||||||
@@ -1206,7 +1211,8 @@ function _list_three(a,b,c) =
|
|||||||
// shape1 = move([-8,-8,0], p=circle(d=50));
|
// shape1 = move([-8,-8,0], p=circle(d=50));
|
||||||
// shape2 = move([ 8, 8,0], p=circle(d=50));
|
// shape2 = move([ 8, 8,0], p=circle(d=50));
|
||||||
// color("green") region(union(shape1,shape2));
|
// color("green") region(union(shape1,shape2));
|
||||||
// for (shape = [shape1,shape2]) color("red") stroke(shape, width=0.5, closed=true);
|
// for (shape = [shape1,shape2])
|
||||||
|
// stroke(shape, width=0.5, closed=true, color="red");
|
||||||
function union(regions=[],b=undef,c=undef,eps=EPSILON) =
|
function union(regions=[],b=undef,c=undef,eps=EPSILON) =
|
||||||
let(regions=_list_three(regions,b,c))
|
let(regions=_list_three(regions,b,c))
|
||||||
len(regions)==0? [] :
|
len(regions)==0? [] :
|
||||||
@@ -1242,7 +1248,8 @@ function union(regions=[],b=undef,c=undef,eps=EPSILON) =
|
|||||||
// Example(2D):
|
// Example(2D):
|
||||||
// shape1 = move([-8,-8,0], p=circle(d=50));
|
// shape1 = move([-8,-8,0], p=circle(d=50));
|
||||||
// shape2 = move([ 8, 8,0], p=circle(d=50));
|
// shape2 = move([ 8, 8,0], p=circle(d=50));
|
||||||
// for (shape = [shape1,shape2]) color("red") stroke(shape, width=0.5, closed=true);
|
// for (shape = [shape1,shape2])
|
||||||
|
// stroke(shape, width=0.5, color="red", closed=true);
|
||||||
// color("green") region(difference(shape1,shape2));
|
// color("green") region(difference(shape1,shape2));
|
||||||
function difference(regions=[],b=undef,c=undef,eps=EPSILON) =
|
function difference(regions=[],b=undef,c=undef,eps=EPSILON) =
|
||||||
let(regions = _list_three(regions,b,c))
|
let(regions = _list_three(regions,b,c))
|
||||||
@@ -1277,7 +1284,8 @@ function difference(regions=[],b=undef,c=undef,eps=EPSILON) =
|
|||||||
// Example(2D):
|
// Example(2D):
|
||||||
// shape1 = move([-8,-8,0], p=circle(d=50));
|
// shape1 = move([-8,-8,0], p=circle(d=50));
|
||||||
// shape2 = move([ 8, 8,0], p=circle(d=50));
|
// shape2 = move([ 8, 8,0], p=circle(d=50));
|
||||||
// for (shape = [shape1,shape2]) color("red") stroke(shape, width=0.5, closed=true);
|
// for (shape = [shape1,shape2])
|
||||||
|
// stroke(shape,width=0.5, color="red", closed=true);
|
||||||
// color("green") region(intersection(shape1,shape2));
|
// color("green") region(intersection(shape1,shape2));
|
||||||
function intersection(regions=[],b=undef,c=undef,eps=EPSILON) =
|
function intersection(regions=[],b=undef,c=undef,eps=EPSILON) =
|
||||||
let(regions = _list_three(regions,b,c))
|
let(regions = _list_three(regions,b,c))
|
||||||
@@ -1307,7 +1315,7 @@ function intersection(regions=[],b=undef,c=undef,eps=EPSILON) =
|
|||||||
// When called as a function and given a list of regions or 2D polygons,
|
// When called as a function and given a list of regions or 2D polygons,
|
||||||
// returns the exclusive_or of all given regions. Result is a single region.
|
// returns the exclusive_or of all given regions. Result is a single region.
|
||||||
// When called as a module, performs a Boolean exclusive-or of up to 10 children. Note that when
|
// When called as a module, performs a Boolean exclusive-or of up to 10 children. Note that when
|
||||||
// the input regions cross each other the exclusive-or operator will produce shapes that
|
// the input regions cross each other, the exclusive-or operator produces shapes that
|
||||||
// meet at corners (non-simple regions), which do not render in CGAL.
|
// meet at corners (non-simple regions), which do not render in CGAL.
|
||||||
// This function is **much** slower than the native intersection module acting on geometry,
|
// This function is **much** slower than the native intersection module acting on geometry,
|
||||||
// so you should only use it when you need a point list for further processing.
|
// so you should only use it when you need a point list for further processing.
|
||||||
@@ -1451,7 +1459,7 @@ module exclusive_or() {
|
|||||||
children(9);
|
children(9);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
assert($children<=10, "exclusive_or() can only handle up to 10 children.");
|
assert($children<=10, "\nexclusive_or() can handle up to 10 children.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1466,7 +1474,7 @@ module exclusive_or() {
|
|||||||
// hull_region(region);
|
// hull_region(region);
|
||||||
// Description:
|
// Description:
|
||||||
// Given a path, or a region, compute the convex hull
|
// Given a path, or a region, compute the convex hull
|
||||||
// and return it as a path. This differs from {{hull()}} and {{hull2d_path()}} which
|
// and return it as a path. This differs from {{hull()}} and {{hull2d_path()}}, which
|
||||||
// return an index list into the point list. As a module invokes the native hull() on
|
// return an index list into the point list. As a module invokes the native hull() on
|
||||||
// the specified region.
|
// the specified region.
|
||||||
// Arguments:
|
// Arguments:
|
||||||
|
Reference in New Issue
Block a user