← Garden room - 4x3 - Design Profile Demo

Rear Roof Panel

v5 Gemini approved

Create the rear roof OSB/EPS SIP panel, 4200mm long x 860mm wide x 122mm thick. Connects via span splice joint on its front edge to the rear face of the LVL bearer. Rests on the back top plate. Oriented at a 5-degree slope to complete the roof.

Dependencies

roof_splice_lvl.rear_splice back_top_plate.top_face
◉
Agent initialized (status: waiting_for_dependencies, action: check_dependencies)
◉
Provider: google | Profile: balanced
◉
Provider updated to google / balanced
◇
Received interface back_top_plate.top_face: %{name: "top_face", type: "face", value: %{}, agent_id: "back_top_plate"}
◇
Received interface roof_splice_lvl.rear_splice: %{name: "rear_splice", type: "splice", value: %{}, agent_id: "roof_splice_lvl"}
◉
All dependencies resolved, starting generation
◉
Generating v1
○
Calling Gemini API for code generation (gemini-3.1-pro-preview)...
◉
Generation token budget: max_tokens=12288, thinking_budget=0 (retry_count=0)
◉
Provider updated to google / balanced
◉
Provider updated to google / balanced
●
Received response (2355 tokens)
◆
roof_panel_back_v1.py · 178 lines
import FreeCAD
import Part
import math
from FreeCAD import Vector

# === PARAMETERS ===
# Dimensions in mm
LENGTH = 4200            # Span from eave to splice (Y-axis)
WIDTH = 860              # Panel width (X-axis)
THICKNESS = 122          # Total panel thickness (SIP-100)
CORE_THICKNESS = 100     # EPS foam core
FACE_THICKNESS = 11      # OSB outer faces
PITCH_DEG = 5            # Roof pitch angle

# === DOCUMENT SETUP ===
doc = FreeCAD.ActiveDocument or FreeCAD.newDocument("Model")

# === BUILD FLAT PANEL ===
# Model the 3-layer panel horizontally (base at Z=0). 
# It will be pitched by the assembly script, but needs plumb cuts at the ends.

# Layer 1: Bottom OSB
face1 = Part.makeBox(WIDTH, LENGTH, FACE_THICKNESS, Vector(0, 0, 0))
# Layer 2: EPS Core
core = Part.makeBox(WIDTH, LENGTH, CORE_THICKNESS, Vector(0, 0, FACE_THICKNESS))
# Layer 3: Top OSB
face2 = Part.makeBox(WIDTH, LENGTH, FACE_THICKNESS, Vector(0, 0, FACE_THICKNESS + CORE_THICKNESS))

# Combine layers into one continuous panel
panel = face1.fuse(core).fuse(face2)
panel = panel.removeSplitter()

# === APPLY BEVEL CUTS ===
# Creates vertical/plumb faces at both ends when the panel is later pitched to 5 degrees.
pitch_rad = math.radians(PITCH_DEG)
bevel_y = THICKNESS * math.tan(pitch_rad)

# 1. Back Cutter (Eave edge at Y = 0)
# Removes the overhanging top corner to create a plumb eave face.
y_bot_b = -10 * math.tan(pitch_rad)
y_ext_b = (THICKNESS + 10) * math.tan(pitch_rad)

pts_back = [
    Vector(-10, y_bot_b, -10),
    Vector(-10, y_ext_b, THICKNESS + 10),
    Vector(-10, -10, THICKNESS + 10),
    Vector(-10, -10, -10),
    Vector(-10, y_bot_b, -10)
]
wire_back = Part.makePolygon(pts_back)
face_back = Part.Face(wire_back)
cutter_back = face_back.extrude(Vector(WIDTH + 20, 0, 0))

panel = panel.cut(cutter_back)

# 2. Front Cutter (Splice edge at Y = LENGTH)
# Removes the protruding bottom corner to sit flush against the plumb LVL bearer face.
y_bot_f = LENGTH - bevel_y - 10 * math.tan(pitch_rad)
y_ext_f = LENGTH - bevel_y + (THICKNESS + 10) * math.tan(pitch_rad)

pts_front = [
    Vector(-10, y_bot_f, -10),
    Vector(-10, y_ext_f, THICKNESS + 10),
    Vector(-10, LENGTH + 10, THICKNESS + 10),
    Vector(-10, LENGTH + 10, -10),
    Vector(-10, y_bot_f, -10)
]
wire_front = Part.makePolygon(pts_front)
face_front = Part.Face(wire_front)
cutter_front = face_front.extrude(Vector(WIDTH + 20, 0, 0))

result = panel.cut(cutter_front)
result = result.removeSplitter()

# === PART CODE ===
PART_CODE = "E-2"
PRINT_SCALE = 50  # 1:50 model scale — adjust if printing at a different scale

def _emboss_part_code(shape, code, print_scale=PRINT_SCALE):
    """Cut part code into largest face. Returns shape unmodified on any failure."""
    import os, math
    depth_raw = 0.6 * print_scale
    try:
        # --- font ---
        rd = FreeCAD.getResourceDir()
        font = None
        for fn in ("osifont-lgpl3fe.ttf", "FreeSans.ttf", "FreeCAD.ttf"):
            fp = os.path.join(rd, "Mod", "TechDraw", "Resources", "fonts", fn)
            if os.path.exists(fp):
                font = fp; break
        if font is None:
            return shape

        # --- cap depth at 33% of thinnest component dimension ---
        all_dims = sorted([shape.BoundBox.XLength, shape.BoundBox.YLength, shape.BoundBox.ZLength])
        depth = min(depth_raw, all_dims[0] * 0.33)

        # --- find largest face ---
        best = max(shape.Faces, key=lambda f: f.Area)
        n = best.normalAt(0, 0)
        c = best.CenterOfMass

        # --- text height: 2 mm at print scale, capped at 80% of longest face edge ---
        fb = best.BoundBox
        fb_dims = sorted([d for d in (fb.XLength, fb.YLength, fb.ZLength) if d > 1.0])
        text_h = min(2.0 * print_scale, (fb_dims[-1] * 0.80) if fb_dims else 2.0 * print_scale)

        # --- orthonormal face frame: right (r), up (u), out (n) ---
        wz = Vector(0, 0, 1)
        wy = Vector(0, 1, 0)
        up_ref = wy if abs(n.dot(wz)) > 0.9 else wz
        r = up_ref.cross(n).normalize()
        u = n.cross(r).normalize()

        # --- build text faces from wires (handles letters with holes) ---
        ss = Part.makeShapeString(String=code, FontFile=font, Size=text_h, Tracking=0)
        wires = sorted(ss.Wires, key=lambda w: w.BoundBox.DiagonalLength, reverse=True)
        faces_out = []
        used = set()
        for i, ow in enumerate(wires):
            if i in used:
                continue
            ob = ow.BoundBox
            holes = []
            for j, iw in enumerate(wires):
                if j <= i or j in used:
                    continue
                ib = iw.BoundBox
                if ob.XMin < ib.XMin and ob.XMax > ib.XMax and ob.YMin < ib.YMin and ob.YMax > ib.YMax:
                    holes.append(iw)
                    used.add(j)
            used.add(i)
            try:
                faces_out.append(Part.Face([ow] + holes) if holes else Part.Face(ow))
            except Exception:
                try:
                    faces_out.append(Part.Face(ow))
                except Exception:
                    pass
        if not faces_out:
            return shape
        tf = Part.Compound(faces_out) if len(faces_out) > 1 else faces_out[0]

        # --- combined matrix: center text → rotate onto face → translate to face center ---
        tb = tf.BoundBox
        mc = FreeCAD.Matrix()          # center text at local origin
        mc.A14 = -tb.XLength / 2
        mc.A24 = -tb.YLength / 2
        mr = FreeCAD.Matrix()          # rotate: X→r, Y→u, Z→n
        mr.A11 = r.x;  mr.A12 = u.x;  mr.A13 = n.x
        mr.A21 = r.y;  mr.A22 = u.y;  mr.A23 = n.y
        mr.A31 = r.z;  mr.A32 = u.z;  mr.A33 = n.z
        mt = FreeCAD.Matrix()          # translate to face center, offset 5% outward
        mt.A14 = c.x + n.x * depth * 0.05
        mt.A24 = c.y + n.y * depth * 0.05
        mt.A34 = c.z + n.z * depth * 0.05
        tf2 = tf.transformGeometry(mt.multiply(mr.multiply(mc)))

        # --- extrude inward and cut ---
        extrude_vec = Vector(-n.x * depth * 1.1, -n.y * depth * 1.1, -n.z * depth * 1.1)
        text_solid = tf2.extrude(extrude_vec)
        return shape.cut(text_solid)

    except Exception as e:
        print(f"[part_code] emboss skipped: {e}")
        return shape

# Apply embossing
result = _emboss_part_code(result, PART_CODE)

# === FINAL OUTPUT ===
feature = doc.addObject("Part::Feature", "RearRoofPanel")
feature.Shape = result

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)
◇
Received interface back_top_plate.top_face: %{name: "top_face", type: "face", value: %{}, agent_id: "back_top_plate"}
◉
Upstream interface changed: back_top_plate.top_face
◉
Upstream interface changed: back_top_plate.top_face
◉
Provider updated to google / balanced
◉
Provider updated to google
◉
Provider updated to google / balanced
◉
Regeneration requested by user
◉
Generating v2
○
Calling Gemini API for code generation (gemini-3.1-pro-preview)...
◉
Generation token budget: max_tokens=12288, thinking_budget=0 (retry_count=0)
●
Received response (491 tokens)
◆
roof_panel_back_v2.py · 42 lines
An OSB/EPS SIP roof panel modeled using direct coordinate geometry to correctly implement 5-degree plumb bevel cuts on the ends and standard structural routing (front splice groove and rear eave blocking groove) in the EPS core.

```python
import FreeCAD
import Part
import math
from FreeCAD import Vector

# === PARAMETERS ===
LENGTH = 4200            # Bounding box length (X)
WIDTH = 860              # Bounding box width (Y)
THICKNESS = 122          # Total panel thickness (Z)
FACE_T = 11              # OSB face thickness
CORE_T = 100             # EPS core thickness
PITCH_DEG = 5            # Roof pitch for plumb bevel cuts
SPLICE_GROOVE = 22.5     # Front groove depth for span splice LVL
EAVE_GROOVE = 45.0       # Rear groove depth for eave blocking

# === DOCUMENT ===
doc = FreeCAD.ActiveDocument or FreeCAD.newDocument("SIP_Roof_Panel")

# === GEOMETRY BUILD ===
pitch_rad = math.radians(PITCH_DEG)
tan_p = math.tan(pitch_rad)
bevel_x = THICKNESS * tan_p

# The panel slopes down towards the back. To ensure plumb cuts (vertical after tilting 5 degrees),
# the front and back faces are angled at 5 degrees from the local Z-axis.
def x_front(z):
    return z * tan_p

def x_back(z):
    # Ensures the maximum X bounding coordinate is exactly LENGTH at Z = THICKNESS
    return LENGTH - bevel_x + z * tan_p

def make_layer(z_min, z_max, front_offset, back_offset):
    """Creates a polygonal layer with plumb cut ends and optional core offsets."""
    pts =
 [
        Vector(x_front(z_min) + front_offset, 0, z_min),
        Vector(x_back(z_min) - back_offset, 0, z_min),
        Vector(x_back(z_max) - back_offset, 0, z_
●
Executing in FreeCAD...
✖
FreeCAD execution failed: Traceback (most recent call last): File "/opt/freecad/bin/../Mod/CadClaude/cadclaude_worker.py", line 159, in _run_script exec(compile(script_source, "<cadclaude>", "exec"), {}) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "<cadclaude>", line 1 An OSB/EPS SIP roof panel modeled using direct coordinate geometry to correctly implement 5-degree plumb bevel cuts on the ends and standard structural routing (front splice groove and rear eave blocking groove) in the EPS core. ^^^ SyntaxError: invalid syntax
◉
Retrying (attempt 2/4)
◉
Provider updated to google / balanced
●
Received corrected script, re-executing...
●
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 (v2)
◇
Received interface roof_splice_lvl.rear_splice: %{name: "rear_splice", type: "splice", value: %{}, agent_id: "roof_splice_lvl"}
◉
Provider updated to google / balanced
◉
Upstream interface changed: roof_splice_lvl.rear_splice
◉
Regeneration requested by user
◉
Generating v3
○
Calling Gemini API for code generation (gemini-3.1-pro-preview)...
◉
Generation token budget: max_tokens=12288, thinking_budget=0 (retry_count=0)
●
Received response (1825 tokens)
◆
roof_panel_back_v3.py · 134 lines
import FreeCAD
import Part
from FreeCAD import Vector

# === PARAMETERS ===
LENGTH = 4200            # mm (span direction)
WIDTH = 860              # mm
FACE_THICKNESS = 11      # mm (OSB face)
CORE_THICKNESS = 100     # mm (EPS core)
TOTAL_THICKNESS = FACE_THICKNESS * 2 + CORE_THICKNESS  # 122mm
ROUT_DEPTH = 45          # mm (recess for LVL bearer splice and eave blocking)

# === DOCUMENT ===
doc = FreeCAD.ActiveDocument or FreeCAD.newDocument("Model")

# === BUILD GEOMETRY ===
# Bottom OSB face
face_bottom = Part.makeBox(WIDTH, LENGTH, FACE_THICKNESS, Vector(0, 0, 0))

# Top OSB face
face_top = Part.makeBox(WIDTH, LENGTH, FACE_THICKNESS, Vector(0, 0, FACE_THICKNESS + CORE_THICKNESS))

# EPS Core (recessed by ROUT_DEPTH on all four sides for splines/blocking)
core_w = WIDTH - 2 * ROUT_DEPTH
core_l = LENGTH - 2 * ROUT_DEPTH
core = Part.makeBox(core_w, core_l, CORE_THICKNESS, Vector(ROUT_DEPTH, ROUT_DEPTH, FACE_THICKNESS))

# Fuse layers into the final SIP panel solid
panel = face_bottom.fuse(core).fuse(face_top)
panel = panel.removeSplitter()

# === PART CODE ===
PART_CODE = "E-2"
PRINT_SCALE = 50  # 1:50 model scale — adjust if printing at a different scale

def _emboss_part_code(shape, code, print_scale=PRINT_SCALE):
    """Cut part code into largest face. Returns shape unmodified on any failure."""
    import os, math
    depth_raw = 0.6 * print_scale
    try:
        # --- font ---
        rd = FreeCAD.getResourceDir()
        font = None
        for fn in ("osifont-lgpl3fe.ttf", "FreeSans.ttf", "FreeCAD.ttf"):
            fp = os.path.join(rd, "Mod", "TechDraw", "Resources", "fonts", fn)
            if os.path.exists(fp):
                font = fp; break
        if font is None:
            return shape

        # --- cap depth at 33% of thinnest component dimension ---
        all_dims = sorted([shape.BoundBox.XLength, shape.BoundBox.YLength, shape.BoundBox.ZLength])
        depth = min(depth_raw, all_dims[0] * 0.33)

        # --- find largest face ---
        best = max(shape.Faces, key=lambda f: f.Area)
        n = best.normalAt(0, 0)
        c = best.CenterOfMass

        # --- text height: 2 mm at print scale, capped at 80% of longest face edge ---
        fb = best.BoundBox
        fb_dims = sorted([d for d in (fb.XLength, fb.YLength, fb.ZLength) if d > 1.0])
        text_h = min(2.0 * print_scale, (fb_dims[-1] * 0.80) if fb_dims else 2.0 * print_scale)

        # --- orthonormal face frame: right (r), up (u), out (n) ---
        wz = Vector(0, 0, 1)
        wy = Vector(0, 1, 0)
        up_ref = wy if abs(n.dot(wz)) > 0.9 else wz
        r = up_ref.cross(n).normalize()
        u = n.cross(r).normalize()

        # --- build text faces from wires (handles letters with holes) ---
        ss = Part.makeShapeString(String=code, FontFile=font, Size=text_h, Tracking=0)
        wires = sorted(ss.Wires, key=lambda w: w.BoundBox.DiagonalLength, reverse=True)
        faces_out = []
        used = set()
        for i, ow in enumerate(wires):
            if i in used:
                continue
            ob = ow.BoundBox
            holes = []
            for j, iw in enumerate(wires):
                if j <= i or j in used:
                    continue
                ib = iw.BoundBox
                if ob.XMin < ib.XMin and ob.XMax > ib.XMax and ob.YMin < ib.YMin and ob.YMax > ib.YMax:
                    holes.append(iw)
                    used.add(j)
            used.add(i)
            try:
                faces_out.append(Part.Face([ow] + holes) if holes else Part.Face(ow))
            except Exception:
                try:
                    faces_out.append(Part.Face(ow))
                except Exception:
                    pass
        if not faces_out:
            return shape
        tf = Part.Compound(faces_out) if len(faces_out) > 1 else faces_out[0]

        # --- combined matrix: center text → rotate onto face → translate to face center ---
        tb = tf.BoundBox
        mc = FreeCAD.Matrix()          # center text at local origin
        mc.A14 = -tb.XLength / 2
        mc.A24 = -tb.YLength / 2
        mr = FreeCAD.Matrix()          # rotate: X→r, Y→u, Z→n
        mr.A11 = r.x;  mr.A12 = u.x;  mr.A13 = n.x
        mr.A21 = r.y;  mr.A22 = u.y;  mr.A23 = n.y
        mr.A31 = r.z;  mr.A32 = u.z;  mr.A33 = n.z
        mt = FreeCAD.Matrix()          # translate to face center, offset 5% outward
        mt.A14 = c.x + n.x * depth * 0.05
        mt.A24 = c.y + n.y * depth * 0.05
        mt.A34 = c.z + n.z * depth * 0.05
        tf2 = tf.transformGeometry(mt.multiply(mr.multiply(mc)))

        # --- extrude inward and cut ---
        extrude_vec = Vector(-n.x * depth * 1.1, -n.y * depth * 1.1, -n.z * depth * 1.1)
        text_solid = tf2.extrude(extrude_vec)
        return shape.cut(text_solid)

    except Exception as e:
        print(f"[part_code] emboss skipped: {e}")
        return shape

# Apply embossing
final_shape = _emboss_part_code(panel, PART_CODE)

# === ADD TO DOCUMENT ===
feature = doc.addObject("Part::Feature", "Body")
feature.Shape = final_shape

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 (v3)
◉
Provider updated to google / balanced
◉
Upstream interface changed: roof_splice_lvl.rear_splice
◉
Regeneration requested by user
◉
Generating v4
○
Calling Gemini API for code generation (gemini-3.1-pro-preview)...
◉
Generation token budget: max_tokens=12288, thinking_budget=0 (retry_count=0)
◉
Upstream interface changed: roof_splice_lvl.rear_splice
◉
Upstream interface changed: roof_splice_lvl.rear_splice
●
Received response (1768 tokens)
◆
roof_panel_back_v4.py · 137 lines
import FreeCAD
import Part
import math
from FreeCAD import Vector

# === PARAMETERS ===
WIDTH = 860          # Transverse width of the panel
LENGTH = 4200        # Length along the slope
THICKNESS = 122      # Total SIP thickness
OSB_T = 11           # OSB face thickness
EPS_T = 100          # EPS core thickness
SPLICE_DEPTH = 22.5  # Half of a standard 45mm LVL bearer

# === BUILD GEOMETRY ===
doc = FreeCAD.ActiveDocument or FreeCAD.newDocument("SIP_Roof_Panel")

# Bottom OSB layer
osb_bottom = Part.makeBox(WIDTH, LENGTH, OSB_T, Vector(0, 0, 0))

# EPS Core (recessed at the front edge Y=0 for the LVL splice joint)
eps_core = Part.makeBox(
    WIDTH, 
    LENGTH - SPLICE_DEPTH, 
    EPS_T, 
    Vector(0, SPLICE_DEPTH, OSB_T)
)

# Top OSB layer
osb_top = Part.makeBox(WIDTH, LENGTH, OSB_T, Vector(0, 0, OSB_T + EPS_T))

# Fuse all layers into a single panel
panel = osb_bottom.fuse(eps_core).fuse(osb_top)
panel = panel.removeSplitter()

# === PART CODE ===
PART_CODE = "E-2"
PRINT_SCALE = 50  # 1:50 model scale — adjust if printing at a different scale

def _emboss_part_code(shape, code, print_scale=PRINT_SCALE):
    """Cut part code into largest face. Returns shape unmodified on any failure."""
    import os, math
    depth_raw = 0.6 * print_scale
    try:
        # --- font ---
        rd = FreeCAD.getResourceDir()
        font = None
        for fn in ("osifont-lgpl3fe.ttf", "FreeSans.ttf", "FreeCAD.ttf"):
            fp = os.path.join(rd, "Mod", "TechDraw", "Resources", "fonts", fn)
            if os.path.exists(fp):
                font = fp; break
        if font is None:
            return shape

        # --- cap depth at 33% of thinnest component dimension ---
        all_dims = sorted([shape.BoundBox.XLength, shape.BoundBox.YLength, shape.BoundBox.ZLength])
        depth = min(depth_raw, all_dims[0] * 0.33)

        # --- find largest face ---
        best = max(shape.Faces, key=lambda f: f.Area)
        n = best.normalAt(0, 0)
        c = best.CenterOfMass

        # --- text height: 2 mm at print scale, capped at 80% of longest face edge ---
        fb = best.BoundBox
        fb_dims = sorted([d for d in (fb.XLength, fb.YLength, fb.ZLength) if d > 1.0])
        text_h = min(2.0 * print_scale, (fb_dims[-1] * 0.80) if fb_dims else 2.0 * print_scale)

        # --- orthonormal face frame: right (r), up (u), out (n) ---
        wz = Vector(0, 0, 1)
        wy = Vector(0, 1, 0)
        up_ref = wy if abs(n.dot(wz)) > 0.9 else wz
        r = up_ref.cross(n).normalize()
        u = n.cross(r).normalize()

        # --- build text faces from wires (handles letters with holes) ---
        ss = Part.makeShapeString(String=code, FontFile=font, Size=text_h, Tracking=0)
        wires = sorted(ss.Wires, key=lambda w: w.BoundBox.DiagonalLength, reverse=True)
        faces_out = []
        used = set()
        for i, ow in enumerate(wires):
            if i in used:
                continue
            ob = ow.BoundBox
            holes = []
            for j, iw in enumerate(wires):
                if j <= i or j in used:
                    continue
                ib = iw.BoundBox
                if ob.XMin < ib.XMin and ob.XMax > ib.XMax and ob.YMin < ib.YMin and ob.YMax > ib.YMax:
                    holes.append(iw)
                    used.add(j)
            used.add(i)
            try:
                faces_out.append(Part.Face([ow] + holes) if holes else Part.Face(ow))
            except Exception:
                try:
                    faces_out.append(Part.Face(ow))
                except Exception:
                    pass
        if not faces_out:
            return shape
        tf = Part.Compound(faces_out) if len(faces_out) > 1 else faces_out[0]

        # --- combined matrix: center text → rotate onto face → translate to face center ---
        tb = tf.BoundBox
        mc = FreeCAD.Matrix()          # center text at local origin
        mc.A14 = -tb.XLength / 2
        mc.A24 = -tb.YLength / 2
        mr = FreeCAD.Matrix()          # rotate: X→r, Y→u, Z→n
        mr.A11 = r.x;  mr.A12 = u.x;  mr.A13 = n.x
        mr.A21 = r.y;  mr.A22 = u.y;  mr.A23 = n.y
        mr.A31 = r.z;  mr.A32 = u.z;  mr.A33 = n.z
        mt = FreeCAD.Matrix()          # translate to face center, offset 5% outward
        mt.A14 = c.x + n.x * depth * 0.05
        mt.A24 = c.y + n.y * depth * 0.05
        mt.A34 = c.z + n.z * depth * 0.05
        tf2 = tf.transformGeometry(mt.multiply(mr.multiply(mc)))

        # --- extrude inward and cut ---
        extrude_vec = Vector(-n.x * depth * 1.1, -n.y * depth * 1.1, -n.z * depth * 1.1)
        text_solid = tf2.extrude(extrude_vec)
        return shape.cut(text_solid)

    except Exception as e:
        print(f"[part_code] emboss skipped: {e}")
        return shape

# Apply part code
final_shape = _emboss_part_code(panel, PART_CODE)

# === ADD TO DOCUMENT ===
feature = doc.addObject("Part::Feature", "Rear_Roof_Panel")
feature.Shape = final_shape

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 (v4)
◉
Provider updated to google / balanced
◉
Provider updated to google / balanced
◉
Upstream interface changed: roof_splice_lvl.rear_splice
◉
Upstream interface changed: roof_splice_lvl.rear_splice
◉
Provider updated to google / balanced
◉
Regeneration requested by user
◉
Generating v5
○
Calling Gemini API for code generation (gemini-3.1-pro-preview)...
◉
Generation token budget: max_tokens=12288, thinking_budget=0 (retry_count=0)
◉
Upstream interface changed: roof_splice_lvl.rear_splice
●
Received response (1815 tokens)
◆
roof_panel_back_v5.py · 143 lines
import FreeCAD
import Part
import math
from FreeCAD import Vector

# === PARAMETERS ===
LENGTH = 4200            # mm (span along X)
WIDTH = 860              # mm (run along Y slope)
THICKNESS = 122          # mm (total Z thickness)
FACE_THICKNESS = 11      # mm (OSB face thickness)
CORE_THICKNESS = 100     # mm (EPS core thickness)
GROOVE_DEPTH = 22.5      # mm (recess depth for the span splice joint)

# === DOCUMENT ===
doc = FreeCAD.ActiveDocument or FreeCAD.newDocument("Model")

# === BUILD GEOMETRY ===
# Bottom OSB face
face1 = Part.makeBox(LENGTH, WIDTH, FACE_THICKNESS, Vector(0, 0, 0))

# EPS Core (recessed on the front edge Y=0 to create the splice groove)
core = Part.makeBox(
    LENGTH, 
    WIDTH - GROOVE_DEPTH, 
    CORE_THICKNESS, 
    Vector(0, GROOVE_DEPTH, FACE_THICKNESS)
)

# Top OSB face
face2 = Part.makeBox(
    LENGTH, 
    WIDTH, 
    FACE_THICKNESS, 
    Vector(0, 0, FACE_THICKNESS + CORE_THICKNESS)
)

# Fuse layers into a single SIP panel solid
panel = face1.fuse(core).fuse(face2)
panel = panel.removeSplitter()

# === PART CODE ===
PART_CODE = "E-2"
PRINT_SCALE = 50  # 1:50 model scale — adjust if printing at a different scale

def _emboss_part_code(shape, code, print_scale=PRINT_SCALE):
    """Cut part code into largest face. Returns shape unmodified on any failure."""
    import os, math
    depth_raw = 0.6 * print_scale
    try:
        # --- font ---
        rd = FreeCAD.getResourceDir()
        font = None
        for fn in ("osifont-lgpl3fe.ttf", "FreeSans.ttf", "FreeCAD.ttf"):
            fp = os.path.join(rd, "Mod", "TechDraw", "Resources", "fonts", fn)
            if os.path.exists(fp):
                font = fp; break
        if font is None:
            return shape

        # --- cap depth at 33% of thinnest component dimension ---
        all_dims = sorted([shape.BoundBox.XLength, shape.BoundBox.YLength, shape.BoundBox.ZLength])
        depth = min(depth_raw, all_dims[0] * 0.33)

        # --- find largest face ---
        best = max(shape.Faces, key=lambda f: f.Area)
        n = best.normalAt(0, 0)
        c = best.CenterOfMass

        # --- text height: 2 mm at print scale, capped at 80% of longest face edge ---
        fb = best.BoundBox
        fb_dims = sorted([d for d in (fb.XLength, fb.YLength, fb.ZLength) if d > 1.0])
        text_h = min(2.0 * print_scale, (fb_dims[-1] * 0.80) if fb_dims else 2.0 * print_scale)

        # --- orthonormal face frame: right (r), up (u), out (n) ---
        wz = Vector(0, 0, 1)
        wy = Vector(0, 1, 0)
        up_ref = wy if abs(n.dot(wz)) > 0.9 else wz
        r = up_ref.cross(n).normalize()
        u = n.cross(r).normalize()

        # --- build text faces from wires (handles letters with holes) ---
        ss = Part.makeShapeString(String=code, FontFile=font, Size=text_h, Tracking=0)
        wires = sorted(ss.Wires, key=lambda w: w.BoundBox.DiagonalLength, reverse=True)
        faces_out = []
        used = set()
        for i, ow in enumerate(wires):
            if i in used:
                continue
            ob = ow.BoundBox
            holes = []
            for j, iw in enumerate(wires):
                if j <= i or j in used:
                    continue
                ib = iw.BoundBox
                if ob.XMin < ib.XMin and ob.XMax > ib.XMax and ob.YMin < ib.YMin and ob.YMax > ib.YMax:
                    holes.append(iw)
                    used.add(j)
            used.add(i)
            try:
                faces_out.append(Part.Face([ow] + holes) if holes else Part.Face(ow))
            except Exception:
                try:
                    faces_out.append(Part.Face(ow))
                except Exception:
                    pass
        if not faces_out:
            return shape
        tf = Part.Compound(faces_out) if len(faces_out) > 1 else faces_out[0]

        # --- combined matrix: center text → rotate onto face → translate to face center ---
        tb = tf.BoundBox
        mc = FreeCAD.Matrix()          # center text at local origin
        mc.A14 = -tb.XLength / 2
        mc.A24 = -tb.YLength / 2
        mr = FreeCAD.Matrix()          # rotate: X→r, Y→u, Z→n
        mr.A11 = r.x;  mr.A12 = u.x;  mr.A13 = n.x
        mr.A21 = r.y;  mr.A22 = u.y;  mr.A23 = n.y
        mr.A31 = r.z;  mr.A32 = u.z;  mr.A33 = n.z
        mt = FreeCAD.Matrix()          # translate to face center, offset 5% outward
        mt.A14 = c.x + n.x * depth * 0.05
        mt.A24 = c.y + n.y * depth * 0.05
        mt.A34 = c.z + n.z * depth * 0.05
        tf2 = tf.transformGeometry(mt.multiply(mr.multiply(mc)))

        # --- extrude inward and cut ---
        extrude_vec = Vector(-n.x * depth * 1.1, -n.y * depth * 1.1, -n.z * depth * 1.1)
        text_solid = tf2.extrude(extrude_vec)
        return shape.cut(text_solid)

    except Exception as e:
        print(f"[part_code] emboss skipped: {e}")
        return shape

# Apply part code embossing
panel = _emboss_part_code(panel, PART_CODE)

# === ADD TO DOCUMENT ===
feature = doc.addObject("Part::Feature", "Body")
feature.Shape = panel

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 (v5)
◉
Provider updated to google / balanced
◉
Provider updated to google / balanced
v0.0.985