← Garden room - 4x3 - Design Profile Demo

Front Roof Panel

v6 Gemini approved

Create the front roof OSB/EPS SIP panel, 4200mm long (x-axis) x 2440mm wide (y-axis) x 122mm thick. Glued and screwed face contact to the front top plate. Must include a prepared rear edge to form a structural span splice with the LVL bearer. Oriented at a 5-degree slope.

Dependencies

front_top_plate.top_face

Interfaces Provided

rear_splice (splice)
◉
Agent initialized (status: waiting_for_dependencies, action: check_dependencies)
◉
Provider: google | Profile: balanced
◉
Provider updated to google / balanced
◇
Received interface front_top_plate.top_face: %{name: "top_face", type: "face", value: %{}, agent_id: "front_top_plate"}
◉
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)
●
Received response (1845 tokens)
◆
roof_panel_front_v1.py · 139 lines
import FreeCAD
import Part
import math
from FreeCAD import Vector

# === PARAMETERS ===
PANEL_LENGTH = 4200      # x-axis length
PANEL_WIDTH = 2440       # y-axis width (along slope)
TOTAL_THICKNESS = 122    # z-axis thickness
FACE_THICKNESS = 11      # Standard OSB thickness
CORE_THICKNESS = TOTAL_THICKNESS - 2 * FACE_THICKNESS  # 100mm EPS core
REAR_ROUT_DEPTH = 45     # Core recess at rear edge for LVL splice
PITCH_ANGLE = 5          # Degrees

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

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

# EPS Core (routed at rear edge, stops 45mm short of the full width)
core_width = PANEL_WIDTH - REAR_ROUT_DEPTH
core = Part.makeBox(PANEL_LENGTH, core_width, CORE_THICKNESS, 
                    Vector(0, 0, FACE_THICKNESS))

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

# Fuse the three SIP layers into a single solid
panel = face_bottom.fuse(core).fuse(face_top)

# Orient at 5-degree slope around the X-axis (lifting the rear edge)
panel.rotate(Vector(0, 0, 0), Vector(1, 0, 0), PITCH_ANGLE)


# === PART CODE ===
PART_CODE = "E-1"
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

panel = _emboss_part_code(panel, PART_CODE)

# === ADD TO DOCUMENT ===
feature = doc.addObject("Part::Feature", "FrontRoofPanel")
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 (v1)
◉
Provider updated to google / balanced
◉
Provider updated to google / balanced
◉
Provider updated to google / balanced
◉
Provider updated to google / balanced
◉
Provider updated to google / balanced
◇
Received interface front_top_plate.top_face: %{name: "top_face", type: "face", value: %{}, agent_id: "front_top_plate"}
◇
Received interface front_top_plate.top_face: %{name: "top_face", type: "face", value: %{}, agent_id: "front_top_plate"}
◉
Upstream interface changed: front_top_plate.top_face
◉
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 (1857 tokens)
◆
roof_panel_front_v2.py · 138 lines
import FreeCAD
import Part
import math
from FreeCAD import Vector, Placement, Rotation

# === PARAMETERS ===
LENGTH = 4200         # x-axis length (mm)
WIDTH = 2440          # y-axis width (mm)
THICKNESS = 122       # total panel thickness (mm)
FACE_T = 11           # OSB face thickness (mm)
CORE_T = 100          # EPS core thickness (mm)
SPLICE_DEPTH = 45     # depth of core routed out at the rear for the LVL bearer (mm)
SLOPE_ANGLE = 5       # roof pitch angle (degrees)

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

# === BUILD GEOMETRY ===
# Create the three layers of the SIP panel flat on the XY plane
bottom_osb = Part.makeBox(LENGTH, WIDTH, FACE_T, Vector(0, 0, 0))

# Core is routed/recessed at the rear edge (max Y) by SPLICE_DEPTH
core = Part.makeBox(LENGTH, WIDTH - SPLICE_DEPTH, CORE_T, Vector(0, 0, FACE_T))

top_osb = Part.makeBox(LENGTH, WIDTH, FACE_T, Vector(0, 0, FACE_T + CORE_T))

# Fuse layers into a single SIP panel solid
panel = bottom_osb.fuse([core, top_osb])
panel = panel.removeSplitter()

# Orient the panel to a 5-degree slope (pitch around the X-axis)
# The front edge (Y=0) stays anchored at Z=0, while the rear edge pitches upward.
panel.rotate(Vector(0, 0, 0), Vector(1, 0, 0), SLOPE_ANGLE)

# === PART CODE ===
PART_CODE = "E-1"
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 to the final generated solid
panel = _emboss_part_code(panel, PART_CODE)

# === ADD TO DOCUMENT ===
feature = doc.addObject("Part::Feature", "FrontRoofPanel")
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 (v2)
◉
Provider updated to google / balanced
◉
Provider updated to google / balanced
◉
Upstream interface changed: front_top_plate.top_face
◉
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)
◉
Upstream interface changed: front_top_plate.top_face
◉
Upstream interface changed: front_top_plate.top_face
●
Received response (1887 tokens)
◆
roof_panel_front_v3.py · 138 lines
import FreeCAD
import Part
import math
from FreeCAD import Vector

# === PARAMETERS ===
LENGTH_X = 4200         # Span of the roof panel (mm)
WIDTH_Y = 2440          # Width/depth of the roof panel (mm)
THICKNESS_Z = 122       # Total thickness (mm)
FACE_T = 11             # OSB face thickness (mm)
CORE_T = 100            # EPS core thickness (mm)
SPLICE_DEPTH = 45       # Depth of the core recess at the rear edge for the LVL bearer (mm)
PITCH_DEG = 5           # Roof slope angle (degrees)

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

# === BUILD GEOMETRY ===
# 1. Bottom OSB Face
face1 = Part.makeBox(LENGTH_X, WIDTH_Y, FACE_T, Vector(0, 0, 0))

# 2. EPS Core (recessed at the rear edge Y = WIDTH_Y by SPLICE_DEPTH)
core = Part.makeBox(LENGTH_X, WIDTH_Y - SPLICE_DEPTH, CORE_T, Vector(0, 0, FACE_T))

# 3. Top OSB Face
face2 = Part.makeBox(LENGTH_X, WIDTH_Y, FACE_T, Vector(0, 0, FACE_T + CORE_T))

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

# Orient the panel to the 5-degree slope
# Rotating around the X-axis lifts the rear edge (Y) upwards
panel.rotate(Vector(0, 0, 0), Vector(1, 0, 0), PITCH_DEG)

# === PART CODE ===
PART_CODE = "E-1"
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
panel = _emboss_part_code(panel, PART_CODE)

# === ADD TO DOCUMENT ===
obj = doc.addObject("Part::Feature", "FrontRoofPanel")
obj.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 (v3)
◉
Upstream interface changed: front_top_plate.top_face
◉
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)
◉
Provider updated to google / balanced
◉
Provider updated to google / balanced
●
Received response (1910 tokens)
◆
roof_panel_front_v4.py · 139 lines
import FreeCAD
import Part
import math
from FreeCAD import Vector

# === PARAMETERS ===
LENGTH_X = 4200.0        # Length along the roof edge
WIDTH_Y = 2440.0         # Width spanning up the slope
THICKNESS_Z = 122.0      # Total SIP thickness
OSB_THICK = 11.0         # Outer skin thickness
CORE_THICK = 100.0       # EPS foam core thickness
SPLICE_RECESS = 45.0     # Core routing depth for rear LVL bearer
SLOPE_ANGLE = 5.0        # Roof pitch in degrees

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

# === BUILD GEOMETRY ===
# 1. Bottom OSB Face
osb_bottom = Part.makeBox(LENGTH_X, WIDTH_Y, OSB_THICK, Vector(0, 0, 0))

# 2. EPS Core (recessed at the rear edge for LVL span splice)
# Starts at Y=0 (front), ends at Y = WIDTH_Y - SPLICE_RECESS
core_width = WIDTH_Y - SPLICE_RECESS
core = Part.makeBox(LENGTH_X, core_width, CORE_THICK, Vector(0, 0, OSB_THICK))

# 3. Top OSB Face
osb_top = Part.makeBox(LENGTH_X, WIDTH_Y, OSB_THICK, Vector(0, 0, OSB_THICK + CORE_THICK))

# 4. Fuse layers into a single panel
panel = osb_bottom.fuse(core).fuse(osb_top)
panel = panel.removeSplitter()

# 5. Orient the panel to the 5-degree slope
# Rotating around the X-axis pitches the panel up towards the rear (Y-direction)
panel.rotate(Vector(0, 0, 0), Vector(1, 0, 0), SLOPE_ANGLE)

# === PART CODE ===
PART_CODE = "E-1"
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

final_shape = _emboss_part_code(panel, PART_CODE)

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

if FreeCAD.GuiUp:
    FreeCAD.Gui.ActiveDocument.ActiveView.fitAll()
●
Executing in FreeCAD...
◉
Upstream interface changed: front_top_plate.top_face
◉
Upstream interface changed: front_top_plate.top_face
◉
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
◉
Upstream interface changed: front_top_plate.top_face
◉
Provider updated to google / balanced
◉
Provider updated to google / balanced
◉
Agent initialized (status: waiting_for_dependencies, action: check_dependencies)
◉
Provider: google | Profile: balanced
◇
Seeded interface front_top_plate.top_face from DB (provider already approved)
◉
All dependencies resolved, starting generation
◉
Generating v5
◉
Regeneration requested by user
○
Calling Gemini API for code generation (gemini-3.1-pro-preview)...
◉
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)
◉
Generation token budget: max_tokens=12288, thinking_budget=0 (retry_count=0)
●
Received response (1864 tokens)
◆
roof_panel_front_v5.py · 143 lines
import FreeCAD
import Part
import math
import os
from FreeCAD import Vector, Placement, Rotation

# === PARAMETERS ===
LENGTH = 4200          # x-axis (span)
WIDTH = 2440           # y-axis (run)
THICKNESS = 122        # total panel thickness
FACE_T = 11            # OSB face thickness
CORE_T = 100           # EPS foam core thickness
SPLICE_DEPTH = 22.5    # Half-depth routing for a standard 45mm LVL bearer splice
PITCH_DEG = 5          # 5-degree roof slope

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

# === BUILD GEOMETRY ===
# 1. Base Panel Layers
face1 = Part.makeBox(LENGTH, WIDTH, FACE_T, Vector(0, 0, 0))
core = Part.makeBox(LENGTH, WIDTH, CORE_T, Vector(0, 0, FACE_T))
face2 = Part.makeBox(LENGTH, WIDTH, FACE_T, Vector(0, 0, FACE_T + CORE_T))

panel = face1.fuse(core).fuse(face2)
panel = panel.removeSplitter()

# 2. Prepared Rear Edge (Span Splice)
# Cut back the foam core at the top edge (Y = WIDTH) to receive the LVL bearer
splice_cut = Part.makeBox(
    LENGTH, SPLICE_DEPTH, CORE_T, 
    Vector(0, WIDTH - SPLICE_DEPTH, FACE_T)
)
panel = panel.cut(splice_cut)
panel = panel.removeSplitter()

# 3. Panel Orientation
# Rotate 5 degrees around the X-axis so the panel slopes upwards along the Y-axis
panel.rotate(Vector(0, 0, 0), Vector(1, 0, 0), PITCH_DEG)


# === PART CODE ===
PART_CODE = "E-1"
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."""
    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

panel = _emboss_part_code(panel, PART_CODE)


# === ADD TO DOCUMENT ===
obj = doc.addObject("Part::Feature", "SIP_Front_Roof_Panel")
obj.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)
●
Received response (2136 tokens)
◆
roof_panel_front_v5.py · 163 lines
import FreeCAD
import Part
import math
import os
from FreeCAD import Vector

# === PARAMETERS ===
PANEL_LENGTH = 4200        # X-axis span
PANEL_WIDTH = 2440         # Y-axis span (run)
FACE_THICKNESS = 11        # OSB thickness
CORE_THICKNESS = 100       # EPS thickness
TOTAL_THICKNESS = FACE_THICKNESS * 2 + CORE_THICKNESS  # 122mm
SPLICE_DEPTH = 45          # Depth of routed pocket for LVL bearer at the rear edge
SPLINE_W = 45              # Width of internal block splines connecting panels
PITCH_DEG = 5              # Roof slope

# === PART CODE ===
PART_CODE = "E-1"
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

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

# === GEOMETRY GENERATION ===
# Calculate 1200mm panel loop
full_panels = PANEL_LENGTH // 1200
remainder = PANEL_LENGTH % 1200
widths = [1200] * full_panels + ([remainder] if remainder > 0 else [])

parts = []
current_x = 0

for i, pw in enumerate(widths):
    # Bottom OSB (Full run)
    f1 = Part.makeBox(pw, PANEL_WIDTH, FACE_THICKNESS, 
                      Vector(current_x, 0, 0))
    
    # EPS Core (Routed at the rear edge Y=PANEL_WIDTH for the span splice LVL bearer)
    core = Part.makeBox(pw, PANEL_WIDTH - SPLICE_DEPTH, CORE_THICKNESS, 
                        Vector(current_x, 0, FACE_THICKNESS))
    
    # Top OSB (Full run)
    f2 = Part.makeBox(pw, PANEL_WIDTH, FACE_THICKNESS, 
                      Vector(current_x, 0, FACE_THICKNESS + CORE_THICKNESS))
    
    parts.extend([f1, core, f2])
    
    # Inter-panel block spline
    if i < len(widths) - 1:
        spline = Part.makeBox(SPLINE_W, PANEL_WIDTH - SPLICE_DEPTH, CORE_THICKNESS,
                              Vector(current_x + pw - SPLINE_W/2, 0, FACE_THICKNESS))
        parts.append(spline)
        
    current_x += pw

# Combine the panels and splines into a single solid assembly
result = parts[0]
for p in parts[1:]:
    result = result.fuse(p)
result = result.removeSplitter()

# Rotate the entire assembly to the 5-degree roof pitch (tilts +Y edge up)
result.rotate(Vector(0, 0, 0), Vector(1, 0, 0), PITCH_DEG)

# Emboss the part code
result = _emboss_part_code(result, PART_CODE)

# === ADD TO DOCUMENT ===
obj = doc.addObject("Part::Feature", "FrontRoofPanel")
obj.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 (v6)
◉
Provider updated to google / balanced
◉
Provider updated to google / balanced
◉
Provider updated to google / balanced
◉
Provider updated to google / balanced
◉
Provider updated to google / balanced
v0.0.985