Merge branch 'blender-v3.3-release'
[blender-addons.git] / mesh_tools / mesh_edge_roundifier.py
blob7e1874e2fd5e9edf7104258387950e7924eb4b9a
1 # SPDX-License-Identifier: GPL-2.0-or-later
3 bl_info = {
4 "name": "Edge Roundifier",
5 "author": "Piotr Komisarczyk (komi3D), PKHG",
6 "version": (1, 0, 2),
7 "blender": (2, 80, 0),
8 "location": "SPACE > Edge Roundifier or CTRL-E > "
9 "Edge Roundifier or Tools > Addons > Edge Roundifier",
10 "description": "Mesh editing script allowing edge rounding",
11 "doc_url": "",
12 "category": "Mesh"
15 import bpy
16 import bmesh
17 from bpy.types import Operator
18 from bpy.props import (
19 BoolProperty,
20 FloatProperty,
21 EnumProperty,
22 IntProperty,
24 from math import (
25 sqrt, acos, pi,
26 radians, degrees, sin,
28 from mathutils import (
29 Vector, Euler,
30 Quaternion,
33 # CONSTANTS
34 two_pi = 2 * pi
35 XY = "XY"
36 XZ = "XZ"
37 YZ = "YZ"
38 SPIN_END_THRESHOLD = 0.001
39 LINE_TOLERANCE = 0.0001
40 d_XABS_YABS = False
41 d_Edge_Info = False
42 d_Plane = False
43 d_Radius_Angle = False
44 d_Roots = False
45 d_RefObject = False
46 d_LineAB = False
47 d_Selected_edges = False
48 d_Rotate_Around_Spin_Center = False
50 # Enable debug prints
51 DEBUG = False
54 # for debugging PKHG #
55 def debugPrintNew(debugs, *text):
56 if DEBUG and debugs:
57 tmp = [el for el in text]
58 for row in tmp:
59 print(row)
62 # Geometry and math calculation methods #
64 class CalculationHelper:
66 def __init__(self):
67 """
68 Constructor
69 """
70 def getLineCoefficientsPerpendicularToVectorInPoint(self, point, vector, plane):
71 x, y, z = point
72 xVector, yVector, zVector = vector
73 destinationPoint = (x + yVector, y - xVector, z)
74 if plane == 'YZ':
75 destinationPoint = (x, y + zVector, z - yVector)
76 if plane == 'XZ':
77 destinationPoint = (x + zVector, y, z - xVector)
78 return self.getCoefficientsForLineThrough2Points(point, destinationPoint, plane)
80 def getQuadraticRoots(self, coef):
81 if len(coef) != 3:
82 return None # Replaced NaN with None
83 else:
84 a, b, c = coef
85 delta = b ** 2 - 4 * a * c
86 if delta == 0:
87 x = -b / (2 * a)
88 return (x, x)
89 elif delta < 0:
90 return None
91 else:
92 x1 = (-b - sqrt(delta)) / (2 * a)
93 x2 = (-b + sqrt(delta)) / (2 * a)
94 return (x1, x2)
96 def getCoefficientsForLineThrough2Points(self, point1, point2, plane):
97 x1, y1, z1 = point1
98 x2, y2, z2 = point2
100 # mapping x1,x2, y1,y2 to proper values based on plane
101 if plane == YZ:
102 x1 = y1
103 x2 = y2
104 y1 = z1
105 y2 = z2
106 if plane == XZ:
107 y1 = z1
108 y2 = z2
110 # Further calculations the same as for XY plane
111 xabs = abs(x2 - x1)
112 yabs = abs(y2 - y1)
113 debugPrintNew(d_XABS_YABS, "XABS = " + str(xabs) + " YABS = " + str(yabs))
115 if xabs <= LINE_TOLERANCE:
116 return None # this means line x = edgeCenterX
117 if yabs <= LINE_TOLERANCE:
118 A = 0
119 B = y1
120 return A, B
121 A = (y2 - y1) / (x2 - x1)
122 B = y1 - (A * x1)
123 return (A, B)
125 def getLineCircleIntersections(self, lineAB, circleMidPoint, radius):
126 # (x - a)**2 + (y - b)**2 = r**2 - circle equation
127 # y = A*x + B - line equation
128 # f * x**2 + g * x + h = 0 - quadratic equation
129 A, B = lineAB
130 a, b = circleMidPoint
131 f = 1 + (A ** 2)
132 g = -2 * a + 2 * A * B - 2 * A * b
133 h = (B ** 2) - 2 * b * B - (radius ** 2) + (a ** 2) + (b ** 2)
134 coef = [f, g, h]
135 roots = self.getQuadraticRoots(coef)
136 if roots is not None:
137 x1 = roots[0]
138 x2 = roots[1]
139 point1 = [x1, A * x1 + B]
140 point2 = [x2, A * x2 + B]
141 return [point1, point2]
142 else:
143 return None
145 def getLineCircleIntersectionsWhenXPerpendicular(self, edgeCenter,
146 circleMidPoint, radius, plane):
147 # (x - a)**2 + (y - b)**2 = r**2 - circle equation
148 # x = xValue - line equation
149 # f * x**2 + g * x + h = 0 - quadratic equation
150 xValue = edgeCenter[0]
151 if plane == YZ:
152 xValue = edgeCenter[1]
153 if plane == XZ:
154 xValue = edgeCenter[0]
156 a, b = circleMidPoint
157 f = 1
158 g = -2 * b
159 h = (a ** 2) + (b ** 2) + (xValue ** 2) - 2 * a * xValue - (radius ** 2)
160 coef = [f, g, h]
161 roots = self.getQuadraticRoots(coef)
162 if roots is not None:
163 y1 = roots[0]
164 y2 = roots[1]
165 point1 = [xValue, y1]
166 point2 = [xValue, y2]
167 return [point1, point2]
168 else:
169 return None
171 # point1 is the point near 90 deg angle
172 def getAngle(self, point1, point2, point3):
173 distance1 = (Vector(point1) - Vector(point2)).length
174 distance2 = (Vector(point2) - Vector(point3)).length
175 cos = distance1 / distance2
177 if abs(cos) > 1: # prevents Domain Error
178 cos = round(cos)
180 alpha = acos(cos)
181 return (alpha, degrees(alpha))
183 # get two of three coordinates used for further calculation of spin center
184 # PKHG>nice if rescriction to these 3 types or planes is to be done
185 # komi3D> from 0.0.2 there is a restriction. In future I would like Edge
186 # komi3D> Roundifier to work on Normal and View coordinate systems
187 def getCircleMidPointOnPlane(self, V1, plane):
188 X = V1[0]
189 Y = V1[1]
190 if plane == 'XZ':
191 X = V1[0]
192 Y = V1[2]
193 elif plane == 'YZ':
194 X = V1[1]
195 Y = V1[2]
196 return [X, Y]
198 def getEdgeReference(self, edge, edgeCenter, plane):
199 vert1 = edge.verts[1].co
200 V = vert1 - edgeCenter
201 orthoVector = Vector((V[1], -V[0], V[2]))
202 if plane == 'XZ':
203 orthoVector = Vector((V[2], V[1], -V[0]))
204 elif plane == 'YZ':
205 orthoVector = Vector((V[0], V[2], -V[1]))
206 refPoint = edgeCenter + orthoVector
207 return refPoint
210 # Selection Methods #
212 class SelectionHelper:
214 def selectVertexInMesh(self, mesh, vertex):
215 bpy.ops.object.mode_set(mode="OBJECT")
216 for v in mesh.vertices:
217 if v.co == vertex:
218 v.select = True
219 break
221 bpy.ops.object.mode_set(mode="EDIT")
223 def getSelectedVertex(self, mesh):
224 bpy.ops.object.mode_set(mode="OBJECT")
225 for v in mesh.vertices:
226 if v.select is True:
227 bpy.ops.object.mode_set(mode="EDIT")
228 return v
230 bpy.ops.object.mode_set(mode="EDIT")
231 return None
233 def refreshMesh(self, bm, mesh):
234 bpy.ops.object.mode_set(mode='OBJECT')
235 bm.to_mesh(mesh)
236 bpy.ops.object.mode_set(mode='EDIT')
239 # Operator
241 class EdgeRoundifier(Operator):
242 bl_idname = "mesh.edge_roundifier"
243 bl_label = "Edge Roundifier"
244 bl_description = "Mesh modeling tool for building arcs on selected Edges"
245 bl_options = {'REGISTER', 'UNDO', 'PRESET'}
247 threshold = 0.0005
248 obj = None
250 edgeScaleFactor: FloatProperty(
251 name="",
252 description="Set the Factor of scaling",
253 default=1.0,
254 min=0.00001, max=100000.0,
255 step=0.5,
256 precision=5
258 r: FloatProperty(
259 name="",
260 description="User Defined arc steepness by a Radius\n"
261 "Enabled only if Entry mode is set to Radius\n",
262 default=1,
263 min=0.00001, max=1000.0,
264 step=0.1,
265 precision=3
267 a: FloatProperty(
268 name="",
269 description="User defined arc steepness calculated from an Angle\n"
270 "Enabled only if Entry mode is set to Angle and\n"
271 "Angle presets is set Other",
272 default=180.0,
273 min=0.1, max=180.0,
274 step=0.5,
275 precision=1
277 n: IntProperty(
278 name="",
279 description="Arc subdivision level",
280 default=4,
281 min=1, max=100,
282 step=1
284 flip: BoolProperty(
285 name="Flip",
286 description="If True, flip the side of the selected edges where the arcs are drawn",
287 default=False
289 invertAngle: BoolProperty(
290 name="Invert",
291 description="If True, uses an inverted angle to draw the arc (360 degrees - angle)",
292 default=False
294 fullCircles: BoolProperty(
295 name="Circles",
296 description="If True, uses an angle of 360 degrees to draw the arcs",
297 default=False
299 bothSides: BoolProperty(
300 name="Both sides",
301 description="If True, draw arcs on both sides of the selected edges",
302 default=False
304 drawArcCenters: BoolProperty(
305 name="Centers",
306 description="If True, draws a vertex for each spin center",
307 default=False
309 removeEdges: BoolProperty(
310 name="Edges",
311 description="If True removes the Original selected edges",
312 default=False
314 removeScaledEdges: BoolProperty(
315 name="Scaled edges",
316 description="If True removes the Scaled edges (not part of the arcs)",
317 default=False
319 connectArcWithEdge: BoolProperty(
320 name="Arc - Edge",
321 description="Connect Arcs to Edges",
322 default=False
324 connectArcs: BoolProperty(
325 name="Arcs",
326 description="Connect subsequent Arcs",
327 default=False
329 connectScaledAndBase: BoolProperty(
330 name="Scaled - Base Edge",
331 description="Connect Scaled to Base Edge",
332 default=False
334 connectArcsFlip: BoolProperty(
335 name="Flip Arcs",
336 description="Flip the connection of subsequent Arcs",
337 default=False
339 connectArcWithEdgeFlip: BoolProperty(
340 name="Flip Arc - Edge",
341 description="Flip the connection of the Arcs to Edges",
342 default=False
344 axisAngle: FloatProperty(
345 name="",
346 description="Rotate Arc around the perpendicular axis",
347 default=0.0,
348 min=-180.0, max=180.0,
349 step=0.5,
350 precision=1
352 edgeAngle: FloatProperty(
353 name="",
354 description="Rotate Arc around the Edge (Edge acts like as the axis)",
355 default=0.0,
356 min=-180.0, max=180.0,
357 step=0.5,
358 precision=1
360 offset: FloatProperty(
361 name="",
362 description="Offset Arc perpendicular the Edge",
363 default=0.0,
364 min=-1000000.0, max=1000000.0,
365 step=0.1,
366 precision=5
368 offset2: FloatProperty(
369 name="",
370 description="Offset Arc in parallel to the Edge",
371 default=0.0,
372 min=-1000000.0, max=1000000.0,
373 step=0.1,
374 precision=5
376 ellipticFactor: FloatProperty(
377 name="",
378 description="Make Arc elliptic",
379 default=0.0,
380 min=-1000000.0, max=1000000.0,
381 step=0.1,
382 precision=5
384 workModeItems = [("Normal", "Normal", ""), ("Reset", "Reset", "")]
385 workMode: EnumProperty(
386 items=workModeItems,
387 name="",
388 default='Normal',
389 description="Normal work with the current given parameters set by the user\n"
390 "Reset - changes back the parameters to their default values"
392 entryModeItems = [("Radius", "Radius", ""), ("Angle", "Angle", "")]
393 entryMode: EnumProperty(
394 items=entryModeItems,
395 name="",
396 default='Angle',
397 description="Entry mode switch between Angle and Radius\n"
398 "If Angle is selected, arc radius is calculated from it"
400 rotateCenterItems = [
401 ("Spin", "Spin", ""), ("V1", "V1", ""),
402 ("Edge", "Edge", ""), ("V2", "V2", "")
404 rotateCenter: EnumProperty(
405 items=rotateCenterItems,
406 name="",
407 default='Edge',
408 description="Rotate center for spin axis rotate"
410 arcModeItems = [("FullEdgeArc", "Full", "Full"), ('HalfEdgeArc', "Half", "Half")]
411 arcMode: EnumProperty(
412 items=arcModeItems,
413 name="",
414 default='FullEdgeArc',
415 description="Arc mode - switch between Full and Half arcs"
417 angleItems = [
418 ('Other', "Other", "User defined angle"), ('180', "180", "HemiCircle (2 sides)"),
419 ('120', "120", "TriangleCircle (3 sides)"), ('90', "90", "QuadCircle (4 sides)"),
420 ('72', "72", "PentagonCircle (5 sides)"), ('60', "60", "HexagonCircle (6 sides)"),
421 ('45', "45", "OctagonCircle (8 sides)"), ('30', "30", "DodecagonCircle (12 sides)")
423 angleEnum: EnumProperty(
424 items=angleItems,
425 name="",
426 default='180',
427 description="Presets prepare standard angles and calculate proper ray"
429 refItems = [('ORG', "Origin", "Use Origin Location"), ('CUR', "3D Cursor", "Use 3DCursor Location"),
430 ('EDG', "Edge", "Use Individual Edge Reference")]
431 referenceLocation: EnumProperty(
432 items=refItems,
433 name="",
434 default='ORG',
435 description="Reference location used to calculate initial centers of drawn arcs"
437 planeItems = [
438 (XY, "XY", "XY Plane (Z=0)"),
439 (YZ, "YZ", "YZ Plane (X=0)"),
440 (XZ, "XZ", "XZ Plane (Y=0)")
442 planeEnum: EnumProperty(
443 items=planeItems,
444 name="",
445 default='XY',
446 description="Plane used to calculate spin plane of drawn arcs"
448 edgeScaleCenterItems = [
449 ('V1', "V1", "v1 - First Edge's Vertex"),
450 ('CENTER', "Center", "Center of the Edge"),
451 ('V2', "V2", "v2 - Second Edge's Vertex")
453 edgeScaleCenterEnum: EnumProperty(
454 items=edgeScaleCenterItems,
455 name="Edge scale center",
456 default='CENTER',
457 description="Center used for scaling the initial edge"
460 calc = CalculationHelper()
461 sel = SelectionHelper()
463 @classmethod
464 def poll(cls, context):
465 obj = context.active_object
466 return (obj and obj.type == 'MESH' and
467 obj.mode == 'EDIT')
469 def prepareMesh(self, context):
470 bpy.ops.object.mode_set(mode='OBJECT')
471 bpy.ops.object.mode_set(mode='EDIT')
473 mesh = context.view_layer.objects.active.data
474 bm = bmesh.new()
475 bm.from_mesh(mesh)
477 edges = [ele for ele in bm.edges if ele.select]
478 return edges, mesh, bm
480 def prepareParameters(self):
481 parameters = {"a": "a"}
482 parameters["arcMode"] = self.arcMode
483 parameters["edgeScaleFactor"] = self.edgeScaleFactor
484 parameters["edgeScaleCenterEnum"] = self.edgeScaleCenterEnum
485 parameters["plane"] = self.planeEnum
486 parameters["radius"] = self.r
487 parameters["angle"] = self.a
488 parameters["segments"] = self.n
489 parameters["fullCircles"] = self.fullCircles
490 parameters["invertAngle"] = self.invertAngle
491 parameters["bothSides"] = self.bothSides
492 parameters["angleEnum"] = self.angleEnum
493 parameters["entryMode"] = self.entryMode
494 parameters["workMode"] = self.workMode
495 parameters["refObject"] = self.referenceLocation
496 parameters["flip"] = self.flip
497 parameters["drawArcCenters"] = self.drawArcCenters
498 parameters["removeEdges"] = self.removeEdges
499 parameters["removeScaledEdges"] = self.removeScaledEdges
500 parameters["connectArcWithEdge"] = self.connectArcWithEdge
501 parameters["connectScaledAndBase"] = self.connectScaledAndBase
502 parameters["connectArcs"] = self.connectArcs
503 parameters["connectArcsFlip"] = self.connectArcsFlip
504 parameters["connectArcWithEdgeFlip"] = self.connectArcWithEdgeFlip
505 parameters["axisAngle"] = self.axisAngle
506 parameters["edgeAngle"] = self.edgeAngle
507 parameters["offset"] = self.offset
508 parameters["offset2"] = self.offset2
509 parameters["ellipticFactor"] = self.ellipticFactor
510 parameters["rotateCenter"] = self.rotateCenter
511 return parameters
513 def draw(self, context):
514 layout = self.layout
515 box = layout.box()
516 uiPercentage = 0.333
518 self.addEnumParameterToUI(box, False, uiPercentage, 'Mode:', 'workMode')
519 self.addEnumParameterToUI(box, False, uiPercentage, 'Plane:', 'planeEnum')
520 self.addEnumParameterToUI(box, False, uiPercentage, 'Reference:', 'referenceLocation')
522 box = layout.box()
523 self.addEnumParameterToUI(box, False, uiPercentage, 'Scale base:', 'edgeScaleCenterEnum')
524 self.addParameterToUI(box, False, uiPercentage, 'Scale factor:', 'edgeScaleFactor')
526 box = layout.box()
527 self.addEnumParameterToUI(box, False, uiPercentage, 'Entry mode:', 'entryMode')
529 row = box.row(align=False)
530 row.prop(self, 'angleEnum', expand=True, text="Angle presets")
532 disable_a = bool(self.entryMode == 'Angle' and self.angleEnum == 'Other')
533 disable_r = bool(self.entryMode == 'Radius')
535 self.addParameterToUI(box, False, uiPercentage, 'Angle:', 'a', disable_a)
536 self.addParameterToUI(box, False, uiPercentage, 'Radius:', 'r', disable_r)
537 self.addParameterToUI(box, False, uiPercentage, 'Segments:', 'n')
539 box = layout.box()
540 self.addCheckboxToUI(box, True, 'Options:', 'flip', 'invertAngle')
541 self.addCheckboxToUI(box, True, '', 'bothSides', 'fullCircles')
542 self.addCheckboxToUI(box, True, '', 'drawArcCenters')
544 box = layout.box()
545 self.addCheckboxToUI(box, True, 'Remove:', 'removeEdges', 'removeScaledEdges')
547 box = layout.box()
548 self.addCheckboxToUI(box, True, 'Connect:', 'connectArcs', 'connectArcsFlip')
549 self.addCheckboxToUI(box, True, '', 'connectArcWithEdge', 'connectArcWithEdgeFlip')
550 self.addCheckboxToUI(box, True, '', 'connectScaledAndBase')
552 box = layout.box()
553 self.addParameterToUI(box, False, uiPercentage, 'Orhto offset:', 'offset')
554 self.addParameterToUI(box, False, uiPercentage, 'Parallel offset:', 'offset2')
556 box = layout.box()
557 self.addParameterToUI(box, False, uiPercentage, 'Edge rotate :', 'edgeAngle')
558 self.addEnumParameterToUI(box, False, uiPercentage, 'Axis rotate center:', 'rotateCenter')
559 self.addParameterToUI(box, False, uiPercentage, 'Axis rotate:', 'axisAngle')
561 box = layout.box()
562 self.addParameterToUI(box, False, uiPercentage, 'Elliptic factor:', 'ellipticFactor')
564 def addParameterToUI(self, layout, alignment, percent, label, properties, disable=True):
565 row = layout.row(align=alignment)
566 split = row.split(factor=percent)
567 col = split.column()
569 col.label(text=label)
570 col2 = split.column()
571 row = col2.row(align=alignment)
572 row.enabled = disable
573 row.prop(self, properties)
575 def addCheckboxToUI(self, layout, alignment, label, property1, property2=None):
576 if label not in (""):
577 row = layout.row()
578 row.label(text=label)
579 row2 = layout.row(align=alignment)
580 if property2:
581 split = row2.split(factor=0.5)
582 split.prop(self, property1, toggle=True)
583 split.prop(self, property2, toggle=True)
584 else:
585 row2.prop(self, property1, toggle=True)
586 layout.separator()
588 def addEnumParameterToUI(self, layout, alignment, percent, label, properties):
589 row = layout.row(align=alignment)
590 split = row.split(factor=percent)
591 col = split.column()
593 col.label(text=label)
594 col2 = split.column()
595 row = col2.row(align=alignment)
596 row.prop(self, properties, expand=True, text="a")
598 def execute(self, context):
600 edges, mesh, bm = self.prepareMesh(context)
601 parameters = self.prepareParameters()
603 self.resetValues(parameters["workMode"])
605 self.obj = context.view_layer.objects.active
606 scaledEdges = self.scaleDuplicatedEdges(bm, edges, parameters)
608 if len(scaledEdges) > 0:
609 self.roundifyEdges(scaledEdges, parameters, bm, mesh)
611 if parameters["connectScaledAndBase"]:
612 self.connectScaledEdgesWithBaseEdge(scaledEdges, edges, bm, mesh)
614 self.sel.refreshMesh(bm, mesh)
615 self.selectEdgesAfterRoundifier(context, scaledEdges)
616 else:
617 debugPrintNew(True, "No edges selected!")
619 if parameters["removeEdges"]:
620 bmesh.ops.delete(bm, geom=edges, context='EDGES')
622 if parameters["removeScaledEdges"] and self.edgeScaleFactor != 1.0:
623 bmesh.ops.delete(bm, geom=scaledEdges, context='EDGES')
625 bpy.ops.object.mode_set(mode='OBJECT')
626 bm.to_mesh(mesh)
627 bpy.ops.object.mode_set(mode='EDIT')
628 bpy.ops.mesh.select_all(action='SELECT')
629 bpy.ops.mesh.remove_doubles()
631 bm.free()
633 return {'FINISHED'}
635 def resetValues(self, workMode):
636 if workMode == "Reset":
637 self.setAllParamsToDefaults()
639 def setAllParamsToDefaults(self):
640 try:
641 self.edgeScaleFactor = 1.0
642 self.r = 1
643 self.a = 180.0
644 self.n = 4
645 self.flip = False
646 self.invertAngle = False
647 self.fullCircles = False
648 self.bothSides = False
649 self.drawArcCenters = False
650 self.removeEdges = False
651 self.removeScaledEdges = False
653 self.connectArcWithEdge = False
654 self.connectArcs = False
655 self.connectScaledAndBase = False
656 self.connectArcsFlip = False
657 self.connectArcWithEdgeFlip = False
659 self.axisAngle = 0.0
660 self.edgeAngle = 0.0
661 self.offset = 0.0
662 self.offset2 = 0.0
663 self.ellipticFactor = 0.0
665 self.workMode = 'Normal'
666 self.entryMode = 'Angle'
667 self.angleEnum = '180'
668 self.referenceLocation = 'ORG'
669 self.planeEnum = 'XY'
670 self.edgeScaleCenterEnum = 'CENTER'
671 self.rotateCenter = 'Edge'
673 self.report({'INFO'}, "The parameters have been reset to default values")
674 except Exception as e:
675 self.report({'WARNING'}, "The parameters could not be reset")
676 debugPrintNew(True, "\n[setAllParamsToDefaults]\n parameter reset error\n" + e)
678 def scaleDuplicatedEdges(self, bm, edges, parameters):
679 scaleCenter = parameters["edgeScaleCenterEnum"]
680 factor = parameters["edgeScaleFactor"]
681 # this code is based on Zeffi's answer to my question
682 duplicateEdges = []
683 if factor == 1:
684 duplicateEdges = edges
685 else:
686 for e in edges:
687 v1 = e.verts[0].co
688 v2 = e.verts[1].co
689 origin = None
690 if scaleCenter == 'CENTER':
691 origin = (v1 + v2) * 0.5
692 elif scaleCenter == 'V1':
693 origin = v1
694 elif scaleCenter == 'V2':
695 origin = v2
697 bmv1 = bm.verts.new(((v1 - origin) * factor) + origin)
698 bmv2 = bm.verts.new(((v2 - origin) * factor) + origin)
699 bme = bm.edges.new([bmv1, bmv2])
700 duplicateEdges.append(bme)
701 return duplicateEdges
703 def roundifyEdges(self, edges, parameters, bm, mesh):
704 arcs = []
705 for e in edges:
706 arcVerts = self.roundify(e, parameters, bm, mesh)
707 arcs.append(arcVerts)
709 if parameters["connectArcs"]:
710 self.connectArcsTogether(arcs, bm, mesh, parameters)
712 def getNormalizedEdgeVector(self, edge):
713 V1 = edge.verts[0].co
714 V2 = edge.verts[1].co
715 edgeVector = V2 - V1
716 normEdge = edgeVector.normalized()
717 return normEdge
719 def getEdgePerpendicularVector(self, edge, plane):
720 normEdge = self.getNormalizedEdgeVector(edge)
722 edgePerpendicularVector = Vector((normEdge[1], -normEdge[0], 0))
723 if plane == YZ:
724 edgePerpendicularVector = Vector((0, normEdge[2], -normEdge[1]))
725 if plane == XZ:
726 edgePerpendicularVector = Vector((normEdge[2], 0, -normEdge[0]))
727 return edgePerpendicularVector
729 def getEdgeInfo(self, edge):
730 V1 = edge.verts[0].co
731 V2 = edge.verts[1].co
732 edgeVector = V2 - V1
733 edgeLength = edgeVector.length
734 edgeCenter = (V2 + V1) * 0.5
735 return V1, V2, edgeVector, edgeLength, edgeCenter
737 def roundify(self, edge, parameters, bm, mesh):
738 V1, V2, edgeVector, edgeLength, edgeCenter = self.getEdgeInfo(edge)
739 if self.skipThisEdge(V1, V2, parameters["plane"]):
740 return
742 roundifyParams = None
743 arcVerts = None
744 roundifyParams = self.calculateRoundifyParams(edge, parameters, bm, mesh)
745 if roundifyParams is None:
746 return
748 arcVerts = self.spinAndPostprocess(edge, parameters, bm, mesh, edgeCenter, roundifyParams)
749 return arcVerts
751 def spinAndPostprocess(self, edge, parameters, bm, mesh, edgeCenter, roundifyParams):
752 spinnedVerts, roundifyParamsUpdated = self.drawSpin(
753 edge, edgeCenter,
754 roundifyParams,
755 parameters, bm, mesh
757 postProcessedArcVerts = self.arcPostprocessing(
758 edge, parameters, bm, mesh,
759 roundifyParamsUpdated,
760 spinnedVerts, edgeCenter
762 return postProcessedArcVerts
764 def rotateArcAroundEdge(self, bm, mesh, arcVerts, parameters):
765 angle = parameters["edgeAngle"]
766 if angle != 0:
767 self.arc_rotator(arcVerts, angle, parameters)
769 # arc_rotator method was created by PKHG, I (komi3D) adjusted it to fit the rest
770 def arc_rotator(self, arcVerts, extra_rotation, parameters):
771 bpy.ops.object.mode_set(mode='OBJECT')
772 old_location = self.obj.location.copy()
773 bpy.ops.transform.translate(
774 value=-old_location,
775 constraint_axis=(False, False, False),
776 orient_type='GLOBAL',
777 mirror=False,
778 use_proportional_edit=False,
780 bpy.ops.object.mode_set(mode='EDIT')
781 adjust_matrix = self.obj.matrix_parent_inverse
782 bm = bmesh.from_edit_mesh(self.obj.data)
783 lastVert = len(arcVerts) - 1
784 if parameters["drawArcCenters"]:
785 lastVert = lastVert - 1 # center gets added as last vert of arc
786 v0_old = adjust_matrix @ arcVerts[0].co.copy()
788 # PKHG>INFO move if necessary v0 to origin such that the axis gos through origin and v1
789 if v0_old != Vector((0, 0, 0)):
790 for i, ele in enumerate(arcVerts):
791 arcVerts[i].co += - v0_old
793 axis = arcVerts[0].co - arcVerts[lastVert].co
794 a_mat = Quaternion(axis, radians(extra_rotation)).normalized().to_matrix()
796 for ele in arcVerts:
797 ele.co = a_mat @ ele.co
799 # PKHG>INFO move back if needed
800 if v0_old != Vector((0, 0, 0)):
801 for i, ele in enumerate(arcVerts):
802 arcVerts[i].co += + v0_old
804 bpy.ops.object.mode_set(mode='OBJECT')
805 # PKHG>INFO move origin object back print("old location = " , old_location)
806 bpy.ops.transform.translate(
807 value=old_location,
808 constraint_axis=(False, False, False),
809 orient_type='GLOBAL',
810 mirror=False,
811 use_proportional_edit=False,
813 bpy.ops.object.mode_set(mode='EDIT')
815 def makeElliptic(self, bm, mesh, arcVertices, parameters):
816 if parameters["ellipticFactor"] != 0: # if 0 then nothing has to be done
817 lastVert = len(arcVertices) - 1
818 if parameters["drawArcCenters"]:
819 lastVert = lastVert - 1 # center gets added as last vert of arc
820 v0co = arcVertices[0].co
821 v1co = arcVertices[lastVert].co
823 for vertex in arcVertices: # range(len(res_list)):
824 # PKHg>INFO compute the base on the edge of the height-vector
825 top = vertex.co # res_list[nr].co
826 t = 0
827 if v1co - v0co != 0:
828 t = (v1co - v0co).dot(top - v0co) / (v1co - v0co).length ** 2
829 h_bottom = v0co + t * (v1co - v0co)
830 height = (h_bottom - top)
831 vertex.co = top + parameters["ellipticFactor"] * height
833 return arcVertices
835 def arcPostprocessing(self, edge, parameters, bm, mesh, roundifyParams, spinnedVerts, edgeCenter):
836 [chosenSpinCenter, otherSpinCenter, spinAxis, angle, steps, refObjectLocation] = roundifyParams
837 rotatedVerts = []
838 if parameters["rotateCenter"] == 'Edge':
839 rotatedVerts = self.rotateArcAroundSpinAxis(
840 bm, mesh, spinnedVerts, parameters, edgeCenter
842 elif parameters["rotateCenter"] == 'Spin':
843 rotatedVerts = self.rotateArcAroundSpinAxis(
844 bm, mesh, spinnedVerts, parameters, chosenSpinCenter
846 elif parameters["rotateCenter"] == 'V1':
847 rotatedVerts = self.rotateArcAroundSpinAxis(
848 bm, mesh, spinnedVerts, parameters, edge.verts[0].co
850 elif parameters["rotateCenter"] == 'V2':
851 rotatedVerts = self.rotateArcAroundSpinAxis(
852 bm, mesh, spinnedVerts, parameters, edge.verts[1].co
855 offsetVerts = self.offsetArcPerpendicular(
856 bm, mesh, rotatedVerts, edge, parameters
858 offsetVerts2 = self.offsetArcParallel(
859 bm, mesh, offsetVerts, edge, parameters
861 ellipticVerts = self.makeElliptic(
862 bm, mesh, offsetVerts2, parameters
864 self.rotateArcAroundEdge(bm, mesh, ellipticVerts, parameters)
866 if parameters["connectArcWithEdge"]:
867 self.connectArcTogetherWithEdge(
868 edge, offsetVerts2, bm, mesh, parameters
870 return offsetVerts2
872 def connectArcTogetherWithEdge(self, edge, arcVertices, bm, mesh, parameters):
873 lastVert = len(arcVertices) - 1
874 if parameters["drawArcCenters"]:
875 lastVert = lastVert - 1 # center gets added as last vert of arc
876 edgeV1 = edge.verts[0].co
877 edgeV2 = edge.verts[1].co
878 arcV1 = arcVertices[0].co
879 arcV2 = arcVertices[lastVert].co
881 bmv1 = bm.verts.new(edgeV1)
882 bmv2 = bm.verts.new(arcV1)
884 bmv3 = bm.verts.new(edgeV2)
885 bmv4 = bm.verts.new(arcV2)
887 if parameters["connectArcWithEdgeFlip"] is False:
888 bme = bm.edges.new([bmv1, bmv2])
889 bme2 = bm.edges.new([bmv3, bmv4])
890 else:
891 bme = bm.edges.new([bmv1, bmv4])
892 bme2 = bm.edges.new([bmv3, bmv2])
893 self.sel.refreshMesh(bm, mesh)
895 def connectScaledEdgesWithBaseEdge(self, scaledEdges, baseEdges, bm, mesh):
896 for i in range(0, len(scaledEdges)):
897 scaledEdgeV1 = scaledEdges[i].verts[0].co
898 baseEdgeV1 = baseEdges[i].verts[0].co
899 scaledEdgeV2 = scaledEdges[i].verts[1].co
900 baseEdgeV2 = baseEdges[i].verts[1].co
902 bmv1 = bm.verts.new(baseEdgeV1)
903 bmv2 = bm.verts.new(scaledEdgeV1)
904 bme = bm.edges.new([bmv1, bmv2])
906 bmv3 = bm.verts.new(scaledEdgeV2)
907 bmv4 = bm.verts.new(baseEdgeV2)
908 bme = bm.edges.new([bmv3, bmv4])
909 self.sel.refreshMesh(bm, mesh)
911 def connectArcsTogether(self, arcs, bm, mesh, parameters):
912 for i in range(0, len(arcs) - 1):
913 # in case on XZ or YZ there are no arcs drawn
914 if arcs[i] is None or arcs[i + 1] is None:
915 return
917 lastVert = len(arcs[i]) - 1
918 if parameters["drawArcCenters"]:
919 lastVert = lastVert - 1 # center gets added as last vert of arc
920 # take last vert of arc i and first vert of arc i+1
922 V1 = arcs[i][lastVert].co
923 V2 = arcs[i + 1][0].co
925 if parameters["connectArcsFlip"]:
926 V1 = arcs[i][0].co
927 V2 = arcs[i + 1][lastVert].co
929 bmv1 = bm.verts.new(V1)
930 bmv2 = bm.verts.new(V2)
931 bme = bm.edges.new([bmv1, bmv2])
933 # connect last arc and first one
934 lastArcId = len(arcs) - 1
935 lastVertIdOfLastArc = len(arcs[lastArcId]) - 1
936 if parameters["drawArcCenters"]:
937 # center gets added as last vert of arc
938 lastVertIdOfLastArc = lastVertIdOfLastArc - 1
940 V1 = arcs[lastArcId][lastVertIdOfLastArc].co
941 V2 = arcs[0][0].co
942 if parameters["connectArcsFlip"]:
943 V1 = arcs[lastArcId][0].co
944 V2 = arcs[0][lastVertIdOfLastArc].co
946 bmv1 = bm.verts.new(V1)
947 bmv2 = bm.verts.new(V2)
948 bme = bm.edges.new([bmv1, bmv2])
950 self.sel.refreshMesh(bm, mesh)
952 def offsetArcPerpendicular(self, bm, mesh, Verts, edge, parameters):
953 perpendicularVector = self.getEdgePerpendicularVector(edge, parameters["plane"])
954 offset = parameters["offset"]
955 translation = offset * perpendicularVector
957 try:
958 bmesh.ops.translate(bm, verts=Verts, vec=translation)
959 except ValueError:
960 print("[Edge Roundifier]: Perpendicular translate value error - "
961 "multiple vertices in list - try unchecking 'Centers'")
963 indexes = [v.index for v in Verts]
964 self.sel.refreshMesh(bm, mesh)
965 offsetVertices = [bm.verts[i] for i in indexes]
966 return offsetVertices
968 def offsetArcParallel(self, bm, mesh, Verts, edge, parameters):
969 edgeVector = self.getNormalizedEdgeVector(edge)
970 offset = parameters["offset2"]
971 translation = offset * edgeVector
973 try:
974 bmesh.ops.translate(bm, verts=Verts, vec=translation)
975 except ValueError:
976 print("[Edge Roundifier]: Parallel translate value error - "
977 "multiple vertices in list - try unchecking 'Centers'")
979 indexes = [v.index for v in Verts]
980 self.sel.refreshMesh(bm, mesh)
981 offsetVertices = [bm.verts[i] for i in indexes]
982 return offsetVertices
984 def skipThisEdge(self, V1, V2, plane):
985 # Check If It is possible to spin selected verts on this plane if not exit roundifier
986 if(plane == XY):
987 if (V1[0] == V2[0] and V1[1] == V2[1]):
988 return True
989 elif(plane == YZ):
990 if (V1[1] == V2[1] and V1[2] == V2[2]):
991 return True
992 elif(plane == XZ):
993 if (V1[0] == V2[0] and V1[2] == V2[2]):
994 return True
995 return False
997 def calculateRoundifyParams(self, edge, parameters, bm, mesh):
998 # Because all data from mesh is in local coordinates
999 # and spin operator works on global coordinates
1000 # We first need to translate all input data by vector equal
1001 # to origin position and then perform calculations
1002 # At least that is my understanding :) <komi3D>
1004 # V1 V2 stores Local Coordinates
1005 V1, V2, edgeVector, edgeLength, edgeCenter = self.getEdgeInfo(edge)
1007 debugPrintNew(d_Plane, "PLANE: " + parameters["plane"])
1008 lineAB = self.calc.getLineCoefficientsPerpendicularToVectorInPoint(
1009 edgeCenter, edgeVector,
1010 parameters["plane"]
1012 circleMidPoint = V1
1013 circleMidPointOnPlane = self.calc.getCircleMidPointOnPlane(
1014 V1, parameters["plane"]
1016 radius = parameters["radius"]
1018 angle = 0
1019 if (parameters["entryMode"] == 'Angle'):
1020 if (parameters["angleEnum"] != 'Other'):
1021 radius, angle = self.CalculateRadiusAndAngleForAnglePresets(
1022 parameters["angleEnum"], radius,
1023 angle, edgeLength
1025 else:
1026 radius, angle = self.CalculateRadiusAndAngle(edgeLength)
1027 debugPrintNew(d_Radius_Angle, "RADIUS = " + str(radius) + " ANGLE = " + str(angle))
1028 roots = None
1029 if angle != pi: # mode other than 180
1030 if lineAB is None:
1031 roots = self.calc.getLineCircleIntersectionsWhenXPerpendicular(
1032 edgeCenter, circleMidPointOnPlane,
1033 radius, parameters["plane"]
1035 else:
1036 roots = self.calc.getLineCircleIntersections(
1037 lineAB, circleMidPointOnPlane, radius
1040 if roots is None:
1041 debugPrintNew(True,
1042 "[Edge Roundifier]: No centers were found. Change radius to higher value")
1043 return None
1044 roots = self.addMissingCoordinate(roots, V1, parameters["plane"]) # adds X, Y or Z coordinate
1045 else:
1046 roots = [edgeCenter, edgeCenter]
1047 debugPrintNew(d_Roots, "roots=" + str(roots))
1049 refObjectLocation = None
1050 objectLocation = bpy.context.active_object.location # Origin Location
1052 if parameters["refObject"] == "ORG":
1053 refObjectLocation = [0, 0, 0]
1054 elif parameters["refObject"] == "CUR":
1055 refObjectLocation = bpy.context.scene.cursor.location - objectLocation
1056 else:
1057 refObjectLocation = self.calc.getEdgeReference(edge, edgeCenter, parameters["plane"])
1059 debugPrintNew(d_RefObject, parameters["refObject"], refObjectLocation)
1060 chosenSpinCenter, otherSpinCenter = self.getSpinCenterClosestToRefCenter(
1061 refObjectLocation, roots
1064 if (parameters["entryMode"] == "Radius"):
1065 halfAngle = self.calc.getAngle(edgeCenter, chosenSpinCenter, circleMidPoint)
1066 angle = 2 * halfAngle[0] # in radians
1067 self.a = degrees(angle) # in degrees
1069 spinAxis = self.getSpinAxis(parameters["plane"])
1070 steps = parameters["segments"]
1071 angle = -angle # rotate clockwise by default
1073 return [chosenSpinCenter, otherSpinCenter, spinAxis, angle, steps, refObjectLocation]
1075 def drawSpin(self, edge, edgeCenter, roundifyParams, parameters, bm, mesh):
1076 [chosenSpinCenter, otherSpinCenter, spinAxis, angle, steps, refObjectLocation] = roundifyParams
1078 v0org, v1org = (edge.verts[0], edge.verts[1])
1080 if parameters["flip"]:
1081 angle = -angle
1082 spinCenterTemp = chosenSpinCenter
1083 chosenSpinCenter = otherSpinCenter
1084 otherSpinCenter = spinCenterTemp
1086 if(parameters["invertAngle"]):
1087 if angle < 0:
1088 angle = two_pi + angle
1089 elif angle > 0:
1090 angle = -two_pi + angle
1091 else:
1092 angle = two_pi
1094 if(parameters["fullCircles"]):
1095 angle = two_pi
1097 v0 = bm.verts.new(v0org.co)
1099 result = bmesh.ops.spin(
1100 bm, geom=[v0], cent=chosenSpinCenter, axis=spinAxis,
1101 angle=angle, steps=steps, use_duplicate=False
1104 # it seems there is something wrong with last index of this spin
1105 # I need to calculate the last index manually here
1106 vertsLength = len(bm.verts)
1107 bm.verts.ensure_lookup_table()
1108 lastVertIndex = bm.verts[vertsLength - 1].index
1109 lastSpinVertIndices = self.getLastSpinVertIndices(steps, lastVertIndex)
1111 self.sel.refreshMesh(bm, mesh)
1113 alternativeLastSpinVertIndices = []
1114 bothSpinVertices = []
1115 spinVertices = []
1116 alternate = False
1118 if ((angle == pi or angle == -pi) and not parameters["bothSides"]):
1120 midVertexIndex = lastVertIndex - round(steps / 2)
1121 bm.verts.ensure_lookup_table()
1122 midVert = bm.verts[midVertexIndex].co
1124 midVertexDistance = (Vector(refObjectLocation) - Vector(midVert)).length
1125 midEdgeDistance = (Vector(refObjectLocation) - Vector(edgeCenter)).length
1127 if ((parameters["invertAngle"]) or (parameters["flip"])):
1128 if (midVertexDistance > midEdgeDistance):
1129 alternativeLastSpinVertIndices = self.alternateSpin(
1130 bm, mesh, angle, chosenSpinCenter,
1131 spinAxis, steps, v0, v1org, lastSpinVertIndices
1133 else:
1134 if (midVertexDistance < midEdgeDistance):
1135 alternativeLastSpinVertIndices = self.alternateSpin(
1136 bm, mesh, angle, chosenSpinCenter,
1137 spinAxis, steps, v0, v1org, lastSpinVertIndices
1139 elif (angle != two_pi): # to allow full circles
1140 if (result['geom_last'][0].co - v1org.co).length > SPIN_END_THRESHOLD:
1141 alternativeLastSpinVertIndices = self.alternateSpin(
1142 bm, mesh, angle, chosenSpinCenter,
1143 spinAxis, steps, v0, v1org, lastSpinVertIndices
1145 alternate = True
1147 self.sel.refreshMesh(bm, mesh)
1148 if alternativeLastSpinVertIndices != []:
1149 lastSpinVertIndices = alternativeLastSpinVertIndices
1151 if lastSpinVertIndices.stop <= len(bm.verts): # make sure arc was added to bmesh
1152 spinVertices = [bm.verts[i] for i in lastSpinVertIndices]
1153 if alternativeLastSpinVertIndices != []:
1154 spinVertices = spinVertices + [v0]
1155 else:
1156 spinVertices = [v0] + spinVertices
1158 if (parameters["bothSides"]):
1159 # do some more testing here!!!
1160 if (angle == pi or angle == -pi):
1161 alternativeLastSpinVertIndices = self.alternateSpinNoDelete(
1162 bm, mesh, -angle, chosenSpinCenter,
1163 spinAxis, steps, v0, v1org, []
1165 elif alternate:
1166 alternativeLastSpinVertIndices = self.alternateSpinNoDelete(
1167 bm, mesh, angle, otherSpinCenter,
1168 spinAxis, steps, v0, v1org, []
1170 elif not alternate:
1171 alternativeLastSpinVertIndices = self.alternateSpinNoDelete(
1172 bm, mesh, -angle, otherSpinCenter,
1173 spinAxis, steps, v0, v1org, []
1175 bothSpinVertices = [bm.verts[i] for i in lastSpinVertIndices]
1176 alternativeSpinVertices = [bm.verts[i] for i in alternativeLastSpinVertIndices]
1177 bothSpinVertices = [v0] + bothSpinVertices + alternativeSpinVertices
1178 spinVertices = bothSpinVertices
1180 if (parameters["fullCircles"]):
1181 v1 = bm.verts.new(v1org.co)
1182 spinVertices = spinVertices + [v1]
1184 if (parameters['drawArcCenters']):
1185 centerVert = bm.verts.new(chosenSpinCenter)
1186 spinVertices.append(centerVert)
1188 return spinVertices, [chosenSpinCenter, otherSpinCenter, spinAxis, angle, steps, refObjectLocation]
1190 def deleteSpinVertices(self, bm, mesh, lastSpinVertIndices):
1191 verticesForDeletion = []
1192 bm.verts.ensure_lookup_table()
1193 for i in lastSpinVertIndices:
1194 vi = bm.verts[i]
1195 vi.select = True
1196 debugPrintNew(True, str(i) + ") " + str(vi))
1197 verticesForDeletion.append(vi)
1199 bmesh.ops.delete(bm, geom=verticesForDeletion, context = 'VERTS')
1200 bmesh.update_edit_mesh(mesh, loop_triangles=True)
1201 bpy.ops.object.mode_set(mode='OBJECT')
1202 bpy.ops.object.mode_set(mode='EDIT')
1204 def alternateSpinNoDelete(self, bm, mesh, angle, chosenSpinCenter,
1205 spinAxis, steps, v0, v1org, lastSpinVertIndices):
1206 v0prim = v0
1208 result2 = bmesh.ops.spin(bm, geom=[v0prim], cent=chosenSpinCenter, axis=spinAxis,
1209 angle=angle, steps=steps, use_duplicate=False)
1210 vertsLength = len(bm.verts)
1211 bm.verts.ensure_lookup_table()
1212 lastVertIndex2 = bm.verts[vertsLength - 1].index
1214 lastSpinVertIndices2 = self.getLastSpinVertIndices(steps, lastVertIndex2)
1215 return lastSpinVertIndices2
1217 def alternateSpin(self, bm, mesh, angle, chosenSpinCenter,
1218 spinAxis, steps, v0, v1org, lastSpinVertIndices):
1220 self.deleteSpinVertices(bm, mesh, lastSpinVertIndices)
1221 v0prim = v0
1223 result2 = bmesh.ops.spin(
1224 bm, geom=[v0prim], cent=chosenSpinCenter, axis=spinAxis,
1225 angle=-angle, steps=steps, use_duplicate=False
1227 # it seems there is something wrong with last index of this spin
1228 # I need to calculate the last index manually here
1229 vertsLength = len(bm.verts)
1230 bm.verts.ensure_lookup_table()
1231 lastVertIndex2 = bm.verts[vertsLength - 1].index
1233 lastSpinVertIndices2 = self.getLastSpinVertIndices(steps, lastVertIndex2)
1234 # second spin also does not hit the v1org
1235 if (result2['geom_last'][0].co - v1org.co).length > SPIN_END_THRESHOLD:
1237 self.deleteSpinVertices(bm, mesh, lastSpinVertIndices2)
1238 self.deleteSpinVertices(bm, mesh, range(v0.index, v0.index + 1))
1239 return []
1240 else:
1241 return lastSpinVertIndices2
1243 def getLastSpinVertIndices(self, steps, lastVertIndex):
1244 arcfirstVertexIndex = lastVertIndex - steps + 1
1245 lastSpinVertIndices = range(arcfirstVertexIndex, lastVertIndex + 1)
1246 return lastSpinVertIndices
1248 def rotateArcAroundSpinAxis(self, bm, mesh, vertices, parameters, edgeCenter):
1249 axisAngle = parameters["axisAngle"]
1250 plane = parameters["plane"]
1251 # compensate rotation center
1252 objectLocation = bpy.context.active_object.location
1253 center = objectLocation + edgeCenter
1255 rot = Euler((0.0, 0.0, radians(axisAngle)), 'XYZ').to_matrix()
1256 if plane == YZ:
1257 rot = Euler((radians(axisAngle), 0.0, 0.0), 'XYZ').to_matrix()
1258 if plane == XZ:
1259 rot = Euler((0.0, radians(axisAngle), 0.0), 'XYZ').to_matrix()
1261 indexes = [v.index for v in vertices]
1263 bmesh.ops.rotate(
1265 cent=center,
1266 matrix=rot,
1267 verts=vertices,
1268 space=bpy.context.edit_object.matrix_world
1270 self.sel.refreshMesh(bm, mesh)
1271 bm.verts.ensure_lookup_table()
1272 rotatedVertices = [bm.verts[i] for i in indexes]
1274 return rotatedVertices
1276 def CalculateRadiusAndAngle(self, edgeLength):
1277 degAngle = self.a
1278 angle = radians(degAngle)
1279 self.r = radius = edgeLength / (2 * sin(angle / 2))
1280 return radius, angle
1282 def CalculateRadiusAndAngleForAnglePresets(self, angleEnum, initR, initA, edgeLength):
1283 radius = initR
1284 angle = initA
1285 try:
1286 # Note - define an integer string in the angleEnum
1287 angle_convert = int(angleEnum)
1288 self.a = angle_convert
1289 except:
1290 self.a = 180 # fallback
1291 debugPrintNew(True,
1292 "CalculateRadiusAndAngleForAnglePresets problem with int conversion")
1294 return self.CalculateRadiusAndAngle(edgeLength)
1296 def getSpinCenterClosestToRefCenter(self, objLocation, roots):
1297 root0Distance = (Vector(objLocation) - Vector(roots[0])).length
1298 root1Distance = (Vector(objLocation) - Vector(roots[1])).length
1300 chosenId = 0
1301 rejectedId = 1
1302 if (root0Distance > root1Distance):
1303 chosenId = 1
1304 rejectedId = 0
1305 return roots[chosenId], roots[rejectedId]
1307 def addMissingCoordinate(self, roots, startVertex, plane):
1308 if roots is not None:
1309 a, b = roots[0]
1310 c, d = roots[1]
1311 if plane == XY:
1312 roots[0] = Vector((a, b, startVertex[2]))
1313 roots[1] = Vector((c, d, startVertex[2]))
1314 if plane == YZ:
1315 roots[0] = Vector((startVertex[0], a, b))
1316 roots[1] = Vector((startVertex[0], c, d))
1317 if plane == XZ:
1318 roots[0] = Vector((a, startVertex[1], b))
1319 roots[1] = Vector((c, startVertex[1], d))
1320 return roots
1322 def selectEdgesAfterRoundifier(self, context, edges):
1323 bpy.ops.object.mode_set(mode='OBJECT')
1324 bpy.ops.object.mode_set(mode='EDIT')
1325 mesh = context.view_layer.objects.active.data
1326 bmnew = bmesh.new()
1327 bmnew.from_mesh(mesh)
1329 self.deselectEdges(bmnew)
1330 for selectedEdge in edges:
1331 for e in bmnew.edges:
1332 if (e.verts[0].co - selectedEdge.verts[0].co).length <= self.threshold \
1333 and (e.verts[1].co - selectedEdge.verts[1].co).length <= self.threshold:
1334 e.select_set(True)
1336 bpy.ops.object.mode_set(mode='OBJECT')
1337 bmnew.to_mesh(mesh)
1338 bmnew.free()
1339 bpy.ops.object.mode_set(mode='EDIT')
1341 def deselectEdges(self, bm):
1342 for edge in bm.edges:
1343 edge.select_set(False)
1345 def getSpinAxis(self, plane):
1346 axis = (0, 0, 1)
1347 if plane == YZ:
1348 axis = (1, 0, 0)
1349 if plane == XZ:
1350 axis = (0, 1, 0)
1351 return axis
1354 @classmethod
1355 def poll(cls, context):
1356 return (context.view_layer.objects.active.type == 'MESH') and (context.view_layer.objects.active.mode == 'EDIT')
1358 def draw_item(self, context):
1359 self.layout.operator_context = 'INVOKE_DEFAULT'
1360 self.layout.operator('mesh.edge_roundifier')
1363 classes = (
1364 EdgeRoundifier,
1367 reg_cls, unreg_cls = bpy.utils.register_classes_factory(classes)
1370 def register():
1371 reg_cls()
1372 bpy.types.VIEW3D_MT_edit_mesh_edges.append(draw_item)
1375 def unregister():
1376 unreg_cls()
1377 bpy.types.VIEW3D_MT_edit_mesh_edges.remove(draw_item)
1379 if __name__ == "__main__":
1380 register()