Refactor: Node Wrangler: PreviewNode operator
[blender-addons.git] / mesh_tools / mesh_edge_roundifier.py
blob212e5fa941ddd1c6169c17282b6371f3846572da
1 # SPDX-FileCopyrightText: 2019-2022 Blender Foundation
3 # SPDX-License-Identifier: GPL-2.0-or-later
5 bl_info = {
6 "name": "Edge Roundifier",
7 "author": "Piotr Komisarczyk (komi3D), PKHG",
8 "version": (1, 0, 2),
9 "blender": (2, 80, 0),
10 "location": "SPACE > Edge Roundifier or CTRL-E > "
11 "Edge Roundifier or Tools > Addons > Edge Roundifier",
12 "description": "Mesh editing script allowing edge rounding",
13 "doc_url": "",
14 "category": "Mesh"
17 import bpy
18 import bmesh
19 from bpy.types import Operator
20 from bpy.props import (
21 BoolProperty,
22 FloatProperty,
23 EnumProperty,
24 IntProperty,
26 from math import (
27 sqrt, acos, pi,
28 radians, degrees, sin,
30 from mathutils import (
31 Vector, Euler,
32 Quaternion,
35 # CONSTANTS
36 two_pi = 2 * pi
37 XY = "XY"
38 XZ = "XZ"
39 YZ = "YZ"
40 SPIN_END_THRESHOLD = 0.001
41 LINE_TOLERANCE = 0.0001
42 d_XABS_YABS = False
43 d_Edge_Info = False
44 d_Plane = False
45 d_Radius_Angle = False
46 d_Roots = False
47 d_RefObject = False
48 d_LineAB = False
49 d_Selected_edges = False
50 d_Rotate_Around_Spin_Center = False
52 # Enable debug prints
53 DEBUG = False
56 # for debugging PKHG #
57 def debugPrintNew(debugs, *text):
58 if DEBUG and debugs:
59 tmp = [el for el in text]
60 for row in tmp:
61 print(row)
64 # Geometry and math calculation methods #
66 class CalculationHelper:
68 def __init__(self):
69 """
70 Constructor
71 """
72 def getLineCoefficientsPerpendicularToVectorInPoint(self, point, vector, plane):
73 x, y, z = point
74 xVector, yVector, zVector = vector
75 destinationPoint = (x + yVector, y - xVector, z)
76 if plane == 'YZ':
77 destinationPoint = (x, y + zVector, z - yVector)
78 if plane == 'XZ':
79 destinationPoint = (x + zVector, y, z - xVector)
80 return self.getCoefficientsForLineThrough2Points(point, destinationPoint, plane)
82 def getQuadraticRoots(self, coef):
83 if len(coef) != 3:
84 return None # Replaced NaN with None
85 else:
86 a, b, c = coef
87 delta = b ** 2 - 4 * a * c
88 if delta == 0:
89 x = -b / (2 * a)
90 return (x, x)
91 elif delta < 0:
92 return None
93 else:
94 x1 = (-b - sqrt(delta)) / (2 * a)
95 x2 = (-b + sqrt(delta)) / (2 * a)
96 return (x1, x2)
98 def getCoefficientsForLineThrough2Points(self, point1, point2, plane):
99 x1, y1, z1 = point1
100 x2, y2, z2 = point2
102 # mapping x1,x2, y1,y2 to proper values based on plane
103 if plane == YZ:
104 x1 = y1
105 x2 = y2
106 y1 = z1
107 y2 = z2
108 if plane == XZ:
109 y1 = z1
110 y2 = z2
112 # Further calculations the same as for XY plane
113 xabs = abs(x2 - x1)
114 yabs = abs(y2 - y1)
115 debugPrintNew(d_XABS_YABS, "XABS = " + str(xabs) + " YABS = " + str(yabs))
117 if xabs <= LINE_TOLERANCE:
118 return None # this means line x = edgeCenterX
119 if yabs <= LINE_TOLERANCE:
120 A = 0
121 B = y1
122 return A, B
123 A = (y2 - y1) / (x2 - x1)
124 B = y1 - (A * x1)
125 return (A, B)
127 def getLineCircleIntersections(self, lineAB, circleMidPoint, radius):
128 # (x - a)**2 + (y - b)**2 = r**2 - circle equation
129 # y = A*x + B - line equation
130 # f * x**2 + g * x + h = 0 - quadratic equation
131 A, B = lineAB
132 a, b = circleMidPoint
133 f = 1 + (A ** 2)
134 g = -2 * a + 2 * A * B - 2 * A * b
135 h = (B ** 2) - 2 * b * B - (radius ** 2) + (a ** 2) + (b ** 2)
136 coef = [f, g, h]
137 roots = self.getQuadraticRoots(coef)
138 if roots is not None:
139 x1 = roots[0]
140 x2 = roots[1]
141 point1 = [x1, A * x1 + B]
142 point2 = [x2, A * x2 + B]
143 return [point1, point2]
144 else:
145 return None
147 def getLineCircleIntersectionsWhenXPerpendicular(self, edgeCenter,
148 circleMidPoint, radius, plane):
149 # (x - a)**2 + (y - b)**2 = r**2 - circle equation
150 # x = xValue - line equation
151 # f * x**2 + g * x + h = 0 - quadratic equation
152 xValue = edgeCenter[0]
153 if plane == YZ:
154 xValue = edgeCenter[1]
155 if plane == XZ:
156 xValue = edgeCenter[0]
158 a, b = circleMidPoint
159 f = 1
160 g = -2 * b
161 h = (a ** 2) + (b ** 2) + (xValue ** 2) - 2 * a * xValue - (radius ** 2)
162 coef = [f, g, h]
163 roots = self.getQuadraticRoots(coef)
164 if roots is not None:
165 y1 = roots[0]
166 y2 = roots[1]
167 point1 = [xValue, y1]
168 point2 = [xValue, y2]
169 return [point1, point2]
170 else:
171 return None
173 # point1 is the point near 90 deg angle
174 def getAngle(self, point1, point2, point3):
175 distance1 = (Vector(point1) - Vector(point2)).length
176 distance2 = (Vector(point2) - Vector(point3)).length
177 cos = distance1 / distance2
179 if abs(cos) > 1: # prevents Domain Error
180 cos = round(cos)
182 alpha = acos(cos)
183 return (alpha, degrees(alpha))
185 # get two of three coordinates used for further calculation of spin center
186 # PKHG>nice if rescriction to these 3 types or planes is to be done
187 # komi3D> from 0.0.2 there is a restriction. In future I would like Edge
188 # komi3D> Roundifier to work on Normal and View coordinate systems
189 def getCircleMidPointOnPlane(self, V1, plane):
190 X = V1[0]
191 Y = V1[1]
192 if plane == 'XZ':
193 X = V1[0]
194 Y = V1[2]
195 elif plane == 'YZ':
196 X = V1[1]
197 Y = V1[2]
198 return [X, Y]
200 def getEdgeReference(self, edge, edgeCenter, plane):
201 vert1 = edge.verts[1].co
202 V = vert1 - edgeCenter
203 orthoVector = Vector((V[1], -V[0], V[2]))
204 if plane == 'XZ':
205 orthoVector = Vector((V[2], V[1], -V[0]))
206 elif plane == 'YZ':
207 orthoVector = Vector((V[0], V[2], -V[1]))
208 refPoint = edgeCenter + orthoVector
209 return refPoint
212 # Selection Methods #
214 class SelectionHelper:
216 def selectVertexInMesh(self, mesh, vertex):
217 bpy.ops.object.mode_set(mode="OBJECT")
218 for v in mesh.vertices:
219 if v.co == vertex:
220 v.select = True
221 break
223 bpy.ops.object.mode_set(mode="EDIT")
225 def getSelectedVertex(self, mesh):
226 bpy.ops.object.mode_set(mode="OBJECT")
227 for v in mesh.vertices:
228 if v.select is True:
229 bpy.ops.object.mode_set(mode="EDIT")
230 return v
232 bpy.ops.object.mode_set(mode="EDIT")
233 return None
235 def refreshMesh(self, bm, mesh):
236 bpy.ops.object.mode_set(mode='OBJECT')
237 bm.to_mesh(mesh)
238 bpy.ops.object.mode_set(mode='EDIT')
241 # Operator
243 class EdgeRoundifier(Operator):
244 bl_idname = "mesh.edge_roundifier"
245 bl_label = "Edge Roundifier"
246 bl_description = "Mesh modeling tool for building arcs on selected Edges"
247 bl_options = {'REGISTER', 'UNDO', 'PRESET'}
249 threshold = 0.0005
250 obj = None
252 edgeScaleFactor: FloatProperty(
253 name="",
254 description="Set the Factor of scaling",
255 default=1.0,
256 min=0.00001, max=100000.0,
257 step=0.5,
258 precision=5
260 r: FloatProperty(
261 name="",
262 description="User Defined arc steepness by a Radius\n"
263 "Enabled only if Entry mode is set to Radius\n",
264 default=1,
265 min=0.00001, max=1000.0,
266 step=0.1,
267 precision=3
269 a: FloatProperty(
270 name="",
271 description="User defined arc steepness calculated from an Angle\n"
272 "Enabled only if Entry mode is set to Angle and\n"
273 "Angle presets is set Other",
274 default=180.0,
275 min=0.1, max=180.0,
276 step=0.5,
277 precision=1
279 n: IntProperty(
280 name="",
281 description="Arc subdivision level",
282 default=4,
283 min=1, max=100,
284 step=1
286 flip: BoolProperty(
287 name="Flip",
288 description="If True, flip the side of the selected edges where the arcs are drawn",
289 default=False
291 invertAngle: BoolProperty(
292 name="Invert",
293 description="If True, uses an inverted angle to draw the arc (360 degrees - angle)",
294 default=False
296 fullCircles: BoolProperty(
297 name="Circles",
298 description="If True, uses an angle of 360 degrees to draw the arcs",
299 default=False
301 bothSides: BoolProperty(
302 name="Both sides",
303 description="If True, draw arcs on both sides of the selected edges",
304 default=False
306 drawArcCenters: BoolProperty(
307 name="Centers",
308 description="If True, draws a vertex for each spin center",
309 default=False
311 removeEdges: BoolProperty(
312 name="Edges",
313 description="If True removes the Original selected edges",
314 default=False
316 removeScaledEdges: BoolProperty(
317 name="Scaled edges",
318 description="If True removes the Scaled edges (not part of the arcs)",
319 default=False
321 connectArcWithEdge: BoolProperty(
322 name="Arc - Edge",
323 description="Connect Arcs to Edges",
324 default=False
326 connectArcs: BoolProperty(
327 name="Arcs",
328 description="Connect subsequent Arcs",
329 default=False
331 connectScaledAndBase: BoolProperty(
332 name="Scaled - Base Edge",
333 description="Connect Scaled to Base Edge",
334 default=False
336 connectArcsFlip: BoolProperty(
337 name="Flip Arcs",
338 description="Flip the connection of subsequent Arcs",
339 default=False
341 connectArcWithEdgeFlip: BoolProperty(
342 name="Flip Arc - Edge",
343 description="Flip the connection of the Arcs to Edges",
344 default=False
346 axisAngle: FloatProperty(
347 name="",
348 description="Rotate Arc around the perpendicular axis",
349 default=0.0,
350 min=-180.0, max=180.0,
351 step=0.5,
352 precision=1
354 edgeAngle: FloatProperty(
355 name="",
356 description="Rotate Arc around the Edge (Edge acts like as the axis)",
357 default=0.0,
358 min=-180.0, max=180.0,
359 step=0.5,
360 precision=1
362 offset: FloatProperty(
363 name="",
364 description="Offset Arc perpendicular the Edge",
365 default=0.0,
366 min=-1000000.0, max=1000000.0,
367 step=0.1,
368 precision=5
370 offset2: FloatProperty(
371 name="",
372 description="Offset Arc in parallel to the Edge",
373 default=0.0,
374 min=-1000000.0, max=1000000.0,
375 step=0.1,
376 precision=5
378 ellipticFactor: FloatProperty(
379 name="",
380 description="Make Arc elliptic",
381 default=0.0,
382 min=-1000000.0, max=1000000.0,
383 step=0.1,
384 precision=5
386 workModeItems = [("Normal", "Normal", ""), ("Reset", "Reset", "")]
387 workMode: EnumProperty(
388 items=workModeItems,
389 name="",
390 default='Normal',
391 description="Normal work with the current given parameters set by the user\n"
392 "Reset - changes back the parameters to their default values"
394 entryModeItems = [("Radius", "Radius", ""), ("Angle", "Angle", "")]
395 entryMode: EnumProperty(
396 items=entryModeItems,
397 name="",
398 default='Angle',
399 description="Entry mode switch between Angle and Radius\n"
400 "If Angle is selected, arc radius is calculated from it"
402 rotateCenterItems = [
403 ("Spin", "Spin", ""), ("V1", "V1", ""),
404 ("Edge", "Edge", ""), ("V2", "V2", "")
406 rotateCenter: EnumProperty(
407 items=rotateCenterItems,
408 name="",
409 default='Edge',
410 description="Rotate center for spin axis rotate"
412 arcModeItems = [("FullEdgeArc", "Full", "Full"), ('HalfEdgeArc', "Half", "Half")]
413 arcMode: EnumProperty(
414 items=arcModeItems,
415 name="",
416 default='FullEdgeArc',
417 description="Arc mode - switch between Full and Half arcs"
419 angleItems = [
420 ('Other', "Other", "User defined angle"), ('180', "180", "HemiCircle (2 sides)"),
421 ('120', "120", "TriangleCircle (3 sides)"), ('90', "90", "QuadCircle (4 sides)"),
422 ('72', "72", "PentagonCircle (5 sides)"), ('60', "60", "HexagonCircle (6 sides)"),
423 ('45', "45", "OctagonCircle (8 sides)"), ('30', "30", "DodecagonCircle (12 sides)")
425 angleEnum: EnumProperty(
426 items=angleItems,
427 name="",
428 default='180',
429 description="Presets prepare standard angles and calculate proper ray"
431 refItems = [('ORG', "Origin", "Use Origin Location"), ('CUR', "3D Cursor", "Use 3DCursor Location"),
432 ('EDG', "Edge", "Use Individual Edge Reference")]
433 referenceLocation: EnumProperty(
434 items=refItems,
435 name="",
436 default='ORG',
437 description="Reference location used to calculate initial centers of drawn arcs"
439 planeItems = [
440 (XY, "XY", "XY Plane (Z=0)"),
441 (YZ, "YZ", "YZ Plane (X=0)"),
442 (XZ, "XZ", "XZ Plane (Y=0)")
444 planeEnum: EnumProperty(
445 items=planeItems,
446 name="",
447 default='XY',
448 description="Plane used to calculate spin plane of drawn arcs"
450 edgeScaleCenterItems = [
451 ('V1', "V1", "v1 - First Edge's Vertex"),
452 ('CENTER', "Center", "Center of the Edge"),
453 ('V2', "V2", "v2 - Second Edge's Vertex")
455 edgeScaleCenterEnum: EnumProperty(
456 items=edgeScaleCenterItems,
457 name="Edge scale center",
458 default='CENTER',
459 description="Center used for scaling the initial edge"
462 calc = CalculationHelper()
463 sel = SelectionHelper()
465 @classmethod
466 def poll(cls, context):
467 obj = context.active_object
468 return (obj and obj.type == 'MESH' and
469 obj.mode == 'EDIT')
471 def prepareMesh(self, context):
472 bpy.ops.object.mode_set(mode='OBJECT')
473 bpy.ops.object.mode_set(mode='EDIT')
475 mesh = context.view_layer.objects.active.data
476 bm = bmesh.new()
477 bm.from_mesh(mesh)
479 edges = [ele for ele in bm.edges if ele.select]
480 return edges, mesh, bm
482 def prepareParameters(self):
483 parameters = {"a": "a"}
484 parameters["arcMode"] = self.arcMode
485 parameters["edgeScaleFactor"] = self.edgeScaleFactor
486 parameters["edgeScaleCenterEnum"] = self.edgeScaleCenterEnum
487 parameters["plane"] = self.planeEnum
488 parameters["radius"] = self.r
489 parameters["angle"] = self.a
490 parameters["segments"] = self.n
491 parameters["fullCircles"] = self.fullCircles
492 parameters["invertAngle"] = self.invertAngle
493 parameters["bothSides"] = self.bothSides
494 parameters["angleEnum"] = self.angleEnum
495 parameters["entryMode"] = self.entryMode
496 parameters["workMode"] = self.workMode
497 parameters["refObject"] = self.referenceLocation
498 parameters["flip"] = self.flip
499 parameters["drawArcCenters"] = self.drawArcCenters
500 parameters["removeEdges"] = self.removeEdges
501 parameters["removeScaledEdges"] = self.removeScaledEdges
502 parameters["connectArcWithEdge"] = self.connectArcWithEdge
503 parameters["connectScaledAndBase"] = self.connectScaledAndBase
504 parameters["connectArcs"] = self.connectArcs
505 parameters["connectArcsFlip"] = self.connectArcsFlip
506 parameters["connectArcWithEdgeFlip"] = self.connectArcWithEdgeFlip
507 parameters["axisAngle"] = self.axisAngle
508 parameters["edgeAngle"] = self.edgeAngle
509 parameters["offset"] = self.offset
510 parameters["offset2"] = self.offset2
511 parameters["ellipticFactor"] = self.ellipticFactor
512 parameters["rotateCenter"] = self.rotateCenter
513 return parameters
515 def draw(self, context):
516 layout = self.layout
517 box = layout.box()
518 uiPercentage = 0.333
520 self.addEnumParameterToUI(box, False, uiPercentage, 'Mode:', 'workMode')
521 self.addEnumParameterToUI(box, False, uiPercentage, 'Plane:', 'planeEnum')
522 self.addEnumParameterToUI(box, False, uiPercentage, 'Reference:', 'referenceLocation')
524 box = layout.box()
525 self.addEnumParameterToUI(box, False, uiPercentage, 'Scale base:', 'edgeScaleCenterEnum')
526 self.addParameterToUI(box, False, uiPercentage, 'Scale factor:', 'edgeScaleFactor')
528 box = layout.box()
529 self.addEnumParameterToUI(box, False, uiPercentage, 'Entry mode:', 'entryMode')
531 row = box.row(align=False)
532 row.prop(self, 'angleEnum', expand=True, text="Angle presets")
534 disable_a = bool(self.entryMode == 'Angle' and self.angleEnum == 'Other')
535 disable_r = bool(self.entryMode == 'Radius')
537 self.addParameterToUI(box, False, uiPercentage, 'Angle:', 'a', disable_a)
538 self.addParameterToUI(box, False, uiPercentage, 'Radius:', 'r', disable_r)
539 self.addParameterToUI(box, False, uiPercentage, 'Segments:', 'n')
541 box = layout.box()
542 self.addCheckboxToUI(box, True, 'Options:', 'flip', 'invertAngle')
543 self.addCheckboxToUI(box, True, '', 'bothSides', 'fullCircles')
544 self.addCheckboxToUI(box, True, '', 'drawArcCenters')
546 box = layout.box()
547 self.addCheckboxToUI(box, True, 'Remove:', 'removeEdges', 'removeScaledEdges')
549 box = layout.box()
550 self.addCheckboxToUI(box, True, 'Connect:', 'connectArcs', 'connectArcsFlip')
551 self.addCheckboxToUI(box, True, '', 'connectArcWithEdge', 'connectArcWithEdgeFlip')
552 self.addCheckboxToUI(box, True, '', 'connectScaledAndBase')
554 box = layout.box()
555 self.addParameterToUI(box, False, uiPercentage, 'Orhto offset:', 'offset')
556 self.addParameterToUI(box, False, uiPercentage, 'Parallel offset:', 'offset2')
558 box = layout.box()
559 self.addParameterToUI(box, False, uiPercentage, 'Edge rotate :', 'edgeAngle')
560 self.addEnumParameterToUI(box, False, uiPercentage, 'Axis rotate center:', 'rotateCenter')
561 self.addParameterToUI(box, False, uiPercentage, 'Axis rotate:', 'axisAngle')
563 box = layout.box()
564 self.addParameterToUI(box, False, uiPercentage, 'Elliptic factor:', 'ellipticFactor')
566 def addParameterToUI(self, layout, alignment, percent, label, properties, disable=True):
567 row = layout.row(align=alignment)
568 split = row.split(factor=percent)
569 col = split.column()
571 col.label(text=label)
572 col2 = split.column()
573 row = col2.row(align=alignment)
574 row.enabled = disable
575 row.prop(self, properties)
577 def addCheckboxToUI(self, layout, alignment, label, property1, property2=None):
578 if label not in (""):
579 row = layout.row()
580 row.label(text=label)
581 row2 = layout.row(align=alignment)
582 if property2:
583 split = row2.split(factor=0.5)
584 split.prop(self, property1, toggle=True)
585 split.prop(self, property2, toggle=True)
586 else:
587 row2.prop(self, property1, toggle=True)
588 layout.separator()
590 def addEnumParameterToUI(self, layout, alignment, percent, label, properties):
591 row = layout.row(align=alignment)
592 split = row.split(factor=percent)
593 col = split.column()
595 col.label(text=label)
596 col2 = split.column()
597 row = col2.row(align=alignment)
598 row.prop(self, properties, expand=True, text="a")
600 def execute(self, context):
602 edges, mesh, bm = self.prepareMesh(context)
603 parameters = self.prepareParameters()
605 self.resetValues(parameters["workMode"])
607 self.obj = context.view_layer.objects.active
608 scaledEdges = self.scaleDuplicatedEdges(bm, edges, parameters)
610 if len(scaledEdges) > 0:
611 self.roundifyEdges(scaledEdges, parameters, bm, mesh)
613 if parameters["connectScaledAndBase"]:
614 self.connectScaledEdgesWithBaseEdge(scaledEdges, edges, bm, mesh)
616 self.sel.refreshMesh(bm, mesh)
617 self.selectEdgesAfterRoundifier(context, scaledEdges)
618 else:
619 debugPrintNew(True, "No edges selected!")
621 if parameters["removeEdges"]:
622 bmesh.ops.delete(bm, geom=edges, context='EDGES')
624 if parameters["removeScaledEdges"] and self.edgeScaleFactor != 1.0:
625 bmesh.ops.delete(bm, geom=scaledEdges, context='EDGES')
627 bpy.ops.object.mode_set(mode='OBJECT')
628 bm.to_mesh(mesh)
629 bpy.ops.object.mode_set(mode='EDIT')
630 bpy.ops.mesh.select_all(action='SELECT')
631 bpy.ops.mesh.remove_doubles()
633 bm.free()
635 return {'FINISHED'}
637 def resetValues(self, workMode):
638 if workMode == "Reset":
639 self.setAllParamsToDefaults()
641 def setAllParamsToDefaults(self):
642 try:
643 self.edgeScaleFactor = 1.0
644 self.r = 1
645 self.a = 180.0
646 self.n = 4
647 self.flip = False
648 self.invertAngle = False
649 self.fullCircles = False
650 self.bothSides = False
651 self.drawArcCenters = False
652 self.removeEdges = False
653 self.removeScaledEdges = False
655 self.connectArcWithEdge = False
656 self.connectArcs = False
657 self.connectScaledAndBase = False
658 self.connectArcsFlip = False
659 self.connectArcWithEdgeFlip = False
661 self.axisAngle = 0.0
662 self.edgeAngle = 0.0
663 self.offset = 0.0
664 self.offset2 = 0.0
665 self.ellipticFactor = 0.0
667 self.workMode = 'Normal'
668 self.entryMode = 'Angle'
669 self.angleEnum = '180'
670 self.referenceLocation = 'ORG'
671 self.planeEnum = 'XY'
672 self.edgeScaleCenterEnum = 'CENTER'
673 self.rotateCenter = 'Edge'
675 self.report({'INFO'}, "The parameters have been reset to default values")
676 except Exception as e:
677 self.report({'WARNING'}, "The parameters could not be reset")
678 debugPrintNew(True, "\n[setAllParamsToDefaults]\n parameter reset error\n" + e)
680 def scaleDuplicatedEdges(self, bm, edges, parameters):
681 scaleCenter = parameters["edgeScaleCenterEnum"]
682 factor = parameters["edgeScaleFactor"]
683 # this code is based on Zeffi's answer to my question
684 duplicateEdges = []
685 if factor == 1:
686 duplicateEdges = edges
687 else:
688 for e in edges:
689 v1 = e.verts[0].co
690 v2 = e.verts[1].co
691 origin = None
692 if scaleCenter == 'CENTER':
693 origin = (v1 + v2) * 0.5
694 elif scaleCenter == 'V1':
695 origin = v1
696 elif scaleCenter == 'V2':
697 origin = v2
699 bmv1 = bm.verts.new(((v1 - origin) * factor) + origin)
700 bmv2 = bm.verts.new(((v2 - origin) * factor) + origin)
701 bme = bm.edges.new([bmv1, bmv2])
702 duplicateEdges.append(bme)
703 return duplicateEdges
705 def roundifyEdges(self, edges, parameters, bm, mesh):
706 arcs = []
707 for e in edges:
708 arcVerts = self.roundify(e, parameters, bm, mesh)
709 arcs.append(arcVerts)
711 if parameters["connectArcs"]:
712 self.connectArcsTogether(arcs, bm, mesh, parameters)
714 def getNormalizedEdgeVector(self, edge):
715 V1 = edge.verts[0].co
716 V2 = edge.verts[1].co
717 edgeVector = V2 - V1
718 normEdge = edgeVector.normalized()
719 return normEdge
721 def getEdgePerpendicularVector(self, edge, plane):
722 normEdge = self.getNormalizedEdgeVector(edge)
724 edgePerpendicularVector = Vector((normEdge[1], -normEdge[0], 0))
725 if plane == YZ:
726 edgePerpendicularVector = Vector((0, normEdge[2], -normEdge[1]))
727 if plane == XZ:
728 edgePerpendicularVector = Vector((normEdge[2], 0, -normEdge[0]))
729 return edgePerpendicularVector
731 def getEdgeInfo(self, edge):
732 V1 = edge.verts[0].co
733 V2 = edge.verts[1].co
734 edgeVector = V2 - V1
735 edgeLength = edgeVector.length
736 edgeCenter = (V2 + V1) * 0.5
737 return V1, V2, edgeVector, edgeLength, edgeCenter
739 def roundify(self, edge, parameters, bm, mesh):
740 V1, V2, edgeVector, edgeLength, edgeCenter = self.getEdgeInfo(edge)
741 if self.skipThisEdge(V1, V2, parameters["plane"]):
742 return
744 roundifyParams = None
745 arcVerts = None
746 roundifyParams = self.calculateRoundifyParams(edge, parameters, bm, mesh)
747 if roundifyParams is None:
748 return
750 arcVerts = self.spinAndPostprocess(edge, parameters, bm, mesh, edgeCenter, roundifyParams)
751 return arcVerts
753 def spinAndPostprocess(self, edge, parameters, bm, mesh, edgeCenter, roundifyParams):
754 spinnedVerts, roundifyParamsUpdated = self.drawSpin(
755 edge, edgeCenter,
756 roundifyParams,
757 parameters, bm, mesh
759 postProcessedArcVerts = self.arcPostprocessing(
760 edge, parameters, bm, mesh,
761 roundifyParamsUpdated,
762 spinnedVerts, edgeCenter
764 return postProcessedArcVerts
766 def rotateArcAroundEdge(self, bm, mesh, arcVerts, parameters):
767 angle = parameters["edgeAngle"]
768 if angle != 0:
769 self.arc_rotator(arcVerts, angle, parameters)
771 # arc_rotator method was created by PKHG, I (komi3D) adjusted it to fit the rest
772 def arc_rotator(self, arcVerts, extra_rotation, parameters):
773 bpy.ops.object.mode_set(mode='OBJECT')
774 old_location = self.obj.location.copy()
775 bpy.ops.transform.translate(
776 value=-old_location,
777 constraint_axis=(False, False, False),
778 orient_type='GLOBAL',
779 mirror=False,
780 use_proportional_edit=False,
782 bpy.ops.object.mode_set(mode='EDIT')
783 adjust_matrix = self.obj.matrix_parent_inverse
784 bm = bmesh.from_edit_mesh(self.obj.data)
785 lastVert = len(arcVerts) - 1
786 if parameters["drawArcCenters"]:
787 lastVert = lastVert - 1 # center gets added as last vert of arc
788 v0_old = adjust_matrix @ arcVerts[0].co.copy()
790 # PKHG>INFO move if necessary v0 to origin such that the axis gos through origin and v1
791 if v0_old != Vector((0, 0, 0)):
792 for i, ele in enumerate(arcVerts):
793 arcVerts[i].co += - v0_old
795 axis = arcVerts[0].co - arcVerts[lastVert].co
796 a_mat = Quaternion(axis, radians(extra_rotation)).normalized().to_matrix()
798 for ele in arcVerts:
799 ele.co = a_mat @ ele.co
801 # PKHG>INFO move back if needed
802 if v0_old != Vector((0, 0, 0)):
803 for i, ele in enumerate(arcVerts):
804 arcVerts[i].co += + v0_old
806 bpy.ops.object.mode_set(mode='OBJECT')
807 # PKHG>INFO move origin object back print("old location = " , old_location)
808 bpy.ops.transform.translate(
809 value=old_location,
810 constraint_axis=(False, False, False),
811 orient_type='GLOBAL',
812 mirror=False,
813 use_proportional_edit=False,
815 bpy.ops.object.mode_set(mode='EDIT')
817 def makeElliptic(self, bm, mesh, arcVertices, parameters):
818 if parameters["ellipticFactor"] != 0: # if 0 then nothing has to be done
819 lastVert = len(arcVertices) - 1
820 if parameters["drawArcCenters"]:
821 lastVert = lastVert - 1 # center gets added as last vert of arc
822 v0co = arcVertices[0].co
823 v1co = arcVertices[lastVert].co
825 for vertex in arcVertices: # range(len(res_list)):
826 # PKHg>INFO compute the base on the edge of the height-vector
827 top = vertex.co # res_list[nr].co
828 t = 0
829 if v1co - v0co != 0:
830 t = (v1co - v0co).dot(top - v0co) / (v1co - v0co).length ** 2
831 h_bottom = v0co + t * (v1co - v0co)
832 height = (h_bottom - top)
833 vertex.co = top + parameters["ellipticFactor"] * height
835 return arcVertices
837 def arcPostprocessing(self, edge, parameters, bm, mesh, roundifyParams, spinnedVerts, edgeCenter):
838 [chosenSpinCenter, otherSpinCenter, spinAxis, angle, steps, refObjectLocation] = roundifyParams
839 rotatedVerts = []
840 if parameters["rotateCenter"] == 'Edge':
841 rotatedVerts = self.rotateArcAroundSpinAxis(
842 bm, mesh, spinnedVerts, parameters, edgeCenter
844 elif parameters["rotateCenter"] == 'Spin':
845 rotatedVerts = self.rotateArcAroundSpinAxis(
846 bm, mesh, spinnedVerts, parameters, chosenSpinCenter
848 elif parameters["rotateCenter"] == 'V1':
849 rotatedVerts = self.rotateArcAroundSpinAxis(
850 bm, mesh, spinnedVerts, parameters, edge.verts[0].co
852 elif parameters["rotateCenter"] == 'V2':
853 rotatedVerts = self.rotateArcAroundSpinAxis(
854 bm, mesh, spinnedVerts, parameters, edge.verts[1].co
857 offsetVerts = self.offsetArcPerpendicular(
858 bm, mesh, rotatedVerts, edge, parameters
860 offsetVerts2 = self.offsetArcParallel(
861 bm, mesh, offsetVerts, edge, parameters
863 ellipticVerts = self.makeElliptic(
864 bm, mesh, offsetVerts2, parameters
866 self.rotateArcAroundEdge(bm, mesh, ellipticVerts, parameters)
868 if parameters["connectArcWithEdge"]:
869 self.connectArcTogetherWithEdge(
870 edge, offsetVerts2, bm, mesh, parameters
872 return offsetVerts2
874 def connectArcTogetherWithEdge(self, edge, arcVertices, bm, mesh, parameters):
875 lastVert = len(arcVertices) - 1
876 if parameters["drawArcCenters"]:
877 lastVert = lastVert - 1 # center gets added as last vert of arc
878 edgeV1 = edge.verts[0].co
879 edgeV2 = edge.verts[1].co
880 arcV1 = arcVertices[0].co
881 arcV2 = arcVertices[lastVert].co
883 bmv1 = bm.verts.new(edgeV1)
884 bmv2 = bm.verts.new(arcV1)
886 bmv3 = bm.verts.new(edgeV2)
887 bmv4 = bm.verts.new(arcV2)
889 if parameters["connectArcWithEdgeFlip"] is False:
890 bme = bm.edges.new([bmv1, bmv2])
891 bme2 = bm.edges.new([bmv3, bmv4])
892 else:
893 bme = bm.edges.new([bmv1, bmv4])
894 bme2 = bm.edges.new([bmv3, bmv2])
895 self.sel.refreshMesh(bm, mesh)
897 def connectScaledEdgesWithBaseEdge(self, scaledEdges, baseEdges, bm, mesh):
898 for i in range(0, len(scaledEdges)):
899 scaledEdgeV1 = scaledEdges[i].verts[0].co
900 baseEdgeV1 = baseEdges[i].verts[0].co
901 scaledEdgeV2 = scaledEdges[i].verts[1].co
902 baseEdgeV2 = baseEdges[i].verts[1].co
904 bmv1 = bm.verts.new(baseEdgeV1)
905 bmv2 = bm.verts.new(scaledEdgeV1)
906 bme = bm.edges.new([bmv1, bmv2])
908 bmv3 = bm.verts.new(scaledEdgeV2)
909 bmv4 = bm.verts.new(baseEdgeV2)
910 bme = bm.edges.new([bmv3, bmv4])
911 self.sel.refreshMesh(bm, mesh)
913 def connectArcsTogether(self, arcs, bm, mesh, parameters):
914 for i in range(0, len(arcs) - 1):
915 # in case on XZ or YZ there are no arcs drawn
916 if arcs[i] is None or arcs[i + 1] is None:
917 return
919 lastVert = len(arcs[i]) - 1
920 if parameters["drawArcCenters"]:
921 lastVert = lastVert - 1 # center gets added as last vert of arc
922 # take last vert of arc i and first vert of arc i+1
924 V1 = arcs[i][lastVert].co
925 V2 = arcs[i + 1][0].co
927 if parameters["connectArcsFlip"]:
928 V1 = arcs[i][0].co
929 V2 = arcs[i + 1][lastVert].co
931 bmv1 = bm.verts.new(V1)
932 bmv2 = bm.verts.new(V2)
933 bme = bm.edges.new([bmv1, bmv2])
935 # connect last arc and first one
936 lastArcId = len(arcs) - 1
937 lastVertIdOfLastArc = len(arcs[lastArcId]) - 1
938 if parameters["drawArcCenters"]:
939 # center gets added as last vert of arc
940 lastVertIdOfLastArc = lastVertIdOfLastArc - 1
942 V1 = arcs[lastArcId][lastVertIdOfLastArc].co
943 V2 = arcs[0][0].co
944 if parameters["connectArcsFlip"]:
945 V1 = arcs[lastArcId][0].co
946 V2 = arcs[0][lastVertIdOfLastArc].co
948 bmv1 = bm.verts.new(V1)
949 bmv2 = bm.verts.new(V2)
950 bme = bm.edges.new([bmv1, bmv2])
952 self.sel.refreshMesh(bm, mesh)
954 def offsetArcPerpendicular(self, bm, mesh, Verts, edge, parameters):
955 perpendicularVector = self.getEdgePerpendicularVector(edge, parameters["plane"])
956 offset = parameters["offset"]
957 translation = offset * perpendicularVector
959 try:
960 bmesh.ops.translate(bm, verts=Verts, vec=translation)
961 except ValueError:
962 print("[Edge Roundifier]: Perpendicular translate value error - "
963 "multiple vertices in list - try unchecking 'Centers'")
965 indexes = [v.index for v in Verts]
966 self.sel.refreshMesh(bm, mesh)
967 offsetVertices = [bm.verts[i] for i in indexes]
968 return offsetVertices
970 def offsetArcParallel(self, bm, mesh, Verts, edge, parameters):
971 edgeVector = self.getNormalizedEdgeVector(edge)
972 offset = parameters["offset2"]
973 translation = offset * edgeVector
975 try:
976 bmesh.ops.translate(bm, verts=Verts, vec=translation)
977 except ValueError:
978 print("[Edge Roundifier]: Parallel translate value error - "
979 "multiple vertices in list - try unchecking 'Centers'")
981 indexes = [v.index for v in Verts]
982 self.sel.refreshMesh(bm, mesh)
983 offsetVertices = [bm.verts[i] for i in indexes]
984 return offsetVertices
986 def skipThisEdge(self, V1, V2, plane):
987 # Check If It is possible to spin selected verts on this plane if not exit roundifier
988 if(plane == XY):
989 if (V1[0] == V2[0] and V1[1] == V2[1]):
990 return True
991 elif(plane == YZ):
992 if (V1[1] == V2[1] and V1[2] == V2[2]):
993 return True
994 elif(plane == XZ):
995 if (V1[0] == V2[0] and V1[2] == V2[2]):
996 return True
997 return False
999 def calculateRoundifyParams(self, edge, parameters, bm, mesh):
1000 # Because all data from mesh is in local coordinates
1001 # and spin operator works on global coordinates
1002 # We first need to translate all input data by vector equal
1003 # to origin position and then perform calculations
1004 # At least that is my understanding :) <komi3D>
1006 # V1 V2 stores Local Coordinates
1007 V1, V2, edgeVector, edgeLength, edgeCenter = self.getEdgeInfo(edge)
1009 debugPrintNew(d_Plane, "PLANE: " + parameters["plane"])
1010 lineAB = self.calc.getLineCoefficientsPerpendicularToVectorInPoint(
1011 edgeCenter, edgeVector,
1012 parameters["plane"]
1014 circleMidPoint = V1
1015 circleMidPointOnPlane = self.calc.getCircleMidPointOnPlane(
1016 V1, parameters["plane"]
1018 radius = parameters["radius"]
1020 angle = 0
1021 if (parameters["entryMode"] == 'Angle'):
1022 if (parameters["angleEnum"] != 'Other'):
1023 radius, angle = self.CalculateRadiusAndAngleForAnglePresets(
1024 parameters["angleEnum"], radius,
1025 angle, edgeLength
1027 else:
1028 radius, angle = self.CalculateRadiusAndAngle(edgeLength)
1029 debugPrintNew(d_Radius_Angle, "RADIUS = " + str(radius) + " ANGLE = " + str(angle))
1030 roots = None
1031 if angle != pi: # mode other than 180
1032 if lineAB is None:
1033 roots = self.calc.getLineCircleIntersectionsWhenXPerpendicular(
1034 edgeCenter, circleMidPointOnPlane,
1035 radius, parameters["plane"]
1037 else:
1038 roots = self.calc.getLineCircleIntersections(
1039 lineAB, circleMidPointOnPlane, radius
1042 if roots is None:
1043 debugPrintNew(True,
1044 "[Edge Roundifier]: No centers were found. Change radius to higher value")
1045 return None
1046 roots = self.addMissingCoordinate(roots, V1, parameters["plane"]) # adds X, Y or Z coordinate
1047 else:
1048 roots = [edgeCenter, edgeCenter]
1049 debugPrintNew(d_Roots, "roots=" + str(roots))
1051 refObjectLocation = None
1052 objectLocation = bpy.context.active_object.location # Origin Location
1054 if parameters["refObject"] == "ORG":
1055 refObjectLocation = [0, 0, 0]
1056 elif parameters["refObject"] == "CUR":
1057 refObjectLocation = bpy.context.scene.cursor.location - objectLocation
1058 else:
1059 refObjectLocation = self.calc.getEdgeReference(edge, edgeCenter, parameters["plane"])
1061 debugPrintNew(d_RefObject, parameters["refObject"], refObjectLocation)
1062 chosenSpinCenter, otherSpinCenter = self.getSpinCenterClosestToRefCenter(
1063 refObjectLocation, roots
1066 if (parameters["entryMode"] == "Radius"):
1067 halfAngle = self.calc.getAngle(edgeCenter, chosenSpinCenter, circleMidPoint)
1068 angle = 2 * halfAngle[0] # in radians
1069 self.a = degrees(angle) # in degrees
1071 spinAxis = self.getSpinAxis(parameters["plane"])
1072 steps = parameters["segments"]
1073 angle = -angle # rotate clockwise by default
1075 return [chosenSpinCenter, otherSpinCenter, spinAxis, angle, steps, refObjectLocation]
1077 def drawSpin(self, edge, edgeCenter, roundifyParams, parameters, bm, mesh):
1078 [chosenSpinCenter, otherSpinCenter, spinAxis, angle, steps, refObjectLocation] = roundifyParams
1080 v0org, v1org = (edge.verts[0], edge.verts[1])
1082 if parameters["flip"]:
1083 angle = -angle
1084 spinCenterTemp = chosenSpinCenter
1085 chosenSpinCenter = otherSpinCenter
1086 otherSpinCenter = spinCenterTemp
1088 if(parameters["invertAngle"]):
1089 if angle < 0:
1090 angle = two_pi + angle
1091 elif angle > 0:
1092 angle = -two_pi + angle
1093 else:
1094 angle = two_pi
1096 if(parameters["fullCircles"]):
1097 angle = two_pi
1099 v0 = bm.verts.new(v0org.co)
1101 result = bmesh.ops.spin(
1102 bm, geom=[v0], cent=chosenSpinCenter, axis=spinAxis,
1103 angle=angle, steps=steps, use_duplicate=False
1106 # it seems there is something wrong with last index of this spin
1107 # I need to calculate the last index manually here
1108 vertsLength = len(bm.verts)
1109 bm.verts.ensure_lookup_table()
1110 lastVertIndex = bm.verts[vertsLength - 1].index
1111 lastSpinVertIndices = self.getLastSpinVertIndices(steps, lastVertIndex)
1113 self.sel.refreshMesh(bm, mesh)
1115 alternativeLastSpinVertIndices = []
1116 bothSpinVertices = []
1117 spinVertices = []
1118 alternate = False
1120 if ((angle == pi or angle == -pi) and not parameters["bothSides"]):
1122 midVertexIndex = lastVertIndex - round(steps / 2)
1123 bm.verts.ensure_lookup_table()
1124 midVert = bm.verts[midVertexIndex].co
1126 midVertexDistance = (Vector(refObjectLocation) - Vector(midVert)).length
1127 midEdgeDistance = (Vector(refObjectLocation) - Vector(edgeCenter)).length
1129 if ((parameters["invertAngle"]) or (parameters["flip"])):
1130 if (midVertexDistance > midEdgeDistance):
1131 alternativeLastSpinVertIndices = self.alternateSpin(
1132 bm, mesh, angle, chosenSpinCenter,
1133 spinAxis, steps, v0, v1org, lastSpinVertIndices
1135 else:
1136 if (midVertexDistance < midEdgeDistance):
1137 alternativeLastSpinVertIndices = self.alternateSpin(
1138 bm, mesh, angle, chosenSpinCenter,
1139 spinAxis, steps, v0, v1org, lastSpinVertIndices
1141 elif (angle != two_pi): # to allow full circles
1142 if (result['geom_last'][0].co - v1org.co).length > SPIN_END_THRESHOLD:
1143 alternativeLastSpinVertIndices = self.alternateSpin(
1144 bm, mesh, angle, chosenSpinCenter,
1145 spinAxis, steps, v0, v1org, lastSpinVertIndices
1147 alternate = True
1149 self.sel.refreshMesh(bm, mesh)
1150 if alternativeLastSpinVertIndices != []:
1151 lastSpinVertIndices = alternativeLastSpinVertIndices
1153 if lastSpinVertIndices.stop <= len(bm.verts): # make sure arc was added to bmesh
1154 spinVertices = [bm.verts[i] for i in lastSpinVertIndices]
1155 if alternativeLastSpinVertIndices != []:
1156 spinVertices = spinVertices + [v0]
1157 else:
1158 spinVertices = [v0] + spinVertices
1160 if (parameters["bothSides"]):
1161 # do some more testing here!!!
1162 if (angle == pi or angle == -pi):
1163 alternativeLastSpinVertIndices = self.alternateSpinNoDelete(
1164 bm, mesh, -angle, chosenSpinCenter,
1165 spinAxis, steps, v0, v1org, []
1167 elif alternate:
1168 alternativeLastSpinVertIndices = self.alternateSpinNoDelete(
1169 bm, mesh, angle, otherSpinCenter,
1170 spinAxis, steps, v0, v1org, []
1172 elif not alternate:
1173 alternativeLastSpinVertIndices = self.alternateSpinNoDelete(
1174 bm, mesh, -angle, otherSpinCenter,
1175 spinAxis, steps, v0, v1org, []
1177 bothSpinVertices = [bm.verts[i] for i in lastSpinVertIndices]
1178 alternativeSpinVertices = [bm.verts[i] for i in alternativeLastSpinVertIndices]
1179 bothSpinVertices = [v0] + bothSpinVertices + alternativeSpinVertices
1180 spinVertices = bothSpinVertices
1182 if (parameters["fullCircles"]):
1183 v1 = bm.verts.new(v1org.co)
1184 spinVertices = spinVertices + [v1]
1186 if (parameters['drawArcCenters']):
1187 centerVert = bm.verts.new(chosenSpinCenter)
1188 spinVertices.append(centerVert)
1190 return spinVertices, [chosenSpinCenter, otherSpinCenter, spinAxis, angle, steps, refObjectLocation]
1192 def deleteSpinVertices(self, bm, mesh, lastSpinVertIndices):
1193 verticesForDeletion = []
1194 bm.verts.ensure_lookup_table()
1195 for i in lastSpinVertIndices:
1196 vi = bm.verts[i]
1197 vi.select = True
1198 debugPrintNew(True, str(i) + ") " + str(vi))
1199 verticesForDeletion.append(vi)
1201 bmesh.ops.delete(bm, geom=verticesForDeletion, context = 'VERTS')
1202 bmesh.update_edit_mesh(mesh, loop_triangles=True)
1203 bpy.ops.object.mode_set(mode='OBJECT')
1204 bpy.ops.object.mode_set(mode='EDIT')
1206 def alternateSpinNoDelete(self, bm, mesh, angle, chosenSpinCenter,
1207 spinAxis, steps, v0, v1org, lastSpinVertIndices):
1208 v0prim = v0
1210 result2 = bmesh.ops.spin(bm, geom=[v0prim], cent=chosenSpinCenter, axis=spinAxis,
1211 angle=angle, steps=steps, use_duplicate=False)
1212 vertsLength = len(bm.verts)
1213 bm.verts.ensure_lookup_table()
1214 lastVertIndex2 = bm.verts[vertsLength - 1].index
1216 lastSpinVertIndices2 = self.getLastSpinVertIndices(steps, lastVertIndex2)
1217 return lastSpinVertIndices2
1219 def alternateSpin(self, bm, mesh, angle, chosenSpinCenter,
1220 spinAxis, steps, v0, v1org, lastSpinVertIndices):
1222 self.deleteSpinVertices(bm, mesh, lastSpinVertIndices)
1223 v0prim = v0
1225 result2 = bmesh.ops.spin(
1226 bm, geom=[v0prim], cent=chosenSpinCenter, axis=spinAxis,
1227 angle=-angle, steps=steps, use_duplicate=False
1229 # it seems there is something wrong with last index of this spin
1230 # I need to calculate the last index manually here
1231 vertsLength = len(bm.verts)
1232 bm.verts.ensure_lookup_table()
1233 lastVertIndex2 = bm.verts[vertsLength - 1].index
1235 lastSpinVertIndices2 = self.getLastSpinVertIndices(steps, lastVertIndex2)
1236 # second spin also does not hit the v1org
1237 if (result2['geom_last'][0].co - v1org.co).length > SPIN_END_THRESHOLD:
1239 self.deleteSpinVertices(bm, mesh, lastSpinVertIndices2)
1240 self.deleteSpinVertices(bm, mesh, range(v0.index, v0.index + 1))
1241 return []
1242 else:
1243 return lastSpinVertIndices2
1245 def getLastSpinVertIndices(self, steps, lastVertIndex):
1246 arcfirstVertexIndex = lastVertIndex - steps + 1
1247 lastSpinVertIndices = range(arcfirstVertexIndex, lastVertIndex + 1)
1248 return lastSpinVertIndices
1250 def rotateArcAroundSpinAxis(self, bm, mesh, vertices, parameters, edgeCenter):
1251 axisAngle = parameters["axisAngle"]
1252 plane = parameters["plane"]
1253 # compensate rotation center
1254 objectLocation = bpy.context.active_object.location
1255 center = objectLocation + edgeCenter
1257 rot = Euler((0.0, 0.0, radians(axisAngle)), 'XYZ').to_matrix()
1258 if plane == YZ:
1259 rot = Euler((radians(axisAngle), 0.0, 0.0), 'XYZ').to_matrix()
1260 if plane == XZ:
1261 rot = Euler((0.0, radians(axisAngle), 0.0), 'XYZ').to_matrix()
1263 indexes = [v.index for v in vertices]
1265 bmesh.ops.rotate(
1267 cent=center,
1268 matrix=rot,
1269 verts=vertices,
1270 space=bpy.context.edit_object.matrix_world
1272 self.sel.refreshMesh(bm, mesh)
1273 bm.verts.ensure_lookup_table()
1274 rotatedVertices = [bm.verts[i] for i in indexes]
1276 return rotatedVertices
1278 def CalculateRadiusAndAngle(self, edgeLength):
1279 degAngle = self.a
1280 angle = radians(degAngle)
1281 self.r = radius = edgeLength / (2 * sin(angle / 2))
1282 return radius, angle
1284 def CalculateRadiusAndAngleForAnglePresets(self, angleEnum, initR, initA, edgeLength):
1285 radius = initR
1286 angle = initA
1287 try:
1288 # Note - define an integer string in the angleEnum
1289 angle_convert = int(angleEnum)
1290 self.a = angle_convert
1291 except:
1292 self.a = 180 # fallback
1293 debugPrintNew(True,
1294 "CalculateRadiusAndAngleForAnglePresets problem with int conversion")
1296 return self.CalculateRadiusAndAngle(edgeLength)
1298 def getSpinCenterClosestToRefCenter(self, objLocation, roots):
1299 root0Distance = (Vector(objLocation) - Vector(roots[0])).length
1300 root1Distance = (Vector(objLocation) - Vector(roots[1])).length
1302 chosenId = 0
1303 rejectedId = 1
1304 if (root0Distance > root1Distance):
1305 chosenId = 1
1306 rejectedId = 0
1307 return roots[chosenId], roots[rejectedId]
1309 def addMissingCoordinate(self, roots, startVertex, plane):
1310 if roots is not None:
1311 a, b = roots[0]
1312 c, d = roots[1]
1313 if plane == XY:
1314 roots[0] = Vector((a, b, startVertex[2]))
1315 roots[1] = Vector((c, d, startVertex[2]))
1316 if plane == YZ:
1317 roots[0] = Vector((startVertex[0], a, b))
1318 roots[1] = Vector((startVertex[0], c, d))
1319 if plane == XZ:
1320 roots[0] = Vector((a, startVertex[1], b))
1321 roots[1] = Vector((c, startVertex[1], d))
1322 return roots
1324 def selectEdgesAfterRoundifier(self, context, edges):
1325 bpy.ops.object.mode_set(mode='OBJECT')
1326 bpy.ops.object.mode_set(mode='EDIT')
1327 mesh = context.view_layer.objects.active.data
1328 bmnew = bmesh.new()
1329 bmnew.from_mesh(mesh)
1331 self.deselectEdges(bmnew)
1332 for selectedEdge in edges:
1333 for e in bmnew.edges:
1334 if (e.verts[0].co - selectedEdge.verts[0].co).length <= self.threshold \
1335 and (e.verts[1].co - selectedEdge.verts[1].co).length <= self.threshold:
1336 e.select_set(True)
1338 bpy.ops.object.mode_set(mode='OBJECT')
1339 bmnew.to_mesh(mesh)
1340 bmnew.free()
1341 bpy.ops.object.mode_set(mode='EDIT')
1343 def deselectEdges(self, bm):
1344 for edge in bm.edges:
1345 edge.select_set(False)
1347 def getSpinAxis(self, plane):
1348 axis = (0, 0, 1)
1349 if plane == YZ:
1350 axis = (1, 0, 0)
1351 if plane == XZ:
1352 axis = (0, 1, 0)
1353 return axis
1356 @classmethod
1357 def poll(cls, context):
1358 return (context.view_layer.objects.active.type == 'MESH') and (context.view_layer.objects.active.mode == 'EDIT')
1360 def draw_item(self, context):
1361 self.layout.operator_context = 'INVOKE_DEFAULT'
1362 self.layout.operator('mesh.edge_roundifier')
1365 classes = (
1366 EdgeRoundifier,
1369 reg_cls, unreg_cls = bpy.utils.register_classes_factory(classes)
1372 def register():
1373 reg_cls()
1374 bpy.types.VIEW3D_MT_edit_mesh_edges.append(draw_item)
1377 def unregister():
1378 unreg_cls()
1379 bpy.types.VIEW3D_MT_edit_mesh_edges.remove(draw_item)
1381 if __name__ == "__main__":
1382 register()