|
| 1 | +import os |
1 | 2 | import subprocess |
2 | 3 | import psutil |
3 | 4 | import pythoncom |
4 | 5 | import win32com.client |
5 | 6 | from enum import Enum |
| 7 | +import time |
6 | 8 |
|
7 | 9 | # project by jones peter |
8 | 10 | class Color(Enum): |
@@ -691,20 +693,37 @@ def __init__(self): |
691 | 693 | CADException: If AutoCAD application cannot be initialized. |
692 | 694 | """ |
693 | 695 | try: |
694 | | - self.acad = win32com.client.Dispatch("AutoCAD.Application") |
| 696 | + try: |
| 697 | + self.acad = win32com.client.gencache.EnsureDispatch("AutoCAD.Application") |
| 698 | + except Exception: |
| 699 | + self.acad = win32com.client.Dispatch("AutoCAD.Application") |
695 | 700 | self.acad.Visible = True |
696 | 701 | except Exception as e: |
697 | 702 | raise CADException(f"Error initializing AutoCAD: {e}") |
698 | 703 |
|
| 704 | + def _get_with_retry(self, func, retries=5, delay=0.5): |
| 705 | + """ |
| 706 | + Helper method to retry COM property/method access. |
| 707 | + Often needed when AutoCAD is busy initializing or loading a document. |
| 708 | + """ |
| 709 | + for i in range(retries): |
| 710 | + try: |
| 711 | + return func() |
| 712 | + except Exception as e: |
| 713 | + # "Call was rejected by callee" (-2147418111) usually means busy |
| 714 | + if i == retries - 1: |
| 715 | + raise e |
| 716 | + time.sleep(delay) |
| 717 | + |
699 | 718 | @property |
700 | 719 | def doc(self): |
701 | 720 | """ Returns `ActiveDocument` of current :attr:`Application`""" |
702 | | - return self.acad.ActiveDocument |
| 721 | + return self._get_with_retry(lambda: self.acad.ActiveDocument) |
703 | 722 |
|
704 | 723 | @property |
705 | 724 | def modelspace(self): |
706 | | - """ Returns `ActiveDocument` of current :attr:`Application`""" |
707 | | - return self.doc.ModelSpace |
| 725 | + """ Returns `ModelSpace` of current :attr:`Application`""" |
| 726 | + return self._get_with_retry(lambda: self.doc.ModelSpace) |
708 | 727 |
|
709 | 728 | @property |
710 | 729 | def properties(self): |
@@ -751,9 +770,17 @@ def iter_objects(self, object_type=None): |
751 | 770 | Yields: |
752 | 771 | The AutoCAD object. |
753 | 772 | """ |
754 | | - for obj in self.modelspace: |
755 | | - if object_type is None or obj.EntityName == object_type: |
756 | | - yield obj |
| 773 | + # Changed to index-based iteration to avoid COM enumeration issues with early binding |
| 774 | + model_space = self.modelspace |
| 775 | + count = self._get_with_retry(lambda: model_space.Count) |
| 776 | + for i in range(count): |
| 777 | + try: |
| 778 | + obj = self._get_with_retry(lambda: model_space.Item(i), retries=3, delay=0.1) |
| 779 | + entity_name = self._get_with_retry(lambda: obj.EntityName, retries=3, delay=0.1) |
| 780 | + if object_type is None or entity_name == object_type: |
| 781 | + yield obj |
| 782 | + except Exception: |
| 783 | + continue |
757 | 784 |
|
758 | 785 | def add_circle(self, center, radius): |
759 | 786 | """ |
@@ -798,8 +825,9 @@ def _ensure_linetype_loaded(self, linetype_name): |
798 | 825 | try: |
799 | 826 | linetypes = self.doc.Linetypes |
800 | 827 | linetype_exists = False |
801 | | - for lt in linetypes: |
802 | | - if lt.Name.lower() == linetype_name.lower(): |
| 828 | + # Iterate by index to avoid potential enumeration issues |
| 829 | + for i in range(linetypes.Count): |
| 830 | + if linetypes.Item(i).Name.lower() == linetype_name.lower(): |
803 | 831 | linetype_exists = True |
804 | 832 | break |
805 | 833 |
|
@@ -1101,8 +1129,12 @@ def get_user_defined_blocks(self): |
1101 | 1129 | """ |
1102 | 1130 | try: |
1103 | 1131 | blocks = self.doc.Blocks |
1104 | | - user_defined_blocks = [block.Name for block in blocks |
1105 | | - if not block.IsLayout and not block.Name.startswith('*') and block.Name != 'GENAXEH'] |
| 1132 | + user_defined_blocks = [] |
| 1133 | + # Changed to index-based iteration |
| 1134 | + for i in range(blocks.Count): |
| 1135 | + block = blocks.Item(i) |
| 1136 | + if not block.IsLayout and not block.Name.startswith('*') and block.Name != 'GENAXEH': |
| 1137 | + user_defined_blocks.append(block.Name) |
1106 | 1138 | return user_defined_blocks |
1107 | 1139 | except Exception as e: |
1108 | 1140 | raise CADException(f"Error getting user-defined blocks: {e}") |
@@ -1204,6 +1236,8 @@ def open_file(self, file_path): |
1204 | 1236 | """ |
1205 | 1237 | try: |
1206 | 1238 | self.acad.Documents.Open(file_path) |
| 1239 | + # Give AutoCAD a small moment to initialize the COM objects for the newly opened file |
| 1240 | + time.sleep(1) |
1207 | 1241 | except Exception as e: |
1208 | 1242 | raise CADException(f"Error opening file '{file_path}': {e}") |
1209 | 1243 |
|
@@ -1364,9 +1398,10 @@ def set_layer_linetype(self, layer_name, linetype_name): |
1364 | 1398 | """ |
1365 | 1399 | try: |
1366 | 1400 | layer = self.doc.Layers.Item(layer_name) |
1367 | | - linetypes = self.doc.Linetypes |
1368 | | - if linetype_name not in linetypes: |
1369 | | - self.doc.Linetypes.Load(linetype_name, linetype_name) |
| 1401 | + |
| 1402 | + # Ensure linetype is loaded using the helper method |
| 1403 | + self._ensure_linetype_loaded(linetype_name) |
| 1404 | + |
1370 | 1405 | layer.Linetype = linetype_name |
1371 | 1406 | except Exception as e: |
1372 | 1407 | raise CADException(f"Error setting linetype of layer '{layer_name}': {e}") |
@@ -1494,20 +1529,120 @@ def insert_block_from_file(self, file_path, insertion_point, scale=1.0, rotation |
1494 | 1529 | except Exception as e: |
1495 | 1530 | raise CADException(f"Error inserting block from file '{file_path}': {e}") |
1496 | 1531 |
|
1497 | | - def export_block_to_file(self, block_name, file_path): |
1498 | | - """ |
1499 | | - Exports a block definition from the current drawing to a new .dwg file. |
1500 | | - Args: |
1501 | | - block_name (str): The name of the block to export. |
1502 | | - file_path (str): The destination path for the new .dwg file. |
1503 | | - Raises: |
1504 | | - CADException: If the block cannot be exported. |
1505 | | - """ |
| 1532 | + def export_blocks_to_file(self, block_names, file_path): |
| 1533 | + ss_name = "TEMP_EXPORT_SS" |
| 1534 | + temp_file = file_path + ".tmp.dwg" |
| 1535 | + temp_instances = [] |
| 1536 | + |
| 1537 | + def retry_com(func, retries=10, delay=2.0): |
| 1538 | + for attempt in range(retries): |
| 1539 | + try: |
| 1540 | + return func() |
| 1541 | + except Exception as e: |
| 1542 | + if "callee" in str(e).lower() or "-2147418111" in str(e): |
| 1543 | + print(f"AutoCAD busy, retrying ({attempt + 1}/{retries})...") |
| 1544 | + time.sleep(delay) |
| 1545 | + else: |
| 1546 | + raise |
| 1547 | + raise CADException(f"AutoCAD rejected the call after {retries} retries.") |
| 1548 | + |
1506 | 1549 | try: |
1507 | | - block = self.doc.Blocks.Item(block_name) |
1508 | | - block.Export(file_path) |
| 1550 | + try: |
| 1551 | + self.doc.SelectionSets.Item(ss_name).Delete() |
| 1552 | + except: |
| 1553 | + pass |
| 1554 | + |
| 1555 | + export_ss = self.doc.SelectionSets.Add(ss_name) |
| 1556 | + |
| 1557 | + # Use a far-away point so we never overlap existing objects |
| 1558 | + safe_point = win32com.client.VARIANT(pythoncom.VT_ARRAY | pythoncom.VT_R8, [1e18, 1e18, 0.0]) |
| 1559 | + |
| 1560 | + for block_name in block_names: |
| 1561 | + found_block = None |
| 1562 | + for i in range(self.doc.Blocks.Count): |
| 1563 | + block = self.doc.Blocks.Item(i) |
| 1564 | + if block.Name.lower() == block_name.lower(): |
| 1565 | + found_block = block |
| 1566 | + break |
| 1567 | + |
| 1568 | + if not found_block: |
| 1569 | + raise CADException(f"Block '{block_name}' not found in the current drawing.") |
| 1570 | + |
| 1571 | + print(f"Found '{block_name}'") |
| 1572 | + temp_instance = retry_com(lambda: self.doc.ModelSpace.InsertBlock(safe_point, block_name, 1, 1, 1, 0)) |
| 1573 | + temp_instances.append(temp_instance) |
| 1574 | + time.sleep(0.3) |
| 1575 | + |
| 1576 | + objs_array = win32com.client.VARIANT(pythoncom.VT_ARRAY | pythoncom.VT_DISPATCH, temp_instances) |
| 1577 | + export_ss.AddItems(objs_array) |
| 1578 | + |
| 1579 | + time.sleep(1.0) |
| 1580 | + |
| 1581 | + old_expert = self.doc.GetVariable("EXPERT") |
| 1582 | + self.doc.SetVariable("EXPERT", 2) |
| 1583 | + try: |
| 1584 | + retry_com(lambda: self.doc.Wblock(temp_file, export_ss)) |
| 1585 | + finally: |
| 1586 | + self.doc.SetVariable("EXPERT", old_expert) |
| 1587 | + |
| 1588 | + # Clean up temp instances from ModelSpace |
| 1589 | + for inst in temp_instances: |
| 1590 | + try: |
| 1591 | + inst.Delete() |
| 1592 | + except: |
| 1593 | + pass |
| 1594 | + temp_instances = [] |
| 1595 | + |
| 1596 | + try: |
| 1597 | + self.doc.SelectionSets.Item(ss_name).Delete() |
| 1598 | + except: |
| 1599 | + pass |
| 1600 | + |
| 1601 | + time.sleep(1.0) |
| 1602 | + |
| 1603 | + if os.path.exists(file_path): |
| 1604 | + target_doc = retry_com(lambda: self.acad.Documents.Open(file_path)) |
| 1605 | + inserted = retry_com(lambda: target_doc.ModelSpace.InsertBlock( |
| 1606 | + win32com.client.VARIANT(pythoncom.VT_ARRAY | pythoncom.VT_R8, [1e18, 1e18, 0.0]), |
| 1607 | + temp_file, 1, 1, 1, 0 |
| 1608 | + )) |
| 1609 | + try: |
| 1610 | + inserted.Delete() |
| 1611 | + except: |
| 1612 | + pass |
| 1613 | + target_doc.Save() |
| 1614 | + target_doc.Close(False) |
| 1615 | + else: |
| 1616 | + os.rename(temp_file, file_path) |
| 1617 | + |
| 1618 | + print(f"Exported {block_names} to '{file_path}'") |
| 1619 | + |
| 1620 | + self.close(save_changes=False) |
| 1621 | + |
| 1622 | + except CADException: |
| 1623 | + raise |
1509 | 1624 | except Exception as e: |
1510 | | - raise CADException(f"Error exporting block '{block_name}' to '{file_path}': {e}") |
| 1625 | + raise CADException(f"Export failed: {e}") |
| 1626 | + |
| 1627 | + finally: |
| 1628 | + for inst in temp_instances: |
| 1629 | + try: |
| 1630 | + inst.Delete() |
| 1631 | + except: |
| 1632 | + pass |
| 1633 | + try: |
| 1634 | + self.doc.SelectionSets.Item(ss_name).Delete() |
| 1635 | + except: |
| 1636 | + pass |
| 1637 | + if os.path.exists(temp_file): |
| 1638 | + for attempt in range(5): |
| 1639 | + try: |
| 1640 | + time.sleep(0.5) |
| 1641 | + os.remove(temp_file) |
| 1642 | + print(f"Temp file deleted: {temp_file}") |
| 1643 | + break |
| 1644 | + except Exception as e: |
| 1645 | + print(f"Warning: Could not delete temp file (attempt {attempt + 1}/5): {e}") |
1511 | 1646 |
|
1512 | 1647 | def modify_block_attribute(self, block_ref, tag, new_value): |
1513 | 1648 | """ |
@@ -1717,8 +1852,9 @@ def add_mtext(self, content: str, insertion_point: APoint, width: float, height: |
1717 | 1852 | if text_style: |
1718 | 1853 | text_styles = self.doc.TextStyles |
1719 | 1854 | style_exists = False |
1720 | | - for style in text_styles: |
1721 | | - if style.Name.lower() == text_style.lower(): |
| 1855 | + # Changed to index-based iteration |
| 1856 | + for i in range(text_styles.Count): |
| 1857 | + if text_styles.Item(i).Name.lower() == text_style.lower(): |
1722 | 1858 | style_exists = True |
1723 | 1859 | break |
1724 | 1860 |
|
|
0 commit comments