File size: 191,558 Bytes
985c397 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858 859 860 861 862 863 864 865 866 867 868 869 870 871 872 873 874 875 876 877 878 879 880 881 882 883 884 885 886 887 888 889 890 891 892 893 894 895 896 897 898 899 900 901 902 903 904 905 906 907 908 909 910 911 912 913 914 915 916 917 918 919 920 921 922 923 924 925 926 927 928 929 930 931 932 933 934 935 936 937 938 939 940 941 942 943 944 945 946 947 948 949 950 951 952 953 954 955 956 957 958 959 960 961 962 963 964 965 966 967 968 969 970 971 972 973 974 975 976 977 978 979 980 981 982 983 984 985 986 987 988 989 990 991 992 993 994 995 996 997 998 999 1000 1001 1002 1003 1004 1005 1006 1007 1008 1009 1010 1011 1012 1013 1014 1015 1016 1017 1018 1019 1020 1021 1022 1023 1024 1025 1026 1027 1028 1029 1030 1031 1032 1033 1034 1035 1036 1037 1038 1039 1040 1041 1042 1043 1044 1045 1046 1047 1048 1049 1050 1051 1052 1053 1054 1055 1056 1057 1058 1059 1060 1061 1062 1063 1064 1065 1066 1067 1068 1069 1070 1071 1072 1073 1074 1075 1076 1077 1078 1079 1080 1081 1082 1083 1084 1085 1086 1087 1088 1089 1090 1091 1092 1093 1094 1095 1096 1097 1098 1099 1100 1101 1102 1103 1104 1105 1106 1107 1108 1109 1110 1111 1112 1113 1114 1115 1116 1117 1118 1119 1120 1121 1122 1123 1124 1125 1126 1127 1128 1129 1130 1131 1132 1133 1134 1135 1136 1137 1138 1139 1140 1141 1142 1143 1144 1145 1146 1147 1148 1149 1150 1151 1152 1153 1154 1155 1156 1157 1158 1159 1160 1161 1162 1163 1164 1165 1166 1167 1168 1169 1170 1171 1172 1173 1174 1175 1176 1177 1178 1179 1180 1181 1182 1183 1184 1185 1186 1187 1188 1189 1190 1191 1192 1193 1194 1195 1196 1197 1198 1199 1200 1201 1202 1203 1204 1205 1206 1207 1208 1209 1210 1211 1212 1213 1214 1215 1216 1217 1218 1219 1220 1221 1222 1223 1224 1225 1226 1227 1228 1229 1230 1231 1232 1233 1234 1235 1236 1237 1238 1239 1240 1241 1242 1243 1244 1245 1246 1247 1248 1249 1250 1251 1252 1253 1254 1255 1256 1257 1258 1259 1260 1261 1262 1263 1264 1265 1266 1267 1268 1269 1270 1271 1272 1273 1274 1275 1276 1277 1278 1279 1280 1281 1282 1283 1284 1285 1286 1287 1288 1289 1290 1291 1292 1293 1294 1295 1296 1297 1298 1299 1300 1301 1302 1303 1304 1305 1306 1307 1308 1309 1310 1311 1312 1313 1314 1315 1316 1317 1318 1319 1320 1321 1322 1323 1324 1325 1326 1327 1328 1329 1330 1331 1332 1333 1334 1335 1336 1337 1338 1339 1340 1341 1342 1343 1344 1345 1346 1347 1348 1349 1350 1351 1352 1353 1354 1355 1356 1357 1358 1359 1360 1361 1362 1363 1364 1365 1366 1367 1368 1369 1370 1371 1372 1373 1374 1375 1376 1377 1378 1379 1380 1381 1382 1383 1384 1385 1386 1387 1388 1389 1390 1391 1392 1393 1394 1395 1396 1397 1398 1399 1400 1401 1402 1403 1404 1405 1406 1407 1408 1409 1410 1411 1412 1413 1414 1415 1416 1417 1418 1419 1420 1421 1422 1423 1424 1425 1426 1427 1428 1429 1430 1431 1432 1433 1434 1435 1436 1437 1438 1439 1440 1441 1442 1443 1444 1445 1446 1447 1448 1449 1450 1451 1452 1453 1454 1455 1456 1457 1458 1459 1460 1461 1462 1463 1464 1465 1466 1467 1468 1469 1470 1471 1472 1473 1474 1475 1476 1477 1478 1479 1480 1481 1482 1483 1484 1485 1486 1487 1488 1489 1490 1491 1492 1493 1494 1495 1496 1497 1498 1499 1500 1501 1502 1503 1504 1505 1506 1507 1508 1509 1510 1511 1512 1513 1514 1515 1516 1517 1518 1519 1520 1521 1522 1523 1524 1525 1526 1527 1528 1529 1530 1531 1532 1533 1534 1535 1536 1537 1538 1539 1540 1541 1542 1543 1544 1545 1546 1547 1548 1549 1550 1551 1552 1553 1554 1555 1556 1557 1558 1559 1560 1561 1562 1563 1564 1565 1566 1567 1568 1569 1570 1571 1572 1573 1574 1575 1576 1577 1578 1579 1580 1581 1582 1583 1584 1585 1586 1587 1588 1589 1590 1591 1592 1593 1594 1595 1596 1597 1598 1599 1600 1601 1602 1603 1604 1605 1606 1607 1608 1609 1610 1611 1612 1613 1614 1615 1616 1617 1618 1619 1620 1621 1622 1623 1624 1625 1626 1627 1628 1629 1630 1631 1632 1633 1634 1635 1636 1637 1638 1639 1640 1641 1642 1643 1644 1645 1646 1647 1648 1649 1650 1651 1652 1653 1654 1655 1656 1657 1658 1659 1660 1661 1662 1663 1664 1665 1666 1667 1668 1669 1670 1671 1672 1673 1674 1675 1676 1677 1678 1679 1680 1681 1682 1683 1684 1685 1686 1687 1688 1689 1690 1691 1692 1693 1694 1695 1696 1697 1698 1699 1700 1701 1702 1703 1704 1705 1706 1707 1708 1709 1710 1711 1712 1713 1714 1715 1716 1717 1718 1719 1720 1721 1722 1723 1724 1725 1726 1727 1728 1729 1730 1731 1732 1733 1734 1735 1736 1737 1738 1739 1740 1741 1742 1743 1744 1745 1746 1747 1748 1749 1750 1751 1752 1753 1754 1755 1756 1757 1758 1759 1760 1761 1762 1763 1764 1765 1766 1767 1768 1769 1770 1771 1772 1773 1774 1775 1776 1777 1778 1779 1780 1781 1782 1783 1784 1785 1786 1787 1788 1789 1790 1791 1792 1793 1794 1795 1796 1797 1798 1799 1800 1801 1802 1803 1804 1805 1806 1807 1808 1809 1810 1811 1812 1813 1814 1815 1816 1817 1818 1819 1820 1821 1822 1823 1824 1825 1826 1827 1828 1829 1830 1831 1832 1833 1834 1835 1836 1837 1838 1839 1840 1841 1842 1843 1844 1845 1846 1847 1848 1849 1850 1851 1852 1853 1854 1855 1856 1857 1858 1859 1860 1861 1862 1863 1864 1865 1866 1867 1868 1869 1870 1871 1872 1873 1874 1875 1876 1877 1878 1879 1880 1881 1882 1883 1884 1885 1886 1887 1888 1889 1890 1891 1892 1893 1894 1895 1896 1897 1898 1899 1900 1901 1902 1903 1904 1905 1906 1907 1908 1909 1910 1911 1912 1913 1914 1915 1916 1917 1918 1919 1920 1921 1922 1923 1924 1925 1926 1927 1928 1929 1930 1931 1932 1933 1934 1935 1936 1937 1938 1939 1940 1941 1942 1943 1944 1945 1946 1947 1948 1949 1950 1951 1952 1953 1954 1955 1956 1957 1958 1959 1960 1961 1962 1963 1964 1965 1966 1967 1968 1969 1970 1971 1972 1973 1974 1975 1976 1977 1978 1979 1980 1981 1982 1983 1984 1985 1986 1987 1988 1989 1990 1991 1992 1993 1994 1995 1996 1997 1998 1999 2000 2001 2002 2003 2004 2005 2006 2007 2008 2009 2010 2011 2012 2013 2014 2015 2016 2017 2018 2019 2020 2021 2022 2023 2024 2025 2026 2027 2028 2029 2030 2031 2032 2033 2034 2035 2036 2037 2038 2039 2040 2041 2042 2043 2044 2045 2046 2047 2048 2049 2050 2051 2052 2053 2054 2055 2056 2057 2058 2059 2060 2061 2062 2063 2064 2065 2066 2067 2068 2069 2070 2071 2072 2073 2074 2075 2076 2077 2078 2079 2080 2081 2082 2083 2084 2085 2086 2087 2088 2089 2090 2091 2092 2093 2094 2095 2096 2097 2098 2099 2100 2101 2102 2103 2104 2105 2106 2107 2108 2109 2110 2111 2112 2113 2114 2115 2116 2117 2118 2119 2120 2121 2122 2123 2124 2125 2126 2127 2128 2129 2130 2131 2132 2133 2134 2135 2136 2137 2138 2139 2140 2141 2142 2143 2144 2145 2146 2147 2148 2149 2150 2151 2152 2153 2154 2155 2156 2157 2158 2159 2160 2161 2162 2163 2164 2165 2166 2167 2168 2169 2170 2171 2172 2173 2174 2175 2176 2177 2178 2179 2180 2181 2182 2183 2184 2185 2186 2187 2188 2189 2190 2191 2192 2193 2194 2195 2196 2197 2198 2199 2200 2201 2202 2203 2204 2205 2206 2207 2208 2209 2210 2211 2212 2213 2214 2215 2216 2217 2218 2219 2220 2221 2222 2223 2224 2225 2226 2227 2228 2229 2230 2231 2232 2233 2234 2235 2236 2237 2238 2239 2240 2241 2242 2243 2244 2245 2246 2247 2248 2249 2250 2251 2252 2253 2254 2255 2256 2257 2258 2259 2260 2261 2262 2263 2264 2265 2266 2267 2268 2269 2270 2271 2272 2273 2274 2275 2276 2277 2278 2279 2280 2281 2282 2283 2284 2285 2286 2287 2288 2289 2290 2291 2292 2293 2294 2295 2296 2297 2298 2299 2300 2301 2302 2303 2304 2305 2306 2307 2308 2309 2310 2311 2312 2313 2314 2315 2316 2317 2318 2319 2320 2321 2322 2323 2324 2325 2326 2327 2328 2329 2330 2331 2332 2333 2334 2335 2336 2337 2338 2339 2340 2341 2342 2343 2344 2345 2346 2347 2348 2349 2350 2351 2352 2353 2354 2355 2356 2357 2358 2359 2360 2361 2362 2363 2364 2365 2366 2367 2368 2369 2370 2371 2372 2373 2374 2375 2376 2377 2378 2379 2380 2381 2382 2383 2384 2385 2386 2387 2388 2389 2390 2391 2392 2393 2394 2395 2396 2397 2398 2399 2400 2401 2402 2403 2404 2405 2406 2407 2408 2409 2410 2411 2412 2413 2414 2415 2416 2417 2418 2419 2420 2421 2422 2423 2424 2425 2426 2427 2428 2429 2430 2431 2432 2433 2434 2435 2436 2437 2438 2439 2440 2441 2442 2443 2444 2445 2446 2447 2448 2449 2450 2451 2452 2453 2454 2455 2456 2457 2458 2459 2460 2461 2462 2463 2464 2465 2466 2467 2468 2469 2470 2471 2472 2473 2474 2475 2476 2477 2478 2479 2480 2481 2482 2483 2484 2485 2486 2487 2488 2489 2490 2491 2492 2493 2494 2495 2496 2497 2498 2499 2500 2501 2502 2503 2504 2505 2506 2507 2508 2509 2510 2511 2512 2513 2514 2515 2516 2517 2518 2519 2520 2521 2522 2523 2524 2525 2526 2527 2528 2529 2530 2531 2532 2533 2534 2535 2536 2537 2538 2539 2540 2541 2542 2543 2544 2545 2546 2547 2548 2549 2550 2551 2552 2553 2554 2555 2556 2557 2558 2559 2560 2561 2562 2563 2564 2565 2566 2567 2568 2569 2570 2571 2572 2573 2574 2575 2576 2577 2578 2579 2580 2581 2582 2583 2584 2585 2586 2587 2588 2589 2590 2591 2592 2593 2594 2595 2596 2597 2598 2599 2600 2601 2602 2603 2604 2605 2606 2607 2608 2609 2610 2611 2612 2613 2614 2615 2616 2617 2618 2619 2620 2621 2622 2623 2624 2625 2626 2627 2628 2629 2630 2631 2632 2633 2634 2635 2636 2637 2638 2639 2640 2641 2642 2643 2644 2645 2646 2647 2648 2649 2650 2651 2652 2653 2654 2655 2656 2657 2658 2659 2660 2661 2662 2663 2664 2665 2666 2667 2668 2669 2670 2671 2672 2673 2674 2675 2676 2677 2678 2679 2680 2681 2682 2683 2684 2685 2686 2687 2688 2689 2690 2691 2692 2693 2694 2695 2696 2697 2698 2699 2700 2701 2702 2703 2704 2705 2706 2707 2708 2709 2710 2711 2712 2713 2714 2715 2716 2717 2718 2719 2720 2721 2722 2723 2724 2725 2726 2727 2728 2729 2730 2731 2732 2733 2734 2735 2736 2737 2738 2739 2740 2741 2742 2743 2744 2745 2746 2747 2748 2749 2750 2751 2752 2753 2754 2755 2756 2757 2758 2759 2760 2761 2762 2763 2764 2765 2766 2767 2768 2769 2770 2771 2772 2773 2774 2775 2776 2777 2778 2779 2780 2781 2782 2783 2784 2785 2786 2787 2788 2789 2790 2791 2792 2793 2794 2795 2796 2797 2798 2799 2800 2801 2802 2803 2804 2805 2806 2807 2808 2809 2810 2811 2812 2813 2814 2815 2816 2817 2818 2819 2820 2821 2822 2823 2824 2825 2826 2827 2828 2829 2830 2831 2832 2833 2834 2835 2836 2837 2838 2839 2840 2841 2842 2843 2844 2845 2846 2847 2848 2849 2850 2851 2852 2853 2854 2855 2856 2857 2858 2859 2860 2861 2862 2863 2864 2865 2866 2867 2868 2869 2870 2871 2872 2873 2874 2875 2876 2877 2878 2879 2880 2881 2882 2883 2884 2885 2886 2887 2888 2889 2890 2891 2892 2893 2894 2895 2896 2897 2898 2899 2900 2901 2902 2903 2904 2905 2906 2907 2908 2909 2910 2911 2912 2913 2914 2915 2916 2917 2918 2919 2920 2921 2922 2923 2924 2925 2926 2927 2928 2929 2930 2931 2932 2933 2934 2935 2936 2937 2938 2939 2940 2941 2942 2943 2944 2945 2946 2947 2948 2949 2950 2951 2952 2953 2954 2955 2956 2957 2958 2959 2960 2961 2962 2963 2964 2965 2966 2967 2968 2969 2970 2971 2972 2973 2974 2975 2976 2977 2978 2979 2980 2981 2982 2983 2984 2985 2986 2987 2988 2989 2990 2991 2992 2993 2994 2995 2996 2997 2998 2999 3000 3001 3002 3003 3004 3005 3006 3007 3008 3009 3010 3011 3012 3013 3014 3015 3016 3017 3018 3019 3020 3021 3022 3023 3024 3025 3026 3027 3028 3029 3030 3031 3032 3033 3034 3035 3036 3037 3038 3039 3040 3041 3042 3043 3044 3045 3046 3047 3048 3049 3050 3051 3052 3053 3054 3055 3056 3057 3058 3059 3060 3061 3062 3063 3064 3065 3066 3067 3068 3069 3070 3071 3072 3073 3074 3075 3076 3077 3078 3079 3080 3081 3082 3083 3084 3085 3086 3087 3088 3089 3090 3091 3092 3093 3094 3095 3096 3097 3098 3099 3100 3101 3102 3103 3104 3105 3106 3107 3108 3109 3110 3111 3112 3113 3114 3115 3116 3117 3118 3119 3120 3121 3122 3123 3124 3125 3126 3127 3128 3129 3130 3131 3132 3133 3134 3135 3136 3137 3138 3139 3140 3141 3142 3143 3144 3145 3146 3147 3148 3149 3150 3151 3152 3153 3154 3155 3156 3157 3158 3159 3160 3161 3162 3163 3164 3165 3166 3167 3168 3169 3170 3171 3172 3173 3174 3175 3176 3177 3178 3179 3180 3181 3182 3183 3184 3185 3186 3187 3188 3189 3190 3191 3192 3193 3194 3195 3196 3197 3198 3199 3200 3201 3202 3203 3204 3205 3206 3207 3208 3209 3210 3211 3212 3213 3214 3215 3216 3217 3218 3219 3220 3221 3222 3223 3224 3225 3226 3227 3228 3229 3230 3231 3232 3233 3234 3235 3236 3237 3238 3239 3240 3241 3242 3243 3244 3245 3246 3247 3248 3249 3250 3251 3252 3253 3254 3255 3256 3257 3258 3259 3260 3261 3262 3263 3264 3265 3266 3267 3268 3269 3270 3271 3272 3273 3274 3275 3276 3277 3278 3279 3280 3281 3282 3283 3284 3285 3286 3287 3288 3289 3290 3291 3292 3293 3294 3295 3296 3297 3298 3299 3300 3301 3302 3303 3304 3305 3306 3307 3308 3309 3310 3311 3312 3313 3314 3315 3316 3317 3318 3319 3320 3321 3322 3323 3324 3325 3326 3327 3328 3329 3330 3331 3332 3333 3334 3335 3336 3337 3338 3339 3340 3341 3342 3343 3344 3345 3346 3347 3348 3349 3350 3351 3352 3353 3354 3355 3356 3357 3358 3359 3360 3361 3362 3363 3364 3365 3366 3367 3368 3369 3370 3371 3372 3373 3374 3375 3376 3377 3378 3379 3380 3381 3382 3383 3384 3385 3386 3387 3388 3389 3390 3391 3392 3393 3394 3395 3396 3397 3398 3399 3400 3401 3402 3403 3404 3405 3406 3407 3408 3409 3410 3411 3412 3413 3414 3415 3416 3417 3418 3419 3420 3421 3422 3423 3424 3425 3426 3427 3428 3429 3430 3431 3432 3433 3434 3435 3436 3437 3438 3439 3440 3441 3442 3443 3444 3445 3446 3447 3448 3449 3450 3451 3452 3453 3454 3455 3456 3457 3458 3459 3460 3461 3462 3463 3464 3465 3466 3467 3468 3469 3470 3471 3472 3473 3474 3475 3476 3477 3478 3479 3480 3481 3482 3483 3484 3485 3486 3487 3488 3489 3490 3491 3492 3493 3494 3495 3496 3497 3498 3499 3500 3501 3502 3503 3504 3505 3506 3507 3508 3509 3510 3511 3512 3513 3514 3515 3516 3517 3518 3519 3520 3521 3522 3523 3524 3525 3526 3527 3528 3529 3530 3531 3532 3533 3534 3535 3536 3537 3538 3539 3540 3541 3542 3543 3544 3545 3546 3547 3548 3549 3550 3551 3552 3553 3554 3555 3556 3557 3558 3559 3560 3561 3562 3563 3564 3565 3566 3567 3568 3569 3570 3571 3572 3573 3574 3575 3576 3577 3578 3579 3580 3581 3582 3583 3584 3585 3586 3587 3588 3589 3590 3591 3592 3593 3594 3595 3596 3597 3598 3599 3600 3601 3602 3603 3604 3605 3606 3607 3608 3609 3610 3611 3612 3613 3614 3615 3616 3617 3618 3619 3620 3621 3622 3623 3624 3625 3626 3627 3628 3629 3630 3631 3632 3633 3634 3635 3636 3637 3638 3639 3640 3641 3642 3643 3644 3645 3646 3647 3648 3649 3650 3651 3652 3653 3654 3655 3656 3657 3658 3659 3660 3661 3662 3663 3664 3665 3666 3667 3668 3669 3670 3671 3672 3673 3674 3675 3676 3677 3678 3679 3680 3681 3682 3683 3684 3685 3686 3687 3688 3689 3690 3691 3692 3693 3694 3695 3696 3697 3698 3699 3700 3701 3702 3703 3704 3705 3706 3707 3708 3709 3710 3711 3712 3713 3714 3715 3716 3717 3718 3719 3720 3721 3722 3723 3724 3725 3726 3727 3728 3729 3730 3731 3732 3733 3734 3735 3736 3737 3738 3739 3740 3741 3742 3743 3744 3745 3746 3747 3748 3749 3750 3751 3752 3753 3754 3755 3756 3757 3758 3759 3760 3761 3762 3763 3764 3765 3766 3767 3768 3769 3770 3771 3772 3773 3774 3775 3776 3777 3778 3779 3780 3781 3782 3783 3784 3785 3786 3787 3788 3789 3790 3791 3792 3793 3794 3795 3796 3797 3798 3799 3800 3801 3802 3803 3804 3805 3806 3807 3808 3809 3810 3811 3812 3813 3814 3815 3816 3817 3818 3819 3820 3821 3822 3823 3824 3825 3826 3827 3828 3829 3830 3831 3832 3833 3834 3835 3836 3837 3838 3839 3840 3841 3842 3843 3844 3845 3846 3847 3848 3849 3850 3851 3852 3853 3854 3855 3856 3857 3858 3859 3860 3861 3862 3863 3864 3865 3866 3867 3868 3869 3870 3871 3872 3873 3874 3875 3876 3877 3878 3879 3880 3881 3882 3883 3884 3885 3886 3887 3888 3889 3890 3891 3892 3893 3894 3895 3896 3897 3898 3899 3900 3901 3902 3903 3904 3905 3906 3907 3908 3909 3910 3911 3912 3913 3914 3915 3916 3917 3918 3919 3920 3921 3922 3923 3924 3925 3926 3927 3928 3929 3930 3931 3932 3933 3934 3935 3936 3937 3938 3939 3940 3941 3942 3943 3944 3945 3946 3947 3948 3949 3950 3951 3952 3953 3954 3955 3956 3957 3958 3959 3960 3961 3962 3963 3964 3965 3966 3967 3968 3969 3970 3971 3972 3973 3974 3975 3976 3977 3978 3979 3980 3981 3982 3983 3984 3985 3986 3987 3988 3989 3990 3991 3992 3993 3994 3995 3996 3997 3998 3999 4000 4001 4002 4003 4004 4005 4006 4007 4008 4009 4010 4011 4012 4013 4014 4015 4016 4017 4018 4019 4020 4021 4022 4023 4024 4025 4026 4027 4028 4029 4030 4031 4032 4033 4034 4035 4036 4037 4038 4039 4040 4041 4042 4043 4044 4045 4046 4047 4048 4049 4050 4051 4052 4053 4054 4055 4056 4057 4058 4059 4060 4061 4062 4063 4064 4065 4066 4067 4068 4069 4070 4071 4072 4073 4074 4075 4076 4077 4078 4079 4080 4081 4082 4083 4084 4085 4086 4087 4088 4089 4090 4091 4092 4093 4094 4095 4096 4097 4098 4099 4100 4101 4102 4103 4104 4105 4106 4107 4108 4109 4110 4111 4112 4113 4114 4115 4116 4117 4118 4119 4120 4121 4122 4123 4124 4125 4126 4127 4128 4129 4130 4131 4132 4133 4134 4135 4136 4137 4138 4139 4140 4141 4142 4143 4144 4145 4146 4147 4148 4149 4150 4151 4152 4153 4154 4155 4156 4157 4158 4159 4160 4161 4162 4163 4164 4165 4166 4167 4168 4169 4170 4171 4172 4173 4174 4175 4176 4177 4178 4179 4180 4181 4182 4183 4184 4185 4186 4187 4188 4189 4190 4191 4192 4193 4194 4195 4196 4197 4198 4199 4200 4201 4202 4203 4204 4205 4206 4207 4208 4209 4210 4211 4212 4213 4214 4215 4216 4217 4218 4219 4220 4221 4222 4223 4224 4225 4226 4227 4228 4229 4230 4231 4232 4233 4234 4235 4236 4237 4238 4239 4240 4241 4242 4243 4244 4245 4246 4247 4248 4249 4250 4251 4252 4253 4254 4255 4256 4257 4258 4259 4260 4261 4262 4263 4264 4265 4266 4267 4268 4269 4270 4271 4272 4273 4274 4275 4276 4277 4278 4279 4280 4281 4282 4283 4284 4285 4286 4287 4288 4289 4290 4291 4292 4293 4294 4295 4296 4297 4298 4299 4300 4301 4302 4303 4304 4305 4306 4307 4308 4309 4310 4311 4312 4313 4314 4315 4316 4317 4318 4319 4320 4321 4322 4323 4324 4325 4326 4327 4328 4329 4330 4331 4332 4333 4334 4335 4336 4337 4338 4339 4340 4341 4342 4343 4344 4345 4346 4347 4348 4349 4350 4351 4352 4353 4354 4355 4356 4357 4358 4359 4360 4361 4362 4363 4364 4365 4366 4367 4368 4369 4370 4371 4372 4373 4374 4375 4376 4377 4378 4379 4380 4381 4382 4383 4384 4385 4386 4387 4388 4389 4390 4391 4392 4393 4394 4395 4396 4397 4398 4399 4400 4401 4402 4403 4404 4405 4406 4407 4408 4409 4410 4411 4412 4413 4414 4415 4416 4417 4418 4419 4420 4421 4422 4423 4424 4425 4426 4427 4428 4429 4430 4431 4432 4433 4434 4435 4436 4437 4438 4439 4440 4441 4442 4443 4444 4445 4446 4447 4448 4449 4450 4451 4452 4453 4454 4455 4456 4457 4458 4459 4460 4461 4462 4463 4464 4465 4466 4467 4468 4469 4470 4471 4472 4473 4474 4475 4476 4477 4478 4479 4480 4481 4482 4483 4484 4485 4486 4487 4488 4489 4490 4491 4492 4493 4494 4495 4496 4497 4498 4499 4500 4501 4502 4503 4504 4505 4506 4507 4508 4509 4510 4511 4512 4513 4514 4515 4516 4517 4518 4519 4520 4521 4522 4523 4524 4525 4526 4527 4528 4529 4530 4531 4532 4533 4534 4535 4536 4537 4538 4539 4540 4541 4542 4543 4544 4545 4546 4547 4548 4549 4550 4551 4552 4553 4554 4555 4556 4557 4558 4559 4560 4561 4562 4563 4564 4565 4566 4567 4568 4569 4570 4571 4572 4573 4574 4575 4576 4577 4578 4579 4580 4581 4582 4583 4584 4585 4586 4587 4588 4589 4590 4591 4592 4593 4594 4595 4596 4597 4598 4599 4600 4601 4602 4603 4604 4605 4606 4607 4608 4609 4610 4611 4612 4613 4614 4615 4616 4617 4618 4619 4620 4621 4622 4623 4624 4625 4626 4627 4628 4629 4630 4631 4632 4633 4634 4635 4636 4637 4638 4639 4640 4641 4642 4643 4644 4645 4646 4647 4648 4649 4650 4651 4652 4653 4654 4655 4656 4657 4658 4659 4660 4661 4662 4663 4664 4665 4666 4667 4668 4669 4670 4671 4672 4673 4674 4675 4676 4677 4678 4679 4680 4681 4682 4683 4684 4685 4686 4687 4688 4689 4690 4691 4692 4693 4694 4695 4696 4697 4698 4699 4700 4701 4702 4703 4704 4705 4706 4707 4708 4709 4710 4711 4712 4713 4714 4715 4716 4717 4718 4719 4720 4721 4722 4723 4724 4725 4726 4727 4728 4729 4730 4731 4732 4733 4734 4735 4736 4737 4738 4739 4740 4741 4742 4743 4744 4745 4746 4747 4748 4749 4750 4751 4752 4753 4754 4755 4756 4757 4758 4759 4760 4761 4762 4763 4764 4765 4766 4767 4768 4769 4770 4771 4772 4773 4774 4775 4776 4777 4778 4779 4780 4781 4782 4783 4784 4785 4786 4787 4788 4789 4790 4791 4792 4793 4794 4795 4796 4797 4798 4799 4800 4801 4802 4803 4804 4805 4806 4807 4808 4809 4810 4811 4812 4813 4814 4815 4816 4817 4818 4819 4820 4821 4822 4823 4824 4825 4826 4827 4828 4829 4830 4831 4832 4833 4834 4835 4836 4837 4838 4839 4840 4841 4842 4843 4844 4845 4846 4847 4848 4849 4850 4851 4852 4853 4854 4855 4856 4857 4858 4859 4860 4861 4862 4863 4864 4865 4866 4867 4868 4869 4870 4871 4872 4873 4874 4875 4876 4877 4878 4879 4880 4881 4882 4883 4884 4885 4886 4887 4888 4889 4890 4891 4892 4893 4894 4895 4896 4897 4898 4899 4900 4901 4902 4903 4904 4905 4906 4907 4908 4909 4910 4911 4912 4913 4914 4915 4916 4917 4918 4919 4920 4921 4922 4923 4924 4925 4926 4927 4928 4929 4930 4931 4932 4933 4934 4935 4936 4937 4938 4939 4940 4941 4942 4943 4944 4945 4946 4947 4948 4949 4950 4951 4952 4953 4954 4955 4956 4957 4958 4959 4960 4961 4962 4963 4964 4965 4966 4967 4968 4969 4970 4971 4972 4973 4974 4975 4976 4977 4978 4979 4980 4981 4982 4983 4984 4985 4986 4987 4988 4989 4990 4991 4992 4993 4994 4995 4996 4997 4998 4999 5000 5001 5002 5003 5004 5005 5006 5007 5008 5009 5010 5011 5012 5013 5014 5015 5016 5017 5018 5019 5020 5021 5022 5023 5024 5025 5026 5027 5028 5029 5030 5031 5032 5033 5034 5035 5036 5037 5038 5039 5040 5041 5042 5043 5044 5045 5046 5047 5048 5049 5050 5051 5052 5053 5054 5055 5056 5057 5058 5059 5060 5061 5062 5063 5064 5065 5066 5067 5068 5069 5070 5071 5072 5073 5074 5075 5076 5077 5078 5079 5080 5081 5082 5083 5084 | # SPDX-License-Identifier: LGPL-2.1-or-later
# -*- coding: utf8 -*-
# Check code with
# flake8 --ignore=E226,E266,E401,W503
# ***************************************************************************
# * Copyright (c) 2009 Yorik van Havre <yorik@uncreated.net> *
# * *
# * This program is free software; you can redistribute it and/or modify *
# * it under the terms of the GNU Lesser General Public License (LGPL) *
# * as published by the Free Software Foundation; either version 2 of *
# * the License, or (at your option) any later version. *
# * for detail see the LICENCE text file. *
# * *
# * This program is distributed in the hope that it will be useful, *
# * but WITHOUT ANY WARRANTY; without even the implied warranty of *
# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
# * GNU Library General Public License for more details. *
# * *
# * You should have received a copy of the GNU Library General Public *
# * License along with this program; if not, write to the Free Software *
# * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 *
# * USA *
# * *
# ***************************************************************************
__title__ = "FreeCAD Draft Workbench - DXF importer/exporter"
__author__ = "Yorik van Havre <yorik@uncreated.net>"
__url__ = "https://www.freecad.org"
## @package importDXF
# \ingroup DRAFT
# \brief DXF file importer & exporter
#
# This module provides support for importing and exporting Autodesk DXF files
"""
This script uses a DXF-parsing library created by Stani,
Kitsu and Migius for Blender
imports:
line, polylines, lwpolylines, arcs, circles, texts,
mtexts, layers (as groups), colors
exports:
lines, polylines, lwpolylines, circles, arcs,
texts, colors,layers (from groups)
"""
# scaling factor between autocad font sizes and coin font sizes
TEXTSCALING = 1.35
# the minimum version of the dxfLibrary needed to run
CURRENTDXFLIB = 1.42
import sys
import os
import math
import re
import time
import FreeCAD
import Part
import Draft
import Mesh
import DraftVecUtils
import DraftGeomUtils
import WorkingPlane
from FreeCAD import Vector
from FreeCAD import Console as FCC
from Draft import LinearDimension
from draftobjects.dimension import _Dimension
from draftutils import params
from draftutils import utils
from draftutils.utils import pyopen
from PySide import QtCore, QtGui
gui = FreeCAD.GuiUp
draftui = None
if gui:
import FreeCADGui
try:
draftui = FreeCADGui.draftToolBar
except (AttributeError, NameError):
draftui = None
try:
from draftviewproviders.view_base import ViewProviderDraft
from draftviewproviders.view_wire import ViewProviderWire
from draftviewproviders.view_dimension import ViewProviderLinearDimension
except ImportError:
ViewProviderDraft = None
ViewProviderWire = None
from draftutils.translate import translate
from PySide import QtWidgets
else:
def translate(context, txt):
return txt
dxfReader = None
dxfColorMap = None
dxfLibrary = None
def errorDXFLib(gui):
"""Download the files required to convert DXF files.
It checks the parameter `'dxfAllowDownload'` to decide whether it
has access to download the required DXF libraries.
Parameters
----------
gui : bool
If `True` it will display error messages in graphical
text boxes; otherwise it will display the messages in the terminal.
To do
-----
Use local variables, not global variables.
"""
dxfAllowDownload = params.get_param("dxfAllowDownload")
if dxfAllowDownload:
files = ["dxfColorMap.py", "dxfImportObjects.py", "dxfLibrary.py", "dxfReader.py"]
baseurl = "https://raw.githubusercontent.com/yorikvanhavre/"
baseurl += "Draft-dxf-importer/master/"
import ArchCommands
from FreeCAD import Base
progressbar = Base.ProgressIndicator()
progressbar.start("Downloading files...", 4)
for f in files:
progressbar.next()
p = None
p = ArchCommands.download(baseurl + f, force=True)
if not p:
if gui:
message = translate(
"Draft",
"""Download of DXF libraries failed.
Please install the DXF Library addon manually
from menu Tools → Addon Manager""",
)
QtWidgets.QMessageBox.information(None, "", message)
else:
FCC.PrintWarning(
"The DXF import/export libraries needed by FreeCAD to handle the DXF format are not installed.\n"
)
FCC.PrintWarning(
"Please install the DXF Library addon from Tools → Addon Manager\n"
)
break
progressbar.stop()
sys.path.append(FreeCAD.ConfigGet("UserAppData"))
else:
if gui:
message = translate(
"draft",
"""The DXF import/export libraries needed by FreeCAD to handle
the DXF format were not found on this system.
Please either allow FreeCAD to download these libraries:
1 - Load Draft workbench
2 - Menu Edit → Preferences → Import-Export → DXF → Enable downloads
Or download these libraries manually, as explained on
https://github.com/yorikvanhavre/Draft-dxf-importer
To enabled FreeCAD to download these libraries, answer Yes.""",
)
reply = QtWidgets.QMessageBox.question(
None,
"",
message,
QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No,
QtWidgets.QMessageBox.No,
)
if reply == QtWidgets.QMessageBox.Yes:
params.set_param("dxfAllowDownload", True)
errorDXFLib(gui)
if reply == QtWidgets.QMessageBox.No:
pass
else:
FCC.PrintWarning(
"The DXF import/export libraries needed by FreeCAD to handle the DXF format are not installed.\n"
)
_ver = FreeCAD.Version()
_maj = _ver[0]
_min = _ver[1]
if float(_maj + "." + _min) >= 0.17:
FCC.PrintWarning(
"Please install the DXF Library addon from Tools → Addon Manager\n"
)
else:
FCC.PrintWarning(
"Please check https://github.com/yorikvanhavre/Draft-dxf-importer\n"
)
def getDXFlibs():
"""Load the DXF Python libraries.
It tries loading the global libraries for use in the system
`dxfLibrary`, `dxfColorMap`, `dxfReader`,
If they are not present, they are downloaded.
To do
-----
Use local variables, not global variables.
"""
try:
if FreeCAD.ConfigGet("UserAppData") not in sys.path:
sys.path.append(FreeCAD.ConfigGet("UserAppData"))
global dxfLibrary, dxfColorMap, dxfReader
import dxfLibrary
import dxfColorMap
try:
import dxfReader
except Exception:
libsok = False
except ImportError:
libsok = False
FCC.PrintWarning("DXF libraries not found. Trying to download…\n")
else:
if float(dxfLibrary.__version__[1:5]) >= CURRENTDXFLIB:
libsok = True
else:
FCC.PrintWarning("DXF libraries need to be updated. " "Trying to download…\n")
libsok = False
if not libsok:
errorDXFLib(gui)
try:
import dxfColorMap, dxfLibrary, dxfReader
import importlib
importlib.reload(dxfColorMap)
importlib.reload(dxfLibrary)
importlib.reload(dxfReader)
except Exception:
dxfReader = None
dxfLibrary = None
FCC.PrintWarning("DXF libraries not available. Aborting.\n")
def deformat(text):
"""Remove weird formats in texts and wipes UTF characters.
It removes `{}`, html codes, \\(U...) characters,
Parameters
----------
text : str
The input string.
Results
-------
str
The deformatted string.
"""
# remove ACAD string formatation
# t = re.sub(r'{([^!}]([^}]|\n)*)}', '', text)
# print("input text: ",text)
t = text.strip("{}")
t = re.sub(r"\\\\.*?;", "", t)
# replace UTF codes by utf chars
sts = re.split("\\\\(U\\+....)", t)
t = "".join(sts)
# replace degrees, diameters chars
t = re.sub(r"%%d", "°", t)
t = re.sub(r"%%c", "Ø", t)
t = re.sub(r"%%D", "°", t)
t = re.sub(r"%%C", "Ø", t)
# print("output text: ", t)
return t
def locateLayer(wantedLayer, color=None, drawstyle=None, visibility=True):
"""Return layer group and create it if needed.
This function iterates over a global list named `layers`, which is
defined in `processdxf`.
If no layers are found it looks for the global `dxfUseDraftVisGroup`
variable defined in `readPreferences`, and creates a new `Draft Layer`
with the specified color.
Otherwise it creates a group (`App::DocumentObjectGroup`)
to use as a layer container.
Parameters
----------
wantedLayer : str
The name of a layer to search in the global `layers` list.
color : tuple of four floats, optional
It defaults to `None`.
A tuple with color information `(r,g,b,a)`, where each value
is a float between 0 and 1.
drawstyle : str, optional
It defaults to `None`. In which case "Solid" is used.
"Solid", "Dashed", "Dotted" or "Dashdot".
Visibility : bool, optional
It defaults to `True`.
Visibility of the new layer.
Returns
-------
App::FeaturePython or App::DocumentObjectGroup
If the `wantedLayer` is found in the global list of layers,
it is returned.
Otherwise, a new layer or group is created and returned.
If the global variable `dxfUseDraftVisGroup` is set,
it creates a `Draft Layer` (`App::FeaturePython`).
Otherwise, it creates a simple group (`App::DocumentObjectGroup`).
See also
--------
Draft.make_layer
To do
-----
Use local variables, not global variables.
"""
# layers is a global variable.
# It should probably be passed as an argument.
if wantedLayer is None:
wantedLayer = "0"
for layer in layers:
if layer.Label == wantedLayer:
return layer
if dxfUseDraftVisGroups:
newLayer = Draft.make_layer(
name=wantedLayer,
line_color=(0.0, 0.0, 0.0) if not color else color,
draw_style="Solid" if not drawstyle else drawstyle,
)
newLayer.Visibility = visibility
else:
newLayer = doc.addObject("App::DocumentObjectGroup", wantedLayer)
newLayer.Label = wantedLayer
layers.append(newLayer)
return newLayer
def getdimheight(style):
"""Return the dimension text height from the given dimstyle.
It searches the global variable `drawing.tables.data`,
created in `processdxf`, for a `dimstyle`; then iterates on the data,
and if a `dimstyle` is found, it compares if its raw value with DXF code 2
(Name) is equal to `style`.
Parameters
---------
style : str
A raw value of DXF code 3 (other text or name value).
Returns
-------
float
The data of DXF code 140 (DIMSTYLE setting),
or just 1 if no `dimstyle` was found in `drawing.tables.data`.
To do
-----
Use local variables, not global variables.
"""
for t in drawing.tables.data:
if t.name == "dimstyle":
for a in t.data:
if hasattr(a, "type"):
if a.type == "dimstyle":
if rawValue(a, 2) == style:
return rawValue(a, 140)
return 1
def calcBulge(v1, bulge, v2):
"""Calculate intermediary vertex for a curved segment.
Considering an arc of a circle, it can be defined by two vertices `v1`
and `v2`, and a `bulge` value that indicates how curved the arc is.
A `bulge` of 0 is a straight line, while a `bulge` of 1 is the maximum
curvature, or a semicircle.
A vertex that is in the curve, equidistant to the two vertices,
can be found by finding the sagitta of the arc, that is,
the perpendicular to the chord that goes from `v1` to `v2`.
It uses the algorithm from http://www.afralisp.net/lisp/Bulges1.htm
Parameters
----------
v1 : Base::Vector3
The first point.
bulge : float
The bulge is the tangent of 1/4 of the included angle for the arc
between `v1` and `v2`. A negative `bulge` indicates that the arc
goes clockwise from `v1` to `v2`. A `bulge` of 0 indicates
a straight segment, and a `bulge` of 1 is a semicircle.
v2 : Base::Vector3
The second point.
Returns
-------
Base::Vector3
The new point between `v1` and `v2`.
"""
chord = v2.sub(v1)
sagitta = (bulge * chord.Length) / 2
perp = chord.cross(Vector(0, 0, 1))
startpoint = v1.add(chord.multiply(0.5))
if not DraftVecUtils.isNull(perp):
perp.normalize()
endpoint = perp.multiply(sagitta)
return startpoint.add(endpoint)
def getGroup(ob):
"""Get the name of the group or Draft layer that contains the object.
It looks for the global `dxfUseDraftVisGroup` variable defined
in `readPreferences`. Then searches all objects of type "Layer"
for the one that contains `ob`.
Otherwise, it searches all objects derived from
`App::DocumentObjectGroup` for the one that contains `ob`.
Parameters
----------
ob : App::DocumentObject
Any object to test as belonging to a layer or group.
Returns
-------
str
The label of the layer, or of the group, if it contains `ob`.
Otherwise, return "0".
To do
-----
Use local variables, not global variables.
"""
all_objs = FreeCAD.ActiveDocument.Objects
if dxfUseDraftVisGroups:
for layer in [o for o in all_objs if Draft.getType(o) == "Layer"]:
if ob in layer.Group:
return layer.Label
for i in all_objs:
if i.isDerivedFrom("App::DocumentObjectGroup"):
for j in i.Group:
if j == ob:
return i.Label
return "0"
def getACI(ob, text=False):
"""Get the AutoCAD color index (ACI) color closest to the object's color.
This function only works if the graphical interface is loaded,
as it checks the `ViewObject` attribute of the object
which only exists when the GUI is available.
Parameters
----------
ob : App::DocumentObject
Any object.
text : bool, optional
It defaults ot `False`. If `True`, use the `TextColor`
instead of the `LineColor` of the object.
Returns
-------
int
The numerical value of the AutoCAD color index (ACI) color,
which goes from 0 to 255.
It returns 0 (black) if no graphical interface is loaded.
It returns 256 (`BYLAYER`) if `ob` is inside a Draft Layer,
and the layer's `OverrideChildren` view property is `True`.
"""
if not gui:
return 0
else:
# detect if we need to set "BYLAYER"
for parent in ob.InList:
if Draft.getType(parent) == "Layer":
if ob in parent.Group:
if hasattr(parent, "ViewObject") and hasattr(
parent.ViewObject, "OverrideChildren"
):
if parent.ViewObject.OverrideChildren:
return 256 # BYLAYER
if text:
col = ob.ViewObject.TextColor
else:
col = ob.ViewObject.LineColor
aci = [0, 442]
for i in range(255, -1, -1):
ref = dxfColorMap.color_map[i]
dist = (ref[0] - col[0]) ** 2 + (ref[1] - col[1]) ** 2 + (ref[2] - col[2]) ** 2
if dist <= aci[1]:
aci = [i, dist]
return aci[0]
def rawValue(entity, code):
"""Return the value of a DXF code in an entity section.
Parameters
----------
entity : drawing.entities
A DXF entity in the `drawing` data obtained from `processdxf`.
code : int
A numerical value of the code.
Returns
-------
float or str
The value corresponding to the code. It may be numeric or a string.
"""
value = None
for pair in entity.data:
if pair[0] == code:
value = pair[1]
return value
def getMultiplePoints(entity):
"""Scan the given entity (paths, leaders, etc.) for multiple points.
Parameters
----------
entity : drawing.entities
A DXF entity in the `drawing` data obtained from `processdxf`.
Returns
-------
list of Base::Vector3
The list of points (vectors).
Each point has three coordinates `(X,Y,Z)`.
If the original point only had two, the third coordinate
is set to zero `(X,Y,0)`.
"""
pts = []
for d in entity.data:
if d[0] == 10:
pts.append([d[1]])
elif d[0] in [20, 30]:
pts[-1].append(d[1])
pts.reverse()
points = []
for p in pts:
if len(p) == 3:
points.append(Vector(p[0], p[1], p[2]))
else:
points.append(Vector(p[0], p[1], 0))
return points
def isBrightBackground():
"""Check if the current viewport's background is a bright color.
It considers the values of `BackgroundColor` for a solid background,
or a combination of `BackgroundColor2` and `BackgroundColor3`
for a gradient background from the parameter database.
Returns
-------
bool
Returns `True` if the value of the color is larger than 128,
which is considered light; otherwise it is considered dark
and returns `False`.
"""
if params.get_param_view("Gradient"):
r1, g1, b1, _ = utils.get_rgba_tuple(params.get_param_view("BackgroundColor2"))
r2, g2, b2, _ = utils.get_rgba_tuple(params.get_param_view("BackgroundColor3"))
v1 = Vector(r1, g1, b1)
v2 = Vector(r2, g2, b2)
v = v2.sub(v1)
v.multiply(0.5)
cv = v1.add(v)
else:
r1, g1, b1, _ = utils.get_rgba_tuple(params.get_param_view("BackgroundColor"))
cv = Vector(r1, g1, b1)
value = cv.x * 0.3 + cv.y * 0.59 + cv.z * 0.11
if value < 128:
return False
else:
return True
def getGroupColor(dxfobj, index=False):
"""Get the color of the layer.
It searches the global variable `drawing.tables`,
created in `processdxf`, for a `layer`; then iterates on the data,
and if the layer name matches the layer of `dxfobj`, it will try
to return the color of its layer.
It searches the global variable `dxfBrightBackground` to determine
if it should return black, or a color from the global
`dxfColorMap.color_map` dictionary.
Parameters
----------
dxfobj : Part::Feature
An imported DXF object.
index : bool, optional
It defaults to `False`. If it is `True` it will return the layer's
color; otherwise it will check the global variable
`dxfBrightBackground`, and return black or a mapped color.
Returns
-------
list of 3 floats
The layer's color as a list `[r, g, b]`, black `[0, 0, 0]`
or the mapped color `dxfColorMap.color_map[color]`.
To do
-----
Use local variables, not global variables.
"""
name = dxfobj.layer
for table in drawing.tables.get_type("table"):
if table.name == "layer":
for l in table.get_type("layer"):
if l.name == name:
if index:
return l.color
else:
if (l.color == 7) and dxfBrightBackground:
return [0.0, 0.0, 0.0]
else:
if isinstance(l.color, int):
if l.color > 0:
return dxfColorMap.color_map[l.color]
return [0.0, 0.0, 0.0]
def getColor():
"""Get the Draft color defined in the Draft toolbar or preferences.
Returns
-------
tuple of 4 floats
Return the `(r, g, b, 0.0)` tuple with the colors defined
in the Draft toolbar, if the graphical user interface is active.
Otherwise, return the tuple with the color
of the `DefaultShapeLineColor` in the parameter database.
"""
if gui and draftui:
r = float(draftui.color.red() / 255.0)
g = float(draftui.color.green() / 255.0)
b = float(draftui.color.blue() / 255.0)
return (r, g, b, 0.0)
else:
r, g, b, _ = utils.get_rgba_tuple(params.get_param_view("DefaultShapeLineColor"))
return (r, g, b, 0.0)
def formatObject(obj, dxfobj=None):
"""Apply text and line color to an object from a DXF object.
If `dxfUseDraftVisGroups` is `True` the function returns immediately.
The color of the object then depends on the Draft Layer the object is in.
Else this function only works if the graphical user interface is loaded
as it needs access to the `ViewObject` attribute of the objects.
If `dxfobj` and the global variable `dxfGetColors` exist
the `TextColor` and `LineColor` of `obj` will be set to the color
indicated by the global dictionary
`dxfColorMap.color_map[dxfobj.color_index]`.
If the global `dxfBrightBackground` is set, it will set the `LineColor`
to black.
If no `dxfobj` is given, `TextColor` and `LineColor`
are set to the global variable `dxfDefaultColor`.
Parameters
----------
obj : App::DocumentObject
Object that will use the DXF color.
dxfobj : drawing.entities, optional
It defaults to `None`. DXF object from which the color will be taken.
To do
-----
Use local variables, not global variables.
"""
if dxfUseDraftVisGroups:
return
if dxfGetColors and dxfobj and hasattr(dxfobj, "color_index"):
if hasattr(obj.ViewObject, "TextColor"):
if dxfobj.color_index == 256:
cm = getGroupColor(dxfobj)[:3]
else:
cm = dxfColorMap.color_map[dxfobj.color_index]
obj.ViewObject.TextColor = (cm[0], cm[1], cm[2])
elif hasattr(obj.ViewObject, "LineColor"):
if dxfobj.color_index == 256:
cm = getGroupColor(dxfobj)
elif (dxfobj.color_index == 7) and dxfBrightBackground:
cm = [0.0, 0.0, 0.0]
else:
cm = dxfColorMap.color_map[dxfobj.color_index]
obj.ViewObject.LineColor = (cm[0], cm[1], cm[2], 0.0)
else:
if hasattr(obj.ViewObject, "TextColor"):
obj.ViewObject.TextColor = dxfDefaultColor
elif hasattr(obj.ViewObject, "LineColor"):
obj.ViewObject.LineColor = dxfDefaultColor
def vec(pt):
"""Return a rounded and scaled Vector from a DXF point.
Parameters
----------
pt : Base::Vector3, or list of three numerical values, or float, or int
A point with three coordinates `(x, y, z)`,
or just a single numerical value.
Returns
-------
Base::Vector3 or float
Each of the components of the vector, or the single numerical value,
is rounded to the precision defined by `prec`,
and scaled by the amount of the global variable `resolvedScale`.
To do
-----
Use local variables, not global variables.
"""
pre = Draft.precision()
if isinstance(pt, (int, float)):
v = round(pt, pre)
if resolvedScale != 1:
v = v * resolvedScale
else:
v = Vector(round(pt[0], pre), round(pt[1], pre), round(pt[2], pre))
if resolvedScale != 1:
v.multiply(resolvedScale)
return v
def placementFromDXFOCS(ent):
"""Return the placement of an object from AutoCAD's OCS.
In AutoCAD DXF's the points of each entity are expressed in terms
of the entity's object coordinate system (OCS).
Then to determine the entity's position in 3D space,
what is needed is a 3D vector defining the Z axis of the OCS,
and the elevation value over it.
It uses `WorkingPlane.align_to_point_and_axis()` to align the working plane
to the origin and to `ent.extrusion` (the plane's `axis`).
Then it gets the global coordinates of the entity
by using `WorkingPlane.get_global_coords()`
and either `ent.elevation` (Z coordinate) or `ent.loc` a `(x,y,z)` tuple.
Parameters
----------
ent : A DXF entity
It could be of several types, like `lwpolyline`, `polyline`,
and others, and with `ent.extrusion`, `ent.elevation`
or `ent.loc` attributes.
Returns
-------
Base::Placement
A placement, comprised of a `Base` (`Base::Vector3`),
and a `Rotation` (`Base::Rotation`).
See also
--------
WorkingPlane.align_to_point_and_axis, WorkingPlane.get_global_coords
"""
draftWPlane = WorkingPlane.PlaneBase()
draftWPlane.align_to_point_and_axis(Vector(0.0, 0.0, 0.0), vec(ent.extrusion), 0.0)
# Object Coordinate Systems (OCS)
# http://docs.autodesk.com/ACD/2011/ENU/filesDXF/WS1a9193826455f5ff18cb41610ec0a2e719-7941.htm
# Arbitrary Axis Algorithm
# http://docs.autodesk.com/ACD/2011/ENU/filesDXF/WS1a9193826455f5ff18cb41610ec0a2e719-793d.htm#WSc30cd3d5faa8f6d81cb25f1ffb755717d-7ff5
# Riferimenti dell'algoritmo dell'asse arbitrario in italiano
# http://docs.autodesk.com/ACD/2011/ITA/filesDXF/WS1a9193826455f5ff18cb41610ec0a2e719-7941.htm
# http://docs.autodesk.com/ACD/2011/ITA/filesDXF/WS1a9193826455f5ff18cb41610ec0a2e719-793d.htm#WSc30cd3d5faa8f6d81cb25f1ffb755717d-7ff5
if draftWPlane.axis == FreeCAD.Vector(1.0, 0.0, 0.0):
draftWPlane.u = FreeCAD.Vector(0.0, 1.0, 0.0)
draftWPlane.v = FreeCAD.Vector(0.0, 0.0, 1.0)
elif draftWPlane.axis == FreeCAD.Vector(-1.0, 0.0, 0.0):
draftWPlane.u = FreeCAD.Vector(0.0, -1.0, 0.0)
draftWPlane.v = FreeCAD.Vector(0.0, 0.0, 1.0)
else:
if (abs(ent.extrusion[0]) < (1.0 / 64.0)) and (abs(ent.extrusion[1]) < (1.0 / 64.0)):
draftWPlane.u = FreeCAD.Vector(0.0, 1.0, 0.0).cross(draftWPlane.axis)
else:
draftWPlane.u = FreeCAD.Vector(0.0, 0.0, 1.0).cross(draftWPlane.axis)
draftWPlane.u.normalize()
draftWPlane.v = draftWPlane.axis.cross(draftWPlane.u)
draftWPlane.v.normalize()
draftWPlane.position = Vector(0.0, 0.0, 0.0)
pl = draftWPlane.get_placement()
if (ent.type == "lwpolyline") or (ent.type == "polyline"):
pl.Base = draftWPlane.get_global_coords(vec([0.0, 0.0, ent.elevation]))
else:
pl.Base = draftWPlane.get_global_coords(vec(ent.loc))
return pl
def drawLine(line, forceShape=False):
"""Return a Part shape (Wire or Edge) from a DXF line.
Parameters
----------
line : drawing.entities
The DXF object of type `'line'`.
forceShape : bool, optional
It defaults to `False`. If it is `True` it will produce a `Part.Edge`,
otherwise it produces a `Draft Wire`.
Returns
-------
Part::Feature or Part::TopoShape ('Edge')
The returned object is normally a `Wire`, if the global
variables `dxfCreateDraft` or `dxfCreateSketch` are set,
and `forceShape` is `False`.
Otherwise it produces a `Part.Edge`.
It returns `None` if it fails.
See also
--------
drawBlock
To do
-----
Use local variables, not global variables.
"""
if len(line.points) > 1:
v1 = vec(line.points[0])
v2 = vec(line.points[1])
if not DraftVecUtils.equals(v1, v2):
try:
if (dxfCreateDraft or dxfCreateSketch) and (not forceShape):
return Draft.make_wire([v1, v2], face=False)
else:
return Part.LineSegment(v1, v2).toShape()
except Part.OCCError:
warn(line)
return None
def drawPolyline(polyline, forceShape=False, num=None):
"""Return a Part shape (Wire, Face, or Shell) from a DXF polyline.
It traverses the points of the polyline checking for straight edges,
and for curvatures (bulges) between two points.
Then it produces `Part.Edges` and `Part.Arcs`, and decides what to output
at the end based on the options.
Parameters
----------
polyline : drawing.entities
The DXF object of type `'polyline'` or `'lwpolyline'`.
forceShape : bool, optional
It defaults to `False`. If it is `True` it will try to produce
a `Part.Wire`, otherwise it try to produce a `Draft Wire`.
num : float, optional
It defaults to `None`. A simple number that identifies this polyline.
Returns
-------
Part::Feature or Part::TopoShape ('Wire', 'Face', 'Shell')
It returns `None` if it fails producing a shape.
If the polyline has a `width` and the global variable
`dxfRenderPolylineWidth` is set, it will try to return a face simulating
a thick line. If the polyline is closed, it will cut the interior loop
to produce the a shell.
If the polyline doesn't have curvatures, and the global variables
`dxfCreateDraft` or `dxfCreateSketch` are set, and `forceShape` is `False`
it creates a straight `Draft Wire`.
Otherwise, it will return a `Part.Wire`.
See also
--------
drawBlock
To do
-----
Use local variables, not global variables.
"""
if len(polyline.points) > 1:
edges = []
curves = False
verts = []
for p in range(len(polyline.points) - 1):
p1 = polyline.points[p]
p2 = polyline.points[p + 1]
v1 = vec(p1)
v2 = vec(p2)
verts.append(v1)
if not DraftVecUtils.equals(v1, v2):
if polyline.points[p].bulge:
curves = True
cv = calcBulge(v1, polyline.points[p].bulge, v2)
if DraftVecUtils.isColinear([v1, cv, v2]):
try:
edges.append(Part.LineSegment(v1, v2).toShape())
except Part.OCCError:
warn(polyline, num)
else:
try:
edges.append(Part.Arc(v1, cv, v2).toShape())
except Part.OCCError:
warn(polyline, num)
else:
try:
edges.append(Part.LineSegment(v1, v2).toShape())
except Part.OCCError:
warn(polyline, num)
verts.append(v2)
if polyline.closed:
p1 = polyline.points[len(polyline.points) - 1]
p2 = polyline.points[0]
v1 = vec(p1)
v2 = vec(p2)
cv = calcBulge(v1, polyline.points[-1].bulge, v2)
if not DraftVecUtils.equals(v1, v2):
if DraftVecUtils.isColinear([v1, cv, v2]):
try:
edges.append(Part.LineSegment(v1, v2).toShape())
except Part.OCCError:
warn(polyline, num)
else:
try:
edges.append(Part.Arc(v1, cv, v2).toShape())
except Part.OCCError:
warn(polyline, num)
if edges:
try:
width = rawValue(polyline, 43)
if width and dxfRenderPolylineWidth:
w = Part.Wire(edges)
w1 = w.makeOffset(width / 2)
if polyline.closed:
w2 = w.makeOffset(-width / 2)
w1 = Part.Face(w1)
w2 = Part.Face(w2)
if w1.BoundBox.DiagonalLength > w2.BoundBox.DiagonalLength:
return w1.cut(w2)
else:
return w2.cut(w1)
else:
return Part.Face(w1)
elif (dxfCreateDraft or dxfCreateSketch) and (not curves) and (not forceShape):
# Create parametric Draft.Wire for straight polylines
ob = Draft.make_wire(verts, face=False)
ob.Closed = polyline.closed
ob.Placement = placementFromDXFOCS(polyline)
return ob
else:
w = Part.Wire(edges)
w.Placement = placementFromDXFOCS(polyline)
return w
except Part.OCCError:
warn(polyline, num)
return None
def drawArc(arc, forceShape=False):
"""Return a Part shape (Arc, Edge) from a DXF arc.
Parameters
----------
arc : drawing.entities
The DXF object of type `'arc'`. The `'arc'` object is different from
a `'circle'` because it has different start and end angles.
forceShape : bool, optional
It defaults to `False`. If it is `True` it will try to produce
a `Part.Edge`, otherwise it tries to produce a `Draft Arc`.
Returns
-------
Part::Part2DObject or Part::TopoShape ('Edge')
The returned object is normally a `Draft Arc` with no face,
if the global variables `dxfCreateDraft` or `dxfCreateSketch` are set,
and `forceShape` is `False`.
Otherwise it produces a `Part.Edge`.
It returns `None` if it fails producing a shape.
See also
--------
drawCircle, drawBlock
To do
-----
Use local variables, not global variables.
"""
pre = Draft.precision()
pl = placementFromDXFOCS(arc)
rad = vec(arc.radius)
firstangle = round(arc.start_angle % 360, pre)
lastangle = round(arc.end_angle % 360, pre)
try:
if (dxfCreateDraft or dxfCreateSketch) and (not forceShape):
return Draft.make_circle(rad, pl, face=False, startangle=firstangle, endangle=lastangle)
else:
circle = Part.Circle()
circle.Radius = rad
shape = circle.toShape(math.radians(firstangle), math.radians(lastangle))
shape.Placement = pl
return shape
except Part.OCCError:
warn(arc)
return None
def drawCircle(circle, forceShape=False):
"""Return a Part shape (Circle, Edge) from a DXF circle.
Parameters
----------
circle : drawing.entities
The DXF object of type `'circle'`. The `'circle'` object is different
from an `'arc'` because the circle forms a full circumference.
forceShape : bool, optional
It defaults to `False`. If it is `True` it will try to produce
a `Part.Edge`, otherwise it tries to produce a `Draft Circle`.
Returns
-------
Part::Part2DObject or Part::TopoShape ('Edge')
The returned object is normally a `Draft Circle` with no face,
if the global variables `dxfCreateDraft` or `dxfCreateSketch` are set,
and `forceShape` is `False`.
Otherwise it produces a `Part.Edge`.
It returns `None` if it fails producing a shape.
See also
--------
drawArc, drawBlock
To do
-----
Use local variables, not global variables.
"""
pl = placementFromDXFOCS(circle)
rad = vec(circle.radius)
try:
if (dxfCreateDraft or dxfCreateSketch) and (not forceShape):
return Draft.make_circle(rad, pl, face=False)
else:
curve = Part.Circle()
curve.Radius = rad
shape = curve.toShape()
shape.Placement = pl
return shape
except Part.OCCError:
warn(circle)
return None
def drawEllipse(ellipse, forceShape=False):
"""Return a Part shape (Ellipse, Edge) from a DXF ellipse.
Parameters
----------
ellipse : drawing.entities
The DXF object of type `'ellipse'`. The ellipse can be a full ellipse
or an elliptical arc.
forceShape : bool, optional
It defaults to `False`. If it is `True` it will try to produce
a `Part.Edge`, otherwise it tries to produce a `Draft Ellipse`.
Returns
-------
Part::Part2DObject or Part::TopoShape ('Edge')
The returned object is normally a `Draft Ellipse` with a face,
if the global variables `dxfCreateDraft` or `dxfCreateSketch` are set,
and `forceShape` is `False`.
Otherwise it produces a `Part.Edge`.
It returns `None` if it fails producing a shape.
See also
--------
drawArc, drawCircle
To do
-----
Use local variables, not global variables.
"""
try:
pre = Draft.precision()
c = vec(ellipse.loc)
start = round(ellipse.start_angle, pre)
end = round(ellipse.end_angle, pre)
majv = vec(ellipse.major)
majr = majv.Length
minr = majr * ellipse.ratio
el = Part.Ellipse(vec((0, 0, 0)), majr, minr)
x = majv.normalize()
z = vec(ellipse.extrusion).normalize()
y = z.cross(x)
m = DraftVecUtils.getPlaneRotation(x, y)
pl = FreeCAD.Placement(m)
pl.move(c)
if (dxfCreateDraft or dxfCreateSketch) and (not forceShape):
if (start != 0.0) or ((end != 0.0) or (end != round(math.pi / 2, pre))):
shape = el.toShape(start, end)
shape.Placement = pl
return shape
else:
return Draft.make_ellipse(majr, minr, pl, face=False)
else:
shape = el.toShape(start, end)
shape.Placement = pl
return shape
except Part.OCCError:
warn(arc)
return None
def drawFace(face):
"""Return a Part face from a list of points.
Parameters
----------
face : drawing.entities
The DXF object of type `'3dface'`.
Returns
-------
Part::TopoShape ('Face')
The returned object is a `Part.Face`.
It returns `None` if it fails producing a shape.
"""
pl = []
for p in face.points:
pl.append(vec(p))
p1 = face.points[0]
pl.append(vec(p1))
try:
pol = Part.makePolygon(pl)
return Part.Face(pol)
except Part.OCCError:
warn(face)
return None
def drawMesh(mesh, forceShape=False):
"""Return a Mesh (Mesh, Shell) from a DXF mesh.
Parameters
----------
mesh : drawing.entities
The DXF object of type `'polyline'` or `'lwpolyline'`
with `flags` of 16 (3D polygon mesh) or 64 (polyface mesh).
forceShape : bool, optional
It defaults to `False`. If it is `True` it will try to produce
a `Part.Shape` of type `'Shell'`,
otherwise it tries to produce a `Mesh::MeshObject`.
Returns
-------
Mesh::MeshObject or Part::TopoShape ('Shell')
The returned object is normally a `Mesh` if `forceShape` is `False`.
Otherwise it produces a `Part.Shape` of type `'Shell'`.
It returns `None` if it fails producing a shape.
See also
--------
drawBlock
"""
md = []
if mesh.flags == 16:
pts = mesh.points
udim = rawValue(mesh, 71)
vdim = rawValue(mesh, 72)
for u in range(udim - 1):
for v in range(vdim - 1):
b = u + v * udim
p1 = pts[b]
p2 = pts[b + 1]
p3 = pts[b + udim]
p4 = pts[b + udim + 1]
md.append([p1, p2, p4])
md.append([p1, p4, p3])
elif mesh.flags == 64:
pts = []
fcs = []
for p in mesh.points:
if p.flags == 192:
pts.append(p)
elif p.flags == 128:
fcs.append(p)
# print("Creating polyface with", len(pts),
# "points and", len(fcs), "facets")
for f in fcs:
p1 = pts[abs(rawValue(f, 71)) - 1]
p2 = pts[abs(rawValue(f, 72)) - 1]
p3 = pts[abs(rawValue(f, 73)) - 1]
md.append([p1, p2, p3])
if rawValue(f, 74) is not None:
p4 = pts[abs(rawValue(f, 74)) - 1]
md.append([p1, p3, p4])
try:
m = Mesh.Mesh(md)
if forceShape:
s = Part.Shape()
s.makeShapeFromMesh(m.Topology, 1)
s = s.removeSplitter()
m = s
except FreeCAD.Base.FreeCADError:
warn(mesh)
else:
return m
return None
def drawSolid(solid):
"""Return a Part shape (Face) from a DXF solid.
It takes three or four points from a `solid`, if possible.
It adds the first point again to the end of the points list, and creates
a polygon, which is then used to create a face.
Parameters
----------
solid : drawing.entities
The DXF object of type `'solid'`.
Returns
-------
Part::TopoShape ('Face')
The returned object is a `Part.Face`.
It returns `None` if it fails producing a shape.
See also
--------
drawBlock
"""
p4 = None
p1x = rawValue(solid, 10)
p1y = rawValue(solid, 20)
p1z = rawValue(solid, 30) or 0
p2x = rawValue(solid, 11)
p2y = rawValue(solid, 21)
p2z = rawValue(solid, 31) or p1z
p3x = rawValue(solid, 12)
p3y = rawValue(solid, 22)
p3z = rawValue(solid, 32) or p1z
p4x = rawValue(solid, 13)
p4y = rawValue(solid, 23)
p4z = rawValue(solid, 33) or p1z
p1 = Vector(p1x, p1y, p1z)
p2 = Vector(p2x, p2y, p2z)
p3 = Vector(p3x, p3y, p3z)
if p4x is not None:
p4 = Vector(p4x, p4y, p4z)
if p4 and (p4 != p3) and (p4 != p2) and (p4 != p1):
try:
return Part.Face(Part.makePolygon([p1, p2, p4, p3, p1]))
except Part.OCCError:
warn(solid)
else:
try:
return Part.Face(Part.makePolygon([p1, p2, p3, p1]))
except Part.OCCError:
warn(solid)
return None
def drawSplineIterpolation(verts, closed=False, forceShape=False, alwaysDiscretize=False):
"""Return a wire or spline, opened or closed.
Parameters
----------
verts : Base::Vector3
A list of points.
closed : bool, optional
It defaults to `False`. If it is `True` it will create a closed
Wire, closed BSpline, or a Face.
forceShape : bool, optional
It defaults to `False`. If it is `True` it will try to produce
a `Part.Shape` of type `'Edge'` or `'Face'`.
Otherwise it tries to produce a `Draft Wire` or `Draft BSpline`.
alwaysDiscretize : bool, optional
It defaults to `False`. If it is `True` it will try to produce
straight lines (Wires, Edges).
Otherwise it will try to produce BSplines.
Returns
-------
Part::Feature or Part::TopoShape ('Edge', 'Face')
The returned object is normally a `Draft Wire` or `Draft BSpline`,
if the global variables `dxfCreateDraft` or `dxfCreateSketch` are set,
and `forceShape` is `False`.
It is a `Draft Wire` if the global variables
`dxfDiscretizeCurves` or `alwaysDiscretize` are `True`,
and a `Draft BSpline` otherwise.
Otherwise it produces a `Part.Wire`.
To do
-----
Use local variables, not global variables.
"""
if (dxfCreateDraft or dxfCreateSketch) and (not forceShape):
if dxfDiscretizeCurves or alwaysDiscretize:
ob = Draft.make_wire(verts, face=False)
else:
ob = Draft.make_bspline(verts, face=False)
ob.Closed = closed
return ob
else:
if dxfDiscretizeCurves or alwaysDiscretize:
sh = Part.makePolygon(verts + [verts[0]])
else:
sp = Part.BSplineCurve()
# print(knots)
sp.interpolate(verts)
sh = Part.Wire(sp.toShape())
return sh
def drawSplineOld(spline, forceShape=False):
"""Return a Part Shape from a DXF spline. DEPRECATED.
It takes the vertices from the spline data,
considers the value from code 70 to know if the spline
is closed or not, and then calls
`drawSplineIterpolation(verts, closed, forceShape)`.
Parameters
----------
spline : drawing.entities
The DXF object of type `'spline'`.
forceShape : bool, optional
It defaults to `False`. If it is `True` it will try to produce
a `Part.Shape` of type `'Edge'` or `'Face'`.
Otherwise it tries to produce a `Draft Wire` or `Draft BSpline`.
Returns
-------
Part::Feature or Part::TopoShape ('Edge', 'Face')
The returned object is normally a `Draft Wire` or `Draft BSpline`
as returned from `drawSplineIterpolation()`.
It returns `None` if it fails producing a shape.
See also
--------
drawSplineIterpolation
"""
flag = rawValue(spline, 70)
if flag == 1:
closed = True
else:
closed = False
verts = []
knots = []
for dline in spline.data:
if dline[0] == 10:
cp = [dline[1]]
elif dline[0] == 20:
cp.append(dline[1])
elif dline[0] == 30:
cp.append(dline[1])
pt = vec(cp)
if verts:
if pt != verts[-1]:
verts.append(pt)
else:
verts.append(pt)
elif dline[0] == 40:
knots.append(dline[1])
try:
return drawSplineIterpolation(verts, closed, forceShape)
except Part.OCCError:
warn(spline)
return None
def drawSpline(spline, forceShape=False):
"""Return a Part Shape (BSpline, Wire) from a DXF spline.
A BSpline may be defined in several ways, by knots,
control points, fit points, and weights.
The function searches all values to determine the best way
of building the BSpline with Draft or Part tools.
Parameters
----------
spline : drawing.entities
The DXF object of type `'spline'`.
forceShape : bool, optional
It defaults to `False`. If it is `True` it will try to produce
a `Part.Shape` of type `'Wire'`.
Otherwise it tries to produce a `Draft BSpline`.
Returns
-------
Part::Feature or Part::TopoShape ('Edge', 'Face')
The returned object is normally a `Draft BezCurve`
created with `Draft.make_bezcurve(controlpoints, degree=degree)`,
if `forceShape` is `False` and there are no weights.
Otherwise it tries to return a `Part.Shape` of type `'Wire'`,
by first creating a Bezier curve with `Part.BezierCurve()`.
If it's impossible to create the BSpline in this way,
it will try to create an interpolated BSpline with
`drawSplineIterpolation(controlpoints)`.
If fit points exist and control points do not,
it will try to create an interpolated BSpline with
`drawSplineIterpolation(fitpoints)`.
In other cases it will try to create a `Part.Shape`
from a BSpline, using the available control points,
multiplicity vector, the kot vector, the degree,
the periodic data, and the weights.
It returns `None` if it fails producing a shape.
Raises
------
ValueError
If there are wrong number of knots, wrong number of control points,
wrong number of fit points, an inconsistent rational flag, or wrong
number of weights.
See also
--------
drawBlock, Draft.make_bezcurve, Part.BezierCurve, drawSplineIterpolation,
Part.BSplineCurve.buildFromPolesMultsKnots
To do
----
As there is currently no Draft primitive to handle splines
the result is a non-parametric curve.
**2019:** There is a `Draft BSpline` now, but it's not used.
"""
flags = rawValue(spline, 70)
closed = (flags & 1) != 0
periodic = (flags & 2) != 0 and False # workaround
rational = (flags & 4) != 0
planar = (flags & 8) != 0
linear = (flags & 16) != 0
degree = rawValue(spline, 71)
nbknots = rawValue(spline, 72) or 0
nbcontrolp = rawValue(spline, 73) or 0
nbfitp = rawValue(spline, 74) or 0
knots = []
weights = []
controlpoints = []
fitpoints = []
# parse the knots and points
dataremain = spline.data[:]
while len(dataremain) > 0:
groupnumber = dataremain[0][0]
if groupnumber == 40: # knot
knots.append(dataremain[0][1])
dataremain = dataremain[1:]
elif groupnumber == 41: # weight
weights.append(dataremain[0][1])
dataremain = dataremain[1:]
elif groupnumber in (10, 11): # control or fit point
x = dataremain[0][1]
if dataremain[1][0] in (20, 21):
y = dataremain[1][1]
if dataremain[2][0] in (30, 31):
z = dataremain[2][1]
dataremain = dataremain[3:]
else:
z = 0.0
dataremain = dataremain[2:]
else:
y = 0.0
dataremain = dataremain[1:]
v = vec([x, y, z])
if groupnumber == 10:
controlpoints.append(v)
elif groupnumber == 11:
fitpoints.append(v)
else:
dataremain = dataremain[1:]
# print(groupnumber)
if nbknots != len(knots):
raise ValueError("Wrong number of knots")
if nbcontrolp != len(controlpoints):
raise ValueError("Wrong number of control points")
if nbfitp != len(fitpoints):
raise ValueError("Wrong number of fit points")
if rational == all((w == 1.0 or w is None) for w in weights):
raise ValueError("Inconsistent rational flag")
if len(weights) == 0:
weights = None
elif len(weights) != len(controlpoints):
raise ValueError("Wrong number of weights")
# build knotvector and multvector
# this means to remove duplicate knots
multvector = []
knotvector = []
mult = 0
previousknot = None
for knotvalue in knots:
if knotvalue == previousknot:
mult += 1
else:
if mult > 0:
multvector.append(mult)
mult = 1
previousknot = knotvalue
knotvector.append(knotvalue)
multvector.append(mult)
# check if the multiplicities are valid
innermults = multvector[:] if periodic else multvector[1:-1]
if any(m > degree for m in innermults): # invalid
if all(m == degree + 1 for m in multvector):
if not forceShape and weights is None:
points = controlpoints[:]
del points[degree + 1 :: degree + 1]
return Draft.make_bezcurve(points, degree=degree)
else:
poles = controlpoints[:]
edges = []
while len(poles) >= degree + 1:
# bezier segments
bzseg = Part.BezierCurve()
bzseg.increase(degree)
bzseg.setPoles(poles[0 : degree + 1])
poles = poles[degree + 1 :]
if weights is not None:
bzseg.setWeights(weights[0 : degree + 1])
weights = weights[degree + 1 :]
edges.append(bzseg.toShape())
return Part.Wire(edges)
else:
warn("polygon fallback on %s" % spline)
return drawSplineIterpolation(
controlpoints, closed=closed, forceShape=forceShape, alwaysDiscretize=True
)
if fitpoints and not controlpoints:
return drawSplineIterpolation(fitpoints, closed=closed, forceShape=forceShape)
try:
bspline = Part.BSplineCurve()
bspline.buildFromPolesMultsKnots(
poles=controlpoints,
mults=multvector,
knots=knotvector,
degree=degree,
periodic=periodic,
weights=weights,
)
return bspline.toShape()
except Part.OCCError:
warn(spline)
return None
def drawBlock(blockref, num=None, createObject=False):
"""Return a Part Shape (Compound) from a DXF block reference.
It inspects the `blockref.entities` for objects of types `'line'`,
`'polyline'`, `'lwpolyline'`, `'arc'`, `'circle'`, `'insert'`,
`'solid'`, and `'spline'`.
If they are found they create shapes with `drawLine`,
`drawMesh` or `drawPolyline`, `drawArc`, `drawCircle`, `drawInsert`,
`drawSolid`, `drawSpline`, and adds all shapes to a list.
Then it makes a compound of all those shapes.
In the case of entities of type `'text'` and `'mtext'`
it will only process the entities if the global variable
`dxfImportTexts` exist, and `dxfImportLayouts` exists
or if the DXF code 67 doesn't indicate an empty space (empty text).
Then it will use `addText` and add the found text to its proper
layer.
Parameters
----------
blockref : drawing.blocks.data
The DXF block data.
num : float, optional
It defaults to `None`. A simple number that identifies
the given `blockref`.
createObject : bool, optional
It defaults to `False`. If it is `True` it will try to produce
and return a `'Part::Feature'` with the compound
as its shape attribute.
Otherwise, just return the `Part.Compound`.
Returns
-------
Part::TopoShape ('Compound') or Part::Feature
The returned object is normally a `Part.Compound`
created from the list of all `Part.Shapes` created from
the `blockref` entities, if `createObject` is `False`.
Otherwise, it will return a `'Part::Feature'` document object
with the compound as its shape attribute.
In the first case, it will add the compound shape
to the global dictionary `blockshapes`.
In the latter case, it will add the `'Part::Feature'` object
to the global dictionary `blockobjects`.
It returns `None` if the global variable `dxfStarBlocks`
doesn't exist, if the `blockref.entities.data` is empty,
or if it fails producing the compound shape.
See also
--------
`drawLine`, `drawMesh`, `drawPolyline`, `drawArc`, `drawCircle`,
`drawInsert`, `drawSolid`, `drawSpline`, `addText`.
To do
-----
Use local variables, not global variables.
"""
if not dxfStarBlocks:
if blockref.name[0] == "*":
return None
if len(blockref.entities.data) == 0:
print("skipping empty block ", blockref.name)
return None
# print("creating block ", blockref.name,
# " containing ", len(blockref.entities.data), " entities")
shapes = []
for line in blockref.entities.get_type("line"):
s = drawLine(line, forceShape=True)
if s:
shapes.append(s)
for polyline in blockref.entities.get_type("polyline"):
if hasattr(polyline, "flags") and polyline.flags in [16, 64]:
s = drawMesh(polyline, forceShape=True)
else:
s = drawPolyline(polyline, forceShape=True)
if s:
shapes.append(s)
for polyline in blockref.entities.get_type("lwpolyline"):
s = drawPolyline(polyline, forceShape=True)
if s:
shapes.append(s)
for arc in blockref.entities.get_type("arc"):
s = drawArc(arc, forceShape=True)
if s:
shapes.append(s)
for circle in blockref.entities.get_type("circle"):
s = drawCircle(circle, forceShape=True)
if s:
shapes.append(s)
for insert in blockref.entities.get_type("insert"):
# print("insert ",insert," in block ",insert.block[0])
if dxfStarBlocks or insert.block[0] != "*":
s = drawInsert(insert)
if s:
shapes.append(s)
for solid in blockref.entities.get_type("solid"):
s = drawSolid(solid)
if s:
shapes.append(s)
for spline in blockref.entities.get_type("spline"):
s = drawSpline(spline, forceShape=True)
if s:
shapes.append(s)
for text in blockref.entities.get_type("text"):
if dxfImportTexts:
if dxfImportLayouts or (not rawValue(text, 67)):
addText(text)
for text in blockref.entities.get_type("mtext"):
if dxfImportTexts:
if dxfImportLayouts or (not rawValue(text, 67)):
print("adding block text", text.value, " from ", blockref)
addText(text)
try:
shape = Part.makeCompound(shapes)
except Part.OCCError:
warn(blockref)
if shape:
blockshapes[blockref.name] = shape
if createObject:
newob = doc.addObject("Part::Feature", blockref.name)
newob.Shape = shape
blockobjects[blockref.name] = newob
return newob
return shape
return None
def drawInsert(insert, num=None, clone=False):
"""Return a Part Shape (Compound, Clone) from a DXF insert.
It searches for `insert.block` in `blockobjects`
or `blockshapes`, and returns a clone or a copy of the compound,
with transformations applied: rotation, translation (movement),
and scaling.
If the global variable `dxfImportTexts` is available
it will check the attributes of `insert` and add those text attributes
to their own layers with `addText`.
Parameters
----------
insert : drawing.entities
The DXF object of type `'insert'`.
num : float, optional
It defaults to `None`. A simple number that identifies
the given block being drawn, if it is not a clone.
clone : bool, optional
It defaults to `False`. If it is `True` it will try to produce
and return a `Draft Clone` of the `'insert.block'` contained
in the global dictionary `blockobjects`.
Otherwise, it will try to return a copy of the shape
of the `'insert.block'` contained in the global dictionary
`blockshapes`, or created from the `drawing.blocks.data`
with `drawBlock()`.
Returns
-------
Part::TopoShape ('Compound') or
Part::Part2DObject or Part::Feature (`Draft Clone`)
The returned object is normally a copy of the `Part.Compound`
extracted from `blockshapes` or created with `drawBlock()`.
If `clone` is `True` then it will try returning
a `Draft Clone` from the `'insert.block'` contained
in the global dictionary `blockobjects`.
It returns `None` if `insert.block` isn't in `blockobjects`.
In any of these two cases, it will try to apply the
insert transformations: rotation, translation (movement),
and scaling.
See also
--------
drawBlock
To do
-----
Use local variables, not global variables.
"""
if dxfImportTexts:
attrs = attribs(insert)
for a in attrs:
addText(a, attrib=True)
if clone:
if insert.block in blockobjects:
newob = Draft.make_clone(blockobjects[insert.block])
tsf = FreeCAD.Matrix()
rot = math.radians(insert.rotation)
pos = vec(insert.loc)
tsf.move(pos)
tsf.rotateZ(rot)
sc = insert.scale
sc = vec([sc[0], sc[1], 0])
newob.Placement = FreeCAD.Placement(tsf)
newob.Scale = sc
return newob
else:
shape = None
else:
if insert in blockshapes:
shape = blockshapes[insert.block].copy()
else:
shape = None
for b in drawing.blocks.data:
if b.name == insert.block:
shape = drawBlock(b, num)
if shape:
pos = vec(insert.loc)
rot = math.radians(insert.rotation)
scale = insert.scale
tsf = FreeCAD.Matrix()
# for some reason z must be 0 to work
# tsf.scale(scale[0], scale[1], 0)
tsf.rotateZ(rot)
try:
shape = shape.transformGeometry(tsf)
except Part.OCCError:
tsf.scale(scale[0], scale[1], 0)
try:
shape = shape.transformGeometry(tsf)
except Part.OCCError:
print("importDXF: unable to apply insert transform:", tsf)
shape.translate(pos)
return shape
return None
def drawLayerBlock(objlist, name="LayerBlock"):
"""Return a Draft Block (compound) from the given object list.
Parameters
----------
objlist : list
A list of Draft objects or Part.shapes.
Returns
-------
Part::Feature or Part::TopoShape ('Compound')
If the global variables `dxfCreateDraft` or `dxfCreateSketch` are set,
and no element in `objlist` is a `Part.Shape`,
it will try to return a `Draft Block`.
Otherwise, it will try to return a `Part.Compound`.
It returns `None` if it fails producing a shape.
To do
-----
Use local variables, not global variables.
"""
isObj = True
for o in objlist:
if isinstance(o, Part.Shape):
isObj = False
obj = None
if (dxfCreateDraft or dxfCreateSketch) and isObj:
try:
# obj = Draft.make_block(objlist)
obj = doc.addObject("Part::Compound", name)
obj.Links = objlist
except Part.OCCError:
pass
else:
try:
obj = Part.makeCompound(objlist)
except Part.OCCError:
pass
return obj
def attribs(insert):
"""Check if an insert has attributes, and return the values if positive.
It checks the `drawing.entities.data` for the `insert`,
and saves the index of the element.
Then it iterates again looking for entities with an `'attrib'`,
collecting the entities in a list.
Parameters
----------
insert : drawing.entities
The DXF object of type `'insert'`.
Returns
-------
list
It returns a list with the entities that have `'attrib'` data,
until `'seqend'` is found.
It returns an empty list `[]`, if DXF code 66 ("Entities follow")
is different from 1, or if the `insert` is not found
in `drawing.entities.data`.
"""
atts = []
if rawValue(insert, 66) != 1:
return []
index = None
for i in range(len(drawing.entities.data)):
if drawing.entities.data[i] == insert:
index = i
break
if index is None:
return []
j = index + 1
while True:
ent = drawing.entities.data[j]
if str(ent) == "seqend":
return atts
elif str(ent) == "attrib":
atts.append(ent)
j += 1
def addObject(shape, name="Shape", layer=None):
"""Adds a new object to the document, with the given name and layer.
Parameters
----------
shape : Part.Shape or Part::Feature
The simple Part.Shape or Draft object previously created
from an entity in a DXF file.
name : str, optional
It defaults to "Shape". The name of the new document object.
layer : App::FeaturePython or App::DocumentObjectGroup, optional
It defaults to `None`.
The `Draft Layer` (`App::FeaturePython`)
or simple group (`App::DocumentObjectGroup`)
to which the new object will be added.
Returns
-------
Part::Feature or Part::Part2DObject
If the `shape` is a simple `Part.Shape`, it will be encapsulated
inside a `Part::Feature` object and this will be returned. Otherwise,
it is assumed it is already a Draft object which will just be returned.
It applies the text and line color by calling `formatObject()`
before returning the new object.
"""
if isinstance(shape, Part.Shape):
newob = doc.addObject("Part::Feature", name)
newob.Shape = shape
else:
newob = shape
if layer:
lay = locateLayer(layer)
# For old style layers, which are just groups
if hasattr(lay, "Group"):
pass
# For new Draft Layers
elif hasattr(lay, "Proxy") and hasattr(lay.Proxy, "Group"):
lay = lay.Proxy
else:
lay = None
if lay != None:
if lay not in layerObjects:
l = []
layerObjects[lay] = l
else:
l = layerObjects[lay]
l.append(newob)
formatObject(newob)
return newob
def addText(text, attrib=False):
"""Add a new Draft Text object to the document.
It creates a `Draft Text` from the `text` entity,
and adds the new object to its indicated layer,
creating it if it doesn't exist.
It also applies its rotation, position, justification
('center' or 'right'), and color.
If the graphical interface is available, together with the Draft toolbar,
as well as the global variable `dxfUseStandardSize`, it will
use the toolbar's indicated font size.
Otherwise, it will use the text's height scaled by the value of
the global variable `TEXTSCALING`.
Parameters
----------
text : drawing.entities
The DXF object of type `'text'` or `'mtext'`.
attrib : bool, optional
It defaults to `False`.
If `True` it determines from the DXF:
- the text value - code 1,
- the text colour - code 6,
- the text font - code 7,
- the layer name - code 8,
- the position (X, Y, Z) - codes 10, 20, 30,
- the text height - from code 40,
- the rotation angle - from code 50,
and assigns the name `'Attribute'`.
Otherwise, it assumes these values from `text`
and sets the name to `'Text'`.
See also
--------
locateLayer, drawBlock, Draft.make_text
To do
-----
Use local variables, not global variables.
"""
if attrib:
lay = locateLayer(rawValue(text, 8))
val = rawValue(text, 1)
pos = vec([rawValue(text, 10), rawValue(text, 20), rawValue(text, 30)])
hgt = vec(rawValue(text, 40))
else:
lay = locateLayer(text.layer)
val = text.value
pos = vec(text.loc)
hgt = vec(text.height)
if val:
if attrib:
name = "Attribute"
else:
name = "Text"
val = deformat(val)
newob = Draft.make_text(val.split("\n"))
if hasattr(lay, "addObject"):
lay.addObject(newob)
elif hasattr(lay, "Proxy") and hasattr(lay.Proxy, "addObject"):
lay.Proxy.addObject(lay, newob)
rx = rawValue(text, 11)
ry = rawValue(text, 21)
rz = rawValue(text, 31)
xv = Vector(1, 0, 0)
ax = Vector(0, 0, 1)
if rx or ry or rz:
xv = vec([rx, ry, rz])
if not DraftVecUtils.isNull(xv):
ax = (xv.cross(Vector(1, 0, 0))).negative()
if DraftVecUtils.isNull(ax):
ax = Vector(0, 0, 1)
ang = -math.degrees(DraftVecUtils.angle(xv, Vector(1, 0, 0), ax))
Draft.rotate(newob, ang, axis=ax)
if ax == Vector(0, 0, -1):
ax = Vector(0, 0, 1)
elif hasattr(text, "rotation"):
if text.rotation:
Draft.rotate(newob, text.rotation)
if attrib:
attrot = rawValue(text, 50)
if attrot:
Draft.rotate(newob, attrot)
if gui and draftui and dxfUseStandardSize:
fsize = draftui.fontsize
else:
fsize = float(hgt) * TEXTSCALING
if hasattr(text, "alignment"):
yv = ax.cross(xv)
if text.alignment in [1, 2, 3]:
sup = DraftVecUtils.scaleTo(yv, fsize / TEXTSCALING).negative()
# print(ax, sup)
pos = pos.add(sup)
elif text.alignment in [4, 5, 6]:
sup = DraftVecUtils.scaleTo(yv, fsize / (2 * TEXTSCALING)).negative()
pos = pos.add(sup)
newob.Placement.Base = pos
if gui:
newob.ViewObject.FontSize = fsize
if hasattr(text, "alignment"):
if text.alignment in [2, 5, 8]:
newob.ViewObject.Justification = "Center"
elif text.alignment in [3, 6, 9]:
newob.ViewObject.Justification = "Right"
# newob.ViewObject.DisplayMode = "World"
formatObject(newob, text)
def addToBlock(obj, layer):
"""Add the given object to the layer in the global dictionary.
It searches for `layer` in the global dictionary `layerBlocks`.
If found, it appends the `obj` to the `layer`;
otherwise, it adds the `layer` to `layerBlocks` first,
and then adds `obj`.
Parameters
----------
obj : Part.Shape or App::DocumentObject
Any shape or Draft object previously created from a DXF file.
layer : str
The name of a layer to which `obj` is added.
To do
-----
Use local variables, not global variables.
"""
if layer in layerBlocks:
layerBlocks[layer].append(obj)
else:
layerBlocks[layer] = [obj]
def getScaleFromDXF(header):
"""Get the scale from the header of a drawing object.
Parameters
----------
header : header object
"""
data = header.data
insunits = 0
if [9, "$INSUNITS"] in data:
insunits = data[data.index([9, "$INSUNITS"]) + 1][1]
if insunits == 0 and [9, "$MEASUREMENT"] in data:
measurement = data[data.index([9, "$MEASUREMENT"]) + 1][1]
insunits = 1 if measurement == 0 else 4
if insunits == 0:
# Unspecified
return 1.0
if insunits == 1:
# Inches
return 25.4
if insunits == 2:
# Feet
return 25.4 * 12
if insunits == 3:
# Miles
return 1609344.0
if insunits == 4:
# Millimeters
return 1.0
if insunits == 5:
# Centimeters
return 10.0
if insunits == 6:
# Meters
return 1000.0
if insunits == 7:
# Kilometers
return 1000000.0
if insunits == 8:
# Microinches
return 25.4 / 1000.0
if insunits == 9:
# Mils
return 25.4 / 1000.0
if insunits == 10:
# Yards
return 3 * 12 * 25.4
if insunits == 11:
# Angstroms
return 0.0000001
if insunits == 12:
# Nanometers
return 0.000001
if insunits == 13:
# Microns
return 0.001
if insunits == 14:
# Decimeters
return 100.0
if insunits == 15:
# Decameters
return 10000.0
if insunits == 16:
# Hectometers
return 100000.0
if insunits == 17:
# Gigameters
return 1000000000000.0
if insunits == 18:
# AstronomicalUnits
return 149597870690000.0
if insunits == 19:
# LightYears
return 9454254955500000000.0
if insunits == 20:
# Parsecs
return 30856774879000000000.0
# Unsupported
return 1.0
def processdxf(document, filename, getShapes=False, reComputeFlag=True):
"""Process the DXF file, creating Part objects in the document.
If the `dxfReader` module is not available run `getDXFlibs()`
to get the required libraries and `readPreferences()`.
It defines the global variables `drawing`, `layers`, `doc`,
`blockshapes`, `blockobjects`, `badobjects`, `layerBlocks`.
The read data is placed in the object `drawing`.
It iterates over `drawing.tables` to find tables of type `'layer'`,
and adds them to the document considering its color and drawing style.
Then it iterates over the `drawing.entities` processing the most common
drawing types, that include `'line'`, `'lwpolyline'`, `'polyline'`,
`'arc'`, `'circle'`, `'solid'`, `'spline'`, `'ellipse'`, `'mtext'`,
`'text'`, and `'3dface'`.
If `getShapes` is `False` it will additionally process the types
`'dimension'`, `'point'`, `'leader'`, `'hatch'`, and `'insert'`.
Parameters
----------
document : App::Document
A document object opened in which to create the new Part shapes.
filename : str
The path to the DXF file to process.
getShapes : bool, optional
It defaults to `False`. If it is `True` it will try creating
simple `Part Shapes` instead of Draft objects,
and will immediately return the list of the most common shapes
without processing the entities of types `'dimension'`, `'point'`,
`'leader'`, `'hatch'`, and `'insert'`.
reComputeFlag : bool, optional
It defaults to `True`, in which case it recomputes the document
after finishing processing of the entities.
Otherwise, it skips the recompute.
The recompute causes OpenSCAD import to loop, so this flag
can be set to `False` to prevent this.
Returns
-------
list of `Part.Shapes`
It returns `None` if the edges (lines, polylines, arcs)
are above 100, and the user decides to interrupt (graphically)
the process of joining them.
To do
-----
Use local variables, not global variables.
"""
# for debugging the drawing variable is global so it is still accessible
# after running the script
global drawing
if not dxfReader:
getDXFlibs()
readPreferences()
FCC.PrintMessage("opening " + filename + "...\n")
drawing = dxfReader.readDXF(filename)
global resolvedScale
resolvedScale = getScaleFromDXF(drawing.header) * dxfScaling
global layers
typ = "Layer" if dxfUseDraftVisGroups else "App::DocumentObjectGroup"
layers = [o for o in FreeCAD.ActiveDocument.Objects if Draft.getType(o) == typ]
global doc
doc = document
global blockshapes
blockshapes = {}
global blockobjects
blockobjects = {}
global badobjects
badobjects = []
global layerBlocks
layerBlocks = {}
global layerObjects
layerObjects = {}
sketch = None
shapes = []
# Create layers
if hasattr(drawing, "tables"):
for table in drawing.tables.get_type("table"):
for layer in table.get_type("layer"):
name = layer.name
color = tuple(dxfColorMap.color_map[abs(layer.color)])
drawstyle = "Solid"
lt = rawValue(layer, 6)
if "DASHED" in lt.upper():
drawstyle = "Dashed"
elif "HIDDEN" in lt.upper():
drawstyle = "Dotted"
if ("DASHDOT" in lt.upper()) or ("CENTER" in lt.upper()):
drawstyle = "Dashdot"
locateLayer(name, color, drawstyle, layer.color > 0)
else:
locateLayer("0", (0.0, 0.0, 0.0), "Solid")
# Draw lines
lines = drawing.entities.get_type("line")
if lines:
FCC.PrintMessage("drawing " + str(len(lines)) + " lines...\n")
for line in lines:
if dxfImportLayouts or (not rawValue(line, 67)):
shape = drawLine(line)
if shape:
if dxfCreateSketch:
FreeCAD.ActiveDocument.recompute()
if dxfMakeBlocks or dxfJoin:
if sketch:
shape = Draft.make_sketch(shape, autoconstraints=True, addTo=sketch)
else:
shape = Draft.make_sketch(shape, autoconstraints=True)
sketch = shape
else:
shape = Draft.make_sketch(shape, autoconstraints=True)
elif dxfJoin or getShapes:
if isinstance(shape, Part.Shape):
shapes.append(shape)
else:
shapes.append(shape.Shape)
elif dxfMakeBlocks:
addToBlock(shape, line.layer)
else:
newob = addObject(shape, "Line", line.layer)
if gui:
formatObject(newob, line)
# Draw polylines
pls = drawing.entities.get_type("lwpolyline")
pls.extend(drawing.entities.get_type("polyline"))
polylines = []
meshes = []
for p in pls:
if hasattr(p, "flags"):
if p.flags in [16, 64]:
meshes.append(p)
else:
polylines.append(p)
else:
polylines.append(p)
if polylines:
FCC.PrintMessage("drawing " + str(len(polylines)) + " polylines...\n")
num = 0
for polyline in polylines:
if dxfImportLayouts or (not rawValue(polyline, 67)):
shape = drawPolyline(polyline, num=num)
if shape:
if dxfCreateSketch:
if isinstance(shape, Part.Shape):
t = FreeCAD.ActiveDocument.addObject("Part::Feature", "Shape")
t.Shape = shape
shape = t
FreeCAD.ActiveDocument.recompute()
if dxfMakeBlocks or dxfJoin:
if sketch:
shape = Draft.make_sketch(shape, autoconstraints=True, addTo=sketch)
else:
shape = Draft.make_sketch(shape, autoconstraints=True)
sketch = shape
else:
shape = Draft.make_sketch(shape, autoconstraints=True)
elif dxfJoin or getShapes:
if isinstance(shape, Part.Shape):
shapes.append(shape)
else:
shapes.append(shape.Shape)
elif dxfMakeBlocks:
addToBlock(shape, polyline.layer)
else:
newob = addObject(shape, "Polyline", polyline.layer)
if gui:
formatObject(newob, polyline)
num += 1
# Draw arcs
arcs = drawing.entities.get_type("arc")
if arcs:
FCC.PrintMessage("drawing " + str(len(arcs)) + " arcs...\n")
for arc in arcs:
if dxfImportLayouts or (not rawValue(arc, 67)):
shape = drawArc(arc)
if shape:
if dxfCreateSketch:
FreeCAD.ActiveDocument.recompute()
if dxfMakeBlocks or dxfJoin:
if sketch:
shape = Draft.make_sketch(shape, autoconstraints=True, addTo=sketch)
else:
shape = Draft.make_sketch(shape, autoconstraints=True)
sketch = shape
else:
shape = Draft.make_sketch(shape, autoconstraints=True)
elif dxfJoin or getShapes:
if isinstance(shape, Part.Shape):
shapes.append(shape)
else:
shapes.append(shape.Shape)
elif dxfMakeBlocks:
addToBlock(shape, arc.layer)
else:
newob = addObject(shape, "Arc", arc.layer)
if gui:
formatObject(newob, arc)
# Join lines, polylines and arcs if needed
if dxfJoin and shapes:
FCC.PrintMessage("Joining geometry...\n")
edges = []
for s in shapes:
edges.extend(s.Edges)
if len(edges) > (100):
FCC.PrintMessage(str(len(edges)) + " edges to join\n")
if gui:
d = QtWidgets.QMessageBox()
d.setText("Warning: High number of entities to join (>100)")
d.setInformativeText(
"This might take a long time "
"or even freeze your computer. "
"Are you sure? You can also disable "
"the 'join geometry' setting in DXF "
"import preferences"
)
d.setStandardButtons(QtWidgets.QMessageBox.Ok | QtWidgets.QMessageBox.Cancel)
d.setDefaultButton(QtWidgets.QMessageBox.Cancel)
res = d.exec_()
if res == QtWidgets.QMessageBox.Cancel:
FCC.PrintMessage("Aborted\n")
return
shapes = DraftGeomUtils.findWires(edges)
for s in shapes:
newob = addObject(s)
# Draw circles
circles = drawing.entities.get_type("circle")
if circles:
FCC.PrintMessage("drawing " + str(len(circles)) + " circles...\n")
for circle in circles:
if dxfImportLayouts or (not rawValue(circle, 67)):
shape = drawCircle(circle)
if shape:
if dxfCreateSketch:
FreeCAD.ActiveDocument.recompute()
if dxfMakeBlocks or dxfJoin:
if sketch:
shape = Draft.make_sketch(shape, autoconstraints=True, addTo=sketch)
else:
shape = Draft.make_sketch(shape, autoconstraints=True)
sketch = shape
else:
shape = Draft.make_sketch(shape, autoconstraints=True)
elif dxfMakeBlocks:
addToBlock(shape, circle.layer)
elif getShapes:
if isinstance(shape, Part.Shape):
shapes.append(shape)
else:
shapes.append(shape.Shape)
else:
newob = addObject(shape, "Circle", circle.layer)
if gui:
formatObject(newob, circle)
# Draw solids
solids = drawing.entities.get_type("solid")
if solids:
FCC.PrintMessage("drawing " + str(len(solids)) + " solids...\n")
for solid in solids:
lay = rawValue(solid, 8)
if dxfImportLayouts or (not rawValue(solid, 67)):
shape = drawSolid(solid)
if shape:
if dxfMakeBlocks:
addToBlock(shape, lay)
elif getShapes:
if isinstance(shape, Part.Shape):
shapes.append(shape)
else:
shapes.append(shape.Shape)
else:
newob = addObject(shape, "Solid", lay)
if gui:
formatObject(newob, solid)
# Draw splines
splines = drawing.entities.get_type("spline")
if splines:
FCC.PrintMessage("drawing " + str(len(splines)) + " splines...\n")
for spline in splines:
lay = rawValue(spline, 8)
if dxfImportLayouts or (not rawValue(spline, 67)):
shape = drawSpline(spline)
if shape:
if dxfMakeBlocks:
addToBlock(shape, lay)
elif getShapes:
if isinstance(shape, Part.Shape):
shapes.append(shape)
else:
shapes.append(shape.Shape)
else:
newob = addObject(shape, "Spline", lay)
if gui:
formatObject(newob, spline)
# Draw ellipses
ellipses = drawing.entities.get_type("ellipse")
if ellipses:
FCC.PrintMessage("drawing " + str(len(ellipses)) + " ellipses...\n")
for ellipse in ellipses:
lay = rawValue(ellipse, 8)
if dxfImportLayouts or (not rawValue(ellipse, 67)):
shape = drawEllipse(ellipse)
if shape:
if dxfMakeBlocks:
addToBlock(shape, lay)
elif getShapes:
if isinstance(shape, Part.Shape):
shapes.append(shape)
else:
shapes.append(shape.Shape)
else:
newob = addObject(shape, "Ellipse", lay)
if gui:
formatObject(newob, ellipse)
# Draw texts
if dxfImportTexts:
texts = drawing.entities.get_type("mtext")
texts.extend(drawing.entities.get_type("text"))
if texts:
FCC.PrintMessage("drawing " + str(len(texts)) + " texts...\n")
for text in texts:
if dxfImportLayouts or (not rawValue(text, 67)):
addText(text)
else:
FCC.PrintMessage("skipping texts...\n")
# Draw 3D objects
faces3d = drawing.entities.get_type("3dface")
if faces3d:
FCC.PrintMessage("drawing " + str(len(faces3d)) + " 3dfaces...\n")
for face3d in faces3d:
shape = drawFace(face3d)
if shape:
if getShapes:
if isinstance(shape, Part.Shape):
shapes.append(shape)
else:
shapes.append(shape.Shape)
else:
newob = addObject(shape, "Face", face3d.layer)
if gui:
formatObject(newob, face3d)
if meshes:
FCC.PrintMessage("drawing " + str(len(meshes)) + " 3dmeshes...\n")
for mesh in meshes:
me = drawMesh(mesh)
if me:
newob = doc.addObject("Mesh::Feature", "Mesh")
lay = locateLayer(rawValue(mesh, 8))
lay.addObject(newob)
newob.Mesh = me
if gui:
formatObject(newob, mesh)
# End of shape-based objects, return if we are just getting shapes
if getShapes and shapes:
return shapes
# Draw dimensions
if dxfImportTexts:
dims = drawing.entities.get_type("dimension")
FCC.PrintMessage("drawing " + str(len(dims)) + " dimensions...\n")
for dim in dims:
if dxfImportLayouts or (not rawValue(dim, 67)):
try:
layer = rawValue(dim, 8)
if rawValue(dim, 15) is not None:
# this is a radial or diameter dimension
# x1 = float(rawValue(dim,11))
# y1 = float(rawValue(dim,21))
# z1 = float(rawValue(dim,31))
x2 = float(rawValue(dim, 10))
y2 = float(rawValue(dim, 20))
z2 = float(rawValue(dim, 30))
x3 = float(rawValue(dim, 15))
y3 = float(rawValue(dim, 25))
z3 = float(rawValue(dim, 35))
x1 = x2
y1 = y2
z1 = z2
else:
x1 = float(rawValue(dim, 10))
y1 = float(rawValue(dim, 20))
z1 = float(rawValue(dim, 30))
x2 = float(rawValue(dim, 13))
y2 = float(rawValue(dim, 23))
z2 = float(rawValue(dim, 33))
x3 = float(rawValue(dim, 14))
y3 = float(rawValue(dim, 24))
z3 = float(rawValue(dim, 34))
d = rawValue(dim, 70)
if d:
align = int(d)
else:
align = 0
d = rawValue(dim, 50)
if d:
angle = float(d)
else:
angle = 0
except (ValueError, TypeError):
warn(dim)
else:
lay = locateLayer(layer)
pt = vec([x1, y1, z1])
p1 = vec([x2, y2, z2])
p2 = vec([x3, y3, z3])
if align >= 128:
align -= 128
elif align >= 64:
align -= 64
elif align >= 32:
align -= 32
if align == 0:
if angle in [0, 180]:
p2 = vec([x3, y2, z2])
elif angle in [90, 270]:
p2 = vec([x2, y3, z2])
newob = doc.addObject("App::FeaturePython", "Dimension")
if hasattr(lay, "addObject"):
lay.addObject(newob)
elif hasattr(lay, "Proxy") and hasattr(lay.Proxy, "addObject"):
lay.Proxy.addObject(lay, newob)
_Dimension(newob)
if gui:
from Draft import _ViewProviderDimension
_ViewProviderDimension(newob.ViewObject)
newob.Start = p1
newob.End = p2
newob.Dimline = pt
if gui:
dim.layer = layer
dim.color_index = 256
formatObject(newob, dim)
if dxfUseStandardSize and draftui:
newob.ViewObject.FontSize = draftui.fontsize
else:
st = rawValue(dim, 3)
size = getdimheight(st) or 1
newob.ViewObject.FontSize = float(size) * TEXTSCALING
else:
FCC.PrintMessage("skipping dimensions...\n")
# Draw points
if dxfImportPoints:
points = drawing.entities.get_type("point")
if points:
FCC.PrintMessage("drawing " + str(len(points)) + " points...\n")
for point in points:
x = vec(rawValue(point, 10))
y = vec(rawValue(point, 20))
# For DXF file without Z values.
if rawValue(point, 30):
z = vec(rawValue(point, 30))
else:
z = 0
lay = rawValue(point, 8)
if dxfImportLayouts or (not rawValue(point, 67)):
if dxfMakeBlocks:
shape = Part.Vertex(x, y, z)
addToBlock(shape, lay)
else:
newob = Draft.make_point(x, y, z)
lay = locateLayer(lay)
lay.addObject(newob)
if gui:
formatObject(newob, point)
else:
FCC.PrintMessage("skipping points...\n")
# Draw leaders
if dxfImportTexts:
leaders = drawing.entities.get_type("leader")
if leaders:
FCC.PrintMessage("drawing " + str(len(leaders)) + " leaders...\n")
for leader in leaders:
if dxfImportLayouts or (not rawValue(leader, 67)):
points = getMultiplePoints(leader)
newob = Draft.make_wire(points)
lay = locateLayer(rawValue(leader, 8))
lay.addObject(newob)
if gui:
newob.ViewObject.EndArrow = True
formatObject(newob, leader)
else:
FCC.PrintMessage("skipping leaders...\n")
# Draw hatches
if dxfImportHatches:
hatches = drawing.entities.get_type("hatch")
if hatches:
FCC.PrintMessage("drawing " + str(len(hatches)) + " hatches...\n")
for hatch in hatches:
if dxfImportLayouts or (not rawValue(hatch, 67)):
points = getMultiplePoints(hatch)
if len(points) > 1:
lay = rawValue(hatch, 8)
points = points[:-1]
newob = None
if dxfCreatePart or dxfMakeBlocks:
points.append(points[0])
s = Part.makePolygon(points)
if dxfMakeBlocks:
addToBlock(s, lay)
else:
newob = addObject(s, "Hatch", lay)
if gui:
formatObject(newob, hatch)
else:
newob = Draft.make_wire(points)
locateLayer(lay).addObject(newob)
if gui:
formatObject(newob, hatch)
else:
FCC.PrintMessage("skipping hatches...\n")
# Draw blocks
inserts = drawing.entities.get_type("insert")
if not dxfStarBlocks:
FCC.PrintMessage("skipping *blocks...\n")
newinserts = []
for i in inserts:
if dxfImportLayouts or (not rawValue(i, 67)):
if i.block[0] != "*":
newinserts.append(i)
inserts = newinserts
if inserts:
FCC.PrintMessage("drawing " + str(len(inserts)) + " blocks...\n")
blockrefs = drawing.blocks.data
for ref in blockrefs:
if dxfCreateDraft or dxfCreateSketch:
drawBlock(ref, createObject=True)
else:
drawBlock(ref, createObject=False)
num = 0
for insert in inserts:
if (dxfCreateDraft or dxfCreateSketch) and not dxfMakeBlocks:
shape = drawInsert(insert, num, clone=True)
else:
shape = drawInsert(insert, num)
if shape:
if dxfMakeBlocks:
addToBlock(shape, insert.layer)
else:
newob = addObject(shape, "Block." + insert.block, insert.layer)
if gui:
formatObject(newob, insert)
num += 1
# Make blocks, if any
if dxfMakeBlocks:
print("creating layerblocks...")
for k, l in layerBlocks.items():
shape = drawLayerBlock(l, "LayerBlock_" + k)
if shape:
newob = addObject(shape, "LayerBlock_" + k, k)
del layerBlocks
# Hide block objects, if any
for k, o in blockobjects.items():
if o.ViewObject:
o.ViewObject.hide()
del blockobjects
# Move layer contents to layers
for l, contents in layerObjects.items():
l.Group += contents
# Finishing
print("done processing")
if reComputeFlag:
doc.recompute()
print("recompute done")
FCC.PrintMessage("successfully imported " + filename + "\n")
if badobjects:
print("dxf: ", len(badobjects), " objects were not imported")
del doc
def warn(dxfobject, num=None):
"""Print a warning that the DXF object couldn't be imported.
Also add the object to the global list `badobjects`.
Parameters
----------
dxfobject : drawing.entities
The DXF object that couldn't be imported.
num : float, optional
It defaults to `None`. A simple number that identifies
the given `dxfobject`.
To do
-----
Use local variables, not global variables.
"""
print("dxf: couldn't import ", dxfobject, " (", num, ")")
badobjects.append(dxfobject)
def _import_dxf_file(filename, doc_name=None):
"""
Internal helper to handle the core logic for both open and insert.
"""
hGrp = FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Mod/Draft")
use_legacy = hGrp.GetBool("dxfUseLegacyImporter", False)
readPreferences()
# --- Dialog Workflow ---
try:
if gui:
FreeCADGui.suspendWaitCursor()
if gui and not use_legacy and hGrp.GetBool("dxfShowDialog", True):
try:
import ImportGui
entity_counts = ImportGui.preScanDxf(filename)
except Exception:
entity_counts = {}
from DxfImportDialog import DxfImportDialog
dlg = DxfImportDialog(entity_counts)
if dlg.exec_():
# Save the integer mode from the pop-up dialog.
hGrp.SetInt("DxfImportMode", dlg.get_selected_mode())
# Keep the main preferences booleans
# in sync with the choice just made in the pop-up dialog.
mode = dlg.get_selected_mode()
params.set_param("dxfImportAsDraft", mode == 0)
params.set_param("dxfImportAsPrimitives", mode == 1)
params.set_param("dxfImportAsShapes", mode == 2)
params.set_param("dxfImportAsFused", mode == 3)
hGrp.SetBool("dxfShowDialog", dlg.get_show_dialog_again())
else:
return None, None, None, None # Return None to indicate cancellation
finally:
if gui:
FreeCADGui.resumeWaitCursor()
import_mode = hGrp.GetInt("DxfImportMode", 2)
# --- Document Handling ---
if doc_name: # INSERT operation
try:
doc = FreeCAD.getDocument(doc_name)
except NameError:
doc = FreeCAD.newDocument(doc_name)
FreeCAD.setActiveDocument(doc_name)
else: # OPEN operation
docname = os.path.splitext(os.path.basename(filename))[0]
doc = FreeCAD.newDocument(docname)
doc.Label = docname
FreeCAD.setActiveDocument(doc.Name)
# --- Core Import Execution ---
processing_start_time = time.perf_counter()
# Take snapshot of objects before import
objects_before = set(doc.Objects)
stats = None # For C++ importer stats
if use_legacy:
getDXFlibs()
if dxfReader:
processdxf(doc, filename)
else:
errorDXFLib(gui)
return None, None
else: # Modern C++ Importer
if gui:
import ImportGui
stats = ImportGui.readDXF(filename)
else:
import Import
stats = Import.readDXF(filename)
# Find the newly created objects
objects_after = set(doc.Objects)
newly_created_objects = objects_after - objects_before
# --- Post-processing step ---
if not use_legacy and newly_created_objects:
draft_postprocessor = DxfDraftPostProcessor(doc, newly_created_objects, import_mode)
draft_postprocessor.run()
Draft.convert_draft_texts() # This is a general utility that should run for both importers
doc.recompute()
processing_end_time = time.perf_counter()
# Return the results for the reporter
return doc, stats, processing_start_time, processing_end_time
def open(filename):
"""Open a file and return a new document.
This function handles the import of a DXF file into a new document.
It shows an import dialog for the modern C++ importer if configured to do so.
It manages the import workflow, including pre-processing, calling the
correct backend (legacy or modern C++), and post-processing.
Parameters
----------
filename : str
The path to the file to open.
Returns
-------
App::Document or None
The new document object with imported content, or None if the
operation was cancelled or failed.
"""
doc, stats, start_time, end_time = _import_dxf_file(filename, doc_name=None)
if doc and stats:
reporter = DxfImportReporter(filename, stats, end_time - start_time)
reporter.report_to_console()
return doc
def insert(filename, docname):
"""Import a file into the specified document.
This function handles the import of a DXF file into a specified document.
If the document does not exist, it will be created. It shows an import
dialog for the modern C++ importer if configured to do so.
Parameters
----------
filename : str
The path to the file to import.
docname : str
The name of an App::Document instance to import the content into.
"""
doc, stats, start_time, end_time = _import_dxf_file(filename, doc_name=docname)
if doc and stats:
reporter = DxfImportReporter(filename, stats, end_time - start_time)
reporter.report_to_console()
def getShapes(filename):
"""Read a DXF file, and return a list of shapes from its contents.
This is an auxiliary function that processes the DXF file to list its
contents but doesn't open or create a new document.
Parameters
----------
filename : str
The path to the file to read.
Returns
-------
list of `Part.Shapes`
It returns `None` if the edges (lines, polylines, arcs)
are above 100, and the user decides to interrupt (graphically)
the process of joining them.
See also
--------
open, insert
"""
if dxfReader:
return processdxf(None, filename, getShapes=True)
# EXPORT ######################################################################
def projectShape(shape, direction, tess=None):
"""Project shape in a given direction.
It uses `TechDraw.projectEx(shape, direction)`
to return a list with all the parts of the projection.
The first five elements are added to a list of edges,
which are then put in a `Part.Compound`.
Parameters
----------
shape : Part.Shape
Any shape previously created from a DXF file.
direction : Base::Vector3
The direction of the projection.
tess : list, optional
It defaults to `None`. If it is available, it is a list with
two elements, `[True, segment_length]` which are used by
`DraftGeomUtils.cleanProjection(compound, tess[0], tess[1])`
to create a valid compound of edges.
Otherwise, a simple `Part.Compound` is produced.
Returns
-------
Part::TopoShape ('Compound')
A `Part.Compound` of edges.
It returns the original `shape` if it fails producing the projection
in the given `direction`.
See also
--------
TechDraw.projectEx, DraftGeomUtils.cleanProjection
"""
import TechDraw
edges = []
try:
groups = TechDraw.projectEx(shape, direction)
except Part.OCCError:
print("unable to project shape on direction ", direction)
return shape
else:
for g in groups[0:5]:
if g:
edges.append(g)
# return DraftGeomUtils.cleanProjection(Part.makeCompound(edges))
if tess:
return DraftGeomUtils.cleanProjection(Part.makeCompound(edges), tess[0], tess[1])
else:
return Part.makeCompound(edges)
# return DraftGeomUtils.cleanProjection(Part.makeCompound(edges))
def getArcData(edge):
"""Return center, radius, start, and end angles of a circle-based edge.
Parameters
----------
edge : Part::TopoShape ('Edge')
An edge representing a circular arc, either open or closed.
Returns
-------
(tuple, float, float, float)
It returns a tuple of four values; the first value is a tuple
with the coordinates of the center `(x, y, z)`;
the other three represent the magnitude of the radius,
and the start and end angles in degrees that define the arc.
(tuple, float, 0, 0)
If the number of vertices in the `edge` is only one, only the center
point exists, so it's a full circumference; in this case, both
angles are zero.
"""
ce = edge.Curve.Center
radius = edge.Curve.Radius
if len(edge.Vertexes) == 1:
# closed circle
return DraftVecUtils.tup(ce), radius, 0, 0
else:
# new method: recalculate ourselves as we cannot trust edge.Curve.Axis
# or XAxis
p1 = edge.Vertexes[0].Point
p2 = edge.Vertexes[-1].Point
v1 = p1.sub(ce)
v2 = p2.sub(ce)
# print(v1.cross(v2))
# print(edge.Curve.Axis)
# print(p1)
# print(p2)
# we can use Z check since arcs getting here will ALWAYS be in XY plane
# Z can be 0 if the arc is 180 deg
# if (v1.cross(v2).z >= 0) or (edge.Curve.Axis.z > 0):
# Calculates the angles of the first and last points
# in the circular arc, with respect to the global X axis.
if edge.Curve.Axis.z > 0:
# clockwise
ang1 = -DraftVecUtils.angle(v1)
ang2 = -DraftVecUtils.angle(v2)
else:
# counterclockwise
ang2 = -DraftVecUtils.angle(v1)
ang1 = -DraftVecUtils.angle(v2)
# obsolete method - fails a lot
# if round(edge.Curve.Axis.dot(Vector(0, 0, 1))) == 1:
# ang1, ang2 = edge.ParameterRange
# else:
# ang2, ang1 = edge.ParameterRange
# if edge.Curve.XAxis != Vector(1, 0, 0):
# ang1 -= DraftVecUtils.angle(edge.Curve.XAxis)
# ang2 -= DraftVecUtils.angle(edge.Curve.XAxis)
return (DraftVecUtils.tup(ce), radius, math.degrees(ang1), math.degrees(ang2))
def getSplineSegs(edge):
"""Return a list of points from an edge that is a spline or bezier curve.
Parameters
----------
edge : Part::TopoShape ('Edge')
An edge representing a spline or bezier curve.
Returns
-------
list of Base::Vector3
It returns a list with the points that form the curve.
It returns the point in `edge.FirstParameter`,
all the intermediate points, and the point in `edge.LastParameter`.
If the `segmentlength` variable is zero in the parameters database,
then it only returns the first and the last point of the `edge`.
"""
seglength = params.get_param("maxsegmentlength")
points = []
if seglength == 0:
points.append(edge.Vertexes[0].Point)
points.append(edge.Vertexes[-1].Point)
else:
points.append(edge.valueAt(edge.FirstParameter))
if edge.Length > seglength:
nbsegs = int(math.ceil(edge.Length / seglength))
step = (edge.LastParameter - edge.FirstParameter) / nbsegs
for nv in range(1, nbsegs):
# print("value at", nv*step, "=", edge.valueAt(nv*step))
v = edge.valueAt(edge.FirstParameter + (nv * step))
points.append(v)
points.append(edge.valueAt(edge.LastParameter))
return points
def getWire(wire, nospline=False, lw=True, asis=False):
"""Return a list of DXF ready points and bulges from a wire.
It builds a list of points from the edges of a `wire`.
If the edges are circular arcs, the "bulge" of that edge is calculated,
for other cases, the bulge is considered zero.
Parameters
----------
wire : Part::TopoShape ('Wire')
A shape representing a wire.
nospline : bool, optional
It defaults to `False`.
If it is `True`, the edges of the wire are not considered as
being one of `'BSplineCurve'`, `'BezierCurve'`, or `'Ellipse'`,
and a simple point is added to the list.
Otherwise, `getSplineSegs(edge)` is used to extract
the points and add them to the list.
lw : bool, optional
It defaults to `True`. If it is `True` it assumes the `wire`
is a `'lwpolyline'`.
Otherwise, it assumes it is a `'polyline'`.
asis : bool, optional
It defaults to `False`. If it is `True`, it just returns
the points of the vertices of the `wire`, and considers the bulge
is zero.
Otherwise, it processes the edges of the `wire` and calculates
the bulge of the edges if they are of type `'Circle'`.
For types of edges that are `'BSplineCurve'`, `'BezierCurve'`,
or `'Ellipse'`, the bulge is zero
Returns
-------
list of tuples
It returns a list of tuples ``[(...), (...), ...]``
where each tuple indicates a point with additional information
besides the coordinates.
Two types of tuples may be returned.
[(float, float, float, None, None, float), ...]
When `lw` is `True` (`'lwpolyline'`)
the first three values represent the coordinates of the point,
the next two are `None`, and the last value is the bulge.
[((float, float, float), None, [None, None], float), ...]
When `lw` is `False` (`'polyline'`)
the first element is a tuple of three values that indicate
the coordinates of the point, the next element is `None`,
the next element is a list of two `None` values,
and the last element is the value of the bulge.
See also
--------
calcBulge
"""
def fmt(v, b=0.0):
if lw:
# LWpolyline format
return (v.x, v.y, v.z, None, None, b)
else:
# Polyline format
return ((v.x, v.y, v.z), None, [None, None], b)
points = []
if asis:
points = [fmt(v.Point) for v in wire.OrderedVertexes]
else:
edges = Part.__sortEdges__(wire.Edges)
# print("processing wire ",wire.Edges)
for edge in edges:
v1 = edge.Vertexes[0].Point
if DraftGeomUtils.geomType(edge) == "Circle":
# polyline bulge -> negative makes the arc go clockwise
angle = edge.LastParameter - edge.FirstParameter
bul = math.tan(angle / 4)
# if cross1[2] < 0:
# # polyline bulge -> negative makes the arc go clockwise
# bul = -bul
if edge.Curve.Axis.dot(Vector(0, 0, 1)) < 0:
bul = -bul
points.append(fmt(v1, bul))
elif (DraftGeomUtils.geomType(edge) in ["BSplineCurve", "BezierCurve", "Ellipse"]) and (
not nospline
):
spline = getSplineSegs(edge)
spline.pop()
for p in spline:
points.append(fmt(p))
else:
points.append(fmt(v1))
if not DraftGeomUtils.isReallyClosed(wire):
v = edges[-1].Vertexes[-1].Point
points.append(fmt(v))
# print("wire verts: ",points)
return points
def getBlock(sh, obj, lwPoly=False):
"""Return a DXF block with the contents of the object.
It creates a `block` object using `dxfLibrary.Block`,
and then writes the given shape with
`writeShape(sh, obj, block, lwPoly)`.
Parameters
----------
sh : Part::TopoShape
Any shape in the document.
obj : App::DocumentObject
Any object in the document.
lwPoly : bool, optional
It defaults to `False`. If it is `True` it will write
a `'lwpolyline'`.
Otherwise, it will be a `'polyline'`.
Returns
-------
dxfLibrary.Block
The block of data with the given `sh` shape and `obj` object.
"""
block = dxfLibrary.Block(name=obj.Name, layer=getStrGroup(obj))
writeShape(sh, obj, block, lwPoly)
return block
def writeShape(sh, ob, dxfobject, nospline=False, lwPoly=False, layer=None, color=None, asis=False):
"""Write the object's shape contents in the given DXF object.
Iterates over the wires (polylines) and lone edges of `sh`.
Then it creates DXF object depending of the type of wire,
and adds those objects to the `dxfobject` list.
If the wire only has one edge and it is of type `'Circle'`
it will create an object of type `dxfLibrary.Circle` or `dxfLibrary.Arc`.
In other cases, it will try creating objects of type
`dxfLibrary.LwPolyLine` or `dxfLibrary.PolyLine`.
When parsing lone edges it will approximate single closed edges of type
`'BSplineCurve'` or `'BezierCurve'` with a `dxfLibrary.Circle`.
In the case of edges of type `Ellipse`, it can approximate
the edge as a `dxfLibrary.PolyLine`, depending on the value
of `'DiscretizeEllipses'` in the parameter database.
Otherwise it creates an object of type `dxfLibrary.Ellipse`.
For other lone edges, they are treated as lines,
so they create an object of type `linesdxfLibrary.Line`.
Parameters
----------
sh : Part::TopoShape
Any shape in the document.
ob : App::DocumentObject
Any object in the document.
dxfobject : dxfLibrary.Drawing
An object which will be populated with DXF objects created
from `sh.Wires` and `sh.Edges`.
nospline : bool, optional
It defaults to `False`.
If it is `True`, the edges of the wire are not considered as
being one of `'BSplineCurve'`, `'BezierCurve'`, or `'Ellipse'`,
and simple points are used to build the new object with
`getWire(wire, nospline=True, asis=asis)`.
lwPoly : bool, optional
It defaults to `False`. If it is `True` it will try producing
a `dxfLibrary.LwPolyLine`, instead of a `dxfLibrary.PolyLine`.
layer : str, optional
It defaults to `None`. It is the name of the layer or group where `ob`
is contained. If it is `None`, `getStrGroup(ob)` is called to search
for the layer's name that contains `ob`.
The created object is placed in this layer.
color : int, optional
It defaults to `None`. It is the AutoCAD color index (ACI)
closest to `ob`'s color obtained with `getACI(ob)`.
The created object uses this color.
asis : bool, optional
It defaults to `False`. If it is `True`, it just extracts
the edges of the wire as is, and creates the `'lwpolyline'`
or `'polyline'` with the simple points returned by
`getWire(wire, nospline, asis=True)`.
Otherwise, the edges are sorted, and then creates
more complex shapes with `getWire(wire, nospline, asis=False)`.
See also
--------
getWire, getStrGroup, getACI, dxfLibrary.Circle, dxfLibrary.Arc,
dxfLibrary.LwPolyLine, dxfLibrary.PolyLine, dxfLibrary.Ellipse,
dxfLibrary.Line
"""
processededges = []
if not layer:
layer = getStrGroup(ob)
if not color:
color = getACI(ob)
for wire in sh.Wires: # polylines
if asis:
edges = wire.Edges
else:
edges = Part.__sortEdges__(wire.Edges)
for e in edges:
processededges.append(e.hashCode())
if (len(wire.Edges) == 1) and (DraftGeomUtils.geomType(wire.Edges[0]) == "Circle"):
center, radius, ang1, ang2 = getArcData(wire.Edges[0])
if center is not None:
if len(wire.Edges[0].Vertexes) == 1: # circle
dxfobject.append(dxfLibrary.Circle(center, radius, color=color, layer=layer))
else: # arc
dxfobject.append(
dxfLibrary.Arc(center, radius, ang1, ang2, color=color, layer=layer)
)
else:
if lwPoly:
if hasattr(dxfLibrary, "LwPolyLine"):
dxfobject.append(
dxfLibrary.LwPolyLine(
getWire(wire, nospline, asis=asis),
[0.0, 0.0],
int(DraftGeomUtils.isReallyClosed(wire)),
color=color,
layer=layer,
)
)
else:
FCC.PrintWarning(
"LwPolyLine support not found. "
"Please delete dxfLibrary.py "
"from your FreeCAD user directory "
"to force auto-update\n"
)
else:
dxfobject.append(
dxfLibrary.PolyLine(
getWire(wire, nospline, lw=False, asis=asis),
[0.0, 0.0, 0.0],
int(DraftGeomUtils.isReallyClosed(wire)),
color=color,
layer=layer,
)
)
if len(processededges) < len(sh.Edges): # lone edges
loneedges = []
for e in sh.Edges:
if e.hashCode() not in processededges:
loneedges.append(e)
# print("lone edges ", loneedges)
for edge in loneedges:
# splines
if DraftGeomUtils.geomType(edge) in ["BSplineCurve", "BezierCurve"]:
if (len(edge.Vertexes) == 1) and (edge.Curve.isClosed()) and (edge.Area > 0):
# special case: 1-vert closed spline, approximate as a circle
c = DraftGeomUtils.getCircleFromSpline(edge)
if c:
dxfobject.append(
dxfLibrary.Circle(
DraftVecUtils.tup(c.Curve.Center),
c.Curve.Radius,
color=color,
layer=layer,
)
)
else:
points = []
spline = getSplineSegs(edge)
for p in spline:
points.append(((p.x, p.y, p.z), None, [None, None], 0.0))
dxfobject.append(
dxfLibrary.PolyLine(points, [0.0, 0.0, 0.0], 0, color=color, layer=layer)
)
elif DraftGeomUtils.geomType(edge) == "Circle": # curves
center, radius, ang1, ang2 = getArcData(edge)
if center is not None:
if not isinstance(center, tuple):
center = DraftVecUtils.tup(center)
if len(edge.Vertexes) == 1: # circles
dxfobject.append(
dxfLibrary.Circle(center, radius, color=color, layer=layer)
)
else: # arcs
dxfobject.append(
dxfLibrary.Arc(
center, radius, ang1, ang2, color=getACI(ob), layer=layer
)
)
elif DraftGeomUtils.geomType(edge) == "Ellipse": # ellipses:
if params.get_param("DiscretizeEllipses"):
points = []
spline = getSplineSegs(edge)
for p in spline:
points.append(((p.x, p.y, p.z), None, [None, None], 0.0))
dxfobject.append(
dxfLibrary.PolyLine(points, [0.0, 0.0, 0.0], 0, color=color, layer=layer)
)
else:
if hasattr(dxfLibrary, "Ellipse"):
center = DraftVecUtils.tup(edge.Curve.Center)
norm = DraftVecUtils.tup(edge.Curve.Axis)
start = edge.FirstParameter
end = edge.LastParameter
ax = edge.Curve.Focus1.sub(edge.Curve.Center)
major = DraftVecUtils.tup(DraftVecUtils.scaleTo(ax, edge.Curve.MajorRadius))
minor = edge.Curve.MinorRadius / edge.Curve.MajorRadius
# print("exporting ellipse: ", center, norm,
# start, end, major, minor)
dxfobject.append(
dxfLibrary.Ellipse(
center=center,
majorAxis=major,
normalAxis=norm,
minorAxisRatio=minor,
startParameter=start,
endParameter=end,
color=color,
layer=layer,
)
)
else:
FCC.PrintWarning(
"Ellipses support not found. "
"Please delete dxfLibrary.py "
"from your FreeCAD user directory "
"to force auto-update\n"
)
else: # anything else is treated as lines
if len(edge.Vertexes) > 1:
ve1 = edge.Vertexes[0].Point
ve2 = edge.Vertexes[1].Point
dxfobject.append(
dxfLibrary.Line(
[DraftVecUtils.tup(ve1), DraftVecUtils.tup(ve2)],
color=color,
layer=layer,
)
)
def writeMesh(ob, dxf):
"""Write an object's shape as a polyface mesh in the given DXF list.
It tessellates the `ob.Shape` with a tolerance of 0.5,
to produce mesh data, that is, lists of vertices and face indices:
``([ point1, point2, ...], [(face1 indices), (face2 indices), ...])``
The points and faces are extracted, and used with
`dxfLibrary.PolyLine` to produce a polyface mesh, that is added
to the `dxf` object.
Parameters
----------
ob : App::DocumentObject
Any object in the document.
dxf : dxfLibrary.Drawing
An object which will be populated with a DXF polyface mesh
created from `ob.Shape`.
See also
--------
dxfLibrary.Drawing, dxfLibrary.PolyLine, Part.Shape.tessellate
"""
meshdata = ob.Shape.tessellate(0.5)
# print(meshdata)
points = []
faces = []
for p in meshdata[0]:
points.append([p.x, p.y, p.z])
for f in meshdata[1]:
faces.append([f[0] + 1, f[1] + 1, f[2] + 1])
# print(len(points),len(faces))
dxf.append(
dxfLibrary.PolyLine(
[points, faces], [0.0, 0.0, 0.0], 64, color=getACI(ob), layer=getGroup(ob)
)
)
def writePanelCut(ob, dxf, nospline, lwPoly, parent=None):
"""Create an object's outline and add it to the given DXF list.
Given an object `ob` that contains an outline in its proxy object,
it tries obtaining the outline `outl`, the inline `inl`, and a `tag`.
Then tries creating each shape using the `parent` object as base
(or `ob` itself), and placing the result in the `dxf` list.
For `outl` it places the result in an `'Outlines'` layer of color index 5
(blue).
For `intl`, if it exists, it places the result in a `'Cuts'` layer
of color index 4 (light blue).
For `tag`, if it exists, it places the result in a `'Tags'` layer
of color index 2 (yellow).
::
writeShape(outl, parent, dxf, nospline, lwPoly, ...)
writeShape(inl, parent, dxf, nospline, lwPoly, ...)
writeShape(tag, parent, dxf, nospline, lwPoly, ...)
Parameters
----------
ob : App::DocumentObject
Any object in the document.
dxf : dxfLibrary.Drawing
An object which will be populated with a DXF object created
from `writeShape()`.
nospline : bool
If it is `True`, the edges of the wire are not considered as
being one of `'BSplineCurve'`, `'BezierCurve'`, or `'Ellipse'`,
and simple points are used to build the new shape with
`writeShape()`.
lwPoly : bool
If it is `True` it will try producing
a `dxfLibrary.LwPolyLine`, instead of a `dxfLibrary.PolyLine`,
by using `writeShape()`.
parent : App::DocumentObject, optional
It defaults to `None`.
If it exists, its `Base::Placement` is used to modify the
Placement of the output object and its tag.
Otherwise, `ob` is also used as the `parent`.
See also
--------
writeShape
"""
if not hasattr(ob.Proxy, "outline"):
ob.Proxy.execute(ob)
if hasattr(ob.Proxy, "outline"):
outl = ob.Proxy.outline.copy()
tag = None
if hasattr(ob.Proxy, "tag"):
tag = ob.Proxy.tag
if tag:
tag = tag.copy()
tag.Placement = ob.Placement.multiply(tag.Placement)
if parent:
tag.Placement = parent.Placement.multiply(tag.Placement)
outl.Placement = ob.Placement.multiply(outl.Placement)
if parent:
outl.Placement = parent.Placement.multiply(outl.Placement)
else:
parent = ob
if len(outl.Wires) > 1:
# separate outline
d = 0
ow = None
for w in outl.Wires:
if w.BoundBox.DiagonalLength > d:
d = w.BoundBox.DiagonalLength
ow = w
if ow:
inl = Part.Compound([w for w in outl.Wires if w.hashCode() != ow.hashCode()])
outl = ow
else:
inl = None
outl = outl.Wires[0]
writeShape(outl, parent, dxf, nospline, lwPoly, layer="Outlines", color=5)
if inl:
writeShape(inl, parent, dxf, nospline, lwPoly, layer="Cuts", color=4)
if tag:
writeShape(tag, parent, dxf, nospline, lwPoly, layer="Tags", color=2, asis=True)
# sticky fonts can render very odd wires...
# for w in tag.Edges:
# pts = [(v.X, v.Y, v.Z) for v in w.Vertexes]
# dxf.append(dxfLibrary.Line(pts, color=getACI(ob),
# layer="Tags"))
def getStrGroup(ob):
"""Get a string version of the group or layer that contains the object.
Parameters
----------
ob : App::DocumentObject
Any object in the document.
Returns
-------
str
The name of the layer in capital letters,
as the DXF R12 format seems to favor this style.
"""
return getGroup(ob).upper()
def export(objectslist, filename, nospline=False, lwPoly=False):
"""Export a DXF file into the specified filename.
If will read the preferences. If the global variable
`dxfUseLegacyExporter` exists, it will try using the `Import` module
to write the DXF file.
::
Import.writeDXFObject(objectslist, filename, version, lwPoly)
Where `version` is 14, or 12 if `nospline` is `True`.
Otherwise it will try to use the DXF export libraries
by running `getDXFlibs()`.
Iterating over all objects it writes shapes individually
with `writeShape()`, looking for types `'PanelSheet'`, `'PanelCut'`,
`'Axis'`, `'Annotation'`, `'DraftText'`, `'Dimension'`.
For objects derived from `'Part::Feature'` it may use `writeMesh()`
depending on the parameter `'dxfmesh'`, or it may project the object
in the camera view, depending on the parameter `'dxfproject'`.
Parameters
----------
objectslist : list of App::DocumentObject
A list with all objects that will be exported.
If any object of the given list is a group, its contents are appended
to the export list.
If the list only contains an `'ArchSectionView'` object
it will use its `getDXF()` method to provide the DXF information
to write into `filename`.
If the list only contains a `'TechDraw::DrawPage'` object it will use
`exportPage()` to produce the DXF file.
filename : str
The path of the new DXF file.
nospline : bool, optional
It defaults to `False`.
If it is `True`, the BSplines are exported as straight segments,
when passing the objects to `writeShape()`.
lwPoly : bool, optional.
It defaults to `False`.
If it is `True` it will try producing
a `dxfLibrary.LwPolyLine`, instead of a `dxfLibrary.PolyLine`,
by using `writeShape()`.
This is required to produce an OpenSCAD DXF.
Returns
-------
It returns `None` if the export is successful.
See also
--------
dxfLibrary.Drawing, readPreferences, getDXFlibs, errorDXFLib,
writeShape, writeMesh, Import.writeDXFObject
To do
-----
Use local variables, not global variables.
"""
readPreferences()
if not dxfUseLegacyExporter:
import Import
version = 14
if nospline:
version = 12
Import.writeDXFObject(objectslist, filename, version, lwPoly)
return
getDXFlibs()
if dxfLibrary:
global exportList
exportList = Draft.get_group_contents(objectslist, spaces=True)
nlist = []
exportLayers = []
for ob in exportList:
t = Draft.getType(ob)
if t == "AxisSystem":
nlist.extend(ob.Axes)
elif t == "Layer":
exportLayers.append(ob)
for child in ob.Group:
if child not in nlist:
nlist.append(child)
else:
if ob not in nlist:
nlist.append(ob)
exportList = nlist
if (len(exportList) == 1) and (Draft.getType(exportList[0]) == "ArchSectionView"):
# arch view: export it "as is"
dxf = exportList[0].Proxy.getDXF()
if dxf:
f = pyopen(filename, "w")
f.write(dxf)
f.close()
elif (len(exportList) == 1) and (exportList[0].isDerivedFrom("TechDraw::DrawPage")):
# page: special hack-export! (see below)
exportPage(exportList[0], filename)
else:
# other cases, treat objects one by one
dxf = dxfLibrary.Drawing()
# add global variables
if hasattr(dxf, "header"):
dxf.header.append(
" 9\n$DIMTXT\n 40\n" + str(params.get_param("textheight")) + "\n"
)
dxf.header.append(" 9\n$INSUNITS\n 70\n4\n")
for ob in exportLayers:
if ob.Label != "0": # dxflibrary already creates it
ltype = "continuous"
if ob.ViewObject:
if ob.ViewObject.DrawStyle == "Dashed":
ltype = "DASHED"
elif ob.ViewObject.DrawStyle == "Dotted":
ltype = "HIDDEN"
elif ob.ViewObject.DrawStyle == "Dashdot":
ltype = "DASHDOT"
# print("exporting layer:", ob.Label,
# getACI(ob), ltype)
dxf.layers.append(
dxfLibrary.Layer(name=ob.Label, color=getACI(ob), lineType=ltype)
)
base_sketch_pla = None # Placement of the 1st sketch.
for ob in exportList:
obtype = Draft.getType(ob)
# print("processing " + str(ob.Name))
if obtype == "PanelSheet":
if not hasattr(ob.Proxy, "sheetborder"):
ob.Proxy.execute(ob)
sb = ob.Proxy.sheetborder
if sb:
sb.Placement = ob.Placement
writeShape(sb, ob, dxf, nospline, lwPoly, layer="Sheets", color=1)
ss = ob.Proxy.sheettag
if ss:
ss.Placement = ob.Placement.multiply(ss.Placement)
writeShape(ss, ob, dxf, nospline, lwPoly, layer="SheetTags", color=1)
for subob in ob.Group:
if Draft.getType(subob) == "PanelCut":
writePanelCut(subob, dxf, nospline, lwPoly, parent=ob)
elif subob.isDerivedFrom("Part::Feature"):
shp = subob.Shape.copy()
shp.Placement = ob.Placement.multiply(shp.Placement)
writeShape(shp, ob, dxf, nospline, lwPoly, layer="Outlines", color=5)
elif obtype == "PanelCut":
writePanelCut(ob, dxf, nospline, lwPoly)
elif obtype == "Space" and gui:
vobj = ob.ViewObject
rotation = math.degrees(ob.Placement.Rotation.Angle)
t1 = "".join(vobj.Proxy.text1.string.getValues())
t2 = "".join(vobj.Proxy.text2.string.getValues())
h1 = vobj.FirstLine.Value
h2 = vobj.FontSize.Value
_v = vobj.Proxy.coords.translation.getValue().getValue()
_h = vobj.Proxy.header.translation.getValue().getValue()
p2 = FreeCAD.Vector(_v)
lspc = FreeCAD.Vector(_h)
p1 = ob.Placement.multVec(p2 + lspc)
justifyhor = ("Left", "Center", "Right").index(vobj.TextAlign)
dxf.append(
dxfLibrary.Text(
t1,
p1,
alignment=p1 if justifyhor else None,
height=h1 * 0.8,
justifyhor=justifyhor,
rotation=rotation,
color=getACI(ob, text=True),
style="STANDARD",
layer=getStrGroup(ob),
)
)
if t2:
ofs = FreeCAD.Vector(0, -lspc.Length, 0)
if rotation:
Z = FreeCAD.Vector(0, 0, 1)
ofs = FreeCAD.Rotation(Z, rotation).multVec(ofs)
dxf.append(
dxfLibrary.Text(
t2,
p1.add(ofs),
alignment=p1.add(ofs) if justifyhor else None,
height=h2 * 0.8,
justifyhor=justifyhor,
rotation=rotation,
color=getACI(ob, text=True),
style="STANDARD",
layer=getStrGroup(ob),
)
)
elif obtype == "Axis":
axes = ob.Proxy.getAxisData(ob)
if not axes:
continue
for ax in axes:
dxf.append(
dxfLibrary.Line([ax[0], ax[1]], color=getACI(ob), layer=getStrGroup(ob))
)
h = 1
if gui:
vobj = ob.ViewObject
h = float(vobj.FontSize)
for text in vobj.Proxy.getTextData():
pos = text[1].add(FreeCAD.Vector(0, -h / 2, 0))
dxf.append(
dxfLibrary.Text(
text[0],
pos,
alignment=pos,
height=h,
justifyhor=1,
color=getACI(ob),
style="STANDARD",
layer=getStrGroup(ob),
)
)
for shape in vobj.Proxy.getShapeData():
if hasattr(shape, "Curve") and isinstance(shape.Curve, Part.Circle):
dxf.append(
dxfLibrary.Circle(
shape.Curve.Center,
shape.Curve.Radius,
color=getACI(ob),
layer=getStrGroup(ob),
)
)
else:
if lwPoly:
points = [
(v.Point.x, v.Point.y, v.Point.z, None, None, 0.0)
for v in shape.Vertexes
]
dxf.append(
dxfLibrary.LwPolyLine(
points,
[0.0, 0.0],
1,
color=getACI(ob),
layer=getGroup(ob),
)
)
else:
points = [
((v.Point.x, v.Point.y, v.Point.z), None, [None, None], 0.0)
for v in shape.Vertexes
]
dxf.append(
dxfLibrary.PolyLine(
points,
[0.0, 0.0, 0.0],
1,
color=getACI(ob),
layer=getGroup(ob),
)
)
elif ob.isDerivedFrom("Part::Feature"):
tess = None
if getattr(ob, "Tessellation", False):
tess = [ob.Tessellation, ob.SegmentLength]
if ob.isDerivedFrom("Sketcher::SketchObject"):
if base_sketch_pla is None:
base_sketch_pla = ob.Placement
sh = Part.Compound()
sh.Placement = base_sketch_pla
sh.add(ob.Shape.copy())
sh.transformShape(base_sketch_pla.inverse().Matrix)
elif params.get_param("dxfmesh"):
sh = None
if not ob.Shape.isNull():
writeMesh(ob, dxf)
elif gui and params.get_param("dxfproject"):
_view = FreeCADGui.ActiveDocument.ActiveView
direction = _view.getViewDirection().multiply(-1)
sh = projectShape(ob.Shape, direction, tess)
elif ob.Shape.Volume > 0:
sh = projectShape(ob.Shape, Vector(0, 0, 1), tess)
else:
sh = ob.Shape
if sh:
if not sh.isNull():
if sh.ShapeType == "Compound":
if len(sh.Wires) == 1:
# only one wire in this compound,
# no lone edge -> polyline
if len(sh.Wires[0].Edges) == len(sh.Edges):
writeShape(sh, ob, dxf, nospline, lwPoly)
else:
# 1 wire + lone edges -> block
block = getBlock(sh, ob, lwPoly)
dxf.blocks.append(block)
dxf.append(
dxfLibrary.Insert(
name=ob.Name.upper(),
color=getACI(ob),
layer=getStrGroup(ob),
)
)
else:
# all other cases: block
block = getBlock(sh, ob, lwPoly)
dxf.blocks.append(block)
dxf.append(
dxfLibrary.Insert(
name=ob.Name.upper(),
color=getACI(ob),
layer=getStrGroup(ob),
)
)
else:
writeShape(sh, ob, dxf, nospline, lwPoly)
elif obtype == "Annotation":
# old-style texts
# temporary - as dxfLibrary doesn't support mtexts well,
# we use several single-line texts
# well, anyway, at the moment, Draft only writes
# single-line texts, so...
for text in ob.LabelText:
point = DraftVecUtils.tup(
Vector(
ob.Position.x,
ob.Position.y - ob.LabelText.index(text),
ob.Position.z,
)
)
if gui:
height = float(ob.ViewObject.FontSize)
justifyhor = ("Left", "Center", "Right").index(
ob.ViewObject.Justification
)
else:
height = 1
justifyhor = 0
dxf.append(
dxfLibrary.Text(
text,
point,
alignment=point if justifyhor else None,
height=height,
justifyhor=justifyhor,
color=getACI(ob, text=True),
style="STANDARD",
layer=getStrGroup(ob),
)
)
elif obtype in ("DraftText", "Text"):
# texts
if gui:
height = float(ob.ViewObject.FontSize)
justifyhor = ("Left", "Center", "Right").index(ob.ViewObject.Justification)
else:
height = 1
justifyhor = 0
for idx, text in enumerate(ob.Text):
point = DraftVecUtils.tup(
Vector(
ob.Placement.Base.x,
ob.Placement.Base.y - (height * 1.2 * idx),
ob.Placement.Base.z,
)
)
rotation = math.degrees(ob.Placement.Rotation.Angle)
dxf.append(
dxfLibrary.Text(
text,
point,
alignment=point if justifyhor else None,
height=height * 0.8,
justifyhor=justifyhor,
rotation=rotation,
color=getACI(ob, text=True),
style="STANDARD",
layer=getStrGroup(ob),
)
)
elif obtype in ["Dimension", "LinearDimension"]:
p1 = DraftVecUtils.tup(ob.Start)
p2 = DraftVecUtils.tup(ob.End)
base = Part.LineSegment(ob.Start, ob.End).toShape()
proj = DraftGeomUtils.findDistance(ob.Dimline, base)
if not proj:
pbase = DraftVecUtils.tup(ob.End)
else:
pbase = DraftVecUtils.tup(ob.End.add(proj.negative()))
dxf.append(
dxfLibrary.Dimension(pbase, p1, p2, color=getACI(ob), layer=getStrGroup(ob))
)
dxf.saveas(filename)
FCC.PrintMessage("successfully exported" + " " + filename + "\n")
else:
errorDXFLib(gui)
class dxfcounter:
"""DXF counter class to count the number of entities."""
def __init__(self):
# this leaves 10000 entities for the template
self.count = 10000
def incr(self, matchobj):
self.count += 1
# print(format(self.count, '02x'))
return format(self.count, "02x")
def exportPage(page, filename):
"""Export a page created with Drawing or TechDraw workbenches.
The template is extracted from the page.
If the template exists in the system, it will be searched
for editable text fields, and replaced with their text values.
If no template is found a dummy default DXF template is used.
For TechDraw pages their templates are not supported currently,
so the dummy template will be used.
It considers all views or groups in the page,
and tries to get the blocks and entities with `getViewDXF(view)`.
It also increments the counter by using the `dxfcounter` class.
The blocks and entities are added to the template, and finally
this template is written into the `filename`.
Parameters
----------
page : object derived from 'TechDraw::DrawPage'
A TechDraw page to export.
filename : str
The path of the new DXF file.
"""
if hasattr(page.Template, "Template"): # techdraw
template = "" # not supported for now...
views = page.Views
else: # drawing
template = os.path.splitext(page.Template)[0] + ".dxf"
views = page.Group
if os.path.exists(template):
f = pyopen(template, "U")
template = f.read()
f.close()
# find & replace editable texts
f = pyopen(page.Template, "rb")
svgtemplate = f.read()
f.close()
editables = re.findall(r"freecad:editable=\"(.*?)\"", svgtemplate)
values = page.EditableTexts
for i in range(len(editables)):
if len(values) > i:
template = template.replace(editables[i], values[i])
else:
# dummy default template
print("DXF version of the template not found. " "Creating a default empty template.")
_v = FreeCAD.Version()
_version = _v[0] + "." + _v[1] + "-" + _v[2]
template = "999\nFreeCAD DXF exporter v" + _version + "\n"
template += "0\nSECTION\n2\nHEADER\n9\n$ACADVER\n1\nAC1009\n0\nENDSEC\n"
template += "0\nSECTION\n2\nBLOCKS\n999\n$blocks\n0\nENDSEC\n"
template += "0\nSECTION\n2\nENTITIES\n999\n$entities\n0\nENDSEC\n"
template += "0\nEOF"
blocks = ""
entities = ""
r12 = False
ver = re.findall(r"\\$ACADVER\n.*?\n(.*?)\n", template)
if ver:
# at the moment this is not used.
# TODO: if r12, do not print ellipses or splines
if ver[0].upper() in ["AC1009", "AC1010", "AC1011", "AC1012", "AC1013"]:
r12 = True
for view in views:
b, e = getViewDXF(view)
blocks += b
entities += e
if blocks:
template = template.replace("999\n$blocks", blocks[:-1])
if entities:
template = template.replace("999\n$entities", entities[:-1])
c = dxfcounter()
pat = re.compile(r"(_handle_)")
template = pat.sub(c.incr, template)
f = pyopen(filename, "w")
f.write(template)
f.close()
def getViewBlock(geom, view, blockcount):
"""Get a view block.
It iterates over all `geom` objects.
If the global variable `dxfExportBlocks` exists, it will create
the appropriate strings for `BLOCK` and `INSERT` sections,
and increment the `blockcount`.
Otherwise, it will just create an insert by changing the layer,
and setting a handle.
Parameters
----------
geom : list of str
A list string objects or a single object, returned by
the `getDXF()` method of the `view`.
view : page view
A TechDraw view which may be of different types
depending on the objects being projected:
`'TechDraw::DrawViewDraft'`, or `'TechDraw::DrawViewArch'`.
blockcount : int
A counter that increments by one each time an insert and block
are added to the output strings, if the global variable
`dxfExportBlocks` exists.
Returns
-------
str, str, int
A tuple containing the strings for blocks, inserts,
and the final value of `blockcount`.
To do
-----
Use local variables, not global variables.
"""
insert = ""
block = ""
r = view.Rotation
if r != 0:
r = -r # fix rotation direction
if not isinstance(geom, list):
geom = [geom]
for g in geom: # getDXF returns a list of entities
if dxfExportBlocks:
# change layer and set color and ltype to BYBLOCK (0)
g = g.replace("sheet_layer\n", "0\n6\nBYBLOCK\n62\n0\n5\n_handle_\n")
block += "0\nBLOCK\n5\n_handle_\n100\nAcDbEntity\n8\n0\n100\nAcDbBlockBegin\n2\n"
block += view.Name + str(blockcount)
block += "\n70\n0\n10\n0\n20\n0\n3\n"
block += view.Name + str(blockcount) + "\n1\n\n"
block += g
block += "0\nENDBLK\n5\n_handle_\n100\nAcDbEntity\n8\n0\n100\nAcDbBlockEnd\n"
insert += "0\nINSERT\n5\n_handle_\n8\n0\n6\nBYLAYER\n62\n256\n2\n"
insert += view.Name + str(blockcount)
insert += "\n10\n" + str(view.X) + "\n20\n" + str(view.Y)
insert += (
"\n30\n0\n41\n"
+ str(view.Scale)
+ "\n42\n"
+ str(view.Scale)
+ "\n43\n"
+ str(view.Scale)
)
insert += "\n50\n" + str(r) + "\n"
blockcount += 1
else:
# change layer, add handle
g = g.replace("sheet_layer\n", "0\n5\n_handle_\n")
insert += g
return block, insert, blockcount
def getViewDXF(view):
"""Return a DXF fragment from a TechDraw view.
Depending on the type of page view, it will try
obtaining `geom`, the DXF representation of `view`,
and then extract the block and insert strings
with `getViewBlock(geom, view, blockcount)`,
starting with a `blockcount` of 1.
If the `view` is `'TechDraw::DrawViewPart'`,
and if the global variable `dxfExportBlocks` exists, it will create
the appropriate strings for `BLOCK` and `INSERT` sections,
and increment the `blockcount`.
Otherwise, it will just create an insert by changing the layer,
and setting a handle
Parameters
----------
view : App::DocumentObjectGroup or page view
A TechDraw view which may be of different types
depending on the objects being projected:
`'TechDraw::DrawViewDraft'`, `'TechDraw::DrawViewArch'`,
`'TechDraw::DrawViewPart'`, `'TechDraw::DrawViewAnnotation'`
Returns
-------
str, str
It returns the two strings for DXF blocks and inserts.
To do
-----
Use local variables, not global variables.
"""
block = ""
insert = ""
blockcount = 1
if view.isDerivedFrom("TechDraw::DrawViewDraft"):
geom = Draft.get_dxf(view)
block, insert, blockcount = getViewBlock(geom, view, blockcount)
elif view.isDerivedFrom("TechDraw::DrawViewArch"):
import ArchSectionPlane
geom = ArchSectionPlane.getDXF(view)
block, insert, blockcount = getViewBlock(geom, view, blockcount)
elif view.isDerivedFrom("TechDraw::DrawViewPart"):
import TechDraw
for obj in view.Source:
proj = TechDraw.projectToDXF(obj.Shape, view.Direction)
if dxfExportBlocks:
# change layer and set color and ltype to BYBLOCK (0)
proj = proj.replace("sheet_layer\n", "0\n6\nBYBLOCK\n62\n0\n5\n_handle_\n")
block += "0\nBLOCK\n5\n_handle_\n100\nAcDbEntity\n8\n0\n100\nAcDbBlockBegin\n2\n"
block += view.Name + str(blockcount)
block += "\n70\n0\n10\n0\n20\n0\n3\n" + view.Name + str(blockcount)
block += "\n1\n\n"
block += proj
block += "0\nENDBLK\n5\n_handle_\n100\nAcDbEntity\n8\n0\n100\nAcDbBlockEnd\n"
insert += "0\nINSERT\n5\n_handle_\n8\n0\n6\nBYLAYER\n62\n256\n2\n"
insert += view.Name + str(blockcount)
insert += "\n10\n" + str(view.X) + "\n20\n" + str(view.Y)
insert += "\n30\n0\n41\n" + str(view.Scale)
insert += "\n42\n" + str(view.Scale) + "\n43\n" + str(view.Scale)
insert += "\n50\n" + str(view.Rotation) + "\n"
blockcount += 1
else:
proj = proj.replace("sheet_layer\n", "0\n5\n_handle_\n")
insert += proj # view.Rotation is ignored
elif view.isDerivedFrom("TechDraw::DrawViewAnnotation"):
insert = "0\nTEXT\n5\n_handle_\n8\n0\n100\nAcDbEntity\n100\nAcDbText\n5\n_handle_"
insert += "\n10\n" + str(view.X) + "\n20\n" + str(view.Y)
insert += "\n30\n0\n40\n" + str(view.Scale / 2)
insert += "\n50\n" + str(view.Rotation)
insert += "\n1\n" + view.Text[0] + "\n"
else:
print("Unable to get DXF representation from view: ", view.Label)
return block, insert
def readPreferences():
"""Read the preferences of the this module from the parameter database.
It creates and sets the global variables:
`dxfCreatePart`, `dxfCreateDraft`, `dxfCreateSketch`,
`dxfDiscretizeCurves`, `dxfStarBlocks`, `dxfMakeBlocks`, `dxfJoin`,
`dxfRenderPolylineWidth`, `dxfImportTexts`, `dxfImportLayouts`,
`dxfImportPoints`, `dxfImportHatches`, `dxfUseStandardSize`,
`dxfGetColors`, `dxfUseDraftVisGroups`,
`dxfBrightBackground`, `dxfDefaultColor`, `dxfUseLegacyImporter`,
`dxfExportBlocks`, `dxfScaling`, `dxfUseLegacyExporter`
The parameter path is ``User parameter:BaseApp/Preferences/Mod/Draft``
To do
-----
Use local variables, not global variables.
"""
global dxfCreatePart, dxfCreateDraft, dxfCreateSketch
global dxfDiscretizeCurves, dxfStarBlocks, dxfMakeBlocks, dxfJoin, dxfRenderPolylineWidth
global dxfImportTexts, dxfImportLayouts, dxfImportPoints, dxfImportHatches, dxfUseStandardSize
global dxfGetColors, dxfUseDraftVisGroups, dxfBrightBackground, dxfDefaultColor
global dxfUseLegacyImporter, dxfExportBlocks, dxfScaling, dxfUseLegacyExporter
# Use the direct C++ API via Python for all parameter access
hGrp = FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Mod/Draft")
dxfUseLegacyImporter = hGrp.GetBool("dxfUseLegacyImporter", False)
# Synchronization Bridge (Booleans -> Integer)
# Read the boolean parameters from the main preferences dialog. Based on which one is true, set
# the single 'DxfImportMode' integer parameter that the C++ importer and legacy importer logic
# rely on. This ensures the setting from the main preferences is always respected at the start
# of an import.
if hGrp.GetBool("dxfImportAsDraft", False):
import_mode = 0
elif hGrp.GetBool("dxfImportAsPrimitives", False):
import_mode = 1
elif hGrp.GetBool("dxfImportAsFused", False):
import_mode = 3
else: # Default to "Individual part shapes"
import_mode = 2
hGrp.SetInt("DxfImportMode", import_mode)
# The legacy importer logic now reads the unified import_mode integer.
# The modern importer reads its settings directly in C++.
if dxfUseLegacyImporter:
# Legacy override for sketch creation takes highest priority
dxfCreateSketch = hGrp.GetBool("dxfCreateSketch", False)
if dxfCreateSketch: # dxfCreateSketch overrides the import mode for the legacy importer
dxfCreatePart = False
dxfCreateDraft = False
dxfMakeBlocks = False
# The 'import_mode' variable is now set by the UI synchronization bridge that runs just
# before this block. We now translate the existing 'import_mode' variable into the old
# flags.
elif import_mode == 0: # Editable draft objects
dxfMakeBlocks = False
dxfCreatePart = False
dxfCreateDraft = True
elif import_mode == 3: # Fused part shapes
dxfMakeBlocks = True
dxfCreatePart = False
dxfCreateDraft = False
else: # Individual part shapes or Primitives (modes 1 and 2)
dxfMakeBlocks = False
dxfCreatePart = True
dxfCreateDraft = False
# The legacy importer still uses these global variables, so we read them all.
dxfDiscretizeCurves = hGrp.GetBool("DiscretizeEllipses", True)
dxfStarBlocks = hGrp.GetBool("dxfstarblocks", False)
dxfJoin = hGrp.GetBool("joingeometry", False)
dxfRenderPolylineWidth = hGrp.GetBool("renderPolylineWidth", False)
dxfImportTexts = hGrp.GetBool("dxftext", False)
dxfImportLayouts = hGrp.GetBool("dxflayout", False)
dxfImportPoints = hGrp.GetBool("dxfImportPoints", True)
dxfImportHatches = hGrp.GetBool("importDxfHatches", False)
dxfUseStandardSize = hGrp.GetBool("dxfStdSize", False)
dxfGetColors = hGrp.GetBool("dxfGetOriginalColors", True)
dxfUseDraftVisGroups = hGrp.GetBool("dxfUseDraftVisGroups", True)
dxfUseLegacyExporter = hGrp.GetBool("dxfUseLegacyExporter", False)
dxfExportBlocks = hGrp.GetBool("dxfExportBlocks", True)
dxfScaling = hGrp.GetFloat("dxfScaling", 1.0)
dxfBrightBackground = isBrightBackground()
dxfDefaultColor = getColor()
class DxfImportReporter:
"""Formats and reports statistics from a DXF import process."""
def __init__(self, filename, stats_dict, total_time=0.0):
self.filename = filename
self.stats = stats_dict
self.total_time = total_time
def to_console_string(self):
"""
Formats the statistics into a human-readable string for console output.
"""
if not self.stats:
return "DXF Import: no statistics were returned from the importer.\n"
lines = ["\n--- DXF import summary ---"]
lines.append(f"Import of file: '{self.filename}'\n")
# General info
lines.append(f"DXF version: {self.stats.get('dxfVersion', 'Unknown')}")
lines.append(f"File encoding: {self.stats.get('dxfEncoding', 'Unknown')}")
# Scaling info
file_units = self.stats.get("fileUnits", "Not specified")
source = self.stats.get("scalingSource", "")
if source:
lines.append(f"File units: {file_units} (from {source})")
else:
lines.append(f"File units: {file_units}")
manual_scaling = self.stats.get("importSettings", {}).get("Manual scaling factor", "1.0")
lines.append(f"Manual scaling factor: {manual_scaling}")
final_scaling = self.stats.get("finalScalingFactor", 1.0)
lines.append(f"Final scaling: 1 DXF unit = {final_scaling:.4f} mm")
lines.append("")
# Timing
lines.append("Performance:")
cpp_time = self.stats.get("importTimeSeconds", 0.0)
lines.append(f" - C++ import time: {cpp_time:.4f} seconds")
lines.append(f" - Total import time: {self.total_time:.4f} seconds")
lines.append("")
# Settings
lines.append("Import settings:")
settings = self.stats.get("importSettings", {})
if settings:
for key, value in sorted(settings.items()):
lines.append(f" - {key}: {value}")
else:
lines.append(" (No settings recorded)")
lines.append("")
# Counts
lines.append("Entity counts:")
total_read = 0
unsupported_keys = self.stats.get("unsupportedFeatures", {}).keys()
unsupported_entity_names = set()
for key in unsupported_keys:
# Extract the entity name from the key string, e.g., 'HATCH' from "Entity type 'HATCH'"
entity_name_match = re.search(r"\'(.*?)\'", key)
if entity_name_match:
unsupported_entity_names.add(entity_name_match.group(1))
has_unsupported_indicator = False
entities = self.stats.get("entityCounts", {})
if entities:
for key, value in sorted(entities.items()):
indicator = ""
if key in unsupported_entity_names:
indicator = " (*)"
has_unsupported_indicator = True
lines.append(f" - {key}: {value}{indicator}")
total_read += value
lines.append("----------------------------")
lines.append(f" Total entities read: {total_read}")
else:
lines.append(" (No entities recorded)")
lines.append(f"FreeCAD objects created: {self.stats.get('totalEntitiesCreated', 0)}")
lines.append("")
# System Blocks
lines.append("System Blocks:")
system_blocks = self.stats.get("systemBlockCounts", {})
if system_blocks:
for key, value in sorted(system_blocks.items()):
lines.append(f" - {key}: {value}")
else:
lines.append(" (None found or imported)")
lines.append("")
if has_unsupported_indicator:
lines.append("(*) Entity type not supported by importer.")
lines.append("")
lines.append("Unsupported features:")
unsupported = self.stats.get("unsupportedFeatures", {})
if unsupported:
for key, occurrences in sorted(unsupported.items()):
count = len(occurrences)
max_details_to_show = 5
details_list = []
for i, (line, handle) in enumerate(occurrences):
if i >= max_details_to_show:
break
if handle:
details_list.append(f"line {line} (handle {handle})")
else:
details_list.append(f"line {line} (no handle available)")
details_str = ", ".join(details_list)
if count > max_details_to_show:
lines.append(f" - {key}: {count} time(s). Examples: {details_str}, ...")
else:
lines.append(f" - {key}: {count} time(s) at {details_str}")
else:
lines.append(" (none)")
lines.append("--- End of summary ---\n")
return "\n".join(lines)
def report_to_console(self):
"""
Prints the formatted statistics string to the FreeCAD console.
"""
output_string = self.to_console_string()
FCC.PrintMessage(output_string)
class DxfDraftPostProcessor:
"""
Handles the post-processing of DXF files imported as Part objects,
converting them into fully parametric Draft objects while preserving
the block and layer hierarchy.
"""
def __init__(self, doc, new_objects, import_mode):
self.doc = doc
self.all_imported_objects = new_objects
self.import_mode = import_mode
self.all_originals_to_delete = set()
self.newly_created_draft_objects = []
def _categorize_objects(self):
"""
Scans newly created objects from the C++ importer and categorizes them.
"""
block_definitions = {}
for group_name in ["_BlockDefinitions", "_UnreferencedBlocks"]:
block_group = self.doc.getObject(group_name)
if block_group:
for block_def_obj in block_group.Group:
if block_def_obj.isValid() and block_def_obj.isDerivedFrom("Part::Compound"):
block_definitions[block_def_obj] = [
child for child in block_def_obj.Links if child.isValid()
]
all_block_internal_objects_set = set()
for block_def, children in block_definitions.items():
all_block_internal_objects_set.add(block_def)
all_block_internal_objects_set.update(children)
top_level_geometry = []
placeholders = []
for obj in self.all_imported_objects:
if not obj.isValid() or obj in all_block_internal_objects_set:
continue
if obj.isDerivedFrom("App::FeaturePython") and hasattr(obj, "DxfEntityType"):
placeholders.append(obj)
elif obj.isDerivedFrom("Part::Feature") or obj.isDerivedFrom("App::Link"):
top_level_geometry.append(obj)
return block_definitions, top_level_geometry, placeholders
def _create_draft_object_from_part(self, part_obj):
"""
Converts an intermediate Part object (from C++ importer) to a final Draft object,
ensuring correct underlying C++ object typing and property management.
Returns a tuple: (new_draft_object, type_string) or (None, None).
"""
if self.import_mode != 0:
# In non-Draft modes, do not convert geometry. Return it as is.
return part_obj, "KeptAsIs"
# Skip invalid objects or objects that are block definitions themselves (their
# links/children will be converted)
if not part_obj.isValid() or (
part_obj.isDerivedFrom("Part::Compound") and hasattr(part_obj, "Links")
):
return None, None
new_obj = None
obj_type_str = None # Will be set based on converted type
# Handle specific Part primitives (created directly by C++ importer as Part::Line,
# Part::Circle, Part::Vertex) These C++ primitives (Part::Line, Part::Circle) inherently
# have Shape and Placement. Part::Vertex is special, handled separately below.
if part_obj.isDerivedFrom("Part::Line"):
# Input `part_obj` is Part::Line. Create a Part::Part2DObjectPython as the
# Python-extensible base for Draft Line. Part::Part2DObjectPython (via Part::Feature)
# inherently has Shape and Placement, and supports .Proxy.
new_obj = self.doc.addObject(
"Part::Part2DObjectPython", self.doc.getUniqueObjectName("Line")
)
# Transfer the TopoDS_Shape from the original Part::Line to the new object's Shape
# property.
new_obj.Shape = part_obj.Shape
Draft.Wire(new_obj) # Attach the Python proxy. It will find Shape, Placement.
# Manually transfer the parametric data from the Part::Line primitive
# to the new Draft.Wire's 'Points' property.
start_point = FreeCAD.Vector(part_obj.X1.Value, part_obj.Y1.Value, part_obj.Z1.Value)
end_point = FreeCAD.Vector(part_obj.X2.Value, part_obj.Y2.Value, part_obj.Z2.Value)
new_obj.Points = [start_point, end_point]
new_obj.MakeFace = False
obj_type_str = "Line"
elif part_obj.isDerivedFrom("Part::Circle"):
# Input `part_obj` is Part::Circle. Create a Part::Part2DObjectPython.
new_obj = self.doc.addObject(
"Part::Part2DObjectPython", self.doc.getUniqueObjectName("Circle")
)
# Transfer the TopoDS_Shape from the original Part::Circle. This needs to happen
# *before* proxy attach.
new_obj.Shape = part_obj.Shape
# Attach the Python proxy
# This call will add properties like Radius, FirstAngle, LastAngle to new_obj.
Draft.Circle(new_obj)
# Transfer data *after* proxy attachment.
# Now that Draft.Circle(new_obj) has run and added the properties, we can assign values
# to them.
# Part::Circle has Radius, Angle1, Angle2 properties.
# Draft.Circle proxy uses FirstAngle and LastAngle instead of Angle1 and Angle2.
if hasattr(part_obj, "Radius"):
new_obj.Radius = FreeCAD.Units.Quantity(part_obj.Radius.Value, "mm")
# Calculate and transfer angles
if hasattr(part_obj, "Angle1") and hasattr(part_obj, "Angle2"):
start_angle, end_angle = self._get_canonical_angles(
part_obj.Angle1.Value, part_obj.Angle2.Value, part_obj.Radius.Value
)
new_obj.FirstAngle = FreeCAD.Units.Quantity(start_angle, "deg")
new_obj.LastAngle = FreeCAD.Units.Quantity(end_angle, "deg")
# Determine the final object type string based on the canonical angles
is_full_circle = (
abs(new_obj.FirstAngle.Value - 0.0) < 1e-7
and abs(new_obj.LastAngle.Value - 360.0) < 1e-7
)
new_obj.MakeFace = False
obj_type_str = "Circle" if is_full_circle else "Arc"
elif part_obj.isDerivedFrom(
"Part::Vertex"
): # Input `part_obj` is Part::Vertex (C++ primitive for a point location).
# For Draft.Point, the proxy expects an App::FeaturePython base.
new_obj = self.doc.addObject(
"App::FeaturePython", self.doc.getUniqueObjectName("Point")
)
new_obj.addExtension(
"Part::AttachExtensionPython"
) # Needed to provide Placement for App::FeaturePython.
# Transfer Placement explicitly from the original Part::Vertex.
if hasattr(part_obj, "Placement"):
new_obj.Placement = part_obj.Placement
else:
new_obj.Placement = FreeCAD.Placement()
Draft.Point(new_obj) # Attach the Python proxy.
obj_type_str = "Point"
elif part_obj.isDerivedFrom("Part::Ellipse"):
# Determine if it's a full ellipse or an arc
# The span check handles cases like (0, 360) or (-180, 180)
span = abs(part_obj.Angle2.Value - part_obj.Angle1.Value)
is_full_ellipse = abs(span % 360.0) < 1e-6
if is_full_ellipse:
# Create the C++ base object that has .Shape and .Placement.
new_obj = self.doc.addObject(
"Part::Part2DObjectPython", self.doc.getUniqueObjectName("Ellipse")
)
# Attach the parametric Draft.Ellipse Python proxy.
Draft.Ellipse(new_obj)
# Transfer the parametric properties from the imported primitive to the new Draft
# object. The proxy will handle recomputing the shape.
new_obj.MajorRadius = part_obj.MajorRadius
new_obj.MinorRadius = part_obj.MinorRadius
new_obj.Placement = part_obj.Placement
obj_type_str = "Ellipse"
else:
# Fallback for elliptical arcs.
new_obj = self.doc.addObject(
"Part::Part2DObjectPython", self.doc.getUniqueObjectName("EllipticalArc")
)
Draft.Wire(new_obj) # Attach proxy.
# Re-create geometry at the origin using parametric properties.
# Convert degrees back to radians for the geometry kernel.
center_at_origin = FreeCAD.Vector(0, 0, 0)
geom = Part.Ellipse(
center_at_origin, part_obj.MajorRadius.Value, part_obj.MinorRadius.Value
)
shape_at_origin = geom.toShape(
math.radians(part_obj.Angle1.Value), math.radians(part_obj.Angle2.Value)
)
# Assign the un-transformed shape and the separate placement.
new_obj.Shape = shape_at_origin
new_obj.Placement = part_obj.Placement
new_obj.MakeFace = False
obj_type_str = "Shape"
# --- Handle generic Part::Feature objects (from C++ importer, wrapping TopoDS_Shapes like Wires, Splines, Ellipses) ---
elif part_obj.isDerivedFrom(
"Part::Feature"
): # Input `part_obj` is a generic Part::Feature (from C++ importer).
shape = (
part_obj.Shape
) # This is the underlying TopoDS_Shape (Wire, Edge, Compound, Face etc.).
if not shape.isValid():
return None, None
# Determine specific Draft object type based on the ShapeType of the TopoDS_Shape.
if shape.ShapeType == "Wire": # If the TopoDS_Shape is a Wire (from DXF POLYLINE).
# Create a Part::Part2DObjectPython as the Python-extensible base for Draft Wire.
new_obj = self.doc.addObject(
"Part::Part2DObjectPython", self.doc.getUniqueObjectName("Wire")
)
new_obj.Shape = shape # Transfer the TopoDS_Wire from the original Part::Feature.
Draft.Wire(new_obj) # Attach Python proxy. It will find Shape, Placement.
# Check if all segments of the wire are straight lines.
# If so, we can safely populate the .Points property to make it parametric.
# Otherwise, we do nothing, leaving it as a non-parametric but geometrically correct shape.
is_all_lines = True
for edge in shape.Edges:
if edge.Curve.TypeId == "Part::GeomLine":
continue # This is a straight segment
else:
is_all_lines = False
break # Found a curve, no need to check further
if is_all_lines and shape.OrderedVertexes:
# All segments are straight, so we can make it an editable wire
points = [v.Point for v in shape.OrderedVertexes]
new_obj.Points = points
new_obj.Closed = (
shape.isClosed()
) # Transfer specific properties expected by Draft.Wire.
new_obj.MakeFace = False
obj_type_str = "Wire"
# Fallback for other Part::Feature shapes (e.g., 3DFACE, SOLID, or unsupported Edge types).
else: # If the TopoDS_Shape is not a recognized primitive (e.g., Compound, Face, Solid).
# Wrap it in a Part::FeaturePython to allow Python property customization if needed.
new_obj = self.doc.addObject(
"Part::FeaturePython", self.doc.getUniqueObjectName("Shape")
)
new_obj.addExtension(
"Part::AttachExtensionPython"
) # Add extension for Placement for App::FeaturePython.
new_obj.Shape = shape # Assign the TopoDS_Shape from the original Part::Feature.
# Explicitly set Placement for App::FeaturePython.
if hasattr(part_obj, "Placement"):
new_obj.Placement = part_obj.Placement
else:
new_obj.Placement = FreeCAD.Placement()
# No specific Draft proxy for generic "Shape", but it's Python extensible.
obj_type_str = "Shape"
# --- Handle App::Link objects (block instances from C++ importer) ---
elif part_obj.isDerivedFrom("App::Link"): # Input `part_obj` is an App::Link.
# App::Link objects are already suitable as a base for Draft.Clone/Array links.
# They natively have Placement and Link properties, and support .Proxy.
new_obj = part_obj # Reuse the object directly.
obj_type_str = "Link"
# --- Handle App::FeaturePython placeholder objects (Text, Dimension from C++ importer) ---
elif part_obj.isDerivedFrom(
"App::FeaturePython"
): # Input `part_obj` is an App::FeaturePython placeholder.
# These are specific placeholders the C++ importer created (`DxfEntityType` property).
# They are processed later in `_create_from_placeholders` to become proper Draft.Text/Dimension objects.
return None, None # Don't process them here; let the dedicated function handle them.
# --- Final Common Steps for Newly Created Draft Objects ---
if new_obj:
new_obj.Label = part_obj.Label # Always transfer label.
# If `new_obj` was freshly created (not `part_obj` reused), and `part_obj` had a Placement,
# ensure `new_obj`'s Placement is correctly set from `part_obj`.
# For `Part::*` types, Placement is set implicitly by the `addObject` call based on their `Shape`.
# For `App::FeaturePython` (like for Point and generic Shape fallback), explicit assignment is needed.
if new_obj is not part_obj:
if hasattr(part_obj, "Placement") and hasattr(new_obj, "Placement"):
new_obj.Placement = part_obj.Placement
elif not hasattr(new_obj, "Placement"):
# This should ideally not happen with the corrected logic above.
FCC.PrintWarning(
f"Created object '{new_obj.Label}' of type '{obj_type_str}' does not have a 'Placement' property even after intended setup. This is unexpected.\n"
)
# Add the original object (from C++ importer) to the list for deletion.
if new_obj is not part_obj:
self.all_originals_to_delete.add(part_obj)
return new_obj, obj_type_str
# If no conversion could be made (e.g., unsupported DXF entity not falling into a handled case),
# mark original for deletion and return None.
self.all_originals_to_delete.add(part_obj)
FCC.PrintWarning(
f"DXF Post-Processor: Failed to convert object '{part_obj.Label}'. Discarding.\n"
)
return None, None
def _parent_object_to_layer(self, new_obj, original_obj):
"""Finds the correct layer from the original object and parents the new object to it."""
if hasattr(original_obj, "OriginalLayer"):
layer_name = original_obj.OriginalLayer
found_layers = self.doc.getObjectsByLabel(layer_name)
layer_obj = None
if found_layers:
for l_obj in found_layers:
if Draft.get_type(l_obj) == "Layer":
layer_obj = l_obj
break
if layer_obj:
layer_obj.Proxy.addObject(layer_obj, new_obj)
else:
FCC.PrintWarning(
f"DXF Post-Processor: Could not find a valid Draft Layer with label '{layer_name}' for object '{new_obj.Label}'.\n"
)
def _create_and_parent_geometry(self, intermediate_obj):
"""High-level helper to convert, name, and parent a single geometric object."""
new_draft_obj, obj_type_str = self._create_draft_object_from_part(intermediate_obj)
if new_draft_obj:
label = intermediate_obj.Label
if not label or "__Feature" in label:
label = self.doc.getUniqueObjectName(obj_type_str)
new_draft_obj.Label = label
self._parent_object_to_layer(new_draft_obj, intermediate_obj)
self.newly_created_draft_objects.append(new_draft_obj)
else:
FCC.PrintWarning(
f"DXF Post-Processor: Failed to convert object '{intermediate_obj.Label}'. Discarding.\n"
)
return new_draft_obj
def _create_from_placeholders(self, placeholders):
"""Creates final Draft objects from text/dimension placeholders."""
if not placeholders:
return
for placeholder in placeholders:
if not placeholder.isValid():
continue
new_obj = None
try:
if placeholder.DxfEntityType == "DIMENSION":
# 1. Create the base object and attach the proxy, which adds the needed properties.
dim = self.doc.addObject("App::FeaturePython", "Dimension")
_Dimension(dim)
if FreeCAD.GuiUp:
ViewProviderLinearDimension(dim.ViewObject)
# 2. Get the transformation from the placeholder's Placement property.
plc = placeholder.Placement
# 3. Transform the defining points from the placeholder's local coordinate system
# into the world coordinate system.
p_start = plc.multVec(placeholder.Start)
p_end = plc.multVec(placeholder.End)
p_dimline = plc.multVec(placeholder.Dimline)
# 4. Assign these new, transformed points to the final dimension object.
dim.Start = p_start
dim.End = p_end
dim.Dimline = p_dimline
# Do NOT try to set dim.Placement, as it does not exist.
new_obj = dim
# Check for and apply the dimension type (horizontal, vertical, etc.)
# This information is now plumbed through from the C++ importer.
if hasattr(placeholder, "DxfDimensionType"):
# The lower bits of the type flag define the dimension's nature.
# 0 = Rotated, Horizontal, or Vertical
# 1 = Aligned
# Other values are for angular, diameter, etc., not handled here.
dim_type = placeholder.DxfDimensionType & 0x0F
# A type of 0 indicates that the dimension is projected. The
# projection direction is given by its rotation angle.
if dim_type == 0 and hasattr(placeholder, "DxfRotation"):
angle = placeholder.DxfRotation.Value # Angle is in radians
# The Direction property on a Draft.Dimension controls its
# projection. Setting it here ensures the ViewProvider
# will draw it correctly as horizontal, vertical, or rotated.
direction_vector = FreeCAD.Vector(math.cos(angle), math.sin(angle), 0)
dim.Direction = direction_vector
elif placeholder.DxfEntityType == "TEXT":
text_obj = Draft.make_text(placeholder.Text)
text_obj.Placement = placeholder.Placement
if FreeCAD.GuiUp:
text_obj.addProperty("App::PropertyFloat", "DxfTextHeight", "Internal")
text_obj.DxfTextHeight = placeholder.DxfTextHeight
new_obj = text_obj
if new_obj:
new_obj.Label = placeholder.Label
self._parent_object_to_layer(new_obj, placeholder)
self.newly_created_draft_objects.append(new_obj)
except Exception as e:
FCC.PrintWarning(
f"Could not create Draft object from placeholder '{placeholder.Label}': {e}\n"
)
self.all_originals_to_delete.update(placeholders)
def _apply_gui_styles(self):
"""Attaches correct ViewProviders and styles to new Draft objects."""
if not FreeCAD.GuiUp:
return
# We style all newly created Draft objects, which are collected in this list.
# This now includes block children, top-level geometry, and placeholders.
all_objects_to_style = self.newly_created_draft_objects
for obj in all_objects_to_style:
if obj.isValid() and hasattr(obj, "ViewObject") and hasattr(obj, "Proxy"):
try:
proxy_name = obj.Proxy.__class__.__name__
if proxy_name in ("Wire", "Line"):
if ViewProviderWire:
ViewProviderWire(obj.ViewObject)
elif proxy_name == "Circle":
if ViewProviderDraft:
ViewProviderDraft(obj.ViewObject)
elif proxy_name == "Text":
if hasattr(obj, "DxfTextHeight"):
obj.ViewObject.FontSize = obj.DxfTextHeight * TEXTSCALING
except Exception as e:
FCC.PrintWarning(f"Failed to set ViewProvider for {obj.Name}: {e}\n")
def _delete_objects_in_batch(self):
"""Safely deletes all objects marked for removal."""
if not self.all_originals_to_delete:
return
for obj in self.all_originals_to_delete:
if obj.isValid() and self.doc.getObject(obj.Name) is not None:
try:
if not obj.isDerivedFrom("App::DocumentObjectGroup") and not obj.isDerivedFrom(
"App::Link"
):
self.doc.removeObject(obj.Name)
except Exception as e:
FCC.PrintWarning(
f"Failed to delete object '{getattr(obj, 'Label', obj.Name)}': {e}\n"
)
def _cleanup_organizational_groups(self):
"""Removes empty organizational groups after processing."""
for group_name in ["_BlockDefinitions", "_UnreferencedBlocks"]:
group = self.doc.getObject(group_name)
if group and not group.Group:
try:
self.doc.removeObject(group.Name)
except Exception as e:
FCC.PrintWarning(
"DXF Post-Processor: Could not remove temporary group "
f"'{group.Name}': {e}\n"
)
def _get_canonical_angles(self, start_angle_deg, end_angle_deg, radius_mm):
"""
Calculates canonical start and end angles for a Draft Arc/Circle that are
both geometrically equivalent to the input and syntactically valid for
FreeCAD's App::PropertyAngle, which constrains values to [-360, 360].
This is necessary because the C++ importer may provide angles outside this
range (e.g., end_angle > 360) to unambiguously define an arc's span and
distinguish between minor and major arcs. This function finds an
equivalent angle pair that respects the C++ constraints while preserving
the original geometry (span and direction).
"""
# Calculate the original angular span.
span = end_angle_deg - start_angle_deg
# Handle degenerate and full-circle cases first.
# Case: A zero-radius, zero-span arc is a point.
if abs(radius_mm) < 1e-9 and abs(span) < 1e-9:
return 0.0, 0.0
# A span that is a multiple of 360 degrees is a full circle.
# Use a tolerance for floating point inaccuracies.
if abs(span % 360.0) < 1e-6 and abs(span) > 1e-7:
# Return the canonical representation for a full circle in Draft.
return 0.0, 360.0
# Normalize the start angle to a canonical [0, 360] range.
canonical_start = start_angle_deg % 360.0
if canonical_start < 0:
canonical_start += 360.0
# Calculate the geometrically correct end angle based on the preserved span.
canonical_end = canonical_start + span
# Find a valid representation within the [-360, 360] constraints.
# We can shift both start and end by multiples of 360 without changing the geometry.
# This "slides" the angular window until it fits within the allowed range.
# This handles cases where the calculated end > 360 or start is very negative.
while canonical_start > 360.0 or canonical_end > 360.0:
canonical_start -= 360.0
canonical_end -= 360.0
while canonical_start < -360.0 or canonical_end < -360.0:
canonical_start += 360.0
canonical_end += 360.0
# At this point, the pair (canonical_start, canonical_end) is both
# geometrically correct and should be valid for App::PropertyAngle.
return canonical_start, canonical_end
def run(self):
"""Executes the entire post-processing workflow."""
FCC.PrintMessage("\n--- DXF DRAFT POST-PROCESSING ---\n")
if not self.all_imported_objects:
return
self.doc.openTransaction("DXF Post-processing")
try:
block_defs, top_geo, placeholders = self._categorize_objects()
# Process geometry inside block definitions
for block_def_obj, original_children in block_defs.items():
new_draft_children = [
self._create_and_parent_geometry(child) for child in original_children
]
block_def_obj.Links = [obj for obj in new_draft_children if obj]
self.all_originals_to_delete.update(
set(original_children) - set(new_draft_children)
)
# Process top-level geometry
converted_top_geo = []
for part_obj in top_geo:
new_obj = self._create_and_parent_geometry(part_obj)
if new_obj:
converted_top_geo.append(new_obj)
self.all_originals_to_delete.update(set(top_geo) - set(converted_top_geo))
# Process placeholders like Text and Dimensions
self._create_from_placeholders(placeholders)
# Perform all deletions at once
self._delete_objects_in_batch()
except Exception as e:
self.doc.abortTransaction()
FCC.PrintError(f"Aborting DXF post-processing due to an error: {e}\n")
import traceback
traceback.print_exc()
return
finally:
self.doc.commitTransaction()
self._apply_gui_styles()
self._cleanup_organizational_groups()
self.doc.recompute()
FCC.PrintMessage("--- Draft post-processing finished. ---\n")
|