diff options
author | Rune Morling <ermo.blender.org@spammesenseless.net> | 2019-12-09 02:52:27 +0300 |
---|---|---|
committer | Rune Morling <ermo.blender.org@spammesenseless.net> | 2019-12-09 02:52:27 +0300 |
commit | e7594d45b1dd3d38a795bcafd9c0221ce7922a8d (patch) | |
tree | 4dfba991cefea30c97ca99a67a8b226d4d58dbcd /precision_drawing_tools | |
parent | d94489ea875a20f2bc0aa2b5e490d5ab0c230833 (diff) |
Add Clockmender's Precision Drawing Tools v1.1.5
Accepted for inclusion per T70238
Diffstat (limited to 'precision_drawing_tools')
-rw-r--r-- | precision_drawing_tools/__init__.py | 539 | ||||
-rw-r--r-- | precision_drawing_tools/pdt_bix.py | 118 | ||||
-rw-r--r-- | precision_drawing_tools/pdt_cad_module.py | 210 | ||||
-rw-r--r-- | precision_drawing_tools/pdt_command.py | 880 | ||||
-rw-r--r-- | precision_drawing_tools/pdt_design.py | 1478 | ||||
-rw-r--r-- | precision_drawing_tools/pdt_etof.py | 132 | ||||
-rw-r--r-- | precision_drawing_tools/pdt_functions.py | 688 | ||||
-rw-r--r-- | precision_drawing_tools/pdt_library.py | 173 | ||||
-rw-r--r-- | precision_drawing_tools/pdt_menus.py | 330 | ||||
-rw-r--r-- | precision_drawing_tools/pdt_msg_strings.py | 171 | ||||
-rw-r--r-- | precision_drawing_tools/pdt_pivot_point.py | 442 | ||||
-rw-r--r-- | precision_drawing_tools/pdt_view.py | 241 | ||||
-rw-r--r-- | precision_drawing_tools/pdt_xall.py | 195 |
13 files changed, 5597 insertions, 0 deletions
diff --git a/precision_drawing_tools/__init__.py b/precision_drawing_tools/__init__.py new file mode 100644 index 00000000..b3c3024d --- /dev/null +++ b/precision_drawing_tools/__init__.py @@ -0,0 +1,539 @@ +# ##### BEGIN GPL LICENSE BLOCK ##### +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software Foundation, +# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# ##### END GPL LICENSE BLOCK ##### +# +# <pep8 compliant> +# +# ----------------------------------------------------------------------- +# Author: Alan Odom (Clockmender), Rune Morling (ermo) Copyright (c) 2019 +# ----------------------------------------------------------------------- +# +# ---------------------------------------------- +# Define Addon info +# ---------------------------------------------- +# +bl_info = { + "name": "Precision Drawing Tools (PDT)", + "author": "Alan Odom (Clockmender), Rune Morling (ermo)", + "version": (1, 1, 5), + "blender": (2, 80, 0), + "location": "View3D > UI > PDT", + "description": "Precision Drawing Tools for Acccurate Modelling", + "warning": "", + "wiki_url": "https://github.com/Clockmender/Precision-Drawing-Tools/wiki", + "category": "3D View", +} + + +# ---------------------------------------------- +# Import modules +# ---------------------------------------------- +if "bpy" in locals(): + import importlib + + importlib.reload(pdt_design) + importlib.reload(pdt_pivot_point) + importlib.reload(pdt_menus) + importlib.reload(pdt_library) + importlib.reload(pdt_view) + importlib.reload(pdt_xall) + importlib.reload(pdt_bix) + importlib.reload(pdt_etof) +else: + from . import pdt_design + from . import pdt_pivot_point + from . import pdt_menus + from . import pdt_library + from . import pdt_view + from . import pdt_xall + from . import pdt_bix + from . import pdt_etof + +import bpy +import os +from pathlib import Path +from bpy.types import AddonPreferences, PropertyGroup, Scene, WindowManager +from bpy.props import ( + BoolProperty, + CollectionProperty, + EnumProperty, + FloatProperty, + FloatVectorProperty, + IntProperty, + PointerProperty, + StringProperty, +) +from .pdt_msg_strings import ( + PDT_DES_COORDS, + PDT_DES_FILLETPROF, + PDT_DES_FILLETRAD, + PDT_DES_FILLETSEG, + PDT_DES_FILLETVERTS, + PDT_DES_FLIPANG, + PDT_DES_FLIPPER, + PDT_DES_LIBCOLS, + PDT_DES_LIBMATS, + PDT_DES_LIBMODE, + PDT_DES_LIBOBS, + PDT_DES_LIBSER, + PDT_DES_MOVESEL, + PDT_DES_OBORDER, + PDT_DES_OFFANG, + PDT_DES_OFFDIS, + PDT_DES_OFFPER, + PDT_DES_OPMODE, + PDT_DES_PIVOTDIS, + PDT_DES_PPLOC, + PDT_DES_PPSCALEFAC, + PDT_DES_PPSIZE, + PDT_DES_PPTRANS, + PDT_DES_PPWIDTH, + PDT_DES_ROTMOVAX, + PDT_DES_TRIM, + PDT_DES_VALIDLET, + PDT_DES_WORPLANE +) +from .pdt_command import command_run +from .pdt_functions import scale_set + + +# Declare enum items variables +# +_pdt_obj_items = [] +_pdt_col_items = [] +_pdt_mat_items = [] + + +def enumlist_objects(self, context): + """Populate Objects List from Parts Library. + + Creates list of objects that optionally have search string contained in them + to populate variable pdt_lib_objects enumerator. + + Args: + context: Blender bpy.context instance. + + Returns: + list of Object Names. + """ + + scene = context.scene + pg = scene.pdt_pg + file_path = context.preferences.addons[__package__].preferences.pdt_library_path + path = Path(file_path) + _pdt_obj_items.clear() + + if path.is_file() and ".blend" in str(path): + with bpy.data.libraries.load(str(path)) as (data_from, _): + if len(pg.object_search_string) == 0: + object_names = [ob for ob in data_from.objects] + else: + object_names = [ob for ob in data_from.objects if pg.object_search_string in ob] + for object_name in object_names: + _pdt_obj_items.append((object_name, object_name, "")) + else: + _pdt_obj_items.append(("MISSING", "Library is Missing", "")) + return _pdt_obj_items + + +def enumlist_collections(self, context): + """Populate Collections List from Parts Library. + + Creates list of collections that optionally have search string contained in them + to populate variable pg.lib_collections enumerator + + Args: + context: Blender bpy.context instance. + + Returns: + list of Collections Names. + """ + + scene = context.scene + pg = scene.pdt_pg + file_path = context.preferences.addons[__package__].preferences.pdt_library_path + path = Path(file_path) + _pdt_col_items.clear() + + if path.is_file() and ".blend" in str(path): + with bpy.data.libraries.load(str(path)) as (data_from, _): + if len(pg.collection_search_string) == 0: + object_names = [ob for ob in data_from.collections] + else: + object_names = [ob for ob in data_from.collections if pg.collection_search_string in ob] + for object_name in object_names: + _pdt_col_items.append((object_name, object_name, "")) + else: + _pdt_col_items.append(("MISSING", "Library is Missing", "")) + return _pdt_col_items + + +def enumlist_materials(self, context): + """Populate Materials List from Parts Library. + + Creates list of materials that optionally have search string contained in them + to populate variable pg.lib_materials enumerator. + + Args: + context: Blender bpy.context instance. + + Returns: + list of Object Names. + """ + + scene = context.scene + pg = scene.pdt_pg + file_path = context.preferences.addons[__package__].preferences.pdt_library_path + path = Path(file_path) + _pdt_mat_items.clear() + + if path.is_file() and ".blend" in str(path): + with bpy.data.libraries.load(str(path)) as (data_from, _): + if len(pg.material_search_string) == 0: + object_names = [ob for ob in data_from.materials] + else: + object_names = [ob for ob in data_from.materials if pg.material_search_string in ob] + for object_name in object_names: + _pdt_mat_items.append((object_name, object_name, "")) + else: + _pdt_mat_items.append(("MISSING", "Library is Missing", "")) + return _pdt_mat_items + + +class PDTSceneProperties(PropertyGroup): + """Contains all PDT related properties.""" + + object_search_string : StringProperty( + name="Search", default="", description=PDT_DES_LIBSER + ) + collection_search_string : StringProperty( + name="Search", default="", description=PDT_DES_LIBSER + ) + material_search_string : StringProperty( + name="Search", default="", description=PDT_DES_LIBSER + ) + + cartesian_coords : FloatVectorProperty( + name="Coords", + default=(0.0, 0.0, 0.0), + subtype="XYZ", + description=PDT_DES_COORDS + ) + distance : FloatProperty( + name="Distance", default=0.0, precision=5, description=PDT_DES_OFFDIS, unit="LENGTH" + ) + angle : FloatProperty( + name="Angle", min=-180, max=180, default=0.0, precision=5, description=PDT_DES_OFFANG + ) + percent : FloatProperty( + name="Percent", default=0.0, precision=5, description=PDT_DES_OFFPER + ) + plane : EnumProperty( + items=( + ("XZ", "Front(X-Z)", "Use X-Z Plane"), + ("XY", "Top(X-Y)", "Use X-Y Plane"), + ("YZ", "Right(Y-Z)", "Use Y-Z Plane"), + ("LO", "View", "Use View Plane"), + ), + name="Working Plane", + default="XZ", + description=PDT_DES_WORPLANE, + ) + select : EnumProperty( + items=( + ("REL", "Current", "Moved Relative to Current Position"), + ( + "SEL", + "Selected", + "Moved Relative to Selected Object, or Vertex, Cursor & Pivot Only", + ), + ), + name="Move Mode", + default="SEL", + description=PDT_DES_MOVESEL, + ) + operation : EnumProperty( + items=( + ("CU", "Cursor", "This Function will Move the Cursor"), + ("PP", "Pivot", "This Function will Move the Pivot Point"), + ("MV", "Move", "This function will Move Vertices, or Objects"), + ("NV", "New Vertex", "This function will Add a New Vertex"), + ("EV", "Extrude Vertices", "This function will Extrude Vertices Only in EDIT Mode"), + ("SE", "Split Edges", "This function will Split Edges Only in EDIT Mode"), + ( + "DG", + "Duplicate Geometry", + "This function will Duplicate Geometry in EDIT Mode (Delta & Direction Only)", + ), + ( + "EG", + "Extrude Geometry", + "This function will Extrude Geometry in EDIT Mode (Delta & Direction Only)", + ), + ), + name="Operation", + default="CU", + description=PDT_DES_OPMODE, + ) + taper : EnumProperty( + items=( + ("RX-MY", "RotX-MovY", "Rotate X - Move Y"), + ("RX-MZ", "RotX-MovZ", "Rotate X - Move Z"), + ("RY-MX", "RotY-MovX", "Rotate Y - Move X"), + ("RY-MZ", "RotY-MovZ", "Rotate Y - Move Z"), + ("RZ-MX", "RotZ-MovX", "Rotate Z - Move X"), + ("RZ-MY", "RotZ-MovY", "Rotate Z - Move Y"), + ), + name="Axes", + default="RX-MY", + description=PDT_DES_ROTMOVAX, + ) + + flip_angle : BoolProperty( + name="Flip Angle", default=False, description=PDT_DES_FLIPANG + ) + flip_percent : BoolProperty( + name="Flip %", default=False, description=PDT_DES_FLIPPER + ) + + extend : BoolProperty( + name="Trim/Extend All", default=False, description=PDT_DES_TRIM + ) + + lib_objects : EnumProperty( + items=enumlist_objects, name="Objects", description=PDT_DES_LIBOBS + ) + lib_collections : EnumProperty( + items=enumlist_collections, name="Collections", description=PDT_DES_LIBCOLS + ) + lib_materials : EnumProperty( + items=enumlist_materials, name="Materials", description=PDT_DES_LIBMATS + ) + lib_mode : EnumProperty( + items=( + ("OBJECTS", "Objects", "Use Objects"), + ("COLLECTIONS", "Collections", "Use Collections"), + ("MATERIALS", "Materials", "Use Materials"), + ), + name="Mode", + default="OBJECTS", + description=PDT_DES_LIBMODE, + ) + + rotation_coords : FloatVectorProperty( + name="Rotation", + default=(0.0, 0.0, 0.0), + subtype="XYZ", + description="Rotation Coordinates" + ) + + object_order : EnumProperty( + items=( + ("1,2,3,4", "1,2,3,4", "Objects 1 & 2 are First Line"), + ("1,3,2,4", "1,3,2,4", "Objects 1 & 3 are First Line"), + ("1,4,2,3", "1,4,2,3", "Objects 1 & 4 are First Line"), + ), + name="Order", + default="1,2,3,4", + description=PDT_DES_OBORDER, + ) + vrotangle : FloatProperty(name="View Rotate Angle", default=10, max=180, min=-180) + command : StringProperty( + name="Command", + default="CA0,0,0", + update=command_run, + description=PDT_DES_VALIDLET, + ) + error : StringProperty(name="Error", default="") + + # Was pivot* -- is now pivot_* + pivot_loc : FloatVectorProperty( + name="Pivot Location", + default=(0.0, 0.0, 0.0), + subtype="XYZ", + description=PDT_DES_PPLOC, + ) + pivot_scale : FloatVectorProperty( + name="Pivot Scale", default=(1.0, 1.0, 1.0), subtype="XYZ", description=PDT_DES_PPSCALEFAC + ) + pivot_size : FloatProperty( + name="Pivot Factor", min=0.4, max=10, default=1, precision=1, description=PDT_DES_PPSIZE + ) + pivot_width : IntProperty( + name="Width", min=1, max=5, default=2, description=PDT_DES_PPWIDTH + ) + # FIXME: might as well become pivot_angle + pivot_ang : FloatProperty(name="Pivot Angle", min=-180, max=180, default=0.0) + # FIXME: pivot_dist for consistency? + pivot_dis : FloatProperty(name="Pivot Dist", + default=0.0, + min = 0, + update=scale_set, + description=PDT_DES_PIVOTDIS, + ) + pivot_alpha : FloatProperty( + name="Alpha", + min=0.2, + max=1, + default=0.6, + precision=1, + description=PDT_DES_PPTRANS, + ) + pivot_show : BoolProperty() + + # Was filletrad + fillet_radius : FloatProperty( + name="Fillet Radius", min=0.0, default=1.0, description=PDT_DES_FILLETRAD + ) + # Was filletnum + fillet_segments : IntProperty( + name="Fillet Segments", min=1, default=4, description=PDT_DES_FILLETSEG + ) + # Was filletpro + fillet_profile : FloatProperty( + name="Fillet Profile", min=0.0, max=1.0, default=0.5, description=PDT_DES_FILLETPROF + ) + # Was filletbool + fillet_vertices_only : BoolProperty( + name="Fillet Vertices Only", + default=True, + description=PDT_DES_FILLETVERTS, + ) + + +class PDTPreferences(AddonPreferences): + # This must match the addon name, use '__package__' when defining this in a submodule of a python package. + + bl_idname = __name__ + + pdt_library_path : StringProperty( + name="Parts Library", default="", description="Parts Library File", + maxlen=1024, subtype='FILE_PATH' + ) + + debug : BoolProperty( + name="Enable console debug output from PDT scripts", default=False, + description="NOTE: Does not enable debugging globally in Blender (only in PDT scripts)" + ) + + def draw(self, context): + layout = self.layout + + box = layout.box() + row1 = box.row() + row2 = box.row() + row1.prop(self, "debug") + row2.prop(self, "pdt_library_path") + + +# List of All Classes in the Add-on to register +# +# Due to the way PropertyGroups work, this needs to be listed/loaded first +# (and unloaded last) +# +classes = ( + PDTSceneProperties, + PDTPreferences, + pdt_bix.PDT_OT_LineOnBisection, + pdt_design.PDT_OT_PlacementAbs, + pdt_design.PDT_OT_PlacementDelta, + pdt_design.PDT_OT_PlacementDis, + pdt_design.PDT_OT_PlacementCen, + pdt_design.PDT_OT_PlacementPer, + pdt_design.PDT_OT_PlacementNormal, + pdt_design.PDT_OT_PlacementInt, + pdt_design.PDT_OT_JoinVerts, + pdt_design.PDT_OT_Angle2, + pdt_design.PDT_OT_Angle3, + pdt_design.PDT_OT_Origin, + pdt_design.PDT_OT_Taper, + pdt_design.PDT_OT_Fillet, + pdt_etof.PDT_OT_EdgeToFace, + pdt_library.PDT_OT_Append, + pdt_library.PDT_OT_Link, + pdt_library.PDT_OT_LibShow, + pdt_menus.PDT_PT_PanelDesign, + pdt_menus.PDT_PT_PanelCommandLine, + pdt_menus.PDT_PT_PanelViewControl, + pdt_menus.PDT_PT_PanelPivotPoint, + pdt_menus.PDT_PT_PanelPartsLibrary, + pdt_pivot_point.PDT_OT_ModalDrawOperator, + pdt_pivot_point.PDT_OT_ViewPlaneRotate, + pdt_pivot_point.PDT_OT_ViewPlaneScale, + pdt_pivot_point.PDT_OT_PivotToCursor, + pdt_pivot_point.PDT_OT_CursorToPivot, + pdt_pivot_point.PDT_OT_PivotSelected, + pdt_pivot_point.PDT_OT_PivotOrigin, + pdt_pivot_point.PDT_OT_PivotWrite, + pdt_pivot_point.PDT_OT_PivotRead, + pdt_view.PDT_OT_ViewRot, + pdt_view.PDT_OT_vRotL, + pdt_view.PDT_OT_vRotR, + pdt_view.PDT_OT_vRotU, + pdt_view.PDT_OT_vRotD, + pdt_view.PDT_OT_vRoll, + pdt_view.PDT_OT_viso, + pdt_xall.PDT_OT_IntersectAllEdges, +) + + +def register(): + """Register Classes and Create Scene Variables. + + Operates on the classes list defined above. + """ + + from bpy.utils import register_class + + for cls in classes: + register_class(cls) + + # OpenGL flag + # + wm = WindowManager + # Register Internal OpenGL Property + # + wm.pdt_run_opengl = BoolProperty(default=False) + + Scene.pdt_pg = PointerProperty(type=PDTSceneProperties) + + +def unregister(): + """Unregister Classes and Delete Scene Variables. + + Operates on the classes list defined above. + """ + + from bpy.utils import unregister_class + + # remove OpenGL data + pdt_pivot_point.PDT_OT_ModalDrawOperator.handle_remove( + pdt_pivot_point.PDT_OT_ModalDrawOperator, bpy.context + ) + wm = bpy.context.window_manager + p = "pdt_run_opengl" + if p in wm: + del wm[p] + + for cls in reversed(classes): + unregister_class(cls) + + del Scene.pdt_pg + + +if __name__ == "__main__": + register() diff --git a/precision_drawing_tools/pdt_bix.py b/precision_drawing_tools/pdt_bix.py new file mode 100644 index 00000000..e59c1e08 --- /dev/null +++ b/precision_drawing_tools/pdt_bix.py @@ -0,0 +1,118 @@ +# ##### BEGIN GPL LICENSE BLOCK ##### +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software Foundation, +# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# ##### END GPL LICENSE BLOCK ##### +# +# <pep8 compliant> +# +# ---------------------------------------------------------- +# Author: Zeffii +# Modified by: Alan Odom (Clockmender) & Rune Morling (ermo) +# ---------------------------------------------------------- +# +# +import bpy +import bmesh +from . import pdt_cad_module as cm +from .pdt_msg_strings import PDT_ERR_2CPNPE, PDT_ERR_NCEDGES +from .pdt_functions import debug + +def add_line_to_bisection(self, context): + """Computes Bisector of 2 Co-Planar Edges. + + Args: + context: Blender bpy.context instance + + Returns: + Nothing. + """ + + obj = context.object + me = obj.data + bm = bmesh.from_edit_mesh(me) + + bm.verts.ensure_lookup_table() + bm.edges.ensure_lookup_table() + + edges = [e for e in bm.edges if e.select and not e.hide] + + if not len(edges) == 2: + msg = PDT_ERR_2CPNPE + self.report({"ERROR"}, msg) + return + + [[v1, v2], [v3, v4]] = [[v.co for v in e.verts] for e in edges] + debug(f"vectors found:\n {v1}\n {v2}\n {v3}\n {v4}") + + dist1 = (v1 - v2).length + dist2 = (v3 - v4).length + bdist = min([dist1, dist2]) + edge1 = (v1, v2) + edge2 = (v3, v4) + + if not cm.test_coplanar(edge1, edge2): + msg = PDT_ERR_NCEDGES + self.report({"ERROR"}, msg) + return + + # get pt and pick farthest vertex from (projected) intersections + pt = cm.get_intersection(edge1, edge2) + far1 = v2 if (v1 - pt).length < (v2 - pt).length else v1 + far2 = v4 if (v3 - pt).length < (v4 - pt).length else v3 + + dex1 = far1 - pt + dex2 = far2 - pt + dex1 = dex1 * (bdist / dex1.length) + dex2 = dex2 * (bdist / dex2.length) + pt2 = pt + (dex1).lerp(dex2, 0.5) + pt3 = pt2.lerp(pt, 2.0) + + vec1 = bm.verts.new(pt2) + vec2 = bm.verts.new(pt) + vec3 = bm.verts.new(pt3) + bm.edges.new((vec1, vec2)) + bm.edges.new((vec2, vec3)) + bmesh.ops.remove_doubles(bm, verts=bm.verts, dist=0.0001) + bmesh.update_edit_mesh(me) + + +class PDT_OT_LineOnBisection(bpy.types.Operator): + """Create Bisector between 2 Selected Edges.""" + + bl_idname = "pdt.linetobisect" + bl_label = "Add Edges Bisector" + bl_options = {"REGISTER", "UNDO"} + + @classmethod + def poll(cls, context): + """Only allow operation on a mesh object in EDIT mode.""" + ob = context.active_object + if ob is None: + return False + return all([ob is not None, ob.type == "MESH", ob.mode == "EDIT"]) + + def execute(self, context): + """Computes Bisector of 2 Co-Planar Edges. + + Args: + context: Blender bpy.context instance. + + Returns: + Status Set. + """ + + add_line_to_bisection(self, context) + return {"FINISHED"} diff --git a/precision_drawing_tools/pdt_cad_module.py b/precision_drawing_tools/pdt_cad_module.py new file mode 100644 index 00000000..21a5c6c7 --- /dev/null +++ b/precision_drawing_tools/pdt_cad_module.py @@ -0,0 +1,210 @@ +# ##### BEGIN GPL LICENSE BLOCK ##### +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software Foundation, +# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# ##### END GPL LICENSE BLOCK ##### +# +# <pep8 compliant> +# +# ---------------------------------------------------------- +# Author: Zeffii +# Modified by: Alan Odom (Clockmender) & Rune Morling (ermo) +# ---------------------------------------------------------- +# +import bmesh +from mathutils import Vector +from mathutils.geometry import intersect_line_line, intersect_point_line +from .pdt_functions import debug + +def point_on_edge(p, edge): + """Find Point on Edge. + + Args: + p: vector + edge: tuple containing 2 vectors. + + Returns: + True if point p happens to lie on the edge, False otherwise. + """ + + pt, _percent = intersect_point_line(p, *edge) + on_line = (pt - p).length < 1.0e-5 + return on_line and (0.0 <= _percent <= 1.0) + + +def line_from_edge_intersect(edge1, edge2): + """Get New Line from Intersections. + + Prepares input for sending to intersect_line_line + + Args: + edge1, edge2: tuples containing 2 vectors. + + Returns: + Output of intersect_line_line. + """ + + [p1, p2], [p3, p4] = edge1, edge2 + return intersect_line_line(p1, p2, p3, p4) + + +def get_intersection(edge1, edge2): + """Get Intersections of 2 Edges. + + Args: + edge1, edge2: tuples containing 2 vectors. + + Returns: + The point halfway on line. See intersect_line_line. + """ + + line = line_from_edge_intersect(edge1, edge2) + if line: + return (line[0] + line[1]) / 2 + + +def test_coplanar(edge1, edge2): + """Test 2 Edges are Co-planar. + + The line that describes the shortest line between the two edges would be short if the + lines intersect mathematically. If this line is longer than 1.0e-5 then they are either + coplanar or parallel + + Args: + edge1, edge2: tuples containing 2 vectors. + + Returns: + True if edge1 and edge2 or coplanar, False otherwise. + """ + + line = line_from_edge_intersect(edge1, edge2) + if line: + return (line[0] - line[1]).length < 1.0e-5 + + +def closest_idx(pt, e): + """Get Closest Vertex to input point. + + If both points in e are equally far from pt, then v1 is returned. + + Args: + pt: vector + e: bmesh edge + + Returns: + Index of vertex closest to pt. + """ + + if isinstance(e, bmesh.types.BMEdge): + ev = e.verts + v1 = ev[0].co + v2 = ev[1].co + distance_test = (v1 - pt).length <= (v2 - pt).length + return ev[0].index if distance_test else ev[1].index + + debug(f"Received {e}, check expected input in docstring ") + + +def closest_vector(pt, e): + """Return Closest Vector to input Point. + + If both points in e are equally far from pt, then v1 is returned. + + Args: + pt: vector + e: tuple containing 2 vectors + + Returns: + Vector closest to pt. + """ + + if isinstance(e, tuple) and all([isinstance(co, Vector) for co in e]): + v1, v2 = e + distance_test = (v1 - pt).length <= (v2 - pt).length + return v1 if distance_test else v2 + + debug(f"Received {e}, check expected input in docstring ") + + +def coords_tuple_from_edge_idx(bm, idx): + """Return Tuple from Vertex.""" + return tuple(v.co for v in bm.edges[idx].verts) + + +def vectors_from_indices(bm, raw_vert_indices): + """Return List of vectors from input indices.""" + return [bm.verts[i].co for i in raw_vert_indices] + + +def vertex_indices_from_edges_tuple(bm, edge_tuple): + """Return List of vertices. + + Args: + bm: is a bmesh representation + edge_tuple: contains 2 edge indices. + + Returns: + The vertex indices of edge_tuple. + """ + + def k(v, w): + return bm.edges[edge_tuple[v]].verts[w].index + + return [k(i >> 1, i % 2) for i in range(4)] + + +def get_vert_indices_from_bmedges(edges): + """Return List of Edges for evaluation. + + Args: + bmedges: a list of 2 bm edges + + Returns: + The vertex indices of edge_tuple as a flat list. + """ + temp_edges = [] + debug(edges) + for e in edges: + for v in e.verts: + temp_edges.append(v.index) + return temp_edges + + +def num_edges_point_lies_on(pt, edges): + """Returns the number of edges that a point lies on.""" + res = [point_on_edge(pt, edge) for edge in [edges[:2], edges[2:]]] + return len([i for i in res if i]) + + +def find_intersecting_edges(bm, pt, idx1, idx2): + """Find Intercecting Edges. + + Args: + pt: Vector + idx1, ix2: edge indices + + Returns: + The list of edge indices where pt is on those edges. + """ + if not pt: + return [] + idxs = [idx1, idx2] + edges = [coords_tuple_from_edge_idx(bm, idx) for idx in idxs] + return [idx for edge, idx in zip(edges, idxs) if point_on_edge(pt, edge)] + + +def vert_idxs_from_edge_idx(bm, idx): + edge = bm.edges[idx] + return edge.verts[0].index, edge.verts[1].index diff --git a/precision_drawing_tools/pdt_command.py b/precision_drawing_tools/pdt_command.py new file mode 100644 index 00000000..423bc83c --- /dev/null +++ b/precision_drawing_tools/pdt_command.py @@ -0,0 +1,880 @@ +# ***** BEGIN GPL LICENSE BLOCK ***** +# +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software Foundation, +# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# ***** END GPL LICENCE BLOCK ***** +# +# ----------------------------------------------------------------------- +# Author: Alan Odom (Clockmender), Rune Morling (ermo) Copyright (c) 2019 +# ----------------------------------------------------------------------- +# +import bpy +import bmesh +from mathutils import Vector +import math +from .pdt_functions import ( + debug, + disAng, + getPercent, + objCheck, + oops, + updateSel, +) +from .pdt_msg_strings import ( + PDT_ERR_ADDVEDIT, + PDT_ERR_BAD1VALS, + PDT_ERR_BAD2VALS, + PDT_ERR_BAD3VALS, + PDT_ERR_BADCOORDL, + PDT_ERR_BADFLETTER, + PDT_ERR_BADMATHS, + PDT_ERR_BADSLETTER, + PDT_ERR_CHARS_NUM, + PDT_ERR_DUPEDIT, + PDT_ERR_EXTEDIT, + PDT_ERR_FACE_SEL, + PDT_ERR_FILEDIT, + PDT_ERR_NOCOMMAS, + PDT_ERR_NON_VALID, + PDT_ERR_NO_ACT_OBJ, + PDT_ERR_NO_SEL_GEOM, + PDT_ERR_SEL_1_EDGE, + PDT_ERR_SEL_1_EDGEM, + PDT_ERR_SEL_1_VERT, + PDT_ERR_SPLITEDIT +) + + +def pdt_help(self, context): + """Display PDT Command Line help in a pop-up.""" + label = self.layout.label + label(text="Primary Letters (Available Secondary Letters):") + label(text="") + label(text="C: Cursor (a, d, i, p)") + label(text="D: Duplicate Geometry (d, i)") + label(text="E: Extrude Geometry (d, i)") + label(text="F: Fillet (v, e)") + label(text="G: Grab (Move) (a, d, i, p)") + label(text="N: New Vertex (a, d, i, p)") + label(text="M: Maths Functions (x, y, z, d, a, p)") + label(text="P: Pivot Point (a, d, i, p)") + label(text="V: Extrude Vertice Only (a, d, i, p)") + label(text="S: Split Edges (a, d, i, p)") + label(text="?: Quick Help") + label(text="") + label(text="Secondary Letters:") + label(text="") + label(text="- General Options:") + label(text="a: Absolute (Global) Coordinates e.g. 1,3,2") + label(text="d: Delta (Relative) Coordinates, e.g. 0.5,0,1.2") + label(text="i: Directional (Polar) Coordinates e.g. 2.6,45") + label(text="p: Percent e.g. 67.5") + label(text="- Fillet Options:") + label(text="v: Fillet Vertices") + label(text="e: Fillet Edges") + label(text="- Math Options:") + label(text="x, y, z: Send result to X, Y and Z input fields in PDT Design") + label(text="d, a, p: Send result to Distance, Angle or Percent input field in PDT Design") + label(text="") + label(text="Note that commands are case-insensitive: ED = Ed = eD = ed") + label(text="") + label(text="Examples:") + label(text="") + label(text="ed0.5,,0.6") + label(text="'- Extrude Geometry Delta 0.5 in X, 0 in Y, 0.6 in Z") + label(text="") + label(text="fe0.1,4,0.5") + label(text="'- Fillet Edges") + label(text="'- Radius: 0.1 (float) -- the radius (or offset) of the bevel/fillet") + label(text="'- Segments: 4 (int) -- choosing an even amount of segments gives better geometry") + label(text="'- Profile: 0.5 (float[0.0;1.0]) -- 0.5 (default) yields a circular, convex shape") + +def command_run(self, context): + """Run Command String as input into Command Line. + + Args: + context: Blender bpy.context instance. + + Note: + Uses pg.command, pg.error & many other 'pg.' variables to set PDT menu items, + or alter functions + + Command Format; Operation(single letter) Mode(single letter) Values(up to 3 values + separated by commas) + + Example; CD0.4,0.6,1.1 - Moves Cursor Delta XYZ = 0.4,0.6,1.1 from Current Position/Active + Vertex/Object Origin + + Example; SP35 - Splits active Edge at 35% of separation between edge's vertices + + Valid First Letters (as 'oper' - pg.command[0]) + C = Cursor, G = Grab(move), N = New Vertex, V = Extrude Vertices Only, + E = Extrude geometry, P = Move Pivot Point, D = Duplicate geometry, S = Split Edges + + Capitals and lower case letters are both allowed + + Valid Second Letters (as 'mode' - pg.command[1]) + + A = Absolute XYZ, D = Delta XYZ, I = Distance at Angle, P = Percent + X = X Delta, Y = Y, Delta Z, = Z Delta (Maths Operation only) + V = Vertex Bevel, E = Edge Bevel + + Capitals and lower case letters are both allowed + + Valid Values (pdt_command[2:]) + Only Integers and Floats, missing values are set to 0, appropriate length checks are + performed as Values is split by commas. + + Example; CA,,3 - Cursor to Absolute, is re-interpreted as CA0,0,3 + + Exception for Maths Operation, Values section is evaluated as Maths command + + Example; madegrees(atan(3/4)) - sets PDT Angle to smallest angle of 3,4,5 Triangle; + (36.8699 degrees) + + This is why all Math functions are imported + + Returns: + Nothing. + """ + + scene = context.scene + pg = scene.pdt_pg + cmd = pg.command + + if cmd.strip() == "?" or cmd.lower().strip() == "help": + # fmt: off + context.window_manager.popup_menu(pdt_help, title="PDT Command Line Help", icon="INFO") + # fmt: on + return + if len(cmd) < 3: + pg.error = PDT_ERR_CHARS_NUM + context.window_manager.popup_menu(oops, title="Error", icon="ERROR") + return + oper = cmd[0].upper() + if oper not in {"C", "D", "E", "F", "G", "N", "M", "P", "V", "S"}: + pg.error = PDT_ERR_BADFLETTER + context.window_manager.popup_menu(oops, title="Error", icon="ERROR") + return + mode = cmd[1].lower() + if mode not in {"a", "d", "e", "g", "i", "p", "v", "x", "y", "z"}: + pg.error = PDT_ERR_BADSLETTER + context.window_manager.popup_menu(oops, title="Error", icon="ERROR") + return + + # -------------- + # Math Operation + if oper == "M": + if mode not in {"x", "y", "z", "d", "a", "p"}: + pg.error = f"{mode} {PDT_ERR_NON_VALID} Maths)" + context.window_manager.popup_menu(oops, title="Error", icon="ERROR") + return + exp = cmd[2:] + namespace = {} + namespace.update(vars(math)) + if "," in exp: + pg.error = PDT_ERR_NOCOMMAS + context.window_manager.popup_menu(oops, title="Error", icon="ERROR") + try: + num = eval(exp, namespace, namespace) + except ValueError: + pg.error = PDT_ERR_BADMATHS + context.window_manager.popup_menu(oops, title="Error", icon="ERROR") + return + if mode == "x": + pg.cartesian_coords.x = num + elif mode == "y": + pg.cartesian_coords.y = num + elif mode == "z": + pg.cartesian_coords.z = num + elif mode == "d": + pg.distance = num + elif mode == "a": + pg.angle = num + elif mode == "p": + pg.percent = num + return + # "x"/"y"/"z" modes are only legal for Math Operation + else: + if mode in {"x", "y", "z"}: + pg.error = PDT_ERR_BADCOORDL + context.window_manager.popup_menu(oops, title="Error", icon="ERROR") + return + + # ----------------------------------------------------- + # Not a Math Operation, so let's parse the command line + vals = cmd[2:].split(",") + ind = 0 + for r in vals: + try: + _ = float(r) + good = True + except ValueError: + vals[ind] = "0" + ind = ind + 1 + mode_s = pg.select + flip_a = pg.flip_angle + flip_p = pg.flip_percent + ext_a = pg.extend + plane = pg.plane + obj = context.view_layer.objects.active + #FIXME: What does call this imply in terms of invariants? + # There's a lot of places in the code where we rely on bm not being None... + bm, good = objCheck(obj, scene, oper) + obj_loc = None + if good: + obj_loc = obj.matrix_world.decompose()[0] + debug(f"cmd: {cmd}") + debug(f"obj: {obj}, bm: {bm}, obj_loc: {obj_loc}") + + # static set variable for use in recurring comparisons + adip = {"a", "d", "i", "p"} + + # --------------------- + # Cursor or Pivot Point + if oper in {"C", "P"}: + if mode not in adip: + pg.error = f"'{mode}' {PDT_ERR_NON_VALID} '{oper}'" + context.window_manager.popup_menu(oops, title="Error", icon="ERROR") + return + # Absolute/Global Coordinates + if mode == "a": + if len(vals) != 3: + pg.error = PDT_ERR_BAD3VALS + context.window_manager.popup_menu(oops, title="Error", icon="ERROR") + return + vector_delta = Vector((float(vals[0]), float(vals[1]), float(vals[2]))) + if oper == "C": + scene.cursor.location = vector_delta + else: + pg.pivot_loc = vector_delta + # Delta/Relative Coordinates + elif mode == "d": + if len(vals) != 3: + pg.error = PDT_ERR_BAD3VALS + context.window_manager.popup_menu(oops, title="Error", icon="ERROR") + return + vector_delta = Vector((float(vals[0]), float(vals[1]), float(vals[2]))) + if mode_s == "REL": + if oper == "C": + scene.cursor.location = scene.cursor.location + vector_delta + else: + pg.pivot_loc = pg.pivot_loc + vector_delta + elif mode_s == "SEL": + if obj.mode == "EDIT": + if oper == "C": + scene.cursor.location = ( + bm.select_history[-1].co + obj_loc + vector_delta + ) + else: + pg.pivot_loc = bm.select_history[-1].co + obj_loc + vector_delta + elif obj.mode == "OBJECT": + if oper == "C": + scene.cursor.location = obj_loc + vector_delta + else: + pg.pivot_loc = obj_loc + vector_delta + # Direction/Polar Coordinates + elif mode == "i": + if len(vals) != 2: + pg.error = PDT_ERR_BAD2VALS + context.window_manager.popup_menu(oops, title="Error", icon="ERROR") + return + vector_delta = disAng(vals, flip_a, plane, scene) + if mode_s == "REL": + if oper == "C": + scene.cursor.location = scene.cursor.location + vector_delta + else: + pg.pivot_loc = pg.pivot_loc + vector_delta + elif mode_s == "SEL": + if obj.mode == "EDIT": + if oper == "C": + scene.cursor.location = ( + bm.select_history[-1].co + obj_loc + vector_delta + ) + else: + pg.pivot_loc = bm.select_history[-1].co + obj_loc + vector_delta + elif obj.mode == "OBJECT": + if oper == "C": + scene.cursor.location = obj_loc + vector_delta + else: + pg.pivot_loc = obj_loc + vector_delta + # Percent Options + elif mode == "p": + if len(vals) != 1: + pg.error = PDT_ERR_BAD1VALS + context.window_manager.popup_menu(oops, title="Error", icon="ERROR") + return + vector_delta = getPercent(obj, flip_p, float(vals[0]), oper, scene) + if vector_delta is None: + return + if obj.mode == "EDIT": + if oper == "C": + scene.cursor.location = obj_loc + vector_delta + else: + pg.pivot_loc = obj_loc + vector_delta + elif obj.mode == "OBJECT": + if oper == "C": + scene.cursor.location = vector_delta + else: + pg.pivot_loc = vector_delta + return + + # ------------------------ + # Move Vertices or Objects + elif oper == "G": + if mode not in adip: + pg.error = f"'{mode}' {PDT_ERR_NON_VALID} '{oper}'" + context.window_manager.popup_menu(oops, title="Error", icon="ERROR") + return + # Absolute/Global Coordinates + if mode == "a": + if len(vals) != 3: + pg.error = PDT_ERR_BAD3VALS + context.window_manager.popup_menu(oops, title="Error", icon="ERROR") + return + vector_delta = Vector((float(vals[0]), float(vals[1]), float(vals[2]))) + if obj.mode == "EDIT": + verts = [v for v in bm.verts if v.select] + if len(verts) == 0: + pg.error = PDT_ERR_NO_SEL_GEOM + context.window_manager.popup_menu(oops, title="Error", icon="ERROR") + return + for v in verts: + v.co = vector_delta - obj_loc + bmesh.ops.remove_doubles( + bm, verts=[v for v in bm.verts if v.select], dist=0.0001 + ) + bmesh.update_edit_mesh(obj.data) + bm.select_history.clear() + elif obj.mode == "OBJECT": + for ob in context.view_layer.objects.selected: + ob.location = vector_delta + # Delta/Relative Coordinates + elif mode == "d": + if len(vals) != 3: + pg.error = PDT_ERR_BAD3VALS + context.window_manager.popup_menu(oops, title="Error", icon="ERROR") + return + vector_delta = Vector((float(vals[0]), float(vals[1]), float(vals[2]))) + if obj.mode == "EDIT": + # FIXME: Show error popup if nothing is selected? + bmesh.ops.translate( + bm, verts=[v for v in bm.verts if v.select], vec=vector_delta + ) + bmesh.update_edit_mesh(obj.data) + bm.select_history.clear() + elif obj.mode == "OBJECT": + for ob in context.view_layer.objects.selected: + ob.location = obj_loc + vector_delta + # Direction/Polar Coordinates + elif mode == "i": + if len(vals) != 2: + pg.error = PDT_ERR_BAD2VALS + context.window_manager.popup_menu(oops, title="Error", icon="ERROR") + return + vector_delta = disAng(vals, flip_a, plane, scene) + if obj.mode == "EDIT": + verts = [v for v in bm.verts if v.select] + if len(verts) == 0: + pg.error = PDT_ERR_NO_SEL_GEOM + context.window_manager.popup_menu(oops, title="Error", icon="ERROR") + return + bmesh.ops.translate(bm, verts=verts, vec=vector_delta) + bmesh.update_edit_mesh(obj.data) + bm.select_history.clear() + elif obj.mode == "OBJECT": + for ob in context.view_layer.objects.selected: + ob.location = ob.location + vector_delta + # Percent Options + elif mode == "p": + if obj.mode == "OBJECT": + if len(vals) != 1: + pg.error = PDT_ERR_BAD1VALS + context.window_manager.popup_menu(oops, title="Error", icon="ERROR") + return + vector_delta = getPercent(obj, flip_p, float(vals[0]), oper, scene) + if vector_delta is None: + return + ob.location = vector_delta + return + + # -------------- + # Add New Vertex + elif oper == "N": + if mode not in adip: + pg.error = f"'{mode}' {PDT_ERR_NON_VALID} '{oper}'" + context.window_manager.popup_menu(oops, title="Error", icon="ERROR") + return + if not obj.mode == "EDIT": + pg.error = PDT_ERR_ADDVEDIT + context.window_manager.popup_menu(oops, title="Error", icon="ERROR") + return + # Absolute/Global Coordinates + if mode == "a": + if len(vals) != 3: + pg.error = PDT_ERR_BAD3VALS + context.window_manager.popup_menu(oops, title="Error", icon="ERROR") + return + vector_delta = Vector((float(vals[0]), float(vals[1]), float(vals[2]))) + vNew = vector_delta - obj_loc + nVert = bm.verts.new(vNew) + for v in [v for v in bm.verts if v.select]: + v.select_set(False) + nVert.select_set(True) + bmesh.update_edit_mesh(obj.data) + bm.select_history.clear() + # Delta/Relative Coordinates + elif mode == "d": + if len(vals) != 3: + pg.error = PDT_ERR_BAD3VALS + context.window_manager.popup_menu(oops, title="Error", icon="ERROR") + return + vector_delta = Vector((float(vals[0]), float(vals[1]), float(vals[2]))) + vNew = bm.select_history[-1].co + vector_delta + nVert = bm.verts.new(vNew) + for v in [v for v in bm.verts if v.select]: + v.select_set(False) + nVert.select_set(True) + bmesh.update_edit_mesh(obj.data) + bm.select_history.clear() + # Direction/Polar Coordinates + elif mode == "d": + if len(vals) != 2: + pg.error = PDT_ERR_BAD2VALS + context.window_manager.popup_menu(oops, title="Error", icon="ERROR") + return + vector_delta = disAng(vals, flip_a, plane, scene) + vNew = bm.select_history[-1].co + vector_delta + nVert = bm.verts.new(vNew) + for v in [v for v in bm.verts if v.select]: + v.select_set(False) + nVert.select_set(True) + bmesh.update_edit_mesh(obj.data) + bm.select_history.clear() + # Percent Options + elif mode == "p": + if len(vals) != 1: + pg.error = PDT_ERR_BAD1VALS + context.window_manager.popup_menu(oops, title="Error", icon="ERROR") + return + vector_delta = getPercent(obj, flip_p, float(vals[0]), oper, scene) + vNew = vector_delta + nVert = bm.verts.new(vNew) + for v in [v for v in bm.verts if v.select]: + v.select_set(False) + nVert.select_set(True) + bmesh.update_edit_mesh(obj.data) + bm.select_history.clear() + return + + # ----------- + # Split Edges + elif oper == "S": + if mode not in adip: + pg.error = f"'{mode}' {PDT_ERR_NON_VALID} '{oper}'" + context.window_manager.popup_menu(oops, title="Error", icon="ERROR") + return + if not obj.mode == "EDIT": + pg.error = PDT_ERR_SPLITEDIT + context.window_manager.popup_menu(oops, title="Error", icon="ERROR") + return + # Absolute/Global Coordinates + if mode == "a": + if len(vals) != 3: + pg.error = PDT_ERR_BAD3VALS + context.window_manager.popup_menu(oops, title="Error", icon="ERROR") + return + vector_delta = Vector((float(vals[0]), float(vals[1]), float(vals[2]))) + edges = [e for e in bm.edges if e.select] + if len(edges) != 1: + pg.error = f"{PDT_ERR_SEL_1_EDGE} {len(edges)})" + context.window_manager.popup_menu(oops, title="Error", icon="ERROR") + return + geom = bmesh.ops.subdivide_edges(bm, edges=edges, cuts=1) + new_verts = [v for v in geom["geom_split"] if isinstance(v, bmesh.types.BMVert)] + nVert = new_verts[0] + nVert.co = vector_delta - obj_loc + for v in [v for v in bm.verts if v.select]: + v.select_set(False) + nVert.select_set(True) + bmesh.update_edit_mesh(obj.data) + bm.select_history.clear() + # Delta/Relative Coordinates + elif mode == "d": + if len(vals) != 3: + pg.error = PDT_ERR_BAD3VALS + context.window_manager.popup_menu(oops, title="Error", icon="ERROR") + return + vector_delta = Vector((float(vals[0]), float(vals[1]), float(vals[2]))) + edges = [e for e in bm.edges if e.select] + faces = [f for f in bm.faces if f.select] + if len(faces) != 0: + pg.error = PDT_ERR_FACE_SEL + context.window_manager.popup_menu(oops, title="Error", icon="ERROR") + return + if len(edges) < 1: + pg.error = f"{PDT_ERR_SEL_1_EDGEM} {len(edges)})" + context.window_manager.popup_menu(oops, title="Error", icon="ERROR") + return + geom = bmesh.ops.subdivide_edges(bm, edges=edges, cuts=1) + new_verts = [v for v in geom["geom_split"] if isinstance(v, bmesh.types.BMVert)] + for v in [v for v in bm.verts if v.select]: + v.select_set(False) + for v in new_verts: + v.select_set(False) + bmesh.ops.translate(bm, verts=new_verts, vec=vector_delta) + for v in [v for v in bm.verts if v.select]: + v.select_set(False) + for v in new_verts: + v.select_set(False) + bmesh.update_edit_mesh(obj.data) + bm.select_history.clear() + # Directional/Polar Coordinates + elif mode == "i": + if len(vals) != 2: + pg.error = PDT_ERR_BAD2VALS + context.window_manager.popup_menu(oops, title="Error", icon="ERROR") + return + vector_delta = disAng(vals, flip_a, plane, scene) + edges = [e for e in bm.edges if e.select] + faces = [f for f in bm.faces if f.select] + if len(faces) != 0: + pg.error = PDT_ERR_FACE_SEL + context.window_manager.popup_menu(oops, title="Error", icon="ERROR") + return + if len(edges) < 1: + pg.error = f"{PDT_ERR_SEL_1_EDGEM} {len(edges)})" + context.window_manager.popup_menu(oops, title="Error", icon="ERROR") + return + geom = bmesh.ops.subdivide_edges(bm, edges=edges, cuts=1) + new_verts = [v for v in geom["geom_split"] if isinstance(v, bmesh.types.BMVert)] + bmesh.ops.translate(bm, verts=new_verts, vec=vector_delta) + for v in [v for v in bm.verts if v.select]: + v.select_set(False) + for v in new_verts: + v.select_set(False) + bmesh.update_edit_mesh(obj.data) + bm.select_history.clear() + # Percent Options + elif mode == "p": + if len(vals) != 1: + pg.error = PDT_ERR_BAD1VALS + context.window_manager.popup_menu(oops, title="Error", icon="ERROR") + return + vector_delta = getPercent(obj, flip_p, float(vals[0]), oper, scene) + if vector_delta is None: + return + edges = [e for e in bm.edges if e.select] + faces = [f for f in bm.faces if f.select] + if len(faces) != 0: + pg.error = PDT_ERR_FACE_SEL + context.window_manager.popup_menu(oops, title="Error", icon="ERROR") + return + if len(edges) < 1: + pg.error = f"{PDT_ERR_SEL_1_EDGEM} {len(edges)})" + context.window_manager.popup_menu(oops, title="Error", icon="ERROR") + return + geom = bmesh.ops.subdivide_edges(bm, edges=edges, cuts=1) + new_verts = [v for v in geom["geom_split"] if isinstance(v, bmesh.types.BMVert)] + nVert = new_verts[0] + nVert.co = vector_delta + for v in [v for v in bm.verts if v.select]: + v.select_set(False) + nVert.select_set(True) + bmesh.update_edit_mesh(obj.data) + bm.select_history.clear() + return + + # ---------------- + # Extrude Vertices + elif oper == "V": + if mode not in adip: + pg.error = f"'{mode}' {PDT_ERR_NON_VALID} '{oper}'" + context.window_manager.popup_menu(oops, title="Error", icon="ERROR") + return + if not obj.mode == "EDIT": + pg.error = PDT_ERR_EXTEDIT + context.window_manager.popup_menu(oops, title="Error", icon="ERROR") + return + # Absolute/Global Coordinates + if mode == "a": + if len(vals) != 3: + pg.error = PDT_ERR_BAD3VALS + context.window_manager.popup_menu(oops, title="Error", icon="ERROR") + return + vector_delta = Vector((float(vals[0]), float(vals[1]), float(vals[2]))) + vNew = vector_delta - obj_loc + nVert = bm.verts.new(vNew) + for v in [v for v in bm.verts if v.select]: + bm.edges.new([v, nVert]) + v.select_set(False) + nVert.select_set(True) + bmesh.ops.remove_doubles( + bm, verts=[v for v in bm.verts if v.select], dist=0.0001 + ) + bmesh.update_edit_mesh(obj.data) + bm.select_history.clear() + # Delta/Relative Coordinates + elif mode == "d": + if len(vals) != 3: + pg.error = PDT_ERR_BAD3VALS + context.window_manager.popup_menu(oops, title="Error", icon="ERROR") + return + vector_delta = Vector((float(vals[0]), float(vals[1]), float(vals[2]))) + verts = [v for v in bm.verts if v.select] + if len(verts) == 0: + pg.error = PDT_ERR_NO_SEL_GEOM + context.window_manager.popup_menu(oops, title="Error", icon="ERROR") + return + for v in verts: + nVert = bm.verts.new(v.co) + nVert.co = nVert.co + vector_delta + bm.edges.new([v, nVert]) + v.select_set(False) + nVert.select_set(True) + bmesh.update_edit_mesh(obj.data) + bm.select_history.clear() + # Direction/Polar Coordinates + elif mode == "i": + if len(vals) != 2: + pg.error = PDT_ERR_BAD2VALS + context.window_manager.popup_menu(oops, title="Error", icon="ERROR") + return + vector_delta = disAng(vals, flip_a, plane, scene) + verts = [v for v in bm.verts if v.select] + if len(verts) == 0: + pg.error = PDT_ERR_NO_SEL_GEOM + context.window_manager.popup_menu(oops, title="Error", icon="ERROR") + return + for v in verts: + nVert = bm.verts.new(v.co) + nVert.co = nVert.co + vector_delta + bm.edges.new([v, nVert]) + v.select_set(False) + nVert.select_set(True) + bmesh.update_edit_mesh(obj.data) + bm.select_history.clear() + # Percent Options + elif mode == "p": + vector_delta = getPercent(obj, flip_p, float(vals[0]), oper, scene) + verts = [v for v in bm.verts if v.select] + if len(verts) == 0: + pg.error = PDT_ERR_NO_SEL_GEOM + context.window_manager.popup_menu(oops, title="Error", icon="ERROR") + return + nVert = bm.verts.new(vector_delta) + if ext_a: + for v in [v for v in bm.verts if v.select]: + bm.edges.new([v, nVert]) + v.select_set(False) + else: + bm.edges.new([bm.select_history[-1], nVert]) + nVert.select_set(True) + bmesh.update_edit_mesh(obj.data) + bm.select_history.clear() + return + + # ---------------- + # Extrude Geometry + elif oper == "E": + if mode not in {"d", "i"}: + pg.error = f"'{mode}' {PDT_ERR_NON_VALID} '{oper}'" + context.window_manager.popup_menu(oops, title="Error", icon="ERROR") + return + if not obj.mode == "EDIT": + pg.error = PDT_ERR_EXTEDIT + context.window_manager.popup_menu(oops, title="Error", icon="ERROR") + return + # Delta/Relative Coordinates + if mode == "d": + if len(vals) != 3: + pg.error = PDT_ERR_BAD3VALS + context.window_manager.popup_menu(oops, title="Error", icon="ERROR") + return + vector_delta = Vector((float(vals[0]), float(vals[1]), float(vals[2]))) + verts = [v for v in bm.verts if v.select] + if len(verts) == 0: + pg.error = PDT_ERR_NO_SEL_GEOM + context.window_manager.popup_menu(oops, title="Error", icon="ERROR") + return + ret = bmesh.ops.extrude_face_region( + bm, + geom=( + [f for f in bm.faces if f.select] + + [e for e in bm.edges if e.select] + + [v for v in bm.verts if v.select] + ), + use_select_history=True, + ) + geom_extr = ret["geom"] + verts_extr = [v for v in geom_extr if isinstance(v, bmesh.types.BMVert)] + edges_extr = [e for e in geom_extr if isinstance(e, bmesh.types.BMEdge)] + faces_extr = [f for f in geom_extr if isinstance(f, bmesh.types.BMFace)] + del ret + bmesh.ops.translate(bm, verts=verts_extr, vec=vector_delta) + updateSel(bm, verts_extr, edges_extr, faces_extr) + bmesh.update_edit_mesh(obj.data) + bm.select_history.clear() + # Direction/Polar Coordinates + elif mode == "i": + if len(vals) != 2: + pg.error = PDT_ERR_BAD2VALS + context.window_manager.popup_menu(oops, title="Error", icon="ERROR") + return + vector_delta = disAng(vals, flip_a, plane, scene) + verts = [v for v in bm.verts if v.select] + if len(verts) == 0: + pg.error = PDT_ERR_NO_SEL_GEOM + context.window_manager.popup_menu(oops, title="Error", icon="ERROR") + return + ret = bmesh.ops.extrude_face_region( + bm, + geom=( + [f for f in bm.faces if f.select] + + [e for e in bm.edges if e.select] + + [v for v in bm.verts if v.select] + ), + use_select_history=True, + ) + geom_extr = ret["geom"] + verts_extr = [v for v in geom_extr if isinstance(v, bmesh.types.BMVert)] + edges_extr = [e for e in geom_extr if isinstance(e, bmesh.types.BMEdge)] + faces_extr = [f for f in geom_extr if isinstance(f, bmesh.types.BMFace)] + del ret + bmesh.ops.translate(bm, verts=verts_extr, vec=vector_delta) + updateSel(bm, verts_extr, edges_extr, faces_extr) + bmesh.update_edit_mesh(obj.data) + bm.select_history.clear() + return + + # ------------------ + # Duplicate Geometry + elif oper == "D": + if mode not in {"d", "i"}: + pg.error = f"'{mode}' {PDT_ERR_NON_VALID} '{oper}'" + context.window_manager.popup_menu(oops, title="Error", icon="ERROR") + return + if not obj.mode == "EDIT": + pg.error = PDT_ERR_DUPEDIT + context.window_manager.popup_menu(oops, title="Error", icon="ERROR") + return + # Delta/Relative Coordinates + if mode == "d": + if len(vals) != 3: + pg.error = PDT_ERR_BAD3VALS + context.window_manager.popup_menu(oops, title="Error", icon="ERROR") + return + vector_delta = Vector((float(vals[0]), float(vals[1]), float(vals[2]))) + verts = [v for v in bm.verts if v.select] + if len(verts) == 0: + pg.error = PDT_ERR_NO_SEL_GEOM + context.window_manager.popup_menu(oops, title="Error", icon="ERROR") + return + ret = bmesh.ops.duplicate( + bm, + geom=( + [f for f in bm.faces if f.select] + + [e for e in bm.edges if e.select] + + [v for v in bm.verts if v.select] + ), + use_select_history=True, + ) + geom_dupe = ret["geom"] + verts_dupe = [v for v in geom_dupe if isinstance(v, bmesh.types.BMVert)] + edges_dupe = [e for e in geom_dupe if isinstance(e, bmesh.types.BMEdge)] + faces_dupe = [f for f in geom_dupe if isinstance(f, bmesh.types.BMFace)] + del ret + bmesh.ops.translate(bm, verts=verts_dupe, vec=vector_delta) + updateSel(bm, verts_dupe, edges_dupe, faces_dupe) + bmesh.update_edit_mesh(obj.data) + bm.select_history.clear() + # Direction/Polar Coordinates + elif mode == "i": + if len(vals) != 2: + pg.error = PDT_ERR_BAD2VALS + context.window_manager.popup_menu(oops, title="Error", icon="ERROR") + return + vector_delta = disAng(vals, flip_a, plane, scene) + verts = [v for v in bm.verts if v.select] + if len(verts) == 0: + pg.error = PDT_ERR_NO_SEL_GEOM + context.window_manager.popup_menu(oops, title="Error", icon="ERROR") + return + ret = bmesh.ops.duplicate( + bm, + geom=( + [f for f in bm.faces if f.select] + + [e for e in bm.edges if e.select] + + [v for v in bm.verts if v.select] + ), + use_select_history=True, + ) + geom_dupe = ret["geom"] + verts_dupe = [v for v in geom_dupe if isinstance(v, bmesh.types.BMVert)] + edges_dupe = [e for e in geom_dupe if isinstance(e, bmesh.types.BMEdge)] + faces_dupe = [f for f in geom_dupe if isinstance(f, bmesh.types.BMFace)] + del ret + bmesh.ops.translate(bm, verts=verts_dupe, vec=vector_delta) + updateSel(bm, verts_dupe, edges_dupe, faces_dupe) + bmesh.update_edit_mesh(obj.data) + bm.select_history.clear() + return + + # --------------- + # Fillet Geometry + elif oper == "F": + if mode not in {"v", "e"}: + pg.error = f"'{mode}' {PDT_ERR_NON_VALID} '{oper}'" + context.window_manager.popup_menu(oops, title="Error", icon="ERROR") + return + if obj is None: + pg.error = PDT_ERR_NO_ACT_OBJ + context.window_manager.popup_menu(oops, title="Error", icon="ERROR") + return + if not obj.mode == "EDIT": + pg.error = PDT_ERR_FILEDIT + context.window_manager.popup_menu(oops, title="Error", icon="ERROR") + return + if len(vals) != 3: + pg.error = PDT_ERR_BAD3VALS + context.window_manager.popup_menu(oops, title="Error", icon="ERROR") + return + if mode == "v": + vert_bool = True + elif mode == "e": + vert_bool = False + verts = [v for v in bm.verts if v.select] + if len(verts) == 0: + pg.error = PDT_ERR_SEL_1_VERT + context.window_manager.popup_menu(oops, title="Error", icon="ERROR") + return + # Note that passing an empty parameter results in that parameter being seen as "0" + # _offset <= 0 is ignored since a bevel/fillet radius must be > 0 to make sense + _offset = float(vals[0]) + _segments = int(vals[1]) + if _segments < 1: + _segments = 1 # This is a single, flat segment (ignores profile) + _profile = float(vals[2]) + if _profile < 0.0 or _profile > 1.0: + _profile = 0.5 # This is a circular profile + bpy.ops.mesh.bevel( + offset_type="OFFSET", + offset=_offset, + segments=_segments, + profile=_profile, + vertex_only=vert_bool + ) + return diff --git a/precision_drawing_tools/pdt_design.py b/precision_drawing_tools/pdt_design.py new file mode 100644 index 00000000..ddde1231 --- /dev/null +++ b/precision_drawing_tools/pdt_design.py @@ -0,0 +1,1478 @@ +# ***** BEGIN GPL LICENSE BLOCK ***** +# +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software Foundation, +# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# ***** END GPL LICENCE BLOCK ***** +# +# ----------------------------------------------------------------------- +# Author: Alan Odom (Clockmender), Rune Morling (ermo) Copyright (c) 2019 +# ----------------------------------------------------------------------- +# +import bmesh +import bpy +import numpy as np +from bpy.types import Operator +from mathutils import Vector +from mathutils.geometry import intersect_point_line +from math import sin, cos, tan, pi, sqrt +from .pdt_functions import ( + setMode, + checkSelection, + setAxis, + updateSel, + viewCoords, + viewCoordsI, + viewDir, + arcCentre, + intersection, + getPercent, +) +from .pdt_msg_strings import ( + PDT_ERR_CONNECTED, + PDT_ERR_EDIT_MODE, + PDT_ERR_EDOB_MODE, + PDT_ERR_FACE_SEL, + PDT_ERR_INT_LINES, + PDT_ERR_INT_NO_ALL, + PDT_ERR_NON_VALID, + PDT_ERR_NO_ACT_OBJ, + PDT_ERR_NO_ACT_VERTS, + PDT_ERR_SEL_1_EDGE, + PDT_ERR_SEL_1_VERT, + PDT_ERR_SEL_1_VERTI, + PDT_ERR_SEL_2_OBJS, + PDT_ERR_SEL_2_VERTIO, + PDT_ERR_SEL_2_VERTS, + PDT_ERR_SEL_3_OBJS, + PDT_ERR_SEL_3_VERTIO, + PDT_ERR_SEL_3_VERTS, + PDT_ERR_SEL_4_OBJS, + PDT_ERR_SEL_4_VERTS, + PDT_ERR_STRIGHT_LINE, + PDT_ERR_TAPER_ANG, + PDT_ERR_TAPER_SEL, + PDT_ERR_VERT_MODE, + PDT_INF_OBJ_MOVED, + PDT_LAB_ABS, + PDT_LAB_ARCCENTRE, + PDT_LAB_DEL, + PDT_LAB_DIR, + PDT_LAB_INTERSECT, + PDT_LAB_NOR, + PDT_LAB_PERCENT, + PDT_LAB_PLANE +) + + +class PDT_OT_PlacementAbs(Operator): + """Use Absolute, or Global Placement.""" + + bl_idname = "pdt.absolute" + bl_label = "Absolute Mode" + bl_options = {"REGISTER", "UNDO"} + + def execute(self, context): + """Manipulates Geometry, or Objects by Absolute (World) Coordinates. + + - Reads pg.operate from Operation Mode Selector as 'data' + - Reads pg.cartesian_coords scene variables to: + -- set position of CUrsor (CU) + -- set postion of Pivot Point (PP) + -- MoVe geometry/objects (MV) + -- Extrude Vertices (EV) + -- Split Edges (SE) + -- add a New Vertex (NV) + + Invalid Options result in self.report Error. + + Local vector variable 'vector_delta' is used to reposition features. + + Args: + context: Blender bpy.context instance. + + Returns: + Status Set. + """ + + scene = context.scene + pg = scene.pdt_pg + oper = pg.operation + + vector_delta = pg.cartesian_coords + if oper not in {"CU", "PP", "NV"}: + obj = context.view_layer.objects.active + if obj is None: + errmsg = PDT_ERR_NO_ACT_OBJ + self.report({"ERROR"}, errmsg) + return {"FINISHED"} + obj_loc = obj.matrix_world.decompose()[0] + if obj.mode == "EDIT": + bm = bmesh.from_edit_mesh(obj.data) + verts = [v for v in bm.verts if v.select] + if len(verts) == 0: + errmsg = PDT_ERR_NO_ACT_VERTS + self.report({"ERROR"}, errmsg) + return {"FINISHED"} + if oper == "CU": + scene.cursor.location = vector_delta + scene.cursor.rotation_euler = (0, 0, 0) + elif oper == "PP": + pg.pivot_loc = vector_delta + elif oper == "MV": + if obj.mode == "EDIT": + for v in verts: + v.co = vector_delta - obj_loc + bm.select_history.clear() + bmesh.ops.remove_doubles(bm, verts=[v for v in bm.verts if v.select], dist=0.0001) + bmesh.update_edit_mesh(obj.data) + elif obj.mode == "OBJECT": + for ob in context.view_layer.objects.selected: + ob.location = vector_delta + elif oper == "SE" and obj.mode == "EDIT": + edges = [e for e in bm.edges if e.select] + if len(edges) != 1: + errmsg = f"{PDT_ERR_SEL_1_EDGE} {len(edges)})" + self.report({"ERROR"}, errmsg) + return {"FINISHED"} + geom = bmesh.ops.subdivide_edges(bm, edges=edges, cuts=1) + new_verts = [v for v in geom["geom_split"] if isinstance(v, bmesh.types.BMVert)] + nVert = new_verts[0] + nVert.co = vector_delta - obj_loc + for v in [v for v in bm.verts if v.select]: + v.select_set(False) + nVert.select_set(True) + bmesh.update_edit_mesh(obj.data) + bm.select_history.clear() + elif oper == "NV": + obj = context.view_layer.objects.active + if obj is None: + errmsg = PDT_ERR_NO_ACT_OBJ + self.report({"ERROR"}, errmsg) + return {"FINISHED"} + if obj.mode == "EDIT": + bm = bmesh.from_edit_mesh(obj.data) + vNew = vector_delta - obj.location + nVert = bm.verts.new(vNew) + bmesh.update_edit_mesh(obj.data) + bm.select_history.clear() + nVert.select_set(True) + else: + errmsg = f"{PDT_ERR_EDIT_MODE} {obj.mode})" + self.report({"ERROR"}, errmsg) + return {"FINISHED"} + elif oper == "EV" and obj.mode == "EDIT": + vNew = vector_delta - obj_loc + nVert = bm.verts.new(vNew) + for v in [v for v in bm.verts if v.select]: + bm.edges.new([v, nVert]) + v.select_set(False) + nVert.select_set(True) + bm.select_history.clear() + bmesh.ops.remove_doubles(bm, verts=[v for v in bm.verts if v.select], dist=0.0001) + bmesh.update_edit_mesh(obj.data) + else: + errmsg = f"{oper} {PDT_ERR_NON_VALID} {PDT_LAB_ABS}" + self.report({"ERROR"}, errmsg) + return {"FINISHED"} + + +class PDT_OT_PlacementDelta(Operator): + """Use Delta, or Incremental Placement.""" + + bl_idname = "pdt.delta" + bl_label = "Delta Mode" + bl_options = {"REGISTER", "UNDO"} + + def execute(self, context): + """Manipulates Geometry, or Objects by Delta Offset (Increment). + + - Reads pg.operation from Operation Mode Selector as 'oper' + - Reads pg.select, pg.plane, pg.cartesian_coords scene variables to: + -- set position of CUrsor (CU) + -- set position of Pivot Point (PP) + -- MoVe geometry/objects (MV) + -- Extrude Vertices (EV) + -- Split Edges (SE) + -- add a New Vertex (NV) + -- Duplicate Geometry (DG) + -- Extrude Geometry (EG) + + Invalid Options result in self.report Error. + + Local vector variable 'vector_delta' used to reposition features. + + Args: + context: Blender bpy.context instance. + + Returns: + Status Set. + """ + + scene = context.scene + pg = scene.pdt_pg + x_loc = pg.cartesian_coords.x + y_loc = pg.cartesian_coords.y + z_loc = pg.cartesian_coords.z + mode_s = pg.select + oper = pg.operation + + if pg.plane == "LO": + vector_delta = viewCoords(x_loc, y_loc, z_loc) + else: + vector_delta = Vector((x_loc, y_loc, z_loc)) + if mode_s == "REL" and oper == "CU": + scene.cursor.location = scene.cursor.location + vector_delta + elif mode_s == "REL" and oper == "PP": + pg.pivot_loc = pg.pivot_loc + vector_delta + else: + obj = context.view_layer.objects.active + if obj is None: + errmsg = PDT_ERR_NO_ACT_OBJ + self.report({"ERROR"}, errmsg) + return {"FINISHED"} + obj_loc = obj.matrix_world.decompose()[0] + if obj.mode == "EDIT": + bm = bmesh.from_edit_mesh(obj.data) + if oper not in {"MV", "SE", "EV", "DG", "EG"}: + if len(bm.select_history) >= 1: + actV = checkSelection(1, bm, obj) + if actV is None: + errmsg = PDT_ERR_VERT_MODE + self.report({"ERROR"}, errmsg) + return {"FINISHED"} + else: + errmsg = f"{PDT_ERR_SEL_1_VERTI} {len(bm.select_history)})" + self.report({"ERROR"}, errmsg) + return {"FINISHED"} + if oper not in {"CU", "PP", "NV"} and obj.mode == "EDIT": + verts = [v for v in bm.verts if v.select] + if len(verts) == 0: + errmsg = PDT_ERR_NO_ACT_VERTS + self.report({"ERROR"}, errmsg) + return {"FINISHED"} + if oper == "CU": + if obj.mode == "EDIT": + scene.cursor.location = obj_loc + actV + vector_delta + elif obj.mode == "OBJECT": + scene.cursor.location = obj_loc + vector_delta + elif oper == "PP": + if obj.mode == "EDIT": + pg.pivot_loc = obj_loc + actV + vector_delta + elif obj.mode == "OBJECT": + pg.pivot_loc = obj_loc + vector_delta + elif oper == "MV": + if obj.mode == "EDIT": + bmesh.ops.translate(bm, verts=verts, vec=vector_delta) + bmesh.update_edit_mesh(obj.data) + bm.select_history.clear() + elif obj.mode == "OBJECT": + for ob in context.view_layer.objects.selected: + ob.location = obj_loc + vector_delta + elif oper == "SE" and obj.mode == "EDIT": + edges = [e for e in bm.edges if e.select] + faces = [f for f in bm.faces if f.select] + if len(faces) != 0: + errmsg = PDT_ERR_FACE_SEL + self.report({"ERROR"}, errmsg) + return {"FINISHED"} + if len(edges) < 1: + errmsg = f"{PDT_ERR_SEL_1_EDGE} {len(edges)})" + self.report({"ERROR"}, errmsg) + return {"FINISHED"} + geom = bmesh.ops.subdivide_edges(bm, edges=edges, cuts=1) + new_verts = [v for v in geom["geom_split"] if isinstance(v, bmesh.types.BMVert)] + bmesh.ops.translate(bm, verts=new_verts, vec=vector_delta) + for v in [v for v in bm.verts if v.select]: + v.select_set(False) + bmesh.update_edit_mesh(obj.data) + bm.select_history.clear() + elif oper == "NV": + if obj.mode == "EDIT": + vNew = actV + vector_delta + nVert = bm.verts.new(vNew) + bmesh.update_edit_mesh(obj.data) + bm.select_history.clear() + for v in [v for v in bm.verts if v.select]: + v.select_set(False) + nVert.select_set(True) + else: + errmsg = f"{PDT_ERR_EDIT_MODE} {obj.mode})" + self.report({"ERROR"}, errmsg) + return {"FINISHED"} + elif oper == "EV" and obj.mode == "EDIT": + for v in [v for v in bm.verts if v.select]: + nVert = bm.verts.new(v.co) + nVert.co = nVert.co + vector_delta + bm.edges.new([v, nVert]) + v.select_set(False) + nVert.select_set(True) + bmesh.update_edit_mesh(obj.data) + bm.select_history.clear() + elif oper == "DG" and obj.mode == "EDIT": + ret = bmesh.ops.duplicate( + bm, + geom=( + [f for f in bm.faces if f.select] + + [e for e in bm.edges if e.select] + + [v for v in bm.verts if v.select] + ), + use_select_history=True, + ) + geom_dupe = ret["geom"] + verts_dupe = [v for v in geom_dupe if isinstance(v, bmesh.types.BMVert)] + edges_dupe = [e for e in geom_dupe if isinstance(e, bmesh.types.BMEdge)] + faces_dupe = [f for f in geom_dupe if isinstance(f, bmesh.types.BMFace)] + del ret + bmesh.ops.translate(bm, verts=verts_dupe, vec=vector_delta) + updateSel(bm, verts_dupe, edges_dupe, faces_dupe) + bmesh.update_edit_mesh(obj.data) + bm.select_history.clear() + elif oper == "EG" and obj.mode == "EDIT": + ret = bmesh.ops.extrude_face_region( + bm, + geom=( + [f for f in bm.faces if f.select] + + [e for e in bm.edges if e.select] + + [v for v in bm.verts if v.select] + ), + use_select_history=True, + ) + geom_extr = ret["geom"] + verts_extr = [v for v in geom_extr if isinstance(v, bmesh.types.BMVert)] + edges_extr = [e for e in geom_extr if isinstance(e, bmesh.types.BMEdge)] + faces_extr = [f for f in geom_extr if isinstance(f, bmesh.types.BMFace)] + del ret + bmesh.ops.translate(bm, verts=verts_extr, vec=vector_delta) + updateSel(bm, verts_extr, edges_extr, faces_extr) + bmesh.update_edit_mesh(obj.data) + bm.select_history.clear() + else: + errmsg = f"{oper} {PDT_ERR_NON_VALID} {PDT_LAB_DEL}" + self.report({"ERROR"}, errmsg) + return {"FINISHED"} + + +class PDT_OT_PlacementDis(Operator): + """Use Directional, or Distance @ Angle Placement.""" + + bl_idname = "pdt.distance" + bl_label = "Distance@Angle Mode" + bl_options = {"REGISTER", "UNDO"} + + def execute(self, context): + """Manipulates Geometry, or Objects by Distance at Angle (Direction). + + - Reads pg.operation from Operation Mode Selector as 'oper' + - Reads pg.select, pg.distance, pg.angle, pg.plane & pg.flip_angle scene variables to: + -- set position of CUrsor (CU) + -- set position of Pivot Point (PP) + -- MoVe geometry/objects (MV) + -- Extrude Vertices (EV) + -- Split Edges (SE) + -- add a New Vertex (NV) + -- Duplicate Geometry (DG) + -- Extrude Geometry (EG) + + Invalid Options result in self.report Error. + + Local vector variable 'vector_delta' used to reposition features. + + Args: + context: Blender bpy.context instance. + + Returns: + Status Set. + """ + + scene = context.scene + pg = scene.pdt_pg + dis_v = pg.distance + ang_v = pg.angle + plane = pg.plane + mode_s = pg.select + oper = pg.operation + flip_a = pg.flip_angle + if flip_a: + if ang_v > 0: + ang_v = ang_v - 180 + else: + ang_v = ang_v + 180 + pg.angle = ang_v + if plane == "LO": + vector_delta = viewDir(dis_v, ang_v) + else: + a1, a2, _ = setMode(plane) + vector_delta = Vector((0, 0, 0)) + vector_delta[a1] = vector_delta[a1] + (dis_v * cos(ang_v * pi / 180)) + vector_delta[a2] = vector_delta[a2] + (dis_v * sin(ang_v * pi / 180)) + if mode_s == "REL" and oper == "CU": + scene.cursor.location = scene.cursor.location + vector_delta + elif mode_s == "REL" and oper == "PP": + pg.pivot_loc = pg.pivot_loc + vector_delta + else: + obj = context.view_layer.objects.active + if obj is None: + errmsg = PDT_ERR_NO_ACT_OBJ + self.report({"ERROR"}, errmsg) + return {"FINISHED"} + obj_loc = obj.matrix_world.decompose()[0] + if obj.mode == "EDIT": + bm = bmesh.from_edit_mesh(obj.data) + if oper not in {"MV", "SE", "EV", "DG", "EG"}: + if len(bm.select_history) >= 1: + actV = checkSelection(1, bm, obj) + if actV is None: + errmsg = PDT_ERR_VERT_MODE + self.report({"ERROR"}, errmsg) + return {"FINISHED"} + else: + errmsg = f"{PDT_ERR_SEL_1_VERTI} {len(bm.select_history)})" + self.report({"ERROR"}, errmsg) + return {"FINISHED"} + if oper not in {"CU", "PP", "NV"} and obj.mode == "EDIT": + verts = [v for v in bm.verts if v.select] + if len(verts) == 0: + errmsg = PDT_ERR_NO_ACT_VERTS + self.report({"ERROR"}, errmsg) + return {"FINISHED"} + if oper == "CU": + if obj.mode == "EDIT": + scene.cursor.location = obj_loc + actV + vector_delta + elif obj.mode == "OBJECT": + scene.cursor.location = obj_loc + vector_delta + elif oper == "PP": + if obj.mode == "EDIT": + pg.pivot_loc = obj_loc + actV + vector_delta + elif obj.mode == "OBJECT": + pg.pivot_loc = obj_loc + vector_delta + elif oper == "MV": + if obj.mode == "EDIT": + bmesh.ops.translate(bm, verts=verts, vec=vector_delta) + bmesh.update_edit_mesh(obj.data) + bm.select_history.clear() + elif obj.mode == "OBJECT": + for ob in context.view_layer.objects.selected: + ob.location = ob.location + vector_delta + elif oper == "SE" and obj.mode == "EDIT": + edges = [e for e in bm.edges if e.select] + faces = [f for f in bm.faces if f.select] + if len(faces) != 0: + errmsg = PDT_ERR_FACE_SEL + self.report({"ERROR"}, errmsg) + return {"FINISHED"} + if len(edges) < 1: + errmsg = f"{PDT_ERR_SEL_1_EDGE} {len(edges)})" + self.report({"ERROR"}, errmsg) + return {"FINISHED"} + geom = bmesh.ops.subdivide_edges(bm, edges=edges, cuts=1) + new_verts = [v for v in geom["geom_split"] if isinstance(v, bmesh.types.BMVert)] + bmesh.ops.translate(bm, verts=new_verts, vec=vector_delta) + for v in [v for v in bm.verts if v.select]: + v.select_set(False) + bmesh.update_edit_mesh(obj.data) + bm.select_history.clear() + elif oper == "NV": + if obj.mode == "EDIT": + vNew = actV + vector_delta + nVert = bm.verts.new(vNew) + bmesh.update_edit_mesh(obj.data) + bm.select_history.clear() + for v in [v for v in bm.verts if v.select]: + v.select_set(False) + nVert.select_set(True) + else: + errmsg = f"{PDT_ERR_EDIT_MODE} {obj.mode})" + self.report({"ERROR"}, errmsg) + return {"FINISHED"} + elif oper == "EV" and obj.mode == "EDIT": + for v in [v for v in bm.verts if v.select]: + nVert = bm.verts.new(v.co) + nVert.co = nVert.co + vector_delta + bm.edges.new([v, nVert]) + v.select_set(False) + nVert.select_set(True) + bmesh.update_edit_mesh(obj.data) + bm.select_history.clear() + elif oper == "DG" and obj.mode == "EDIT": + ret = bmesh.ops.duplicate( + bm, + geom=( + [f for f in bm.faces if f.select] + + [e for e in bm.edges if e.select] + + [v for v in bm.verts if v.select] + ), + use_select_history=True, + ) + geom_dupe = ret["geom"] + verts_dupe = [v for v in geom_dupe if isinstance(v, bmesh.types.BMVert)] + edges_dupe = [e for e in geom_dupe if isinstance(e, bmesh.types.BMEdge)] + faces_dupe = [f for f in geom_dupe if isinstance(f, bmesh.types.BMFace)] + del ret + bmesh.ops.translate(bm, verts=verts_dupe, vec=vector_delta) + updateSel(bm, verts_dupe, edges_dupe, faces_dupe) + bmesh.update_edit_mesh(obj.data) + bm.select_history.clear() + elif oper == "EG" and obj.mode == "EDIT": + ret = bmesh.ops.extrude_face_region( + bm, + geom=( + [f for f in bm.faces if f.select] + + [e for e in bm.edges if e.select] + + [v for v in bm.verts if v.select] + ), + use_select_history=True, + ) + geom_extr = ret["geom"] + verts_extr = [v for v in geom_extr if isinstance(v, bmesh.types.BMVert)] + edges_extr = [e for e in geom_extr if isinstance(e, bmesh.types.BMEdge)] + faces_extr = [f for f in geom_extr if isinstance(f, bmesh.types.BMFace)] + del ret + bmesh.ops.translate(bm, verts=verts_extr, vec=vector_delta) + updateSel(bm, verts_extr, edges_extr, faces_extr) + bmesh.update_edit_mesh(obj.data) + bm.select_history.clear() + else: + errmsg = f"{oper} {PDT_ERR_NON_VALID} {PDT_LAB_DIR}" + self.report({"ERROR"}, errmsg) + return {"FINISHED"} + + +class PDT_OT_PlacementPer(Operator): + """Use Percentage Placement.""" + + bl_idname = "pdt.percent" + bl_label = "Percentage Mode" + bl_options = {"REGISTER", "UNDO"} + + + def execute(self, context): + """Manipulates Geometry, or Objects by Percentage between 2 points. + + - Reads pg.operation from Operation Mode Selector as 'oper' + - Reads pg.percent, pg.extend & pg.flip_percent scene variables to: + -- set position of CUrsor (CU) + -- set position of Pivot Point (PP) + -- MoVe geometry/objects (MV) + -- Extrude Vertices (EV) + -- Split edges (SE) + -- add a New vertex (NV) + + Invalid Options result in self.report Error. + + Local vector variable 'vector_delta' used to reposition features. + + Args: + context: Blender bpy.context instance. + + Returns: + Status Set. + """ + + scene = context.scene + pg = scene.pdt_pg + per_v = pg.percent + oper = pg.operation + ext_a = pg.extend + flip_p = pg.flip_percent + obj = context.view_layer.objects.active + if obj is None: + errmsg = PDT_ERR_NO_ACT_OBJ + self.report({"ERROR"}, errmsg) + return {"FINISHED"} + if obj.mode == "EDIT": + bm = bmesh.from_edit_mesh(obj.data) + obj_loc = obj.matrix_world.decompose()[0] + vector_delta = getPercent(obj, flip_p, per_v, oper, scene) + if vector_delta is None: + return {"FINISHED"} + + if oper == "CU": + if obj.mode == "EDIT": + scene.cursor.location = obj_loc + vector_delta + elif obj.mode == "OBJECT": + scene.cursor.location = vector_delta + elif oper == "PP": + if obj.mode == "EDIT": + pg.pivot_loc = obj_loc + vector_delta + elif obj.mode == "OBJECT": + pg.pivot_loc = vector_delta + elif oper == "MV": + if obj.mode == "EDIT": + bm.select_history[-1].co = vector_delta + bmesh.update_edit_mesh(obj.data) + bm.select_history.clear() + elif obj.mode == "OBJECT": + obj.location = vector_delta + elif oper == "SE" and obj.mode == "EDIT": + edges = [e for e in bm.edges if e.select] + if len(edges) != 1: + errmsg = f"{PDT_ERR_SEL_1_EDGE} {len(edges)})" + self.report({"ERROR"}, errmsg) + return {"FINISHED"} + geom = bmesh.ops.subdivide_edges(bm, edges=edges, cuts=1) + new_verts = [v for v in geom["geom_split"] if isinstance(v, bmesh.types.BMVert)] + nVert = new_verts[0] + nVert.co = vector_delta + for v in [v for v in bm.verts if v.select]: + v.select_set(False) + nVert.select_set(True) + bmesh.update_edit_mesh(obj.data) + bm.select_history.clear() + elif oper == "NV": + if obj.mode == "EDIT": + nVert = bm.verts.new(vector_delta) + bmesh.update_edit_mesh(obj.data) + bm.select_history.clear() + for v in [v for v in bm.verts if v.select]: + v.select_set(False) + nVert.select_set(True) + else: + errmsg = f"{PDT_ERR_EDIT_MODE} {obj.mode})" + self.report({"ERROR"}, errmsg) + return {"FINISHED"} + elif oper == "EV" and obj.mode == "EDIT": + nVert = bm.verts.new(vector_delta) + if ext_a: + for v in [v for v in bm.verts if v.select]: + bm.edges.new([v, nVert]) + v.select_set(False) + else: + bm.edges.new([bm.select_history[-1], nVert]) + nVert.select_set(True) + bmesh.update_edit_mesh(obj.data) + bm.select_history.clear() + else: + errmsg = f"{oper} {PDT_ERR_NON_VALID} {PDT_LAB_PERCENT}" + self.report({"ERROR"}, errmsg) + return {"FINISHED"} + + +class PDT_OT_PlacementNormal(Operator): + """Use Normal, or Perpendicular Placement.""" + + bl_idname = "pdt.normal" + bl_label = "Normal Mode" + bl_options = {"REGISTER", "UNDO"} + + + def execute(self, context): + """Manipulates Geometry, or Objects by Normal Intersection between 3 points. + + - Reads pg.operation from Operation Mode Selector as 'oper' + - Reads pg.extend scene variable to: + -- set position of CUrsor (CU) + -- set position of Pivot Point (PP) + -- MoVe geometry/objects (MV) + -- Extrude Vertices (EV) + -- Split Edges (SE) + -- add a New Vertex (NV) + + Invalid Options result in self.report Error. + + Local vector variable 'vector_delta' used to reposition features. + + Args: + context: Blender bpy.context instance. + + Returns: + Status Set. + """ + + scene = context.scene + pg = scene.pdt_pg + oper = pg.operation + ext_a = pg.extend + obj = context.view_layer.objects.active + if obj is None: + errmsg = PDT_ERR_NO_ACT_OBJ + self.report({"ERROR"}, errmsg) + return {"FINISHED"} + obj_loc = obj.matrix_world.decompose()[0] + if obj.mode == "EDIT": + bm = bmesh.from_edit_mesh(obj.data) + if len(bm.select_history) == 3: + actV, othV, lstV = checkSelection(3, bm, obj) + if actV is None: + errmsg = PDT_ERR_VERT_MODE + self.report({"ERROR"}, errmsg) + return {"FINISHED"} + else: + errmsg = f"{PDT_ERR_SEL_3_VERTS} {len(bm.select_history)})" + self.report({"ERROR"}, errmsg) + return {"FINISHED"} + elif obj.mode == "OBJECT": + objs = context.view_layer.objects.selected + if len(objs) != 3: + errmsg = f"{PDT_ERR_SEL_3_OBJS} {len(objs)})" + self.report({"ERROR"}, errmsg) + return {"FINISHED"} + else: + objs_s = [ob for ob in objs if ob.name != obj.name] + actV = obj.matrix_world.decompose()[0] + othV = objs_s[-1].matrix_world.decompose()[0] + lstV = objs_s[-2].matrix_world.decompose()[0] + vector_delta = intersect_point_line(actV, othV, lstV)[0] + if oper == "CU": + if obj.mode == "EDIT": + scene.cursor.location = obj_loc + vector_delta + elif obj.mode == "OBJECT": + scene.cursor.location = vector_delta + elif oper == "PP": + if obj.mode == "EDIT": + pg.pivot_loc = obj_loc + vector_delta + elif obj.mode == "OBJECT": + pg.pivot_loc = vector_delta + elif oper == "MV": + if obj.mode == "EDIT": + if ext_a: + for v in [v for v in bm.verts if v.select]: + v.co = vector_delta + bm.select_history.clear() + bmesh.ops.remove_doubles( + bm, verts=[v for v in bm.verts if v.select], dist=0.0001 + ) + else: + bm.select_history[-1].co = vector_delta + bm.select_history.clear() + bmesh.update_edit_mesh(obj.data) + elif obj.mode == "OBJECT": + context.view_layer.objects.active.location = vector_delta + elif oper == "NV": + if obj.mode == "EDIT": + nVert = bm.verts.new(vector_delta) + bmesh.update_edit_mesh(obj.data) + bm.select_history.clear() + for v in [v for v in bm.verts if v.select]: + v.select_set(False) + nVert.select_set(True) + else: + errmsg = f"{PDT_ERR_EDIT_MODE} {obj.mode})" + self.report({"ERROR"}, errmsg) + return {"FINISHED"} + elif oper == "EV" and obj.mode == "EDIT": + vNew = vector_delta + nVert = bm.verts.new(vNew) + if ext_a: + for v in [v for v in bm.verts if v.select]: + bm.edges.new([v, nVert]) + else: + bm.edges.new([bm.select_history[-1], nVert]) + for v in [v for v in bm.verts if v.select]: + v.select_set(False) + nVert.select_set(True) + bmesh.update_edit_mesh(obj.data) + bm.select_history.clear() + else: + errmsg = f"{oper} {PDT_ERR_NON_VALID} {PDT_LAB_NOR}" + self.report({"ERROR"}, errmsg) + return {"FINISHED"} + + +class PDT_OT_PlacementInt(Operator): + """Use Intersection, or Convergence Placement.""" + + bl_idname = "pdt.intersect" + bl_label = "Intersect Mode" + bl_options = {"REGISTER", "UNDO"} + + def execute(self, context): + """Manipulates Geometry, or Objects by Convergance Intersection between 4 points, or 2 Edges. + + - Reads pg.operation from Operation Mode Selector as 'oper' + - Reads pg.plane scene variable and operates in Working Plane to: + -- set position of CUrsor (CU) + -- set position of Pivot Point (PP) + -- MoVe geometry/objects (MV) + -- Extrude Vertices (EV) + -- add a New vertex (NV) + + Invalid Options result in self.report Error. + + Local vector variable 'vector_delta' used to reposition features. + + Args: + context: Blender bpy.context instance. + + Returns: + Status Set. + """ + + scene = context.scene + pg = scene.pdt_pg + oper = pg.operation + plane = pg.plane + obj = context.view_layer.objects.active + if obj is None: + errmsg = PDT_ERR_NO_ACT_OBJ + self.report({"ERROR"}, errmsg) + return {"FINISHED"} + if obj.mode == "EDIT": + obj_loc = obj.matrix_world.decompose()[0] + bm = bmesh.from_edit_mesh(obj.data) + edges = [e for e in bm.edges if e.select] + if len(edges) == 2: + ext_a = True + va = edges[0].verts[0] + actV = va.co + vo = edges[0].verts[1] + othV = vo.co + vl = edges[1].verts[0] + lstV = vl.co + vf = edges[1].verts[1] + fstV = vf.co + elif len(bm.select_history) == 4: + ext_a = pg.extend + va = bm.select_history[-1] + vo = bm.select_history[-2] + vl = bm.select_history[-3] + vf = bm.select_history[-4] + actV, othV, lstV, fstV = checkSelection(4, bm, obj) + if actV is None: + errmsg = PDT_ERR_VERT_MODE + self.report({"ERROR"}, errmsg) + return {"FINISHED"} + else: + errmsg = ( + PDT_ERR_SEL_4_VERTS + + str(len(bm.select_history)) + + " Vertices/" + + str(len(edges)) + + " Edges)" + ) + self.report({"ERROR"}, errmsg) + return {"FINISHED"} + vector_delta, done = intersection(actV, othV, lstV, fstV, plane) + if not done: + errmsg = f"{PDT_ERR_INT_LINES} {plane} {PDT_LAB_PLANE}" + self.report({"ERROR"}, errmsg) + return {"FINISHED"} + + if oper == "CU": + scene.cursor.location = obj_loc + vector_delta + elif oper == "PP": + pg.pivot_loc = obj_loc + vector_delta + elif oper == "NV": + vNew = vector_delta + nVert = bm.verts.new(vNew) + for v in [v for v in bm.verts if v.select]: + v.select_set(False) + for f in bm.faces: + f.select_set(False) + for e in bm.edges: + e.select_set(False) + nVert.select_set(True) + bmesh.update_edit_mesh(obj.data) + bm.select_history.clear() + elif oper in {"MV", "EV"}: + nVert = None + proc = False + + if (actV - vector_delta).length < (othV - vector_delta).length: + if oper == "MV": + va.co = vector_delta + proc = True + elif oper == "EV": + nVert = bm.verts.new(vector_delta) + bm.edges.new([va, nVert]) + proc = True + else: + if oper == "MV" and ext_a: + vo.co = vector_delta + elif oper == "EV" and ext_a: + nVert = bm.verts.new(vector_delta) + bm.edges.new([vo, nVert]) + + if (lstV - vector_delta).length < (fstV - vector_delta).length: + if oper == "MV" and ext_a: + vl.co = vector_delta + elif oper == "EV" and ext_a: + bm.edges.new([vl, nVert]) + else: + if oper == "MV" and ext_a: + vf.co = vector_delta + elif oper == "EV" and ext_a: + bm.edges.new([vf, nVert]) + bm.select_history.clear() + bmesh.ops.remove_doubles(bm, verts=bm.verts, dist=0.0001) + + if not proc and not ext_a: + errmsg = PDT_ERR_INT_NO_ALL + self.report({"ERROR"}, errmsg) + bmesh.update_edit_mesh(obj.data) + return {"FINISHED"} + else: + for v in bm.verts: + v.select_set(False) + for f in bm.faces: + f.select_set(False) + for e in bm.edges: + e.select_set(False) + + if nVert is not None: + nVert.select_set(True) + for v in bm.select_history: + if v is not None: + v.select_set(True) + bmesh.update_edit_mesh(obj.data) + else: + errmsg = f"{oper} {PDT_ERR_NON_VALID} {PDT_LAB_INTERSECT}" + self.report({"ERROR"}, errmsg) + return {"FINISHED"} + elif obj.mode == "OBJECT": + if len(context.view_layer.objects.selected) != 4: + errmsg = f"{PDT_ERR_SEL_4_OBJS} {len(context.view_layer.objects.selected)})" + self.report({"ERROR"}, errmsg) + return {"FINISHED"} + else: + order = pg.object_order.split(",") + objs = sorted( + [ob for ob in context.view_layer.objects.selected], key=lambda x: x.name + ) + message = ( + "Original Object Order was: " + + objs[0].name + + ", " + + objs[1].name + + ", " + + objs[2].name + + ", " + + objs[3].name + ) + self.report({"INFO"}, message) + + actV = objs[int(order[0]) - 1].matrix_world.decompose()[0] + othV = objs[int(order[1]) - 1].matrix_world.decompose()[0] + lstV = objs[int(order[2]) - 1].matrix_world.decompose()[0] + fstV = objs[int(order[3]) - 1].matrix_world.decompose()[0] + vector_delta, done = intersection(actV, othV, lstV, fstV, plane) + if not done: + errmsg = f"{PDT_ERR_INT_LINES} {plane} {PDT_LAB_PLANE}" + self.report({"ERROR"}, errmsg) + return {"FINISHED"} + if oper == "CU": + scene.cursor.location = vector_delta + elif oper == "PP": + pg.pivot_loc = vector_delta + elif oper == "MV": + context.view_layer.objects.active.location = vector_delta + infmsg = PDT_INF_OBJ_MOVED + message + self.report({"INFO"}, infmsg) + else: + errmsg = f"{oper} {PDT_ERR_NON_VALID} {PDT_LAB_INTERSECT}" + self.report({"ERROR"}, errmsg) + return {"FINISHED"} + + +class PDT_OT_PlacementCen(Operator): + """Use Placement at Arc Centre.""" + + bl_idname = "pdt.centre" + bl_label = "Centre Mode" + bl_options = {"REGISTER", "UNDO"} + + def execute(self, context): + """Manipulates Geometry, or Objects to an Arc Centre defined by 3 points on an Imaginary Arc. + + Valid Options for pg.operation; CU PP MV NV EV + - Reads pg.operation from Operation Mode Selector as 'oper' + - Reads pg.extend scene variable to: + -- set position of CUrsor (CU) + -- set position of Pivot Point (PP) + -- MoVe geometry/objects (MV) + -- Extrude Vertices (EV) + -- add a New vertex (NV) + + Invalid Options result in self.report Error. + + Local vector variable 'vector_delta' used to reposition features. + + Args: + context: Blender bpy.context instance. + + Returns: + Status Set. + """ + + scene = context.scene + pg = scene.pdt_pg + oper = pg.operation + ext_a = pg.extend + obj = context.view_layer.objects.active + + if obj is None: + errmsg = PDT_ERR_NO_ACT_OBJ + self.report({"ERROR"}, errmsg) + return {"FINISHED"} + if obj.mode == "EDIT": + obj = context.view_layer.objects.active + obj_loc = obj.matrix_world.decompose()[0] + bm = bmesh.from_edit_mesh(obj.data) + verts = [v for v in bm.verts if v.select] + if len(verts) == 3: + actV = verts[0].co + othV = verts[1].co + lstV = verts[2].co + else: + errmsg = f"{PDT_ERR_SEL_3_VERTS} {len(verts)})" + self.report({"ERROR"}, errmsg) + return {"FINISHED"} + vector_delta, radius = arcCentre(actV, othV, lstV) + if str(radius) == "inf": + errmsg = PDT_ERR_STRIGHT_LINE + self.report({"ERROR"}, errmsg) + return {"FINISHED"} + pg.distance = radius + if oper == "CU": + scene.cursor.location = obj_loc + vector_delta + elif oper == "PP": + pg.pivot_loc = obj_loc + vector_delta + elif oper == "NV": + vNew = vector_delta + nVert = bm.verts.new(vNew) + for v in [v for v in bm.verts if v.select]: + v.select_set(False) + nVert.select_set(True) + bmesh.update_edit_mesh(obj.data) + bm.select_history.clear() + nVert.select_set(True) + elif oper == "MV": + if obj.mode == "EDIT": + if ext_a: + for v in [v for v in bm.verts if v.select]: + v.co = vector_delta + bm.select_history.clear() + bmesh.ops.remove_doubles( + bm, verts=[v for v in bm.verts if v.select], dist=0.0001 + ) + else: + bm.select_history[-1].co = vector_delta + bm.select_history.clear() + bmesh.update_edit_mesh(obj.data) + elif obj.mode == "OBJECT": + context.view_layer.objects.active.location = vector_delta + elif oper == "EV": + nVert = bm.verts.new(vector_delta) + if ext_a: + for v in [v for v in bm.verts if v.select]: + bm.edges.new([v, nVert]) + v.select_set(False) + nVert.select_set(True) + bm.select_history.clear() + bmesh.ops.remove_doubles( + bm, verts=[v for v in bm.verts if v.select], dist=0.0001 + ) + bmesh.update_edit_mesh(obj.data) + else: + bm.edges.new([bm.select_history[-1], nVert]) + bmesh.update_edit_mesh(obj.data) + bm.select_history.clear() + else: + errmsg = f"{oper} {PDT_ERR_NON_VALID} {PDT_LAB_ARCCENTRE}" + self.report({"ERROR"}, errmsg) + return {"FINISHED"} + elif obj.mode == "OBJECT": + if len(context.view_layer.objects.selected) != 3: + errmsg = f"{PDT_ERR_SEL_3_OBJS} {len(context.view_layer.objects.selected)})" + self.report({"ERROR"}, errmsg) + return {"FINISHED"} + else: + actV = context.view_layer.objects.selected[0].matrix_world.decompose()[0] + othV = context.view_layer.objects.selected[1].matrix_world.decompose()[0] + lstV = context.view_layer.objects.selected[2].matrix_world.decompose()[0] + vector_delta, radius = arcCentre(actV, othV, lstV) + pg.distance = radius + if oper == "CU": + scene.cursor.location = vector_delta + elif oper == "PP": + pg.pivot_loc = vector_delta + elif oper == "MV": + context.view_layer.objects.active.location = vector_delta + else: + errmsg = f"{oper} {PDT_ERR_NON_VALID} {PDT_LAB_ARCCENTRE}" + self.report({"ERROR"}, errmsg) + return {"FINISHED"} + + +class PDT_OT_JoinVerts(Operator): + """Join 2 Free Vertices into an Edge.""" + + bl_idname = "pdt.join" + bl_label = "Join 2 Vertices" + bl_options = {"REGISTER", "UNDO"} + + @classmethod + def poll(cls, context): + ob = context.object + if ob is None: + return False + return all([bool(ob), ob.type == "MESH", ob.mode == "EDIT"]) + + def execute(self, context): + """Joins 2 Free Vertices that do not form part of a Face. + + Joins two vertices that do not form part of a single face + It is designed to close open Edge Loops, where a face is not required + or to join two disconnected Edges. + + Args: + context: Blender bpy.context instance. + + Returns: + Status Set. + """ + + obj = context.view_layer.objects.active + bm = bmesh.from_edit_mesh(obj.data) + verts = [v for v in bm.verts if v.select] + if len(verts) == 2: + try: + bm.edges.new([verts[-1], verts[-2]]) + bmesh.update_edit_mesh(obj.data) + bm.select_history.clear() + return {"FINISHED"} + except ValueError: + errmsg = PDT_ERR_CONNECTED + self.report({"ERROR"}, errmsg) + return {"FINISHED"} + else: + errmsg = f"{PDT_ERR_SEL_2_VERTS} {len(verts)})" + self.report({"ERROR"}, errmsg) + return {"FINISHED"} + + +class PDT_OT_Fillet(Operator): + """Fillet Edges by Vertex, Set Use Verts to False for Extruded Structure.""" + + bl_idname = "pdt.fillet" + bl_label = "Fillet" + bl_options = {"REGISTER", "UNDO"} + + @classmethod + def poll(cls, context): + ob = context.object + if ob is None: + return False + return all([bool(ob), ob.type == "MESH", ob.mode == "EDIT"]) + + def execute(self, context): + """Create Fillets by Vertex or by Geometry. + + Fillets connected edges, or connected faces + Uses: + - pg.fillet_radius ; Radius of fillet + - pg.fillet_segments ; Number of segments + - pg.fillet_profile ; Profile, values 0 to 1 + - pg.fillet_vertices_only ; Vertices (True), or Face/Edges + + Args: + context: Blender bpy.context instance. + + Returns: + Status Set. + """ + + scene = context.scene + pg = scene.pdt_pg + obj = context.view_layer.objects.active + bm = bmesh.from_edit_mesh(obj.data) + verts = [v for v in bm.verts if v.select] + if len(verts) == 0: + errmsg = PDT_ERR_SEL_1_VERT + self.report({"ERROR"}, errmsg) + return {"FINISHED"} + else: + bpy.ops.mesh.bevel( + offset_type="OFFSET", + offset=pg.fillet_radius, + segments=pg.fillet_segments, + profile=pg.fillet_profile, + vertex_only=pg.fillet_vertices_only, + ) + return {"FINISHED"} + + +class PDT_OT_Angle2(Operator): + """Measure Distance and Angle in Working Plane, Also sets Deltas.""" + + bl_idname = "pdt.angle2" + bl_label = "Measure 2D" + bl_options = {"REGISTER", "UNDO"} + + def execute(self, context): + """Measures Angle and Offsets between 2 Points in View Plane. + + Uses 2 Selected Vertices to set pg.angle and pg.distance scene variables + also sets delta offset from these 2 points using standard Numpy Routines + Works in Edit and Oject Modes. + + Args: + context: Blender bpy.context instance. + + Returns: + Status Set. + """ + + scene = context.scene + pg = scene.pdt_pg + plane = pg.plane + flip_a = pg.flip_angle + obj = context.view_layer.objects.active + if obj is None: + errmsg = PDT_ERR_NO_ACT_OBJ + self.report({"ERROR"}, errmsg) + return {"FINISHED"} + if obj.mode == "EDIT": + bm = bmesh.from_edit_mesh(obj.data) + verts = [v for v in bm.verts if v.select] + if len(verts) == 2: + if len(bm.select_history) == 2: + actV, othV = checkSelection(2, bm, obj) + if actV is None: + errmsg = PDT_ERR_VERT_MODE + self.report({"ERROR"}, errmsg) + return {"FINISHED"} + else: + errmsg = f"{PDT_ERR_SEL_2_VERTIO} {len(bm.select_history)})" + self.report({"ERROR"}, errmsg) + return {"FINISHED"} + else: + errmsg = f"{PDT_ERR_SEL_2_VERTIO} {len(verts)})" + self.report({"ERROR"}, errmsg) + return {"FINISHED"} + elif obj.mode == "OBJECT": + objs = context.view_layer.objects.selected + if len(objs) < 2: + errmsg = f"{PDT_ERR_SEL_2_OBJS} {len(objs)})" + self.report({"ERROR"}, errmsg) + return {"FINISHED"} + objs_s = [ob for ob in objs if ob.name != obj.name] + actV = obj.matrix_world.decompose()[0] + othV = objs_s[-1].matrix_world.decompose()[0] + if plane == "LO": + disV = othV - actV + othV = viewCoordsI(disV.x, disV.y, disV.z) + actV = Vector((0, 0, 0)) + v0 = np.array([actV.x + 1, actV.y]) - np.array([actV.x, actV.y]) + v1 = np.array([othV.x, othV.y]) - np.array([actV.x, actV.y]) + else: + a1, a2, _ = setMode(plane) + v0 = np.array([actV[a1] + 1, actV[a2]]) - np.array([actV[a1], actV[a2]]) + v1 = np.array([othV[a1], othV[a2]]) - np.array([actV[a1], actV[a2]]) + ang = np.rad2deg(np.arctan2(np.linalg.det([v0, v1]), np.dot(v0, v1))) + if flip_a: + if ang > 0: + pg.angle = ang - 180 + else: + pg.angle = ang + 180 + else: + pg.angle = ang + if plane == "LO": + pg.distance = sqrt((actV.x - othV.x) ** 2 + (actV.y - othV.y) ** 2) + else: + pg.distance = sqrt((actV[a1] - othV[a1]) ** 2 + (actV[a2] - othV[a2]) ** 2) + pg.cartesian_coords = othV - actV + return {"FINISHED"} + + +class PDT_OT_Angle3(Operator): + """Measure Distance and Angle in 3D Space.""" + + bl_idname = "pdt.angle3" + bl_label = "Measure 3D" + bl_options = {"REGISTER", "UNDO"} + + def execute(self, context): + """Measures Angle and Offsets between 3 Points in World Space, Also sets Deltas. + + Uses 3 Selected Vertices to set pg.angle and pg.distance scene variables + also sets delta offset from these 3 points using standard Numpy Routines + Works in Edit and Oject Modes. + + Args: + context: Blender bpy.context instance. + + Returns: + Status Set. + """ + + pg = context.scene.pdt_pg + flip_a = pg.flip_angle + obj = context.view_layer.objects.active + if obj is None: + errmsg = PDT_ERR_NO_ACT_OBJ + self.report({"ERROR"}, errmsg) + return {"FINISHED"} + if obj.mode == "EDIT": + bm = bmesh.from_edit_mesh(obj.data) + verts = [v for v in bm.verts if v.select] + if len(verts) == 3: + if len(bm.select_history) == 3: + actV, othV, lstV = checkSelection(3, bm, obj) + if actV is None: + errmsg = PDT_ERR_VERT_MODE + self.report({"ERROR"}, errmsg) + return {"FINISHED"} + else: + errmsg = f"{PDT_ERR_SEL_3_VERTIO} {len(bm.select_history)})" + self.report({"ERROR"}, errmsg) + return {"FINISHED"} + else: + errmsg = f"{PDT_ERR_SEL_3_VERTIO} {len(verts)})" + self.report({"ERROR"}, errmsg) + return {"FINISHED"} + elif obj.mode == "OBJECT": + objs = context.view_layer.objects.selected + if len(objs) < 3: + errmsg = PDT_ERR_SEL_3_OBJS + str(len(objs)) + self.report({"ERROR"}, errmsg) + return {"FINISHED"} + objs_s = [ob for ob in objs if ob.name != obj.name] + actV = obj.matrix_world.decompose()[0] + othV = objs_s[-1].matrix_world.decompose()[0] + lstV = objs_s[-2].matrix_world.decompose()[0] + ba = np.array([othV.x, othV.y, othV.z]) - np.array([actV.x, actV.y, actV.z]) + bc = np.array([lstV.x, lstV.y, lstV.z]) - np.array([actV.x, actV.y, actV.z]) + cosA = np.dot(ba, bc) / (np.linalg.norm(ba) * np.linalg.norm(bc)) + ang = np.degrees(np.arccos(cosA)) + if flip_a: + if ang > 0: + pg.angle = ang - 180 + else: + pg.angle = ang + 180 + else: + pg.angle = ang + pg.distance = (actV - othV).length + pg.cartesian_coords = othV - actV + return {"FINISHED"} + + +class PDT_OT_Origin(Operator): + """Move Object Origin to Cursor Location.""" + + bl_idname = "pdt.origin" + bl_label = "Move Origin" + bl_options = {"REGISTER", "UNDO"} + + def execute(self, context): + """Sets Object Origin in Edit Mode to Cursor Location. + + Keeps geometry static in World Space whilst moving Object Origin + Requires cursor location + Works in Edit and Object Modes. + + Args: + context: Blender bpy.context instance. + + Returns: + Status Set. + """ + + scene = context.scene + obj = context.view_layer.objects.active + if obj is None: + errmsg = PDT_ERR_NO_ACT_OBJ + self.report({"ERROR"}, errmsg) + return {"FINISHED"} + obj_loc = obj.matrix_world.decompose()[0] + cur_loc = scene.cursor.location + diff_v = obj_loc - cur_loc + if obj.mode == "EDIT": + bm = bmesh.from_edit_mesh(obj.data) + for v in bm.verts: + v.co = v.co + diff_v + obj.location = cur_loc + bmesh.update_edit_mesh(obj.data) + bm.select_history.clear() + elif obj.mode == "OBJECT": + for v in obj.data.vertices: + v.co = v.co + diff_v + obj.location = cur_loc + else: + errmsg = f"{PDT_ERR_EDOB_MODE} {obj.mode})" + self.report({"ERROR"}, errmsg) + return {"FINISHED"} + return {"FINISHED"} + + +class PDT_OT_Taper(Operator): + """Taper Vertices at Angle in Chosen Axis Mode.""" + + bl_idname = "pdt.taper" + bl_label = "Taper" + bl_options = {"REGISTER", "UNDO"} + + + @classmethod + def poll(cls, context): + ob = context.object + if ob is None: + return False + return all([bool(ob), ob.type == "MESH", ob.mode == "EDIT"]) + + + def execute(self, context): + """Taper Geometry along World Axes. + + Similar to Shear command except that it shears by angle rather than displacement. + Rotates about World Axes and displaces along World Axes, angle must not exceed +-80 degrees. + Rotation axis is centred on Active Vertex. + Works only in Edit mode. + + Args: + context: Blender bpy.context instance. + + Note: + Uses pg.taper & pg.angle scene variables + + Returns: + Status Set. + """ + + scene = context.scene + pg = scene.pdt_pg + tap_ax = pg.taper + ang_v = pg.angle + obj = context.view_layer.objects.active + if ang_v > 80 or ang_v < -80: + errmsg = f"{PDT_ERR_TAPER_ANG} {ang_v})" + self.report({"ERROR"}, errmsg) + return {"FINISHED"} + if obj is None: + errmsg = PDT_ERR_NO_ACT_OBJ + self.report({"ERROR"}, errmsg) + return {"FINISHED"} + _, a2, a3 = setAxis(tap_ax) + bm = bmesh.from_edit_mesh(obj.data) + if len(bm.select_history) >= 1: + rotV = bm.select_history[-1] + viewV = viewCoords(rotV.co.x, rotV.co.y, rotV.co.z) + else: + errmsg = f"{PDT_ERR_TAPER_SEL} {len(bm.select_history)})" + self.report({"ERROR"}, errmsg) + return {"FINISHED"} + for v in [v for v in bm.verts if v.select]: + if pg.plane == "LO": + v_loc = viewCoords(v.co.x, v.co.y, v.co.z) + dis_v = sqrt((viewV.x - v_loc.x) ** 2 + (viewV.y - v_loc.y) ** 2) + x_loc = dis_v * tan(ang_v * pi / 180) + vm = viewDir(x_loc, 0) + v.co = v.co - vm + else: + dis_v = sqrt((rotV.co[a3] - v.co[a3]) ** 2 + (rotV.co[a2] - v.co[a2]) ** 2) + v.co[a2] = v.co[a2] - (dis_v * tan(ang_v * pi / 180)) + bmesh.update_edit_mesh(obj.data) + bm.select_history.clear() + return {"FINISHED"} diff --git a/precision_drawing_tools/pdt_etof.py b/precision_drawing_tools/pdt_etof.py new file mode 100644 index 00000000..5c2fbc43 --- /dev/null +++ b/precision_drawing_tools/pdt_etof.py @@ -0,0 +1,132 @@ +# ##### BEGIN GPL LICENSE BLOCK ##### +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software Foundation, +# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# ##### END GPL LICENSE BLOCK ##### +# +# <pep8 compliant> +# +# ---------------------------------------------------------- +# Author: Zeffii +# Modified by: Alan Odom (Clockmender) & Rune Morling (ermo) +# ---------------------------------------------------------- +# +# +import bpy +import bmesh +from mathutils.geometry import intersect_line_plane +from .pdt_msg_strings import ( + PDT_ERR_NOINT, + PDT_ERR_SEL_1_E_1_F +) + + +def failure_message(self): + """Warn to the user to select 1 edge and 1 face.""" + self.report({"WARNING"}, PDT_ERR_SEL_1_E_1_F) + + +def failure_message_on_plane(self): + """Report an informative error message in a popup.""" + msg2 = """\ +Edge2Face expects the edge to intersect at one point on the plane of the selected face. You're +seeing this warning because mathutils.geometry.intersect_line_plane is being called on an edge/face +combination that has no clear intersection point ( both points of the edge either touch the same +plane as the face or they lie in a plane that is offset along the face's normal )""" + lines = msg2.split("\n") + for line in lines: + self.report({"INFO"}, line) + self.report({"ERROR"}, PDT_ERR_NOINT) + + +def extend_vertex(self): + """Computes Edge Extension to Face. + + Args: + None + + Returns: + Nothing.""" + + obj = bpy.context.edit_object + me = obj.data + bm = bmesh.from_edit_mesh(me) + verts = bm.verts + faces = bm.faces + + planes = [f for f in faces if f.select] + if not len(planes) == 1: + failure_message(self) + return + + plane = planes[0] + plane_vert_indices = plane.verts[:] + all_selected_vert_indices = [v for v in verts if v.select] + + M = set(plane_vert_indices) + N = set(all_selected_vert_indices) + O = N.difference(M) + O = list(O) + + if not len(O) == 2: + failure_message(self) + return + + (v1_ref, v1), (v2_ref, v2) = [(i, i.co) for i in O] + + plane_co = plane.calc_center_median() + plane_no = plane.normal + + new_co = intersect_line_plane(v1, v2, plane_co, plane_no, False) + + if new_co: + new_vertex = verts.new(new_co) + A_len = (v1 - new_co).length + B_len = (v2 - new_co).length + + vertex_reference = v1_ref if (A_len < B_len) else v2_ref + bm.edges.new([vertex_reference, new_vertex]) + bmesh.update_edit_mesh(me, True) + + else: + failure_message_on_plane(self) + + +class PDT_OT_EdgeToFace(bpy.types.Operator): + """Extend Selected Edge to Projected Intersection with Selected Face.""" + + bl_idname = "pdt.edge_to_face" + bl_label = "Extend Edge to Face" + bl_options = {"REGISTER", "UNDO"} + + @classmethod + def poll(cls, context): + """Only allow this to work if a mesh is selected in EDIT mode.""" + ob = context.object + if ob is None: + return False + return all([bool(ob), ob.type == "MESH", ob.mode == "EDIT"]) + + def execute(self, context): + """Extends Disconnected Edge to Intersect with Face. + + Args: + context: Blender bpy.context instance. + + Returns: + Status Set.""" + + extend_vertex(self) + return {"FINISHED"} diff --git a/precision_drawing_tools/pdt_functions.py b/precision_drawing_tools/pdt_functions.py new file mode 100644 index 00000000..d9573ac8 --- /dev/null +++ b/precision_drawing_tools/pdt_functions.py @@ -0,0 +1,688 @@ +# ***** BEGIN GPL LICENSE BLOCK ***** +# +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software Foundation, +# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# ***** END GPL LICENCE BLOCK ***** + +# ----------------------------------------------------------------------- +# Author: Alan Odom (Clockmender), Rune Morling (ermo) Copyright (c) 2019 +# ----------------------------------------------------------------------- +# +# Common Functions used in more than one place in PDT Operations + +import bpy +import bmesh +import bgl +import gpu +import numpy as np +from mathutils import Vector, Quaternion +from gpu_extras.batch import batch_for_shader +from math import cos, sin, pi +from .pdt_msg_strings import ( + PDT_ERR_VERT_MODE, + PDT_ERR_SEL_2_V_1_E, + PDT_ERR_SEL_2_OBJS, + PDT_ERR_NO_ACT_OBJ, + PDT_ERR_SEL_1_EDGEM +) + + +def debug(msg, prefix=""): + """Print a debug message to the console if PDT's or Blender's debug flags are set. + + The printed message will be of the form: + + {prefix}{caller file name:line number}| {msg} + """ + + pdt_debug = bpy.context.preferences.addons[__package__].preferences.debug + if bpy.app.debug or bpy.app.debug_python or pdt_debug: + import traceback + + def extract_filename(fullpath): + """Return only the filename part of fullpath (excluding its path).""" + # Expected to end up being a string containing only the filename + # (i.e. excluding its preceding '/' separated path) + filename = fullpath.split('/')[-1] + #print(filename) + # something went wrong + if len(filename) < 1: + return fullpath + # since this is a string, just return it + return filename + + # stack frame corresponding to the line where debug(msg) was called + #print(traceback.extract_stack()[-2]) + laststack = traceback.extract_stack()[-2] + #print(laststack[0]) + # laststack[0] is the caller's full file name, laststack[1] is the line number + print(f"{prefix}{extract_filename(laststack[0])}:{laststack[1]}| {msg}") + +def oops(self, context): + """Error Routine. + + Displays error message in a popup. + + Args: + context: Blender bpy.context instance. + + Note: + Uses pg.error scene variable + """ + + scene = context.scene + pg = scene.pdt_pg + self.layout.label(text=pg.error) + + +def setMode(mode_pl): + """Sets Active Axes for View Orientation. + + Sets indices of axes for locational vectors + + Args: + mode_pl: Plane Selector variable as input + + Returns: + 3 Integer indices. + """ + + if mode_pl == "XY": + # a1 = x a2 = y a3 = z + return 0, 1, 2 + if mode_pl == "XZ": + # a1 = x a2 = z a3 = y + return 0, 2, 1 + if mode_pl == "YZ": + # a1 = y a2 = z a3 = x + return 1, 2, 0 + #FIXME: This needs a proper specification and a default + + +def setAxis(mode_pl): + """Sets Active Axes for View Orientation. + + Sets indices for axes from taper vectors + + Args: + mode_pl: Taper Axis Selector variable as input + + Note: + Axis order: Rotate Axis, Move Axis, Height Axis + + Returns: + 3 Integer Indicies. + """ + + if mode_pl == "RX-MY": + return 0, 1, 2 + if mode_pl == "RX-MZ": + return 0, 2, 1 + if mode_pl == "RY-MX": + return 1, 0, 2 + if mode_pl == "RY-MZ": + return 1, 2, 0 + if mode_pl == "RZ-MX": + return 2, 0, 1 + if mode_pl == "RZ-MY": + return 2, 1, 0 + #FIXME: This needs a proper specification and a default + + +def checkSelection(num, bm, obj): + """Check that the Object's select_history has sufficient entries. + + If selection history is not Verts, clears selection and history. + + Args: + num: The number of entries required for each operation + bm: The Bmesh from the Object + obj: The Object + + Returns: + list of 3D points as Vectors. + """ + + if len(bm.select_history) < num: + return None + else: + actE = bm.select_history[-1] + if isinstance(actE, bmesh.types.BMVert): + actV = actE.co + if num == 1: + return actV + elif num == 2: + othV = bm.select_history[-2].co + return actV, othV + elif num == 3: + othV = bm.select_history[-2].co + lstV = bm.select_history[-3].co + return actV, othV, lstV + elif num == 4: + othV = bm.select_history[-2].co + lstV = bm.select_history[-3].co + fstV = bm.select_history[-4].co + return actV, othV, lstV, fstV + else: + for f in bm.faces: + f.select_set(False) + for e in bm.edges: + e.select_set(False) + for v in bm.verts: + v.select_set(False) + bmesh.update_edit_mesh(obj.data) + bm.select_history.clear() + return None + + +def updateSel(bm, verts, edges, faces): + """Updates Vertex, Edge and Face Selections following a function. + + Args: + bm: Object Bmesh + verts: New Selection for Vertices + edges: The Edges on which to operate + faces: The Faces on which to operate + + Returns: + Nothing. + """ + for f in bm.faces: + f.select_set(False) + for e in bm.edges: + e.select_set(False) + for v in bm.verts: + v.select_set(False) + for v in verts: + v.select_set(True) + for e in edges: + e.select_set(True) + for f in faces: + f.select_set(True) + + +def viewCoords(x_loc, y_loc, z_loc): + """Converts input Vector values to new Screen Oriented Vector. + + Args: + x_loc: X coordinate from vector + y_loc: Y coordinate from vector + z_loc: Z coordinate from vector + + Returns: + Vector adjusted to View's Inverted Tranformation Matrix. + """ + + areas = [a for a in bpy.context.screen.areas if a.type == "VIEW_3D"] + if len(areas) > 0: + vm = areas[0].spaces.active.region_3d.view_matrix + vm = vm.to_3x3().normalized().inverted() + vl = Vector((x_loc, y_loc, z_loc)) + vw = vm @ vl + return vw + else: + return Vector((0, 0, 0)) + + +def viewCoordsI(x_loc, y_loc, z_loc): + """Converts Screen Oriented input Vector values to new World Vector. + + Converts View tranformation Matrix to Rotational Matrix + + Args: + x_loc: X coordinate from vector + y_loc: Y coordinate from vector + z_loc: Z coordinate from vector + + Returns: + Vector adjusted to View's Transformation Matrix. + """ + + areas = [a for a in bpy.context.screen.areas if a.type == "VIEW_3D"] + if len(areas) > 0: + vm = areas[0].spaces.active.region_3d.view_matrix + vm = vm.to_3x3().normalized() + vl = Vector((x_loc, y_loc, z_loc)) + vw = vm @ vl + return vw + else: + return Vector((0, 0, 0)) + + +def viewDir(dis_v, ang_v): + """Converts Distance and Angle to View Oriented Vector. + + Converts View Transformation Matrix to Rotational Matrix (3x3) + Angles are converted to Radians from degrees. + + Args: + dis_v: Scene distance + ang_v: Scene angle + + Returns: + World Vector. + """ + + areas = [a for a in bpy.context.screen.areas if a.type == "VIEW_3D"] + if len(areas) > 0: + vm = areas[0].spaces.active.region_3d.view_matrix + vm = vm.to_3x3().normalized().inverted() + vl = Vector((0, 0, 0)) + vl.x = dis_v * cos(ang_v * pi / 180) + vl.y = dis_v * sin(ang_v * pi / 180) + vw = vm @ vl + return vw + else: + return Vector((0, 0, 0)) + + +def euler_to_quaternion(roll, pitch, yaw): + """Converts Euler Rotation to Quaternion Rotation. + + Args: + roll: Roll in Euler rotation + pitch: Pitch in Euler rotation + yaw: Yaw in Euler rotation + + Returns: + Quaternion Rotation. + """ + + # fmt: off + qx = (np.sin(roll/2) * np.cos(pitch/2) * np.cos(yaw/2) + - np.cos(roll/2) * np.sin(pitch/2) * np.sin(yaw/2)) + qy = (np.cos(roll/2) * np.sin(pitch/2) * np.cos(yaw/2) + + np.sin(roll/2) * np.cos(pitch/2) * np.sin(yaw/2)) + qz = (np.cos(roll/2) * np.cos(pitch/2) * np.sin(yaw/2) + - np.sin(roll/2) * np.sin(pitch/2) * np.cos(yaw/2)) + qw = (np.cos(roll/2) * np.cos(pitch/2) * np.cos(yaw/2) + + np.sin(roll/2) * np.sin(pitch/2) * np.sin(yaw/2)) + # fmt: on + return Quaternion((qw, qx, qy, qz)) + + +def arcCentre(actV, othV, lstV): + """Calculates Centre of Arc from 3 Vector Locations using standard Numpy routine + + Args: + actV: Active vector location + othV: Other vector location + lstV: Last vector location + + Returns: + Vector representing Arc Centre and Float representing Arc Radius. + """ + + A = np.array([actV.x, actV.y, actV.z]) + B = np.array([othV.x, othV.y, othV.z]) + C = np.array([lstV.x, lstV.y, lstV.z]) + a = np.linalg.norm(C - B) + b = np.linalg.norm(C - A) + c = np.linalg.norm(B - A) + # fmt: off + s = (a+b+c) / 2 + R = a*b*c/4 / np.sqrt(s * (s-a) * (s-b) * (s-c)) + b1 = a*a * (b*b + c*c - a*a) + b2 = b*b * (a*a + c*c - b*b) + b3 = c*c * (a*a + b*b - c*c) + # fmt: on + P = np.column_stack((A, B, C)).dot(np.hstack((b1, b2, b3))) + P /= b1 + b2 + b3 + return Vector((P[0], P[1], P[2])), R + + +def intersection(actV, othV, lstV, fstV, plane): + """Calculates Intersection Point of 2 Imagined Lines from 4 Vectors. + + Calculates Converging Intersect Location and indication of + whether the lines are convergent using standard Numpy Routines + + Args: + actV: Active vector location of first line + othV: Other vector location of first line + lstV: Last vector location of 2nd line + fstV: First vector location of 2nd line + plane: Working Plane 4 Vector Locations representing 2 lines and Working Plane + + Returns: + Intersection Vector and Boolean for convergent state. + """ + + if plane == "LO": + disV = othV - actV + othV = viewCoordsI(disV.x, disV.y, disV.z) + disV = lstV - actV + lstV = viewCoordsI(disV.x, disV.y, disV.z) + disV = fstV - actV + fstV = viewCoordsI(disV.x, disV.y, disV.z) + refV = Vector((0, 0, 0)) + ap1 = (fstV.x, fstV.y) + ap2 = (lstV.x, lstV.y) + bp1 = (othV.x, othV.y) + bp2 = (refV.x, refV.y) + else: + a1, a2, a3 = setMode(plane) + ap1 = (fstV[a1], fstV[a2]) + ap2 = (lstV[a1], lstV[a2]) + bp1 = (othV[a1], othV[a2]) + bp2 = (actV[a1], actV[a2]) + s = np.vstack([ap1, ap2, bp1, bp2]) + h = np.hstack((s, np.ones((4, 1)))) + l1 = np.cross(h[0], h[1]) + l2 = np.cross(h[2], h[3]) + x, y, z = np.cross(l1, l2) + if z == 0: + return Vector((0, 0, 0)), False + nx = x / z + nz = y / z + if plane == "LO": + ly = 0 + else: + ly = actV[a3] + # Order Vector Delta + if plane == "XZ": + vector_delta = Vector((nx, ly, nz)) + elif plane == "XY": + vector_delta = Vector((nx, nz, ly)) + elif plane == "YZ": + vector_delta = Vector((ly, nx, nz)) + elif plane == "LO": + vector_delta = viewCoords(nx, nz, ly) + actV + return vector_delta, True + + +def getPercent(obj, flip_p, per_v, data, scene): + """Calculates a Percentage Distance between 2 Vectors. + + Calculates a point that lies a set percentage between two given points + using standard Numpy Routines. + + Works for either 2 vertices for an object in Edit mode + or 2 selected objects in Object mode. + + Args: + obj: The Object under consideration + flip_p: Setting this to True measures the percentage starting from the second vector + per_v: Percentage Input Value + data: pg.flip, pg.percent scene variables & Operational Mode + scene: Context Scene + + Returns: + World Vector. + """ + + pg = scene.pdt_pg + + if obj.mode == "EDIT": + bm = bmesh.from_edit_mesh(obj.data) + verts = [v for v in bm.verts if v.select] + if len(verts) == 2: + actV = verts[0].co + othV = verts[1].co + if actV is None: + pg.error = PDT_ERR_VERT_MODE + bpy.context.window_manager.popup_menu(oops, title="Error", icon="ERROR") + return None + else: + pg.error = PDT_ERR_SEL_2_V_1_E + str(len(verts)) + " Vertices" + bpy.context.window_manager.popup_menu(oops, title="Error", icon="ERROR") + return None + p1 = np.array([actV.x, actV.y, actV.z]) + p2 = np.array([othV.x, othV.y, othV.z]) + if obj.mode == "OBJECT": + objs = bpy.context.view_layer.objects.selected + if len(objs) != 2: + pg.error = PDT_ERR_SEL_2_OBJS + str(len(objs)) + ")" + bpy.context.window_manager.popup_menu(oops, title="Error", icon="ERROR") + return None + p1 = np.array( + [ + objs[-1].matrix_world.decompose()[0].x, + objs[-1].matrix_world.decompose()[0].y, + objs[-1].matrix_world.decompose()[0].z, + ] + ) + p2 = np.array( + [ + objs[-2].matrix_world.decompose()[0].x, + objs[-2].matrix_world.decompose()[0].y, + objs[-2].matrix_world.decompose()[0].z, + ] + ) + p4 = np.array([0, 0, 0]) + p3 = p2 - p1 + _per_v = per_v + if (flip_p and data != "MV") or data == "MV": + _per_v = 100 - per_v + V = (p4+p3) * (_per_v / 100) + p1 + return Vector((V[0], V[1], V[2])) + + +def objCheck(obj, scene, oper): + """Check Object & Selection Validity. + + Args: + obj: Active Object + scene: Active Scene + oper: Operation to check + + Returns: + Object Bmesh and Validity Boolean. + """ + + pg = scene.pdt_pg + _oper = oper.upper() + + if obj is None: + pg.error = PDT_ERR_NO_ACT_OBJ + bpy.context.window_manager.popup_menu(oops, title="Error", icon="ERROR") + return None, False + if obj.mode == "EDIT": + bm = bmesh.from_edit_mesh(obj.data) + if _oper == "S": + if len(bm.edges) < 1: + pg.error = f"{PDT_ERR_SEL_1_EDGEM} {len(bm.edges)})" + bpy.context.window_manager.popup_menu(oops, title="Error", icon="ERROR") + return None, False + else: + return bm, True + if len(bm.select_history) >= 1: + if _oper not in {"D", "E", "G", "N", "S"}: + actV = checkSelection(1, bm, obj) + else: + verts = [v for v in bm.verts if v.select] + if len(verts) > 0: + actV = verts[0] + else: + actV = None + if actV is None: + pg.error = PDT_ERR_VERT_MODE + bpy.context.window_manager.popup_menu(oops, title="Error", icon="ERROR") + return None, False + return bm, True + elif obj.mode == "OBJECT": + return None, True + + +def disAng(vals, flip_a, plane, scene): + """Set Working Axes when using Direction command. + + Args: + vals: Input Arguments (Values) + flip_a: Whether to flip the angle + plane: Working Plane + scene: Current Scene + + Returns: + Directional Offset as a Vector. + """ + + pg = scene.pdt_pg + dis_v = float(vals[0]) + ang_v = float(vals[1]) + if flip_a: + if ang_v > 0: + ang_v = ang_v - 180 + else: + ang_v = ang_v + 180 + pg.angle = ang_v + if plane == "LO": + vector_delta = viewDir(dis_v, ang_v) + else: + a1, a2, _ = setMode(plane) + vector_delta = Vector((0, 0, 0)) + # fmt: off + vector_delta[a1] = vector_delta[a1] + (dis_v * cos(ang_v * pi/180)) + vector_delta[a2] = vector_delta[a2] + (dis_v * sin(ang_v * pi/180)) + # FIXME: Is a3 just ignored? + # fmt: on + return vector_delta + + +# Shader for displaying the Pivot Point as Graphics. +# +shader = gpu.shader.from_builtin("3D_UNIFORM_COLOR") if not bpy.app.background else None + + +def draw3D(coords, gtype, rgba, context): + """Draw Pivot Point Graphics. + + Draws either Lines Points, or Tris using defined shader + + Args: + coords: Input Coordinates List + gtype: Graphic Type + rgba: Colour in RGBA format + context: Blender bpy.context instance. + + Returns: + Nothing. + """ + + batch = batch_for_shader(shader, gtype, {"pos": coords}) + + try: + if coords is not None: + bgl.glEnable(bgl.GL_BLEND) + shader.bind() + shader.uniform_float("color", rgba) + batch.draw(shader) + except: + pass + + +def drawCallback3D(self, context): + """Create Coordinate List for Pivot Point Graphic. + + Creates coordinates for Pivot Point Graphic consisting of 6 Tris + and one Point colour coded Red; X axis, Green; Y axis, Blue; Z axis + and a yellow point based upon screen scale + + Args: + context: Blender bpy.context instance. + + Returns: + Nothing. + """ + + scene = context.scene + pg = scene.pdt_pg + w = context.region.width + x = pg.pivot_loc.x + y = pg.pivot_loc.y + z = pg.pivot_loc.z + # Scale it from view + areas = [a for a in context.screen.areas if a.type == "VIEW_3D"] + if len(areas) > 0: + sf = abs(areas[0].spaces.active.region_3d.window_matrix.decompose()[2][1]) + a = w / sf / 10000 * pg.pivot_size + b = a * 0.65 + c = a * 0.05 + (pg.pivot_width * a * 0.02) + o = c / 3 + + # fmt: off + # X Axis + coords = [ + (x, y, z), + (x+b, y-o, z), + (x+b, y+o, z), + (x+a, y, z), + (x+b, y+c, z), + (x+b, y-c, z), + ] + # fmt: on + colour = (1.0, 0.0, 0.0, pg.pivot_alpha) + draw3D(coords, "TRIS", colour, context) + coords = [(x, y, z), (x+a, y, z)] + draw3D(coords, "LINES", colour, context) + # fmt: off + # Y Axis + coords = [ + (x, y, z), + (x-o, y+b, z), + (x+o, y+b, z), + (x, y+a, z), + (x+c, y+b, z), + (x-c, y+b, z), + ] + # fmt: on + colour = (0.0, 1.0, 0.0, pg.pivot_alpha) + draw3D(coords, "TRIS", colour, context) + coords = [(x, y, z), (x, y + a, z)] + draw3D(coords, "LINES", colour, context) + # fmt: off + # Z Axis + coords = [ + (x, y, z), + (x-o, y, z+b), + (x+o, y, z+b), + (x, y, z+a), + (x+c, y, z+b), + (x-c, y, z+b), + ] + # fmt: on + colour = (0.2, 0.5, 1.0, pg.pivot_alpha) + draw3D(coords, "TRIS", colour, context) + coords = [(x, y, z), (x, y, z + a)] + draw3D(coords, "LINES", colour, context) + # Centre + coords = [(x, y, z)] + colour = (1.0, 1.0, 0.0, pg.pivot_alpha) + draw3D(coords, "POINTS", colour, context) + + +def scale_set(self, context): + """Sets Scale by dividing Pivot Distance by System Distance. + + Sets Pivot Point Scale Factors by Measurement + + Args: + context: Blender bpy.context instance. + + Note: + Uses pg.pivotdis & pg.distance scene variables + + Returns: + Status Set. + """ + + scene = context.scene + pg = scene.pdt_pg + sys_dis = pg.distance + scale_dis = pg.pivot_dis + if scale_dis > 0: + scale_fac = scale_dis / sys_dis + pg.pivot_scale = Vector((scale_fac, scale_fac, scale_fac)) diff --git a/precision_drawing_tools/pdt_library.py b/precision_drawing_tools/pdt_library.py new file mode 100644 index 00000000..30f26408 --- /dev/null +++ b/precision_drawing_tools/pdt_library.py @@ -0,0 +1,173 @@ +# ***** BEGIN GPL LICENSE BLOCK ***** +# +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software Foundation, +# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# ***** END GPL LICENCE BLOCK ***** +# +# ----------------------------------------------------------------------- +# Author: Alan Odom (Clockmender), Rune Morling (ermo) Copyright (c) 2019 +# ----------------------------------------------------------------------- +# +import bpy +from bpy.types import Operator +from mathutils import Vector +from pathlib import Path +from .pdt_functions import debug, oops +from .pdt_msg_strings import PDT_ERR_NO_LIBRARY + + +class PDT_OT_LibShow(Operator): + """Show Library File Details.""" + bl_idname = "pdt.lib_show" + bl_label = "Show Library Details" + bl_options = {"REGISTER", "UNDO"} + + def execute(self, context): + """Shows Location Of PDT Library File. + + Args: + context: Blender bpy.context instance. + + Returns: + Status Set. + """ + + scene = context.scene + pg = scene.pdt_pg + file_path = context.preferences.addons[__package__].preferences.pdt_library_path + pg.error = str(Path(file_path)) + debug("PDT Parts Library:") + debug(f"{pg.error}") + bpy.context.window_manager.popup_menu(oops, title="Information - Parts Library File", icon="INFO") + return {"FINISHED"} + + +class PDT_OT_Append(Operator): + """Append from Library at cursor Location.""" + + bl_idname = "pdt.append" + bl_label = "Append" + bl_options = {"REGISTER", "UNDO"} + + def execute(self, context): + """Appends Objects from PDT Library file. + + Appended Objects are placed at Cursor Location. + + Args: + context: Blender bpy.context instance. + + Notes: + Uses pg.lib_objects, pg.lib_collections & pg.lib_materials + + Returns: + Status Set. + """ + + scene = context.scene + pg = scene.pdt_pg + obj_names = [o.name for o in context.view_layer.objects] + file_path = context.preferences.addons[__package__].preferences.pdt_library_path + path = Path(file_path) + + if path.is_file() and ".blend" in str(path): + if pg.lib_mode == "OBJECTS": + # Force object Mode + bpy.ops.object.mode_set(mode='OBJECT') + bpy.ops.wm.append( + filepath=str(path), directory=str(path) + "/Object", filename=pg.lib_objects + ) + for obj in context.view_layer.objects: + if obj.name not in obj_names: + obj.select_set(False) + obj.location = Vector( + (scene.cursor.location.x, scene.cursor.location.y, scene.cursor.location.z) + ) + return {"FINISHED"} + elif pg.lib_mode == "COLLECTIONS": + bpy.ops.wm.append( + filepath=str(path), directory=str(path) + "/Collection", filename=pg.lib_collections + ) + for obj in context.view_layer.objects: + if obj.name not in obj_names: + obj.select_set(False) + obj.location = Vector( + (scene.cursor.location.x, scene.cursor.location.y, scene.cursor.location.z) + ) + return {"FINISHED"} + elif pg.lib_mode == "MATERIALS": + bpy.ops.wm.append( + filepath=str(path), directory=str(path) + "/Material", filename=pg.lib_materials + ) + return {"FINISHED"} + else: + errmsg = PDT_ERR_NO_LIBRARY + self.report({"ERROR"}, errmsg) + return {"FINISHED"} + + +class PDT_OT_Link(Operator): + """Link from Library at Object's Origin.""" + + bl_idname = "pdt.link" + bl_label = "Link" + bl_options = {"REGISTER", "UNDO"} + + def execute(self, context): + """Links Objects from PDT Library file. + + Linked Objects are placed at Cursor Location + + Args: + context + + Notes: + Uses pg.lib_objects, pg.lib_collections & pg.lib_materials + + Returns: + Status Set. + """ + + scene = context.scene + pg = scene.pdt_pg + file_path = context.preferences.addons[__package__].preferences.pdt_library_path + path = Path(file_path) + if path.is_file() and ".blend" in str(path): + if pg.lib_mode == "OBJECTS": + # Force object Mode + bpy.ops.object.mode_set(mode='OBJECT') + bpy.ops.wm.link( + filepath=str(path), directory=str(path) + "/Object", filename=pg.lib_objects + ) + for obj in context.view_layer.objects: + obj.select_set(False) + return {"FINISHED"} + elif pg.lib_mode == "COLLECTIONS": + bpy.ops.wm.link( + filepath=str(path), directory=str(path) + "/Collection", filename=pg.lib_collections + ) + for obj in context.view_layer.objects: + obj.select_set(False) + return {"FINISHED"} + elif pg.lib_mode == "MATERIALS": + bpy.ops.wm.link( + filepath=str(path), directory=str(path) + "/Material", filename=pg.lib_materials + ) + return {"FINISHED"} + else: + errmsg = PDT_ERR_NO_LIBRARY + self.report({"ERROR"}, errmsg) + return {"FINISHED"} diff --git a/precision_drawing_tools/pdt_menus.py b/precision_drawing_tools/pdt_menus.py new file mode 100644 index 00000000..0949f7a0 --- /dev/null +++ b/precision_drawing_tools/pdt_menus.py @@ -0,0 +1,330 @@ +# ***** BEGIN GPL LICENSE BLOCK ***** +# +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software Foundation, +# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# ***** END GPL LICENCE BLOCK ***** +# +# ----------------------------------------------------------------------- +# Author: Alan Odom (Clockmender), Rune Morling (ermo) Copyright (c) 2019 +# ----------------------------------------------------------------------- +# +from bpy.types import Panel +from .pdt_msg_strings import ( + PDT_LAB_ABS, + PDT_LAB_AD2D, + PDT_LAB_AD3D, + PDT_LAB_ALLACTIVE, + PDT_LAB_ANGLEVALUE, + PDT_LAB_ARCCENTRE, + PDT_LAB_BISECT, + PDT_LAB_CVALUE, + PDT_LAB_DEL, + PDT_LAB_DIR, + PDT_LAB_DISVALUE, + PDT_LAB_EDGETOEFACE, + PDT_LAB_FILLET, + PDT_LAB_FLIPANGLE, + PDT_LAB_FLIPPERCENT, + PDT_LAB_INTERSECT, + PDT_LAB_INTERSETALL, + PDT_LAB_JOIN2VERTS, + PDT_LAB_MODE, + PDT_LAB_NOR, + PDT_LAB_OPERATION, + PDT_LAB_ORDER, + PDT_LAB_ORIGINCURSOR, + PDT_LAB_PERCENT, + PDT_LAB_PERCENTS, + PDT_LAB_PIVOTALPHA, + PDT_LAB_PIVOTLOC, + PDT_LAB_PIVOTLOCH, + PDT_LAB_PIVOTSIZE, + PDT_LAB_PIVOTWIDTH, + PDT_LAB_PLANE, + PDT_LAB_PROFILE, + PDT_LAB_RADIUS, + PDT_LAB_SEGMENTS, + PDT_LAB_TAPER, + PDT_LAB_TAPERAXES, + PDT_LAB_TOOLS, + PDT_LAB_USEVERTS, + PDT_LAB_VARIABLES +) + + +# PDT Panel menus +# +class PDT_PT_PanelDesign(Panel): + bl_idname = "PDT_PT_PanelDesign" + bl_label = "PDT Design" + bl_space_type = "VIEW_3D" + bl_region_type = "UI" + bl_category = "PDT" + bl_options = {'DEFAULT_CLOSED'} + + def draw(self, context): + layout = self.layout + pdt_pg = context.scene.pdt_pg + row = layout.row() + col = row.column() + col.prop(pdt_pg, "plane", text=PDT_LAB_PLANE) + col = row.column() + col.prop(pdt_pg, "select", text=PDT_LAB_MODE) + box = layout.box() + row = box.row() + row.prop(pdt_pg, "operation", text=PDT_LAB_OPERATION) + row = box.row() + col = row.column() + col.operator("pdt.absolute", icon="EMPTY_AXIS", text=PDT_LAB_ABS) + col = row.column() + col.operator("pdt.delta", icon="EMPTY_AXIS", text=PDT_LAB_DEL) + col = row.column() + col.operator("pdt.distance", icon="EMPTY_AXIS", text=PDT_LAB_DIR) + row = box.row() + col = row.column() + col.operator("pdt.percent", text=PDT_LAB_PERCENT) + col = row.column() + col.operator("pdt.normal", text=PDT_LAB_NOR) + col = row.column() + col.operator("pdt.centre", text=PDT_LAB_ARCCENTRE) + row = box.row() + col = row.column() + col.operator("pdt.intersect", text=PDT_LAB_INTERSECT) + col = row.column() + col.prop(pdt_pg, "object_search_string", text=PDT_LAB_ORDER) + row = box.row() + col = row.column() + col.prop(pdt_pg, "flip_angle", text=PDT_LAB_FLIPANGLE) + col = row.column() + col.prop(pdt_pg, "flip_percent", text=PDT_LAB_FLIPPERCENT) + col = row.column() + col.prop(pdt_pg, "extend", text=PDT_LAB_ALLACTIVE) + box = layout.box() + row = box.row() + row.label(text=PDT_LAB_VARIABLES) + row = box.row() + row.prop(pdt_pg, "cartesian_coords", text=PDT_LAB_CVALUE) + row = box.row() + col = row.column() + col.prop(pdt_pg, "distance", text=PDT_LAB_DISVALUE) + col = row.column() + col.prop(pdt_pg, "angle", text=PDT_LAB_ANGLEVALUE) + col = row.column() + col.prop(pdt_pg, "percent", text=PDT_LAB_PERCENTS) + box = layout.box() + row = box.row() + row.label(text=PDT_LAB_TOOLS) + row = box.row() + col = row.column() + col.operator("pdt.angle2", text=PDT_LAB_AD2D) + col = row.column() + col.operator("pdt.angle3", text=PDT_LAB_AD3D) + row = box.row() + col = row.column() + col.operator("pdt.join", text=PDT_LAB_JOIN2VERTS) + col = row.column() + col.operator("pdt.origin", text=PDT_LAB_ORIGINCURSOR) + row = box.row() + col = row.column() + col.prop(pdt_pg, "taper", text=PDT_LAB_TAPERAXES) + col = row.column() + col.operator("pdt.taper", text=PDT_LAB_TAPER) + # New for 1.1.5 + row = box.row() + col = row.column() + col.operator("pdt.intersectall", text=PDT_LAB_INTERSETALL) + col = row.column() + col.operator("pdt.linetobisect", text=PDT_LAB_BISECT) + col = row.column() + col.operator("pdt.edge_to_face", text=PDT_LAB_EDGETOEFACE) + # + # Add Fillet Tool + row = box.row() + col = row.column() + col.operator("pdt.fillet", text=PDT_LAB_FILLET) + col = row.column() + col.prop(pdt_pg, "fillet_segments", text=PDT_LAB_SEGMENTS) + col = row.column() + col.prop(pdt_pg, "fillet_vertices_only", text=PDT_LAB_USEVERTS) + row = box.row() + col = row.column() + col.prop(pdt_pg, "fillet_radius", text=PDT_LAB_RADIUS) + col = row.column() + col.prop(pdt_pg, "fillet_profile", text=PDT_LAB_PROFILE) + + +class PDT_PT_PanelPivotPoint(Panel): + bl_idname = "PDT_PT_PanelPivotPoint" + bl_label = "PDT Pivot Point" + bl_space_type = "VIEW_3D" + bl_region_type = "UI" + bl_category = "PDT" + bl_options = {'DEFAULT_CLOSED'} + + def draw(self, context): + pdt_pg = context.scene.pdt_pg + layout = self.layout + row = layout.row() + split = row.split(factor=0.4, align=True) + if context.window_manager.pdt_run_opengl is False: + icon = "PLAY" + txt = "Show" + else: + icon = "PAUSE" + txt = "Hide" + split.operator("pdt.modaldraw", icon=icon, text=txt) + split.prop(pdt_pg, "pivot_size", text=PDT_LAB_PIVOTSIZE) + split.prop(pdt_pg, "pivot_width", text=PDT_LAB_PIVOTWIDTH) + split.prop(pdt_pg, "pivot_alpha", text=PDT_LAB_PIVOTALPHA) + row = layout.row() + row.label(text=PDT_LAB_PIVOTLOCH) + row = layout.row() + row.prop(pdt_pg, "pivot_loc", text=PDT_LAB_PIVOTLOC) + row = layout.row() + col = row.column() + col.operator("pdt.pivotselected", icon="EMPTY_AXIS", text="Selection") + col = row.column() + col.operator("pdt.pivotcursor", icon="EMPTY_AXIS", text="Cursor") + col = row.column() + col.operator("pdt.pivotorigin", icon="EMPTY_AXIS", text="Origin") + row = layout.row() + col = row.column() + col.operator("pdt.viewplanerot", icon="EMPTY_AXIS", text="Rotate") + col = row.column() + col.prop(pdt_pg, "pivot_ang", text="Angle") + row = layout.row() + col = row.column() + col.operator("pdt.viewscale", icon="EMPTY_AXIS", text="Scale") + col = row.column() + col.operator("pdt.cursorpivot", icon="EMPTY_AXIS", text="Cursor To Pivot") + row = layout.row() + col = row.column() + col.prop(pdt_pg, "pivot_dis", text="Scale Distance") + col = row.column() + col.prop(pdt_pg, "distance", text="System Distance") + row = layout.row() + row.label(text="Pivot Point Scale Factors") + row = layout.row() + row.prop(pdt_pg, "pivot_scale", text="") + row = layout.row() + col = row.column() + col.operator("pdt.pivotwrite", icon="FILE_TICK", text="PP Write") + col = row.column() + col.operator("pdt.pivotread", icon="FILE", text="PP Read") + + +class PDT_PT_PanelPartsLibrary(Panel): + bl_idname = "PDT_PT_PanelPartsLibrary" + bl_label = "PDT Parts Library" + bl_space_type = "VIEW_3D" + bl_region_type = "UI" + bl_category = "PDT" + bl_options = {'DEFAULT_CLOSED'} + + def draw(self, context): + layout = self.layout + pdt_pg = context.scene.pdt_pg + row = layout.row() + col = row.column() + col.operator("pdt.append", text="Append") + col = row.column() + col.operator("pdt.link", text="Link") + col = row.column() + col.prop(pdt_pg, "lib_mode", text="") + box = layout.box() + row = box.row() + col = row.column() + col.label(text="Objects") + col = row.column() + col.prop(pdt_pg, "object_search_string") + row = box.row() + row.prop(pdt_pg, "lib_objects", text="") + box = layout.box() + row = box.row() + col = row.column() + col.label(text="Collections") + col = row.column() + col.prop(pdt_pg, "collection_search_string") + row = box.row() + row.prop(pdt_pg, "lib_collections", text="") + box = layout.box() + row = box.row() + col = row.column() + col.label(text="Materials") + col = row.column() + col.prop(pdt_pg, "material_search_string") + row = box.row() + row.prop(pdt_pg, "lib_materials", text="") + row = box.row() + row.operator("pdt.lib_show", text="Show Library File", icon='INFO') + + +class PDT_PT_PanelViewControl(Panel): + bl_idname = "PDT_PT_PanelViewControl" + bl_label = "PDT View Control" + bl_space_type = "VIEW_3D" + bl_region_type = "UI" + bl_category = "PDT" + bl_options = {'DEFAULT_CLOSED'} + + def draw(self, context): + layout = self.layout + pdt_pg = context.scene.pdt_pg + box = layout.box() + row = box.row() + col = row.column() + col.label(text="View Rotation") + col = row.column() + col.operator("pdt.viewrot", text="Rotate Abs") + row = box.row() + row.prop(pdt_pg, "rotation_coords", text="Rotation") + row = box.row() + col = row.column() + col.prop(pdt_pg, "vrotangle", text="Angle") + col = row.column() + col.operator("pdt.viewleft", text="", icon="TRIA_LEFT") + col = row.column() + col.operator("pdt.viewright", text="", icon="TRIA_RIGHT") + col = row.column() + col.operator("pdt.viewup", text="", icon="TRIA_UP") + col = row.column() + col.operator("pdt.viewdown", text="", icon="TRIA_DOWN") + col = row.column() + col.operator("pdt.viewroll", text="", icon="RECOVER_LAST") + row = box.row() + row.operator("pdt.viewiso", text="Isometric View") + + +class PDT_PT_PanelCommandLine(Panel): + bl_idname = "PDT_PT_PanelCommandLine" + bl_label = "PDT Command Line (? for help)" + bl_space_type = "VIEW_3D" + bl_region_type = "UI" + bl_category = "PDT" + bl_options = {'DEFAULT_CLOSED'} + + def draw(self, context): + layout = self.layout + pdt_pg = context.scene.pdt_pg + row = layout.row() + col = row.column() + col.prop(pdt_pg, "plane", text="Plane") + col = row.column() + col.prop(pdt_pg, "select", text="Mode") + row = layout.row() + row.label(text="Comand Line, uses Plane & Mode Options") + row = layout.row() + row.prop(pdt_pg, "command", text="") diff --git a/precision_drawing_tools/pdt_msg_strings.py b/precision_drawing_tools/pdt_msg_strings.py new file mode 100644 index 00000000..2c6cab25 --- /dev/null +++ b/precision_drawing_tools/pdt_msg_strings.py @@ -0,0 +1,171 @@ +# ***** BEGIN GPL LICENSE BLOCK ***** +# +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software Foundation, +# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# ***** END GPL LICENCE BLOCK ***** +# +# ----------------------------------------------------------------------- +# Author: Alan Odom (Clockmender), Rune Morling (ermo) Copyright (c) 2019 +# ----------------------------------------------------------------------- +# +# English Version +# +# If you edit this file do not change any of the PDT_ format, just the Text Value in "'s +# Do not delete any of the PDT_ lines +# +# Menu Labels +# +PDT_LAB_ABS = "Absolute" # "Global" +PDT_LAB_DEL = "Delta" # "Relative" +PDT_LAB_DIR = "Direction" # "Polar" +PDT_LAB_NOR = "Normal" # "Perpendicular" +PDT_LAB_ARCCENTRE = "Arc Centre" +PDT_LAB_PLANE = "Plane" +PDT_LAB_MODE = "Mode" +PDT_LAB_OPERATION = "Operation" +PDT_LAB_PERCENT = "Percent" +PDT_LAB_INTERSECT = "Intersect" # "Convergance" +PDT_LAB_ORDER = "Order" +PDT_LAB_FLIPANGLE = "Flip Angle" +PDT_LAB_FLIPPERCENT = "Flip %" +PDT_LAB_ALLACTIVE = "All/Active" +PDT_LAB_VARIABLES = "Coordinates/Delta Offsets & Other Variables" +PDT_LAB_CVALUE = "" # Intentionally left blank +PDT_LAB_DISVALUE = "Distance" +PDT_LAB_ANGLEVALUE = "Angle" +PDT_LAB_PERCENTS = "%" +PDT_LAB_TOOLS = "Tools" +PDT_LAB_JOIN2VERTS = "Join 2 Verts" +PDT_LAB_ORIGINCURSOR = "Origin To Cursor" +PDT_LAB_AD2D = "Set A/D 2D" +PDT_LAB_AD3D = "Set A/D 3D" +PDT_LAB_TAPERAXES = "" # Intentionally left blank +PDT_LAB_TAPER = "Taper" +PDT_LAB_INTERSETALL = "Intersect All" +PDT_LAB_BISECT = "Bisect" +PDT_LAB_EDGETOEFACE = "Edge-Face" +PDT_LAB_FILLET = "Fillet" +PDT_LAB_SEGMENTS = "Segments" +PDT_LAB_USEVERTS = "Use Verts" +PDT_LAB_RADIUS = "Radius" +PDT_LAB_PROFILE = "Profile" +PDT_LAB_PIVOTSIZE = "" # Intentionally left blank +PDT_LAB_PIVOTWIDTH = "" # Intentionally left blank +PDT_LAB_PIVOTALPHA = "" # Intentionally left blank +PDT_LAB_PIVOTLOC = "" # Intentionally left blank +PDT_LAB_PIVOTLOCH = "Pivot Point Location" +# +# Error Message +# +PDT_ERR_NO_ACT_OBJ = "No Active Object - Please Select an Object" +PDT_ERR_NO_ACT_VERT = "No Active Vertex - Select One Vertex Individually" +PDT_ERR_NO_SEL_GEOM = "No Geometry/Objects Selected" +PDT_ERR_NO_ACT_VERTS = "No Selected Geometry - Please Select some Geometry" +PDT_ERR_NON_VALID = "is Not a Valid Option in Selected Object's Mode for Command:" +PDT_ERR_VERT_MODE = "Work in Vertex Mode for this Function" +PDT_ERR_NOPPLOC = "Custom Property PDT_PP_LOC for this object not found, have you Written it yet?" +PDT_ERR_NO_LIBRARY = "PDT Library Blend File (parts_library.blend) is Missing from Addons/clockworxpdt Folder" + +PDT_ERR_SEL_1_VERTI = "Select at least 1 Vertex Individually (Currently selected:" +PDT_ERR_SEL_1_VERT = "Select at least 1 Vertex (Currently selected:" +PDT_ERR_SEL_2_VERTI = "Select at least 2 Vertices Individually (Currently selected:" +PDT_ERR_SEL_2_VERTIO = "Select Exactly 2 Vertices Individually (Currently selected:" +PDT_ERR_SEL_2_VERTS = "Select Exactly 2 Vertices (Currently selected:" +PDT_ERR_SEL_3_VERTS = "Select Exactly 3 Vertices (Currently selected:" +PDT_ERR_SEL_3_VERTIO = "Select Exactly 3 Vertices Individually (Currently selected:" +PDT_ERR_SEL_2_V_1_E = "Select 2 Vertices Individually, or 1 Edge (Currently selected:" +PDT_ERR_SEL_4_VERTS = "Select 4 Vertices Individually, or 2 Edges (Currently selected:" +PDT_ERR_SEL_1_E_1_F = "Select 1 Face and 1 Detached Edge" + +PDT_ERR_SEL_1_EDGE = "Select Exactly 1 Edge (Currently selected:" +PDT_ERR_SEL_1_EDGEM = "Select at least 1 Edge (Currently selected:" + +PDT_ERR_SEL_1_OBJ = "Select Exactly 1 Object (Currently selected:" +PDT_ERR_SEL_2_OBJS = "Select Exactly 2 Objects (Currently selected:" +PDT_ERR_SEL_3_OBJS = "Select Exactly 3 Objects (Currently selected:" +PDT_ERR_SEL_4_OBJS = "Select Exactly 4 Objects (Currently selected:" + +PDT_ERR_FACE_SEL = "You have a Face Selected, this would have ruined the Topology" + +PDT_ERR_INT_LINES = "Implied Lines Do Not Intersect in" +PDT_ERR_INT_NO_ALL = "Active Vertex was not Closest to Intersection and All/Act was not Selected" +PDT_ERR_STRIGHT_LINE = "Selected Points all lie in a Straight Line" +PDT_ERR_CONNECTED = "Vertices are already Connected" +PDT_ERR_EDIT_MODE = "Only Works in EDIT Mode (Current mode:" +PDT_ERR_EDOB_MODE = "Only Works in EDIT, or OBJECT Modes (Current mode:" +PDT_ERR_TAPER_ANG = "Angle must be in Range -80 to +80 (Currently set to:" +PDT_ERR_TAPER_SEL = "Select at Least 2 Vertices Individually - Active is Rotation Point (Currently selected:" +PDT_ERR_NO3DVIEW = "View3D not found, cannot run operator" +PDT_ERR_SCALEZERO = "Scale Distance is 0" + +PDT_ERR_CHARS_NUM = "Bad Command Format, not enough Characters" +PDT_ERR_BADFLETTER = "Bad Operator (1st Letter); C D E F G N M P S V or ? only" +PDT_ERR_BADSLETTER = "Bad Mode (2nd Letter); A D I or P only (+ X Y & Z for Maths) (+ V & G for Fillet)" +PDT_ERR_BADMATHS = "Not a Valid Mathematical Expression!" +PDT_ERR_BADCOORDL = "X Y & Z Not permitted in anything other than Maths Operations" +PDT_ERR_BAD1VALS = "Bad Command - 1 Value needed" +PDT_ERR_BAD2VALS = "Bad Command - 2 Values needed" +PDT_ERR_BAD3VALS = "Bad Command - 3 Coords needed" +PDT_ERR_ADDVEDIT = "Only Add New Vertices in Edit Mode" +PDT_ERR_SPLITEDIT = "Only Split Edges in Edit Mode" +PDT_ERR_EXTEDIT = "Only Extrude Vertices in Edit Mode" +PDT_ERR_DUPEDIT = "Only Duplicate Geometry in Edit Mode" +PDT_ERR_FILEDIT = "Only Fillet Geometry in Edit Mode" +PDT_ERR_NOCOMMAS = "No commas allowed in Maths Command" + +PDT_ERR_2CPNPE = "Select 2 Co-Planar Non-Parallel Edges" +PDT_ERR_NCEDGES = "Edges must be Co-Planar Non-Parallel Edges, Selected Edges aren't" +PDT_ERR_1EDGE1FACE = "Select 1 face and 1 Detached Edge" +PDT_ERR_NOINT = "No Intersection Found, see the Info Panel for Details" + +# Info messages +# +PDT_INF_OBJ_MOVED = "Active Object Moved to Intersection, " + +# Confirm Messages +# +PDT_CON_AREYOURSURE = "Are You Sure About This?" + +# Descriptions +# +PDT_DES_COORDS = "Cartesian Inputs" +PDT_DES_OFFDIS = "Offset Distance" +PDT_DES_OFFANG = "Offset Angle" +PDT_DES_OFFPER = "Offset Percentage" +PDT_DES_WORPLANE = "Choose Working Plane" +PDT_DES_MOVESEL = "Select Move Mode" +PDT_DES_OPMODE = "Select Operation Mode" +PDT_DES_ROTMOVAX = "Rotational Axis - Movement Axis" +PDT_DES_FLIPANG = "Flip Angle 180 degrees" +PDT_DES_FLIPPER = "Flip Percent to 100 - %" +PDT_DES_TRIM = "Trim/Extend only Active Vertex, or All" +PDT_DES_LIBOBS = "Objects in Library" +PDT_DES_LIBCOLS = "Collections in Library" +PDT_DES_LIBMATS = "Materials in Library" +PDT_DES_LIBMODE = "Library Mode" +PDT_DES_LIBSER = "Enter A Search String (Contained)" +PDT_DES_OBORDER = "Object Order to Lines" +PDT_DES_VALIDLET = "Valid 1st letters; C D E G N P S V, Valid 2nd letters: A D I P" +PDT_DES_PPLOC = "Location of PivotPoint" +PDT_DES_PPSCALEFAC = "Scale Factors" +PDT_DES_PPSIZE = "Pivot Size Factor" +PDT_DES_PPWIDTH = "Pivot Line Width in Pixels" +PDT_DES_PPTRANS = "Pivot Point Transparency" +PDT_DES_PIVOTDIS = "Input Distance to Compare with Sytem Distance to set Scales" +PDT_DES_FILLETRAD = "Fillet Radius" +PDT_DES_FILLETSEG = "Number of Fillet Segments" +PDT_DES_FILLETPROF = "Fillet Profile" +PDT_DES_FILLETVERTS = "Use Vertices, or Edges, Set to False for Extruded Geometry" diff --git a/precision_drawing_tools/pdt_pivot_point.py b/precision_drawing_tools/pdt_pivot_point.py new file mode 100644 index 00000000..f09ac1da --- /dev/null +++ b/precision_drawing_tools/pdt_pivot_point.py @@ -0,0 +1,442 @@ +# ***** BEGIN GPL LICENSE BLOCK ***** +# +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software Foundation, +# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# ***** END GPL LICENCE BLOCK ***** +# +# ----------------------------------------------------------------------- +# Author: Alan Odom (Clockmender), Rune Morling (ermo) Copyright (c) 2019 +# ----------------------------------------------------------------------- +# +import bpy +import bmesh +from bpy.types import Operator, SpaceView3D +from mathutils import Vector, Matrix +from math import pi +from .pdt_functions import viewCoords, drawCallback3D +from .pdt_msg_strings import ( + PDT_CON_AREYOURSURE, + PDT_ERR_EDIT_MODE, + PDT_ERR_NO3DVIEW, + PDT_ERR_NOPPLOC, + PDT_ERR_NO_ACT_OBJ, + PDT_ERR_NO_SEL_GEOM +) + + +class PDT_OT_ModalDrawOperator(bpy.types.Operator): + """Show/Hide Pivot Point.""" + + bl_idname = "pdt.modaldraw" + bl_label = "PDT Modal Draw" + + _handle = None # keep function handler + + @staticmethod + def handle_add(self, context): + """Draw Pivot Point Graphic if not displayed. + + Draws 7 element Pivot Point Graphic + + Args: + context: Blender bpy.context instance. + + Returns: + Nothing. + """ + + if PDT_OT_ModalDrawOperator._handle is None: + PDT_OT_ModalDrawOperator._handle = SpaceView3D.draw_handler_add( + drawCallback3D, (self, context), "WINDOW", "POST_VIEW" + ) + context.window_manager.pdt_run_opengl = True + + @staticmethod + def handle_remove(self, context): + """Remove Pivot Point Graphic if displayed. + + Removes 7 element Pivot Point Graphic + + Args: + context: Blender bpy.context instance. + + Returns: + Nothing. + """ + + if PDT_OT_ModalDrawOperator._handle is not None: + SpaceView3D.draw_handler_remove(PDT_OT_ModalDrawOperator._handle, "WINDOW") + PDT_OT_ModalDrawOperator._handle = None + context.window_manager.pdt_run_opengl = False + + def execute(self, context): + """Pivot Point Show/Hide Button Function. + + Operational execute function for Show/Hide Pivot Point function + + Args: + context: Blender bpy.context instance. + + Returns: + Status Set. + """ + + if context.area.type == "VIEW_3D": + if context.window_manager.pdt_run_opengl is False: + self.handle_add(self, context) + context.area.tag_redraw() + else: + self.handle_remove(self, context) + context.area.tag_redraw() + + return {"FINISHED"} + else: + self.report({"ERROR"}, PDT_ERR_NO3DVIEW) + + return {"CANCELLED"} + + +class PDT_OT_ViewPlaneRotate(Operator): + """Rotate Selected Vertices about Pivot Point in View Plane.""" + + bl_idname = "pdt.viewplanerot" + bl_label = "PDT View Rotate" + + @classmethod + def poll(cls, context): + ob = context.object + if ob is None: + return False + return all([bool(ob), ob.type == "MESH", ob.mode == "EDIT"]) + + + def execute(self, context): + """Rotate Selected Vertices about Pivot Point. + + Rotates any selected vertices about the Pivot Point + in View Oriented coordinates, works in any view orientation. + + Args: + context: Blender bpy.context instance. + + Notes: + Uses pg.pivot_loc, pg.pivot_ang scene variables + + Returns: + Status Set. + """ + + scene = context.scene + pg = scene.pdt_pg + obj = bpy.context.view_layer.objects.active + if obj is None: + self.report({"ERROR"}, PDT_ERR_NO_ACT_OBJ) + return {"FINISHED"} + if obj.mode != "EDIT": + errmsg = f"{PDT_ERR_EDIT_MODE} {obj.mode})" + self.report({"ERROR"}, errmsg) + return {"FINISHED"} + bm = bmesh.from_edit_mesh(obj.data) + v1 = Vector((0, 0, 0)) + v2 = viewCoords(0, 0, 1) + axis = (v2 - v1).normalized() + rot = Matrix.Rotation((pg.pivot_ang * pi / 180), 4, axis) + verts = verts = [v for v in bm.verts if v.select] + bmesh.ops.rotate( + bm, cent=pg.pivot_loc - obj.matrix_world.decompose()[0], matrix=rot, verts=verts + ) + bmesh.update_edit_mesh(obj.data) + return {"FINISHED"} + + +class PDT_OT_ViewPlaneScale(Operator): + """Scale Selected Vertices about Pivot Point.""" + + bl_idname = "pdt.viewscale" + bl_label = "PDT View Scale" + + @classmethod + def poll(cls, context): + ob = context.object + if ob is None: + return False + return all([bool(ob), ob.type == "MESH", ob.mode == "EDIT"]) + + + def execute(self, context): + """Scales Selected Vertices about Pivot Point. + + Scales any selected vertices about the Pivot Point + in View Oriented coordinates, works in any view orientation + + Args: + context: Blender bpy.context instance. + + Note: + Uses pg.pivot_loc, pg.pivot_scale scene variables + + Returns: + Status Set. + """ + + scene = context.scene + pg = scene.pdt_pg + obj = bpy.context.view_layer.objects.active + if obj is None: + self.report({"ERROR"}, PDT_ERR_NO_ACT_OBJ) + return {"FINISHED"} + if obj.mode != "EDIT": + errmsg = f"{PDT_ERR_EDIT_MODE} {obj.mode})" + self.report({"ERROR"}, errmsg) + return {"FINISHED"} + bm = bmesh.from_edit_mesh(obj.data) + verts = verts = [v for v in bm.verts if v.select] + for v in verts: + dx = (pg.pivot_loc.x - obj.matrix_world.decompose()[0].x - v.co.x) * ( + 1 - pg.pivot_scale.x + ) + dy = (pg.pivot_loc.y - obj.matrix_world.decompose()[0].y - v.co.y) * ( + 1 - pg.pivot_scale.y + ) + dz = (pg.pivot_loc.z - obj.matrix_world.decompose()[0].z - v.co.z) * ( + 1 - pg.pivot_scale.z + ) + dv = Vector((dx, dy, dz)) + v.co = v.co + dv + bmesh.update_edit_mesh(obj.data) + return {"FINISHED"} + + +class PDT_OT_PivotToCursor(Operator): + """Set The Pivot Point to Cursor Location.""" + + bl_idname = "pdt.pivotcursor" + bl_label = "PDT Pivot To Cursor" + + def execute(self, context): + """Moves Pivot Point to Cursor Location. + + Moves Pivot Point to Cursor Location in active scene + + Args: + context: Blender bpy.context instance. + + Returns: + Status Set. + """ + + scene = context.scene + pg = scene.pdt_pg + pg.pivot_loc = scene.cursor.location + return {"FINISHED"} + + +class PDT_OT_CursorToPivot(Operator): + """Set The Cursor Location to Pivot Point.""" + + bl_idname = "pdt.cursorpivot" + bl_label = "PDT Cursor To Pivot" + + def execute(self, context): + """Moves Cursor to Pivot Point Location. + + Moves Cursor to Pivot Point Location in active scene + + Args: + context: Blender bpy.context instance. + + Returns: + Status Set. + """ + + scene = context.scene + pg = scene.pdt_pg + scene.cursor.location = pg.pivot_loc + return {"FINISHED"} + + +class PDT_OT_PivotSelected(Operator): + """Set Pivot Point to Selected Geometry.""" + + bl_idname = "pdt.pivotselected" + bl_label = "PDT Pivot to Selected" + + @classmethod + def poll(cls, context): + ob = context.object + if ob is None: + return False + return all([bool(ob), ob.type == "MESH", ob.mode == "EDIT"]) + + + def execute(self, context): + """Moves Pivot Point centroid of Selected Geometry. + + Moves Pivot Point centroid of Selected Geometry in active scene + using Snap_Cursor_To_Selected, then puts cursor back to original location. + + Args: + context: Blender bpy.context instance. + + Returns: + Status Set. + """ + + scene = context.scene + obj = bpy.context.view_layer.objects.active + if obj is None: + self.report({"ERROR"}, PDT_ERR_NO_ACT_OBJ) + return {"FINISHED"} + if obj.mode != "EDIT": + errmsg = f"{PDT_ERR_EDIT_MODE} {obj.mode})" + self.report({"ERROR"}, errmsg) + return {"FINISHED"} + bm = bmesh.from_edit_mesh(obj.data) + verts = verts = [v for v in bm.verts if v.select] + if len(verts) > 0: + old_cursor_loc = scene.cursor.location.copy() + bpy.ops.view3d.snap_cursor_to_selected() + pg.pivot_loc = scene.cursor.location + scene.cursor.location = old_cursor_loc + return {"FINISHED"} + else: + self.report({"ERROR"}, PDT_ERR_NO_SEL_GEOM) + return {"FINISHED"} + + +class PDT_OT_PivotOrigin(Operator): + """Set Pivot Point at Object Origin.""" + + bl_idname = "pdt.pivotorigin" + bl_label = "PDT Pivot to Object Origin" + + @classmethod + def poll(cls, context): + ob = context.object + if ob is None: + return False + return all([bool(ob), ob.type == "MESH"]) + + def execute(self, context): + """Moves Pivot Point to Object Origin. + + Moves Pivot Point to Object Origin in active scene + + Args: + context: Blender bpy.context instance. + + Returns: + Status Set. + """ + + scene = context.scene + pg = scene.pdt_pg + obj = bpy.context.view_layer.objects.active + if obj is None: + self.report({"ERROR"}, PDT_ERR_NO_ACT_OBJ) + return {"FINISHED"} + obj_loc = obj.matrix_world.decompose()[0] + pg.pivot_loc = obj_loc + return {"FINISHED"} + + +class PDT_OT_PivotWrite(Operator): + """Write Pivot Point Location to Object.""" + + bl_idname = "pdt.pivotwrite" + bl_label = "PDT Write PP to Object?" + + @classmethod + def poll(cls, context): + ob = context.object + if ob is None: + return False + return all([bool(ob), ob.type == "MESH"]) + + def execute(self, context): + """Writes Pivot Point Location to Object's Custom Properties. + + Writes Pivot Point Location to Object's Custom Properties + as Vector to 'PDT_PP_LOC' - Requires Confirmation through dialogue + + Args: + context: Blender bpy.context instance. + + Note: + Uses pg.pivot_loc scene variable + + Returns: + Status Set. + """ + + scene = context.scene + pg = scene.pdt_pg + obj = bpy.context.view_layer.objects.active + if obj is None: + self.report({"ERROR"}, PDT_ERR_NO_ACT_OBJ) + return {"FINISHED"} + obj["PDT_PP_LOC"] = pg.pivot_loc + return {"FINISHED"} + + def invoke(self, context, event): + return context.window_manager.invoke_props_dialog(self) + + def draw(self, context): + row = self.layout + row.label(text=PDT_CON_AREYOURSURE) + + +class PDT_OT_PivotRead(Operator): + """Read Pivot Point Location from Object.""" + + bl_idname = "pdt.pivotread" + bl_label = "PDT Read PP" + + @classmethod + def poll(cls, context): + ob = context.object + if ob is None: + return False + return all([bool(ob), ob.type == "MESH"]) + + def execute(self, context): + """Reads Pivot Point Location from Object's Custom Properties. + + Sets Pivot Point Location from Object's Custom Properties + using 'PDT_PP_LOC' + + Args: + context: Blender bpy.context instance. + + Note: + Uses pg.pivot_loc scene variable + + Returns: + Status Set. + """ + + scene = context.scene + pg = scene.pdt_pg + obj = bpy.context.view_layer.objects.active + if obj is None: + self.report({"ERROR"}, PDT_ERR_NO_ACT_OBJ) + return {"FINISHED"} + if "PDT_PP_LOC" in obj: + pg.pivot_loc = obj["PDT_PP_LOC"] + return {"FINISHED"} + else: + self.report({"ERROR"}, PDT_ERR_NOPPLOC) + return {"FINISHED"} diff --git a/precision_drawing_tools/pdt_view.py b/precision_drawing_tools/pdt_view.py new file mode 100644 index 00000000..77919877 --- /dev/null +++ b/precision_drawing_tools/pdt_view.py @@ -0,0 +1,241 @@ +# ***** BEGIN GPL LICENSE BLOCK ***** +# +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software Foundation, +# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# ***** END GPL LICENCE BLOCK ***** +# +# ----------------------------------------------------------------------- +# Author: Alan Odom (Clockmender), Rune Morling (ermo) Copyright (c) 2019 +# ----------------------------------------------------------------------- +# +import bpy +from bpy.types import Operator +from math import pi +from mathutils import Quaternion +from .pdt_functions import euler_to_quaternion + + +class PDT_OT_ViewRot(Operator): + """Rotate View using X Y Z Absolute Rotations.""" + + bl_idname = "pdt.viewrot" + bl_label = "Rotate View" + bl_options = {"REGISTER", "UNDO"} + + def execute(self, context): + """View Rotation by Absolute Values. + + Rotations are converted to 3x3 Quaternion Rotation Matrix. + This is an Absolute Rotation, not an Incremental Orbit. + + Args: + context: Blender bpy.context instance. + + Notes: + Uses pg.rotation_coords scene variables + + Returns: + Status Set. + """ + + scene = context.scene + pg = scene.pdt_pg + areas = [a for a in context.screen.areas if a.type == "VIEW_3D"] + if len(areas) > 0: + roll_value = euler_to_quaternion( + pg.rotation_coords.x * pi / 180, + pg.rotation_coords.y * pi / 180, + pg.rotation_coords.z * pi / 180 + ) + areas[0].spaces.active.region_3d.view_rotation = roll_value + return {"FINISHED"} + + +class PDT_OT_vRotL(Operator): + """Orbit View to Left by Angle.""" + + bl_idname = "pdt.viewleft" + bl_label = "Rotate Left" + bl_options = {"REGISTER", "UNDO"} + + def execute(self, context): + """View Orbit Left by Delta Value. + + Orbits view to the left about its vertical axis + + Args: + context: Blender bpy.context instance. + + Notes: + Uses pg.vrotangle scene variable + + Returns: Status Set. + """ + + scene = context.scene + pg = scene.pdt_pg + areas = [a for a in context.screen.areas if a.type == "VIEW_3D"] + if len(areas) > 0: + bpy.ops.view3d.view_orbit(angle=(pg.vrotangle * pi / 180), type="ORBITLEFT") + return {"FINISHED"} + + +class PDT_OT_vRotR(Operator): + """Orbit View to Right by Angle.""" + + bl_idname = "pdt.viewright" + bl_label = "Rotate Right" + bl_options = {"REGISTER", "UNDO"} + + def execute(self, context): + """View Orbit Right by Delta Value. + + Orbits view to the right about its vertical axis + + Args: + context: Blender bpy.context instance. + + Notes: + Uses pg.vrotangle scene variable + + Returns: + Status Set. + """ + + scene = context.scene + pg = scene.pdt_pg + areas = [a for a in context.screen.areas if a.type == "VIEW_3D"] + if len(areas) > 0: + bpy.ops.view3d.view_orbit(angle=(pg.vrotangle * pi / 180), type="ORBITRIGHT") + return {"FINISHED"} + + +class PDT_OT_vRotU(Operator): + """Orbit View to Up by Angle.""" + + bl_idname = "pdt.viewup" + bl_label = "Rotate Up" + bl_options = {"REGISTER", "UNDO"} + + def execute(self, context): + """View Orbit Up by Delta Value. + + Orbits view up about its horizontal axis + + Args: + context: Blender bpy.context instance. + + Notes: + Uses pg.vrotangle scene variable + + Returns: + Status Set. + """ + + scene = context.scene + pg = scene.pdt_pg + areas = [a for a in context.screen.areas if a.type == "VIEW_3D"] + if len(areas) > 0: + bpy.ops.view3d.view_orbit(angle=(pg.vrotangle * pi / 180), type="ORBITUP") + return {"FINISHED"} + + +class PDT_OT_vRotD(Operator): + """Orbit View to Down by Angle.""" + + bl_idname = "pdt.viewdown" + bl_label = "Rotate Down" + bl_options = {"REGISTER", "UNDO"} + + def execute(self, context): + """View Orbit Down by Delta Value. + + Orbits view down about its horizontal axis + + Args: + context: Blender bpy.context instance. + + Notes: + Uses pg.vrotangle scene variable + + Returns: + Status Set. + """ + + scene = context.scene + pg = scene.pdt_pg + areas = [a for a in context.screen.areas if a.type == "VIEW_3D"] + if len(areas) > 0: + bpy.ops.view3d.view_orbit(angle=(pg.vrotangle * pi / 180), type="ORBITDOWN") + return {"FINISHED"} + + +class PDT_OT_vRoll(Operator): + """Roll View by Angle.""" + + bl_idname = "pdt.viewroll" + bl_label = "Roll View" + bl_options = {"REGISTER", "UNDO"} + + def execute(self, context): + """View Roll by Delta Value. + + Rolls view about its normal axis + + Args: + context: Blender bpy.context instance. + + Notes: + Uses pg.vrotangle scene variable + + Returns: + Status Set. + """ + + scene = context.scene + pg = scene.pdt_pg + areas = [a for a in context.screen.areas if a.type == "VIEW_3D"] + if len(areas) > 0: + bpy.ops.view3d.view_roll(angle=(pg.vrotangle * pi / 180), type="ANGLE") + return {"FINISHED"} + + +class PDT_OT_viso(Operator): + """Isometric View.""" + + bl_idname = "pdt.viewiso" + bl_label = "Isometric View" + bl_options = {"REGISTER", "UNDO"} + + def execute(self, context): + """Set Isometric View. + + Set view orientation to Isometric + + Args: + context: Blender bpy.context instance. + + Returns: + Status Set. + """ + + areas = [a for a in context.screen.areas if a.type == "VIEW_3D"] + if len(areas) > 0: + # Try working this out in your head! + areas[0].spaces.active.region_3d.view_rotation = Quaternion( + (0.8205, 0.4247, -0.1759, -0.3399) + ) + return {"FINISHED"} diff --git a/precision_drawing_tools/pdt_xall.py b/precision_drawing_tools/pdt_xall.py new file mode 100644 index 00000000..f27ae6c5 --- /dev/null +++ b/precision_drawing_tools/pdt_xall.py @@ -0,0 +1,195 @@ +# ##### BEGIN GPL LICENSE BLOCK ##### +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software Foundation, +# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# ##### END GPL LICENSE BLOCK ##### +# +# <pep8 compliant> +# +# ---------------------------------------------------------- +# Author: Zeffii +# Modified by: Alan Odom (Clockmender) & Rune Morling (ermo) +# ---------------------------------------------------------- +# +import bpy +import bmesh +from mathutils.geometry import intersect_line_line as LineIntersect +import itertools +from collections import defaultdict +from . import pdt_cad_module as cm + + +def order_points(edge, point_list): + """Order these edges from distance to v1, then sandwich the sorted list with v1, v2.""" + v1, v2 = edge + + def dist(co): + return (v1 - co).length + + point_list = sorted(point_list, key=dist) + return [v1] + point_list + [v2] + + +def remove_permutations_that_share_a_vertex(bm, permutations): + """Get useful Permutations.""" + + final_permutations = [] + for edges in permutations: + raw_vert_indices = cm.vertex_indices_from_edges_tuple(bm, edges) + if len(set(raw_vert_indices)) < 4: + continue + + # reaches this point if they do not share. + final_permutations.append(edges) + + return final_permutations + + +def get_valid_permutations(bm, edge_indices): + """Get useful Permutations.""" + + raw_permutations = itertools.permutations(edge_indices, 2) + permutations = [r for r in raw_permutations if r[0] < r[1]] + return remove_permutations_that_share_a_vertex(bm, permutations) + + +def can_skip(closest_points, vert_vectors): + """Check if the intersection lies on both edges and return True + when criteria are not met, and thus this point can be skipped.""" + + if not closest_points: + return True + if not isinstance(closest_points[0].x, float): + return True + if cm.num_edges_point_lies_on(closest_points[0], vert_vectors) < 2: + return True + + # if this distance is larger than than 1.0e-5, we can skip it. + cpa, cpb = closest_points + return (cpa - cpb).length > 1.0e-5 + + +def get_intersection_dictionary(bm, edge_indices): + """Return a dictionary of edge indices and points found on those edges.""" + + bm.verts.ensure_lookup_table() + bm.edges.ensure_lookup_table() + + permutations = get_valid_permutations(bm, edge_indices) + + k = defaultdict(list) + d = defaultdict(list) + + for edges in permutations: + raw_vert_indices = cm.vertex_indices_from_edges_tuple(bm, edges) + vert_vectors = cm.vectors_from_indices(bm, raw_vert_indices) + + points = LineIntersect(*vert_vectors) + + # some can be skipped. (NaN, None, not on both edges) + if can_skip(points, vert_vectors): + continue + + # reaches this point only when an intersection happens on both edges. + [k[edge].append(points[0]) for edge in edges] + + # k will contain a dict of edge indices and points found on those edges. + for edge_idx, unordered_points in k.items(): + tv1, tv2 = bm.edges[edge_idx].verts + v1 = bm.verts[tv1.index].co + v2 = bm.verts[tv2.index].co + ordered_points = order_points((v1, v2), unordered_points) + d[edge_idx].extend(ordered_points) + + return d + + +def update_mesh(bm, int_dict): + """Make new geometry (delete old first).""" + + oe = bm.edges + ov = bm.verts + + new_verts = [] + collect = new_verts.extend + for _, point_list in int_dict.items(): + num_edges_to_add = len(point_list) - 1 + for i in range(num_edges_to_add): + a = ov.new(point_list[i]) + b = ov.new(point_list[i + 1]) + oe.new((a, b)) + bm.normal_update() + collect([a, b]) + + bmesh.ops.delete(bm, geom=[edge for edge in bm.edges if edge.select], context="EDGES") + bmesh.ops.remove_doubles(bm, verts=bm.verts, dist=0.0001) + + +def unselect_nonintersecting(bm, d_edges, edge_indices): + """Deselects Non-Intersection Edges""" + + if len(edge_indices) > len(d_edges): + reserved_edges = set(edge_indices) - set(d_edges) + for edge in reserved_edges: + bm.edges[edge].select = False + + +class PDT_OT_IntersectAllEdges(bpy.types.Operator): + """Cut Selected Edges at All Intersections.""" + + bl_idname = "pdt.intersectall" + bl_label = "Intersect All Edges" + bl_options = {"REGISTER", "UNDO"} + + @classmethod + def poll(cls, context): + ob = context.active_object + if ob is None: + return False + return ob is not None and ob.type == "MESH" and ob.mode == "EDIT" + + def execute(self, context): + """Computes All intersections with Crossing Geometry. + + Deletes original edges and replaces with new intersected edges + + Args: + context: Blender bpy.context instance. + + Returns: + Status Set. + """ + + # must force edge selection mode here + bpy.context.tool_settings.mesh_select_mode = (False, True, False) + + obj = context.active_object + if obj.mode == "EDIT": + bm = bmesh.from_edit_mesh(obj.data) + + selected_edges = [edge for edge in bm.edges if edge.select] + edge_indices = [i.index for i in selected_edges] + + int_dict = get_intersection_dictionary(bm, edge_indices) + + unselect_nonintersecting(bm, int_dict.keys(), edge_indices) + update_mesh(bm, int_dict) + + bmesh.update_edit_mesh(obj.data) + else: + msg = PDT_ERR_EDIT_MODE + obj.mode + ")" + self.report({"ERROR"}, msg) + + return {"FINISHED"} |