From bd54740ed08be078a870fcb3d83c7bd4ad304d43 Mon Sep 17 00:00:00 2001 From: "Vladimir Spivak(cwolf3d)" Date: Thu, 12 Mar 2020 04:10:42 +0200 Subject: Fix T74493 and D7045. Redesign. --- curve_tools/__init__.py | 394 +++++++++++++++++++++---------------------- curve_tools/cad.py | 18 +- curve_tools/curves.py | 6 +- curve_tools/internal.py | 40 ++++- curve_tools/intersections.py | 6 +- curve_tools/operators.py | 149 +++++++++++++++- curve_tools/toolpath.py | 125 +++++++++++--- 7 files changed, 501 insertions(+), 237 deletions(-) (limited to 'curve_tools') diff --git a/curve_tools/__init__.py b/curve_tools/__init__.py index 2fcec1d9..4a9d283c 100644 --- a/curve_tools/__init__.py +++ b/curve_tools/__init__.py @@ -25,7 +25,7 @@ bl_info = { "name": "Curve Tools", "description": "Adds some functionality for bezier/nurbs curve/surface modeling", "author": "Mackraken", - "version": (0, 4, 3), + "version": (0, 4, 4), "blender": (2, 80, 0), "location": "View3D > Tool Shelf > Edit Tab", "warning": "WIP", @@ -128,13 +128,13 @@ class curvetoolsSettings(PropertyGroup): 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') + ('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', + default='At_midpoint', description="Determines how the splines will be joined" ) # curve intersection @@ -147,7 +147,7 @@ class curvetoolsSettings(PropertyGroup): intAlgorithmItems = ( ('3D', '3D', 'Detect where curves intersect in 3D'), - ('From View', 'From View', 'Detect where curves intersect in the RegionView3D') + ('From_View', 'From View', 'Detect where curves intersect in the RegionView3D') ) IntersectCurvesAlgorithm: EnumProperty( items=intAlgorithmItems, @@ -229,227 +229,223 @@ class curvetoolsSettings(PropertyGroup): ) -class VIEW3D_PT_CurvePanel(Panel): - bl_label = "Curve Tools" +# 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'} - bl_category = "Edit" - @classmethod - def poll(cls, context): - return context.scene is not None + 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 - SINGLEDROP = scene.UTSingleDrop - MOREDROP = scene.UTMOREDROP - LOFTDROP = scene.UTLoftDrop - ADVANCEDDROP = scene.UTAdvancedDrop - EXTENDEDDROP = scene.UTExtendedDrop - UTILSDROP = scene.UTUtilsDrop layout = self.layout - # Single Curve options - box1 = self.layout.box() - col = box1.column(align=True) + col = layout.column(align=True) + col.operator("curvetools.bezier_points_fillet", text='Fillet/Chamfer') row = col.row(align=True) - row.prop(scene, "UTSingleDrop", icon="TRIA_DOWN") - if SINGLEDROP: - # A. 1 curve - row = col.row(align=True) - - # A.1 curve info/length - row.operator("curvetools.operatorcurveinfo", text="Curve info") - row = col.row(align=True) - row.operator("curvetools.operatorcurvelength", text="Calc Length") - row.prop(context.scene.curvetools, "CurveLength", text="") - - # A.2 splines info - row = col.row(align=True) - row.operator("curvetools.operatorsplinesinfo", text="Curve splines info") - - # A.3 segments info - row = col.row(align=True) - row.operator("curvetools.operatorsegmentsinfo", text="Curve segments info") - - # A.4 origin to spline0start - row = col.row(align=True) - row.operator("curvetools.operatororigintospline0start", text="Set origin to spline start") - - # Double Curve options - box2 = self.layout.box() - col = box2.column(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.prop(scene, "UTMOREDROP", icon="TRIA_DOWN") + 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") - if MOREDROP: - # B. 2 curves - row = col.row(align=True) +# 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 - # B.1 curve intersections - row = col.row(align=True) - row.operator("curvetools.operatorintersectcurves", text="Intersect curves") + 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") - row = col.row(align=True) - row.prop(context.scene.curvetools, "LimitDistance", text="LimitDistance") +# 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'} - row = col.row(align=True) - row.prop(context.scene.curvetools, "IntersectCurvesAlgorithm", text="Algorithm") + def draw(self, context): + wm = context.window_manager + scene = context.scene + layout = self.layout - row = col.row(align=True) - row.prop(context.scene.curvetools, "IntersectCurvesMode", text="Mode") + 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) - row = col.row(align=True) - row.prop(context.scene.curvetools, "IntersectCurvesAffect", text="Affect") - # Loft options - box1 = self.layout.box() - col = box1.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) - row.prop(scene, "UTLoftDrop", icon="TRIA_DOWN") - - if LOFTDROP: - # B.2 surface generation - wm = context.window_manager - scene = context.scene - layout = self.layout - layout.operator("curvetools.create_auto_loft") - lofters = [o for o in scene.objects if "autoloft" in o.keys()] - for o in lofters: - layout.label(text=o.name) - # layout.prop(o, '["autoloft"]', toggle=True) - layout.prop(wm, "auto_loft", toggle=True) - layout.operator("curvetools.update_auto_loft_curves") - - # Advanced options - box1 = self.layout.box() - col = box1.column(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.prop(scene, "UTAdvancedDrop", icon="TRIA_DOWN") - if ADVANCEDDROP: - # C. 3 curves - row = col.row(align=True) - row.operator("curvetools.outline", text="Curve Outline") - row = col.row(align=True) - row.operator("curvetools.sep_outline", text="Separate Outline or selected") - row = col.row(align=True) - row.operator("curvetools.bezier_curve_boolean", text="2D Curve Boolean") - row = col.row(align=True) - row.operator("curvetools.bezier_points_fillet", text='Fillet') - row = col.row(align=True) - row.operator("curvetools.bezier_cad_handle_projection", text='Handle Projection') - row = col.row(align=True) - row.operator("curvetools.bezier_spline_divide", text='Divide') - row = col.row(align=True) - row.operator("curvetools.scale_reset", text='Scale Reset') - row = col.row(align=True) - row.operator("curvetools.operatorbirail", text="Birail") - row = col.row(align=True) - row.operator("curvetools.convert_selected_face_to_bezier", text="Convert selected faces to Bezier") - row = col.row(align=True) - row.operator("curvetools.convert_bezier_to_surface", text="Convert Bezier to Surface") - - # Extended options - box1 = self.layout.box() - col = box1.column(align=True) + row.label(text="Curve Resolution:") row = col.row(align=True) - row.prop(scene, "UTExtendedDrop", icon="TRIA_DOWN") - if EXTENDEDDROP: - row = col.row(align=True) - row.operator("curvetools.add_toolpath_offset_curve", text="Offset Curve") - row = col.row(align=True) - row.operator("curvetools.bezier_cad_boolean", text="Boolean 2 selected spline") - row = col.row(align=True) - row.operator("curvetools.bezier_cad_subdivide", text="Multi Subdivide") - row = col.row(align=True) - row.operator("curvetools.split", text='Split by selected points') - row = col.row(align=True) - row.operator("curvetools.remove_doubles", text='Remove Doubles') - row = col.row(align=True) - row.operator("curvetools.add_toolpath_discretize_curve", text="Discretize Curve") - row = col.row(align=True) - row.operator("curvetools.bezier_cad_array", text="Array selected spline") - - # Utils Curve options - box1 = self.layout.box() - col = box1.column(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.prop(scene, "UTUtilsDrop", icon="TRIA_DOWN") - if UTILSDROP: - # D.1 set spline resolution - row = col.row(align=True) - row.label(text="Show point Resolution:") - 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.prop(context.scene.curvetools, "curve_vertcolor", text="") - row = col.row(align=True) - row.operator("curvetools.show_resolution", text="Run [ESC]") - - # D.1 set spline sequence - row = col.row(align=True) - row.label(text="Show and rearrange spline sequence:") - row = col.row(align=True) - row.prop(context.scene.curvetools, "sequence_color", text="") - row.prop(context.scene.curvetools, "font_thickness", text="") - row.prop(context.scene.curvetools, "font_size", text="") - 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("curvetools.show_splines_sequence", text="Run [ESC]") - - # D.2 remove splines - row = col.row(align=True) - row.label(text="Remove splines:") - row = col.row(align=True) - row.operator("curvetools.operatorsplinesremovezerosegment", text="Remove 0-segments splines") - row = col.row(align=True) - row.operator("curvetools.operatorsplinesremoveshort", text="Remove short splines") - row = col.row(align=True) - row.prop(context.scene.curvetools, "SplineRemoveLength", text="Threshold remove") - - # D.3 join splines - row = col.row(align=True) - row.label(text="Join splines:") - row = col.row(align=True) - row.operator("curvetools.operatorsplinesjoinneighbouring", text="Join neighbouring splines") - row = col.row(align=True) - row.prop(context.scene.curvetools, "SplineJoinDistance", text="Threshold join") - row = col.row(align=True) - row.prop(context.scene.curvetools, "SplineJoinStartEnd", text="Only at start & end") - row = col.row(align=True) - row.prop(context.scene.curvetools, "SplineJoinMode", text="Join mode") - - row = col.row(align=True) - row.label(text="PathFinder:") - row = col.row(align=True) - row.prop(context.scene.curvetools, "PathFinderRadius", text="PathFinder Radius") - row = col.row(align=True) - row.prop(context.scene.curvetools, "path_color", text="") - row.prop(context.scene.curvetools, "path_thickness", text="") - row = col.row(align=True) - row.operator("curvetools.pathfinder", text="Run Path Finder [ESC]") - row = col.row(align=True) - row.label(text="ESC or TAB - exit from PathFinder") - row = col.row(align=True) - row.label(text="X or DEL - delete") - row = col.row(align=True) - row.label(text="Alt + mouse click - select spline") - row = col.row(align=True) - row.label(text="Alt + Shift + mouse click - add spline to select") - row = col.row(align=True) - row.label(text="A - deselect all") + 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_CurvePanel, + 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 ) diff --git a/curve_tools/cad.py b/curve_tools/cad.py index 3d3f87bd..e46af8d3 100644 --- a/curve_tools/cad.py +++ b/curve_tools/cad.py @@ -156,10 +156,14 @@ class MergeEnds(bpy.types.Operator): self.report({'WARNING'}, 'Invalid selection') return {'CANCELLED'} + if is_last_point[0]: + points[1], points[0] = points[0], points[1] + selected_splines[1], selected_splines[0] = selected_splines[0], selected_splines[1] + is_last_point[1], is_last_point[0] = is_last_point[0], is_last_point[1] + points[0].handle_left_type = 'FREE' points[0].handle_right_type = 'FREE' new_co = (points[0].co+points[1].co)*0.5 - handle = (points[1].handle_left if is_last_point[1] else points[1].handle_right)+new_co-points[1].co if is_last_point[0]: points[0].handle_left += new_co-points[0].co @@ -169,13 +173,13 @@ class MergeEnds(bpy.types.Operator): points[0].handle_left = handle points[0].co = new_co - bpy.ops.curve.select_all(action='DESELECT') - points[1].select_control_point = True - bpy.ops.curve.delete() - selected_splines[0].bezier_points[-1 if is_last_point[0] else 0].select_control_point = True - selected_splines[1].bezier_points[-1 if is_last_point[1] else 0].select_control_point = True + point_index = 0 if selected_splines[0] == selected_splines[1] else len(selected_splines[1].bezier_points) bpy.ops.curve.make_segment() - bpy.ops.curve.select_all(action='DESELECT') + point = selected_splines[0].bezier_points[point_index] + point.select_control_point = False + point.select_left_handle = False + point.select_right_handle = False + bpy.ops.curve.delete() return {'FINISHED'} class Subdivide(bpy.types.Operator): diff --git a/curve_tools/curves.py b/curve_tools/curves.py index da0b1398..202487de 100644 --- a/curve_tools/curves.py +++ b/curve_tools/curves.py @@ -357,12 +357,12 @@ class BezierSpline: return [newSpline1, newSpline2] - def Join(self, spline2, mode = 'At midpoint'): - if mode == 'At midpoint': + def Join(self, spline2, mode = 'At_midpoint'): + if mode == 'At_midpoint': self.JoinAtMidpoint(spline2) return - if mode == 'Insert segment': + if mode == 'Insert_segment': self.JoinInsertSegment(spline2) return diff --git a/curve_tools/internal.py b/curve_tools/internal.py index e967fc6e..96816189 100644 --- a/curve_tools/internal.py +++ b/curve_tools/internal.py @@ -103,11 +103,11 @@ def nearestPointOfLines(originA, dirA, originB, dirB, tollerance=0.0): def lineSegmentLineSegmentIntersection(beginA, endA, beginB, endB, tollerance=0.001): dirA = endA-beginA dirB = endB-beginB - intersection = nearestPointOfLines(beginA, dirA, beginB, dirB) - if math.isnan(intersection[0]) or (intersection[2]-intersection[3]).length > tollerance or \ - intersection[0] < 0 or intersection[0] > 1 or intersection[1] < 0 or intersection[1] > 1: + paramA, paramB, pointA, pointB = nearestPointOfLines(beginA, dirA, beginB, dirB) + if math.isnan(paramA) or (pointA-pointB).length > tollerance or \ + paramA < 0 or paramA > 1 or paramB < 0 or paramB > 1: return None - return intersection + return (paramA, paramB, pointA, pointB) def aabbOfPoints(points): min = Vector(points[0]) @@ -290,6 +290,16 @@ def isSegmentLinear(points, tollerance=0.0001): def bezierSegmentPoints(begin, end): return [begin.co, begin.handle_right, end.handle_left, end.co] +def grab_cursor(context, event): + if event.mouse_region_x < 0: + context.window.cursor_warp(context.region.x+context.region.width, event.mouse_y) + elif event.mouse_region_x > context.region.width: + context.window.cursor_warp(context.region.x, event.mouse_y) + elif event.mouse_region_y < 0: + context.window.cursor_warp(event.mouse_x, context.region.y+context.region.height) + elif event.mouse_region_y > context.region.height: + context.window.cursor_warp(event.mouse_x, context.region.y) + def deleteFromArray(item, array): for index, current in enumerate(array): if current is item: @@ -586,7 +596,6 @@ def getSelectedSplines(include_bezier, include_polygon, allow_partial_selection= return result def addObject(type, name): - bpy.ops.object.select_all(action='DESELECT') if type == 'CURVE': data = bpy.data.curves.new(name=name, type='CURVE') data.dimensions = '3D' @@ -780,6 +789,27 @@ def filletSpline(spline, radius, chamfer_mode, limit_half_way, tollerance=0.0001 i = i+1 return addBezierSpline(bpy.context.object, spline.use_cyclic_u, vertices) +def dogBone(spline, radius): + vertices = [] + def handlePoint(prev_segment_points, next_segment_points, selected, prev_tangent, current_tangent, next_tangent, normal, angle, is_first, is_last): + if not selected or is_first or is_last or angle == 0 or normal[2] > 0.0 or \ + (spline.type == 'BEZIER' and not (isSegmentLinear(prev_segment_points) and isSegmentLinear(next_segment_points))): + prev_handle = next_segment_points[0] if is_first else prev_segment_points[2] if spline.type == 'BEZIER' else prev_segment_points[0] + next_handle = next_segment_points[0] if is_last else next_segment_points[1] if spline.type == 'BEZIER' else next_segment_points[3] + vertices.append([prev_handle, next_segment_points[0], next_handle]) + return + tan_factor = math.tan(angle*0.5) + corner = next_segment_points[0]+normal.cross(prev_tangent)*radius-prev_tangent*radius*tan_factor + direction = next_segment_points[0]-corner + distance = direction.length + corner = next_segment_points[0]+direction/distance*(distance-radius) + vertices.append([prev_segment_points[0], next_segment_points[0], corner]) + vertices.append([next_segment_points[0], corner, next_segment_points[0]]) + vertices.append([corner, next_segment_points[0], next_segment_points[3]]) + iterateSpline(spline, handlePoint) + print(vertices) + return vertices + def discretizeCurve(spline, step_angle, samples): vertices = [] def handlePoint(prev_segment_points, next_segment_points, selected, prev_tangent, current_tangent, next_tangent, normal, angle, is_first, is_last): diff --git a/curve_tools/intersections.py b/curve_tools/intersections.py index 77f19861..f0b8e96f 100644 --- a/curve_tools/intersections.py +++ b/curve_tools/intersections.py @@ -29,7 +29,7 @@ class BezierSegmentsIntersector: if algorithm == '3D': return self.CalcFirstRealIntersection3D(nrSamples1, nrSamples2) - if algorithm == 'From View': + if algorithm == 'From_View': global algoDIR if algoDIR is not None: return self.CalcFirstRealIntersectionFromViewDIR(nrSamples1, nrSamples2) @@ -309,7 +309,7 @@ class BezierSegmentsIntersector: if algorithm == '3D': return self.CalcIntersections3D(nrSamples1, nrSamples2) - if algorithm == 'From View': + if algorithm == 'From_View': global algoDIR if algoDIR is not None: return self.CalcIntersectionsFromViewDIR(nrSamples1, nrSamples2) @@ -527,7 +527,7 @@ class CurvesIntersector: global algoDIR algo = bpy.context.scene.curvetools.IntersectCurvesAlgorithm - if algo == 'From View': + if algo == 'From_View': regionView3D = util.GetFirstRegionView3D() if regionView3D is None: print("### ERROR: regionView3D is None. Stopping.") diff --git a/curve_tools/operators.py b/curve_tools/operators.py index aeb4672c..ea11aef3 100644 --- a/curve_tools/operators.py +++ b/curve_tools/operators.py @@ -1145,7 +1145,153 @@ class CurveBoolean(bpy.types.Operator): j += 1 - bpy.ops.object.mode_set (mode = current_mode) + bpy.ops.object.mode_set(mode = 'EDIT') + bpy.ops.curve.select_all(action='SELECT') + + return {'FINISHED'} + +# ---------------------------- +# Set first points operator +class SetFirstPoints(bpy.types.Operator): + bl_idname = "curvetools.set_first_points" + bl_label = "Set first points" + bl_description = "Set the selected points as the first point of each spline" + bl_options = {'REGISTER', 'UNDO'} + + @classmethod + def poll(cls, context): + return util.Selected1OrMoreCurves() + + def execute(self, context): + splines_to_invert = [] + + curve = bpy.context.object + + bpy.ops.object.mode_set('INVOKE_REGION_WIN', mode='EDIT') + + # Check non-cyclic splines to invert + for i in range(len(curve.data.splines)): + b_points = curve.data.splines[i].bezier_points + + if i not in self.cyclic_splines: # Only for non-cyclic splines + if b_points[len(b_points) - 1].select_control_point: + splines_to_invert.append(i) + + # Reorder points of cyclic splines, and set all handles to "Automatic" + + # Check first selected point + cyclic_splines_new_first_pt = {} + for i in self.cyclic_splines: + sp = curve.data.splines[i] + + for t in range(len(sp.bezier_points)): + bp = sp.bezier_points[t] + if bp.select_control_point or bp.select_right_handle or bp.select_left_handle: + cyclic_splines_new_first_pt[i] = t + break # To take only one if there are more + + # Reorder + for spline_idx in cyclic_splines_new_first_pt: + sp = curve.data.splines[spline_idx] + + spline_old_coords = [] + for bp_old in sp.bezier_points: + coords = (bp_old.co[0], bp_old.co[1], bp_old.co[2]) + + left_handle_type = str(bp_old.handle_left_type) + left_handle_length = float(bp_old.handle_left.length) + left_handle_xyz = ( + float(bp_old.handle_left.x), + float(bp_old.handle_left.y), + float(bp_old.handle_left.z) + ) + right_handle_type = str(bp_old.handle_right_type) + right_handle_length = float(bp_old.handle_right.length) + right_handle_xyz = ( + float(bp_old.handle_right.x), + float(bp_old.handle_right.y), + float(bp_old.handle_right.z) + ) + spline_old_coords.append( + [coords, left_handle_type, + right_handle_type, left_handle_length, + right_handle_length, left_handle_xyz, + right_handle_xyz] + ) + + for t in range(len(sp.bezier_points)): + bp = sp.bezier_points + + if t + cyclic_splines_new_first_pt[spline_idx] + 1 <= len(bp) - 1: + new_index = t + cyclic_splines_new_first_pt[spline_idx] + 1 + else: + new_index = t + cyclic_splines_new_first_pt[spline_idx] + 1 - len(bp) + + bp[t].co = Vector(spline_old_coords[new_index][0]) + + bp[t].handle_left.length = spline_old_coords[new_index][3] + bp[t].handle_right.length = spline_old_coords[new_index][4] + + bp[t].handle_left_type = "FREE" + bp[t].handle_right_type = "FREE" + + bp[t].handle_left.x = spline_old_coords[new_index][5][0] + bp[t].handle_left.y = spline_old_coords[new_index][5][1] + bp[t].handle_left.z = spline_old_coords[new_index][5][2] + + bp[t].handle_right.x = spline_old_coords[new_index][6][0] + bp[t].handle_right.y = spline_old_coords[new_index][6][1] + bp[t].handle_right.z = spline_old_coords[new_index][6][2] + + bp[t].handle_left_type = spline_old_coords[new_index][1] + bp[t].handle_right_type = spline_old_coords[new_index][2] + + # Invert the non-cyclic splines designated above + for i in range(len(splines_to_invert)): + bpy.ops.curve.select_all('INVOKE_REGION_WIN', action='DESELECT') + + bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN') + curve.data.splines[splines_to_invert[i]].bezier_points[0].select_control_point = True + bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN') + + bpy.ops.curve.switch_direction() + + bpy.ops.curve.select_all('INVOKE_REGION_WIN', action='DESELECT') + + # Keep selected the first vert of each spline + bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN') + for i in range(len(curve.data.splines)): + if not curve.data.splines[i].use_cyclic_u: + bp = curve.data.splines[i].bezier_points[0] + else: + bp = curve.data.splines[i].bezier_points[ + len(curve.data.splines[i].bezier_points) - 1 + ] + + bp.select_control_point = True + bp.select_right_handle = True + bp.select_left_handle = True + + bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN') + + return {'FINISHED'} + + def invoke(self, context, event): + curve = bpy.context.object + + # Check if all curves are Bezier, and detect which ones are cyclic + self.cyclic_splines = [] + for i in range(len(curve.data.splines)): + if curve.data.splines[i].type != "BEZIER": + self.report({'WARNING'}, "All splines must be Bezier type") + + return {'CANCELLED'} + else: + if curve.data.splines[i].use_cyclic_u: + self.cyclic_splines.append(i) + + self.execute(context) + self.report({'INFO'}, "First points have been set") return {'FINISHED'} @@ -1182,4 +1328,5 @@ operators = [ Split, SeparateOutline, CurveBoolean, + SetFirstPoints, ] diff --git a/curve_tools/toolpath.py b/curve_tools/toolpath.py index 2b422280..fec6693a 100644 --- a/curve_tools/toolpath.py +++ b/curve_tools/toolpath.py @@ -17,6 +17,7 @@ # ***** GPL LICENSE BLOCK ***** import bpy, math, bmesh +from bpy_extras import view3d_utils from mathutils import Vector, Matrix from . import internal @@ -68,28 +69,19 @@ class SliceMesh(bpy.types.Operator): bl_description = bl_label = 'Slice Mesh' bl_options = {'REGISTER', 'UNDO'} - pitch_axis: bpy.props.FloatVectorProperty(name='Pitch & Axis', unit='LENGTH', description='Vector between to slices', subtype='DIRECTION', default=(0.0, 0.0, 0.1), size=3) - offset: bpy.props.FloatProperty(name='Offset', unit='LENGTH', description='Position of first slice along axis', default=-0.4) - slice_count: bpy.props.IntProperty(name='Count', description='Number of slices', min=1, default=9) + pitch: bpy.props.FloatProperty(name='Pitch', unit='LENGTH', description='Distance between two slices', default=0.1) + offset: bpy.props.FloatProperty(name='Offset', unit='LENGTH', description='Position of first slice along the axis', default=0.0) + slice_count: bpy.props.IntProperty(name='Count', description='Number of slices', min=1, default=3) @classmethod def poll(cls, context): return bpy.context.object != None and bpy.context.object.mode == 'OBJECT' - def execute(self, context): - if bpy.context.object.type != 'MESH': - self.report({'WARNING'}, 'Active object must be a mesh') - return {'CANCELLED'} - depsgraph = context.evaluated_depsgraph_get() - mesh = bmesh.new() - mesh.from_object(bpy.context.object, depsgraph, deform=True, cage=False, face_normals=True) - mesh.transform(bpy.context.object.matrix_world) - toolpath = internal.addObject('CURVE', 'Slices Toolpath') - pitch_axis = Vector(self.pitch_axis) - axis = pitch_axis.normalized() + def perform(self, context): + axis = Vector((0.0, 0.0, 1.0)) for i in range(0, self.slice_count): - aux_mesh = mesh.copy() - cut_geometry = bmesh.ops.bisect_plane(aux_mesh, geom=aux_mesh.edges[:]+aux_mesh.faces[:], dist=0, plane_co=pitch_axis*i+axis*self.offset, plane_no=axis, clear_outer=False, clear_inner=False)['geom_cut'] + aux_mesh = self.mesh.copy() + cut_geometry = bmesh.ops.bisect_plane(aux_mesh, geom=aux_mesh.edges[:]+aux_mesh.faces[:], dist=0, plane_co=axis*(i*self.pitch+self.offset), plane_no=axis, clear_outer=False, clear_inner=False)['geom_cut'] edge_pool = set([e for e in cut_geometry if isinstance(e, bmesh.types.BMEdge)]) while len(edge_pool) > 0: current_edge = edge_pool.pop() @@ -110,9 +102,104 @@ class SliceMesh(bpy.types.Operator): break current_vertex = current_edge.other_vert(current_vertex) vertices.append(current_vertex.co) - internal.addPolygonSpline(toolpath, False, vertices) + internal.addPolygonSpline(self.result, False, vertices) aux_mesh.free() - mesh.free() + + def invoke(self, context, event): + if bpy.context.object.type != 'MESH': + self.report({'WARNING'}, 'Active object must be a mesh') + return {'CANCELLED'} + self.pitch = 0.1 + self.offset = 0.0 + self.slice_count = 3 + self.mode = 'PITCH' + self.execute(context) + context.window_manager.modal_handler_add(self) + return {'RUNNING_MODAL'} + + def modal(self, context, event): + if event.type == 'MOUSEMOVE': + mouse = (event.mouse_region_x, event.mouse_region_y) + input_value = internal.nearestPointOfLines( + bpy.context.scene.cursor.location, + bpy.context.scene.cursor.matrix.col[2].xyz, + view3d_utils.region_2d_to_origin_3d(context.region, context.region_data, mouse), + view3d_utils.region_2d_to_vector_3d(context.region, context.region_data, mouse) + )[0] + if self.mode == 'PITCH': + self.pitch = input_value/(self.slice_count-1) if self.slice_count > 2 else input_value + elif self.mode == 'OFFSET': + self.offset = input_value-self.pitch*0.5*((self.slice_count-1) if self.slice_count > 2 else 1.0) + elif event.type == 'WHEELUPMOUSE': + if self.slice_count > 2: + self.pitch *= (self.slice_count-1) + self.slice_count += 1 + if self.slice_count > 2: + self.pitch /= (self.slice_count-1) + elif event.type == 'WHEELDOWNMOUSE': + if self.slice_count > 2: + self.pitch *= (self.slice_count-1) + if self.slice_count > 1: + self.slice_count -= 1 + if self.slice_count > 2: + self.pitch /= (self.slice_count-1) + elif event.type == 'LEFTMOUSE' and event.value == 'RELEASE': + if self.mode == 'PITCH': + self.mode = 'OFFSET' + return {'RUNNING_MODAL'} + elif self.mode == 'OFFSET': + return {'FINISHED'} + elif event.type in {'RIGHTMOUSE', 'ESC'}: + bpy.context.scene.collection.objects.unlink(self.result) + return {'CANCELLED'} + else: + return {'PASS_THROUGH'} + self.result.data.splines.clear() + self.perform(context) + return {'RUNNING_MODAL'} + + def execute(self, context): + depsgraph = context.evaluated_depsgraph_get() + self.mesh = bmesh.new() + self.mesh.from_object(bpy.context.object, depsgraph, deform=True, cage=False, face_normals=True) + self.mesh.transform(bpy.context.scene.cursor.matrix.inverted()@bpy.context.object.matrix_world) + self.result = internal.addObject('CURVE', 'Slices') + self.result.matrix_world = bpy.context.scene.cursor.matrix + self.perform(context) + return {'FINISHED'} + +class DogBone(bpy.types.Operator): + bl_idname = 'curvetools.add_toolpath_dogbone' + bl_description = bl_label = 'Dog Bone' + bl_options = {'REGISTER', 'UNDO'} + + radius: bpy.props.FloatProperty(name='Radius', description='Tool radius to compensate for', unit='LENGTH', min=0.0, default=0.1) + + @classmethod + def poll(cls, context): + return bpy.context.object != None and bpy.context.object.type == 'CURVE' + + def execute(self, context): + if bpy.context.object.mode == 'EDIT': + splines = internal.getSelectedSplines(True, False) + else: + splines = bpy.context.object.data.splines + + if len(splines) == 0: + self.report({'WARNING'}, 'Nothing selected') + return {'CANCELLED'} + + if bpy.context.object.mode != 'EDIT': + internal.addObject('CURVE', 'Dog Bone') + origin = bpy.context.scene.cursor.location + else: + origin = Vector((0.0, 0.0, 0.0)) + + for spline in splines: + if spline.type != 'BEZIER': + continue + result = internal.dogBone(spline, self.radius) + internal.addBezierSpline(bpy.context.object, spline.use_cyclic_u, result) # [vertex-origin for vertex in result]) return {'FINISHED'} class DiscretizeCurve(bpy.types.Operator): @@ -295,4 +382,4 @@ def unregister(): if __name__ == "__main__": register() -operators = [OffsetCurve, SliceMesh, DiscretizeCurve, Truncate, RectMacro, DrillMacro] +operators = [OffsetCurve, SliceMesh, DogBone, DiscretizeCurve, Truncate, RectMacro, DrillMacro] -- cgit v1.2.3