PSD-Tutorials.de
Forum für Design, Fotografie & Bildbearbeitung
Tutkit
Agentur
Hilfe
Kontakt
Start
Forum
Aktuelles
Besonderer Inhalt
Foren durchsuchen
Tutorials
News
Anmelden
Kostenlos registrieren
Aktuelles
Suche
Suche
Nur Titel durchsuchen
Von:
Menü
Anmelden
Kostenlos registrieren
App installieren
Installieren
JavaScript ist deaktiviert. Für eine bessere Darstellung aktiviere bitte JavaScript in deinem Browser, bevor du fortfährst.
Du verwendest einen veralteten Browser. Es ist möglich, dass diese oder andere Websites nicht korrekt angezeigt werden.
Du solltest ein Upgrade durchführen oder einen
alternativen Browser
verwenden.
Antworten auf deine Fragen:
Neues Thema erstellen
Start
Forum
3D: Modeling, Texturen, Licht, Animation, Rendern
Blender
Blender - Extrusion mit 30° Seitenwinkel
Beitrag
<blockquote data-quote="noltehans" data-source="post: 2735986" data-attributes="member: 311830"><p>Wunderbar, das ist doch die Hauptsache.</p><p></p><p>Ich habe übrigens noch eine Möglichkeit gefunden Kanten in einem bestimmten Winkel zu extrudieren.</p><p>Du musst die abgeschrägten Kanten aber per Hand füllen (also den Deckel drauf machen).</p><p>Das kleine Addon nennt sich mesh_offset_edges.py</p><p></p><p>Kante auswählen, Strg+E -> Offset Edges (und dann z.B. Extrude)</p><p></p><p>Den Quellcode in einen Texteditor laden und als mesh_offset_edges.py abspeichern.</p><p></p><p>[CODE]# ***** BEGIN GPL LICENSE BLOCK *****</p><p>#</p><p>#</p><p># This program is free software; you can redistribute it and/or</p><p># modify it under the terms of the GNU General Public License</p><p># as published by the Free Software Foundation; either version 2</p><p># of the License, or (at your option) any later version.</p><p>#</p><p># This program is distributed in the hope that it will be useful,</p><p># but WITHOUT ANY WARRANTY; without even the implied warranty of</p><p># MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the</p><p># GNU General Public License for more details.</p><p>#</p><p># You should have received a copy of the GNU General Public License</p><p># along with this program; if not, write to the Free Software Foundation,</p><p># Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.</p><p>#</p><p># ***** END GPL LICENCE BLOCK *****</p><p></p><p>bl_info = {</p><p> "name": "Hidesato Offset Edges",</p><p> "author": "Hidesato Ikeya",</p><p> "version": (0, 4, 0),</p><p> "blender": (2, 82, 0),</p><p> "location": "VIEW3D > Edge menu(CTRL-E) > Offset Edges",</p><p> "description": "Offset Edges",</p><p> "warning": "",</p><p> #"wiki_url": "http://wiki.blender.org/index.php/Extensions:2.6/Py/Scripts/Modeling/offset_edges",</p><p> "tracker_url": "",</p><p> "category": "Mesh"}</p><p></p><p>import math</p><p>from math import sin, cos, pi, copysign, radians, degrees, atan, sqrt</p><p>import bpy</p><p>import mathutils</p><p>from bpy_extras import view3d_utils</p><p>import bmesh</p><p>from mathutils import Vector</p><p>from time import perf_counter</p><p></p><p>X_UP = Vector((1.0, .0, .0))</p><p>Y_UP = Vector((.0, 1.0, .0))</p><p>Z_UP = Vector((.0, .0, 1.0))</p><p>ZERO_VEC = Vector((.0, .0, .0))</p><p>ANGLE_1 = pi / 180</p><p>ANGLE_90 = pi / 2</p><p>ANGLE_180 = pi</p><p>ANGLE_360 = 2 * pi</p><p></p><p>class OffsetEdgesPreferences(bpy.types.AddonPreferences):</p><p> bl_idname = __name__</p><p> </p><p> interactive: bpy.props.BoolProperty(</p><p> name = "Interactive",</p><p> description = "makes operation interactive",</p><p> default = True)</p><p> free_move: bpy.props.BoolProperty(</p><p> name = "Free Move",</p><p> description = "enables to adjust both width and depth while pressing ctrl-key",</p><p> default = False)</p><p></p><p> def draw(self, context):</p><p> layout = self.layout</p><p> row = layout.row()</p><p> row.prop(self, "interactive")</p><p> if self.interactive:</p><p> row.prop(self, "free_move")</p><p></p><p>#######################################################################</p><p></p><p>class OffsetBase:</p><p> threshold: bpy.props.FloatProperty(</p><p> name="Flat Face Threshold", default=radians(0.05), precision=5,</p><p> step=1.0e-4, subtype='ANGLE',</p><p> description="If difference of angle between two adjacent faces is "</p><p> "below this value, those faces are regarded as flat.",</p><p> options={'HIDDEN'})</p><p> caches_valid: bpy.props.BoolProperty(</p><p> name="Caches Valid", default=False,</p><p> options={'HIDDEN'})</p><p></p><p> _cache_offset_infos = None</p><p> _cache_edges_orig = None</p><p></p><p> def use_caches(self, context):</p><p> self.caches_valid = True</p><p></p><p> def get_caches(self, bm):</p><p> bmverts = tuple(bm.verts)</p><p> bmedges = tuple(bm.edges)</p><p></p><p> offset_infos = \</p><p> [(bmverts[vix], co, d) for vix, co, d in self._cache_offset_infos]</p><p> edges_orig = [bmedges[eix] for eix in self._cache_edges_orig]</p><p></p><p> for e in edges_orig:</p><p> e.select = False</p><p> for f in bm.faces:</p><p> f.select = False</p><p></p><p> return offset_infos, edges_orig</p><p></p><p> def save_caches(self, offset_infos, edges_orig):</p><p> self._cache_offset_infos = tuple((v.index, co, d) for v, co, d in offset_infos)</p><p> self._cache_edges_orig = tuple(e.index for e in edges_orig)</p><p></p><p> @staticmethod</p><p> def is_face_selected(ob_edit):</p><p> bpy.ops.object.mode_set(mode="OBJECT")</p><p> me = ob_edit.data</p><p> for p in me.polygons:</p><p> if p.select:</p><p> bpy.ops.object.mode_set(mode="EDIT")</p><p> return True</p><p> bpy.ops.object.mode_set(mode="EDIT")</p><p></p><p> return False</p><p> @staticmethod</p><p> def is_mirrored(ob_edit):</p><p> for mod in ob_edit.modifiers:</p><p> if mod.type == 'MIRROR' and mod.use_mirror_merge:</p><p> return True</p><p> return False</p><p></p><p> @staticmethod</p><p> def reorder_loop(verts, edges, lp_normal, adj_faces):</p><p> for i, adj_f in enumerate(adj_faces):</p><p> if adj_f is None:</p><p> continue</p><p> v1, v2 = verts[i], verts[i+1]</p><p> e = edges[i]</p><p> fv = tuple(adj_f.verts)</p><p> if fv[fv.index(v1)-1] is v2:</p><p> # Align loop direction</p><p> verts.reverse()</p><p> edges.reverse()</p><p> adj_faces.reverse()</p><p> if lp_normal.dot(adj_f.normal) < .0:</p><p> lp_normal *= -1</p><p> break</p><p> else:</p><p> # All elements in adj_faces are None</p><p> for v in verts:</p><p> if v.normal != ZERO_VEC:</p><p> if lp_normal.dot(v.normal) < .0:</p><p> verts.reverse()</p><p> edges.reverse()</p><p> lp_normal *= -1</p><p> break</p><p></p><p> return verts, edges, lp_normal, adj_faces</p><p></p><p> @staticmethod</p><p> def get_cross_rail(vec_tan, vec_edge_r, vec_edge_l, normal_r, normal_l):</p><p> # Cross rail is a cross vector between normal_r and normal_l.</p><p></p><p> vec_cross = normal_r.cross(normal_l)</p><p> if vec_cross.dot(vec_tan) < .0:</p><p> vec_cross *= -1</p><p> cos_min = min(vec_tan.dot(vec_edge_r), vec_tan.dot(-vec_edge_l))</p><p> cos = vec_tan.dot(vec_cross)</p><p> if cos >= cos_min:</p><p> vec_cross.normalize()</p><p> return vec_cross</p><p> else:</p><p> return None</p><p></p><p> @staticmethod</p><p> def get_edge_rail(vert, set_edges_orig):</p><p> co_edges = co_edges_selected = 0</p><p> vec_inner = None</p><p> for e in vert.link_edges:</p><p> if (e not in set_edges_orig and</p><p> (e.select or (co_edges_selected == 0 and not e.hide))):</p><p> v_other = e.other_vert(vert)</p><p> vec = v_other.co - vert.co</p><p> if vec != ZERO_VEC:</p><p> vec_inner = vec</p><p> if e.select:</p><p> co_edges_selected += 1</p><p> if co_edges_selected == 2:</p><p> return None</p><p> else:</p><p> co_edges += 1</p><p> if co_edges_selected == 1:</p><p> vec_inner.normalize()</p><p> return vec_inner</p><p> elif co_edges == 1:</p><p> # No selected edges, one unselected edge.</p><p> vec_inner.normalize()</p><p> return vec_inner</p><p> else:</p><p> return None</p><p></p><p> @staticmethod</p><p> def get_mirror_rail(mirror_plane, vec_up):</p><p> p_norm = mirror_plane[1]</p><p> mirror_rail = vec_up.cross(p_norm)</p><p> if mirror_rail != ZERO_VEC:</p><p> mirror_rail.normalize()</p><p> # Project vec_up to mirror_plane</p><p> vec_up = vec_up - vec_up.project(p_norm)</p><p> vec_up.normalize()</p><p> return mirror_rail, vec_up</p><p> else:</p><p> return None, vec_up</p><p></p><p> @staticmethod</p><p> def get_vert_mirror_pairs(set_edges_orig, mirror_planes):</p><p> if mirror_planes:</p><p> set_edges_copy = set_edges_orig.copy()</p><p> vert_mirror_pairs = dict()</p><p> for e in set_edges_orig:</p><p> v1, v2 = e.verts</p><p> for mp in mirror_planes:</p><p> p_co, p_norm, mlimit = mp</p><p> v1_dist = abs(p_norm.dot(v1.co - p_co))</p><p> v2_dist = abs(p_norm.dot(v2.co - p_co))</p><p> if v1_dist <= mlimit:</p><p> # v1 is on a mirror plane.</p><p> vert_mirror_pairs[v1] = mp</p><p> if v2_dist <= mlimit:</p><p> # v2 is on a mirror plane.</p><p> vert_mirror_pairs[v2] = mp</p><p> if v1_dist <= mlimit and v2_dist <= mlimit:</p><p> # This edge is on a mirror_plane, so should not be offsetted.</p><p> set_edges_copy.remove(e)</p><p> return vert_mirror_pairs, set_edges_copy</p><p> else:</p><p> return None, set_edges_orig</p><p></p><p> @staticmethod</p><p> def collect_mirror_planes(ob_edit):</p><p> mirror_planes = []</p><p> eob_mat_inv = ob_edit.matrix_world.inverted()</p><p> for m in ob_edit.modifiers:</p><p> if (m.type == 'MIRROR' and m.use_mirror_merge):</p><p> merge_limit = m.merge_threshold</p><p> if not m.mirror_object:</p><p> loc = ZERO_VEC</p><p> norm_x, norm_y, norm_z = X_UP, Y_UP, Z_UP</p><p> else:</p><p> mirror_mat_local = eob_mat_inv * m.mirror_object.matrix_world</p><p> loc = mirror_mat_local.to_translation()</p><p> norm_x, norm_y, norm_z, _ = mirror_mat_local.adjugated()</p><p> norm_x = norm_x.to_3d().normalized()</p><p> norm_y = norm_y.to_3d().normalized()</p><p> norm_z = norm_z.to_3d().normalized()</p><p> if m.use_x:</p><p> mirror_planes.append((loc, norm_x, merge_limit))</p><p> if m.use_y:</p><p> mirror_planes.append((loc, norm_y, merge_limit))</p><p> if m.use_z:</p><p> mirror_planes.append((loc, norm_z, merge_limit))</p><p> return mirror_planes</p><p></p><p> @staticmethod</p><p> def collect_edges(bm):</p><p> set_edges_orig = set()</p><p> for e in bm.edges:</p><p> if e.select:</p><p> co_faces_selected = 0</p><p> for f in e.link_faces:</p><p> if f.select:</p><p> co_faces_selected += 1</p><p> if co_faces_selected == 2:</p><p> break</p><p> else:</p><p> set_edges_orig.add(e)</p><p></p><p> if not set_edges_orig:</p><p> return None</p><p></p><p> return set_edges_orig</p><p> @staticmethod</p><p> def collect_loops(set_edges_orig):</p><p> set_edges_copy = set_edges_orig.copy()</p><p></p><p> loops = [] # [v, e, v, e, ... , e, v]</p><p> while set_edges_copy:</p><p> edge_start = set_edges_copy.pop()</p><p> v_left, v_right = edge_start.verts</p><p> lp = [v_left, edge_start, v_right]</p><p> reverse = False</p><p> while True:</p><p> edge = None</p><p> for e in v_right.link_edges:</p><p> if e in set_edges_copy:</p><p> if edge:</p><p> # Overlap detected.</p><p> return None</p><p> edge = e</p><p> set_edges_copy.remove(e)</p><p> if edge:</p><p> v_right = edge.other_vert(v_right)</p><p> lp.extend((edge, v_right))</p><p> continue</p><p> else:</p><p> if v_right is v_left:</p><p> # Real loop.</p><p> loops.append(lp)</p><p> break</p><p> elif reverse is False:</p><p> # Right side of half loop.</p><p> # Reversing the loop to operate same procedure on the left side.</p><p> lp.reverse()</p><p> v_right, v_left = v_left, v_right</p><p> reverse = True</p><p> continue</p><p> else:</p><p> # Half loop, completed.</p><p> loops.append(lp)</p><p> break</p><p> return loops</p><p></p><p> @staticmethod</p><p> def calc_loop_normal(verts, fallback=Z_UP):</p><p> # Calculate normal from verts using Newell's method.</p><p> normal = ZERO_VEC.copy()</p><p></p><p> if verts[0] is verts[-1]:</p><p> # Perfect loop</p><p> range_verts = range(1, len(verts))</p><p> else:</p><p> # Half loop</p><p> range_verts = range(0, len(verts))</p><p></p><p> for i in range_verts:</p><p> v1co, v2co = verts[i-1].co, verts[i].co</p><p> normal.x += (v1co.y - v2co.y) * (v1co.z + v2co.z)</p><p> normal.y += (v1co.z - v2co.z) * (v1co.x + v2co.x)</p><p> normal.z += (v1co.x - v2co.x) * (v1co.y + v2co.y)</p><p></p><p> if normal != ZERO_VEC:</p><p> normal.normalize()</p><p> else:</p><p> normal = fallback</p><p></p><p> return normal</p><p></p><p> @staticmethod</p><p> def get_adj_faces(edges):</p><p> adj_faces = []</p><p> for e in edges:</p><p> adj_f = None</p><p> co_adj = 0</p><p> for f in e.link_faces:</p><p> # Search an adjacent face.</p><p> # Selected face has precedance.</p><p> if not f.hide and f.normal != ZERO_VEC:</p><p> adj_exist = True</p><p> adj_f = f</p><p> co_adj += 1</p><p> if f.select:</p><p> adj_faces.append(adj_f)</p><p> break</p><p> else:</p><p> if co_adj == 1:</p><p> adj_faces.append(adj_f)</p><p> else:</p><p> adj_faces.append(None)</p><p> return adj_faces</p><p></p><p> @staticmethod</p><p> def get_directions(lp, vec_upward, normal_fallback, vert_mirror_pairs,</p><p> **options):</p><p> opt_follow_face = options.get("follow_face")</p><p> opt_edge_rail = options.get("edge_rail")</p><p> opt_er_only_end = options.get("edge_rail_only_end")</p><p> opt_threshold = options.get("threshold")</p><p> opt_normal_override = options.get("normal_override")</p><p></p><p> verts, edges = lp[::2], lp[1::2]</p><p> set_edges = set(edges)</p><p> if opt_normal_override is None:</p><p> lp_normal = OffsetBase.calc_loop_normal(verts, fallback=normal_fallback)</p><p> else:</p><p> lp_normal = opt_normal_override</p><p> opt_follow_face = False</p><p></p><p> ##### Loop order might be changed below.</p><p> if lp_normal.dot(vec_upward) < .0:</p><p> # Make this loop's normal towards vec_upward.</p><p> verts.reverse()</p><p> edges.reverse()</p><p> lp_normal *= -1</p><p></p><p> if opt_follow_face:</p><p> adj_faces = OffsetBase.get_adj_faces(edges)</p><p> verts, edges, lp_normal, adj_faces = \</p><p> OffsetBase.reorder_loop(verts, edges, lp_normal, adj_faces)</p><p> else:</p><p> adj_faces = (None, ) * len(edges)</p><p> ##### Loop order might be changed above.</p><p></p><p> vec_edges = tuple((e.other_vert(v).co - v.co).normalized()</p><p> for v, e in zip(verts, edges))</p><p></p><p> if verts[0] is verts[-1]:</p><p> # Real loop. Popping last vertex.</p><p> verts.pop()</p><p> HALF_LOOP = False</p><p> else:</p><p> # Half loop</p><p> HALF_LOOP = True</p><p></p><p> len_verts = len(verts)</p><p> directions = []</p><p> for i in range(len_verts):</p><p> vert = verts[i]</p><p> ix_right, ix_left = i, i-1</p><p></p><p> VERT_END = False</p><p> if HALF_LOOP:</p><p> if i == 0:</p><p> # First vert</p><p> ix_left = ix_right</p><p> VERT_END = True</p><p> elif i == len_verts - 1:</p><p> # Last vert</p><p> ix_right = ix_left</p><p> VERT_END = True</p><p></p><p> edge_right, edge_left = vec_edges[ix_right], vec_edges[ix_left]</p><p> face_right, face_left = adj_faces[ix_right], adj_faces[ix_left]</p><p></p><p> norm_right = face_right.normal if face_right else lp_normal</p><p> norm_left = face_left.normal if face_left else lp_normal</p><p> if norm_right.angle(norm_left) > opt_threshold:</p><p> # Two faces are not flat.</p><p> two_normals = True</p><p> else:</p><p> two_normals = False</p><p></p><p> tan_right = edge_right.cross(norm_right).normalized()</p><p> tan_left = edge_left.cross(norm_left).normalized()</p><p> tan_avr = (tan_right + tan_left).normalized()</p><p> norm_avr = (norm_right + norm_left).normalized()</p><p></p><p> rail = None</p><p> if two_normals or opt_edge_rail:</p><p> # Get edge rail.</p><p> # edge rail is a vector of an inner edge.</p><p> if two_normals or (not opt_er_only_end) or VERT_END:</p><p> rail = OffsetBase.get_edge_rail(vert, set_edges)</p><p> if vert_mirror_pairs and VERT_END:</p><p> if vert in vert_mirror_pairs:</p><p> rail, norm_avr = \</p><p> OffsetBase.get_mirror_rail(vert_mirror_pairs[vert], norm_avr)</p><p> if (not rail) and two_normals:</p><p> # Get cross rail.</p><p> # Cross rail is a cross vector between norm_right and norm_left.</p><p> rail = OffsetBase.get_cross_rail(</p><p> tan_avr, edge_right, edge_left, norm_right, norm_left)</p><p> if rail:</p><p> dot = tan_avr.dot(rail)</p><p> if dot > .0:</p><p> tan_avr = rail</p><p> elif dot < .0:</p><p> tan_avr = -rail</p><p></p><p> vec_plane = norm_avr.cross(tan_avr)</p><p> e_dot_p_r = edge_right.dot(vec_plane)</p><p> e_dot_p_l = edge_left.dot(vec_plane)</p><p> if e_dot_p_r or e_dot_p_l:</p><p> if e_dot_p_r > e_dot_p_l:</p><p> vec_edge, e_dot_p = edge_right, e_dot_p_r</p><p> else:</p><p> vec_edge, e_dot_p = edge_left, e_dot_p_l</p><p></p><p> vec_tan = (tan_avr - tan_avr.project(vec_edge)).normalized()</p><p> # Make vec_tan perpendicular to vec_edge</p><p> vec_up = vec_tan.cross(vec_edge)</p><p></p><p> vec_width = vec_tan - (vec_tan.dot(vec_plane) / e_dot_p) * vec_edge</p><p> vec_depth = vec_up - (vec_up.dot(vec_plane) / e_dot_p) * vec_edge</p><p> else:</p><p> vec_width = tan_avr</p><p> vec_depth = norm_avr</p><p></p><p> directions.append((vec_width, vec_depth))</p><p></p><p> return verts, directions</p><p></p><p> def get_offset_infos(self, bm, ob_edit, **options):</p><p> time = perf_counter()</p><p> opt_mirror_modifier = options.get("mirror_modifier")</p><p></p><p> set_edges_orig = self.collect_edges(bm)</p><p> if set_edges_orig is None:</p><p> self.report({'WARNING'},</p><p> "No edges are selected.")</p><p> return False, False</p><p></p><p> if opt_mirror_modifier:</p><p> mirror_planes = self.collect_mirror_planes(ob_edit)</p><p> vert_mirror_pairs, set_edges = \</p><p> self.get_vert_mirror_pairs(set_edges_orig, mirror_planes)</p><p></p><p> if set_edges:</p><p> set_edges_orig = set_edges</p><p> else:</p><p> #self.report({'WARNING'},</p><p> # "All selected edges are on mirror planes.")</p><p> vert_mirror_pairs = None</p><p> else:</p><p> vert_mirror_pairs = None</p><p> edges_orig = list(set_edges_orig)</p><p></p><p> loops = self.collect_loops(set_edges_orig)</p><p> if loops is None:</p><p> self.report({'WARNING'},</p><p> "Overlapping edge loops detected. Select discrete edge loops")</p><p> return False, False</p><p></p><p> vec_upward = (X_UP + Y_UP + Z_UP).normalized()</p><p> # vec_upward is used to unify loop normals when follow_face is off.</p><p> normal_fallback = Z_UP</p><p> #normal_fallback = Vector(context.region_data.view_matrix[2][:3])</p><p> # normal_fallback is used when loop normal cannot be calculated.</p><p></p><p> offset_infos = []</p><p> for lp in loops:</p><p> verts, directions = self.get_directions(</p><p> lp, vec_upward, normal_fallback, vert_mirror_pairs,</p><p> **options</p><p> )</p><p> if verts:</p><p> # convert vert objects to vert indexs</p><p> for v, d in zip(verts, directions):</p><p> offset_infos.append((v, v.co.copy(), d))</p><p></p><p> for e in edges_orig:</p><p> e.select = False</p><p> for f in bm.faces:</p><p> f.select = False</p><p></p><p> #print("OffsetEdges - Calculating: ", perf_counter() - time)</p><p></p><p> return offset_infos, edges_orig</p><p></p><p> @staticmethod</p><p> def extrude_and_pairing(bm, edges_orig, ref_verts):</p><p> """ ref_verts is a list of vertices, each of which should be</p><p> one end of an edge in edges_orig"""</p><p> extruded = bmesh.ops.extrude_edge_only(bm, edges=edges_orig)['geom']</p><p> n_edges = n_faces = len(edges_orig)</p><p> n_verts = len(extruded) - n_edges - n_faces</p><p></p><p> exverts = set(extruded[:n_verts])</p><p> exedges = set(extruded[n_verts:n_verts + n_edges])</p><p> #faces = set(extruded[n_verts + n_edges:])</p><p> side_edges = set(e for v in exverts for e in v.link_edges if e not in exedges)</p><p></p><p> # ref_verts[i] and ret[i] are both ends of a side edge.</p><p> exverts_ordered = \</p><p> [e.other_vert(v) for v in ref_verts for e in v.link_edges if e in side_edges]</p><p></p><p> return exverts_ordered, list(exedges), list(side_edges)</p><p></p><p> @staticmethod</p><p> def move_verts(bm, me, width, depth, offset_infos, verts_offset=None, update=True):</p><p> if verts_offset is None:</p><p> for v, co, (vec_w, vec_d) in offset_infos:</p><p> v.co = co + width * vec_w + depth * vec_d</p><p> else:</p><p> for (_, co, (vec_w, vec_d)), v in zip(offset_infos, verts_offset):</p><p> v.co = co + width * vec_w + depth * vec_d</p><p></p><p> if update:</p><p> bm.normal_update()</p><p> bmesh.update_edit_mesh(me)</p><p></p><p></p><p>class OffsetEdges(bpy.types.Operator, OffsetBase):</p><p> """Offset Edges"""</p><p> bl_idname = "mesh.hidesato_offset_edges"</p><p> bl_label = "Offset Edges"</p><p> bl_options = {'REGISTER', 'UNDO'}</p><p></p><p> follow_face: bpy.props.BoolProperty(</p><p> name="Follow Face", default=False,</p><p> description="Offset along faces around"</p><p> )</p><p> mirror_modifier: bpy.props.BoolProperty(</p><p> name="Mirror Modifier", default=False,</p><p> description="Take into account of Mirror modifier"</p><p> )</p><p> edge_rail: bpy.props.BoolProperty(</p><p> name="Edge Rail", default=False,</p><p> description="Align vertices along inner edges"</p><p> )</p><p> edge_rail_only_end: bpy.props.BoolProperty(</p><p> name="Edge Rail Only End", default=False,</p><p> description="Apply edge rail to end verts only"</p><p> )</p><p> lock_axis: bpy.props.EnumProperty(</p><p> items=[</p><p> ('none', "None", "Don't lock axis"),</p><p> ('x', "X", "Lock X axis"),</p><p> ('y', "Y", "Lock Y axis"),</p><p> ('z', "Z", "Lock Z axis"),</p><p> ('view', "VIEW", "Lock view axis")</p><p> ],</p><p> name="Lock Axis", default='none'</p><p> )</p><p></p><p> # Functions below are update functions.</p><p></p><p> def assign_angle_presets(self, context):</p><p> angle_presets = {'0В°': 0,</p><p> '15В°': radians(15),</p><p> '30В°': radians(30),</p><p> '45В°': radians(45),</p><p> '60В°': radians(60),</p><p> '75В°': radians(75),</p><p> '90В°': radians(90),}</p><p> self.angle = angle_presets[self.angle_presets]</p><p></p><p> def change_depth_mode(self, context):</p><p> if self.depth_mode == 'angle':</p><p> self.width, self.angle = OffsetEdges.depth_to_angle(self.width, self.depth)</p><p> else:</p><p> self.width, self.depth = OffsetEdges.angle_to_depth(self.width, self.angle)</p><p></p><p></p><p> def angle_to_depth(width, angle):</p><p> """Returns: (converted_width, converted_depth)"""</p><p> return width * cos(angle), width * sin(angle)</p><p></p><p></p><p> def depth_to_angle(width, depth):</p><p> """Returns: (converted_width, converted_angle)"""</p><p> ret_width = sqrt(width * width + depth * depth)</p><p></p><p> if width:</p><p> ret_angle = atan(depth / width)</p><p> elif depth == 0:</p><p> ret_angle = 0</p><p> elif depth > 0:</p><p> ret_angle = ANGLE_90</p><p> elif depth < 0:</p><p> ret_angle = -ANGLE_90</p><p></p><p> return ret_width, ret_angle</p><p></p><p> geometry_mode: bpy.props.EnumProperty(</p><p> items=[('offset', "Offset", "Offset edges"),</p><p> ('extrude', "Extrude", "Extrude edges"),</p><p> ('move', "Move", "Move selected edges")],</p><p> name="Geometory mode", default='offset',</p><p> update=OffsetBase.use_caches)</p><p> width: bpy.props.FloatProperty(</p><p> name="Width", default=.2, precision=4, step=1,</p><p> update=OffsetBase.use_caches)</p><p> flip_width: bpy.props.BoolProperty(</p><p> name="Flip Width", default=False,</p><p> description="Flip width direction",</p><p> update=OffsetBase.use_caches)</p><p> depth: bpy.props.FloatProperty(</p><p> name="Depth", default=.0, precision=4, step=1,</p><p> update=OffsetBase.use_caches)</p><p> flip_depth: bpy.props.BoolProperty(</p><p> name="Flip Depth", default=False,</p><p> description="Flip depth direction",</p><p> update=OffsetBase.use_caches)</p><p> depth_mode: bpy.props.EnumProperty(</p><p> items=[('angle', "Angle", "Angle"),</p><p> ('depth', "Depth", "Depth")],</p><p> name="Depth mode", default='angle',</p><p> update=change_depth_mode)</p><p> angle: bpy.props.FloatProperty(</p><p> name="Angle", default=0, precision=3, step=100,</p><p> min=-2*pi, max=2*pi, subtype='ANGLE', description="Angle",</p><p> update=OffsetBase.use_caches)</p><p> flip_angle: bpy.props.BoolProperty(</p><p> name="Flip Angle", default=False,</p><p> description="Flip Angle",</p><p> update=OffsetBase.use_caches)</p><p> angle_presets: bpy.props.EnumProperty(</p><p> items=[('0В°', "0В°", "0В°"),</p><p> ('15В°', "15В°", "15В°"),</p><p> ('30В°', "30В°", "30В°"),</p><p> ('45В°', "45В°", "45В°"),</p><p> ('60В°', "60В°", "60В°"),</p><p> ('75В°', "75В°", "75В°"),</p><p> ('90В°', "90В°", "90В°"), ],</p><p> name="Angle Presets", default='0В°',</p><p> update=assign_angle_presets)</p><p></p><p></p><p> def get_lockvector(self, context):</p><p> axis = self.lock_axis</p><p> if axis == 'x':</p><p> return X_UP</p><p> elif axis == 'y':</p><p> return Y_UP</p><p> elif axis == 'z':</p><p> return Z_UP</p><p> elif axis == 'view' and context.region_data:</p><p> vec = Z_UP.copy()</p><p> vec.rotate(context.region_data.view_rotation)</p><p> return vec</p><p> return None</p><p></p><p> def get_exverts(self, bm, offset_infos, edges_orig):</p><p> ref_verts = [v for v, _, _ in offset_infos]</p><p></p><p> if self.geometry_mode == 'move':</p><p> exverts = ref_verts</p><p> exedges = edges_orig</p><p> else:</p><p> exverts, exedges, side_edges = self.extrude_and_pairing(bm, edges_orig, ref_verts)</p><p> if self.geometry_mode == 'offset':</p><p> bmesh.ops.delete(bm, geom=side_edges, context="EDGES")</p><p></p><p> for e in exedges:</p><p> e.select = True</p><p></p><p> return exverts</p><p></p><p> def do_offset(self, bm, me, offset_infos, verts_offset):</p><p> if self.depth_mode == 'angle':</p><p> w = self.width if not self.flip_width else -self.width</p><p> angle = self.angle if not self.flip_angle else -self.angle</p><p> width = w * cos(angle)</p><p> depth = w * sin(angle)</p><p> else:</p><p> width = self.width if not self.flip_width else -self.width</p><p> depth = self.depth if not self.flip_depth else -self.depth</p><p></p><p> self.move_verts(bm, me, width, depth, offset_infos, verts_offset)</p><p></p><p> @classmethod</p><p> def poll(self, context):</p><p> return context.mode == 'EDIT_MESH'</p><p></p><p> def draw(self, context):</p><p> layout = self.layout</p><p> layout.row().prop(self, 'geometry_mode', expand=True)</p><p></p><p> row = layout.row(align=True)</p><p> row.prop(self, 'width')</p><p> row.prop(self, 'flip_width', icon='ARROW_LEFTRIGHT', icon_only=True)</p><p> </p><p> layout.label(text="Depth Mode:")</p><p> layout.row().prop(self, 'depth_mode', expand=True)</p><p> if self.depth_mode == 'angle':</p><p> d_mode = 'angle'</p><p> flip = 'flip_angle'</p><p> else:</p><p> d_mode = 'depth'</p><p> flip = 'flip_depth'</p><p> row = layout.row(align=True)</p><p> row.prop(self, d_mode)</p><p> row.prop(self, flip, icon='ARROW_LEFTRIGHT', icon_only=True)</p><p> if self.depth_mode == 'angle':</p><p> layout.row().prop(self, 'angle_presets', text="Presets", expand=True)</p><p></p><p> layout.label(text="Lock Axis:")</p><p> layout.row().prop(self, 'lock_axis', text="Lock Axis", expand=True)</p><p></p><p> layout.separator()</p><p> </p><p> row = layout.row()</p><p> row.prop(self, 'follow_face')</p><p> if self.follow_face:</p><p> row.prop(self, "threshold", text="Threshold")</p><p> </p><p> row = layout.row()</p><p> row.prop(self, 'edge_rail')</p><p> if self.edge_rail:</p><p> row.prop(self, 'edge_rail_only_end', text="OnlyEnd", toggle=True)</p><p></p><p> layout.prop(self, 'mirror_modifier')</p><p></p><p> #layout.operator('mesh.offset_edges', text='Repeat')</p><p></p><p></p><p> def execute(self, context):</p><p> # In edit mode</p><p> edit_object = context.edit_object</p><p> me = edit_object.data</p><p> bm = bmesh.from_edit_mesh(me)</p><p></p><p> if self.caches_valid and self._cache_offset_infos:</p><p> offset_infos, edges_orig = self.get_caches(bm)</p><p> else:</p><p> offset_infos, edges_orig = self.get_offset_infos(</p><p> bm, edit_object,</p><p> follow_face=self.follow_face,</p><p> edge_rail=self.edge_rail,</p><p> edge_rail_only_end=self.edge_rail_only_end,</p><p> mirror_modifier=self.mirror_modifier,</p><p> normal_override=self.get_lockvector(context),</p><p> threshold=self.threshold</p><p> )</p><p> if offset_infos is False:</p><p> return {'CANCELLED'}</p><p> self.save_caches(offset_infos, edges_orig)</p><p></p><p> exverts = self.get_exverts(bm, offset_infos, edges_orig)</p><p> self.do_offset(bm, me, offset_infos, exverts)</p><p></p><p> self.caches_valid = False</p><p> return {'FINISHED'}</p><p></p><p> def invoke(self, context, event):</p><p> # in edit mode</p><p> ob_edit = context.edit_object</p><p> if self.is_face_selected(ob_edit):</p><p> self.follow_face = True</p><p> if self.is_mirrored(ob_edit):</p><p> self.mirror_modifier = True</p><p></p><p> me = ob_edit.data</p><p></p><p> pref = context.preferences.addons[__name__].preferences</p><p> if pref.interactive and context.space_data.type == 'VIEW_3D':</p><p> # interactive mode</p><p> if pref.free_move:</p><p> self.depth_mode = 'depth'</p><p></p><p> ret = self.modal_prepare_bmeshes(context, ob_edit)</p><p> if ret is False:</p><p> return {'CANCELLED'}</p><p></p><p> self.width = self.angle = self.depth = .0</p><p> self.flip_depth = self.flip_angle = self.flip_width = False</p><p> self._mouse_init = self._mouse_prev = \</p><p> Vector((event.mouse_x, event.mouse_y))</p><p> context.window_manager.modal_handler_add(self)</p><p></p><p> self._factor = self.get_factor(context, self._edges_orig)</p><p></p><p> # toggle switchs of keys</p><p> self._F = 0</p><p> self._A = 0</p><p> </p><p> return {'RUNNING_MODAL'}</p><p> else:</p><p> return self.execute(context)</p><p></p><p> def modal(self, context, event):</p><p> # In edit mode</p><p> ob_edit = context.edit_object</p><p> me = ob_edit.data</p><p> pref = context.preferences.addons[__name__].preferences</p><p></p><p> if event.type == 'F':</p><p> # toggle follow_face</p><p> # event.type == 'F' is True both when 'F' is pressed and when released,</p><p> # so these codes should be executed every other loop.</p><p> self._F = 1 - self._F</p><p> if self._F:</p><p> self.follow_face = 1 - self.follow_face</p><p></p><p> self.modal_clean_bmeshes(context, ob_edit)</p><p> ret = self.modal_prepare_bmeshes(context, ob_edit)</p><p> if ret:</p><p> self.do_offset(self._bm, me, self._offset_infos, self._exverts)</p><p> return {'RUNNING_MODAL'}</p><p> else:</p><p> return {'CANCELLED'}</p><p></p><p> if event.type == 'A':</p><p> # toggle depth_mode</p><p> self._A = 1 - self._A</p><p> if self._A:</p><p> if self.depth_mode == 'angle':</p><p> self.depth_mode = 'depth'</p><p> else:</p><p> self.depth_mode = 'angle'</p><p> </p><p> context.area.header_text_set(self.create_header())</p><p></p><p> if event.type == 'MOUSEMOVE':</p><p> _mouse_current = Vector((event.mouse_x, event.mouse_y))</p><p> vec_delta = _mouse_current - self._mouse_prev</p><p></p><p> if pref.free_move or not event.ctrl:</p><p> self.width += vec_delta.x * self._factor</p><p></p><p> if event.ctrl:</p><p> if self.depth_mode == 'angle':</p><p> self.angle += vec_delta.y * ANGLE_1</p><p> elif self.depth_mode == 'depth':</p><p> self.depth += vec_delta.y * self._factor</p><p></p><p> self._mouse_prev = _mouse_current</p><p></p><p> self.do_offset(self._bm, me, self._offset_infos, self._exverts)</p><p> return {'RUNNING_MODAL'}</p><p></p><p> elif event.type == 'LEFTMOUSE':</p><p> self._bm_orig.free()</p><p> context.area.header_text_set(text=None)</p><p> return {'FINISHED'}</p><p></p><p> elif event.type in {'RIGHTMOUSE', 'ESC'}:</p><p> self.modal_clean_bmeshes(context, ob_edit)</p><p> context.area.header_text_set(text=None)</p><p> return {'CANCELLED'}</p><p></p><p> return {'RUNNING_MODAL'}</p><p></p><p> # methods below are usded in interactive mode</p><p> def create_header(self):</p><p> header = "".join(</p><p> ["Width {width: .4} ",</p><p> "Depth {depth: .4}('A' to Angle) " if self.depth_mode == 'depth' else "Angle {angle: 4.0F}В°('A' to Depth) ",</p><p> "FollowFace(F):",</p><p> "(ON)" if self.follow_face else "(OFF)",</p><p> ])</p><p></p><p> return header.format(width=self.width, depth=self.depth, angle=degrees(self.angle))</p><p></p><p> def modal_prepare_bmeshes(self, context, ob_edit):</p><p> bpy.ops.object.mode_set(mode="OBJECT")</p><p> self._bm_orig = bmesh.new()</p><p> self._bm_orig.from_mesh(ob_edit.data)</p><p> bpy.ops.object.mode_set(mode="EDIT")</p><p></p><p> self._bm = bmesh.from_edit_mesh(ob_edit.data)</p><p></p><p> self._offset_infos, self._edges_orig = self.get_offset_infos(</p><p> self._bm, ob_edit,</p><p> edge_rail=self.edge_rail,</p><p> edge_rail_only_end=self.edge_rail_only_end,</p><p> mirror_modifier=self.mirror_modifier,</p><p> normal_override=self.get_lockvector(context),</p><p> threshold=self.threshold</p><p> )</p><p> if self._offset_infos is False:</p><p> return False</p><p> self._exverts = \</p><p> self.get_exverts(self._bm, self._offset_infos, self._edges_orig)</p><p> bmesh.update_edit_mesh(ob_edit.data)</p><p> return True</p><p></p><p> def modal_clean_bmeshes(self, context, ob_edit):</p><p> bpy.ops.object.mode_set(mode="OBJECT")</p><p> self._bm_orig.to_mesh(ob_edit.data)</p><p> bpy.ops.object.mode_set(mode="EDIT")</p><p> self._bm_orig.free()</p><p> self._bm.free()</p><p></p><p> def get_factor(self, context, edges_orig):</p><p> """get the length in the space of edited object</p><p> which correspond to 1px of 3d view. This method</p><p> is used to convert the distance of mouse movement</p><p> to offsetting width in interactive mode.</p><p> """</p><p> ob = context.edit_object</p><p> mat_w = ob.matrix_world</p><p> reg = context.region</p><p> reg3d = context.space_data.region_3d # Don't use context.region_data</p><p> # because this will cause error</p><p> # when invoked from header menu.</p><p></p><p> co_median = Vector((0, 0, 0))</p><p> for e in edges_orig:</p><p> co_median += e.verts[0].co</p><p> co_median /= len(edges_orig)</p><p> depth_loc = mat_w @ co_median # World coords of median point</p><p></p><p> win_left = Vector((0, 0))</p><p> win_right = Vector((reg.width, 0))</p><p> left = view3d_utils.region_2d_to_location_3d(reg, reg3d, win_left, depth_loc)</p><p> right = view3d_utils.region_2d_to_location_3d(reg, reg3d, win_right, depth_loc)</p><p> vec_width = mat_w.inverted_safe() @ (right - left) # width vector in the object space</p><p> width_3d = vec_width.length # window width in the object space</p><p></p><p> return width_3d / reg.width</p><p></p><p>class OffsetEdgesProfile(bpy.types.Operator, OffsetBase):</p><p> """Offset Edges using a profile curve."""</p><p> bl_idname = "mesh.hidesato_offset_edges_profile"</p><p> bl_label = "Offset Edges Profile"</p><p> bl_options = {'REGISTER', 'UNDO'}</p><p></p><p> follow_face: bpy.props.BoolProperty(</p><p> name="Follow Face", default=False,</p><p> description="Offset along faces around")</p><p> mirror_modifier: bpy.props.BoolProperty(</p><p> name="Mirror Modifier", default=False,</p><p> description="Take into account of Mirror modifier")</p><p> edge_rail: bpy.props.BoolProperty(</p><p> name="Edge Rail", default=False,</p><p> description="Align vertices along inner edges")</p><p> edge_rail_only_end: bpy.props.BoolProperty(</p><p> name="Edge Rail Only End", default=False,</p><p> description="Apply edge rail to end verts only")</p><p> res_profile: bpy.props.IntProperty(</p><p> name="Resolution", default =6, min=0, max=100,</p><p> update=OffsetBase.use_caches)</p><p> magni_w: bpy.props.FloatProperty(</p><p> name="Magnification of Width", default=1., precision=4, step=1,</p><p> update=OffsetBase.use_caches)</p><p> magni_d: bpy.props.FloatProperty(</p><p> name="Magniofication of Depth", default=1., precision=4, step=1,</p><p> update=OffsetBase.use_caches)</p><p> name_profile: bpy.props.StringProperty(update=OffsetBase.use_caches)</p><p></p><p> @classmethod</p><p> def poll(self, context):</p><p> return context.mode == 'EDIT_MESH'</p><p></p><p> def draw(self, context):</p><p> layout = self.layout</p><p></p><p> layout.prop_search(self, 'name_profile', context.scene, 'objects', text="Profile")</p><p> layout.separator()</p><p></p><p> layout.prop(self, 'res_profile')</p><p></p><p> row = layout.row()</p><p> row.prop(self, 'magni_w', text="Width")</p><p> row.prop(self, 'magni_d', text="Depth")</p><p></p><p> layout.separator()</p><p> layout.prop(self, 'follow_face')</p><p></p><p> row = layout.row()</p><p> row.prop(self, 'edge_rail')</p><p> if self.edge_rail:</p><p> row.prop(self, 'edge_rail_only_end', text="OnlyEnd", toggle=True)</p><p></p><p> layout.prop(self, 'mirror_modifier')</p><p></p><p> #layout.operator('mesh.offset_edges', text='Repeat')</p><p></p><p> if self.follow_face:</p><p> layout.separator()</p><p> layout.prop(self, 'threshold', text='Threshold')</p><p></p><p> @staticmethod</p><p> def analize_profile(context, ob_profile, resolution):</p><p> curve = ob_profile.data</p><p> res_orig = curve.resolution_u</p><p> curve.resolution_u = resolution</p><p> me = ob_profile.to_mesh(depsgraph=context.evaluated_depsgraph_get())</p><p> curve.resolution_u = res_orig</p><p></p><p> vco_start = me.vertices[0].co</p><p> info_profile = [v.co - vco_start for v in me.vertices[1:]]</p><p></p><p> return info_profile</p><p></p><p> @staticmethod</p><p> def get_profile(context):</p><p> ob_edit = context.edit_object</p><p> for ob in context.selected_objects:</p><p> if ob != ob_edit and ob.type == 'CURVE':</p><p> return ob</p><p> else:</p><p> self.report({'WARNING'},</p><p> "Profile curve is not selected.")</p><p> return None</p><p></p><p> def offset_profile(self, ob_edit, info_profile):</p><p> me = ob_edit.data</p><p> bm = bmesh.from_edit_mesh(me)</p><p></p><p> if self.caches_valid and self._cache_offset_infos:</p><p> offset_infos, edges_orig = self.get_caches(bm)</p><p> else:</p><p> offset_infos, edges_orig = self.get_offset_infos(</p><p> bm, ob_edit,</p><p> edge_rail=self.edge_rail,</p><p> edge_rail_only_end=self.edge_rail_only_end,</p><p> mirror_modifier=self.mirror_modifier,</p><p> threshold=self.threshold</p><p> )</p><p> if offset_infos is False:</p><p> return {'CANCELLED'}</p><p> self.save_caches(offset_infos, edges_orig)</p><p></p><p> ref_verts = [v for v, _, _ in offset_infos]</p><p> edges = edges_orig</p><p> for width, depth, _ in info_profile:</p><p> exverts, exedges, _ = self.extrude_and_pairing(bm, edges, ref_verts)</p><p> self.move_verts(</p><p> bm, me, width * self.magni_w,</p><p> depth * self.magni_d, offset_infos,</p><p> exverts, update=False</p><p> )</p><p> ref_verts = exverts</p><p> edges = exedges</p><p></p><p> bm.normal_update()</p><p> bmesh.update_edit_mesh(me)</p><p></p><p> self.caches_valid = False</p><p></p><p> return {'FINISHED'}</p><p></p><p> @staticmethod</p><p> def get_profile(context):</p><p> ob_edit = context.edit_object</p><p> for ob in context.selected_objects:</p><p> if ob != ob_edit and ob.type == 'CURVE':</p><p> return ob</p><p> return None</p><p></p><p> def execute(self, context):</p><p> if not self.name_profile:</p><p> self.report({'WARNING'},</p><p> "Select a curve object as profile.")</p><p> return {'FINISHED'}</p><p></p><p> ob_profile = context.scene.objects[self.name_profile]</p><p> if ob_profile and ob_profile.type == "CURVE":</p><p> info_profile = self.analize_profile(</p><p> context, ob_profile, self.res_profile</p><p> )</p><p> return self.offset_profile(context.edit_object, info_profile)</p><p> else:</p><p> self.name_profile = ""</p><p> self.report({'WARNING'},</p><p> "Select a curve object as profile.")</p><p> return {'FINISHED'}</p><p></p><p> def invoke(self, context, event):</p><p> ob_edit = context.edit_object</p><p> if self.is_face_selected(ob_edit):</p><p> self.follow_face = True</p><p> if self.is_mirrored(ob_edit):</p><p> self.mirror_modifier = True</p><p></p><p> ob_profile = self.get_profile(context)</p><p> if ob_profile is None:</p><p> self.report({'WARNING'},</p><p> "Profile curve is not selected.")</p><p> return {'CANCELLED'}</p><p></p><p> self.name_profile = ob_profile.name</p><p> self.res_profile = ob_profile.data.resolution_u</p><p> return self.execute(context)</p><p></p><p></p><p>def draw_offset_edges(self, context):</p><p> lay = self.layout</p><p> lay.separator()</p><p> lay.operator_context = 'INVOKE_DEFAULT'</p><p> lay.operator(OffsetEdges.bl_idname, text='Offset').geometry_mode='offset'</p><p> lay.operator(OffsetEdges.bl_idname, text='Offset Extrude').geometry_mode='extrude'</p><p> lay.operator(OffsetEdges.bl_idname, text='Offset Move').geometry_mode='move'</p><p> lay.operator(OffsetEdgesProfile.bl_idname, text='Offset with Profile')</p><p></p><p></p><p>def register():</p><p> bpy.utils.register_class(OffsetEdgesPreferences)</p><p> bpy.utils.register_class(OffsetEdges)</p><p> bpy.utils.register_class(OffsetEdgesProfile)</p><p> bpy.types.VIEW3D_MT_edit_mesh_edges.append(draw_offset_edges)</p><p></p><p></p><p>def unregister():</p><p> bpy.utils.unregister_class(OffsetEdgesPreferences)</p><p> bpy.utils.unregister_class(OffsetEdges)</p><p> bpy.utils.unregister_class(OffsetEdgesProfile)</p><p> bpy.types.VIEW3D_MT_edit_mesh_edges.remove(draw_offset_edges)</p><p></p><p></p><p>if __name__ == '__main__':</p><p> register()</p><p>[/CODE]</p></blockquote><p></p>
[QUOTE="noltehans, post: 2735986, member: 311830"] Wunderbar, das ist doch die Hauptsache. Ich habe übrigens noch eine Möglichkeit gefunden Kanten in einem bestimmten Winkel zu extrudieren. Du musst die abgeschrägten Kanten aber per Hand füllen (also den Deckel drauf machen). Das kleine Addon nennt sich mesh_offset_edges.py Kante auswählen, Strg+E -> Offset Edges (und dann z.B. Extrude) Den Quellcode in einen Texteditor laden und als mesh_offset_edges.py abspeichern. [CODE]# ***** 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 ***** bl_info = { "name": "Hidesato Offset Edges", "author": "Hidesato Ikeya", "version": (0, 4, 0), "blender": (2, 82, 0), "location": "VIEW3D > Edge menu(CTRL-E) > Offset Edges", "description": "Offset Edges", "warning": "", #"wiki_url": "http://wiki.blender.org/index.php/Extensions:2.6/Py/Scripts/Modeling/offset_edges", "tracker_url": "", "category": "Mesh"} import math from math import sin, cos, pi, copysign, radians, degrees, atan, sqrt import bpy import mathutils from bpy_extras import view3d_utils import bmesh from mathutils import Vector from time import perf_counter X_UP = Vector((1.0, .0, .0)) Y_UP = Vector((.0, 1.0, .0)) Z_UP = Vector((.0, .0, 1.0)) ZERO_VEC = Vector((.0, .0, .0)) ANGLE_1 = pi / 180 ANGLE_90 = pi / 2 ANGLE_180 = pi ANGLE_360 = 2 * pi class OffsetEdgesPreferences(bpy.types.AddonPreferences): bl_idname = __name__ interactive: bpy.props.BoolProperty( name = "Interactive", description = "makes operation interactive", default = True) free_move: bpy.props.BoolProperty( name = "Free Move", description = "enables to adjust both width and depth while pressing ctrl-key", default = False) def draw(self, context): layout = self.layout row = layout.row() row.prop(self, "interactive") if self.interactive: row.prop(self, "free_move") ####################################################################### class OffsetBase: threshold: bpy.props.FloatProperty( name="Flat Face Threshold", default=radians(0.05), precision=5, step=1.0e-4, subtype='ANGLE', description="If difference of angle between two adjacent faces is " "below this value, those faces are regarded as flat.", options={'HIDDEN'}) caches_valid: bpy.props.BoolProperty( name="Caches Valid", default=False, options={'HIDDEN'}) _cache_offset_infos = None _cache_edges_orig = None def use_caches(self, context): self.caches_valid = True def get_caches(self, bm): bmverts = tuple(bm.verts) bmedges = tuple(bm.edges) offset_infos = \ [(bmverts[vix], co, d) for vix, co, d in self._cache_offset_infos] edges_orig = [bmedges[eix] for eix in self._cache_edges_orig] for e in edges_orig: e.select = False for f in bm.faces: f.select = False return offset_infos, edges_orig def save_caches(self, offset_infos, edges_orig): self._cache_offset_infos = tuple((v.index, co, d) for v, co, d in offset_infos) self._cache_edges_orig = tuple(e.index for e in edges_orig) @staticmethod def is_face_selected(ob_edit): bpy.ops.object.mode_set(mode="OBJECT") me = ob_edit.data for p in me.polygons: if p.select: bpy.ops.object.mode_set(mode="EDIT") return True bpy.ops.object.mode_set(mode="EDIT") return False @staticmethod def is_mirrored(ob_edit): for mod in ob_edit.modifiers: if mod.type == 'MIRROR' and mod.use_mirror_merge: return True return False @staticmethod def reorder_loop(verts, edges, lp_normal, adj_faces): for i, adj_f in enumerate(adj_faces): if adj_f is None: continue v1, v2 = verts[i], verts[i+1] e = edges[i] fv = tuple(adj_f.verts) if fv[fv.index(v1)-1] is v2: # Align loop direction verts.reverse() edges.reverse() adj_faces.reverse() if lp_normal.dot(adj_f.normal) < .0: lp_normal *= -1 break else: # All elements in adj_faces are None for v in verts: if v.normal != ZERO_VEC: if lp_normal.dot(v.normal) < .0: verts.reverse() edges.reverse() lp_normal *= -1 break return verts, edges, lp_normal, adj_faces @staticmethod def get_cross_rail(vec_tan, vec_edge_r, vec_edge_l, normal_r, normal_l): # Cross rail is a cross vector between normal_r and normal_l. vec_cross = normal_r.cross(normal_l) if vec_cross.dot(vec_tan) < .0: vec_cross *= -1 cos_min = min(vec_tan.dot(vec_edge_r), vec_tan.dot(-vec_edge_l)) cos = vec_tan.dot(vec_cross) if cos >= cos_min: vec_cross.normalize() return vec_cross else: return None @staticmethod def get_edge_rail(vert, set_edges_orig): co_edges = co_edges_selected = 0 vec_inner = None for e in vert.link_edges: if (e not in set_edges_orig and (e.select or (co_edges_selected == 0 and not e.hide))): v_other = e.other_vert(vert) vec = v_other.co - vert.co if vec != ZERO_VEC: vec_inner = vec if e.select: co_edges_selected += 1 if co_edges_selected == 2: return None else: co_edges += 1 if co_edges_selected == 1: vec_inner.normalize() return vec_inner elif co_edges == 1: # No selected edges, one unselected edge. vec_inner.normalize() return vec_inner else: return None @staticmethod def get_mirror_rail(mirror_plane, vec_up): p_norm = mirror_plane[1] mirror_rail = vec_up.cross(p_norm) if mirror_rail != ZERO_VEC: mirror_rail.normalize() # Project vec_up to mirror_plane vec_up = vec_up - vec_up.project(p_norm) vec_up.normalize() return mirror_rail, vec_up else: return None, vec_up @staticmethod def get_vert_mirror_pairs(set_edges_orig, mirror_planes): if mirror_planes: set_edges_copy = set_edges_orig.copy() vert_mirror_pairs = dict() for e in set_edges_orig: v1, v2 = e.verts for mp in mirror_planes: p_co, p_norm, mlimit = mp v1_dist = abs(p_norm.dot(v1.co - p_co)) v2_dist = abs(p_norm.dot(v2.co - p_co)) if v1_dist <= mlimit: # v1 is on a mirror plane. vert_mirror_pairs[v1] = mp if v2_dist <= mlimit: # v2 is on a mirror plane. vert_mirror_pairs[v2] = mp if v1_dist <= mlimit and v2_dist <= mlimit: # This edge is on a mirror_plane, so should not be offsetted. set_edges_copy.remove(e) return vert_mirror_pairs, set_edges_copy else: return None, set_edges_orig @staticmethod def collect_mirror_planes(ob_edit): mirror_planes = [] eob_mat_inv = ob_edit.matrix_world.inverted() for m in ob_edit.modifiers: if (m.type == 'MIRROR' and m.use_mirror_merge): merge_limit = m.merge_threshold if not m.mirror_object: loc = ZERO_VEC norm_x, norm_y, norm_z = X_UP, Y_UP, Z_UP else: mirror_mat_local = eob_mat_inv * m.mirror_object.matrix_world loc = mirror_mat_local.to_translation() norm_x, norm_y, norm_z, _ = mirror_mat_local.adjugated() norm_x = norm_x.to_3d().normalized() norm_y = norm_y.to_3d().normalized() norm_z = norm_z.to_3d().normalized() if m.use_x: mirror_planes.append((loc, norm_x, merge_limit)) if m.use_y: mirror_planes.append((loc, norm_y, merge_limit)) if m.use_z: mirror_planes.append((loc, norm_z, merge_limit)) return mirror_planes @staticmethod def collect_edges(bm): set_edges_orig = set() for e in bm.edges: if e.select: co_faces_selected = 0 for f in e.link_faces: if f.select: co_faces_selected += 1 if co_faces_selected == 2: break else: set_edges_orig.add(e) if not set_edges_orig: return None return set_edges_orig @staticmethod def collect_loops(set_edges_orig): set_edges_copy = set_edges_orig.copy() loops = [] # [v, e, v, e, ... , e, v] while set_edges_copy: edge_start = set_edges_copy.pop() v_left, v_right = edge_start.verts lp = [v_left, edge_start, v_right] reverse = False while True: edge = None for e in v_right.link_edges: if e in set_edges_copy: if edge: # Overlap detected. return None edge = e set_edges_copy.remove(e) if edge: v_right = edge.other_vert(v_right) lp.extend((edge, v_right)) continue else: if v_right is v_left: # Real loop. loops.append(lp) break elif reverse is False: # Right side of half loop. # Reversing the loop to operate same procedure on the left side. lp.reverse() v_right, v_left = v_left, v_right reverse = True continue else: # Half loop, completed. loops.append(lp) break return loops @staticmethod def calc_loop_normal(verts, fallback=Z_UP): # Calculate normal from verts using Newell's method. normal = ZERO_VEC.copy() if verts[0] is verts[-1]: # Perfect loop range_verts = range(1, len(verts)) else: # Half loop range_verts = range(0, len(verts)) for i in range_verts: v1co, v2co = verts[i-1].co, verts[i].co normal.x += (v1co.y - v2co.y) * (v1co.z + v2co.z) normal.y += (v1co.z - v2co.z) * (v1co.x + v2co.x) normal.z += (v1co.x - v2co.x) * (v1co.y + v2co.y) if normal != ZERO_VEC: normal.normalize() else: normal = fallback return normal @staticmethod def get_adj_faces(edges): adj_faces = [] for e in edges: adj_f = None co_adj = 0 for f in e.link_faces: # Search an adjacent face. # Selected face has precedance. if not f.hide and f.normal != ZERO_VEC: adj_exist = True adj_f = f co_adj += 1 if f.select: adj_faces.append(adj_f) break else: if co_adj == 1: adj_faces.append(adj_f) else: adj_faces.append(None) return adj_faces @staticmethod def get_directions(lp, vec_upward, normal_fallback, vert_mirror_pairs, **options): opt_follow_face = options.get("follow_face") opt_edge_rail = options.get("edge_rail") opt_er_only_end = options.get("edge_rail_only_end") opt_threshold = options.get("threshold") opt_normal_override = options.get("normal_override") verts, edges = lp[::2], lp[1::2] set_edges = set(edges) if opt_normal_override is None: lp_normal = OffsetBase.calc_loop_normal(verts, fallback=normal_fallback) else: lp_normal = opt_normal_override opt_follow_face = False ##### Loop order might be changed below. if lp_normal.dot(vec_upward) < .0: # Make this loop's normal towards vec_upward. verts.reverse() edges.reverse() lp_normal *= -1 if opt_follow_face: adj_faces = OffsetBase.get_adj_faces(edges) verts, edges, lp_normal, adj_faces = \ OffsetBase.reorder_loop(verts, edges, lp_normal, adj_faces) else: adj_faces = (None, ) * len(edges) ##### Loop order might be changed above. vec_edges = tuple((e.other_vert(v).co - v.co).normalized() for v, e in zip(verts, edges)) if verts[0] is verts[-1]: # Real loop. Popping last vertex. verts.pop() HALF_LOOP = False else: # Half loop HALF_LOOP = True len_verts = len(verts) directions = [] for i in range(len_verts): vert = verts[i] ix_right, ix_left = i, i-1 VERT_END = False if HALF_LOOP: if i == 0: # First vert ix_left = ix_right VERT_END = True elif i == len_verts - 1: # Last vert ix_right = ix_left VERT_END = True edge_right, edge_left = vec_edges[ix_right], vec_edges[ix_left] face_right, face_left = adj_faces[ix_right], adj_faces[ix_left] norm_right = face_right.normal if face_right else lp_normal norm_left = face_left.normal if face_left else lp_normal if norm_right.angle(norm_left) > opt_threshold: # Two faces are not flat. two_normals = True else: two_normals = False tan_right = edge_right.cross(norm_right).normalized() tan_left = edge_left.cross(norm_left).normalized() tan_avr = (tan_right + tan_left).normalized() norm_avr = (norm_right + norm_left).normalized() rail = None if two_normals or opt_edge_rail: # Get edge rail. # edge rail is a vector of an inner edge. if two_normals or (not opt_er_only_end) or VERT_END: rail = OffsetBase.get_edge_rail(vert, set_edges) if vert_mirror_pairs and VERT_END: if vert in vert_mirror_pairs: rail, norm_avr = \ OffsetBase.get_mirror_rail(vert_mirror_pairs[vert], norm_avr) if (not rail) and two_normals: # Get cross rail. # Cross rail is a cross vector between norm_right and norm_left. rail = OffsetBase.get_cross_rail( tan_avr, edge_right, edge_left, norm_right, norm_left) if rail: dot = tan_avr.dot(rail) if dot > .0: tan_avr = rail elif dot < .0: tan_avr = -rail vec_plane = norm_avr.cross(tan_avr) e_dot_p_r = edge_right.dot(vec_plane) e_dot_p_l = edge_left.dot(vec_plane) if e_dot_p_r or e_dot_p_l: if e_dot_p_r > e_dot_p_l: vec_edge, e_dot_p = edge_right, e_dot_p_r else: vec_edge, e_dot_p = edge_left, e_dot_p_l vec_tan = (tan_avr - tan_avr.project(vec_edge)).normalized() # Make vec_tan perpendicular to vec_edge vec_up = vec_tan.cross(vec_edge) vec_width = vec_tan - (vec_tan.dot(vec_plane) / e_dot_p) * vec_edge vec_depth = vec_up - (vec_up.dot(vec_plane) / e_dot_p) * vec_edge else: vec_width = tan_avr vec_depth = norm_avr directions.append((vec_width, vec_depth)) return verts, directions def get_offset_infos(self, bm, ob_edit, **options): time = perf_counter() opt_mirror_modifier = options.get("mirror_modifier") set_edges_orig = self.collect_edges(bm) if set_edges_orig is None: self.report({'WARNING'}, "No edges are selected.") return False, False if opt_mirror_modifier: mirror_planes = self.collect_mirror_planes(ob_edit) vert_mirror_pairs, set_edges = \ self.get_vert_mirror_pairs(set_edges_orig, mirror_planes) if set_edges: set_edges_orig = set_edges else: #self.report({'WARNING'}, # "All selected edges are on mirror planes.") vert_mirror_pairs = None else: vert_mirror_pairs = None edges_orig = list(set_edges_orig) loops = self.collect_loops(set_edges_orig) if loops is None: self.report({'WARNING'}, "Overlapping edge loops detected. Select discrete edge loops") return False, False vec_upward = (X_UP + Y_UP + Z_UP).normalized() # vec_upward is used to unify loop normals when follow_face is off. normal_fallback = Z_UP #normal_fallback = Vector(context.region_data.view_matrix[2][:3]) # normal_fallback is used when loop normal cannot be calculated. offset_infos = [] for lp in loops: verts, directions = self.get_directions( lp, vec_upward, normal_fallback, vert_mirror_pairs, **options ) if verts: # convert vert objects to vert indexs for v, d in zip(verts, directions): offset_infos.append((v, v.co.copy(), d)) for e in edges_orig: e.select = False for f in bm.faces: f.select = False #print("OffsetEdges - Calculating: ", perf_counter() - time) return offset_infos, edges_orig @staticmethod def extrude_and_pairing(bm, edges_orig, ref_verts): """ ref_verts is a list of vertices, each of which should be one end of an edge in edges_orig""" extruded = bmesh.ops.extrude_edge_only(bm, edges=edges_orig)['geom'] n_edges = n_faces = len(edges_orig) n_verts = len(extruded) - n_edges - n_faces exverts = set(extruded[:n_verts]) exedges = set(extruded[n_verts:n_verts + n_edges]) #faces = set(extruded[n_verts + n_edges:]) side_edges = set(e for v in exverts for e in v.link_edges if e not in exedges) # ref_verts[i] and ret[i] are both ends of a side edge. exverts_ordered = \ [e.other_vert(v) for v in ref_verts for e in v.link_edges if e in side_edges] return exverts_ordered, list(exedges), list(side_edges) @staticmethod def move_verts(bm, me, width, depth, offset_infos, verts_offset=None, update=True): if verts_offset is None: for v, co, (vec_w, vec_d) in offset_infos: v.co = co + width * vec_w + depth * vec_d else: for (_, co, (vec_w, vec_d)), v in zip(offset_infos, verts_offset): v.co = co + width * vec_w + depth * vec_d if update: bm.normal_update() bmesh.update_edit_mesh(me) class OffsetEdges(bpy.types.Operator, OffsetBase): """Offset Edges""" bl_idname = "mesh.hidesato_offset_edges" bl_label = "Offset Edges" bl_options = {'REGISTER', 'UNDO'} follow_face: bpy.props.BoolProperty( name="Follow Face", default=False, description="Offset along faces around" ) mirror_modifier: bpy.props.BoolProperty( name="Mirror Modifier", default=False, description="Take into account of Mirror modifier" ) edge_rail: bpy.props.BoolProperty( name="Edge Rail", default=False, description="Align vertices along inner edges" ) edge_rail_only_end: bpy.props.BoolProperty( name="Edge Rail Only End", default=False, description="Apply edge rail to end verts only" ) lock_axis: bpy.props.EnumProperty( items=[ ('none', "None", "Don't lock axis"), ('x', "X", "Lock X axis"), ('y', "Y", "Lock Y axis"), ('z', "Z", "Lock Z axis"), ('view', "VIEW", "Lock view axis") ], name="Lock Axis", default='none' ) # Functions below are update functions. def assign_angle_presets(self, context): angle_presets = {'0В°': 0, '15В°': radians(15), '30В°': radians(30), '45В°': radians(45), '60В°': radians(60), '75В°': radians(75), '90В°': radians(90),} self.angle = angle_presets[self.angle_presets] def change_depth_mode(self, context): if self.depth_mode == 'angle': self.width, self.angle = OffsetEdges.depth_to_angle(self.width, self.depth) else: self.width, self.depth = OffsetEdges.angle_to_depth(self.width, self.angle) def angle_to_depth(width, angle): """Returns: (converted_width, converted_depth)""" return width * cos(angle), width * sin(angle) def depth_to_angle(width, depth): """Returns: (converted_width, converted_angle)""" ret_width = sqrt(width * width + depth * depth) if width: ret_angle = atan(depth / width) elif depth == 0: ret_angle = 0 elif depth > 0: ret_angle = ANGLE_90 elif depth < 0: ret_angle = -ANGLE_90 return ret_width, ret_angle geometry_mode: bpy.props.EnumProperty( items=[('offset', "Offset", "Offset edges"), ('extrude', "Extrude", "Extrude edges"), ('move', "Move", "Move selected edges")], name="Geometory mode", default='offset', update=OffsetBase.use_caches) width: bpy.props.FloatProperty( name="Width", default=.2, precision=4, step=1, update=OffsetBase.use_caches) flip_width: bpy.props.BoolProperty( name="Flip Width", default=False, description="Flip width direction", update=OffsetBase.use_caches) depth: bpy.props.FloatProperty( name="Depth", default=.0, precision=4, step=1, update=OffsetBase.use_caches) flip_depth: bpy.props.BoolProperty( name="Flip Depth", default=False, description="Flip depth direction", update=OffsetBase.use_caches) depth_mode: bpy.props.EnumProperty( items=[('angle', "Angle", "Angle"), ('depth', "Depth", "Depth")], name="Depth mode", default='angle', update=change_depth_mode) angle: bpy.props.FloatProperty( name="Angle", default=0, precision=3, step=100, min=-2*pi, max=2*pi, subtype='ANGLE', description="Angle", update=OffsetBase.use_caches) flip_angle: bpy.props.BoolProperty( name="Flip Angle", default=False, description="Flip Angle", update=OffsetBase.use_caches) angle_presets: bpy.props.EnumProperty( items=[('0В°', "0В°", "0В°"), ('15В°', "15В°", "15В°"), ('30В°', "30В°", "30В°"), ('45В°', "45В°", "45В°"), ('60В°', "60В°", "60В°"), ('75В°', "75В°", "75В°"), ('90В°', "90В°", "90В°"), ], name="Angle Presets", default='0В°', update=assign_angle_presets) def get_lockvector(self, context): axis = self.lock_axis if axis == 'x': return X_UP elif axis == 'y': return Y_UP elif axis == 'z': return Z_UP elif axis == 'view' and context.region_data: vec = Z_UP.copy() vec.rotate(context.region_data.view_rotation) return vec return None def get_exverts(self, bm, offset_infos, edges_orig): ref_verts = [v for v, _, _ in offset_infos] if self.geometry_mode == 'move': exverts = ref_verts exedges = edges_orig else: exverts, exedges, side_edges = self.extrude_and_pairing(bm, edges_orig, ref_verts) if self.geometry_mode == 'offset': bmesh.ops.delete(bm, geom=side_edges, context="EDGES") for e in exedges: e.select = True return exverts def do_offset(self, bm, me, offset_infos, verts_offset): if self.depth_mode == 'angle': w = self.width if not self.flip_width else -self.width angle = self.angle if not self.flip_angle else -self.angle width = w * cos(angle) depth = w * sin(angle) else: width = self.width if not self.flip_width else -self.width depth = self.depth if not self.flip_depth else -self.depth self.move_verts(bm, me, width, depth, offset_infos, verts_offset) @classmethod def poll(self, context): return context.mode == 'EDIT_MESH' def draw(self, context): layout = self.layout layout.row().prop(self, 'geometry_mode', expand=True) row = layout.row(align=True) row.prop(self, 'width') row.prop(self, 'flip_width', icon='ARROW_LEFTRIGHT', icon_only=True) layout.label(text="Depth Mode:") layout.row().prop(self, 'depth_mode', expand=True) if self.depth_mode == 'angle': d_mode = 'angle' flip = 'flip_angle' else: d_mode = 'depth' flip = 'flip_depth' row = layout.row(align=True) row.prop(self, d_mode) row.prop(self, flip, icon='ARROW_LEFTRIGHT', icon_only=True) if self.depth_mode == 'angle': layout.row().prop(self, 'angle_presets', text="Presets", expand=True) layout.label(text="Lock Axis:") layout.row().prop(self, 'lock_axis', text="Lock Axis", expand=True) layout.separator() row = layout.row() row.prop(self, 'follow_face') if self.follow_face: row.prop(self, "threshold", text="Threshold") row = layout.row() row.prop(self, 'edge_rail') if self.edge_rail: row.prop(self, 'edge_rail_only_end', text="OnlyEnd", toggle=True) layout.prop(self, 'mirror_modifier') #layout.operator('mesh.offset_edges', text='Repeat') def execute(self, context): # In edit mode edit_object = context.edit_object me = edit_object.data bm = bmesh.from_edit_mesh(me) if self.caches_valid and self._cache_offset_infos: offset_infos, edges_orig = self.get_caches(bm) else: offset_infos, edges_orig = self.get_offset_infos( bm, edit_object, follow_face=self.follow_face, edge_rail=self.edge_rail, edge_rail_only_end=self.edge_rail_only_end, mirror_modifier=self.mirror_modifier, normal_override=self.get_lockvector(context), threshold=self.threshold ) if offset_infos is False: return {'CANCELLED'} self.save_caches(offset_infos, edges_orig) exverts = self.get_exverts(bm, offset_infos, edges_orig) self.do_offset(bm, me, offset_infos, exverts) self.caches_valid = False return {'FINISHED'} def invoke(self, context, event): # in edit mode ob_edit = context.edit_object if self.is_face_selected(ob_edit): self.follow_face = True if self.is_mirrored(ob_edit): self.mirror_modifier = True me = ob_edit.data pref = context.preferences.addons[__name__].preferences if pref.interactive and context.space_data.type == 'VIEW_3D': # interactive mode if pref.free_move: self.depth_mode = 'depth' ret = self.modal_prepare_bmeshes(context, ob_edit) if ret is False: return {'CANCELLED'} self.width = self.angle = self.depth = .0 self.flip_depth = self.flip_angle = self.flip_width = False self._mouse_init = self._mouse_prev = \ Vector((event.mouse_x, event.mouse_y)) context.window_manager.modal_handler_add(self) self._factor = self.get_factor(context, self._edges_orig) # toggle switchs of keys self._F = 0 self._A = 0 return {'RUNNING_MODAL'} else: return self.execute(context) def modal(self, context, event): # In edit mode ob_edit = context.edit_object me = ob_edit.data pref = context.preferences.addons[__name__].preferences if event.type == 'F': # toggle follow_face # event.type == 'F' is True both when 'F' is pressed and when released, # so these codes should be executed every other loop. self._F = 1 - self._F if self._F: self.follow_face = 1 - self.follow_face self.modal_clean_bmeshes(context, ob_edit) ret = self.modal_prepare_bmeshes(context, ob_edit) if ret: self.do_offset(self._bm, me, self._offset_infos, self._exverts) return {'RUNNING_MODAL'} else: return {'CANCELLED'} if event.type == 'A': # toggle depth_mode self._A = 1 - self._A if self._A: if self.depth_mode == 'angle': self.depth_mode = 'depth' else: self.depth_mode = 'angle' context.area.header_text_set(self.create_header()) if event.type == 'MOUSEMOVE': _mouse_current = Vector((event.mouse_x, event.mouse_y)) vec_delta = _mouse_current - self._mouse_prev if pref.free_move or not event.ctrl: self.width += vec_delta.x * self._factor if event.ctrl: if self.depth_mode == 'angle': self.angle += vec_delta.y * ANGLE_1 elif self.depth_mode == 'depth': self.depth += vec_delta.y * self._factor self._mouse_prev = _mouse_current self.do_offset(self._bm, me, self._offset_infos, self._exverts) return {'RUNNING_MODAL'} elif event.type == 'LEFTMOUSE': self._bm_orig.free() context.area.header_text_set(text=None) return {'FINISHED'} elif event.type in {'RIGHTMOUSE', 'ESC'}: self.modal_clean_bmeshes(context, ob_edit) context.area.header_text_set(text=None) return {'CANCELLED'} return {'RUNNING_MODAL'} # methods below are usded in interactive mode def create_header(self): header = "".join( ["Width {width: .4} ", "Depth {depth: .4}('A' to Angle) " if self.depth_mode == 'depth' else "Angle {angle: 4.0F}В°('A' to Depth) ", "FollowFace(F):", "(ON)" if self.follow_face else "(OFF)", ]) return header.format(width=self.width, depth=self.depth, angle=degrees(self.angle)) def modal_prepare_bmeshes(self, context, ob_edit): bpy.ops.object.mode_set(mode="OBJECT") self._bm_orig = bmesh.new() self._bm_orig.from_mesh(ob_edit.data) bpy.ops.object.mode_set(mode="EDIT") self._bm = bmesh.from_edit_mesh(ob_edit.data) self._offset_infos, self._edges_orig = self.get_offset_infos( self._bm, ob_edit, edge_rail=self.edge_rail, edge_rail_only_end=self.edge_rail_only_end, mirror_modifier=self.mirror_modifier, normal_override=self.get_lockvector(context), threshold=self.threshold ) if self._offset_infos is False: return False self._exverts = \ self.get_exverts(self._bm, self._offset_infos, self._edges_orig) bmesh.update_edit_mesh(ob_edit.data) return True def modal_clean_bmeshes(self, context, ob_edit): bpy.ops.object.mode_set(mode="OBJECT") self._bm_orig.to_mesh(ob_edit.data) bpy.ops.object.mode_set(mode="EDIT") self._bm_orig.free() self._bm.free() def get_factor(self, context, edges_orig): """get the length in the space of edited object which correspond to 1px of 3d view. This method is used to convert the distance of mouse movement to offsetting width in interactive mode. """ ob = context.edit_object mat_w = ob.matrix_world reg = context.region reg3d = context.space_data.region_3d # Don't use context.region_data # because this will cause error # when invoked from header menu. co_median = Vector((0, 0, 0)) for e in edges_orig: co_median += e.verts[0].co co_median /= len(edges_orig) depth_loc = mat_w @ co_median # World coords of median point win_left = Vector((0, 0)) win_right = Vector((reg.width, 0)) left = view3d_utils.region_2d_to_location_3d(reg, reg3d, win_left, depth_loc) right = view3d_utils.region_2d_to_location_3d(reg, reg3d, win_right, depth_loc) vec_width = mat_w.inverted_safe() @ (right - left) # width vector in the object space width_3d = vec_width.length # window width in the object space return width_3d / reg.width class OffsetEdgesProfile(bpy.types.Operator, OffsetBase): """Offset Edges using a profile curve.""" bl_idname = "mesh.hidesato_offset_edges_profile" bl_label = "Offset Edges Profile" bl_options = {'REGISTER', 'UNDO'} follow_face: bpy.props.BoolProperty( name="Follow Face", default=False, description="Offset along faces around") mirror_modifier: bpy.props.BoolProperty( name="Mirror Modifier", default=False, description="Take into account of Mirror modifier") edge_rail: bpy.props.BoolProperty( name="Edge Rail", default=False, description="Align vertices along inner edges") edge_rail_only_end: bpy.props.BoolProperty( name="Edge Rail Only End", default=False, description="Apply edge rail to end verts only") res_profile: bpy.props.IntProperty( name="Resolution", default =6, min=0, max=100, update=OffsetBase.use_caches) magni_w: bpy.props.FloatProperty( name="Magnification of Width", default=1., precision=4, step=1, update=OffsetBase.use_caches) magni_d: bpy.props.FloatProperty( name="Magniofication of Depth", default=1., precision=4, step=1, update=OffsetBase.use_caches) name_profile: bpy.props.StringProperty(update=OffsetBase.use_caches) @classmethod def poll(self, context): return context.mode == 'EDIT_MESH' def draw(self, context): layout = self.layout layout.prop_search(self, 'name_profile', context.scene, 'objects', text="Profile") layout.separator() layout.prop(self, 'res_profile') row = layout.row() row.prop(self, 'magni_w', text="Width") row.prop(self, 'magni_d', text="Depth") layout.separator() layout.prop(self, 'follow_face') row = layout.row() row.prop(self, 'edge_rail') if self.edge_rail: row.prop(self, 'edge_rail_only_end', text="OnlyEnd", toggle=True) layout.prop(self, 'mirror_modifier') #layout.operator('mesh.offset_edges', text='Repeat') if self.follow_face: layout.separator() layout.prop(self, 'threshold', text='Threshold') @staticmethod def analize_profile(context, ob_profile, resolution): curve = ob_profile.data res_orig = curve.resolution_u curve.resolution_u = resolution me = ob_profile.to_mesh(depsgraph=context.evaluated_depsgraph_get()) curve.resolution_u = res_orig vco_start = me.vertices[0].co info_profile = [v.co - vco_start for v in me.vertices[1:]] return info_profile @staticmethod def get_profile(context): ob_edit = context.edit_object for ob in context.selected_objects: if ob != ob_edit and ob.type == 'CURVE': return ob else: self.report({'WARNING'}, "Profile curve is not selected.") return None def offset_profile(self, ob_edit, info_profile): me = ob_edit.data bm = bmesh.from_edit_mesh(me) if self.caches_valid and self._cache_offset_infos: offset_infos, edges_orig = self.get_caches(bm) else: offset_infos, edges_orig = self.get_offset_infos( bm, ob_edit, edge_rail=self.edge_rail, edge_rail_only_end=self.edge_rail_only_end, mirror_modifier=self.mirror_modifier, threshold=self.threshold ) if offset_infos is False: return {'CANCELLED'} self.save_caches(offset_infos, edges_orig) ref_verts = [v for v, _, _ in offset_infos] edges = edges_orig for width, depth, _ in info_profile: exverts, exedges, _ = self.extrude_and_pairing(bm, edges, ref_verts) self.move_verts( bm, me, width * self.magni_w, depth * self.magni_d, offset_infos, exverts, update=False ) ref_verts = exverts edges = exedges bm.normal_update() bmesh.update_edit_mesh(me) self.caches_valid = False return {'FINISHED'} @staticmethod def get_profile(context): ob_edit = context.edit_object for ob in context.selected_objects: if ob != ob_edit and ob.type == 'CURVE': return ob return None def execute(self, context): if not self.name_profile: self.report({'WARNING'}, "Select a curve object as profile.") return {'FINISHED'} ob_profile = context.scene.objects[self.name_profile] if ob_profile and ob_profile.type == "CURVE": info_profile = self.analize_profile( context, ob_profile, self.res_profile ) return self.offset_profile(context.edit_object, info_profile) else: self.name_profile = "" self.report({'WARNING'}, "Select a curve object as profile.") return {'FINISHED'} def invoke(self, context, event): ob_edit = context.edit_object if self.is_face_selected(ob_edit): self.follow_face = True if self.is_mirrored(ob_edit): self.mirror_modifier = True ob_profile = self.get_profile(context) if ob_profile is None: self.report({'WARNING'}, "Profile curve is not selected.") return {'CANCELLED'} self.name_profile = ob_profile.name self.res_profile = ob_profile.data.resolution_u return self.execute(context) def draw_offset_edges(self, context): lay = self.layout lay.separator() lay.operator_context = 'INVOKE_DEFAULT' lay.operator(OffsetEdges.bl_idname, text='Offset').geometry_mode='offset' lay.operator(OffsetEdges.bl_idname, text='Offset Extrude').geometry_mode='extrude' lay.operator(OffsetEdges.bl_idname, text='Offset Move').geometry_mode='move' lay.operator(OffsetEdgesProfile.bl_idname, text='Offset with Profile') def register(): bpy.utils.register_class(OffsetEdgesPreferences) bpy.utils.register_class(OffsetEdges) bpy.utils.register_class(OffsetEdgesProfile) bpy.types.VIEW3D_MT_edit_mesh_edges.append(draw_offset_edges) def unregister(): bpy.utils.unregister_class(OffsetEdgesPreferences) bpy.utils.unregister_class(OffsetEdges) bpy.utils.unregister_class(OffsetEdgesProfile) bpy.types.VIEW3D_MT_edit_mesh_edges.remove(draw_offset_edges) if __name__ == '__main__': register() [/CODE] [/QUOTE]
Bilder bitte
hier hochladen
und danach über das Bild-Icon (Direktlink vorher kopieren) platzieren.
Zitate einfügen…
Authentifizierung
Wenn ▲ = 7, ▼ = 3, ◇ = 2 und die Summe von ▲ und ▼ durch ◇ geteilt wird, was ist das Ergebnis?
Antworten
Start
Forum
3D: Modeling, Texturen, Licht, Animation, Rendern
Blender
Blender - Extrusion mit 30° Seitenwinkel
Oben