- tech drawing and render improv

This commit is contained in:
bklronin
2026-08-18 15:06:51 +02:00
parent 813ddc3596
commit 67b73c13b8
10 changed files with 1886 additions and 54 deletions
+159
View File
@@ -0,0 +1,159 @@
"""Smoke test: layout optimizer fills the page, no overlaps, title block clear."""
import math
import os
import sys
os.environ.setdefault("QT_QPA_PLATFORM", "offscreen")
sys.path.insert(0, "/Volumes/Data_drive/Programming/fluency/src")
from fluency.models.data_model import DrawingView
from fluency.technical_drawing import (
_layout_views_on_sheet,
build_manual_candidates,
)
A3W, A3H = 420.0, 297.0
TB = (235.0, 0.0, 420.0, 62.0) # title block zone incl. clearance
def b(x, y, w, h):
return (x, y, x + w, y + h)
def overlaps(r1, r2, clear=0.0):
x0, y0, w, h = r1
x1, y1, w2, h2 = r2
return not (x0 + w <= x1 + clear or x1 + w2 <= x0 + clear
or y0 + h <= y1 + clear or y1 + h2 <= y0 + clear)
def tb_overlap(r, clear=5.0):
x0, y0, w, h = r
x1, y1, w2, h2 = TB
return not (x0 + w <= x1 + clear or x1 + w2 <= x0 + clear
or y0 + h <= y1 + clear or y1 + h2 <= y0 + clear)
def union_rect(rects):
x0 = min(r[0] for r in rects)
y0 = min(r[1] for r in rects)
x1 = max(r[0] + r[2] for r in rects)
y1 = max(r[1] + r[3] for r in rects)
return (x0, y0, x1 - x0, y1 - y0)
def check(name, kinds, boxes, expect_rot=None):
views = [DrawingView(kind=k) for k in kinds]
bboxes = {k: boxes[k] for k in kinds}
slots, scale, rots = _layout_views_on_sheet(views, bboxes)
print(f"--- {name}: scale={scale:.4f} rots={rots}")
# all slots within sheet
for k, s in slots.items():
assert 10 - 1e-6 <= s[0] and 10 - 1e-6 <= s[1], f"{k} outside sheet {s}"
assert s[0] + s[2] <= A3W - 10 + 1e-6, f"{k} beyond right {s}"
assert s[1] + s[3] <= A3H - 10 + 1e-6, f"{k} beyond top {s}"
# no overlaps between slots
ks = list(slots)
for i in range(len(ks)):
for j in range(i + 1, len(ks)):
assert not overlaps(slots[ks[i]], slots[ks[j]], 11.9), \
f"{ks[i]} overlaps {ks[j]}: {slots[ks[i]]} / {slots[ks[j]]}"
# title block clear
for k, s in slots.items():
assert not tb_overlap(s), f"{k} intrudes title block {s}"
# fill report
u = union_rect(list(slots.values()))
area = u[2] * u[3]
print(f" union: x0={u[0]:.1f} y0={u[1]:.1f} w={u[2]:.1f} h={u[3]:.1f} "
f"area={area:.0f}mm^2 ({100*area/(A3W*A3H):.0f}% of sheet)")
for k in ks:
print(f" {k}: {tuple(round(v,1) for v in slots[k])}")
if expect_rot is not None:
assert rots == expect_rot, f"expected {expect_rot}, got {rots}"
return slots, scale, rots
# 1. Two square views (front+top): should fill the page, no rotation.
check("two square", ["front", "top"],
{"front": b(0, 0, 40, 40), "top": b(0, 0, 40, 20)})
# 2. Wide bar, 4 views (old screenshot case): front+back wide, top+right.
check("wide bar 4", ["front", "top", "right", "back"],
{"front": b(0, 0, 120, 25), "top": b(0, 0, 25, 40),
"right": b(0, 0, 25, 40), "back": b(0, 0, 120, 25)})
# 3. Six views of a long thin part: rotation should kick in.
check("thin part 6", ["front", "top", "right", "left", "back", "bottom"],
{"front": b(0, 0, 200, 30), "top": b(0, 0, 30, 50),
"right": b(0, 0, 50, 30), "left": b(0, 0, 50, 30),
"back": b(0, 0, 200, 30), "bottom": b(0, 0, 30, 50)})
# 4. Single front view: fills the whole page.
check("single", ["front"], {"front": b(0, 0, 10, 20)})
# 5. All 6 + isometric + custom.
check("everything",
["front", "top", "right", "left", "back", "bottom", "isometric"],
{"front": b(0, 0, 80, 40), "top": b(0, 0, 80, 30),
"right": b(0, 0, 30, 40), "left": b(0, 0, 30, 40),
"back": b(0, 0, 80, 40), "bottom": b(0, 0, 80, 30),
"isometric": b(0, 0, 60, 60)})
# ── Inverse-transform roundtrip ─────────────────────────────────────────
# A 90°-rotated view: forward via _assemble_view's recorded 6-tuple,
# inverse via the widget's formula.
t = (2.0, 150.0, 80.0, 90.0, 10.0, 5.0) # s, o_x, o_y, deg, cx, cy
scale, ox, oy, deg, cx, cy = t
th = math.radians(deg)
cos_t, sin_t = math.cos(th), math.sin(th)
def fwd(p):
dx, dy = p[0] - cx, p[1] - cy
return ((dx * cos_t - dy * sin_t) * scale + ox,
(dx * sin_t + dy * cos_t) * scale + oy)
def inv(p):
sx, sy = (p[0] - ox) / scale, (p[1] - oy) / scale
return (sx * cos_t + sy * sin_t + cx, -sx * sin_t + sy * cos_t + cy)
for p in [(0, 0), (10, 5), (3, -7), (42.5, 11.25)]:
rt = inv(fwd(p))
assert abs(rt[0] - p[0]) < 1e-9 and abs(rt[1] - p[1]) < 1e-9, (p, rt)
print("inverse roundtrip OK")
# Legacy 3-tuple still works through build_manual_candidates.
from fluency.models.data_model import DrawingAnnotation, TechnicalDrawing
d = TechnicalDrawing(source_kind="component", source_id="c")
ann = DrawingAnnotation(kind="dimension", dimension_kind="length",
view_id="front", anchors=[(0.0, 0.0), (0.0, 12.5)],
direction=(0.0, 1.0))
d.annotations.append(ann)
cands, res, unres = build_manual_candidates(d, {"front": (2.0, 10.0, 20.0)})
assert unres == [] and cands[0].anchor_points[1] == (10.0, 45.0)
print("legacy 3-tuple OK")
# 6-tuple manual: rotated length direction must rotate too.
ann2 = DrawingAnnotation(kind="dimension", dimension_kind="length",
view_id="front",
anchors=[(0.0, 0.0), (0.0, 10.0)],
direction=(0.0, 1.0))
d2 = TechnicalDrawing(source_kind="component", source_id="c")
d2.annotations.append(ann2)
# 90° rotation about centre c=(5,5), scale 2, o=(100,80)
cands, res, unres = build_manual_candidates(
d2, {"front": (2.0, 100.0, 80.0, 90.0, 5.0, 5.0)}
)
c = cands[0]
# anchors: (0,0)->rot90 about (5,5) = (5-(0-5)*0 - ... compute: dx=-5,dy=-5
# fwd: (dx*cos - dy*sin)*2+100 = (0 - (-5))*2+100 = 110 ; (dx*sin+dy*cos)*2+80 = (-5)*2+80=70
# (0,10): dx=-5, dy=5 -> (0-5)*2+100=90 ; (5*1+0)*2+80=70
assert c.anchor_points[0] == (110.0, 70.0), c.anchor_points
assert c.anchor_points[1] == (90.0, 70.0), c.anchor_points
# direction (0,1) rotated 90° CCW -> (-1, 0)
assert c.direction[0] == -1.0 and abs(c.direction[1]) < 1e-9, c.direction
print("6-tuple manual (rotated) OK")
print("ALL SMOKE CHECKS PASSED")