Files
fluencyCAD/.smoke_layout.py
2026-08-18 15:06:51 +02:00

160 lines
6.1 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""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")