"""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")