# -*- coding: utf-8 -*-
# FreeCAD Macro: OSE 10x12 Cabin Cube (2x6 @ 24" OC) + 4-floor Stair Tower Module
#
# IMPORTANT:
# - This creates conceptual framing geometry for layout/collision and build sequencing.
# - Not a code-compliant structural design. Connections, bracing, guardrails, landings, etc. must be engineered.
#
# Tested assumptions:
# - FreeCAD 0.20+ / 0.21+ (Part workbench objects)
# - Uses Part::Box only (no external dependencies)

import FreeCAD as App
import FreeCADGui as Gui

# -------------------------
# CONFIG
# -------------------------

# Cabin footprint (outer)
CABIN_W_FT = 10.0          # width in feet (X)
CABIN_L_FT = 12.0          # length in feet (Y)

WALL_FRAMING_H_FT = 8.0    # stud height / wall framing height
FLOOR_TO_FLOOR_FT = 9.0    # stacked module increment (overall, incl. floor+roof zone)

# Lumber actual dimensions
# 2x6 actual: 1.5" x 5.5"
THK_IN = 1.5
DEP_IN = 5.5

# Spacing
OC_IN = 24.0               # 24" on center for studs/joists/rafters

# Openings (rough openings)
DOOR_ROUGH_W_IN = 38.0
DOOR_ROUGH_H_IN = 82.5

WIN_ROUGH_W_IN  = 38.0
WIN_ROUGH_H_IN  = 48.0
WIN_SILL_H_IN   = 36.0

# Stair module
STAIR_FLOORS = 4                 # goes up 4 floors (0->1->2->3->4)
STAIR_WIDTH_FT = 4.0
STAIR_TOWER_L_FT = 12.0
STAIR_TOWER_W_FT = 6.0

RISER_IN = 7.5
TREAD_IN = 10.5
LANDING_DEPTH_FT = 3.0

POST_SIZE_IN = 3.5               # 4x4 actual approx

# -------------------------
# HELPERS
# -------------------------

def in_to_mm(x): return x * 25.4
def ft_to_mm(x): return x * 304.8

def ensure_doc():
    doc = App.ActiveDocument
    if doc is None:
        doc = App.newDocument("OSE_CabinCube_StairTower")
    return doc

def new_group(doc, name):
    grp = doc.addObject("App::DocumentObjectGroup", name)
    return grp

def add_box(doc, name, lx, ly, lz, x=0, y=0, z=0):
    obj = doc.addObject("Part::Box", name)
    obj.Length = lx
    obj.Width = ly
    obj.Height = lz
    obj.Placement.Base = App.Vector(x, y, z)
    return obj

def add_to(group, obj):
    group.addObject(obj)

def centered_positions(total_len, oc, member_thk, inset=0.0):
    """
    Centers start at inset + thk/2, step by oc, end <= total_len - inset - thk/2.
    Returns (origins, centers) where origin = center - thk/2.
    """
    start_c = inset + member_thk/2.0
    end_c = total_len - inset - member_thk/2.0
    if end_c < start_c:
        return [], []
    n = int((end_c - start_c) // oc) + 1
    centers = [start_c + i*oc for i in range(n)]
    origins = [c - member_thk/2.0 for c in centers]
    return origins, centers

# -------------------------
# BUILD CABIN
# -------------------------

def build_cabin(doc):
    # Convert dims
    cabin_w = ft_to_mm(CABIN_W_FT)
    cabin_l = ft_to_mm(CABIN_L_FT)
    wall_h  = ft_to_mm(WALL_FRAMING_H_FT)

    thk = in_to_mm(THK_IN)
    dep = in_to_mm(DEP_IN)
    oc = in_to_mm(OC_IN)

    door_w = in_to_mm(DOOR_ROUGH_W_IN)
    door_h = in_to_mm(DOOR_ROUGH_H_IN)

    win_w = in_to_mm(WIN_ROUGH_W_IN)
    win_h = in_to_mm(WIN_ROUGH_H_IN)
    win_sill = in_to_mm(WIN_SILL_H_IN)

    floor_depth = dep
    roof_depth  = dep

    plate_thk = thk
    header_thk = thk * 2.0  # 2-ply
    sill_thk = thk

    # Z zones
    floor_z0 = 0.0
    floor_z1 = floor_z0 + floor_depth

    wall_z0 = floor_z1
    wall_z1 = wall_z0 + wall_h

    roof_z0 = wall_z1
    roof_z1 = roof_z0 + roof_depth

    cabin_grp = new_group(doc, "CabinCube_10x12")

    # FLOOR
    floor_grp = new_group(doc, "Floor_2x6_24OC")
    add_to(cabin_grp, floor_grp)

    # Rim joists (perimeter)
    # Rim along Y at x=0 and x=cabin_w-thk, joists span Y
    rim_x0 = add_box(doc, "Rim_X0", thk, cabin_l, floor_depth, 0, 0, floor_z0)
    rim_x1 = add_box(doc, "Rim_X1", thk, cabin_l, floor_depth, cabin_w - thk, 0, floor_z0)
    # Rim along X at y=0 and y=cabin_l-thk
    rim_y0 = add_box(doc, "Rim_Y0", cabin_w, thk, floor_depth, 0, 0, floor_z0)
    rim_y1 = add_box(doc, "Rim_Y1", cabin_w, thk, floor_depth, 0, cabin_l - thk, floor_z0)

    for r in [rim_x0, rim_x1, rim_y0, rim_y1]:
        add_to(floor_grp, r)

    # Floor joists @ 24" OC: run along Y, spaced along X inside rims
    x_origins, _ = centered_positions(cabin_w, oc, thk, inset=thk)
    for i, x0 in enumerate(x_origins):
        j = add_box(doc, f"FloorJoist_{i:02d}", thk, cabin_l - 2*thk, floor_depth, x0, thk, floor_z0)
        add_to(floor_grp, j)

    # WALLS GROUP
    walls_grp = new_group(doc, "Walls_2x6_24OC")
    add_to(cabin_grp, walls_grp)

    # Helper to build a wall along X (depth in +Y)
    def wall_X(name, y_base, opening=None):
        wgrp = new_group(doc, name)
        add_to(walls_grp, wgrp)

        # Plates
        bp = add_box(doc, f"{name}_BottomPlate", cabin_w, dep, plate_thk, 0, y_base, wall_z0)
        tp1 = add_box(doc, f"{name}_TopPlate1", cabin_w, dep, plate_thk, 0, y_base, wall_z1 - 2*plate_thk)
        tp2 = add_box(doc, f"{name}_TopPlate2", cabin_w, dep, plate_thk, 0, y_base, wall_z1 - plate_thk)
        for p in [bp, tp1, tp2]:
            add_to(wgrp, p)

        # opening = dict(type='door'/'win', cx=?, w=?, h=?, sill=?)
        open_x0 = open_x1 = None
        if opening:
            cx = opening["cx"]
            w = opening["w"]
            open_x0 = cx - w/2.0
            open_x1 = cx + w/2.0

        # Studs @ 24" OC
        x_origins, x_centers = centered_positions(cabin_w, oc, thk, inset=0.0)
        stud_h = wall_h - 3*plate_thk
        for i, (x0, xc) in enumerate(zip(x_origins, x_centers)):
            if opening and (xc > open_x0) and (xc < open_x1):
                continue
            s = add_box(doc, f"{name}_Stud_{i:02d}", thk, dep, stud_h, x0, y_base, wall_z0 + plate_thk)
            add_to(wgrp, s)

        # Opening framing
        if opening:
            # kings
            kingL = add_box(doc, f"{name}_King_L", thk, dep, stud_h, open_x0 - thk, y_base, wall_z0 + plate_thk)
            kingR = add_box(doc, f"{name}_King_R", thk, dep, stud_h, open_x1, y_base, wall_z0 + plate_thk)
            add_to(wgrp, kingL); add_to(wgrp, kingR)

            if opening["type"] == "door":
                jack_h = opening["h"]
                header_z = wall_z0 + plate_thk + jack_h
                # jacks
                jackL = add_box(doc, f"{name}_Jack_L", thk, dep, jack_h, open_x0, y_base, wall_z0 + plate_thk)
                jackR = add_box(doc, f"{name}_Jack_R", thk, dep, jack_h, open_x1 - thk, y_base, wall_z0 + plate_thk)
                add_to(wgrp, jackL); add_to(wgrp, jackR)
                # header
                hdr = add_box(doc, f"{name}_Header", open_x1 - open_x0, dep, header_thk, open_x0, y_base, header_z)
                add_to(wgrp, hdr)
            else:
                # window
                sill = opening["sill"]
                jack_h = sill + opening["h"]
                header_z = wall_z0 + plate_thk + jack_h
                jackL = add_box(doc, f"{name}_Jack_L", thk, dep, jack_h, open_x0, y_base, wall_z0 + plate_thk)
                jackR = add_box(doc, f"{name}_Jack_R", thk, dep, jack_h, open_x1 - thk, y_base, wall_z0 + plate_thk)
                add_to(wgrp, jackL); add_to(wgrp, jackR)
                hdr = add_box(doc, f"{name}_Header", open_x1 - open_x0, dep, header_thk, open_x0, y_base, header_z)
                add_to(wgrp, hdr)

                sill_z = wall_z0 + plate_thk + sill
                sill_box = add_box(doc, f"{name}_Sill", open_x1 - open_x0, dep, sill_thk, open_x0, y_base, sill_z)
                add_to(wgrp, sill_box)

                # cripple studs under sill within opening
                span = open_x1 - open_x0
                ox, _ = centered_positions(span, oc, thk, inset=0.0)
                for k, local_x0 in enumerate(ox):
                    cr = add_box(doc, f"{name}_CrippleUnderSill_{k:02d}", thk, dep, sill, open_x0 + local_x0, y_base, wall_z0 + plate_thk)
                    add_to(wgrp, cr)

        return wgrp

    # Helper to build a wall along Y (depth in +X)
    def wall_Y(name, x_base, opening=None):
        wgrp = new_group(doc, name)
        add_to(walls_grp, wgrp)

        bp = add_box(doc, f"{name}_BottomPlate", dep, cabin_l, plate_thk, x_base, 0, wall_z0)
        tp1 = add_box(doc, f"{name}_TopPlate1", dep, cabin_l, plate_thk, x_base, 0, wall_z1 - 2*plate_thk)
        tp2 = add_box(doc, f"{name}_TopPlate2", dep, cabin_l, plate_thk, x_base, 0, wall_z1 - plate_thk)
        for p in [bp, tp1, tp2]:
            add_to(wgrp, p)

        open_y0 = open_y1 = None
        if opening:
            cy = opening["cy"]
            w = opening["w"]
            open_y0 = cy - w/2.0
            open_y1 = cy + w/2.0

        y_origins, y_centers = centered_positions(cabin_l, oc, thk, inset=0.0)
        stud_h = wall_h - 3*plate_thk
        for i, (y0, yc) in enumerate(zip(y_origins, y_centers)):
            if opening and (yc > open_y0) and (yc < open_y1):
                continue
            s = add_box(doc, f"{name}_Stud_{i:02d}", dep, thk, stud_h, x_base, y0, wall_z0 + plate_thk)
            add_to(wgrp, s)

        if opening:
            kingL = add_box(doc, f"{name}_King_L", dep, thk, stud_h, x_base, open_y0 - thk, wall_z0 + plate_thk)
            kingR = add_box(doc, f"{name}_King_R", dep, thk, stud_h, x_base, open_y1, wall_z0 + plate_thk)
            add_to(wgrp, kingL); add_to(wgrp, kingR)

            if opening["type"] == "door":
                jack_h = opening["h"]
                header_z = wall_z0 + plate_thk + jack_h
                jackL = add_box(doc, f"{name}_Jack_L", dep, thk, jack_h, x_base, open_y0, wall_z0 + plate_thk)
                jackR = add_box(doc, f"{name}_Jack_R", dep, thk, jack_h, x_base, open_y1 - thk, wall_z0 + plate_thk)
                add_to(wgrp, jackL); add_to(wgrp, jackR)
                hdr = add_box(doc, f"{name}_Header", dep, open_y1 - open_y0, header_thk, x_base, open_y0, header_z)
                add_to(wgrp, hdr)
            else:
                sill = opening["sill"]
                jack_h = sill + opening["h"]
                header_z = wall_z0 + plate_thk + jack_h
                jackL = add_box(doc, f"{name}_Jack_L", dep, thk, jack_h, x_base, open_y0, wall_z0 + plate_thk)
                jackR = add_box(doc, f"{name}_Jack_R", dep, thk, jack_h, x_base, open_y1 - thk, wall_z0 + plate_thk)
                add_to(wgrp, jackL); add_to(wgrp, jackR)
                hdr = add_box(doc, f"{name}_Header", dep, open_y1 - open_y0, header_thk, x_base, open_y0, header_z)
                add_to(wgrp, hdr)

                sill_z = wall_z0 + plate_thk + sill
                sill_box = add_box(doc, f"{name}_Sill", dep, open_y1 - open_y0, sill_thk, x_base, open_y0, sill_z)
                add_to(wgrp, sill_box)

                span = open_y1 - open_y0
                oy, _ = centered_positions(span, oc, thk, inset=0.0)
                for k, local_y0 in enumerate(oy):
                    cr = add_box(doc, f"{name}_CrippleUnderSill_{k:02d}", dep, thk, sill, x_base, open_y0 + local_y0, wall_z0 + plate_thk)
                    add_to(wgrp, cr)

        return wgrp

    # Place openings (simple choices)
    # Door on Y=0 wall, centered at X = 3' from left (configurable)
    door_cx = ft_to_mm(3.0)
    # Windows on opposite wall Y=cabin_l-dep, one left, one right
    win1_cx = ft_to_mm(3.0)
    win2_cx = cabin_w - ft_to_mm(3.0)

    wall_front = wall_X("Wall_Y0_Front", y_base=0.0, opening={"type":"door", "cx":door_cx, "w":door_w, "h":door_h})
    wall_back  = wall_X("Wall_Y1_Back",  y_base=cabin_l - dep, opening={"type":"win", "cx":win1_cx, "w":win_w, "h":win_h, "sill":win_sill})
    # Add second window on back wall by building a second opening frame “overlay” (simple hack):
    # For a cleaner parametric model you'd merge openings in one wall; this is adequate for concept framing.
    wall_back2 = wall_X("Wall_Y1_Back_Window2", y_base=cabin_l - dep, opening={"type":"win", "cx":win2_cx, "w":win_w, "h":win_h, "sill":win_sill})

    wall_left  = wall_Y("Wall_X0_Left",  x_base=0.0, opening=None)
    wall_right = wall_Y("Wall_X1_Right", x_base=cabin_w - dep, opening=None)

    # ROOF (flat, stackable): joists run along Y, spaced along X, same as floor
    roof_grp = new_group(doc, "Roof_2x6_24OC_Flat")
    add_to(cabin_grp, roof_grp)

    # roof rim
    rr_x0 = add_box(doc, "RoofRim_X0", thk, cabin_l, roof_depth, 0, 0, roof_z0)
    rr_x1 = add_box(doc, "RoofRim_X1", thk, cabin_l, roof_depth, cabin_w - thk, 0, roof_z0)
    rr_y0 = add_box(doc, "RoofRim_Y0", cabin_w, thk, roof_depth, 0, 0, roof_z0)
    rr_y1 = add_box(doc, "RoofRim_Y1", cabin_w, thk, roof_depth, 0, cabin_l - thk, roof_z0)
    for r in [rr_x0, rr_x1, rr_y0, rr_y1]:
        add_to(roof_grp, r)

    x_origins, _ = centered_positions(cabin_w, oc, thk, inset=thk)
    for i, x0 in enumerate(x_origins):
        j = add_box(doc, f"RoofJoist_{i:02d}", thk, cabin_l - 2*thk, roof_depth, x0, thk, roof_z0)
        add_to(roof_grp, j)

    # A simple bounding "module envelope" to visualize the 9' stack height
    envelope_grp = new_group(doc, "ModuleEnvelope_9ft")
    add_to(cabin_grp, envelope_grp)
    env = add_box(doc, "Envelope", cabin_w, cabin_l, ft_to_mm(FLOOR_TO_FLOOR_FT), 0, 0, 0)
    env.ViewObject.Transparency = 85
    add_to(envelope_grp, env)

    return cabin_grp

# -------------------------
# BUILD STAIR TOWER (SIMPLIFIED)
# -------------------------

def build_stair_tower(doc, base_x, base_y):
    tower_grp = new_group(doc, "StairTower_4Floors")

    thk = in_to_mm(THK_IN)
    post = in_to_mm(POST_SIZE_IN)

    tower_L = ft_to_mm(STAIR_TOWER_L_FT)
    tower_W = ft_to_mm(STAIR_TOWER_W_FT)
    stair_W = ft_to_mm(STAIR_WIDTH_FT)

    floor_to_floor = ft_to_mm(FLOOR_TO_FLOOR_FT)

    riser = in_to_mm(RISER_IN)
    tread = in_to_mm(TREAD_IN)
    landing_depth = ft_to_mm(LANDING_DEPTH_FT)

    # Posts at corners (concept)
    for i, (dx, dy) in enumerate([(0,0), (tower_W-post,0), (0,tower_L-post), (tower_W-post,tower_L-post)]):
        p = add_box(doc, f"Post_{i}", post, post, floor_to_floor*(STAIR_FLOORS+1), base_x+dx, base_y+dy, 0)
        add_to(tower_grp, p)

    # Landings at each level
    for lvl in range(STAIR_FLOORS+1):
        z = lvl * floor_to_floor
        land = add_box(doc, f"Landing_L{lvl}", tower_W, landing_depth, thk, base_x, base_y, z)
        add_to(tower_grp, land)

    # For each level, add a switchback: Flight A up half, mid-landing, Flight B up half
    # This is conceptual: we’ll model treads as thin boxes.
    def add_flight(name_prefix, start_x, start_y, start_z, dir_y=1):
        # number of risers for half-story
        half = floor_to_floor / 2.0
        n = int(max(1, round(half / riser)))
        dz = half / n
        # Each tread advances by "tread" in y
        for i in range(n):
            z = start_z + i*dz
            y = start_y + dir_y*i*tread
            step = add_box(doc, f"{name_prefix}_Step_{i:02d}", stair_W, tread, thk, start_x, y, z)
            add_to(tower_grp, step)
        # end position
        end_z = start_z + half
        end_y = start_y + dir_y*(n-1)*tread
        return end_y, end_z

    for lvl in range(STAIR_FLOORS):
        z0 = lvl * floor_to_floor
        # Flight A: go "north" in Y
        start_x = base_x + (tower_W - stair_W)/2.0
        start_y = base_y + landing_depth
        end_y, mid_z = add_flight(f"L{lvl}_A", start_x, start_y, z0, dir_y=1)

        # Mid-landing
        mid_land = add_box(doc, f"MidLanding_L{lvl}", tower_W, landing_depth, thk, base_x, base_y + (tower_L - landing_depth), mid_z)
        add_to(tower_grp, mid_land)

        # Flight B: go "south" in Y (switchback) up the remaining half
        start_y2 = base_y + (tower_L - landing_depth - tread)  # slightly inset
        _end_y2, _end_z2 = add_flight(f"L{lvl}_B", start_x, start_y2, mid_z, dir_y=-1)

    # Simple tower envelope
    env = add_box(doc, "StairTower_Envelope", tower_W, tower_L, floor_to_floor*(STAIR_FLOORS+1), base_x, base_y, 0)
    env.ViewObject.Transparency = 90
    add_to(tower_grp, env)

    return tower_grp

# -------------------------
# RUN
# -------------------------

doc = ensure_doc()

# Build cabin at origin
cabin = build_cabin(doc)

# Build stair tower offset to the side (so you can see both)
offset_x = ft_to_mm(CABIN_W_FT) + ft_to_mm(4.0)
offset_y = 0.0
stair = build_stair_tower(doc, base_x=offset_x, base_y=offset_y)

doc.recompute()

try:
    Gui.ActiveDocument.ActiveView.viewAxonometric()
    Gui.SendMsgToActiveView("ViewFit")
except Exception:
    pass

print("OSE cabin cube + stair tower generated. Check Tree view for groups.")
