Antworten auf deine Fragen:
Neues Thema erstellen

Blender - Extrusion mit 30° Seitenwinkel

pixelmaker

jeden Tag neu hier

Hallo
ich suche eine Möglichkeit in Blender Schriften und andere Bereiche so zu extrudieren das die Kanten einen Winkel von 60° erhalten.
Ich habe es mit Bevel versucht, allerdings ist über "Custom Profil Type nur ein "ungefähr" einstellbar.
Die Oberflächen zu skalieren ist auch keine Lösung da ich für jede Höhe einer Extrusion die Skalierung extra ausrechnen müsste
Eine andere Möglichkeit in einem Winkel zu extrudieren habe ich nicht gefunden.

Ich suche also eine Möglichkeit eine Extrusion in unterschiedlichen Höhen (z.B. auf 0.3, 0.6, 0.8) zu machen bei der die Seiten in einem definierten Winkel abgeschrägt werden.
Es soll dann so aussehen wie hier:

Kann mir jemand einen Tipp geben, bitte

ralfg
 

Nedsch

Aktives Mitglied

Vorweg: habe noch nie mit Blender gearbeitet.
Geht es dir bei deinem Beispiel um die Typo ode die linke Form? Die Typo geht spitz zusammen. Hat also oben keine Fläche mehr. Das wird mit einfachem Beveln oder Extrudieren eh nicht funktionieren, oder? Wie sieht denn dein Mesh mit dem ungefähr eingestellten Profil aus?
 

pixelmaker

jeden Tag neu hier

Hallo
kurz zur Anforderung. Die Objekte werden Entwürfe für Münzen. Es wird ein Wettbewerb durch den Bund veranstaltet.
Von denen kommen auch die Anforderungen.

Ich bekomme von der Künstlerin den Entwurf als AI Datei. Die "Münze" wird ca 200mm im Durchmesser.
Für den ersten Test habe ich die einzelnen Ebenen extrudiert und einen 3D Druck aus Polyamid lasersinthern lassen. Das ging alles recht gut. Ich hänge nur an den Seitenkanten, die waren beim Test senkrecht.
Es gibt in den Entwürfen Texte und Objekte, wie eben den Adler, alle mit unterschiedlichen Höhen.

• Es geht mit den Texten über Bevel. Allerdings wird die Schräge zum Objekt dazugegeben. Der Text und die Objekte werden also um die Schräge fetter, des geht nicht.
• Objekte habe ich über den Bevel Modifier abgeschrägt, da geht es leider in die entgegen gesetzte Richtung, die Objekte werden kleiner und flacher durch den Bevel Modifier.
• Über Bevel (spacebar -> cmd + B) geht es gut, allerdings wird das eine Qual werden die ganzen Kanten zu selektieren. Außerdem kann ich über die Custom-Einstellung nur die Bevel-Kurve nach Augenmaß abschrägen.
Das ist aber der beste Weg den ich bisher gefunden habe.

Das muss doch besser gehen.
Ich könnte das Ganze auch mit meinem alten C4D R17 fertig stellen, damit möchte ich aber eigentlich nicht mehr arbeiten.
Dort kann ich beim Beveln allerdings "Kontur beibehalten" auswählen. Allerdings gehen dort auch nur 45° Abschrägung.


grüße
ralfg
 

noltehans

Aktives Mitglied

Das muss doch besser gehen.
Du kannst das Münzrand-Profil in deinem Lieblings Vektorprogramm erstellen.

Die SVG in Blender importieren.
Umschalten von 2D auf 3D. (Bild weiter unten)
Tab (Edit Modus)
A (alle Punkte auswählen)
R+x+90 (90 Grad um die X-Achse rotieren)
Tab (in den Objectmodus gehen)
RMB Convert to->Mesh
Tab into Editmode
A (select All)
F (füllt das ganze Ding mit einem N-Gon)
Video anschauen



Umschalten von 2D auf 3D:



Polysweeper Addon:
https://blenderartists.org/t/polysweeper/1147066
Thread

https://blenderartists.org/uploads/short-url/oSJCqdPHCpFspe5jaMJOXRUkIix.zip
Direkter Download (Free Version). Das Video ist mit der Free Version gemacht.

https://gumroad.com/l/wVOCh
Kommerzielle Version - 15$ - falls es ein bißchen mehr sein soll.


Doku
 

pixelmaker

jeden Tag neu hier

Hallo noltehans
VIELEN DANK für diese ausführliche und professionelle Antwort.
Die Zeichnung mit dem Profil ist aus dem Anforderungsprofil des Bundesverwaltugsamtes das diesen Wettbewerb durchführt und zeigt die Anforderungen in der Seitenansicht.
Ich muss also nicht dieses Profil an die Kanten setzen, sondern eine 30° Schräge. Eine schräge Kante müsste ich, so wie ich es verstehe, ebenfalls mit dem Add on erzeugen können.
Das ist nämlich das was ich nicht schaffe. Also Bereiche der Kante auswählen und die nach oben hin abschrägen.
Ich werde mir heute abend die ganze Polysweaper Dokumentation zu Gemüte führen, einschliesslich der ganzen Videos.
Nochmals vielen Dank.

ralfg
 

pixelmaker

jeden Tag neu hier

Ja, genau, es geht nur um eine Schräge. Also die Verjüngung von Objekten nach oben hin. Die Zeichnung ist ein Schnitt in Seitenansicht.
Ich zeichne also links und unten im rechten Winkel und die rechte Kante im spitzen Winkel von 60°
So müsste es gehen.
Wäre nur die Dokumentation des Polysweeper Addon nur halb so gut wie Deine Beschreibung…

grüße
ralfg
 

pixelmaker

jeden Tag neu hier

Dir geht es also nur um diese Schräge?
Hallo,
ich habe das Add on jetzt ausgiebig getestet.
Es löst mein Problem nicht. Grundsätzlich wird das erzeugte Profil immer außen angesetzt. Bei einer Schrift bedeutet das ein Auffetten der Schrift. Bei dem Winkel muss die Schrift nach oben hin dünner werden, nicht die Basis der Extrusion nach außen versetzen.
Die Basis der Schrift, die optisch das Schriftbild wieder gibt, darf sich nicht ändern. Ein Light-Font muss auch so bleiben und darf nicht durch eine größere Extrusionshöhe zu einer halbfetten Schrift werden.
Ich habe jetzt alles was mir eingefallen ist versucht. Ich finde keine Weg mit dem Add on die ursprünglich extrudierte Kante nach innen hin zu verändern. Ich müsste also für jede Höhe ausrechnen um wie viel sich das Objekt vergrößert und die Zeichnung entsprechend verändern. Bei den dünnen Schriften und kleinen Objekten ist dann nichts mehr da, eckige Objekte werden abgerundet und es ist ein absolut destruktiver Prozess. Änderungen sind später nur mit extremem Aufwand zu machen.

Ich habe den Weg versucht nach der Extrusion die oberen Kanten oder die obere Fläche (Deckel) zu skalieren.
Auch das geht nicht, denn Blender skaliert immer zum Zentrum des gesamten Objekts. So verschiebt sich die obere Kante oder der skalierte Deckel zum gemeinsamen Mittelpunkt hin.
Im Prinzip müsste ich bei der Extrusion der Seitenkanten eine Extrusion in einem Winkel <90° machen können und dann das Objekt oben schließen. Die Vorgehensweise sollte simpel sein, aber ich finde den Weg nicht.

Bis jetzt habe ich Formen in hartes Wachs CNC gefräst und einen 60° Fräser an den Kanten eingesetzt. Schriften wurden also um den 30° Schneidenwinkel abgeschrägt. Bis jetzt mussten immer Gipsmodelle abgeliefert werden. Das Gipsmodell habe ich dann vom Frästeil gegossen. Das Fräsen so großer glatter Oberflächen dauert extrem lange. 40 Stunden sind keine Seltenheit. Geht in der Zeit irgendetwas schief, da reicht schon eine Verunreinigung im Wachsblock, kann ich wieder von voerne anfangen.
Jetzt sind 3D Drucke als Modell zugelassen. Der erste Druck ist fertig und ok. Der Druck hat bei i.materialise 50€ gekostet, kein Vergleich zu den Fräsarbeiten. Allerdings sind die extrudierten Kanten alle 90°.
Jetzt scheitere ich daran das ich keine schräge Extrusion hin bekomme. Kann doch nicht sein. Ich bitte um Hilfe

grüße
ralfg
 

pixelmaker

jeden Tag neu hier

Evtl. wären hier Rhino3D oder MoI 3D die bessere Wahl.
Ja, danke.
Das ganze ist für mich keine kommerzielle Aufgabe. Eine Software für 1K€ kann ich dafür nicht anschaffen und lernen. Das ist ein Projekt für Kunststudenten denen ihr Werklehrer abhanden gekommen ist. Der alte Herr hat das immer gefräst. Und mit der Frage ob ich das fräsen kann ist man auch an mich heran getreten. Meine Lösung mit dem 3D Druck ist sehr gut angekommen nur müssen halt die Vorgaben erfüllt werden.
Für mich ist es allerdings eine Zeitfrage. Ich muss nebenher kräftig arbeiten. Ich habe das vielleicht etwas zu blauäugig angeboten.
Ich habe nicht mit so einem Problem gerechnet.

Ich werde heut abend mein altes C4DR17 wieder installieren.
Dort gibt es "Kante extrudieren mit Winkel (Kante Winkel [-∞..+∞°])
Eigentlich wollte ich meine 3D Ausflüge zukünftig mit Blender machen und das damit geschürfte Geld lieber an blender foundation spenden als zu Maxon tragen.

grüße
ralfg
 
Zuletzt bearbeitet:

Nedsch

Aktives Mitglied

In Modo können eigene Bevel-Profile angelegt werden. Also auch eine schräge Linie im Winkel von 30°. In Blender gibts wohl auch etwas ähnliches. Sieht aber so aus, dass das nur per Augenmaß funktioniert.

 

KBB

Mod 3D | Blaubaer

Teammitglied
Fusion 360 für Privatanwender oder SketchUp Free kämen vermutlich noch in Frage, beide kostenlos. Wobei ich Letzteres nicht gut genug kenne, um definitiv sagen zu können, dass die Abschrägung Deinen Vorgaben nach möglich ist.

Es gibt durchaus noch weitere kostenfreie CAD Programme auf dem Markt. Hier ist eine der vielen Listen, in denen aber auch gerne die echte gesuchte Software mit völlig anderen Sachen gemischt wird. Deshalb taucht in dieser u.a. Blender oder Wings3D auf, die beide nix mit CAD zu tun haben.
 

pixelmaker

jeden Tag neu hier

Hallo
ich kämpfe immer noch.
Mittlerweile tickt die Zeit denn ich habe die finalen Entwürfe zur Bearbeitung erhalten.
Was ich rausgefunden habe ist folgendes.
Exportiere ich die Konturen aus Illustrator in SVG oder DXF finde ich in Blender keinen logischen Weg Bevel oder andere Bearbeitungen anzuwenden. Mal geht es mal nicht.
Ich habe jetzt ein DXF aus Illu in meinem CAM geöffnet. Dort sehe ich das die Linien aus Illu teilweise als Splines und teilweise als Polylinien ankommen. Ich habe nun alle Splines in Polylinien geändert und wieder als DXF exportiert.
Die Konturen dieser Datei habe ich nun in Blender extrudiert und zu Mesh konvertiert und oben geschlossen. Es gibt plötzlich keine Probleme mehr die Objekte mit Bevel abzuschrägen.
Die ganzen unlogischen Probleme die vorher aufgetreten sind gibt es nicht mehr. Es scheint wohl so das die Splines in den Illu-Dateien die weitere Bearbeitung verhindern.

grüße
 

pixelmaker

jeden Tag neu hier

Hallo
eigentlich sind zwei Fehler in Deiner Aussage.
1.) Hätte ich nur gerade Polylinien dann wären die, genau wie die daraus erzeugten Polygone gerade Stücke. Da wir ja mit Meshes arbeiten könnte ich als aus geraden Polylinien ein Mesh erzeugen was genauso "glatt" ist wie das Polygonobjekt. Es braucht also nur eine aus geraden Polylinien bestehende gebogene Linie deren kurze Geraden nur kürzer sind als die Länge der erzeugten Polygone.
Und die Rechenzeiten sind nicht so aufwändig wie von Dir beschrieben. In Maschinensteuerungen werden kurze Bögen immer in Geraden umgewandelt. Keine Fräse, Plotter, 3D Drucker, etc. bearbeitet kurze Bögen als Bogen. Dadurch würden eben die Rechenzeiten zu lang. Oft werden durch die Steuerungen alle Bögen in Geraden konvertiert. Jeder 3D Drucker arbeitet nur mit G1 Befehlen, also Gerade von Anfangskoordinate zu Endkoordinate. Ich kenne keine Steuerung für 3D Drucker die mit G2 oder G3 (Bogen rechts, Bogen Links) arbeitet. In meinem CAM zur Fräse werden die Bögen unter 1/100mm Länge automatisch in Geraden konvertiert.

2.) Eine Polylinie muss nicht gerade sein, sie kann auch gebogen sein.
Im karthesischen Koordinatenkreuz wird ein Kreis aus Polylinien erzeugt, es dürfen keine Koordinaten doppelt vorkommen. Ein Kreis besteht also mindestens aus 2, unterschiedlich langen, Polylinien-Bögen.
Eine gebogene Polylinie besteht aus Knotenpunkten X,Y,Z, denen die Info einer Biegung hinzugefügt wird (bulge).




Ein Spline besteht aus Stützpunkten, die Koordinaten XYZ haben und Kontrollpunkten (im Bild gelb) die den Bogen durch eine Formel steuern.


Illustrator exportiert die Kurven als Splines, dabei ist es egal ob im DXF Format oder als XML im SVG Format. Daraus resultiert in Blender das es das gleiche Verhalten gibt wie bei einem Mesh aus Tris (dreieckige Polygone) oder einem Mesh was aus Tris wieder in Quads konvertiert wurde. Das beveln funktioniert nicht. Zu sehen ist das in Blender daran, das eine Fläche (mit F erzeugt) aussieht als würde sie aus Tris bestehen.

Es gibt den Online-Konvertierer Convertio. Der erzeugt Bögen nur aus Polylinien. Dadurch bin ich auf das Problem gestoßen. Es hat mit dem Beveln nicht funktioniert, nachdem ich meine Zeichnung dort zu SVG oder DXF konvertiert habe funktionierte die von Dir beschriebene Vorgehensweise in Blender.
Mit nativen Dateien aus Illustrator geht es eben nicht.
Zu sehen sind die unterschiedlichen Linienaufbauten in meinem CAM, das kann mit Splines und Polylinien umgehen und auch die Unterschiede anzeigen und Splines zu Polylinien konvertieren.
Ich bin erstmal froh einen funktionierenden Weg gefunden zu haben.

grüße
ralfg
 
Zuletzt bearbeitet:

noltehans

Aktives Mitglied

Ich bin erstmal froh einen funktionierenden Weg gefunden zu haben.
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()
 

CUBEMAN

Polyboy

Im Textobjekt selbst, gibt es die Möglichkeit Bevel über eine Kurve zu definieren. Allerdings ist die Handhabung etwas tricky, um es mal vorsichtig auszudrücken.
 
Bilder bitte hier hochladen und danach über das Bild-Icon (Direktlink vorher kopieren) platzieren.
Antworten auf deine Fragen:
Neues Thema erstellen

Willkommen auf PSD-Tutorials.de

In unseren Foren vernetzt du dich mit anderen Personen, um dich rund um die Themen Fotografie, Grafik, Gestaltung, Bildbearbeitung und 3D auszutauschen. Außerdem schalten wir für dich regelmäßig kostenlose Inhalte frei. Liebe Grüße senden dir die PSD-Gründer Stefan und Matthias Petri aus Waren an der Müritz. Hier erfährst du mehr über uns.

Stefan und Matthias Petri von PSD-Tutorials.de

Nächster neuer Gratisinhalt

03
Stunden
:
:
25
Minuten
:
:
19
Sekunden

Neueste Themen & Antworten

Flatrate für Tutorials, Assets, Vorlagen

Zurzeit aktive Besucher

Statistik des Forums

Themen
118.632
Beiträge
1.538.443
Mitglieder
67.556
Neuestes Mitglied
Mattlack
Oben