1# Copyright (c) 2020-2021, Manfred Moitzi
2# License: MIT License
3from pathlib import Path
4import math
5import ezdxf
6from ezdxf import zoom
7from ezdxf.math import (
8    Vec3, estimate_tangents, estimate_end_tangent_magnitude,
9    global_bspline_interpolation, linspace, cubic_bezier_interpolation,
10    bezier_to_bspline, fit_points_to_cad_cv, fit_points_to_cubic_bezier,
11)
12from ezdxf.render import random_2d_path
13
14DIR = Path('~/Desktop/Outbox').expanduser()
15points = Vec3.list([(0, 0), (0, 10), (10, 10), (20, 10), (20, 0)])
16closed_points = list(points)
17closed_points.append(closed_points[0])
18
19
20def sine_wave(count: int, scale: float = 1.0):
21    for t in linspace(0, math.tau, count):
22        yield Vec3(t * scale, math.sin(t) * scale)
23
24
25def setup():
26    doc = ezdxf.new()
27    msp = doc.modelspace()
28    msp.add_lwpolyline(points, dxfattribs={'color': 5, 'layer': 'frame'})
29    for p in points:
30        msp.add_circle(p, radius=0.1, dxfattribs={'color': 1, 'layer': 'frame'})
31    return doc, msp
32
33
34# 1. Fit points from DXF file: Interpolation without any constraints
35doc, msp = setup()
36# First spline defined by control vertices interpolated from given fit points
37s = global_bspline_interpolation(points, degree=3)
38msp.add_spline(
39    dxfattribs={
40        'color': 4,
41        'layer': 'Global Curve Interpolation'
42    }
43).apply_construction_tool(s)
44# Second spline defined only by fit points as reference, does not match the
45# BricsCAD interpolation.
46spline = msp.add_spline(
47    points,
48    degree=3,
49    dxfattribs={
50        'layer': 'BricsCAD B-spline',
51        'color': 2
52    }
53)
54
55zoom.extents(msp)
56doc.saveas(DIR / 'concept-0-fit-points-only.dxf')
57
58# ------------------------------------------------------------------------------
59# SPLINE from fit points WITH given end tangents.
60# ------------------------------------------------------------------------------
61
62# 2. Store fit points, start- and end tangent values in DXF file:
63doc, msp = setup()
64# Tangent estimation method: "Total Chord Length",
65# returns sum of chords for m1 and m2
66m1, m2 = estimate_end_tangent_magnitude(points, method='chord')
67# Multiply tangent vectors by total chord length for global interpolation:
68start_tangent = Vec3.from_deg_angle(100) * m1
69end_tangent = Vec3.from_deg_angle(-100) * m2
70# Interpolate control vertices from fit points and end derivatives as constraints
71s = global_bspline_interpolation(points, degree=3,
72                                 tangents=(start_tangent, end_tangent))
73msp.add_spline(
74    dxfattribs={
75        'color': 4,
76        'layer': 'Global Interpolation'
77    }
78).apply_construction_tool(s)
79
80# Result matches the BricsCAD interpolation if fit points, start- and end
81# tangents are stored explicit in the DXF file.
82spline = msp.add_spline(
83    points,
84    degree=3,
85    dxfattribs={
86        'layer': 'BricsCAD B-spline',
87        'color': 2
88    }
89)
90spline.dxf.start_tangent = Vec3.from_deg_angle(100)
91spline.dxf.end_tangent = Vec3.from_deg_angle(-100)
92
93zoom.extents(msp)
94doc.saveas(DIR / 'concept-1-fit-points-and-tangents.dxf')
95
96# 3. Need control vertices to render the B-spline but start- and
97# end tangents are not stored in the DXF file like in scenario 1.
98# Estimation of start- and end tangents is required, best result by:
99# "5 Point Interpolation" from "The NURBS Book", Piegl & Tiller
100doc, msp = setup()
101tangents = estimate_tangents(points, method='5-points')
102# Estimated tangent angles: (108.43494882292201, -108.43494882292201) degree
103m1, m2 = estimate_end_tangent_magnitude(points, method='chord')
104start_tangent = tangents[0].normalize(m1)
105end_tangent = tangents[-1].normalize(m2)
106# Interpolate control vertices from fit points and end derivatives as constraints
107s = global_bspline_interpolation(points, degree=3,
108                                 tangents=(start_tangent, end_tangent))
109msp.add_spline(
110    dxfattribs={
111        'color': 4,
112        'layer': 'Global Interpolation'
113    }
114).apply_construction_tool(s)
115# Result does not matches the BricsCAD interpolation
116# tangents angle: (101.0035408517495, -101.0035408517495) degree
117msp.add_spline(
118    points,
119    degree=3,
120    dxfattribs={
121        'layer': 'BricsCAD B-spline',
122        'color': 2
123    }
124)
125
126zoom.extents(msp)
127doc.saveas(DIR / 'concept-2-tangents-estimated.dxf')
128
129# Theory Check:
130doc, msp = setup()
131m1, m2 = estimate_end_tangent_magnitude(points, method='chord')
132# Following values are calculated from a DXF file saved by Brics CAD
133# and SPLINE "Method" switched from "fit points" to "control vertices"
134# tangent vector = 2nd control vertex - 1st control vertex
135required_angle = 101.0035408517495  # angle of tangent vector in degrees
136required_magnitude = m1 * 1.3097943444804256  # magnitude of tangent vector
137start_tangent = Vec3.from_deg_angle(required_angle, required_magnitude)
138end_tangent = Vec3.from_deg_angle(-required_angle, required_magnitude)
139s = global_bspline_interpolation(points, degree=3,
140                                 tangents=(start_tangent, end_tangent))
141msp.add_spline(dxfattribs={
142    'color': 4,
143    'layer': 'Global Interpolation'}).apply_construction_tool(s)
144# Now result matches the BricsCAD interpolation - but only in this case
145msp.add_spline(points, degree=3,
146               dxfattribs={'layer': 'BricsCAD B-spline', 'color': 2})
147
148zoom.extents(msp)
149doc.saveas(DIR / 'concept-3-theory-check.dxf')
150
151# 1. If tangents are given (stored in DXF) the magnitude of the input tangents for the
152#    interpolation function is "total chord length".
153# 2. Without given tangents the magnitude is different, in this case: m1*1.3097943444804256,
154#    but it is not a constant factor.
155# The required information is the estimated start- and end tangent in direction and magnitude
156
157# ----------------------------------------------------------------------------
158# Recommend way to create a SPLINE defined by control vertices from fit points
159# with given end tangents:
160# ----------------------------------------------------------------------------
161doc, msp = setup()
162
163# Given start- and end tangent:
164start_tangent = Vec3.from_deg_angle(100)
165end_tangent = Vec3.from_deg_angle(-100)
166
167# Create SPLINE defined by fit points only:
168spline = msp.add_spline(
169    points,
170    degree=2,  # degree is ignored by BricsCAD and AutoCAD, both use degree=3
171    dxfattribs={
172        'layer': 'SPLINE from fit points by CAD applications',
173        'color': 2
174    }
175)
176spline.dxf.start_tangent = start_tangent
177spline.dxf.end_tangent = end_tangent
178
179# Create SPLINE defined by control vertices from fit points:
180s = fit_points_to_cad_cv(points, tangents=[start_tangent, end_tangent])
181msp.add_spline(
182    dxfattribs={
183        'color': 4,
184        'layer': 'SPLINE from control vertices by ezdxf'
185    }
186).apply_construction_tool(s)
187
188zoom.extents(msp)
189doc.saveas(DIR / 'fit_points_to_cad_cv_with_tangents.dxf')
190
191# ------------------------------------------------------------------------------
192# SPLINE from fit points WITHOUT given end tangents.
193# ------------------------------------------------------------------------------
194# Cubic Bézier curve Interpolation:
195#
196# This works only for cubic B-splines (the most common used B-spline), and
197# BricsCAD/AutoCAD allow only a degree of 2 or 3 for SPLINE entities defined
198# only by fit points.
199#
200# Further research showed that quadratic B-splines defined by fit points are
201# loaded into BricsCAD / AutoCAD as cubic B-splines. Addition to the statement
202# above: BricsCAD and AutoCAD only use a degree of 3 for SPLINE entities defined
203# only by fit points.
204#
205# http://help.autodesk.com/view/OARX/2018/ENU/?guid=OREF-AcDbSpline__setFitData_AcGePoint3dArray__AcGeVector3d__AcGeVector3d__AcGe__KnotParameterization_int_double
206# Remark in the AutoCAD ObjectARX reference for AcDbSpline about construction
207# of a B-spline from fit points:
208# degree has no effect. A spline with degree=3 is always constructed when
209# interpolating a series of fit points.
210# Sadly this works only for short simple splines.
211
212doc, msp = setup()
213msp.add_spline(points, degree=2,
214               dxfattribs={'layer': 'BricsCAD B-spline', 'color': 2})
215bezier_curves = cubic_bezier_interpolation(points)
216s = bezier_to_bspline(bezier_curves)
217msp.add_spline(
218    dxfattribs={
219        'color': 6,
220        'layer': 'Cubic Bezier Curve Interpolation'
221    }
222).apply_construction_tool(s)
223
224zoom.extents(msp)
225doc.saveas(DIR / 'concept-4-cubic-bezier-curves.dxf')
226
227# ----------------------------------------------------------------------------
228# A better way to create a SPLINE defined by control vertices from fit points
229# without given end tangents for SHORT B-splines:
230# ----------------------------------------------------------------------------
231doc, msp = setup()
232
233# Create SPLINE defined by fit points only:
234msp.add_spline(
235    points,
236    degree=2,  # degree is ignored by BricsCAD and AutoCAD, both use degree=3
237    dxfattribs={
238        'layer': 'SPLINE from fit points by CAD applications',
239        'color': 2
240    }
241)
242
243# Create SPLINE defined by control vertices from fit points:
244msp.add_spline(
245    dxfattribs={
246        'color': 3,
247        'layer': 'Cubic Bezier Curve Interpolation'
248    }
249).apply_construction_tool(fit_points_to_cubic_bezier(points))
250
251for color, mode in [(1, 'dif'), (4, '3-p'), (5, '5-p'), (6, 'bez')]:
252    msp.add_spline(
253        dxfattribs={
254            'color': color,
255            'layer': f'Global Curve Interpolation ({mode})'
256        }
257    ).apply_construction_tool(fit_points_to_cad_cv(points, estimate=mode))
258
259# This is the ONLY scenario where the cubic Bézier interpolation
260# works better (perfect) than the Global Curve Interpolation!
261zoom.extents(msp)
262doc.saveas(DIR / 'fit_points_to_cubic_bezier_open.dxf')
263
264# ------------------------------------------------------------------------------
265# Closed SPLINE from fit points WITHOUT given end tangents.
266# ------------------------------------------------------------------------------
267# IMPORTANT: first points == last point is required
268
269doc, msp = setup()
270# Create closed SPLINE defined by fit points only:
271spline = msp.add_spline(
272    closed_points,
273    dxfattribs={
274        'layer': 'SPLINE from fit points by CAD applications',
275        'color': 2
276    }
277)
278# spline.closed = True  # ignored if first points != last point
279
280# Create SPLINE defined by control vertices from fit points:
281msp.add_spline(
282    dxfattribs={
283        'color': 6,
284        'layer': 'Cubic Bezier Curve Interpolation'
285    }
286).apply_construction_tool(fit_points_to_cubic_bezier(closed_points))
287
288msp.add_spline(
289    dxfattribs={
290        'color': 4,
291        'layer': 'Global Curve Interpolation'
292    }
293).apply_construction_tool(fit_points_to_cad_cv(closed_points))
294
295# Global curve interpolation works better than cubic Bézier interpolation!
296zoom.extents(msp)
297doc.saveas(DIR / 'fit_points_to_cubic_bezier_closed.dxf')
298
299# ------------------------------------------------------------------------------
300# Closed SPLINE from fit points WITH given end tangents.
301# ------------------------------------------------------------------------------
302# IMPORTANT: first points == last point is required
303
304doc, msp = setup()
305# Create closed SPLINE defined by fit points only:
306spline = msp.add_spline(
307    closed_points,
308    dxfattribs={
309        'layer': 'SPLINE from fit points by CAD applications',
310        'color': 2
311    }
312)
313# spline.closed = True # ignored for splines from fit points
314# same tangent for start and ent point
315spline.dxf.start_tangent = start_tangent
316spline.dxf.end_tangent = end_tangent
317
318# Create SPLINE defined by control vertices from fit points:
319msp.add_spline(
320    dxfattribs={
321        'color': 4,
322        'layer': 'Global Curve Interpolation'
323    }
324).apply_construction_tool(fit_points_to_cad_cv(
325    closed_points, [start_tangent, end_tangent]))
326
327# The cubic Bèzier curve interpolation does not yield usable results fot this
328# scenario.
329
330zoom.extents(msp)
331doc.saveas(DIR / 'fit_points_to_cad_cv_closed_with_tangents.dxf')
332
333# ------------------------------------------------------------------------------
334# Random walk open SPLINE from fit points
335# ------------------------------------------------------------------------------
336
337doc = ezdxf.new()
338msp = doc.modelspace()
339walk = list(random_2d_path(10))
340
341msp.add_spline(
342    walk,
343    dxfattribs={
344        'layer': 'SPLINE from fit points by CAD applications',
345        'color': 2
346    }
347)
348
349for color, mode in [(1, 'dif'), (4, '3-p'), (5, '5-p'), (6, 'bez')]:
350    msp.add_spline(
351        dxfattribs={
352            'color': color,
353            'layer': f'Global Curve Interpolation ({mode})'
354        }
355    ).apply_construction_tool(fit_points_to_cad_cv(walk, estimate=mode))
356
357zoom.extents(msp, 1.1)
358doc.saveas(DIR / 'random_walk.dxf')
359