diff --git a/geometry.scad b/geometry.scad index ec4e4ea..7dab6e8 100644 --- a/geometry.scad +++ b/geometry.scad @@ -158,7 +158,12 @@ function line_normal(p1,p2) = // the intersection lies on the segment. Otherwise it lies somewhere on // the extension of the segment. If lines are parallel or coincident then // it returns undef. + +// This kludge of calling path2d is because vnf_bend passed 3d input. FIXME! + function _general_line_intersection(s1,s2,eps=EPSILON) = + len(s1[0])==3 ? _general_line_intersection(path2d(s1), path2d(s2),eps) + : let( denominator = cross(s1[0]-s1[1],s2[0]-s2[1]) ) @@ -1471,7 +1476,7 @@ function polygon_normal(poly) = // color("red")back(28/(2/3))text("Even-Odd", size=5/(2/3), halign="center"); // } // right(40){ -// dp = polygon_parts(path,closed=true); +// dp = polygon_parts(path,nonzero=true); // region(dp); // color("red"){stroke(path,width=1,closed=true); // back(28/(2/3))text("Nonzero", size=5/(2/3), halign="center"); @@ -1482,7 +1487,7 @@ function polygon_normal(poly) = // poly = The list of 2D points forming the perimeter of the polygon. // nonzero = The rule to use: true for "Nonzero" rule and false for "Even-Odd". Default: false (Even-Odd) // eps = Tolerance in geometric comparisons. Default: `EPSILON` (1e-9) -// Example(2D): With nonzero set to true, we get this result. Green dots are inside the polygon and red are outside: +// Example(2D): With nonzero set to false (the default), we get this result. Green dots are inside the polygon and red are outside: // a=20*2/3; // b=30*2/3; // ofs = 17*2/3; @@ -1496,9 +1501,9 @@ function polygon_normal(poly) = // pts = [[0,0],[10,0],[0,20]]; // for(p=pts){ // color(point_in_polygon(p,path)==1 ? "green" : "red") -// move(p)circle(r=1, $fn=12); +// move(p)circle(r=1.5, $fn=12); // } -// Example(2D): With nonzero set to false, one dot changes color: +// Example(2D): With nonzero set to true, one dot changes color: // a=20*2/3; // b=30*2/3; // ofs = 17*2/3; @@ -1511,8 +1516,8 @@ function polygon_normal(poly) = // stroke(path,closed=true); // pts = [[0,0],[10,0],[0,20]]; // for(p=pts){ -// color(point_in_polygon(p,path,nonzero=false)==1 ? "green" : "red") -// move(p)circle(r=1, $fn=12); +// color(point_in_polygon(p,path,nonzero=true)==1 ? "green" : "red") +// move(p)circle(r=1.5, $fn=12); // } // Internal function for point_in_polygon diff --git a/paths.scad b/paths.scad index a5f283d..bd4b403 100644 --- a/paths.scad +++ b/paths.scad @@ -125,6 +125,56 @@ function path_merge_collinear(path, closed=false, eps=EPSILON) = ) [for (i=indices) path[i]]; +// Function: are_polygons_equal() +// Usage: +// b = are_polygons_equal(poly1, poly2, [eps]) +// Description: +// Returns true if poly1 and poly2 are the same polongs +// within given epsilon tolerance. +// Arguments: +// poly1 = first polygon +// poly2 = second polygon +// eps = tolerance for comparison +// Example(NORENDER): +// are_polygons_equal(pentagon(r=4), +// rot(360/5, p=pentagon(r=4))); // returns true +// are_polygons_equal(pentagon(r=4), +// rot(90, p=pentagon(r=4))); // returns false +function are_polygons_equal(poly1, poly2, eps=EPSILON) = + let( + poly1 = cleanup_path(poly1), + poly2 = cleanup_path(poly2), + l1 = len(poly1), + l2 = len(poly2) + ) l1 != l2 ? false : + let( maybes = find_first_match(poly1[0], poly2, eps=eps, all=true) ) + maybes == []? false : + [for (i=maybes) if (_are_polygons_equal(poly1, poly2, eps, i)) 1] != []; + +function _are_polygons_equal(poly1, poly2, eps, st) = + max([for(d=poly1-select(poly2,st,st-1)) d*d])= len(polys)? false : + are_polygons_equal(poly, polys[i])? true : + __is_polygon_in_list(poly, polys, i+1); + + + // Section: Path length calculation @@ -211,33 +261,34 @@ function _path_self_intersections(path, closed=true, eps=EPSILON) = let( path = cleanup_path(path, eps=eps), plen = len(path) - ) [ - for (i = [0:1:plen-(closed?2:3)], j=[i+2:1:plen-(closed?1:2)]) let( - a1 = path[i], - a2 = path[(i+1)%plen], - b1 = path[j], - b2 = path[(j+1)%plen], - isect = - (max(a1.x, a2.x) < min(b1.x, b2.x))? undef : - (min(a1.x, a2.x) > max(b1.x, b2.x))? undef : - (max(a1.y, a2.y) < min(b1.y, b2.y))? undef : - (min(a1.y, a2.y) > max(b1.y, b2.y))? undef : + ) + [ + for (i = [0:1:plen-(closed?2:3)]) + let( + a1 = path[i], + a2 = path[(i+1)%plen], + maxax = max(a1.x,a2.x), + minax = min(a1.x,a2.x), + maxay = max(a1.y,a2.y), + minay = min(a1.y,a2.y) + ) + for(j=[i+2:1:plen-(closed?1:2)]) let( - c = a1-a2, - d = b1-b2, - denom = (c.x*d.y)-(c.y*d.x) - ) abs(denom)=-eps && isect[1]<=1+eps && - isect[2]>=-eps && isect[2]<=1+eps - ) [isect[0], i, isect[1], j, isect[2]] + b1 = path[j], + b2 = path[(j+1)%plen], + isect = + maxax < b1.x && maxax < b2.x || + minax > b1.x && minax > b2.x || + maxay < b1.y && maxay < b2.y || + minay > b1.y && minay > b2.y + ? undef + : _general_line_intersection([a1,a2],[b1,b2]) + ) + if ((!closed || i!=0 || j!=plen-1) + && isect != undef + && isect[1]>=-eps && isect[1]<=1+eps + && isect[2]>=-eps && isect[2]<=1+eps) + [isect[0], i, isect[1], j, isect[2]] ]; @@ -434,12 +485,22 @@ function resample_path(path, N, spacing, closed=false) = // bool = is_path_simple(path, [closed], [eps]); // Description: // Returns true if the path is simple, meaning that it has no self-intersections. +// Repeated points are not considered self-intersections: a path with such points can +// still be simple. // If closed is set to true then treat the path as a polygon. // Arguments: // path = path to check // closed = set to true to treat path as a polygon. Default: false // eps = Epsilon error value used for determine if points coincide. Default: `EPSILON` (1e-9) function is_path_simple(path, closed=false, eps=EPSILON) = + [for(i=[0:1:len(path)-(closed?2:3)]) + let(v1=path[i+1]-path[i], + v2=select(path,i+2)-path[i+1], + normv1 = norm(v1), + normv2 = norm(v2) + ) + if (approx(v1*v2/normv1/normv2,-1)) 1] == [] + && _path_self_intersections(path,closed=closed,eps=eps) == []; @@ -1043,10 +1104,10 @@ function _tag_self_crossing_subpaths(path, nonzero, closed=true, eps=EPSILON) = // polygon(path); // right(27)rainbow(polygon_parts(path)) polygon($item); // move([16,-14])rainbow(polygon_parts(path,nonzero=true)) polygon($item); -function polygon_parts(path, nonzero=false, closed=true, eps=EPSILON) = +function polygon_parts(path, nonzero=false, eps=EPSILON) = let( path = cleanup_path(path, eps=eps), - tagged = _tag_self_crossing_subpaths(path, nonzero=nonzero, closed=closed, eps=eps), + tagged = _tag_self_crossing_subpaths(path, nonzero=nonzero, closed=true, eps=eps), kept = [for (sub = tagged) if(sub[0] == "O") sub[1]], outregion = _assemble_path_fragments(kept, eps=eps) ) outregion; diff --git a/regions.scad b/regions.scad index 6203b0a..83ad84a 100644 --- a/regions.scad +++ b/regions.scad @@ -1,6 +1,10 @@ ////////////////////////////////////////////////////////////////////// // LibFile: regions.scad -// Regions and 2D boolean geometry +// This file provides 2D boolean geometry operations on paths, where you can +// compute the intersection or union of the shape defined by point lists, producing +// a new point list. Of course, boolean operations may produce shapes with multiple +// components. To handle that, we use "regions" which are defined by sets of +// multiple paths. // Includes: // include ////////////////////////////////////////////////////////////////////// @@ -11,6 +15,16 @@ // Section: Regions +// A region is a list of non-crossing simple polygons. Simple polygons are those without self intersections, +// and the polygons of a region can touch at corners, but their segments should not +// cross each other. The actual geometry of the region is defined by XORing together +// all of the polygons on the list. This may sound obscure, but it simply means that nested +// boundaries make rings in the obvious fashion, and non-nested shapes simply union together. +// Checking that the polygons on a list are simple and non-crossing 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 list of polygons, or by using +// boolean function operations such as union() or difference(). And if you must you +// can clean up an ill-formed region using sanitize_region(). // Function: is_region() @@ -78,6 +92,16 @@ function check_and_fix_path(path, valid_dim=undef, closed=false, name="path") = +// Function: sanitize_region() +// Usage: +// r_fixed = sanitize_region(r); +// Description: +// Takes a malformed input region that contains self-intersecting polygons or polygons +// that cross each other and converts it into a properly defined region without +// these defects. +function sanitize_region(r) = exclusive_or([for(poly=r) each polygon_parts(poly)]); + + // Module: region() // Usage: @@ -109,7 +133,7 @@ module region(r) // Function: point_in_region() // Usage: -// point_in_region(point, region); +// check = point_in_region(point, region); // Description: // Tests if a point is inside, outside, or on the border of a region. // Returns -1 if the point is outside the region. @@ -120,64 +144,35 @@ module region(r) // region = The region to test against. Given as a list of polygon paths. // eps = Acceptable variance. Default: `EPSILON` (1e-9) function point_in_region(point, region, eps=EPSILON, _i=0, _cnt=0) = - (_i >= len(region))? ((_cnt%2==1)? 1 : -1) : let( - pip = point_in_polygon(point, region[_i], eps=eps) - ) pip==0? 0 : point_in_region(point, region, eps=eps, _i=_i+1, _cnt = _cnt + (pip>0? 1 : 0)); + _i >= len(region) ? ((_cnt%2==1)? 1 : -1) + : let( + pip = point_in_polygon(point, region[_i], eps=eps) + ) + pip==0? 0 + : point_in_region(point, region, eps=eps, _i=_i+1, _cnt = _cnt + (pip>0? 1 : 0)); -// Function: polygons_equal() +// Function: is_region_simple() // Usage: -// b = polygons_equal(poly1, poly2, [eps]) +// bool = is_region_simple(region, [eps]); // Description: -// Returns true if poly1 and poly2 are the same polongs -// within given epsilon tolerance. +// Returns true if the region is entirely non-self-intersecting, meaning that it is +// formed from a list of simple polygons that do not intersect each other. // Arguments: -// poly1 = first polygon -// poly2 = second polygon -// eps = tolerance for comparison -// Example(NORENDER): -// polygons_equal(pentagon(r=4), -// rot(360/5, p=pentagon(r=4))); // returns true -// polygons_equal(pentagon(r=4), -// rot(90, p=pentagon(r=4))); // returns false -function polygons_equal(poly1, poly2, eps=EPSILON) = - let( - poly1 = cleanup_path(poly1), - poly2 = cleanup_path(poly2), - l1 = len(poly1), - l2 = len(poly2) - ) l1 != l2 ? false : - let( maybes = find_first_match(poly1[0], poly2, eps=eps, all=true) ) - maybes == []? false : - [for (i=maybes) if (__polygons_equal(poly1, poly2, eps, i)) 1] != []; - -function __polygons_equal(poly1, poly2, eps, st) = - max([for(d=poly1-select(poly2,st,st-1)) d*d])= len(polys)? false : - polygons_equal(poly, polys[i])? true : - __is_polygon_in_list(poly, polys, i+1); - - -// Function: regions_equal() -// Usage: -// b = regions_equal(region1, region2, [eps]) +// b = are_regions_equal(region1, region2, [eps]) // Description: // Returns true if the components of region1 and region2 are the same polygons (in any order) // within given epsilon tolerance. @@ -185,39 +180,63 @@ function __is_polygon_in_list(poly, polys, i) = // region1 = first region // region2 = second region // eps = tolerance for comparison -function regions_equal(region1, region2) = +function are_regions_equal(region1, region2) = assert(is_region(region1) && is_region(region2)) len(region1) != len(region2)? false : - __regions_equal(region1, region2, 0); + __are_regions_equal(region1, region2, 0); -function __regions_equal(region1, region2, i) = +function __are_regions_equal(region1, region2, i) = i >= len(region1)? true : !is_polygon_in_list(region1[i], region2)? false : - __regions_equal(region1, region2, i+1); + __are_regions_equal(region1, region2, i+1); -/// Internal Function: _region_path_crossings() +/// Internal Function: _path_region_intersections() /// Usage: -/// _region_path_crossings(path, region); +/// _path_region_intersections(path, region, [closed], [eps]); /// Description: -/// Returns a sorted list of [SEGMENT, U] that describe where a given path is crossed by a second path. +/// Returns a sorted list of [SEGMENT, U] that describe where a given path intersects the region +// in a single point. (Note that intersections of collinear segments, where the intersection is another segment, are +// ignored.) /// Arguments: /// path = The path to find crossings on. /// region = Region to test for crossings of. /// closed = If true, treat path as a closed polygon. Default: true /// eps = Acceptable variance. Default: `EPSILON` (1e-9) -function _region_path_crossings(path, region, closed=true, eps=EPSILON) = +function _path_region_intersections(path, region, closed=true, eps=EPSILON) = let( - segs = pair(closed? close_path(path) : cleanup_path(path)) + pathclosed = closed && !is_closed_path(path), + pathlen = len(path), + regionsegs = [for(poly=region) each pair(poly, is_closed_path(poly)?false:true)] ) - sort([for (si = idx(segs), p = close_region(region), s2 = pair(p)) - let ( - isect = _general_line_intersection(segs[si], s2, eps=eps) - ) - if (!is_undef(isect[0]) && isect[1] >= 0-eps && isect[1] < 1+eps - && isect[2] >= 0-eps && isect[2] < 1+eps ) - [si, isect[1]] - ]); + sort( + [for(si = [0:1:len(path)-(pathclosed?1:2)]) + let( + a1 = path[si], + a2 = path[(si+1)%pathlen], + maxax = max(a1.x,a2.x), + minax = min(a1.x,a2.x), + maxay = max(a1.y,a2.y), + minay = min(a1.y,a2.y) + ) + for(rseg=regionsegs) + let( + b1 = rseg[0], + b2 = rseg[1], + isect = + maxax < b1.x && maxax < b2.x || + minax > b1.x && minax > b2.x || + maxay < b1.y && maxay < b2.y || + minay > b1.y && minay > b2.y + ? undef + : _general_line_intersection([a1,a2],rseg,eps) + ) + if (isect && isect[1]>=-eps && isect[1]<=1+eps + && isect[2]>=-eps && isect[2]<=1+eps) + [si,isect[1]] + ] + ); + // Function: split_path_at_region_crossings() @@ -241,7 +260,7 @@ function split_path_at_region_crossings(path, region, closed=true, eps=EPSILON) let( path = deduplicate(path, eps=eps), region = [for (path=region) deduplicate(path, eps=eps)], - xings = _region_path_crossings(path, region, closed=closed, eps=eps), + xings = _path_region_intersections(path, region, closed=closed, eps=eps), crossings = deduplicate( concat([[0,0]], xings, [[len(path)-1,1]]), eps=eps @@ -254,7 +273,7 @@ function split_path_at_region_crossings(path, region, closed=true, eps=EPSILON) ) ] ) - subpaths; + [for(s=subpaths) if (len(s)>1) s]; // Function: split_nested_region() @@ -831,23 +850,32 @@ function offset( ) ) return_faces? [edges,faces] : edges; - +/// Internal Function: _tag_subpaths() +/// splits the polygon (path) into subpaths by region crossing and then tags each subpath: +/// "O" - the subpath is outside the region +/// "I" - the subpath is inside the region's interior +/// "S" - the subpath is on the region's border and the polygon and region are on the same side of the subpath +/// "U" - the subpath is on the region's border and the polygon and region meet at the subpath (from opposite sides) +/// The return has the form of a list with entries [TAG, SUBPATH] function _tag_subpaths(path, region, eps=EPSILON) = let( subpaths = split_path_at_region_crossings(path, region, eps=eps), tagged = [ - for (sub = subpaths) let( - subpath = deduplicate(sub) - ) if (len(sub)>1) let( - midpt = lerp(subpath[0], subpath[1], 0.5), - rel = point_in_region(midpt,region,eps=eps) - ) rel<0? ["O", subpath] : rel>0? ["I", subpath] : let( - vec = unit(subpath[1]-subpath[0]), - perp = rot(90, planar=true, p=vec), - sidept = midpt + perp*0.01, - rel1 = point_in_polygon(sidept,path,eps=eps)>0, - rel2 = point_in_region(sidept,region,eps=eps)>0 - ) rel1==rel2? ["S", subpath] : ["U", subpath] + for (subpath = subpaths) + let( + midpt = mean([subpath[0], subpath[1]]), + rel = point_in_region(midpt,region,eps=eps) + ) + rel<0? ["O", subpath] + : rel>0? ["I", subpath] + : let( + vec = unit(subpath[1]-subpath[0]), + perp = rot(90, planar=true, p=vec), + sidept = midpt + perp*0.01, + rel1 = point_in_polygon(sidept,path,eps=eps)>0, + rel2 = point_in_region(sidept,region,eps=eps)>0 + ) + rel1==rel2? ["S", subpath] : ["U", subpath] ] ) tagged; @@ -858,16 +886,14 @@ function _tag_region_subpaths(region1, region2, eps=EPSILON) = function _tagged_region(region1,region2,keep1,keep2,eps=EPSILON) = let( - region1 = close_region(region1, eps=eps), - region2 = close_region(region2, eps=eps), tagged1 = _tag_region_subpaths(region1, region2, eps=eps), tagged2 = _tag_region_subpaths(region2, region1, eps=eps), - tagged = concat( - [for (tagpath = tagged1) if (in_list(tagpath[0], keep1)) tagpath[1]], - [for (tagpath = tagged2) if (in_list(tagpath[0], keep2)) tagpath[1]] - ), - outregion = _assemble_path_fragments(tagged, eps=eps) - ) outregion; + tagged = [ + for (tagpath = tagged1) if (in_list(tagpath[0], keep1)) tagpath[1], + for (tagpath = tagged2) if (in_list(tagpath[0], keep2)) tagpath[1] + ] + ) + _assemble_path_fragments(tagged, eps=eps); @@ -986,7 +1012,7 @@ function intersection(regions=[],b=undef,c=undef,eps=EPSILON) = // circle(d=40); // } function exclusive_or(regions=[],b=undef,c=undef,eps=EPSILON) = - b!=undef? exclusive_or(concat([regions],[b],c==undef?[]:[c]),eps=eps) : + b!=undef? exclusive_or([regions, b, if(is_def(c)) c],eps=eps) : len(regions)<=1? regions[0] : exclusive_or( let(regions=[for (r=regions) is_path(r)? [r] : r]) diff --git a/skin.scad b/skin.scad index be13941..4f1326d 100644 --- a/skin.scad +++ b/skin.scad @@ -402,7 +402,7 @@ function skin(profiles, slices, refine=1, method="direct", sampling, caps, close legal_methods = ["direct","reindex","distance","fast_distance","tangent"], caps = is_def(caps) ? caps : closed ? false : true, - capsOK = is_bool(caps) || (is_list(caps) && len(caps)==2 && is_bool(caps[0]) && is_bool(caps[1])), + capsOK = is_bool(caps) || is_bool_list(caps,2), fullcaps = is_bool(caps) ? [caps,caps] : caps, refine = is_list(refine) ? refine : repeat(refine, len(profiles)), slices = is_list(slices) ? slices : repeat(slices, profcount), @@ -831,7 +831,7 @@ function path_sweep(shape, path, method="incremental", normal, closed=false, twi path = path3d(path), caps = is_def(caps) ? caps : closed ? false : true, - capsOK = is_bool(caps) || (is_list(caps) && len(caps)==2 && is_bool(caps[0]) && is_bool(caps[1])), + capsOK = is_bool(caps) || is_bool_list(caps,2), fullcaps = is_bool(caps) ? [caps,caps] : caps, normalOK = is_undef(normal) || (method!="natural" && is_vector(normal,3)) || (method=="manual" && same_shape(normal,path)) @@ -910,8 +910,8 @@ function path_sweep(shape, path, method="incremental", normal, closed=false, twi : let( rshape = is_path(shape) ? [path3d(shape)] : [for(s=shape) path3d(s)] ) - regions_equal(apply(transform_list[0], rshape), - apply(transform_list[L], rshape)), + are_regions_equal(apply(transform_list[0], rshape), + apply(transform_list[L], rshape)), dummy = ends_match ? 0 : echo("WARNING: ***** The points do not match when closing the model *****") ) transforms ? transform_list : sweep(is_path(shape)?clockwise_polygon(shape):shape, transform_list, closed=false, caps=fullcaps,style=style); @@ -966,7 +966,7 @@ function path_sweep2d(shape, path, closed=false, caps, quality=1, style="min_edg let( caps = is_def(caps) ? caps : closed ? false : true, - capsOK = is_bool(caps) || (is_list(caps) && len(caps)==2 && is_bool(caps[0]) && is_bool(caps[1])), + capsOK = is_bool(caps) || is_bool_list(caps,2), fullcaps = is_bool(caps) ? [caps,caps] : caps, shape = check_and_fix_path(shape,valid_dim=2,closed=true,name="shape") ) @@ -1095,7 +1095,7 @@ function sweep(shape, transforms, closed=false, caps, style="min_edge") = let( caps = is_def(caps) ? caps : closed ? false : true, - capsOK = is_bool(caps) || (is_list(caps) && len(caps)==2 && is_bool(caps[0]) && is_bool(caps[1])), + capsOK = is_bool(caps) || is_bool_list(caps,2), fullcaps = is_bool(caps) ? [caps,caps] : caps ) assert(len(transforms), "transformation must be length 2 or more") diff --git a/tests/test_vnf.scad b/tests/test_vnf.scad index 7ac7c5e..a119999 100644 --- a/tests/test_vnf.scad +++ b/tests/test_vnf.scad @@ -34,15 +34,6 @@ module test_vnf_faces() { test_vnf_faces(); -module test_vnf_get_vertex() { - vnf = [[[-1,-1,-1],[1,-1,-1],[0,1,-1],[0,0,1]],[[0,1,2],[0,3,1],[1,3,2],[2,3,0]]]; - assert(vnf_get_vertex(vnf,[0,1,-1]) == [2,vnf]); - assert(vnf_get_vertex(vnf,[0,1,2]) == [4,[concat(vnf[0],[[0,1,2]]),vnf[1]]]); - assert(vnf_get_vertex(vnf,[[0,1,-1],[0,1,2]]) == [[2,4],[concat(vnf[0],[[0,1,2]]),vnf[1]]]); -} -test_vnf_get_vertex(); - - module test_vnf_add_face() { verts = [[-1,-1,-1],[1,-1,-1],[0,1,-1],[0,0,1]]; faces = [[0,1,2],[0,3,1],[1,3,2],[2,3,0]]; diff --git a/vnf.scad b/vnf.scad index 3ea55d3..4d9177e 100644 --- a/vnf.scad +++ b/vnf.scad @@ -10,13 +10,15 @@ // Section: Creating Polyhedrons with VNF Structures - // VNF stands for "Vertices'N'Faces". VNF structures are 2-item lists, `[VERTICES,FACES]` where the // first item is a list of vertex points, and the second is a list of face indices into the vertex // list. Each VNF is self contained, with face indices referring only to its own vertex list. // You can construct a `polyhedron()` in parts by describing each part in a self-contained VNF, then // merge the various VNFs to get the completed polyhedron vertex list and faces. +// Constant: EMPTY_VNF +// Description: +// The empty VNF data structure. Equal to `[[],[]]`. EMPTY_VNF = [[],[]]; // The standard empty VNF with no vertices or faces. @@ -413,34 +415,6 @@ function vnf_vertices(vnf) = vnf[0]; function vnf_faces(vnf) = vnf[1]; -// Function: vnf_get_vertex() -// Usage: -// vvnf = vnf_get_vertex(vnf, p); -// Description: -// Finds the index number of the given vertex point `p` in the given VNF structure `vnf`. -// If said point does not already exist in the VNF vertex list, it is added to the returned VNF. -// Returns: `[INDEX, VNF]` where INDEX is the index of the point in the returned VNF's vertex list, -// and VNF is the possibly modified new VNF structure. If `p` is given as a list of points, then -// the returned INDEX will be a list of indices. -// Arguments: -// vnf = The VNF structue to get the point index from. -// p = The point, or list of points to get the index of. -// Example: -// vnf1 = vnf_get_vertex(p=[3,5,8]); // Returns: [0, [[[3,5,8]],[]]] -// vnf2 = vnf_get_vertex(vnf1, p=[3,2,1]); // Returns: [1, [[[3,5,8],[3,2,1]],[]]] -// vnf3 = vnf_get_vertex(vnf2, p=[3,5,8]); // Returns: [0, [[[3,5,8],[3,2,1]],[]]] -// vnf4 = vnf_get_vertex(vnf3, p=[[1,3,2],[3,2,1]]); // Returns: [[1,2], [[[3,5,8],[3,2,1],[1,3,2]],[]]] -function vnf_get_vertex(vnf=EMPTY_VNF, p) = - let( - isvec = is_vector(p), - pts = isvec? [p] : p, - res = set_union(vnf[0], pts, get_indices=true) - ) [ - (isvec? res[0][0] : res[0]), - [ res[1], vnf[1] ] - ]; - - // Section: Altering the VNF Internals