- tech drawing and render improv
This commit is contained in:
+376
@@ -0,0 +1,376 @@
|
||||
# ── 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")
|
||||
|
||||
_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.
|
||||
|
||||
The union size grows monotonically with the scale, so a bisection
|
||||
converges to the tight fit.
|
||||
"""
|
||||
_rx0, _ry0, rw, rh = region
|
||||
|
||||
def fits(s: float) -> bool:
|
||||
rects = place(
|
||||
{k: (w * s, h * s) for k, (w, h) in dims_m.items()}, _VIEW_GAP_MM
|
||||
)
|
||||
if not rects:
|
||||
return True
|
||||
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)
|
||||
return (maxx - minx) <= rw + 1e-9 and (maxy - miny) <= rh + 1e-9
|
||||
|
||||
s_lo, s_hi = 0.0, 1.0
|
||||
if fits(s_hi):
|
||||
s_lo = s_hi
|
||||
while s_hi < 1.0e6 and fits(s_hi * 2.0):
|
||||
s_hi *= 2.0
|
||||
for _ in range(60):
|
||||
mid = 0.5 * (s_lo + s_hi)
|
||||
if fits(mid):
|
||||
s_lo = mid
|
||||
else:
|
||||
s_hi = mid
|
||||
return s_lo
|
||||
|
||||
|
||||
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],
|
||||
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,
|
||||
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).
|
||||
|
||||
The sheet is filled, not just used: every candidate arrangement
|
||||
(classic third-angle cross, the cross with the view families swapped,
|
||||
and 1/2/3-row grids) is combined with every per-view 90° rotation
|
||||
assignment, 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. 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).
|
||||
"""
|
||||
slots: Dict[str, Tuple[float, float, float, float]] = {}
|
||||
rotations: Dict[str, float] = {}
|
||||
common_scale: Optional[float] = None
|
||||
|
||||
ortho = [
|
||||
v for v in views if v.kind in _STANDARD_VIEWS and v.kind != "isometric"
|
||||
]
|
||||
|
||||
used_rects: List[Tuple[float, float, float, float]] = []
|
||||
if ortho:
|
||||
dims0: Dict[str, Tuple[float, float]] = {}
|
||||
for v in ortho:
|
||||
b = bboxes.get(v.kind)
|
||||
vs = max(v.scale, 1e-9)
|
||||
if b is None:
|
||||
dims0[v.kind] = (1.0 * vs, 1.0 * vs)
|
||||
else:
|
||||
dims0[v.kind] = (
|
||||
max(b[2] - b[0], 1e-6) * vs,
|
||||
max(b[3] - b[1], 1e-6) * vs,
|
||||
)
|
||||
keys = list(dims0)
|
||||
arrangements: Tuple[
|
||||
Tuple[str, int, Callable[[Dict[str, Tuple[float, float]], float], _RectList]]
|
||||
] = (
|
||||
("cross", 0, _place_cross),
|
||||
("swapped", 1, _place_swapped),
|
||||
("grid1", 2, lambda d, g: _place_grid(d, g, 1)),
|
||||
("grid2", 3, lambda d, g: _place_grid(d, g, 2)),
|
||||
("grid3", 4, 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]]
|
||||
] = []
|
||||
for mask in range(1 << len(keys)):
|
||||
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, _REGION_UPPER)
|
||||
s_left = _fit_scale(place, dims_m, _REGION_LEFT)
|
||||
if s_up >= s_left:
|
||||
s, region = s_up, _REGION_UPPER
|
||||
else:
|
||||
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)]
|
||||
|
||||
# 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, rotations
|
||||
Reference in New Issue
Block a user