kopia lustrzana https://github.com/vilemduha/blendercam
296 wiersze
9.4 KiB
Python
296 wiersze
9.4 KiB
Python
"""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,
|
|
)
|
|
|
|
|
|
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)
|
|
|
|
|
|
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)
|
|
|
|
|
|
class PackObjectsSettings(PropertyGroup):
|
|
"""stores all data for machines"""
|
|
|
|
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",
|
|
)
|
|
sheet_x: FloatProperty(
|
|
name="X Size",
|
|
description="Sheet size",
|
|
min=0.001,
|
|
max=10,
|
|
default=0.5,
|
|
precision=constants.PRECISION,
|
|
unit="LENGTH",
|
|
)
|
|
sheet_y: FloatProperty(
|
|
name="Y Size",
|
|
description="Sheet size",
|
|
min=0.001,
|
|
max=10,
|
|
default=0.5,
|
|
precision=constants.PRECISION,
|
|
unit="LENGTH",
|
|
)
|
|
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",
|
|
)
|
|
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",
|
|
)
|
|
rotate: BoolProperty(
|
|
name="Enable Rotation",
|
|
description="Enable rotation of elements",
|
|
default=True,
|
|
)
|
|
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",
|
|
)
|