Newer
Older
# ***** 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
from .pdt_functions import (
debug,
from .pdt_command_functions import (
command_maths,
vector_build,
move_cursor_pivot,
join_two_vertices,
set_angle_distance_two,
set_angle_distance_three,
origin_to_cursor,
taper,
placement_normal,
placement_centre,
placement_intersect,
)
from .pdt_msg_strings import (
PDT_ERR_ADDVEDIT,
PDT_ERR_BAD3VALS,
PDT_ERR_BADFLETTER,
PDT_ERR_CHARS_NUM,
PDT_ERR_DUPEDIT,
PDT_ERR_EXTEDIT,
PDT_ERR_FACE_SEL,
PDT_ERR_FILEDIT,
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
)
from .pdt_bix import add_line_to_bisection
from .pdt_etof import extend_vertex
from .pdt_xall import intersect_all
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="i: Fillet & Intersect 2 Disconnected 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="o: Send Maths Calculation to Output")
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")
class PDT_OT_CommandReRun(Operator):
"""Repeat Current Displayed Command."""
bl_idname = "pdt.command_rerun"
bl_label = "Re-run Current Command"
bl_options = {"REGISTER", "UNDO"}
def execute(self, context):
"""Repeat Current Command Line Input.
Args:
context: Blender bpy.context instance.
Returns:
Nothing.
"""
command_run(self, context)
return {"FINISHED"}
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 'operation' - 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, O = Output (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)
Returns:
Nothing.
"""
scene = context.scene
pg = scene.pdt_pg
# Check First Letter.
if command == "?" or command.lower() == "help":
# fmt: off
context.window_manager.popup_menu(pdt_help, title="PDT Command Line Help", icon="INFO")
# fmt: on
return
elif command == "":
return
elif command.upper() == "J2V":
return
elif command.upper() == "AD2":
set_angle_distance_two(context)
return
elif command.upper() == "AD3":
set_angle_distance_three(context)
return
elif command.upper() == "OTC":
return
elif command.upper() == "TAP":
taper(self, context)
add_line_to_bisection(context)
return
elif command.upper() == "ETF":
extend_vertex(self, context)
return
elif command.upper() == "INTALL":
return
elif command.upper()[1:4] == "NML":
placement_normal(context, command.upper()[0])
return
elif command.upper()[1:4] == "CEN":
placement_centre(context, command.upper()[0])
return
elif command.upper()[1:4] == "INT":
placement_intersect(context, command.upper()[0])
pg.error = PDT_ERR_CHARS_NUM
context.window_manager.popup_menu(oops, title="Error", icon="ERROR")
return
operation = command[0].upper()
if operation 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
# Check Second Letter.
if operation == "F":
if mode not in {"v", "e", "i"}:
pg.error = f"'{mode}' {PDT_ERR_NON_VALID} '{operation}'"
context.window_manager.popup_menu(oops, title="Error", icon="ERROR")
return
elif operation in {"D", "E"}:
if mode not in {"d", "i"}:
pg.error = f"'{mode}' {PDT_ERR_NON_VALID} '{operation}'"
context.window_manager.popup_menu(oops, title="Error", icon="ERROR")
return
elif operation == "M":
if mode not in {"a", "d", "i", "p", "o", "x", "y", "z"}:
pg.error = f"'{mode}' {PDT_ERR_NON_VALID} '{operation}'"
context.window_manager.popup_menu(oops, title="Error", icon="ERROR")
return
else:
if mode not in {"a", "d", "i", "p"}:
pg.error = f"'{mode}' {PDT_ERR_NON_VALID} '{operation}'"
context.window_manager.popup_menu(oops, title="Error", icon="ERROR")
return
# --------------
# Maths Operation
if operation == "M":
command_maths(context, mode, pg, command[2:], mode)
# -----------------------------------------------------
# Not a Maths Operation, so let's parse the command line
values = command[2:].split(",")
try:
_ = float(r)
good = True
except ValueError:
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
bm, good = obj_check(obj, scene, operation)
if good:
obj_loc = obj.matrix_world.decompose()[0]
if mode_s == 'SEL' and bm is not None and mode not in {"a"}:
if len(bm.select_history) == 0:
if len([v for v in bm.verts if v.select]) == 0:
pg.error = PDT_ERR_NO_SEL_GEOM
context.window_manager.popup_menu(oops, title="Error", icon="ERROR")
return
else:
verts = [v for v in bm.verts if v.select]
else:
verts = bm.select_history
else:
verts = []
debug(f"command: {command}")
debug(f"obj: {obj}, bm: {bm}, obj_loc: {obj_loc}")
# ---------------------
# Cursor or Pivot Point
if operation in {"C", "P"}:
# Absolute/Global Coordinates, or Delta/Relative Coordinates
if mode in {"a", "d"}:
valid_result, vector_delta = vector_build(context, pg, obj, operation, values, 3)
# Direction/Polar Coordinates
elif mode == "i":
valid_result, vector_delta = vector_build(context, pg, obj, operation, values, 2)
# Percent Options
elif mode == "p":
valid_result, vector_delta = vector_build(context, pg, obj, operation, values, 1)
if valid_result:
move_cursor_pivot(context, pg, obj, verts, operation,
return
# ------------------------
# Move Vertices or Objects
elif operation == "G":
if obj.mode == "EDIT":
if len(verts) == 0:
pg.error = PDT_ERR_NO_SEL_GEOM
context.window_manager.popup_menu(oops, title="Error", icon="ERROR")
return
# Absolute/Global Coordinates
if mode == "a":
valid_result, vector_delta = vector_build(context, pg, obj, operation, values, 3)
if not valid_result:
return
if obj.mode == "EDIT":
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
)
elif obj.mode == "OBJECT":
for ob in context.view_layer.objects.selected:
ob.location = vector_delta
elif mode in {"d", "i"}:
if mode == "d":
# Delta/Relative Coordinates
valid_result, vector_delta = vector_build(context, pg, obj, operation, values, 3)
if not valid_result:
return
else:
# Direction/Polar Coordinates
valid_result, vector_delta = vector_build(context, pg, obj, operation, values, 2)
if not valid_result:
return
if vector_delta is not None:
if obj.mode == "EDIT":
bmesh.ops.translate(
bm, verts=[v for v in bm.verts if v.select], vec=vector_delta
)
elif obj.mode == "OBJECT":
for ob in context.view_layer.objects.selected:
ob.location = obj_loc + vector_delta
# Percent Options
elif mode == "p":
valid_result, vector_delta = vector_build(context, pg, obj, operation, values, 1)
if not valid_result:
return
if vector_delta is not None:
if obj.mode == 'EDIT':
verts[-1].co = vector_delta
elif obj.mode == "OBJECT":
obj.location = vector_delta
if obj.mode == 'EDIT':
bmesh.update_edit_mesh(obj.data)
bm.select_history.clear()
return
# --------------
# Add New Vertex
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":
valid_result, vector_delta = vector_build(context, pg, obj, operation, values, 3)
if not valid_result:
return
new_vertex = bm.verts.new(vector_delta - obj_loc)
# Delta/Relative Coordinates
elif mode == "d":
valid_result, vector_delta = vector_build(context, pg, obj, operation, values, 3)
if not valid_result:
return
new_vertex = bm.verts.new(verts[-1].co + vector_delta)
# Direction/Polar Coordinates
valid_result, vector_delta = vector_build(context, pg, obj, operation, values, 2)
if not valid_result:
return
new_vertex = bm.verts.new(verts[-1].co + vector_delta)
# Percent Options
elif mode == "p":
valid_result, vector_delta = vector_build(context, pg, obj, operation, values, 1)
if not valid_result:
return
new_vertex = bm.verts.new(vector_delta)
for v in [v for v in bm.verts if v.select]:
v.select_set(False)
new_vertex.select_set(True)
bmesh.update_edit_mesh(obj.data)
bm.select_history.clear()
return
# -----------
# Split Edges
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":
valid_result, vector_delta = vector_build(context, pg, obj, operation, values, 3)
if not valid_result:
return
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)]
new_vertex = new_verts[0]
new_vertex.co = vector_delta - obj_loc
# Delta/Relative Coordinates
elif mode == "d":
valid_result, vector_delta = vector_build(context, pg, obj, operation, values, 3)
if not valid_result:
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)]
bmesh.ops.translate(bm, verts=new_verts, vec=vector_delta)
# Directional/Polar Coordinates
elif mode == "i":
valid_result, vector_delta = vector_build(context, pg, obj, operation, values, 2)
if not valid_result:
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)]
bmesh.ops.translate(bm, verts=new_verts, vec=vector_delta)
# Percent Options
elif mode == "p":
valid_result, vector_delta = vector_build(context, pg, obj, operation, values, 1)
if not valid_result:
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
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)]
new_vertex = new_verts[0]
new_vertex.co = 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()
return
# ----------------
# Extrude Vertices
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":
valid_result, vector_delta = vector_build(context, pg, obj, operation, values, 3)
if not valid_result:
return
new_vertex = bm.verts.new(vector_delta - obj_loc)
for v in verts:
bm.edges.new([v, new_vertex])
bmesh.ops.remove_doubles(
bm, verts=[v for v in bm.verts if v.select], dist=0.0001
)
# Delta/Relative Coordinates
elif mode == "d":
valid_result, vector_delta = vector_build(context, pg, obj, operation, values, 3)
if not valid_result:
return
new_vertex = bm.verts.new(v.co)
new_vertex.co = new_vertex.co + vector_delta
bm.edges.new([v, new_vertex])
# Direction/Polar Coordinates
elif mode == "i":
valid_result, vector_delta = vector_build(context, pg, obj, operation, values, 2)
if not valid_result:
return
new_vertex = bm.verts.new(v.co)
new_vertex.co = new_vertex.co + vector_delta
bm.edges.new([v, new_vertex])
# Percent Options
elif mode == "p":
valid_result, vector_delta = vector_build(context, pg, obj, operation, values, 1)
if not valid_result:
return
verts = [v for v in bm.verts if v.select].copy()
if len(verts) == 0:
pg.error = PDT_ERR_NO_SEL_GEOM
context.window_manager.popup_menu(oops, title="Error", icon="ERROR")
return
new_vertex = bm.verts.new(vector_delta)
if ext_a:
for v in [v for v in bm.verts if v.select]:
v.select_set(False)
else:
bm.edges.new([verts[-1], new_vertex])
new_vertex.select_set(True)
bmesh.update_edit_mesh(obj.data)
bm.select_history.clear()
return
# ----------------
# Extrude Geometry
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":
valid_result, vector_delta = vector_build(context, pg, obj, operation, values, 3)
if not valid_result:
return
# Direction/Polar Coordinates
elif mode == "i":
valid_result, vector_delta = vector_build(context, pg, obj, values, 2)
if not valid_result:
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)
update_sel(bm, verts_extr, edges_extr, faces_extr)
bmesh.update_edit_mesh(obj.data)
bm.select_history.clear()
return
# ------------------
# Duplicate Geometry
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":
valid_result, vector_delta = vector_build(context, pg, obj, operation, values, 3)
if not valid_result:
return
# Direction/Polar Coordinates
elif mode == "i":
valid_result, vector_delta = vector_build(context, pg, obj, operation, values, 2)
if not valid_result:
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)
update_sel(bm, verts_dupe, edges_dupe, faces_dupe)
bmesh.update_edit_mesh(obj.data)
bm.select_history.clear()
return
# ---------------
# Fillet Geometry
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
pg.error = PDT_ERR_BAD3VALS
context.window_manager.popup_menu(oops, title="Error", icon="ERROR")
return
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(values[0])
_segments = int(values[1])
if _segments < 1:
_segments = 1 # This is a single, flat segment (ignores profile)
if _profile < 0.0 or _profile > 1.0:
_profile = 0.5 # This is a circular profile
if mode == "i":
# Fillet & Intersect Two Edges
edges = [e for e in bm.edges if e.select]
if len(edges) == 2 and len(verts) == 4:
v_active = edges[0].verts[0]
v_other = edges[0].verts[1]
v_last = edges[1].verts[0]
v_first = edges[1].verts[1]
vector_delta, done = intersection(v_active.co,
v_other.co,
v_last.co,
v_first.co,
plane
)
if not done:
errmsg = f"{PDT_ERR_INT_LINES} {plane} {PDT_LAB_PLANE}"
self.report({"ERROR"}, errmsg)
return {"FINISHED"}
if (v_active.co - vector_delta).length < (v_other.co - vector_delta).length:
v_active.co = vector_delta
v_other.select_set(False)
v_other.co = vector_delta
v_active.select_set(False)
if (v_last.co - vector_delta).length < (v_first.co - vector_delta).length:
v_last.co = vector_delta
v_first.select_set(False)
v_first.co = vector_delta
v_last.select_set(False)
bmesh.ops.remove_doubles(bm, verts=bm.verts, dist=0.0001)
bpy.ops.mesh.bevel(
offset_type="OFFSET",
offset=_offset,
segments=_segments,
profile=_profile,
vertex_only=vert_bool
)
return