"""Fabex 'curve_cam_tools.py' © 2012 Vilem Novak, 2021 Alain Pelletier
Operators that perform various functions on existing curves.
"""
from math import pi, tan
import shapely
from shapely.geometry import (
LineString,
MultiLineString,
)
import bpy
from bpy.props import (
BoolProperty,
EnumProperty,
FloatProperty,
)
from bpy.types import Operator
from mathutils import Vector
from ..cam_chunk import (
curve_to_shapely,
polygon_boolean,
polygon_convex_hull,
silhouette_offset,
get_object_silhouette,
)
from ..utilities.geom_utils import circle
from ..utilities.shapely_utils import (
shapely_to_curve,
)
from ..utilities.simple_utils import (
remove_multiple,
join_multiple,
)
# boolean operations for curve objects
[docs]
class CamCurveBoolean(Operator):
"""Perform Boolean Operation on Two or More Curves"""
[docs]
bl_idname = "object.curve_boolean"
[docs]
bl_label = "Curve Boolean"
[docs]
bl_options = {"REGISTER", "UNDO"}
[docs]
boolean_type: EnumProperty(
name="Type",
items=(
("UNION", "Union", ""),
("DIFFERENCE", "Difference", ""),
("INTERSECT", "Intersect", ""),
),
description="Boolean type",
default="UNION",
)
@classmethod
[docs]
def poll(cls, context):
return context.active_object is not None and context.active_object.type in ["CURVE", "FONT"]
[docs]
def execute(self, context):
if len(context.selected_objects) > 1:
polygon_boolean(context, self.boolean_type)
return {"FINISHED"}
else:
self.report({"ERROR"}, "at least 2 curves must be selected")
return {"CANCELLED"}
[docs]
def invoke(self, context, event):
return context.window_manager.invoke_props_dialog(self)
[docs]
class CamCurveConvexHull(Operator):
"""Perform Hull Operation on Single or Multiple Curves""" # by Alain Pelletier april 2021
[docs]
bl_idname = "object.convex_hull"
[docs]
bl_label = "Convex Hull"
[docs]
bl_options = {"REGISTER", "UNDO"}
@classmethod
[docs]
def poll(cls, context):
return context.active_object is not None and context.active_object.type in ["CURVE", "FONT"]
[docs]
def execute(self, context):
polygon_convex_hull(context)
return {"FINISHED"}
# intarsion or joints
[docs]
class CamCurveIntarsion(Operator):
"""Makes Curve Cuttable Both Inside and Outside, for Intarsion and Joints"""
[docs]
bl_idname = "object.curve_intarsion"
[docs]
bl_options = {"REGISTER", "UNDO", "PRESET"}
[docs]
diameter: FloatProperty(
name="Cutter Diameter",
default=0.001,
min=0,
max=0.025,
precision=4,
unit="LENGTH",
)
[docs]
tolerance: FloatProperty(
name="Cutout Tolerance",
default=0.0001,
min=0,
max=0.005,
precision=4,
unit="LENGTH",
)
[docs]
backlight: FloatProperty(
name="Backlight Seat",
default=0.000,
min=0,
max=0.010,
precision=4,
unit="LENGTH",
)
[docs]
perimeter_cut: FloatProperty(
name="Perimeter Cut Offset",
default=0.000,
min=0,
max=0.100,
precision=4,
unit="LENGTH",
)
[docs]
base_thickness: FloatProperty(
name="Base Material Thickness",
default=0.000,
min=0,
max=0.100,
precision=4,
unit="LENGTH",
)
[docs]
intarsion_thickness: FloatProperty(
name="Intarsion Material Thickness",
default=0.000,
min=0,
max=0.100,
precision=4,
unit="LENGTH",
)
[docs]
backlight_depth_from_top: FloatProperty(
name="Backlight Well Depth",
default=0.000,
min=0,
max=0.100,
precision=4,
unit="LENGTH",
)
@classmethod
[docs]
def poll(cls, context):
return context.active_object is not None and (
context.active_object.type in ["CURVE", "FONT"]
)
[docs]
def execute(self, context):
selected = context.selected_objects # save original selected items
remove_multiple("intarsion_")
for ob in selected:
ob.select_set(True) # select original curves
# Perimeter cut largen then intarsion pocket externally, optional
# make the diameter 5% larger and compensate for backlight
diam = self.diameter * 1.05 + self.backlight * 2
silhouette_offset(context, -diam / 2)
o1 = bpy.context.active_object
silhouette_offset(context, diam)
o2 = bpy.context.active_object
silhouette_offset(context, -diam / 2)
o3 = bpy.context.active_object
o1.select_set(True)
o2.select_set(True)
o3.select_set(False)
# delete o1 and o2 temporary working curves
bpy.ops.object.delete(use_global=False)
o3.name = "intarsion_pocket" # this is the pocket for intarsion
bpy.context.object.location[2] = -self.intarsion_thickness
if self.perimeter_cut > 0.0:
silhouette_offset(context, self.perimeter_cut)
bpy.context.active_object.name = "intarsion_perimeter"
bpy.context.object.location[2] = -self.base_thickness
bpy.ops.object.select_all(action="DESELECT") # deselect new curve
o3.select_set(True)
context.view_layer.objects.active = o3
# intarsion profile is the inside piece of the intarsion
# make smaller curve for material profile
silhouette_offset(context, -self.tolerance / 2)
bpy.context.object.location[2] = self.intarsion_thickness
o4 = bpy.context.active_object
bpy.context.active_object.name = "intarsion_profil"
o4.select_set(False)
if self.backlight > 0.0: # Make a smaller curve for backlighting purposes
silhouette_offset(context, (-self.tolerance / 2) - self.backlight)
bpy.context.active_object.name = "intarsion_backlight"
bpy.context.object.location[2] = (
-self.backlight_depth_from_top - self.intarsion_thickness
)
o4.select_set(True)
o3.select_set(True)
return {"FINISHED"}
[docs]
def invoke(self, context, event):
return context.window_manager.invoke_props_dialog(self)
# intarsion or joints
[docs]
class CamCurveSimpleOvercuts(Operator):
"""Adds Simple Fillets / Overcuts for Slots"""
[docs]
bl_idname = "object.curve_overcuts"
[docs]
bl_label = "Simple Fillet Overcuts"
[docs]
bl_options = {"REGISTER", "UNDO"}
[docs]
diameter: FloatProperty(
name="Diameter",
default=0.003175,
min=0,
max=100,
precision=4,
unit="LENGTH",
)
[docs]
threshold: FloatProperty(
name="Threshold",
default=pi / 2 * 0.99,
min=-3.14,
max=3.14,
precision=4,
step=500,
subtype="ANGLE",
unit="ROTATION",
)
[docs]
do_outer: BoolProperty(
name="Outer Polygons",
default=True,
)
[docs]
invert: BoolProperty(
name="Invert",
default=False,
)
@classmethod
[docs]
def poll(cls, context):
return context.active_object is not None and (
context.active_object.type in ["CURVE", "FONT"]
)
[docs]
def execute(self, context):
bpy.ops.object.curve_remove_doubles()
o1 = bpy.context.active_object
shapes = curve_to_shapely(o1)
negative_overcuts = []
positive_overcuts = []
diameter = self.diameter * 1.001
for s in shapes.geoms:
s = shapely.geometry.polygon.orient(s, 1)
if s.boundary.geom_type == "LineString":
loops = MultiLineString([s.boundary])
else:
loops = s.boundary
for ci, c in enumerate(loops.geoms):
if ci > 0 or self.do_outer:
for i, co in enumerate(c.coords):
i1 = i - 1
if i1 == -1:
i1 = -2
i2 = i + 1
if i2 == len(c.coords):
i2 = 0
v1 = Vector(co) - Vector(c.coords[i1])
v1 = v1.xy # Vector((v1.x,v1.y,0))
v2 = Vector(c.coords[i2]) - Vector(co)
v2 = v2.xy # v2 = Vector((v2.x,v2.y,0))
if not v1.length == 0 and not v2.length == 0:
a = v1.angle_signed(v2)
sign = 1
if self.invert: # and ci>0:
sign *= -1
if (sign < 0 and a < -self.threshold) or (
sign > 0 and a > self.threshold
):
p = Vector((co[0], co[1]))
v1.normalize()
v2.normalize()
v = v1 - v2
v.normalize()
p = p - v * diameter / 2
if abs(a) < pi / 2:
shape = circle(diameter / 2, 64)
shape = shapely.affinity.translate(shape, p.x, p.y)
else:
l = tan(a / 2) * diameter / 2
p1 = p - sign * v * l
l = shapely.geometry.LineString((p, p1))
shape = l.buffer(diameter / 2, resolution=64)
if sign > 0:
negative_overcuts.append(shape)
else:
positive_overcuts.append(shape)
negative_overcuts = shapely.ops.unary_union(negative_overcuts)
positive_overcuts = shapely.ops.unary_union(positive_overcuts)
fs = shapely.ops.unary_union(shapes)
fs = fs.union(positive_overcuts)
fs = fs.difference(negative_overcuts)
shapely_to_curve(o1.name + "_overcuts", fs, o1.location.z)
return {"FINISHED"}
[docs]
def invoke(self, context, event):
return context.window_manager.invoke_props_dialog(self)
# Overcut type B
[docs]
class CamCurveBoneFilletOvercuts(Operator):
"""Adds Dogbone, T-bone Fillets / Overcuts for Slots"""
[docs]
bl_idname = "object.curve_overcuts_b"
[docs]
bl_label = "Bone Fillet Overcuts"
[docs]
bl_options = {"REGISTER", "UNDO"}
[docs]
diameter: FloatProperty(
name="Tool Diameter",
default=0.003175,
description="Tool bit diameter used in cut operation",
min=0,
max=100,
precision=4,
unit="LENGTH",
)
[docs]
style: EnumProperty(
name="Style",
items=(
("OPEDGE", "opposite edge", "place corner overcuts on opposite edges"),
("DOGBONE", "Dog-bone / Corner Point", "place overcuts at center of corners"),
("TBONE", "T-bone", "place corner overcuts on the same edge"),
),
default="DOGBONE",
description="style of overcut to use",
)
[docs]
threshold: FloatProperty(
name="Max Inside Angle",
default=pi / 2,
min=-3.14,
max=3.14,
description="The maximum angle to be considered as an inside corner",
precision=4,
step=500,
subtype="ANGLE",
unit="ROTATION",
)
[docs]
do_outer: BoolProperty(
name="Include Outer Curve",
description="Include the outer curve if there are curves inside",
default=True,
)
[docs]
do_invert: BoolProperty(
name="Invert",
description="invert overcut operation on all curves",
default=True,
)
[docs]
other_edge: BoolProperty(
name="Other Edge",
description="change to the other edge for the overcut to be on",
default=False,
)
@classmethod
[docs]
def poll(cls, context):
return context.active_object is not None and context.active_object.type == "CURVE"
[docs]
def execute(self, context):
bpy.ops.object.curve_remove_doubles()
o1 = bpy.context.active_object
shapes = curve_to_shapely(o1)
negative_overcuts = []
positive_overcuts = []
# count all the corners including inside and out
cornerCnt = 0
# a list of tuples for defining the inside corner
# tuple is: (pos, v1, v2, angle, allCorners list index)
insideCorners = []
diameter = self.diameter * 1.002 # make bit size slightly larger to allow cutter
radius = diameter / 2
anglethreshold = pi - self.threshold
centerv = Vector((0, 0))
extendedv = Vector((0, 0))
pos = Vector((0, 0))
sign = -1 if self.do_invert else 1
isTBone = self.style == "TBONE"
# indexes in insideCorner tuple
POS, V1, V2, A, IDX = range(5)
def add_overcut(a):
nonlocal pos, centerv, radius, extendedv, sign, negative_overcuts, positive_overcuts
# move the overcut shape center position 1 radius in direction v
pos -= centerv * radius
print("abs(a)", abs(a))
if abs(a) <= pi / 2 + 0.0001:
print("<=pi/2")
shape = circle(radius, 64)
shape = shapely.affinity.translate(shape, pos.x, pos.y)
else: # elongate overcut circle to make sure tool bit can fit into slot
print(">pi/2")
p1 = pos + (extendedv * radius)
l = shapely.geometry.LineString((pos, p1))
shape = l.buffer(radius, resolution=64)
if sign > 0:
negative_overcuts.append(shape)
else:
positive_overcuts.append(shape)
def set_other_edge(v1, v2, a):
nonlocal centerv, extendedv
if self.other_edge:
centerv = v1
extendedv = v2
else:
centerv = -v2
extendedv = -v1
add_overcut(a)
def set_center_offset(a):
nonlocal centerv, extendedv, sign
centerv = v1 - v2
centerv.normalize()
extendedv = centerv * tan(a / 2) * -sign
add_overcut(a)
def get_corner(idx, offset):
nonlocal insideCorners
idx += offset
if idx >= len(insideCorners):
idx -= len(insideCorners)
return insideCorners[idx]
def get_corner_delta(curidx, nextidx):
nonlocal cornerCnt
delta = nextidx - curidx
if delta < 0:
delta += cornerCnt
return delta
for s in shapes.geoms:
# ensure the shape is counterclockwise
s = shapely.geometry.polygon.orient(s, 1)
if s.boundary.geom_type == "LineString":
from shapely import MultiLineString
loops = MultiLineString([s.boundary])
else:
loops = s.boundary
outercurve = self.do_outer or len(loops.geoms) == 1
for ci, c in enumerate(loops.geoms):
if ci > 0 or outercurve:
if isTBone:
cornerCnt = 0
insideCorners = []
for i, co in enumerate(c.coords):
i1 = i - 1
if i1 == -1:
i1 = -2
i2 = i + 1
if i2 == len(c.coords):
i2 = 0
v1 = Vector(co).xy - Vector(c.coords[i1]).xy
v2 = Vector(c.coords[i2]).xy - Vector(co).xy
if not v1.length == 0 and not v2.length == 0:
a = v1.angle_signed(v2)
insideCornerFound = False
outsideCornerFound = False
if a < -anglethreshold:
if sign < 0:
insideCornerFound = True
else:
outsideCornerFound = True
elif a > anglethreshold:
if sign > 0:
insideCornerFound = True
else:
outsideCornerFound = True
if insideCornerFound:
# an inside corner with an overcut has been found
# which means a new side has been found
pos = Vector((co[0], co[1]))
v1.normalize()
v2.normalize()
# figure out which direction vector to use
# v is the main direction vector to move the overcut shape along
# ev is the direction vector used to elongate the overcut shape
if self.style != "DOGBONE":
# t-bone and opposite edge styles get treated nearly the same
if isTBone:
cornerCnt += 1
# insideCorner tuplet: (pos, v1, v2, angle, corner index)
insideCorners.append((pos, v1, v2, a, cornerCnt - 1))
# processing of corners for T-Bone are done after all points are processed
continue
set_other_edge(v1, v2, a)
else: # DOGBONE style
set_center_offset(a)
elif isTBone and outsideCornerFound:
# add an outside corner to the list
cornerCnt += 1
# check if t-bone processing required
# if no inside corners then nothing to do
if isTBone and len(insideCorners) > 0:
print("corner count", cornerCnt, "inside corner count", len(insideCorners))
# process all of the inside corners
for i, corner in enumerate(insideCorners):
pos, v1, v2, a, idx = corner
# figure out which side of the corner to do overcut
# if prev corner is outside corner
# calc index distance between current corner and prev
prevCorner = get_corner(i, -1)
print("first:", i, idx, prevCorner[IDX])
if get_corner_delta(prevCorner[IDX], idx) == 1:
# make sure there is an outside corner
print(get_corner_delta(get_corner(i, -2)[IDX], idx))
if get_corner_delta(get_corner(i, -2)[IDX], idx) > 2:
set_other_edge(v1, v2, a)
print("first won")
continue
nextCorner = get_corner(i, 1)
print("second:", i, idx, nextCorner[IDX])
if get_corner_delta(idx, nextCorner[IDX]) == 1:
# make sure there is an outside corner
print(get_corner_delta(idx, get_corner(i, 2)[IDX]))
if get_corner_delta(idx, get_corner(i, 2)[IDX]) > 2:
print("second won")
set_other_edge(-v2, -v1, a)
continue
print("third")
if get_corner_delta(prevCorner[IDX], idx) == 3:
# check if they share the same edge
a1 = v1.angle_signed(prevCorner[V2]) * 180.0 / pi
print("third won", a1)
if a1 < -135 or a1 > 135:
set_other_edge(-v2, -v1, a)
continue
print("fourth")
if get_corner_delta(idx, nextCorner[IDX]) == 3:
# check if they share the same edge
a1 = v2.angle_signed(nextCorner[V1]) * 180.0 / pi
print("fourth won", a1)
if a1 < -135 or a1 > 135:
set_other_edge(v1, v2, a)
continue
print("***No Win***")
# the default if no other rules pass
set_center_offset(a)
negative_overcuts = shapely.ops.unary_union(negative_overcuts)
positive_overcuts = shapely.ops.unary_union(positive_overcuts)
fs = shapely.ops.unary_union(shapes)
fs = fs.union(positive_overcuts)
fs = fs.difference(negative_overcuts)
shapely_to_curve(o1.name + "_overcuts", fs, o1.location.z)
return {"FINISHED"}
[docs]
def invoke(self, context, event):
return context.window_manager.invoke_props_dialog(self)
[docs]
class CamCurveRemoveDoubles(Operator):
"""Remove Duplicate Points from the Selected Curve"""
[docs]
bl_idname = "object.curve_remove_doubles"
[docs]
bl_label = "Remove Curve Doubles"
[docs]
bl_options = {"REGISTER", "UNDO"}
[docs]
merge_distance: FloatProperty(
name="Merge distance",
default=0.0001,
min=0,
max=0.01,
)
[docs]
keep_bezier: BoolProperty(
name="Keep bezier",
default=False,
)
@classmethod
[docs]
def poll(cls, context):
return context.active_object is not None and (context.active_object.type == "CURVE")
[docs]
def execute(self, context):
obj = bpy.context.selected_objects
for ob in obj:
if ob.type == "CURVE":
if self.keep_bezier:
if ob.data.splines and ob.data.splines[0].type == "BEZIER":
bpy.ops.curvetools.operatorsplinesremoveshort()
bpy.context.view_layer.objects.active = ob
ob.data.resolution_u = 64
if bpy.context.mode == "OBJECT":
bpy.ops.object.editmode_toggle()
bpy.ops.curve.select_all()
bpy.ops.curve.remove_double(distance=self.merge_distance)
bpy.ops.object.editmode_toggle()
else:
self.merge_distance = 0
if bpy.context.mode == "EDIT_CURVE":
bpy.ops.object.editmode_toggle()
bpy.ops.object.convert(target="MESH")
bpy.ops.object.editmode_toggle()
bpy.ops.mesh.select_all(action="SELECT")
bpy.ops.mesh.remove_doubles(threshold=self.merge_distance)
bpy.ops.object.editmode_toggle()
bpy.ops.object.convert(target="CURVE")
return {"FINISHED"}
[docs]
def draw(self, context):
layout = self.layout
obj = context.active_object
if obj.type == "CURVE":
if obj.data.splines and obj.data.splines[0].type == "BEZIER":
layout.prop(self, "keep_bezier", text="Keep Bezier")
layout.prop(self, "merge_distance", text="Merge Distance")
[docs]
def invoke(self, context, event):
return context.window_manager.invoke_props_dialog(self)
[docs]
class CamMeshGetPockets(Operator):
"""Detect Pockets in a Mesh and Extract Them as Curves"""
[docs]
bl_idname = "object.mesh_get_pockets"
[docs]
bl_label = "Get Pocket Surfaces"
[docs]
bl_options = {"REGISTER", "UNDO"}
[docs]
threshold: FloatProperty(
name="Horizontal Threshold",
description="How horizontal the surface must be for a pocket: "
"1.0 perfectly flat, 0.0 is any orientation",
default=0.99,
min=0,
max=1.0,
precision=4,
)
[docs]
z_limit: FloatProperty(
name="Z Limit",
description="Maximum z height considered for pocket operation, " "default is 0.0",
default=0.0,
min=-1000.0,
max=1000.0,
precision=4,
unit="LENGTH",
)
@classmethod
[docs]
def poll(cls, context):
return context.active_object is not None and (context.active_object.type == "MESH")
[docs]
def execute(self, context):
obs = bpy.context.selected_objects
s = bpy.context.scene
cobs = []
for ob in obs:
if ob.type == "MESH":
pockets = {}
mw = ob.matrix_world
mesh = ob.data
bpy.ops.object.editmode_toggle()
bpy.ops.mesh.select_mode(use_extend=False, use_expand=False, type="FACE")
bpy.ops.mesh.select_all(action="DESELECT")
bpy.ops.object.editmode_toggle()
i = 0
for face in mesh.polygons:
# n = mw @ face.normal
n = face.normal.to_4d()
n.w = 0
n = (mw @ n).to_3d().normalized()
if n.z > self.threshold:
face.select = True
z = (mw @ mesh.vertices[face.vertices[0]].co).z
if z < self.z_limit:
if pockets.get(z) is None:
pockets[z] = [i]
else:
pockets[z].append(i)
i += 1
print(len(pockets))
for p in pockets:
print(p)
ao = bpy.context.active_object
i = 0
for p in pockets:
print(i)
i += 1
sf = pockets[p]
for face in mesh.polygons:
face.select = False
for fi in sf:
face = mesh.polygons[fi]
face.select = True
bpy.ops.object.editmode_toggle()
bpy.ops.mesh.select_mode(use_extend=False, use_expand=False, type="EDGE")
bpy.ops.mesh.region_to_loop()
bpy.ops.mesh.separate(type="SELECTED")
bpy.ops.mesh.select_mode(use_extend=False, use_expand=False, type="FACE")
bpy.ops.object.editmode_toggle()
ao.select_set(state=False)
bpy.context.view_layer.objects.active = bpy.context.selected_objects[0]
cobs.append(bpy.context.selected_objects[0])
bpy.ops.object.convert(target="CURVE")
bpy.ops.object.origin_set(type="ORIGIN_GEOMETRY")
bpy.context.selected_objects[0].select_set(False)
ao.select_set(state=True)
bpy.context.view_layer.objects.active = ao
# bpy.ops.mesh.select_mode(use_extend=False, use_expand=False, type='EDGE')
# turn off selection of all objects in 3d view
bpy.ops.object.select_all(action="DESELECT")
# make new curves more visible by making them selected in the 3d view
# This also allows the active object to still work with the operator
# if the user decides to change the horizontal threshold property
col = bpy.data.collections.new("multi level pocket ")
s.collection.children.link(col)
for obj in cobs:
col.objects.link(obj)
return {"FINISHED"}
# this operator finds the silhouette of objects(meshes, curves just get converted) and offsets it.
[docs]
class CamOffsetSilhouete(Operator):
"""Offset Object Silhouette"""
[docs]
bl_idname = "object.silhouette_offset"
[docs]
bl_label = "Silhouette & Offset"
[docs]
bl_options = {"REGISTER", "UNDO", "PRESET"}
[docs]
offset: FloatProperty(
name="Offset",
default=0.003,
min=-100,
max=100,
precision=4,
unit="LENGTH",
)
[docs]
mitre_limit: FloatProperty(
name="Mitre Limit",
default=2,
min=0.00000001,
max=20,
precision=4,
unit="LENGTH",
)
[docs]
style: EnumProperty(
name="Corner Type",
items=(("1", "Round", ""), ("2", "Mitre", ""), ("3", "Bevel", "")),
)
[docs]
caps: EnumProperty(
name="Cap Type",
items=(("round", "Round", ""), ("square", "Square", ""), ("flat", "Flat", "")),
)
[docs]
align: EnumProperty(
name="Alignment",
items=(("worldxy", "World XY", ""), ("bottom", "Base Bottom", ""), ("top", "Base Top", "")),
)
[docs]
open_type: EnumProperty(
name="Curve Type",
items=(
("dilate", "Dilate open curve", ""),
("leaveopen", "Leave curve open", ""),
("closecurve", "Close curve", ""),
),
default="closecurve",
)
@classmethod
[docs]
def poll(cls, context):
return (
context.active_object is not None
and context.active_object.type in ["CURVE", "FONT", "MESH"]
and context.mode == "OBJECT"
)
[docs]
def is_straight(self, geom):
assert geom.geom_type == "LineString", geom.geom_type
length = geom.length
start_pt = geom.interpolate(0)
end_pt = geom.interpolate(1, normalized=True)
straight_dist = start_pt.distance(end_pt)
if straight_dist == 0.0:
if length == 0.0:
return True
return False
elif length / straight_dist == 1:
return True
else:
return False
# this is almost same as getobjectoutline, just without the need of operation data
[docs]
def execute(self, context):
# bpy.ops.object.curve_remove_doubles()
ob = context.active_object
if ob.type == "FONT":
bpy.context.object.data.resolution_u = 64
if ob.type == "CURVE":
if ob.data.splines and ob.data.splines[0].type == "BEZIER":
bpy.context.object.data.resolution_u = 64
bpy.ops.object.curve_remove_doubles(merge_distance=0.0001, keep_bezier=True)
else:
bpy.ops.object.curve_remove_doubles()
bpy.ops.object.duplicate()
obj = context.active_object
if context.active_object.type != "MESH":
obj.data.dimensions = "3D"
bpy.ops.object.transform_apply(
location=True, rotation=True, scale=True
) # apply all transforms
bpy.ops.object.convert(target="MESH")
bpy.context.active_object.name = "temp_mesh"
# get the Z align point from the base
if self.align == "top":
point = max(
[
(bpy.context.object.matrix_world @ v.co).z
for v in bpy.context.object.data.vertices
]
)
elif self.align == "bottom":
point = min(
[
(bpy.context.object.matrix_world @ v.co).z
for v in bpy.context.object.data.vertices
]
)
else:
point = 0
# extract X,Y coordinates from the vertices data and put them into a LineString object
coords = []
for v in obj.data.vertices:
coords.append((v.co.x, v.co.y))
remove_multiple("temp_mesh") # delete temporary mesh
remove_multiple("dilation") # delete old dilation objects
# convert coordinates to shapely LineString datastructure
line = LineString(coords)
# if curve is a straight segment, change offset type to dilate
if self.is_straight(line) and self.open_type != "leaveopen":
self.open_type = "dilate"
# make the dilate or open curve offset
if (self.open_type != "closecurve") and ob.type == "CURVE":
print("line length=", round(line.length * 1000), "mm")
if self.style == "3":
style = "bevel"
elif self.style == "2":
style = "mitre"
else:
style = "round"
if self.open_type == "leaveopen":
new_shape = shapely.offset_curve(
line, self.offset, join_style=style
) # use shapely to expand without closing the curve
name = "Offset: " + "%.2f" % round(self.offset * 1000) + "mm - " + ob.name
else:
new_shape = line.buffer(
self.offset,
cap_style=self.caps,
resolution=16,
join_style=style,
mitre_limit=self.mitre_limit,
) # use shapely to expand, closing the curve
name = "Dilation: " + "%.2f" % round(self.offset * 1000) + "mm - " + ob.name
# create the actual offset object based on the Shapely offset
shapely_to_curve(name, new_shape, 0, self.open_type != "leaveopen")
# position the object according to the calculated point
bpy.context.object.location.z = point
# if curve is not a straight line and neither dilate or leave open are selected, create a normal offset
else:
bpy.context.view_layer.objects.active = ob
silhouette_offset(context, self.offset, int(self.style), self.mitre_limit)
return {"FINISHED"}
[docs]
def draw(self, context):
layout = self.layout
layout.prop(self, "offset", text="Offset")
layout.prop(self, "open_type", text="Type")
layout.prop(self, "style", text="Corner")
if self.style == "2":
layout.prop(self, "mitrelimit", text="Mitre Limit")
if self.open_type == "dilate":
layout.prop(self, "caps", text="Cap")
if self.open_type != "closecurve":
layout.prop(self, "align", text="Align")
[docs]
def invoke(self, context, event):
return context.window_manager.invoke_props_dialog(self)
# Finds object silhouette, usefull for meshes, since with curves it's not needed.
[docs]
class CamObjectSilhouette(Operator):
"""Create Object Silhouette"""
[docs]
bl_idname = "object.silhouette"
[docs]
bl_label = "Object Silhouette"
[docs]
bl_options = {"REGISTER", "UNDO"}
@classmethod
[docs]
def poll(cls, context):
return context.active_object is not None and (
context.active_object.type == "FONT" or context.active_object.type == "MESH"
)
# this is almost same as getobjectoutline, just without the need of operation data
[docs]
def execute(self, context):
ob = bpy.context.active_object
self.silh = get_object_silhouette("OBJECTS", objects=bpy.context.selected_objects)
bpy.context.scene.cursor.location = (0, 0, 0)
for smp in self.silh.geoms:
shapely_to_curve(ob.name + "_silhouette", smp, 0)
join_multiple(ob.name + "_silhouette")
bpy.context.scene.cursor.location = ob.location
bpy.ops.object.origin_set(type="ORIGIN_CURSOR")
bpy.ops.object.curve_remove_doubles()
return {"FINISHED"}