Generate wall library.py: Difference between revisions

From Open Source Ecology
Jump to navigation Jump to search
(Created page with "#!/usr/bin/env python3 from pathlib import Path from typing import Any import math import sys import yaml import FreeCAD import Part OUTPUT_DIR = Path("cad_library") def load_yaml(path: Path) -> Any: with path.open("r", encoding="utf-8") as f: return yaml.safe_load(f) def nominal_to_actual_lumber(nominal: str) -> tuple[float, float]: mapping = { "2x2": (1.5, 1.5), "2x3": (1.5, 2.5), "2x4": (1.5, 3.5), "2x6": (1.5,...")
 
No edit summary
 
(2 intermediate revisions by the same user not shown)
Line 2: Line 2:


from pathlib import Path
from pathlib import Path
from typing import Any
import math
import sys
import sys
import yaml
import yaml
import FreeCAD
import FreeCAD
import Part
import Part


OUTPUT_DIR = Path("cad_library")


OUTPUT_DIR = Path("cad_library")
IN = 25.4




def load_yaml(path: Path) -> Any:
def load_yaml(path):
     with path.open("r", encoding="utf-8") as f:
     with open(path, "r") as f:
         return yaml.safe_load(f)
         return yaml.safe_load(f)




def nominal_to_actual_lumber(nominal: str) -> tuple[float, float]:
def nominal_to_actual_lumber(nominal):
     mapping = {
 
     table = {
         "2x2": (1.5, 1.5),
         "2x2": (1.5, 1.5),
         "2x3": (1.5, 2.5),
         "2x3": (1.5, 2.5),
Line 30: Line 28:
         "2x12": (1.5, 11.25),
         "2x12": (1.5, 11.25),
     }
     }
    if nominal not in mapping:
        raise ValueError(f"Unsupported nominal lumber size: {nominal}")
    return mapping[nominal]


    return table[nominal]


def ft_to_in(value_ft: float) -> float:
    return value_ft * 12.0


def in_to_mm(v):
    return v * IN


def make_box(x_len: float, y_len: float, z_len: float, x: float, y: float, z: float):
    shape = Part.makeBox(x_len, y_len, z_len)
    shape.translate(FreeCAD.Vector(x, y, z))
    return shape


def ft_to_in(v):
    return v * 12
def derive_stud_positions(width_in, stud_thickness_in, spacing_oc_in):


def derive_stud_positions(width_in: float, stud_thickness_in: float, spacing_oc_in: float) -> list[float]:
    """
    Returns x positions of stud left faces.
    Includes left end stud and right end stud.
    Intermediate studs placed at requested on-center spacing where possible.
    """
     positions = [0.0]
     positions = [0.0]


     current_left_face = spacing_oc_in
     right_edge = width_in - stud_thickness_in
     right_end_left_face = width_in - stud_thickness_in
     current = spacing_oc_in


     while current_left_face + stud_thickness_in <= right_end_left_face - 1e-6:
     while current + stud_thickness_in <= right_edge:
         positions.append(current_left_face)
         positions.append(current)
         current_left_face += spacing_oc_in
         current += spacing_oc_in


     if right_end_left_face > 0.0 and (not math.isclose(positions[-1], right_end_left_face, abs_tol=1e-6)):
     if positions[-1] != right_edge:
         positions.append(right_end_left_face)
         positions.append(right_edge)


     # Deduplicate while preserving order
     return positions
    deduped = []
    for p in positions:
        if not deduped or not math.isclose(deduped[-1], p, abs_tol=1e-6):
            deduped.append(p)


    return deduped


def build_wall(instance):


def build_wall(instance: dict[str, Any]) -> FreeCAD.Document:
     instance_id = instance["id"]
     instance_id = instance["id"]
     params = instance["parameters"]
     params = instance["parameters"]


     width_ft = float(params["nominal_width_ft"])
     width_ft = params["nominal_width_ft"]
     height_ft = float(params["nominal_height_ft"])
     height_ft = params["nominal_height_ft"]
     stud_nominal = str(params["stud_lumber_nominal"])
     stud_nominal = params["stud_lumber_nominal"]
     spacing_oc_in = float(params["stud_spacing_oc_in"])
     spacing_oc_in = params["stud_spacing_oc_in"]
     osb_thickness_in = float(params["osb_thickness_in"])
     osb_thickness_in = params["osb_thickness_in"]
    exterior_face = str(params["exterior_face"])


     stud_thickness_in, stud_depth_in = nominal_to_actual_lumber(stud_nominal)
     stud_thickness_in, stud_depth_in = nominal_to_actual_lumber(stud_nominal)
Line 89: Line 74:


     plate_thickness_in = stud_thickness_in
     plate_thickness_in = stud_thickness_in
     stud_length_in = framed_height_in - 2.0 * plate_thickness_in
     stud_length_in = framed_height_in - (2 * plate_thickness_in)
 
    if stud_length_in <= 0:
        raise ValueError(f"{instance_id}: derived stud length is non-positive")


     doc = FreeCAD.newDocument(instance_id)
     width_mm = in_to_mm(width_in)
    stud_depth_mm = in_to_mm(stud_depth_in)
    plate_thickness_mm = in_to_mm(plate_thickness_in)
    stud_length_mm = in_to_mm(stud_length_in)
    osb_thickness_mm = in_to_mm(osb_thickness_in)
    framed_height_mm = in_to_mm(framed_height_in)


     shapes = []
     shapes = []


     # Coordinate convention:
     # bottom plate
     # X = wall width
     shapes.append(
    # Y = wall thickness (interior -> exterior)
        Part.makeBox(
    # Z = wall height
            width_mm,
    #
            stud_depth_mm,
    # Framing occupies Y = 0 to stud_depth_in
            plate_thickness_mm
    # OSB sits on exterior side.
        )
     #
     )
    # exterior_face is metadata for now; geometry is built with exterior on +Y.


     # Bottom plate
     # top plate
     bottom_plate = make_box(
     top_plate = Part.makeBox(
         width_in,
         width_mm,
        stud_depth_in,
         stud_depth_mm,
        plate_thickness_in,
         plate_thickness_mm
         0.0,
         0.0,
        0.0,
     )
     )
    shapes.append(("bottom_plate", bottom_plate))


    # Top plate
     top_plate.translate(
     top_plate = make_box(
         FreeCAD.Vector(
         width_in,
            0,
        stud_depth_in,
            0,
        plate_thickness_in,
            framed_height_mm - plate_thickness_mm
        0.0,
         )
        0.0,
         framed_height_in - plate_thickness_in,
     )
     )
    shapes.append(("top_plate", top_plate))


     # Studs
    shapes.append(top_plate)
 
     # studs
     stud_positions = derive_stud_positions(width_in, stud_thickness_in, spacing_oc_in)
     stud_positions = derive_stud_positions(width_in, stud_thickness_in, spacing_oc_in)
     for i, x in enumerate(stud_positions):
 
         stud = make_box(
     for x in stud_positions:
             stud_thickness_in,
 
             stud_depth_in,
         stud = Part.makeBox(
             stud_length_in,
             in_to_mm(stud_thickness_in),
             x,
             stud_depth_mm,
            0.0,
             stud_length_mm
             plate_thickness_in,
        )
 
        stud.translate(
             FreeCAD.Vector(
                in_to_mm(x),
                0,
                plate_thickness_mm
             )
         )
         )
        shapes.append((f"stud_{i+1}", stud))


     # OSB on exterior side (+Y)
        shapes.append(stud)
     osb = make_box(
 
         width_in,
     # OSB sheathing (south side = negative Y)
        osb_thickness_in,
     osb = Part.makeBox(
        framed_height_in,
         width_mm,
         0.0,
         osb_thickness_mm,
         stud_depth_in,
         framed_height_mm
        0.0,
     )
     )
    shapes.append(("osb_exterior", osb))


     for name, shape in shapes:
     osb.translate(
         obj = doc.addObject("Part::Feature", name)
         FreeCAD.Vector(
        obj.Shape = shape
            0,
            -osb_thickness_mm,
            0
        )
    )


     # Add simple metadata object
    shapes.append(osb)
     meta = doc.addObject("App::FeaturePython", "metadata")
 
     meta.Label = f"{instance_id} | exterior={exterior_face}"
     # create compound module
     wall = Part.makeCompound(shapes)
 
    doc = FreeCAD.newDocument(instance_id)
 
    obj = doc.addObject("Part::Feature", "wall_module")
 
     obj.Shape = wall


     doc.recompute()
     doc.recompute()
     return doc
     return doc




def save_document(doc: FreeCAD.Document, out_path: Path) -> None:
def save_document(doc, path):
    out_path.parent.mkdir(parents=True, exist_ok=True)
    doc.saveAs(str(out_path))


    path.parent.mkdir(exist_ok=True)
    doc.saveAs(str(path))


def main() -> int:
    if len(sys.argv) != 2:
        print("Usage: python generate_wall_library.py <instances.yaml>")
        return 2


    instances_path = Path(sys.argv[1])
def main():
    if not instances_path.exists():
        print(f"Error: instances file not found: {instances_path}")
        return 2


     try:
     if len(sys.argv) != 2:
        instances_doc = load_yaml(instances_path)
         print("Usage: generate_wall_library.py instances.yaml")
    except Exception as e:
         sys.exit(1)
         print(f"Error loading instances file: {e}")
         return 2


     if not isinstance(instances_doc, dict) or "instances" not in instances_doc:
     instances_doc = load_yaml(sys.argv[1])
        print("Instances file must contain top-level key 'instances'")
        return 1


     instances = instances_doc["instances"]
     instances = instances_doc["instances"]
    if not isinstance(instances, list):
        print("'instances' must be a list")
        return 1


     OUTPUT_DIR.mkdir(exist_ok=True)
     OUTPUT_DIR.mkdir(exist_ok=True)


     for instance in instances:
     for inst in instances:
         instance_id = instance["id"]
 
         instance_id = inst["id"]
 
         print(f"Generating {instance_id}...")
         print(f"Generating {instance_id}...")
         doc = build_wall(instance)
 
         doc = build_wall(inst)
 
         out_path = OUTPUT_DIR / f"{instance_id}.FCStd"
         out_path = OUTPUT_DIR / f"{instance_id}.FCStd"
         save_document(doc, out_path)
         save_document(doc, out_path)
        print(f"  Saved: {out_path}")


     print(f"Generated {len(instances)} wall module(s) in {OUTPUT_DIR}")
        print(f"Saved {out_path}")
    return 0
 
     print(f"Generated {len(instances)} modules")




if __name__ == "__main__":
if __name__ == "__main__":
     raise SystemExit(main())
     main()

Latest revision as of 03:32, 11 March 2026

  1. !/usr/bin/env python3

from pathlib import Path import sys import yaml import FreeCAD import Part

OUTPUT_DIR = Path("cad_library")

IN = 25.4


def load_yaml(path):

   with open(path, "r") as f:
       return yaml.safe_load(f)


def nominal_to_actual_lumber(nominal):

   table = {
       "2x2": (1.5, 1.5),
       "2x3": (1.5, 2.5),
       "2x4": (1.5, 3.5),
       "2x6": (1.5, 5.5),
       "2x8": (1.5, 7.25),
       "2x10": (1.5, 9.25),
       "2x12": (1.5, 11.25),
   }
   return table[nominal]


def in_to_mm(v):

   return v * IN


def ft_to_in(v):

   return v * 12


def derive_stud_positions(width_in, stud_thickness_in, spacing_oc_in):

   positions = [0.0]
   right_edge = width_in - stud_thickness_in
   current = spacing_oc_in
   while current + stud_thickness_in <= right_edge:
       positions.append(current)
       current += spacing_oc_in
   if positions[-1] != right_edge:
       positions.append(right_edge)
   return positions


def build_wall(instance):

   instance_id = instance["id"]
   params = instance["parameters"]
   width_ft = params["nominal_width_ft"]
   height_ft = params["nominal_height_ft"]
   stud_nominal = params["stud_lumber_nominal"]
   spacing_oc_in = params["stud_spacing_oc_in"]
   osb_thickness_in = params["osb_thickness_in"]
   stud_thickness_in, stud_depth_in = nominal_to_actual_lumber(stud_nominal)
   width_in = ft_to_in(width_ft)
   framed_height_in = ft_to_in(height_ft)
   plate_thickness_in = stud_thickness_in
   stud_length_in = framed_height_in - (2 * plate_thickness_in)
   width_mm = in_to_mm(width_in)
   stud_depth_mm = in_to_mm(stud_depth_in)
   plate_thickness_mm = in_to_mm(plate_thickness_in)
   stud_length_mm = in_to_mm(stud_length_in)
   osb_thickness_mm = in_to_mm(osb_thickness_in)
   framed_height_mm = in_to_mm(framed_height_in)
   shapes = []
   # bottom plate
   shapes.append(
       Part.makeBox(
           width_mm,
           stud_depth_mm,
           plate_thickness_mm
       )
   )
   # top plate
   top_plate = Part.makeBox(
       width_mm,
       stud_depth_mm,
       plate_thickness_mm
   )
   top_plate.translate(
       FreeCAD.Vector(
           0,
           0,
           framed_height_mm - plate_thickness_mm
       )
   )
   shapes.append(top_plate)
   # studs
   stud_positions = derive_stud_positions(width_in, stud_thickness_in, spacing_oc_in)
   for x in stud_positions:
       stud = Part.makeBox(
           in_to_mm(stud_thickness_in),
           stud_depth_mm,
           stud_length_mm
       )
       stud.translate(
           FreeCAD.Vector(
               in_to_mm(x),
               0,
               plate_thickness_mm
           )
       )
       shapes.append(stud)
   # OSB sheathing (south side = negative Y)
   osb = Part.makeBox(
       width_mm,
       osb_thickness_mm,
       framed_height_mm
   )
   osb.translate(
       FreeCAD.Vector(
           0,
           -osb_thickness_mm,
           0
       )
   )
   shapes.append(osb)
   # create compound module
   wall = Part.makeCompound(shapes)
   doc = FreeCAD.newDocument(instance_id)
   obj = doc.addObject("Part::Feature", "wall_module")
   obj.Shape = wall
   doc.recompute()
   return doc


def save_document(doc, path):

   path.parent.mkdir(exist_ok=True)
   doc.saveAs(str(path))


def main():

   if len(sys.argv) != 2:
       print("Usage: generate_wall_library.py instances.yaml")
       sys.exit(1)
   instances_doc = load_yaml(sys.argv[1])
   instances = instances_doc["instances"]
   OUTPUT_DIR.mkdir(exist_ok=True)
   for inst in instances:
       instance_id = inst["id"]
       print(f"Generating {instance_id}...")
       doc = build_wall(inst)
       out_path = OUTPUT_DIR / f"{instance_id}.FCStd"
       save_document(doc, out_path)
       print(f"Saved {out_path}")
   print(f"Generated {len(instances)} modules")


if __name__ == "__main__":

   main()