Source code for cam.pack

"""BlenderCAM 'pack.py' © 2012 Vilem Novak

Takes all selected curves, converts them to polygons, offsets them by the pre-set margin
then chooses a starting location possibly inside the already occupied area and moves and rotates the
polygon out of the occupied area if one or more positions are found where the poly doesn't overlap,
it is placed and added to the occupied area - allpoly
Very slow and STUPID, a collision algorithm would be much much faster...
"""

from math import pi
import random
import time

import shapely
from shapely import geometry as sgeometry
from shapely import (
    affinity,
    prepared,
    speedups
)

import bpy
from bpy.types import PropertyGroup
from bpy.props import (
    BoolProperty,
    EnumProperty,
    FloatProperty,
)
from mathutils import (
    Euler,
    Vector
)

from . import (
    constants,
    polygon_utils_cam,
    simple,
    utils,
)


[docs] def srotate(s, r, x, y): """Rotate a polygon's coordinates around a specified point. This function takes a polygon and rotates its exterior coordinates around a given point (x, y) by a specified angle (r) in radians. It uses the Euler rotation to compute the new coordinates for each point in the polygon's exterior. The resulting coordinates are then used to create a new polygon. Args: s (shapely.geometry.Polygon): The polygon to be rotated. r (float): The angle of rotation in radians. x (float): The x-coordinate of the point around which to rotate. y (float): The y-coordinate of the point around which to rotate. Returns: shapely.geometry.Polygon: A new polygon with the rotated coordinates. """ ncoords = [] e = Euler((0, 0, r)) for p in s.exterior.coords: v1 = Vector((p[0], p[1], 0)) v2 = Vector((x, y, 0)) v = v1 - v2 v.rotate(e) ncoords.append((v[0], v[1])) return sgeometry.Polygon(ncoords)
[docs] def packCurves(): """Pack selected curves into a defined area based on specified settings. This function organizes selected curve objects in Blender by packing them into a specified area defined by the camera pack settings. It calculates the optimal positions for each curve while considering parameters such as sheet size, fill direction, distance, tolerance, and rotation. The function utilizes geometric operations to ensure that the curves do not overlap and fit within the defined boundaries. The packed curves are then transformed and their properties are updated accordingly. The function performs the following steps: 1. Activates speedup features if available. 2. Retrieves packing settings from the current scene. 3. Processes each selected object to create polygons from curves. 4. Attempts to place each polygon within the defined area while avoiding overlaps and respecting the specified fill direction. 5. Outputs the final arrangement of polygons. """ if speedups.available: speedups.enable() t = time.time() packsettings = bpy.context.scene.cam_pack sheetsizex = packsettings.sheet_x sheetsizey = packsettings.sheet_y direction = packsettings.sheet_fill_direction distance = packsettings.distance tolerance = packsettings.tolerance rotate = packsettings.rotate rotate_angle = packsettings.rotate_angle # in this, position, rotation, and actual poly will be stored. polyfield = [] for ob in bpy.context.selected_objects: simple.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 = utils.curveToChunks(ob) npolys = utils.chunksToShapely(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(sgeometry.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 i = 0 p = polyfield[0][2] placedpolys = [] rotcenter = sgeometry.Point(0, 0) for pf in polyfield: print(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)): if 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) # print(mindist) mindist = mindist - 0.5 * (xmax - xmin) # print(mindist) # print(iter) # reset polygon to best position here: ptrans = affinity.rotate(porig, best[2], rotcenter, use_radians=True) ptrans = affinity.translate(ptrans, best[0], best[1]) print(best[0], best[1], itera) placedpolys.append(ptrans) allpoly = prepared.prep(sgeometry.MultiPolygon(placedpolys)) # cleanup allpoly print(itera, hits, 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 i += 1 t = time.time() - t polygon_utils_cam.shapelyToCurve('test', sgeometry.MultiPolygon(placedpolys), 0) print(t)
[docs] class PackObjectsSettings(PropertyGroup): """stores all data for machines"""
[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=constants.PRECISION, unit="LENGTH", )
[docs] sheet_y: FloatProperty( name="Y Size", description="Sheet size", min=0.001, max=10, default=0.5, precision=constants.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=constants.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=constants.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, subtype="ANGLE", unit="ROTATION", )