Blender - Extrusion mit 30° Seitenwinkel

Blender - Extrusion mit 30° Seitenwinkel | PSD-Tutorials.de

Erstellt von pixelmaker, 01.04.2021.

  1. pixelmaker

    pixelmaker jeden Tag neu hier

    Dabei seit:
    20.03.2009
    Beiträge:
    1.098
    Geschlecht:
    männlich
    Ort:
    Callenberg, bei Hohenstein-Er
    Software:
    CC aktuell, C4D, Blender, CamBam, mac OS X 11.5 auf Mac Pro 12core,RAM und Sonstiges satt
    Kameratyp:
    DSLR Nikon
    Blender - Extrusion mit 30° Seitenwinkel
    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:
    [​IMG]
    Kann mir jemand einen Tipp geben, bitte

    ralfg
     
    #1      
  2. TutKit

    TutKit Vollzugriff

    Vollzugriff auf sämtliche Inhalte für Photoshop, InDesign, Affinity, 3D, Video & Office

    Suchst du einen effektiven Weg, um deine Geschäftsideen aber auch persönlichen Kenntnisse zu fördern? Teste unsere Lösung mit Vollzugriff auf Tutorials und Vorlagen/Erweiterungen, die dich schneller zum Ziel bringen. Klicke jetzt hier und teste uns kostenlos!

  3. Nedsch

    Nedsch Aktives Mitglied

    Dabei seit:
    23.07.2010
    Beiträge:
    1.760
    Geschlecht:
    männlich
    Ort:
    Hamburg
    Software:
    Photoshop CC, Modo
    Blender - Extrusion mit 30° Seitenwinkel
    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?
     
    #2      
  4. noltehans

    noltehans Aktives Mitglied

    Dabei seit:
    02.06.2009
    Beiträge:
    713
    Geschlecht:
    männlich
    Blender - Extrusion mit 30° Seitenwinkel
    #3      
    subcomtom gefällt das.
  5. pixelmaker

    pixelmaker jeden Tag neu hier

    Dabei seit:
    20.03.2009
    Beiträge:
    1.098
    Geschlecht:
    männlich
    Ort:
    Callenberg, bei Hohenstein-Er
    Software:
    CC aktuell, C4D, Blender, CamBam, mac OS X 11.5 auf Mac Pro 12core,RAM und Sonstiges satt
    Kameratyp:
    DSLR Nikon
    Blender - Extrusion mit 30° Seitenwinkel
    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.
    [​IMG]
    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
     
    #4      
  6. noltehans

    noltehans Aktives Mitglied

    Dabei seit:
    02.06.2009
    Beiträge:
    713
    Geschlecht:
    männlich
    Blender - Extrusion mit 30° Seitenwinkel
    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
    https://we.tl/t-4CqrUTCcW8


    Umschalten von 2D auf 3D:
    [​IMG]


    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.

    https://sites.google.com/view/polysweeper/home
    Doku
     
    #5      
  7. pixelmaker

    pixelmaker jeden Tag neu hier

    Dabei seit:
    20.03.2009
    Beiträge:
    1.098
    Geschlecht:
    männlich
    Ort:
    Callenberg, bei Hohenstein-Er
    Software:
    CC aktuell, C4D, Blender, CamBam, mac OS X 11.5 auf Mac Pro 12core,RAM und Sonstiges satt
    Kameratyp:
    DSLR Nikon
    Blender - Extrusion mit 30° Seitenwinkel
    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
     
    #6      
  8. noltehans

    noltehans Aktives Mitglied

    Dabei seit:
    02.06.2009
    Beiträge:
    713
    Geschlecht:
    männlich
    Blender - Extrusion mit 30° Seitenwinkel
    Dir geht es also nur um diese Schräge?
    [​IMG]

    Falls ja,...
    [​IMG]
     
    #7      
  9. pixelmaker

    pixelmaker jeden Tag neu hier

    Dabei seit:
    20.03.2009
    Beiträge:
    1.098
    Geschlecht:
    männlich
    Ort:
    Callenberg, bei Hohenstein-Er
    Software:
    CC aktuell, C4D, Blender, CamBam, mac OS X 11.5 auf Mac Pro 12core,RAM und Sonstiges satt
    Kameratyp:
    DSLR Nikon
    Blender - Extrusion mit 30° Seitenwinkel
    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
     
    #8      
  10. pixelmaker

    pixelmaker jeden Tag neu hier

    Dabei seit:
    20.03.2009
    Beiträge:
    1.098
    Geschlecht:
    männlich
    Ort:
    Callenberg, bei Hohenstein-Er
    Software:
    CC aktuell, C4D, Blender, CamBam, mac OS X 11.5 auf Mac Pro 12core,RAM und Sonstiges satt
    Kameratyp:
    DSLR Nikon
    Blender - Extrusion mit 30° Seitenwinkel
    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
     
    #9      
  11. KBB

    KBB Mod 3D | Blaubaer Teammitglied

    Dabei seit:
    18.03.2009
    Beiträge:
    9.510
    Geschlecht:
    männlich
    Blender - Extrusion mit 30° Seitenwinkel
    CAD Programme dafür nutzen.
    Evtl. wären hier Rhino3D oder MoI 3D die bessere Wahl.
     
    #10      
  12. pixelmaker

    pixelmaker jeden Tag neu hier

    Dabei seit:
    20.03.2009
    Beiträge:
    1.098
    Geschlecht:
    männlich
    Ort:
    Callenberg, bei Hohenstein-Er
    Software:
    CC aktuell, C4D, Blender, CamBam, mac OS X 11.5 auf Mac Pro 12core,RAM und Sonstiges satt
    Kameratyp:
    DSLR Nikon
    Blender - Extrusion mit 30° Seitenwinkel
    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: 08.04.2021
    #11      
  13. Nedsch

    Nedsch Aktives Mitglied

    Dabei seit:
    23.07.2010
    Beiträge:
    1.760
    Geschlecht:
    männlich
    Ort:
    Hamburg
    Software:
    Photoshop CC, Modo
    Blender - Extrusion mit 30° Seitenwinkel
    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.


     
    #12      
  14. KBB

    KBB Mod 3D | Blaubaer Teammitglied

    Dabei seit:
    18.03.2009
    Beiträge:
    9.510
    Geschlecht:
    männlich
    Blender - Extrusion mit 30° Seitenwinkel
    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.
     
    #13      
  15. pixelmaker

    pixelmaker jeden Tag neu hier

    Dabei seit:
    20.03.2009
    Beiträge:
    1.098
    Geschlecht:
    männlich
    Ort:
    Callenberg, bei Hohenstein-Er
    Software:
    CC aktuell, C4D, Blender, CamBam, mac OS X 11.5 auf Mac Pro 12core,RAM und Sonstiges satt
    Kameratyp:
    DSLR Nikon
    Blender - Extrusion mit 30° Seitenwinkel
    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
     
    #14      
  16. noltehans

    noltehans Aktives Mitglied

    Dabei seit:
    02.06.2009
    Beiträge:
    713
    Geschlecht:
    männlich
    Blender - Extrusion mit 30° Seitenwinkel
    Einfacher wäre es wohl gewesen die Illu-SVG mit Inkscape zu "konvertieren".


    Was machst du da eigentlich?
    Überleg mal wieviel "Trillionen" Polylinien du benötigst um etwas wirklich rund zu bekommen.
     
    #15      
  17. pixelmaker

    pixelmaker jeden Tag neu hier

    Dabei seit:
    20.03.2009
    Beiträge:
    1.098
    Geschlecht:
    männlich
    Ort:
    Callenberg, bei Hohenstein-Er
    Software:
    CC aktuell, C4D, Blender, CamBam, mac OS X 11.5 auf Mac Pro 12core,RAM und Sonstiges satt
    Kameratyp:
    DSLR Nikon
    Blender - Extrusion mit 30° Seitenwinkel
    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).

    [​IMG]
    [​IMG]

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

    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: 14.04.2021
    #16      
  18. noltehans

    noltehans Aktives Mitglied

    Dabei seit:
    02.06.2009
    Beiträge:
    713
    Geschlecht:
    männlich
    Blender - Extrusion mit 30° Seitenwinkel
    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 (Text):
    1. # ***** BEGIN GPL LICENSE BLOCK *****
    2. #
    3. #
    4. # This program is free software; you can redistribute it and/or
    5. # modify it under the terms of the GNU General Public License
    6. # as published by the Free Software Foundation; either version 2
    7. # of the License, or (at your option) any later version.
    8. #
    9. # This program is distributed in the hope that it will be useful,
    10. # but WITHOUT ANY WARRANTY; without even the implied warranty of
    11. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.    See the
    12. # GNU General Public License for more details.
    13. #
    14. # You should have received a copy of the GNU General Public License
    15. # along with this program; if not, write to the Free Software Foundation,
    16. # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
    17. #
    18. # ***** END GPL LICENCE BLOCK *****
    19.  
    20. bl_info = {
    21.     "name": "Hidesato Offset Edges",
    22.     "author": "Hidesato Ikeya",
    23.     "version": (0, 4, 0),
    24.     "blender": (2, 82, 0),
    25.     "location": "VIEW3D > Edge menu(CTRL-E) > Offset Edges",
    26.     "description": "Offset Edges",
    27.     "warning": "",
    28.     #"wiki_url": "http://wiki.blender.org/index.php/Extensions:2.6/Py/Scripts/Modeling/offset_edges",
    29.     "tracker_url": "",
    30.     "category": "Mesh"}
    31.  
    32. import math
    33. from math import sin, cos, pi, copysign, radians, degrees, atan, sqrt
    34. import bpy
    35. import mathutils
    36. from bpy_extras import view3d_utils
    37. import bmesh
    38. from mathutils import Vector
    39. from time import perf_counter
    40.  
    41. X_UP = Vector((1.0, .0, .0))
    42. Y_UP = Vector((.0, 1.0, .0))
    43. Z_UP = Vector((.0, .0, 1.0))
    44. ZERO_VEC = Vector((.0, .0, .0))
    45. ANGLE_1 = pi / 180
    46. ANGLE_90 = pi / 2
    47. ANGLE_180 = pi
    48. ANGLE_360 = 2 * pi
    49.  
    50. class OffsetEdgesPreferences(bpy.types.AddonPreferences):
    51.     bl_idname = __name__
    52.    
    53.     interactive: bpy.props.BoolProperty(
    54.         name = "Interactive",
    55.         description = "makes operation interactive",
    56.         default = True)
    57.     free_move: bpy.props.BoolProperty(
    58.         name = "Free Move",
    59.         description = "enables to adjust both width and depth while pressing ctrl-key",
    60.         default = False)
    61.  
    62.     def draw(self, context):
    63.         layout = self.layout
    64.         row = layout.row()
    65.         row.prop(self, "interactive")
    66.         if self.interactive:
    67.             row.prop(self, "free_move")
    68.  
    69. #######################################################################
    70.  
    71. class OffsetBase:
    72.     threshold: bpy.props.FloatProperty(
    73.         name="Flat Face Threshold", default=radians(0.05), precision=5,
    74.         step=1.0e-4, subtype='ANGLE',
    75.         description="If difference of angle between two adjacent faces is "
    76.                     "below this value, those faces are regarded as flat.",
    77.         options={'HIDDEN'})
    78.     caches_valid: bpy.props.BoolProperty(
    79.         name="Caches Valid", default=False,
    80.         options={'HIDDEN'})
    81.  
    82.     _cache_offset_infos = None
    83.     _cache_edges_orig = None
    84.  
    85.     def use_caches(self, context):
    86.         self.caches_valid = True
    87.  
    88.     def get_caches(self, bm):
    89.         bmverts = tuple(bm.verts)
    90.         bmedges = tuple(bm.edges)
    91.  
    92.         offset_infos = \
    93.             [(bmverts[vix], co, d) for vix, co, d in self._cache_offset_infos]
    94.         edges_orig = [bmedges[eix] for eix in self._cache_edges_orig]
    95.  
    96.         for e in edges_orig:
    97.             e.select = False
    98.         for f in bm.faces:
    99.             f.select = False
    100.  
    101.         return offset_infos, edges_orig
    102.  
    103.     def save_caches(self, offset_infos, edges_orig):
    104.         self._cache_offset_infos = tuple((v.index, co, d) for v, co, d in offset_infos)
    105.         self._cache_edges_orig = tuple(e.index for e in edges_orig)
    106.  
    107.     @staticmethod
    108.     def is_face_selected(ob_edit):
    109.         bpy.ops.object.mode_set(mode="OBJECT")
    110.         me = ob_edit.data
    111.         for p in me.polygons:
    112.             if p.select:
    113.                 bpy.ops.object.mode_set(mode="EDIT")
    114.                 return True
    115.         bpy.ops.object.mode_set(mode="EDIT")
    116.  
    117.         return False
    118.     @staticmethod
    119.     def is_mirrored(ob_edit):
    120.         for mod in ob_edit.modifiers:
    121.             if mod.type == 'MIRROR' and mod.use_mirror_merge:
    122.                 return True
    123.         return False
    124.  
    125.     @staticmethod
    126.     def reorder_loop(verts, edges, lp_normal, adj_faces):
    127.         for i, adj_f in enumerate(adj_faces):
    128.             if adj_f is None:
    129.                 continue
    130.             v1, v2 = verts[i], verts[i+1]
    131.             e = edges[i]
    132.             fv = tuple(adj_f.verts)
    133.             if fv[fv.index(v1)-1] is v2:
    134.                 # Align loop direction
    135.                 verts.reverse()
    136.                 edges.reverse()
    137.                 adj_faces.reverse()
    138.             if lp_normal.dot(adj_f.normal) < .0:
    139.                 lp_normal *= -1
    140.             break
    141.         else:
    142.             # All elements in adj_faces are None
    143.             for v in verts:
    144.                 if v.normal != ZERO_VEC:
    145.                     if lp_normal.dot(v.normal) < .0:
    146.                         verts.reverse()
    147.                         edges.reverse()
    148.                         lp_normal *= -1
    149.                     break
    150.  
    151.         return verts, edges, lp_normal, adj_faces
    152.  
    153.     @staticmethod
    154.     def get_cross_rail(vec_tan, vec_edge_r, vec_edge_l, normal_r, normal_l):
    155.         # Cross rail is a cross vector between normal_r and normal_l.
    156.  
    157.         vec_cross = normal_r.cross(normal_l)
    158.         if vec_cross.dot(vec_tan) < .0:
    159.             vec_cross *= -1
    160.         cos_min = min(vec_tan.dot(vec_edge_r), vec_tan.dot(-vec_edge_l))
    161.         cos = vec_tan.dot(vec_cross)
    162.         if cos >= cos_min:
    163.             vec_cross.normalize()
    164.             return vec_cross
    165.         else:
    166.             return None
    167.  
    168.     @staticmethod
    169.     def get_edge_rail(vert, set_edges_orig):
    170.         co_edges = co_edges_selected = 0
    171.         vec_inner = None
    172.         for e in vert.link_edges:
    173.             if (e not in set_edges_orig and
    174.                (e.select or (co_edges_selected == 0 and not e.hide))):
    175.                 v_other = e.other_vert(vert)
    176.                 vec = v_other.co - vert.co
    177.                 if vec != ZERO_VEC:
    178.                     vec_inner = vec
    179.                     if e.select:
    180.                         co_edges_selected += 1
    181.                         if co_edges_selected == 2:
    182.                             return None
    183.                     else:
    184.                         co_edges += 1
    185.         if co_edges_selected == 1:
    186.             vec_inner.normalize()
    187.             return vec_inner
    188.         elif co_edges == 1:
    189.             # No selected edges, one unselected edge.
    190.             vec_inner.normalize()
    191.             return vec_inner
    192.         else:
    193.             return None
    194.  
    195.     @staticmethod
    196.     def get_mirror_rail(mirror_plane, vec_up):
    197.         p_norm = mirror_plane[1]
    198.         mirror_rail = vec_up.cross(p_norm)
    199.         if mirror_rail != ZERO_VEC:
    200.             mirror_rail.normalize()
    201.             # Project vec_up to mirror_plane
    202.             vec_up = vec_up - vec_up.project(p_norm)
    203.             vec_up.normalize()
    204.             return mirror_rail, vec_up
    205.         else:
    206.             return None, vec_up
    207.  
    208.     @staticmethod
    209.     def get_vert_mirror_pairs(set_edges_orig, mirror_planes):
    210.         if mirror_planes:
    211.             set_edges_copy = set_edges_orig.copy()
    212.             vert_mirror_pairs = dict()
    213.             for e in set_edges_orig:
    214.                 v1, v2 = e.verts
    215.                 for mp in mirror_planes:
    216.                     p_co, p_norm, mlimit = mp
    217.                     v1_dist = abs(p_norm.dot(v1.co - p_co))
    218.                     v2_dist = abs(p_norm.dot(v2.co - p_co))
    219.                     if v1_dist <= mlimit:
    220.                         # v1 is on a mirror plane.
    221.                         vert_mirror_pairs[v1] = mp
    222.                     if v2_dist <= mlimit:
    223.                         # v2 is on a mirror plane.
    224.                         vert_mirror_pairs[v2] = mp
    225.                     if v1_dist <= mlimit and v2_dist <= mlimit:
    226.                         # This edge is on a mirror_plane, so should not be offsetted.
    227.                         set_edges_copy.remove(e)
    228.             return vert_mirror_pairs, set_edges_copy
    229.         else:
    230.             return None, set_edges_orig
    231.  
    232.     @staticmethod
    233.     def collect_mirror_planes(ob_edit):
    234.         mirror_planes = []
    235.         eob_mat_inv = ob_edit.matrix_world.inverted()
    236.         for m in ob_edit.modifiers:
    237.             if (m.type == 'MIRROR' and m.use_mirror_merge):
    238.                 merge_limit = m.merge_threshold
    239.                 if not m.mirror_object:
    240.                     loc = ZERO_VEC
    241.                     norm_x, norm_y, norm_z = X_UP, Y_UP, Z_UP
    242.                 else:
    243.                     mirror_mat_local = eob_mat_inv * m.mirror_object.matrix_world
    244.                     loc = mirror_mat_local.to_translation()
    245.                     norm_x, norm_y, norm_z, _ = mirror_mat_local.adjugated()
    246.                     norm_x = norm_x.to_3d().normalized()
    247.                     norm_y = norm_y.to_3d().normalized()
    248.                     norm_z = norm_z.to_3d().normalized()
    249.                 if m.use_x:
    250.                     mirror_planes.append((loc, norm_x, merge_limit))
    251.                 if m.use_y:
    252.                     mirror_planes.append((loc, norm_y, merge_limit))
    253.                 if m.use_z:
    254.                     mirror_planes.append((loc, norm_z, merge_limit))
    255.         return mirror_planes
    256.  
    257.     @staticmethod
    258.     def collect_edges(bm):
    259.         set_edges_orig = set()
    260.         for e in bm.edges:
    261.             if e.select:
    262.                 co_faces_selected = 0
    263.                 for f in e.link_faces:
    264.                     if f.select:
    265.                         co_faces_selected += 1
    266.                         if co_faces_selected == 2:
    267.                             break
    268.                 else:
    269.                     set_edges_orig.add(e)
    270.  
    271.         if not set_edges_orig:
    272.             return None
    273.  
    274.         return set_edges_orig
    275.     @staticmethod
    276.     def collect_loops(set_edges_orig):
    277.         set_edges_copy = set_edges_orig.copy()
    278.  
    279.         loops = []  # [v, e, v, e, ... , e, v]
    280.         while set_edges_copy:
    281.             edge_start = set_edges_copy.pop()
    282.             v_left, v_right = edge_start.verts
    283.             lp = [v_left, edge_start, v_right]
    284.             reverse = False
    285.             while True:
    286.                 edge = None
    287.                 for e in v_right.link_edges:
    288.                     if e in set_edges_copy:
    289.                         if edge:
    290.                             # Overlap detected.
    291.                             return None
    292.                         edge = e
    293.                         set_edges_copy.remove(e)
    294.                 if edge:
    295.                     v_right = edge.other_vert(v_right)
    296.                     lp.extend((edge, v_right))
    297.                     continue
    298.                 else:
    299.                     if v_right is v_left:
    300.                         # Real loop.
    301.                         loops.append(lp)
    302.                         break
    303.                     elif reverse is False:
    304.                         # Right side of half loop.
    305.                         # Reversing the loop to operate same procedure on the left side.
    306.                         lp.reverse()
    307.                         v_right, v_left = v_left, v_right
    308.                         reverse = True
    309.                         continue
    310.                     else:
    311.                         # Half loop, completed.
    312.                         loops.append(lp)
    313.                         break
    314.         return loops
    315.  
    316.     @staticmethod
    317.     def calc_loop_normal(verts, fallback=Z_UP):
    318.         # Calculate normal from verts using Newell's method.
    319.         normal = ZERO_VEC.copy()
    320.  
    321.         if verts[0] is verts[-1]:
    322.             # Perfect loop
    323.             range_verts = range(1, len(verts))
    324.         else:
    325.             # Half loop
    326.             range_verts = range(0, len(verts))
    327.  
    328.         for i in range_verts:
    329.             v1co, v2co = verts[i-1].co, verts[i].co
    330.             normal.x += (v1co.y - v2co.y) * (v1co.z + v2co.z)
    331.             normal.y += (v1co.z - v2co.z) * (v1co.x + v2co.x)
    332.             normal.z += (v1co.x - v2co.x) * (v1co.y + v2co.y)
    333.  
    334.         if normal != ZERO_VEC:
    335.             normal.normalize()
    336.         else:
    337.             normal = fallback
    338.  
    339.         return normal
    340.  
    341.     @staticmethod
    342.     def get_adj_faces(edges):
    343.         adj_faces = []
    344.         for e in edges:
    345.             adj_f = None
    346.             co_adj = 0
    347.             for f in e.link_faces:
    348.                 # Search an adjacent face.
    349.                 # Selected face has precedance.
    350.                 if not f.hide and f.normal != ZERO_VEC:
    351.                     adj_exist = True
    352.                     adj_f = f
    353.                     co_adj += 1
    354.                     if f.select:
    355.                         adj_faces.append(adj_f)
    356.                         break
    357.             else:
    358.                 if co_adj == 1:
    359.                     adj_faces.append(adj_f)
    360.                 else:
    361.                     adj_faces.append(None)
    362.         return adj_faces
    363.  
    364.     @staticmethod
    365.     def get_directions(lp, vec_upward, normal_fallback, vert_mirror_pairs,
    366.                        **options):
    367.         opt_follow_face = options.get("follow_face")
    368.         opt_edge_rail = options.get("edge_rail")
    369.         opt_er_only_end = options.get("edge_rail_only_end")
    370.         opt_threshold = options.get("threshold")
    371.         opt_normal_override = options.get("normal_override")
    372.  
    373.         verts, edges = lp[::2], lp[1::2]
    374.         set_edges = set(edges)
    375.         if opt_normal_override is None:
    376.             lp_normal = OffsetBase.calc_loop_normal(verts, fallback=normal_fallback)
    377.         else:
    378.             lp_normal = opt_normal_override
    379.             opt_follow_face = False
    380.  
    381.         ##### Loop order might be changed below.
    382.         if lp_normal.dot(vec_upward) < .0:
    383.             # Make this loop's normal towards vec_upward.
    384.             verts.reverse()
    385.             edges.reverse()
    386.             lp_normal *= -1
    387.  
    388.         if opt_follow_face:
    389.             adj_faces = OffsetBase.get_adj_faces(edges)
    390.             verts, edges, lp_normal, adj_faces = \
    391.                 OffsetBase.reorder_loop(verts, edges, lp_normal, adj_faces)
    392.         else:
    393.             adj_faces = (None, ) * len(edges)
    394.         ##### Loop order might be changed above.
    395.  
    396.         vec_edges = tuple((e.other_vert(v).co - v.co).normalized()
    397.                           for v, e in zip(verts, edges))
    398.  
    399.         if verts[0] is verts[-1]:
    400.             # Real loop. Popping last vertex.
    401.             verts.pop()
    402.             HALF_LOOP = False
    403.         else:
    404.             # Half loop
    405.             HALF_LOOP = True
    406.  
    407.         len_verts = len(verts)
    408.         directions = []
    409.         for i in range(len_verts):
    410.             vert = verts[i]
    411.             ix_right, ix_left = i, i-1
    412.  
    413.             VERT_END = False
    414.             if HALF_LOOP:
    415.                 if i == 0:
    416.                     # First vert
    417.                     ix_left = ix_right
    418.                     VERT_END = True
    419.                 elif i == len_verts - 1:
    420.                     # Last vert
    421.                     ix_right = ix_left
    422.                     VERT_END = True
    423.  
    424.             edge_right, edge_left = vec_edges[ix_right], vec_edges[ix_left]
    425.             face_right, face_left = adj_faces[ix_right], adj_faces[ix_left]
    426.  
    427.             norm_right = face_right.normal if face_right else lp_normal
    428.             norm_left = face_left.normal if face_left else lp_normal
    429.             if norm_right.angle(norm_left) > opt_threshold:
    430.                 # Two faces are not flat.
    431.                 two_normals = True
    432.             else:
    433.                 two_normals = False
    434.  
    435.             tan_right = edge_right.cross(norm_right).normalized()
    436.             tan_left = edge_left.cross(norm_left).normalized()
    437.             tan_avr = (tan_right + tan_left).normalized()
    438.             norm_avr = (norm_right + norm_left).normalized()
    439.  
    440.             rail = None
    441.             if two_normals or opt_edge_rail:
    442.                 # Get edge rail.
    443.                 # edge rail is a vector of an inner edge.
    444.                 if two_normals or (not opt_er_only_end) or VERT_END:
    445.                     rail = OffsetBase.get_edge_rail(vert, set_edges)
    446.             if vert_mirror_pairs and VERT_END:
    447.                 if vert in vert_mirror_pairs:
    448.                     rail, norm_avr = \
    449.                         OffsetBase.get_mirror_rail(vert_mirror_pairs[vert], norm_avr)
    450.             if (not rail) and two_normals:
    451.                 # Get cross rail.
    452.                 # Cross rail is a cross vector between norm_right and norm_left.
    453.                 rail = OffsetBase.get_cross_rail(
    454.                     tan_avr, edge_right, edge_left, norm_right, norm_left)
    455.             if rail:
    456.                 dot = tan_avr.dot(rail)
    457.                 if dot > .0:
    458.                     tan_avr = rail
    459.                 elif dot < .0:
    460.                     tan_avr = -rail
    461.  
    462.             vec_plane = norm_avr.cross(tan_avr)
    463.             e_dot_p_r = edge_right.dot(vec_plane)
    464.             e_dot_p_l = edge_left.dot(vec_plane)
    465.             if e_dot_p_r or e_dot_p_l:
    466.                 if e_dot_p_r > e_dot_p_l:
    467.                     vec_edge, e_dot_p = edge_right, e_dot_p_r
    468.                 else:
    469.                     vec_edge, e_dot_p = edge_left, e_dot_p_l
    470.  
    471.                 vec_tan = (tan_avr - tan_avr.project(vec_edge)).normalized()
    472.                 # Make vec_tan perpendicular to vec_edge
    473.                 vec_up = vec_tan.cross(vec_edge)
    474.  
    475.                 vec_width = vec_tan - (vec_tan.dot(vec_plane) / e_dot_p) * vec_edge
    476.                 vec_depth = vec_up - (vec_up.dot(vec_plane) / e_dot_p) * vec_edge
    477.             else:
    478.                 vec_width = tan_avr
    479.                 vec_depth = norm_avr
    480.  
    481.             directions.append((vec_width, vec_depth))
    482.  
    483.         return verts, directions
    484.  
    485.     def get_offset_infos(self, bm, ob_edit, **options):
    486.         time = perf_counter()
    487.         opt_mirror_modifier = options.get("mirror_modifier")
    488.  
    489.         set_edges_orig = self.collect_edges(bm)
    490.         if set_edges_orig is None:
    491.             self.report({'WARNING'},
    492.                         "No edges are selected.")
    493.             return False, False
    494.  
    495.         if opt_mirror_modifier:
    496.             mirror_planes = self.collect_mirror_planes(ob_edit)
    497.             vert_mirror_pairs, set_edges = \
    498.                 self.get_vert_mirror_pairs(set_edges_orig, mirror_planes)
    499.  
    500.             if set_edges:
    501.                 set_edges_orig = set_edges
    502.             else:
    503.                 #self.report({'WARNING'},
    504.                 #            "All selected edges are on mirror planes.")
    505.                 vert_mirror_pairs = None
    506.         else:
    507.             vert_mirror_pairs = None
    508.         edges_orig = list(set_edges_orig)
    509.  
    510.         loops = self.collect_loops(set_edges_orig)
    511.         if loops is None:
    512.             self.report({'WARNING'},
    513.                         "Overlapping edge loops detected. Select discrete edge loops")
    514.             return False, False
    515.  
    516.         vec_upward = (X_UP + Y_UP + Z_UP).normalized()
    517.         # vec_upward is used to unify loop normals when follow_face is off.
    518.         normal_fallback = Z_UP
    519.         #normal_fallback = Vector(context.region_data.view_matrix[2][:3])
    520.         # normal_fallback is used when loop normal cannot be calculated.
    521.  
    522.         offset_infos = []
    523.         for lp in loops:
    524.             verts, directions = self.get_directions(
    525.                 lp, vec_upward, normal_fallback, vert_mirror_pairs,
    526.                 **options
    527.             )
    528.             if verts:
    529.                 # convert vert objects to vert indexs
    530.                 for v, d in zip(verts, directions):
    531.                     offset_infos.append((v, v.co.copy(), d))
    532.  
    533.         for e in edges_orig:
    534.             e.select = False
    535.         for f in bm.faces:
    536.             f.select = False
    537.  
    538.         #print("OffsetEdges - Calculating: ", perf_counter() - time)
    539.  
    540.         return offset_infos, edges_orig
    541.  
    542.     @staticmethod
    543.     def extrude_and_pairing(bm, edges_orig, ref_verts):
    544.         """ ref_verts is a list of vertices, each of which should be
    545.         one end of an edge in edges_orig"""
    546.         extruded = bmesh.ops.extrude_edge_only(bm, edges=edges_orig)['geom']
    547.         n_edges = n_faces = len(edges_orig)
    548.         n_verts = len(extruded) - n_edges - n_faces
    549.  
    550.         exverts = set(extruded[:n_verts])
    551.         exedges = set(extruded[n_verts:n_verts + n_edges])
    552.         #faces = set(extruded[n_verts + n_edges:])
    553.         side_edges = set(e for v in exverts for e in v.link_edges if e not in exedges)
    554.  
    555.         # ref_verts[i] and ret[i] are both ends of a side edge.
    556.         exverts_ordered = \
    557.             [e.other_vert(v) for v in ref_verts for e in v.link_edges if e in side_edges]
    558.  
    559.         return exverts_ordered, list(exedges), list(side_edges)
    560.  
    561.     @staticmethod
    562.     def move_verts(bm, me, width, depth, offset_infos, verts_offset=None, update=True):
    563.         if verts_offset is None:
    564.             for v, co, (vec_w, vec_d) in offset_infos:
    565.                 v.co = co + width * vec_w + depth * vec_d
    566.         else:
    567.             for (_, co, (vec_w, vec_d)), v in zip(offset_infos, verts_offset):
    568.                 v.co = co + width * vec_w + depth * vec_d
    569.  
    570.         if update:
    571.             bm.normal_update()
    572.             bmesh.update_edit_mesh(me)
    573.  
    574.  
    575. class OffsetEdges(bpy.types.Operator, OffsetBase):
    576.     """Offset Edges"""
    577.     bl_idname = "mesh.hidesato_offset_edges"
    578.     bl_label = "Offset Edges"
    579.     bl_options = {'REGISTER', 'UNDO'}
    580.  
    581.     follow_face: bpy.props.BoolProperty(
    582.         name="Follow Face", default=False,
    583.         description="Offset along faces around"
    584.     )
    585.     mirror_modifier: bpy.props.BoolProperty(
    586.         name="Mirror Modifier", default=False,
    587.         description="Take into account of Mirror modifier"
    588.     )
    589.     edge_rail: bpy.props.BoolProperty(
    590.         name="Edge Rail", default=False,
    591.         description="Align vertices along inner edges"
    592.     )
    593.     edge_rail_only_end: bpy.props.BoolProperty(
    594.         name="Edge Rail Only End", default=False,
    595.         description="Apply edge rail to end verts only"
    596.     )
    597.     lock_axis: bpy.props.EnumProperty(
    598.         items=[
    599.             ('none', "None", "Don't lock axis"),
    600.             ('x', "X", "Lock X axis"),
    601.             ('y', "Y", "Lock Y axis"),
    602.             ('z', "Z", "Lock Z axis"),
    603.             ('view', "VIEW", "Lock view axis")
    604.         ],
    605.         name="Lock Axis", default='none'
    606.     )
    607.  
    608.     # Functions below are update functions.
    609.  
    610.     def assign_angle_presets(self, context):
    611.         angle_presets = {'0В°': 0,
    612.                          '15В°': radians(15),
    613.                          '30В°': radians(30),
    614.                          '45В°': radians(45),
    615.                          '60В°': radians(60),
    616.                          '75В°': radians(75),
    617.                          '90В°': radians(90),}
    618.         self.angle = angle_presets[self.angle_presets]
    619.  
    620.     def change_depth_mode(self, context):
    621.         if self.depth_mode == 'angle':
    622.             self.width, self.angle = OffsetEdges.depth_to_angle(self.width, self.depth)
    623.         else:
    624.             self.width, self.depth = OffsetEdges.angle_to_depth(self.width, self.angle)
    625.  
    626.  
    627.     def angle_to_depth(width, angle):
    628.         """Returns: (converted_width, converted_depth)"""
    629.         return width * cos(angle), width * sin(angle)
    630.  
    631.  
    632.     def depth_to_angle(width, depth):
    633.         """Returns: (converted_width, converted_angle)"""
    634.         ret_width = sqrt(width * width + depth * depth)
    635.  
    636.         if width:
    637.             ret_angle = atan(depth / width)
    638.         elif depth == 0:
    639.             ret_angle = 0
    640.         elif depth > 0:
    641.             ret_angle = ANGLE_90
    642.         elif depth < 0:
    643.             ret_angle = -ANGLE_90
    644.  
    645.         return ret_width, ret_angle
    646.  
    647.     geometry_mode: bpy.props.EnumProperty(
    648.         items=[('offset', "Offset", "Offset edges"),
    649.                ('extrude', "Extrude", "Extrude edges"),
    650.                ('move', "Move", "Move selected edges")],
    651.         name="Geometory mode", default='offset',
    652.         update=OffsetBase.use_caches)
    653.     width: bpy.props.FloatProperty(
    654.         name="Width", default=.2, precision=4, step=1,
    655.         update=OffsetBase.use_caches)
    656.     flip_width: bpy.props.BoolProperty(
    657.         name="Flip Width", default=False,
    658.         description="Flip width direction",
    659.         update=OffsetBase.use_caches)
    660.     depth: bpy.props.FloatProperty(
    661.         name="Depth", default=.0, precision=4, step=1,
    662.         update=OffsetBase.use_caches)
    663.     flip_depth: bpy.props.BoolProperty(
    664.         name="Flip Depth", default=False,
    665.         description="Flip depth direction",
    666.         update=OffsetBase.use_caches)
    667.     depth_mode: bpy.props.EnumProperty(
    668.         items=[('angle', "Angle", "Angle"),
    669.                ('depth', "Depth", "Depth")],
    670.         name="Depth mode", default='angle',
    671.         update=change_depth_mode)
    672.     angle: bpy.props.FloatProperty(
    673.         name="Angle", default=0, precision=3, step=100,
    674.         min=-2*pi, max=2*pi, subtype='ANGLE', description="Angle",
    675.         update=OffsetBase.use_caches)
    676.     flip_angle: bpy.props.BoolProperty(
    677.         name="Flip Angle", default=False,
    678.         description="Flip Angle",
    679.         update=OffsetBase.use_caches)
    680.     angle_presets: bpy.props.EnumProperty(
    681.         items=[('0В°', "0В°", "0В°"),
    682.                ('15В°', "15В°", "15В°"),
    683.                ('30В°', "30В°", "30В°"),
    684.                ('45В°', "45В°", "45В°"),
    685.                ('60В°', "60В°", "60В°"),
    686.                ('75В°', "75В°", "75В°"),
    687.                ('90В°', "90В°", "90В°"), ],
    688.         name="Angle Presets", default='0В°',
    689.         update=assign_angle_presets)
    690.  
    691.  
    692.     def get_lockvector(self, context):
    693.         axis = self.lock_axis
    694.         if axis == 'x':
    695.             return X_UP
    696.         elif axis == 'y':
    697.             return Y_UP
    698.         elif axis == 'z':
    699.             return Z_UP
    700.         elif axis == 'view' and context.region_data:
    701.             vec = Z_UP.copy()
    702.             vec.rotate(context.region_data.view_rotation)
    703.             return vec
    704.         return None
    705.  
    706.     def get_exverts(self, bm, offset_infos, edges_orig):
    707.         ref_verts = [v for v, _, _ in offset_infos]
    708.  
    709.         if self.geometry_mode == 'move':
    710.             exverts = ref_verts
    711.             exedges = edges_orig
    712.         else:
    713.             exverts, exedges, side_edges = self.extrude_and_pairing(bm, edges_orig, ref_verts)
    714.             if self.geometry_mode == 'offset':
    715.                 bmesh.ops.delete(bm, geom=side_edges, context="EDGES")
    716.  
    717.         for e in exedges:
    718.             e.select = True
    719.  
    720.         return exverts
    721.  
    722.     def do_offset(self, bm, me, offset_infos, verts_offset):
    723.         if self.depth_mode == 'angle':
    724.             w = self.width if not self.flip_width else -self.width
    725.             angle = self.angle if not self.flip_angle else -self.angle
    726.             width = w * cos(angle)
    727.             depth = w * sin(angle)
    728.         else:
    729.             width = self.width if not self.flip_width else -self.width
    730.             depth = self.depth if not self.flip_depth else -self.depth
    731.  
    732.         self.move_verts(bm, me, width, depth, offset_infos, verts_offset)
    733.  
    734.     @classmethod
    735.     def poll(self, context):
    736.         return context.mode == 'EDIT_MESH'
    737.  
    738.     def draw(self, context):
    739.         layout = self.layout
    740.         layout.row().prop(self, 'geometry_mode', expand=True)
    741.  
    742.         row = layout.row(align=True)
    743.         row.prop(self, 'width')
    744.         row.prop(self, 'flip_width', icon='ARROW_LEFTRIGHT', icon_only=True)
    745.        
    746.         layout.label(text="Depth Mode:")
    747.         layout.row().prop(self, 'depth_mode', expand=True)
    748.         if self.depth_mode == 'angle':
    749.             d_mode = 'angle'
    750.             flip = 'flip_angle'
    751.         else:
    752.             d_mode = 'depth'
    753.             flip = 'flip_depth'
    754.         row = layout.row(align=True)
    755.         row.prop(self, d_mode)
    756.         row.prop(self, flip, icon='ARROW_LEFTRIGHT', icon_only=True)
    757.         if self.depth_mode == 'angle':
    758.             layout.row().prop(self, 'angle_presets', text="Presets", expand=True)
    759.  
    760.         layout.label(text="Lock Axis:")
    761.         layout.row().prop(self, 'lock_axis', text="Lock Axis", expand=True)
    762.  
    763.         layout.separator()
    764.        
    765.         row = layout.row()
    766.         row.prop(self, 'follow_face')
    767.         if self.follow_face:
    768.             row.prop(self, "threshold", text="Threshold")
    769.        
    770.         row = layout.row()
    771.         row.prop(self, 'edge_rail')
    772.         if self.edge_rail:
    773.             row.prop(self, 'edge_rail_only_end', text="OnlyEnd", toggle=True)
    774.  
    775.         layout.prop(self, 'mirror_modifier')
    776.  
    777.         #layout.operator('mesh.offset_edges', text='Repeat')
    778.  
    779.  
    780.     def execute(self, context):
    781.         # In edit mode
    782.         edit_object = context.edit_object
    783.         me = edit_object.data
    784.         bm = bmesh.from_edit_mesh(me)
    785.  
    786.         if self.caches_valid and self._cache_offset_infos:
    787.             offset_infos, edges_orig = self.get_caches(bm)
    788.         else:
    789.             offset_infos, edges_orig = self.get_offset_infos(
    790.                 bm, edit_object,
    791.                 follow_face=self.follow_face,
    792.                 edge_rail=self.edge_rail,
    793.                 edge_rail_only_end=self.edge_rail_only_end,
    794.                 mirror_modifier=self.mirror_modifier,
    795.                 normal_override=self.get_lockvector(context),
    796.                 threshold=self.threshold
    797.             )
    798.             if offset_infos is False:
    799.                 return {'CANCELLED'}
    800.             self.save_caches(offset_infos, edges_orig)
    801.  
    802.         exverts = self.get_exverts(bm, offset_infos, edges_orig)
    803.         self.do_offset(bm, me, offset_infos, exverts)
    804.  
    805.         self.caches_valid = False
    806.         return {'FINISHED'}
    807.  
    808.     def invoke(self, context, event):
    809.         # in edit mode
    810.         ob_edit = context.edit_object
    811.         if self.is_face_selected(ob_edit):
    812.             self.follow_face = True
    813.         if self.is_mirrored(ob_edit):
    814.             self.mirror_modifier = True
    815.  
    816.         me = ob_edit.data
    817.  
    818.         pref = context.preferences.addons[__name__].preferences
    819.         if pref.interactive and context.space_data.type == 'VIEW_3D':
    820.             # interactive mode
    821.             if pref.free_move:
    822.                 self.depth_mode = 'depth'
    823.  
    824.             ret = self.modal_prepare_bmeshes(context, ob_edit)
    825.             if ret is False:
    826.                 return {'CANCELLED'}
    827.  
    828.             self.width = self.angle = self.depth = .0
    829.             self.flip_depth = self.flip_angle = self.flip_width = False
    830.             self._mouse_init = self._mouse_prev = \
    831.                 Vector((event.mouse_x, event.mouse_y))
    832.             context.window_manager.modal_handler_add(self)
    833.  
    834.             self._factor = self.get_factor(context, self._edges_orig)
    835.  
    836.             # toggle switchs of keys
    837.             self._F = 0
    838.             self._A = 0
    839.            
    840.             return {'RUNNING_MODAL'}
    841.         else:
    842.             return self.execute(context)
    843.  
    844.     def modal(self, context, event):
    845.         # In edit mode
    846.         ob_edit = context.edit_object
    847.         me = ob_edit.data
    848.         pref = context.preferences.addons[__name__].preferences
    849.  
    850.         if event.type == 'F':
    851.             # toggle follow_face
    852.             # event.type == 'F' is True both when 'F' is pressed and when released,
    853.             # so these codes should be executed every other loop.
    854.             self._F = 1 - self._F
    855.             if self._F:
    856.                 self.follow_face = 1 - self.follow_face
    857.  
    858.                 self.modal_clean_bmeshes(context, ob_edit)
    859.                 ret = self.modal_prepare_bmeshes(context, ob_edit)
    860.                 if ret:
    861.                     self.do_offset(self._bm, me, self._offset_infos, self._exverts)
    862.                     return {'RUNNING_MODAL'}
    863.                 else:
    864.                     return {'CANCELLED'}
    865.  
    866.         if event.type == 'A':
    867.             # toggle depth_mode
    868.             self._A = 1 - self._A
    869.             if self._A:
    870.                 if self.depth_mode == 'angle':
    871.                     self.depth_mode = 'depth'
    872.                 else:
    873.                     self.depth_mode = 'angle'
    874.                    
    875.         context.area.header_text_set(self.create_header())
    876.  
    877.         if event.type == 'MOUSEMOVE':
    878.             _mouse_current = Vector((event.mouse_x, event.mouse_y))
    879.             vec_delta = _mouse_current - self._mouse_prev
    880.  
    881.             if pref.free_move or not event.ctrl:
    882.                 self.width += vec_delta.x * self._factor
    883.  
    884.             if event.ctrl:
    885.                 if self.depth_mode == 'angle':
    886.                     self.angle += vec_delta.y * ANGLE_1
    887.                 elif self.depth_mode == 'depth':
    888.                     self.depth += vec_delta.y * self._factor
    889.  
    890.             self._mouse_prev = _mouse_current
    891.  
    892.             self.do_offset(self._bm, me, self._offset_infos, self._exverts)
    893.             return {'RUNNING_MODAL'}
    894.  
    895.         elif event.type == 'LEFTMOUSE':
    896.             self._bm_orig.free()
    897.             context.area.header_text_set(text=None)
    898.             return {'FINISHED'}
    899.  
    900.         elif event.type in {'RIGHTMOUSE', 'ESC'}:
    901.             self.modal_clean_bmeshes(context, ob_edit)
    902.             context.area.header_text_set(text=None)
    903.             return {'CANCELLED'}
    904.  
    905.         return {'RUNNING_MODAL'}
    906.  
    907.     # methods below are usded in interactive mode
    908.     def create_header(self):
    909.         header = "".join(
    910.             ["Width {width: .4}  ",
    911.              "Depth {depth: .4}('A' to Angle)  " if self.depth_mode == 'depth' else "Angle {angle: 4.0F}В°('A' to Depth)  ",
    912.              "FollowFace(F):",
    913.              "(ON)" if self.follow_face else "(OFF)",
    914.             ])
    915.  
    916.         return header.format(width=self.width, depth=self.depth, angle=degrees(self.angle))
    917.  
    918.     def modal_prepare_bmeshes(self, context, ob_edit):
    919.         bpy.ops.object.mode_set(mode="OBJECT")
    920.         self._bm_orig = bmesh.new()
    921.         self._bm_orig.from_mesh(ob_edit.data)
    922.         bpy.ops.object.mode_set(mode="EDIT")
    923.  
    924.         self._bm = bmesh.from_edit_mesh(ob_edit.data)
    925.  
    926.         self._offset_infos, self._edges_orig = self.get_offset_infos(
    927.             self._bm, ob_edit,
    928.             edge_rail=self.edge_rail,
    929.             edge_rail_only_end=self.edge_rail_only_end,
    930.             mirror_modifier=self.mirror_modifier,
    931.             normal_override=self.get_lockvector(context),
    932.             threshold=self.threshold
    933.         )
    934.         if self._offset_infos is False:
    935.             return False
    936.         self._exverts = \
    937.             self.get_exverts(self._bm, self._offset_infos, self._edges_orig)
    938.         bmesh.update_edit_mesh(ob_edit.data)
    939.         return True
    940.  
    941.     def modal_clean_bmeshes(self, context, ob_edit):
    942.         bpy.ops.object.mode_set(mode="OBJECT")
    943.         self._bm_orig.to_mesh(ob_edit.data)
    944.         bpy.ops.object.mode_set(mode="EDIT")
    945.         self._bm_orig.free()
    946.         self._bm.free()
    947.  
    948.     def get_factor(self, context, edges_orig):
    949.         """get the length in the space of edited object
    950.         which correspond to 1px of 3d view. This method
    951.         is used to convert the distance of mouse movement
    952.         to offsetting width in interactive mode.
    953.         """
    954.         ob = context.edit_object
    955.         mat_w = ob.matrix_world
    956.         reg = context.region
    957.         reg3d = context.space_data.region_3d  # Don't use context.region_data
    958.                                               # because this will cause error
    959.                                               # when invoked from header menu.
    960.  
    961.         co_median = Vector((0, 0, 0))
    962.         for e in edges_orig:
    963.             co_median += e.verts[0].co
    964.         co_median /= len(edges_orig)
    965.         depth_loc = mat_w @ co_median  # World coords of median point
    966.  
    967.         win_left = Vector((0, 0))
    968.         win_right = Vector((reg.width, 0))
    969.         left = view3d_utils.region_2d_to_location_3d(reg, reg3d, win_left, depth_loc)
    970.         right = view3d_utils.region_2d_to_location_3d(reg, reg3d, win_right, depth_loc)
    971.         vec_width = mat_w.inverted_safe() @ (right - left)  # width vector in the object space
    972.         width_3d = vec_width.length   # window width in the object space
    973.  
    974.         return width_3d / reg.width
    975.  
    976. class OffsetEdgesProfile(bpy.types.Operator, OffsetBase):
    977.     """Offset Edges using a profile curve."""
    978.     bl_idname = "mesh.hidesato_offset_edges_profile"
    979.     bl_label = "Offset Edges Profile"
    980.     bl_options = {'REGISTER', 'UNDO'}
    981.  
    982.     follow_face: bpy.props.BoolProperty(
    983.         name="Follow Face", default=False,
    984.         description="Offset along faces around")
    985.     mirror_modifier: bpy.props.BoolProperty(
    986.         name="Mirror Modifier", default=False,
    987.         description="Take into account of Mirror modifier")
    988.     edge_rail: bpy.props.BoolProperty(
    989.         name="Edge Rail", default=False,
    990.         description="Align vertices along inner edges")
    991.     edge_rail_only_end: bpy.props.BoolProperty(
    992.         name="Edge Rail Only End", default=False,
    993.         description="Apply edge rail to end verts only")
    994.     res_profile: bpy.props.IntProperty(
    995.         name="Resolution", default =6, min=0, max=100,
    996.         update=OffsetBase.use_caches)
    997.     magni_w: bpy.props.FloatProperty(
    998.         name="Magnification of Width", default=1., precision=4, step=1,
    999.         update=OffsetBase.use_caches)
    1000.     magni_d: bpy.props.FloatProperty(
    1001.         name="Magniofication of Depth", default=1., precision=4, step=1,
    1002.         update=OffsetBase.use_caches)
    1003.     name_profile: bpy.props.StringProperty(update=OffsetBase.use_caches)
    1004.  
    1005.     @classmethod
    1006.     def poll(self, context):
    1007.         return context.mode == 'EDIT_MESH'
    1008.  
    1009.     def draw(self, context):
    1010.         layout = self.layout
    1011.  
    1012.         layout.prop_search(self, 'name_profile', context.scene, 'objects', text="Profile")
    1013.         layout.separator()
    1014.  
    1015.         layout.prop(self, 'res_profile')
    1016.  
    1017.         row = layout.row()
    1018.         row.prop(self, 'magni_w', text="Width")
    1019.         row.prop(self, 'magni_d', text="Depth")
    1020.  
    1021.         layout.separator()
    1022.         layout.prop(self, 'follow_face')
    1023.  
    1024.         row = layout.row()
    1025.         row.prop(self, 'edge_rail')
    1026.         if self.edge_rail:
    1027.             row.prop(self, 'edge_rail_only_end', text="OnlyEnd", toggle=True)
    1028.  
    1029.         layout.prop(self, 'mirror_modifier')
    1030.  
    1031.         #layout.operator('mesh.offset_edges', text='Repeat')
    1032.  
    1033.         if self.follow_face:
    1034.             layout.separator()
    1035.             layout.prop(self, 'threshold', text='Threshold')
    1036.  
    1037.     @staticmethod
    1038.     def analize_profile(context, ob_profile, resolution):
    1039.         curve = ob_profile.data
    1040.         res_orig = curve.resolution_u
    1041.         curve.resolution_u = resolution
    1042.         me = ob_profile.to_mesh(depsgraph=context.evaluated_depsgraph_get())
    1043.         curve.resolution_u = res_orig
    1044.  
    1045.         vco_start = me.vertices[0].co
    1046.         info_profile = [v.co - vco_start for v in me.vertices[1:]]
    1047.  
    1048.         return info_profile
    1049.  
    1050.     @staticmethod
    1051.     def get_profile(context):
    1052.         ob_edit = context.edit_object
    1053.         for ob in context.selected_objects:
    1054.             if ob != ob_edit and ob.type == 'CURVE':
    1055.                 return ob
    1056.         else:
    1057.             self.report({'WARNING'},
    1058.                          "Profile curve is not selected.")
    1059.             return None
    1060.  
    1061.     def offset_profile(self, ob_edit, info_profile):
    1062.         me = ob_edit.data
    1063.         bm = bmesh.from_edit_mesh(me)
    1064.  
    1065.         if self.caches_valid and self._cache_offset_infos:
    1066.             offset_infos, edges_orig = self.get_caches(bm)
    1067.         else:
    1068.             offset_infos, edges_orig = self.get_offset_infos(
    1069.                 bm, ob_edit,
    1070.                 edge_rail=self.edge_rail,
    1071.                 edge_rail_only_end=self.edge_rail_only_end,
    1072.                 mirror_modifier=self.mirror_modifier,
    1073.                 threshold=self.threshold
    1074.             )
    1075.             if offset_infos is False:
    1076.                 return {'CANCELLED'}
    1077.             self.save_caches(offset_infos, edges_orig)
    1078.  
    1079.         ref_verts = [v for v, _, _ in offset_infos]
    1080.         edges = edges_orig
    1081.         for width, depth, _ in info_profile:
    1082.             exverts, exedges, _ = self.extrude_and_pairing(bm, edges, ref_verts)
    1083.             self.move_verts(
    1084.                 bm, me, width * self.magni_w,
    1085.                 depth * self.magni_d, offset_infos,
    1086.                 exverts, update=False
    1087.             )
    1088.             ref_verts = exverts
    1089.             edges = exedges
    1090.  
    1091.         bm.normal_update()
    1092.         bmesh.update_edit_mesh(me)
    1093.  
    1094.         self.caches_valid = False
    1095.  
    1096.         return {'FINISHED'}
    1097.  
    1098.     @staticmethod
    1099.     def get_profile(context):
    1100.         ob_edit = context.edit_object
    1101.         for ob in context.selected_objects:
    1102.             if ob != ob_edit and ob.type == 'CURVE':
    1103.                 return ob
    1104.         return None
    1105.  
    1106.     def execute(self, context):
    1107.         if not self.name_profile:
    1108.             self.report({'WARNING'},
    1109.                          "Select a curve object as profile.")
    1110.             return {'FINISHED'}
    1111.  
    1112.         ob_profile = context.scene.objects[self.name_profile]
    1113.         if ob_profile and ob_profile.type == "CURVE":
    1114.             info_profile = self.analize_profile(
    1115.                 context, ob_profile, self.res_profile
    1116.             )
    1117.             return self.offset_profile(context.edit_object, info_profile)
    1118.         else:
    1119.             self.name_profile = ""
    1120.             self.report({'WARNING'},
    1121.                          "Select a curve object as profile.")
    1122.             return {'FINISHED'}
    1123.  
    1124.     def invoke(self, context, event):
    1125.         ob_edit = context.edit_object
    1126.         if self.is_face_selected(ob_edit):
    1127.             self.follow_face = True
    1128.         if self.is_mirrored(ob_edit):
    1129.             self.mirror_modifier = True
    1130.  
    1131.         ob_profile = self.get_profile(context)
    1132.         if ob_profile is None:
    1133.             self.report({'WARNING'},
    1134.                          "Profile curve is not selected.")
    1135.             return {'CANCELLED'}
    1136.  
    1137.         self.name_profile = ob_profile.name
    1138.         self.res_profile = ob_profile.data.resolution_u
    1139.         return self.execute(context)
    1140.  
    1141.  
    1142. def draw_offset_edges(self, context):
    1143.     lay = self.layout
    1144.     lay.separator()
    1145.     lay.operator_context = 'INVOKE_DEFAULT'
    1146.     lay.operator(OffsetEdges.bl_idname, text='Offset').geometry_mode='offset'
    1147.     lay.operator(OffsetEdges.bl_idname, text='Offset Extrude').geometry_mode='extrude'
    1148.     lay.operator(OffsetEdges.bl_idname, text='Offset Move').geometry_mode='move'
    1149.     lay.operator(OffsetEdgesProfile.bl_idname, text='Offset with Profile')
    1150.  
    1151.  
    1152. def register():
    1153.     bpy.utils.register_class(OffsetEdgesPreferences)
    1154.     bpy.utils.register_class(OffsetEdges)
    1155.     bpy.utils.register_class(OffsetEdgesProfile)
    1156.     bpy.types.VIEW3D_MT_edit_mesh_edges.append(draw_offset_edges)
    1157.  
    1158.  
    1159. def unregister():
    1160.     bpy.utils.unregister_class(OffsetEdgesPreferences)
    1161.     bpy.utils.unregister_class(OffsetEdges)
    1162.     bpy.utils.unregister_class(OffsetEdgesProfile)
    1163.     bpy.types.VIEW3D_MT_edit_mesh_edges.remove(draw_offset_edges)
    1164.  
    1165.  
    1166. if __name__ == '__main__':
    1167.     register()
    1168.  
     
    #17      
    KBB gefällt das.
  19. CUBEMAN

    CUBEMAN Polyboy

    Dabei seit:
    09.06.2008
    Beiträge:
    3.036
    Geschlecht:
    männlich
    Ort:
    Hamburg
    Software:
    Cinema 4D, VRay, ZBrush, Rhino 5, Photoshop, After Effects
    Blender - Extrusion mit 30° Seitenwinkel
    #18      
Seobility SEO Tool
x
×
×