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
Line 2: Line 2:


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


Line 11: Line 9:
import Part
import Part


OUTPUT_DIR = Path("cad_library")


OUTPUT_DIR = Path("cad_library")
IN = 25.4
FT = 304.8




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 30:
         "2x12": (1.5, 11.25),
         "2x12": (1.5, 11.25),
     }
     }
    if nominal not in mapping:
     return table[nominal]
        raise ValueError(f"Unsupported nominal lumber size: {nominal}")
     return mapping[nominal]




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




def make_box(x_len: float, y_len: float, z_len: float, x: float, y: float, z: float):
def ft_to_mm(x):
    return x * FT
 
 
def make_box(x_len, y_len, z_len, x, y, z):
     shape = Part.makeBox(x_len, y_len, z_len)
     shape = Part.makeBox(x_len, y_len, z_len)
     shape.translate(FreeCAD.Vector(x, y, z))
     shape.translate(FreeCAD.Vector(x, y, z))
Line 45: Line 47:




def derive_stud_positions(width_in: float, stud_thickness_in: float, spacing_oc_in: float) -> list[float]:
def derive_stud_positions(width_in, stud_thickness_in, spacing_oc_in):
    """
 
    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)


     width_in = ft_to_in(width_ft)
     width_in = width_ft * 12
     framed_height_in = ft_to_in(height_ft)
     framed_height_in = height_ft * 12


     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)
     doc = FreeCAD.newDocument(instance_id)
Line 98: Line 87:
     shapes = []
     shapes = []


     # Coordinate convention:
     width_mm = in_to_mm(width_in)
     # X = wall width
     stud_depth_mm = in_to_mm(stud_depth_in)
     # Y = wall thickness (interior -> exterior)
     plate_thickness_mm = in_to_mm(plate_thickness_in)
     # Z = wall height
     stud_length_mm = in_to_mm(stud_length_in)
     #
     osb_thickness_mm = in_to_mm(osb_thickness_in)
     # Framing occupies Y = 0 to stud_depth_in
     framed_height_mm = in_to_mm(framed_height_in)
    # OSB sits on exterior side.
    #
    # exterior_face is metadata for now; geometry is built with exterior on +Y.


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


    # Top plate
     top_plate = make_box(
     top_plate = make_box(
         width_in,
         width_mm,
         stud_depth_in,
         stud_depth_mm,
         plate_thickness_in,
         plate_thickness_mm,
         0.0,
         0,
         0.0,
         0,
         framed_height_in - plate_thickness_in,
         framed_height_mm - plate_thickness_mm
     )
     )
     shapes.append(("top_plate", top_plate))
     shapes.append(("top_plate", 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):
     for i, x in enumerate(stud_positions):
         stud = make_box(
         stud = make_box(
             stud_thickness_in,
             in_to_mm(stud_thickness_in),
             stud_depth_in,
             stud_depth_mm,
             stud_length_in,
             stud_length_mm,
             x,
             in_to_mm(x),
             0.0,
             0,
             plate_thickness_in,
             plate_thickness_mm
         )
         )
        shapes.append((f"stud_{i+1}", stud))


    # OSB on exterior side (+Y)
        shapes.append((f"stud_{i+1:02d}", stud))
 
     osb = make_box(
     osb = make_box(
         width_in,
         width_mm,
         osb_thickness_in,
         osb_thickness_mm,
         framed_height_in,
         framed_height_mm,
         0.0,
         0,
         stud_depth_in,
         stud_depth_mm,
         0.0,
         0
     )
     )
     shapes.append(("osb_exterior", osb))
     shapes.append(("osb_exterior", osb))


Line 158: Line 144:
         obj.Shape = shape
         obj.Shape = shape


     # Add simple metadata object
     doc.recompute()
    meta = doc.addObject("App::FeaturePython", "metadata")
    meta.Label = f"{instance_id} | exterior={exterior_face}"


    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)
     path.parent.mkdir(exist_ok=True)
     doc.saveAs(str(out_path))
     doc.saveAs(str(path))
 


def main():


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


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


     try:
     for inst in instances:
        instances_doc = load_yaml(instances_path)
    except Exception as e:
        print(f"Error loading instances file: {e}")
        return 2


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


    instances = instances_doc["instances"]
         print(f"Generating {instance_id}...")
    if not isinstance(instances, list):
         print("'instances' must be a list")
        return 1


    OUTPUT_DIR.mkdir(exist_ok=True)
        doc = build_wall(inst)


    for instance in instances:
        instance_id = instance["id"]
        print(f"Generating {instance_id}...")
        doc = build_wall(instance)
         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()

Revision as of 02:41, 11 March 2026

  1. !/usr/bin/env python3

from pathlib import Path import sys import math import yaml

import FreeCAD import Part

OUTPUT_DIR = Path("cad_library")

IN = 25.4 FT = 304.8


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(x):

   return x * IN


def ft_to_mm(x):

   return x * FT


def make_box(x_len, y_len, z_len, x, y, z):

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


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 = width_ft * 12
   framed_height_in = height_ft * 12
   plate_thickness_in = stud_thickness_in
   stud_length_in = framed_height_in - 2 * plate_thickness_in
   doc = FreeCAD.newDocument(instance_id)
   shapes = []
   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)
   bottom_plate = make_box(
       width_mm,
       stud_depth_mm,
       plate_thickness_mm,
       0,
       0,
       0
   )
   shapes.append(("bottom_plate", bottom_plate))
   top_plate = make_box(
       width_mm,
       stud_depth_mm,
       plate_thickness_mm,
       0,
       0,
       framed_height_mm - plate_thickness_mm
   )
   shapes.append(("top_plate", top_plate))
   stud_positions = derive_stud_positions(width_in, stud_thickness_in, spacing_oc_in)
   for i, x in enumerate(stud_positions):
       stud = make_box(
           in_to_mm(stud_thickness_in),
           stud_depth_mm,
           stud_length_mm,
           in_to_mm(x),
           0,
           plate_thickness_mm
       )
       shapes.append((f"stud_{i+1:02d}", stud))
   osb = make_box(
       width_mm,
       osb_thickness_mm,
       framed_height_mm,
       0,
       stud_depth_mm,
       0
   )
   shapes.append(("osb_exterior", osb))
   for name, shape in shapes:
       obj = doc.addObject("Part::Feature", name)
       obj.Shape = shape
   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()