СПДС - Обращение к объектам базы элементов

Платформа nanoCAD x64 24.1 24.1.6616.4528

Windows 11 Home 23H2 22631.5909

Пишу автоматизацию на Python и COM API для нанокада. Есть ли какая-то документация где описаны методы и свойства различных объектов на чертеже и в целом для рабочей области нанокада? В первую очередь интересует обращение к базе элементов в приложении СПДС 24. Я подключился к БД с помощью postgresql 12, и добавил в эту базу свои три .mcdi файла (добавилось ещё три папки в базу элементов). так вот как можно обращаться к ним и работать со всем этим с активной сессии нанокада или СПДС?

пробовал такой код и он не работает:

import win32com.client

def get_spds_library():
    try:
        mc_lib = win32com.client.Dispatch("McCOM2.Library")
        return mc_lib
    except Exception as e:
        print(f"Ошибка подключения к McCOM2: {e}")
        return None

def scan_folders(folder, target_names, indent=""):
    """Рекурсивный поиск нужных папок в базе"""
    try:
        sub_folders = folder.SubFolders
        for i in range(sub_folders.Count):
            sub = sub_folders.Item(i)
            print(f"{indent}Папка: {sub.Name}")
            
            if sub.Name in target_names:
                print(f"{indent}  >>> НАЙДЕНО: {sub.Name} (ID: {sub.ID})")
                # Здесь можно получить список элементов внутри папки
                items = sub.Items
                print(f"{indent}  Количество элементов в папке: {items.Count}")

            scan_folders(sub, target_names, indent + "  ")
    except Exception as e:
        pass

mc_lib = get_spds_library()

if mc_lib:
    print("Подключено к Базе элементов СПДС")
    
    # Получаем корневую папку базы
    root = mc_lib.RootFolder
    
    # Список папок, которые вы импортировали и хотите найти
    my_targets = ["***", "***", "***"]
    
    print(f"Корневая папка: {root.Name}")
    scan_folders(root, my_targets)
else:
    print("Не удалось инициализировать интерфейс McCOM2. Проверьте, установлен ли модуль СПДС.")

p.s. также посоветуйте в чем может быть проблема, не могу подключить свою MS SQL БД как источник базы.

Код не смотрел, соответственно цель не понял.
Переключалка баз
Надо иметь в виду,что графика на чертеже это только картинка, вся его логика в базе.
Те если объект на чертеже есть, а в базе его нет это ни о чем.
Для автоматизации через ActiveX смотреть в сторону McCom2, в бложике немного есть.
Ты хочешь что в базе объекты менять?
Вставлять из базы
Менять на чертеже?
Вставку из базы с параметрами омичи что-то делали, можно на гитхабе поискать по слову multicad

1 лайк

По дефолту SQL не ставится
Доставлять недостающее руками, благо в кабинете компоненты все есть

нет, немного не так. вообще у меня задача изначально автоматизировать проверку рамки/штампа на чертежах. Сначала я сделал скрипт на Python, который получает из конкретной указанной области листа все отрезки/тексты/мтексты и формировал из них excel таблицу. Вот этот код:

import win32com.client
import math
import re
from openpyxl import Workbook
from openpyxl.utils import get_column_letter
from openpyxl.styles import Border, Side, Alignment

# -----------------------------
# Конфигурация
# -----------------------------

DWG_PATH = r"***\input_files\образец рамки.dwg"
OUTPUT_XLSX = "stamp.xlsx"

LINE_MIN_LENGTH = 10.0  # мм — фильтр против логотипа
ROUND_PRECISION = 2  # округление координат (0.01 мм)

STAMP_AREA = {
    "min_x": min(230.0, 415.0),
    "max_x": max(230.0, 415.0),
    "min_y": min(60.0, 0.0),
    "max_y": max(60.0, 5.0),
}


# -----------------------------
# Утилиты
# -----------------------------

def r(v):
    return round(v, ROUND_PRECISION)


def line_length(p1, p2):
    return math.dist(p1[:2], p2[:2])


def is_vertical(p1, p2):
    return abs(p1[0] - p2[0]) < 0.01


def is_horizontal(p1, p2):
    return abs(p1[1] - p2[1]) < 0.01


def in_stamp_area(x, y, area):
    return (
            area["min_x"] <= x <= area["max_x"] and
            area["min_y"] <= y <= area["max_y"]
    )


def transform_point(pt, blockref):
    """
    Локальные координаты блока → глобальные координаты чертежа
    """
    x, y = pt[0], pt[1]

    sx = blockref.XScaleFactor
    sy = blockref.YScaleFactor
    angle = blockref.Rotation
    ins_x, ins_y = blockref.InsertionPoint[0], blockref.InsertionPoint[1]

    # масштаб
    x *= sx
    y *= sy

    # поворот
    xr = x * math.cos(angle) - y * math.sin(angle)
    yr = x * math.sin(angle) + y * math.cos(angle)

    # смещение
    return xr + ins_x, yr + ins_y


def clear_mtext_formatting(text):
    """
    Очистка форматирования MText NanoCAD/AutoCAD:
    - Удаляет {\f...; и другие управляющие последовательности
    - Декодирует \\uXXXX в символы Юникода
    """
    if not text:
        return ""

    # Декодируем \\uXXXX → символы (например, \\u1058 → 'Т')
    text = re.sub(r'\\u([0-9a-fA-F]{4})', lambda m: chr(int(m.group(1), 16)), text)

    # Извлекаем текст после последней ';' внутри фигурных скобок
    if text.startswith('{') and text.endswith('}'):
        semicolon_pos = text.rfind(';')
        if semicolon_pos != -1:
            text = text[semicolon_pos + 1:-1].strip()

    # Удаляем оставшиеся управляющие коды: \P (новая строка), \L...\l (нижний регистр) и др.
    text = re.sub(r'\\[A-Z][^\\{}]*', '', text)  # удаляем \P, \L...\l и подобные
    text = re.sub(r'[{}]', '', text)  # удаляем оставшиеся фигурные скобки
    text = text.replace(r'\~', ' ')  # неразрывный пробел → обычный

    return text.strip()


# -----------------------------
# 1. Извлечение данных из блока
# -----------------------------

def extract_block_entities(doc):
    grid_lines = []
    texts = []

    for obj in doc.PaperSpace:
        if obj.EntityName != "AcDbBlockReference":
            continue

        blockref = win32com.client.CastTo(obj, "IAcadBlockReference")
        block_def = doc.Blocks.Item(blockref.Name)

        for ent in block_def:

            # ---- ЛИНИИ
            if ent.EntityName == "AcDbLine":
                line = win32com.client.CastTo(ent, "IAcadLine")

                p1 = line.StartPoint
                p2 = line.EndPoint

                if line_length(p1, p2) < LINE_MIN_LENGTH:
                    continue

                if not (is_vertical(p1, p2) or is_horizontal(p1, p2)):
                    continue

                # центр линии (локальный)
                cx_l = (p1[0] + p2[0]) / 2
                cy_l = (p1[1] + p2[1]) / 2

                # → глобальный
                cx, cy = transform_point((cx_l, cy_l), blockref)

                if not in_stamp_area(cx, cy, STAMP_AREA):
                    continue

                p1g = transform_point(p1, blockref)
                p2g = transform_point(p2, blockref)

                grid_lines.append((
                    r(p1g[0]), r(p1g[1]),
                    r(p2g[0]), r(p2g[1])
                ))

            # ---- TEXT
            elif ent.EntityName == "AcDbText":
                t = win32com.client.CastTo(ent, "IAcadText")
                try:
                    min_pt, max_pt = t.GetBoundingBox()
                except:
                    # Если GetBoundingBox недоступен, используем InsertionPoint как ориентир
                    min_pt = t.InsertionPoint
                    max_pt = (min_pt[0] + 10, min_pt[1] + 5, 0)

                cx_l = (min_pt[0] + max_pt[0]) / 2
                cy_l = (min_pt[1] + max_pt[1]) / 2

                cx, cy = transform_point((cx_l, cy_l), blockref)

                if not in_stamp_area(cx, cy, STAMP_AREA):
                    continue

                texts.append({
                    "text": t.TextString.strip(),
                    "center": (r(cx), r(cy)),
                    "source": "block"
                })

            # ---- MTEXT
            # ---- MTEXT (в extract_block_entities)
            elif ent.EntityName == "AcDbMText":
                try:
                    mtext = win32com.client.CastTo(ent, "IAcadMText")  # ← КЛЮЧЕВОЙ КАСТИНГ
                    min_pt, max_pt = mtext.GetBoundingBox()

                    cx_l = (min_pt[0] + max_pt[0]) / 2
                    cy_l = (min_pt[1] + max_pt[1]) / 2

                    cx, cy = transform_point((cx_l, cy_l), blockref)

                    if not in_stamp_area(cx, cy, STAMP_AREA):
                        continue

                    # Извлекаем текст ДО очистки
                    raw_text = mtext.TextString
                    clean_text = clear_mtext_formatting(raw_text)

                    # Отладка (временно раскомментируйте для проверки):
                    # print(f"MText (block): raw='{raw_text}' → clean='{clean_text}', center=({cx:.2f},{cy:.2f})")

                    if clean_text:  # пропускаем пустые тексты
                        texts.append({
                            "text": clean_text,
                            "center": (r(cx), r(cy)),
                            "source": "block"
                        })
                except Exception as e:
                    # Отладка ошибок:
                    # print(f"Ошибка обработки MText в блоке: {e}")
                    pass

    return grid_lines, texts


# -----------------------------
# 2. Извлечение текстов напрямую с листа (вне блока)
# -----------------------------

def extract_paper_texts(doc):
    """
    Извлечение текстов напрямую с листа (не входящих в BlockReference),
    находящихся в области штампа.
    """
    texts = []

    for obj in doc.PaperSpace:
        # Пропускаем блоки — их тексты уже обработаны в extract_block_entities
        if obj.EntityName == "AcDbBlockReference":
            continue

        # ---- Однострочный текст
        if obj.EntityName == "AcDbText":
            try:
                t = win32com.client.CastTo(obj, "IAcadText")
                min_pt, max_pt = t.GetBoundingBox()

                cx = (min_pt[0] + max_pt[0]) / 2
                cy = (min_pt[1] + max_pt[1]) / 2

                if not in_stamp_area(cx, cy, STAMP_AREA):
                    continue

                texts.append({
                    "text": t.TextString.strip(),
                    "center": (r(cx), r(cy)),
                    "source": "paper"
                })
            except:
                continue

        # ---- Многострочный текст
        # ---- MTEXT (в extract_paper_texts)
        elif obj.EntityName == "AcDbMText":
            try:
                mtext = win32com.client.CastTo(obj, "IAcadMText")  # ← КЛЮЧЕВОЙ КАСТИНГ
                min_pt, max_pt = mtext.GetBoundingBox()

                cx = (min_pt[0] + max_pt[0]) / 2
                cy = (min_pt[1] + max_pt[1]) / 2

                if not in_stamp_area(cx, cy, STAMP_AREA):
                    continue

                raw_text = mtext.TextString
                clean_text = clear_mtext_formatting(raw_text)

                # Отладка:
                # print(f"MText (paper): raw='{raw_text}' → clean='{clean_text}', center=({cx:.2f},{cy:.2f})")

                if clean_text:
                    texts.append({
                        "text": clean_text,
                        "center": (r(cx), r(cy)),
                        "source": "paper"
                    })
            except Exception as e:
                # print(f"Ошибка обработки MText на листе: {e}")
                continue

    return texts


# -----------------------------
# 3. Построение сетки
# -----------------------------

def build_grid(lines):
    x_coords = set()
    y_coords = set()

    for x1, y1, x2, y2 in lines:
        if x1 == x2:
            x_coords.add(x1)
        if y1 == y2:
            y_coords.add(y1)

    xs = sorted(x_coords)
    ys = sorted(y_coords, reverse=True)

    return xs, ys


def find_cell(xs, ys, x, y):
    for i in range(len(xs) - 1):
        if xs[i] <= x <= xs[i + 1]:
            for j in range(len(ys) - 1):
                if ys[j] >= y >= ys[j + 1]:
                    return j + 1, i + 1
    return None, None


# -----------------------------
# 4. Excel экспорт
# -----------------------------

def build_line_maps(lines):
    vertical = set()
    horizontal = set()

    for x1, y1, x2, y2 in lines:
        if x1 == x2:
            vertical.add((x1, min(y1, y2), max(y1, y2)))
        elif y1 == y2:
            horizontal.add((y1, min(x1, x2), max(x1, x2)))

    return vertical, horizontal


def has_vertical_border(x, y_top, y_bottom, vertical_lines):
    for lx, ly1, ly2 in vertical_lines:
        if abs(lx - x) < 0.01 and ly1 <= y_bottom and ly2 >= y_top:
            return True
    return False


def has_horizontal_border(y, x_left, x_right, horizontal_lines):
    for ly, lx1, lx2 in horizontal_lines:
        if abs(ly - y) < 0.01 and lx1 <= x_left and lx2 >= x_right:
            return True
    return False


def merge_cells(ws, xs, ys, vertical_lines, horizontal_lines):
    rows = len(ys) - 1
    cols = len(xs) - 1

    merged = [[False] * cols for _ in range(rows)]

    for r in range(rows):
        for c in range(cols):
            if merged[r][c]:
                continue

            r2 = r
            c2 = c

            # растягиваем вправо
            while c2 + 1 < cols:
                x_border = xs[c2 + 1]
                y_top = ys[r]
                y_bottom = ys[r + 1]
                if has_vertical_border(x_border, y_top, y_bottom, vertical_lines):
                    break
                c2 += 1

            # растягиваем вниз
            while r2 + 1 < rows:
                y_border = ys[r2 + 1]
                x_left = xs[c]
                x_right = xs[c2 + 1]
                if has_horizontal_border(y_border, x_left, x_right, horizontal_lines):
                    break
                r2 += 1

            if r2 > r or c2 > c:
                ws.merge_cells(
                    start_row=r + 1,
                    start_column=c + 1,
                    end_row=r2 + 1,
                    end_column=c2 + 1
                )
                for rr in range(r, r2 + 1):
                    for cc in range(c, c2 + 1):
                        merged[rr][cc] = True


def autofit_columns(ws):
    for col in ws.columns:
        max_length = 0
        col_letter = get_column_letter(col[0].column)
        for cell in col:
            if cell.value:
                max_length = max(max_length, len(str(cell.value)))
        ws.column_dimensions[col_letter].width = max_length + 2


def autofit_rows(ws):
    for row in ws.rows:
        max_height = 0
        for cell in row:
            if cell.value:
                lines = str(cell.value).count("\n") + 1
                max_height = max(max_height, lines)
        ws.row_dimensions[row[0].row].height = max_height * 15


def export_to_excel(xs, ys, texts, vertical_lines, horizontal_lines):
    wb = Workbook()
    ws = wb.active
    ws.title = "Штамп"

    # Размеры столбцов (примерный расчет)
    for i in range(len(xs) - 1):
        ws.column_dimensions[get_column_letter(i + 1)].width = abs(xs[i + 1] - xs[i]) / 5

    # Размеры строк
    for j in range(len(ys) - 1):
        ws.row_dimensions[j + 1].height = abs(ys[j] - ys[j + 1]) * 1.5

    # Сначала создаем структуру объединенных ячеек
    merge_cells(ws, xs, ys, vertical_lines, horizontal_lines)

    # 1. Сортируем тексты по Y (сверху вниз), чтобы соблюдался порядок строк в ячейке
    # ys у нас идут по убыванию, поэтому сортируем center[1] по убыванию
    sorted_texts = sorted(texts, key=lambda x: x["center"][1], reverse=True)

    for t in sorted_texts:
        row, col = find_cell(xs, ys, *t["center"])
        if row and col:
            # Находим основную ячейку, если эта часть входит в MergeArea
            target_cell = ws.cell(row=row, column=col)
            for merged in ws.merged_cells.ranges:
                if merged.min_row <= row <= merged.max_row and merged.min_col <= col <= merged.max_col:
                    target_cell = ws.cell(row=merged.min_row, column=merged.min_col)
                    break

            # Если в ячейке уже что-то есть, добавляем через перенос строки
            if target_cell.value:
                target_cell.value = f"{target_cell.value}\n{t['text']}"
            else:
                target_cell.value = t["text"]

            # Включаем перенос текста и центрирование (по желанию)
            target_cell.alignment = Alignment(wrap_text=True, vertical='center', horizontal='left')
    # ------------------------------------------

    # Границы
    thin = Side(border_style="thin", color="000000")
    border = Border(top=thin, left=thin, right=thin, bottom=thin)

    max_row = len(ys)
    max_col = len(xs)
    for row_idx in range(1, max_row):
        for col_idx in range(1, max_col):
            cell = ws.cell(row=row_idx, column=col_idx)
            cell.border = border

    # Автоподбор (опционально, может конфликтовать с ручной настройкой высоты)
    autofit_columns(ws)
    autofit_rows(ws)

    wb.save(OUTPUT_XLSX)


# -----------------------------
# MAIN
# -----------------------------

def main():
    nanocad = win32com.client.Dispatch("nanoCAD.Application")
    doc = nanocad.Documents.Open(DWG_PATH)

    # Извлечение линий и текстов из блока
    lines, block_texts = extract_block_entities(doc)

    # Извлечение текстов напрямую с листа
    paper_texts = extract_paper_texts(doc)

    # Объединение текстов (тексты с листа имеют приоритет при совпадении позиций)
    all_texts = block_texts + paper_texts

    xs, ys = build_grid(lines)
    vertical, horizontal = build_line_maps(lines)

    export_to_excel(xs, ys, all_texts, vertical, horizontal)

    print("Линий в штампе:", len(lines))
    print("Текстов из блока:", len(block_texts))
    print("Текстов с листа:", len(paper_texts))
    print("Всего текстов:", len(all_texts))
    print("Excel создан:", OUTPUT_XLSX)

    # Закрытие документа без сохранения
    # doc.Close(False)
    # nanocad.Quit()


if __name__ == "__main__":
    main()

он работает корректно с горизонтальным листом А3 и рамкой моего предприятия. (если что поправьте меня в терминологии что такое штамп и рамка). То есть все эти объекты на чертеже из которых состоял штамп справа в углу были объединены в один элемент нанокада, называемый Блок (BlockReference если быть точным).

Потом мне сообщили что их инженеры используют конкретные рамки и штампы в зависимости от страны, для которой делают и прочих факторов. Все эти заготовки у них есть в этой базе СПДС (где я так понял они и занимаются конечным оформлением чертежа) в виде нескольких папок.

Вот я хочу попробовать сделать программу, которая будет получать на вход чертеж в формате dwg, какие-то данные о нем и проверять на наличие в нем определенного блока из этих папок (я так понимаю вставляя какой-либо объект из этой папки он также вставляется на чертёж Блоком - BlockReference) и его координаты вставки.

Для этого мне нужно как-то программно получить всю структуру этих папок себе чтобы я мог проверить с тем что фактически из этого есть на чертеже.

Плюс мой вопрос по поводу наличия какой-либо документации остаётся открытым.

по поводу этого можете немного прояснить пожалуйста? насколько я помню есть несколько способов взаимодействия с API NanoCAD, и вот этот ActiveX один из них. А файл McCOM2 это по-моему dll файлик который лежит в nanoSPDS/bin в папке нанокада. как с этим взаимодействовать можно

Тут можно посмотреть. Help

1 лайк

Нелишне уточнить чем вставляется
Если действительно в базе блоки, то ятд принадлежность базе спдс уточнить проблематично.
Если форматом спдс, то наверное можно, но через COM это наверное невозможно.

Проверять принадлежность имха самое простое по имени если блок.

прикрепляю фото содержимого одной из этих папок ниже.

я так понимаю подавляющее большинство этих элементов это Блоки nanocad. Да, как вы и сказали проще было бы проверять по имени блока, то есть проходить по всем объектам на чертеже и искать там блок с конкретным именем. Вот интересует насколько это вероятно сделать.

И я правильно понимаю что через какой бы интерфейс программирования я не пробовал это реализовать, везде будет одинаковый функционал? Или разные интерфейсы под разные задачи. (я про то что ниже)

этот .chm файл который вы скинули нужен для подключения nanocad’а к экселю? или его нужно запускать

можно чуть поподробнее пожалуйста) я просто не инженер немного, с CAD системами до этого не работал

В основном это форматы спдс
Через API нанокад доступны базовые свойства примитива: цвет, слой…
Через API Multicad доступны все свойства объекта: ширина, вышина, масштаб, ориентация, значения полей формата…

——

Смотри, если ты только начинаешь погружаться в тему программирования, по идее тебе пофих на язык.
Поэтому рекомендую c# net
С нуля сложность вхождения мало отличается, но
Готовых примеров и хелпов море
С остальыми языками похуже

1 лайк

да, у меня уже получилось сделать рабочий скрипт на Python который получает:

  1. с рабочего пространства нанокада:
  • все типы линий
  • все текстовые стили
  • все размерные стили
  • все слои
  1. с конкретного открытого файла:
  • все объекты любого типа (отрезки/полилинии/текст/мтекст/блок)
  • у этих объектов свойства (название/тип объекта, слой, цвет, тип линии, масштаб типа линии, толщина, материал, видимость, уникальный ID- HEX, ID владельца)

Вот пример:

Название объекта: AcDbText
Слой №: 0
Цвет: 7
Тип линии: ByLayer
Масштаб типа линий (LinetypeScale): 1.0
Толщина/Вес линии: 0.25 мм
Материал: ByLayer
        Текст: 'Утв.', 
позиция=(235.56468398878246, 21.26185214945287, 0.0)
Стиль текста: 'SPDS'
Аннотативный: 'не сделал'
Выравнивание ??????: '0'
Высота (мм): '2.5'
Поворот ???: '0.0'
Коэффициент сжатия: '1.0'
Угол наклона (должен быть '0.0'): '0.0'
Видимость: True
Уникальный идентификатор объекта (строка HEX): 117F70F2
ID владельца (например, ModelSpace): 2128159291360

короче вытягивает достаточно много информации, если нужно - могу поделиться.

вообще я студент на практике у предприятия, пишу в основном бэкенд на python (до этого был Django, сейчас изучаю fastapi), а тут дали такую нестандартную задачу по автоматизации проверки чертежей. Если на C# реально больше информации и не менее функциональное API, то был бы очень признателен за ссылки на полезные ресурсы по работе с ним и NanoCAD

Клуб разработчиков

Его надо читать
Пкм на файле, свойства, разблокировать

1 лайк

спасибо большое, я правильно понимаю что это документация к Visual basic скриптам?

Это SDK ко всему.

а вы не знаете как можно обратиться именно к элементам СПДС? Посмотрел в этом и других SDK (конкретно к COM API) и не нашёл ничего. Там описаны только примеры работы с объектами платформы NanoCAD, а не её модулями (СПДС и Механика). Пробовал разные варианты подключения, везде пишет примерно такую ошибку: Запрос интерфейса McCOM2…
Ошибка при работе с СПДС: (-2147352567, ‘Ошибка.’, (0, None, None, None, 0, -2147221005), None)
Проверьте, загружен ли модуль СПДС в текущей сессии nanoCAD.

Хотя в одном из этих файлов документации я нашёл следующие строки:

Для работы с ActiveX API nanoCAD для платформы и модулей СПДС и Механика существует 3 разных COM библиотеки. Ниже приведена таблица с информацией о версиях COM-библиотек, используемых в nanoCAD.

•COM-библиотека для доступа к приложению nanoCAD, документам, UI: ncauto или "nanoCAD x64 Type Library";
•COM-библиотека для доступа к объектам чертежа nanoCAD: odaX или "OdaX *** (x64) Type Library";
•COM-библиотека для доступа к объектам модулей nanoCAD СПДС и Механика: mccom2 или "MechaniCS COM 2.0 type library";

искать можно не только в SDK… тем более там про McCom2 написано только то что он есть

на этом форуме

на дружественном

гугол

также искать можно по словам: СПДС, мультикад, multicad

с чего то жэж надо начинать
nanoCad.zip (3,1 КБ)

McCOM2.ZIP (1,1 МБ)

В справке даже примеры есть

2 лайка

да, спасибо. что-то из этого я уже смотрел ранее, всё равно не нашёл. Возможно как вы и говорили, то что я хочу сделать, не получится через COM.

Посмотрю sdk к .NET API, может там найду

Если сможешь понятно сформулировать, что должно в итоге получится, возможно и ответы появятся.
Пока программа общеобразовательная

Отмечу очевидное, что через API net возможностей значительно больше чем через com
Еще больше через c++, но там и порог вхождения значительно выше

1 лайк