Source code for fabex.operators.pack_op

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

Blender Operator definitions are in this file.
They mostly call the functions from 'utils.py'
"""

import random
import time
from math import pi
from typing import ClassVar

import bpy
import shapely
from bpy.props import (
    BoolProperty,
    EnumProperty,
    FloatProperty,
)
from bpy.types import Operator
from shapely import (
    affinity,
    prepared,
    speedups,
)
from shapely.geometry import (
    MultiPolygon,
    Point,
    Polygon,
)

from ..constants import PRECISION
from ..utilities.curve_utils import curve_to_chunks
from ..utilities.logging_utils import log
from ..utilities.shapely_utils import chunks_to_shapely, shapely_to_curve
from ..utilities.simple_utils import activate


[docs] class CamPackObjects(Operator): """Calculate All CAM Paths"""
[docs] bl_idname = "object.cam_pack_objects"
[docs] bl_label = "Pack Curves on Sheet"
[docs] bl_options: ClassVar = {"REGISTER", "UNDO"}
[docs] sheet_fill_direction: EnumProperty( name="Fill Direction", items=( ("X", "X", "Fills sheet in X axis direction"), ("Y", "Y", "Fills sheet in Y axis direction"), ), description="Fill direction of the packer algorithm", default="Y", )
[docs] sheet_x: FloatProperty( name="X Size", description="Sheet size", min=0.001, max=10, default=0.5, precision=PRECISION, unit="LENGTH", )
[docs] sheet_y: FloatProperty( name="Y Size", description="Sheet size", min=0.001, max=10, default=0.5, precision=PRECISION, unit="LENGTH", )
[docs] distance: FloatProperty( name="Minimum Distance", description="Minimum distance between objects(should be at least cutter diameter!)", min=0.001, max=10, default=0.01, precision=PRECISION, unit="LENGTH", )
[docs] tolerance: FloatProperty( name="Placement Tolerance", description="Tolerance for placement: smaller value slower placemant", min=0.001, max=0.02, default=0.005, precision=PRECISION, unit="LENGTH", )
[docs] rotate: BoolProperty( name="Enable Rotation", description="Enable rotation of elements", default=True, )
[docs] rotate_angle: FloatProperty( name="Placement Angle Rotation Step", description="Bigger rotation angle, faster placemant", default=0.19635 * 4, min=pi / 180, max=pi, precision=5, step=500, subtype="ANGLE", unit="ROTATION", )
@classmethod
[docs] def poll(cls, context): return context.active_object is not None and context.active_object.type == "CURVE"
[docs] def invoke(self, context, event): wm = context.window_manager return wm.invoke_props_dialog(self)
[docs] def execute(self, context): """Execute the operation in the given context. This function sets the Blender object mode to 'OBJECT', retrieves the currently selected objects, and calls the `pack_curves` function from the `pack` module. It is typically used to finalize operations on selected objects in Blender. Args: context: The context in which the operation is executed. Returns: dict: A dictionary indicating the completion status of the operation. """ bpy.ops.object.mode_set(mode="OBJECT") # force object mode if speedups.available: speedups.enable() t = time.time() sheetsizex = self.sheet_x sheetsizey = self.sheet_y direction = self.sheet_fill_direction distance = self.distance tolerance = self.tolerance rotate = self.rotate rotate_angle = self.rotate_angle # in this, position, rotation, and actual poly will be stored. polyfield = [] for ob in bpy.context.selected_objects: activate(ob) bpy.ops.object.make_single_user(type="SELECTED_OBJECTS") bpy.ops.object.origin_set(type="ORIGIN_GEOMETRY") z = ob.location.z bpy.ops.object.location_clear() bpy.ops.object.rotation_clear() chunks = curve_to_chunks(ob) npolys = chunks_to_shapely(chunks) # add all polys in silh to one poly poly = shapely.ops.unary_union(npolys) poly = poly.buffer(distance / 1.5, 8) poly = poly.simplify(0.0003) polyfield.append([[0, 0], 0.0, poly, ob, z]) random.shuffle(polyfield) # primitive layout here: allpoly = prepared.prep(Polygon()) # main collision poly. shift = tolerance # one milimeter by now. rotchange = rotate_angle # in radians xmin, ymin, xmax, ymax = polyfield[0][2].bounds if direction == "X": mindist = -xmin else: mindist = -ymin p = polyfield[0][2] placedpolys = [] rotcenter = Point(0, 0) for i, pf in enumerate(polyfield): log.info(f"Polygon: {i}") rot = 0 porig = pf[2] placed = False xmin, ymin, xmax, ymax = p.bounds if direction == "X": x = mindist y = -ymin if direction == "Y": x = -xmin y = mindist itera = 0 best = None hits = 0 besthit = None while not placed: # swap x and y, and add to x # print(x,y) p = porig if rotate: ptrans = affinity.rotate(p, rot, origin=rotcenter, use_radians=True) ptrans = affinity.translate(ptrans, x, y) else: ptrans = affinity.translate(p, x, y) xmin, ymin, xmax, ymax = ptrans.bounds # print(iter,p.bounds) if ( xmin > 0 and ymin > 0 and ( (direction == "Y" and xmax < sheetsizex) or (direction == "X" and ymax < sheetsizey) ) ) and not allpoly.intersects(ptrans): # we do more good solutions, choose best out of them: hits += 1 if best is None: best = [x, y, rot, xmax, ymax] besthit = hits if direction == "X": if xmax < best[3]: best = [x, y, rot, xmax, ymax] besthit = hits elif ymax < best[4]: best = [x, y, rot, xmax, ymax] besthit = hits if hits >= 15 or (itera > 20000 and hits > 0): # here was originally more, but 90% of best solutions are still 1 placed = True pf[3].location.x = best[0] pf[3].location.y = best[1] pf[3].location.z = pf[4] pf[3].rotation_euler.z = best[2] pf[3].select_set(state=True) mindist = mindist - 0.5 * (xmax - xmin) # reset polygon to best position here: ptrans = affinity.rotate(porig, best[2], rotcenter, use_radians=True) ptrans = affinity.translate(ptrans, best[0], best[1]) log.info(f"X: {best[0]}") log.info(f"Y: {best[1]}") log.info(f"Iteration: {itera}") placedpolys.append(ptrans) allpoly = prepared.prep(MultiPolygon(placedpolys)) # cleanup allpoly log.info(f"Iteration: {itera}") log.info(f"Hits: {hits}") log.info(f"Best Hit: {besthit}") if not placed: if direction == "Y": x += shift mindist = y if xmax + shift > sheetsizex: x = x - xmin y += shift if direction == "X": y += shift mindist = x if ymax + shift > sheetsizey: y = y - ymin x += shift if rotate: rot += rotchange itera += 1 t = time.time() - t shapely_to_curve("test", MultiPolygon(placedpolys), 0) log.info(f"Time: {t}") # layout. return {"FINISHED"}
[docs] def draw(self, context): layout = self.layout layout.use_property_split = True layout.use_property_decorate = False box = layout.box() col = box.column(align=True) col.label(text="Sheet Size") col.prop(self, "sheet_x", text="X") col.prop(self, "sheet_y", text="Y") col.prop(self, "sheet_fill_direction") col = layout.column(align=True) col.prop(self, "distance") col.prop(self, "tolerance") header, panel = col.panel_prop(self, "rotate") header.label(text="Rotation") if panel: col = panel.column(align=True) col.prop(self, "rotate_angle", text="Placement Angle Step")