feat: strut constraint + construction layers for dome MCP #155

Merged
clawdie merged 1 commit from feature/0.12.0 into main 2026-06-23 16:56:18 +02:00

View file

@ -200,6 +200,97 @@ def analyze_structure(verts, edges, triangles, radius_meters=5.0):
}
def analyze_strut_constraint(struts, max_length=2.0, bar_length=6.0):
"""Enforce 2m max strut length for FI12 rebar (6m bars → 3× 2m cuts)."""
pieces_per_bar = int(bar_length / max_length) # 3 pieces of 2m
over = []
ok = []
total_bars = 0
total_waste_m = 0.0
for s in struts:
L = s["length_m"]
count = s["count"]
if L > max_length:
over.append({"length_m": L, "count": count, "excess_m": round(L - max_length, 2)})
else:
ok.append(s)
# Calculate rebar bars needed and waste
total_pieces = sum(s["count"] for s in struts)
total_bars = math.ceil(total_pieces / pieces_per_bar)
total_cut_m = total_bars * bar_length
total_strut_m_needed = sum(s["length_m"] * s["count"] for s in struts)
total_waste_m = round(total_cut_m - total_strut_m_needed, 2)
waste_pieces = total_bars * pieces_per_bar - total_pieces
return {
"max_strut_length_m": max_length,
"bar_length_m": bar_length,
"pieces_per_bar": pieces_per_bar,
"struts_over_limit": over,
"struts_ok": len(ok),
"total_pieces_needed": total_pieces,
"rebar_bars_needed": total_bars,
"total_bar_meters": round(total_bars * bar_length, 1),
"waste_meters": total_waste_m,
"waste_pieces": waste_pieces,
"waste_use": "corner hardening gussets" if waste_pieces > 0 else "none",
"compliant": len(over) == 0,
}
def construction_layers(verts, triangles, radius):
"""Group triangles into construction layers ordered top-to-bottom.
Welding sequence: start at apex (highest Z), weld that layer,
lift the completed section, weld the next layer underneath.
"""
# Compute centroid Z for each triangle
tri_zs = []
for idx, (v0, v1, v2) in enumerate(triangles):
z = (verts[v0][2] + verts[v1][2] + verts[v2][2]) / 3 * radius
tri_zs.append((z, idx))
# Sort top-to-bottom (descending Z)
tri_zs.sort(key=lambda x: -x[0])
# Group into layers by Z proximity (every 0.5m drop = new layer)
layers = []
current_layer = []
current_z = None
layer_threshold = 0.5 # meters
for z, idx in tri_zs:
if current_z is None:
current_z = z
if abs(z - current_z) > layer_threshold:
layers.append({
"layer": len(layers) + 1,
"height_m": round(current_z, 2),
"triangles": len(current_layer),
"sequence": "weld → lift → next layer" if len(layers) > 0 else "start at apex",
})
current_layer = []
current_z = z
current_layer.append(idx)
if current_layer:
layers.append({
"layer": len(layers) + 1,
"height_m": round(current_z, 2),
"triangles": len(current_layer),
"sequence": "weld → lift → next layer" if len(layers) > 0 else "start at apex",
})
return {
"total_layers": len(layers),
"construction_method": "top-to-bottom welding, lift after each layer",
"layer_threshold_m": layer_threshold,
"layers": layers,
}
def render(verts, edges, size=2048, yaw=0.3, pitch=0.4,
line_color="#00b4d8", bg_color="#0d1117", lw=1):
sy, cy = math.sin(yaw), math.cos(yaw)
@ -259,6 +350,14 @@ def generate(params):
analysis["frequency"] = freq
analysis["half_sphere"] = half
# ── Strut constraint: 2m max (FI12 rebar, 6m bars → 3× 2m cuts) ──
strut_constraint = analyze_strut_constraint(analysis["struts"], max_length=2.0, bar_length=6.0)
analysis["strut_constraint"] = strut_constraint
# ── Construction layers: order triangles top-to-bottom for welding ──
layers = construction_layers(verts, triangles, radius)
analysis["construction_layers"] = layers
result = {"success": True, "analysis": analysis}
if not analysis_only: