- tech draw draft v2
This commit is contained in:
Generated
+9
-12
@@ -6,10 +6,7 @@
|
||||
<component name="ChangeListManager">
|
||||
<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/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" />
|
||||
</list>
|
||||
<option name="SHOW_DIALOG" value="false" />
|
||||
@@ -122,14 +119,6 @@
|
||||
<option name="presentableId" value="Default" />
|
||||
<updated>1703867682707</updated>
|
||||
</task>
|
||||
<task id="LOCAL-00003" summary="- Sketch projection partly works again :)">
|
||||
<option name="closed" value="true" />
|
||||
<created>1735563255455</created>
|
||||
<option name="number" value="00003" />
|
||||
<option name="presentableId" value="LOCAL-00003" />
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1735563255455</updated>
|
||||
</task>
|
||||
<task id="LOCAL-00004" summary="- Sketch projection partly works again :)">
|
||||
<option name="closed" value="true" />
|
||||
<created>1735585968733</created>
|
||||
@@ -514,7 +503,15 @@
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1786910497589</updated>
|
||||
</task>
|
||||
<option name="localTasksCounter" value="52" />
|
||||
<task id="LOCAL-00052" summary="- tech draw draft v2">
|
||||
<option name="closed" value="true" />
|
||||
<created>1786984519780</created>
|
||||
<option name="number" value="00052" />
|
||||
<option name="presentableId" value="LOCAL-00052" />
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1786984519781</updated>
|
||||
</task>
|
||||
<option name="localTasksCounter" value="53" />
|
||||
<servers />
|
||||
</component>
|
||||
<component name="TypeScriptGeneratedFilesManager">
|
||||
|
||||
+475
-148
@@ -85,7 +85,13 @@ class DrawingRenderResult:
|
||||
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_id → (scale, sheet_cx, sheet_cy, angle_deg, cx, cy) with
|
||||
# sheet(p) = scale · R(angle_deg) · (p − (cx, cy)) + (sheet_cx, sheet_cy),
|
||||
# i.e. (sheet_cx, sheet_cy) is the sheet position of the view's
|
||||
# geometry centre and angle_deg is the view's sheet rotation (0/90).
|
||||
# Legacy 3-tuples (scale, offset_x, offset_y) — sheet(p) = p*scale +
|
||||
# offset — are still accepted by the consumers.
|
||||
view_transforms: Dict[str, Tuple[float, ...]] = field(default_factory=dict)
|
||||
|
||||
|
||||
# ── View presets ───────────────────────────────────────────────────────────
|
||||
@@ -325,7 +331,8 @@ def _assemble_view(
|
||||
view: DrawingView,
|
||||
slot: Optional[Tuple[float, float, float, float]] = None,
|
||||
scale_override: Optional[float] = None,
|
||||
transforms: Optional[Dict[str, Tuple[float, float, float]]] = None,
|
||||
transforms: Optional[Dict[str, Tuple[float, ...]]] = None,
|
||||
rotation: float = 0.0,
|
||||
) -> Tuple[Tuple[DrawingPrimitive, ...], Tuple[DrawingCandidate, ...], Tuple[str, ...]]:
|
||||
"""Fit projected edges into *slot* and emit primitives + candidates.
|
||||
|
||||
@@ -334,7 +341,12 @@ def _assemble_view(
|
||||
whole sheet. *scale_override* forces a specific model→sheet 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.
|
||||
transform is recorded under the view id as
|
||||
``(scale, sheet_cx, sheet_cy, angle_deg, cx, cy)`` — the sheet
|
||||
position of the geometry centre plus the rotation actually applied.
|
||||
*rotation* turns the projection 90° steps about its centre before
|
||||
fitting (0 or 90), so a long thin view can be drawn sideways to make
|
||||
the whole sheet layout fit better.
|
||||
"""
|
||||
warnings = list(warnings)
|
||||
primitives: List[DrawingPrimitive] = []
|
||||
@@ -344,6 +356,35 @@ def _assemble_view(
|
||||
warnings.append(f"View '{view.name or view.kind}': no projected edges")
|
||||
return tuple(primitives), tuple(candidates), tuple(warnings)
|
||||
|
||||
# Rotate the projection about its centre first: everything downstream
|
||||
# (fitting, candidates, direction vectors) then works on the rotated
|
||||
# frame with a plain uniform scale + translation, and rotation
|
||||
# preserves all measured lengths.
|
||||
min_x, min_y, max_x, max_y = _edges_bounds(edges)
|
||||
rcx = (min_x + max_x) / 2.0
|
||||
rcy = (min_y + max_y) / 2.0
|
||||
if rotation:
|
||||
th = math.radians(rotation)
|
||||
cos_t, sin_t = math.cos(th), math.sin(th)
|
||||
|
||||
def _rot_pt(p: Tuple[float, float]) -> Tuple[float, float]:
|
||||
dx, dy = p[0] - rcx, p[1] - rcy
|
||||
return (
|
||||
dx * cos_t - dy * sin_t + rcx,
|
||||
dx * sin_t + dy * cos_t + rcy,
|
||||
)
|
||||
|
||||
rotated_edges: List[Tuple[Tuple[float, float], Tuple[float, float], str, str]] = []
|
||||
for p1, p2, curve_type, style in edges:
|
||||
if curve_type == "circle_full":
|
||||
# p2 only encodes the radius as the x-offset of p1.
|
||||
r = p2[0] - p1[0]
|
||||
nc = _rot_pt(p1)
|
||||
rotated_edges.append((nc, (nc[0] + r, nc[1]), curve_type, style))
|
||||
else:
|
||||
rotated_edges.append((_rot_pt(p1), _rot_pt(p2), curve_type, style))
|
||||
edges = rotated_edges
|
||||
|
||||
# Separate geometry by curve type (model units).
|
||||
line_segments: List[Tuple[Tuple[float, float], Tuple[float, float]]] = []
|
||||
circle_data: List[Tuple[float, float, float]] = [] # (cx, cy, r)
|
||||
@@ -378,7 +419,17 @@ def _assemble_view(
|
||||
# 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)
|
||||
# Record o = offset + scale·c: the sheet position of the model
|
||||
# centre, so consumers can invert via
|
||||
# model = R(-angle)·(sheet − o)/scale + c.
|
||||
transforms[view_id] = (
|
||||
scale,
|
||||
offset_x + scale * rcx,
|
||||
offset_y + scale * rcy,
|
||||
float(rotation),
|
||||
rcx,
|
||||
rcy,
|
||||
)
|
||||
|
||||
def _to_sheet(x: float, y: float) -> Tuple[float, float]:
|
||||
return (x * scale + offset_x, y * scale + offset_y)
|
||||
@@ -400,6 +451,28 @@ def _assemble_view(
|
||||
view_id=view_id,
|
||||
)
|
||||
)
|
||||
# ISO 14128 centre mark: a thin cross extending just past the
|
||||
# circle, so the centre is visible and usable as a dimension
|
||||
# reference (centre-to-centre, centre-to-edge distances).
|
||||
sc = _to_sheet(cx, cy)
|
||||
sr = radius * scale
|
||||
ext = sr + max(2.0, sr * 0.15)
|
||||
primitives.append(
|
||||
DrawingPrimitive(
|
||||
kind="line",
|
||||
points=((sc[0] - ext, sc[1]), (sc[0] + ext, sc[1])),
|
||||
style="center",
|
||||
view_id=view_id,
|
||||
)
|
||||
)
|
||||
primitives.append(
|
||||
DrawingPrimitive(
|
||||
kind="line",
|
||||
points=((sc[0], sc[1] - ext), (sc[0], sc[1] + ext)),
|
||||
style="center",
|
||||
view_id=view_id,
|
||||
)
|
||||
)
|
||||
else:
|
||||
primitives.append(
|
||||
DrawingPrimitive(
|
||||
@@ -786,161 +859,383 @@ def _extract_angle_candidates(
|
||||
angle_count += 1
|
||||
|
||||
|
||||
# ── Drawing generation ─────────────────────────────────────────────────────
|
||||
# ── Sheet layout regions ────────────────────────────────────────────────────
|
||||
|
||||
_LAYOUT_MARGIN_MM = 10.0
|
||||
_VIEW_GAP_MM = 12.0
|
||||
|
||||
# Title block box (see _title_block_primitives): 180 × 52 at the bottom-right
|
||||
# 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 = (
|
||||
_LAYOUT_MARGIN_MM,
|
||||
_LAYOUT_MARGIN_MM,
|
||||
_A3_WIDTH_MM - _LAYOUT_MARGIN_MM,
|
||||
_A3_HEIGHT_MM - _LAYOUT_MARGIN_MM,
|
||||
)
|
||||
|
||||
# Regions the orthographic layout may occupy, as (x0, y0, w, h):
|
||||
# - UPPER: full sheet width above the title block
|
||||
# - LEFT: full sheet height in the left strip beside the title block
|
||||
_REGION_UPPER = (
|
||||
_LAYOUT_MARGIN_MM,
|
||||
_TB_TOP_MM + _TB_CLEARANCE_MM,
|
||||
_A3_WIDTH_MM - 2 * _LAYOUT_MARGIN_MM,
|
||||
_A3_HEIGHT_MM - _LAYOUT_MARGIN_MM - (_TB_TOP_MM + _TB_CLEARANCE_MM),
|
||||
)
|
||||
_REGION_LEFT = (
|
||||
_LAYOUT_MARGIN_MM,
|
||||
_LAYOUT_MARGIN_MM,
|
||||
_TB_LEFT_MM - _TB_CLEARANCE_MM - _LAYOUT_MARGIN_MM,
|
||||
_A3_HEIGHT_MM - 2 * _LAYOUT_MARGIN_MM,
|
||||
)
|
||||
|
||||
_MID_ORDER = ("left", "front", "right", "back")
|
||||
_COL_ORDER = ("top", "front", "bottom") # sheet top → bottom
|
||||
_GRID_ORDER = ("front", "right", "back", "top", "left", "bottom")
|
||||
|
||||
# Leave room for dimension lines: the shared scale fits the views into
|
||||
# this fraction of the region, so extension lines and labels have space
|
||||
# between and around the views instead of colliding with neighbours.
|
||||
_DIM_ROOM_FACTOR = 0.8
|
||||
_RectList = List[Tuple[str, float, float, float, float]]
|
||||
|
||||
|
||||
def _place_cross(
|
||||
dims: Dict[str, Tuple[float, float]], gap: float
|
||||
) -> _RectList:
|
||||
"""Classic third-angle cross: mid views run left→right (left, front,
|
||||
right, back), col views stack top→bottom (top, front, bottom), with the
|
||||
anchor view (front, or the first present one) at the intersection.
|
||||
|
||||
*dims* maps view id → ``(w, h)`` in sheet units. Returns local
|
||||
``(vid, x, y, w, h)`` rects (origin arbitrary — the caller centres the
|
||||
union on the sheet).
|
||||
"""
|
||||
mid = [k for k in _MID_ORDER if k in dims]
|
||||
col = [k for k in _COL_ORDER if k in dims]
|
||||
if not mid and not col:
|
||||
return []
|
||||
rects: Dict[str, Tuple[float, float, float, float]] = {}
|
||||
|
||||
if col:
|
||||
# Stack bottom → top.
|
||||
y = 0.0
|
||||
for k in reversed(col):
|
||||
w, h = dims[k]
|
||||
rects[k] = (0.0, y, w, h)
|
||||
y += h + gap
|
||||
col_h = y - gap
|
||||
cx = max(dims[k][0] for k in col) / 2.0
|
||||
for k in col:
|
||||
_x, yy, w, h = rects[k]
|
||||
rects[k] = (cx - w / 2.0, yy, w, h)
|
||||
else:
|
||||
col_h = 0.0
|
||||
cx = 0.0
|
||||
|
||||
anchor = "front" if "front" in dims else (mid[0] if mid else col[0])
|
||||
if anchor in rects:
|
||||
ax, ay, aw, _ah = rects[anchor]
|
||||
anchor_cy = ay + _ah / 2.0
|
||||
else:
|
||||
aw, ah = dims[anchor]
|
||||
ax = cx - aw / 2.0
|
||||
ay = col_h / 2.0 - ah / 2.0
|
||||
rects[anchor] = (ax, ay, aw, ah)
|
||||
anchor_cy = col_h / 2.0
|
||||
|
||||
ia = mid.index(anchor) if anchor in mid else -1
|
||||
x = ax
|
||||
for k in reversed(mid[:ia]):
|
||||
w, h = dims[k]
|
||||
x -= w + gap
|
||||
rects[k] = (x, anchor_cy - h / 2.0, w, h)
|
||||
x = ax + aw
|
||||
for k in mid[ia + 1 :]:
|
||||
w, h = dims[k]
|
||||
x += gap
|
||||
rects[k] = (x, anchor_cy - h / 2.0, w, h)
|
||||
x += w
|
||||
return [(k, *r) for k, r in rects.items()]
|
||||
|
||||
|
||||
def _place_swapped(dims: Dict[str, Tuple[float, float]], gap: float) -> _RectList:
|
||||
"""Cross with the view families swapped: the mid views stack vertically
|
||||
(left, front, right, back from the top) and the col views run
|
||||
horizontally (bottom, front, top from the left) — the classic cross
|
||||
turned a quarter turn, for sheets where that orientation fits more.
|
||||
"""
|
||||
mid = [k for k in _MID_ORDER if k in dims]
|
||||
col = [k for k in _COL_ORDER if k in dims]
|
||||
if not mid or not col:
|
||||
return []
|
||||
rects: Dict[str, Tuple[float, float, float, float]] = {}
|
||||
cx = max(dims[k][0] for k in mid) / 2.0
|
||||
y = 0.0
|
||||
for k in mid: # top → bottom
|
||||
w, h = dims[k]
|
||||
rects[k] = (cx - w / 2.0, y, w, h)
|
||||
y += h + gap
|
||||
anchor = "front" if "front" in dims else mid[0]
|
||||
anchor_cy = rects[anchor][1] + dims[anchor][1] / 2.0
|
||||
|
||||
row: Dict[str, Tuple[float, float, float, float]] = {}
|
||||
x = 0.0
|
||||
x_anchor = 0.0
|
||||
for k in reversed(col): # bottom, front, top → left to right
|
||||
w, h = dims[k]
|
||||
if k == anchor:
|
||||
x_anchor = x
|
||||
row[k] = (x, anchor_cy - h / 2.0, w, h)
|
||||
x += w + gap
|
||||
shift = rects[anchor][0] - x_anchor
|
||||
for k, r in row.items():
|
||||
if k == anchor:
|
||||
continue
|
||||
x0, y0, w, h = r
|
||||
rects[k] = (x0 + shift, y0, w, h)
|
||||
return [(k, *r) for k, r in rects.items()]
|
||||
|
||||
|
||||
def _place_grid(
|
||||
dims: Dict[str, Tuple[float, float]], gap: float, rows: int
|
||||
) -> _RectList:
|
||||
"""Wrap the present standard views into a grid of *rows* rows, filled
|
||||
bottom→top and left→right (so the primary views sit near the bottom,
|
||||
like in the cross)."""
|
||||
order = [k for k in _GRID_ORDER if k in dims]
|
||||
if not order:
|
||||
return []
|
||||
cols = max(1, -(-len(order) // rows))
|
||||
rects: _RectList = []
|
||||
y = 0.0
|
||||
for r in range(rows):
|
||||
chunk = order[r * cols : (r + 1) * cols]
|
||||
if not chunk:
|
||||
break
|
||||
x = 0.0
|
||||
row_h = 0.0
|
||||
for k in chunk:
|
||||
w, h = dims[k]
|
||||
rects.append((k, x, y, w, h))
|
||||
x += w + gap
|
||||
row_h = max(row_h, h)
|
||||
y += row_h + gap
|
||||
return rects
|
||||
|
||||
|
||||
|
||||
def _fit_scale(
|
||||
place: Callable[[Dict[str, Tuple[float, float]], float], _RectList],
|
||||
dims_m: Dict[str, Tuple[float, float]],
|
||||
region: Tuple[float, float, float, float],
|
||||
) -> float:
|
||||
"""Largest shared scale at which *dims_m* (model units) laid out by
|
||||
*place* fits the ``(x0, y0, w, h)`` *region* of the sheet.
|
||||
|
||||
Every placer scales its output linearly with the input dims, so the
|
||||
union size at scale s is s times its size at scale 1 — the fit is the
|
||||
closed-form ``min(region_w / u_w, region_h / u_h)``.
|
||||
"""
|
||||
_rx0, _ry0, rw, rh = region
|
||||
rects = place(dims_m, _VIEW_GAP_MM)
|
||||
if not rects:
|
||||
return 0.0
|
||||
minx = min(r[1] for r in rects)
|
||||
miny = min(r[2] for r in rects)
|
||||
maxx = max(r[1] + r[3] for r in rects)
|
||||
maxy = max(r[2] + r[4] for r in rects)
|
||||
uw = max(maxx - minx, 1e-9)
|
||||
uh = max(maxy - miny, 1e-9)
|
||||
return min(rw / uw, rh / uh)
|
||||
|
||||
def _free_rects(
|
||||
used_rects: Sequence[Tuple[float, float, float, float]]
|
||||
) -> List[Tuple[float, float, float, float]]:
|
||||
"""Axis-aligned free rects ``(x0, y0, w, h)`` around *used_rects*,
|
||||
clearing the sheet border and the title block zone."""
|
||||
ix0, iy0, ix1, iy1 = _SHEET_INNER
|
||||
if used_rects:
|
||||
ux0 = min(r[0] for r in used_rects)
|
||||
uy0 = min(r[1] for r in used_rects)
|
||||
ux1 = max(r[0] + r[2] for r in used_rects)
|
||||
uy1 = max(r[1] + r[3] for r in used_rects)
|
||||
cands = [
|
||||
(ux1 + _VIEW_GAP_MM, iy0, ix1, iy1), # right of the used block
|
||||
(ix0, iy0, ux0 - _VIEW_GAP_MM, iy1), # left
|
||||
(ix0, uy1 + _VIEW_GAP_MM, ix1, iy1), # above
|
||||
(ix0, iy0, ix1, uy0 - _VIEW_GAP_MM), # below
|
||||
]
|
||||
else:
|
||||
cands = [(ix0, iy0, ix1, iy1)]
|
||||
tb = (
|
||||
_TB_LEFT_MM - _TB_CLEARANCE_MM,
|
||||
0.0,
|
||||
_A3_WIDTH_MM - (_TB_LEFT_MM - _TB_CLEARANCE_MM),
|
||||
_TB_TOP_MM + _TB_CLEARANCE_MM,
|
||||
)
|
||||
out: List[Tuple[float, float, float, float]] = []
|
||||
for x0, y0, x1, y1 in cands:
|
||||
x0, y0 = max(x0, ix0), max(y0, iy0)
|
||||
x1, y1 = min(x1, ix1), min(y1, iy1)
|
||||
if x1 - x0 < 1.0 or y1 - y0 < 1.0:
|
||||
continue
|
||||
if not (x1 <= tb[0] or tb[2] <= x0 or y1 <= tb[1] or tb[3] <= y0):
|
||||
# Overlaps the title block zone — keep the parts above/left of it.
|
||||
subs = [
|
||||
(x0, max(y0, tb[3]), x1, y1),
|
||||
(x0, y0, min(x1, tb[0]), y1),
|
||||
]
|
||||
else:
|
||||
subs = [(x0, y0, x1, y1)]
|
||||
for sx0, sy0, sx1, sy1 in subs:
|
||||
if sx1 - sx0 > 1.0 and sy1 - sy0 > 1.0:
|
||||
out.append((sx0, sy0, sx1 - sx0, sy1 - sy0))
|
||||
return out
|
||||
|
||||
|
||||
def _layout_views_on_sheet(
|
||||
views: Sequence[DrawingView],
|
||||
bboxes: Dict[str, Tuple[float, float, float, float]],
|
||||
) -> Tuple[Dict[str, Tuple[float, float, float, float]], Optional[float]]:
|
||||
"""Compute a slot rectangle for each view in standard orthographic layout.
|
||||
) -> Tuple[
|
||||
Dict[str, Tuple[float, float, float, float]],
|
||||
Optional[float],
|
||||
Dict[str, float],
|
||||
]:
|
||||
"""Compute a slot rectangle and sheet rotation for each view.
|
||||
|
||||
*bboxes* maps view_id → ``(min_x, min_y, max_x, max_y)`` in model
|
||||
units (from :func:`_edges_bounds`). Returns ``(slots, common_scale)``:
|
||||
slots are ``(left, bottom, width, height)`` in sheet mm (origin at the
|
||||
sheet's bottom-left corner, +y up).
|
||||
units (from :func:`_edges_bounds`). Returns ``(slots, common_scale,
|
||||
rotations)``: slots are ``(left, bottom, width, height)`` in sheet mm
|
||||
(origin at the sheet's bottom-left corner, +y up), common_scale is the
|
||||
shared model→sheet scale of the standard orthographic views, and
|
||||
rotations maps view_id → sheet rotation in degrees (0 or 90).
|
||||
|
||||
Layout (third-angle projection, aligned projections)::
|
||||
|
||||
top isometric
|
||||
left front right back
|
||||
bottom
|
||||
|
||||
All orthographic views share one scale (the tightest fit that keeps
|
||||
every projection in its footprint) so the views stay mutually
|
||||
consistent, and each view is centred in its allotted space.
|
||||
The sheet is filled, not just used: every candidate arrangement
|
||||
(classic third-angle cross, the cross with the view families swapped,
|
||||
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
|
||||
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
|
||||
fewer rotated views, then the more conventional arrangement, so layouts
|
||||
stay stable and standard whenever they are already the best fit. The
|
||||
shared scale is further reduced to leave room for dimension lines
|
||||
between and around the views. All orthographic views share one scale
|
||||
so the projections stay mutually consistent. Isometric and custom
|
||||
views take the largest remaining free rect (clearing the title block).
|
||||
"""
|
||||
if not views:
|
||||
return {}, None
|
||||
|
||||
margin = 10.0
|
||||
gap = 12.0
|
||||
title_block_h = 55.0
|
||||
full_w = _A3_WIDTH_MM - 2 * margin
|
||||
# The bottom view sits at the bottom of the projection column, so the
|
||||
# whole column stays clear of the title block (bottom-right corner).
|
||||
col_bottom = margin + title_block_h
|
||||
col_top = _A3_HEIGHT_MM - margin
|
||||
col_avail = col_top - col_bottom
|
||||
|
||||
def dims(vid: str) -> Tuple[float, float]:
|
||||
b = bboxes.get(vid)
|
||||
if b is None:
|
||||
return 1.0, 1.0
|
||||
return max(b[2] - b[0], 1e-6), max(b[3] - b[1], 1e-6)
|
||||
|
||||
ortho_kinds = [
|
||||
v.kind for v in views if v.kind in _STANDARD_VIEWS and v.kind != "isometric"
|
||||
]
|
||||
mid_order = ["left", "front", "right", "back"]
|
||||
col_order = ["top", "front", "bottom"] # top → bottom
|
||||
present_mid = [k for k in mid_order if k in ortho_kinds]
|
||||
present_col = [k for k in col_order if k in ortho_kinds]
|
||||
|
||||
slots: Dict[str, Tuple[float, float, float, float]] = {}
|
||||
rotations: Dict[str, float] = {}
|
||||
common_scale: Optional[float] = None
|
||||
|
||||
if present_mid or present_col:
|
||||
row_w = sum(dims(k)[0] for k in present_mid)
|
||||
col_h = sum(dims(k)[1] for k in present_col)
|
||||
scale_opts: List[float] = []
|
||||
if present_mid:
|
||||
scale_opts.append((full_w - gap * (len(present_mid) - 1)) / row_w)
|
||||
if present_col:
|
||||
scale_opts.append((col_avail - gap * (len(present_col) - 1)) / col_h)
|
||||
common_scale = min(scale_opts)
|
||||
ortho = [
|
||||
v for v in views if v.kind in _STANDARD_VIEWS and v.kind != "isometric"
|
||||
]
|
||||
|
||||
# Middle row: left → front → right → back, centred on the sheet.
|
||||
total_row = row_w * common_scale + gap * (len(present_mid) - 1)
|
||||
x = margin + (full_w - total_row) / 2.0
|
||||
row_slots: Dict[str, Tuple[float, float, float]] = {}
|
||||
for k in present_mid:
|
||||
w, h = dims(k)
|
||||
row_slots[k] = (x, w * common_scale, h * common_scale)
|
||||
x += w * common_scale + gap
|
||||
|
||||
# Column: top → front → bottom, stacked from the top edge down and
|
||||
# centred in the available column (which stays clear of the title
|
||||
# block).
|
||||
total_col = col_h * common_scale + gap * (len(present_col) - 1)
|
||||
y = col_bottom + col_avail - (col_avail - total_col) / 2.0
|
||||
col_slots: Dict[str, Tuple[float, float, float]] = {}
|
||||
for k in present_col:
|
||||
w, h = dims(k)
|
||||
sh = h * common_scale
|
||||
col_slots[k] = (y - sh, w * common_scale, sh)
|
||||
y -= sh + gap
|
||||
|
||||
anchor = (
|
||||
"front"
|
||||
if "front" in present_mid
|
||||
else (present_mid[0] if present_mid else present_col[0])
|
||||
)
|
||||
if anchor in row_slots:
|
||||
ax, aw, ah = row_slots[anchor]
|
||||
ay = (
|
||||
col_slots[anchor][0]
|
||||
if anchor in col_slots
|
||||
else col_bottom + (col_avail - ah) / 2.0
|
||||
)
|
||||
used_rects: List[Tuple[float, float, float, float]] = []
|
||||
if ortho:
|
||||
# Nominal (scale-1) sizes: the per-view ``scale`` factor is applied
|
||||
# at assembly time on top of the shared scale, exactly as before.
|
||||
dims0: Dict[str, Tuple[float, float]] = {}
|
||||
for v in ortho:
|
||||
b = bboxes.get(v.kind)
|
||||
if b is None:
|
||||
dims0[v.kind] = (1.0, 1.0)
|
||||
else:
|
||||
ay, aw, ah = col_slots[anchor]
|
||||
ax = margin + (full_w - aw) / 2.0
|
||||
anchor_cx = ax + aw / 2.0
|
||||
|
||||
for k in set(present_mid) | set(present_col):
|
||||
w, h = dims(k)
|
||||
sw, sh = w * common_scale, h * common_scale
|
||||
if k in row_slots and k in col_slots:
|
||||
sx = row_slots[k][0]
|
||||
sy = col_slots[k][0]
|
||||
elif k in row_slots:
|
||||
# Mid-row view without a column slot: centre on the anchor.
|
||||
sx = row_slots[k][0]
|
||||
sy = ay + (ah - sh) / 2.0
|
||||
else:
|
||||
# Column view without a mid slot: align with the anchor.
|
||||
sx = anchor_cx - sw / 2.0
|
||||
sy = col_slots[k][0]
|
||||
slots[k] = (sx, sy, sw, sh)
|
||||
|
||||
# Isometric: free region to the right of the main block.
|
||||
if any(v.kind == "isometric" for v in views):
|
||||
iso_x0 = ax + aw + gap
|
||||
if "top" in col_slots:
|
||||
iso_x0 = max(iso_x0, anchor_cx + col_slots["top"][1] / 2.0 + gap)
|
||||
iso_y0 = ay + ah + gap
|
||||
iso_x1 = _A3_WIDTH_MM - margin
|
||||
iso_y1 = _A3_HEIGHT_MM - margin
|
||||
if iso_x1 - iso_x0 < 30.0 or iso_y1 - iso_y0 < 30.0:
|
||||
# No room at the right — fall back to the bottom-left corner.
|
||||
left_x = slots.get("left", (margin + full_w * 0.5,))[0]
|
||||
iso_x1 = min(iso_x1, left_x - gap)
|
||||
bottom_y = slots.get("bottom", (0.0, col_bottom + col_avail * 0.5, 0, 0))[1]
|
||||
iso_y1 = min(iso_y1, bottom_y - gap)
|
||||
slots["isometric"] = (
|
||||
iso_x0,
|
||||
iso_y0,
|
||||
max(iso_x1 - iso_x0, 10.0),
|
||||
max(iso_y1 - iso_y0, 10.0),
|
||||
dims0[v.kind] = (
|
||||
max(b[2] - b[0], 1e-6),
|
||||
max(b[3] - b[1], 1e-6),
|
||||
)
|
||||
keys = list(dims0)
|
||||
arrangements: Tuple[
|
||||
Tuple[str, int, Callable[[Dict[str, Tuple[float, float]], float], _RectList]]
|
||||
] = (
|
||||
("cross", 0, _place_cross),
|
||||
("swapped", 1, _place_swapped),
|
||||
("grid2", 2, lambda d, g: _place_grid(d, g, 2)),
|
||||
("grid3", 3, lambda d, g: _place_grid(d, g, 3)),
|
||||
)
|
||||
cands: List[
|
||||
Tuple[float, int, int, Tuple[float, float, float, float],
|
||||
Dict[str, Tuple[float, float]],
|
||||
Callable[[Dict[str, Tuple[float, float]], float], _RectList],
|
||||
List[bool]]
|
||||
] = []
|
||||
# Rotated views are a packing tool for multi-view layouts; with
|
||||
# 1–2 views the projection is kept upright and only scaled to fit.
|
||||
n_masks = 1 << len(keys) if len(keys) >= 3 else 1
|
||||
# Shrink each region for dimension clearance (views are centred in
|
||||
# the full region, so this only reduces the scale).
|
||||
upper = _REGION_UPPER[:2] + tuple(
|
||||
d * _DIM_ROOM_FACTOR for d in _REGION_UPPER[2:]
|
||||
)
|
||||
left = _REGION_LEFT[:2] + tuple(
|
||||
d * _DIM_ROOM_FACTOR for d in _REGION_LEFT[2:]
|
||||
)
|
||||
for mask in range(n_masks):
|
||||
rotated = [bool(mask & (1 << i)) for i in range(len(keys))]
|
||||
dims_m = {
|
||||
k: (
|
||||
dims0[k][1] if rotated[i] else dims0[k][0],
|
||||
dims0[k][0] if rotated[i] else dims0[k][1],
|
||||
)
|
||||
for i, k in enumerate(keys)
|
||||
}
|
||||
for _name, rank, place in arrangements:
|
||||
s_up = _fit_scale(place, dims_m, upper)
|
||||
s_left = _fit_scale(place, dims_m, left)
|
||||
if s_up >= s_left:
|
||||
s, region = s_up, _REGION_UPPER
|
||||
else:
|
||||
# No standard ortho views — give the isometric most of the sheet.
|
||||
if any(v.kind == "isometric" for v in views):
|
||||
slots["isometric"] = (margin, margin, full_w * 0.55, col_avail)
|
||||
s, region = s_left, _REGION_LEFT
|
||||
if s <= 0.0:
|
||||
continue
|
||||
cands.append((s, sum(rotated), rank, region, dims_m, place, rotated))
|
||||
if cands:
|
||||
best_s = max(c[0] for c in cands)
|
||||
s, _nrot, _rank, region, dims_m, place, rotated = min(
|
||||
(c for c in cands if c[0] >= best_s * 0.995),
|
||||
key=lambda c: (c[1], c[2], -c[0]),
|
||||
)
|
||||
rx0, _ry0, rw, rh = region
|
||||
ds = {k: (w * s, h * s) for k, (w, h) in dims_m.items()}
|
||||
rects = place(ds, _VIEW_GAP_MM)
|
||||
minx = min(r[1] for r in rects)
|
||||
miny = min(r[2] for r in rects)
|
||||
maxx = max(r[1] + r[3] for r in rects)
|
||||
maxy = max(r[2] + r[4] for r in rects)
|
||||
ox = rx0 + (rw - (maxx - minx)) / 2.0
|
||||
oy = _ry0 + (rh - (maxy - miny)) / 2.0
|
||||
for vid, x, y, w, h in rects:
|
||||
slots[vid] = (x - minx + ox, y - miny + oy, w, h)
|
||||
rotations[vid] = 90.0 if rotated[keys.index(vid)] else 0.0
|
||||
common_scale = s
|
||||
used_rects = [(ox, oy, maxx - minx, maxy - miny)]
|
||||
|
||||
# Custom (non-standard) views fill the bottom-left corner.
|
||||
custom = [v for v in views if v.kind not in _STANDARD_VIEWS]
|
||||
if custom:
|
||||
left_edge = slots.get("left", (margin + full_w * 0.4,))[0]
|
||||
bottom_edge = slots.get("bottom", (0.0, col_bottom + col_avail * 0.4, 0, 0))[1]
|
||||
cw = max(left_edge - margin - gap, 60.0)
|
||||
ch = max(bottom_edge - margin - gap, 60.0)
|
||||
for i, v in enumerate(custom):
|
||||
vid = v.name or v.id
|
||||
slots[vid] = (margin, margin + i * (ch + gap), cw, ch)
|
||||
# Isometric and custom views: the largest remaining free rect each,
|
||||
# clearing the title block.
|
||||
extra = [
|
||||
v for v in views
|
||||
if v.kind == "isometric" or v.kind not in _STANDARD_VIEWS
|
||||
]
|
||||
assigned = list(used_rects)
|
||||
for i, v in enumerate(extra):
|
||||
vid = v.kind if v.kind in _STANDARD_VIEWS else (v.name or v.id)
|
||||
free = _free_rects(assigned)
|
||||
if free:
|
||||
slot = max(free, key=lambda r: r[2] * r[3])
|
||||
else:
|
||||
# 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)
|
||||
slots[vid] = slot
|
||||
assigned.append(slot)
|
||||
|
||||
return slots, common_scale
|
||||
return slots, common_scale, rotations
|
||||
|
||||
|
||||
# ── Dimension selection & placement ───────────────────────────────────────
|
||||
@@ -1365,7 +1660,7 @@ _MANUAL_DIMENSION_KINDS = ("length", "diameter", "angle")
|
||||
|
||||
def build_manual_candidates(
|
||||
drawing: TechnicalDrawing,
|
||||
view_transforms: Dict[str, Tuple[float, float, float]],
|
||||
view_transforms: Dict[str, Tuple[float, ...]],
|
||||
) -> Tuple[List[DrawingCandidate], List[str], List[str]]:
|
||||
"""Convert user-placed annotations into renderable dimension candidates.
|
||||
|
||||
@@ -1389,10 +1684,33 @@ def build_manual_candidates(
|
||||
if transform is None:
|
||||
unresolved.append(ann.id)
|
||||
continue
|
||||
scale, offset_x, offset_y = transform
|
||||
scale, ox, oy = transform[0], transform[1], transform[2]
|
||||
if len(transform) >= 6:
|
||||
# New form: sheet(p) = s·R(θ)(p − c) + o, where o is the sheet
|
||||
# position of the geometry centre c and θ the view rotation.
|
||||
th = math.radians(transform[3])
|
||||
cos_t, sin_t = math.cos(th), math.sin(th)
|
||||
rcx, rcy = transform[4], transform[5]
|
||||
|
||||
def to_sheet(pt: Tuple[float, float]) -> Tuple[float, float]:
|
||||
return (pt[0] * scale + offset_x, pt[1] * scale + offset_y)
|
||||
dx, dy = pt[0] - rcx, pt[1] - rcy
|
||||
return (
|
||||
(dx * cos_t - dy * sin_t) * scale + ox,
|
||||
(dx * sin_t + dy * cos_t) * scale + oy,
|
||||
)
|
||||
|
||||
def rot_dir(d: Tuple[float, float]) -> Tuple[float, float]:
|
||||
return (
|
||||
d[0] * cos_t - d[1] * sin_t,
|
||||
d[0] * sin_t + d[1] * cos_t,
|
||||
)
|
||||
else:
|
||||
|
||||
def to_sheet(pt: Tuple[float, float]) -> Tuple[float, float]:
|
||||
return (pt[0] * scale + ox, pt[1] * scale + oy)
|
||||
|
||||
def rot_dir(d: Tuple[float, float]) -> Tuple[float, float]:
|
||||
return d
|
||||
|
||||
a0, a1 = ann.anchors[0], ann.anchors[1]
|
||||
if ann.dimension_kind == "angle":
|
||||
@@ -1439,7 +1757,7 @@ def build_manual_candidates(
|
||||
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)
|
||||
direction: Tuple[float, float] = rot_dir((dx / mag, dy / mag))
|
||||
label = f"{value:.{_DISPLAY_PRECISION}f}"
|
||||
else: # diameter
|
||||
direction = ()
|
||||
@@ -1510,7 +1828,9 @@ def generate_drawing(
|
||||
_vid_of(view): _edges_bounds(projections[_vid_of(view)][0])
|
||||
for view in drawing.views
|
||||
}
|
||||
view_slots, common_scale = _layout_views_on_sheet(drawing.views, bboxes)
|
||||
view_slots, common_scale, view_rotations = _layout_views_on_sheet(
|
||||
drawing.views, bboxes
|
||||
)
|
||||
|
||||
ortho_vids = {
|
||||
_vid_of(v)
|
||||
@@ -1519,7 +1839,7 @@ def generate_drawing(
|
||||
}
|
||||
|
||||
# Per-view model→sheet transforms, filled in by _assemble_view.
|
||||
transforms: Dict[str, Tuple[float, float, float]] = {}
|
||||
transforms: Dict[str, Tuple[float, ...]] = {}
|
||||
|
||||
for view in drawing.views:
|
||||
vid = _vid_of(view)
|
||||
@@ -1529,7 +1849,13 @@ def generate_drawing(
|
||||
common_scale * view.scale if vid in ortho_vids and common_scale else None
|
||||
)
|
||||
prims, cands, vwarns = _assemble_view(
|
||||
edges, view_warnings, view, slot, scale_override, transforms
|
||||
edges,
|
||||
view_warnings,
|
||||
view,
|
||||
slot,
|
||||
scale_override,
|
||||
transforms,
|
||||
rotation=view_rotations.get(vid, 0.0),
|
||||
)
|
||||
all_primitives.extend(prims)
|
||||
all_candidates.extend(cands)
|
||||
@@ -1683,6 +2009,7 @@ def render_drawing(
|
||||
style_pens = {
|
||||
"visible": QPen(QColor(0, 0, 0), 1.5),
|
||||
"hidden": QPen(QColor(128, 128, 128), 1.0),
|
||||
"center": QPen(QColor(0, 0, 0), 0.35),
|
||||
"construction": QPen(QColor(0, 0, 255), 0.5),
|
||||
"dimension": QPen(QColor(0, 0, 0), 1.0),
|
||||
}
|
||||
|
||||
@@ -243,7 +243,16 @@ class DrawingCanvas(QWidget):
|
||||
t = self._render_result.view_transforms.get(view_id)
|
||||
if t is None or t[0] <= 0:
|
||||
return None
|
||||
scale, ox, oy = t
|
||||
scale, ox, oy = t[0], t[1], t[2]
|
||||
if len(t) >= 6:
|
||||
# sheet(p) = s·R(θ)(p − c) + o → invert around the centre.
|
||||
th = math.radians(t[3])
|
||||
cos_t, sin_t = math.cos(th), math.sin(th)
|
||||
sx, sy = (pt[0] - ox) / scale, (pt[1] - oy) / scale
|
||||
return (
|
||||
sx * cos_t + sy * sin_t + t[4],
|
||||
-sx * sin_t + sy * cos_t + t[5],
|
||||
)
|
||||
return ((pt[0] - ox) / scale, (pt[1] - oy) / scale)
|
||||
|
||||
def _pick_feature(self, pos: QPointF) -> Optional[dict]:
|
||||
@@ -283,12 +292,19 @@ class DrawingCanvas(QWidget):
|
||||
"p2": m2,
|
||||
}
|
||||
elif prim.kind == "circle" and prim.center and prim.radius:
|
||||
d = abs(
|
||||
math.hypot(
|
||||
pt[0] - prim.center[0], pt[1] - prim.center[1]
|
||||
)
|
||||
- prim.radius
|
||||
)
|
||||
dc = math.hypot(pt[0] - prim.center[0], pt[1] - prim.center[1])
|
||||
if dc <= tol:
|
||||
# Hit the circle's centre mark: a point feature — circle
|
||||
# centres are first-class dimension references.
|
||||
mc = self._sheet_to_model(prim.view_id, prim.center)
|
||||
if mc:
|
||||
return {
|
||||
"kind": "point",
|
||||
"view_id": prim.view_id,
|
||||
"point": mc,
|
||||
"radius": prim.radius / scale,
|
||||
}
|
||||
d = abs(dc - prim.radius)
|
||||
if d < best_d:
|
||||
best_d = d
|
||||
mc = self._sheet_to_model(prim.view_id, prim.center)
|
||||
@@ -506,19 +522,11 @@ class TechnicalDrawingWidget(QWidget):
|
||||
(
|
||||
"distance",
|
||||
"Distance",
|
||||
"Pick two edges — the dimension line is placed "
|
||||
"perpendicular to them, measuring the distance between",
|
||||
),
|
||||
(
|
||||
"diameter",
|
||||
"Diameter",
|
||||
"Pick a circle — its diameter is added",
|
||||
),
|
||||
(
|
||||
"angle",
|
||||
"Angle",
|
||||
"Pick two edges — the angle between them is added",
|
||||
"Pick two edges, an edge and a circle centre, or two "
|
||||
"circle centres — measures the distance between them",
|
||||
),
|
||||
("diameter", "Diameter", "Pick a circle — its diameter is added"),
|
||||
("angle", "Angle", "Pick two edges — the angle between them is added"),
|
||||
):
|
||||
btn = QPushButton(label)
|
||||
btn.setCheckable(True)
|
||||
@@ -818,7 +826,7 @@ class TechnicalDrawingWidget(QWidget):
|
||||
self._first_pick = None
|
||||
self._canvas.set_pick_mode(tool)
|
||||
prompts = {
|
||||
"distance": "Distance: click the first edge",
|
||||
"distance": "Distance: click an edge or a circle centre",
|
||||
"diameter": "Diameter: click a circle",
|
||||
"angle": "Angle: click the first edge",
|
||||
}
|
||||
@@ -883,6 +891,40 @@ class TechnicalDrawingWidget(QWidget):
|
||||
elif mode in ("distance", "angle"):
|
||||
self._on_edge_pick(info, mode)
|
||||
|
||||
@staticmethod
|
||||
def _pick_point(info: dict) -> Optional[Tuple[float, float]]:
|
||||
"""The measurable point of a pick: a point pick is its point, a
|
||||
circle pick counts as its centre; a bare segment is None."""
|
||||
if info.get("kind") == "point":
|
||||
return info["point"]
|
||||
if info.get("kind") == "circle":
|
||||
return info["center"]
|
||||
return None
|
||||
|
||||
@classmethod
|
||||
def _distance_anchors(
|
||||
cls, first: dict, second: dict
|
||||
) -> Tuple[Tuple[float, float], Tuple[float, float]]:
|
||||
"""Anchor pair for a distance between two picks (model coords).
|
||||
|
||||
A pick may be a segment (edge), a circle (measured at its centre)
|
||||
or a point (a picked circle centre). Point/segment mixes use the
|
||||
closest point on the segment so the dimension lands perpendicular
|
||||
to the edge, ISO style.
|
||||
"""
|
||||
fp = cls._pick_point(first)
|
||||
sp = cls._pick_point(second)
|
||||
if fp is not None and sp is not None:
|
||||
return fp, sp
|
||||
if fp is not None:
|
||||
return fp, _closest_point_on_segment(fp, second["p1"], second["p2"])
|
||||
if sp is not None:
|
||||
return sp, _closest_point_on_segment(sp, first["p1"], first["p2"])
|
||||
q1, q2, _d = _closest_points_on_segments(
|
||||
first["p1"], first["p2"], second["p1"], second["p2"]
|
||||
)
|
||||
return q1, q2
|
||||
|
||||
def _on_edge_pick(self, info: dict, mode: str) -> None:
|
||||
if (
|
||||
self._first_pick is None
|
||||
@@ -891,16 +933,16 @@ class TechnicalDrawingWidget(QWidget):
|
||||
# First edge (or picked in a different view: restart there).
|
||||
self._first_pick = info
|
||||
self._status_label.setText(
|
||||
"Select the second edge in the same view (Esc cancels)"
|
||||
"Select the second feature in the same view (Esc cancels)"
|
||||
)
|
||||
return
|
||||
|
||||
a1, a2 = self._first_pick["p1"], self._first_pick["p2"]
|
||||
b1, b2 = info["p1"], info["p2"]
|
||||
first = self._first_pick
|
||||
view_id = info["view_id"]
|
||||
|
||||
if mode == "distance":
|
||||
q1, q2, dist = _closest_points_on_segments(a1, a2, b1, b2)
|
||||
p1, p2 = self._distance_anchors(first, info)
|
||||
dist = math.hypot(p2[0] - p1[0], p2[1] - p1[1])
|
||||
if dist < 0.01:
|
||||
self._status_label.setText(
|
||||
"The two edges coincide — no distance to measure"
|
||||
@@ -908,12 +950,20 @@ class TechnicalDrawingWidget(QWidget):
|
||||
return
|
||||
self._add_manual_dimension(
|
||||
"length",
|
||||
anchors=(q1, q2),
|
||||
anchors=(p1, p2),
|
||||
view_id=view_id,
|
||||
direction=((q2[0] - q1[0]) / dist, (q2[1] - q1[1]) / dist),
|
||||
direction=((p2[0] - p1[0]) / dist, (p2[1] - p1[1]) / dist),
|
||||
done_msg=f"Distance dimension added: {dist:.2f}",
|
||||
)
|
||||
else: # angle
|
||||
if first.get("kind") != "segment" or info.get("kind") != "segment":
|
||||
self._status_label.setText(
|
||||
"Angle needs two edges — cancel and pick edge lines"
|
||||
)
|
||||
self._first_pick = None
|
||||
return
|
||||
a1, a2 = first["p1"], first["p2"]
|
||||
b1, b2 = info["p1"], info["p2"]
|
||||
vertex = _line_intersection(a1, a2, b1, b2)
|
||||
if vertex is None:
|
||||
self._status_label.setText(
|
||||
@@ -937,6 +987,17 @@ class TechnicalDrawingWidget(QWidget):
|
||||
)
|
||||
|
||||
def _on_diameter_pick(self, info: dict) -> None:
|
||||
if info.get("kind") == "point" and info.get("radius"):
|
||||
# Picked the centre mark of a circle.
|
||||
cx, cy = info["point"]
|
||||
r = info["radius"]
|
||||
self._add_manual_dimension(
|
||||
"diameter",
|
||||
anchors=((cx - r, cy), (cx + r, cy)),
|
||||
view_id=info["view_id"],
|
||||
done_msg=f"Diameter dimension added: Ø{2 * r:.2f}",
|
||||
)
|
||||
return
|
||||
if info.get("kind") != "circle":
|
||||
return
|
||||
cx, cy = info["center"]
|
||||
|
||||
Reference in New Issue
Block a user