- added iso sheet for tech draw
This commit is contained in:
Generated
+56
-58
@@ -4,12 +4,10 @@
|
|||||||
<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 v2">
|
<list default="true" id="8f0bafd6-58a0-4b20-aa2b-ddc3ba278873" name="Changes" comment="- tech drawing and render improv">
|
||||||
<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/geometry_occ/kernel.py" beforeDir="false" afterPath="$PROJECT_DIR$/src/fluency/geometry_occ/kernel.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/render_window.py" beforeDir="false" afterPath="$PROJECT_DIR$/src/fluency/ui/render_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" />
|
||||||
@@ -47,48 +45,48 @@
|
|||||||
<option name="hideEmptyMiddlePackages" value="true" />
|
<option name="hideEmptyMiddlePackages" value="true" />
|
||||||
<option name="showLibraryContents" value="true" />
|
<option name="showLibraryContents" value="true" />
|
||||||
</component>
|
</component>
|
||||||
<component name="PropertiesComponent">{
|
<component name="PropertiesComponent"><![CDATA[{
|
||||||
"keyToString": {
|
"keyToString": {
|
||||||
"Python.2dtest.executor": "Run",
|
"Python.2dtest.executor": "Run",
|
||||||
"Python.3d_windows.executor": "Run",
|
"Python.3d_windows.executor": "Run",
|
||||||
"Python.Unnamed.executor": "Run",
|
"Python.Unnamed.executor": "Run",
|
||||||
"Python.base.executor": "Run",
|
"Python.base.executor": "Run",
|
||||||
"Python.data_model.executor": "Run",
|
"Python.data_model.executor": "Run",
|
||||||
"Python.debug_dragging.executor": "Run",
|
"Python.debug_dragging.executor": "Run",
|
||||||
"Python.draw_widget2d.executor": "Run",
|
"Python.draw_widget2d.executor": "Run",
|
||||||
"Python.draw_widget_solve.executor": "Run",
|
"Python.draw_widget_solve.executor": "Run",
|
||||||
"Python.fluency.executor": "Run",
|
"Python.fluency.executor": "Run",
|
||||||
"Python.fluencyb.executor": "Run",
|
"Python.fluencyb.executor": "Run",
|
||||||
"Python.gl_widget.executor": "Run",
|
"Python.gl_widget.executor": "Run",
|
||||||
"Python.gui_ui.executor": "Run",
|
"Python.gui_ui.executor": "Run",
|
||||||
"Python.kernel.executor": "Run",
|
"Python.kernel.executor": "Run",
|
||||||
"Python.main.executor": "Run",
|
"Python.main.executor": "Run",
|
||||||
"Python.main_window.executor": "Run",
|
"Python.main_window.executor": "Run",
|
||||||
"Python.meshtest.executor": "Run",
|
"Python.meshtest.executor": "Run",
|
||||||
"Python.occ_renderer.executor": "Run",
|
"Python.occ_renderer.executor": "Run",
|
||||||
"Python.occ_to_mesh.executor": "Run",
|
"Python.occ_to_mesh.executor": "Run",
|
||||||
"Python.render_backend.executor": "Run",
|
"Python.render_backend.executor": "Run",
|
||||||
"Python.side_fluency.executor": "Run",
|
"Python.side_fluency.executor": "Run",
|
||||||
"Python.simple_mesh.executor": "Run",
|
"Python.simple_mesh.executor": "Run",
|
||||||
"Python.sketch.executor": "Run",
|
"Python.sketch.executor": "Run",
|
||||||
"Python.technical_drawing_widget.executor": "Run",
|
"Python.technical_drawing_widget.executor": "Run",
|
||||||
"Python.vtk_widget.executor": "Run",
|
"Python.vtk_widget.executor": "Run",
|
||||||
"Python.vulkan.executor": "Run",
|
"Python.vulkan.executor": "Run",
|
||||||
"RunOnceActivity.OpenProjectViewOnStart": "true",
|
"RunOnceActivity.OpenProjectViewOnStart": "true",
|
||||||
"RunOnceActivity.ShowReadmeOnStart": "true",
|
"RunOnceActivity.ShowReadmeOnStart": "true",
|
||||||
"RunOnceActivity.TerminalTabsStorage.copyFrom.TerminalArrangementManager.252": "true",
|
"RunOnceActivity.TerminalTabsStorage.copyFrom.TerminalArrangementManager.252": "true",
|
||||||
"RunOnceActivity.git.unshallow": "true",
|
"RunOnceActivity.git.unshallow": "true",
|
||||||
"RunOnceActivity.typescript.service.memoryLimit.init": "true",
|
"RunOnceActivity.typescript.service.memoryLimit.init": "true",
|
||||||
"codeWithMe.voiceChat.enabledByDefault": "false",
|
"codeWithMe.voiceChat.enabledByDefault": "false",
|
||||||
"git-widget-placeholder": "feature/occ-migration",
|
"git-widget-placeholder": "feature/occ-migration",
|
||||||
"last_opened_file_path": "/Volumes/Data_drive/Programming/fluency",
|
"last_opened_file_path": "/Volumes/Data_drive/Programming/fluency/src/fluency",
|
||||||
"node.js.detected.package.eslint": "true",
|
"node.js.detected.package.eslint": "true",
|
||||||
"node.js.selected.package.eslint": "(autodetect)",
|
"node.js.selected.package.eslint": "(autodetect)",
|
||||||
"node.js.selected.package.tslint": "(autodetect)",
|
"node.js.selected.package.tslint": "(autodetect)",
|
||||||
"nodejs_package_manager_path": "npm",
|
"nodejs_package_manager_path": "npm",
|
||||||
"settings.editor.selected.configurable": "project.propVCSSupport.DirectoryMappings"
|
"settings.editor.selected.configurable": "project.propVCSSupport.DirectoryMappings"
|
||||||
}
|
}
|
||||||
}</component>
|
}]]></component>
|
||||||
<component name="QodanaReportsService">
|
<component name="QodanaReportsService">
|
||||||
<option name="descriptions">
|
<option name="descriptions">
|
||||||
<ReportDescription localRun="true" path="/private/var/folders/kg/zm48w_r96yb68mlbzvb9gtq40000gn/T/qodana_output/qodana.sarif.json" reportGuid="5f5b823c-c594-48c5-ae1f-062e30303918" reportId="fluency/qodana/2024-02-04" />
|
<ReportDescription localRun="true" path="/private/var/folders/kg/zm48w_r96yb68mlbzvb9gtq40000gn/T/qodana_output/qodana.sarif.json" reportGuid="5f5b823c-c594-48c5-ae1f-062e30303918" reportId="fluency/qodana/2024-02-04" />
|
||||||
@@ -96,9 +94,9 @@
|
|||||||
</component>
|
</component>
|
||||||
<component name="RecentsManager">
|
<component name="RecentsManager">
|
||||||
<key name="CopyFile.RECENT_KEYS">
|
<key name="CopyFile.RECENT_KEYS">
|
||||||
|
<recent name="$PROJECT_DIR$/src/fluency" />
|
||||||
<recent name="$PROJECT_DIR$" />
|
<recent name="$PROJECT_DIR$" />
|
||||||
<recent name="$PROJECT_DIR$/src/fluency/Tesfiles" />
|
<recent name="$PROJECT_DIR$/src/fluency/Tesfiles" />
|
||||||
<recent name="$PROJECT_DIR$/src/fluency" />
|
|
||||||
<recent name="$PROJECT_DIR$/src/fluency/rendering" />
|
<recent name="$PROJECT_DIR$/src/fluency/rendering" />
|
||||||
<recent name="$PROJECT_DIR$/drawing_modules" />
|
<recent name="$PROJECT_DIR$/drawing_modules" />
|
||||||
</key>
|
</key>
|
||||||
@@ -122,14 +120,6 @@
|
|||||||
<option name="presentableId" value="Default" />
|
<option name="presentableId" value="Default" />
|
||||||
<updated>1703867682707</updated>
|
<updated>1703867682707</updated>
|
||||||
</task>
|
</task>
|
||||||
<task id="LOCAL-00005" summary="- Added new componnt controls">
|
|
||||||
<option name="closed" value="true" />
|
|
||||||
<created>1735601610504</created>
|
|
||||||
<option name="number" value="00005" />
|
|
||||||
<option name="presentableId" value="LOCAL-00005" />
|
|
||||||
<option name="project" value="LOCAL" />
|
|
||||||
<updated>1735601610504</updated>
|
|
||||||
</task>
|
|
||||||
<task id="LOCAL-00006" summary="- Added new componnt controls">
|
<task id="LOCAL-00006" summary="- Added new componnt controls">
|
||||||
<option name="closed" value="true" />
|
<option name="closed" value="true" />
|
||||||
<created>1735601786207</created>
|
<created>1735601786207</created>
|
||||||
@@ -514,7 +504,15 @@
|
|||||||
<option name="project" value="LOCAL" />
|
<option name="project" value="LOCAL" />
|
||||||
<updated>1786995561376</updated>
|
<updated>1786995561376</updated>
|
||||||
</task>
|
</task>
|
||||||
<option name="localTasksCounter" value="54" />
|
<task id="LOCAL-00054" summary="- tech drawing and render improv">
|
||||||
|
<option name="closed" value="true" />
|
||||||
|
<created>1787058413911</created>
|
||||||
|
<option name="number" value="00054" />
|
||||||
|
<option name="presentableId" value="LOCAL-00054" />
|
||||||
|
<option name="project" value="LOCAL" />
|
||||||
|
<updated>1787058413911</updated>
|
||||||
|
</task>
|
||||||
|
<option name="localTasksCounter" value="55" />
|
||||||
<servers />
|
<servers />
|
||||||
</component>
|
</component>
|
||||||
<component name="TypeScriptGeneratedFilesManager">
|
<component name="TypeScriptGeneratedFilesManager">
|
||||||
@@ -535,7 +533,6 @@
|
|||||||
<ignored-roots>
|
<ignored-roots>
|
||||||
<path value="$PROJECT_DIR$/pythonProject" />
|
<path value="$PROJECT_DIR$/pythonProject" />
|
||||||
</ignored-roots>
|
</ignored-roots>
|
||||||
<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" />
|
||||||
<MESSAGE value="- Added contrain displayed next to line - Slight change to point check from solver." />
|
<MESSAGE value="- Added contrain displayed next to line - Slight change to point check from solver." />
|
||||||
@@ -560,6 +557,7 @@
|
|||||||
<MESSAGE value="- Operation highlighting, body highlighting" />
|
<MESSAGE value="- Operation highlighting, body highlighting" />
|
||||||
<MESSAGE value="- tech draw draft" />
|
<MESSAGE value="- tech draw draft" />
|
||||||
<MESSAGE value="- tech draw draft v2" />
|
<MESSAGE value="- tech draw draft v2" />
|
||||||
<option name="LAST_COMMIT_MESSAGE" value="- tech draw draft v2" />
|
<MESSAGE value="- tech drawing and render improv" />
|
||||||
|
<option name="LAST_COMMIT_MESSAGE" value="- tech drawing and render improv" />
|
||||||
</component>
|
</component>
|
||||||
</project>
|
</project>
|
||||||
@@ -705,7 +705,9 @@ def _technical_drawing_from_dict(data: Dict[str, Any]) -> TechnicalDrawing:
|
|||||||
material=data.get("material", ""),
|
material=data.get("material", ""),
|
||||||
revision=data.get("revision", ""),
|
revision=data.get("revision", ""),
|
||||||
notes=data.get("notes", ""),
|
notes=data.get("notes", ""),
|
||||||
sheet_size=data.get("sheet_size", "A3"),
|
# The renderer only supports the A4 ISO 5457 sheet; normalize
|
||||||
|
# legacy drawings (stored as A3) instead of surfacing stale sizes.
|
||||||
|
sheet_size="A4",
|
||||||
units=data.get("units", "mm"),
|
units=data.get("units", "mm"),
|
||||||
auto_dimensions=bool(data.get("auto_dimensions", False)),
|
auto_dimensions=bool(data.get("auto_dimensions", False)),
|
||||||
auto_views=bool(data.get("auto_views", True)),
|
auto_views=bool(data.get("auto_views", True)),
|
||||||
|
|||||||
@@ -975,8 +975,8 @@ class TechnicalDrawing:
|
|||||||
revision: str = ""
|
revision: str = ""
|
||||||
notes: str = ""
|
notes: str = ""
|
||||||
|
|
||||||
# Sheet size (A0..A4 or custom mm). Default A3.
|
# Sheet size (A0..A4 or custom mm). Default A4 (ISO 5457 template).
|
||||||
sheet_size: str = "A3"
|
sheet_size: str = "A4"
|
||||||
units: str = "mm" # mm, in
|
units: str = "mm" # mm, in
|
||||||
|
|
||||||
# Auto-generation flags. Auto dimensions are opt-in: the drawing
|
# Auto-generation flags. Auto dimensions are opt-in: the drawing
|
||||||
|
|||||||
+261
-106
@@ -15,6 +15,7 @@ from __future__ import annotations
|
|||||||
|
|
||||||
import math
|
import math
|
||||||
from dataclasses import dataclass, field
|
from dataclasses import dataclass, field
|
||||||
|
from datetime import date
|
||||||
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 +69,8 @@ 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, ...] = ()
|
||||||
|
# Text cap height in sheet mm (None = the default dimension font).
|
||||||
|
font_size: Optional[float] = None
|
||||||
# View this primitive belongs to (geometry primitives); used for
|
# View this primitive belongs to (geometry primitives); used for
|
||||||
# hit-testing in the drawing workbench.
|
# hit-testing in the drawing workbench.
|
||||||
view_id: Optional[str] = None
|
view_id: Optional[str] = None
|
||||||
@@ -112,11 +115,29 @@ _STANDARD_VIEW_ROWS: List[Tuple[str, str, Tuple[float, float, float], Tuple[floa
|
|||||||
(k, k.capitalize(), v[0], v[1]) for k, v in _STANDARD_VIEWS.items()
|
(k, k.capitalize(), v[0], v[1]) for k, v in _STANDARD_VIEWS.items()
|
||||||
]
|
]
|
||||||
|
|
||||||
_A3_WIDTH_MM = 420.0
|
# Sheet size: A4 landscape (ISO 5457 title block, see the
|
||||||
_A3_HEIGHT_MM = 297.0
|
# A4_Landscape_ISO5457_minimal.svg template).
|
||||||
_TITLE_MARGIN_MM = 40.0
|
_SHEET_WIDTH_MM = 297.0
|
||||||
|
_SHEET_HEIGHT_MM = 210.0
|
||||||
_DISPLAY_PRECISION = 2
|
_DISPLAY_PRECISION = 2
|
||||||
|
|
||||||
|
# ISO 5457 sheet furniture geometry in sheet mm (origin bottom-left,
|
||||||
|
# +y up; the reference SVG is y-down, so y_sheet = 210 - y_svg).
|
||||||
|
_FRAME_X0_MM = 20.0 # drawing-space frame
|
||||||
|
_FRAME_Y0_MM = 10.0
|
||||||
|
_FRAME_W_MM = 267.0
|
||||||
|
_FRAME_H_MM = 190.0
|
||||||
|
_TB_LEFT_MM = 107.0 # ISO 5457 title block (180 x 36)
|
||||||
|
_TB_BOTTOM_MM = 10.0
|
||||||
|
_TB_W_MM = 180.0
|
||||||
|
_TB_H_MM = 36.0
|
||||||
|
# Zone the views must clear: the title block plus the part-material /
|
||||||
|
# general-tolerance / scale strip above it and the projection symbol.
|
||||||
|
_TB_RESERVE_X0_MM = 102.0
|
||||||
|
_TB_RESERVE_TOP_MM = 64.0
|
||||||
|
|
||||||
|
# ISO dash-dot centreline pattern in mm.
|
||||||
|
_CENTERLINE_DASH_MM = (6.0, 0.75, 0.125, 0.75)
|
||||||
|
|
||||||
def _normalize(v: Tuple[float, float, float]) -> Tuple[float, float, float]:
|
def _normalize(v: Tuple[float, float, float]) -> Tuple[float, float, float]:
|
||||||
x, y, z = v
|
x, y, z = v
|
||||||
@@ -417,9 +438,9 @@ def _assemble_view(
|
|||||||
if slot is not None:
|
if slot is not None:
|
||||||
left, bottom, avail_w, avail_h = slot
|
left, bottom, avail_w, avail_h = slot
|
||||||
else:
|
else:
|
||||||
left, bottom = 0.0, 0.0
|
left, bottom = _FRAME_X0_MM, _FRAME_Y0_MM
|
||||||
avail_w = _A3_WIDTH_MM - _TITLE_MARGIN_MM * 2
|
avail_w = _FRAME_W_MM
|
||||||
avail_h = _A3_HEIGHT_MM - _TITLE_MARGIN_MM * 2
|
avail_h = _FRAME_H_MM
|
||||||
|
|
||||||
if scale_override is not None:
|
if scale_override is not None:
|
||||||
scale = scale_override
|
scale = scale_override
|
||||||
@@ -474,6 +495,7 @@ def _assemble_view(
|
|||||||
kind="line",
|
kind="line",
|
||||||
points=((sc[0] - ext, sc[1]), (sc[0] + ext, sc[1])),
|
points=((sc[0] - ext, sc[1]), (sc[0] + ext, sc[1])),
|
||||||
style="center",
|
style="center",
|
||||||
|
dash_pattern=_CENTERLINE_DASH_MM,
|
||||||
view_id=view_id,
|
view_id=view_id,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@@ -482,6 +504,7 @@ def _assemble_view(
|
|||||||
kind="line",
|
kind="line",
|
||||||
points=((sc[0], sc[1] - ext), (sc[0], sc[1] + ext)),
|
points=((sc[0], sc[1] - ext), (sc[0], sc[1] + ext)),
|
||||||
style="center",
|
style="center",
|
||||||
|
dash_pattern=_CENTERLINE_DASH_MM,
|
||||||
view_id=view_id,
|
view_id=view_id,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@@ -551,8 +574,8 @@ def generate_view(
|
|||||||
"""Project one view from source parts.
|
"""Project one view from source parts.
|
||||||
|
|
||||||
*slot* optionally limits the fit to ``(left, bottom, width, height)``
|
*slot* optionally limits the fit to ``(left, bottom, width, height)``
|
||||||
in sheet mm; when omitted the projection is fitted to the full A3
|
in sheet mm; when omitted the projection is fitted to the full sheet
|
||||||
sheet. Returns ``(primitives, candidates, warnings)``.
|
frame. Returns ``(primitives, candidates, warnings)``.
|
||||||
"""
|
"""
|
||||||
edges, warnings = _project_view(source_parts, view)
|
edges, warnings = _project_view(source_parts, view)
|
||||||
return _assemble_view(edges, warnings, view, slot, None)
|
return _assemble_view(edges, warnings, view, slot, None)
|
||||||
@@ -873,41 +896,37 @@ def _extract_angle_candidates(
|
|||||||
|
|
||||||
# ── Sheet layout regions ────────────────────────────────────────────────────
|
# ── Sheet layout regions ────────────────────────────────────────────────────
|
||||||
|
|
||||||
_LAYOUT_MARGIN_MM = 10.0
|
|
||||||
_VIEW_GAP_MM = 12.0
|
_VIEW_GAP_MM = 12.0
|
||||||
|
|
||||||
# Title block box (see _title_block_primitives): 180 × 52 at the bottom-right
|
# The sheet interior is the ISO 5457 drawing-space frame.
|
||||||
# corner with a 5 mm sheet margin. Views must clear it (plus clearance).
|
|
||||||
_TB_LEFT_MM = _A3_WIDTH_MM - 180.0 - 5.0
|
|
||||||
_TB_TOP_MM = 5.0 + 52.0
|
|
||||||
_TB_CLEARANCE_MM = 5.0
|
|
||||||
|
|
||||||
# Sheet interior (border margin) as (x0, y0, x1, y1) in sheet mm.
|
|
||||||
_SHEET_INNER = (
|
_SHEET_INNER = (
|
||||||
_LAYOUT_MARGIN_MM,
|
_FRAME_X0_MM,
|
||||||
_LAYOUT_MARGIN_MM,
|
_FRAME_Y0_MM,
|
||||||
_A3_WIDTH_MM - _LAYOUT_MARGIN_MM,
|
_FRAME_X0_MM + _FRAME_W_MM,
|
||||||
_A3_HEIGHT_MM - _LAYOUT_MARGIN_MM,
|
_FRAME_Y0_MM + _FRAME_H_MM,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Regions the orthographic layout may occupy, as (x0, y0, w, h):
|
# Regions the orthographic layout may occupy, as (x0, y0, w, h):
|
||||||
# - UPPER: full sheet width above the title block
|
# - UPPER: frame width above the title-block reserve zone
|
||||||
# - LEFT: full sheet height in the left strip beside the title block
|
# - LEFT: full frame height in the left strip beside the reserve zone
|
||||||
_REGION_UPPER = (
|
_REGION_UPPER = (
|
||||||
_LAYOUT_MARGIN_MM,
|
_FRAME_X0_MM,
|
||||||
_TB_TOP_MM + _TB_CLEARANCE_MM,
|
_TB_RESERVE_TOP_MM,
|
||||||
_A3_WIDTH_MM - 2 * _LAYOUT_MARGIN_MM,
|
_FRAME_W_MM,
|
||||||
_A3_HEIGHT_MM - _LAYOUT_MARGIN_MM - (_TB_TOP_MM + _TB_CLEARANCE_MM),
|
_FRAME_Y0_MM + _FRAME_H_MM - _TB_RESERVE_TOP_MM,
|
||||||
)
|
)
|
||||||
_REGION_LEFT = (
|
_REGION_LEFT = (
|
||||||
_LAYOUT_MARGIN_MM,
|
_FRAME_X0_MM,
|
||||||
_LAYOUT_MARGIN_MM,
|
_FRAME_Y0_MM,
|
||||||
_TB_LEFT_MM - _TB_CLEARANCE_MM - _LAYOUT_MARGIN_MM,
|
_TB_RESERVE_X0_MM - _FRAME_X0_MM,
|
||||||
_A3_HEIGHT_MM - 2 * _LAYOUT_MARGIN_MM,
|
_FRAME_H_MM,
|
||||||
)
|
)
|
||||||
|
|
||||||
_MID_ORDER = ("left", "front", "right", "back")
|
# First-angle projection (ISO 128): the view from the right sits left of
|
||||||
_COL_ORDER = ("top", "front", "bottom") # sheet top → bottom
|
# the front view, the view from the left right of it, and the top view
|
||||||
|
# below the front view.
|
||||||
|
_MID_ORDER = ("right", "front", "left", "back")
|
||||||
|
_COL_ORDER = ("bottom", "front", "top") # sheet top → bottom
|
||||||
_GRID_ORDER = ("front", "right", "back", "top", "left", "bottom")
|
_GRID_ORDER = ("front", "right", "back", "top", "left", "bottom")
|
||||||
|
|
||||||
# Leave room for dimension lines: the shared scale fits the views into
|
# Leave room for dimension lines: the shared scale fits the views into
|
||||||
@@ -920,9 +939,10 @@ _RectList = List[Tuple[str, float, float, float, float]]
|
|||||||
def _place_cross(
|
def _place_cross(
|
||||||
dims: Dict[str, Tuple[float, float]], gap: float
|
dims: Dict[str, Tuple[float, float]], gap: float
|
||||||
) -> _RectList:
|
) -> _RectList:
|
||||||
"""Classic third-angle cross: mid views run left→right (left, front,
|
"""Classic first-angle cross: mid views run left→right (right, front,
|
||||||
right, back), col views stack top→bottom (top, front, bottom), with the
|
left, back), col views stack sheet-top→bottom (bottom, front, top),
|
||||||
anchor view (front, or the first present one) at the intersection.
|
with the anchor view (front, or the first present one) at the
|
||||||
|
intersection.
|
||||||
|
|
||||||
*dims* maps view id → ``(w, h)`` in sheet units. Returns local
|
*dims* maps view id → ``(w, h)`` in sheet units. Returns local
|
||||||
``(vid, x, y, w, h)`` rects (origin arbitrary — the caller centres the
|
``(vid, x, y, w, h)`` rects (origin arbitrary — the caller centres the
|
||||||
@@ -978,8 +998,8 @@ def _place_cross(
|
|||||||
|
|
||||||
def _place_swapped(dims: Dict[str, Tuple[float, float]], gap: float) -> _RectList:
|
def _place_swapped(dims: Dict[str, Tuple[float, float]], gap: float) -> _RectList:
|
||||||
"""Cross with the view families swapped: the mid views stack vertically
|
"""Cross with the view families swapped: the mid views stack vertically
|
||||||
(left, front, right, back from the top) and the col views run
|
(right, front, left, back from the top) and the col views run
|
||||||
horizontally (bottom, front, top from the left) — the classic cross
|
horizontally (top, front, bottom from the left) — the classic cross
|
||||||
turned a quarter turn, for sheets where that orientation fits more.
|
turned a quarter turn, for sheets where that orientation fits more.
|
||||||
"""
|
"""
|
||||||
mid = [k for k in _MID_ORDER if k in dims]
|
mid = [k for k in _MID_ORDER if k in dims]
|
||||||
@@ -1086,10 +1106,10 @@ def _free_rects(
|
|||||||
else:
|
else:
|
||||||
cands = [(ix0, iy0, ix1, iy1)]
|
cands = [(ix0, iy0, ix1, iy1)]
|
||||||
tb = (
|
tb = (
|
||||||
_TB_LEFT_MM - _TB_CLEARANCE_MM,
|
_TB_RESERVE_X0_MM,
|
||||||
0.0,
|
0.0,
|
||||||
_A3_WIDTH_MM - (_TB_LEFT_MM - _TB_CLEARANCE_MM),
|
_SHEET_WIDTH_MM,
|
||||||
_TB_TOP_MM + _TB_CLEARANCE_MM,
|
_TB_RESERVE_TOP_MM,
|
||||||
)
|
)
|
||||||
out: List[Tuple[float, float, float, float]] = []
|
out: List[Tuple[float, float, float, float]] = []
|
||||||
for x0, y0, x1, y1 in cands:
|
for x0, y0, x1, y1 in cands:
|
||||||
@@ -1129,13 +1149,14 @@ def _layout_views_on_sheet(
|
|||||||
rotations maps view_id → sheet rotation in degrees (0 or 90).
|
rotations maps view_id → sheet rotation in degrees (0 or 90).
|
||||||
|
|
||||||
The sheet is filled, not just used: every candidate arrangement
|
The sheet is filled, not just used: every candidate arrangement
|
||||||
(classic third-angle cross, the cross with the view families swapped,
|
(classic first-angle cross, the cross with the view families swapped,
|
||||||
and 2/3-row grids) is combined with every per-view 90° rotation
|
and 2/3-row grids) is combined with every per-view 90° rotation
|
||||||
assignment (with 1–2 views the projections stay upright and only the
|
assignment (with 1–2 views the projections stay upright and only the
|
||||||
scale is optimised), and the candidate giving the largest shared scale
|
scale is optimised), and the candidate giving the largest shared scale
|
||||||
is used. Candidates within 0.5% of the best scale prefer the one with
|
is used. A grid may pack views slightly tighter than the cross, but
|
||||||
fewer rotated views, then the more conventional arrangement, so layouts
|
only the cross keeps the projections in their standard alignment, so
|
||||||
stay stable and standard whenever they are already the best fit. The
|
candidates within 10% of the best scale prefer the one with fewer
|
||||||
|
rotated views, then the more conventional arrangement. The
|
||||||
shared scale is further reduced to leave room for dimension lines
|
shared scale is further reduced to leave room for dimension lines
|
||||||
between and around the views. All orthographic views share one scale
|
between and around the views. All orthographic views share one scale
|
||||||
so the projections stay mutually consistent. Isometric and custom
|
so the projections stay mutually consistent. Isometric and custom
|
||||||
@@ -1211,7 +1232,7 @@ def _layout_views_on_sheet(
|
|||||||
if cands:
|
if cands:
|
||||||
best_s = max(c[0] for c in cands)
|
best_s = max(c[0] for c in cands)
|
||||||
s, _nrot, _rank, region, dims_m, place, rotated = min(
|
s, _nrot, _rank, region, dims_m, place, rotated = min(
|
||||||
(c for c in cands if c[0] >= best_s * 0.995),
|
(c for c in cands if c[0] >= best_s * 0.9),
|
||||||
key=lambda c: (c[1], c[2], -c[0]),
|
key=lambda c: (c[1], c[2], -c[0]),
|
||||||
)
|
)
|
||||||
rx0, _ry0, rw, rh = region
|
rx0, _ry0, rw, rh = region
|
||||||
@@ -1243,7 +1264,12 @@ def _layout_views_on_sheet(
|
|||||||
slot = max(free, key=lambda r: r[2] * r[3])
|
slot = max(free, key=lambda r: r[2] * r[3])
|
||||||
else:
|
else:
|
||||||
# No free rect left — park in the bottom-left corner stack.
|
# No free rect left — park in the bottom-left corner stack.
|
||||||
slot = (_LAYOUT_MARGIN_MM, _LAYOUT_MARGIN_MM + i * 60.0, 120.0, 50.0)
|
slot = (
|
||||||
|
_FRAME_X0_MM,
|
||||||
|
_FRAME_Y0_MM + i * 50.0,
|
||||||
|
_TB_RESERVE_X0_MM - _FRAME_X0_MM,
|
||||||
|
40.0,
|
||||||
|
)
|
||||||
slots[vid] = slot
|
slots[vid] = slot
|
||||||
assigned.append(slot)
|
assigned.append(slot)
|
||||||
|
|
||||||
@@ -1257,9 +1283,9 @@ _DIM_KIND_CAPS = {"diameter": 3, "extent": 2, "length": 4, "angle": 2}
|
|||||||
_MAX_DIMENSIONS_PER_VIEW = 10
|
_MAX_DIMENSIONS_PER_VIEW = 10
|
||||||
|
|
||||||
# Rendering metrics for the drawing font size.
|
# Rendering metrics for the drawing font size.
|
||||||
_DIM_TEXT_H_MM = 3.0 # text cap height (standard A3 drawing)
|
_DIM_TEXT_H_MM = 3.5 # text cap height (ISO 3098 group C, A4 drawing)
|
||||||
_DIM_FONT_W = 1.9 # approx. mm width per character
|
_DIM_FONT_W = 2.2 # approx. mm width per character
|
||||||
_DIM_FONT_H = 4.0 # text box height in mm
|
_DIM_FONT_H = 5.0 # text box height in mm
|
||||||
_DIM_OFFSET_MM = 6.0 # dimension-line offset from the measured feature
|
_DIM_OFFSET_MM = 6.0 # dimension-line offset from the measured feature
|
||||||
_DIM_EXT_OVERSHOOT_MM = 2.0 # extension-line overshoot past the dim line
|
_DIM_EXT_OVERSHOOT_MM = 2.0 # extension-line overshoot past the dim line
|
||||||
_DIM_STANDOFF_MM = 11.0 # min gap between stacked parallel dim lines
|
_DIM_STANDOFF_MM = 11.0 # min gap between stacked parallel dim lines
|
||||||
@@ -1339,8 +1365,8 @@ def _generate_dimension_primitives(
|
|||||||
horizontal tail. Angles become a small vertex arc + text.
|
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, _SHEET_WIDTH_MM - 6.0
|
||||||
_sheet_min_y, _sheet_max_y = 6.0, _A3_HEIGHT_MM - 6.0
|
_sheet_min_y, _sheet_max_y = 6.0, _SHEET_HEIGHT_MM - 6.0
|
||||||
|
|
||||||
def _clamp_sheet(pt: Tuple[float, float]) -> Tuple[float, float]:
|
def _clamp_sheet(pt: Tuple[float, float]) -> Tuple[float, float]:
|
||||||
return (
|
return (
|
||||||
@@ -1894,7 +1920,7 @@ def generate_drawing(
|
|||||||
center = (
|
center = (
|
||||||
(slot[0] + slot[2] / 2.0, slot[1] + slot[3] / 2.0)
|
(slot[0] + slot[2] / 2.0, slot[1] + slot[3] / 2.0)
|
||||||
if slot
|
if slot
|
||||||
else (_A3_WIDTH_MM / 2.0, _A3_HEIGHT_MM / 2.0)
|
else (_SHEET_WIDTH_MM / 2.0, _SHEET_HEIGHT_MM / 2.0)
|
||||||
)
|
)
|
||||||
selected: List[DrawingCandidate] = []
|
selected: List[DrawingCandidate] = []
|
||||||
if drawing.auto_dimensions:
|
if drawing.auto_dimensions:
|
||||||
@@ -1932,8 +1958,11 @@ def generate_drawing(
|
|||||||
unresolved_ids.append(ann.id)
|
unresolved_ids.append(ann.id)
|
||||||
|
|
||||||
# Title block.
|
# Title block.
|
||||||
title_prims = _title_block_primitives(drawing)
|
# Sheet furniture: ISO 5457 drawing frame, grid reference system,
|
||||||
all_primitives.extend(title_prims)
|
# trimming marks, projection symbol and title block.
|
||||||
|
all_primitives.extend(
|
||||||
|
_sheet_frame_primitives(drawing, _format_drawing_scale(common_scale))
|
||||||
|
)
|
||||||
|
|
||||||
return DrawingRenderResult(
|
return DrawingRenderResult(
|
||||||
primitives=tuple(all_primitives),
|
primitives=tuple(all_primitives),
|
||||||
@@ -1948,42 +1977,158 @@ def generate_drawing(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def _title_block_primitives(drawing: TechnicalDrawing) -> List[DrawingPrimitive]:
|
def _format_drawing_scale(scale: Optional[float]) -> str:
|
||||||
"""Generate title block primitives at the bottom-right of the sheet."""
|
"""Format the shared view scale for the title block's scale field."""
|
||||||
|
if scale is None or scale <= 0:
|
||||||
|
return "1 : 1"
|
||||||
|
|
||||||
|
def _num(v: float) -> str:
|
||||||
|
s = f"{v:.2f}".rstrip("0").rstrip(".")
|
||||||
|
return s or "0"
|
||||||
|
|
||||||
|
if scale >= 1.0:
|
||||||
|
return f"{_num(scale)} : 1"
|
||||||
|
return f"1 : {_num(round(1.0 / scale, 2))}"
|
||||||
|
|
||||||
|
|
||||||
|
def _sheet_frame_primitives(
|
||||||
|
drawing: TechnicalDrawing, scale_label: str
|
||||||
|
) -> List[DrawingPrimitive]:
|
||||||
|
"""ISO 5457 sheet furniture: drawing-space frame, centring marks,
|
||||||
|
grid reference system, trimming marks, first-angle projection symbol
|
||||||
|
and title block — mirroring the A4_Landscape_ISO5457_minimal.svg
|
||||||
|
template (y-down) onto sheet coordinates (origin bottom-left, +y up).
|
||||||
|
"""
|
||||||
prims: List[DrawingPrimitive] = []
|
prims: List[DrawingPrimitive] = []
|
||||||
margin = 5.0
|
|
||||||
box_h = 52.0
|
|
||||||
box_w = 180.0
|
|
||||||
left = _A3_WIDTH_MM - box_w - margin
|
|
||||||
bottom = margin
|
|
||||||
line_h = 12.0
|
|
||||||
|
|
||||||
# Border.
|
def line(p1: Tuple[float, float], p2: Tuple[float, float], style: str) -> None:
|
||||||
for sx, sy, ex, ey in [
|
prims.append(DrawingPrimitive(kind="line", points=(p1, p2), style=style))
|
||||||
(left, bottom, left + box_w, bottom),
|
|
||||||
(left, bottom + box_h, left + box_w, bottom + box_h),
|
|
||||||
(left, bottom, left, bottom + box_h),
|
|
||||||
(left + box_w, bottom, left + box_w, bottom + box_h),
|
|
||||||
]:
|
|
||||||
prims.append(DrawingPrimitive(kind="line", points=((sx, sy), (ex, ey)), style="visible"))
|
|
||||||
|
|
||||||
fields = [
|
def text(x: float, y: float, t: str, size: float, anchor: str = "start") -> None:
|
||||||
("Title:", drawing.title, 0),
|
if anchor == "middle":
|
||||||
("Part No:", drawing.part_number, 1),
|
# Pre-centre: the renderer draws text left-aligned at the point.
|
||||||
("Material:", drawing.material, 2),
|
x -= len(t) * size * 0.6 / 2.0
|
||||||
("Rev:", drawing.revision, 3),
|
|
||||||
]
|
|
||||||
for label, value, row in fields:
|
|
||||||
ty = bottom + box_h - line_h * (row + 1) + 3
|
|
||||||
prims.append(
|
prims.append(
|
||||||
DrawingPrimitive(
|
DrawingPrimitive(
|
||||||
kind="text",
|
kind="text",
|
||||||
points=((left + 4, ty),),
|
points=((x, y),),
|
||||||
style="dimension",
|
style="sheet_text",
|
||||||
text=f"{label} {value}",
|
text=t,
|
||||||
|
font_size=size,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def rect(x: float, y: float, w: float, h: float, style: str) -> None:
|
||||||
|
line((x, y), (x + w, y), style)
|
||||||
|
line((x + w, y), (x + w, y + h), style)
|
||||||
|
line((x + w, y + h), (x, y + h), style)
|
||||||
|
line((x, y + h), (x, y), style)
|
||||||
|
|
||||||
|
# ── Drawing-space frame and centring marks (0.7 mm) ─────────────
|
||||||
|
rect(20.0, 10.0, 267.0, 190.0, "frame")
|
||||||
|
line((148.5, 205.0), (148.5, 195.0), "frame") # top
|
||||||
|
line((292.0, 105.0), (282.0, 105.0), "frame") # right
|
||||||
|
line((148.5, 5.0), (148.5, 11.0), "frame") # bottom
|
||||||
|
line((15.0, 105.0), (25.0, 105.0), "frame") # left
|
||||||
|
|
||||||
|
# ── Grid reference border and ticks (0.35 mm) ───────────────────
|
||||||
|
rect(15.0, 5.0, 277.0, 200.0, "sheet")
|
||||||
|
for x in (48.5, 98.5, 198.5, 248.5):
|
||||||
|
line((x, 205.0), (x, 200.0), "sheet") # upper ticks
|
||||||
|
line((x, 5.0), (x, 10.0), "sheet") # lower ticks
|
||||||
|
for y in (155.0, 55.0):
|
||||||
|
line((292.0, y), (287.0, y), "sheet") # right ticks
|
||||||
|
line((15.0, y), (20.0, y), "sheet") # left ticks
|
||||||
|
|
||||||
|
# Grid reference labels (3.5 mm).
|
||||||
|
for i, x in enumerate((23.5, 73.5, 123.5, 173.5, 223.5, 273.5), 1):
|
||||||
|
text(x, 201.2, str(i), 3.5, anchor="middle")
|
||||||
|
for i, y in enumerate((178.7, 128.7, 78.7, 28.7)):
|
||||||
|
text(289.5, y, "ABCD"[i], 3.5, anchor="middle")
|
||||||
|
text(273.5, 6.2, "A4", 3.5, anchor="middle")
|
||||||
|
|
||||||
|
# ── Trimming marks (corner steps, 0.7 mm) ───────────────────────
|
||||||
|
for step in (
|
||||||
|
((0.0, 200.0), (5.0, 200.0), (5.0, 205.0), (10.0, 205.0), (10.0, 210.0)),
|
||||||
|
((287.0, 200.0), (292.0, 200.0), (292.0, 205.0), (297.0, 205.0), (297.0, 210.0)),
|
||||||
|
((287.0, 10.0), (292.0, 10.0), (292.0, 5.0), (297.0, 5.0), (297.0, 0.0)),
|
||||||
|
((10.0, 10.0), (5.0, 10.0), (5.0, 5.0), (0.0, 5.0), (0.0, 0.0)),
|
||||||
|
):
|
||||||
|
for a, b in zip(step, step[1:]):
|
||||||
|
line(a, b, "frame")
|
||||||
|
|
||||||
|
# ── First-angle projection symbol (0.5 mm outline) ──────────────
|
||||||
|
trap = ((266.0, 63.0), (256.0, 60.5), (256.0, 55.5), (266.0, 53.0))
|
||||||
|
for a, b in zip(trap, trap[1:] + trap[:1]):
|
||||||
|
line(a, b, "medium")
|
||||||
|
for r in (2.5, 5.0):
|
||||||
|
prims.append(
|
||||||
|
DrawingPrimitive(
|
||||||
|
kind="circle", points=(), style="medium",
|
||||||
|
center=(273.0, 58.0), radius=r,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
for p1, p2 in (((255.0, 58.0), (279.0, 58.0)), ((273.0, 52.0), (273.0, 64.0))):
|
||||||
|
prims.append(
|
||||||
|
DrawingPrimitive(
|
||||||
|
kind="line", points=(p1, p2), style="center",
|
||||||
|
dash_pattern=_CENTERLINE_DASH_MM,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
# ── Title block borders ─────────────────────────────────────────
|
||||||
|
rect(_TB_LEFT_MM, _TB_BOTTOM_MM, _TB_W_MM, _TB_H_MM, "frame")
|
||||||
|
for x, y, w, h in (
|
||||||
|
(247.0, 22.0, 40.0, 24.0), # owner
|
||||||
|
(107.0, 10.0, 80.0, 12.0), # drawing number
|
||||||
|
(247.0, 10.0, 20.0, 12.0), # revision
|
||||||
|
(207.0, 10.0, 40.0, 12.0), # issue date
|
||||||
|
(267.0, 10.0, 20.0, 12.0), # sheet
|
||||||
|
(187.0, 10.0, 20.0, 12.0), # language
|
||||||
|
(107.0, 34.0, 80.0, 12.0), # title
|
||||||
|
(187.0, 22.0, 60.0, 12.0), # approved by
|
||||||
|
(187.0, 34.0, 60.0, 12.0), # created by
|
||||||
|
(107.0, 22.0, 80.0, 12.0), # document type
|
||||||
|
):
|
||||||
|
rect(x, y, w, h, "sheet")
|
||||||
|
|
||||||
|
# ── Title block labels (3.5 mm) ─────────────────────────────────
|
||||||
|
for x, y, t in (
|
||||||
|
(247.5, 42.4, "Owner:"),
|
||||||
|
(107.5, 18.4, "Drawing number:"),
|
||||||
|
(247.5, 18.4, "Revision:"),
|
||||||
|
(207.5, 18.4, "Issue date:"),
|
||||||
|
(267.5, 18.4, "Sheet:"),
|
||||||
|
(187.5, 18.4, "Language:"),
|
||||||
|
(107.5, 42.4, "Title:"),
|
||||||
|
(187.5, 30.4, "Approved by:"),
|
||||||
|
(187.5, 42.4, "Created by:"),
|
||||||
|
(107.5, 30.4, "Document type:"),
|
||||||
|
(227.5, 54.4, "Scale:"),
|
||||||
|
(187.5, 54.4, "General tolerances:"),
|
||||||
|
(107.5, 54.4, "Part Material:"),
|
||||||
|
):
|
||||||
|
text(x, y, t, 3.5)
|
||||||
|
|
||||||
|
# ── Title block data (5 mm) ─────────────────────────────────────
|
||||||
|
doc_type = (
|
||||||
|
"Assembly Drawing" if drawing.source_kind == "assembly" else "Component Drawing"
|
||||||
|
)
|
||||||
|
for x, y, t, anchor in (
|
||||||
|
(108.0, 36.8, drawing.title, "start"),
|
||||||
|
(108.0, 24.8, doc_type, "start"),
|
||||||
|
(108.0, 48.8, drawing.material, "start"),
|
||||||
|
(188.0, 48.8, "ISO 2768-m", "start"),
|
||||||
|
(237.0, 48.8, scale_label, "middle"),
|
||||||
|
(197.0, 12.8, "EN", "middle"),
|
||||||
|
(277.0, 12.8, "1 / 1", "middle"),
|
||||||
|
(208.0, 12.8, date.today().isoformat(), "start"),
|
||||||
|
(108.0, 12.8, drawing.part_number, "start"),
|
||||||
|
(257.0, 12.8, drawing.revision, "middle"),
|
||||||
|
):
|
||||||
|
if t:
|
||||||
|
text(x, y, t, 5.0, anchor=anchor)
|
||||||
|
|
||||||
return prims
|
return prims
|
||||||
|
|
||||||
|
|
||||||
@@ -1997,33 +2142,33 @@ def render_drawing(
|
|||||||
|
|
||||||
*sheet_rect* defines the canvas area in device coordinates (mm).
|
*sheet_rect* defines the canvas area in device coordinates (mm).
|
||||||
"""
|
"""
|
||||||
sx = sheet_rect.width() / _A3_WIDTH_MM
|
sx = sheet_rect.width() / _SHEET_WIDTH_MM
|
||||||
sy = sheet_rect.height() / _A3_HEIGHT_MM
|
sy = sheet_rect.height() / _SHEET_HEIGHT_MM
|
||||||
scale = min(sx, sy)
|
scale = min(sx, sy)
|
||||||
|
|
||||||
draw_w = _A3_WIDTH_MM * scale
|
draw_w = _SHEET_WIDTH_MM * scale
|
||||||
draw_h = _A3_HEIGHT_MM * scale
|
draw_h = _SHEET_HEIGHT_MM * scale
|
||||||
ox = sheet_rect.x() + (sheet_rect.width() - draw_w) / 2.0
|
ox = sheet_rect.x() + (sheet_rect.width() - draw_w) / 2.0
|
||||||
oy = sheet_rect.y() + (sheet_rect.height() - draw_h) / 2.0
|
oy = sheet_rect.y() + (sheet_rect.height() - draw_h) / 2.0
|
||||||
|
|
||||||
def _to_device(x_mm: float, y_mm: float) -> QPointF:
|
def _to_device(x_mm: float, y_mm: float) -> QPointF:
|
||||||
return QPointF(ox + x_mm * scale, oy + (_A3_HEIGHT_MM - y_mm) * scale)
|
return QPointF(ox + x_mm * scale, oy + (_SHEET_HEIGHT_MM - y_mm) * scale)
|
||||||
|
|
||||||
# White background.
|
# White background.
|
||||||
painter.fillRect(QRectF(ox, oy, draw_w, draw_h), QColor(255, 255, 255))
|
painter.fillRect(QRectF(ox, oy, draw_w, draw_h), QColor(255, 255, 255))
|
||||||
|
|
||||||
# Sheet border.
|
# Pen weights follow the ISO 5457 template (0.35 / 0.5 / 0.7 mm),
|
||||||
border_pen = QPen(QColor(0, 0, 0), 1.0 * scale)
|
# scaled to device px so line weights are constant on the sheet.
|
||||||
border_pen.setCosmetic(True)
|
|
||||||
painter.setPen(border_pen)
|
|
||||||
painter.drawRect(QRectF(ox, oy, draw_w, draw_h))
|
|
||||||
|
|
||||||
style_pens = {
|
style_pens = {
|
||||||
"visible": QPen(QColor(0, 0, 0), 1.5),
|
"visible": QPen(QColor(0, 0, 0), 0.5 * scale),
|
||||||
"hidden": QPen(QColor(128, 128, 128), 1.0),
|
"hidden": QPen(QColor(128, 128, 128), 0.35 * scale),
|
||||||
"center": QPen(QColor(0, 0, 0), 0.35),
|
"center": QPen(QColor(0, 0, 0), 0.25 * scale),
|
||||||
"construction": QPen(QColor(0, 0, 255), 0.5),
|
"construction": QPen(QColor(0, 0, 255), 0.35 * scale),
|
||||||
"dimension": QPen(QColor(0, 0, 0), 1.0),
|
"dimension": QPen(QColor(0, 0, 0), 0.35 * scale),
|
||||||
|
"frame": QPen(QColor(0, 0, 0), 0.7 * scale),
|
||||||
|
"medium": QPen(QColor(0, 0, 0), 0.5 * scale),
|
||||||
|
"sheet": QPen(QColor(0, 0, 0), 0.35 * scale),
|
||||||
|
"sheet_text": QPen(QColor(0, 0, 0), 0.35 * scale),
|
||||||
}
|
}
|
||||||
for sp in style_pens.values():
|
for sp in style_pens.values():
|
||||||
sp.setCosmetic(True)
|
sp.setCosmetic(True)
|
||||||
@@ -2038,6 +2183,12 @@ def render_drawing(
|
|||||||
|
|
||||||
for prim in render_result.primitives:
|
for prim in render_result.primitives:
|
||||||
pen = style_pens.get(prim.style, style_pens["visible"])
|
pen = style_pens.get(prim.style, style_pens["visible"])
|
||||||
|
if prim.dash_pattern:
|
||||||
|
# Qt dash values are multiples of the pen width; the pattern
|
||||||
|
# is specified in sheet mm, so convert through device px.
|
||||||
|
w = max(pen.width(), 1e-6)
|
||||||
|
pen = QPen(pen)
|
||||||
|
pen.setDashPattern([d * scale / w for d in prim.dash_pattern])
|
||||||
painter.setPen(pen)
|
painter.setPen(pen)
|
||||||
|
|
||||||
if prim.kind == "line":
|
if prim.kind == "line":
|
||||||
@@ -2052,11 +2203,15 @@ def render_drawing(
|
|||||||
painter.drawEllipse(c, r, r)
|
painter.drawEllipse(c, r, r)
|
||||||
|
|
||||||
elif prim.kind == "text":
|
elif prim.kind == "text":
|
||||||
if not prim.points:
|
if not prim.points or not prim.text:
|
||||||
continue
|
continue
|
||||||
p = _to_device(*prim.points[0])
|
if prim.font_size:
|
||||||
if prim.text:
|
text_font = QFont(font)
|
||||||
painter.drawText(p, prim.text)
|
text_font.setPixelSize(max(5, int(prim.font_size * scale)))
|
||||||
|
painter.setFont(text_font)
|
||||||
|
painter.drawText(_to_device(*prim.points[0]), prim.text)
|
||||||
|
if prim.font_size:
|
||||||
|
painter.setFont(font)
|
||||||
|
|
||||||
elif prim.kind == "dimension":
|
elif prim.kind == "dimension":
|
||||||
if not prim.points:
|
if not prim.points:
|
||||||
@@ -2084,13 +2239,13 @@ def export_drawing_svg(
|
|||||||
|
|
||||||
generator = QSvgGenerator()
|
generator = QSvgGenerator()
|
||||||
generator.setFileName(filepath)
|
generator.setFileName(filepath)
|
||||||
generator.setSize(QSize(2100, 1485)) # ~A3 at 5px/mm
|
generator.setSize(QSize(1485, 1050)) # A4 at 5px/mm
|
||||||
generator.setViewBox(QRectF(0, 0, _A3_WIDTH_MM, _A3_HEIGHT_MM))
|
generator.setViewBox(QRectF(0, 0, _SHEET_WIDTH_MM, _SHEET_HEIGHT_MM))
|
||||||
generator.setTitle("Fluency Technical Drawing")
|
generator.setTitle("Fluency Technical Drawing")
|
||||||
|
|
||||||
painter = QPainter(generator)
|
painter = QPainter(generator)
|
||||||
try:
|
try:
|
||||||
render_drawing(painter, render_result, QRectF(0, 0, _A3_WIDTH_MM, _A3_HEIGHT_MM))
|
render_drawing(painter, render_result, QRectF(0, 0, _SHEET_WIDTH_MM, _SHEET_HEIGHT_MM))
|
||||||
finally:
|
finally:
|
||||||
painter.end()
|
painter.end()
|
||||||
|
|
||||||
@@ -2112,12 +2267,12 @@ def export_drawing_pdf(
|
|||||||
printer = QPrinter(QPrinter.PrinterMode.HighResolution)
|
printer = QPrinter(QPrinter.PrinterMode.HighResolution)
|
||||||
printer.setOutputFormat(QPrinter.OutputFormat.PdfFormat)
|
printer.setOutputFormat(QPrinter.OutputFormat.PdfFormat)
|
||||||
printer.setOutputFileName(filepath)
|
printer.setOutputFileName(filepath)
|
||||||
printer.setPageSize(QPageSize(QPageSize.PageSizeId.A3))
|
printer.setPageSize(QPageSize(QPageSize.PageSizeId.A4))
|
||||||
printer.setPageOrientation(QPageLayout.Orientation.Landscape)
|
printer.setPageOrientation(QPageLayout.Orientation.Landscape)
|
||||||
printer.setFullPage(True)
|
printer.setFullPage(True)
|
||||||
|
|
||||||
painter = QPainter(printer)
|
painter = QPainter(printer)
|
||||||
try:
|
try:
|
||||||
render_drawing(painter, render_result, QRectF(0, 0, _A3_WIDTH_MM, _A3_HEIGHT_MM))
|
render_drawing(painter, render_result, QRectF(0, 0, _SHEET_WIDTH_MM, _SHEET_HEIGHT_MM))
|
||||||
finally:
|
finally:
|
||||||
painter.end()
|
painter.end()
|
||||||
|
|||||||
@@ -0,0 +1,213 @@
|
|||||||
|
"""Tests for the A4 ISO 5457 sheet: layout regions, first-angle view
|
||||||
|
ordering, sheet furniture primitives, scale formatting, and the A4
|
||||||
|
normalisation of drawings loaded from .fluency files.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import unittest
|
||||||
|
|
||||||
|
# Allow running this file directly: ``python tests/test_iso5457_sheet.py``.
|
||||||
|
sys.path.insert(0, os.path.join(os.path.dirname(__file__), os.pardir, "src"))
|
||||||
|
|
||||||
|
os.environ.setdefault("QT_QPA_PLATFORM", "offscreen")
|
||||||
|
|
||||||
|
from fluency.models.data_model import DrawingView, TechnicalDrawing
|
||||||
|
from fluency.technical_drawing import (
|
||||||
|
_SHEET_HEIGHT_MM,
|
||||||
|
_SHEET_WIDTH_MM,
|
||||||
|
_format_drawing_scale,
|
||||||
|
_layout_views_on_sheet,
|
||||||
|
_sheet_frame_primitives,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _view(kind):
|
||||||
|
return DrawingView(kind=kind, name=kind)
|
||||||
|
|
||||||
|
|
||||||
|
class TestSheetSize(unittest.TestCase):
|
||||||
|
"""The workbench renders a single A4 landscape sheet."""
|
||||||
|
|
||||||
|
def test_a4_dimensions(self):
|
||||||
|
self.assertEqual((_SHEET_WIDTH_MM, _SHEET_HEIGHT_MM), (297.0, 210.0))
|
||||||
|
|
||||||
|
def test_default_drawing_is_a4(self):
|
||||||
|
self.assertEqual(TechnicalDrawing().sheet_size, "A4")
|
||||||
|
|
||||||
|
|
||||||
|
class TestScaleFormatting(unittest.TestCase):
|
||||||
|
"""Title-block scale field formatting (``N : 1`` / ``1 : N``)."""
|
||||||
|
|
||||||
|
def test_none_and_one(self):
|
||||||
|
self.assertEqual(_format_drawing_scale(None), "1 : 1")
|
||||||
|
self.assertEqual(_format_drawing_scale(0.0), "1 : 1")
|
||||||
|
self.assertEqual(_format_drawing_scale(1.0), "1 : 1")
|
||||||
|
|
||||||
|
def test_enlarged(self):
|
||||||
|
self.assertEqual(_format_drawing_scale(2.0), "2 : 1")
|
||||||
|
self.assertEqual(_format_drawing_scale(1.5), "1.5 : 1")
|
||||||
|
|
||||||
|
def test_reduced(self):
|
||||||
|
self.assertEqual(_format_drawing_scale(0.5), "1 : 2")
|
||||||
|
self.assertEqual(_format_drawing_scale(0.33333), "1 : 3")
|
||||||
|
|
||||||
|
|
||||||
|
class TestFirstAngleLayout(unittest.TestCase):
|
||||||
|
"""View slots stay inside the ISO 5457 drawing-space frame, clear the
|
||||||
|
title-block reserve zone, and follow first-angle ordering (view from
|
||||||
|
the right left of front, view from the left right of front, top view
|
||||||
|
below front)."""
|
||||||
|
|
||||||
|
def _slots(self, kinds):
|
||||||
|
bboxes = {k: (0.0, 0.0, 40.0, 30.0) for k in kinds}
|
||||||
|
slots, scale, _rots = _layout_views_on_sheet(
|
||||||
|
[_view(k) for k in kinds], bboxes
|
||||||
|
)
|
||||||
|
return slots, scale
|
||||||
|
|
||||||
|
def test_slots_inside_frame(self):
|
||||||
|
slots, scale = self._slots(
|
||||||
|
("front", "top", "right", "left", "isometric")
|
||||||
|
)
|
||||||
|
self.assertIsNotNone(scale)
|
||||||
|
self.assertGreater(scale, 0)
|
||||||
|
for vid, (x, y, w, h) in slots.items():
|
||||||
|
# Drawing-space frame: 20..287 x 10..200.
|
||||||
|
self.assertGreaterEqual(x, 19.9, vid)
|
||||||
|
self.assertGreaterEqual(y, 9.9, vid)
|
||||||
|
self.assertLessEqual(x + w, 287.1, vid)
|
||||||
|
self.assertLessEqual(y + h, 200.1, vid)
|
||||||
|
# Title-block reserve zone: x >= 102, y <= 64.
|
||||||
|
self.assertTrue(
|
||||||
|
x + w <= 102.01 or y + h <= 0.01 or y >= 63.99,
|
||||||
|
(vid, (x, y, w, h)),
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_first_angle_ordering(self):
|
||||||
|
slots, _ = self._slots(("front", "top", "right", "left"))
|
||||||
|
xf, wf = slots["front"][0], slots["front"][2]
|
||||||
|
yf = slots["front"][1]
|
||||||
|
# View from the right sits left of the front view.
|
||||||
|
self.assertLessEqual(
|
||||||
|
slots["right"][0] + slots["right"][2], xf + 0.01
|
||||||
|
)
|
||||||
|
# View from the left sits right of the front view.
|
||||||
|
self.assertGreaterEqual(slots["left"][0], xf + wf + 0.01)
|
||||||
|
# Top view sits below the front view.
|
||||||
|
self.assertLessEqual(slots["top"][1] + slots["top"][3], yf + 0.01)
|
||||||
|
|
||||||
|
def test_single_view_centered_upright(self):
|
||||||
|
slots, scale, rots = _layout_views_on_sheet(
|
||||||
|
[_view("front")], {"front": (0.0, 0.0, 40.0, 30.0)}
|
||||||
|
)
|
||||||
|
self.assertIsNotNone(scale)
|
||||||
|
self.assertEqual(rots.get("front", 0.0), 0.0)
|
||||||
|
self.assertGreater(scale, 0)
|
||||||
|
x, y, w, h = slots["front"]
|
||||||
|
self.assertGreaterEqual(x, 19.9)
|
||||||
|
self.assertGreaterEqual(y, 9.9)
|
||||||
|
|
||||||
|
|
||||||
|
class TestSheetFurniture(unittest.TestCase):
|
||||||
|
"""ISO 5457 furniture primitives mirror the reference SVG template."""
|
||||||
|
|
||||||
|
def _prims(self):
|
||||||
|
d = TechnicalDrawing(
|
||||||
|
title="Test Bracket",
|
||||||
|
part_number="PN-42",
|
||||||
|
material="Alu 6082",
|
||||||
|
revision="B",
|
||||||
|
)
|
||||||
|
return _sheet_frame_primitives(d, "1 : 2")
|
||||||
|
|
||||||
|
def test_all_geometry_inside_sheet(self):
|
||||||
|
for p in self._prims():
|
||||||
|
for (x, y) in p.points:
|
||||||
|
self.assertGreaterEqual(x, -0.01)
|
||||||
|
self.assertLessEqual(x, 297.01)
|
||||||
|
self.assertGreaterEqual(y, -0.01)
|
||||||
|
self.assertLessEqual(y, 210.01)
|
||||||
|
|
||||||
|
def test_drawing_space_frame(self):
|
||||||
|
# Frame rect 20,10 267x190: four edges present as line prims.
|
||||||
|
lines = [
|
||||||
|
(tuple(p.points[0]), tuple(p.points[1]))
|
||||||
|
for p in self._prims()
|
||||||
|
if p.kind == "line" and p.style == "frame"
|
||||||
|
]
|
||||||
|
for a, b in (
|
||||||
|
((20.0, 10.0), (287.0, 10.0)),
|
||||||
|
((287.0, 10.0), (287.0, 200.0)),
|
||||||
|
((287.0, 200.0), (20.0, 200.0)),
|
||||||
|
((20.0, 200.0), (20.0, 10.0)),
|
||||||
|
):
|
||||||
|
self.assertIn((a, b), lines)
|
||||||
|
|
||||||
|
def test_grid_reference_labels(self):
|
||||||
|
texts = [p.text for p in self._prims() if p.kind == "text" and p.text]
|
||||||
|
for t in ("1", "2", "3", "4", "5", "6", "A", "B", "C", "D", "A4"):
|
||||||
|
self.assertIn(t, texts)
|
||||||
|
|
||||||
|
def test_title_block_fields(self):
|
||||||
|
texts = [p.text for p in self._prims() if p.kind == "text" and p.text]
|
||||||
|
for expect in (
|
||||||
|
"Owner:",
|
||||||
|
"Drawing number:",
|
||||||
|
"Revision:",
|
||||||
|
"Issue date:",
|
||||||
|
"Sheet:",
|
||||||
|
"Language:",
|
||||||
|
"Title:",
|
||||||
|
"Approved by:",
|
||||||
|
"Created by:",
|
||||||
|
"Document type:",
|
||||||
|
"Part Material:",
|
||||||
|
"General tolerances:",
|
||||||
|
"Scale:",
|
||||||
|
"Test Bracket",
|
||||||
|
"PN-42",
|
||||||
|
"Alu 6082",
|
||||||
|
"B",
|
||||||
|
"1 : 2",
|
||||||
|
"EN",
|
||||||
|
"1 / 1",
|
||||||
|
"ISO 2768-m",
|
||||||
|
"Component Drawing",
|
||||||
|
):
|
||||||
|
self.assertIn(expect, texts)
|
||||||
|
|
||||||
|
def test_first_angle_projection_symbol(self):
|
||||||
|
# Two concentric circles at the symbol centre (273, 58) in sheet
|
||||||
|
# coords (SVG y-down 152 → 210 - 152 = 58).
|
||||||
|
circles = [
|
||||||
|
p
|
||||||
|
for p in self._prims()
|
||||||
|
if p.kind == "circle" and p.center == (273.0, 58.0)
|
||||||
|
]
|
||||||
|
self.assertEqual(len(circles), 2)
|
||||||
|
self.assertEqual(
|
||||||
|
sorted(c.radius for c in circles if c.radius), [2.5, 5.0]
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class TestSheetSizeNormalisation(unittest.TestCase):
|
||||||
|
"""Drawings stored as A3 in .fluency files load as A4 — the renderer
|
||||||
|
only supports the A4 ISO 5457 sheet."""
|
||||||
|
|
||||||
|
def test_legacy_a3_loads_as_a4(self):
|
||||||
|
from fluency.io.project_io import _technical_drawing_from_dict
|
||||||
|
|
||||||
|
d = _technical_drawing_from_dict(
|
||||||
|
{
|
||||||
|
"id": "x",
|
||||||
|
"source_kind": "component",
|
||||||
|
"source_id": "c",
|
||||||
|
"sheet_size": "A3",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
self.assertEqual(d.sheet_size, "A4")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
@@ -54,8 +54,9 @@ from fluency.technical_drawing import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
# Sheet layout constants.
|
# Sheet layout constants.
|
||||||
_A3_MM_W = 420.0
|
# Sheet layout constants (A4 landscape, ISO 5457 template).
|
||||||
_A3_MM_H = 297.0
|
_A4_MM_W = 297.0
|
||||||
|
_A4_MM_H = 210.0
|
||||||
|
|
||||||
|
|
||||||
# ── 2D geometry helpers (view-plane model coordinates) ────────────────────
|
# ── 2D geometry helpers (view-plane model coordinates) ────────────────────
|
||||||
@@ -202,7 +203,7 @@ class DrawingCanvas(QWidget):
|
|||||||
|
|
||||||
def _sheet_rect(self) -> QRectF:
|
def _sheet_rect(self) -> QRectF:
|
||||||
"""Sheet rect in device coordinates (zoom/pan applied)."""
|
"""Sheet rect in device coordinates (zoom/pan applied)."""
|
||||||
aspect = _A3_MM_W / _A3_MM_H
|
aspect = _A4_MM_W / _A4_MM_H
|
||||||
w = self.width()
|
w = self.width()
|
||||||
h = self.height()
|
h = self.height()
|
||||||
|
|
||||||
@@ -220,18 +221,18 @@ class DrawingCanvas(QWidget):
|
|||||||
def to_sheet(self, pos: QPointF) -> Tuple[float, float]:
|
def to_sheet(self, pos: QPointF) -> Tuple[float, float]:
|
||||||
"""Convert a device pixel position to sheet mm coordinates."""
|
"""Convert a device pixel position to sheet mm coordinates."""
|
||||||
rect = self._sheet_rect()
|
rect = self._sheet_rect()
|
||||||
scale = rect.width() / _A3_MM_W
|
scale = rect.width() / _A4_MM_W
|
||||||
if scale <= 0:
|
if scale <= 0:
|
||||||
return (0.0, 0.0)
|
return (0.0, 0.0)
|
||||||
return (
|
return (
|
||||||
(pos.x() - rect.x()) / scale,
|
(pos.x() - rect.x()) / scale,
|
||||||
_A3_MM_H - (pos.y() - rect.y()) / scale,
|
_A4_MM_H - (pos.y() - rect.y()) / scale,
|
||||||
)
|
)
|
||||||
|
|
||||||
def _pick_tolerance_mm(self) -> float:
|
def _pick_tolerance_mm(self) -> float:
|
||||||
"""Pick radius in sheet mm (about 6 device pixels)."""
|
"""Pick radius in sheet mm (about 6 device pixels)."""
|
||||||
rect = self._sheet_rect()
|
rect = self._sheet_rect()
|
||||||
scale = rect.width() / _A3_MM_W
|
scale = rect.width() / _A4_MM_W
|
||||||
return 6.0 / scale if scale > 0 else 6.0
|
return 6.0 / scale if scale > 0 else 6.0
|
||||||
|
|
||||||
def _sheet_to_model(
|
def _sheet_to_model(
|
||||||
|
|||||||
Reference in New Issue
Block a user