Update for 2.8
[blender-addons.git] / mesh_extra_tools / mesh_edge_roundifier.py
blobde3eb2bf39fe668e3586d30ff8049f3bc8be4970
1 # ##### BEGIN GPL LICENSE BLOCK #####
3 # This program is free software; you can redistribute it and/or
4 # modify it under the terms of the GNU General Public License
5 # as published by the Free Software Foundation; either version 2
6 # of the License, or (at your option) any later version.
8 # This program is distributed in the hope that it will be useful,
9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # GNU General Public License for more details.
13 # You should have received a copy of the GNU General Public License
14 # along with this program; if not, write to the Free Software Foundation,
15 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 # ##### END GPL LICENSE BLOCK #####
19 bl_info = {
20 "name": "Edge Roundifier",
21 "category": "Mesh",
22 "author": "Piotr Komisarczyk (komi3D), PKHG",
23 "version": (1, 0, 1),
24 "blender": (2, 7, 3),
25 "location": "SPACE > Edge Roundifier or CTRL-E > "
26 "Edge Roundifier or Tools > Addons > Edge Roundifier",
27 "description": "Mesh editing script allowing edge rounding",
28 "wiki_url": "",
29 "category": "Mesh"
32 import bpy
33 import bmesh
34 from bpy.types import Operator
35 from bpy.props import (
36 BoolProperty,
37 FloatProperty,
38 EnumProperty,
39 IntProperty,
41 from math import (
42 sqrt, acos, pi,
43 radians, degrees, sin,
45 from mathutils import (
46 Vector, Euler,
47 Quaternion,
50 # CONSTANTS
51 two_pi = 2 * pi
52 XY = "XY"
53 XZ = "XZ"
54 YZ = "YZ"
55 SPIN_END_THRESHOLD = 0.001
56 LINE_TOLERANCE = 0.0001
57 d_XABS_YABS = False
58 d_Edge_Info = False
59 d_Plane = False
60 d_Radius_Angle = False
61 d_Roots = False
62 d_RefObject = False
63 d_LineAB = False
64 d_Selected_edges = False
65 d_Rotate_Around_Spin_Center = False
67 # Enable debug prints
68 DEBUG = False
71 # for debugging PKHG #
72 def debugPrintNew(debugs, *text):
73 if DEBUG and debugs:
74 tmp = [el for el in text]
75 for row in tmp:
76 print(row)
79 # Geometry and math calcualtion methods #
81 class CalculationHelper:
83 def __init__(self):
84 """
85 Constructor
86 """
87 def getLineCoefficientsPerpendicularToVectorInPoint(self, point, vector, plane):
88 x, y, z = point
89 xVector, yVector, zVector = vector
90 destinationPoint = (x + yVector, y - xVector, z)
91 if plane == 'YZ':
92 destinationPoint = (x, y + zVector, z - yVector)
93 if plane == 'XZ':
94 destinationPoint = (x + zVector, y, z - xVector)
95 return self.getCoefficientsForLineThrough2Points(point, destinationPoint, plane)
97 def getQuadraticRoots(self, coef):
98 if len(coef) != 3:
99 return None # Replaced NaN with None
100 else:
101 a, b, c = coef
102 delta = b ** 2 - 4 * a * c
103 if delta == 0:
104 x = -b / (2 * a)
105 return (x, x)
106 elif delta < 0:
107 return None
108 else:
109 x1 = (-b - sqrt(delta)) / (2 * a)
110 x2 = (-b + sqrt(delta)) / (2 * a)
111 return (x1, x2)
113 def getCoefficientsForLineThrough2Points(self, point1, point2, plane):
114 x1, y1, z1 = point1
115 x2, y2, z2 = point2
117 # mapping x1,x2, y1,y2 to proper values based on plane
118 if plane == YZ:
119 x1 = y1
120 x2 = y2
121 y1 = z1
122 y2 = z2
123 if plane == XZ:
124 y1 = z1
125 y2 = z2
127 # Further calculations the same as for XY plane
128 xabs = abs(x2 - x1)
129 yabs = abs(y2 - y1)
130 debugPrintNew(d_XABS_YABS, "XABS = " + str(xabs) + " YABS = " + str(yabs))
132 if xabs <= LINE_TOLERANCE:
133 return None # this means line x = edgeCenterX
134 if yabs <= LINE_TOLERANCE:
135 A = 0
136 B = y1
137 return A, B
138 A = (y2 - y1) / (x2 - x1)
139 B = y1 - (A * x1)
140 return (A, B)
142 def getLineCircleIntersections(self, lineAB, circleMidPoint, radius):
143 # (x - a)**2 + (y - b)**2 = r**2 - circle equation
144 # y = A*x + B - line equation
145 # f * x**2 + g * x + h = 0 - quadratic equation
146 A, B = lineAB
147 a, b = circleMidPoint
148 f = 1 + (A ** 2)
149 g = -2 * a + 2 * A * B - 2 * A * b
150 h = (B ** 2) - 2 * b * B - (radius ** 2) + (a ** 2) + (b ** 2)
151 coef = [f, g, h]
152 roots = self.getQuadraticRoots(coef)
153 if roots is not None:
154 x1 = roots[0]
155 x2 = roots[1]
156 point1 = [x1, A * x1 + B]
157 point2 = [x2, A * x2 + B]
158 return [point1, point2]
159 else:
160 return None
162 def getLineCircleIntersectionsWhenXPerpendicular(self, edgeCenter,
163 circleMidPoint, radius, plane):
164 # (x - a)**2 + (y - b)**2 = r**2 - circle equation
165 # x = xValue - line equation
166 # f * x**2 + g * x + h = 0 - quadratic equation
167 xValue = edgeCenter[0]
168 if plane == YZ:
169 xValue = edgeCenter[1]
170 if plane == XZ:
171 xValue = edgeCenter[0]
173 a, b = circleMidPoint
174 f = 1
175 g = -2 * b
176 h = (a ** 2) + (b ** 2) + (xValue ** 2) - 2 * a * xValue - (radius ** 2)
177 coef = [f, g, h]
178 roots = self.getQuadraticRoots(coef)
179 if roots is not None:
180 y1 = roots[0]
181 y2 = roots[1]
182 point1 = [xValue, y1]
183 point2 = [xValue, y2]
184 return [point1, point2]
185 else:
186 return None
188 # point1 is the point near 90 deg angle
189 def getAngle(self, point1, point2, point3):
190 distance1 = (Vector(point1) - Vector(point2)).length
191 distance2 = (Vector(point2) - Vector(point3)).length
192 cos = distance1 / distance2
194 if abs(cos) > 1: # prevents Domain Error
195 cos = round(cos)
197 alpha = acos(cos)
198 return (alpha, degrees(alpha))
200 # get two of three coordinates used for further calculation of spin center
201 # PKHG>nice if rescriction to these 3 types or planes is to be done
202 # komi3D> from 0.0.2 there is a restriction. In future I would like Edge
203 # komi3D> Roundifier to work on Normal and View coordinate systems
204 def getCircleMidPointOnPlane(self, V1, plane):
205 X = V1[0]
206 Y = V1[1]
207 if plane == 'XZ':
208 X = V1[0]
209 Y = V1[2]
210 elif plane == 'YZ':
211 X = V1[1]
212 Y = V1[2]
213 return [X, Y]
215 def getEdgeReference(self, edge, edgeCenter, plane):
216 vert1 = edge.verts[1].co
217 V = vert1 - edgeCenter
218 orthoVector = Vector((V[1], -V[0], V[2]))
219 if plane == 'XZ':
220 orthoVector = Vector((V[2], V[1], -V[0]))
221 elif plane == 'YZ':
222 orthoVector = Vector((V[0], V[2], -V[1]))
223 refPoint = edgeCenter + orthoVector
224 return refPoint
227 # Selection Methods #
229 class SelectionHelper:
231 def selectVertexInMesh(self, mesh, vertex):
232 bpy.ops.object.mode_set(mode="OBJECT")
233 for v in mesh.vertices:
234 if v.co == vertex:
235 v.select = True
236 break
238 bpy.ops.object.mode_set(mode="EDIT")
240 def getSelectedVertex(self, mesh):
241 bpy.ops.object.mode_set(mode="OBJECT")
242 for v in mesh.vertices:
243 if v.select is True:
244 bpy.ops.object.mode_set(mode="EDIT")
245 return v
247 bpy.ops.object.mode_set(mode="EDIT")
248 return None
250 def refreshMesh(self, bm, mesh):
251 bpy.ops.object.mode_set(mode='OBJECT')
252 bm.to_mesh(mesh)
253 bpy.ops.object.mode_set(mode='EDIT')
256 # Operator
258 class EdgeRoundifier(Operator):
259 bl_idname = "mesh.edge_roundifier"
260 bl_label = "Edge Roundifier"
261 bl_description = "Mesh modeling tool for building arcs on selected Edges"
262 bl_options = {'REGISTER', 'UNDO', 'PRESET'}
264 threshold = 0.0005
265 obj = None
267 edgeScaleFactor = FloatProperty(
268 name="",
269 description="Set the Factor of scaling",
270 default=1.0,
271 min=0.00001, max=100000.0,
272 step=0.5,
273 precision=5
275 r = FloatProperty(
276 name="",
277 description="User Defined arc steepness by a Radius\n"
278 "Enabled only if Entry mode is set to Radius\n",
279 default=1,
280 min=0.00001, max=1000.0,
281 step=0.1,
282 precision=3
284 a = FloatProperty(
285 name="",
286 description="User defined arc steepness calculated from an Angle\n"
287 "Enabled only if Entry mode is set to Angle and\n"
288 "Angle presets is set Other",
289 default=180.0,
290 min=0.1, max=180.0,
291 step=0.5,
292 precision=1
294 n = IntProperty(
295 name="",
296 description="Arc subdivision level",
297 default=4,
298 min=1, max=100,
299 step=1
301 flip = BoolProperty(
302 name="Flip",
303 description="If True, flip the side of the selected edges where the arcs are drawn",
304 default=False
306 invertAngle = BoolProperty(
307 name="Invert",
308 description="If True, uses an inverted angle to draw the arc (360 degrees - angle)",
309 default=False
311 fullCircles = BoolProperty(
312 name="Circles",
313 description="If True, uses an angle of 360 degrees to draw the arcs",
314 default=False
316 bothSides = BoolProperty(
317 name="Both sides",
318 description="If True, draw arcs on both sides of the selected edges",
319 default=False
321 drawArcCenters = BoolProperty(
322 name="Centers",
323 description="If True, draws a vertex for each spin center",
324 default=False
326 removeEdges = BoolProperty(
327 name="Edges",
328 description="If True removes the Original selected edges",
329 default=False
331 removeScaledEdges = BoolProperty(
332 name="Scaled edges",
333 description="If True removes the Scaled edges (not part of the arcs)",
334 default=False
336 connectArcWithEdge = BoolProperty(
337 name="Arc - Edge",
338 description="Connect Arcs to Edges",
339 default=False
341 connectArcs = BoolProperty(
342 name="Arcs",
343 description="Connect subsequent Arcs",
344 default=False
346 connectScaledAndBase = BoolProperty(
347 name="Scaled - Base Edge",
348 description="Connect Scaled to Base Edge",
349 default=False
351 connectArcsFlip = BoolProperty(
352 name="Flip Arcs",
353 description="Flip the connection of subsequent Arcs",
354 default=False
356 connectArcWithEdgeFlip = BoolProperty(
357 name="Flip Arc - Edge",
358 description="Flip the connection of the Arcs to Edges",
359 default=False
361 axisAngle = FloatProperty(
362 name="",
363 description="Rotate Arc around the perpendicular axis",
364 default=0.0,
365 min=-180.0, max=180.0,
366 step=0.5,
367 precision=1
369 edgeAngle = FloatProperty(
370 name="",
371 description="Rotate Arc around the Edge (Edge acts like as the axis)",
372 default=0.0,
373 min=-180.0, max=180.0,
374 step=0.5,
375 precision=1
377 offset = FloatProperty(
378 name="",
379 description="Offset Arc perpendicular the Edge",
380 default=0.0,
381 min=-1000000.0, max=1000000.0,
382 step=0.1,
383 precision=5
385 offset2 = FloatProperty(
386 name="",
387 description="Offset Arc in parallel to the Edge",
388 default=0.0,
389 min=-1000000.0, max=1000000.0,
390 step=0.1,
391 precision=5
393 ellipticFactor = FloatProperty(
394 name="",
395 description="Make Arc elliptic",
396 default=0.0,
397 min=-1000000.0, max=1000000.0,
398 step=0.1,
399 precision=5
401 workModeItems = [("Normal", "Normal", ""), ("Reset", "Reset", "")]
402 workMode = EnumProperty(
403 items=workModeItems,
404 name="",
405 default='Normal',
406 description="Normal work with the current given paramaters set by the user\n"
407 "Reset - changes back the parameters to their default values"
409 entryModeItems = [("Radius", "Radius", ""), ("Angle", "Angle", "")]
410 entryMode = EnumProperty(
411 items=entryModeItems,
412 name="",
413 default='Angle',
414 description="Entry mode switch between Angle and Radius\n"
415 "If Angle is selected, arc radius is calculated from it"
417 rotateCenterItems = [
418 ("Spin", "Spin", ""), ("V1", "V1", ""),
419 ("Edge", "Edge", ""), ("V2", "V2", "")
421 rotateCenter = EnumProperty(
422 items=rotateCenterItems,
423 name="",
424 default='Edge',
425 description="Rotate center for spin axis rotate"
427 arcModeItems = [("FullEdgeArc", "Full", "Full"), ('HalfEdgeArc', "Half", "Half")]
428 arcMode = EnumProperty(
429 items=arcModeItems,
430 name="",
431 default='FullEdgeArc',
432 description="Arc mode - switch between Full and Half arcs"
434 angleItems = [
435 ('Other', "Other", "User defined angle"), ('180', "180", "HemiCircle (2 sides)"),
436 ('120', "120", "TriangleCircle (3 sides)"), ('90', "90", "QuadCircle (4 sides)"),
437 ('72', "72", "PentagonCircle (5 sides)"), ('60', "60", "HexagonCircle (6 sides)"),
438 ('45', "45", "OctagonCircle (8 sides)"), ('30', "30", "DodecagonCircle (12 sides)")
440 angleEnum = EnumProperty(
441 items=angleItems,
442 name="",
443 default='180',
444 description="Presets prepare standard angles and calculate proper ray"
446 refItems = [('ORG', "Origin", "Use Origin Location"), ('CUR', "3D Cursor", "Use 3DCursor Location"),
447 ('EDG', "Edge", "Use Individual Edge Reference")]
448 referenceLocation = EnumProperty(
449 items=refItems,
450 name="",
451 default='ORG',
452 description="Reference location used to calculate initial centers of drawn arcs"
454 planeItems = [
455 (XY, "XY", "XY Plane (Z=0)"),
456 (YZ, "YZ", "YZ Plane (X=0)"),
457 (XZ, "XZ", "XZ Plane (Y=0)")
459 planeEnum = EnumProperty(
460 items=planeItems,
461 name="",
462 default='XY',
463 description="Plane used to calculate spin plane of drawn arcs"
465 edgeScaleCenterItems = [
466 ('V1', "V1", "v1 - First Edge's Vertex"),
467 ('CENTER', "Center", "Center of the Edge"),
468 ('V2', "V2", "v2 - Second Edge's Vertex")
470 edgeScaleCenterEnum = EnumProperty(
471 items=edgeScaleCenterItems,
472 name="Edge scale center",
473 default='CENTER',
474 description="Center used for scaling the initial edge"
477 calc = CalculationHelper()
478 sel = SelectionHelper()
480 @classmethod
481 def poll(cls, context):
482 obj = context.active_object
483 return (obj and obj.type == 'MESH' and
484 obj.mode == 'EDIT')
486 def prepareMesh(self, context):
487 bpy.ops.object.mode_set(mode='OBJECT')
488 bpy.ops.object.mode_set(mode='EDIT')
490 mesh = context.scene.objects.active.data
491 bm = bmesh.new()
492 bm.from_mesh(mesh)
494 edges = [ele for ele in bm.edges if ele.select]
495 return edges, mesh, bm
497 def prepareParameters(self):
498 parameters = {"a": "a"}
499 parameters["arcMode"] = self.arcMode
500 parameters["edgeScaleFactor"] = self.edgeScaleFactor
501 parameters["edgeScaleCenterEnum"] = self.edgeScaleCenterEnum
502 parameters["plane"] = self.planeEnum
503 parameters["radius"] = self.r
504 parameters["angle"] = self.a
505 parameters["segments"] = self.n
506 parameters["fullCircles"] = self.fullCircles
507 parameters["invertAngle"] = self.invertAngle
508 parameters["bothSides"] = self.bothSides
509 parameters["angleEnum"] = self.angleEnum
510 parameters["entryMode"] = self.entryMode
511 parameters["workMode"] = self.workMode
512 parameters["refObject"] = self.referenceLocation
513 parameters["flip"] = self.flip
514 parameters["drawArcCenters"] = self.drawArcCenters
515 parameters["removeEdges"] = self.removeEdges
516 parameters["removeScaledEdges"] = self.removeScaledEdges
517 parameters["connectArcWithEdge"] = self.connectArcWithEdge
518 parameters["connectScaledAndBase"] = self.connectScaledAndBase
519 parameters["connectArcs"] = self.connectArcs
520 parameters["connectArcsFlip"] = self.connectArcsFlip
521 parameters["connectArcWithEdgeFlip"] = self.connectArcWithEdgeFlip
522 parameters["axisAngle"] = self.axisAngle
523 parameters["edgeAngle"] = self.edgeAngle
524 parameters["offset"] = self.offset
525 parameters["offset2"] = self.offset2
526 parameters["ellipticFactor"] = self.ellipticFactor
527 parameters["rotateCenter"] = self.rotateCenter
528 return parameters
530 def draw(self, context):
531 layout = self.layout
532 box = layout.box()
533 uiPercentage = 0.333
535 self.addEnumParameterToUI(box, False, uiPercentage, 'Mode:', 'workMode')
536 self.addEnumParameterToUI(box, False, uiPercentage, 'Plane:', 'planeEnum')
537 self.addEnumParameterToUI(box, False, uiPercentage, 'Reference:', 'referenceLocation')
539 box = layout.box()
540 self.addEnumParameterToUI(box, False, uiPercentage, 'Scale base:', 'edgeScaleCenterEnum')
541 self.addParameterToUI(box, False, uiPercentage, 'Scale factor:', 'edgeScaleFactor')
543 box = layout.box()
544 self.addEnumParameterToUI(box, False, uiPercentage, 'Entry mode:', 'entryMode')
546 row = box.row(align=False)
547 row.prop(self, 'angleEnum', expand=True, text="Angle presets")
549 disable_a = bool(self.entryMode == 'Angle' and self.angleEnum == 'Other')
550 disable_r = bool(self.entryMode == 'Radius')
552 self.addParameterToUI(box, False, uiPercentage, 'Angle:', 'a', disable_a)
553 self.addParameterToUI(box, False, uiPercentage, 'Radius:', 'r', disable_r)
554 self.addParameterToUI(box, False, uiPercentage, 'Segments:', 'n')
556 box = layout.box()
557 self.addCheckboxToUI(box, True, 'Options:', 'flip', 'invertAngle')
558 self.addCheckboxToUI(box, True, '', 'bothSides', 'fullCircles')
559 self.addCheckboxToUI(box, True, '', 'drawArcCenters')
561 box = layout.box()
562 self.addCheckboxToUI(box, True, 'Remove:', 'removeEdges', 'removeScaledEdges')
564 box = layout.box()
565 self.addCheckboxToUI(box, True, 'Connect:', 'connectArcs', 'connectArcsFlip')
566 self.addCheckboxToUI(box, True, '', 'connectArcWithEdge', 'connectArcWithEdgeFlip')
567 self.addCheckboxToUI(box, True, '', 'connectScaledAndBase')
569 box = layout.box()
570 self.addParameterToUI(box, False, uiPercentage, 'Orhto offset:', 'offset')
571 self.addParameterToUI(box, False, uiPercentage, 'Parallel offset:', 'offset2')
573 box = layout.box()
574 self.addParameterToUI(box, False, uiPercentage, 'Edge rotate :', 'edgeAngle')
575 self.addEnumParameterToUI(box, False, uiPercentage, 'Axis rotate center:', 'rotateCenter')
576 self.addParameterToUI(box, False, uiPercentage, 'Axis rotate:', 'axisAngle')
578 box = layout.box()
579 self.addParameterToUI(box, False, uiPercentage, 'Elliptic factor:', 'ellipticFactor')
581 def addParameterToUI(self, layout, alignment, percent, label, properties, disable=True):
582 row = layout.row(align=alignment)
583 split = row.split(percentage=percent)
584 col = split.column()
586 col.label(label)
587 col2 = split.column()
588 row = col2.row(align=alignment)
589 row.enabled = disable
590 row.prop(self, properties)
592 def addCheckboxToUI(self, layout, alignment, label, property1, property2=None):
593 if label not in (""):
594 row = layout.row()
595 row.label(label)
596 row2 = layout.row(align=alignment)
597 if property2:
598 split = row2.split(percentage=0.5)
599 split.prop(self, property1, toggle=True)
600 split.prop(self, property2, toggle=True)
601 else:
602 row2.prop(self, property1, toggle=True)
603 layout.separator()
605 def addEnumParameterToUI(self, layout, alignment, percent, label, properties):
606 row = layout.row(align=alignment)
607 split = row.split(percentage=percent)
608 col = split.column()
610 col.label(label)
611 col2 = split.column()
612 row = col2.row(align=alignment)
613 row.prop(self, properties, expand=True, text="a")
615 def execute(self, context):
617 edges, mesh, bm = self.prepareMesh(context)
618 parameters = self.prepareParameters()
620 self.resetValues(parameters["workMode"])
622 self.obj = context.scene.objects.active
623 scaledEdges = self.scaleDuplicatedEdges(bm, edges, parameters)
625 if len(scaledEdges) > 0:
626 self.roundifyEdges(scaledEdges, parameters, bm, mesh)
628 if parameters["connectScaledAndBase"]:
629 self.connectScaledEdgesWithBaseEdge(scaledEdges, edges, bm, mesh)
631 self.sel.refreshMesh(bm, mesh)
632 self.selectEdgesAfterRoundifier(context, scaledEdges)
633 else:
634 debugPrintNew(True, "No edges selected!")
636 if parameters["removeEdges"]:
637 bmesh.ops.delete(bm, geom=edges, context=2)
639 if parameters["removeScaledEdges"] and self.edgeScaleFactor != 1.0:
640 bmesh.ops.delete(bm, geom=scaledEdges, context=2)
642 bpy.ops.object.mode_set(mode='OBJECT')
643 bm.to_mesh(mesh)
644 bpy.ops.object.mode_set(mode='EDIT')
645 bpy.ops.mesh.select_all(action='SELECT')
646 bpy.ops.mesh.remove_doubles()
648 bm.free()
650 return {'FINISHED'}
652 def resetValues(self, workMode):
653 if workMode == "Reset":
654 self.setAllParamsToDefaults()
656 def setAllParamsToDefaults(self):
657 try:
658 self.edgeScaleFactor = 1.0
659 self.r = 1
660 self.a = 180.0
661 self.n = 4
662 self.flip = False
663 self.invertAngle = False
664 self.fullCircles = False
665 self.bothSides = False
666 self.drawArcCenters = False
667 self.removeEdges = False
668 self.removeScaledEdges = False
670 self.connectArcWithEdge = False
671 self.connectArcs = False
672 self.connectScaledAndBase = False
673 self.connectArcsFlip = False
674 self.connectArcWithEdgeFlip = False
676 self.axisAngle = 0.0
677 self.edgeAngle = 0.0
678 self.offset = 0.0
679 self.offset2 = 0.0
680 self.ellipticFactor = 0.0
682 self.workMode = 'Normal'
683 self.entryMode = 'Angle'
684 self.angleEnum = '180'
685 self.referenceLocation = 'ORG'
686 self.planeEnum = 'XY'
687 self.edgeScaleCenterEnum = 'CENTER'
688 self.rotateCenter = 'Edge'
690 self.report({'INFO'}, "The parameters have been reset to default values")
691 except Exception as e:
692 self.report({'WARNING'}, "The parameters could not be reset")
693 debugPrintNew(True, "\n[setAllParamsToDefaults]\n parameter reset error\n" + e)
695 def scaleDuplicatedEdges(self, bm, edges, parameters):
696 scaleCenter = parameters["edgeScaleCenterEnum"]
697 factor = parameters["edgeScaleFactor"]
698 # this code is based on Zeffi's answer to my question
699 duplicateEdges = []
700 if factor == 1:
701 duplicateEdges = edges
702 else:
703 for e in edges:
704 v1 = e.verts[0].co
705 v2 = e.verts[1].co
706 origin = None
707 if scaleCenter == 'CENTER':
708 origin = (v1 + v2) * 0.5
709 elif scaleCenter == 'V1':
710 origin = v1
711 elif scaleCenter == 'V2':
712 origin = v2
714 bmv1 = bm.verts.new(((v1 - origin) * factor) + origin)
715 bmv2 = bm.verts.new(((v2 - origin) * factor) + origin)
716 bme = bm.edges.new([bmv1, bmv2])
717 duplicateEdges.append(bme)
718 return duplicateEdges
720 def roundifyEdges(self, edges, parameters, bm, mesh):
721 arcs = []
722 for e in edges:
723 arcVerts = self.roundify(e, parameters, bm, mesh)
724 arcs.append(arcVerts)
726 if parameters["connectArcs"]:
727 self.connectArcsTogether(arcs, bm, mesh, parameters)
729 def getNormalizedEdgeVector(self, edge):
730 V1 = edge.verts[0].co
731 V2 = edge.verts[1].co
732 edgeVector = V2 - V1
733 normEdge = edgeVector.normalized()
734 return normEdge
736 def getEdgePerpendicularVector(self, edge, plane):
737 normEdge = self.getNormalizedEdgeVector(edge)
739 edgePerpendicularVector = Vector((normEdge[1], -normEdge[0], 0))
740 if plane == YZ:
741 edgePerpendicularVector = Vector((0, normEdge[2], -normEdge[1]))
742 if plane == XZ:
743 edgePerpendicularVector = Vector((normEdge[2], 0, -normEdge[0]))
744 return edgePerpendicularVector
746 def getEdgeInfo(self, edge):
747 V1 = edge.verts[0].co
748 V2 = edge.verts[1].co
749 edgeVector = V2 - V1
750 edgeLength = edgeVector.length
751 edgeCenter = (V2 + V1) * 0.5
752 return V1, V2, edgeVector, edgeLength, edgeCenter
754 def roundify(self, edge, parameters, bm, mesh):
755 V1, V2, edgeVector, edgeLength, edgeCenter = self.getEdgeInfo(edge)
756 if self.skipThisEdge(V1, V2, parameters["plane"]):
757 return
759 roundifyParams = None
760 arcVerts = None
761 roundifyParams = self.calculateRoundifyParams(edge, parameters, bm, mesh)
762 if roundifyParams is None:
763 return
765 arcVerts = self.spinAndPostprocess(edge, parameters, bm, mesh, edgeCenter, roundifyParams)
766 return arcVerts
768 def spinAndPostprocess(self, edge, parameters, bm, mesh, edgeCenter, roundifyParams):
769 spinnedVerts, roundifyParamsUpdated = self.drawSpin(
770 edge, edgeCenter,
771 roundifyParams,
772 parameters, bm, mesh
774 postProcessedArcVerts = self.arcPostprocessing(
775 edge, parameters, bm, mesh,
776 roundifyParamsUpdated,
777 spinnedVerts, edgeCenter
779 return postProcessedArcVerts
781 def rotateArcAroundEdge(self, bm, mesh, arcVerts, parameters):
782 angle = parameters["edgeAngle"]
783 if angle != 0:
784 self.arc_rotator(arcVerts, angle, parameters)
786 # arc_rotator method was created by PKHG, I (komi3D) adjusted it to fit the rest
787 def arc_rotator(self, arcVerts, extra_rotation, parameters):
788 bpy.ops.object.mode_set(mode='OBJECT')
789 old_location = self.obj.location.copy()
790 bpy.ops.transform.translate(
791 value=-old_location, constraint_axis=(False, False, False),
792 constraint_orientation='GLOBAL', mirror=False, proportional='DISABLED',
793 proportional_edit_falloff='SMOOTH', proportional_size=1
795 bpy.ops.object.mode_set(mode='EDIT')
796 adjust_matrix = self.obj.matrix_parent_inverse
797 bm = bmesh.from_edit_mesh(self.obj.data)
798 lastVert = len(arcVerts) - 1
799 if parameters["drawArcCenters"]:
800 lastVert = lastVert - 1 # center gets added as last vert of arc
801 v0_old = adjust_matrix * arcVerts[0].co.copy()
803 # PKHG>INFO move if necessary v0 to origin such that the axis gos through origin and v1
804 if v0_old != Vector((0, 0, 0)):
805 for i, ele in enumerate(arcVerts):
806 arcVerts[i].co += - v0_old
808 axis = arcVerts[0].co - arcVerts[lastVert].co
809 a_mat = Quaternion(axis, radians(extra_rotation)).normalized().to_matrix()
811 for ele in arcVerts:
812 ele.co = a_mat * ele.co
814 # PKHG>INFO move back if needed
815 if v0_old != Vector((0, 0, 0)):
816 for i, ele in enumerate(arcVerts):
817 arcVerts[i].co += + v0_old
819 bpy.ops.object.mode_set(mode='OBJECT')
820 # PKHG>INFO move origin object back print("old location = " , old_location)
821 bpy.ops.transform.translate(
822 value=old_location, constraint_axis=(False, False, False),
823 constraint_orientation='GLOBAL', mirror=False, proportional='DISABLED',
824 proportional_edit_falloff='SMOOTH', proportional_size=1
826 bpy.ops.object.mode_set(mode='EDIT')
828 def makeElliptic(self, bm, mesh, arcVertices, parameters):
829 if parameters["ellipticFactor"] != 0: # if 0 then nothing has to be done
830 lastVert = len(arcVertices) - 1
831 if parameters["drawArcCenters"]:
832 lastVert = lastVert - 1 # center gets added as last vert of arc
833 v0co = arcVertices[0].co
834 v1co = arcVertices[lastVert].co
836 for vertex in arcVertices: # range(len(res_list)):
837 # PKHg>INFO compute the base on the edge of the height-vector
838 top = vertex.co # res_list[nr].co
839 t = 0
840 if v1co - v0co != 0:
841 t = (v1co - v0co).dot(top - v0co) / (v1co - v0co).length ** 2
842 h_bottom = v0co + t * (v1co - v0co)
843 height = (h_bottom - top)
844 vertex.co = top + parameters["ellipticFactor"] * height
846 return arcVertices
848 def arcPostprocessing(self, edge, parameters, bm, mesh, roundifyParams, spinnedVerts, edgeCenter):
849 [chosenSpinCenter, otherSpinCenter, spinAxis, angle, steps, refObjectLocation] = roundifyParams
850 rotatedVerts = []
851 if parameters["rotateCenter"] == 'Edge':
852 rotatedVerts = self.rotateArcAroundSpinAxis(
853 bm, mesh, spinnedVerts, parameters, edgeCenter
855 elif parameters["rotateCenter"] == 'Spin':
856 rotatedVerts = self.rotateArcAroundSpinAxis(
857 bm, mesh, spinnedVerts, parameters, chosenSpinCenter
859 elif parameters["rotateCenter"] == 'V1':
860 rotatedVerts = self.rotateArcAroundSpinAxis(
861 bm, mesh, spinnedVerts, parameters, edge.verts[0].co
863 elif parameters["rotateCenter"] == 'V2':
864 rotatedVerts = self.rotateArcAroundSpinAxis(
865 bm, mesh, spinnedVerts, parameters, edge.verts[1].co
868 offsetVerts = self.offsetArcPerpendicular(
869 bm, mesh, rotatedVerts, edge, parameters
871 offsetVerts2 = self.offsetArcParallel(
872 bm, mesh, offsetVerts, edge, parameters
874 ellipticVerts = self.makeElliptic(
875 bm, mesh, offsetVerts2, parameters
877 self.rotateArcAroundEdge(bm, mesh, ellipticVerts, parameters)
879 if parameters["connectArcWithEdge"]:
880 self.connectArcTogetherWithEdge(
881 edge, offsetVerts2, bm, mesh, parameters
883 return offsetVerts2
885 def connectArcTogetherWithEdge(self, edge, arcVertices, bm, mesh, parameters):
886 lastVert = len(arcVertices) - 1
887 if parameters["drawArcCenters"]:
888 lastVert = lastVert - 1 # center gets added as last vert of arc
889 edgeV1 = edge.verts[0].co
890 edgeV2 = edge.verts[1].co
891 arcV1 = arcVertices[0].co
892 arcV2 = arcVertices[lastVert].co
894 bmv1 = bm.verts.new(edgeV1)
895 bmv2 = bm.verts.new(arcV1)
897 bmv3 = bm.verts.new(edgeV2)
898 bmv4 = bm.verts.new(arcV2)
900 if parameters["connectArcWithEdgeFlip"] is False:
901 bme = bm.edges.new([bmv1, bmv2])
902 bme2 = bm.edges.new([bmv3, bmv4])
903 else:
904 bme = bm.edges.new([bmv1, bmv4])
905 bme2 = bm.edges.new([bmv3, bmv2])
906 self.sel.refreshMesh(bm, mesh)
908 def connectScaledEdgesWithBaseEdge(self, scaledEdges, baseEdges, bm, mesh):
909 for i in range(0, len(scaledEdges)):
910 scaledEdgeV1 = scaledEdges[i].verts[0].co
911 baseEdgeV1 = baseEdges[i].verts[0].co
912 scaledEdgeV2 = scaledEdges[i].verts[1].co
913 baseEdgeV2 = baseEdges[i].verts[1].co
915 bmv1 = bm.verts.new(baseEdgeV1)
916 bmv2 = bm.verts.new(scaledEdgeV1)
917 bme = bm.edges.new([bmv1, bmv2])
919 bmv3 = bm.verts.new(scaledEdgeV2)
920 bmv4 = bm.verts.new(baseEdgeV2)
921 bme = bm.edges.new([bmv3, bmv4])
922 self.sel.refreshMesh(bm, mesh)
924 def connectArcsTogether(self, arcs, bm, mesh, parameters):
925 for i in range(0, len(arcs) - 1):
926 # in case on XZ or YZ there are no arcs drawn
927 if arcs[i] is None or arcs[i + 1] is None:
928 return
930 lastVert = len(arcs[i]) - 1
931 if parameters["drawArcCenters"]:
932 lastVert = lastVert - 1 # center gets added as last vert of arc
933 # take last vert of arc i and first vert of arc i+1
935 V1 = arcs[i][lastVert].co
936 V2 = arcs[i + 1][0].co
938 if parameters["connectArcsFlip"]:
939 V1 = arcs[i][0].co
940 V2 = arcs[i + 1][lastVert].co
942 bmv1 = bm.verts.new(V1)
943 bmv2 = bm.verts.new(V2)
944 bme = bm.edges.new([bmv1, bmv2])
946 # connect last arc and first one
947 lastArcId = len(arcs) - 1
948 lastVertIdOfLastArc = len(arcs[lastArcId]) - 1
949 if parameters["drawArcCenters"]:
950 # center gets added as last vert of arc
951 lastVertIdOfLastArc = lastVertIdOfLastArc - 1
953 V1 = arcs[lastArcId][lastVertIdOfLastArc].co
954 V2 = arcs[0][0].co
955 if parameters["connectArcsFlip"]:
956 V1 = arcs[lastArcId][0].co
957 V2 = arcs[0][lastVertIdOfLastArc].co
959 bmv1 = bm.verts.new(V1)
960 bmv2 = bm.verts.new(V2)
961 bme = bm.edges.new([bmv1, bmv2])
963 self.sel.refreshMesh(bm, mesh)
965 def offsetArcPerpendicular(self, bm, mesh, Verts, edge, parameters):
966 perpendicularVector = self.getEdgePerpendicularVector(edge, parameters["plane"])
967 offset = parameters["offset"]
968 translation = offset * perpendicularVector
970 try:
971 bmesh.ops.translate(bm, verts=Verts, vec=translation)
972 except ValueError:
973 print("[Edge Roundifier]: Perpendicular translate value error - "
974 "multiple vertices in list - try unchecking 'Centers'")
976 indexes = [v.index for v in Verts]
977 self.sel.refreshMesh(bm, mesh)
978 offsetVertices = [bm.verts[i] for i in indexes]
979 return offsetVertices
981 def offsetArcParallel(self, bm, mesh, Verts, edge, parameters):
982 edgeVector = self.getNormalizedEdgeVector(edge)
983 offset = parameters["offset2"]
984 translation = offset * edgeVector
986 try:
987 bmesh.ops.translate(bm, verts=Verts, vec=translation)
988 except ValueError:
989 print("[Edge Roundifier]: Parallel translate value error - "
990 "multiple vertices in list - try unchecking 'Centers'")
992 indexes = [v.index for v in Verts]
993 self.sel.refreshMesh(bm, mesh)
994 offsetVertices = [bm.verts[i] for i in indexes]
995 return offsetVertices
997 def skipThisEdge(self, V1, V2, plane):
998 # Check If It is possible to spin selected verts on this plane if not exit roundifier
999 if(plane == XY):
1000 if (V1[0] == V2[0] and V1[1] == V2[1]):
1001 return True
1002 elif(plane == YZ):
1003 if (V1[1] == V2[1] and V1[2] == V2[2]):
1004 return True
1005 elif(plane == XZ):
1006 if (V1[0] == V2[0] and V1[2] == V2[2]):
1007 return True
1008 return False
1010 def calculateRoundifyParams(self, edge, parameters, bm, mesh):
1011 # Because all data from mesh is in local coordinates
1012 # and spin operator works on global coordinates
1013 # We first need to translate all input data by vector equal
1014 # to origin position and then perform calculations
1015 # At least that is my understanding :) <komi3D>
1017 # V1 V2 stores Local Coordinates
1018 V1, V2, edgeVector, edgeLength, edgeCenter = self.getEdgeInfo(edge)
1020 debugPrintNew(d_Plane, "PLANE: " + parameters["plane"])
1021 lineAB = self.calc.getLineCoefficientsPerpendicularToVectorInPoint(
1022 edgeCenter, edgeVector,
1023 parameters["plane"]
1025 circleMidPoint = V1
1026 circleMidPointOnPlane = self.calc.getCircleMidPointOnPlane(
1027 V1, parameters["plane"]
1029 radius = parameters["radius"]
1031 angle = 0
1032 if (parameters["entryMode"] == 'Angle'):
1033 if (parameters["angleEnum"] != 'Other'):
1034 radius, angle = self.CalculateRadiusAndAngleForAnglePresets(
1035 parameters["angleEnum"], radius,
1036 angle, edgeLength
1038 else:
1039 radius, angle = self.CalculateRadiusAndAngle(edgeLength)
1040 debugPrintNew(d_Radius_Angle, "RADIUS = " + str(radius) + " ANGLE = " + str(angle))
1041 roots = None
1042 if angle != pi: # mode other than 180
1043 if lineAB is None:
1044 roots = self.calc.getLineCircleIntersectionsWhenXPerpendicular(
1045 edgeCenter, circleMidPointOnPlane,
1046 radius, parameters["plane"]
1048 else:
1049 roots = self.calc.getLineCircleIntersections(
1050 lineAB, circleMidPointOnPlane, radius
1053 if roots is None:
1054 debugPrintNew(True,
1055 "[Edge Roundifier]: No centers were found. Change radius to higher value")
1056 return None
1057 roots = self.addMissingCoordinate(roots, V1, parameters["plane"]) # adds X, Y or Z coordinate
1058 else:
1059 roots = [edgeCenter, edgeCenter]
1060 debugPrintNew(d_Roots, "roots=" + str(roots))
1062 refObjectLocation = None
1063 objectLocation = bpy.context.active_object.location # Origin Location
1065 if parameters["refObject"] == "ORG":
1066 refObjectLocation = [0, 0, 0]
1067 elif parameters["refObject"] == "CUR":
1068 refObjectLocation = bpy.context.scene.cursor_location - objectLocation
1069 else:
1070 refObjectLocation = self.calc.getEdgeReference(edge, edgeCenter, parameters["plane"])
1072 debugPrintNew(d_RefObject, parameters["refObject"], refObjectLocation)
1073 chosenSpinCenter, otherSpinCenter = self.getSpinCenterClosestToRefCenter(
1074 refObjectLocation, roots
1077 if (parameters["entryMode"] == "Radius"):
1078 halfAngle = self.calc.getAngle(edgeCenter, chosenSpinCenter, circleMidPoint)
1079 angle = 2 * halfAngle[0] # in radians
1080 self.a = degrees(angle) # in degrees
1082 spinAxis = self.getSpinAxis(parameters["plane"])
1083 steps = parameters["segments"]
1084 angle = -angle # rotate clockwise by default
1086 return [chosenSpinCenter, otherSpinCenter, spinAxis, angle, steps, refObjectLocation]
1088 def drawSpin(self, edge, edgeCenter, roundifyParams, parameters, bm, mesh):
1089 [chosenSpinCenter, otherSpinCenter, spinAxis, angle, steps, refObjectLocation] = roundifyParams
1091 v0org, v1org = (edge.verts[0], edge.verts[1])
1093 if parameters["flip"]:
1094 angle = -angle
1095 spinCenterTemp = chosenSpinCenter
1096 chosenSpinCenter = otherSpinCenter
1097 otherSpinCenter = spinCenterTemp
1099 if(parameters["invertAngle"]):
1100 if angle < 0:
1101 angle = two_pi + angle
1102 elif angle > 0:
1103 angle = -two_pi + angle
1104 else:
1105 angle = two_pi
1107 if(parameters["fullCircles"]):
1108 angle = two_pi
1110 v0 = bm.verts.new(v0org.co)
1112 result = bmesh.ops.spin(
1113 bm, geom=[v0], cent=chosenSpinCenter, axis=spinAxis,
1114 angle=angle, steps=steps, use_duplicate=False
1117 # it seems there is something wrong with last index of this spin
1118 # I need to calculate the last index manually here
1119 vertsLength = len(bm.verts)
1120 bm.verts.ensure_lookup_table()
1121 lastVertIndex = bm.verts[vertsLength - 1].index
1122 lastSpinVertIndices = self.getLastSpinVertIndices(steps, lastVertIndex)
1124 self.sel.refreshMesh(bm, mesh)
1126 alternativeLastSpinVertIndices = []
1127 bothSpinVertices = []
1128 spinVertices = []
1129 alternate = False
1131 if ((angle == pi or angle == -pi) and not parameters["bothSides"]):
1133 midVertexIndex = lastVertIndex - round(steps / 2)
1134 bm.verts.ensure_lookup_table()
1135 midVert = bm.verts[midVertexIndex].co
1137 midVertexDistance = (Vector(refObjectLocation) - Vector(midVert)).length
1138 midEdgeDistance = (Vector(refObjectLocation) - Vector(edgeCenter)).length
1140 if ((parameters["invertAngle"]) or (parameters["flip"])):
1141 if (midVertexDistance > midEdgeDistance):
1142 alternativeLastSpinVertIndices = self.alternateSpin(
1143 bm, mesh, angle, chosenSpinCenter,
1144 spinAxis, steps, v0, v1org, lastSpinVertIndices
1146 else:
1147 if (midVertexDistance < midEdgeDistance):
1148 alternativeLastSpinVertIndices = self.alternateSpin(
1149 bm, mesh, angle, chosenSpinCenter,
1150 spinAxis, steps, v0, v1org, lastSpinVertIndices
1152 elif (angle != two_pi): # to allow full circles
1153 if (result['geom_last'][0].co - v1org.co).length > SPIN_END_THRESHOLD:
1154 alternativeLastSpinVertIndices = self.alternateSpin(
1155 bm, mesh, angle, chosenSpinCenter,
1156 spinAxis, steps, v0, v1org, lastSpinVertIndices
1158 alternate = True
1160 self.sel.refreshMesh(bm, mesh)
1161 if alternativeLastSpinVertIndices != []:
1162 lastSpinVertIndices = alternativeLastSpinVertIndices
1164 if lastSpinVertIndices.stop <= len(bm.verts): # make sure arc was added to bmesh
1165 spinVertices = [bm.verts[i] for i in lastSpinVertIndices]
1166 if alternativeLastSpinVertIndices != []:
1167 spinVertices = spinVertices + [v0]
1168 else:
1169 spinVertices = [v0] + spinVertices
1171 if (parameters["bothSides"]):
1172 # do some more testing here!!!
1173 if (angle == pi or angle == -pi):
1174 alternativeLastSpinVertIndices = self.alternateSpinNoDelete(
1175 bm, mesh, -angle, chosenSpinCenter,
1176 spinAxis, steps, v0, v1org, []
1178 elif alternate:
1179 alternativeLastSpinVertIndices = self.alternateSpinNoDelete(
1180 bm, mesh, angle, otherSpinCenter,
1181 spinAxis, steps, v0, v1org, []
1183 elif not alternate:
1184 alternativeLastSpinVertIndices = self.alternateSpinNoDelete(
1185 bm, mesh, -angle, otherSpinCenter,
1186 spinAxis, steps, v0, v1org, []
1188 bothSpinVertices = [bm.verts[i] for i in lastSpinVertIndices]
1189 alternativeSpinVertices = [bm.verts[i] for i in alternativeLastSpinVertIndices]
1190 bothSpinVertices = [v0] + bothSpinVertices + alternativeSpinVertices
1191 spinVertices = bothSpinVertices
1193 if (parameters["fullCircles"]):
1194 v1 = bm.verts.new(v1org.co)
1195 spinVertices = spinVertices + [v1]
1197 if (parameters['drawArcCenters']):
1198 centerVert = bm.verts.new(chosenSpinCenter)
1199 spinVertices.append(centerVert)
1201 return spinVertices, [chosenSpinCenter, otherSpinCenter, spinAxis, angle, steps, refObjectLocation]
1203 def deleteSpinVertices(self, bm, mesh, lastSpinVertIndices):
1204 verticesForDeletion = []
1205 bm.verts.ensure_lookup_table()
1206 for i in lastSpinVertIndices:
1207 vi = bm.verts[i]
1208 vi.select = True
1209 debugPrintNew(True, str(i) + ") " + str(vi))
1210 verticesForDeletion.append(vi)
1212 bmesh.ops.delete(bm, geom=verticesForDeletion, context=1)
1213 bmesh.update_edit_mesh(mesh, True)
1214 bpy.ops.object.mode_set(mode='OBJECT')
1215 bpy.ops.object.mode_set(mode='EDIT')
1217 def alternateSpinNoDelete(self, bm, mesh, angle, chosenSpinCenter,
1218 spinAxis, steps, v0, v1org, lastSpinVertIndices):
1219 v0prim = v0
1221 result2 = bmesh.ops.spin(bm, geom=[v0prim], cent=chosenSpinCenter, axis=spinAxis,
1222 angle=angle, steps=steps, use_duplicate=False)
1223 vertsLength = len(bm.verts)
1224 bm.verts.ensure_lookup_table()
1225 lastVertIndex2 = bm.verts[vertsLength - 1].index
1227 lastSpinVertIndices2 = self.getLastSpinVertIndices(steps, lastVertIndex2)
1228 return lastSpinVertIndices2
1230 def alternateSpin(self, bm, mesh, angle, chosenSpinCenter,
1231 spinAxis, steps, v0, v1org, lastSpinVertIndices):
1233 self.deleteSpinVertices(bm, mesh, lastSpinVertIndices)
1234 v0prim = v0
1236 result2 = bmesh.ops.spin(
1237 bm, geom=[v0prim], cent=chosenSpinCenter, axis=spinAxis,
1238 angle=-angle, steps=steps, use_duplicate=False
1240 # it seems there is something wrong with last index of this spin
1241 # I need to calculate the last index manually here
1242 vertsLength = len(bm.verts)
1243 bm.verts.ensure_lookup_table()
1244 lastVertIndex2 = bm.verts[vertsLength - 1].index
1246 lastSpinVertIndices2 = self.getLastSpinVertIndices(steps, lastVertIndex2)
1247 # second spin also does not hit the v1org
1248 if (result2['geom_last'][0].co - v1org.co).length > SPIN_END_THRESHOLD:
1250 self.deleteSpinVertices(bm, mesh, lastSpinVertIndices2)
1251 self.deleteSpinVertices(bm, mesh, range(v0.index, v0.index + 1))
1252 return []
1253 else:
1254 return lastSpinVertIndices2
1256 def getLastSpinVertIndices(self, steps, lastVertIndex):
1257 arcfirstVertexIndex = lastVertIndex - steps + 1
1258 lastSpinVertIndices = range(arcfirstVertexIndex, lastVertIndex + 1)
1259 return lastSpinVertIndices
1261 def rotateArcAroundSpinAxis(self, bm, mesh, vertices, parameters, edgeCenter):
1262 axisAngle = parameters["axisAngle"]
1263 plane = parameters["plane"]
1264 # compensate rotation center
1265 objectLocation = bpy.context.active_object.location
1266 center = objectLocation + edgeCenter
1268 rot = Euler((0.0, 0.0, radians(axisAngle)), 'XYZ').to_matrix()
1269 if plane == YZ:
1270 rot = Euler((radians(axisAngle), 0.0, 0.0), 'XYZ').to_matrix()
1271 if plane == XZ:
1272 rot = Euler((0.0, radians(axisAngle), 0.0), 'XYZ').to_matrix()
1274 indexes = [v.index for v in vertices]
1276 bmesh.ops.rotate(
1278 cent=center,
1279 matrix=rot,
1280 verts=vertices,
1281 space=bpy.context.edit_object.matrix_world
1283 self.sel.refreshMesh(bm, mesh)
1284 bm.verts.ensure_lookup_table()
1285 rotatedVertices = [bm.verts[i] for i in indexes]
1287 return rotatedVertices
1289 def CalculateRadiusAndAngle(self, edgeLength):
1290 degAngle = self.a
1291 angle = radians(degAngle)
1292 self.r = radius = edgeLength / (2 * sin(angle / 2))
1293 return radius, angle
1295 def CalculateRadiusAndAngleForAnglePresets(self, angleEnum, initR, initA, edgeLength):
1296 radius = initR
1297 angle = initA
1298 try:
1299 # Note - define an integer string in the angleEnum
1300 angle_convert = int(angleEnum)
1301 self.a = angle_convert
1302 except:
1303 self.a = 180 # fallback
1304 debugPrintNew(True,
1305 "CalculateRadiusAndAngleForAnglePresets problem with int conversion")
1307 return self.CalculateRadiusAndAngle(edgeLength)
1309 def getSpinCenterClosestToRefCenter(self, objLocation, roots):
1310 root0Distance = (Vector(objLocation) - Vector(roots[0])).length
1311 root1Distance = (Vector(objLocation) - Vector(roots[1])).length
1313 chosenId = 0
1314 rejectedId = 1
1315 if (root0Distance > root1Distance):
1316 chosenId = 1
1317 rejectedId = 0
1318 return roots[chosenId], roots[rejectedId]
1320 def addMissingCoordinate(self, roots, startVertex, plane):
1321 if roots is not None:
1322 a, b = roots[0]
1323 c, d = roots[1]
1324 if plane == XY:
1325 roots[0] = Vector((a, b, startVertex[2]))
1326 roots[1] = Vector((c, d, startVertex[2]))
1327 if plane == YZ:
1328 roots[0] = Vector((startVertex[0], a, b))
1329 roots[1] = Vector((startVertex[0], c, d))
1330 if plane == XZ:
1331 roots[0] = Vector((a, startVertex[1], b))
1332 roots[1] = Vector((c, startVertex[1], d))
1333 return roots
1335 def selectEdgesAfterRoundifier(self, context, edges):
1336 bpy.ops.object.mode_set(mode='OBJECT')
1337 bpy.ops.object.mode_set(mode='EDIT')
1338 mesh = context.scene.objects.active.data
1339 bmnew = bmesh.new()
1340 bmnew.from_mesh(mesh)
1342 self.deselectEdges(bmnew)
1343 for selectedEdge in edges:
1344 for e in bmnew.edges:
1345 if (e.verts[0].co - selectedEdge.verts[0].co).length <= self.threshold \
1346 and (e.verts[1].co - selectedEdge.verts[1].co).length <= self.threshold:
1347 e.select_set(True)
1349 bpy.ops.object.mode_set(mode='OBJECT')
1350 bmnew.to_mesh(mesh)
1351 bmnew.free()
1352 bpy.ops.object.mode_set(mode='EDIT')
1354 def deselectEdges(self, bm):
1355 for edge in bm.edges:
1356 edge.select_set(False)
1358 def getSpinAxis(self, plane):
1359 axis = (0, 0, 1)
1360 if plane == YZ:
1361 axis = (1, 0, 0)
1362 if plane == XZ:
1363 axis = (0, 1, 0)
1364 return axis
1367 def register():
1368 bpy.utils.register_class(EdgeRoundifier)
1371 def unregister():
1372 bpy.utils.unregister_class(EdgeRoundifier)
1375 if __name__ == "__main__":
1376 register()