- tech draw draft

This commit is contained in:
bklronin
2026-08-16 15:54:41 +02:00
parent a00129603c
commit 1eee203ba8
5 changed files with 2202 additions and 76 deletions
+800
View File
@@ -0,0 +1,800 @@
"""
Technical Drawing workbench widget.
Embeddable QWidget for creating, editing, and exporting technical drawings
from component and assembly geometry. Designed to sit in MainWindow's
InputTab alongside Sketch, Code, and Render tabs.
"""
from __future__ import annotations
from typing import Dict, List, Optional
from PySide6.QtCore import Qt, Signal, QRectF, QPointF
from PySide6.QtGui import QPainter, QColor, QFont, QMouseEvent, QWheelEvent
from PySide6.QtWidgets import (
QWidget,
QVBoxLayout,
QHBoxLayout,
QSplitter,
QPushButton,
QCheckBox,
QRadioButton,
QButtonGroup,
QComboBox,
QLabel,
QLineEdit,
QGroupBox,
QScrollArea,
QTableWidget,
QTableWidgetItem,
QHeaderView,
QTextEdit,
QAbstractItemView,
QSizePolicy,
)
from fluency.models.data_model import TechnicalDrawing, DrawingView, DrawingAnnotation
from fluency.technical_drawing import (
DrawingRenderResult,
DrawingPrimitive,
generate_drawing,
render_drawing,
export_drawing_svg,
export_drawing_pdf,
_STANDARD_VIEW_ROWS,
)
# Sheet layout constants.
_A3_MM_W = 420.0
_A3_MM_H = 297.0
class DrawingCanvas(QWidget):
"""Custom QPainter canvas with zoom/pan for technical drawing display."""
def __init__(self, parent=None):
super().__init__(parent)
self._render_result: Optional[DrawingRenderResult] = None
self._zoom: float = 1.0
self._pan_x: float = 0.0
self._pan_y: float = 0.0
self._last_mouse: Optional[QPointF] = None
self.setMinimumSize(400, 300)
self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
self.setMouseTracking(True)
def set_render_result(self, result: Optional[DrawingRenderResult]) -> None:
self._render_result = result
self._zoom = 1.0
self._pan_x = 0.0
self._pan_y = 0.0
self.update()
def paintEvent(self, event) -> None:
painter = QPainter(self)
painter.setRenderHint(QPainter.Antialiasing)
painter.fillRect(self.rect(), QColor(200, 200, 200))
if self._render_result is None or not self._render_result.primitives:
painter.setPen(QColor(128, 128, 128))
font = QFont("sans-serif", 14)
painter.setFont(font)
painter.drawText(self.rect(), Qt.AlignCenter, "No drawing generated")
painter.end()
return
# Compute sheet rect in device coords with zoom/pan.
aspect = _A3_MM_W / _A3_MM_H
w = self.width()
h = self.height()
if w / h > aspect:
draw_h = h * 0.85 * self._zoom
draw_w = draw_h * aspect
else:
draw_w = w * 0.85 * self._zoom
draw_h = draw_w / aspect
ox = (w - draw_w) / 2.0 + self._pan_x
oy = (h - draw_h) / 2.0 + self._pan_y
sheet_rect = QRectF(ox, oy, draw_w, draw_h)
render_drawing(painter, self._render_result, sheet_rect)
painter.end()
def wheelEvent(self, event: QWheelEvent) -> None:
factor = 1.1 if event.angleDelta().y() > 0 else 0.9
self._zoom = max(0.1, min(10.0, self._zoom * factor))
self.update()
def mousePressEvent(self, event: QMouseEvent) -> None:
if event.button() == Qt.MiddleButton:
self._last_mouse = event.position()
self.setCursor(Qt.ClosedHandCursor)
def mouseMoveEvent(self, event: QMouseEvent) -> None:
if self._last_mouse is not None:
delta = event.position() - self._last_mouse
self._pan_x += delta.x()
self._pan_y += delta.y()
self._last_mouse = event.position()
self.update()
def mouseReleaseEvent(self, event: QMouseEvent) -> None:
if event.button() == Qt.MiddleButton:
self._last_mouse = None
self.setCursor(Qt.ArrowCursor)
class TechnicalDrawingWidget(QWidget):
"""Embeddable technical drawing workbench."""
drawing_changed = Signal()
source_requested = Signal(str, str) # source_kind, source_id
export_requested = Signal(str, object) # format ("pdf"/"svg"), drawing
def __init__(self, parent=None):
super().__init__(parent)
self._drawing: Optional[TechnicalDrawing] = None
self._render_result: Optional[DrawingRenderResult] = None
self._project = None
self._kernel = None
self._component_ids: List[str] = []
self._assembly_ids: List[str] = []
self._view_checkboxes: Dict[str, QCheckBox] = {}
self._hidden_line_checks: Dict[str, QCheckBox] = {}
self._selected_dim_keys: set = set()
self._centerline_checks: Dict[str, QCheckBox] = {}
self._init_ui()
# ── Public API ──────────────────────────────────────────────────────────
def set_project(self, project, kernel) -> None:
"""Set the current project and kernel reference."""
self._project = project
self._kernel = kernel
def set_source_catalog(
self, component_ids: List[str], assembly_ids: List[str]
) -> None:
"""Update the source combo with available components/assemblies."""
self._component_ids = component_ids
self._assembly_ids = assembly_ids
self._rebuild_source_combo()
def set_active_component(self, component) -> None:
"""Update the drawing tab to use the given component/assembly as source.
Selects it in the combo and regenerates."""
self._mode_component_btn.setChecked(True)
# Ensure the combo is in component mode and has this component selected.
self._rebuild_source_combo()
idx = self._source_combo.findData(component.id)
if idx >= 0:
self._source_combo.setCurrentIndex(idx)
# Delegate to the same path as the Generate button.
self._on_generate()
def set_drawing(self, drawing: Optional[TechnicalDrawing]) -> None:
"""Load a drawing definition."""
self._drawing = drawing
self._render_result = None
if drawing is not None:
self._populate_from_drawing(drawing)
self._canvas.set_render_result(None)
self._generate_btn.setEnabled(self._drawing is not None)
self._refresh_annotation_table()
def get_drawing(self) -> Optional[TechnicalDrawing]:
"""Get the current drawing (may be modified by UI)."""
if self._drawing is None:
return None
self._sync_views_to_drawing()
self._drawing.title = self._title_edit.text()
self._drawing.part_number = self._part_no_edit.text()
self._drawing.material = self._material_edit.text()
self._drawing.revision = self._revision_edit.text()
self._drawing.notes = self._notes_edit.toPlainText()
return self._drawing
def set_render_result(self, result: Optional[DrawingRenderResult]) -> None:
"""Display a generated render result."""
self._render_result = result
self._canvas.set_render_result(result)
if result is not None:
self._status_label.setText(
f"Generated — {len(result.primitives)} primitives, "
f"{len(result.candidates)} candidates"
)
if result.warnings:
self._status_label.setText(
self._status_label.text() + f" ({len(result.warnings)} warnings)"
)
self._export_pdf_btn.setEnabled(True)
self._export_svg_btn.setEnabled(True)
else:
self._export_pdf_btn.setEnabled(False)
self._export_svg_btn.setEnabled(False)
self._refresh_dim_table()
# ── UI construction ─────────────────────────────────────────────────────
def _init_ui(self) -> None:
layout = QHBoxLayout(self)
# ── Left controls ───────────────────────────────────────────────
left = QWidget()
left.setFixedWidth(300)
left_layout = QVBoxLayout(left)
left_layout.setContentsMargins(4, 4, 4, 4)
# Source selection.
src_group = QGroupBox("Source")
src_layout = QVBoxLayout(src_group)
mode_layout = QHBoxLayout()
self._mode_component_btn = QRadioButton("Component")
self._mode_assembly_btn = QRadioButton("Assembly")
self._mode_component_btn.setChecked(True)
# Rebuild the combo on user click so the selected id always matches
# the active mode — otherwise Generate combines a kind from one mode
# with an id from the other.
self._mode_component_btn.clicked.connect(self._rebuild_source_combo)
self._mode_assembly_btn.clicked.connect(self._rebuild_source_combo)
mode_layout.addWidget(self._mode_component_btn)
mode_layout.addWidget(self._mode_assembly_btn)
src_layout.addLayout(mode_layout)
self._source_combo = QComboBox()
src_layout.addWidget(self._source_combo)
self._use_selection_btn = QPushButton("Use Current Selection")
self._use_selection_btn.clicked.connect(self._on_use_selection)
src_layout.addWidget(self._use_selection_btn)
left_layout.addWidget(src_group)
# Views.
views_group = QGroupBox("Views")
views_layout = QVBoxLayout(views_group)
for vid, vname, vdir, vup in _STANDARD_VIEW_ROWS:
row = QHBoxLayout()
cb = QCheckBox(vname)
self._view_checkboxes[vid] = cb
row.addWidget(cb)
hl = QCheckBox("HL")
hl.setToolTip("Show hidden lines")
self._hidden_line_checks[vid] = hl
row.addWidget(hl)
cl = QCheckBox("CL")
cl.setToolTip("Show centerlines")
self._centerline_checks[vid] = cl
row.addWidget(cl)
views_layout.addLayout(row)
# Wire view/HL/CL toggles to regenerate.
for vid, cb in self._view_checkboxes.items():
cb.toggled.connect(lambda checked, v=vid: self._on_view_toggled(v))
for vid, cb in self._hidden_line_checks.items():
cb.toggled.connect(lambda checked, v=vid: self._on_view_toggled(v))
for vid, cb in self._centerline_checks.items():
cb.toggled.connect(lambda checked, v=vid: self._on_view_toggled(v))
# Default: Front, Top, Right, Isometric.
for vid in ("front", "top", "right", "isometric"):
self._view_checkboxes[vid].setChecked(True)
# Match the DrawingView model defaults (hidden lines + centerlines
# on) so Generate produces the same drawing as the tab-switch
# auto-load in MainWindow._load_drawing_tab_source.
for cb in self._hidden_line_checks.values():
cb.setChecked(True)
for cb in self._centerline_checks.values():
cb.setChecked(True)
left_layout.addWidget(views_group)
# Sheet / title block.
sheet_group = QGroupBox("Title Block")
sheet_layout = QVBoxLayout(sheet_group)
for lbl, attr in [
("Title", "title"),
("Part No.", "part_number"),
("Material", "material"),
("Revision", "revision"),
]:
row = QHBoxLayout()
row.addWidget(QLabel(lbl))
edit = QLineEdit()
setattr(self, f"_{attr}_edit", edit)
row.addWidget(edit)
sheet_layout.addLayout(row)
self._title_edit = self.__dict__.get("_title_edit", QLineEdit())
self._part_no_edit = self.__dict__.get("_part_no_edit", QLineEdit())
self._material_edit = self.__dict__.get("_material_edit", QLineEdit())
self._revision_edit = self.__dict__.get("_revision_edit", QLineEdit())
sheet_layout.addWidget(QLabel("Notes:"))
self._notes_edit = QTextEdit()
self._notes_edit.setMaximumHeight(80)
sheet_layout.addWidget(self._notes_edit)
left_layout.addWidget(sheet_group)
# Actions.
actions_group = QGroupBox("Actions")
actions_layout = QVBoxLayout(actions_group)
self._auto_generate_btn = QPushButton("Auto-Generate Drawing")
self._auto_generate_btn.setToolTip(
"Create a new drawing from current source with optimal views and dimensions"
)
self._auto_generate_btn.clicked.connect(self._on_auto_generate)
actions_layout.addWidget(self._auto_generate_btn)
self._generate_btn = QPushButton("Generate / Regenerate")
self._generate_btn.setEnabled(True)
self._generate_btn.clicked.connect(self._on_generate)
actions_layout.addWidget(self._generate_btn)
# Dimension options.
dim_row = QHBoxLayout()
self._auto_dim_check = QCheckBox("Auto dimensions")
self._auto_dim_check.setChecked(True)
dim_row.addWidget(self._auto_dim_check)
actions_layout.addLayout(dim_row)
self._save_btn = QPushButton("Save Drawing")
self._save_btn.clicked.connect(self._on_save)
actions_layout.addWidget(self._save_btn)
export_row = QHBoxLayout()
self._export_pdf_btn = QPushButton("Export PDF")
self._export_pdf_btn.setEnabled(False)
self._export_pdf_btn.clicked.connect(lambda: self._on_export("pdf"))
export_row.addWidget(self._export_pdf_btn)
self._export_svg_btn = QPushButton("Export SVG")
self._export_svg_btn.setEnabled(False)
self._export_svg_btn.clicked.connect(lambda: self._on_export("svg"))
export_row.addWidget(self._export_svg_btn)
actions_layout.addLayout(export_row)
left_layout.addWidget(actions_group)
# Status.
self._status_label = QLabel("No drawing loaded")
self._status_label.setWordWrap(True)
left_layout.addWidget(self._status_label)
left_layout.addStretch()
left.setLayout(left_layout)
# ── Scroll area for left panel ──────────────────────────────────
scroll = QScrollArea()
scroll.setWidget(left)
scroll.setWidgetResizable(True)
scroll.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
# ── Right side: canvas + annotation table ───────────────────────
right = QWidget()
right_layout = QVBoxLayout(right)
right_layout.setContentsMargins(0, 0, 0, 0)
self._canvas = DrawingCanvas()
right_layout.addWidget(self._canvas, 1)
# Annotation table.
ann_group = QGroupBox("Annotations")
ann_layout = QVBoxLayout(ann_group)
self._annotation_table = QTableWidget(0, 5)
self._annotation_table.setHorizontalHeaderLabels(
["Kind", "Value / Text", "Visible", "Orphaned", "Action"]
)
self._annotation_table.horizontalHeader().setSectionResizeMode(
QHeaderView.Stretch
)
self._annotation_table.setMaximumHeight(150)
self._annotation_table.setSelectionBehavior(QAbstractItemView.SelectRows)
ann_layout.addWidget(self._annotation_table)
add_ann_row = QHBoxLayout()
add_ann_row.addWidget(QLabel("Add:"))
self._add_note_btn = QPushButton("Note")
self._add_note_btn.clicked.connect(self._on_add_note)
add_ann_row.addWidget(self._add_note_btn)
add_ann_row.addStretch()
ann_layout.addLayout(add_ann_row)
right_layout.addWidget(ann_group)
# Dimension candidates table (for user selection).
dim_group = QGroupBox("Dimension Candidates")
dim_layout = QVBoxLayout(dim_group)
self._dim_table = QTableWidget(0, 5)
self._dim_table.setHorizontalHeaderLabels(["View", "Kind", "Value", "Include", "Action"])
self._dim_table.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch)
self._dim_table.setMaximumHeight(120)
self._dim_table.setSelectionBehavior(QAbstractItemView.SelectRows)
dim_layout.addWidget(self._dim_table)
self._refresh_dim_table()
right_layout.addWidget(dim_group)
splitter = QSplitter(Qt.Horizontal)
splitter.addWidget(scroll)
splitter.addWidget(right)
splitter.setStretchFactor(0, 0)
splitter.setStretchFactor(1, 1)
layout.addWidget(splitter)
# ── Internal helpers ──────────────────────────────────────────────────
def _rebuild_source_combo(self) -> None:
self._source_combo.clear()
if self._mode_component_btn.isChecked():
for cid in self._component_ids:
self._source_combo.addItem(cid, userData=cid)
else:
for aid in self._assembly_ids:
self._source_combo.addItem(aid, userData=aid)
def _resolve_source(self):
"""Resolve ``(source_kind, source_id)`` from the current UI state.
Prefers the combo selection when it still refers to an id present in
the current project. Otherwise falls back to the project's active
component, then the first assembly — mirroring the tab-switch
auto-load in ``MainWindow._load_drawing_tab_source``. Returns
``(None, None)`` when no source is available.
"""
source_kind = (
"component" if self._mode_component_btn.isChecked() else "assembly"
)
source_id = self._source_combo.currentData()
# The combo may still hold an id from a previous project after a
# project swap — treat that as unselected.
catalog = (
self._project.components
if source_kind == "component"
else self._project.assemblies
)
if source_id not in catalog:
source_id = None
if not source_id:
comp = self._project.get_active_component()
if comp and any(b.visible and b.geometry for b in comp.bodies.values()):
return "component", comp.id
if self._project.assemblies:
return "assembly", next(iter(self._project.assemblies.keys()))
return None, None
return source_kind, source_id
def _populate_from_drawing(self, drawing: TechnicalDrawing) -> None:
"""Sync UI controls from a drawing definition."""
# Block checkbox signals during population to avoid triggering
# _on_view_toggled mid-update (which would call _on_generate with
# partially-set checkboxes and clear the views).
for cb in self._view_checkboxes.values():
cb.blockSignals(True)
for cb in self._hidden_line_checks.values():
cb.blockSignals(True)
for cb in self._centerline_checks.values():
cb.blockSignals(True)
try:
# Source mode.
if drawing.source_kind == "component":
self._mode_component_btn.setChecked(True)
else:
self._mode_assembly_btn.setChecked(True)
# Select source in combo (match by text since ID may not be in the list yet).
idx = self._source_combo.findText(drawing.source_id)
if idx >= 0:
self._source_combo.setCurrentIndex(idx)
else:
# Fallback: set userData directly so currentData() returns it.
self._source_combo.clear()
if drawing.source_kind == "component":
for cid in self._component_ids:
self._source_combo.addItem(cid, userData=cid)
else:
for aid in self._assembly_ids:
self._source_combo.addItem(aid, userData=aid)
idx = self._source_combo.findText(drawing.source_id)
if idx >= 0:
self._source_combo.setCurrentIndex(idx)
# Views.
enabled_views = {v.kind for v in drawing.views}
for vid, cb in self._view_checkboxes.items():
cb.setChecked(vid in enabled_views)
for v in drawing.views:
if v.kind in self._hidden_line_checks:
self._hidden_line_checks[v.kind].setChecked(v.show_hidden_lines)
if v.kind in self._centerline_checks:
self._centerline_checks[v.kind].setChecked(v.show_centerlines)
# Title block.
self._title_edit.setText(drawing.title)
self._part_no_edit.setText(drawing.part_number)
self._material_edit.setText(drawing.material)
self._revision_edit.setText(drawing.revision)
self._notes_edit.setPlainText(drawing.notes)
self._refresh_annotation_table()
finally:
for cb in self._view_checkboxes.values():
cb.blockSignals(False)
for cb in self._hidden_line_checks.values():
cb.blockSignals(False)
for cb in self._centerline_checks.values():
cb.blockSignals(False)
def _sync_views_to_drawing(self) -> None:
"""Write current view checkboxes back to the drawing."""
if self._drawing is None:
return
new_views: List[DrawingView] = []
for vid, vname, vdir, vup in _STANDARD_VIEW_ROWS:
if not self._view_checkboxes[vid].isChecked():
continue
# Keep existing view if present, else create.
existing = None
for v in self._drawing.views:
if v.kind == vid:
existing = v
break
if existing is not None:
existing.show_hidden_lines = self._hidden_line_checks[vid].isChecked()
existing.show_centerlines = self._centerline_checks[vid].isChecked()
new_views.append(existing)
else:
new_views.append(
DrawingView(
name=vname,
kind=vid,
direction=vdir,
up_vector=vup,
show_hidden_lines=self._hidden_line_checks[vid].isChecked(),
show_centerlines=self._centerline_checks[vid].isChecked(),
)
)
self._drawing.views = new_views
def _refresh_annotation_table(self) -> None:
"""Refresh annotation table from current drawing."""
self._annotation_table.setRowCount(0)
if self._drawing is None:
return
for i, ann in enumerate(self._drawing.annotations):
self._annotation_table.insertRow(i)
self._annotation_table.setItem(
i, 0, QTableWidgetItem(ann.kind)
)
self._annotation_table.setItem(
i, 1, QTableWidgetItem(
ann.text or ""
)
)
vis_cb = QCheckBox()
vis_cb.setChecked(ann.visible)
vis_cb.toggled.connect(
lambda checked, a=ann: setattr(a, "visible", checked)
)
self._annotation_table.setCellWidget(i, 2, vis_cb)
orphan_label = QLabel("")
self._annotation_table.setCellWidget(i, 3, orphan_label)
del_btn = QPushButton("×")
del_btn.setFixedWidth(24)
del_btn.clicked.connect(
lambda checked=False, aid=ann.id: self._delete_annotation(aid)
)
self._annotation_table.setCellWidget(i, 4, del_btn)
def _refresh_dim_table(self) -> None:
"""Refresh the dimension candidates table from render result."""
self._dim_table.setRowCount(0)
if self._render_result is None:
return
for i, c in enumerate(self._render_result.candidates):
self._dim_table.insertRow(i)
self._dim_table.setItem(i, 0, QTableWidgetItem(c.view_id))
self._dim_table.setItem(i, 1, QTableWidgetItem(c.kind))
self._dim_table.setItem(i, 2, QTableWidgetItem(c.label))
inc_cb = QCheckBox()
inc_cb.setChecked(c.key in self._selected_dim_keys)
inc_cb.toggled.connect(lambda checked, k=c.key: self._on_dim_include(k, checked))
self._dim_table.setCellWidget(i, 3, inc_cb)
del_btn = QPushButton("×")
del_btn.setFixedWidth(24)
del_btn.clicked.connect(lambda checked=False, k=c.key: self._on_dim_remove(k))
self._dim_table.setCellWidget(i, 4, del_btn)
def _on_dim_include(self, key: str, checked: bool) -> None:
if checked:
self._selected_dim_keys.add(key)
else:
self._selected_dim_keys.discard(key)
def _on_view_toggled(self, vid: str) -> None:
"""Regenerate when any view/HL/CL checkbox changes."""
if self._drawing is not None and self._project is not None and self._kernel is not None:
try:
result = generate_drawing(self._drawing, self._project, self._kernel)
self.set_render_result(result)
except Exception as e:
self._status_label.setText(f"Regeneration failed: {e}")
def _delete_annotation(self, ann_id: str) -> None:
if self._drawing is None:
return
self._drawing.annotations = [
a for a in self._drawing.annotations if a.id != ann_id
]
self._refresh_annotation_table()
# ── Slots ──────────────────────────────────────────────────────────────
def _on_use_selection(self) -> None:
src_kind = (
"component" if self._mode_component_btn.isChecked() else "assembly"
)
self.source_requested.emit(src_kind, "")
def _on_auto_generate(self) -> None:
"""Create a new drawing from current source with optimal views."""
from fluency.models.data_model import TechnicalDrawing, DrawingView
if self._project is None or self._kernel is None:
self._status_label.setText("No project/kernel available")
return
source_kind, source_id = self._resolve_source()
if not source_id:
self._status_label.setText("No source selected")
return
# Build default drawing with standard views.
views = []
for vid, vname, vdir, vup in _STANDARD_VIEW_ROWS:
if self._view_checkboxes[vid].isChecked():
views.append(
DrawingView(
kind=vid,
name=vname,
direction=vdir,
show_hidden_lines=self._hidden_line_checks[vid].isChecked(),
show_centerlines=self._centerline_checks[vid].isChecked(),
)
)
comp = None
if source_kind == "component":
comp = self._project.get_component_by_id(source_id)
drawing = TechnicalDrawing(
name=f"Drawing of {comp.name if comp else 'Assembly'}",
source_kind=source_kind,
source_id=source_id,
views=views,
title=comp.name if comp else "Assembly Drawing",
part_number="",
material="",
revision="A",
auto_dimensions=self._auto_dim_check.isChecked(),
)
self._drawing = drawing
self._populate_from_drawing(drawing)
self._on_generate()
def _on_generate(self) -> None:
"""Generate the drawing — always re-reads the current source and
builds a fresh TechnicalDrawing, same as tab-switching."""
from fluency.models.data_model import TechnicalDrawing, DrawingView
if self._project is None or self._kernel is None:
self._status_label.setText("No project/kernel available")
return
# Determine source: prefer combo selection, fall back to the
# project's active component / first assembly (see _resolve_source).
source_kind, source_id = self._resolve_source()
if not source_id:
self._status_label.setText("No source available for drawing")
return
# Build views from current checkbox state.
views = []
for vid, vname, vdir, vup in _STANDARD_VIEW_ROWS:
if self._view_checkboxes[vid].isChecked():
views.append(
DrawingView(
kind=vid,
name=vname,
direction=vdir,
up_vector=vup,
show_hidden_lines=self._hidden_line_checks[vid].isChecked(),
show_centerlines=self._centerline_checks[vid].isChecked(),
)
)
if not views:
for vid, vname, vdir, vup in _STANDARD_VIEW_ROWS:
views.append(
DrawingView(kind=vid, name=vname, direction=vdir, up_vector=vup)
)
# Resolve component name for the title.
comp = None
if source_kind == "component":
comp = self._project.get_component_by_id(source_id)
drawing = TechnicalDrawing(
name=f"Drawing of {comp.name if comp else 'Assembly'}",
source_kind=source_kind,
source_id=source_id,
views=views,
title=comp.name if comp else "Assembly Drawing",
part_number="",
material="",
revision="A",
auto_dimensions=self._auto_dim_check.isChecked(),
)
self._drawing = drawing
self._populate_from_drawing(drawing)
try:
result = generate_drawing(drawing, self._project, self._kernel)
self.set_render_result(result)
self.drawing_changed.emit()
except Exception as e:
self._status_label.setText(f"Generation failed: {e}")
def _on_export(self, fmt: str) -> None:
if self._drawing is None:
return
self._sync_views_to_drawing()
self.export_requested.emit(fmt, self._drawing)
def _on_add_note(self) -> None:
if self._drawing is None:
return
ann = DrawingAnnotation(kind="note", text="New note")
self._drawing.annotations.append(ann)
self._refresh_annotation_table()
def _on_save(self) -> None:
"""Save the current drawing."""
if self._drawing is None:
return
self._sync_views_to_drawing()
from pathlib import Path
from qtpy.QtWidgets import QFileDialog
path, _ = QFileDialog.getSaveFileName(
self, "Save Drawing", "", "Drawing Files (*.drawing.json)"
)
if not path:
return
try:
Path(path).write_text(self._drawing.model_dump_json(indent=2))
self._status_label.setText(f"Saved to {path}")
except Exception as e:
self._status_label.setText(f"Save failed: {e}")