591 lines
20 KiB
Python
591 lines
20 KiB
Python
# SPDX-License-Identifier: GPL-2.0-or-later
|
|
#
|
|
# Contributed to by guy lateur, Alexander Meißner (Lichtso),
|
|
# Dealga McArdle (zeffii), Marvin.K.Breuer (MKB),
|
|
# Spivak Vladimir (cwolf3d)
|
|
# Originally an addon by Mackraken
|
|
|
|
|
|
bl_info = {
|
|
"name": "Curve Tools",
|
|
"description": "Adds some functionality for bezier/nurbs curve/surface modeling",
|
|
"author": "Mackraken, Spivak Vladimir (cwolf3d)",
|
|
"version": (0, 4, 5),
|
|
"blender": (2, 80, 0),
|
|
"location": "View3D > Tool Shelf > Edit Tab",
|
|
"doc_url": "{BLENDER_MANUAL_URL}/addons/add_curve/curve_tools.html",
|
|
"category": "Add Curve",
|
|
}
|
|
|
|
|
|
import os, bpy, importlib, math
|
|
from bpy.types import (
|
|
Operator,
|
|
Panel,
|
|
PropertyGroup,
|
|
)
|
|
from bpy.props import (
|
|
BoolProperty,
|
|
IntProperty,
|
|
FloatProperty,
|
|
EnumProperty,
|
|
CollectionProperty,
|
|
StringProperty,
|
|
FloatVectorProperty,
|
|
)
|
|
from . import properties, operators, auto_loft, outline, remove_doubles
|
|
from . import path_finder, show_resolution, splines_sequence, fillet
|
|
from . import internal, cad, toolpath, exports
|
|
|
|
if 'bpy' in locals():
|
|
importlib.reload(properties)
|
|
importlib.reload(operators)
|
|
importlib.reload(auto_loft)
|
|
importlib.reload(outline)
|
|
importlib.reload(remove_doubles)
|
|
importlib.reload(path_finder)
|
|
importlib.reload(show_resolution)
|
|
importlib.reload(splines_sequence)
|
|
importlib.reload(fillet)
|
|
importlib.reload(internal)
|
|
importlib.reload(cad)
|
|
importlib.reload(toolpath)
|
|
importlib.reload(exports)
|
|
|
|
from bpy.types import (
|
|
AddonPreferences,
|
|
)
|
|
|
|
|
|
def UpdateDummy(object, context):
|
|
scene = context.scene
|
|
SINGLEDROP = scene.UTSingleDrop
|
|
MOREDROP = scene.UTMOREDROP
|
|
LOFTDROP = scene.UTLoftDrop
|
|
ADVANCEDDROP = scene.UTAdvancedDrop
|
|
EXTENDEDDROP = scene.UTExtendedDrop
|
|
UTILSDROP = scene.UTUtilsDrop
|
|
|
|
|
|
class curvetoolsSettings(PropertyGroup):
|
|
# selection
|
|
SelectedObjects: CollectionProperty(
|
|
type=properties.curvetoolsSelectedObject
|
|
)
|
|
NrSelectedObjects: IntProperty(
|
|
name="NrSelectedObjects",
|
|
default=0,
|
|
description="Number of selected objects",
|
|
update=UpdateDummy
|
|
)
|
|
# curve
|
|
CurveLength: FloatProperty(
|
|
name="CurveLength",
|
|
default=0.0,
|
|
precision=6
|
|
)
|
|
# splines
|
|
SplineResolution: IntProperty(
|
|
name="SplineResolution",
|
|
default=64,
|
|
min=2, max=1024,
|
|
soft_min=2,
|
|
description="Spline resolution will be set to this value"
|
|
)
|
|
SplineRemoveLength: FloatProperty(
|
|
name="SplineRemoveLength",
|
|
default=0.001,
|
|
precision=6,
|
|
description="Splines shorter than this threshold length will be removed"
|
|
)
|
|
SplineJoinDistance: FloatProperty(
|
|
name="SplineJoinDistance",
|
|
default=0.001,
|
|
precision=6,
|
|
description="Splines with starting/ending points closer to each other "
|
|
"than this threshold distance will be joined"
|
|
)
|
|
SplineJoinStartEnd: BoolProperty(
|
|
name="SplineJoinStartEnd",
|
|
default=False,
|
|
description="Only join splines at the starting point of one and the ending point of the other"
|
|
)
|
|
splineJoinModeItems = (
|
|
('At_midpoint', 'At midpoint', 'Join splines at midpoint of neighbouring points'),
|
|
('Insert_segment', 'Insert segment', 'Insert segment between neighbouring points')
|
|
)
|
|
SplineJoinMode: EnumProperty(
|
|
items=splineJoinModeItems,
|
|
name="SplineJoinMode",
|
|
default='At_midpoint',
|
|
description="Determines how the splines will be joined"
|
|
)
|
|
# curve intersection
|
|
LimitDistance: FloatProperty(
|
|
name="LimitDistance",
|
|
default=0.0001,
|
|
precision=6,
|
|
description="Displays the result of the curve length calculation"
|
|
)
|
|
|
|
intAlgorithmItems = (
|
|
('3D', '3D', 'Detect where curves intersect in 3D'),
|
|
('From_View', 'From View', 'Detect where curves intersect in the RegionView3D')
|
|
)
|
|
IntersectCurvesAlgorithm: EnumProperty(
|
|
items=intAlgorithmItems,
|
|
name="IntersectCurvesAlgorithm",
|
|
description="Determines how the intersection points will be detected",
|
|
default='3D'
|
|
)
|
|
intModeItems = (
|
|
('Insert', 'Insert', 'Insert points into the existing spline(s)'),
|
|
('Split', 'Split', 'Split the existing spline(s) into 2'),
|
|
('Empty', 'Empty', 'Add empty at intersections')
|
|
)
|
|
IntersectCurvesMode: EnumProperty(
|
|
items=intModeItems,
|
|
name="IntersectCurvesMode",
|
|
description="Determines what happens at the intersection points",
|
|
default='Split'
|
|
)
|
|
intAffectItems = (
|
|
('Both', 'Both', 'Insert points into both curves'),
|
|
('Active', 'Active', 'Insert points into active curve only'),
|
|
('Other', 'Other', 'Insert points into other curve only')
|
|
)
|
|
IntersectCurvesAffect: EnumProperty(
|
|
items=intAffectItems,
|
|
name="IntersectCurvesAffect",
|
|
description="Determines which of the selected curves will be affected by the operation",
|
|
default='Both'
|
|
)
|
|
PathFinderRadius: FloatProperty(
|
|
name="PathFinder detection radius",
|
|
default=0.2,
|
|
precision=6,
|
|
description="PathFinder detection radius"
|
|
)
|
|
curve_vertcolor: FloatVectorProperty(
|
|
name="OUT",
|
|
default=(0.2, 0.9, 0.9, 1),
|
|
size=4,
|
|
subtype="COLOR",
|
|
min=0,
|
|
max=1
|
|
)
|
|
path_color: FloatVectorProperty(
|
|
name="OUT",
|
|
default=(0.2, 0.9, 0.9, 0.1),
|
|
size=4,
|
|
subtype="COLOR",
|
|
min=0,
|
|
max=1
|
|
)
|
|
path_thickness: IntProperty(
|
|
name="Path thickness",
|
|
default=10,
|
|
min=1, max=1024,
|
|
soft_min=2,
|
|
description="Path thickness (px)"
|
|
)
|
|
sequence_color: FloatVectorProperty(
|
|
name="OUT",
|
|
default=(0.2, 0.9, 0.9, 1),
|
|
size=4,
|
|
subtype="COLOR",
|
|
min=0,
|
|
max=1
|
|
)
|
|
font_thickness: IntProperty(
|
|
name="Font thickness",
|
|
default=2,
|
|
min=1, max=1024,
|
|
soft_min=2,
|
|
description="Font thickness (px)"
|
|
)
|
|
font_size: FloatProperty(
|
|
name="Font size",
|
|
default=0.1,
|
|
precision=3,
|
|
description="Font size"
|
|
)
|
|
|
|
|
|
# Curve Info
|
|
class VIEW3D_PT_curve_tools_info(Panel):
|
|
bl_space_type = "VIEW_3D"
|
|
bl_region_type = "UI"
|
|
bl_category = "Curve Edit"
|
|
bl_label = "Curve Info"
|
|
bl_options = {'DEFAULT_CLOSED'}
|
|
|
|
def draw(self, context):
|
|
scene = context.scene
|
|
layout = self.layout
|
|
|
|
col = layout.column(align=True)
|
|
col.operator("curvetools.operatorcurveinfo", text="Curve")
|
|
row = col.row(align=True)
|
|
row.operator("curvetools.operatorsplinesinfo", text="Spline")
|
|
row.operator("curvetools.operatorsegmentsinfo", text="Segment")
|
|
row = col.row(align=True)
|
|
row.operator("curvetools.operatorcurvelength", icon = "DRIVER_DISTANCE", text="Length")
|
|
row.prop(context.scene.curvetools, "CurveLength", text="")
|
|
|
|
# Curve Edit
|
|
class VIEW3D_PT_curve_tools_edit(Panel):
|
|
bl_space_type = "VIEW_3D"
|
|
bl_region_type = "UI"
|
|
bl_category = "Curve Edit"
|
|
bl_label = "Curve Edit"
|
|
|
|
|
|
def draw(self, context):
|
|
scene = context.scene
|
|
layout = self.layout
|
|
|
|
col = layout.column(align=True)
|
|
col.operator("curvetools.bezier_points_fillet", text='Fillet/Chamfer')
|
|
row = col.row(align=True)
|
|
row.operator("curvetools.outline", text="Outline")
|
|
row.operator("curvetools.add_toolpath_offset_curve", text="Recursive Offset")
|
|
col.operator("curvetools.sep_outline", text="Separate Offset/Selected")
|
|
col.operator("curvetools.bezier_cad_handle_projection", text='Extend Handles')
|
|
col.operator("curvetools.bezier_cad_boolean", text="Boolean Splines")
|
|
row = col.row(align=True)
|
|
row.operator("curvetools.bezier_spline_divide", text='Subdivide')
|
|
row.operator("curvetools.bezier_cad_subdivide", text="Multi Subdivide")
|
|
|
|
col.operator("curvetools.split", text='Split at Vertex')
|
|
col.operator("curvetools.add_toolpath_discretize_curve", text="Discretize Curve")
|
|
col.operator("curvetools.bezier_cad_array", text="Array Splines")
|
|
|
|
# Curve Intersect
|
|
class VIEW3D_PT_curve_tools_intersect(Panel):
|
|
bl_space_type = "VIEW_3D"
|
|
bl_region_type = "UI"
|
|
bl_category = "Curve Edit"
|
|
bl_label = "Intersect"
|
|
bl_options = {'DEFAULT_CLOSED'}
|
|
|
|
def draw(self, context):
|
|
scene = context.scene
|
|
layout = self.layout
|
|
|
|
col = layout.column(align=True)
|
|
col.operator("curvetools.bezier_curve_boolean", text="2D Curve Boolean")
|
|
col.operator("curvetools.operatorintersectcurves", text="Intersect Curves")
|
|
col.prop(context.scene.curvetools, "LimitDistance", text="Limit Distance")
|
|
col.prop(context.scene.curvetools, "IntersectCurvesAlgorithm", text="Algorithm")
|
|
col.prop(context.scene.curvetools, "IntersectCurvesMode", text="Mode")
|
|
col.prop(context.scene.curvetools, "IntersectCurvesAffect", text="Affect")
|
|
|
|
# Curve Surfaces
|
|
class VIEW3D_PT_curve_tools_surfaces(Panel):
|
|
bl_space_type = "VIEW_3D"
|
|
bl_region_type = "UI"
|
|
bl_category = "Curve Edit"
|
|
bl_label = "Surfaces"
|
|
bl_options = {'DEFAULT_CLOSED'}
|
|
|
|
def draw(self, context):
|
|
wm = context.window_manager
|
|
scene = context.scene
|
|
layout = self.layout
|
|
|
|
col = layout.column(align=True)
|
|
col.operator("curvetools.operatorbirail", text="Birail")
|
|
col.operator("curvetools.convert_bezier_to_surface", text="Convert Bezier to Surface")
|
|
col.operator("curvetools.convert_selected_face_to_bezier", text="Convert Faces to Bezier")
|
|
|
|
# Curve Path Finder
|
|
class VIEW3D_PT_curve_tools_loft(Panel):
|
|
bl_space_type = "VIEW_3D"
|
|
bl_region_type = "UI"
|
|
bl_category = "Curve Edit"
|
|
bl_parent_id = "VIEW3D_PT_curve_tools_surfaces"
|
|
bl_label = "Loft"
|
|
bl_options = {'DEFAULT_CLOSED'}
|
|
|
|
def draw(self, context):
|
|
wm = context.window_manager
|
|
scene = context.scene
|
|
layout = self.layout
|
|
|
|
col = layout.column(align=True)
|
|
col.operator("curvetools.create_auto_loft")
|
|
lofters = [o for o in scene.objects if "autoloft" in o.keys()]
|
|
for o in lofters:
|
|
col.label(text=o.name)
|
|
# layout.prop(o, '["autoloft"]', toggle=True)
|
|
col.prop(wm, "auto_loft", toggle=True)
|
|
col.operator("curvetools.update_auto_loft_curves")
|
|
col = layout.column(align=True)
|
|
|
|
|
|
# Curve Sanitize
|
|
class VIEW3D_PT_curve_tools_sanitize(Panel):
|
|
bl_space_type = "VIEW_3D"
|
|
bl_region_type = "UI"
|
|
bl_category = "Curve Edit"
|
|
bl_label = "Sanitize"
|
|
bl_options = {'DEFAULT_CLOSED'}
|
|
|
|
def draw(self, context):
|
|
scene = context.scene
|
|
layout = self.layout
|
|
|
|
col = layout.column(align=True)
|
|
col.operator("curvetools.operatororigintospline0start", icon = "OBJECT_ORIGIN", text="Set Origin to Spline Start")
|
|
col.operator("curvetools.scale_reset", text='Reset Scale')
|
|
|
|
col.label(text="Cleanup:")
|
|
col.operator("curvetools.remove_doubles", icon = "TRASH", text='Remove Doubles')
|
|
col.operator("curvetools.operatorsplinesremovezerosegment", icon = "TRASH", text="0-Segment Splines")
|
|
row = col.row(align=True)
|
|
row.operator("curvetools.operatorsplinesremoveshort", text="Short Splines")
|
|
row.prop(context.scene.curvetools, "SplineRemoveLength", text="Threshold remove")
|
|
|
|
col.label(text="Join Splines:")
|
|
col.operator("curvetools.operatorsplinesjoinneighbouring", text="Join Neighbouring Splines")
|
|
row = col.row(align=True)
|
|
col.prop(context.scene.curvetools, "SplineJoinDistance", text="Threshold")
|
|
col.prop(context.scene.curvetools, "SplineJoinStartEnd", text="Only at Ends")
|
|
col.prop(context.scene.curvetools, "SplineJoinMode", text="Join Position")
|
|
|
|
# Curve Utilities
|
|
class VIEW3D_PT_curve_tools_utilities(Panel):
|
|
bl_space_type = "VIEW_3D"
|
|
bl_region_type = "UI"
|
|
bl_category = "Curve Edit"
|
|
bl_label = "Utilities"
|
|
bl_options = {'DEFAULT_CLOSED'}
|
|
|
|
def draw(self, context):
|
|
scene = context.scene
|
|
layout = self.layout
|
|
|
|
col = layout.column(align=True)
|
|
row = col.row(align=True)
|
|
row.label(text="Curve Resolution:")
|
|
row = col.row(align=True)
|
|
row.operator("curvetools.show_resolution", icon="HIDE_OFF", text="Show [ESC]")
|
|
row.prop(context.scene.curvetools, "curve_vertcolor", text="")
|
|
row = col.row(align=True)
|
|
row.operator("curvetools.operatorsplinessetresolution", text="Set Resolution")
|
|
row.prop(context.scene.curvetools, "SplineResolution", text="")
|
|
|
|
|
|
row = col.row(align=True)
|
|
row.label(text="Spline Order:")
|
|
row = col.row(align=True)
|
|
row.operator("curvetools.show_splines_sequence", icon="HIDE_OFF", text="Show [ESC]")
|
|
row.prop(context.scene.curvetools, "sequence_color", text="")
|
|
row = col.row(align=True)
|
|
row.prop(context.scene.curvetools, "font_size", text="Font Size")
|
|
row.prop(context.scene.curvetools, "font_thickness", text="Font Thickness")
|
|
row = col.row(align=True)
|
|
oper = row.operator("curvetools.rearrange_spline", text = "<")
|
|
oper.command = 'PREV'
|
|
oper = row.operator("curvetools.rearrange_spline", text = ">")
|
|
oper.command = 'NEXT'
|
|
row = col.row(align=True)
|
|
row.operator("curve.switch_direction", text="Switch Direction")
|
|
row = col.row(align=True)
|
|
row.operator("curvetools.set_first_points", text="Set First Points")
|
|
|
|
# Curve Path Finder
|
|
class VIEW3D_PT_curve_tools_pathfinder(Panel):
|
|
bl_space_type = "VIEW_3D"
|
|
bl_region_type = "UI"
|
|
bl_category = "Curve Edit"
|
|
bl_parent_id = "VIEW3D_PT_curve_tools_utilities"
|
|
bl_label = "Path Finder"
|
|
bl_options = {'DEFAULT_CLOSED'}
|
|
|
|
def draw(self, context):
|
|
scene = context.scene
|
|
layout = self.layout
|
|
|
|
col = layout.column(align=True)
|
|
col.operator("curvetools.pathfinder", text="Path Finder [ESC]")
|
|
col.prop(context.scene.curvetools, "PathFinderRadius", text="PathFinder Radius")
|
|
col.prop(context.scene.curvetools, "path_color", text="")
|
|
col.prop(context.scene.curvetools, "path_thickness", text="Thickness")
|
|
|
|
col = layout.column(align=True)
|
|
col.label(text="ESC or TAB - Exit PathFinder")
|
|
col.label(text="X or DEL - Delete")
|
|
col.label(text="Alt + Mouse Click - Select Spline")
|
|
col.label(text="Alt + Shift + Mouse click - Add Spline to Selection")
|
|
col.label(text="A - Deselect All")
|
|
|
|
# Add-ons Preferences Update Panel
|
|
|
|
# Define Panel classes for updating
|
|
panels = (
|
|
VIEW3D_PT_curve_tools_info, VIEW3D_PT_curve_tools_edit,
|
|
VIEW3D_PT_curve_tools_intersect, VIEW3D_PT_curve_tools_surfaces,
|
|
VIEW3D_PT_curve_tools_loft, VIEW3D_PT_curve_tools_sanitize,
|
|
VIEW3D_PT_curve_tools_utilities, VIEW3D_PT_curve_tools_pathfinder
|
|
)
|
|
|
|
|
|
def update_panel(self, context):
|
|
message = "Curve Tools: Updating Panel locations has failed"
|
|
try:
|
|
for panel in panels:
|
|
if "bl_rna" in panel.__dict__:
|
|
bpy.utils.unregister_class(panel)
|
|
|
|
for panel in panels:
|
|
panel.bl_category = context.preferences.addons[__name__].preferences.category
|
|
bpy.utils.register_class(panel)
|
|
|
|
except Exception as e:
|
|
print("\n[{}]\n{}\n\nError:\n{}".format(__name__, message, e))
|
|
pass
|
|
|
|
|
|
class CurveAddonPreferences(AddonPreferences):
|
|
# this must match the addon name, use '__package__'
|
|
# when defining this in a submodule of a python package.
|
|
bl_idname = __name__
|
|
|
|
category: StringProperty(
|
|
name="Tab Category",
|
|
description="Choose a name for the category of the panel",
|
|
default="Edit",
|
|
update=update_panel
|
|
)
|
|
|
|
def draw(self, context):
|
|
layout = self.layout
|
|
|
|
row = layout.row()
|
|
col = row.column()
|
|
col.label(text="Tab Category:")
|
|
col.prop(self, "category", text="")
|
|
|
|
# Context MENU
|
|
def curve_tools_context_menu(self, context):
|
|
bl_label = 'Curve tools'
|
|
|
|
self.layout.operator("curvetools.bezier_points_fillet", text="Fillet")
|
|
self.layout.operator("curvetools.bezier_cad_handle_projection", text='Handle Projection')
|
|
self.layout.operator("curvetools.bezier_spline_divide", text="Divide")
|
|
self.layout.operator("curvetools.add_toolpath_offset_curve", text="Offset Curve")
|
|
self.layout.operator("curvetools.remove_doubles", text='Remove Doubles')
|
|
self.layout.separator()
|
|
|
|
def curve_tools_object_context_menu(self, context):
|
|
bl_label = 'Curve tools'
|
|
|
|
if context.active_object.type == "CURVE":
|
|
self.layout.operator("curvetools.scale_reset", text="Scale Reset")
|
|
self.layout.operator("curvetools.add_toolpath_offset_curve", text="Offset Curve")
|
|
self.layout.operator("curvetools.remove_doubles", text='Remove Doubles')
|
|
self.layout.separator()
|
|
|
|
# Import-export 2d svg
|
|
def menu_file_export(self, context):
|
|
for operator in exports.operators:
|
|
self.layout.operator(operator.bl_idname)
|
|
|
|
def menu_file_import(self, context):
|
|
for operator in imports.operators:
|
|
self.layout.operator(operator.bl_idname)
|
|
|
|
# REGISTER
|
|
classes = cad.operators + \
|
|
toolpath.operators + \
|
|
exports.operators + \
|
|
operators.operators + \
|
|
properties.operators + \
|
|
path_finder.operators + \
|
|
show_resolution.operators + \
|
|
splines_sequence.operators + \
|
|
outline.operators + \
|
|
fillet.operators + \
|
|
remove_doubles.operators + \
|
|
[
|
|
CurveAddonPreferences,
|
|
curvetoolsSettings,
|
|
]
|
|
|
|
def register():
|
|
bpy.types.Scene.UTSingleDrop = BoolProperty(
|
|
name="One Curve",
|
|
default=False,
|
|
description="One Curve"
|
|
)
|
|
bpy.types.Scene.UTMOREDROP = BoolProperty(
|
|
name="Curves",
|
|
default=False,
|
|
description="Curves"
|
|
)
|
|
bpy.types.Scene.UTLoftDrop = BoolProperty(
|
|
name="Two Curves Loft",
|
|
default=False,
|
|
description="Two Curves Loft"
|
|
)
|
|
bpy.types.Scene.UTAdvancedDrop = BoolProperty(
|
|
name="Advanced",
|
|
default=True,
|
|
description="Advanced"
|
|
)
|
|
bpy.types.Scene.UTExtendedDrop = BoolProperty(
|
|
name="Extended",
|
|
default=False,
|
|
description="Extended"
|
|
)
|
|
bpy.types.Scene.UTUtilsDrop = BoolProperty(
|
|
name="Curves Utils",
|
|
default=True,
|
|
description="Curves Utils"
|
|
)
|
|
|
|
for cls in classes:
|
|
bpy.utils.register_class(cls)
|
|
|
|
for panel in panels:
|
|
bpy.utils.register_class(panel)
|
|
|
|
auto_loft.register()
|
|
|
|
bpy.types.TOPBAR_MT_file_export.append(menu_file_export)
|
|
|
|
bpy.types.Scene.curvetools = bpy.props.PointerProperty(type=curvetoolsSettings)
|
|
|
|
update_panel(None, bpy.context)
|
|
|
|
bpy.types.VIEW3D_MT_edit_curve_context_menu.prepend(curve_tools_context_menu)
|
|
bpy.types.VIEW3D_MT_object_context_menu.prepend(curve_tools_object_context_menu)
|
|
|
|
|
|
def unregister():
|
|
del bpy.types.Scene.UTSingleDrop
|
|
del bpy.types.Scene.UTMOREDROP
|
|
del bpy.types.Scene.UTLoftDrop
|
|
del bpy.types.Scene.UTAdvancedDrop
|
|
del bpy.types.Scene.UTExtendedDrop
|
|
del bpy.types.Scene.UTUtilsDrop
|
|
|
|
auto_loft.unregister()
|
|
|
|
bpy.types.TOPBAR_MT_file_export.remove(menu_file_export)
|
|
|
|
bpy.types.VIEW3D_MT_edit_curve_context_menu.remove(curve_tools_context_menu)
|
|
bpy.types.VIEW3D_MT_object_context_menu.remove(curve_tools_object_context_menu)
|
|
|
|
for panel in panels:
|
|
bpy.utils.unregister_class(panel)
|
|
|
|
for cls in classes:
|
|
bpy.utils.unregister_class(cls)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
register()
|