Newer
Older
SPIS / build_discipline_sets.py
"""Создание комплектов дисциплин с взаимными внешними ссылками.

Итоговая схема (все ссылки 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())