def build_circular_hatch(delta, offset, w, h): center_x = w / 2 center_y = h / 2 ls = [] for r in np.arange(offset, math.sqrt(w * w + h * h), delta): # make a tiny circle as point in the center if r == 0: r = 0.001 # compute a meaningful number of segment adapted to the circle's radius n = max(20, r) t = np.arange(0, 1, 1 / n) # A random phase is useful for circles that end up unmasked. If several such circles # start and stop at the same location, a disgraceful pattern will emerge when plotting. phase = random.random() * 2 * math.pi data = np.array([ center_x + r * np.cos(t * math.pi * 2 + phase), center_y + r * np.sin(t * math.pi * 2 + phase), ]).T ls.append(LinearRing(data)) mls = MultiLineString(ls) # Crop the circle to the final dimension p = Polygon([(0, 0), (w, 0), (w, h), (0, h)]) return mls.intersection(p)
def draw(self, vsk: vsketch.Vsketch) -> None: vsk.size(self.page_size) width = round((vsk.width - 2 * self.margin) / self.base_pitch) * self.base_pitch height = round((vsk.height - 2 * self.margin) / self.base_pitch) * self.base_pitch mls0 = MultiLineString( [ [(0, y), (width, y)] for y in np.arange(0, height + self.base_pitch, self.base_pitch) ] ) mls1 = MultiLineString( [ [(0, y), (width, y)] for y in np.arange(self.base_pitch / 2, height, self.base_pitch) ] ) mls2 = MultiLineString( [ [(0, y), (width, y)] for y in np.arange(self.base_pitch / 4, height, self.base_pitch / 2) ] ) # build a separation yy = np.linspace(0, height, 100) xx = np.array([vsk.noise(y * 0.002) for y in yy]) * width / 1.8 + 3 * width / 5 - 200 p1 = Polygon(list(zip(xx, yy)) + [(width, height), (width, 0)]) vsk.geometry(mls0) circles = [ Point(vsk.random(0, width), vsk.random(0, height)).buffer( vsk.random(self.min_radius, self.max_radius) ) for _ in range(5) ] all_geom = circles + [p1] itrsct = unary_union( [a.intersection(b) for a, b in itertools.combinations(all_geom, 2)] ) vsk.geometry(mls1.intersection(unary_union(all_geom))) vsk.geometry(mls2.intersection(itrsct))
def check_intersections(self): checks_done = MultiLineString() for subpath, elem in self.get_line_strings(): line = subpath.as_linestring() if not line.is_simple: # TODO: find location of self-intersection and introduce some # tolerance # checks_done = checks_done.union(line) yield CheckerResult("self-intersection found", elem) # continue if checks_done.intersects(line): intersection = checks_done.intersection(line) yield CheckerResult("intersection found", elem, extra={"intersection": intersection}) checks_done = checks_done.union(line)
def generate_star(rect: RectType, line_count: int = 20) -> vp.LineCollection: """Generate a set of line from a random point.""" orig_x = np.random.uniform(rect[0], rect[0] + rect[2]) orig_y = np.random.uniform(rect[1], rect[1] + rect[3]) r = math.hypot(rect[2], rect[3]) angles = np.linspace(0, 2 * math.pi, num=line_count, endpoint=False) phase = np.random.normal(0, math.pi / 4) mls = MultiLineString([ ([orig_x, orig_y], [orig_x + r * math.cos(a), orig_y + r * math.sin(a)]) for a in angles + phase ]) return vp.LineCollection(mls.intersection(rect_to_polygon(rect)))
def get_intersection_points_between_shapes(shape0, shape1): """If the two passed shapes intersect in one or more points (a finite number) return all of them, otherwise return an empty list""" intersection_points = list() # the contour of polygons and multi-polygons is used for the check, to detect boundary intersection points if type(shape0) == Polygon: shape0 = shape0.exterior elif type(shape0) == MultiPolygon: shape0 = MultiLineString(shape0.boundary) if type(shape1) == Polygon: shape1 = shape1.exterior elif type(shape1) == MultiPolygon: shape1 = MultiLineString(shape1.boundary) # non-native shape types use their approximate polygon boundaries if type(shape0) == Circle or type(shape0) == Ellipse: shape0 = shape0.polygon.exterior if type(shape1) == Circle or type(shape1) == Ellipse: shape1 = shape1.polygon.exterior intersection_result = shape0.intersection(shape1) if intersection_result: if type(intersection_result) == Point: intersection_points.append( (intersection_result.x, intersection_result.y)) elif type(intersection_result) == MultiPoint: for point in intersection_result: intersection_points.append((point.x, point.y)) return intersection_points
def _build_circular_hatch(delta: float, offset: float, w: int, h: int, center: Tuple[float, float] = (0.5, 0.5)): center_x = w * center[0] center_y = h * center[1] ls = [] # If center_x or center_y > 1, ensure the full image is covered with lines max_radius = max(math.sqrt(w**2 + h**2), math.sqrt(center_x**2 + center_y**2)) for r in np.arange(offset, max_radius, delta): # make a tiny circle as point in the center if r == 0: r = 0.001 # compute a meaningful number of segment adapted to the circle's radius n = max(20, r) t = np.arange(0, 1, 1 / n) # A random phase is useful for circles that end up unmasked. If several such circles # start and stop at the same location, a disgraceful pattern will emerge when plotting. phase = random.random() * 2 * math.pi data = np.array([ center_x + r * np.cos(t * math.pi * 2 + phase), center_y + r * np.sin(t * math.pi * 2 + phase), ]).T ls.append(LinearRing(data)) mls = MultiLineString(ls) # Crop the circle to the final dimension p = Polygon([(0, 0), (w, 0), (w, h), (0, h)]) return mls.intersection(p)
import matplotlib.pyplot as plt from shapely.geometry import MultiLineString, Polygon mls = MultiLineString([[(0, 1), (5, 1)], [(1, 2), (1, 0)]]) p = Polygon([(0.5, 0.5), (0.5, 1.5), (2, 1.5), (2, 0.5)]) results = mls.intersection(p) plt.subplot(1, 2, 1) for ls in mls: plt.plot(*ls.xy) plt.plot(*p.boundary.xy, "-.k") plt.xlim([0, 5]) plt.ylim([0, 2]) plt.subplot(1, 2, 2) for ls in results: plt.plot(*ls.xy) plt.xlim([0, 5]) plt.ylim([0, 2]) plt.show()
def mark_longitudes(self, lon_arr=numpy.arange(-180,180,60), **kwargs): """ mark the longitudes Write down the longitudes on the map for labeling! we are using this because cartopy doesn't have a label by default for non-rectangular projections! This is also trickier compared to latitudes! """ if isinstance(lon_arr, list): lon_arr = numpy.array(lon_arr) else: if not isinstance(lon_arr, numpy.ndarray): raise TypeError('lat_arr must either be a list or numpy array') # get the boundaries [x1, y1], [x2, y2] = self.viewLim.get_points() bound_lim_arr = [] right_bound = LineString(([-x1, y1], [x2, y2])) top_bound = LineString(([x1, -y1], [x2, y2])) bottom_bound = LineString(([x1, y1], [x2, -y2])) left_bound = LineString(([x1, y1], [-x2, y2])) plot_outline = MultiLineString( [\ right_bound,\ top_bound,\ bottom_bound,\ left_bound\ ] ) # get the plot extent, we'll get an intersection # to locate the ticks! plot_extent = self.get_extent(cartopy.crs.Geodetic()) line_constructor = lambda t, n, b: numpy.vstack(\ (numpy.zeros(n) + t, numpy.linspace(b[2], b[3], n))\ ).T for t in lon_arr: xy = line_constructor(t, 30, plot_extent) # print(xy) proj_xyz = self.projection.transform_points(\ cartopy.crs.PlateCarree(), xy[:, 0], xy[:, 1]\ ) xyt = proj_xyz[..., :2] ls = LineString(xyt.tolist()) locs = plot_outline.intersection(ls) if not locs: continue # we need to get the alignment right # so get the boundary closest to the label # and plot it! closest_bound =min( [\ right_bound.distance(locs),\ top_bound.distance(locs),\ bottom_bound.distance(locs),\ left_bound.distance(locs)\ ] ) if closest_bound == right_bound.distance(locs): ha = 'left' va = 'top' elif closest_bound == top_bound.distance(locs): ha = 'left' va = 'bottom' elif closest_bound == bottom_bound.distance(locs): ha = 'left' va = 'top' else: ha = 'right' va = 'top' if self.coords == "aacgmv2_mlt": marker_text = str(int(t/15.)) else: marker_text = str(t) self.text( locs.bounds[0],locs.bounds[1], marker_text, ha=ha, va=va)
class EdgesFilter(object): def __init__(self, contours=None): if contours is not None: self.set_contours(contours) def set_contours(self, contours): # add all contours to a line collection contours = [[p[0] for p in cnt] for cnt in contours] self._wrinkle_lines = MultiLineString(contours) def filter_mesh_by_wrinkles(self, mesh_tri): mesh_tri = mesh_tri points = self._mesh_tri.points simplices = self._mesh_tri.simplices.astype(np.uint32) # find unique edges - iterate over all simplices triangle edges def _filter_specifc_simplices_edge(cur_simplex_edge_indices, simplices_mask, points, wrinkle_lines): cur_simplex_edge_mask = np.zeros((len(cur_simplex_edge_indices), ), dtype=np.bool) for simplex_idx, simplex_edge_idxs in enumerate( cur_simplex_edge_indices): edge_pts = points[simplex_edge_idxs] edge_line = LineString(edge_pts) if wrinkle_lines.intersection(edge_line): #print("Found an intersection of simplex_idx: {}, edge_pts: {}".format(simplex_idx, edge_pts)) cur_simplex_edge_indices[simplex_idx] = True simplices_mask[simplex_idx] = True return cur_simplex_edge_mask relevant_edges_indices = [] simplices_mask = np.zeros((len(simplices), ), dtype=np.bool) edges_indices = np.vstack( (simplices[:, :2] [~_filter_specifc_simplices_edge(simplices[:, :2], simplices_mask, points, self._wrinkle_lines)], simplices[:, 1:][~_filter_specifc_simplices_edge( simplices[:, 1:], simplices_mask, points, self._wrinkle_lines)], simplices[:, [0, 2]][~_filter_specifc_simplices_edge( simplices[:, [0, 2]], simplices_mask, points, self. _wrinkle_lines)])) edges_indices = np.vstack( {tuple(sorted(tuple(row))) for row in edges_indices}).astype(np.uint32) return edges_indices, simplices[~simplices_mask], ~simplices_mask def filter_edges_by_wrinkles(self, edges_pts): """ Returns a mask where indices corresponding to edges that cross wrinkles are True """ edges_mask = np.zeros((len(edges_pts), ), dtype=np.bool) # iterate over all edges, and for each edge find if it crosses the wrinkle_lines for edge_idx, edge_pts in enumerate(edges_pts): edge_line = LineString(edge_pts) if self._wrinkle_lines.intersection(edge_line): #print("Found an intersection of edge_idx: {}, edge_pts: {}".format(edge_idx, edge_pts)) edges_mask[edge_idx] = True return edges_mask def get_min_lengths(self, pts): min_lengths = [Point(pt).distance(self._wrinkle_lines) for pt in pts] return min_lengths
def _salvar_nos_rastreados(self, vertices: np.ndarray): """Salva os nós rastreados pelo layer 'rastreador_nos{i}'""" linhas_layers = self._dicionario_linhas_dxf() pontos_layers = self._dicionario_pontos_dxf() dmed = self.diametro_medio_elementos(self.poligono_estrutura()) # Nós rastreados nos_rastreados = {} # KDTree kd = KDTree(vertices) multipontos = MultiPoint(vertices) nome_arq_txt = ARQUIVOS_DADOS_ZIP[16] if any( i.startswith(Malha.LAYER_RASTREADOR_NOS) for i in linhas_layers): for layer_rastreado in linhas_layers: if layer_rastreado.startswith(Malha.LAYER_RASTREADOR_NOS): linhas = [] nos_rastreados[layer_rastreado] = [] for lin in linhas_layers[layer_rastreado]: x1, y1, x2, y2 = lin linhas.append(LineString([(x1, y1), (x2, y2)])) multiline_i = MultiLineString(linhas) # Identificar pontos multiline_i = multiline_i.buffer(0.1 * dmed) pontos_contorno = multiline_i.intersection(multipontos) # Adicionando os nós rastreados pelas linhas if not isinstance(pontos_contorno, Point): if len(pontos_contorno) > 0: for p in pontos_contorno: nos_rastreados[layer_rastreado].append( kd.query(p.coords[:][0])[1]) else: nos_rastreados[layer_rastreado].append( kd.query(pontos_contorno.coords[:][0])[1]) # Adicionando os nós rastreados pelos pontos. pts = pontos_layers[layer_rastreado] for id_pt in range(pts.shape[0]): nos_rastreados[layer_rastreado].append( kd.query(pts[id_pt])[1]) # Salvar a identificação do ponto no arquivo with open(nome_arq_txt, 'w') as arq: for lay in nos_rastreados: if (len(nos_rastreados[lay])) > 0: arq.write( f'{lay} = {sorted(set(nos_rastreados[lay]))}\n') # Salvar no arquivo zip with zipfile.ZipFile(self.dados.arquivo.name, 'a', compression=zipfile.ZIP_DEFLATED) as arq_zip: arq_zip.write(pathlib.Path(nome_arq_txt).name) # Apagar o arquivo txt os.remove(nome_arq_txt)
n_line = len(linepoints) fin.close() # Print line list with points #print 'Lines with points: ', n_line #for i in range(n_line): # print i+1, ' th lines with points : ', linepoints[i] #print '\n' # ================================================== # Make mutilinestring from line list # ================================================== multilines = MultiLineString(linepoints) x = multilines.intersection(multilines) # Polygonize result, dangles, cuts, invalids = polygonize_full(x) result = MultiPolygon(result) polygon = cascaded_union(result) # Make mutilinestring from line list #multilines = MultiLineString(linepoints) # Polygonize #result, dangles, cuts, invalids = polygonize_full(multilines) #result = MultiPolygon(result) #polygon = cascaded_union(result)
def line_intersect(a: MultiLineString, b: tuple): b = LineString(b) intersection = a.intersection(b) if type(intersection) == Point: return intersection.x, intersection.y return False
def MetricSlopes(axis, **kwargs): talweg_feature = config.filename('ax_talweg', axis=axis, **kwargs) refaxis_feature = config.filename('ax_refaxis', axis=axis) elevation_raster = config.filename('dem', **kwargs) measure_raster = config.filename('ax_axis_measure', axis=axis, **kwargs) swath_raster = config.filename('ax_swaths', axis=axis, **kwargs) swath_features = config.filename('ax_swath_features', axis=axis, **kwargs) all_coordinates = np.zeros((0, 2), dtype='float32') z = np.array([]) m = np.array([]) swathid = np.array([]) # Sort talweg segments by first point M coordinate, descending talweg_fids = list() segments = list() with rio.open(measure_raster) as ds: with fiona.open(talweg_feature) as fs: for feature in fs: fid = feature['id'] firstm = next( ds.sample([feature['geometry']['coordinates'][0][:2]], 1)) talweg_fids.append((fid, firstm)) with fiona.open(talweg_feature) as fs: for fid, _ in reversed(sorted(talweg_fids, key=itemgetter(1))): feature = fs.get(fid) coordinates = np.array(feature['geometry']['coordinates'], dtype='float32') segments.append(asShape(feature['geometry'])) all_coordinates = np.concatenate( [all_coordinates, coordinates[:, :2]], axis=0) with rio.open(elevation_raster) as ds: this_z = np.array(list(ds.sample(coordinates[:, :2], 1))) this_z = this_z[:, 0] this_z[this_z == ds.nodata] = np.nan z = np.concatenate([z, this_z], axis=0) with rio.open(measure_raster) as ds: this_m = np.array(list(ds.sample(coordinates[:, :2], 1))) this_m = this_m[:, 0] m = np.concatenate([m, this_m], axis=0) with rio.open(swath_raster) as ds: this_swathid = np.array(list(ds.sample(coordinates[:, :2], 1))) this_swathid = this_swathid[:, 0] # swathid[swathid == ds.nodata] = 0 swathid = np.concatenate([swathid, this_swathid], axis=0) measure_raster = config.filename('ax_buffer_profile', axis=axis, **kwargs) with fiona.open(refaxis_feature) as fs: assert len(fs) == 1 coord = itemgetter(0, 1) for feature in fs: refaxis_pixels = list() refaxis_m = list() m0 = feature['properties'].get('M0', 0.0) coordinates = np.array([ coord(p) + (m0, ) for p in reversed(feature['geometry']['coordinates']) ], dtype='float32') coordinates[1:, 2] = m0 + np.cumsum( np.linalg.norm(coordinates[1:, :2] - coordinates[:-1, :2], axis=1)) with rio.open(swath_raster) as ds: coordinates[:, :2] = fct.worldtopixel(coordinates[:, :2], ds.transform) for a, b in zip(coordinates[:-1], coordinates[1:]): for i, j, mcoord in rasterize_linestringz(a, b): refaxis_pixels.append((i, j)) refaxis_m.append(mcoord) refaxis_m = np.array(refaxis_m) refaxis_coordinates = fct.pixeltoworld( np.array(refaxis_pixels, dtype='int32'), ds.transform) refaxis_swathid = np.array( list(ds.sample(refaxis_coordinates, 1))) refaxis_swathid = refaxis_swathid[:, 0] # s: curvilinear talweg coordinate, from upstream to downstream s = np.zeros(len(all_coordinates), dtype='float32') s[1:] = np.cumsum( np.linalg.norm(all_coordinates[1:, :] - all_coordinates[:-1, :], axis=1)) talweg = MultiLineString(segments) ref_segments = list() ref_segments_vf = list() with fiona.open(swath_features) as fs: gids = np.zeros(len(fs), dtype='uint32') measures = np.zeros(len(fs), dtype='float32') values = np.zeros((len(fs), 7), dtype='float32') with click.progressbar(fs) as iterator: for k, feature in enumerate(iterator): gid = feature['properties']['GID'] polygon = asShape(feature['geometry']) gids[k] = gid measures[k] = feature['properties']['M'] talweg_length = talweg.intersection(polygon).length values[k, 0] = talweg_length mask = (~np.isnan(z)) & (swathid == gid) if np.sum(mask) > 0: moffset = m[mask][0] Y = z[mask] X = np.column_stack([ s[mask] - s[mask][0], np.ones_like(Y), ]) (slope_talweg, z0_talweg), sqerror_talweg, _, _ = np.linalg.lstsq( X, Y, rcond=None) if len(sqerror_talweg) == 0: sqerror_talweg = 0.0 values[k, 1] = -100.0 * slope_talweg values[k, 2] = z0_talweg values[k, 3] = sqerror_talweg X = np.column_stack([ m[mask] - moffset, np.ones_like(Y), ]) (slope_valley, z0_valley), sqerror_valley, _, _ = np.linalg.lstsq( X, Y, rcond=None) if len(sqerror_valley) == 0: sqerror_valley = 0 # m axis is oriented from downstream to upstream, # and yields positive slopes values[k, 4] = 100.0 * slope_valley values[k, 5] = z0_valley values[k, 6] = sqerror_valley mask_ref = (refaxis_swathid == gid) if np.sum(mask_ref) > 0: refaxis_z = slope_valley * (refaxis_m[mask_ref] - moffset) + z0_valley ref_segments.append( np.column_stack([ refaxis_coordinates[mask_ref], refaxis_z, refaxis_m[mask_ref] ])) swathfile = config.filename('ax_swath_elevation_npz', axis=axis, gid=gid) swathdata = np.load(swathfile, allow_pickle=True) slope_valley_floor = swathdata['slope_valley_floor'] z0_valley_floor = swathdata['z0_valley_floor'] if np.isnan(slope_valley_floor): ref_segments_vf.append( np.column_stack([ refaxis_coordinates[mask_ref], refaxis_z, refaxis_m[mask_ref] ])) else: refaxis_z_vf = slope_valley_floor * ( refaxis_m[mask_ref]) + z0_valley_floor ref_segments_vf.append( np.column_stack([ refaxis_coordinates[mask_ref], refaxis_z_vf, refaxis_m[mask_ref] ])) else: ref_segments.append(np.array([])) ref_segments_vf.append(np.array([])) else: values[k, :] = np.nan ref_segments.append(np.array([])) ref_segments_vf.append(np.array([])) metrics = xr.Dataset( { 'swath': ('measure', gids), 'twl': ('measure', values[:, 0]), 'tws': ('measure', values[:, 1]), 'twz0': ('measure', values[:, 2]), 'twse': ('measure', values[:, 3]), 'vfs': ('measure', values[:, 4]), 'vfz0': ('measure', values[:, 5]), 'vfse': ('measure', values[:, 6]) }, coords={ 'axis': axis, 'measure': measures }) # Metadata metrics['twl'].attrs['long_name'] = 'intercepted talweg length' metrics['twl'].attrs['units'] = 'm' metrics['tws'].attrs['long_name'] = 'talweg slope' metrics['tws'].attrs['units'] = 'percent' metrics['twz0'].attrs[ 'long_name'] = 'talweg z-offset, at first swath point' metrics['twz0'].attrs['units'] = 'm' metrics['twz0'].attrs['vertical_ref'] = config.vertical_ref metrics['twse'].attrs['long_name'] = 'talweg slope square regression error' metrics['twse'].attrs['units'] = 'm²' metrics['vfs'].attrs['long_name'] = 'valley slope' metrics['vfs'].attrs['units'] = 'percent' metrics['vfz0'].attrs[ 'long_name'] = 'valley z-offset, at first swath point' metrics['vfz0'].attrs['units'] = 'm' metrics['vfz0'].attrs['vertical_ref'] = config.vertical_ref metrics['vfse'].attrs['long_name'] = 'valley slope square regression error' metrics['vfse'].attrs['units'] = 'm²' metrics['axis'].attrs['long_name'] = 'stream identifier' metrics['swath'].attrs['long_name'] = 'swath identifier' metrics['measure'].attrs['long_name'] = 'position along reference axis' metrics['measure'].attrs['units'] = 'm' return metrics, ref_segments, ref_segments_vf
def MetricTalwegLength(axis, **kwargs): """ Calculate intercepted talweg and reference axis length for every swath """ talweg_feature = config.filename('ax_talweg', axis=axis, **kwargs) refaxis_feature = config.filename('ax_refaxis', axis=axis) measure_raster = config.tileset().filename('ax_axis_measure', axis=axis, **kwargs) swath_features = config.filename('ax_valley_swaths_polygons', axis=axis, **kwargs) # Sort talweg segments by first point M coordinate, descending talweg_fids = list() segments = list() with rio.open(measure_raster) as ds: with fiona.open(talweg_feature) as fs: for feature in fs: fid = feature['id'] firstm = next(ds.sample([feature['geometry']['coordinates'][0][:2]], 1)) talweg_fids.append((fid, firstm)) with fiona.open(talweg_feature) as fs: for fid, _ in reversed(sorted(talweg_fids, key=itemgetter(1))): feature = fs.get(fid) segments.append(asShape(feature['geometry'])) with fiona.open(refaxis_feature) as fs: # assert len(fs) == 1 refaxis_segments = list() for feature in fs: refaxis_segments.append(asShape(feature['geometry'])) talweg = MultiLineString(segments) refaxis = MultiLineString(refaxis_segments) with fiona.open(swath_features) as fs: size = len(fs) gids = np.zeros(size, dtype='uint32') measures = np.zeros(size, dtype='float32') lengths = np.zeros((size, 2), dtype='float32') with click.progressbar(fs) as iterator: for k, feature in enumerate(iterator): gid = feature['properties']['GID'] polygon = asShape(feature['geometry']) gids[k] = gid measures[k] = feature['properties']['M'] talweg_length = talweg.intersection(polygon).length lengths[k, 0] = talweg_length refaxis_length = refaxis.intersection(polygon).length lengths[k, 1] = refaxis_length metrics = xr.Dataset( { 'swath': ('measure', gids), 'talweg_length': ('measure', lengths[:, 0]), 'refaxis_length': ('measure', lengths[:, 1]), 'swath_length': 200.0 }, coords={ 'axis': axis, 'measure': measures }) # Metadata return metrics
for poly in result.geoms: line = LineString(list(poly.exterior.coords)[:-1]) if line: all_lines.append(line) elif result.geom_type == "Polygon": line = LineString(list(result.exterior.coords)[:-1]) if line: all_lines.append(line) parent = MultiLineString(all_lines) # sketch.geometry(parent) sketch.stroke(i_speaker) for i in range(N_SLICES): box = Polygon([ (i * W, 0), ((i + 1) * W, 0), ((i + 1) * W, MAIN_HEIGHT), (i * W, MAIN_HEIGHT), ]) inter = parent.intersection(box) dx = i * W dy = i * SLICE_SPACING inter = translate(inter, -dx + MARGIN, dy + MARGIN) sketch.geometry(inter) sketch.vpype("linemerge linesimplify reloop linesort") sketch.display() sketch.save(outfilename)