← house

Pitched-L Hanger-Supported Roof Frame

v1 AI approved

Use SIPRoofStructure to emit sloped rafters beneath panel seams, transverse purlins at stock-row joints, and joist hangers wherever rafters terminate at wall eaves. Panels bear on top of this frame.

Dependencies

pitched_l_roof_panels.body

Interfaces Provided

body (component)
◉
Agent initialized (status: waiting_for_dependencies, action: check_dependencies)
◉
Provider: claude_code | Profile: balanced
◉
Provider updated to claude_code / balanced
◉
Provider updated to claude_code / balanced
◉
Provider updated to claude_code / balanced
◉
Provider updated to claude_code / balanced
◇
Received interface pitched_l_roof_panels.body: %{name: "body", type: "component", value: %{}, agent_id: "pitched_l_roof_panels"}
◉
All dependencies resolved, starting generation
◉
Refreshed placement from design spec (bbox %{"x" => 12000.0, "y" => 6000.0, "z" => 1793.3756729740642})
◉
Generating v1
◉
Using deterministic SIPRoofPanelizer design-roof generation
◆
pitched_l_roof_splines_v1.py · 47 lines
import json
import FreeCAD
import Part
from elixifree.domains.sip import SIPRoofPanelizer, SIPRoofStructure

doc = FreeCAD.ActiveDocument or FreeCAD.newDocument("Model")
contract = json.loads("{\"lower_wing\":{\"x0\":0.0,\"x1\":1.2e4,\"y0\":3.0e3,\"y1\":6.0e3},\"pitch_degrees\":30.0,\"stock\":\"SIP-200\",\"upper_wing\":{\"x0\":4.0e3,\"x1\":1.2e4,\"y0\":0.0,\"y1\":6.0e3}}")
skeleton = json.loads("{\"apex_z\":2309.4010767585028,\"edge_roles\":[{\"edge\":0,\"role\":\"gable\",\"valley_adjacent\":false,\"wing\":\"upper\"},{\"edge\":1,\"role\":\"eave\",\"valley_adjacent\":false,\"wing\":\"upper\"},{\"edge\":2,\"role\":\"eave\",\"valley_adjacent\":false,\"wing\":\"lower\"},{\"edge\":3,\"role\":\"gable\",\"valley_adjacent\":false,\"wing\":\"lower\"},{\"edge\":4,\"role\":\"eave\",\"valley_adjacent\":true,\"wing\":\"lower\"},{\"edge\":5,\"role\":\"eave\",\"valley_adjacent\":true,\"wing\":\"upper\"}],\"footprint\":[[4.0e3,0.0],[1.2e4,0.0],[1.2e4,6.0e3],[0.0,6.0e3],[0.0,3.0e3],[4.0e3,3.0e3]],\"pitch_deg\":30.0,\"planes\":[{\"axis\":\"y\",\"direction\":1.0,\"id\":\"lower_south\",\"origin\":3.0e3,\"side\":\"south\",\"tan_pitch\":0.5773502691896257,\"wing\":\"lower\"},{\"axis\":\"y\",\"direction\":-1.0,\"id\":\"lower_north\",\"origin\":6.0e3,\"side\":\"north\",\"tan_pitch\":0.5773502691896257,\"wing\":\"lower\"},{\"axis\":\"x\",\"direction\":1.0,\"id\":\"upper_west\",\"origin\":4.0e3,\"side\":\"west\",\"tan_pitch\":0.5773502691896257,\"wing\":\"upper\"},{\"axis\":\"x\",\"direction\":-1.0,\"id\":\"upper_east\",\"origin\":1.2e4,\"side\":\"east\",\"tan_pitch\":0.5773502691896257,\"wing\":\"upper\"}],\"ridges\":[{\"from\":[0.0,4.5e3,866.0254037844386],\"id\":\"ridge_lower\",\"to\":[1.2e4,4.5e3,866.0254037844386],\"wing\":\"lower\"},{\"from\":[8.0e3,0.0,2309.4010767585028],\"id\":\"ridge_upper\",\"to\":[8.0e3,6.0e3,2309.4010767585028],\"wing\":\"upper\"}],\"type\":\"gable\",\"valley\":{\"adjacent_wings\":[\"lower\",\"upper\"],\"from\":[4.0e3,3.0e3,0.0],\"id\":\"valley\",\"to\":[5.5e3,4.5e3,866.0254037844386]},\"wings\":{\"concave\":[4.0e3,3.0e3],\"lower\":{\"x0\":0.0,\"x1\":1.2e4,\"y0\":3.0e3,\"y1\":6.0e3},\"upper\":{\"x0\":4.0e3,\"x1\":1.2e4,\"y0\":0.0,\"y1\":6.0e3}}}")
ROLES = ["splice_bearer", "jack_rafter"]

if contract.get("lower_wing") and contract.get("upper_wing"):
    panelizer = SIPRoofPanelizer(
        contract["lower_wing"], contract["upper_wing"],
        contract["pitch_degrees"], stock=contract["stock"],
        skeleton=skeleton,
    )
else:
    # cross_gable contracts carry no lower/upper wing boxes; the
    # panelizer derives its wings from the skeleton instead.
    panelizer = SIPRoofPanelizer.for_cross(
        skeleton, contract["pitch_degrees"], stock=contract["stock"]
    )
structure = SIPRoofStructure(skeleton, panelizer.plan()).construct()
frame_shapes = [
    part.shape for part in structure.parts
    if part.role in ROLES and part.shape is not None
]
if not frame_shapes:
    raise RuntimeError(
        "SIPRoofStructure produced no " + "/".join(ROLES) + " members — "
        "the splines agent would ship an empty frame"
    )

compound = Part.makeCompound(frame_shapes)

feature = doc.addObject("Part::Feature", "Body")
feature.Label = "Pitched-L Roof Support Members"
feature.addProperty("App::PropertyString", "RoofGenerator")
feature.RoofGenerator = "SIPRoofStructure"
feature.addProperty("App::PropertyInteger", "FramePartCount")
feature.FramePartCount = len(frame_shapes)
feature.Shape = compound
doc.recompute()

if FreeCAD.GuiUp:
    FreeCAD.Gui.ActiveDocument.ActiveView.fitAll()
●
Executing in FreeCAD...
◉
StepAnalyzer failed: FreeCAD could not read STEP file: FreeCAD exception thrown (File to load not existing or not readable) — skipping
●
FreeCAD execution succeeded
●
Exported: model.stl
◉
Generation succeeded and component was auto-approved (v1)
◉
Provider updated to claude_code / balanced
◉
Provider updated to claude_code / balanced
v0.0.985