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