Welcome to mirror list, hosted at ThFree Co, Russian Federation.

git.blender.org/blender-addons.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorRune Morling <ermo.blender.org@spammesenseless.net>2019-12-09 02:52:27 +0300
committerRune Morling <ermo.blender.org@spammesenseless.net>2019-12-09 02:52:27 +0300
commite7594d45b1dd3d38a795bcafd9c0221ce7922a8d (patch)
tree4dfba991cefea30c97ca99a67a8b226d4d58dbcd /precision_drawing_tools
parentd94489ea875a20f2bc0aa2b5e490d5ab0c230833 (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__.py539
-rw-r--r--precision_drawing_tools/pdt_bix.py118
-rw-r--r--precision_drawing_tools/pdt_cad_module.py210
-rw-r--r--precision_drawing_tools/pdt_command.py880
-rw-r--r--precision_drawing_tools/pdt_design.py1478
-rw-r--r--precision_drawing_tools/pdt_etof.py132
-rw-r--r--precision_drawing_tools/pdt_functions.py688
-rw-r--r--precision_drawing_tools/pdt_library.py173
-rw-r--r--precision_drawing_tools/pdt_menus.py330
-rw-r--r--precision_drawing_tools/pdt_msg_strings.py171
-rw-r--r--precision_drawing_tools/pdt_pivot_point.py442
-rw-r--r--precision_drawing_tools/pdt_view.py241
-rw-r--r--precision_drawing_tools/pdt_xall.py195
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"}