From 722a14aee4f7075ef95f5e6e89b6c1838293c1d8 Mon Sep 17 00:00:00 2001 From: Tristan Strathearn Date: Thu, 3 Nov 2022 11:54:05 +1000 Subject: [PATCH] feat: add circularize operator --- interface/fast_menu.py | 6 ++ interface/ops.py | 1 + sketch/__init__.py | 2 + sketch/circularize.py | 228 +++++++++++++++++++++++++++++++++++++++++ 4 files changed, 237 insertions(+) create mode 100644 sketch/circularize.py diff --git a/interface/fast_menu.py b/interface/fast_menu.py index 9d030ec..af5ecb3 100644 --- a/interface/fast_menu.py +++ b/interface/fast_menu.py @@ -193,6 +193,7 @@ def draw_single_object_mesh_predictions(self, context, layout): has_mod_screw = False has_mod_array_cubed = False has_mod_circular_array = False + has_mod_circularize = False has_mod_recon_poly = False for name in mod_names: @@ -203,6 +204,7 @@ def draw_single_object_mesh_predictions(self, context, layout): has_mod_array_cubed = has_mod_array_cubed or bool("Array³" in name) has_mod_circular_array = has_mod_circular_array or bool("— ND CA" in name) has_mod_recon_poly = has_mod_recon_poly or bool("— ND RCP" in name) + has_mod_circularize = has_mod_circularize or bool("— ND CIRC" in name) was_profile_extrude = has_mod_profile_extrude and not has_mod_solidify @@ -238,6 +240,10 @@ def draw_single_object_mesh_predictions(self, context, layout): if has_mod_recon_poly: layout.operator("nd.recon_poly", icon=icons['nd.recon_poly']) replay_prediction_count += 1 + + if has_mod_circularize: + layout.operator("nd.circularize", icon=icons['nd.circularize']) + replay_prediction_count += 1 if context.active_object.display_type == 'WIRE' and "Bool —" in context.active_object.name: layout.operator("nd.mirror", icon=icons['nd.mirror']) diff --git a/interface/ops.py b/interface/ops.py index 013e0f0..88c38ec 100644 --- a/interface/ops.py +++ b/interface/ops.py @@ -34,6 +34,7 @@ ("nd.geo_lift", 'FACESEL', None, None, False), ("nd.panel", 'MOD_EXPLODE', None, None, False), None, # Separator + ("nd.circularize", 'MESH_CIRCLE', None, None, False), ("nd.recon_poly", 'SURFACE_NCURVE', None, None, False), ("nd.screw_head", 'CANCEL', None, None, False), ] diff --git a/sketch/__init__.py b/sketch/__init__.py index 62de625..a2ad552 100644 --- a/sketch/__init__.py +++ b/sketch/__init__.py @@ -24,6 +24,7 @@ from . import panel from . import single_vertex from . import recon_poly +from . import circularize from . import screw_head from . import clear_vgs from . import make_manifold @@ -35,6 +36,7 @@ panel, single_vertex, recon_poly, + circularize, screw_head, clear_vgs, make_manifold, diff --git a/sketch/circularize.py b/sketch/circularize.py new file mode 100644 index 0000000..4becf45 --- /dev/null +++ b/sketch/circularize.py @@ -0,0 +1,228 @@ +# ███╗ ██╗██████╗ +# ████╗ ██║██╔══██╗ +# ██╔██╗ ██║██║ ██║ +# ██║╚██╗██║██║ ██║ +# ██║ ╚████║██████╔╝ +# ╚═╝ ╚═══╝╚═════╝ +# +# “Commons Clause” License Condition v1.0 +# +# See LICENSE for license details. If you did not receive a copy of the license, +# it may be obtained at https://github.com/hugemenace/nd/blob/main/LICENSE. +# +# Software: ND Blender Addon +# License: MIT +# Licensor: T.S. & I.J. (HugeMenace) +# +# --- +# Contributors: Tristo (HM) +# --- + +import bpy +import bmesh +from math import radians +from .. lib.overlay import update_overlay, init_overlay, toggle_pin_overlay, toggle_operator_passthrough, register_draw_handler, unregister_draw_handler, draw_header, draw_property, draw_hint +from .. lib.events import capture_modifier_keys, pressed +from .. lib.preferences import get_preferences +from .. lib.numeric_input import update_stream, no_stream, get_stream_value, new_stream +from .. lib.modifiers import new_modifier, remove_modifiers_ending_with + + +mod_bevel = "Bevel — ND CIRC" +mod_weld = "Weld — ND CIRC" +mod_summon_list = [mod_bevel, mod_weld] + + +class ND_OT_circularize(bpy.types.Operator): + bl_idname = "nd.circularize" + bl_label = "Circularize" + bl_description = "Adds a vertex bevel operator to the selected plane to convert it into a circular shape" + bl_options = {'UNDO'} + + + def modal(self, context, event): + capture_modifier_keys(self, event) + + segment_factor = 1 if self.key_shift else 2 + + if self.key_toggle_operator_passthrough: + toggle_operator_passthrough(self) + + elif self.key_toggle_pin_overlay: + toggle_pin_overlay(self, event) + + elif self.operator_passthrough: + update_overlay(self, context, event) + + return {'PASS_THROUGH'} + + elif self.key_cancel: + self.revert(context) + + return {'CANCELLED'} + + elif self.key_numeric_input: + if self.key_no_modifiers: + self.segments_input_stream = update_stream(self.segments_input_stream, event.type) + self.segments = int(get_stream_value(self.segments_input_stream, min_value=1)) + self.dirty = True + + elif self.key_reset: + if self.key_no_modifiers: + self.segments_input_stream = new_stream() + self.segments = 1 + self.dirty = True + + elif self.key_step_up: + if no_stream(self.segments_input_stream) and self.key_no_modifiers: + self.segments = 2 if self.segments == 1 else self.segments + segment_factor + self.dirty = True + + elif self.key_step_down: + if no_stream(self.segments_input_stream) and self.key_no_modifiers: + self.segments = max(1, self.segments - segment_factor) + self.dirty = True + + elif self.key_confirm: + self.finish(context) + + return {'FINISHED'} + + elif self.key_movement_passthrough: + return {'PASS_THROUGH'} + + if get_preferences().enable_mouse_values: + if no_stream(self.segments_input_stream) and self.key_no_modifiers: + self.segments = max(1, self.segments + self.mouse_step) + self.dirty = True + + if self.dirty: + self.operate(context) + + update_overlay(self, context, event) + + return {'RUNNING_MODAL'} + + + def invoke(self, context, event): + if event.ctrl: + remove_modifiers_ending_with(context.selected_objects, ' — ND CIRC') + + return {'FINISHED'} + + self.dirty = False + self.segments = 2 + + self.segments_input_stream = new_stream() + self.width_input_stream = new_stream() + self.profile_input_stream = new_stream() + + self.target_object = context.active_object + + mods = context.active_object.modifiers + mod_names = list(map(lambda x: x.name, mods)) + previous_op = all(m in mod_names for m in mod_summon_list) + + if previous_op: + self.summon_old_operator(context, mods) + else: + self.prepare_new_operator(context) + + self.operate(context) + + capture_modifier_keys(self, None, event.mouse_x) + + init_overlay(self, event) + register_draw_handler(self, draw_text_callback) + + context.window_manager.modal_handler_add(self) + + return {'RUNNING_MODAL'} + + + @classmethod + def poll(cls, context): + if context.mode == 'OBJECT': + return len(context.selected_objects) == 1 and context.active_object.type == 'MESH' + + + def summon_old_operator(self, context, mods): + self.summoned = True + + self.bevel = mods[mod_bevel] + + self.segments_prev = self.segments = self.bevel.segments + + + def prepare_new_operator(self, context): + self.summoned = False + + self.add_smooth_shading(context) + self.add_bevel_modifier(context) + + + def add_smooth_shading(self, context): + bpy.ops.object.shade_smooth() + context.active_object.data.use_auto_smooth = True + context.active_object.data.auto_smooth_angle = radians(float(get_preferences().default_smoothing_angle)) + + + def add_bevel_modifier(self, context): + bevel = new_modifier(context.active_object, mod_bevel, 'BEVEL', rectify=False) + bevel.affect = 'VERTICES' + bevel.limit_method = 'NONE' + bevel.offset_type = 'PERCENT' + bevel.width_pct = 50 + + self.bevel = bevel + + + def add_weld_modifier(self, context): + weld = new_modifier(context.active_object, mod_weld, 'WELD', rectify=False) + weld.merge_threshold = 0.00001 + + self.weld = weld + + + def operate(self, context): + self.bevel.segments = self.segments + + self.dirty = False + + + def finish(self, context): + if not self.summoned: + self.add_weld_modifier(context) + + unregister_draw_handler() + + + def revert(self, context): + if self.summoned: + self.bevel.segments = self.segments_prev + + if not self.summoned: + bpy.ops.object.modifier_remove(modifier=self.bevel.name) + + unregister_draw_handler() + + +def draw_text_callback(self): + draw_header(self) + + draw_property( + self, + "Segments: {}".format(self.segments), + "Alt (±2) | Shift (±1)", + active=self.key_no_modifiers, + mouse_value=True, + input_stream=self.segments_input_stream) + + +def register(): + bpy.utils.register_class(ND_OT_circularize) + + +def unregister(): + bpy.utils.unregister_class(ND_OT_circularize) + unregister_draw_handler()