1# Copyright (c) 2020, Manfred Moitzi 2# License: MIT License 3import pytest 4import math 5from ezdxf.layouts import VirtualLayout 6from ezdxf.math import Matrix44, OCS, Vec3, close_vectors 7from ezdxf.path import ( 8 Path, bbox, fit_paths_into_box, transform_paths, transform_paths_to_ocs, 9 to_polylines3d, to_lines, to_lwpolylines, to_polylines2d, 10 to_hatches, to_bsplines_and_vertices, to_splines_and_polylines, 11 from_vertices 12) 13from ezdxf.path import make_path, Command 14 15 16class TestTransformPaths(): 17 def test_empty_paths(self): 18 result = transform_paths([], Matrix44()) 19 assert len(result) == 0 20 21 def test_start_point_only_paths(self): 22 result = transform_paths([Path((1, 2, 3))], Matrix44()) 23 assert len(result) == 1 24 assert len(result[0]) == 0 25 assert result[0].start == (1, 2, 3) 26 27 def test_transformation_is_executed(self): 28 # Real transformation is just tested once, because Matrix44 29 # transformation is tested in 605: 30 result = transform_paths([Path((1, 2, 3))], Matrix44.translate(1, 1, 1)) 31 assert result[0].start == (2, 3, 4) 32 33 def test_one_path_line_to(self): 34 path = Path() 35 path.line_to((1, 0)) 36 result = transform_paths([path], Matrix44()) 37 path0 = result[0] 38 assert path0[0].type == Command.LINE_TO 39 assert path0.start == (0, 0) 40 assert path0.end == (1, 0) 41 42 def test_one_path_curve3_to(self): 43 path = Path() 44 path.curve3_to((2, 0), (1, 1)) 45 result = transform_paths([path], Matrix44()) 46 path0 = result[0] 47 assert path0[0].type == Command.CURVE3_TO 48 assert len(path0[0]) == 2 49 assert path0.start == (0, 0) 50 assert path0.end == (2, 0) 51 52 def test_one_path_curve4_to(self): 53 path = Path() 54 path.curve4_to((2, 0), (0, 1), (2, 1)) 55 result = transform_paths([path], Matrix44()) 56 path0 = result[0] 57 assert path0[0].type == Command.CURVE4_TO 58 assert len(path0[0]) == 3 59 assert path0.start == (0, 0) 60 assert path0.end == (2, 0) 61 62 def test_one_path_multiple_command(self): 63 path = Path() 64 path.line_to((1, 0)) 65 path.curve3_to((2, 0), (2.5, 1)) 66 path.curve4_to((3, 0), (2, 1), (3, 1)) 67 result = transform_paths([path], Matrix44()) 68 69 path0 = result[0] 70 assert path0[0].type == Command.LINE_TO 71 assert path0[1].type == Command.CURVE3_TO 72 assert path0[2].type == Command.CURVE4_TO 73 assert path0.start == (0, 0) 74 assert path0.end == (3, 0) 75 76 def test_two_paths_one_command(self): 77 path_a = Path() 78 path_a.line_to((1, 0)) 79 path_b = Path((2, 0)) 80 path_b.line_to((3, 0)) 81 result = transform_paths([path_a, path_b], Matrix44()) 82 83 path0 = result[0] 84 assert path0[0].type == Command.LINE_TO 85 assert path0.start == (0, 0) 86 assert path0.end == (1, 0) 87 88 path1 = result[1] 89 assert path1[0].type == Command.LINE_TO 90 assert path1.start == (2, 0) 91 assert path1.end == (3, 0) 92 93 def test_two_paths_multiple_commands(self): 94 path_a = Path() 95 path_a.line_to((1, 0)) 96 path_a.curve3_to((2, 0), (2.5, 1)) 97 path_a.curve4_to((3, 0), (2, 1), (3, 1)) 98 99 path_b = path_a.transform(Matrix44.translate(4, 0, 0)) 100 result = transform_paths([path_a, path_b], Matrix44()) 101 102 path0 = result[0] 103 assert path0[0].type == Command.LINE_TO 104 assert path0[1].type == Command.CURVE3_TO 105 assert path0[2].type == Command.CURVE4_TO 106 assert path0.start == (0, 0) 107 assert path0.end == (3, 0) 108 109 path1 = result[1] 110 assert path1[0].type == Command.LINE_TO 111 assert path1[1].type == Command.CURVE3_TO 112 assert path1[2].type == Command.CURVE4_TO 113 assert path1.start == (4, 0) 114 assert path1.end == (7, 0) 115 116 def test_to_ocs(self): 117 p = Path((0, 1, 1)) 118 p.line_to((0, 1, 3)) 119 ocs = OCS((1, 0, 0)) # x-Axis 120 result = list(transform_paths_to_ocs([p], ocs)) 121 p0 = result[0] 122 assert ocs.from_wcs((0, 1, 1)) == p0.start 123 assert ocs.from_wcs((0, 1, 3)) == p0[0].end 124 125 126class TestBoundingBox: 127 def test_empty_paths(self): 128 result = bbox([]) 129 assert result.has_data is False 130 131 def test_one_path(self): 132 p = Path() 133 p.line_to((1, 2, 3)) 134 assert bbox([p]).size == (1, 2, 3) 135 136 def test_two_path(self): 137 p1 = Path() 138 p1.line_to((1, 2, 3)) 139 p2 = Path() 140 p2.line_to((-3, -2, -1)) 141 assert bbox([p1, p2]).size == (4, 4, 4) 142 143 @pytest.fixture(scope='class') 144 def quadratic(self): 145 p = Path() 146 p.curve3_to((2, 0), (1, 1)) 147 return p 148 149 def test_not_precise_box(self, quadratic): 150 result = bbox([quadratic], flatten=0) 151 assert result.extmax.y == pytest.approx(1) # control point 152 153 def test_precise_box(self, quadratic): 154 result = bbox([quadratic], flatten=0.01) 155 assert result.extmax.y == pytest.approx(0.5) # parabola 156 157 158class TestFitPathsIntoBoxUniformScaling: 159 @pytest.fixture(scope='class') 160 def spath(self): 161 p = Path() 162 p.line_to((1, 2, 3)) 163 return p 164 165 def test_empty_paths(self): 166 assert fit_paths_into_box([], (0, 0, 0)) == [] 167 168 def test_uniform_stretch_paths_limited_by_z(self, spath): 169 result = fit_paths_into_box([spath], (6, 6, 6)) 170 box = bbox(result) 171 assert box.size == (2, 4, 6) 172 173 def test_uniform_stretch_paths_limited_by_y(self, spath): 174 result = fit_paths_into_box([spath], (6, 3, 6)) 175 box = bbox(result) 176 # stretch factor: 1.5 177 assert box.size == (1.5, 3, 4.5) 178 179 def test_uniform_stretch_paths_limited_by_x(self, spath): 180 result = fit_paths_into_box([spath], (1.2, 6, 6)) 181 box = bbox(result) 182 # stretch factor: 1.2 183 assert box.size.isclose((1.2, 2.4, 3.6)) 184 185 def test_uniform_shrink_paths(self, spath): 186 result = fit_paths_into_box([spath], (1.5, 1.5, 1.5)) 187 box = bbox(result) 188 assert box.size.isclose((0.5, 1, 1.5)) 189 190 def test_project_into_xy(self, spath): 191 result = fit_paths_into_box([spath], (6, 6, 0)) 192 box = bbox(result) 193 # Note: z-axis is also ignored by extent detection: 194 # scaling factor = 3x 195 assert box.size.isclose((3, 6, 0)), "z-axis should be ignored" 196 197 def test_project_into_xz(self, spath): 198 result = fit_paths_into_box([spath], (6, 0, 6)) 199 box = bbox(result) 200 assert box.size.isclose((2, 0, 6)), "y-axis should be ignored" 201 202 def test_project_into_yz(self, spath): 203 result = fit_paths_into_box([spath], (0, 6, 6)) 204 box = bbox(result) 205 assert box.size.isclose((0, 4, 6)), "x-axis should be ignored" 206 207 def test_invalid_target_size(self, spath): 208 with pytest.raises(ValueError): 209 fit_paths_into_box([spath], (0, 0, 0)) 210 211 212class TestFitPathsIntoBoxNonUniformScaling: 213 @pytest.fixture(scope='class') 214 def spath(self): 215 p = Path() 216 p.line_to((1, 2, 3)) 217 return p 218 219 def test_non_uniform_stretch_paths(self, spath): 220 result = fit_paths_into_box([spath], (8, 7, 6), uniform=False) 221 box = bbox(result) 222 assert box.size == (8, 7, 6) 223 224 def test_non_uniform_shrink_paths(self, spath): 225 result = fit_paths_into_box([spath], (1.5, 1.5, 1.5), 226 uniform=False) 227 box = bbox(result) 228 assert box.size == (1.5, 1.5, 1.5) 229 230 def test_project_into_xy(self, spath): 231 result = fit_paths_into_box([spath], (6, 6, 0), uniform=False) 232 box = bbox(result) 233 assert box.size == (6, 6, 0), "z-axis should be ignored" 234 235 def test_project_into_xz(self, spath): 236 result = fit_paths_into_box([spath], (6, 0, 6), uniform=False) 237 box = bbox(result) 238 assert box.size == (6, 0, 6), "y-axis should be ignored" 239 240 def test_project_into_yz(self, spath): 241 result = fit_paths_into_box([spath], (0, 6, 6), uniform=False) 242 box = bbox(result) 243 assert box.size == (0, 6, 6), "x-axis should be ignored" 244 245 246class TestPathToBsplineAndVertices: 247 def test_empty_path(self): 248 result = list(to_bsplines_and_vertices(Path())) 249 assert result == [] 250 251 def test_only_vertices(self): 252 p = from_vertices([(1, 0), (2, 0), (3, 1)]) 253 result = list(to_bsplines_and_vertices(p)) 254 assert len(result) == 1, "expected one list of vertices" 255 assert len(result[0]) == 3, "expected 3 vertices" 256 257 def test_one_quadratic_bezier(self): 258 p = Path() 259 p.curve3_to((4, 0), (2, 2)) 260 result = list(to_bsplines_and_vertices(p)) 261 assert len(result) == 1, "expected one B-spline" 262 cpnts = result[0].control_points 263 # A quadratic bezier should be converted to cubic bezier curve, which 264 # has a precise cubic B-spline representation. 265 assert len(cpnts) == 4, "expected 4 control vertices" 266 assert cpnts[0] == (0, 0) 267 assert cpnts[3] == (4, 0) 268 269 def test_one_cubic_bezier(self): 270 p = Path() 271 p.curve4_to((4, 0), (1, 2), (3, 2)) 272 result = list(to_bsplines_and_vertices(p)) 273 assert len(result) == 1, "expected one B-spline" 274 # cubic bezier curve maps 1:1 to cubic B-spline curve 275 # see tests: 630b for the bezier_to_bspline() function 276 277 def test_adjacent_cubic_beziers_with_G1_continuity(self): 278 p = Path() 279 p.curve4_to((4, 0), (1, 2), (3, 2)) 280 p.curve4_to((8, 0), (5, -2), (7, -2)) 281 result = list(to_bsplines_and_vertices(p)) 282 assert len(result) == 1, "expected one B-spline" 283 # cubic bezier curve maps 1:1 to cubic B-spline curve 284 # see tests: 630b for the bezier_to_bspline() function 285 286 def test_adjacent_cubic_beziers_without_G1_continuity(self): 287 p = Path() 288 p.curve4_to((4, 0), (1, 2), (3, 2)) 289 p.curve4_to((8, 0), (5, 2), (7, 2)) 290 result = list(to_bsplines_and_vertices(p)) 291 assert len(result) == 2, "expected two B-splines" 292 293 def test_multiple_segments(self): 294 p = Path() 295 p.curve4_to((4, 0), (1, 2), (3, 2)) 296 p.line_to((6, 0)) 297 p.curve3_to((8, 0), (7, 1)) 298 result = list(to_bsplines_and_vertices(p)) 299 assert len(result) == 3, "expected three segments" 300 301 302class TestToEntityConverter: 303 @pytest.fixture 304 def path(self): 305 p = Path() 306 p.line_to((4, 0, 0)) 307 p.curve4_to((0, 0, 0), (3, 1, 1), (1, 1, 1)) 308 return p 309 310 @pytest.fixture 311 def path1(self): 312 p = Path((0, 0, 1)) 313 p.curve4_to((4, 0, 1), (1, 1, 1), (3, 1, 1)) 314 return p 315 316 def test_empty_to_polylines3d(self): 317 assert list(to_polylines3d([])) == [] 318 319 def test_to_polylines3d(self, path): 320 polylines = list(to_polylines3d(path)) 321 assert len(polylines) == 1 322 p0 = polylines[0] 323 assert p0.dxftype() == 'POLYLINE' 324 assert p0.is_3d_polyline is True 325 assert len(p0) == 18 326 assert p0.vertices[0].dxf.location == (0, 0, 0) 327 assert p0.vertices[-1].dxf.location == (0, 0, 0) 328 329 def test_empty_to_lines(self): 330 assert list(to_lines([])) == [] 331 332 def test_to_lines(self, path): 333 lines = list(to_lines(path)) 334 assert len(lines) == 17 335 l0 = lines[0] 336 assert l0.dxftype() == 'LINE' 337 assert l0.dxf.start == (0, 0, 0) 338 assert l0.dxf.end == (4, 0, 0) 339 340 def test_empty_to_lwpolyline(self): 341 assert list(to_lwpolylines([])) == [] 342 343 def test_to_lwpolylines(self, path): 344 polylines = list(to_lwpolylines(path)) 345 assert len(polylines) == 1 346 p0 = polylines[0] 347 assert p0.dxftype() == 'LWPOLYLINE' 348 assert p0[0] == (0, 0, 0, 0, 0) # x, y, swidth, ewidth, bulge 349 assert p0[-1] == (0, 0, 0, 0, 0) 350 351 def test_to_lwpolylines_with_wcs_elevation(self, path1): 352 polylines = list(to_lwpolylines(path1)) 353 p0 = polylines[0] 354 assert p0.dxf.elevation == 1 355 356 def test_to_lwpolylines_with_ocs(self, path1): 357 m = Matrix44.x_rotate(math.pi / 4) 358 path = path1.transform(m) 359 extrusion = m.transform((0, 0, 1)) 360 polylines = list(to_lwpolylines(path, extrusion=extrusion)) 361 p0 = polylines[0] 362 assert p0.dxf.elevation == pytest.approx(1) 363 assert p0.dxf.extrusion.isclose(extrusion) 364 assert p0[0] == (0, 0, 0, 0, 0) 365 assert p0[-1] == (4, 0, 0, 0, 0) 366 367 def test_empty_to_polylines2d(self): 368 assert list(to_polylines2d([])) == [] 369 370 def test_to_polylines2d(self, path): 371 polylines = list(to_polylines2d(path)) 372 assert len(polylines) == 1 373 p0 = polylines[0] 374 assert p0.dxftype() == 'POLYLINE' 375 assert p0.is_2d_polyline is True 376 assert p0[0].dxf.location == (0, 0, 0) 377 assert p0[-1].dxf.location == (0, 0, 0) 378 379 def test_to_polylines2d_with_wcs_elevation(self, path1): 380 polylines = list(to_polylines2d(path1)) 381 p0 = polylines[0] 382 assert p0.dxf.elevation == (0, 0, 1) 383 384 def test_to_polylines2d_with_ocs(self, path1): 385 m = Matrix44.x_rotate(math.pi / 4) 386 path = path1.transform(m) 387 extrusion = m.transform((0, 0, 1)) 388 polylines = list(to_polylines2d(path, extrusion=extrusion)) 389 p0 = polylines[0] 390 assert p0.dxf.elevation.isclose((0, 0, 1)) 391 assert p0.dxf.extrusion.isclose(extrusion) 392 assert p0[0].dxf.location.isclose((0, 0, 1)) 393 assert p0[-1].dxf.location.isclose((4, 0, 1)) 394 395 def test_empty_to_hatches(self): 396 assert list(to_hatches([])) == [] 397 398 def test_to_poly_path_hatches(self, path): 399 hatches = list(to_hatches(path, edge_path=False)) 400 assert len(hatches) == 1 401 h0 = hatches[0] 402 assert h0.dxftype() == 'HATCH' 403 assert len(h0.paths) == 1 404 405 def test_to_poly_path_hatches_with_wcs_elevation(self, path1): 406 hatches = list(to_hatches(path1, edge_path=False)) 407 ho = hatches[0] 408 assert ho.dxf.elevation.isclose((0, 0, 1)) 409 410 def test_to_poly_path_hatches_with_ocs(self, path1): 411 m = Matrix44.x_rotate(math.pi / 4) 412 path = path1.transform(m) 413 extrusion = m.transform((0, 0, 1)) 414 hatches = list(to_hatches(path, edge_path=False, extrusion=extrusion)) 415 h0 = hatches[0] 416 assert h0.dxf.elevation.isclose((0, 0, 1)) 417 assert h0.dxf.extrusion.isclose(extrusion) 418 polypath0 = h0.paths[0] 419 assert polypath0.vertices[0] == (0, 0, 0) # x, y, bulge 420 assert polypath0.vertices[-1] == ( 421 0, 0, 0), "should be closed automatically" 422 423 def test_to_edge_path_hatches(self, path): 424 hatches = list(to_hatches(path, edge_path=True)) 425 assert len(hatches) == 1 426 h0 = hatches[0] 427 assert h0.dxftype() == 'HATCH' 428 assert len(h0.paths) == 1 429 edge_path = h0.paths[0] 430 assert edge_path.PATH_TYPE == 'EdgePath' 431 line, spline = edge_path.edges 432 assert line.EDGE_TYPE == 'LineEdge' 433 assert line.start == (0, 0) 434 assert line.end == (4, 0) 435 assert spline.EDGE_TYPE == 'SplineEdge' 436 assert close_vectors(Vec3.generate(spline.control_points), [ 437 (4, 0), (3, 1), (1, 1), (0, 0) 438 ]) 439 440 def test_to_splines_and_polylines(self, path): 441 entities = list(to_splines_and_polylines([path])) 442 assert len(entities) == 2 443 polyline = entities[0] 444 spline = entities[1] 445 assert polyline.dxftype() == 'POLYLINE' 446 assert spline.dxftype() == 'SPLINE' 447 assert polyline.vertices[0].dxf.location.isclose((0, 0)) 448 assert polyline.vertices[1].dxf.location.isclose((4, 0)) 449 assert close_vectors(Vec3.generate(spline.control_points), [ 450 (4, 0, 0), (3, 1, 1), (1, 1, 1), (0, 0, 0) 451 ]) 452 453 454# Issue #224 regression test 455@pytest.fixture 456def ellipse(): 457 layout = VirtualLayout() 458 return layout.add_ellipse( 459 center=(1999.488177113287, -1598.02265357955, 0.0), 460 major_axis=(629.968069297, 0.0, 0.0), 461 ratio=0.495263197, 462 start_param=-1.261396328799999, 463 end_param=-0.2505454928, 464 dxfattribs={ 465 'layer': "0", 466 'linetype': "Continuous", 467 'color': 3, 468 'extrusion': (0.0, 0.0, -1.0), 469 }, 470 ) 471 472 473def test_issue_224_end_points(ellipse): 474 p = make_path(ellipse) 475 476 assert ellipse.start_point.isclose(p.start) 477 assert ellipse.end_point.isclose(p.end) 478 479 # end point locations measured in BricsCAD: 480 assert ellipse.start_point.isclose((2191.3054, -1300.8375), abs_tol=1e-4) 481 assert ellipse.end_point.isclose((2609.7870, -1520.6677), abs_tol=1e-4) 482