From a9f8993237a3e3f79b17ca53c4c6241c7e42a7d4 Mon Sep 17 00:00:00 2001 From: Furgo <148809153+furgo16@users.noreply.github.com> Date: Fri, 23 May 2025 11:26:12 +0200 Subject: [PATCH] BIM: Arch.makeWindow, expand docstring, add examples --- src/Mod/BIM/Arch.py | 218 +++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 205 insertions(+), 13 deletions(-) diff --git a/src/Mod/BIM/Arch.py b/src/Mod/BIM/Arch.py index 94a1ebbcd1..c8b50ac2c2 100644 --- a/src/Mod/BIM/Arch.py +++ b/src/Mod/BIM/Arch.py @@ -1822,30 +1822,222 @@ def makeWindow( name: Optional[str] = None, ) -> FreeCAD.DocumentObject: """ - Creates a window object based on the given base object. + Creates an Arch Window object, which can represent either a window or a door. + + The created object can be based on a 2D profile (e.g., a Sketch), have its + dimensions set directly, or be defined by custom components. It can be + inserted into host objects like Walls, creating openings. The IfcType of + the object can be set to "Window" or "Door" accordingly (presets often + handle this automatically). Parameters ---------- - baseobj : Draft.Wire or Sketcher.Sketch, optional - The base object for the window. It should be a well-formed, closed - Draft.Wire or Sketcher.Sketch object. Defaults to None. + baseobj : FreeCAD.DocumentObject, optional + The base object for the window/door. + If `baseobj` is an existing `Arch.Window` (or Door), it will be cloned. + If `baseobj` is a 2D object with wires (e.g., `Sketcher::SketchObject`, + `Draft.Wire`), these wires are used to define the geometry. + If `parts` is None, default components are generated from `baseobj.Shape.Wires`: + - If one closed wire: `["Default", "Frame", "Wire0", "1", "0"]` (or "Solid panel"). + - If multiple closed wires (e.g., Wire0 outer, Wire1 inner): + `["Default", "Frame", "Wire0,Wire1", "1", "0"]` (Wire1 cuts Wire0). + The `Normal` direction is derived from `baseobj.Placement`. + Defaults to None. width : float, optional - The width of the window. Defaults to None. + The total width of the window/door. + If `baseobj` is None, this value is used by `ensureBase()` on first + recompute to create a default sketch with a "Width" constraint. + If `baseobj` is a sketch with a "Width" named constraint, setting + `window_or_door.Width` will drive this sketch constraint. `makeWindow` itself + does not initially set the object's `Width` *from* a sketch's constraint. + Defaults to None (or an Arch preference value if `baseobj` is None). height : float, optional - The height of the window. Defaults to None. - parts : list, optional - The parts of the window. Defaults to None. + The total height of the window/door. + If `baseobj` is None, this value is used by `ensureBase()` on first + recompute to create a default sketch with a "Height" constraint. + If `baseobj` is a sketch with a "Height" named constraint, setting + `window_or_door.Height` will drive this sketch constraint. `makeWindow` itself + does not initially set the object's `Height` *from* a sketch's constraint. + Defaults to None (or an Arch preference value if `baseobj` is None). + parts : list[str], optional + A list defining custom components for the window/door. The list is flat, with + every 5 elements describing one component: + `["Name1", "Type1", "WiresStr1", "ThickStr1", "OffsetStr1", ...]` + - `Name`: User-defined name (e.g., "OuterFrame"). + - `Type`: Component type (e.g., "Frame", "Glass panel", "Solid panel"). + See `ArchWindow.WindowPartTypes`. + - `WiresStr`: Comma-separated string defining wire usage from `baseobj.Shape.Wires` + (0-indexed) and optionally hinge/opening from `baseobj.Shape.Edges` (1-indexed). + Example: `"Wire0,Wire1,Edge8,Mode1"`. + - `"WireN"`: Uses Nth wire for the base face. + - `"WireN,WireM"`: WireN is base, WireM is cutout. + - `"EdgeK"`: Kth edge is hinge. + - `"ModeL"`: Lth opening mode from `ArchWindow.WindowOpeningModes`. + - `ThickStr`: Thickness as string (e.g., `"50.0"`). Appending `"+V"` + adds the object's `Frame` property value. + - `OffsetStr`: Offset along normal as string (e.g., `"25.0"`). Appending `"+V"` + adds the object's `Offset` property value. + Defaults to None. If None and `baseobj` is a sketch, default parts + are generated as described under `baseobj`. name : str, optional - The name to assign to the created window. Defaults to None. + The name (label) for the created window/door. If None, a default localized + name ("Window" or "Door", depending on context or subsequent changes) is used. + Defaults to None. Returns ------- - Part::FeaturePython - The created window object. + FreeCAD.DocumentObject + The created Arch Window object (which is a `Part::FeaturePython` instance, + configurable to represent a window or a door). + + See Also + -------- + ArchWindowPresets.makeWindowPreset : Create window/door from predefined types. + ArchWall.addComponents : Add a window/door to a wall (creates opening). Notes ----- - 1. If baseobj is not a closed shape, the tool may not create a proper solid figure. + - **Dual purpose (window/door)**: despite its name, this function is the primary + way to programmatically create both windows and doors in the BIM workbench. + The distinction is often made by setting the `IfcType` property of the + created object to "Window" or "Door", and by the chosen components or preset. + - **Sketch-based dimensions**: If `baseobj` is a `Sketcher::SketchObject` + with named constraints "Width" and "Height", these sketch constraints will be + parametrically driven by the created object's `Width` and `Height` properties + respectively *after* the object is created and its properties are changed. + `makeWindow` itself does not initially populate the object's `Width`/`Height` from + these sketch constraints if `width`/`height` arguments are not passed to it. + The object's internal `Width` and `Height` properties are the drivers. + - **Object from dimensions (No `baseobj` initially)**: if `baseobj` is `None` but + `width` and `height` are provided, `makeWindow` creates an Arch Window object. + Upon the first `doc.recompute()`, the `ensureBase()` mechanism generates + an internal sketch (`obj.Base`) with "Width" and "Height" constraints + driven by `obj.Width` and `obj.Height`. However, `obj.WindowParts` + will remain undefined, resulting in a shapeless object until `WindowParts` + are manually set. + - **`obj.Frame` and `obj.Offset` properties**: these main properties of the + created object (e.g., `my_window.Frame = 50.0`) provide the values used when + `"+V"` is specified in the `ThicknessString` or `OffsetString` of a component + within the `parts` list. + - **Hosting and openings**: to create an opening in a host object (e.g., `Arch.Wall`), + set `obj.Hosts = [my_wall]`. The opening's shape is typically derived + from `obj.HoleWire` (defaulting to the largest wire of `obj.Base`) and + extruded by `obj.HoleDepth` (if 0, tries to match host thickness). + A custom `obj.Subvolume` can also define the opening shape. + - **Component management**: components and their geometry are primarily + managed by the `_Window` class and its methods in `ArchWindow.py`. + - **Initialization from sketch `baseobj`**: when `baseobj` is a sketch + (e.g., `Sketcher::SketchObject`) and `parts` is `None` or provided: + - The `window.Shape` (geometric representation) is correctly generated + at the global position and orientation defined by `baseobj.Placement`. + - However, the created window object's own `window.Placement` property is + **not** automatically initialized from `baseobj.Placement` and typically + remains at the identity placement (origin, no rotation). + - Similarly, the `window.Width` and `window.Height` properties are **not** + automatically populated from the dimensions of the `baseobj` sketch. + These properties will default to 0.0 or values from Arch preferences + (if `width`/`height` arguments to `makeWindow` are also `None`). + - If you need the `window` object's `Placement`, `Width`, or `Height` + properties to reflect the `baseobj` sketch for subsequent operations + (e.g., if other systems query these specific window properties, or if + you intend to parametrically drive the sketch via these window properties), + you may need to set them manually after `makeWindow` is called: + - The `ArchWindow._Window.execute()` method, when recomputing the window, + *does* use `window.Base.Shape` (the sketch's shape in its global position) + to generate the window's geometry. The `ArchWindow._Window.getSubVolume()` + method also correctly uses `window.Base.Shape` and the window object's + (identity) `Placement` for creating the cutting volume. + + Examples + -------- + >>> import FreeCAD as App + >>> import Draft, Arch, Sketcher, Part + >>> doc = App.newDocument("ArchWindowDoorExamples") + + >>> # Ex1: Basic window from sketch and parts definition, oriented to XZ (vertical) plane + >>> sketch_ex1 = doc.addObject('Sketcher::SketchObject', 'WindowSketchEx1_Vertical') + >>> # Define geometry in sketch's local XY plane (width along local X, height along local Y) + >>> sketch_ex1.addGeometry(Part.LineSegment(App.Vector(0,0,0), App.Vector(1000,0,0))) # Wire0 - Outer + >>> sketch_ex1.addGeometry(Part.LineSegment(App.Vector(1000,0,0), App.Vector(1000,1200,0))) + >>> sketch_ex1.addGeometry(Part.LineSegment(App.Vector(1000,1200,0), App.Vector(0,1200,0))) + >>> sketch_ex1.addGeometry(Part.LineSegment(App.Vector(0,1200,0), App.Vector(0,0,0))) + >>> sketch_ex1.addGeometry(Part.LineSegment(App.Vector(100,100,0), App.Vector(900,100,0))) # Wire1 - Inner + >>> sketch_ex1.addGeometry(Part.LineSegment(App.Vector(900,100,0), App.Vector(900,1100,0))) + >>> sketch_ex1.addGeometry(Part.LineSegment(App.Vector(900,1100,0), App.Vector(100,1100,0))) + >>> sketch_ex1.addGeometry(Part.LineSegment(App.Vector(100,1100,0), App.Vector(100,100,0))) + >>> doc.recompute() # Update sketch Wires + >>> # Orient sketch: Rotate +90 deg around X-axis to place sketch's XY onto global XZ. + >>> # Sketch's local Y (height) now aligns with global Z. Sketch normal is global -Y. + >>> sketch_ex1.Placement.Rotation = App.Rotation(App.Vector(1,0,0), 90) + >>> doc.recompute() # Apply sketch placement + >>> window_ex1 = Arch.makeWindow(baseobj=sketch_ex1, name="MyWindowEx1_Vertical") + >>> # Window Normal will be derived as global +Y, extrusion along +Y. + >>> window_ex1.WindowParts = [ + ... "Frame", "Frame", "Wire0,Wire1", "60", "0", # Frame from Wire0-Wire1 + ... "Glass", "Glass panel", "Wire1", "10", "25" # Glass from Wire1, offset in Normal dir + ... ] + >>> doc.recompute() + + >>> # Ex2: Window from sketch with named "Width"/"Height" constraints (on default XY plane) + >>> sketch_ex2 = doc.addObject('Sketcher::SketchObject', 'WindowSketchEx2_Named') + >>> sketch_ex2.addGeometry(Part.LineSegment(App.Vector(0,0,0), App.Vector(800,0,0))) # Edge 0 + >>> sketch_ex2.addGeometry(Part.LineSegment(App.Vector(800,0,0), App.Vector(800,600,0))) # Edge 1 + >>> sketch_ex2.addGeometry(Part.LineSegment(App.Vector(800,600,0), App.Vector(0,600,0))) # Complete Wire0 + >>> sketch_ex2.addGeometry(Part.LineSegment(App.Vector(0,600,0), App.Vector(0,0,0))) + >>> sketch_ex2.addConstraint(Sketcher.Constraint('DistanceX',0,1,0,2, 800)) + >>> sketch_ex2.renameConstraint(sketch_ex2.ConstraintCount-1, "Width") + >>> sketch_ex2.addConstraint(Sketcher.Constraint('DistanceY',1,1,1,2, 600)) + >>> sketch_ex2.renameConstraint(sketch_ex2.ConstraintCount-1, "Height") + >>> doc.recompute() + >>> window_ex2 = Arch.makeWindow(baseobj=sketch_ex2, name="MyWindowEx2_Parametric") + >>> window_ex2.WindowParts = ["Frame", "Frame", "Wire0", "50", "0"] + >>> doc.recompute() + >>> print(f"Ex2 Initial - Sketch Width: {sketch_ex2.getDatum('Width')}, Window Width: {window_ex2.Width.Value}") + >>> window_ex2.Width = 950 # This drives the sketch constraint + >>> doc.recompute() + >>> print(f"Ex2 Updated - Sketch Width: {sketch_ex2.getDatum('Width')}, Window Width: {window_ex2.Width.Value}") + + >>> # Ex3: Window from dimensions only (initially shapeless, sketch on XY plane) + >>> window_ex3 = Arch.makeWindow(width=700, height=900, name="MyWindowEx3_Dims") + >>> print(f"Ex3 Initial - Base: {window_ex3.Base}, Shape isNull: {window_ex3.Shape.isNull()}") + >>> doc.recompute() # ensureBase creates the sketch on XY plane + >>> print(f"Ex3 After Recompute - Base: {window_ex3.Base.Name if window_ex3.Base else 'None'}, Shape isNull: {window_ex3.Shape.isNull()}") + >>> window_ex3.WindowParts = ["SimpleFrame", "Frame", "Wire0", "40", "0"] # Wire0 from auto-generated sketch + >>> doc.recompute() + >>> print(f"Ex3 After Parts - Shape isNull: {window_ex3.Shape.isNull()}") + + >>> # Ex4: Door created using an ArchWindowPresets function + >>> # Note: Arch.makeWindowPreset calls Arch.makeWindow internally + >>> door_ex4_preset = makeWindowPreset( + ... "Simple door", width=900, height=2100, + ... h1=50, h2=0, h3=0, w1=70, w2=40, o1=0, o2=0 # Preset-specific params + ... ) + >>> if door_ex4_preset: + ... door_ex4_preset.Label = "MyDoorEx4_Preset" + ... doc.recompute() + + >>> # Ex5: Door created from a sketch, with IfcType manually set (sketch on XY plane) + >>> sketch_ex5_door = doc.addObject('Sketcher::SketchObject', 'DoorSketchEx5') + >>> sketch_ex5_door.addGeometry(Part.LineSegment(App.Vector(0,0,0), App.Vector(850,0,0))) # Wire0 + >>> sketch_ex5_door.addGeometry(Part.LineSegment(App.Vector(850,0,0), App.Vector(850,2050,0))) + >>> sketch_ex5_door.addGeometry(Part.LineSegment(App.Vector(850,2050,0), App.Vector(0,2050,0))) + >>> sketch_ex5_door.addGeometry(Part.LineSegment(App.Vector(0,2050,0), App.Vector(0,0,0))) + >>> doc.recompute() + >>> door_ex5_manual = Arch.makeWindow(baseobj=sketch_ex5_door, name="MyDoorEx5_Manual") + >>> door_ex5_manual.WindowParts = ["DoorPanel", "Solid panel", "Wire0", "40", "0"] + >>> door_ex5_manual.IfcType = "Door" # Explicitly define as a Door + >>> doc.recompute() + + >>> # Ex6: Hosting the vertical window from Ex1 in an Arch.Wall + >>> wall_ex6 = Arch.makeWall(None, length=4000, width=200, height=2400) + >>> wall_ex6.Label = "WallForOpening_Ex6" + >>> # Window_ex1 is already oriented (its sketch placement was set in Ex1). + >>> # Now, just position the window object itself. + >>> window_ex1.Placement.Base = App.Vector(1500, wall_ex6.Width.Value / 2, 900) # X, Y (center of wall), Z (sill) + >>> window_ex1.HoleDepth = 0 # Use wall's thickness for the opening depth + >>> doc.recompute() # Apply window placement and HoleDepth + >>> window_ex1.Hosts = [wall_ex6] + >>> doc.recompute() # Wall recomputes to create the opening """ import Draft import DraftGeomUtils @@ -1905,7 +2097,7 @@ def makeWindow( part_name, part_type, wires_str, part_frame_thickness, part_offset ] else: - # bind properties from base obj if existing + # Bind properties from base obj if they exist for prop in ["Height", "Width", "Subvolume", "Tag", "Description", "Material"]: for baseobj_prop in baseobj.PropertiesList: if (baseobj_prop == prop) or baseobj_prop.endswith(f"_{prop}"):