Source code for fabex.utilities.shapely_utils

"""Fabex 'shapely_utils.py' © 2012 Vilem Novak

Functions to handle shapely operations and conversions - curve, coords, polygon
"""

import re

import bpy
import numpy as np
import shapely
from mathutils import Vector
from shapely.geometry import (
    MultiPolygon,
    Polygon,
)
from shapely.validation import explain_validity

from mathutils import Vector
import warnings


try:
    import bl_ext.blender_org.simplify_curves_plus as curve_simplify
except ImportError:
    pass

from ..chunk_builder import CamPathChunkBuilder
from ..exception import CamException
from .logging_utils import log
from .simple_utils import remove_multiple


[docs] def shapely_remove_doubles(p, optimize_threshold): """Remove duplicate points from the boundary of a shape. This function simplifies the boundary of a given shape by removing duplicate points using the Ramer-Douglas-Peucker algorithm. It iterates through each contour of the shape, applies the simplification, and adds the resulting contours to a new shape. The optimization threshold can be adjusted to control the level of simplification. Args: p (Shape): The shape object containing boundaries to be simplified. optimize_threshold (float): A threshold value that influences the simplification process. Returns: Shape: A new shape object with simplified boundaries. """ pnew = p optimize_threshold *= 0.000001 simplify_options = [ "distance", "distance", 0.0, 5, optimize_threshold, 5, optimize_threshold, ] for ci, c in enumerate(p.boundary): vector_list = [Vector((v[0], v[1])) for v in c] simple_curve = curve_simplify.simplify_RDP(vector_list, simplify_options) nc = [c[simple_curve[i]] for i in range(len(simple_curve))] if len(nc) > 2: pnew.addContour(nc, p.isHole(ci)) else: pnew.addContour(p[ci], p.isHole(ci)) return pnew
[docs] def shapely_to_multipolygon(anydata): """Convert a Shapely geometry to a MultiPolygon. This function takes a Shapely geometry object and converts it to a MultiPolygon. If the input geometry is already a MultiPolygon, it returns it as is. If the input is a Polygon and not empty, it wraps the Polygon in a MultiPolygon. If the input is an empty Polygon, it returns an empty MultiPolygon. For any other geometry type, it prints a message indicating that the conversion was aborted and returns an empty MultiPolygon. Args: anydata (shapely.geometry.base.BaseGeometry): A Shapely geometry object Returns: shapely.geometry.MultiPolygon: A MultiPolygon representation of the input geometry. """ log.info(f"Geometry Type: {anydata.geom_type}") log.info(f"Anydata Empty: {anydata.is_empty}") ## bug: empty mesh circle makes anydata empty: geometry type 'GeometryCollection' if anydata.geom_type == "MultiPolygon": return anydata elif anydata.geom_type == "Polygon": if not anydata.is_empty: return shapely.geometry.MultiPolygon([anydata]) else: return MultiPolygon() else: log.info("Shapely Conversion Aborted") return MultiPolygon()
[docs] def shapely_to_coordinates(anydata): """Convert a Shapely geometry object to a list of coordinates. This function takes a Shapely geometry object and extracts its coordinates based on the geometry type. It handles various types of geometries including Polygon, MultiPolygon, LineString, MultiLineString, and GeometryCollection. If the geometry is empty or of type MultiPoint, it returns an empty list. The coordinates are returned in a nested list format, where each sublist corresponds to the exterior or interior coordinates of the geometries. Args: anydata (shapely.geometry.base.BaseGeometry): A Shapely geometry object Returns: list: A list of coordinates extracted from the input geometry. The structure of the list depends on the geometry type. """ p = anydata seq = [] if p.is_empty: return seq elif p.geom_type == "Polygon": clen = len(p.exterior.coords) seq = [p.exterior.coords] for interior in p.interiors: seq.append(interior.coords) elif p.geom_type == "MultiPolygon": clen = 0 seq = [] for sp in p.geoms: clen += len(sp.exterior.coords) seq.append(sp.exterior.coords) for interior in sp.interiors: seq.append(interior.coords) elif p.geom_type == "MultiLineString": seq = [] for linestring in p.geoms: seq.append(linestring.coords) elif p.geom_type == "LineString": seq = [] seq.append(p.coords) elif p.geom_type == "MultiPoint": return elif p.geom_type == "GeometryCollection": clen = 0 seq = [] for sp in p.geoms: # TODO clen += len(sp.exterior.coords) seq.append(sp.exterior.coords) for interior in sp.interiors: seq.extend(interior.coords) return seq
[docs] def shapely_to_curve(name, p, z, cyclic=True): """Create a 3D curve object in Blender from a Shapely geometry. This function takes a Shapely geometry and converts it into a 3D curve object in Blender. It extracts the coordinates from the Shapely geometry and creates a new curve object with the specified name. The curve is created in the 3D space at the given z-coordinate, with a default weight for the points. Args: name (str): The name of the curve object to be created. p (shapely.geometry): A Shapely geometry object from which to extract coordinates. z (float): The z-coordinate for all points of the curve. Returns: bpy.types.Object: The newly created curve object in Blender. """ import bpy # verts = [] # edges = [] # vi = 0 # ci = 0 seq = shapely_to_coordinates(p) w = 1 # weight curvedata = bpy.data.curves.new(name=name, type="CURVE") curvedata.dimensions = "3D" objectdata = bpy.data.objects.new(name, curvedata) objectdata.location = (0, 0, 0) # object origin bpy.context.collection.objects.link(objectdata) for c in seq: polyline = curvedata.splines.new("POLY") polyline.points.add(len(c) - 1) for num in range(len(c)): x, y = c[num][0], c[num][1] polyline.points[num].co = (x, y, z, w) bpy.context.view_layer.objects.active = objectdata objectdata.select_set(state=True) for c in objectdata.data.splines: c.use_cyclic_u = cyclic return objectdata # bpy.context.active_object
[docs] def shapely_validate(chunks): validity_error = "Valid Geometry" remove_multiple("Invalid_Geometry_Marker") # remove old errors for ch in chunks: # first convert chunk to poly if len(ch.points) > 2: try: ch.poly = Polygon(ch.points[:, :2]) except Exception as exc: message = "Invalid Curve Geometry" raise CamException(message) from exc if not ch.poly.is_valid: validity_error = explain_validity(ch.poly) if validity_error != "Valid Geometry": numbers = re.findall(r"-?\d+\.?\d*", validity_error) if len(numbers) >= 2: x = float(numbers[0]) y = float(numbers[1]) bpy.ops.curve.primitive_bezier_circle_add( radius=0.003, align="WORLD", location=(x, y, 0.0), ) obj = bpy.context.active_object obj.name = "Invalid_Geometry_Marker" obj.show_name = True # Get 3D View Context for overrides # So that 3D View Operators will still run # When called from Calculate Path button in Properties area = next(a for a in bpy.context.screen.areas if a.type == "VIEW_3D") region = next(r for r in area.regions if r.type == "WINDOW") with bpy.context.temp_override(area=area, region=region): bpy.ops.view3d.view_axis(type="TOP") bpy.ops.view3d.view_selected() space = area.spaces.active if space and space.type == "VIEW_3D": rv3d = space.region_3d rv3d.view_distance *= 0.01 warnings.warn(f"Invalid curve geometry: {validity_error}") #raise CamException(f"Invalid curve geometry: {validity_error}") else: ch.poly = Polygon() return validity_error
# this does more cleve chunks to Poly with hierarchies... ;)
[docs] def chunks_to_shapely(chunks): shapely_validate(chunks) for ppart in chunks: # then add hierarchy relations for ptest in chunks: if ( ppart != ptest and not ppart.poly.is_empty and not ptest.poly.is_empty and ptest.poly.contains(ppart.poly) ): # hierarchy works like this: - children get milled first. ppart.parents.append(ptest) for ch in chunks: # now make only simple polygons with holes, not more polys inside others found = False if len(ch.parents) % 2 == 1: for parent in ch.parents: if len(parent.parents) + 1 == len(ch.parents): # nparents serves as temporary storage for parents, ch.nparents = [parent] # not to get mixed with the first parenting during the check found = True break if not found: ch.nparents = [] for ch in chunks: # then subtract the 1st level holes ch.parents = ch.nparents ch.nparents = None if len(ch.parents) > 0: try: ch.parents[0].poly = ch.parents[0].poly.difference(ch.poly) # Polygon( ch.parents[0].poly, ch.poly) except: log.info("chunksToShapely oops!") lastPt = None tolerance = 0.0000003 newPoints = [] for pt in ch.points: toleranceXok = True toleranceYok = True if lastPt is not None: if abs(pt[0] - lastPt[0]) < tolerance: toleranceXok = False if abs(pt[1] - lastPt[1]) < tolerance: toleranceYok = False if toleranceXok or toleranceYok: newPoints.append(pt) lastPt = pt else: newPoints.append(pt) lastPt = pt toleranceXok = True toleranceYok = True if abs(newPoints[0][0] - lastPt[0]) < tolerance: toleranceXok = False if abs(newPoints[0][1] - lastPt[1]) < tolerance: toleranceYok = False if not toleranceXok and not toleranceYok: newPoints.pop() ch.points = np.array(newPoints) ch.poly = Polygon(ch.points) try: ch.parents[0].poly = ch.parents[0].poly.difference(ch.poly) except: # print('chunksToShapely double oops!') lastPt = None tolerance = 0.0000003 newPoints = [] for pt in ch.parents[0].points: toleranceXok = True toleranceYok = True # print( '{0:.9f}, {0:.9f}, {0:.9f}'.format(pt[0], pt[1], pt[2]) ) # print(pt) if lastPt is not None: if abs(pt[0] - lastPt[0]) < tolerance: toleranceXok = False if abs(pt[1] - lastPt[1]) < tolerance: toleranceYok = False if toleranceXok or toleranceYok: newPoints.append(pt) lastPt = pt else: newPoints.append(pt) lastPt = pt toleranceXok = True toleranceYok = True if abs(newPoints[0][0] - lastPt[0]) < tolerance: toleranceXok = False if abs(newPoints[0][1] - lastPt[1]) < tolerance: toleranceYok = False if not toleranceXok and not toleranceYok: newPoints.pop() # print('starting and ending points too close, removing ending point') ch.parents[0].points = np.array(newPoints) ch.parents[0].poly = Polygon(ch.parents[0].points) ch.parents[0].poly = ch.parents[0].poly.difference(ch.poly) # Polygon( ch.parents[0].poly, ch.poly) returnpolys = [] for polyi in range(len(chunks)): # export only the booleaned polygons ch = chunks[polyi] if not ch.poly.is_empty and len(ch.parents) == 0: returnpolys.append(ch.poly) polys = MultiPolygon(returnpolys) return polys
[docs] def shapely_to_chunks(p, zlevel): chunk_builders = [] seq = shapely_to_coordinates(p) for i, s in enumerate(seq): if len(s) > 1: chunk = CamPathChunkBuilder([]) for v in s: if p.has_z: chunk.points.append((v[0], v[1], v[2])) else: chunk.points.append((v[0], v[1], zlevel)) chunk_builders.append(chunk) chunk_builders.reverse() # this is for smaller shapes first. return [c.to_chunk() for c in chunk_builders]