ours: наш оверлей к апстриму 1.5.1 (дельта для переноса в 1.6.0)
10 изменённых файлов + 2 новых (engineering/gost_titleblock.py, _fonts/isocpeur.ttf). Ветка-донор для трёхстороннего переноса в ours-1.6.
This commit is contained in:
@@ -45,6 +45,10 @@ from .preflight import (
|
||||
preflight_drawing,
|
||||
)
|
||||
from .refiner import RefineResult, RefineRound, RepairAction, refine_drawing
|
||||
from .gost_titleblock import (
|
||||
GostTitleBlockMetadata,
|
||||
apply_gost_form3_titleblock,
|
||||
)
|
||||
from .titleblock import TitleBlockMetadata, apply_iso_a3_titleblock
|
||||
from .validator import DrawingValidator, ValidationFinding, ValidationResult
|
||||
|
||||
@@ -72,6 +76,8 @@ __all__ = [
|
||||
"ensure_standard_linetypes",
|
||||
"apply_iso_a3_titleblock",
|
||||
"TitleBlockMetadata",
|
||||
"apply_gost_form3_titleblock",
|
||||
"GostTitleBlockMetadata",
|
||||
"DrawingValidator",
|
||||
"ValidationResult",
|
||||
"ValidationFinding",
|
||||
|
||||
@@ -4,6 +4,32 @@ from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
|
||||
#: Инлайн-группа шрифта ГОСТ — ровно та, что стоит в каждом многострочном тексте
|
||||
#: образца Павла (``{\fISOCPEUR|b0|i0|c204|p34;\Q0;Вентиляционный канал}``):
|
||||
#: ``c204`` — кириллический набор, ``p34`` — плотность 3/4, ``\Q0`` — прямой
|
||||
#: наклон. Шрифт назван внутри самой строки, поэтому надпись выглядит одинаково
|
||||
#: на любом компьютере, даже если стиль чертежа подменят или SHX не найдётся.
|
||||
INLINE_FONT_CODE = r"{\fISOCPEUR|b0|i0|c204|p34;\Q0;"
|
||||
INLINE_FONT_FACE = "ISOCPEUR"
|
||||
|
||||
#: Признаки того, что содержимое уже размечено управляющими кодами MTEXT.
|
||||
_INLINE_MARKUP_CHARS = ("\\", "{", "}")
|
||||
|
||||
|
||||
def wrap_inline_font(text: str, inline_font: bool = True) -> str:
|
||||
"""Завернуть содержимое многострочного текста в инлайн-группу ISOCPEUR.
|
||||
|
||||
Уже размеченный текст (обратные слэши, группы в фигурных скобках) не
|
||||
трогаем и пишем как есть: вложенная группа ломает разбор MTEXT, а образец
|
||||
показывает, что готовые коды переносятся в файл без переписывания.
|
||||
"""
|
||||
if not inline_font:
|
||||
return str(text)
|
||||
raw = str(text)
|
||||
if any(char in raw for char in _INLINE_MARKUP_CHARS):
|
||||
return raw
|
||||
return f"{INLINE_FONT_CODE}{raw}}}"
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class TableLayout:
|
||||
|
||||
@@ -0,0 +1,517 @@
|
||||
"""ГОСТ Р 21.1101-2013 форма 3 — основная надпись и рамка листа.
|
||||
|
||||
Геометрия взята с официального чертежа Приложения Ж (форма 3): штамп 185 x 55 мм
|
||||
из строк по 5 мм, таблица изменений 65 мм (графы 14-19), правая часть 120 мм
|
||||
(70 + 50), вложенные графы 6-8 «Стадия / Лист / Листов» шириной 15 / 15 / 20 мм.
|
||||
Штамп ставится в правый нижний угол рамки; поля рамки 20/5/5/5 мм.
|
||||
|
||||
Смысл граф — по 5.2.7 и Приложению Ж ГОСТ Р 21.1101-2013:
|
||||
1 — обозначение документа, 2 — наименование предприятия (объекта),
|
||||
3 — наименование здания (сооружения), 4 — наименование изображений листа,
|
||||
6 — стадия (П/Р), 7 — лист, 8 — листов, 9 — организация,
|
||||
10-13 — характер работы, фамилия, подпись и дата,
|
||||
14-19 — таблица изменений, 25 — масштаб, 26 — обозначение формата.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import asdict, dataclass, field
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from backends.base import AutoCADBackend
|
||||
|
||||
# ── форматы листов (ГОСТ 2.301), мм ──────────────────────────────────────────
|
||||
SHEETS: dict[str, tuple[float, float]] = {
|
||||
"A4": (297.0, 210.0),
|
||||
"A3": (420.0, 297.0),
|
||||
"A2": (594.0, 420.0),
|
||||
"A1": (841.0, 594.0),
|
||||
"A0": (1189.0, 841.0),
|
||||
}
|
||||
|
||||
#: Поля рамки по ГОСТ Р 21.1101-2013 (5.2.7, приложение И).
|
||||
MARGIN_LEFT = 20.0
|
||||
MARGIN_RIGHT = 5.0
|
||||
MARGIN_TOP = 5.0
|
||||
MARGIN_BOTTOM = 5.0
|
||||
|
||||
# ── геометрия штампа, мм ─────────────────────────────────────────────────────
|
||||
STAMP_W = 185.0
|
||||
STAMP_H = 55.0
|
||||
ROW_H = 5.0 # 11 строк по 5 мм
|
||||
LEFT_BLOCK_W = 65.0
|
||||
RIGHT_BLOCK_W = 120.0
|
||||
#: графы 14-19 таблицы изменений: (номер, подпись, ширина)
|
||||
#: Ширины подобраны по метрике ISOCPEUR так, чтобы подпись графы вставала в
|
||||
#: свою клетку при высоте 1.8 мм (ГОСТ 2.304): «Кол.уч.» и «№ док.» — 6 знаков,
|
||||
#: то есть 9.1 мм, и в прежние 10 мм они влезали только после уменьшения до
|
||||
#: 1.36/1.59 мм, что ниже нормы читаемости. Сумма остаётся 65 мм.
|
||||
CHANGE_COLUMNS: tuple[tuple[int, str, float], ...] = (
|
||||
(14, "Изм.", 10.0),
|
||||
(15, "Кол.уч.", 11.0),
|
||||
(16, "Лист", 8.0),
|
||||
(17, "№ док.", 11.0),
|
||||
(18, "Подп.", 10.0),
|
||||
(19, "Дата", 15.0),
|
||||
)
|
||||
#: графы 6-8: (номер, подпись, ширина)
|
||||
SHEET_COLUMNS: tuple[tuple[int, str, float], ...] = (
|
||||
(6, "Стадия", 15.0),
|
||||
(7, "Лист", 15.0),
|
||||
(8, "Листов", 20.0),
|
||||
)
|
||||
|
||||
#: Наименьшая высота надписи на листе (ГОСТ 2.304 / ISO 3098). Ниже неё
|
||||
#: подпись не читается на печати — этим полом ограничено автоуменьшение
|
||||
#: значений, а `drawing_export_pdf` отдельно сообщает о том, что всё-таки
|
||||
#: оказалось мельче (поле warnings).
|
||||
MIN_TEXT_H = 1.8
|
||||
|
||||
LABEL_H = 1.8 # подписи граф
|
||||
VALUE_H = 3.0 # значения
|
||||
TITLE_H = 5.0 # обозначение документа
|
||||
GRAPH_NO_H = 1.8 # номера граф в скобках — не мельче нормы читаемости
|
||||
PAD = 1.0
|
||||
#: Отступ подписи графы от её линии. Меньше общего PAD: подписи «Кол.уч.» и
|
||||
#: «№ док.» занимают по 9.1 мм, и при 1.0 мм с каждой стороны они не влезали в
|
||||
#: 11-миллиметровую графу с первого раза.
|
||||
TAG_PAD = 0.6
|
||||
#: Зазор между номером графы в скобках и значением в той же клетке. Номер и
|
||||
#: значение раньше стояли в одной точке, и «(25)Масштаб 1:50» печаталось
|
||||
#: наложением.
|
||||
TAG_GAP = 0.6
|
||||
|
||||
#: Графы 10-14: подписи по ГОСТ Р 21.1101-2013 (приложение Ж) — «Разраб.»,
|
||||
#: «Пров.», «Т.контр.», «Н.контр.», «Утв.». Полные слова («Разработал.») при
|
||||
#: 1.8 мм занимают 16.6 мм и выходят за 17-миллиметровую графу характера работы.
|
||||
SIGN_ROWS: tuple[tuple[str, int], ...] = (
|
||||
("Разраб.", 10),
|
||||
("Пров.", 11),
|
||||
("Т.контр.", 12),
|
||||
("Н.контр.", 13),
|
||||
("Утв.", 14),
|
||||
)
|
||||
#: Ширины колонок блока подписей (характер работы / фамилия / подпись / дата),
|
||||
#: сумма 65 мм — ширина левой части штампа. Прежние 20/22/13/10 не вмещали
|
||||
#: «01.10.2026» в 10 мм (дата ужималась до 1.2 мм), а 17 мм под фамилию
|
||||
#: оставляют «Николашкин» читаемым (2.0 мм).
|
||||
SIGN_LABEL_W = 17.0
|
||||
SIGN_NAME_W = 18.0
|
||||
SIGN_SIGN_W = 12.0
|
||||
SIGN_DATE_W = 18.0
|
||||
|
||||
#: Ширина знака как доля высоты текста. Измерено по рендеру: «ЭО-01» высотой
|
||||
#: 5 мм занимает по листу 21 мм, то есть ~0.84 высоты на знак у ISOCPEUR /
|
||||
#: DejaVu Sans. Нужна, чтобы значения граф не выезжали за свои колонки —
|
||||
#: `entity_create_text` выравнивает строку по левому краю от точки вставки.
|
||||
CHAR_W_RATIO = 0.84
|
||||
#: Полезная ширина графы 1, 2, 3, 4, 9 (70 мм минус отступы).
|
||||
BLOCK_INNER_W = 68.0
|
||||
|
||||
LAYER_FRAME = "TITLEBLOCK"
|
||||
LAYER_TEXT = "TEXT"
|
||||
|
||||
DEFAULT_TEXT_STYLE = "GOST_A"
|
||||
DEFAULT_FONT = "isocpeur.ttf"
|
||||
|
||||
|
||||
@dataclass
|
||||
class GostTitleBlockMetadata:
|
||||
"""Данные основной надписи по ГОСТ Р 21.1101-2013 (форма 3)."""
|
||||
|
||||
drawing_no: str = "" # графа 1 — обозначение документа
|
||||
enterprise: str = "" # графа 2 — предприятие / объект строительства
|
||||
building: str = "" # графа 3 — здание (сооружение)
|
||||
title: str = "" # графа 4 — наименование изображений листа
|
||||
stage: str = "Р" # графа 6 — стадия
|
||||
sheet: str = "1" # графа 7 — лист
|
||||
total_sheets: str = "1" # графа 8 — листов
|
||||
organization: str = "" # графа 9 — организация
|
||||
scale: str = "1:100" # графа 25 — масштаб
|
||||
format: str = "A3" # графа 26 — формат листа
|
||||
developed_by: str = "" # графа 11
|
||||
developed_date: str = "" # графа 13
|
||||
checked_by: str = "" # Проверил
|
||||
checked_date: str = ""
|
||||
norm_control: str = "" # Н. контр.
|
||||
approved_by: str = "" # Утв.
|
||||
change_rows: list[list[str]] = field(default_factory=list)
|
||||
|
||||
|
||||
def _fmt(value: str) -> str:
|
||||
return "" if value is None else str(value)
|
||||
|
||||
|
||||
def _fit_height(value: str, height: float, max_width: float = BLOCK_INNER_W) -> float:
|
||||
"""Уменьшить высоту строки, если она не влезает в графу по ширине.
|
||||
|
||||
Графы 2, 3, 4 и 9 имеют фиксированную ширину 70 мм, а наименования бывают
|
||||
длинными («План расположения электрооборудования» — 37 знаков). Переносить
|
||||
строку внутри пятимиллиметровой строки штампа некуда, поэтому длинное
|
||||
значение рисуется мельче — так, чтобы остаться в своей графе, а не
|
||||
наезжать на соседние.
|
||||
|
||||
Ниже ``MIN_TEXT_H`` (1.8 мм) не опускаемся: подпись мельче нормы читаемости
|
||||
не читается на печати, и лучше чуть выйти за клетку, чем отдать лист с
|
||||
нечитаемой основной надписью.
|
||||
"""
|
||||
text = _fmt(value)
|
||||
if not text or not height:
|
||||
return height
|
||||
width = len(text) * height * CHAR_W_RATIO
|
||||
if width <= max_width:
|
||||
return height
|
||||
return max(MIN_TEXT_H, round(max_width / (len(text) * CHAR_W_RATIO), 2))
|
||||
|
||||
|
||||
def _tag_width(number: int, height: float = GRAPH_NO_H) -> float:
|
||||
"""Ширина номера графы в скобках — '(14)', '(25)'."""
|
||||
return len(f"({number})") * height * CHAR_W_RATIO
|
||||
|
||||
|
||||
def _value_after_tag(base_x: float, number: int, height: float = GRAPH_NO_H) -> float:
|
||||
"""X значения в клетке, где слева уже стоит номер графы.
|
||||
|
||||
Раньше номер и значение рисовались из одной точки, и «(25)Масштаб 1:50»
|
||||
печаталось наложением друг на друга (претензия Павла «надписи накладываются
|
||||
друг на друга», разобрано 01.10.2026).
|
||||
"""
|
||||
return base_x + _tag_width(number, height) + TAG_GAP
|
||||
|
||||
|
||||
async def ensure_layers(backend, names) -> dict:
|
||||
"""Создать слои, если их нет (не мешая уже существующим)."""
|
||||
result: dict[str, bool] = {}
|
||||
try:
|
||||
existing = {str(entry.name) for entry in await backend.layer_list()}
|
||||
except Exception:
|
||||
return result
|
||||
for name in names:
|
||||
if name in existing:
|
||||
result[name] = False
|
||||
continue
|
||||
try:
|
||||
await backend.layer_create(name)
|
||||
result[name] = True
|
||||
except Exception:
|
||||
result[name] = False
|
||||
return result
|
||||
|
||||
|
||||
async def set_layout_paper(backend, layout: str, sheet_w: float, sheet_h: float) -> dict:
|
||||
"""Привести размер листа (страницы) к формату, под который нарисована рамка.
|
||||
|
||||
`drawing_export_pdf(page_size="layout")` берёт страницу PDF из
|
||||
`Layout.dxf.paper_width/paper_height`. Документ, созданный `drawing_new`,
|
||||
начинается с A3 независимо от формата, переданного в `sheet_format`, поэтому
|
||||
рамка A4 уезжала в угол страницы A3. Бэкенд без `layout_set_paper` (COM) не
|
||||
считается ошибкой: там страница задаётся настройкой печати AutoCAD.
|
||||
"""
|
||||
setter = getattr(backend, "layout_set_paper", None)
|
||||
if setter is None:
|
||||
return {"ok": False, "reason": "backend has no layout_set_paper"}
|
||||
try:
|
||||
return await setter(layout, sheet_w, sheet_h)
|
||||
except Exception as exc: # размер страницы не должен валить построение рамки
|
||||
return {"ok": False, "error": str(exc)}
|
||||
|
||||
|
||||
async def ensure_text_style(backend, style: str, font: str, set_default: bool = True) -> dict:
|
||||
"""Best-effort: настроить текстовый стиль с поддержкой кириллицы."""
|
||||
ensure = getattr(backend, "textstyle_ensure", None)
|
||||
if ensure is None:
|
||||
return {"ok": False, "reason": "backend has no textstyle support"}
|
||||
try:
|
||||
return await ensure(style, font=font, set_default=set_default)
|
||||
except Exception as exc: # стиль не должен валить построение штампа
|
||||
return {"ok": False, "error": str(exc)}
|
||||
|
||||
|
||||
async def apply_gost_form3_titleblock(
|
||||
backend: "AutoCADBackend",
|
||||
*,
|
||||
metadata: GostTitleBlockMetadata,
|
||||
sheet: str = "A3",
|
||||
origin: tuple[float, float] = (0.0, 0.0),
|
||||
layout: str | None = None,
|
||||
text_style: str = DEFAULT_TEXT_STYLE,
|
||||
text_font: str = DEFAULT_FONT,
|
||||
) -> dict:
|
||||
"""Нарисовать рамку листа и основную надпись формы 3 в её правом нижнем углу.
|
||||
|
||||
`layout` — имя листа (paper space); без него рисуется в текущем пространстве.
|
||||
`sheet` — формат листа (A3, A4, ...), он же уходит в графу 26.
|
||||
"""
|
||||
sheet_key = str(sheet or "A3").strip().upper()
|
||||
if sheet_key not in SHEETS:
|
||||
raise RuntimeError(
|
||||
f"apply_gost_form3_titleblock: unknown sheet {sheet!r} "
|
||||
f"(known: {', '.join(sorted(SHEETS))})"
|
||||
)
|
||||
sheet_w, sheet_h = SHEETS[sheet_key]
|
||||
ox, oy = float(origin[0]), float(origin[1])
|
||||
|
||||
previous_space = "Model"
|
||||
if layout is not None:
|
||||
listing = await backend.layout_list()
|
||||
names = {str(entry) for entry in (listing.get("layouts") or [])}
|
||||
if layout not in names:
|
||||
raise RuntimeError(
|
||||
f"apply_gost_form3_titleblock: layout {layout!r} does not exist "
|
||||
f"(have: {', '.join(sorted(names))}). Create it with layout_create first."
|
||||
)
|
||||
previous_space = getattr(backend, "_current_space", "Model")
|
||||
switched = await backend.layout_set_current(layout)
|
||||
if not switched.get("ok", True):
|
||||
raise RuntimeError(f"apply_gost_form3_titleblock: {switched.get('error')}")
|
||||
|
||||
try:
|
||||
style_status = await ensure_text_style(backend, text_style, text_font, True)
|
||||
layers_status = await ensure_layers(backend, (LAYER_FRAME, LAYER_TEXT))
|
||||
paper_status = (
|
||||
await set_layout_paper(backend, layout, sheet_w, sheet_h)
|
||||
if layout is not None
|
||||
else {"ok": False, "reason": "no layout given"}
|
||||
)
|
||||
drawn = await _draw(
|
||||
backend,
|
||||
metadata=metadata,
|
||||
ox=ox,
|
||||
oy=oy,
|
||||
sheet_key=sheet_key,
|
||||
sheet_w=sheet_w,
|
||||
sheet_h=sheet_h,
|
||||
layout=layout,
|
||||
style_status=style_status,
|
||||
layers_status=layers_status,
|
||||
)
|
||||
if isinstance(drawn, dict):
|
||||
drawn.setdefault("paper_status", paper_status)
|
||||
return drawn
|
||||
finally:
|
||||
if layout is not None:
|
||||
await backend.layout_set_current(previous_space)
|
||||
|
||||
|
||||
async def _draw(
|
||||
backend,
|
||||
*,
|
||||
metadata: GostTitleBlockMetadata,
|
||||
ox: float,
|
||||
oy: float,
|
||||
sheet_key: str,
|
||||
sheet_w: float,
|
||||
sheet_h: float,
|
||||
layout: str | None,
|
||||
style_status: dict,
|
||||
layers_status: dict,
|
||||
) -> dict:
|
||||
|
||||
async def line(x1: float, y1: float, x2: float, y2: float) -> str:
|
||||
ent = await backend.entity_create_line(x1, y1, x2, y2, layer=LAYER_FRAME)
|
||||
return ent.handle
|
||||
|
||||
async def text(
|
||||
value: str,
|
||||
x: float,
|
||||
y: float,
|
||||
height: float = LABEL_H,
|
||||
*,
|
||||
rotation: float = 0.0,
|
||||
) -> str | None:
|
||||
value = _fmt(value)
|
||||
if not value:
|
||||
return None
|
||||
ent = await backend.entity_create_text(
|
||||
text=value, x=x, y=y, height=height, rotation=rotation, layer=LAYER_TEXT
|
||||
)
|
||||
return ent.handle
|
||||
|
||||
handles: list[str] = []
|
||||
|
||||
# ── рамка листа: поля 20/5/5/5 ───────────────────────────────────────────
|
||||
fx0 = ox + MARGIN_LEFT
|
||||
fy0 = oy + MARGIN_BOTTOM
|
||||
fx1 = ox + sheet_w - MARGIN_RIGHT
|
||||
fy1 = oy + sheet_h - MARGIN_TOP
|
||||
frame = await backend.entity_create_polyline(
|
||||
points=[[fx0, fy0], [fx1, fy0], [fx1, fy1], [fx0, fy1]],
|
||||
closed=True,
|
||||
layer=LAYER_FRAME,
|
||||
)
|
||||
handles.append(frame.handle)
|
||||
|
||||
# ── штамп: правый нижний угол рамки ──────────────────────────────────────
|
||||
sx0 = fx1 - STAMP_W
|
||||
sy0 = fy0
|
||||
sx1 = fx1
|
||||
sy1 = fy0 + STAMP_H
|
||||
|
||||
stamp_frame = await backend.entity_create_polyline(
|
||||
points=[[sx0, sy0], [sx1, sy0], [sx1, sy1], [sx0, sy1]],
|
||||
closed=True,
|
||||
layer=LAYER_FRAME,
|
||||
)
|
||||
handles.append(stamp_frame.handle)
|
||||
|
||||
left_x = sx0 + LEFT_BLOCK_W # 65 мм
|
||||
split_70 = sx0 + LEFT_BLOCK_W + 70.0 # 135 мм
|
||||
split_sheet = split_70 + SHEET_COLUMNS[0][2] # 150 мм
|
||||
split_sheet2 = split_sheet + SHEET_COLUMNS[1][2] # 165 мм
|
||||
|
||||
# горизонтальные линии: строки по 5 мм
|
||||
row_lines: list[float] = []
|
||||
for i in range(1, int(STAMP_H / ROW_H)):
|
||||
y = round(sy0 + i * ROW_H, 4)
|
||||
row_lines.append(y)
|
||||
handles.append(await line(sx0, y, sx1, y))
|
||||
# правая часть штампа: нижняя полоса граф 25/26 отделена, а строки
|
||||
# граф 1-4 идут от sy0+5 вверх
|
||||
handles.append(await line(left_x, sy0, left_x, sy1))
|
||||
handles.append(await line(split_70, sy0, split_70, sy1))
|
||||
handles.append(await line(split_70, sy1 - 25.0, sx1, sy1 - 25.0)) # верх блока 6-8
|
||||
handles.append(await line(split_sheet, sy1 - 25.0, split_sheet, sy1 - 10.0))
|
||||
handles.append(await line(split_sheet2, sy1 - 25.0, split_sheet2, sy1 - 10.0))
|
||||
handles.append(await line(split_sheet, sy1 - 30.0, sx1, sy1 - 30.0)) # подписи «Стадия/Лист/Листов»
|
||||
|
||||
# ── таблица изменений (графы 14-19) ─────────────────────────────────────
|
||||
x = sx0
|
||||
for number, label, width in CHANGE_COLUMNS:
|
||||
await text(f"({number})", x + TAG_PAD, sy1 - ROW_H + 0.6, GRAPH_NO_H)
|
||||
# «Кол.уч.» и «№ док.» при 1.8 мм занимают по 9.1 мм: подписи
|
||||
# подгоняются по полезной ширине графы, а сами графы расширены до 11 мм,
|
||||
# чтобы подпись не уменьшалась ниже ГОСТовских 1.8 мм.
|
||||
await text(label, x + TAG_PAD, sy1 - 2 * ROW_H + 0.6,
|
||||
_fit_height(label, LABEL_H, width - 2 * TAG_PAD))
|
||||
x += width
|
||||
if x < left_x - 0.01:
|
||||
handles.append(await line(x, sy1 - 2 * ROW_H, x, sy0))
|
||||
|
||||
# пустые строки таблицы изменений: 4 строки (sy1-20 .. sy1-45)
|
||||
for row_index, row in enumerate(list(metadata.change_rows)[:4]):
|
||||
y = sy1 - (2 + row_index + 1) * ROW_H + 0.6
|
||||
x = sx0
|
||||
for cell, (_, _, width) in zip(list(row) + [""] * 6, CHANGE_COLUMNS):
|
||||
if _fmt(cell):
|
||||
await text(_fit_height(_fmt(cell), LABEL_H, width - 2 * TAG_PAD),
|
||||
x + TAG_PAD, y)
|
||||
x += width
|
||||
|
||||
# ── подписи (графы 10-14) ───────────────────────────────────────────────
|
||||
sign_values = (
|
||||
(metadata.developed_by, metadata.developed_date),
|
||||
(metadata.checked_by, metadata.checked_date),
|
||||
("", ""),
|
||||
(metadata.norm_control, ""),
|
||||
(metadata.approved_by, ""),
|
||||
)
|
||||
sign_rows = [
|
||||
(label, number, name, date)
|
||||
for (label, number), (name, date) in zip(SIGN_ROWS, sign_values)
|
||||
]
|
||||
# Пропорции блока подписей: характер работы 17 мм, фамилия 18 мм, подпись
|
||||
# 12 мм, дата 18 мм (сумма 65). Прежние 20/22/13/10 не вмещали ни «01.10.2026»
|
||||
# (дата ужималась до 1.2 мм — вдвое мельче нормы), ни полные подписи.
|
||||
col_name_x = sx0 + SIGN_LABEL_W # Ф.И.О.
|
||||
col_sign_x = col_name_x + SIGN_NAME_W # подпись
|
||||
col_date_x = col_sign_x + SIGN_SIGN_W # дата
|
||||
# Строки 7-11 (индекс 0 = верхняя): таблица изменений занимает строки 1-6
|
||||
# (две строки подписей граф 14-19 и четыре пустых), подписи 10-14 — строки
|
||||
# 7-11. Смещение считалось от строки 8, поэтому пятая подпись («Утв.»)
|
||||
# рисовалась на 5 мм НИЖЕ низа штампа — за пределами рамки листа.
|
||||
for index, (label, number, name, date) in enumerate(sign_rows):
|
||||
top = sy1 - (6 + index) * ROW_H
|
||||
y = top - ROW_H + 0.7
|
||||
# Номер графы — в верхнем левом углу СВОЕЙ клетки. Он стоял на 1.4 мм
|
||||
# выше верхней линии, то есть в соседней строке: «(11)» печаталось в
|
||||
# клетке «Разраб.», «(14)» — в строке «Масштаб».
|
||||
await text(f"({number})", sx0 + TAG_PAD, top - GRAPH_NO_H - 0.3, GRAPH_NO_H)
|
||||
await text(label, sx0 + 2.6, y, _fit_height(label, LABEL_H, SIGN_LABEL_W - 3.6))
|
||||
await text(name, col_name_x + TAG_PAD, y,
|
||||
_fit_height(name, VALUE_H, SIGN_NAME_W - 2 * TAG_PAD))
|
||||
# Дата — графа 13: «01.10.2026» в 1.8 мм занимает 15.1 мм, поэтому
|
||||
# колонка расширена до 18 мм, а высота подгоняется по её полезной ширине.
|
||||
await text(date, col_date_x + TAG_PAD, y,
|
||||
_fit_height(date, LABEL_H, SIGN_DATE_W - 2 * TAG_PAD))
|
||||
for gx in (col_name_x, col_sign_x, col_date_x):
|
||||
handles.append(await line(gx, sy0, gx, sy1 - 6 * ROW_H))
|
||||
|
||||
# ── графа 1: обозначение документа ──────────────────────────────────────
|
||||
# Значение выравнивается по правому краю графы 1: `entity_create_text`
|
||||
# ставит текст по левому краю от точки вставки, поэтому обозначение,
|
||||
# записанное «от правой границы», уезжало в графы 6-8. Ширина считается по
|
||||
# фактической метрике знака (CHAR_W_RATIO), а не «на глаз».
|
||||
await text("(1)", sx0 + LEFT_BLOCK_W + PAD, sy1 - 2 * ROW_H, GRAPH_NO_H)
|
||||
await text(
|
||||
metadata.drawing_no,
|
||||
split_70 - 1.5 - len(_fmt(metadata.drawing_no)) * TITLE_H * CHAR_W_RATIO,
|
||||
sy1 - ROW_H - 3.4,
|
||||
TITLE_H,
|
||||
)
|
||||
# Значения граф 2-4 рисуются ПОСЛЕ номера графы, а не из той же точки:
|
||||
# раньше «(2)» и «ИП Николашкин П.В.» печатались полностью наложенными
|
||||
# друг на друга (проверено разбором текстового слоя PDF 01.10.2026).
|
||||
tag_x = sx0 + LEFT_BLOCK_W + PAD
|
||||
await text("(2)", tag_x, sy1 - 4 * ROW_H + 0.6, GRAPH_NO_H)
|
||||
await text(metadata.enterprise, _value_after_tag(tag_x, 2), sy1 - 4 * ROW_H + 1.0,
|
||||
_fit_height(metadata.enterprise, VALUE_H,
|
||||
BLOCK_INNER_W - _tag_width(2) - TAG_GAP - PAD))
|
||||
await text("(3)", tag_x, sy1 - 6 * ROW_H + 0.6, GRAPH_NO_H)
|
||||
await text(metadata.building, _value_after_tag(tag_x, 3), sy1 - 6 * ROW_H + 1.0,
|
||||
_fit_height(metadata.building, VALUE_H,
|
||||
BLOCK_INNER_W - _tag_width(3) - TAG_GAP - PAD))
|
||||
# графа 4 — наименование изображений листа
|
||||
await text("(4)", tag_x, sy0 + 2 * ROW_H + 0.6, GRAPH_NO_H)
|
||||
await text(metadata.title, _value_after_tag(tag_x, 4), sy0 + 2 * ROW_H,
|
||||
_fit_height(metadata.title, VALUE_H,
|
||||
BLOCK_INNER_W - _tag_width(4) - TAG_GAP - PAD))
|
||||
# ── графа 9 — организация ───────────────────────────────────────────────
|
||||
await text("(9)", split_70 + PAD, sy0 + 2 * ROW_H + 0.6, GRAPH_NO_H)
|
||||
# Ниже y = 30 правая часть штампа (365..415) не делится — это графа 9 в
|
||||
# 50 мм; длинное название организации подгоняем по 47 мм полезной ширины.
|
||||
await text(metadata.organization, split_70 + PAD, sy0 + ROW_H + 0.4,
|
||||
_fit_height(metadata.organization, VALUE_H, 47.0))
|
||||
|
||||
# ── графы 6-8: Стадия / Лист / Листов ───────────────────────────────────
|
||||
header_y = sy1 - 25.0 - ROW_H + 1.4
|
||||
x = split_70
|
||||
for number, label, width in SHEET_COLUMNS:
|
||||
await text(label, x + PAD, header_y, LABEL_H)
|
||||
await text(f"({number})", x + PAD, sy1 - 25.0 - ROW_H - 1.6, GRAPH_NO_H)
|
||||
x += width
|
||||
await text(metadata.stage, split_70 + PAD, sy1 - 30.0 - ROW_H - 0.6, VALUE_H)
|
||||
await text(metadata.sheet, split_sheet + PAD, sy1 - 30.0 - ROW_H - 0.6, VALUE_H)
|
||||
await text(metadata.total_sheets, split_sheet2 + PAD, sy1 - 30.0 - ROW_H - 0.6, VALUE_H)
|
||||
|
||||
# ── графы 25 (масштаб) и 26 (формат) ────────────────────────────────────
|
||||
await text("(25)", tag_x, sy0 + 1.4, GRAPH_NO_H)
|
||||
await text(f"Масштаб {metadata.scale}", _value_after_tag(tag_x, 25), sy0 + 0.7, LABEL_H)
|
||||
await text("(26)", split_70 + PAD, sy0 + 1.4, GRAPH_NO_H)
|
||||
await text(f"Формат {sheet_key}", _value_after_tag(split_70 + PAD, 26), sy0 + 0.7, LABEL_H)
|
||||
|
||||
return {
|
||||
"ok": True,
|
||||
"standard": "ГОСТ Р 21.1101-2013, форма 3",
|
||||
"sheet": sheet_key,
|
||||
"layout": layout or "Model",
|
||||
"frame": frame.handle,
|
||||
"stamp_frame": stamp_frame.handle,
|
||||
"lines": handles,
|
||||
"text_style": style_status,
|
||||
"layers": layers_status,
|
||||
"frame_margins": {
|
||||
"left": MARGIN_LEFT,
|
||||
"right": MARGIN_RIGHT,
|
||||
"top": MARGIN_TOP,
|
||||
"bottom": MARGIN_BOTTOM,
|
||||
},
|
||||
"stamp_bbox": {
|
||||
"min": [sx0, sy0],
|
||||
"max": [sx1, sy1],
|
||||
"width": STAMP_W,
|
||||
"height": STAMP_H,
|
||||
},
|
||||
"frame_bbox": {"min": [fx0, fy0], "max": [fx1, fy1]},
|
||||
"metadata": asdict(metadata),
|
||||
}
|
||||
Reference in New Issue
Block a user