"""Создание комплектов дисциплин с взаимными внешними ссылками.
Итоговая схема (все ссылки Overlay, вставка 0,0,0, масштаб 1:1):
ЭТР = АР + СС + ВК + ОВ
СС = АР + ВК + ОВ + ЭТР
ВК = АР + СС + ОВ + ЭТР
ОВ = АР + СС + ВК + ЭТР
АР — основа. Она ни на что не ссылается, но подключается во все разделы.
Каждый раздел не ссылается сам на себя — только на 3 смежных + АР.
Только Overlay — безопасно, циклы не блокируют отображение.
Запуск из AutoCAD:
команда SPISRUNSETS (см. SPIS_RUN_SETS.lsp)
Запуск вручную:
python build_discipline_sets.py "путь\к\корню" "Корпус"
"""
import sys
import time
from pathlib import Path
import pythoncom
from win32com.client import VARIANT, gencache
AP = "АР"
SECTIONS = ["ЭТР", "СС", "ВК", "ОВ"]
SCHEME = {
"ЭТР": [AP, "СС", "ВК", "ОВ"],
"СС": [AP, "ВК", "ОВ", "ЭТР"],
"ВК": [AP, "СС", "ОВ", "ЭТР"],
"ОВ": [AP, "СС", "ВК", "ЭТР"],
}
X, Y, Z = 0.0, 0.0, 0.0
SCALE = 1.0
def log(msg: str) -> None:
print(msg, flush=True)
def retry(fn, *args, **kwargs):
"""Повтор COM-вызова при отклонении (RPC_E_CALL_REJECTED).
Точная копия из fix_larus2.py — проверено, работает.
"""
for _ in range(30):
try:
return fn(*args, **kwargs)
except Exception as e:
if "-2147418111" in str(e) or "отклонен" in str(e):
time.sleep(4.0)
continue
raise
raise RuntimeError("retry: не удалось")
def attach_until_ok(doc, path, name, pt, attempts=30):
"""AttachExternalReference с повтором при отклонении."""
for _ in range(attempts):
try:
doc.ModelSpace.AttachExternalReference(
path, name, pt, SCALE, SCALE, SCALE, 0.0, True)
return True
except Exception as e:
s = str(e)
if "-2147418111" in s or "отклонен" in s or "rejected" in s.lower():
time.sleep(4.0)
continue
raise
raise RuntimeError(f"attach_until_ok: не удалось {name} после {attempts} попыток")
def point():
return VARIANT(pythoncom.VT_ARRAY | pythoncom.VT_R8, [X, Y, Z])
def detach_all_xrefs(doc):
"""Отключить все внешние ссылки (очистка от дубликатов прошлых запусков)."""
try:
for blk in list(doc.Blocks):
if blk.IsXRef:
try:
blk.Detach()
except Exception:
pass
except Exception:
pass
def current_refs(doc) -> set:
result = set()
try:
for blk in doc.Blocks:
if blk.IsXRef:
result.add(blk.Name.upper())
except Exception:
pass
return result
def find_ap_file(root: Path) -> Path | None:
ap_dir = root / AP
if not ap_dir.is_dir():
return None
for f in ap_dir.glob(f"{AP} *.dwg"):
return f
p = ap_dir / f"{AP}.dwg"
if p.is_file():
return p
for f in ap_dir.glob("*.dwg"):
return f
return None
def source_file(section: str, corpus: str, root: Path) -> Path | None:
if section == AP:
return find_ap_file(root)
p = root / section / f"{section} {corpus}.dwg"
return p if p.is_file() else None
def target_path(root: Path, section: str, corpus: str) -> Path:
return root / section / f"{section} {corpus}.dwg"
def main() -> int:
root = Path(sys.argv[1]) if len(sys.argv) > 1 else Path(
r"C:\Users\o.mashkancev\Desktop\Гранель Дубровка\TEST\Ларус Капитал")
corpus = sys.argv[2] if len(sys.argv) > 2 else "Ларус Капитал"
if not root.is_dir():
log(f"ERROR: папка не найдена: {root}")
return 1
ap_file = find_ap_file(root)
if ap_file is None:
log(f"ERROR: не найден базовый файл АР в {root / AP}")
return 1
log(f"Архитектура: {ap_file.name}")
log(f"Корпус: {corpus}")
log(f"Корень: {root}")
pythoncom.CoInitialize()
try:
acad = gencache.EnsureDispatch("AutoCAD.Application")
except Exception as e:
log(f"ERROR: AutoCAD недоступен: {e}")
return 1
log(f"Connected: AutoCAD {acad.Version}")
# Закрываем ВСЕ открытые документы — иначе «висящие» от прошлых
# запусков блокируют сохранение (файлы .dwl/.dwl2).
try:
for doc in list(acad.Documents):
try:
doc.Close(False)
except Exception:
pass
time.sleep(2)
log(f" закрыто открытых документов: {len(acad.Documents)}")
except Exception:
pass
# FILEDIA: сохраняем и гасим диалоги
try:
saved_filedia = acad.ActiveDocument.GetVariable("FILEDIA")
except Exception:
saved_filedia = 1
try:
acad.ActiveDocument.SetVariable("FILEDIA", 0)
except Exception:
pass
# Шаг 1. Создаём пустые файлы разделов (если нет).
for section in SECTIONS:
target = target_path(root, section, corpus)
target.parent.mkdir(parents=True, exist_ok=True)
if target.exists():
log(f" файл уже существует: {target.name}")
continue
try:
doc = retry(acad.Documents.Add)
except Exception as e:
log(f" Documents.Add for {section}: {e}")
return 1
try:
doc.SetVariable("INSUNITS", 4)
doc.SetVariable("MEASUREMENT", 1)
except Exception:
pass
try:
doc.SaveAs(str(target), 24)
log(f" создан: {target.name}")
except Exception as e:
log(f" SaveAs failed for {section}: {e}")
try:
retry(doc.Close, False)
except Exception:
pass
return 1
try:
retry(doc.Close, False)
except Exception:
pass
# Шаг 2. Подключаем ссылки по схеме.
# Точный алгоритм fix_larus2.py: открыть → attach + sleep(10) → Close(True) → sleep(5)
for section in SECTIONS:
path = target_path(root, section, corpus)
try:
doc = retry(acad.Documents.Open, str(path))
except Exception as e:
log(f" Open failed for {path.name}: {e}")
return 1
time.sleep(3)
# Отвязываем ВСЕ старые xref (дубликаты от прошлых запусков
# с именами типа "СС Ларус Капитал" вместо "СС").
# Без очистки AutoCAD создаёт дубли и не может сохранить файл.
detach_all_xrefs(doc)
time.sleep(2)
have = current_refs(doc)
log(f" {path.name}: после очистки ссылок {len(have)}")
for other in SCHEME[section]:
if other == section:
continue
if other.upper() in have:
log(f" пропущен (уже есть): {other}")
continue
src = source_file(other, corpus, root)
if not src:
log(f" пропущен (нет файла): {other}")
continue
try:
pt = point()
attach_until_ok(doc, str(src), other, pt)
log(f" {path.name} += {other}")
have.add(other.upper())
time.sleep(10.0)
except Exception as e:
log(f" {path.name} += {other}: {e}")
try:
retry(doc.Close, True)
log(f" сохранён: {path.name}")
except Exception as e:
log(f" ОШИБКА сохранения {path.name}: {e}")
try:
retry(doc.Close, False)
except Exception:
pass
time.sleep(5.0)
# Шаг 3. Проверяем результат.
ok = 0
for section in SECTIONS:
path = target_path(root, section, corpus)
try:
doc = retry(acad.Documents.Open, str(path))
except Exception:
continue
time.sleep(1)
have = current_refs(doc)
want = set(s.upper() for s in SCHEME[section])
want.discard(section.upper())
missing = want - have
if not missing:
ok += 1
log(f" {path.name}: {len(have)} ссылок — OK")
else:
log(f" {path.name}: {len(have)} ссылок, не хватает {sorted(missing)}")
try:
retry(doc.Close, False)
except Exception:
pass
# Восстанавливаем FILEDIA
try:
acad.ActiveDocument.SetVariable("FILEDIA", saved_filedia)
except Exception:
pass
log("")
if ok == len(SECTIONS):
log(f"ГОТОВО: {ok}/{len(SECTIONS)} файлов со всеми ссылками.")
else:
log(f"ГОТОВО: {ok}/{len(SECTIONS)} файлов со всеми ссылками.")
log("Запустите повторно для добивки недостающих.")
log("Не забудьте: SPISATTACHSETS — подключить АР и разделы в СПИС.")
return 0
if __name__ == "__main__":
sys.exit(main())