File headers: use SPDX license identifiers
[blender-addons.git] / io_import_dxf / dxfimport / convert.py
blobf8fda1f9c9cb21640e3c02db8cc1d60bc77e930c
1 # SPDX-License-Identifier: GPL-2.0-or-later
3 # <pep8 compliant>
5 import itertools
6 from . import is_
7 from .fake_entities import ArcEntity
8 from mathutils import Vector, Matrix, Euler, Color
9 from math import pi, radians, floor, ceil, degrees
10 from copy import deepcopy
13 class ShortVec(Vector):
14 def __str__(self):
15 return "Vec" + str((round(self.x, 2), round(self.y, 2), round(self.z, 2)))
17 def __repr__(self):
18 return self.__str__()
21 def bspline_to_cubic(do, en, curve, errors=None):
22 """
23 do: an instance of Do()
24 en: a DXF entity
25 curve: Blender geometry data of type "CURVE"
26 inserts knots until every knot has multiplicity of 3; returns new spline controlpoints
27 if degree of the spline is > 3 "None" is returned
28 """
29 def clean_knots():
30 start = knots[:degree + 1]
31 end = knots[-degree - 1:]
33 if start.count(start[0]) < degree + 1:
34 maxa = max(start)
35 for i in range(degree + 1):
36 knots[i] = maxa
38 if end.count(end[0]) < degree + 1:
39 mina = min(end)
40 lenk = len(knots)
41 for i in range(lenk - degree - 1, lenk):
42 knots[i] = mina
44 def insert_knot(t, k, p):
45 """ http://www.cs.mtu.edu/~shene/COURSES/cs3621/NOTES/spline/NURBS-knot-insert.html """
46 def a(t, ui, uip):
47 if uip == ui:
48 print("zero!")
49 return 0
50 return (t - ui) / (uip - ui)
52 new_spline = spline.copy()
53 for pp in range(p, 1, -1):
54 i = k - pp + 1
55 ai = a(t, knots[i], knots[i + p])
56 new_spline[i] = (1 - ai) * spline[i - 1] + ai * spline[i]
58 ai = a(t, knots[k], knots[k + p])
59 new_spline.insert(k, (1 - ai) * spline[k - 1] + ai * spline[k % len(spline)])
60 knots.insert(k, t)
62 return new_spline
64 knots = list(en.knots)
65 spline = [ShortVec(cp) for cp in en.control_points]
66 degree = len(knots) - len(spline) - 1
67 if degree <= 3:
68 clean_knots()
69 k = 1
70 st = 1
71 while k < len(knots) - 1:
72 t = knots[k]
73 multilen = knots[st:-st].count(t)
74 if multilen < degree:
75 before = multilen
76 while multilen < degree:
77 spline = insert_knot(t, k, degree)
78 multilen += 1
79 k += 1
80 k += before
81 else:
82 k += degree
84 if degree <= 2:
85 return quad_to_cube(spline)
87 # the ugly truth
88 if len(spline) % 3 == 0:
89 spline.append([spline[-1]])
90 errors.add("Cubic spline: Something went wrong with knot insertion")
91 return spline
94 def quad_to_cube(spline):
95 """
96 spline: list of (x,y,z)-tuples)
97 Converts quad bezier to cubic bezier curve.
98 """
99 s = []
100 for i, p in enumerate(spline):
101 if i % 2 == 1:
102 before = Vector(spline[i - 1])
103 after = Vector(spline[(i + 1) % len(spline)])
104 s.append(before + 2 / 3 * (Vector(p) - before))
105 s.append(after + 2 / 3 * (Vector(p) - after))
106 else:
107 s.append(p)
109 # degree == 1
110 if len(spline) == 2:
111 s.append(spline[-1])
112 return s
115 def bulge_to_arc(point, next, bulge):
117 point: start point of segment in lwpolyline
118 next: end point of segment in lwpolyline
119 bulge: number between 0 and 1
120 Converts a bulge of lwpolyline to an arc with a bulge describing the amount of how much a straight segment should
121 be bended to an arc. With the bulge one can find the center point of the arc that replaces the segment.
124 rot = Matrix(((0, -1, 0), (1, 0, 0), (0, 0, 1)))
125 section = next - point
126 section_length = section.length / 2
127 direction = -bulge / abs(bulge)
128 correction = 1
129 sagitta_len = section_length * abs(bulge)
130 radius = (sagitta_len**2 + section_length**2) / (2 * sagitta_len)
131 if sagitta_len < radius:
132 cosagitta_len = radius - sagitta_len
133 else:
134 cosagitta_len = sagitta_len - radius
135 direction *= -1
136 correction *= -1
137 center = point + section / 2 + section.normalized() * cosagitta_len * direction @ rot
138 cp = point - center
139 cn = next - center
140 cr = cp.to_3d().cross(cn.to_3d()) * correction
141 start = Vector((1, 0))
142 if cr[2] > 0:
143 angdir = 0
144 startangle = -start.angle_signed(cp.to_2d())
145 endangle = -start.angle_signed(cn.to_2d())
146 else:
147 angdir = 1
148 startangle = start.angle_signed(cp.to_2d())
149 endangle = start.angle_signed(cn.to_2d())
150 return ArcEntity(startangle, endangle, center.to_3d(), radius, angdir)
153 def bulgepoly_to_cubic(do, lwpolyline):
155 do: instance of Do()
156 lwpolyline: DXF entity of type polyline
157 Bulges define how much a straight segment of a polyline should be transformed to an arc. Hence do.arc() is called
158 for segments with a bulge and all segments are being connected to a cubic bezier curve in the end.
159 Reference: http://www.afralisp.net/archive/lisp/Bulges1.htm
161 def handle_segment(last, point, bulge):
162 if bulge != 0 and not ((point - last).length == 0 or point == last):
163 arc = bulge_to_arc(last, point, bulge)
164 cubic_bezier = do.arc(arc, None, aunits=1, angdir=arc.angdir, angbase=0)
165 else:
166 la = last.to_3d()
167 po = point.to_3d()
168 section = point - last
169 cubic_bezier = [la, la + section * 1 / 3, la + section * 2 / 3, po]
170 return cubic_bezier
172 points = lwpolyline.points
173 bulges = lwpolyline.bulge
174 lenpo = len(points)
175 spline = []
176 for i in range(1, lenpo):
177 spline += handle_segment(Vector(points[i - 1]), Vector(points[i]), bulges[i - 1])[:-1]
179 if lwpolyline.is_closed:
180 spline += handle_segment(Vector(points[-1]), Vector(points[0]), bulges[-1])
181 else:
182 spline.append(points[-1])
183 return spline
186 def bulgepoly_to_lenlist(lwpolyline):
188 returns a list with the segment lengths of a lwpolyline
190 def handle_segment(last, point, bulge):
191 seglen = (point - last).length
192 if bulge != 0 and seglen != 0:
193 arc = bulge_to_arc(last, point, bulge)
194 if arc.startangle > arc.endangle:
195 arc.endangle += 2 * pi
196 angle = arc.endangle - arc.startangle
197 lenlist.append(abs(arc.radius * angle))
198 else:
199 lenlist.append(seglen)
201 points = lwpolyline.points
202 bulges = lwpolyline.bulge
203 lenpo = len(points)
204 lenlist = []
205 for i in range(1, lenpo):
206 handle_segment(Vector(points[i - 1][:2]), Vector(points[i][:2]), bulges[i - 1])
208 if lwpolyline.is_closed:
209 handle_segment(Vector(points[-1][:2]), Vector(points[0][:2]), bulges[-1])
211 return lenlist
214 def extrusion_to_matrix(entity):
216 Converts an extrusion vector to a rotation matrix that denotes the transformation between world coordinate system
217 and the entity's own coordinate system (described by the extrusion vector).
219 def arbitrary_x_axis(extrusion_normal):
220 world_y = Vector((0, 1, 0))
221 world_z = Vector((0, 0, 1))
222 if abs(extrusion_normal[0]) < 1 / 64 and abs(extrusion_normal[1]) < 1 / 64:
223 a_x = world_y.cross(extrusion_normal)
224 else:
225 a_x = world_z.cross(extrusion_normal)
226 a_x.normalize()
227 return a_x, extrusion_normal.cross(a_x)
229 az = Vector(entity.extrusion)
230 ax, ay = arbitrary_x_axis(az)
231 ax4 = ax.to_4d()
232 ay4 = ay.to_4d()
233 az4 = az.to_4d()
234 ax4[3] = 0
235 ay4[3] = 0
236 az4[3] = 0
237 translation = Vector((0, 0, 0, 1))
238 if hasattr(entity, "elevation"):
239 if type(entity.elevation) is tuple:
240 translation = Vector(entity.elevation).to_4d()
241 else:
242 translation = (az * entity.elevation).to_4d()
243 return Matrix((ax4, ay4, az4, translation)).transposed()
246 def split_by_width(entity):
248 Used to split a curve (polyline, lwpolyline) into smaller segments if their width is varying in the overall curve.
250 class WidthTuple:
251 def __init__(self, w):
252 self.w1 = w[0]
253 self.w2 = w[1]
255 def __eq__(self, other):
256 return self.w1 == other.w1 and self.w2 == other.w2 and self.w1 == self.w2
258 if is_.varying_width(entity):
259 entities = []
260 en_template = deepcopy(entity)
261 en_template.points = []
262 en_template.bulge = []
263 en_template.width = []
264 en_template.tangents = []
266 # is_closed is an attrib only on polyline
267 if en_template.dxftype == 'POLYLINE':
268 en_template.is_closed = False
269 else:
270 # disable closed flag (0x01) when is_closed is a @property
271 en_template.flags ^= 1
273 i = 0
274 for pair, same_width in itertools.groupby(entity.width, key=lambda w: WidthTuple(w)):
275 en = deepcopy(en_template)
276 for segment in same_width:
277 en.points.append(entity.points[i])
278 en.points.append(entity.points[(i + 1) % len(entity.points)])
279 en.bulge.append(entity.bulge[i])
280 en.width.append(entity.width[i])
281 i += 1
282 entities.append(en)
284 if not entity.is_closed:
285 entities.pop(-1)
286 return entities
288 else:
289 return [entity]