- tech draw draft v2
This commit is contained in:
Generated
+15
-12
@@ -4,9 +4,12 @@
|
|||||||
<option name="autoReloadType" value="SELECTIVE" />
|
<option name="autoReloadType" value="SELECTIVE" />
|
||||||
</component>
|
</component>
|
||||||
<component name="ChangeListManager">
|
<component name="ChangeListManager">
|
||||||
<list default="true" id="8f0bafd6-58a0-4b20-aa2b-ddc3ba278873" name="Changes" comment="- tech draw draft">
|
<list default="true" id="8f0bafd6-58a0-4b20-aa2b-ddc3ba278873" name="Changes" comment="- tech draw draft v2">
|
||||||
|
<change beforePath="$PROJECT_DIR$/.idea/workspace.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/workspace.xml" afterDir="false" />
|
||||||
|
<change beforePath="$PROJECT_DIR$/src/fluency/io/project_io.py" beforeDir="false" afterPath="$PROJECT_DIR$/src/fluency/io/project_io.py" afterDir="false" />
|
||||||
<change beforePath="$PROJECT_DIR$/src/fluency/models/data_model.py" beforeDir="false" afterPath="$PROJECT_DIR$/src/fluency/models/data_model.py" afterDir="false" />
|
<change beforePath="$PROJECT_DIR$/src/fluency/models/data_model.py" beforeDir="false" afterPath="$PROJECT_DIR$/src/fluency/models/data_model.py" afterDir="false" />
|
||||||
<change beforePath="$PROJECT_DIR$/src/fluency/technical_drawing.py" beforeDir="false" afterPath="$PROJECT_DIR$/src/fluency/technical_drawing.py" afterDir="false" />
|
<change beforePath="$PROJECT_DIR$/src/fluency/technical_drawing.py" beforeDir="false" afterPath="$PROJECT_DIR$/src/fluency/technical_drawing.py" afterDir="false" />
|
||||||
|
<change beforePath="$PROJECT_DIR$/src/fluency/ui/main_window.py" beforeDir="false" afterPath="$PROJECT_DIR$/src/fluency/ui/main_window.py" afterDir="false" />
|
||||||
<change beforePath="$PROJECT_DIR$/src/fluency/ui/technical_drawing_widget.py" beforeDir="false" afterPath="$PROJECT_DIR$/src/fluency/ui/technical_drawing_widget.py" afterDir="false" />
|
<change beforePath="$PROJECT_DIR$/src/fluency/ui/technical_drawing_widget.py" beforeDir="false" afterPath="$PROJECT_DIR$/src/fluency/ui/technical_drawing_widget.py" afterDir="false" />
|
||||||
</list>
|
</list>
|
||||||
<option name="SHOW_DIALOG" value="false" />
|
<option name="SHOW_DIALOG" value="false" />
|
||||||
@@ -119,14 +122,6 @@
|
|||||||
<option name="presentableId" value="Default" />
|
<option name="presentableId" value="Default" />
|
||||||
<updated>1703867682707</updated>
|
<updated>1703867682707</updated>
|
||||||
</task>
|
</task>
|
||||||
<task id="LOCAL-00002" summary="- Basic oop sketch widget implement">
|
|
||||||
<option name="closed" value="true" />
|
|
||||||
<created>1729958532384</created>
|
|
||||||
<option name="number" value="00002" />
|
|
||||||
<option name="presentableId" value="LOCAL-00002" />
|
|
||||||
<option name="project" value="LOCAL" />
|
|
||||||
<updated>1729958532384</updated>
|
|
||||||
</task>
|
|
||||||
<task id="LOCAL-00003" summary="- Sketch projection partly works again :)">
|
<task id="LOCAL-00003" summary="- Sketch projection partly works again :)">
|
||||||
<option name="closed" value="true" />
|
<option name="closed" value="true" />
|
||||||
<created>1735563255455</created>
|
<created>1735563255455</created>
|
||||||
@@ -511,7 +506,15 @@
|
|||||||
<option name="project" value="LOCAL" />
|
<option name="project" value="LOCAL" />
|
||||||
<updated>1786888483688</updated>
|
<updated>1786888483688</updated>
|
||||||
</task>
|
</task>
|
||||||
<option name="localTasksCounter" value="51" />
|
<task id="LOCAL-00051" summary="- tech draw draft v2">
|
||||||
|
<option name="closed" value="true" />
|
||||||
|
<created>1786910497589</created>
|
||||||
|
<option name="number" value="00051" />
|
||||||
|
<option name="presentableId" value="LOCAL-00051" />
|
||||||
|
<option name="project" value="LOCAL" />
|
||||||
|
<updated>1786910497589</updated>
|
||||||
|
</task>
|
||||||
|
<option name="localTasksCounter" value="52" />
|
||||||
<servers />
|
<servers />
|
||||||
</component>
|
</component>
|
||||||
<component name="TypeScriptGeneratedFilesManager">
|
<component name="TypeScriptGeneratedFilesManager">
|
||||||
@@ -532,7 +535,6 @@
|
|||||||
<ignored-roots>
|
<ignored-roots>
|
||||||
<path value="$PROJECT_DIR$/pythonProject" />
|
<path value="$PROJECT_DIR$/pythonProject" />
|
||||||
</ignored-roots>
|
</ignored-roots>
|
||||||
<MESSAGE value="- Drawing bodys depending on the selected compo - Cut working - Edit sketch working" />
|
|
||||||
<MESSAGE value="- delete sketch working - added mid point snap - added hovering line with distance" />
|
<MESSAGE value="- delete sketch working - added mid point snap - added hovering line with distance" />
|
||||||
<MESSAGE value="- Added new buttons and settings" />
|
<MESSAGE value="- Added new buttons and settings" />
|
||||||
<MESSAGE value="- Added construction lines switching - Moved callbacks into sketchwidget from main. - Changed reset on right click" />
|
<MESSAGE value="- Added construction lines switching - Moved callbacks into sketchwidget from main. - Changed reset on right click" />
|
||||||
@@ -557,6 +559,7 @@
|
|||||||
<MESSAGE value="- arc improvements, fillets, operations, bodys" />
|
<MESSAGE value="- arc improvements, fillets, operations, bodys" />
|
||||||
<MESSAGE value="- Operation highlighting, body highlighting" />
|
<MESSAGE value="- Operation highlighting, body highlighting" />
|
||||||
<MESSAGE value="- tech draw draft" />
|
<MESSAGE value="- tech draw draft" />
|
||||||
<option name="LAST_COMMIT_MESSAGE" value="- tech draw draft" />
|
<MESSAGE value="- tech draw draft v2" />
|
||||||
|
<option name="LAST_COMMIT_MESSAGE" value="- tech draw draft v2" />
|
||||||
</component>
|
</component>
|
||||||
</project>
|
</project>
|
||||||
@@ -43,9 +43,12 @@ from fluency.models.data_model import (
|
|||||||
Body,
|
Body,
|
||||||
Component,
|
Component,
|
||||||
Connector,
|
Connector,
|
||||||
|
DrawingAnnotation,
|
||||||
|
DrawingView,
|
||||||
Feature,
|
Feature,
|
||||||
Project,
|
Project,
|
||||||
Sketch,
|
Sketch,
|
||||||
|
TechnicalDrawing,
|
||||||
Workplane,
|
Workplane,
|
||||||
)
|
)
|
||||||
from fluency.geometry_occ.kernel import OCCGeometryObject, OCGeometryKernel
|
from fluency.geometry_occ.kernel import OCCGeometryObject, OCGeometryKernel
|
||||||
@@ -600,6 +603,122 @@ def _assembly_from_dict(data: Dict[str, Any]) -> Assembly:
|
|||||||
return asm
|
return asm
|
||||||
|
|
||||||
|
|
||||||
|
def _drawing_view_to_dict(v: DrawingView) -> Dict[str, Any]:
|
||||||
|
return {
|
||||||
|
"id": v.id,
|
||||||
|
"kind": v.kind,
|
||||||
|
"name": v.name,
|
||||||
|
"direction": _coerce_listlike(v.direction),
|
||||||
|
"up_vector": _coerce_listlike(v.up_vector),
|
||||||
|
"show_hidden_lines": bool(v.show_hidden_lines),
|
||||||
|
"show_centerlines": bool(v.show_centerlines),
|
||||||
|
"scale": _to_float(v.scale, 1.0),
|
||||||
|
"sheet_origin": _coerce_listlike(v.sheet_origin),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _drawing_view_from_dict(data: Dict[str, Any]) -> DrawingView:
|
||||||
|
view = DrawingView(
|
||||||
|
id=_saved_id(data),
|
||||||
|
kind=data.get("kind", "front"),
|
||||||
|
name=data.get("name"),
|
||||||
|
direction=_to_3tuple(data.get("direction")) or None,
|
||||||
|
up_vector=_to_3tuple(data.get("up_vector")) or None,
|
||||||
|
show_hidden_lines=bool(data.get("show_hidden_lines", False)),
|
||||||
|
show_centerlines=bool(data.get("show_centerlines", False)),
|
||||||
|
scale=_to_float(data.get("scale"), 1.0),
|
||||||
|
)
|
||||||
|
origin = data.get("sheet_origin")
|
||||||
|
if isinstance(origin, (list, tuple)) and len(origin) >= 2:
|
||||||
|
view.sheet_origin = (_to_float(origin[0]), _to_float(origin[1]))
|
||||||
|
return view
|
||||||
|
|
||||||
|
|
||||||
|
def _drawing_annotation_to_dict(a: DrawingAnnotation) -> Dict[str, Any]:
|
||||||
|
return {
|
||||||
|
"id": a.id,
|
||||||
|
"kind": a.kind,
|
||||||
|
"text": a.text,
|
||||||
|
"visible": bool(a.visible),
|
||||||
|
"references": list(a.references),
|
||||||
|
"sheet_position": _coerce_listlike(a.sheet_position),
|
||||||
|
"view_id": a.view_id,
|
||||||
|
"anchors": [list(pt) for pt in a.anchors],
|
||||||
|
"dimension_kind": a.dimension_kind,
|
||||||
|
"direction": _coerce_listlike(a.direction),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _drawing_annotation_from_dict(data: Dict[str, Any]) -> DrawingAnnotation:
|
||||||
|
ann = DrawingAnnotation(
|
||||||
|
id=_saved_id(data),
|
||||||
|
kind=data.get("kind", "dimension"),
|
||||||
|
text=data.get("text"),
|
||||||
|
visible=bool(data.get("visible", True)),
|
||||||
|
references=list(data.get("references") or []),
|
||||||
|
view_id=data.get("view_id", ""),
|
||||||
|
dimension_kind=data.get("dimension_kind", ""),
|
||||||
|
)
|
||||||
|
pos = data.get("sheet_position")
|
||||||
|
if isinstance(pos, (list, tuple)) and len(pos) >= 2:
|
||||||
|
ann.sheet_position = (_to_float(pos[0]), _to_float(pos[1]))
|
||||||
|
anchors = data.get("anchors") or []
|
||||||
|
for pt in anchors:
|
||||||
|
if isinstance(pt, (list, tuple)) and len(pt) >= 2:
|
||||||
|
ann.anchors.append((_to_float(pt[0]), _to_float(pt[1])))
|
||||||
|
direction = data.get("direction")
|
||||||
|
if isinstance(direction, (list, tuple)) and len(direction) >= 2:
|
||||||
|
ann.direction = (_to_float(direction[0]), _to_float(direction[1]))
|
||||||
|
return ann
|
||||||
|
|
||||||
|
|
||||||
|
def _technical_drawing_to_dict(d: TechnicalDrawing) -> Dict[str, Any]:
|
||||||
|
return {
|
||||||
|
"id": d.id,
|
||||||
|
"name": d.name,
|
||||||
|
"source_kind": d.source_kind,
|
||||||
|
"source_id": d.source_id,
|
||||||
|
"views": [_drawing_view_to_dict(v) for v in d.views],
|
||||||
|
"annotations": [_drawing_annotation_to_dict(a) for a in d.annotations],
|
||||||
|
"title": d.title,
|
||||||
|
"part_number": d.part_number,
|
||||||
|
"material": d.material,
|
||||||
|
"revision": d.revision,
|
||||||
|
"notes": d.notes,
|
||||||
|
"sheet_size": d.sheet_size,
|
||||||
|
"units": d.units,
|
||||||
|
"auto_dimensions": bool(d.auto_dimensions),
|
||||||
|
"auto_views": bool(d.auto_views),
|
||||||
|
"created_at": d.created_at.isoformat() if d.created_at else None,
|
||||||
|
"modified_at": d.modified_at.isoformat() if d.modified_at else None,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _technical_drawing_from_dict(data: Dict[str, Any]) -> TechnicalDrawing:
|
||||||
|
drawing = TechnicalDrawing(
|
||||||
|
id=_saved_id(data),
|
||||||
|
name=data.get("name", "Untitled Drawing"),
|
||||||
|
source_kind=data.get("source_kind", "component"),
|
||||||
|
source_id=data.get("source_id", ""),
|
||||||
|
title=data.get("title", ""),
|
||||||
|
part_number=data.get("part_number", ""),
|
||||||
|
material=data.get("material", ""),
|
||||||
|
revision=data.get("revision", ""),
|
||||||
|
notes=data.get("notes", ""),
|
||||||
|
sheet_size=data.get("sheet_size", "A3"),
|
||||||
|
units=data.get("units", "mm"),
|
||||||
|
auto_dimensions=bool(data.get("auto_dimensions", False)),
|
||||||
|
auto_views=bool(data.get("auto_views", True)),
|
||||||
|
)
|
||||||
|
drawing.created_at = _parse_iso(data.get("created_at"))
|
||||||
|
drawing.modified_at = _parse_iso(data.get("modified_at"))
|
||||||
|
for v_data in data.get("views") or []:
|
||||||
|
drawing.views.append(_drawing_view_from_dict(v_data))
|
||||||
|
for a_data in data.get("annotations") or []:
|
||||||
|
drawing.annotations.append(_drawing_annotation_from_dict(a_data))
|
||||||
|
return drawing
|
||||||
|
|
||||||
|
|
||||||
def _project_to_dict(
|
def _project_to_dict(
|
||||||
project: Project,
|
project: Project,
|
||||||
view_state: Optional[Dict[str, Any]] = None,
|
view_state: Optional[Dict[str, Any]] = None,
|
||||||
@@ -612,6 +731,7 @@ def _project_to_dict(
|
|||||||
"active_assembly": project.active_assembly,
|
"active_assembly": project.active_assembly,
|
||||||
"components": {cid: _component_to_dict(c) for cid, c in project.components.items()},
|
"components": {cid: _component_to_dict(c) for cid, c in project.components.items()},
|
||||||
"assemblies": {aid: _assembly_to_dict(a) for aid, a in project.assemblies.items()},
|
"assemblies": {aid: _assembly_to_dict(a) for aid, a in project.assemblies.items()},
|
||||||
|
"drawings": [_technical_drawing_to_dict(d) for d in project.drawings],
|
||||||
"created_at": project.created_at.isoformat() if project.created_at else None,
|
"created_at": project.created_at.isoformat() if project.created_at else None,
|
||||||
"modified_at": project.modified_at.isoformat() if project.modified_at else None,
|
"modified_at": project.modified_at.isoformat() if project.modified_at else None,
|
||||||
"view_state": view_state or {},
|
"view_state": view_state or {},
|
||||||
@@ -886,6 +1006,12 @@ def load_project(filepath: str) -> Tuple[Project, Dict[str, Any]]:
|
|||||||
for aid, a_data in (manifest.get("assemblies") or {}).items():
|
for aid, a_data in (manifest.get("assemblies") or {}).items():
|
||||||
project.assemblies[aid] = _assembly_from_dict(a_data)
|
project.assemblies[aid] = _assembly_from_dict(a_data)
|
||||||
|
|
||||||
|
for d_data in manifest.get("drawings") or []:
|
||||||
|
try:
|
||||||
|
project.drawings.append(_technical_drawing_from_dict(d_data))
|
||||||
|
except Exception as exc:
|
||||||
|
logger.warning("Skipping corrupt drawing in archive: %s", exc)
|
||||||
|
|
||||||
# After all components are loaded, re-wire connector partner ids so
|
# After all components are loaded, re-wire connector partner ids so
|
||||||
# they point to the freshly-loaded AssemblyComponents. (The dict
|
# they point to the freshly-loaded AssemblyComponents. (The dict
|
||||||
# round-trip preserves the raw strings; we just make sure the partner
|
# round-trip preserves the raw strings; we just make sure the partner
|
||||||
|
|||||||
@@ -724,6 +724,10 @@ class Project:
|
|||||||
assemblies: Dict[str, Assembly] = field(default_factory=dict)
|
assemblies: Dict[str, Assembly] = field(default_factory=dict)
|
||||||
active_assembly: Optional[str] = None
|
active_assembly: Optional[str] = None
|
||||||
|
|
||||||
|
# Technical drawings keyed to their source component/assembly.
|
||||||
|
# Manual dimensions and view options added in the drawing workbench
|
||||||
|
# are persisted here so they survive save/load.
|
||||||
|
drawings: List["TechnicalDrawing"] = field(default_factory=list)
|
||||||
kernel: OCGeometryKernel = field(default_factory=OCGeometryKernel)
|
kernel: OCGeometryKernel = field(default_factory=OCGeometryKernel)
|
||||||
|
|
||||||
created_at: datetime = field(default_factory=datetime.now)
|
created_at: datetime = field(default_factory=datetime.now)
|
||||||
@@ -800,6 +804,21 @@ class Project:
|
|||||||
"""Look up a component by id across all project components."""
|
"""Look up a component by id across all project components."""
|
||||||
return self.components.get(component_id)
|
return self.components.get(component_id)
|
||||||
|
|
||||||
|
# ── Drawing helpers ──
|
||||||
|
|
||||||
|
def get_drawing_for(self, source_kind: str, source_id: str) -> Optional["TechnicalDrawing"]:
|
||||||
|
"""Return the drawing bound to *(source_kind, source_id)*, if any."""
|
||||||
|
for drawing in self.drawings:
|
||||||
|
if drawing.source_kind == source_kind and drawing.source_id == source_id:
|
||||||
|
return drawing
|
||||||
|
return None
|
||||||
|
|
||||||
|
def add_drawing(self, drawing: "TechnicalDrawing") -> "TechnicalDrawing":
|
||||||
|
"""Register *drawing* with the project and return it."""
|
||||||
|
self.drawings.append(drawing)
|
||||||
|
self.modified_at = datetime.now()
|
||||||
|
return drawing
|
||||||
|
|
||||||
|
|
||||||
def export_step(self, filepath: str) -> bool:
|
def export_step(self, filepath: str) -> bool:
|
||||||
"""Export all visible bodies to STEP."""
|
"""Export all visible bodies to STEP."""
|
||||||
@@ -917,6 +936,23 @@ class DrawingAnnotation:
|
|||||||
# Associated view id (empty means global/note block).
|
# Associated view id (empty means global/note block).
|
||||||
view_id: str = ""
|
view_id: str = ""
|
||||||
|
|
||||||
|
# Manual-dimension geometry in view-plane model coordinates (model
|
||||||
|
# units in the view's projection plane). Populated for user-placed
|
||||||
|
# dimensions, empty for reference-based annotations:
|
||||||
|
# "length": (point_on_edge1, point_on_edge2) — closest points
|
||||||
|
# "diameter": (left, right) — antipodal points across the centre
|
||||||
|
# "angle": (vertex, arm1_point, arm2_point)
|
||||||
|
anchors: List[Tuple[float, float]] = field(default_factory=list)
|
||||||
|
|
||||||
|
# Sub-kind of the manual dimension: "length" | "diameter" | "angle".
|
||||||
|
# Empty for non-dimension annotations.
|
||||||
|
dimension_kind: str = ""
|
||||||
|
|
||||||
|
# Unit vector along the measured distance in view-plane coordinates
|
||||||
|
# (length dimensions only). The model→sheet transform is a uniform
|
||||||
|
# scale + translation, so the direction is valid in sheet space too.
|
||||||
|
direction: Optional[Tuple[float, float]] = None
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class TechnicalDrawing:
|
class TechnicalDrawing:
|
||||||
@@ -943,8 +979,9 @@ class TechnicalDrawing:
|
|||||||
sheet_size: str = "A3"
|
sheet_size: str = "A3"
|
||||||
units: str = "mm" # mm, in
|
units: str = "mm" # mm, in
|
||||||
|
|
||||||
# Auto-generation flags for future use.
|
# Auto-generation flags. Auto dimensions are opt-in: the drawing
|
||||||
auto_dimensions: bool = True
|
# workbench shows them only while the user has the toggle enabled.
|
||||||
|
auto_dimensions: bool = False
|
||||||
auto_views: bool = True
|
auto_views: bool = True
|
||||||
|
|
||||||
created_at: datetime = field(default_factory=datetime.now)
|
created_at: datetime = field(default_factory=datetime.now)
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ Exact public API:
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import math
|
import math
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass, field
|
||||||
from typing import Any, Dict, List, Optional, Sequence, Tuple
|
from typing import Any, Dict, List, Optional, Sequence, Tuple
|
||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
@@ -68,6 +68,9 @@ class DrawingPrimitive:
|
|||||||
center: Optional[Tuple[float, float]] = None
|
center: Optional[Tuple[float, float]] = None
|
||||||
radius: Optional[float] = None
|
radius: Optional[float] = None
|
||||||
dash_pattern: Tuple[float, ...] = ()
|
dash_pattern: Tuple[float, ...] = ()
|
||||||
|
# View this primitive belongs to (geometry primitives); used for
|
||||||
|
# hit-testing in the drawing workbench.
|
||||||
|
view_id: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
@dataclass(frozen=True)
|
||||||
@@ -80,6 +83,9 @@ class DrawingRenderResult:
|
|||||||
unresolved_annotation_ids: Tuple[str, ...]
|
unresolved_annotation_ids: Tuple[str, ...]
|
||||||
source_fingerprint: str
|
source_fingerprint: str
|
||||||
warnings: Tuple[str, ...]
|
warnings: Tuple[str, ...]
|
||||||
|
# Per-view model→sheet transform: view_id → (scale, offset_x, offset_y)
|
||||||
|
# with sheet(x, y) = (x*scale + offset_x, y*scale + offset_y).
|
||||||
|
view_transforms: Dict[str, Tuple[float, float, float]] = field(default_factory=dict)
|
||||||
|
|
||||||
|
|
||||||
# ── View presets ───────────────────────────────────────────────────────────
|
# ── View presets ───────────────────────────────────────────────────────────
|
||||||
@@ -319,6 +325,7 @@ def _assemble_view(
|
|||||||
view: DrawingView,
|
view: DrawingView,
|
||||||
slot: Optional[Tuple[float, float, float, float]] = None,
|
slot: Optional[Tuple[float, float, float, float]] = None,
|
||||||
scale_override: Optional[float] = None,
|
scale_override: Optional[float] = None,
|
||||||
|
transforms: Optional[Dict[str, Tuple[float, float, float]]] = None,
|
||||||
) -> Tuple[Tuple[DrawingPrimitive, ...], Tuple[DrawingCandidate, ...], Tuple[str, ...]]:
|
) -> Tuple[Tuple[DrawingPrimitive, ...], Tuple[DrawingCandidate, ...], Tuple[str, ...]]:
|
||||||
"""Fit projected edges into *slot* and emit primitives + candidates.
|
"""Fit projected edges into *slot* and emit primitives + candidates.
|
||||||
|
|
||||||
@@ -326,6 +333,8 @@ def _assemble_view(
|
|||||||
bottom-left, +y up). When omitted the projection is fitted to the
|
bottom-left, +y up). When omitted the projection is fitted to the
|
||||||
whole sheet. *scale_override* forces a specific model→sheet scale
|
whole sheet. *scale_override* forces a specific model→sheet scale
|
||||||
(used to keep all orthographic views at one shared scale).
|
(used to keep all orthographic views at one shared scale).
|
||||||
|
When *transforms* is given, the resolved
|
||||||
|
``(scale, offset_x, offset_y)`` is recorded under the view id.
|
||||||
"""
|
"""
|
||||||
warnings = list(warnings)
|
warnings = list(warnings)
|
||||||
primitives: List[DrawingPrimitive] = []
|
primitives: List[DrawingPrimitive] = []
|
||||||
@@ -366,6 +375,11 @@ def _assemble_view(
|
|||||||
offset_x = left + (avail_w - geom_w * scale) / 2.0 - min_x * scale
|
offset_x = left + (avail_w - geom_w * scale) / 2.0 - min_x * scale
|
||||||
offset_y = bottom + (avail_h - geom_h * scale) / 2.0 - min_y * scale
|
offset_y = bottom + (avail_h - geom_h * scale) / 2.0 - min_y * scale
|
||||||
|
|
||||||
|
# Use kind as view_id for readability (UUID is opaque to users).
|
||||||
|
view_id = view.kind if view.kind in _STANDARD_VIEWS else (view.name or view.id)
|
||||||
|
if transforms is not None:
|
||||||
|
transforms[view_id] = (scale, offset_x, offset_y)
|
||||||
|
|
||||||
def _to_sheet(x: float, y: float) -> Tuple[float, float]:
|
def _to_sheet(x: float, y: float) -> Tuple[float, float]:
|
||||||
return (x * scale + offset_x, y * scale + offset_y)
|
return (x * scale + offset_x, y * scale + offset_y)
|
||||||
|
|
||||||
@@ -383,6 +397,7 @@ def _assemble_view(
|
|||||||
style=style,
|
style=style,
|
||||||
center=_to_sheet(cx, cy),
|
center=_to_sheet(cx, cy),
|
||||||
radius=radius * scale,
|
radius=radius * scale,
|
||||||
|
view_id=view_id,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
@@ -391,12 +406,11 @@ def _assemble_view(
|
|||||||
kind="line",
|
kind="line",
|
||||||
points=(_to_sheet(*p1), _to_sheet(*p2)),
|
points=(_to_sheet(*p1), _to_sheet(*p2)),
|
||||||
style=style,
|
style=style,
|
||||||
|
view_id=view_id,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
# ── Dimension candidate extraction ──────────────────────────────
|
# ── Dimension candidate extraction ──────────────────────────────
|
||||||
# Use kind as view_id for readability (UUID is opaque to users).
|
|
||||||
view_id = view.kind if view.kind in _STANDARD_VIEWS else (view.name or view.id)
|
|
||||||
|
|
||||||
# 1. Overall extents (bounding-box width/height), anchored at real
|
# 1. Overall extents (bounding-box width/height), anchored at real
|
||||||
# bbox corners so extension lines can start at feature extremes.
|
# bbox corners so extension lines can start at feature extremes.
|
||||||
@@ -1011,8 +1025,11 @@ def _generate_dimension_primitives(
|
|||||||
|
|
||||||
Linear/extent candidates become extension lines + an offset dimension
|
Linear/extent candidates become extension lines + an offset dimension
|
||||||
line with arrowheads + centred text (dimension line broken for the
|
line with arrowheads + centred text (dimension line broken for the
|
||||||
label). Diameters become a 45° leader with a horizontal tail. Angles
|
label). Anchors on one feature line yield a dimension line
|
||||||
become a small vertex arc + text.
|
perpendicular to the measurement direction; anchors that are the
|
||||||
|
closest points of two parallel edges (distance dimensions) yield a
|
||||||
|
dimension line parallel to it. Diameters become a 45° leader with a
|
||||||
|
horizontal tail. Angles become a small vertex arc + text.
|
||||||
"""
|
"""
|
||||||
# Keep dimension lines inside the sheet (with a small margin).
|
# Keep dimension lines inside the sheet (with a small margin).
|
||||||
_sheet_min_x, _sheet_max_x = 6.0, _A3_WIDTH_MM - 6.0
|
_sheet_min_x, _sheet_max_x = 6.0, _A3_WIDTH_MM - 6.0
|
||||||
@@ -1084,13 +1101,21 @@ def _generate_dimension_primitives(
|
|||||||
d = (vx / vl, vy / vl) if vl > 1e-9 else (1.0, 0.0)
|
d = (vx / vl, vy / vl) if vl > 1e-9 else (1.0, 0.0)
|
||||||
|
|
||||||
mx, my = (p1[0] + p2[0]) / 2.0, (p1[1] + p2[1]) / 2.0
|
mx, my = (p1[0] + p2[0]) / 2.0, (p1[1] + p2[1]) / 2.0
|
||||||
# Half the anchor spread along the measurement direction: the
|
# Decompose the anchor pair: separation along the measurement
|
||||||
# dimension line lands this far (plus the offset) beyond the
|
# direction d and perpendicular to it. cross ≈ 0 means the
|
||||||
# far feature line.
|
# anchors are the closest points on two parallel edges (a
|
||||||
spread = abs((p2[0] - p1[0]) * d[0] + (p2[1] - p1[1]) * d[1]) / 2.0
|
# distance dimension); otherwise they sit on one feature line.
|
||||||
|
wdx, wdy = p2[0] - p1[0], p2[1] - p1[1]
|
||||||
|
along = wdx * d[0] + wdy * d[1]
|
||||||
|
cross = abs(wdx * d[1] - wdy * d[0])
|
||||||
|
|
||||||
|
if cross > 0.5:
|
||||||
|
# ── Feature-line case ─────────────────────────────────
|
||||||
|
# The dimension line is perpendicular to d, offset away
|
||||||
|
# from the view centre (half the anchor spread plus the
|
||||||
|
# standard offset, so it lands beyond the far feature).
|
||||||
|
spread = abs(along) / 2.0
|
||||||
offset = spread + _DIM_OFFSET_MM
|
offset = spread + _DIM_OFFSET_MM
|
||||||
# Place the dimension line on the side of the anchor pair away
|
|
||||||
# from the view centre (outside the feature).
|
|
||||||
sign = (
|
sign = (
|
||||||
-1.0
|
-1.0
|
||||||
if (view_center[0] - mx) * d[0] + (view_center[1] - my) * d[1] > 0
|
if (view_center[0] - mx) * d[0] + (view_center[1] - my) * d[1] > 0
|
||||||
@@ -1143,6 +1168,62 @@ def _generate_dimension_primitives(
|
|||||||
e1 = (p1[0] + d[0] * t1, p1[1] + d[1] * t1)
|
e1 = (p1[0] + d[0] * t1, p1[1] + d[1] * t1)
|
||||||
e2 = (p2[0] + d[0] * t2, p2[1] + d[1] * t2)
|
e2 = (p2[0] + d[0] * t2, p2[1] + d[1] * t2)
|
||||||
half = math.hypot(e2[0] - e1[0], e2[1] - e1[1]) / 2.0
|
half = math.hypot(e2[0] - e1[0], e2[1] - e1[1]) / 2.0
|
||||||
|
ext_dir = (sign * d[0], sign * d[1])
|
||||||
|
else:
|
||||||
|
# ── Inter-edge distance case ──────────────────────────
|
||||||
|
# The measured distance runs along d; the dimension line
|
||||||
|
# is parallel to d, offset from the picked line on the
|
||||||
|
# side away from the view centre.
|
||||||
|
n = (-d[1], d[0])
|
||||||
|
sign = (
|
||||||
|
-1.0
|
||||||
|
if (view_center[0] - mx) * n[0] + (view_center[1] - my) * n[1] > 0
|
||||||
|
else 1.0
|
||||||
|
)
|
||||||
|
cdim = _clamp_sheet(
|
||||||
|
(
|
||||||
|
mx + sign * n[0] * _DIM_OFFSET_MM,
|
||||||
|
my + sign * n[1] * _DIM_OFFSET_MM,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
half = abs(along) / 2.0
|
||||||
|
u_dir = (d[0], d[1]) if along >= 0 else (-d[0], -d[1])
|
||||||
|
|
||||||
|
# Progressive stacking (same rules, pushing along n).
|
||||||
|
for _ in range(8):
|
||||||
|
if half <= 0.5:
|
||||||
|
break
|
||||||
|
e1 = (cdim[0] - u_dir[0] * half, cdim[1] - u_dir[1] * half)
|
||||||
|
e2 = (cdim[0] + u_dir[0] * half, cdim[1] + u_dir[1] * half)
|
||||||
|
pushed = False
|
||||||
|
for (ux0, uy0, qx0, qy0, tmin0, tmax0) in placed_dim_lines:
|
||||||
|
if abs(u_dir[0] * ux0 + u_dir[1] * uy0) < 0.98:
|
||||||
|
continue # not parallel
|
||||||
|
ta1 = (e1[0] - qx0) * ux0 + (e1[1] - qy0) * uy0
|
||||||
|
ta2 = (e2[0] - qx0) * ux0 + (e2[1] - qy0) * uy0
|
||||||
|
if max(ta1, ta2) < tmin0 or min(ta1, ta2) > tmax0:
|
||||||
|
continue # spans do not overlap
|
||||||
|
sep = (cdim[0] - qx0) * n[0] + (cdim[1] - qy0) * n[1]
|
||||||
|
if abs(sep) < _DIM_STANDOFF_MM:
|
||||||
|
prev = cdim
|
||||||
|
cdim = (
|
||||||
|
cdim[0] + sign * n[0] * (_DIM_STANDOFF_MM - abs(sep)),
|
||||||
|
cdim[1] + sign * n[1] * (_DIM_STANDOFF_MM - abs(sep)),
|
||||||
|
)
|
||||||
|
cdim = _clamp_sheet(cdim)
|
||||||
|
if cdim == prev:
|
||||||
|
# Pushed against the sheet edge — stop stacking.
|
||||||
|
break
|
||||||
|
pushed = True
|
||||||
|
break
|
||||||
|
if not pushed:
|
||||||
|
break
|
||||||
|
|
||||||
|
e1 = (cdim[0] - u_dir[0] * half, cdim[1] - u_dir[1] * half)
|
||||||
|
e2 = (cdim[0] + u_dir[0] * half, cdim[1] + u_dir[1] * half)
|
||||||
|
ext_dir = (sign * n[0], sign * n[1])
|
||||||
|
|
||||||
|
# ── Shared rendering of the positioned dimension line ─────
|
||||||
if half > 0.5:
|
if half > 0.5:
|
||||||
u = ((e2[0] - e1[0]) / (2.0 * half), (e2[1] - e1[1]) / (2.0 * half))
|
u = ((e2[0] - e1[0]) / (2.0 * half), (e2[1] - e1[1]) / (2.0 * half))
|
||||||
s1 = (e1[0] - cdim[0]) * u[0] + (e1[1] - cdim[1]) * u[1]
|
s1 = (e1[0] - cdim[0]) * u[0] + (e1[1] - cdim[1]) * u[1]
|
||||||
@@ -1151,10 +1232,10 @@ def _generate_dimension_primitives(
|
|||||||
(u[0], u[1], cdim[0], cdim[1], min(s1, s2), max(s1, s2))
|
(u[0], u[1], cdim[0], cdim[1], min(s1, s2), max(s1, s2))
|
||||||
)
|
)
|
||||||
# Extension lines: anchor → 2mm past the dimension line.
|
# Extension lines: anchor → 2mm past the dimension line.
|
||||||
for anchor, t in ((p1, t1), (p2, t2)):
|
for anchor, foot in ((p1, e1), (p2, e2)):
|
||||||
end = (
|
end = (
|
||||||
anchor[0] + d[0] * (t + sign * _DIM_EXT_OVERSHOOT_MM),
|
foot[0] + ext_dir[0] * _DIM_EXT_OVERSHOOT_MM,
|
||||||
anchor[1] + d[1] * (t + sign * _DIM_EXT_OVERSHOOT_MM),
|
foot[1] + ext_dir[1] * _DIM_EXT_OVERSHOOT_MM,
|
||||||
)
|
)
|
||||||
prims.append(
|
prims.append(
|
||||||
DrawingPrimitive(
|
DrawingPrimitive(
|
||||||
@@ -1279,6 +1360,108 @@ def _generate_dimension_primitives(
|
|||||||
return prims
|
return prims
|
||||||
|
|
||||||
|
|
||||||
|
_MANUAL_DIMENSION_KINDS = ("length", "diameter", "angle")
|
||||||
|
|
||||||
|
|
||||||
|
def build_manual_candidates(
|
||||||
|
drawing: TechnicalDrawing,
|
||||||
|
view_transforms: Dict[str, Tuple[float, float, float]],
|
||||||
|
) -> Tuple[List[DrawingCandidate], List[str], List[str]]:
|
||||||
|
"""Convert user-placed annotations into renderable dimension candidates.
|
||||||
|
|
||||||
|
Manual annotations store their feature geometry in view-plane model
|
||||||
|
coordinates (see ``DrawingAnnotation.anchors``); here it is re-projected
|
||||||
|
through the current view transforms so the dimensions re-lay out
|
||||||
|
correctly whenever views, slots, or the shared scale change. Returns
|
||||||
|
``(candidates, resolved_ids, unresolved_ids)``.
|
||||||
|
"""
|
||||||
|
candidates: List[DrawingCandidate] = []
|
||||||
|
resolved: List[str] = []
|
||||||
|
unresolved: List[str] = []
|
||||||
|
|
||||||
|
for ann in drawing.annotations:
|
||||||
|
if not ann.visible or ann.dimension_kind not in _MANUAL_DIMENSION_KINDS:
|
||||||
|
continue
|
||||||
|
if len(ann.anchors) < 2:
|
||||||
|
unresolved.append(ann.id)
|
||||||
|
continue
|
||||||
|
transform = view_transforms.get(ann.view_id)
|
||||||
|
if transform is None:
|
||||||
|
unresolved.append(ann.id)
|
||||||
|
continue
|
||||||
|
scale, offset_x, offset_y = transform
|
||||||
|
|
||||||
|
def to_sheet(pt: Tuple[float, float]) -> Tuple[float, float]:
|
||||||
|
return (pt[0] * scale + offset_x, pt[1] * scale + offset_y)
|
||||||
|
|
||||||
|
a0, a1 = ann.anchors[0], ann.anchors[1]
|
||||||
|
if ann.dimension_kind == "angle":
|
||||||
|
if len(ann.anchors) < 3:
|
||||||
|
unresolved.append(ann.id)
|
||||||
|
continue
|
||||||
|
vertex, arm1, arm2 = ann.anchors
|
||||||
|
v1x, v1y = arm1[0] - vertex[0], arm1[1] - vertex[1]
|
||||||
|
v2x, v2y = arm2[0] - vertex[0], arm2[1] - vertex[1]
|
||||||
|
m1 = math.hypot(v1x, v1y)
|
||||||
|
m2 = math.hypot(v2x, v2y)
|
||||||
|
if m1 < 1e-9 or m2 < 1e-9:
|
||||||
|
unresolved.append(ann.id)
|
||||||
|
continue
|
||||||
|
cos_a = (v1x * v2x + v1y * v2y) / (m1 * m2)
|
||||||
|
value = math.degrees(math.acos(max(-1.0, min(1.0, cos_a))))
|
||||||
|
if value < 0.5 or value > 179.5:
|
||||||
|
unresolved.append(ann.id)
|
||||||
|
continue
|
||||||
|
candidates.append(
|
||||||
|
DrawingCandidate(
|
||||||
|
key=f"manual:{ann.id}",
|
||||||
|
view_id=ann.view_id,
|
||||||
|
kind="angle",
|
||||||
|
references=(ann.id,),
|
||||||
|
value=value,
|
||||||
|
anchor_points=(to_sheet(vertex), to_sheet(arm1), to_sheet(arm2)),
|
||||||
|
label=f"{value:.{_DISPLAY_PRECISION}f}°",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
resolved.append(ann.id)
|
||||||
|
continue
|
||||||
|
|
||||||
|
value = math.dist(a0, a1)
|
||||||
|
if value < 1e-6:
|
||||||
|
unresolved.append(ann.id)
|
||||||
|
continue
|
||||||
|
if ann.dimension_kind == "length":
|
||||||
|
if ann.direction:
|
||||||
|
dx, dy = float(ann.direction[0]), float(ann.direction[1])
|
||||||
|
if math.hypot(dx, dy) < 1e-9:
|
||||||
|
dx, dy = a1[0] - a0[0], a1[1] - a0[1]
|
||||||
|
else:
|
||||||
|
dx, dy = a1[0] - a0[0], a1[1] - a0[1]
|
||||||
|
# value >= 1e-6 guarantees the fallback vector is non-zero.
|
||||||
|
mag = math.hypot(dx, dy)
|
||||||
|
direction: Tuple[float, float] = (dx / mag, dy / mag)
|
||||||
|
label = f"{value:.{_DISPLAY_PRECISION}f}"
|
||||||
|
else: # diameter
|
||||||
|
direction = ()
|
||||||
|
label = f"Ø{value:.{_DISPLAY_PRECISION}f}"
|
||||||
|
|
||||||
|
candidates.append(
|
||||||
|
DrawingCandidate(
|
||||||
|
key=f"manual:{ann.id}",
|
||||||
|
view_id=ann.view_id,
|
||||||
|
kind=ann.dimension_kind,
|
||||||
|
references=(ann.id,),
|
||||||
|
value=value,
|
||||||
|
anchor_points=(to_sheet(a0), to_sheet(a1)),
|
||||||
|
label=label,
|
||||||
|
direction=direction,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
resolved.append(ann.id)
|
||||||
|
|
||||||
|
return candidates, resolved, unresolved
|
||||||
|
|
||||||
|
|
||||||
def generate_drawing(
|
def generate_drawing(
|
||||||
drawing: TechnicalDrawing,
|
drawing: TechnicalDrawing,
|
||||||
project: Project,
|
project: Project,
|
||||||
@@ -1335,6 +1518,9 @@ def generate_drawing(
|
|||||||
if v.kind in _STANDARD_VIEWS and v.kind != "isometric"
|
if v.kind in _STANDARD_VIEWS and v.kind != "isometric"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Per-view model→sheet transforms, filled in by _assemble_view.
|
||||||
|
transforms: Dict[str, Tuple[float, float, float]] = {}
|
||||||
|
|
||||||
for view in drawing.views:
|
for view in drawing.views:
|
||||||
vid = _vid_of(view)
|
vid = _vid_of(view)
|
||||||
slot = view_slots.get(vid)
|
slot = view_slots.get(vid)
|
||||||
@@ -1343,14 +1529,25 @@ def generate_drawing(
|
|||||||
common_scale * view.scale if vid in ortho_vids and common_scale else None
|
common_scale * view.scale if vid in ortho_vids and common_scale else None
|
||||||
)
|
)
|
||||||
prims, cands, vwarns = _assemble_view(
|
prims, cands, vwarns = _assemble_view(
|
||||||
edges, view_warnings, view, slot, scale_override
|
edges, view_warnings, view, slot, scale_override, transforms
|
||||||
)
|
)
|
||||||
all_primitives.extend(prims)
|
all_primitives.extend(prims)
|
||||||
all_candidates.extend(cands)
|
all_candidates.extend(cands)
|
||||||
warnings.extend(vwarns)
|
warnings.extend(vwarns)
|
||||||
|
|
||||||
# Auto-place dimensions from candidates (isometric views are never
|
# User-placed dimensions: convert the model-space annotations into
|
||||||
# dimensioned on real drawings).
|
# sheet-space candidates so they re-lay out with the current views.
|
||||||
|
manual_cands, manual_resolved, manual_unresolved = build_manual_candidates(
|
||||||
|
drawing, transforms
|
||||||
|
)
|
||||||
|
manual_ids = {c.references[0] for c in manual_cands if c.references}
|
||||||
|
resolved_ids: List[str] = list(manual_resolved)
|
||||||
|
unresolved_ids: List[str] = list(manual_unresolved)
|
||||||
|
|
||||||
|
# Place dimensions per view: the auto selection (only while the user
|
||||||
|
# has auto dimensions enabled) plus the manual dimensions, so the
|
||||||
|
# stacking/overlap logic treats them alike. Isometric views are never
|
||||||
|
# dimensioned on real drawings.
|
||||||
for view in drawing.views:
|
for view in drawing.views:
|
||||||
if view.kind == "isometric":
|
if view.kind == "isometric":
|
||||||
continue
|
continue
|
||||||
@@ -1361,17 +1558,21 @@ def generate_drawing(
|
|||||||
if slot
|
if slot
|
||||||
else (_A3_WIDTH_MM / 2.0, _A3_HEIGHT_MM / 2.0)
|
else (_A3_WIDTH_MM / 2.0, _A3_HEIGHT_MM / 2.0)
|
||||||
)
|
)
|
||||||
|
selected: List[DrawingCandidate] = []
|
||||||
|
if drawing.auto_dimensions:
|
||||||
selected = _select_dimensions_for_placement(all_candidates, vid)
|
selected = _select_dimensions_for_placement(all_candidates, vid)
|
||||||
|
selected.extend(c for c in manual_cands if c.view_id == vid)
|
||||||
all_primitives.extend(_generate_dimension_primitives(selected, center))
|
all_primitives.extend(_generate_dimension_primitives(selected, center))
|
||||||
|
|
||||||
# Match annotations to candidates.
|
# Match legacy (reference-based) annotations to candidates. Manual
|
||||||
resolved_ids: List[str] = []
|
# dimensions were already rendered above as full dimension primitives.
|
||||||
unresolved_ids: List[str] = []
|
|
||||||
candidate_by_key: Dict[str, DrawingCandidate] = {c.key: c for c in all_candidates}
|
candidate_by_key: Dict[str, DrawingCandidate] = {c.key: c for c in all_candidates}
|
||||||
|
|
||||||
for ann in drawing.annotations:
|
for ann in drawing.annotations:
|
||||||
if not ann.visible:
|
if not ann.visible:
|
||||||
continue
|
continue
|
||||||
|
if ann.id in manual_ids:
|
||||||
|
continue
|
||||||
if ann.kind == "note":
|
if ann.kind == "note":
|
||||||
resolved_ids.append(ann.id)
|
resolved_ids.append(ann.id)
|
||||||
continue
|
continue
|
||||||
@@ -1405,6 +1606,7 @@ def generate_drawing(
|
|||||||
drawing.source_kind, drawing.source_id
|
drawing.source_kind, drawing.source_id
|
||||||
),
|
),
|
||||||
warnings=tuple(warnings),
|
warnings=tuple(warnings),
|
||||||
|
view_transforms=transforms,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1917,7 +1917,9 @@ class MainWindow(QMainWindow):
|
|||||||
self._drawing_tab = TechnicalDrawingWidget()
|
self._drawing_tab = TechnicalDrawingWidget()
|
||||||
self._ui.InputTab.addTab(self._drawing_tab, "Drawing")
|
self._ui.InputTab.addTab(self._drawing_tab, "Drawing")
|
||||||
self._drawing_tab.set_project(self._project, self._kernel)
|
self._drawing_tab.set_project(self._project, self._kernel)
|
||||||
self._refresh_drawing_source_catalog()
|
# Dimension tools and view toggles in the workbench mutate the
|
||||||
|
# project's drawings — treat that as an unsaved change.
|
||||||
|
self._drawing_tab.drawing_changed.connect(self._mark_dirty)
|
||||||
|
|
||||||
# Component buttons (dynamically generated per component, not in UI).
|
# Component buttons (dynamically generated per component, not in UI).
|
||||||
# Wrapped in a QScrollArea so many components can scroll horizontally.
|
# Wrapped in a QScrollArea so many components can scroll horizontally.
|
||||||
@@ -2522,7 +2524,6 @@ class MainWindow(QMainWindow):
|
|||||||
logger.info(f"Created component: {comp.name}")
|
logger.info(f"Created component: {comp.name}")
|
||||||
# No sketch in the fresh component — drop the world triad.
|
# No sketch in the fresh component — drop the world triad.
|
||||||
self._sync_sketch_gizmo()
|
self._sync_sketch_gizmo()
|
||||||
self._refresh_drawing_source_catalog()
|
|
||||||
|
|
||||||
def _delete_component(self):
|
def _delete_component(self):
|
||||||
idx = self._get_active_component_index()
|
idx = self._get_active_component_index()
|
||||||
@@ -2541,7 +2542,6 @@ class MainWindow(QMainWindow):
|
|||||||
|
|
||||||
self._refresh_lists()
|
self._refresh_lists()
|
||||||
logger.info("Deleted component")
|
logger.info("Deleted component")
|
||||||
self._refresh_drawing_source_catalog()
|
|
||||||
|
|
||||||
def _on_component_button_clicked(self):
|
def _on_component_button_clicked(self):
|
||||||
idx = self._get_active_component_index()
|
idx = self._get_active_component_index()
|
||||||
@@ -2551,7 +2551,6 @@ class MainWindow(QMainWindow):
|
|||||||
self._assembly_view_active = False
|
self._assembly_view_active = False
|
||||||
self._refresh_lists()
|
self._refresh_lists()
|
||||||
self._redraw_bodies()
|
self._redraw_bodies()
|
||||||
self._refresh_drawing_source_catalog()
|
|
||||||
# Propagate the new selection to the drawing tab.
|
# Propagate the new selection to the drawing tab.
|
||||||
if self._current_component:
|
if self._current_component:
|
||||||
try:
|
try:
|
||||||
@@ -2587,14 +2586,6 @@ class MainWindow(QMainWindow):
|
|||||||
self._body_list.addItem(item)
|
self._body_list.addItem(item)
|
||||||
|
|
||||||
self._refresh_operations_list()
|
self._refresh_operations_list()
|
||||||
def _refresh_drawing_source_catalog(self) -> None:
|
|
||||||
"""Update the drawing tab's list of available components/assemblies."""
|
|
||||||
component_ids = list(self._project.components.keys())
|
|
||||||
assembly_ids = list(self._project.assemblies.keys()) if self._project.assemblies else []
|
|
||||||
try:
|
|
||||||
self._drawing_tab.set_source_catalog(component_ids, assembly_ids)
|
|
||||||
except Exception as e:
|
|
||||||
logger.warning(f"Failed to refresh drawing source catalog: {e}")
|
|
||||||
|
|
||||||
# ── Body operations list (per-body feature history) ───────────────────
|
# ── Body operations list (per-body feature history) ───────────────────
|
||||||
|
|
||||||
@@ -7331,10 +7322,9 @@ class MainWindow(QMainWindow):
|
|||||||
|
|
||||||
self._create_initial_component()
|
self._create_initial_component()
|
||||||
|
|
||||||
# Re-point the drawing tab at the new project — its combo and
|
# Re-point the drawing tab at the new project — its project
|
||||||
# project reference still belong to the previous project.
|
# reference still belongs to the previous project.
|
||||||
self._drawing_tab.set_project(self._project, self._kernel)
|
self._drawing_tab.set_project(self._project, self._kernel)
|
||||||
self._refresh_drawing_source_catalog()
|
|
||||||
if self._current_component:
|
if self._current_component:
|
||||||
self._drawing_tab.set_active_component(self._current_component)
|
self._drawing_tab.set_active_component(self._current_component)
|
||||||
finally:
|
finally:
|
||||||
@@ -7684,7 +7674,6 @@ class MainWindow(QMainWindow):
|
|||||||
# its source selection — it still holds the references captured
|
# its source selection — it still holds the references captured
|
||||||
# at setup, which made Generate draw from the previous project.
|
# at setup, which made Generate draw from the previous project.
|
||||||
self._drawing_tab.set_project(self._project, self._kernel)
|
self._drawing_tab.set_project(self._project, self._kernel)
|
||||||
self._refresh_drawing_source_catalog()
|
|
||||||
if self._current_component:
|
if self._current_component:
|
||||||
self._drawing_tab.set_active_component(self._current_component)
|
self._drawing_tab.set_active_component(self._current_component)
|
||||||
finally:
|
finally:
|
||||||
@@ -7902,52 +7891,20 @@ class MainWindow(QMainWindow):
|
|||||||
self._render_tab.set_assembly(assembly_parts, camera=viewport_camera)
|
self._render_tab.set_assembly(assembly_parts, camera=viewport_camera)
|
||||||
|
|
||||||
def _load_drawing_tab_source(self) -> None:
|
def _load_drawing_tab_source(self) -> None:
|
||||||
"""Auto-populate the drawing tab with the current component or assembly."""
|
"""Point the drawing tab at the current component (or first assembly).
|
||||||
from fluency.models.data_model import TechnicalDrawing, DrawingView
|
|
||||||
from fluency.technical_drawing import generate_drawing, _STANDARD_VIEWS
|
|
||||||
|
|
||||||
source_kind = "component"
|
|
||||||
source_id = ""
|
|
||||||
|
|
||||||
|
The drawing itself is owned by the project per source, so the
|
||||||
|
workbench re-adopts the stored drawing — preserving its views,
|
||||||
|
manually placed dimensions, and the auto-dimensions toggle.
|
||||||
|
"""
|
||||||
# Prefer _current_component (set by component button clicks).
|
# Prefer _current_component (set by component button clicks).
|
||||||
comp = self._current_component or self._project.get_active_component()
|
comp = self._current_component or self._project.get_active_component()
|
||||||
if comp and any(b.visible and b.geometry for b in comp.bodies.values()):
|
if comp and any(b.visible and b.geometry for b in comp.bodies.values()):
|
||||||
source_kind = "component"
|
self._drawing_tab.set_active_component(comp)
|
||||||
source_id = comp.id
|
else:
|
||||||
elif self._project.assemblies:
|
# No drawable component — let the widget fall back to the
|
||||||
asm_id = next(iter(self._project.assemblies.keys()))
|
# project's assemblies (or show "no source").
|
||||||
source_kind = "assembly"
|
self._drawing_tab.generate()
|
||||||
source_id = asm_id
|
|
||||||
|
|
||||||
if not source_id:
|
|
||||||
return
|
|
||||||
|
|
||||||
# Build default drawing with front, top, right, isometric views.
|
|
||||||
views = []
|
|
||||||
for kind in ("front", "top", "right", "isometric"):
|
|
||||||
dir_, up_ = _STANDARD_VIEWS[kind]
|
|
||||||
vname = kind.capitalize() if kind != "isometric" else "Isometric"
|
|
||||||
views.append(DrawingView(kind=kind, name=vname, direction=dir_, up_vector=up_))
|
|
||||||
|
|
||||||
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",
|
|
||||||
)
|
|
||||||
|
|
||||||
self._drawing_tab.set_drawing(drawing)
|
|
||||||
|
|
||||||
# Auto-generate.
|
|
||||||
try:
|
|
||||||
result = generate_drawing(drawing, self._project, self._kernel)
|
|
||||||
self._drawing_tab.set_render_result(result)
|
|
||||||
except Exception as e:
|
|
||||||
logger.warning(f"Failed to auto-generate drawing: {e}")
|
|
||||||
def _on_tab_changed(self, index: int) -> None:
|
def _on_tab_changed(self, index: int) -> None:
|
||||||
"""When the user switches to Render or Drawing tab, auto-load selected geometry."""
|
"""When the user switches to Render or Drawing tab, auto-load selected geometry."""
|
||||||
widget = self._ui.InputTab.widget(index)
|
widget = self._ui.InputTab.widget(index)
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user