Source code for fabex.utilities.silhouette_utils

import time

import numpy as np
from shapely.ops import unary_union
from shapely.geometry import (
    MultiPolygon,
    Polygon,
)

import bpy

from .curve_utils import (
    curve_to_shapely,
    curve_to_chunks,
)
from .image_utils import (
    image_to_chunks,
    render_sample_image,
)

from .logging_utils import log
from .shapely_utils import (
    chunks_to_shapely,
    shapely_to_curve,
    shapely_to_multipolygon,
)


# separate function in blender, so you can offset any curve.
# FIXME: same algorithms as the cutout strategy, because that is hierarchy-respecting.
[docs] def silhouette_offset(context, offset, style=1, mitrelimit=1.0): """Offset the silhouette of a curve or font object in Blender. This function takes an active curve or font object in Blender and creates an offset silhouette based on the specified parameters. It first retrieves the silhouette of the object and then applies a buffer operation to create the offset shape. The resulting shape is then converted back into a curve object in the Blender scene. Args: context (bpy.context): The current Blender context. offset (float): The distance to offset the silhouette. style (int?): The join style for the offset. Defaults to 1. mitrelimit (float?): The mitre limit for the offset. Defaults to 1.0. Returns: dict: A dictionary indicating the operation is finished. """ bpy.context.scene.cursor.location = (0, 0, 0) ob = bpy.context.active_object silhs = ( curve_to_shapely(ob) if ob.type in ["CURVE", "FONT"] else get_object_silhouette("OBJECTS", [ob]) ) mp = silhs.buffer( offset, cap_style=1, join_style=style, resolution=16, mitre_limit=mitrelimit, ) shapely_to_curve(f"{ob.name}_offset_{round(offset, 5)}", mp, ob.location.z) bpy.ops.object.curve_remove_doubles() return {"FINISHED"}
[docs] def get_object_silhouette(stype, objects=None, use_modifiers=False): """Get the silhouette of objects based on the specified type. This function computes the silhouette of a given set of objects in Blender based on the specified type. It can handle both curves and mesh objects, converting curves to polygon format and calculating the silhouette for mesh objects. The function also considers the use of modifiers if specified. The silhouette is generated by processing the geometry of the objects and returning a Shapely representation of the silhouette. Args: stype (str): The type of silhouette to generate ('CURVES' or 'OBJECTS'). objects (list?): A list of Blender objects to process. Defaults to None. use_modifiers (bool?): Whether to apply modifiers to the objects. Defaults to False. Returns: shapely.geometry.MultiPolygon: The computed silhouette as a Shapely MultiPolygon. """ log.info(f"Silhouette Type: {stype}") if stype == "CURVES": # curve conversion to polygon format allchunks = [] for ob in objects: chunks = curve_to_chunks(ob) allchunks.extend(chunks) silhouette = chunks_to_shapely(allchunks) elif stype == "OBJECTS": totfaces = 0 for ob in objects: totfaces += len(ob.data.polygons) if totfaces < 20000000: # boolean polygons method originaly was 20 000 poly limit, now limitless, t = time.time() log.info("Shapely Getting Silhouette") polys = [] for ob in objects: log.info(f"Object: {ob.name}") if use_modifiers: ob = ob.evaluated_get(bpy.context.evaluated_depsgraph_get()) m = ob.to_mesh() else: m = ob.data mw = ob.matrix_world mwi = mw.inverted() r = ob.rotation_euler m.calc_loop_triangles() id = 0 e = 0.000001 scaleup = 100 for tri in m.loop_triangles: n = tri.normal.copy() n.rotate(r) if tri.area > 0 and n.z != 0: s = [] c = mw @ tri.center c = c.xy for vert_index in tri.vertices: v = mw @ m.vertices[vert_index].co s.append((v.x, v.y)) if len(s) > 2: p = Polygon(s) if p.is_valid: polys.append(p.buffer(e, resolution=0)) id += 1 ob.select_set(False) if totfaces < 20000: p = unary_union(polys) else: bigshapes = [] i = 1 part = 20000 log.info("Computing in Parts") while i * part < totfaces: log.info(i) ar = polys[(i - 1) * part : i * part] bigshapes.append(unary_union(ar)) i += 1 if (i - 1) * part < totfaces: last_ar = polys[(i - 1) * part :] bigshapes.append(unary_union(last_ar)) log.info("Joining") p = unary_union(bigshapes) log.info(f"Time: {time.time() - t}") t = time.time() silhouette = shapely_to_multipolygon(p) return silhouette
[docs] def get_operation_silhouette(operation): """Gets the silhouette for the given operation. This function determines the silhouette of an operation using image thresholding techniques. It handles different geometry sources, such as objects or images, and applies specific methods based on the type of geometry. If the geometry source is 'OBJECT' or 'COLLECTION', it checks whether to process curves or not. The function also considers the number of faces in mesh objects to decide on the appropriate method for silhouette extraction. Args: operation (Operation): An object containing the necessary data Returns: Silhouette: The computed silhouette for the operation. """ if operation.update_silhouette_tag: image = None objects = None if operation.geometry_source in ["OBJECT", "COLLECTION"]: if not operation.onlycurves: stype = "OBJECTS" else: stype = "CURVES" else: stype = "IMAGE" totfaces = 0 # totfaces += [ # len(ob.data.polygons) # for ob in operation.objects # if ob.type == "MESH" and stype == "OBJECT" # ] if stype == "OBJECTS": for ob in operation.objects: if ob.type == "MESH": totfaces += len(ob.data.polygons) if (stype == "OBJECTS" and totfaces > 200000) or stype == "IMAGE": log.info("Image Method") samples = render_sample_image(operation) if stype == "OBJECTS": i = samples > operation.min_z - 0.0000001 # #the small number solves issue with totally flat meshes, which people tend to mill instead of # proper pockets. then the minimum was also maximum, and it didn't detect contour. else: # this fixes another numeric imprecision. i = samples > np.min(operation.zbuffer_image) chunks = image_to_chunks(operation, i) operation.silhouette = chunks_to_shapely(chunks) # this conversion happens because we need the silh to be oriented, for milling directions. else: log.info("~ Object Method for Retrieving Silhouette ~") operation.silhouette = get_object_silhouette( stype, objects=operation.objects, use_modifiers=operation.use_modifiers, ) operation.update_silhouette_tag = False return operation.silhouette
[docs] def get_object_outline(radius, o, Offset): """Get the outline of a geometric object based on specified parameters. This function generates an outline for a given geometric object by applying a buffer operation to its polygons. The buffer radius can be adjusted based on the `radius` parameter, and the operation can be offset based on the `Offset` flag. The function also considers whether the polygons should be merged or not, depending on the properties of the object `o`. Args: radius (float): The radius for the buffer operation. o (object): An object containing properties that influence the outline generation. Offset (bool): A flag indicating whether to apply a positive or negative offset. Returns: MultiPolygon: The resulting outline of the geometric object as a MultiPolygon. """ # FIXME: make this one operation independent # circle detail, optimize, optimize thresold. polygons = get_operation_silhouette(o) i = 0 offset = 1 if Offset else -1 outlines = [] i = 0 join = 2 if o.straight else 1 polygon_list = polygons if isinstance(polygons, list) else polygons.geoms for p1 in polygon_list: # sort by size before this??? i += 1 if radius > 0: p1 = p1.buffer( radius * offset, resolution=o.optimisation.circle_detail, join_style=join, mitre_limit=2, ) outlines.append(p1) outline = MultiPolygon(outlines) if o.dont_merge else unary_union(outlines) return outline