First Commit
This commit is contained in:
@@ -0,0 +1,96 @@
|
||||
# IfcOpenShell - IFC toolkit and geometry engine
|
||||
# Copyright (C) 2022 Dion Moult <dion@thinkmoult.com>
|
||||
#
|
||||
# This file is part of IfcOpenShell.
|
||||
#
|
||||
# IfcOpenShell is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Lesser General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# IfcOpenShell is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Lesser General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Lesser General Public License
|
||||
# along with IfcOpenShell. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
"""Create geometric representations and assign them to elements
|
||||
|
||||
These functions support both the creation of arbitrary geometry as well as
|
||||
geometry that follows parametric rules (e.g. layered geometry or profiled
|
||||
geometry extrusions).
|
||||
"""
|
||||
|
||||
from .. import wrap_usecases
|
||||
from .add_axis_representation import add_axis_representation
|
||||
from .add_topology_representation import add_topology_representation
|
||||
from .add_boolean import add_boolean
|
||||
from .clip_solid import clip_solid
|
||||
from .clip_solid_bounded import clip_solid_bounded
|
||||
from .add_door_representation import add_door_representation
|
||||
from .add_footprint_representation import add_footprint_representation
|
||||
from .add_mesh_representation import add_mesh_representation
|
||||
from .add_profile_representation import add_profile_representation
|
||||
from .add_railing_representation import add_railing_representation
|
||||
|
||||
try:
|
||||
from .add_representation import add_representation
|
||||
except (ModuleNotFoundError, ImportError):
|
||||
# ImportError - in case if user has fake-bpy modules.
|
||||
pass # Silently fail. This is Blender / Bonsai specific and on its way out.
|
||||
from .add_shape_aspect import add_shape_aspect
|
||||
from .add_slab_representation import add_slab_representation
|
||||
from .add_wall_representation import add_wall_representation
|
||||
from .add_window_representation import add_window_representation
|
||||
from .assign_representation import assign_representation
|
||||
from .connect_element import connect_element
|
||||
from .connect_path import connect_path
|
||||
from .connect_wall import connect_wall
|
||||
from .create_2pt_wall import create_2pt_wall
|
||||
from .disconnect_element import disconnect_element
|
||||
from .disconnect_path import disconnect_path
|
||||
from .edit_object_placement import edit_object_placement
|
||||
from .map_representation import map_representation
|
||||
from .copy_representation import copy_representation
|
||||
from .regenerate_wall_representation import regenerate_wall_representation
|
||||
from .remove_boolean import remove_boolean
|
||||
from .remove_representation import remove_representation
|
||||
from .unassign_representation import unassign_representation
|
||||
from .validate_type import validate_type
|
||||
|
||||
wrap_usecases(__path__, __name__)
|
||||
|
||||
__all__ = [
|
||||
"add_axis_representation",
|
||||
"add_topology_representation",
|
||||
"add_boolean",
|
||||
"clip_solid",
|
||||
"clip_solid_bounded",
|
||||
"copy_representation",
|
||||
"add_door_representation",
|
||||
"add_footprint_representation",
|
||||
"add_mesh_representation",
|
||||
"add_profile_representation",
|
||||
"add_railing_representation",
|
||||
"add_representation",
|
||||
"add_shape_aspect",
|
||||
"add_slab_representation",
|
||||
"add_wall_representation",
|
||||
"add_window_representation",
|
||||
"assign_representation",
|
||||
"connect_element",
|
||||
"connect_path",
|
||||
"connect_wall",
|
||||
"create_2pt_wall",
|
||||
"disconnect_element",
|
||||
"disconnect_path",
|
||||
"edit_object_placement",
|
||||
"map_representation",
|
||||
"regenerate_wall_representation",
|
||||
"remove_boolean",
|
||||
"remove_representation",
|
||||
"unassign_representation",
|
||||
"validate_type",
|
||||
]
|
||||
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
@@ -0,0 +1,111 @@
|
||||
# IfcOpenShell - IFC toolkit and geometry engine
|
||||
# Copyright (C) 2022 Dion Moult <dion@thinkmoult.com>
|
||||
#
|
||||
# This file is part of IfcOpenShell.
|
||||
#
|
||||
# IfcOpenShell is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Lesser General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# IfcOpenShell is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Lesser General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Lesser General Public License
|
||||
# along with IfcOpenShell. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from typing import Any, Union
|
||||
|
||||
import ifcopenshell.util.unit
|
||||
|
||||
COORD = Union[tuple[float, float], tuple[float, float, float]]
|
||||
|
||||
|
||||
def add_axis_representation(
|
||||
file: ifcopenshell.file, context: ifcopenshell.entity_instance, axis: tuple[COORD, COORD]
|
||||
) -> ifcopenshell.entity_instance:
|
||||
"""Adds a new axis representation
|
||||
|
||||
Certain objects are typically "axis-based", such as walls, beams,
|
||||
and columns. This means you can represent them abstractly by simply
|
||||
drawing a single line either in 2D (such as for walls) or 3D (for beams
|
||||
and columns). Humans can understand this axis-based representation as
|
||||
being a simplification of a layered extrusion or a profile that is being
|
||||
extruded along that axis and joined to other elements.
|
||||
|
||||
Using an axis-based representation makes it easy for users and computers
|
||||
to analyse connectivity and spatial relationships, as well as makes it
|
||||
easy to parametrically edit these objects by simply stretching the start
|
||||
or end of the axis.
|
||||
|
||||
For now, only simple straight line axes are supported, represented by a
|
||||
start and end coordinate. The order is important. For walls, the start
|
||||
must be at the minimum local X ordinate, and the end at the maximum
|
||||
local X ordinate. For beams and columns, the start is at the minimum
|
||||
local Z ordinate, and the end of the maximum local Z ordinate. The first
|
||||
coordinate is the "start" and the second coordinate is the "end". This
|
||||
stat and end is then used to determine any parametric junctions with
|
||||
other elements.
|
||||
|
||||
Using an axis-representation is optional, but highly recommended for
|
||||
"standard" representations of walls, beams, columns, and other
|
||||
structural members. A rule of thumb is that if you can draw it as a line
|
||||
on paper, you can probably represent it using an axis.
|
||||
|
||||
:param context: The IfcGeometricRepresentationContext that the
|
||||
representation is part of. This must be either a
|
||||
Model/Axis/GRAPH_VIEW (3D) or Plan/Axis/GRAPH_VIEW (2D).
|
||||
:param axis: The axis, as a list of two coordinates, the coordinates
|
||||
being either a list of 2 or 3 float coordinates depending on whether
|
||||
the axis is 2D or 3D.
|
||||
:return: The newly created IfcShapeRepresentation entity
|
||||
|
||||
Example:
|
||||
|
||||
.. code:: python
|
||||
|
||||
context = ifcopenshell.util.representation.get_context(model, "Plan", "Axis", "GRAPH_VIEW")
|
||||
axis = ifcopenshell.api.geometry.add_axis_representation(model,
|
||||
context=context, axis=[(0.0, 0.0), (1.0, 0.0)])
|
||||
"""
|
||||
usecase = Usecase()
|
||||
usecase.file = file
|
||||
usecase.settings = {
|
||||
"context": context,
|
||||
"axis": axis or [],
|
||||
}
|
||||
return usecase.execute()
|
||||
|
||||
|
||||
class Usecase:
|
||||
file: ifcopenshell.file
|
||||
settings: dict[str, Any]
|
||||
|
||||
def execute(self):
|
||||
self.settings["unit_scale"] = ifcopenshell.util.unit.calculate_unit_scale(self.file)
|
||||
is_2d = len(self.settings["axis"][0]) == 2
|
||||
points = [self.convert_si_to_unit(p) for p in self.settings["axis"]]
|
||||
if self.file.schema == "IFC2X3":
|
||||
curve = self.file.createIfcPolyline([self.file.createIfcCartesianPoint(p) for p in points])
|
||||
else:
|
||||
if is_2d:
|
||||
curve = self.file.createIfcIndexedPolyCurve(
|
||||
self.file.createIfcCartesianPointList2D(points), None, False
|
||||
)
|
||||
else:
|
||||
curve = self.file.createIfcIndexedPolyCurve(
|
||||
self.file.createIfcCartesianPointList3D(points), None, False
|
||||
)
|
||||
return self.file.createIfcShapeRepresentation(
|
||||
self.settings["context"],
|
||||
self.settings["context"].ContextIdentifier,
|
||||
"Curve2D" if is_2d else "Curve3D",
|
||||
[curve],
|
||||
)
|
||||
|
||||
def convert_si_to_unit(self, co):
|
||||
if isinstance(co, (tuple, list)):
|
||||
return [self.convert_si_to_unit(o) for o in co]
|
||||
return co / self.settings["unit_scale"]
|
||||
@@ -0,0 +1,114 @@
|
||||
# IfcOpenShell - IFC toolkit and geometry engine
|
||||
# Copyright (C) 2022 Dion Moult <dion@thinkmoult.com>
|
||||
#
|
||||
# This file is part of IfcOpenShell.
|
||||
#
|
||||
# IfcOpenShell is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Lesser General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# IfcOpenShell is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Lesser General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Lesser General Public License
|
||||
# along with IfcOpenShell. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Literal
|
||||
|
||||
import ifcopenshell.util.element
|
||||
|
||||
|
||||
def add_boolean(
|
||||
file: ifcopenshell.file,
|
||||
first_item: ifcopenshell.entity_instance,
|
||||
second_items: list[ifcopenshell.entity_instance],
|
||||
operator: Literal["DIFFERENCE", "INTERSECTION", "UNION"] = "DIFFERENCE",
|
||||
) -> list[ifcopenshell.entity_instance]:
|
||||
"""Adds a boolean operation to two or more representation items
|
||||
|
||||
This function protects against recursive booleans.
|
||||
|
||||
After a boolean operation is made, since the items of
|
||||
IfcShapeRepresentation may be modified, it is not guaranteed that the
|
||||
RepresentationType is still valid. After performing all your booleans, it
|
||||
is recommended to run :func:`ifcopenshell.api.geometry.validate_csg` to
|
||||
ensure correctness.
|
||||
|
||||
:param first_item: The IfcBooleanOperand that the operation is performed upon
|
||||
:param second_items: The IfcBooleanOperands that the operation will be
|
||||
performed with, in the order given of the list.
|
||||
:param operator: The type of boolean operation to perform
|
||||
:return: A list of newly created IfcBooleanResult in the order of boolean
|
||||
operations (based on the order of second items). If nothing was
|
||||
created, the list will be empty.
|
||||
"""
|
||||
|
||||
def is_operand(item):
|
||||
return (
|
||||
item.is_a("IfcBooleanResult")
|
||||
or item.is_a("IfcCsgPrimitive3D")
|
||||
or item.is_a("IfcHalfSpaceSolid")
|
||||
or item.is_a("IfcSolidModel")
|
||||
or item.is_a("IfcTessellatedFaceSet")
|
||||
)
|
||||
|
||||
if not is_operand(first_item):
|
||||
return []
|
||||
|
||||
original_first_item = first_item
|
||||
|
||||
second_items = [i for i in second_items if i != first_item and is_operand(i)]
|
||||
|
||||
while True:
|
||||
is_part_of_boolean = False
|
||||
for inverse in file.get_inverse(first_item):
|
||||
if inverse.is_a("IfcBooleanResult"):
|
||||
is_part_of_boolean = True
|
||||
first_item = inverse
|
||||
if inverse.FirstOperand == original_first_item and inverse.SecondOperand in second_items:
|
||||
second_items.remove(inverse.SecondOperand)
|
||||
elif inverse.SecondOperand == original_first_item and inverse.FirstOperand in second_items:
|
||||
second_items.remove(inverse.FirstOperand)
|
||||
break
|
||||
if not is_part_of_boolean:
|
||||
break
|
||||
|
||||
if not second_items:
|
||||
return []
|
||||
|
||||
# Don't replace style or aspect relationships.
|
||||
to_replace = set(
|
||||
[i for i in file.get_inverse(first_item) if i.is_a("IfcShapeRepresentation") or i.is_a("IfcBooleanResult")]
|
||||
)
|
||||
|
||||
first = first_item
|
||||
|
||||
booleans = []
|
||||
for second_item in second_items:
|
||||
if first.is_a("IfcTesselatedFaceSet"):
|
||||
first.Closed = True # For now, trust the user to do the right thing.
|
||||
if second_item.is_a("IfcTesselatedFaceSet"):
|
||||
second_item.Closed = True # For now, trust the user to do the right thing.
|
||||
if (
|
||||
operator == "DIFFERENCE"
|
||||
and second_item.is_a("IfcHalfSpaceSolid")
|
||||
and (
|
||||
first.is_a("IfcSweptAreaSolid")
|
||||
or first.is_a("IfcSweptDiskSolid")
|
||||
or first.is_a("IfcBooleanClippingResult")
|
||||
)
|
||||
):
|
||||
first = file.create_entity("IfcBooleanClippingResult", operator, first, second_item)
|
||||
else:
|
||||
first = file.create_entity("IfcBooleanResult", operator, first, second_item)
|
||||
booleans.append(first)
|
||||
|
||||
for inverse in to_replace:
|
||||
ifcopenshell.util.element.replace_attribute(inverse, first_item, first)
|
||||
|
||||
return booleans
|
||||
@@ -0,0 +1,679 @@
|
||||
# IfcOpenShell - IFC toolkit and geometry engine
|
||||
# Copyright (C) 2023 @Andrej730
|
||||
#
|
||||
# This file is part of IfcOpenShell.
|
||||
#
|
||||
# IfcOpenShell is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Lesser General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# IfcOpenShell is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Lesser General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Lesser General Public License
|
||||
# along with IfcOpenShell. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import dataclasses
|
||||
from math import cos, radians
|
||||
from typing import Any, Literal, Optional, Union, get_args, overload
|
||||
|
||||
import numpy as np
|
||||
|
||||
import ifcopenshell.api.geometry
|
||||
import ifcopenshell.util.unit
|
||||
from ifcopenshell.api.geometry.add_window_representation import create_ifc_window
|
||||
from ifcopenshell.util.shape_builder import ShapeBuilder, V
|
||||
|
||||
DOOR_TYPE = Literal[
|
||||
"SINGLE_SWING_LEFT",
|
||||
"SINGLE_SWING_RIGHT",
|
||||
"DOUBLE_SWING_RIGHT",
|
||||
"DOUBLE_SWING_LEFT",
|
||||
"DOUBLE_DOOR_SINGLE_SWING",
|
||||
"DOUBLE_DOOR_DOUBLE_SWING",
|
||||
"SLIDING_TO_LEFT",
|
||||
"SLIDING_TO_RIGHT",
|
||||
"DOUBLE_DOOR_SLIDING",
|
||||
]
|
||||
SUPPORTED_DOOR_TYPES = get_args(DOOR_TYPE)
|
||||
|
||||
|
||||
def mm(x: float) -> float:
|
||||
"""mm to meters shortcut for readability"""
|
||||
return x / 1000
|
||||
|
||||
|
||||
def create_ifc_door_lining(
|
||||
builder: ShapeBuilder, size: np.ndarray, thickness: Union[list[float], float], position: Optional[np.ndarray] = None
|
||||
) -> ifcopenshell.entity_instance:
|
||||
"""`thickness` of the profile is defined as list in the following order: `(SIDE, TOP)`
|
||||
|
||||
`thickness` can be also defined just as 1 float value.
|
||||
"""
|
||||
np_X, np_Y, np_Z = 0, 1, 2
|
||||
np_XZ = [0, 2]
|
||||
if not isinstance(thickness, list):
|
||||
thickness = [thickness, thickness]
|
||||
|
||||
th_side, th_up = thickness
|
||||
|
||||
points = V(
|
||||
[
|
||||
(0.0, 0.0, 0.0),
|
||||
(0.0, 0.0, size[np_Z]),
|
||||
(size[np_X], 0.0, size[np_Z]),
|
||||
(size[np_X], 0.0, 0.0),
|
||||
(size[np_X] - th_side, 0.0, 0.0),
|
||||
(size[np_X] - th_side, 0.0, size[np_Z] - th_up),
|
||||
(th_side, 0.0, size[np_Z] - th_up),
|
||||
(th_side, 0.0, 0.0),
|
||||
]
|
||||
)
|
||||
|
||||
points = points[:, np_XZ]
|
||||
door_lining = builder.polyline(points, closed=True)
|
||||
door_lining = builder.extrude(door_lining, size[np_Y], **builder.extrude_kwargs("Y"))
|
||||
if position is None:
|
||||
position = np.zeros(3)
|
||||
builder.translate(door_lining, position)
|
||||
|
||||
return door_lining
|
||||
|
||||
|
||||
def create_ifc_box(
|
||||
builder: ShapeBuilder, size: np.ndarray, position: Optional[np.ndarray] = None
|
||||
) -> ifcopenshell.entity_instance:
|
||||
np_Z, np_XY = 2, slice(2)
|
||||
rect = builder.rectangle(size[np_XY])
|
||||
if position is None:
|
||||
position = np.zeros(3)
|
||||
box = builder.extrude(rect, size[np_Z], position=position, extrusion_vector=(0, 0, 1))
|
||||
return box
|
||||
|
||||
|
||||
# we use dataclass as we need default values for arguments
|
||||
# it's okay to use slots since we don't need dynamic attributes
|
||||
@dataclasses.dataclass(slots=True)
|
||||
class DoorLiningProperties:
|
||||
LiningDepth: Optional[float] = None
|
||||
"""Optional, defaults to 50mm."""
|
||||
|
||||
LiningThickness: Optional[float] = None
|
||||
"""Optional, defaults to 50mm."""
|
||||
|
||||
LiningOffset: Optional[float] = None
|
||||
"""Offset from the outer side of the wall (by Y-axis). Optional, defaults to 0.0."""
|
||||
|
||||
LiningToPanelOffsetX: Optional[float] = None
|
||||
"""Offset from the wall. Optional, defaults to 25mm."""
|
||||
|
||||
LiningToPanelOffsetY: Optional[float] = None
|
||||
"""Offset from the X-axis (unlike windows). Optional, defaults to 25mm."""
|
||||
|
||||
TransomThickness: Optional[float] = None
|
||||
"""Vertical distance between door and window panels. Optional, defaults to 0.0."""
|
||||
|
||||
TransomOffset: Optional[float] = None
|
||||
"""Distance from the bottom door opening
|
||||
to the beginning of the transom
|
||||
unlike windows TransomOffset which goes to the center of the transom.
|
||||
Optional, defaults 1.525m."""
|
||||
|
||||
ShapeAspectStyle: None = None
|
||||
"""Optional. Deprecated argument."""
|
||||
|
||||
CasingDepth: Optional[float] = None
|
||||
"""Casing cover wall faces around the opening
|
||||
on the left, right and upper sides
|
||||
Casing should be either on both sides of the wall or no casing
|
||||
If `LiningOffset` is present then therefore casing is not possible on outer wall
|
||||
therefore there will be no casing on inner wall either. Optional, defaults to 5mm."""
|
||||
|
||||
CasingThickness: Optional[float] = None
|
||||
"""Casing thickness by Z-axis. Optional, defaults to 75mm."""
|
||||
|
||||
ThresholdDepth: Optional[float] = None
|
||||
"""Threshold covers the bottom side of the opening. Optional, defaults to 100mm."""
|
||||
|
||||
ThresholdThickness: Optional[float] = None
|
||||
"""Theshold thickness by Z-axis. Optional, defaults to 25mm."""
|
||||
|
||||
ThresholdOffset: Optional[float] = None
|
||||
"""Threshold offset by Y-axis. Optional, defaults to 0.0."""
|
||||
|
||||
def initialize_properties(self, unit_scale: float) -> None:
|
||||
# in meters
|
||||
# fmt: off
|
||||
default_values: dict[str, float] = dict(
|
||||
LiningDepth = mm(50),
|
||||
LiningThickness = mm(50),
|
||||
LiningOffset = 0.0,
|
||||
LiningToPanelOffsetX = mm(25),
|
||||
LiningToPanelOffsetY = mm(25),
|
||||
TransomThickness = 0.0,
|
||||
TransomOffset = mm(1525),
|
||||
CasingDepth = mm(5),
|
||||
CasingThickness = mm(75),
|
||||
ThresholdDepth = mm(100),
|
||||
ThresholdThickness = mm(25),
|
||||
ThresholdOffset = 0.0,
|
||||
)
|
||||
# fmt: on
|
||||
|
||||
si_conversion = 1 / unit_scale
|
||||
for attr, default_value in default_values.items():
|
||||
if getattr(self, attr) is not None:
|
||||
continue
|
||||
setattr(self, attr, default_value * si_conversion)
|
||||
|
||||
|
||||
@dataclasses.dataclass(slots=True)
|
||||
class DoorPanelProperties:
|
||||
PanelDepth: Optional[float] = None
|
||||
"""Frame thickness by Y axis. Optional, defaults to 35 mm."""
|
||||
|
||||
PanelWidth: float = 1.0
|
||||
"""Ratio to the clear door opening. Optional, defaults to 1.0."""
|
||||
|
||||
FrameDepth: Optional[float] = None
|
||||
"""Frame thickness by Y axis. Optional, defaults to 35 mm."""
|
||||
|
||||
FrameThickness: Optional[float] = None
|
||||
"""Frame thickness by X axis. Optional, defaults to 35 mm."""
|
||||
|
||||
PanelPosition: None = None
|
||||
"""Optional, value is never used"""
|
||||
|
||||
PanelOperation: None = None
|
||||
"""Optional, value is never used.
|
||||
Defines the basic ways to describe how door panels operate."""
|
||||
|
||||
ShapeAspectStyle: None = None
|
||||
"""Optional. Deprecated argument."""
|
||||
|
||||
def initialize_properties(self, unit_scale: float) -> None:
|
||||
# in meters
|
||||
# fmt: off
|
||||
default_values: dict[str, float] = dict(
|
||||
PanelDepth = mm(35),
|
||||
FrameDepth = mm(35),
|
||||
FrameThickness = mm(35),
|
||||
)
|
||||
# fmt: on
|
||||
|
||||
si_conversion = 1 / unit_scale
|
||||
for attr, default_value in default_values.items():
|
||||
if getattr(self, attr) is not None:
|
||||
continue
|
||||
setattr(self, attr, default_value * si_conversion)
|
||||
|
||||
|
||||
def add_door_representation(
|
||||
file: ifcopenshell.file,
|
||||
*, # keywords only as this API implementation is probably not final
|
||||
context: ifcopenshell.entity_instance,
|
||||
overall_height: Optional[float] = None,
|
||||
overall_width: Optional[float] = None,
|
||||
# door type
|
||||
# http://ifc43-docs.standards.buildingsmart.org/IFC/RELEASE/IFC4x3/HTML/lexical/IfcDoorTypeOperationEnum.htm
|
||||
operation_type: DOOR_TYPE = "SINGLE_SWING_LEFT",
|
||||
lining_properties: Optional[Union[DoorLiningProperties, dict[str, Any]]] = None,
|
||||
panel_properties: Optional[Union[DoorPanelProperties, dict[str, Any]]] = None,
|
||||
part_of_product: Optional[ifcopenshell.entity_instance] = None,
|
||||
unit_scale: Optional[float] = None,
|
||||
) -> Union[ifcopenshell.entity_instance, None]:
|
||||
"""Add a geometric representation for a door.
|
||||
|
||||
units in usecase_settings expected to be in ifc project units
|
||||
|
||||
:param context: IfcGeometricRepresentationContext for the representation.
|
||||
:param overall_height: Overall door height. Defaults to 2m.
|
||||
:param overall_width: Overall door width. Defaults to 0.9m.
|
||||
:param operation_type: Type of the door. Defaults to SINGLE_SWING_LEFT.
|
||||
:param lining_properties: DoorLiningProperties or a dictionary to create one.
|
||||
See DoorLiningProperties description for details.
|
||||
:param panel_properties: DoorPanelProperties or a dictionary to create one.
|
||||
See DoorPanelProperties description for details.
|
||||
:param unit_scale: The unit scale as calculated by
|
||||
ifcopenshell.util.unit.calculate_unit_scale. If not provided, it
|
||||
will be automatically calculated for you.
|
||||
:return: IfcShapeRepresentation for a door.
|
||||
"""
|
||||
usecase = Usecase()
|
||||
usecase.file = file
|
||||
# http://ifc43-docs.standards.buildingsmart.org/IFC/RELEASE/IFC4x3/HTML/lexical/IfcDoor.htm
|
||||
# http://ifc43-docs.standards.buildingsmart.org/IFC/RELEASE/IFC4x3/HTML/lexical/IfcDoorLiningProperties.htm
|
||||
# http://ifc43-docs.standards.buildingsmart.org/IFC/RELEASE/IFC4x3/HTML/lexical/IfcDoorPanelProperties.htm
|
||||
# define unit_scale first as it's going to be used setting default arguments
|
||||
unit_scale = ifcopenshell.util.unit.calculate_unit_scale(file) if unit_scale is None else unit_scale
|
||||
settings: dict[str, Any] = {"unit_scale": unit_scale}
|
||||
|
||||
if lining_properties is None:
|
||||
lining_properties = DoorLiningProperties()
|
||||
elif not isinstance(lining_properties, DoorLiningProperties):
|
||||
lining_properties = DoorLiningProperties(**lining_properties)
|
||||
lining_properties.initialize_properties(unit_scale)
|
||||
lining_properties = dataclasses.asdict(lining_properties)
|
||||
|
||||
if panel_properties is None:
|
||||
panel_properties = DoorPanelProperties()
|
||||
elif not isinstance(panel_properties, DoorPanelProperties):
|
||||
panel_properties = DoorPanelProperties(**panel_properties)
|
||||
panel_properties.initialize_properties(unit_scale)
|
||||
panel_properties = dataclasses.asdict(panel_properties)
|
||||
|
||||
settings.update(
|
||||
{
|
||||
"context": context,
|
||||
"overall_height": overall_height if overall_height is not None else usecase.convert_si_to_unit(2.0),
|
||||
"overall_width": overall_width if overall_width is not None else usecase.convert_si_to_unit(0.9),
|
||||
"operation_type": operation_type,
|
||||
"lining_properties": lining_properties,
|
||||
"panel_properties": panel_properties,
|
||||
"part_of_product": part_of_product,
|
||||
}
|
||||
)
|
||||
usecase.settings = settings
|
||||
return usecase.execute()
|
||||
|
||||
|
||||
class Usecase:
|
||||
file: ifcopenshell.file
|
||||
settings: dict[str, Any]
|
||||
|
||||
def execute(self) -> Union[ifcopenshell.entity_instance, None]:
|
||||
builder = ShapeBuilder(self.file)
|
||||
|
||||
np_X, np_Y, np_Z = 0, 1, 2
|
||||
np_XY = slice(2)
|
||||
np_YX = [1, 0]
|
||||
|
||||
overall_height: float = self.settings["overall_height"]
|
||||
overall_width: float = self.settings["overall_width"]
|
||||
door_type: DOOR_TYPE = self.settings["operation_type"]
|
||||
double_swing_door = "DOUBLE_SWING" in door_type
|
||||
double_door = "DOUBLE_DOOR" in door_type
|
||||
sliding_door = "SLIDING" in door_type
|
||||
|
||||
if self.settings["context"].TargetView == "ELEVATION_VIEW":
|
||||
rect = builder.rectangle((overall_width, 0, overall_height))
|
||||
representation_evelevation = builder.get_representation(self.settings["context"], rect)
|
||||
return representation_evelevation
|
||||
|
||||
panel_props = self.settings["panel_properties"]
|
||||
lining_props = self.settings["lining_properties"]
|
||||
|
||||
# lining params
|
||||
lining_depth: float = lining_props["LiningDepth"]
|
||||
lining_thickness_default: float = lining_props["LiningThickness"]
|
||||
lining_offset: float = lining_props["LiningOffset"]
|
||||
lining_to_panel_offset_x: float = (
|
||||
lining_props["LiningToPanelOffsetX"] if not sliding_door else lining_thickness_default
|
||||
)
|
||||
panel_depth: float = panel_props["PanelDepth"]
|
||||
lining_to_panel_offset_y_full: float = (
|
||||
lining_props["LiningToPanelOffsetY"] if not sliding_door else -panel_depth
|
||||
)
|
||||
|
||||
transom_thickness: float = lining_props["TransomThickness"] / 2
|
||||
transfom_offset: float = lining_props["TransomOffset"]
|
||||
if transom_thickness == 0:
|
||||
transfom_offset = 0
|
||||
window_lining_height = overall_height - transfom_offset - transom_thickness
|
||||
|
||||
side_lining_thickness = lining_thickness_default
|
||||
panel_lining_overlap_x = max(lining_thickness_default - lining_to_panel_offset_x, 0) if not sliding_door else 0
|
||||
|
||||
top_lining_thickness = transom_thickness or lining_thickness_default
|
||||
panel_top_lining_overlap_x = max(top_lining_thickness - lining_to_panel_offset_x, 0) if not sliding_door else 0
|
||||
door_opening_width = overall_width - lining_to_panel_offset_x * 2
|
||||
if double_swing_door:
|
||||
side_lining_thickness = side_lining_thickness - panel_lining_overlap_x
|
||||
top_lining_thickness = top_lining_thickness - panel_top_lining_overlap_x
|
||||
|
||||
threshold_thickness: float = lining_props["ThresholdThickness"]
|
||||
threshold_depth: float = lining_props["ThresholdDepth"]
|
||||
threshold_offset: float = lining_props["ThresholdOffset"]
|
||||
threshold_width = overall_width - side_lining_thickness * 2
|
||||
|
||||
casing_thickness: float = lining_props["CasingThickness"]
|
||||
casing_depth: float = lining_props["CasingDepth"]
|
||||
|
||||
# panel params
|
||||
panel_width: float = door_opening_width * panel_props["PanelWidth"]
|
||||
frame_depth: float = panel_props["FrameDepth"]
|
||||
frame_thickness: float = panel_props["FrameThickness"]
|
||||
frame_height = window_lining_height - lining_to_panel_offset_x * 2
|
||||
glass_thickness = self.convert_si_to_unit(0.01)
|
||||
|
||||
# handle dimensions (hardcoded)
|
||||
handle_size = self.convert_si_to_unit(V(120, 40, 20) * 0.001)
|
||||
handle_offset = self.convert_si_to_unit(V(60, 0, 1000) * 0.001) # to the handle center
|
||||
handle_center_offset = V(handle_size[np_Y] / 2, 0, handle_size[np_Z]) / 2
|
||||
slider_arrow_symbol_size = self.convert_si_to_unit(30 * 0.001)
|
||||
|
||||
if transfom_offset:
|
||||
panel_height = transfom_offset + transom_thickness - lining_to_panel_offset_x - threshold_thickness
|
||||
lining_height = transfom_offset + transom_thickness
|
||||
else:
|
||||
panel_height = overall_height - lining_to_panel_offset_x - threshold_thickness
|
||||
lining_height = overall_height
|
||||
|
||||
# add lining
|
||||
lining_size = V(overall_width, lining_depth, lining_height)
|
||||
lining_thickness = [side_lining_thickness, top_lining_thickness]
|
||||
|
||||
def l_shape_check(lining_thickness: list[float]) -> bool:
|
||||
return lining_to_panel_offset_y_full < lining_depth and any(
|
||||
lining_to_panel_offset_x < th for th in lining_thickness
|
||||
)
|
||||
|
||||
# create 2d representation
|
||||
if self.settings["context"].TargetView == "PLAN_VIEW":
|
||||
items_2d: list[ifcopenshell.entity_instance] = []
|
||||
panel_size = V(panel_width, panel_depth)
|
||||
if not sliding_door:
|
||||
panel_position = V(lining_to_panel_offset_x, lining_depth)
|
||||
else:
|
||||
panel_position = V(lining_to_panel_offset_x, -panel_size[np_Y])
|
||||
|
||||
if self.settings["context"].ContextIdentifier == "Annotation":
|
||||
# only sliding door has annotation representation
|
||||
if not sliding_door:
|
||||
return None
|
||||
|
||||
# arrow symbol
|
||||
arrow_symbol: list[ifcopenshell.entity_instance] = []
|
||||
arrow_offset = slider_arrow_symbol_size / cos(radians(15))
|
||||
arrow_symbol.append(
|
||||
builder.polyline(
|
||||
points=((0.35 * panel_size[np_X], 0), (0.65 * panel_size[np_X], 0)),
|
||||
)
|
||||
)
|
||||
arrow_symbol.append(
|
||||
builder.polyline(
|
||||
points=(
|
||||
(slider_arrow_symbol_size, arrow_offset),
|
||||
(0, 0),
|
||||
(slider_arrow_symbol_size, -arrow_offset),
|
||||
),
|
||||
position_offset=(0.35 * panel_size[np_X], 0),
|
||||
)
|
||||
)
|
||||
|
||||
builder.translate(arrow_symbol, panel_position + (0, -arrow_offset * 1.5))
|
||||
|
||||
items_2d.extend(arrow_symbol)
|
||||
|
||||
representation_2d = builder.get_representation(self.settings["context"], items_2d, "Curve2D")
|
||||
return representation_2d
|
||||
|
||||
door_items: list[ifcopenshell.entity_instance] = []
|
||||
# create lining
|
||||
if l_shape_check([side_lining_thickness]):
|
||||
lining_points = [
|
||||
(0, 0),
|
||||
(0, lining_depth),
|
||||
(lining_to_panel_offset_x, lining_depth),
|
||||
(lining_to_panel_offset_x, lining_to_panel_offset_y_full),
|
||||
(lining_thickness_default, lining_to_panel_offset_y_full),
|
||||
(lining_thickness_default, 0),
|
||||
]
|
||||
lining = builder.polyline(lining_points, closed=True)
|
||||
else:
|
||||
lining = builder.rectangle((side_lining_thickness, lining_depth))
|
||||
|
||||
items_2d.append(lining)
|
||||
items_2d.append(
|
||||
builder.mirror(lining, mirror_axes=V(1, 0), mirror_point=V(overall_width / 2, 0), create_copy=True)
|
||||
)
|
||||
|
||||
# TODO: make second swing lines dashed
|
||||
def create_ifc_door_panel_2d(
|
||||
panel_size: np.ndarray,
|
||||
panel_position: np.ndarray,
|
||||
door_swing_type: Literal["LEFT", "RIGHT"],
|
||||
sliding: bool = False,
|
||||
) -> list[ifcopenshell.entity_instance]:
|
||||
if sliding:
|
||||
return create_ifc_door_sliding_panel_2d(panel_size, panel_position, door_swing_type)
|
||||
|
||||
door_items: list[ifcopenshell.entity_instance] = []
|
||||
panel_size = panel_size[np_YX]
|
||||
# create semi-semi-circle
|
||||
if double_swing_door:
|
||||
trim_points_mask = (3, 1)
|
||||
second_swing_line = builder.polyline(
|
||||
points=(
|
||||
(0, 0),
|
||||
(0, -panel_size[np_Y]),
|
||||
(panel_size[np_X], -panel_size[np_Y]),
|
||||
)
|
||||
)
|
||||
door_items.append(second_swing_line)
|
||||
else:
|
||||
trim_points_mask = (0, 1)
|
||||
semicircle = builder.create_ellipse_curve(
|
||||
panel_size[np_Y] - panel_size[np_X],
|
||||
panel_size[np_Y],
|
||||
trim_points_mask=trim_points_mask,
|
||||
position=(panel_size[np_X], 0),
|
||||
)
|
||||
door_items.append(semicircle)
|
||||
|
||||
# create door
|
||||
door = builder.rectangle(panel_size)
|
||||
door_items.append(door)
|
||||
|
||||
builder.translate(door_items, panel_position)
|
||||
|
||||
if door_swing_type == "RIGHT":
|
||||
mirror_point = panel_position + (panel_size[np_Y] / 2, 0)
|
||||
builder.mirror(door_items, mirror_axes=(1, 0), mirror_point=mirror_point)
|
||||
return door_items
|
||||
|
||||
def create_ifc_door_sliding_panel_2d(
|
||||
panel_size: np.ndarray, panel_position: np.ndarray, door_swing_type: Literal["LEFT", "RIGHT"]
|
||||
) -> list[ifcopenshell.entity_instance]:
|
||||
door = builder.rectangle(panel_size, position=panel_position - (panel_size[np_X] * 0.5, 0))
|
||||
|
||||
if door_swing_type == "RIGHT":
|
||||
mirror_point = panel_position + (panel_size[np_X] / 2, 0)
|
||||
builder.mirror(door, mirror_axes=(1, 0), mirror_point=mirror_point)
|
||||
return [door]
|
||||
|
||||
door_items: list[ifcopenshell.entity_instance] = []
|
||||
if double_door:
|
||||
panel_size[np_X] = panel_size[np_X] / 2
|
||||
door_items.extend(create_ifc_door_panel_2d(panel_size, panel_position, "LEFT", sliding_door))
|
||||
|
||||
mirror_point = panel_position + V(door_opening_width / 2, 0)
|
||||
door_items.extend(
|
||||
builder.mirror(door_items, mirror_axes=(1, 0), mirror_point=mirror_point, create_copy=True)
|
||||
)
|
||||
else:
|
||||
door_swing_type = "LEFT" if door_type.endswith("LEFT") else "RIGHT"
|
||||
door_items.extend(create_ifc_door_panel_2d(panel_size, panel_position, door_swing_type, sliding_door))
|
||||
items_2d.extend(door_items)
|
||||
|
||||
builder.translate(items_2d, (0, lining_offset))
|
||||
representation_2d = builder.get_representation(self.settings["context"], items_2d)
|
||||
return representation_2d
|
||||
|
||||
lining_items: list[ifcopenshell.entity_instance] = []
|
||||
main_lining_size = lining_size
|
||||
|
||||
# need to check offsets to decide whether lining should be rectangle
|
||||
# or L shaped
|
||||
if l_shape_check(lining_thickness):
|
||||
main_lining_size = lining_size.copy()
|
||||
main_lining_size[np_Y] = lining_to_panel_offset_y_full
|
||||
|
||||
second_lining_size = lining_size.copy()
|
||||
second_lining_size[np_Y] = lining_size[np_Y] - lining_to_panel_offset_y_full
|
||||
second_lining_position = V(0, lining_to_panel_offset_y_full, 0)
|
||||
second_lining_thickness = [min(th, lining_to_panel_offset_x) for th in lining_thickness]
|
||||
|
||||
second_lining = create_ifc_door_lining(
|
||||
builder, second_lining_size, second_lining_thickness, second_lining_position
|
||||
)
|
||||
lining_items.append(second_lining)
|
||||
|
||||
main_lining = create_ifc_door_lining(builder, main_lining_size, lining_thickness)
|
||||
lining_items.append(main_lining)
|
||||
|
||||
# add threshold
|
||||
threshold_items: list[ifcopenshell.entity_instance]
|
||||
if not threshold_thickness:
|
||||
threshold_items = []
|
||||
else:
|
||||
threshold_size = V(threshold_width, threshold_depth, threshold_thickness)
|
||||
threshold_position = V(side_lining_thickness, threshold_offset, 0)
|
||||
threshold_items = [create_ifc_box(builder, threshold_size, threshold_position)]
|
||||
|
||||
# add casings
|
||||
casing_items: list[ifcopenshell.entity_instance] = []
|
||||
if not lining_offset and casing_thickness:
|
||||
casing_wall_overlap = max(casing_thickness - lining_thickness_default, 0)
|
||||
inner_casing_thickness = [
|
||||
casing_thickness - panel_lining_overlap_x,
|
||||
casing_thickness - panel_top_lining_overlap_x,
|
||||
]
|
||||
outer_casing_thickness = inner_casing_thickness.copy() if double_swing_door else casing_thickness
|
||||
|
||||
casing_size = V(overall_width + casing_wall_overlap * 2, casing_depth, overall_height + casing_wall_overlap)
|
||||
casing_position = V(-casing_wall_overlap, -casing_depth, 0)
|
||||
outer_casing = create_ifc_door_lining(builder, casing_size, outer_casing_thickness, casing_position)
|
||||
casing_items.append(outer_casing)
|
||||
|
||||
inner_casing_position = V(-casing_wall_overlap, lining_depth, 0)
|
||||
inner_casing = create_ifc_door_lining(builder, casing_size, inner_casing_thickness, inner_casing_position)
|
||||
casing_items.append(inner_casing)
|
||||
|
||||
def create_ifc_door_panel(
|
||||
panel_size: np.ndarray, panel_position: np.ndarray, door_swing_type: Literal["LEFT", "RIGHT"]
|
||||
) -> list[ifcopenshell.entity_instance]:
|
||||
door_items: list[ifcopenshell.entity_instance] = []
|
||||
# add door panel
|
||||
door_items.append(create_ifc_box(builder, panel_size, panel_position))
|
||||
# add door handle
|
||||
handle_points = [
|
||||
(0, 0),
|
||||
(0, -handle_size[np_Y]),
|
||||
(handle_size[np_X], -handle_size[np_Y]),
|
||||
(handle_size[np_X], -handle_size[np_Y] / 2),
|
||||
(handle_size[np_Y] / 2, -handle_size[np_Y] / 2),
|
||||
(handle_size[np_Y] / 2, 0),
|
||||
]
|
||||
handle_polyline = builder.polyline(handle_points, closed=True)
|
||||
|
||||
handle_position = panel_position + handle_offset - handle_center_offset
|
||||
|
||||
door_handle = builder.extrude(handle_polyline, handle_size[np_Z], position=handle_position)
|
||||
door_items.append(door_handle)
|
||||
|
||||
if door_swing_type == "LEFT":
|
||||
builder.mirror(
|
||||
door_handle, mirror_axes=(1, 0), mirror_point=panel_position[np_XY] + (panel_size[np_X] / 2, 0)
|
||||
)
|
||||
|
||||
door_handle_mirrored = builder.mirror(
|
||||
door_handle,
|
||||
mirror_axes=(0, 1),
|
||||
mirror_point=handle_position[np_XY] + (0, panel_size[np_Y] / 2),
|
||||
create_copy=True,
|
||||
)
|
||||
door_items.append(door_handle_mirrored)
|
||||
return door_items
|
||||
|
||||
door_items: list[ifcopenshell.entity_instance] = []
|
||||
panel_size = V(panel_width, panel_depth, panel_height)
|
||||
panel_position = V(lining_to_panel_offset_x, lining_to_panel_offset_y_full, threshold_thickness)
|
||||
|
||||
if double_door:
|
||||
# keeping a little space between doors for readibility
|
||||
double_door_offset = self.convert_si_to_unit(0.001)
|
||||
panel_size[np_X] = panel_size[np_X] / 2 - double_door_offset
|
||||
door_items.extend(create_ifc_door_panel(panel_size, panel_position, "LEFT"))
|
||||
|
||||
mirror_point = panel_position + V(door_opening_width / 2, 0, 0)
|
||||
door_items.extend(
|
||||
builder.mirror(door_items, mirror_axes=(1, 0), mirror_point=mirror_point[np_XY], create_copy=True)
|
||||
)
|
||||
else:
|
||||
door_swing_type = "LEFT" if door_type.endswith("LEFT") else "RIGHT"
|
||||
door_items.extend(create_ifc_door_panel(panel_size, panel_position, door_swing_type))
|
||||
|
||||
# add on top window
|
||||
if not transom_thickness:
|
||||
window_lining_items = []
|
||||
frame_items = []
|
||||
glass_items = []
|
||||
else:
|
||||
window_lining_thickness = [
|
||||
side_lining_thickness,
|
||||
lining_thickness_default,
|
||||
side_lining_thickness,
|
||||
transom_thickness,
|
||||
]
|
||||
window_lining_size = V(overall_width, lining_depth, window_lining_height)
|
||||
window_position = V(0, 0, overall_height - window_lining_height)
|
||||
frame_size = V(door_opening_width, frame_depth, frame_height)
|
||||
current_window_items = create_ifc_window(
|
||||
builder,
|
||||
window_lining_size,
|
||||
window_lining_thickness,
|
||||
lining_to_panel_offset_x,
|
||||
lining_to_panel_offset_y_full,
|
||||
frame_size,
|
||||
frame_thickness,
|
||||
glass_thickness,
|
||||
window_position,
|
||||
)
|
||||
window_lining_items = current_window_items["Lining"]
|
||||
frame_items = current_window_items["Framing"]
|
||||
glass_items = current_window_items["Glazing"]
|
||||
|
||||
lining_offset_items = lining_items + door_items + window_lining_items + frame_items + glass_items
|
||||
builder.translate(lining_offset_items, (0, lining_offset, 0))
|
||||
|
||||
output_items = lining_offset_items + threshold_items + casing_items
|
||||
|
||||
representation = builder.get_representation(self.settings["context"], output_items)
|
||||
if self.settings["part_of_product"]:
|
||||
ifcopenshell.api.geometry.add_shape_aspect(
|
||||
self.file,
|
||||
"Lining",
|
||||
items=lining_items + window_lining_items + threshold_items + casing_items,
|
||||
representation=representation,
|
||||
part_of_product=self.settings["part_of_product"],
|
||||
)
|
||||
ifcopenshell.api.geometry.add_shape_aspect(
|
||||
self.file,
|
||||
"Framing",
|
||||
items=door_items + frame_items,
|
||||
representation=representation,
|
||||
part_of_product=self.settings["part_of_product"],
|
||||
)
|
||||
if glass_items:
|
||||
ifcopenshell.api.geometry.add_shape_aspect(
|
||||
self.file,
|
||||
"Glazing",
|
||||
items=glass_items,
|
||||
representation=representation,
|
||||
part_of_product=self.settings["part_of_product"],
|
||||
)
|
||||
return representation
|
||||
|
||||
@overload
|
||||
def convert_si_to_unit(self, value: float) -> float: ...
|
||||
@overload
|
||||
def convert_si_to_unit(self, value: np.ndarray) -> np.ndarray: ...
|
||||
def convert_si_to_unit(self, value: Union[float, np.ndarray]) -> Union[float, np.ndarray]:
|
||||
si_conversion = 1 / self.settings["unit_scale"]
|
||||
return value * si_conversion
|
||||
@@ -0,0 +1,34 @@
|
||||
# IfcOpenShell - IFC toolkit and geometry engine
|
||||
# Copyright (C) 2022 Dion Moult <dion@thinkmoult.com>
|
||||
#
|
||||
# This file is part of IfcOpenShell.
|
||||
#
|
||||
# IfcOpenShell is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Lesser General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# IfcOpenShell is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Lesser General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Lesser General Public License
|
||||
# along with IfcOpenShell. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import ifcopenshell.util.unit
|
||||
|
||||
|
||||
def add_footprint_representation(
|
||||
file,
|
||||
# IfcGeometricRepresentationContext
|
||||
context: ifcopenshell.entity_instance,
|
||||
# A list of IFC curves to include in the curve set
|
||||
curves: list[ifcopenshell.entity_instance],
|
||||
) -> ifcopenshell.entity_instance:
|
||||
return file.createIfcShapeRepresentation(
|
||||
context,
|
||||
context.ContextIdentifier,
|
||||
"GeometricCurveSet",
|
||||
[file.createIfcGeometricCurveSet(curves)],
|
||||
)
|
||||
@@ -0,0 +1,134 @@
|
||||
# IfcOpenShell - IFC toolkit and geometry engine
|
||||
# Copyright (C) 2022 Dion Moult <dion@thinkmoult.com>
|
||||
#
|
||||
# This file is part of IfcOpenShell.
|
||||
#
|
||||
# IfcOpenShell is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Lesser General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# IfcOpenShell is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Lesser General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Lesser General Public License
|
||||
# along with IfcOpenShell. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from typing import Optional, TypeVar
|
||||
|
||||
import numpy as np
|
||||
import numpy.typing as npt
|
||||
|
||||
import ifcopenshell.util.unit
|
||||
from ifcopenshell.util.shape_builder import SequenceOfVectors, ShapeBuilder, VectorType
|
||||
|
||||
T = TypeVar("T")
|
||||
COORD_3D = tuple[float, float, float]
|
||||
|
||||
|
||||
def add_mesh_representation(
|
||||
file: ifcopenshell.file,
|
||||
context: ifcopenshell.entity_instance,
|
||||
vertices: list[SequenceOfVectors],
|
||||
edges: Optional[list[list[tuple[int, int]]]] = None,
|
||||
# Optional faces is not supported currently.
|
||||
faces: list[list[list[int]]] = None,
|
||||
coordinate_offset: Optional[VectorType] = None,
|
||||
unit_scale: Optional[float] = None,
|
||||
force_faceted_brep: bool = False,
|
||||
) -> ifcopenshell.entity_instance:
|
||||
"""
|
||||
Add a mesh representation.
|
||||
|
||||
Vertices, edges, and faces are given in the form of: ``[item1, item2, item3, ...]``.
|
||||
Each ``itemN`` is a sublist representing data for a separate IfcRepresentationItem to add.
|
||||
|
||||
You can provide either ``edges`` or ``faces``, no need to provide both.
|
||||
But currently ``edges`` argument is not supported.
|
||||
|
||||
:param context: The IfcGeometricRepresentationContext for the representation.
|
||||
:param vertices: A list of coordinates.
|
||||
where ``itemN = [(0., 0., 0.), (1., 1., 1.), (x, y, z), ...]``
|
||||
:param edges: A list of edges, represented by vertex index pairs
|
||||
where ``itemN = [(0, 1), (1, 2), (v1, v2), ...]``
|
||||
:param faces: A list of polygons, represented by vertex indices.
|
||||
where ``itemN = [(0, 1, 2), (5, 4, 2, 3), (v1, v2, v3, ... vN), ...]``
|
||||
:param coordinate_offset: Optionally apply a vector offset to all coordinates.
|
||||
In project units.
|
||||
:param unit_scale: Scale factor for ``vertices`` units.
|
||||
|
||||
If omitted, it is assumed that ``vertices`` are in SI units.
|
||||
|
||||
If other value is provided ``vertices`` coords will be divided by ``unit_scale``.
|
||||
:param force_faceted_brep: Force using IfcFacetedBreps instead of IfcPolygonalFaceSets.
|
||||
:return: IfcShapeRepresentation.
|
||||
"""
|
||||
# TODO: Support edges without faces.
|
||||
assert faces is not None, f"Currently 'faces' argument is not optional."
|
||||
assert len(faces) != 0
|
||||
assert len(vertices) != 0
|
||||
assert len(faces) == len(vertices)
|
||||
|
||||
usecase = Usecase()
|
||||
usecase.file = file
|
||||
|
||||
# Process arguments.
|
||||
if unit_scale is None:
|
||||
unit_scale = ifcopenshell.util.unit.calculate_unit_scale(file)
|
||||
np_vertices = np.array(vertices, dtype=np.float64) * (1 / unit_scale)
|
||||
if coordinate_offset is not None:
|
||||
np_vertices += coordinate_offset
|
||||
|
||||
return usecase.execute(context, np_vertices, faces, force_faceted_brep)
|
||||
|
||||
|
||||
class Usecase:
|
||||
file: ifcopenshell.file
|
||||
|
||||
vertices: npt.NDArray[np.float64]
|
||||
"""In project units."""
|
||||
|
||||
def execute(
|
||||
self,
|
||||
context: ifcopenshell.entity_instance,
|
||||
vertices: npt.NDArray[np.float64],
|
||||
faces: list[list[list[int]]],
|
||||
force_faceted_brep: bool,
|
||||
) -> ifcopenshell.entity_instance:
|
||||
self.builder = ShapeBuilder(self.file)
|
||||
self.vertices = vertices
|
||||
self.faces = faces
|
||||
self.context = context
|
||||
self.force_faceted_brep = force_faceted_brep
|
||||
return self.create_mesh_representation()
|
||||
|
||||
def create_mesh_representation(self) -> ifcopenshell.entity_instance:
|
||||
if self.force_faceted_brep or self.file.schema == "IFC2X3":
|
||||
return self.create_faceted_brep()
|
||||
return self.create_polygonal_face_set()
|
||||
|
||||
def create_faceted_brep(self) -> ifcopenshell.entity_instance:
|
||||
items: list[ifcopenshell.entity_instance] = []
|
||||
for i in range(0, len(self.vertices)):
|
||||
items.append(self.builder.faceted_brep(self.vertices[i], self.faces[i]))
|
||||
return self.file.create_entity(
|
||||
"IfcShapeRepresentation",
|
||||
self.context,
|
||||
self.context.ContextIdentifier,
|
||||
"Brep",
|
||||
items,
|
||||
)
|
||||
|
||||
def create_polygonal_face_set(self) -> ifcopenshell.entity_instance:
|
||||
items: list[ifcopenshell.entity_instance] = []
|
||||
for i in range(0, len(self.vertices)):
|
||||
items.append(self.builder.polygonal_face_set(self.vertices[i], self.faces[i]))
|
||||
return self.file.create_entity(
|
||||
"IfcShapeRepresentation",
|
||||
self.context,
|
||||
self.context.ContextIdentifier,
|
||||
"Tessellation",
|
||||
items,
|
||||
)
|
||||
@@ -0,0 +1,224 @@
|
||||
# IfcOpenShell - IFC toolkit and geometry engine
|
||||
# Copyright (C) 2022 Dion Moult <dion@thinkmoult.com>
|
||||
#
|
||||
# This file is part of IfcOpenShell.
|
||||
#
|
||||
# IfcOpenShell is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Lesser General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# IfcOpenShell is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Lesser General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Lesser General Public License
|
||||
# along with IfcOpenShell. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from typing import Any, Literal, Optional, Union, get_args
|
||||
|
||||
import ifcopenshell.geom
|
||||
import ifcopenshell.util.element
|
||||
import ifcopenshell.util.shape
|
||||
import ifcopenshell.util.unit
|
||||
from ifcopenshell.util.data import Clipping
|
||||
|
||||
VECTOR_3D = tuple[float, float, float]
|
||||
CardinalPointNumeric = Literal[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19]
|
||||
CardinalPointString = Literal[
|
||||
"bottom left",
|
||||
"bottom centre",
|
||||
"bottom right",
|
||||
"mid-depth left",
|
||||
"mid-depth centre",
|
||||
"mid-depth right",
|
||||
"top left",
|
||||
"top centre",
|
||||
"top right",
|
||||
"geometric centroid",
|
||||
"bottom in line with the geometric centroid",
|
||||
"left in line with the geometric centroid",
|
||||
"right in line with the geometric centroid",
|
||||
"top in line with the geometric centroid",
|
||||
"shear centre",
|
||||
"bottom in line with the shear centre",
|
||||
"left in line with the shear centre",
|
||||
"right in line with the shear centre",
|
||||
"top in line with the shear centre",
|
||||
]
|
||||
CARDINAL_POINT_VALUES: tuple[CardinalPointString, ...] = get_args(CardinalPointString)
|
||||
CardinalPoint = Union[CardinalPointNumeric, CardinalPointString]
|
||||
|
||||
|
||||
def add_profile_representation(
|
||||
file: ifcopenshell.file,
|
||||
context: ifcopenshell.entity_instance,
|
||||
profile: ifcopenshell.entity_instance,
|
||||
depth: float = 1.0,
|
||||
# TODO: None makes more sense as default value?
|
||||
cardinal_point: Union[CardinalPoint, None] = 5,
|
||||
clippings: Optional[list[Union[Clipping, dict[str, Any]]]] = None,
|
||||
placement_zx_axes: tuple[Union[VECTOR_3D, None], Union[VECTOR_3D, None]] = (None, None),
|
||||
) -> ifcopenshell.entity_instance:
|
||||
"""Add profile representation.
|
||||
|
||||
:param context: The IfcGeometricRepresentationContext for the representation,
|
||||
only Model/Body/MODEL_VIEW type of representations are currently supported.
|
||||
:param profile: The IfcProfileDef to extrude.
|
||||
:param depth: The depth of the extrusion in meters.
|
||||
:param cardinal_point: The cardinal point of the profile.
|
||||
:param clippings: A list of planes that define clipping half space solids.
|
||||
Planes are defined either by Clipping objects
|
||||
or by dictionaries of arguments for `Clipping.parse`.
|
||||
:param placement_zx_axes: A tuple of two vectors that define the placement of the profile.
|
||||
The first vector is the Z axis, the second vector is the X axis.
|
||||
:return: IfcShapeRepresentation.
|
||||
"""
|
||||
usecase = Usecase()
|
||||
usecase.file = file
|
||||
clippings = clippings if clippings is not None else []
|
||||
return usecase.execute(context, profile, depth, cardinal_point, clippings, placement_zx_axes)
|
||||
|
||||
|
||||
class Usecase:
|
||||
file: ifcopenshell.file
|
||||
clippings: list[Clipping]
|
||||
|
||||
def execute(
|
||||
self,
|
||||
context: ifcopenshell.entity_instance,
|
||||
profile: ifcopenshell.entity_instance,
|
||||
depth: float,
|
||||
cardinal_point: Union[CardinalPoint, None],
|
||||
clippings: list[Union[Clipping, dict[str, Any]]],
|
||||
placement_zx_axes: tuple[Union[VECTOR_3D, None], Union[VECTOR_3D, None]],
|
||||
) -> ifcopenshell.entity_instance:
|
||||
if isinstance(cardinal_point, int):
|
||||
cardinal_point = CARDINAL_POINT_VALUES[cardinal_point - 1]
|
||||
|
||||
self.cardinal_point = cardinal_point
|
||||
self.profile = profile
|
||||
self.clippings = [Clipping.parse(c) for c in clippings]
|
||||
self.depth = depth
|
||||
self.placement_zx_axes = placement_zx_axes
|
||||
self.unit_scale = ifcopenshell.util.unit.calculate_unit_scale(self.file)
|
||||
return self.file.create_entity(
|
||||
"IfcShapeRepresentation",
|
||||
context,
|
||||
context.ContextIdentifier,
|
||||
"Clipping" if self.clippings else "SweptSolid",
|
||||
[self.create_item()],
|
||||
)
|
||||
|
||||
def create_item(self) -> ifcopenshell.entity_instance:
|
||||
point = self.get_point()
|
||||
placement = self.file.createIfcAxis2Placement3D(
|
||||
point,
|
||||
self.file.create_entity("IfcDirection", self.placement_zx_axes[0] or (0.0, 0.0, 1.0)),
|
||||
self.file.create_entity("IfcDirection", self.placement_zx_axes[1] or (1.0, 0.0, 0.0)),
|
||||
)
|
||||
extrusion = self.file.create_entity(
|
||||
"IfcExtrudedAreaSolid",
|
||||
self.profile,
|
||||
placement,
|
||||
self.file.createIfcDirection((0.0, 0.0, 1.0)),
|
||||
self.convert_si_to_unit(self.depth),
|
||||
)
|
||||
if self.clippings:
|
||||
return self.apply_clippings(extrusion)
|
||||
return extrusion
|
||||
|
||||
def apply_clippings(self, first_operand: ifcopenshell.entity_instance) -> ifcopenshell.entity_instance:
|
||||
while self.clippings:
|
||||
clipping = self.clippings.pop()
|
||||
if isinstance(clipping, ifcopenshell.entity_instance):
|
||||
new = ifcopenshell.util.element.copy(self.file, clipping)
|
||||
new.FirstOperand = first_operand
|
||||
first_operand = new
|
||||
else: # Clipping
|
||||
first_operand = clipping.apply(self.file, first_operand, self.unit_scale)
|
||||
return first_operand
|
||||
|
||||
def convert_si_to_unit(self, co: float) -> float:
|
||||
return co / self.unit_scale
|
||||
|
||||
def get_point(self) -> ifcopenshell.entity_instance:
|
||||
if not self.cardinal_point:
|
||||
return self.file.createIfcCartesianPoint((0.0, 0.0, 0.0))
|
||||
elif self.cardinal_point == "bottom left":
|
||||
return self.file.createIfcCartesianPoint((-self.get_x() / 2, self.get_y() / 2, 0.0))
|
||||
elif self.cardinal_point == "bottom centre":
|
||||
return self.file.createIfcCartesianPoint((0.0, self.get_y() / 2, 0.0))
|
||||
elif self.cardinal_point == "bottom right":
|
||||
return self.file.createIfcCartesianPoint((self.get_x() / 2, self.get_y() / 2, 0.0))
|
||||
elif self.cardinal_point == "mid-depth left":
|
||||
return self.file.createIfcCartesianPoint((-self.get_x() / 2, 0.0, 0.0))
|
||||
elif self.cardinal_point == "mid-depth centre":
|
||||
return self.file.createIfcCartesianPoint((0.0, 0.0, 0.0))
|
||||
elif self.cardinal_point == "mid-depth right":
|
||||
return self.file.createIfcCartesianPoint((self.get_x() / 2, 0.0, 0.0))
|
||||
elif self.cardinal_point == "top left":
|
||||
return self.file.createIfcCartesianPoint((-self.get_x() / 2, -self.get_y() / 2, 0.0))
|
||||
elif self.cardinal_point == "top centre":
|
||||
return self.file.createIfcCartesianPoint((0.0, -self.get_y() / 2, 0.0))
|
||||
elif self.cardinal_point == "top right":
|
||||
return self.file.createIfcCartesianPoint((self.get_x() / 2, -self.get_y() / 2, 0.0))
|
||||
# TODO other cardinal points
|
||||
return self.file.createIfcCartesianPoint((0.0, 0.0, 0.0))
|
||||
|
||||
def get_x(self) -> float:
|
||||
if self.profile.is_a("IfcAsymmetricIShapeProfileDef"):
|
||||
return self.profile.OverallWidth
|
||||
elif self.profile.is_a("IfcCShapeProfileDef"):
|
||||
return self.profile.Width
|
||||
elif self.profile.is_a("IfcCircleProfileDef"):
|
||||
return self.profile.Radius * 2
|
||||
elif self.profile.is_a("IfcEllipseProfileDef"):
|
||||
return self.profile.SemiAxis1 * 2
|
||||
elif self.profile.is_a("IfcIShapeProfileDef"):
|
||||
return self.profile.OverallWidth
|
||||
elif self.profile.is_a("IfcLShapeProfileDef"):
|
||||
return self.profile.Width
|
||||
elif self.profile.is_a("IfcRectangleProfileDef"):
|
||||
return self.profile.XDim
|
||||
elif self.profile.is_a("IfcTShapeProfileDef"):
|
||||
return self.profile.FlangeWidth
|
||||
elif self.profile.is_a("IfcUShapeProfileDef"):
|
||||
return self.profile.FlangeWidth
|
||||
elif self.profile.is_a("IfcZShapeProfileDef"):
|
||||
return (self.profile.FlangeWidth * 2) - self.profile.WebThickness
|
||||
else:
|
||||
settings = ifcopenshell.geom.settings()
|
||||
settings.set("dimensionality", ifcopenshell.ifcopenshell_wrapper.CURVES_SURFACES_AND_SOLIDS)
|
||||
shape = ifcopenshell.geom.create_shape(settings, self.profile)
|
||||
return self.convert_si_to_unit(ifcopenshell.util.shape.get_x(shape))
|
||||
return 0.0
|
||||
|
||||
def get_y(self) -> float:
|
||||
if self.profile.is_a("IfcAsymmetricIShapeProfileDef"):
|
||||
return self.profile.OverallDepth
|
||||
elif self.profile.is_a("IfcCShapeProfileDef"):
|
||||
return self.profile.Depth
|
||||
elif self.profile.is_a("IfcCircleProfileDef"):
|
||||
return self.profile.Radius * 2
|
||||
elif self.profile.is_a("IfcEllipseProfileDef"):
|
||||
return self.profile.SemiAxis2 * 2
|
||||
elif self.profile.is_a("IfcIShapeProfileDef"):
|
||||
return self.profile.OverallDepth
|
||||
elif self.profile.is_a("IfcLShapeProfileDef"):
|
||||
return self.profile.Depth
|
||||
elif self.profile.is_a("IfcRectangleProfileDef"):
|
||||
return self.profile.YDim
|
||||
elif self.profile.is_a("IfcTShapeProfileDef"):
|
||||
return self.profile.Depth
|
||||
elif self.profile.is_a("IfcUShapeProfileDef"):
|
||||
return self.profile.Depth
|
||||
elif self.profile.is_a("IfcZShapeProfileDef"):
|
||||
return self.profile.Depth
|
||||
else:
|
||||
settings = ifcopenshell.geom.settings()
|
||||
settings.set("dimensionality", ifcopenshell.ifcopenshell_wrapper.CURVES_SURFACES_AND_SOLIDS)
|
||||
shape = ifcopenshell.geom.create_shape(settings, self.profile)
|
||||
return self.convert_si_to_unit(ifcopenshell.util.shape.get_y(shape))
|
||||
return 0.0
|
||||
@@ -0,0 +1,405 @@
|
||||
# IfcOpenShell - IFC toolkit and geometry engine
|
||||
# Copyright (C) 2023 @Andrej730
|
||||
#
|
||||
# This file is part of IfcOpenShell.
|
||||
#
|
||||
# IfcOpenShell is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Lesser General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# IfcOpenShell is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Lesser General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Lesser General Public License
|
||||
# along with IfcOpenShell. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from math import cos, pi, radians, sin, tan
|
||||
from typing import Any, Literal, Optional
|
||||
|
||||
import numpy as np
|
||||
from typing_extensions import assert_never
|
||||
|
||||
import ifcopenshell.util.unit
|
||||
from ifcopenshell.util.shape_builder import (
|
||||
SequenceOfVectors,
|
||||
ShapeBuilder,
|
||||
V,
|
||||
is_x,
|
||||
np_angle,
|
||||
np_angle_signed,
|
||||
np_intersect_line_line,
|
||||
np_lerp,
|
||||
np_normal,
|
||||
np_normalized,
|
||||
np_to_3d,
|
||||
)
|
||||
|
||||
|
||||
def mm(x: float) -> float:
|
||||
"""mm to meters shortcut for readability"""
|
||||
return x / 1000
|
||||
|
||||
|
||||
TERMINAL_TYPE = Literal[
|
||||
"180",
|
||||
"TO_END_POST",
|
||||
"TO_WALL",
|
||||
"TO_FLOOR",
|
||||
"TO_END_POST_AND_FLOOR",
|
||||
]
|
||||
|
||||
|
||||
def add_railing_representation(
|
||||
file: ifcopenshell.file,
|
||||
*, # keywords only as this API implementation is probably not final
|
||||
# IfcGeometricRepresentationContext
|
||||
context: ifcopenshell.entity_instance,
|
||||
railing_type: Literal["WALL_MOUNTED_HANDRAIL"] = "WALL_MOUNTED_HANDRAIL",
|
||||
railing_path: SequenceOfVectors,
|
||||
use_manual_supports: bool = False,
|
||||
support_spacing: Optional[float] = None,
|
||||
railing_diameter: Optional[float] = None,
|
||||
clear_width: Optional[float] = None,
|
||||
terminal_type: TERMINAL_TYPE = "180",
|
||||
height: Optional[float] = None,
|
||||
looped_path: bool = False,
|
||||
unit_scale: Optional[float] = None,
|
||||
) -> ifcopenshell.entity_instance:
|
||||
"""
|
||||
Units are expected to be in IFC project units.
|
||||
|
||||
:param context: IfcGeometricRepresentationContext for the representation.
|
||||
:param railing_type: Type of the railing. Defaults to "WALL_MOUNTED_HANDRAIL".
|
||||
:param railing_path: A list of points coordinates for the railing path,
|
||||
coordinates are expected to be at the top of the railing, not at the center.
|
||||
If not provided, default path [(0, 0, 1), (1, 0, 1), (2, 0, 1)] (in meters) will be used
|
||||
:param use_manual_supports: If enabled, supports are added on every vertex on the edges of the railing path.
|
||||
If disabled, supports are added automatically based on the support spacing. Default to False.
|
||||
:param support_spacing: Distance between supports if automatic supports are used. Defaults to 1m.
|
||||
:param railing_diameter: Railing diameter. Defaults to 50mm.
|
||||
:param clear_width: Clear width between the railing and the wall. Defaults to 40mm.
|
||||
:param terminal_type: type of the cap. Defaults to "180".
|
||||
:param height: defaults to 1m
|
||||
:param looped_path: Whether to end the railing on the first point of `railing_path`. Defaults to False.
|
||||
:param unit_scale: The unit scale as calculated by
|
||||
ifcopenshell.util.unit.calculate_unit_scale. If not provided, it
|
||||
will be automatically calculated for you.
|
||||
:return: IfcShapeRepresentation for a railing.
|
||||
"""
|
||||
usecase = Usecase()
|
||||
usecase.file = file
|
||||
# define unit_scale first as it's going to be used setting default arguments
|
||||
settings: dict[str, Any] = {
|
||||
"unit_scale": ifcopenshell.util.unit.calculate_unit_scale(file) if unit_scale is None else unit_scale,
|
||||
}
|
||||
settings.update(
|
||||
{
|
||||
"context": context,
|
||||
"railing_type": railing_path,
|
||||
"railing_path": (
|
||||
railing_path
|
||||
if railing_path is not None
|
||||
else usecase.path_si_to_units(V([(0, 0, 1), (1, 0, 1), (2, 0, 1)]))
|
||||
),
|
||||
"use_manual_supports": use_manual_supports,
|
||||
"support_spacing": support_spacing if support_spacing is not None else usecase.convert_si_to_unit(mm(1000)),
|
||||
"railing_diameter": (
|
||||
railing_diameter if railing_diameter is not None else usecase.convert_si_to_unit(mm(50))
|
||||
),
|
||||
"clear_width": clear_width if clear_width is not None else usecase.convert_si_to_unit(mm(40)),
|
||||
"terminal_type": terminal_type,
|
||||
"height": height if height is not None else usecase.convert_si_to_unit(mm(1000)),
|
||||
"looped_path": looped_path,
|
||||
}
|
||||
)
|
||||
usecase.settings = settings
|
||||
|
||||
if railing_type != "WALL_MOUNTED_HANDRAIL":
|
||||
raise Exception('Only "WALL_MOUNTED_HANDRAIL" railing_type is supported at the moment.')
|
||||
return usecase.execute()
|
||||
|
||||
|
||||
class Usecase:
|
||||
file: ifcopenshell.file
|
||||
settings: dict[str, Any]
|
||||
|
||||
def execute(self):
|
||||
arc_points: list[np.ndarray] = []
|
||||
items_3d: list[ifcopenshell.entity_instance] = []
|
||||
builder = ShapeBuilder(self.file)
|
||||
z_down = V(0, 0, -1)
|
||||
|
||||
# measurements
|
||||
# from settings
|
||||
use_manual_supports: bool = self.settings["use_manual_supports"]
|
||||
railing_radius: float = self.settings["railing_diameter"] / 2
|
||||
support_spacing: float = self.settings["support_spacing"]
|
||||
clear_width: float = self.settings["clear_width"]
|
||||
# for calculations purposes we use height without railing radius
|
||||
height: float = self.settings["height"] - railing_radius
|
||||
cap_type: TERMINAL_TYPE = self.settings["terminal_type"]
|
||||
ifc_context: ifcopenshell.entity_instance = self.settings["context"]
|
||||
railing_coords: SequenceOfVectors = self.settings["railing_path"]
|
||||
looped_path: bool = self.settings["looped_path"]
|
||||
railing_coords: np.ndarray
|
||||
railing_coords = np.subtract(railing_coords, z_down * railing_radius)
|
||||
|
||||
# constant
|
||||
terminal_radius = self.convert_si_to_unit(mm(150))
|
||||
railing_fillet_radius = self.convert_si_to_unit(mm(100))
|
||||
support_length = clear_width + railing_radius
|
||||
support_radius = self.convert_si_to_unit(mm(10))
|
||||
support_disk_radius = railing_radius
|
||||
support_disk_depth = self.convert_si_to_unit(mm(20))
|
||||
|
||||
# util functions
|
||||
def collinear(d0: np.ndarray, d1: np.ndarray) -> bool:
|
||||
return is_x(np_angle(d0, d1), 0)
|
||||
|
||||
np_Z = 2
|
||||
np_XY = slice(2)
|
||||
np_YX = [1, 0]
|
||||
|
||||
def add_support_on_point(
|
||||
point: np.ndarray, railing_direction: np.ndarray
|
||||
) -> tuple[ifcopenshell.entity_instance, ...]:
|
||||
"""create a support arc and a disk based on the position and direction of the railing"""
|
||||
ortho_dir = railing_direction[np_YX] * (1, -1)
|
||||
ortho_dir = np_normalized(np_to_3d(ortho_dir))
|
||||
arc_center = point + ortho_dir * support_length
|
||||
support_points: list[np.ndarray] = [
|
||||
point,
|
||||
arc_center - ortho_dir * support_length * cos(pi / 4) + z_down * support_length * sin(pi / 4),
|
||||
arc_center + z_down * support_length,
|
||||
]
|
||||
polyline = builder.polyline(support_points, closed=False, arc_points=(1,))
|
||||
solid = builder.create_swept_disk_solid(polyline, support_radius)
|
||||
|
||||
support_disk_circle = builder.circle(radius=support_disk_radius)
|
||||
|
||||
angle = np_angle_signed((0, 1), ortho_dir[np_XY])
|
||||
y_extrusion_kwargs = builder.rotate_extrusion_kwargs_by_z(builder.extrude_kwargs("Y"), angle)
|
||||
support_disk = builder.extrude(
|
||||
support_disk_circle, support_disk_depth, position=support_points[-1], **y_extrusion_kwargs
|
||||
)
|
||||
return (solid, support_disk)
|
||||
|
||||
def get_fillet_points(v0: np.ndarray, v1: np.ndarray, v2: np.ndarray, radius: float) -> list[np.ndarray]:
|
||||
"""get fillet points between edges v0v1 and v1v2"""
|
||||
dir1 = np_normalized(v0 - v1)
|
||||
dir2 = np_normalized(v2 - v1)
|
||||
edge_angle = np_angle(dir1, dir2)
|
||||
slide_distance = radius / tan(edge_angle / 2)
|
||||
|
||||
fillet_v1co = v1 + (dir1 * slide_distance)
|
||||
fillet_v2co = v1 + (dir2 * slide_distance)
|
||||
|
||||
normal = np_normal([v0, v1, v2])
|
||||
center = np_intersect_line_line(
|
||||
fillet_v1co,
|
||||
fillet_v1co + np.cross(normal, dir1),
|
||||
fillet_v2co,
|
||||
fillet_v2co + np.cross(normal, dir2),
|
||||
)[0]
|
||||
|
||||
dir_ = np_normalized(np_lerp(fillet_v1co, fillet_v2co, 0.5) - center)
|
||||
midpointco = center + dir_ * radius
|
||||
return [fillet_v1co, midpointco, fillet_v2co]
|
||||
|
||||
def add_arcs_on_turnings_points(base_points: np.ndarray) -> np.ndarray:
|
||||
"""add 3 point fillet arcs on turning points of the railing path"""
|
||||
if len(base_points) < 3:
|
||||
return base_points
|
||||
|
||||
# looking for turning points by checking non-collinear edges
|
||||
output_points: list[np.ndarray] = list(base_points[:1])
|
||||
prev_dir = np_normalized(base_points[1] - base_points[0])
|
||||
i = 1
|
||||
while i < len(base_points) - 1:
|
||||
cur_dir = np_normalized(base_points[i + 1] - base_points[i])
|
||||
|
||||
if collinear(cur_dir, prev_dir):
|
||||
output_points.append(base_points[i])
|
||||
else:
|
||||
fillet_points = get_fillet_points(
|
||||
base_points[i - 1], base_points[i], base_points[i + 1], railing_fillet_radius
|
||||
)
|
||||
output_points.extend(fillet_points)
|
||||
arc_points.append(fillet_points[1])
|
||||
|
||||
prev_dir = cur_dir
|
||||
i = i + 1
|
||||
|
||||
if looped_path:
|
||||
output_points[0] = output_points[-1]
|
||||
else:
|
||||
output_points.append(base_points[-1])
|
||||
return V(output_points)
|
||||
|
||||
def create_supports_items(
|
||||
railing_coords: np.ndarray, manual_supports: bool = False
|
||||
) -> list[ifcopenshell.entity_instance]:
|
||||
"""create supports items based on the railing coordinates"""
|
||||
supports_items: list[ifcopenshell.entity_instance] = []
|
||||
|
||||
# simplified_coords is a list of points that form non-collinear edges
|
||||
simplified_coords: list[np.ndarray] = [railing_coords[0]]
|
||||
prev_dir = np_normalized(railing_coords[1] - railing_coords[0])
|
||||
|
||||
# iterating over each edge of the railing path
|
||||
for i in range(1, len(railing_coords) - 1):
|
||||
cur_dir = np_normalized(railing_coords[i + 1] - railing_coords[i])
|
||||
|
||||
if not collinear(cur_dir, prev_dir):
|
||||
simplified_coords.append(railing_coords[i])
|
||||
prev_dir = cur_dir
|
||||
|
||||
# for manual supports each vertex on the railing path edge
|
||||
# will be a point for a support
|
||||
elif manual_supports:
|
||||
supports_items.extend(add_support_on_point(point=railing_coords[i], railing_direction=cur_dir))
|
||||
|
||||
simplified_coords.append(railing_coords[-1])
|
||||
|
||||
if manual_supports:
|
||||
return supports_items
|
||||
|
||||
# create automatic supports based on the support spacing
|
||||
for i in range(0, len(simplified_coords) - 1):
|
||||
v0, v1 = simplified_coords[i : i + 2]
|
||||
edge = v1 - v0
|
||||
length: float = np.linalg.norm(edge)
|
||||
edge_dir = np_normalized(edge)
|
||||
n_supports, support_offset = divmod(length, support_spacing)
|
||||
n_supports = int(n_supports) + 1
|
||||
support_offset /= 2
|
||||
|
||||
start_position = v0 + support_offset * edge_dir
|
||||
for support_i in range(n_supports):
|
||||
support_position = start_position + support_i * support_spacing * edge_dir
|
||||
supports_items.extend(add_support_on_point(point=support_position, railing_direction=edge))
|
||||
|
||||
return supports_items
|
||||
|
||||
def add_cap(railing_coords: np.ndarray, arc_points: list[np.ndarray], start: bool = False):
|
||||
"""add handrail terminal cap"""
|
||||
railing_coords_for_cap = railing_coords[::-1] if start else railing_coords
|
||||
arc_points = arc_points[::-1] if start else arc_points
|
||||
|
||||
start_point: np.ndarray = railing_coords_for_cap[-1]
|
||||
cap_dir = railing_coords_for_cap[-1] - railing_coords_for_cap[-2]
|
||||
cap_dir = np_normalized(cap_dir)
|
||||
ortho_dir = np_to_3d(cap_dir[np_YX] * (1, -1))
|
||||
ortho_dir = np_normalized(ortho_dir)
|
||||
local_z_down = np.cross(cap_dir, ortho_dir)
|
||||
if start:
|
||||
ortho_dir = -ortho_dir
|
||||
|
||||
arc_middle_point_cos = sin(radians(45))
|
||||
|
||||
if cap_type in ("180", "TO_END_POST"):
|
||||
arc_point = start_point + cap_dir * terminal_radius + terminal_radius * local_z_down
|
||||
arc_points.append(arc_point)
|
||||
cap_coords = [arc_point, start_point + terminal_radius * 2 * local_z_down]
|
||||
|
||||
if cap_type == "TO_END_POST":
|
||||
end_point = railing_coords_for_cap[-2].copy()
|
||||
end_point[np_Z] -= terminal_radius * 2
|
||||
cap_coords.append(end_point)
|
||||
|
||||
elif cap_type == "TO_WALL":
|
||||
arc_point = (
|
||||
start_point
|
||||
+ cap_dir * clear_width * arc_middle_point_cos
|
||||
+ ortho_dir * clear_width * (1 - arc_middle_point_cos)
|
||||
)
|
||||
arc_points.append(arc_point)
|
||||
cap_coords = [arc_point, start_point + ortho_dir * clear_width + cap_dir * clear_width]
|
||||
|
||||
elif cap_type == "TO_FLOOR":
|
||||
arc_point = (
|
||||
start_point
|
||||
+ cap_dir * terminal_radius * arc_middle_point_cos
|
||||
+ z_down * terminal_radius * (1 - arc_middle_point_cos)
|
||||
)
|
||||
arc_points.append(arc_point)
|
||||
arc_end = start_point + cap_dir * terminal_radius + terminal_radius * z_down
|
||||
cap_coords = [
|
||||
arc_point,
|
||||
arc_end,
|
||||
arc_end + z_down * (height - terminal_radius),
|
||||
]
|
||||
|
||||
elif cap_type == "TO_END_POST_AND_FLOOR":
|
||||
first_arc_end = start_point + cap_dir * terminal_radius + terminal_radius * local_z_down
|
||||
first_arc_coords = get_fillet_points(
|
||||
start_point, start_point + cap_dir * terminal_radius, first_arc_end, terminal_radius
|
||||
)
|
||||
arc_points.append(first_arc_coords[1])
|
||||
|
||||
end_point = railing_coords_for_cap[-2].copy()
|
||||
end_point[np_Z] -= height
|
||||
second_arc_coords = get_fillet_points(
|
||||
first_arc_end, first_arc_end + local_z_down * terminal_radius, end_point, terminal_radius
|
||||
)
|
||||
arc_points.append(second_arc_coords[1])
|
||||
cap_coords = [start_point] + first_arc_coords + second_arc_coords + [end_point]
|
||||
else:
|
||||
assert_never(cap_type)
|
||||
|
||||
railing_coords = np.vstack((railing_coords_for_cap, cap_coords))
|
||||
|
||||
if start:
|
||||
railing_coords = railing_coords[::-1]
|
||||
arc_points = arc_points[::-1]
|
||||
return railing_coords, arc_points
|
||||
|
||||
# need to add first two points to the path
|
||||
# to create the turning arcs and supports on the last segment of the loop
|
||||
if looped_path:
|
||||
railing_coords = np.vstack((railing_coords, railing_coords[:2]))
|
||||
|
||||
items_3d.extend(create_supports_items(railing_coords, manual_supports=use_manual_supports))
|
||||
railing_coords = add_arcs_on_turnings_points(railing_coords)
|
||||
|
||||
if not looped_path and cap_type != "NONE":
|
||||
railing_coords, arc_points = add_cap(railing_coords, arc_points, start=True)
|
||||
railing_coords, arc_points = add_cap(railing_coords, arc_points, start=False)
|
||||
|
||||
def get_arc_indices(points: np.ndarray, arc_points: list[np.ndarray]) -> list[int]:
|
||||
points_ = points.copy()
|
||||
arc_indices = []
|
||||
i_base = 0
|
||||
for arc_point in arc_points:
|
||||
for i, point in enumerate(points_):
|
||||
if np.allclose(arc_point, point):
|
||||
current_index = i + i_base
|
||||
arc_indices.append(current_index)
|
||||
i_base = current_index + 1
|
||||
break
|
||||
else:
|
||||
raise Exception(
|
||||
f"Arc point '{arc_point}' is not present in points:\n{points_}\nFull points data:\n{points}"
|
||||
)
|
||||
points_ = points_[i + 1 :]
|
||||
return arc_indices
|
||||
|
||||
railing_path = builder.polyline(
|
||||
railing_coords,
|
||||
closed=False,
|
||||
arc_points=get_arc_indices(railing_coords, arc_points),
|
||||
)
|
||||
railing_solid = builder.create_swept_disk_solid(railing_path, railing_radius)
|
||||
items_3d.append(railing_solid)
|
||||
representation = builder.get_representation(ifc_context, items=items_3d)
|
||||
return representation
|
||||
|
||||
def convert_si_to_unit(self, value: float) -> float:
|
||||
return value / self.settings["unit_scale"]
|
||||
|
||||
def path_si_to_units(self, path: np.ndarray) -> np.ndarray:
|
||||
"""converts list of vectors from SI to ifc project units"""
|
||||
return path / self.settings["unit_scale"]
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,89 @@
|
||||
# IfcOpenShell - IFC toolkit and geometry engine
|
||||
# Copyright (C) 2025 Dion Moult <dion@thinkmoult.com>
|
||||
#
|
||||
# This file is part of IfcOpenShell.
|
||||
#
|
||||
# IfcOpenShell is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Lesser General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# IfcOpenShell is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Lesser General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Lesser General Public License
|
||||
# along with IfcOpenShell. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from typing import Optional
|
||||
|
||||
import ifcopenshell
|
||||
|
||||
|
||||
def add_shape_aspect(
|
||||
file: ifcopenshell.file,
|
||||
name: str,
|
||||
items: list[ifcopenshell.entity_instance],
|
||||
representation: ifcopenshell.entity_instance,
|
||||
part_of_product: ifcopenshell.entity_instance,
|
||||
description: Optional[str] = None,
|
||||
) -> ifcopenshell.entity_instance:
|
||||
"""Adds a shape aspect to items that are part of a representation and product
|
||||
|
||||
Existing shape aspects will be reused where possible. If the items already
|
||||
belong to another shape aspect with a different name, this relationship
|
||||
will be purged.
|
||||
|
||||
Warning: it is not possible to add a shape aspect to types (i.e.
|
||||
IfcRepresentationMap) in IFC2X3.
|
||||
|
||||
:param name: The name of the shape aspect. This is case sensitive.
|
||||
:param items: IfcRepresentationItems that will be assigned to this aspect.
|
||||
:param representation: The IfcShapeRepresentation that the items are in.
|
||||
:param part_of_product: The IfcRepresentationMap or
|
||||
IfcProductDefinitionShape that the representation is in.
|
||||
:param description: A description to set for the shape aspect. It's usually
|
||||
not necessary.
|
||||
:return: The IfcShapeAspect
|
||||
"""
|
||||
result = None
|
||||
items_set = set(items)
|
||||
for aspect in part_of_product.HasShapeAspects or []:
|
||||
if aspect.Name == name:
|
||||
for aspect_rep in aspect.ShapeRepresentations:
|
||||
if aspect_rep.ContextOfItems == representation.ContextOfItems:
|
||||
aspect.Description = description
|
||||
aspect_rep.Items = tuple(set(aspect_rep.Items) | items_set)
|
||||
result = aspect
|
||||
if not result:
|
||||
aspect_rep = file.createIfcShapeRepresentation(
|
||||
ContextOfItems=representation.ContextOfItems,
|
||||
RepresentationIdentifier=representation.RepresentationIdentifier,
|
||||
RepresentationType=representation.RepresentationType,
|
||||
Items=items,
|
||||
)
|
||||
aspect.ShapeRepresentations += (aspect_rep,)
|
||||
result = aspect
|
||||
else:
|
||||
for aspect_rep in aspect.ShapeRepresentations:
|
||||
if aspect_rep.ContextOfItems != representation.ContextOfItems:
|
||||
continue
|
||||
if set(aspect_rep.Items) & items_set:
|
||||
if new_items := set(aspect_rep.Items) - items_set:
|
||||
aspect_rep.Items = tuple(new_items)
|
||||
else:
|
||||
file.remove(aspect_rep)
|
||||
if not aspect.ShapeRepresentations:
|
||||
file.remove(aspect)
|
||||
|
||||
if result:
|
||||
return result
|
||||
|
||||
aspect_rep = file.createIfcShapeRepresentation(
|
||||
ContextOfItems=representation.ContextOfItems,
|
||||
RepresentationIdentifier=representation.RepresentationIdentifier,
|
||||
RepresentationType=representation.RepresentationType,
|
||||
Items=items,
|
||||
)
|
||||
return file.createIfcShapeAspect((aspect_rep,), name, description, True, part_of_product)
|
||||
@@ -0,0 +1,162 @@
|
||||
# IfcOpenShell - IFC toolkit and geometry engine
|
||||
# Copyright (C) 2022 Dion Moult <dion@thinkmoult.com>
|
||||
#
|
||||
# This file is part of IfcOpenShell.
|
||||
#
|
||||
# IfcOpenShell is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Lesser General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# IfcOpenShell is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Lesser General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Lesser General Public License
|
||||
# along with IfcOpenShell. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from math import cos, sin
|
||||
from typing import Optional, Union
|
||||
|
||||
import ifcopenshell.util.element
|
||||
import ifcopenshell.util.unit
|
||||
from ifcopenshell.util.data import Clipping
|
||||
|
||||
|
||||
def add_slab_representation(
|
||||
file: ifcopenshell.file,
|
||||
context: ifcopenshell.entity_instance,
|
||||
depth: float = 0.2,
|
||||
# TODO: document remaining args.
|
||||
direction_sense: str = "POSITIVE",
|
||||
offset: float = 0.0,
|
||||
x_angle: float = 0.0,
|
||||
clippings: Optional[list[Union[Clipping, ifcopenshell.entity_instance]]] = None,
|
||||
polyline: Optional[list[tuple[float, float]]] = None,
|
||||
) -> ifcopenshell.entity_instance:
|
||||
"""
|
||||
Add a geometric representation for a slab.
|
||||
|
||||
:param context: The IfcGeometricRepresentationContext for the representation,
|
||||
only Model/Body/MODEL_VIEW type of representations are currently supported.
|
||||
:param depth: The slab depth, in meters.
|
||||
:param x_angle: The slope angle along the slab's X-axis, in radians.
|
||||
:param clippings: List of planes that define clipping half space solids.
|
||||
Clippings can be `Clipping` objects or dictionaries of arguments for `Clipping.parse`.
|
||||
:return: IfcShapeRepresentation.
|
||||
|
||||
Example:
|
||||
|
||||
.. code:: python
|
||||
|
||||
context = ifcopenshell.util.representation.get_context(ifc_file, "Model", "Body", "MODEL_VIEW")
|
||||
clippings = [ifcopenshell.util.data.Clipping(location=(0.0, 0.0, 0.1), normal=(0.0, 0.0, 1.0),)]
|
||||
representation = ifcopenshell.api.geometry.add_slab_representation(ifc_file, context, depth=0.2, clippings=clippings)
|
||||
ifcopenshell.api.geometry.assign_representation(ifc_file, product=element, representation=representation)
|
||||
"""
|
||||
usecase = Usecase()
|
||||
usecase.file = file
|
||||
return usecase.execute(
|
||||
context,
|
||||
depth,
|
||||
direction_sense,
|
||||
offset,
|
||||
x_angle,
|
||||
clippings if clippings is not None else [],
|
||||
polyline,
|
||||
)
|
||||
|
||||
|
||||
class Usecase:
|
||||
file: ifcopenshell.file
|
||||
|
||||
def execute(
|
||||
self,
|
||||
context: ifcopenshell.entity_instance,
|
||||
depth: float,
|
||||
direction_sense: str,
|
||||
offset: float,
|
||||
x_angle: float,
|
||||
clippings: list[Union[Clipping, ifcopenshell.entity_instance]],
|
||||
polyline: Optional[list[tuple[float, float]]],
|
||||
) -> ifcopenshell.entity_instance:
|
||||
self.unit_scale = ifcopenshell.util.unit.calculate_unit_scale(self.file)
|
||||
self.clippings = clippings
|
||||
self.depth = depth
|
||||
self.direction_sense = direction_sense
|
||||
self.offset = offset
|
||||
self.x_angle = x_angle
|
||||
self.polyline = polyline
|
||||
return self.file.create_entity(
|
||||
"IfcShapeRepresentation",
|
||||
context,
|
||||
context.ContextIdentifier,
|
||||
"Clipping" if self.clippings else "SweptSolid",
|
||||
[self.create_item()],
|
||||
)
|
||||
|
||||
def create_item(self) -> ifcopenshell.entity_instance:
|
||||
size = self.convert_si_to_unit(1)
|
||||
points = ((0.0, 0.0), (size, 0.0), (size, size), (0.0, size), (0.0, 0.0))
|
||||
if self.polyline:
|
||||
points = [
|
||||
(self.convert_si_to_unit(p[0]), self.convert_si_to_unit(p[1] * abs(1 / cos(self.x_angle))))
|
||||
for p in self.polyline
|
||||
]
|
||||
if self.file.schema == "IFC2X3":
|
||||
curve = self.file.createIfcPolyline([self.file.createIfcCartesianPoint(p) for p in points])
|
||||
else:
|
||||
curve = self.file.createIfcIndexedPolyCurve(self.file.createIfcCartesianPointList2D(points))
|
||||
|
||||
if self.x_angle:
|
||||
direction_ratios = (0.0, sin(self.x_angle), cos(self.x_angle))
|
||||
else:
|
||||
direction_ratios = (0.0, 0.0, 1.0)
|
||||
|
||||
offset_direction = direction_ratios # offset direction doesn't change if direction_sense is negative
|
||||
extrusion_direction = self.file.createIfcDirection(direction_ratios)
|
||||
if self.direction_sense == "NEGATIVE":
|
||||
direction_ratios = tuple(-n for n in direction_ratios)
|
||||
extrusion_direction = self.file.createIfcDirection(direction_ratios)
|
||||
|
||||
perpendicular_offset = self.convert_si_to_unit(self.offset) * abs(1 / cos(self.x_angle))
|
||||
perpendicular_depth = self.convert_si_to_unit(self.depth) * abs(1 / cos(self.x_angle))
|
||||
position = None
|
||||
# default position for IFC2X3 where .Position is not optional
|
||||
if self.file.schema == "IFC2X3" or self.offset != 0:
|
||||
position_vector = (
|
||||
offset_direction[0] * perpendicular_offset,
|
||||
offset_direction[1] * perpendicular_offset,
|
||||
offset_direction[2] * perpendicular_offset,
|
||||
)
|
||||
position = self.file.createIfcAxis2Placement3D(
|
||||
self.file.createIfcCartesianPoint(position_vector),
|
||||
self.file.createIfcDirection((0.0, 0.0, 1.0)),
|
||||
self.file.createIfcDirection((1.0, 0.0, 0.0)),
|
||||
)
|
||||
|
||||
extrusion = self.file.create_entity(
|
||||
"IfcExtrudedAreaSolid",
|
||||
self.file.createIfcArbitraryClosedProfileDef("AREA", None, curve),
|
||||
position,
|
||||
extrusion_direction,
|
||||
perpendicular_depth,
|
||||
)
|
||||
if self.clippings:
|
||||
return self.apply_clippings(extrusion)
|
||||
return extrusion
|
||||
|
||||
def apply_clippings(self, first_operand: ifcopenshell.entity_instance) -> ifcopenshell.entity_instance:
|
||||
while self.clippings:
|
||||
clipping = self.clippings.pop()
|
||||
if isinstance(clipping, ifcopenshell.entity_instance):
|
||||
new = ifcopenshell.util.element.copy(self.file, clipping)
|
||||
new.FirstOperand = first_operand
|
||||
first_operand = new
|
||||
else: # Clipping
|
||||
first_operand = clipping.apply(self.file, first_operand, self.unit_scale)
|
||||
return first_operand
|
||||
|
||||
def convert_si_to_unit(self, co: float) -> float:
|
||||
return co / self.unit_scale
|
||||
@@ -0,0 +1,97 @@
|
||||
# IfcOpenShell - IFC toolkit and geometry engine
|
||||
# Copyright (C) 2026 Dion Moult <dion@thinkmoult.com>
|
||||
#
|
||||
# This file is part of IfcOpenShell.
|
||||
#
|
||||
# IfcOpenShell is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Lesser General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# IfcOpenShell is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Lesser General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Lesser General Public License
|
||||
# along with IfcOpenShell. If not, see <http://www.gnu.org/licenses/>.
|
||||
# This file was generated with the assistance of an AI coding tool.
|
||||
|
||||
from typing import Optional
|
||||
|
||||
import ifcopenshell
|
||||
|
||||
_ITEM_TYPE_TO_REP_TYPE = {
|
||||
"IfcVertex": "Vertex",
|
||||
"IfcVertexPoint": "Vertex",
|
||||
"IfcEdge": "Edge",
|
||||
"IfcOrientedEdge": "Edge",
|
||||
"IfcEdgeCurve": "Edge",
|
||||
"IfcEdgeLoop": "Edge",
|
||||
"IfcPath": "Edge",
|
||||
"IfcFace": "Face",
|
||||
"IfcFaceSurface": "Face",
|
||||
"IfcAdvancedFace": "Face",
|
||||
"IfcClosedShell": "Face",
|
||||
"IfcOpenShell": "Face",
|
||||
"IfcConnectedFaceSet": "Face",
|
||||
}
|
||||
|
||||
|
||||
def add_topology_representation(
|
||||
file: ifcopenshell.file,
|
||||
context: ifcopenshell.entity_instance,
|
||||
item: ifcopenshell.entity_instance,
|
||||
representation_identifier: Optional[str] = None,
|
||||
representation_type: Optional[str] = None,
|
||||
) -> ifcopenshell.entity_instance:
|
||||
"""Adds an IfcTopologyRepresentation for a structural element
|
||||
|
||||
Structural analysis elements (IfcStructuralSurfaceMember,
|
||||
IfcStructuralCurveMember) use topology representations rather than solid
|
||||
geometry. This is analogous to :func:`add_axis_representation` and
|
||||
:func:`add_profile_representation` but produces an
|
||||
IfcTopologyRepresentation instead of an IfcShapeRepresentation.
|
||||
|
||||
The representation type ("Face", "Edge", "Vertex") is inferred from the
|
||||
item's IFC class if not provided explicitly.
|
||||
|
||||
:param context: The IfcGeometricRepresentationContext for the
|
||||
representation, typically a Reference context.
|
||||
:param item: The IfcTopologicalRepresentationItem (e.g. IfcFaceSurface,
|
||||
IfcEdge) to include in the representation.
|
||||
:param representation_identifier: The RepresentationIdentifier string.
|
||||
Defaults to the context's ContextIdentifier.
|
||||
:param representation_type: The RepresentationType string ("Face",
|
||||
"Edge", "Vertex"). Inferred from item class if not given.
|
||||
:return: The newly created IfcTopologyRepresentation entity.
|
||||
|
||||
Example:
|
||||
|
||||
.. code:: python
|
||||
|
||||
context = ifcopenshell.util.representation.get_context(
|
||||
model, "Model", "Reference", "GRAPH_VIEW")
|
||||
face = model.createIfcFaceSurface(bounds, surface, True)
|
||||
rep = ifcopenshell.api.geometry.add_topology_representation(
|
||||
model, context=context, item=face)
|
||||
ifcopenshell.api.geometry.assign_representation(
|
||||
model, product=member, representation=rep)
|
||||
"""
|
||||
if representation_identifier is None:
|
||||
representation_identifier = context.ContextIdentifier
|
||||
|
||||
if representation_type is None:
|
||||
for ifc_class, rep_type in _ITEM_TYPE_TO_REP_TYPE.items():
|
||||
if item.is_a(ifc_class):
|
||||
representation_type = rep_type
|
||||
break
|
||||
else:
|
||||
representation_type = "Undefined"
|
||||
|
||||
return file.createIfcTopologyRepresentation(
|
||||
context,
|
||||
representation_identifier,
|
||||
representation_type,
|
||||
[item],
|
||||
)
|
||||
@@ -0,0 +1,145 @@
|
||||
# IfcOpenShell - IFC toolkit and geometry engine
|
||||
# Copyright (C) 2022 Dion Moult <dion@thinkmoult.com>
|
||||
#
|
||||
# This file is part of IfcOpenShell.
|
||||
#
|
||||
# IfcOpenShell is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Lesser General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# IfcOpenShell is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Lesser General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Lesser General Public License
|
||||
# along with IfcOpenShell. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from math import cos, sin
|
||||
from typing import Any, Optional, Union
|
||||
|
||||
import ifcopenshell.util.element
|
||||
import ifcopenshell.util.unit
|
||||
from ifcopenshell.util.data import Clipping
|
||||
|
||||
|
||||
def add_wall_representation(
|
||||
file: ifcopenshell.file,
|
||||
context: ifcopenshell.entity_instance,
|
||||
length: float = 1.0,
|
||||
height: float = 3.0,
|
||||
direction_sense: str = "POSITIVE",
|
||||
offset: float = 0.0,
|
||||
thickness: float = 0.2,
|
||||
x_angle: float = 0.0,
|
||||
clippings: Optional[list[Union[Clipping, dict[str, Any]]]] = None,
|
||||
booleans: Optional[list[ifcopenshell.entity_instance]] = None,
|
||||
) -> ifcopenshell.entity_instance:
|
||||
"""
|
||||
Add a geometric representation for a wall.
|
||||
|
||||
:param context: The IfcGeometricRepresentationContext for the representation,
|
||||
only Model/Body/MODEL_VIEW type of representations are currently supported.
|
||||
:param length: The length of the wall in meters.
|
||||
:param height: The height of the wall in meters.
|
||||
:param offset: The base offset distance of the wall from the origin.
|
||||
:param thickness: The thickness of the wall in meters.
|
||||
:param x_angle: The slope angle along the wall's X-axis, in radians.
|
||||
:param clippings: List of clipping definitions. Clippings can be `Clipping` objects
|
||||
or dictionaries of arguments for `Clipping.parse`. Each clipping has a
|
||||
``normal`` that points toward the removed material (the discarded side),
|
||||
not toward the kept material; see :func:`clip_solid` for details.
|
||||
:param booleans: List of any existing IfcBooleanResults.
|
||||
:return: IfcShapeRepresentation.
|
||||
"""
|
||||
usecase = Usecase()
|
||||
usecase.file = file
|
||||
usecase.settings = {
|
||||
"context": context,
|
||||
"length": length,
|
||||
"height": height,
|
||||
"direction_sense": direction_sense,
|
||||
"offset": offset,
|
||||
"thickness": thickness,
|
||||
"x_angle": x_angle,
|
||||
"clippings": clippings if clippings is not None else [],
|
||||
"booleans": booleans if booleans is not None else [],
|
||||
}
|
||||
return usecase.execute()
|
||||
|
||||
|
||||
class Usecase:
|
||||
file: ifcopenshell.file
|
||||
settings: dict[str, Any]
|
||||
clippings: list[Clipping]
|
||||
|
||||
def execute(self) -> ifcopenshell.entity_instance:
|
||||
self.unit_scale = ifcopenshell.util.unit.calculate_unit_scale(self.file)
|
||||
self.clippings = [Clipping.parse(c) for c in self.settings["clippings"]]
|
||||
return self.file.createIfcShapeRepresentation(
|
||||
self.settings["context"],
|
||||
self.settings["context"].ContextIdentifier,
|
||||
"Clipping" if self.clippings or self.settings["booleans"] else "SweptSolid",
|
||||
[self.create_item()],
|
||||
)
|
||||
|
||||
def create_item(self) -> ifcopenshell.entity_instance:
|
||||
length = self.convert_si_to_unit(self.settings["length"])
|
||||
thickness = self.convert_si_to_unit(self.settings["thickness"])
|
||||
thickness *= 1 / cos(self.settings["x_angle"])
|
||||
if self.settings["direction_sense"] == "NEGATIVE":
|
||||
thickness *= -1
|
||||
points = (
|
||||
(0.0, 0.0),
|
||||
(0.0, thickness),
|
||||
(length, thickness),
|
||||
(length, 0.0),
|
||||
(0.0, 0.0),
|
||||
)
|
||||
if self.file.schema == "IFC2X3":
|
||||
curve = self.file.createIfcPolyline([self.file.createIfcCartesianPoint(p) for p in points])
|
||||
else:
|
||||
curve = self.file.createIfcIndexedPolyCurve(self.file.createIfcCartesianPointList2D(points), None, False)
|
||||
if self.settings["x_angle"]:
|
||||
extrusion_direction = self.file.createIfcDirection(
|
||||
(0.0, sin(self.settings["x_angle"]), cos(self.settings["x_angle"]))
|
||||
)
|
||||
else:
|
||||
extrusion_direction = self.file.createIfcDirection((0.0, 0.0, 1.0))
|
||||
extrusion = self.file.createIfcExtrudedAreaSolid(
|
||||
self.file.createIfcArbitraryClosedProfileDef("AREA", None, curve),
|
||||
self.file.createIfcAxis2Placement3D(
|
||||
self.file.createIfcCartesianPoint((0.0, self.convert_si_to_unit(self.settings["offset"]), 0.0)),
|
||||
self.file.createIfcDirection((0.0, 0.0, 1.0)),
|
||||
self.file.createIfcDirection((1.0, 0.0, 0.0)),
|
||||
),
|
||||
extrusion_direction,
|
||||
self.convert_si_to_unit(self.settings["height"]) * abs(1 / cos(self.settings["x_angle"])),
|
||||
)
|
||||
if self.settings["booleans"]:
|
||||
extrusion = self.apply_booleans(extrusion)
|
||||
if self.clippings:
|
||||
extrusion = self.apply_clippings(extrusion)
|
||||
return extrusion
|
||||
|
||||
def apply_booleans(self, first_operand: ifcopenshell.entity_instance) -> ifcopenshell.entity_instance:
|
||||
while self.settings["booleans"]:
|
||||
boolean = self.settings["booleans"].pop()
|
||||
boolean.FirstOperand = first_operand
|
||||
first_operand = boolean
|
||||
return first_operand
|
||||
|
||||
def apply_clippings(self, first_operand: ifcopenshell.entity_instance) -> ifcopenshell.entity_instance:
|
||||
while self.clippings:
|
||||
clipping = self.clippings.pop()
|
||||
if isinstance(clipping, ifcopenshell.entity_instance):
|
||||
new = ifcopenshell.util.element.copy(self.file, clipping)
|
||||
new.FirstOperand = first_operand
|
||||
first_operand = new
|
||||
else: # Clipping
|
||||
first_operand = clipping.apply(self.file, first_operand, self.unit_scale)
|
||||
return first_operand
|
||||
|
||||
def convert_si_to_unit(self, co: Any) -> Any:
|
||||
return co / self.unit_scale
|
||||
@@ -0,0 +1,783 @@
|
||||
# IfcOpenShell - IFC toolkit and geometry engine
|
||||
# Copyright (C) 2023 @Andrej730
|
||||
#
|
||||
# This file is part of IfcOpenShell.
|
||||
#
|
||||
# IfcOpenShell is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Lesser General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# IfcOpenShell is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Lesser General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Lesser General Public License
|
||||
# along with IfcOpenShell. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import dataclasses
|
||||
from itertools import chain
|
||||
from typing import Any, Literal, Optional, Union, overload
|
||||
|
||||
import numpy as np
|
||||
|
||||
import ifcopenshell.api.geometry
|
||||
import ifcopenshell.util.unit
|
||||
from ifcopenshell.util.shape_builder import ShapeBuilder, V
|
||||
|
||||
# SCHEMAS describe panels setup
|
||||
# where:
|
||||
# - schema rows represent window X axis
|
||||
# - schema columns represent window Y axis
|
||||
# - order of rows is from top of the window to bottom
|
||||
|
||||
WINDOW_TYPE = Literal[
|
||||
"SINGLE_PANEL",
|
||||
"DOUBLE_PANEL_HORIZONTAL",
|
||||
"DOUBLE_PANEL_VERTICAL",
|
||||
"TRIPLE_PANEL_BOTTOM",
|
||||
"TRIPLE_PANEL_HORIZONTAL",
|
||||
"TRIPLE_PANEL_LEFT",
|
||||
"TRIPLE_PANEL_RIGHT",
|
||||
"TRIPLE_PANEL_TOP",
|
||||
"TRIPLE_PANEL_VERTICAL",
|
||||
]
|
||||
|
||||
DEFAULT_PANEL_SCHEMAS = {
|
||||
"SINGLE_PANEL": [[0]],
|
||||
"DOUBLE_PANEL_HORIZONTAL": [[0], [1]],
|
||||
"DOUBLE_PANEL_VERTICAL": [[0, 1]],
|
||||
"TRIPLE_PANEL_BOTTOM": [[0, 1], [2, 2]],
|
||||
"TRIPLE_PANEL_TOP": [[0, 0], [1, 2]],
|
||||
"TRIPLE_PANEL_LEFT": [[0, 1], [0, 2]],
|
||||
"TRIPLE_PANEL_RIGHT": [[0, 1], [2, 1]],
|
||||
"TRIPLE_PANEL_HORIZONTAL": [[0], [1], [2]],
|
||||
"TRIPLE_PANEL_VERTICAL": [[0, 1, 2]],
|
||||
}
|
||||
|
||||
|
||||
def mm(x: float) -> float:
|
||||
"""mm to meters shortcut for readability"""
|
||||
return x / 1000
|
||||
|
||||
|
||||
def create_ifc_window_frame_simple(
|
||||
builder: ShapeBuilder, size: np.ndarray, thickness: Union[list[float], float], position: Optional[np.ndarray] = None
|
||||
) -> list[ifcopenshell.entity_instance]:
|
||||
"""`thickness` of the profile is defined as list in the following order:
|
||||
`(LEFT, TOP, RIGHT, BOTTOM)`
|
||||
|
||||
`thickness` can be also defined just as 1 float value.
|
||||
"""
|
||||
|
||||
if not isinstance(thickness, list):
|
||||
thickness = [thickness] * 4
|
||||
if position is None:
|
||||
position = np.zeros(3)
|
||||
np_X, np_Y, np_Z = 0, 1, 2
|
||||
np_XZ = [0, 2]
|
||||
th_left, th_up, th_right, th_bottom = thickness
|
||||
|
||||
def get_extruded_profile(profile: ifcopenshell.entity_instance):
|
||||
return builder.extrude(profile, size[np_Y], position=position, **builder.extrude_kwargs("Y"))
|
||||
|
||||
# if all lining sides are present then we can just use two rectangles
|
||||
# as inner and outer curves of the profile
|
||||
if thickness.count(0) == 0:
|
||||
panel_rect = builder.rectangle(size=size[np_XZ])
|
||||
|
||||
inner_rect_size = size - (th_left + th_right, 0, th_bottom + th_up)
|
||||
inner_rect = builder.rectangle(size=inner_rect_size[np_XZ], position=(th_left, th_bottom))
|
||||
|
||||
panel_profile = builder.profile(panel_rect, inner_curves=inner_rect)
|
||||
return [get_extruded_profile(panel_profile)]
|
||||
|
||||
# if some side has zero thickness it means we cannot use inner curves
|
||||
# and need to generate L/U shape or just separate rectangles
|
||||
else:
|
||||
|
||||
def get_segments_from_thickness() -> list[tuple[float, ...]]:
|
||||
nonlocal thickness
|
||||
segments = []
|
||||
cur_segment = []
|
||||
for i, thickness_ in enumerate(thickness):
|
||||
if thickness_ == 0:
|
||||
if cur_segment:
|
||||
segments.append(tuple(cur_segment))
|
||||
cur_segment = []
|
||||
else:
|
||||
cur_segment.append(i)
|
||||
|
||||
if cur_segment:
|
||||
if len(segments) > 0 and segments[0][0] == 0:
|
||||
segments[0] = tuple(cur_segment) + segments[0]
|
||||
else:
|
||||
segments.append(tuple(cur_segment))
|
||||
return segments
|
||||
|
||||
# prepare coords to build a lining
|
||||
# fmt: off
|
||||
outer_coords = [
|
||||
((0, 0), (0, size[np_Z])),
|
||||
((0, size[np_Z]), (size[np_X], size[np_Z])),
|
||||
((size[np_X], size[np_Z]), (size[np_X], 0)),
|
||||
((size[np_X], 0), (0, 0)),
|
||||
]
|
||||
inner_coords = [
|
||||
((th_left, th_bottom), (th_left, size[np_Z] - th_up)),
|
||||
((th_left, size[np_Z] - th_up), (size[np_X] - th_right, size[np_Z] - th_up)),
|
||||
((size[np_X] - th_right, size[np_Z] - th_up), (size[np_X] - th_right, th_bottom)),
|
||||
((size[np_X] - th_right, th_bottom), (th_left, th_bottom)),
|
||||
]
|
||||
# fmt: on
|
||||
|
||||
def get_points(segment: tuple[float, ...]) -> list[tuple[float, float]]:
|
||||
points = []
|
||||
for side in segment:
|
||||
outer = outer_coords[side]
|
||||
if side == segment[0]: # first segment
|
||||
points.append(outer[0])
|
||||
points.append(outer[1])
|
||||
|
||||
for side in reversed(segment):
|
||||
inner = inner_coords[side]
|
||||
if side == segment[-1]: # last non zero segment
|
||||
points.append(inner[1])
|
||||
points.append(inner[0])
|
||||
return points
|
||||
|
||||
segments = get_segments_from_thickness()
|
||||
segments_items: list[ifcopenshell.entity_instance] = []
|
||||
for seg in segments:
|
||||
polyline = builder.polyline(points=get_points(seg), closed=True)
|
||||
panel_profile = builder.profile(polyline)
|
||||
segments_items.append(get_extruded_profile(panel_profile))
|
||||
|
||||
return segments_items
|
||||
|
||||
|
||||
def window_l_shape_check(
|
||||
lining_to_panel_offset_y_full: float,
|
||||
lining_depth: float,
|
||||
lining_to_panel_offset_x: list[float],
|
||||
lining_thickness: list[float],
|
||||
) -> bool:
|
||||
"""`lining_thickness` and `lining_to_panel_offset_x` expected to be defined as a list,
|
||||
similarly to `create_ifc_window_frame_simple` `thickness` argument"""
|
||||
l_shape_check = lining_to_panel_offset_y_full < lining_depth and any(
|
||||
x_offset < th for th, x_offset in zip(lining_thickness, lining_to_panel_offset_x, strict=True)
|
||||
)
|
||||
return l_shape_check
|
||||
|
||||
|
||||
def create_ifc_window(
|
||||
builder: ShapeBuilder,
|
||||
lining_size: np.ndarray,
|
||||
lining_thickness: list[float],
|
||||
lining_to_panel_offset_x: float,
|
||||
lining_to_panel_offset_y_full: float,
|
||||
frame_size: np.ndarray,
|
||||
frame_thickness: float,
|
||||
glass_thickness: float,
|
||||
position: np.ndarray,
|
||||
x_offsets: Optional[list[float]] = None,
|
||||
) -> dict[str, list[ifcopenshell.entity_instance]]:
|
||||
"""`lining_thickness` and `x_offsets` are expected to be defined as a list,
|
||||
similarly to `create_ifc_window_frame_simple` `thickness` argument"""
|
||||
lining_items: list[ifcopenshell.entity_instance] = []
|
||||
main_lining_size = lining_size
|
||||
|
||||
np_Y = 1
|
||||
|
||||
if x_offsets is None:
|
||||
x_offsets = [lining_to_panel_offset_x] * 4
|
||||
# need to check offsets to decide whether lining should be rectangle
|
||||
# or L shaped
|
||||
l_shape_check = window_l_shape_check(
|
||||
lining_to_panel_offset_y_full,
|
||||
lining_size[np_Y],
|
||||
x_offsets,
|
||||
lining_thickness,
|
||||
)
|
||||
|
||||
if l_shape_check:
|
||||
main_lining_size = lining_size.copy()
|
||||
main_lining_size[np_Y] = lining_to_panel_offset_y_full
|
||||
|
||||
second_lining_size = lining_size.copy()
|
||||
second_lining_size[np_Y] = lining_size[np_Y] - lining_to_panel_offset_y_full
|
||||
second_lining_position = V(0, lining_to_panel_offset_y_full, 0)
|
||||
second_lining_thickness = [min(th, x_offset) for th, x_offset in zip(lining_thickness, x_offsets, strict=True)]
|
||||
|
||||
second_lining_items = create_ifc_window_frame_simple(
|
||||
builder, second_lining_size, second_lining_thickness, second_lining_position
|
||||
)
|
||||
lining_items.extend(second_lining_items)
|
||||
|
||||
main_lining_items = create_ifc_window_frame_simple(builder, main_lining_size, lining_thickness)
|
||||
lining_items.extend(main_lining_items)
|
||||
|
||||
frame_position = V(
|
||||
x_offsets[0],
|
||||
lining_to_panel_offset_y_full,
|
||||
x_offsets[3],
|
||||
)
|
||||
|
||||
frame_extruded_items = create_ifc_window_frame_simple(builder, frame_size, frame_thickness, frame_position)
|
||||
|
||||
glass_position = frame_position + V(0, frame_size[np_Y] / 2 - glass_thickness / 2, 0)
|
||||
glass_rect = builder.deep_copy(frame_extruded_items[0].SweptArea.InnerCurves[0])
|
||||
glass = builder.extrude(glass_rect, glass_thickness, position=glass_position, **builder.extrude_kwargs("Y"))
|
||||
|
||||
output_items = (lining_items, frame_extruded_items, [glass])
|
||||
builder.translate(chain(*output_items), position)
|
||||
|
||||
return {"Lining": lining_items, "Framing": frame_extruded_items, "Glazing": [glass]}
|
||||
|
||||
|
||||
# we use dataclass as we need default values for arguments
|
||||
# it's okay to use slots since we don't need dynamic attributes
|
||||
@dataclasses.dataclass(slots=True)
|
||||
class WindowLiningProperties:
|
||||
LiningDepth: Optional[float] = None
|
||||
"""Optional, defaults to 50mm."""
|
||||
|
||||
LiningThickness: Optional[float] = None
|
||||
"""Optional, defaults to 50mm."""
|
||||
|
||||
LiningOffset: Optional[float] = None
|
||||
"""Offset to the wall. Optional, defaults to 50mm."""
|
||||
|
||||
LiningToPanelOffsetX: Optional[float] = None
|
||||
"""Offset from the wall. Optional, defaults to 25mm."""
|
||||
|
||||
# that way it allows you to define overall_depth constant between all panels
|
||||
# and still have panels with different size:
|
||||
# overall_depth = lining_depth + offset_y
|
||||
# full offset from X axis = overall_depth - frame_depth.
|
||||
LiningToPanelOffsetY: Optional[float] = None
|
||||
"""Offset from the lining. Optional, defaults to 25mm."""
|
||||
|
||||
MullionThickness: Optional[float] = None
|
||||
"""Mullion thickness (horizontal distance between panels).
|
||||
|
||||
Applies to windows of types: DoublePanelVertical, TriplePanelBottom, TriplePanelTop,
|
||||
TriplePanelLeft, TriplePanelRight.
|
||||
|
||||
Optional, defaults to 50mm."""
|
||||
|
||||
FirstMullionOffset: Optional[float] = None
|
||||
"""Distance from the first lining to the mullion center. Optional, defaults to 300mm."""
|
||||
|
||||
SecondMullionOffset: Optional[float] = None
|
||||
"""Distance from the first lining to the second mullion center.
|
||||
|
||||
Applies to windows of type: TriplePanelVertical.
|
||||
|
||||
Optional, defaults to 450mm."""
|
||||
|
||||
TransomThickness: Optional[float] = None
|
||||
"""Transom thickness (vertical distance between panels), works similar way to mullions.
|
||||
|
||||
Applies to windows of types:DoublePanelHorizontal, TriplePanelBottom, TriplePanelTop,
|
||||
TriplePanelLeft, TriplePanelRight.
|
||||
|
||||
Optional, defaults to 50mm."""
|
||||
|
||||
FirstTransomOffset: Optional[float] = None
|
||||
"""Optional, defaults to 300mm."""
|
||||
|
||||
SecondTransomOffset: Optional[float] = None
|
||||
"""
|
||||
Applies to windows of type: TriplePanelHorizontal.
|
||||
Optional, defaults to 600mm."""
|
||||
|
||||
ShapeAspectStyle: None = None
|
||||
"""Optional. Deprecated argument."""
|
||||
|
||||
def initialize_properties(self, unit_scale: float) -> None:
|
||||
# in meters
|
||||
# fmt: off
|
||||
default_values: dict[str, float] = dict(
|
||||
LiningDepth = mm(50),
|
||||
LiningThickness = mm(50),
|
||||
LiningOffset = mm(50),
|
||||
LiningToPanelOffsetX = mm(25),
|
||||
LiningToPanelOffsetY = mm(25),
|
||||
MullionThickness = mm(50),
|
||||
FirstMullionOffset = mm(300),
|
||||
SecondMullionOffset = mm(450),
|
||||
TransomThickness = mm(50),
|
||||
FirstTransomOffset = mm(300),
|
||||
SecondTransomOffset = mm(600),
|
||||
)
|
||||
# fmt: on
|
||||
|
||||
si_conversion = 1 / unit_scale
|
||||
for attr, default_value in default_values.items():
|
||||
if getattr(self, attr) is not None:
|
||||
continue
|
||||
setattr(self, attr, default_value * si_conversion)
|
||||
|
||||
|
||||
@dataclasses.dataclass(slots=True)
|
||||
class WindowPanelProperties:
|
||||
FrameDepth: Optional[float] = None
|
||||
"""Frame thickness by Y axis. Optional, defaults to 35 mm."""
|
||||
|
||||
FrameThickness: Optional[float] = None
|
||||
"""Frame thickness by X axis. Optional, defaults to 35 mm."""
|
||||
|
||||
PanelPosition: None = None
|
||||
"""Optional, value is never used"""
|
||||
|
||||
PanelOperation: None = None
|
||||
"""Optional, value is never used.
|
||||
Defines the basic ways to describe how window panels operate."""
|
||||
|
||||
ShapeAspectStyle: None = None
|
||||
"""Optional. Deprecated argument."""
|
||||
|
||||
def initialize_properties(self, unit_scale: float) -> None:
|
||||
# in meters
|
||||
# fmt: off
|
||||
default_values: dict[str, float] = dict(
|
||||
FrameDepth = mm(35),
|
||||
FrameThickness = mm(35),
|
||||
)
|
||||
# fmt: on
|
||||
|
||||
si_conversion = 1 / unit_scale
|
||||
for attr, default_value in default_values.items():
|
||||
if getattr(self, attr) is not None:
|
||||
continue
|
||||
setattr(self, attr, default_value * si_conversion)
|
||||
|
||||
|
||||
def add_window_representation(
|
||||
file: ifcopenshell.file,
|
||||
*, # keywords only as this API implementation is probably not final
|
||||
context: ifcopenshell.entity_instance,
|
||||
overall_height: Optional[float] = None,
|
||||
overall_width: Optional[float] = None,
|
||||
partition_type: WINDOW_TYPE = "SINGLE_PANEL",
|
||||
lining_properties: Optional[Union[WindowLiningProperties, dict[str, Any]]] = None,
|
||||
panel_properties: Optional[list[Union[WindowPanelProperties, dict[str, Any]]]] = None,
|
||||
part_of_product: Optional[ifcopenshell.entity_instance] = None,
|
||||
unit_scale: Optional[float] = None,
|
||||
) -> ifcopenshell.entity_instance:
|
||||
"""units in usecase_settings expected to be in ifc project units
|
||||
|
||||
:param context: IfcGeometricRepresentationContext for the representation.
|
||||
:param overall_height: Overall window height. Defaults to 0.9m.
|
||||
:param overall_width: Overall window width. Defaults to 0.6m.
|
||||
:param partition_type: Type of the window. Defaults to SINGLE_PANEL.
|
||||
:param lining_properties: WindowLiningProperties or a dictionary to create one.
|
||||
See WindowLiningProperties description for details.
|
||||
:param panel_properties: A list of WindowPanelProperties or dictionaries to create one.
|
||||
See WindowPanelProperties description for details.
|
||||
:param unit_scale: The unit scale as calculated by
|
||||
ifcopenshell.util.unit.calculate_unit_scale. If not provided, it
|
||||
will be automatically calculated for you.
|
||||
:return: IfcShapeRepresentation for a window.
|
||||
"""
|
||||
usecase = Usecase()
|
||||
usecase.file = file
|
||||
# http://ifc43-docs.standards.buildingsmart.org/IFC/RELEASE/IFC4x3/HTML/lexical/IfcWindow.htm
|
||||
# http://ifc43-docs.standards.buildingsmart.org/IFC/RELEASE/IFC4x3/HTML/lexical/IfcWindowTypePartitioningEnum.htm
|
||||
# http://ifc43-docs.standards.buildingsmart.org/IFC/RELEASE/IFC4x3/HTML/lexical/IfcWindowLiningProperties.htm
|
||||
# http://ifc43-docs.standards.buildingsmart.org/IFC/RELEASE/IFC4x3/HTML/lexical/IfcWindowPanelProperties.htm
|
||||
# define unit_scale first as it's going to be used setting default arguments
|
||||
unit_scale = ifcopenshell.util.unit.calculate_unit_scale(file) if unit_scale is None else unit_scale
|
||||
settings: dict[str, Any] = {"unit_scale": unit_scale}
|
||||
|
||||
if lining_properties is None:
|
||||
lining_properties = WindowLiningProperties()
|
||||
elif not isinstance(lining_properties, WindowLiningProperties):
|
||||
lining_properties = WindowLiningProperties(**lining_properties)
|
||||
lining_properties.initialize_properties(unit_scale)
|
||||
lining_properties = dataclasses.asdict(lining_properties)
|
||||
|
||||
if panel_properties is None:
|
||||
panel_properties = [WindowPanelProperties()]
|
||||
|
||||
for i in range(len(panel_properties)):
|
||||
properties = panel_properties[i]
|
||||
if not isinstance(properties, WindowPanelProperties):
|
||||
properties = WindowPanelProperties(**properties)
|
||||
properties.initialize_properties(unit_scale)
|
||||
panel_properties[i] = dataclasses.asdict(properties)
|
||||
|
||||
settings.update(
|
||||
{
|
||||
"context": context,
|
||||
"overall_height": overall_height if overall_height is not None else usecase.convert_si_to_unit(0.9),
|
||||
"overall_width": overall_width if overall_width is not None else usecase.convert_si_to_unit(0.6),
|
||||
"partition_type": partition_type,
|
||||
"lining_properties": lining_properties,
|
||||
"panel_properties": panel_properties,
|
||||
"part_of_product": part_of_product,
|
||||
}
|
||||
)
|
||||
|
||||
usecase.settings = settings
|
||||
usecase.settings["panel_schema"] = DEFAULT_PANEL_SCHEMAS[usecase.settings["partition_type"]]
|
||||
return usecase.execute()
|
||||
|
||||
|
||||
class Usecase:
|
||||
file: ifcopenshell.file
|
||||
settings: dict[str, Any]
|
||||
|
||||
def execute(self):
|
||||
builder = ShapeBuilder(self.file)
|
||||
np_X, np_Y, np_Z = 0, 1, 2
|
||||
overall_height: float = self.settings["overall_height"]
|
||||
overall_width: float = self.settings["overall_width"]
|
||||
|
||||
if self.settings["context"].TargetView == "ELEVATION_VIEW":
|
||||
rect = builder.rectangle(V(overall_width, 0, overall_height))
|
||||
representation_evelevation = builder.get_representation(self.settings["context"], rect)
|
||||
return representation_evelevation
|
||||
|
||||
panel_schema: list[list[int]] = self.settings["panel_schema"]
|
||||
panels: list[dict[str, Any]] = self.settings["panel_properties"]
|
||||
accumulated_height: list[float] = [0] * len(panel_schema[0])
|
||||
built_panels: list[int] = []
|
||||
window_items: list[ifcopenshell.entity_instance] = []
|
||||
lining_items: list[ifcopenshell.entity_instance] = []
|
||||
framing_items: list[ifcopenshell.entity_instance] = []
|
||||
glazing_items: list[ifcopenshell.entity_instance] = []
|
||||
|
||||
lining_props: dict[str, Any] = self.settings["lining_properties"]
|
||||
lining_thickness: float = lining_props["LiningThickness"]
|
||||
lining_depth: float = lining_props["LiningDepth"]
|
||||
lining_offset: float = lining_props["LiningOffset"]
|
||||
lining_to_panel_offset_x: float = lining_props["LiningToPanelOffsetX"]
|
||||
lining_to_panel_offset_y: float = lining_props["LiningToPanelOffsetY"]
|
||||
overall_depth: float = lining_depth + lining_to_panel_offset_y
|
||||
|
||||
mullion_thickness: float = lining_props["MullionThickness"] / 2
|
||||
first_mullion_offset: float = lining_props["FirstMullionOffset"]
|
||||
second_mullion_offset: float = lining_props["SecondMullionOffset"]
|
||||
transom_thickness: float = lining_props["TransomThickness"] / 2
|
||||
first_transom_offset: float = lining_props["FirstTransomOffset"]
|
||||
second_transom_offset: float = lining_props["SecondTransomOffset"]
|
||||
glass_thickness: float = self.convert_si_to_unit(0.01)
|
||||
|
||||
panel_schema = list(reversed(panel_schema))
|
||||
|
||||
# create 2d representation
|
||||
def create_ifc_window_2d_representation() -> ifcopenshell.entity_instance:
|
||||
items_2d: list[ifcopenshell.entity_instance] = []
|
||||
|
||||
top_row = panel_schema[-1]
|
||||
unique_cols = len(set(top_row))
|
||||
built_panels: list[int] = []
|
||||
accumulated_width: float = 0
|
||||
|
||||
for column_i, panel_i in enumerate(top_row):
|
||||
cur_panel_items: list[ifcopenshell.entity_instance] = []
|
||||
|
||||
# lists represent left and right linings
|
||||
window_lining_thickness = [lining_thickness] * 2
|
||||
closed_lining = [True] * 2
|
||||
|
||||
if panel_i in built_panels:
|
||||
continue
|
||||
|
||||
# detect mullion
|
||||
has_mullion = unique_cols > 1
|
||||
first_column = column_i == 0
|
||||
last_column = column_i == unique_cols - 1
|
||||
left_to_mullion = has_mullion and not last_column
|
||||
right_to_mullion = has_mullion and not first_column
|
||||
|
||||
if has_mullion:
|
||||
if first_column:
|
||||
panel_width = first_mullion_offset
|
||||
elif last_column:
|
||||
panel_width = overall_width - accumulated_width
|
||||
else:
|
||||
panel_width = second_mullion_offset - accumulated_width
|
||||
|
||||
# mullion thickness
|
||||
if not first_column:
|
||||
window_lining_thickness[0] = mullion_thickness # left column
|
||||
closed_lining[0] = False
|
||||
if not last_column:
|
||||
window_lining_thickness[1] = mullion_thickness # right column
|
||||
closed_lining[1] = False
|
||||
else:
|
||||
panel_width = overall_width
|
||||
|
||||
frame_depth: float = panels[panel_i]["FrameDepth"]
|
||||
frame_thickness: float = panels[panel_i]["FrameThickness"]
|
||||
lining_to_panel_offset_y_full = (lining_depth - frame_depth) + lining_to_panel_offset_y
|
||||
base_frame_clear = lining_to_panel_offset_x + frame_thickness - lining_thickness
|
||||
current_offset_x = base_frame_clear - frame_thickness + mullion_thickness
|
||||
|
||||
# add lining
|
||||
cur_panel_items.append(
|
||||
builder.polyline(
|
||||
[
|
||||
(window_lining_thickness[0], 0),
|
||||
(panel_width - window_lining_thickness[1], 0),
|
||||
]
|
||||
)
|
||||
)
|
||||
|
||||
def get_lining_shape(
|
||||
lining_thickness: float, closed: bool = True, mirror: bool = False, x_offset: Optional[float] = None
|
||||
) -> ifcopenshell.entity_instance:
|
||||
if x_offset is None:
|
||||
x_offset = lining_to_panel_offset_x
|
||||
l_shape_check = window_l_shape_check(
|
||||
lining_to_panel_offset_y_full,
|
||||
lining_depth,
|
||||
[x_offset],
|
||||
[lining_thickness],
|
||||
)
|
||||
if l_shape_check:
|
||||
lining_shape = builder.polyline(
|
||||
[
|
||||
(0, lining_depth),
|
||||
(x_offset, lining_depth),
|
||||
(x_offset, lining_to_panel_offset_y_full),
|
||||
(lining_thickness, lining_to_panel_offset_y_full),
|
||||
(lining_thickness, 0),
|
||||
(0, 0),
|
||||
],
|
||||
closed=closed,
|
||||
)
|
||||
else:
|
||||
lining_shape = builder.polyline(
|
||||
[
|
||||
(0, lining_depth),
|
||||
(lining_thickness, lining_depth),
|
||||
(lining_thickness, 0),
|
||||
(0, 0),
|
||||
],
|
||||
closed=closed,
|
||||
)
|
||||
|
||||
if mirror:
|
||||
builder.mirror(
|
||||
lining_shape,
|
||||
mirror_axes=(1, 0),
|
||||
mirror_point=(panel_width / 2, 0),
|
||||
)
|
||||
|
||||
return lining_shape
|
||||
|
||||
cur_panel_items.extend(
|
||||
[
|
||||
get_lining_shape(
|
||||
window_lining_thickness[0],
|
||||
closed=closed_lining[0],
|
||||
x_offset=current_offset_x if right_to_mullion else None,
|
||||
),
|
||||
get_lining_shape(
|
||||
window_lining_thickness[1],
|
||||
closed=closed_lining[1],
|
||||
x_offset=current_offset_x if left_to_mullion else None,
|
||||
mirror=True,
|
||||
),
|
||||
]
|
||||
)
|
||||
|
||||
# add frame
|
||||
frame_items: list[ifcopenshell.entity_instance] = []
|
||||
|
||||
frame_position = (
|
||||
current_offset_x if right_to_mullion else lining_to_panel_offset_x,
|
||||
lining_to_panel_offset_y_full,
|
||||
)
|
||||
|
||||
frame_width = panel_width
|
||||
frame_width -= current_offset_x if left_to_mullion else lining_to_panel_offset_x
|
||||
frame_width -= current_offset_x if right_to_mullion else lining_to_panel_offset_x
|
||||
|
||||
frame_vertical = builder.rectangle(size=(frame_thickness, frame_depth))
|
||||
frame_items.extend(
|
||||
[
|
||||
frame_vertical,
|
||||
builder.mirror(
|
||||
frame_vertical,
|
||||
mirror_axes=(1, 0),
|
||||
mirror_point=(frame_width / 2, 0),
|
||||
create_copy=True,
|
||||
),
|
||||
]
|
||||
)
|
||||
|
||||
frame_horizontal = builder.polyline([(frame_thickness, 0), (frame_width - frame_thickness, 0)])
|
||||
frame_items.extend(
|
||||
[
|
||||
frame_horizontal,
|
||||
builder.translate(frame_horizontal, (0, frame_depth), create_copy=True),
|
||||
]
|
||||
)
|
||||
# glass
|
||||
frame_items.append(builder.translate(frame_horizontal, (0, frame_depth / 2), create_copy=True))
|
||||
|
||||
builder.translate(frame_items, frame_position)
|
||||
cur_panel_items.extend(frame_items)
|
||||
|
||||
builder.translate(cur_panel_items, (accumulated_width, 0))
|
||||
|
||||
accumulated_width += panel_width
|
||||
built_panels.append(panel_i)
|
||||
items_2d.extend(cur_panel_items)
|
||||
|
||||
builder.translate(items_2d, (0, lining_offset))
|
||||
representation_2d = builder.get_representation(self.settings["context"], items_2d)
|
||||
return representation_2d
|
||||
|
||||
if self.settings["context"].TargetView == "PLAN_VIEW":
|
||||
return create_ifc_window_2d_representation()
|
||||
|
||||
# TODO: need more readable way to define panel width and height
|
||||
unique_rows_in_col = [
|
||||
len(set(row[column_i] for row in panel_schema)) for column_i in range(len(panel_schema[0]))
|
||||
]
|
||||
|
||||
for row_i, panel_row in enumerate(panel_schema):
|
||||
accumulated_width = 0
|
||||
unique_cols = len(set(panel_row))
|
||||
|
||||
for column_i, panel_i in enumerate(panel_row):
|
||||
# detect mullion
|
||||
has_mullion = unique_cols > 1
|
||||
first_column = column_i == 0
|
||||
last_column = column_i == unique_cols - 1
|
||||
left_to_mullion = has_mullion and not last_column
|
||||
right_to_mullion = has_mullion and not first_column
|
||||
|
||||
# detect transom
|
||||
has_transom = unique_rows_in_col[column_i] > 1
|
||||
first_row = row_i == 0
|
||||
last_row = row_i == unique_rows_in_col[column_i] - 1
|
||||
top_to_transom = has_transom and not first_row
|
||||
bottom_to_transom = has_transom and not last_row
|
||||
|
||||
# calculate current panel dimensions
|
||||
if has_mullion:
|
||||
# panel_width
|
||||
if first_column:
|
||||
panel_width = first_mullion_offset
|
||||
elif last_column:
|
||||
panel_width = overall_width - accumulated_width
|
||||
else:
|
||||
panel_width = second_mullion_offset - accumulated_width
|
||||
else:
|
||||
panel_width = overall_width
|
||||
|
||||
if has_transom:
|
||||
if first_row:
|
||||
panel_height = first_transom_offset
|
||||
elif last_row:
|
||||
panel_height = overall_height - accumulated_height[column_i]
|
||||
else:
|
||||
panel_height = second_transom_offset - accumulated_height[column_i]
|
||||
else:
|
||||
panel_height = overall_height
|
||||
|
||||
if panel_i in built_panels:
|
||||
accumulated_height[column_i] += panel_height
|
||||
accumulated_width += panel_width
|
||||
continue
|
||||
|
||||
cur_panel = panels[panel_i]
|
||||
frame_depth = cur_panel["FrameDepth"]
|
||||
frame_thickness = cur_panel["FrameThickness"]
|
||||
lining_to_panel_offset_y_full = (lining_depth - frame_depth) + lining_to_panel_offset_y
|
||||
|
||||
# fmt: off
|
||||
# calculate lining thickness and frame size / offset
|
||||
# taking into account mullions and transoms
|
||||
window_lining_thickness = [
|
||||
mullion_thickness if right_to_mullion else lining_thickness,
|
||||
transom_thickness if bottom_to_transom else lining_thickness,
|
||||
mullion_thickness if left_to_mullion else lining_thickness,
|
||||
transom_thickness if top_to_transom else lining_thickness,
|
||||
]
|
||||
|
||||
# x offsets can differ if there are mullions or transoms because we're trying to maintain symmetry
|
||||
base_frame_clear = lining_to_panel_offset_x + frame_thickness - lining_thickness
|
||||
current_offset_x = base_frame_clear - frame_thickness + mullion_thickness
|
||||
current_offset_z = base_frame_clear - frame_thickness + transom_thickness
|
||||
x_offsets = [
|
||||
current_offset_x if right_to_mullion else lining_to_panel_offset_x, # LEFT
|
||||
current_offset_z if bottom_to_transom else lining_to_panel_offset_x, # TOP
|
||||
current_offset_x if left_to_mullion else lining_to_panel_offset_x, # RIGHT
|
||||
current_offset_z if top_to_transom else lining_to_panel_offset_x, # BOTTOM
|
||||
]
|
||||
# fmt: on
|
||||
|
||||
window_lining_size = V(panel_width, lining_depth, panel_height)
|
||||
frame_size = window_lining_size.copy()
|
||||
frame_size[np_Y] = frame_depth
|
||||
frame_size[np_X] -= x_offsets[0] + x_offsets[2]
|
||||
frame_size[np_Z] -= x_offsets[1] + x_offsets[3]
|
||||
|
||||
window_panel_position = V(accumulated_width, 0, accumulated_height[column_i])
|
||||
# create window panel
|
||||
current_window_items = create_ifc_window(
|
||||
builder,
|
||||
window_lining_size,
|
||||
window_lining_thickness,
|
||||
lining_to_panel_offset_x,
|
||||
lining_to_panel_offset_y_full,
|
||||
frame_size,
|
||||
frame_thickness,
|
||||
glass_thickness,
|
||||
window_panel_position,
|
||||
x_offsets,
|
||||
)
|
||||
built_panels.append(panel_i)
|
||||
window_items.extend(chain(*current_window_items.values()))
|
||||
lining_items.extend(current_window_items["Lining"])
|
||||
framing_items.extend(current_window_items["Framing"])
|
||||
glazing_items.extend(current_window_items["Glazing"])
|
||||
|
||||
accumulated_height[column_i] += panel_height
|
||||
accumulated_width += panel_width
|
||||
|
||||
builder.translate(window_items, (0, lining_offset, 0)) # wall offset
|
||||
representation = builder.get_representation(self.settings["context"], window_items)
|
||||
if self.settings["part_of_product"]:
|
||||
ifcopenshell.api.geometry.add_shape_aspect(
|
||||
self.file,
|
||||
"Lining",
|
||||
items=lining_items,
|
||||
representation=representation,
|
||||
part_of_product=self.settings["part_of_product"],
|
||||
)
|
||||
ifcopenshell.api.geometry.add_shape_aspect(
|
||||
self.file,
|
||||
"Framing",
|
||||
items=framing_items,
|
||||
representation=representation,
|
||||
part_of_product=self.settings["part_of_product"],
|
||||
)
|
||||
ifcopenshell.api.geometry.add_shape_aspect(
|
||||
self.file,
|
||||
"Glazing",
|
||||
items=glazing_items,
|
||||
representation=representation,
|
||||
part_of_product=self.settings["part_of_product"],
|
||||
)
|
||||
|
||||
return representation
|
||||
|
||||
@overload
|
||||
def convert_si_to_unit(self, value: float) -> float: ...
|
||||
@overload
|
||||
def convert_si_to_unit(self, value: np.ndarray) -> np.ndarray: ...
|
||||
def convert_si_to_unit(self, value: Union[float, np.ndarray]) -> Union[float, np.ndarray]:
|
||||
si_conversion = 1 / self.settings["unit_scale"]
|
||||
return value * si_conversion
|
||||
@@ -0,0 +1,95 @@
|
||||
# IfcOpenShell - IFC toolkit and geometry engine
|
||||
# Copyright (C) 2021 Dion Moult <dion@thinkmoult.com>
|
||||
#
|
||||
# This file is part of IfcOpenShell.
|
||||
#
|
||||
# IfcOpenShell is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Lesser General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# IfcOpenShell is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Lesser General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Lesser General Public License
|
||||
# along with IfcOpenShell. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
|
||||
import ifcopenshell.api.geometry
|
||||
import ifcopenshell.api.owner
|
||||
import ifcopenshell.util.element
|
||||
|
||||
|
||||
def assign_representation(
|
||||
file: ifcopenshell.file, product: ifcopenshell.entity_instance, representation: ifcopenshell.entity_instance
|
||||
) -> None:
|
||||
usecase = Usecase()
|
||||
usecase.file = file
|
||||
return usecase.execute(product, representation)
|
||||
|
||||
|
||||
class Usecase:
|
||||
file: ifcopenshell.file
|
||||
|
||||
def execute(self, product: ifcopenshell.entity_instance, representation: ifcopenshell.entity_instance) -> None:
|
||||
if product.is_a("IfcProduct"):
|
||||
product_type = ifcopenshell.util.element.get_type(product)
|
||||
if (
|
||||
product_type
|
||||
and product_type.RepresentationMaps
|
||||
and representation.RepresentationType != "MappedRepresentation"
|
||||
# Revit is adding a non-mapped representation to the exported profile-based types,
|
||||
# so assigning representation to occurrence by accident was assigning it to the type.
|
||||
# We guard from this by skipping profile and layer-based types.
|
||||
# See 6934 for example.
|
||||
and not (
|
||||
(material := ifcopenshell.util.element.get_material(product_type))
|
||||
and material.is_a() in ("IfcMaterialProfileSet", "IfcMaterialLayerSet")
|
||||
)
|
||||
):
|
||||
product = product_type
|
||||
|
||||
if product.is_a("IfcProduct"):
|
||||
self.assign_product_representation(product, representation)
|
||||
elif product.is_a("IfcTypeProduct"):
|
||||
if product.RepresentationMaps:
|
||||
maps = list(product.RepresentationMaps)
|
||||
else:
|
||||
maps = []
|
||||
self.zero = self.file.createIfcCartesianPoint((0.0, 0.0, 0.0))
|
||||
self.x_axis = self.file.createIfcDirection((1.0, 0.0, 0.0))
|
||||
self.z_axis = self.file.createIfcDirection((0.0, 0.0, 1.0))
|
||||
maps.append(
|
||||
self.file.create_entity(
|
||||
"IfcRepresentationMap",
|
||||
**{
|
||||
"MappingOrigin": self.file.createIfcAxis2Placement3D(self.zero, self.z_axis, self.x_axis),
|
||||
"MappedRepresentation": representation,
|
||||
},
|
||||
)
|
||||
)
|
||||
product.RepresentationMaps = maps
|
||||
if self.file.schema == "IFC2X3":
|
||||
types = product.ObjectTypeOf
|
||||
else:
|
||||
types = product.Types
|
||||
if types:
|
||||
for element in types[0].RelatedObjects:
|
||||
mapped_representation = ifcopenshell.api.geometry.map_representation(
|
||||
self.file, representation=representation
|
||||
)
|
||||
self.assign_product_representation(element, mapped_representation)
|
||||
ifcopenshell.api.owner.update_owner_history(self.file, element=product)
|
||||
|
||||
def assign_product_representation(
|
||||
self, product: ifcopenshell.entity_instance, representation: ifcopenshell.entity_instance
|
||||
) -> None:
|
||||
definition = product.Representation
|
||||
if not definition:
|
||||
definition = self.file.createIfcProductDefinitionShape()
|
||||
product.Representation = definition
|
||||
representations = list(definition.Representations) if definition.Representations else []
|
||||
representations.append(representation)
|
||||
definition.Representations = representations
|
||||
@@ -0,0 +1,86 @@
|
||||
# IfcOpenShell - IFC toolkit and geometry engine
|
||||
# Copyright (C) 2026 Dion Moult <dion@thinkmoult.com>
|
||||
#
|
||||
# This file is part of IfcOpenShell.
|
||||
#
|
||||
# IfcOpenShell is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Lesser General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# IfcOpenShell is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Lesser General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Lesser General Public License
|
||||
# along with IfcOpenShell. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from typing import Optional, Sequence
|
||||
|
||||
import ifcopenshell.api.pset
|
||||
import ifcopenshell.util.element
|
||||
import ifcopenshell.util.unit
|
||||
from ifcopenshell.util.data import Clipping
|
||||
|
||||
|
||||
def clip_solid(
|
||||
file: ifcopenshell.file,
|
||||
item: ifcopenshell.entity_instance,
|
||||
location: Sequence[float],
|
||||
normal: Sequence[float],
|
||||
element: Optional[ifcopenshell.entity_instance] = None,
|
||||
) -> ifcopenshell.entity_instance:
|
||||
"""Clip a solid with a half-space plane, returning an IfcBooleanClippingResult.
|
||||
|
||||
Convenience wrapper around :class:`ifcopenshell.util.data.Clipping` for
|
||||
use with any solid. This is the same convention used by the ``clippings``
|
||||
parameter of :func:`add_wall_representation`.
|
||||
|
||||
.. warning::
|
||||
|
||||
The ``normal`` points toward the **removed** material (the discarded
|
||||
side), not toward the kept material. For a slope clip the normal
|
||||
points upward into the removed wedge above the slope line. For a
|
||||
side mitre the normal points outward away from the wall body.
|
||||
|
||||
After clipping, set the parent ``IfcShapeRepresentation``
|
||||
``RepresentationType`` to ``"Clipping"``.
|
||||
|
||||
Example — trim an extruded solid to a lean-to slope (removed material is
|
||||
above the slope)::
|
||||
|
||||
bcr = ifcopenshell.api.run(
|
||||
"geometry.clip_solid", model,
|
||||
item=extrusion,
|
||||
location=[0.0, 0.0, 3.26],
|
||||
normal=[0.419, 0.0, 0.908], # points UP toward removed material
|
||||
)
|
||||
|
||||
:param item: The solid to clip (``IfcSweptAreaSolid``, ``IfcSweptDiskSolid``,
|
||||
or ``IfcBooleanClippingResult``).
|
||||
:param location: A point on the clipping plane in the representation's
|
||||
local coordinate system.
|
||||
:param normal: Plane normal pointing toward the material to be removed
|
||||
(see warning above).
|
||||
:param element: If provided, the resulting ``IfcBooleanClippingResult`` is
|
||||
registered in the element's ``BBIM_Boolean`` property set so that
|
||||
:func:`regenerate_wall_representation` preserves it during regeneration.
|
||||
:return: The resulting ``IfcBooleanClippingResult``.
|
||||
"""
|
||||
unit_scale = ifcopenshell.util.unit.calculate_unit_scale(file)
|
||||
clipping = Clipping(location=tuple(location), normal=tuple(normal))
|
||||
result = clipping.apply(file, item, unit_scale)
|
||||
if element is not None:
|
||||
pset_data = ifcopenshell.util.element.get_pset(element, "BBIM_Boolean")
|
||||
if pset_data:
|
||||
pset = file.by_id(pset_data["id"])
|
||||
data = list(set(json.loads(pset_data["Data"]) + [result.id()]))
|
||||
else:
|
||||
pset = ifcopenshell.api.pset.add_pset(file, product=element, name="BBIM_Boolean")
|
||||
data = [result.id()]
|
||||
ifcopenshell.api.pset.edit_pset(file, pset=pset, properties={"Data": json.dumps(data)})
|
||||
return result
|
||||
@@ -0,0 +1,116 @@
|
||||
# IfcOpenShell - IFC toolkit and geometry engine
|
||||
# Copyright (C) 2026 Dion Moult <dion@thinkmoult.com>
|
||||
#
|
||||
# This file is part of IfcOpenShell.
|
||||
#
|
||||
# IfcOpenShell is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Lesser General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# IfcOpenShell is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Lesser General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Lesser General Public License
|
||||
# along with IfcOpenShell. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from typing import Optional, Sequence
|
||||
|
||||
import numpy as np
|
||||
|
||||
import ifcopenshell.api.pset
|
||||
import ifcopenshell.util.element
|
||||
import ifcopenshell.util.unit
|
||||
from ifcopenshell.util.shape_builder import ShapeBuilder
|
||||
|
||||
|
||||
def clip_solid_bounded(
|
||||
file: ifcopenshell.file,
|
||||
item: ifcopenshell.entity_instance,
|
||||
location: Sequence[float],
|
||||
normal: Sequence[float],
|
||||
boundary_points: Sequence[Sequence[float]],
|
||||
boundary_position: Sequence[float] = (0.0, 0.0, 0.0),
|
||||
element: Optional[ifcopenshell.entity_instance] = None,
|
||||
) -> ifcopenshell.entity_instance:
|
||||
"""Clip a solid with a polygonally bounded half-space, returning an IfcBooleanClippingResult.
|
||||
|
||||
Like :func:`clip_solid`, but the boolean subtraction is restricted to the
|
||||
region enclosed by ``boundary_points`` rather than extending across the
|
||||
entire half-space. The clipping plane is still infinite, but material is
|
||||
only removed within the extruded footprint of the polygon.
|
||||
|
||||
The ``normal`` convention is the same as :func:`clip_solid`: it points
|
||||
toward the **removed** material.
|
||||
|
||||
After clipping, set the parent ``IfcShapeRepresentation``
|
||||
``RepresentationType`` to ``"Clipping"``.
|
||||
|
||||
Example::
|
||||
|
||||
bcr = ifcopenshell.api.run(
|
||||
"geometry.clip_solid_bounded", model,
|
||||
item=extrusion,
|
||||
location=[2.5, 0.0, 2.0],
|
||||
normal=[0.6, 0.0, 0.8],
|
||||
boundary_points=[[2.0, 0.0], [3.0, 0.0], [3.0, 2.0], [2.0, 2.0]],
|
||||
)
|
||||
|
||||
:param item: The solid to clip (``IfcSweptAreaSolid``, ``IfcSweptDiskSolid``,
|
||||
or ``IfcBooleanClippingResult``).
|
||||
:param location: A point on the clipping plane in the representation's
|
||||
local coordinate system.
|
||||
:param normal: Plane normal pointing toward the material to be removed.
|
||||
:param boundary_points: 2D ``[x, y]`` points defining the closed polygonal
|
||||
boundary in the coordinate system of ``boundary_position``. The polygon
|
||||
is automatically closed — do not repeat the first point.
|
||||
:param boundary_position: 3D origin of the boundary coordinate system
|
||||
(axes default to the global X/Y/Z directions). Defaults to the origin.
|
||||
:param element: If provided, the resulting ``IfcBooleanClippingResult`` is
|
||||
registered in the element's ``BBIM_Boolean`` property set so that
|
||||
:func:`regenerate_wall_representation` preserves it during regeneration.
|
||||
:return: The resulting ``IfcBooleanClippingResult``.
|
||||
"""
|
||||
unit_scale = ifcopenshell.util.unit.calculate_unit_scale(file)
|
||||
builder = ShapeBuilder(file)
|
||||
|
||||
normal_arr = np.array(normal)
|
||||
if np.allclose(normal_arr, [0.0, 0.0, 1.0], atol=1e-2) or np.allclose(normal_arr, [0.0, 0.0, -1.0], atol=1e-2):
|
||||
arbitrary_vector = np.array([0.0, 1.0, 0.0])
|
||||
else:
|
||||
arbitrary_vector = np.array([0.0, 0.0, 1.0])
|
||||
x_axis = np.cross(normal_arr, arbitrary_vector)
|
||||
x_axis /= np.linalg.norm(x_axis)
|
||||
|
||||
scaled_location = [i / unit_scale for i in location]
|
||||
plane_placement = builder.create_axis2_placement_3d(scaled_location, normal, x_axis)
|
||||
plane = file.create_entity("IfcPlane", plane_placement)
|
||||
|
||||
scaled_boundary_position = [i / unit_scale for i in boundary_position]
|
||||
boundary_pos_entity = file.create_entity(
|
||||
"IfcAxis2Placement3D",
|
||||
file.create_entity("IfcCartesianPoint", scaled_boundary_position),
|
||||
)
|
||||
|
||||
scaled_pts = [[p[0] / unit_scale, p[1] / unit_scale] for p in boundary_points]
|
||||
scaled_pts.append(scaled_pts[0]) # close the polygon
|
||||
ifc_pts = [file.create_entity("IfcCartesianPoint", p) for p in scaled_pts]
|
||||
boundary = file.createIfcPolyline(ifc_pts)
|
||||
|
||||
half_space = file.create_entity("IfcPolygonalBoundedHalfSpace", plane, False, boundary_pos_entity, boundary)
|
||||
result = file.create_entity("IfcBooleanClippingResult", "DIFFERENCE", item, half_space)
|
||||
if element is not None:
|
||||
pset_data = ifcopenshell.util.element.get_pset(element, "BBIM_Boolean")
|
||||
if pset_data:
|
||||
pset = file.by_id(pset_data["id"])
|
||||
data = list(set(json.loads(pset_data["Data"]) + [result.id()]))
|
||||
else:
|
||||
pset = ifcopenshell.api.pset.add_pset(file, product=element, name="BBIM_Boolean")
|
||||
data = [result.id()]
|
||||
ifcopenshell.api.pset.edit_pset(file, pset=pset, properties={"Data": json.dumps(data)})
|
||||
return result
|
||||
@@ -0,0 +1,61 @@
|
||||
# IfcOpenShell - IFC toolkit and geometry engine
|
||||
# Copyright (C) 2022 Dion Moult <dion@thinkmoult.com>
|
||||
#
|
||||
# This file is part of IfcOpenShell.
|
||||
#
|
||||
# IfcOpenShell is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Lesser General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# IfcOpenShell is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Lesser General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Lesser General Public License
|
||||
# along with IfcOpenShell. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from typing import Optional
|
||||
|
||||
import ifcopenshell
|
||||
import ifcopenshell.api.owner
|
||||
import ifcopenshell.guid
|
||||
import ifcopenshell.util.element
|
||||
|
||||
|
||||
def connect_element(
|
||||
file: ifcopenshell.file,
|
||||
relating_element: ifcopenshell.entity_instance,
|
||||
related_element: ifcopenshell.entity_instance,
|
||||
description: Optional[str] = None,
|
||||
) -> ifcopenshell.entity_instance:
|
||||
incompatible_connections = []
|
||||
|
||||
for rel in relating_element.ConnectedFrom:
|
||||
if rel.is_a() == "IfcRelConnectsElements" and rel.RelatingElement == related_element:
|
||||
incompatible_connections.append(rel)
|
||||
|
||||
for rel in related_element.ConnectedTo:
|
||||
if rel.is_a() == "IfcRelConnectsElements" and rel.RelatedElement == relating_element:
|
||||
incompatible_connections.append(rel)
|
||||
|
||||
if incompatible_connections:
|
||||
for connection in set(incompatible_connections):
|
||||
history = connection.OwnerHistory
|
||||
file.remove(connection)
|
||||
if history:
|
||||
ifcopenshell.util.element.remove_deep2(file, history)
|
||||
|
||||
for rel in relating_element.ConnectedTo:
|
||||
if rel.is_a() == "IfcRelConnectsElements" and rel.RelatedElement == related_element:
|
||||
rel.Description = description
|
||||
return rel
|
||||
|
||||
return file.createIfcRelConnectsElements(
|
||||
ifcopenshell.guid.new(),
|
||||
OwnerHistory=ifcopenshell.api.owner.create_owner_history(file),
|
||||
Description=description,
|
||||
RelatingElement=relating_element,
|
||||
RelatedElement=related_element,
|
||||
)
|
||||
@@ -0,0 +1,84 @@
|
||||
# IfcOpenShell - IFC toolkit and geometry engine
|
||||
# Copyright (C) 2022 Dion Moult <dion@thinkmoult.com>
|
||||
#
|
||||
# This file is part of IfcOpenShell.
|
||||
#
|
||||
# IfcOpenShell is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Lesser General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# IfcOpenShell is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Lesser General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Lesser General Public License
|
||||
# along with IfcOpenShell. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from typing import Optional
|
||||
|
||||
import ifcopenshell
|
||||
import ifcopenshell.api.owner
|
||||
import ifcopenshell.guid
|
||||
import ifcopenshell.util.element
|
||||
|
||||
|
||||
def connect_path(
|
||||
file: ifcopenshell.file,
|
||||
relating_element: ifcopenshell.entity_instance,
|
||||
related_element: ifcopenshell.entity_instance,
|
||||
relating_connection: str = "NOTDEFINED",
|
||||
related_connection: str = "NOTDEFINED",
|
||||
description: Optional[str] = None,
|
||||
connection_geometry: Optional[ifcopenshell.entity_instance] = None,
|
||||
) -> ifcopenshell.entity_instance:
|
||||
incompatible_connections: list[ifcopenshell.entity_instance] = []
|
||||
for rel in relating_element.ConnectedTo:
|
||||
if not rel.is_a("IfcRelConnectsPathElements"):
|
||||
continue
|
||||
if rel.RelatedElement == related_element:
|
||||
incompatible_connections.append(rel)
|
||||
elif rel.RelatingConnectionType in ["ATSTART", "ATEND"] and rel.RelatingConnectionType == relating_connection:
|
||||
incompatible_connections.append(rel)
|
||||
|
||||
for rel in relating_element.ConnectedFrom:
|
||||
if not rel.is_a("IfcRelConnectsPathElements"):
|
||||
continue
|
||||
if rel.RelatedConnectionType in ["ATSTART", "ATEND"] and rel.RelatedConnectionType == relating_connection:
|
||||
incompatible_connections.append(rel)
|
||||
|
||||
for rel in related_element.ConnectedFrom:
|
||||
if not rel.is_a("IfcRelConnectsPathElements"):
|
||||
continue
|
||||
if rel.RelatedConnectionType in ["ATSTART", "ATEND"] and rel.RelatedConnectionType == related_connection:
|
||||
incompatible_connections.append(rel)
|
||||
|
||||
for rel in related_element.ConnectedTo:
|
||||
if not rel.is_a("IfcRelConnectsPathElements"):
|
||||
continue
|
||||
if rel.RelatedElement == relating_element:
|
||||
incompatible_connections.append(rel)
|
||||
elif rel.RelatingConnectionType in ["ATSTART", "ATEND"] and rel.RelatingConnectionType == related_connection:
|
||||
incompatible_connections.append(rel)
|
||||
|
||||
if incompatible_connections:
|
||||
for connection in set(incompatible_connections):
|
||||
history = connection.OwnerHistory
|
||||
file.remove(connection)
|
||||
if history:
|
||||
ifcopenshell.util.element.remove_deep2(file, history)
|
||||
|
||||
return file.create_entity(
|
||||
"IfcRelConnectsPathElements",
|
||||
ifcopenshell.guid.new(),
|
||||
OwnerHistory=ifcopenshell.api.owner.create_owner_history(file),
|
||||
Description=description,
|
||||
ConnectionGeometry=connection_geometry,
|
||||
RelatingElement=relating_element,
|
||||
RelatedElement=related_element,
|
||||
RelatingConnectionType=relating_connection,
|
||||
RelatedConnectionType=related_connection,
|
||||
RelatingPriorities=[],
|
||||
RelatedPriorities=[],
|
||||
)
|
||||
@@ -0,0 +1,64 @@
|
||||
# IfcOpenShell - IFC toolkit and geometry engine
|
||||
# Copyright (C) 2025 Dion Moult <dion@thinkmoult.com>
|
||||
#
|
||||
# This file is part of IfcOpenShell.
|
||||
#
|
||||
# IfcOpenShell is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Lesser General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# IfcOpenShell is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Lesser General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Lesser General Public License
|
||||
# along with IfcOpenShell. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from typing import Optional
|
||||
|
||||
import numpy as np
|
||||
|
||||
import ifcopenshell
|
||||
import ifcopenshell.api.geometry
|
||||
import ifcopenshell.util.placement
|
||||
import ifcopenshell.util.representation
|
||||
import ifcopenshell.util.shape_builder
|
||||
|
||||
|
||||
def connect_wall(
|
||||
file: ifcopenshell.file,
|
||||
wall1: ifcopenshell.entity_instance,
|
||||
wall2: ifcopenshell.entity_instance,
|
||||
is_atpath: bool = False,
|
||||
) -> Optional[ifcopenshell.entity_instance]:
|
||||
matrix1i = np.linalg.inv(ifcopenshell.util.placement.get_local_placement(wall1.ObjectPlacement))
|
||||
matrix2 = ifcopenshell.util.placement.get_local_placement(wall2.ObjectPlacement)
|
||||
axis1 = ifcopenshell.util.representation.get_reference_line(wall1)
|
||||
axis2 = ifcopenshell.util.representation.get_reference_line(wall2)
|
||||
axis2[0] = (matrix1i @ matrix2 @ np.concatenate((axis2[0], (0, 1))))[:2]
|
||||
axis2[1] = (matrix1i @ matrix2 @ np.concatenate((axis2[1], (0, 1))))[:2]
|
||||
midx = (axis1[0][0] + axis1[1][0]) / 2
|
||||
starty = axis2[0][1]
|
||||
endy = axis2[1][1]
|
||||
y = axis1[0][1]
|
||||
|
||||
if (x := ifcopenshell.util.shape_builder.intersect_x_axis_2d(*axis2, y=y)) is None:
|
||||
return
|
||||
|
||||
wall1_end = "ATEND" if x > midx else "ATSTART"
|
||||
if is_atpath:
|
||||
wall2_end = "ATPATH"
|
||||
elif abs(y - starty) < abs(y - endy):
|
||||
wall2_end = "ATSTART"
|
||||
else:
|
||||
wall2_end = "ATEND"
|
||||
|
||||
return ifcopenshell.api.geometry.connect_path(
|
||||
file,
|
||||
relating_element=wall1,
|
||||
related_element=wall2,
|
||||
relating_connection=wall1_end,
|
||||
related_connection=wall2_end,
|
||||
)
|
||||
@@ -0,0 +1,76 @@
|
||||
# IfcOpenShell - IFC toolkit and geometry engine
|
||||
# Copyright (C) 2026 Dion Moult <dion@thinkmoult.com>
|
||||
#
|
||||
# This file is part of IfcOpenShell.
|
||||
#
|
||||
# IfcOpenShell is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Lesser General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# IfcOpenShell is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Lesser General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Lesser General Public License
|
||||
# along with IfcOpenShell. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Optional
|
||||
|
||||
import ifcopenshell.api.geometry
|
||||
import ifcopenshell.util.element
|
||||
import ifcopenshell.util.representation
|
||||
|
||||
|
||||
def copy_representation(
|
||||
file: ifcopenshell.file,
|
||||
source: ifcopenshell.entity_instance,
|
||||
target: ifcopenshell.entity_instance,
|
||||
context_identifier: str = "Body",
|
||||
) -> Optional[ifcopenshell.entity_instance]:
|
||||
"""Copy a geometric representation from one element to another.
|
||||
|
||||
Finds the named representation on ``source``, deep-copies its entity
|
||||
graph (geometry items, profiles, placements, etc.), and assigns the copy
|
||||
to ``target``. Representation contexts are shared rather than copied.
|
||||
If ``target`` already has a matching representation it is removed and
|
||||
replaced.
|
||||
|
||||
If no matching representation is found on ``source``, returns ``None``
|
||||
and leaves ``target`` unchanged.
|
||||
|
||||
:param source: The element to copy the representation from.
|
||||
:param target: The element to assign the copied representation to.
|
||||
:param context_identifier: The RepresentationIdentifier to look up on
|
||||
``source`` (e.g. ``"Body"``, ``"Axis"``, ``"Box"``).
|
||||
Defaults to ``"Body"``.
|
||||
:return: The newly created IfcShapeRepresentation, or None if no
|
||||
matching representation was found on ``source``.
|
||||
|
||||
Example:
|
||||
|
||||
.. code:: python
|
||||
|
||||
wall_a = model.by_id(1)
|
||||
wall_b = model.by_id(2)
|
||||
|
||||
# Give wall_b the same body geometry as wall_a.
|
||||
ifcopenshell.api.geometry.copy_representation(model,
|
||||
source=wall_a, target=wall_b)
|
||||
"""
|
||||
source_rep = ifcopenshell.util.representation.get_representation(source, "Model", context_identifier)
|
||||
if source_rep is None:
|
||||
return None
|
||||
|
||||
new_rep = ifcopenshell.util.element.copy_deep(file, source_rep, exclude=["IfcGeometricRepresentationContext"])
|
||||
|
||||
existing_rep = ifcopenshell.util.representation.get_representation(target, "Model", context_identifier)
|
||||
if existing_rep:
|
||||
ifcopenshell.api.geometry.unassign_representation(file, product=target, representation=existing_rep)
|
||||
ifcopenshell.api.geometry.remove_representation(file, representation=existing_rep)
|
||||
|
||||
ifcopenshell.api.geometry.assign_representation(file, product=target, representation=new_rep)
|
||||
return new_rep
|
||||
@@ -0,0 +1,92 @@
|
||||
# IfcOpenShell - IFC toolkit and geometry engine
|
||||
# Copyright (C) 2023 Dion Moult <dion@thinkmoult.com>
|
||||
#
|
||||
# This file is part of IfcOpenShell.
|
||||
#
|
||||
# IfcOpenShell is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Lesser General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# IfcOpenShell is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Lesser General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Lesser General Public License
|
||||
# along with IfcOpenShell. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from typing import Any
|
||||
|
||||
import numpy as np
|
||||
|
||||
import ifcopenshell.api.geometry
|
||||
import ifcopenshell.util.unit
|
||||
|
||||
|
||||
def create_2pt_wall(
|
||||
file: ifcopenshell.file,
|
||||
element: ifcopenshell.entity_instance,
|
||||
context: ifcopenshell.entity_instance,
|
||||
p1: tuple[float, float],
|
||||
p2: tuple[float, float],
|
||||
elevation: float,
|
||||
height: float,
|
||||
thickness: float,
|
||||
is_si: bool = True,
|
||||
) -> ifcopenshell.entity_instance:
|
||||
"""
|
||||
Create a wall between two points (p1 and p2).
|
||||
A shortcut for geometry.add_wall_representation.
|
||||
|
||||
:param element: Wall IFC element.
|
||||
:param context: IfcGeometricRepresentationContext for the representation.
|
||||
only Model/Body/MODEL_VIEW type of representations are currently supported.
|
||||
:param p1: The starting point (x, y) of the wall.
|
||||
:param p2: The ending point (x, y) of the wall.
|
||||
:param elevation: The base elevation (z-coordinate) for the wall.
|
||||
:param height: The height of the wall.
|
||||
:param thickness: The thickness of the wall.
|
||||
:param is_si: If True, provided arguments units are treated as SI (meters).
|
||||
If False, values are converted from project units to SI.
|
||||
:return: IfcShapeRepresentation.
|
||||
"""
|
||||
si_conversion = ifcopenshell.util.unit.calculate_unit_scale(file)
|
||||
|
||||
p1_ = np.array(p1).astype(float)
|
||||
p2_ = np.array(p2).astype(float)
|
||||
|
||||
length = float(np.linalg.norm(p2_ - p1_))
|
||||
|
||||
if not is_si:
|
||||
length = convert_unit_to_si(length, si_conversion)
|
||||
height = convert_unit_to_si(height, si_conversion)
|
||||
thickness = convert_unit_to_si(thickness, si_conversion)
|
||||
# No need to convert p2 as length is already calculated.
|
||||
p1_ = convert_unit_to_si(p1_, si_conversion)
|
||||
elevation = convert_unit_to_si(elevation, si_conversion)
|
||||
|
||||
representation = ifcopenshell.api.geometry.add_wall_representation(
|
||||
file,
|
||||
context=context,
|
||||
length=length,
|
||||
height=height,
|
||||
thickness=thickness,
|
||||
)
|
||||
|
||||
v = p2_ - p1_
|
||||
v /= float(np.linalg.norm(v))
|
||||
matrix = np.array(
|
||||
[
|
||||
[v[0], -v[1], 0, p1_[0]],
|
||||
[v[1], v[0], 0, p1_[1]],
|
||||
[0, 0, 1, elevation],
|
||||
[0, 0, 0, 1],
|
||||
]
|
||||
)
|
||||
ifcopenshell.api.geometry.edit_object_placement(file, product=element, matrix=matrix)
|
||||
return representation
|
||||
|
||||
|
||||
def convert_unit_to_si(co: Any, si_conversion: float) -> Any:
|
||||
return co * si_conversion
|
||||
@@ -0,0 +1,55 @@
|
||||
# IfcOpenShell - IFC toolkit and geometry engine
|
||||
# Copyright (C) 2022 Dion Moult <dion@thinkmoult.com>
|
||||
#
|
||||
# This file is part of IfcOpenShell.
|
||||
#
|
||||
# IfcOpenShell is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Lesser General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# IfcOpenShell is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Lesser General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Lesser General Public License
|
||||
# along with IfcOpenShell. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import ifcopenshell
|
||||
import ifcopenshell.util.element
|
||||
|
||||
|
||||
def disconnect_element(
|
||||
file: ifcopenshell.file,
|
||||
relating_element: ifcopenshell.entity_instance,
|
||||
related_element: ifcopenshell.entity_instance,
|
||||
) -> None:
|
||||
# TODO: arguments relating_element, related_element probably
|
||||
# should be renamed to element1, element2
|
||||
# as api call doesn't really treat them as "relating" and "related"
|
||||
# and just purging all connections between them
|
||||
incompatible_connections = []
|
||||
|
||||
for rel in relating_element.ConnectedTo:
|
||||
if rel.is_a() == "IfcRelConnectsElements" and rel.RelatedElement == related_element:
|
||||
incompatible_connections.append(rel)
|
||||
|
||||
for rel in relating_element.ConnectedFrom:
|
||||
if rel.is_a() == "IfcRelConnectsElements" and rel.RelatingElement == related_element:
|
||||
incompatible_connections.append(rel)
|
||||
|
||||
for rel in related_element.ConnectedTo:
|
||||
if rel.is_a() == "IfcRelConnectsElements" and rel.RelatedElement == relating_element:
|
||||
incompatible_connections.append(rel)
|
||||
|
||||
for rel in related_element.ConnectedFrom:
|
||||
if rel.is_a() == "IfcRelConnectsElements" and rel.RelatingElement == relating_element:
|
||||
incompatible_connections.append(rel)
|
||||
|
||||
if incompatible_connections:
|
||||
for connection in set(incompatible_connections):
|
||||
history = connection.OwnerHistory
|
||||
file.remove(connection)
|
||||
if history:
|
||||
ifcopenshell.util.element.remove_deep2(file, history)
|
||||
@@ -0,0 +1,58 @@
|
||||
# IfcOpenShell - IFC toolkit and geometry engine
|
||||
# Copyright (C) 2022 Dion Moult <dion@thinkmoult.com>
|
||||
#
|
||||
# This file is part of IfcOpenShell.
|
||||
#
|
||||
# IfcOpenShell is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Lesser General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# IfcOpenShell is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Lesser General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Lesser General Public License
|
||||
# along with IfcOpenShell. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from typing import Optional
|
||||
|
||||
import ifcopenshell
|
||||
import ifcopenshell.util.element
|
||||
|
||||
|
||||
def disconnect_path(
|
||||
file: ifcopenshell.file,
|
||||
element: Optional[ifcopenshell.entity_instance] = None,
|
||||
connection_type: Optional[str] = None,
|
||||
relating_element: Optional[ifcopenshell.entity_instance] = None,
|
||||
related_element: Optional[ifcopenshell.entity_instance] = None,
|
||||
) -> None:
|
||||
"""There are two options to use this API method:
|
||||
- provide `element` (connected from) and `connection_type` that should be disconnected.
|
||||
- provide connected elements to disconnect explicitly:
|
||||
`relating_element` (connected from) and `related_element` (connected to)
|
||||
"""
|
||||
if connection_type and element:
|
||||
connections = [
|
||||
r
|
||||
for r in element.ConnectedTo
|
||||
if r.is_a("IfcRelConnectsPathElements") and r.RelatingConnectionType == connection_type
|
||||
] + [
|
||||
r
|
||||
for r in element.ConnectedFrom
|
||||
if r.is_a("IfcRelConnectsPathElements") and r.RelatedConnectionType == connection_type
|
||||
]
|
||||
elif related_element:
|
||||
connections = [
|
||||
r
|
||||
for r in relating_element.ConnectedTo
|
||||
if r.is_a("IfcRelConnectsPathElements") and r.RelatedElement == related_element
|
||||
]
|
||||
|
||||
for connection in set(connections):
|
||||
history = connection.OwnerHistory
|
||||
file.remove(connection)
|
||||
if history:
|
||||
ifcopenshell.util.element.remove_deep2(file, history)
|
||||
@@ -0,0 +1,201 @@
|
||||
# IfcOpenShell - IFC toolkit and geometry engine
|
||||
# Copyright (C) 2021 Dion Moult <dion@thinkmoult.com>
|
||||
#
|
||||
# This file is part of IfcOpenShell.
|
||||
#
|
||||
# IfcOpenShell is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Lesser General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# IfcOpenShell is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Lesser General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Lesser General Public License
|
||||
# along with IfcOpenShell. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from typing import Any, Optional, Union
|
||||
|
||||
import numpy as np
|
||||
import numpy.typing as npt
|
||||
|
||||
import ifcopenshell.api.owner
|
||||
import ifcopenshell.util.element
|
||||
import ifcopenshell.util.placement
|
||||
import ifcopenshell.util.unit
|
||||
from ifcopenshell.util.shape_builder import ShapeBuilder
|
||||
|
||||
NPArrayOfFloats = npt.NDArray[np.float64]
|
||||
|
||||
|
||||
def edit_object_placement(
|
||||
file: ifcopenshell.file,
|
||||
product: ifcopenshell.entity_instance,
|
||||
matrix: Optional[NPArrayOfFloats] = None,
|
||||
is_si: bool = True,
|
||||
should_transform_children: bool = False,
|
||||
) -> ifcopenshell.entity_instance:
|
||||
"""Changes the object placement matrix of an element
|
||||
|
||||
The placement matrix is a 4x4 matrix describing the location and
|
||||
orientation of an element in 3D. See
|
||||
https://docs.ifcopenshell.org/ifcopenshell-python/geometry_creation.html#object-placements
|
||||
for more details.
|
||||
|
||||
This only supports local placements. Grid and linear placements are not
|
||||
supported.
|
||||
|
||||
:param matrix: A 4x4 matrix in numpy. If left blank, it is the identity
|
||||
matrix (equivalent to ``np.eye(4)``).
|
||||
:param is_si: If True, the matrix is given in SI units. If false, in
|
||||
project units.
|
||||
:param should_transform_children: A child element is a nested element,
|
||||
opening, filling, etc. If True, child elements move along with the
|
||||
parent; pass True when moving an assembly (roof, furniture group, etc.)
|
||||
and you want all children to follow. If False (default), child elements
|
||||
keep their current world positions; their local placements are rewritten
|
||||
to compensate for the parent move.
|
||||
:return: The new or updated IfcLocalPlacement entity
|
||||
"""
|
||||
usecase = Usecase()
|
||||
usecase.file = file
|
||||
usecase.settings = {
|
||||
"product": product,
|
||||
"matrix": matrix if matrix is not None else np.eye(4),
|
||||
"is_si": is_si,
|
||||
"should_transform_children": should_transform_children,
|
||||
}
|
||||
return usecase.execute()
|
||||
|
||||
|
||||
class Usecase:
|
||||
file: ifcopenshell.file
|
||||
settings: dict[str, Any]
|
||||
|
||||
def execute(self):
|
||||
if not hasattr(self.settings["product"], "ObjectPlacement"):
|
||||
return
|
||||
self.unit_scale = ifcopenshell.util.unit.calculate_unit_scale(self.file)
|
||||
self.builder = ShapeBuilder(self.file)
|
||||
|
||||
if not self.settings["is_si"]:
|
||||
self.convert_matrix_to_si(self.settings["matrix"])
|
||||
|
||||
children_settings = []
|
||||
if not self.settings["should_transform_children"]:
|
||||
children_settings = self.get_children_settings(self.settings["product"].ObjectPlacement)
|
||||
|
||||
placement_rel_to = self.get_placement_rel_to()
|
||||
relative_placement = self.get_relative_placement(placement_rel_to)
|
||||
new_placement = self.file.createIfcLocalPlacement(RelativePlacement=relative_placement)
|
||||
|
||||
old_placement = self.settings["product"].ObjectPlacement
|
||||
|
||||
if old_placement:
|
||||
for inverse in self.file.get_inverse(old_placement):
|
||||
if inverse.is_a("IfcLocalPlacement"):
|
||||
ifcopenshell.util.element.replace_attribute(inverse, old_placement, new_placement)
|
||||
|
||||
if self.file.get_total_inverses(old_placement) == 1:
|
||||
self.settings["product"].ObjectPlacement = None
|
||||
old_placement.PlacementRelTo = None
|
||||
ifcopenshell.util.element.remove_deep2(self.file, old_placement)
|
||||
|
||||
new_placement.PlacementRelTo = placement_rel_to
|
||||
self.settings["product"].ObjectPlacement = new_placement
|
||||
|
||||
ifcopenshell.api.owner.update_owner_history(self.file, element=self.settings["product"])
|
||||
|
||||
for settings in children_settings:
|
||||
self.settings = settings
|
||||
self.execute()
|
||||
|
||||
return new_placement
|
||||
|
||||
def convert_matrix_to_si(self, matrix: NPArrayOfFloats):
|
||||
matrix[0][3] *= self.unit_scale
|
||||
matrix[1][3] *= self.unit_scale
|
||||
matrix[2][3] *= self.unit_scale
|
||||
|
||||
def get_placement_rel_to(self) -> Union[ifcopenshell.entity_instance, None]:
|
||||
product = self.settings["product"]
|
||||
relating_object = None
|
||||
|
||||
if rels := getattr(product, "Decomposes", None):
|
||||
relating_object = rels[0].RelatingObject
|
||||
elif rels := getattr(product, "Nests", None):
|
||||
relating_object = rels[0].RelatingObject
|
||||
elif rels := getattr(product, "ContainedIn", None):
|
||||
relating_object = rels[0].RelatedElement
|
||||
elif rels := getattr(product, "VoidsElements", None):
|
||||
relating_object = rels[0].RelatingBuildingElement
|
||||
elif rels := getattr(product, "FillsVoids", None):
|
||||
relating_object = rels[0].RelatingOpeningElement
|
||||
elif rels := getattr(product, "ProjectsElements", None):
|
||||
relating_object = rels[0].RelatingElement
|
||||
# TODO: add tests when there will be adherence api
|
||||
elif rels := getattr(product, "AdheresToElement", None):
|
||||
relating_object = rels[0].RelatingElement
|
||||
elif rels := getattr(product, "ContainedInStructure", None):
|
||||
return rels[0].RelatingStructure.ObjectPlacement
|
||||
|
||||
if relating_object:
|
||||
return getattr(relating_object, "ObjectPlacement", None)
|
||||
|
||||
def get_children_settings(self, placement: Union[ifcopenshell.entity_instance, None]) -> list[dict]:
|
||||
if not placement:
|
||||
return []
|
||||
results = []
|
||||
# NOTE: we ignore subchildren as we already adjust position for their parent
|
||||
# therefore they're not present in `results` and `should_transform_children` should be `True`
|
||||
for referenced_placement in placement.ReferencedByPlacements:
|
||||
matrix = ifcopenshell.util.placement.get_local_placement(referenced_placement)
|
||||
for obj in referenced_placement.PlacesObject:
|
||||
if obj.is_a("IfcDistributionPort"):
|
||||
# Although a port is technically a nested child, it is generally
|
||||
# more intuitive that the ports always move with the parent.
|
||||
continue
|
||||
elif obj.is_a("IfcFeatureElement"):
|
||||
# Feature elements affect the geometry of their parent, and
|
||||
# so logically should always move with the parent. However,
|
||||
# subchildren (fillings) shouldn't move.
|
||||
placement2 = obj.ObjectPlacement
|
||||
for referenced_placement2 in placement2.ReferencedByPlacements:
|
||||
matrix2 = ifcopenshell.util.placement.get_local_placement(referenced_placement2)
|
||||
for obj2 in referenced_placement2.PlacesObject:
|
||||
results.append(
|
||||
{"product": obj2, "matrix": matrix2, "is_si": False, "should_transform_children": True}
|
||||
)
|
||||
continue
|
||||
results.append({"product": obj, "matrix": matrix, "is_si": False, "should_transform_children": True})
|
||||
return results
|
||||
|
||||
def get_relative_placement(
|
||||
self, placement_rel_to: Union[ifcopenshell.entity_instance, None]
|
||||
) -> ifcopenshell.entity_instance:
|
||||
if placement_rel_to:
|
||||
relating_object_matrix = ifcopenshell.util.placement.get_local_placement(placement_rel_to)
|
||||
relating_object_matrix[0][3] = self.convert_unit_to_si(relating_object_matrix[0][3])
|
||||
relating_object_matrix[1][3] = self.convert_unit_to_si(relating_object_matrix[1][3])
|
||||
relating_object_matrix[2][3] = self.convert_unit_to_si(relating_object_matrix[2][3])
|
||||
else:
|
||||
relating_object_matrix = np.eye(4)
|
||||
m = self.settings["matrix"]
|
||||
x = np.array((m[0][0], m[1][0], m[2][0]))
|
||||
z = np.array((m[0][2], m[1][2], m[2][2]))
|
||||
o = np.array((m[0][3], m[1][3], m[2][3]))
|
||||
object_matrix = ifcopenshell.util.placement.a2p(o, z, x)
|
||||
relative_placement_matrix = np.linalg.inv(relating_object_matrix) @ object_matrix
|
||||
return self.builder.create_axis2_placement_3d(
|
||||
self.convert_si_to_unit(relative_placement_matrix[:, 3][0:3]),
|
||||
relative_placement_matrix[:, 2][0:3],
|
||||
relative_placement_matrix[:, 0][0:3],
|
||||
)
|
||||
|
||||
def convert_si_to_unit(self, co: NPArrayOfFloats) -> NPArrayOfFloats:
|
||||
return co / self.unit_scale
|
||||
|
||||
def convert_unit_to_si(self, co: NPArrayOfFloats) -> NPArrayOfFloats:
|
||||
return co * self.unit_scale
|
||||
@@ -0,0 +1,64 @@
|
||||
# IfcOpenShell - IFC toolkit and geometry engine
|
||||
# Copyright (C) 2021 Dion Moult <dion@thinkmoult.com>
|
||||
#
|
||||
# This file is part of IfcOpenShell.
|
||||
#
|
||||
# IfcOpenShell is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Lesser General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# IfcOpenShell is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Lesser General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Lesser General Public License
|
||||
# along with IfcOpenShell. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import ifcopenshell
|
||||
|
||||
|
||||
def map_representation(
|
||||
file: ifcopenshell.file, representation: ifcopenshell.entity_instance
|
||||
) -> ifcopenshell.entity_instance:
|
||||
usecase = Usecase()
|
||||
usecase.file = file
|
||||
return usecase.execute(representation)
|
||||
|
||||
|
||||
class Usecase:
|
||||
file: ifcopenshell.file
|
||||
|
||||
def execute(self, representation: ifcopenshell.entity_instance) -> ifcopenshell.entity_instance:
|
||||
self.representation = representation
|
||||
mapping_source = self.get_mapping_source()
|
||||
|
||||
zero = self.file.createIfcCartesianPoint((0.0, 0.0, 0.0))
|
||||
x_axis = self.file.createIfcDirection((1.0, 0.0, 0.0))
|
||||
y_axis = self.file.createIfcDirection((0.0, 1.0, 0.0))
|
||||
z_axis = self.file.createIfcDirection((0.0, 0.0, 1.0))
|
||||
|
||||
mapping_target = self.file.createIfcCartesianTransformationOperator3D(x_axis, y_axis, zero, 1, z_axis)
|
||||
mapped_item = self.file.createIfcMappedItem(mapping_source, mapping_target)
|
||||
return self.file.create_entity(
|
||||
"IfcShapeRepresentation",
|
||||
**{
|
||||
"ContextOfItems": representation.ContextOfItems,
|
||||
"RepresentationIdentifier": representation.RepresentationIdentifier,
|
||||
"RepresentationType": "MappedRepresentation",
|
||||
"Items": [mapped_item],
|
||||
}
|
||||
)
|
||||
|
||||
def get_mapping_source(self) -> ifcopenshell.entity_instance:
|
||||
for inverse in self.file.get_inverse(self.representation):
|
||||
if inverse.is_a("IfcRepresentationMap"):
|
||||
return inverse
|
||||
zero = self.file.createIfcCartesianPoint((0.0, 0.0, 0.0))
|
||||
x_axis = self.file.createIfcDirection((1.0, 0.0, 0.0))
|
||||
z_axis = self.file.createIfcDirection((0.0, 0.0, 1.0))
|
||||
mapping_origin = self.file.createIfcAxis2Placement3D(zero, z_axis, x_axis)
|
||||
return self.file.createIfcRepresentationMap(
|
||||
MappingOrigin=mapping_origin, MappedRepresentation=self.representation
|
||||
)
|
||||
@@ -0,0 +1,643 @@
|
||||
# IfcOpenShell - IFC toolkit and geometry engine
|
||||
# Copyright (C) 2025 Dion Moult <dion@thinkmoult.com>
|
||||
#
|
||||
# This file is part of IfcOpenShell.
|
||||
#
|
||||
# IfcOpenShell is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Lesser General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# IfcOpenShell is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Lesser General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Lesser General Public License
|
||||
# along with IfcOpenShell. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import json
|
||||
from collections import namedtuple
|
||||
from math import cos, sin
|
||||
from typing import Optional
|
||||
|
||||
import numpy as np
|
||||
|
||||
import ifcopenshell
|
||||
import ifcopenshell.api.context
|
||||
import ifcopenshell.api.geometry
|
||||
import ifcopenshell.util.element
|
||||
import ifcopenshell.util.placement
|
||||
import ifcopenshell.util.representation
|
||||
import ifcopenshell.util.shape_builder
|
||||
import ifcopenshell.util.unit
|
||||
|
||||
# https://stackoverflow.com/a/9184560/9627415
|
||||
# Possible optimisation to linalg.norm?
|
||||
|
||||
PrioritisedLayer = namedtuple("PrioritisedLayer", "priority thickness")
|
||||
|
||||
|
||||
def regenerate_wall_representation(
|
||||
file: ifcopenshell.file,
|
||||
wall: ifcopenshell.entity_instance,
|
||||
length: float = 1.0,
|
||||
height: float = 1.0,
|
||||
angle: Optional[float] = None,
|
||||
) -> ifcopenshell.entity_instance:
|
||||
"""
|
||||
Regenerate the body representation of a wall taking into account connections.
|
||||
|
||||
IFC defines how a standard (case) wall should behave that has a material
|
||||
layer set and connections to other walls using IfcRelConnectsPathElements.
|
||||
This function will regenerate the body geometry of a wall taking into
|
||||
account the notches, butts, mitres, etc in the wall due to connections with
|
||||
other walls.
|
||||
|
||||
A standard wall has a 2D axis line as well as parameters defined in terms
|
||||
of layer thicknesses and priorities. The body geometry is defined as a 2D
|
||||
XY profile which is extruded in the +Z direction. For this function to
|
||||
work, a wall must have these defined and the project must have an axis and
|
||||
body representation context.
|
||||
|
||||
For non-sloped walls, a 2D profile is generated and extruded in the +Z
|
||||
direction. The profile may be a composite profile, if the wall is split due
|
||||
to wall joins along the path of the wall that protrude all the way through
|
||||
the wall.
|
||||
|
||||
For sloped walls, a basic rectangular 2D profile is extruded, and then
|
||||
additional extrusions are generated for each connection that boolean
|
||||
difference the base extrusion.
|
||||
|
||||
Clippings applied via :func:`geometry.clip_solid` or
|
||||
:func:`geometry.clip_solid_bounded` are preserved only if the ``element``
|
||||
parameter was passed when creating them, which registers the result in the
|
||||
``BBIM_Boolean`` property set. Clippings created without that parameter
|
||||
are silently discarded during regeneration.
|
||||
|
||||
This will also update the axis line representation (e.g. trim the axis line
|
||||
to any connections).
|
||||
|
||||
The wall's object placement will also be updated such that the placement is
|
||||
equivalent to the axis line's start point (which therefore becomes (0.0,
|
||||
0.0)). This is a logical, consistent, and useful placement coordinate
|
||||
(especially for apps that can pivot using this point).
|
||||
|
||||
All this functionality relies on the Plan/Axis/GRAPH_VIEW representation
|
||||
context. It will be created if it does not exist.
|
||||
|
||||
:param wall: The IfcWall for the representation,
|
||||
only Model/Body/MODEL_VIEW type of representations are currently supported.
|
||||
:param length: If the wall doesn't have an axis length, this is the default
|
||||
length in SI units.
|
||||
:param height: If the wall doesn't already have a height, this is the
|
||||
default height in SI units.
|
||||
:param angle: If the wall doesn't already have a slope, this is the default
|
||||
angle in radians. Left as none or 0 defines no slope.
|
||||
:return: The newly generated body IfcShapeRepresentation
|
||||
"""
|
||||
return Regenerator(file).regenerate(wall, length=length, height=height, angle=angle)
|
||||
|
||||
|
||||
class Regenerator:
|
||||
def __init__(self, file):
|
||||
self.file = file
|
||||
self.body = ifcopenshell.util.representation.get_context(file, "Model", "Body", "MODEL_VIEW")
|
||||
self.axis = ifcopenshell.util.representation.get_context(file, "Plan", "Axis", "GRAPH_VIEW")
|
||||
self.unit_scale = ifcopenshell.util.unit.calculate_unit_scale(file)
|
||||
self.is_angled = False
|
||||
|
||||
if not self.axis:
|
||||
if not (plan := ifcopenshell.util.representation.get_context(file, "Plan")):
|
||||
plan = ifcopenshell.api.context.add_context(file, context_type="Plan")
|
||||
self.axis = ifcopenshell.api.context.add_context(
|
||||
file, context_type="Plan", context_identifier="Axis", target_view="GRAPH_VIEW", parent=plan
|
||||
)
|
||||
|
||||
def regenerate(self, wall, length=1.0, height=1.0, angle=None):
|
||||
self.fallback_length = length / self.unit_scale
|
||||
self.fallback_height = height / self.unit_scale
|
||||
self.fallback_angle = angle
|
||||
layers = self.get_layers(wall)
|
||||
if not layers:
|
||||
return
|
||||
reference = ifcopenshell.util.representation.get_reference_line(wall, self.fallback_length)
|
||||
self.reference_p1, self.reference_p2 = reference
|
||||
self.wall_vectors = self.get_wall_vectors(wall)
|
||||
axes = self.get_axes(wall, reference, layers, self.wall_vectors["a"])
|
||||
self.miny = axes[0][0][1]
|
||||
self.maxy = axes[-1][0][1]
|
||||
self.end_point = None
|
||||
self.start_points = []
|
||||
self.start_vector = np.array((0.0, 0.0, 1.0))
|
||||
self.start_offset = 0.0
|
||||
self.atpath_points = []
|
||||
self.split_points = []
|
||||
self.maxpath_points = []
|
||||
self.minpath_points = []
|
||||
self.end_points = []
|
||||
self.end_vector = np.array((0.0, 0.0, 1.0))
|
||||
self.end_offset = 0.0
|
||||
manual_booleans = self.get_manual_booleans(wall)
|
||||
|
||||
for rel in wall.ConnectedTo:
|
||||
if rel.is_a("IfcRelConnectsPathElements"):
|
||||
wall2 = rel.RelatedElement
|
||||
layers1 = self.combine_layers(layers.copy(), rel.RelatingPriorities)
|
||||
layers2 = self.combine_layers(self.get_layers(wall2), rel.RelatedPriorities)
|
||||
if not layers1 or not layers2:
|
||||
continue
|
||||
self.join(wall, wall2, layers1, layers2, rel.RelatingConnectionType, rel.RelatedConnectionType)
|
||||
|
||||
for rel in wall.ConnectedFrom:
|
||||
if rel.is_a("IfcRelConnectsPathElements"):
|
||||
wall2 = rel.RelatingElement
|
||||
layers1 = self.combine_layers(layers.copy(), rel.RelatedPriorities)
|
||||
layers2 = self.combine_layers(self.get_layers(wall2), rel.RelatingPriorities)
|
||||
if not layers1 or not layers2:
|
||||
continue
|
||||
self.join(wall, wall2, layers1, layers2, rel.RelatedConnectionType, rel.RelatingConnectionType)
|
||||
|
||||
miny = axes[-2][0][1]
|
||||
maxy = axes[-1][0][1]
|
||||
if not self.start_points:
|
||||
minx = axes[0][0][0]
|
||||
self.start_points = [
|
||||
np.array((minx, axes[0][0][1])),
|
||||
np.array((minx, axes[-1][0][1])),
|
||||
]
|
||||
if not self.end_points:
|
||||
maxx = axes[0][1][0]
|
||||
self.end_points = [
|
||||
np.array((maxx, axes[0][0][1])),
|
||||
np.array((maxx, axes[-1][0][1])),
|
||||
]
|
||||
|
||||
if self.start_points[0][1] > self.start_points[-1][1]: # Canonicalise to the +Y direction
|
||||
self.start_points.reverse()
|
||||
if self.end_points[0][1] > self.end_points[-1][1]: # Canonicalise to the +Y direction
|
||||
self.end_points.reverse()
|
||||
|
||||
builder = ifcopenshell.util.shape_builder.ShapeBuilder(self.file)
|
||||
|
||||
# Don't offset wall if there are manual booleans, because that'll also shift operands
|
||||
offset = None if manual_booleans else self.reference_p1 * -1
|
||||
|
||||
if self.is_angled:
|
||||
start_points = [p.copy() for p in self.start_points]
|
||||
end_points = [p.copy() for p in self.end_points]
|
||||
if self.end_offset > 0:
|
||||
for point in end_points:
|
||||
point[0] += self.end_offset
|
||||
if self.start_offset < 0:
|
||||
for point in start_points:
|
||||
point[0] += self.start_offset
|
||||
points = []
|
||||
points.extend(start_points)
|
||||
end_points.reverse()
|
||||
points.extend(end_points)
|
||||
item = builder.extrude(
|
||||
builder.polyline(points, closed=True, position_offset=offset),
|
||||
magnitude=self.wall_vectors["d"],
|
||||
extrusion_vector=self.wall_vectors["z"],
|
||||
)
|
||||
|
||||
operands = []
|
||||
if not np.allclose(self.start_vector, np.array((0.0, 0.0, 1.0))):
|
||||
points = self.start_points.copy()
|
||||
while ifcopenshell.util.shape_builder.is_x(points[0][1], points[1][1]):
|
||||
points.pop(0)
|
||||
while ifcopenshell.util.shape_builder.is_x(points[-1][1], points[-2][1]):
|
||||
points.pop()
|
||||
newx = min([p[0] for p in points]) - abs(self.start_offset)
|
||||
p1 = points[-1].copy()
|
||||
p1[0] = newx
|
||||
p2 = p1.copy()
|
||||
p2[1] = points[0][1]
|
||||
points.extend((p1, p2))
|
||||
magnitude = np.linalg.norm(self.start_vector * (self.wall_vectors["h"] / self.start_vector[2]))
|
||||
operands.append(
|
||||
builder.extrude(
|
||||
builder.polyline(points, closed=True, position_offset=offset),
|
||||
magnitude=magnitude,
|
||||
extrusion_vector=self.start_vector,
|
||||
)
|
||||
)
|
||||
|
||||
if not np.allclose(self.end_vector, np.array((0.0, 0.0, 1.0))):
|
||||
points = self.end_points.copy()
|
||||
while ifcopenshell.util.shape_builder.is_x(points[0][1], points[1][1]):
|
||||
points.pop(0)
|
||||
while ifcopenshell.util.shape_builder.is_x(points[-1][1], points[-2][1]):
|
||||
points.pop()
|
||||
|
||||
newx = max([p[0] for p in points]) + abs(self.end_offset)
|
||||
p1 = points[-1].copy()
|
||||
p1[0] = newx
|
||||
p2 = p1.copy()
|
||||
p2[1] = points[0][1]
|
||||
points.extend((p1, p2))
|
||||
magnitude = np.linalg.norm(self.end_vector * (self.wall_vectors["h"] / self.end_vector[2]))
|
||||
operands.append(
|
||||
builder.extrude(
|
||||
builder.polyline(points, closed=True, position_offset=offset),
|
||||
magnitude=magnitude,
|
||||
extrusion_vector=self.end_vector,
|
||||
)
|
||||
)
|
||||
|
||||
for atpath_vector, points in self.atpath_points:
|
||||
if len(points) <= 2:
|
||||
continue
|
||||
magnitude = np.linalg.norm(atpath_vector * (self.wall_vectors["h"] / atpath_vector[2]))
|
||||
operands.append(
|
||||
builder.extrude(
|
||||
builder.polyline(points, closed=True, position_offset=offset),
|
||||
magnitude=magnitude,
|
||||
extrusion_vector=atpath_vector,
|
||||
)
|
||||
)
|
||||
|
||||
if operands:
|
||||
item = ifcopenshell.api.geometry.add_boolean(self.file, first_item=item, second_items=operands)[-1]
|
||||
else:
|
||||
# A wall footprint may be multiple profiles if the wall is split into two due to an ATPATH connection
|
||||
profiles = []
|
||||
minx = max([p[0] for p in self.start_points])
|
||||
maxx = min([p[0] for p in self.end_points])
|
||||
split_points = []
|
||||
for points in sorted(self.split_points, key=lambda x: x[0][0]): # Sort islands in the +X direction
|
||||
if any([p[0] > maxx or p[0] < minx for p in points]): # Can't have anything outside our start/end
|
||||
continue
|
||||
split_points.append(points)
|
||||
start_points = [p.copy() for p in self.start_points]
|
||||
end_points = [p.copy() for p in self.end_points]
|
||||
split_points.insert(0, start_points)
|
||||
split_points.append(end_points)
|
||||
split_points = iter(split_points)
|
||||
|
||||
if maxy < miny:
|
||||
self.maxpath_points, self.minpath_points = self.minpath_points, self.maxpath_points
|
||||
if self.maxpath_points:
|
||||
self.maxpath_points[0] = list(reversed(self.maxpath_points[0]))
|
||||
if self.minpath_points:
|
||||
self.minpath_points[0] = list(reversed(self.minpath_points[0]))
|
||||
|
||||
while True:
|
||||
# Draw each profile as clockwise starting from (minx, miny)
|
||||
start_split = next(split_points, None)
|
||||
if not start_split:
|
||||
break
|
||||
end_split = next(split_points, None)
|
||||
if not end_split:
|
||||
break
|
||||
maxy_minx = start_split[-1][0]
|
||||
maxy_maxx = end_split[-1][0]
|
||||
miny_minx = start_split[0][0]
|
||||
miny_maxx = end_split[0][0]
|
||||
# Do more defensive checks here?
|
||||
points = start_split
|
||||
|
||||
remaining_path_points = []
|
||||
for maxpath_points in self.maxpath_points:
|
||||
if maxpath_points[0][0] > maxy_minx and maxpath_points[-1][0] < maxy_maxx:
|
||||
points.extend(maxpath_points)
|
||||
else:
|
||||
remaining_path_points.append(maxpath_points)
|
||||
self.maxpath_points = remaining_path_points
|
||||
|
||||
points.extend(end_split[::-1])
|
||||
|
||||
remaining_path_points = []
|
||||
for minpath_points in self.minpath_points:
|
||||
if minpath_points[0][0] < miny_maxx and minpath_points[-1][0] > miny_minx:
|
||||
points.extend(minpath_points)
|
||||
else:
|
||||
remaining_path_points.append(minpath_points)
|
||||
self.minpath_points = remaining_path_points
|
||||
|
||||
profiles.append(builder.profile(builder.polyline(points, closed=True, position_offset=offset)))
|
||||
|
||||
for points in self.maxpath_points + self.minpath_points:
|
||||
profiles.append(builder.profile(builder.polyline(points, closed=True, position_offset=offset)))
|
||||
|
||||
if len(profiles) > 1:
|
||||
profile = self.file.createIfcCompositeProfileDef("AREA", Profiles=profiles)
|
||||
else:
|
||||
profile = profiles[0]
|
||||
|
||||
item = builder.extrude(profile, magnitude=self.wall_vectors["d"], extrusion_vector=self.wall_vectors["z"])
|
||||
for boolean in self.get_manual_booleans(wall):
|
||||
boolean.FirstOperand = item
|
||||
item = boolean
|
||||
|
||||
body_rep = builder.get_representation(self.body, items=[item])
|
||||
if old_rep := ifcopenshell.util.representation.get_representation(wall, self.body):
|
||||
ifcopenshell.util.element.replace_element(old_rep, body_rep)
|
||||
ifcopenshell.util.element.remove_deep2(self.file, old_rep)
|
||||
else:
|
||||
ifcopenshell.api.geometry.assign_representation(self.file, product=wall, representation=body_rep)
|
||||
|
||||
item = builder.polyline([self.reference_p1, self.reference_p2], position_offset=offset)
|
||||
axis_rep = builder.get_representation(self.axis, items=[item])
|
||||
if old_rep := ifcopenshell.util.representation.get_representation(wall, self.axis):
|
||||
ifcopenshell.util.element.replace_element(old_rep, axis_rep)
|
||||
ifcopenshell.util.element.remove_deep2(self.file, old_rep)
|
||||
else:
|
||||
ifcopenshell.api.geometry.assign_representation(self.file, product=wall, representation=axis_rep)
|
||||
|
||||
if not np.allclose(self.reference_p1, np.array((0.0, 0.0))) and not manual_booleans:
|
||||
children = []
|
||||
for referenced_placement in wall.ObjectPlacement.ReferencedByPlacements:
|
||||
matrix = ifcopenshell.util.placement.get_local_placement(referenced_placement)
|
||||
children.append((matrix, referenced_placement.PlacesObject))
|
||||
|
||||
matrix = ifcopenshell.util.placement.get_local_placement(wall.ObjectPlacement)
|
||||
matrix[:, 3] = matrix @ np.concatenate((self.reference_p1, (0, 1)))
|
||||
ifcopenshell.api.geometry.edit_object_placement(
|
||||
self.file, product=wall, matrix=matrix, is_si=False, should_transform_children=True
|
||||
)
|
||||
|
||||
# Restore children to their previous location
|
||||
for matrix, elements in children:
|
||||
for element in elements:
|
||||
ifcopenshell.api.geometry.edit_object_placement(
|
||||
self.file, product=element, matrix=matrix, is_si=False, should_transform_children=True
|
||||
)
|
||||
|
||||
return body_rep
|
||||
|
||||
def join(self, wall1, wall2, layers1, layers2, connection1, connection2):
|
||||
if connection1 == "NOTDEFINED" or connection2 == "NOTDEFINED":
|
||||
return
|
||||
if connection1 == "ATPATH" and connection2 == "ATPATH":
|
||||
return
|
||||
|
||||
reference1 = ifcopenshell.util.representation.get_reference_line(wall1, self.fallback_length)
|
||||
reference2 = ifcopenshell.util.representation.get_reference_line(wall2, self.fallback_length)
|
||||
wall_vectors2 = self.get_wall_vectors(wall2)
|
||||
axes1 = self.get_axes(wall1, reference1, layers1, self.wall_vectors["a"])
|
||||
axes2 = self.get_axes(wall2, reference2, layers2, wall_vectors2["a"])
|
||||
matrix1i = np.linalg.inv(ifcopenshell.util.placement.get_local_placement(wall1.ObjectPlacement))
|
||||
matrix2 = ifcopenshell.util.placement.get_local_placement(wall2.ObjectPlacement)
|
||||
|
||||
# Convert wall2 data to wall1 local coordinates
|
||||
for axis in axes2:
|
||||
axis[0] = (matrix1i @ matrix2 @ np.concatenate((axis[0], (0, 1))))[:2]
|
||||
axis[1] = (matrix1i @ matrix2 @ np.concatenate((axis[1], (0, 1))))[:2]
|
||||
reference2[0] = (matrix1i @ matrix2 @ np.concatenate((reference2[0], (0, 1))))[:2]
|
||||
reference2[1] = (matrix1i @ matrix2 @ np.concatenate((reference2[1], (0, 1))))[:2]
|
||||
wall_vectors2["z"] = (matrix1i @ matrix2 @ np.append(wall_vectors2["z"], 0.0))[:3]
|
||||
wall_vectors2["y"] = (matrix1i @ matrix2 @ np.append(wall_vectors2["y"], 0.0))[:3]
|
||||
|
||||
axis2 = axes2[0] # Take an arbitrary axis of wall2
|
||||
if ifcopenshell.util.shape_builder.is_x(axis2[0][1], axis2[1][1]):
|
||||
return # Parallel
|
||||
|
||||
# Sort axes from interior to exterior
|
||||
if connection1 == "ATEND":
|
||||
if axes2[0][0][0] > axes2[-1][0][0]: # We process layers in a +X direction
|
||||
axes2 = list(reversed(axes2))
|
||||
layers2 = list(reversed(layers2))
|
||||
elif connection1 == "ATSTART":
|
||||
if axes2[-1][0][0] > axes2[0][0][0]: # We process layers in a -X direction
|
||||
axes2 = list(reversed(axes2))
|
||||
layers2 = list(reversed(layers2))
|
||||
|
||||
axis2 = axes2[0] # Take an arbitrary axis of wall2
|
||||
if connection2 == "ATSTART":
|
||||
axis2 = [axis2[1], axis2[0]] # Flip direction so the axis "points" in the direction of join
|
||||
if axis2[0][1] < axis2[1][1]: # Pointing +Y
|
||||
if axes1[-1][0][1] < axes1[0][0][1]: # We process layers1 in a +Y direction
|
||||
axes1 = list(reversed(axes1))
|
||||
layers1 = list(reversed(layers1))
|
||||
else: # Pointing -Y
|
||||
if axes1[0][0][1] < axes1[-1][0][1]: # We process layers1 in a -Y direction
|
||||
axes1 = list(reversed(axes1))
|
||||
layers1 = list(reversed(layers1))
|
||||
|
||||
if connection1 == "ATPATH":
|
||||
first_axis2 = axes2[0]
|
||||
last_axis2 = axes2[-1]
|
||||
first_y = axes1[0][0][1]
|
||||
last_y = axes1[-1][0][1]
|
||||
p0 = np.array((ifcopenshell.util.shape_builder.intersect_x_axis_2d(*first_axis2, y=first_y), first_y))
|
||||
pN = np.array((ifcopenshell.util.shape_builder.intersect_x_axis_2d(*last_axis2, y=first_y), first_y))
|
||||
|
||||
# Generate CurveOnRelating/RelatedElement
|
||||
points = [p0]
|
||||
axes2 = iter(axes2)
|
||||
axis2 = next(axes2)
|
||||
for layer2 in layers2:
|
||||
ys = iter([a[0][1] for a in axes1])
|
||||
y = next(ys)
|
||||
for layer1 in layers1:
|
||||
if layer2.priority <= layer1.priority:
|
||||
break
|
||||
y = next(ys)
|
||||
p1 = np.array((ifcopenshell.util.shape_builder.intersect_x_axis_2d(*axis2, y=y), y))
|
||||
axis2 = next(axes2)
|
||||
p2 = np.array((ifcopenshell.util.shape_builder.intersect_x_axis_2d(*axis2, y=y), y))
|
||||
if points and np.allclose(points[-1], p1):
|
||||
points[-1] = p2 # Just slide along previous point
|
||||
else:
|
||||
points.extend((p1, p2))
|
||||
|
||||
# The curve must end at pN
|
||||
if not np.allclose(points[-1], pN):
|
||||
points.append(pN)
|
||||
|
||||
# Categorise our points into a segment that either splits or cuts the wall
|
||||
split_ys = {first_y, last_y}
|
||||
segment = []
|
||||
atpath_vector = self.get_join_vector(self.wall_vectors["y"], wall_vectors2["y"])
|
||||
self.atpath_points.append((atpath_vector, points))
|
||||
for point in points:
|
||||
segment.append(point)
|
||||
if len(segment) == 1: # Not enough points to categorise the segment
|
||||
continue
|
||||
elif {segment[0][1], segment[-1][1]} == split_ys: # This segment splits the wall
|
||||
if segment[0][1] > segment[-1][1]: # Go in the +Y direction
|
||||
segment.reverse()
|
||||
self.split_points.append(segment)
|
||||
segment = []
|
||||
elif segment[0][1] == segment[-1][1]: # This segment cuts some of the wall
|
||||
if segment[0][1] == self.maxy: # Go in the +X direction
|
||||
if segment[0][0] > segment[-1][0]:
|
||||
segment.reverse()
|
||||
self.maxpath_points.append(segment)
|
||||
elif segment[0][1] == self.miny: # Go in the -X direction
|
||||
if segment[-1][0] > segment[0][0]:
|
||||
segment.reverse()
|
||||
self.minpath_points.append(segment)
|
||||
segment = []
|
||||
elif connection2 == "ATPATH":
|
||||
points = []
|
||||
ys = iter([a[0][1] for a in axes1])
|
||||
y = next(ys)
|
||||
for layer1 in layers1:
|
||||
axes2_iter = iter(axes2)
|
||||
axis2 = next(axes2_iter)
|
||||
for layer2 in layers2:
|
||||
if layer1.priority <= layer2.priority:
|
||||
break
|
||||
axis2 = next(axes2_iter)
|
||||
x = ifcopenshell.util.shape_builder.intersect_x_axis_2d(*axis2, y=y)
|
||||
p1 = np.array((x, y))
|
||||
y = next(ys)
|
||||
x = ifcopenshell.util.shape_builder.intersect_x_axis_2d(*axis2, y=y)
|
||||
p2 = np.array((x, y))
|
||||
if points and np.allclose(points[-1], p1):
|
||||
points.append(p2)
|
||||
else:
|
||||
points.extend((p1, p2))
|
||||
|
||||
if connection1 == "ATSTART":
|
||||
self.start_points = points
|
||||
self.start_vector = self.get_join_vector(self.wall_vectors["y"], wall_vectors2["y"])
|
||||
self.start_offset = (self.start_vector * (self.wall_vectors["h"] / self.start_vector[2]))[0]
|
||||
self.reference_p1[0] = ifcopenshell.util.shape_builder.intersect_x_axis_2d(
|
||||
*reference2, y=reference1[0][1]
|
||||
)
|
||||
elif connection1 == "ATEND":
|
||||
self.end_points = points
|
||||
self.end_vector = self.get_join_vector(self.wall_vectors["y"], wall_vectors2["y"])
|
||||
self.end_offset = (self.end_vector * (self.wall_vectors["h"] / self.end_vector[2]))[0]
|
||||
self.reference_p2[0] = ifcopenshell.util.shape_builder.intersect_x_axis_2d(
|
||||
*reference2, y=reference1[0][1]
|
||||
)
|
||||
else: # A connection at either end of both walls
|
||||
last_y = axes1[-1][0][1]
|
||||
ys = iter([a[0][1] for a in axes1])
|
||||
|
||||
last_axis2 = axes2[-1]
|
||||
axes2 = iter(axes2)
|
||||
axis2 = next(axes2)
|
||||
y = next(ys)
|
||||
x = ifcopenshell.util.shape_builder.intersect_x_axis_2d(*axis2, y=y)
|
||||
points = [np.array((x, y))]
|
||||
|
||||
layers1 = iter(layers1)
|
||||
layers2 = iter(layers2)
|
||||
layer1 = next(layers1, None)
|
||||
layer2 = next(layers2, None)
|
||||
|
||||
# This creates "mitering" behaviour which is an ambiguity by bSI.
|
||||
while layer1 and layer2:
|
||||
if layer1.priority > layer2.priority:
|
||||
axis2 = next(axes2)
|
||||
x = ifcopenshell.util.shape_builder.intersect_x_axis_2d(*axis2, y=y)
|
||||
layer2 = next(layers2, None)
|
||||
elif layer2.priority > layer1.priority:
|
||||
y = next(ys)
|
||||
x = ifcopenshell.util.shape_builder.intersect_x_axis_2d(*axis2, y=y)
|
||||
layer1 = next(layers1, None)
|
||||
else:
|
||||
y = next(ys)
|
||||
x = ifcopenshell.util.shape_builder.intersect_x_axis_2d(*next(axes2), y=y)
|
||||
layer1 = next(layers1, None)
|
||||
layer2 = next(layers2, None)
|
||||
points.append(np.array((x, y)))
|
||||
|
||||
if points[-1][1] != last_y:
|
||||
points.append(
|
||||
np.array((ifcopenshell.util.shape_builder.intersect_x_axis_2d(*last_axis2, y=last_y), last_y))
|
||||
)
|
||||
|
||||
if connection1 == "ATSTART":
|
||||
self.start_points = points
|
||||
self.start_vector = self.get_join_vector(self.wall_vectors["y"], wall_vectors2["y"])
|
||||
self.start_offset = (self.start_vector * (self.wall_vectors["h"] / self.start_vector[2]))[0]
|
||||
self.reference_p1[0] = ifcopenshell.util.shape_builder.intersect_x_axis_2d(
|
||||
*reference2, y=reference1[0][1]
|
||||
)
|
||||
elif connection1 == "ATEND":
|
||||
self.end_points = points
|
||||
self.end_vector = self.get_join_vector(self.wall_vectors["y"], wall_vectors2["y"])
|
||||
self.end_offset = (self.end_vector * (self.wall_vectors["h"] / self.end_vector[2]))[0]
|
||||
self.reference_p2[0] = ifcopenshell.util.shape_builder.intersect_x_axis_2d(
|
||||
*reference2, y=reference1[0][1]
|
||||
)
|
||||
|
||||
def get_layers(self, wall) -> list:
|
||||
material = ifcopenshell.util.element.get_material(wall, should_skip_usage=True)
|
||||
if not material or not material.is_a("IfcMaterialLayerSet"):
|
||||
return []
|
||||
return [PrioritisedLayer(getattr(l, "Priority", 0) or 0, l.LayerThickness) for l in material.MaterialLayers]
|
||||
|
||||
def combine_layers(self, layers, override_priorities):
|
||||
results = []
|
||||
if override_priorities:
|
||||
for i, priority in enumerate(override_priorities[: len(layers)]):
|
||||
layers[i][0] = priority
|
||||
if not layers:
|
||||
return []
|
||||
results = [layers.pop(0)]
|
||||
for layer in layers:
|
||||
if not layer.thickness:
|
||||
continue
|
||||
if layer.priority == results[-1].priority:
|
||||
results[-1] = PrioritisedLayer(layer.priority, results[-1].thickness + layer.thickness)
|
||||
else:
|
||||
results.append(layer)
|
||||
return results
|
||||
|
||||
def get_wall_vectors(self, wall):
|
||||
if body := ifcopenshell.util.representation.get_representation(wall, "Model", "Body", "MODEL_VIEW"):
|
||||
for item in ifcopenshell.util.representation.resolve_representation(body).Items:
|
||||
while item.is_a("IfcBooleanResult"):
|
||||
item = item.FirstOperand
|
||||
if item.is_a("IfcExtrudedAreaSolid"):
|
||||
z = np.array(item.ExtrudedDirection.DirectionRatios)
|
||||
z /= np.linalg.norm(z)
|
||||
y = np.cross(z, np.array((1.0, 0.0, 0.0)))
|
||||
d = item.Depth
|
||||
h = (z * d)[2]
|
||||
a = ifcopenshell.util.shape_builder.np_angle_signed(np.array((0.0, 1.0)), z[1:])
|
||||
if not ifcopenshell.util.shape_builder.is_x(a, 0):
|
||||
self.is_angled = True
|
||||
return {"z": z, "y": y, "a": a, "d": d, "h": h}
|
||||
elif self.fallback_angle:
|
||||
a = self.fallback_angle
|
||||
z = np.array([0.0, sin(a), cos(a)])
|
||||
y = np.cross(z, np.array((1.0, 0.0, 0.0)))
|
||||
h = self.fallback_height
|
||||
d = np.linalg.norm(z * (h / z[2]))
|
||||
if not ifcopenshell.util.shape_builder.is_x(a, 0):
|
||||
self.is_angled = True
|
||||
return {"z": z, "y": y, "a": a, "d": d, "h": h}
|
||||
return {
|
||||
"z": np.array((0.0, 0.0, 1.0)),
|
||||
"y": np.array((0.0, 1.0, 0.0)),
|
||||
"a": 0.0,
|
||||
"d": self.fallback_height,
|
||||
"h": self.fallback_height,
|
||||
}
|
||||
|
||||
def get_join_vector(self, y1, y2):
|
||||
result = np.cross(y1, y2)
|
||||
if result[2] < 0:
|
||||
return result * -1
|
||||
return result
|
||||
|
||||
def get_axes(self, wall: ifcopenshell.entity_instance, reference, layers: list[PrioritisedLayer], angle: float):
|
||||
axes = [[p.copy() for p in reference]]
|
||||
# Apply usage to convert the Reference line into MlsBase
|
||||
sense_factor = 1
|
||||
if (usage := ifcopenshell.util.element.get_material(wall)) and usage.is_a("IfcMaterialLayerSetUsage"):
|
||||
for point in axes[0]:
|
||||
point[1] += usage.OffsetFromReferenceLine
|
||||
sense_factor = 1 if usage.DirectionSense == "POSITIVE" else -1
|
||||
|
||||
for layer in layers:
|
||||
y_offset = (layer.thickness * sense_factor) / cos(angle)
|
||||
axes.append([p.copy() + np.array((0.0, y_offset)) for p in axes[-1]])
|
||||
return axes
|
||||
|
||||
def get_manual_booleans(self, element: ifcopenshell.entity_instance):
|
||||
if pset := ifcopenshell.util.element.get_pset(element, "BBIM_Boolean"):
|
||||
try:
|
||||
return [self.file.by_id(boolean_id) for boolean_id in json.loads(pset["Data"])]
|
||||
except:
|
||||
return []
|
||||
return []
|
||||
@@ -0,0 +1,61 @@
|
||||
# IfcOpenShell - IFC toolkit and geometry engine
|
||||
# Copyright (C) 2022 Dion Moult <dion@thinkmoult.com>
|
||||
#
|
||||
# This file is part of IfcOpenShell.
|
||||
#
|
||||
# IfcOpenShell is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Lesser General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# IfcOpenShell is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Lesser General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Lesser General Public License
|
||||
# along with IfcOpenShell. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import ifcopenshell.util.element
|
||||
|
||||
|
||||
def remove_boolean(file: ifcopenshell.file, item: ifcopenshell.entity_instance) -> None:
|
||||
"""Removes a boolean operation without deleting the operands
|
||||
|
||||
The first operand will replace the boolean result itself, and the second
|
||||
operand will be reset as a top level representation item.
|
||||
|
||||
This may affect the Items of IfcShapeRepresentation, so it is recommended
|
||||
to run :func:`ifcopenshell.api.geometry.validate_type` after all boolean
|
||||
modifications are complete.
|
||||
|
||||
:param item: This may either be an IfcBooleanResult or an
|
||||
IfcRepresentationItem that is participating in one or more boolean
|
||||
results (in which case all are removed).
|
||||
"""
|
||||
if not item.is_a("IfcBooleanResult"):
|
||||
for inverse in file.get_inverse(item):
|
||||
if inverse.is_a("IfcBooleanResult"):
|
||||
remove_boolean(file, inverse)
|
||||
return
|
||||
|
||||
representations = []
|
||||
queue = list(file.get_inverse(item))
|
||||
while queue:
|
||||
inverse = queue.pop()
|
||||
if inverse.is_a("IfcShapeRepresentation"):
|
||||
representations.append(inverse)
|
||||
elif inverse.is_a("IfcBooleanResult"):
|
||||
queue.extend(file.get_inverse(inverse))
|
||||
elif inverse.is_a("IfcCsgSolid"):
|
||||
queue.extend(file.get_inverse(inverse))
|
||||
|
||||
first = item.FirstOperand
|
||||
second = item.SecondOperand
|
||||
for inverse in file.get_inverse(item):
|
||||
ifcopenshell.util.element.replace_attribute(inverse, item, first)
|
||||
|
||||
for representation in set(representations):
|
||||
representation.Items = list(representation.Items) + [second]
|
||||
|
||||
file.remove(item)
|
||||
@@ -0,0 +1,92 @@
|
||||
# IfcOpenShell - IFC toolkit and geometry engine
|
||||
# Copyright (C) 2021 Dion Moult <dion@thinkmoult.com>
|
||||
#
|
||||
# This file is part of IfcOpenShell.
|
||||
#
|
||||
# IfcOpenShell is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Lesser General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# IfcOpenShell is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Lesser General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Lesser General Public License
|
||||
# along with IfcOpenShell. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import ifcopenshell.util.element
|
||||
|
||||
|
||||
def remove_representation(
|
||||
file: ifcopenshell.file, representation: ifcopenshell.entity_instance, should_keep_named_profiles: bool = True
|
||||
) -> None:
|
||||
"""Remove a representation.
|
||||
|
||||
Also purges representation items and their related elements
|
||||
like IfcStyledItem, tessellated facesets colours and UV map.
|
||||
|
||||
By default, named profiles are assumed to be significant (i.e. curated as
|
||||
part of a profile library) and will not be removed.
|
||||
|
||||
:param representation: IfcRepresentation to remove.
|
||||
Note that it's expected that IfcRepresentation won't be in use
|
||||
before calling this method (in such elements as IfcProductRepresentation, IfcShapeAspect)
|
||||
otherwise representation won't be removed.
|
||||
:param should_keep_named_profiles: If true, named profile defs will not be
|
||||
removed as they are assumed to be significant.
|
||||
"""
|
||||
is_ifc2x3 = file.schema == "IFC2X3"
|
||||
styled_items = set()
|
||||
presentation_layer_assignments_items: set[ifcopenshell.entity_instance] = set()
|
||||
presentation_layer_assignments_reps: set[ifcopenshell.entity_instance] = set()
|
||||
textures: set[ifcopenshell.entity_instance] = set()
|
||||
colours: set[ifcopenshell.entity_instance] = set()
|
||||
named_profiles: set[ifcopenshell.entity_instance] = set()
|
||||
for subelement in file.traverse(representation):
|
||||
if subelement.is_a("IfcRepresentationItem"):
|
||||
[styled_items.add(s) for s in subelement.StyledByItem or []]
|
||||
# IFC2X3 is using LayerAssignments
|
||||
for s in subelement.LayerAssignment if not is_ifc2x3 else subelement.LayerAssignments:
|
||||
presentation_layer_assignments_items.add(s)
|
||||
# IfcTessellatedFaceSet inverses
|
||||
if subelement.is_a("IfcTessellatedFaceSet"):
|
||||
textures.update(subelement.HasTextures)
|
||||
colours.update(subelement.HasColours)
|
||||
elif subelement.is_a("IfcRepresentation"):
|
||||
for layer in subelement.LayerAssignments:
|
||||
presentation_layer_assignments_reps.add(layer)
|
||||
elif subelement.is_a("IfcProfileDef") and subelement.ProfileName:
|
||||
named_profiles.add(subelement)
|
||||
|
||||
do_not_delete = file.by_type("IfcGeometricRepresentationContext")
|
||||
if should_keep_named_profiles:
|
||||
do_not_delete += named_profiles
|
||||
|
||||
# Order matters - layer assignments may reference representation directly.
|
||||
also_consider = list(presentation_layer_assignments_reps)
|
||||
also_consider.extend(presentation_layer_assignments_items - presentation_layer_assignments_reps)
|
||||
also_consider.extend(styled_items)
|
||||
also_consider.extend(textures)
|
||||
ifcopenshell.util.element.remove_deep2(
|
||||
file,
|
||||
representation,
|
||||
also_consider=also_consider,
|
||||
do_not_delete=set(do_not_delete),
|
||||
)
|
||||
|
||||
for texture in textures:
|
||||
ifcopenshell.util.element.remove_deep2(file, texture)
|
||||
for colour in colours:
|
||||
ifcopenshell.util.element.remove_deep2(file, colour)
|
||||
|
||||
to_delete = file.to_delete or set()
|
||||
for element in styled_items:
|
||||
item = element.Item
|
||||
if not item or item in to_delete:
|
||||
file.remove(element)
|
||||
presentation_layer_assignments = presentation_layer_assignments_reps | presentation_layer_assignments_items
|
||||
for element in presentation_layer_assignments:
|
||||
if all(item in to_delete for item in element.AssignedItems):
|
||||
file.remove(element)
|
||||
@@ -0,0 +1,113 @@
|
||||
# IfcOpenShell - IFC toolkit and geometry engine
|
||||
# Copyright (C) 2021 Dion Moult <dion@thinkmoult.com>
|
||||
#
|
||||
# This file is part of IfcOpenShell.
|
||||
#
|
||||
# IfcOpenShell is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Lesser General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# IfcOpenShell is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Lesser General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Lesser General Public License
|
||||
# along with IfcOpenShell. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from typing import Any
|
||||
|
||||
import ifcopenshell.api.geometry
|
||||
import ifcopenshell.util.element
|
||||
|
||||
|
||||
def unassign_representation(
|
||||
file: ifcopenshell.file, product: ifcopenshell.entity_instance, representation: ifcopenshell.entity_instance
|
||||
) -> None:
|
||||
usecase = Usecase()
|
||||
usecase.file = file
|
||||
usecase.settings = {"product": product, "representation": representation}
|
||||
return usecase.execute()
|
||||
|
||||
|
||||
class Usecase:
|
||||
file: ifcopenshell.file
|
||||
settings: dict[str, Any]
|
||||
|
||||
def execute(self) -> None:
|
||||
product: ifcopenshell.entity_instance = self.settings["product"]
|
||||
representation: ifcopenshell.entity_instance = self.settings["representation"]
|
||||
if product.is_a("IfcProduct"):
|
||||
self.unassign_product_representation(product, representation)
|
||||
elif product.is_a("IfcTypeProduct"):
|
||||
self.unassign_type_representation()
|
||||
|
||||
def unassign_product_representation(
|
||||
self, product: ifcopenshell.entity_instance, representation: ifcopenshell.entity_instance
|
||||
) -> None:
|
||||
representations = list(product.Representation.Representations or [])
|
||||
if representation not in representations:
|
||||
return
|
||||
representations.remove(representation)
|
||||
if not representations:
|
||||
product_def = product.Representation
|
||||
# TODO: should somehow find matching shape aspect and remove it
|
||||
# even before the last representation is removed.
|
||||
self.process_shape_aspects(product_def)
|
||||
self.file.remove(product_def)
|
||||
else:
|
||||
product.Representation.Representations = representations
|
||||
|
||||
def unassign_type_representation(self) -> None:
|
||||
matching_representation_map = None
|
||||
|
||||
for representation_map in self.settings["product"].RepresentationMaps or []:
|
||||
if representation_map.MappedRepresentation == self.settings["representation"]:
|
||||
matching_representation_map = representation_map
|
||||
break
|
||||
|
||||
if matching_representation_map:
|
||||
self.unassign_products_using_mapped_representation(matching_representation_map)
|
||||
self.settings["product"].RepresentationMaps = [
|
||||
rm for rm in self.settings["product"].RepresentationMaps if rm != matching_representation_map
|
||||
] or None
|
||||
self.process_shape_aspects(matching_representation_map)
|
||||
self.remove_representation_map_only(matching_representation_map)
|
||||
|
||||
def process_shape_aspects(self, product_representation: ifcopenshell.entity_instance) -> None:
|
||||
# Technically IfcShapeAspect doesn't become invalid when product representation is removed,
|
||||
# but shape aspect makes sense only in context of some other representation.
|
||||
if self.file.schema == "IFC2X3" and product_representation.is_a("IfcRepresentationMap"):
|
||||
shape_aspects = [
|
||||
a
|
||||
for a in self.file.by_type("IfcShapeAspect")
|
||||
if a.PartOfProductDefinitionShape == product_representation
|
||||
]
|
||||
else:
|
||||
shape_aspects = product_representation.HasShapeAspects
|
||||
for shape_aspect in shape_aspects:
|
||||
representations = shape_aspect.ShapeRepresentations
|
||||
self.file.remove(shape_aspect)
|
||||
for rep in representations:
|
||||
ifcopenshell.api.geometry.remove_representation(self.file, rep)
|
||||
|
||||
def remove_representation_map_only(self, representation_map: ifcopenshell.entity_instance) -> None:
|
||||
representation_map.MappedRepresentation = self.file.createIfcShapeRepresentation()
|
||||
ifcopenshell.util.element.remove_deep2(self.file, representation_map)
|
||||
|
||||
def unassign_products_using_mapped_representation(self, representation_map: ifcopenshell.entity_instance) -> None:
|
||||
mapped_representations: list[dict[str, ifcopenshell.entity_instance]] = []
|
||||
just_representations: list[ifcopenshell.entity_instance] = []
|
||||
for map_usage in representation_map.MapUsage or []:
|
||||
for inverse in self.file.get_inverse(map_usage):
|
||||
if not inverse.is_a("IfcShapeRepresentation"):
|
||||
continue
|
||||
for definition in inverse.OfProductRepresentation or []:
|
||||
for product in definition.ShapeOfProduct or []:
|
||||
mapped_representations.append({"product": product, "representation": inverse})
|
||||
just_representations.append(inverse)
|
||||
for item in mapped_representations:
|
||||
self.unassign_product_representation(item["product"], item["representation"])
|
||||
for representation in just_representations:
|
||||
ifcopenshell.api.geometry.remove_representation(self.file, representation=representation)
|
||||
@@ -0,0 +1,91 @@
|
||||
# IfcOpenShell - IFC toolkit and geometry engine
|
||||
# Copyright (C) 2025 Dion Moult <dion@thinkmoult.com>
|
||||
#
|
||||
# This file is part of IfcOpenShell.
|
||||
#
|
||||
# IfcOpenShell is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Lesser General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# IfcOpenShell is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Lesser General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Lesser General Public License
|
||||
# along with IfcOpenShell. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from typing import Union
|
||||
|
||||
import ifcopenshell.api.geometry
|
||||
import ifcopenshell.util.representation
|
||||
|
||||
|
||||
def validate_type(
|
||||
file: ifcopenshell.file,
|
||||
representation: ifcopenshell.entity_instance,
|
||||
preferred_item: Union[ifcopenshell.entity_instance, None] = None,
|
||||
) -> bool:
|
||||
"""Validates the RepresentationType of an IfcShapeRepresentation
|
||||
|
||||
A shape representation has to identify its geometry using the
|
||||
RepresentationType attribute. For example, if it holds tessellated
|
||||
geometry, it should store "Tessellation" as its RepresentationType.
|
||||
|
||||
This function checks whether or not the RepresentationType is valid. This
|
||||
is a wrapper around :func:`ifcopenshell.util.representation.guess_type`. It
|
||||
will then set RepresentationType to the most appropriate value, or return
|
||||
False otherwise. In addition, it also attempts to reconcile otherwise
|
||||
invalid CSG geometry by unioning all remaining top level items to existing
|
||||
boolean results.
|
||||
|
||||
:param representation: The IfcShapeRepresentation with Items
|
||||
:param preferred_item: If the type is expected to be a CSG, this will be
|
||||
the preferred item to union all remaining items to. If no preferred
|
||||
item is provided, the first boolean result will be chosen.
|
||||
:return: True if the representation type was set and it is a valid
|
||||
combination, or False otherwise.
|
||||
"""
|
||||
|
||||
def is_operand(item: ifcopenshell.entity_instance) -> bool:
|
||||
return (
|
||||
item.is_a("IfcBooleanResult")
|
||||
or item.is_a("IfcCsgPrimitive3D")
|
||||
or item.is_a("IfcHalfSpaceSolid")
|
||||
or item.is_a("IfcSolidModel")
|
||||
or item.is_a("IfcTessellatedFaceSet")
|
||||
)
|
||||
|
||||
has_boolean = False
|
||||
remaining_items = []
|
||||
for item in representation.Items:
|
||||
if item.is_a("IfcBooleanResult"):
|
||||
has_boolean = True
|
||||
if item != preferred_item and is_operand(item):
|
||||
remaining_items.append(item)
|
||||
|
||||
if not has_boolean:
|
||||
result = ifcopenshell.util.representation.guess_type(representation.Items)
|
||||
if result:
|
||||
representation.RepresentationType = result
|
||||
return True
|
||||
return False
|
||||
|
||||
if not preferred_item:
|
||||
# Prioritise an existing boolean result
|
||||
for i in remaining_items:
|
||||
if i.is_a("IfcBooleanResult"):
|
||||
preferred_item = i
|
||||
break
|
||||
if not preferred_item and remaining_items:
|
||||
preferred_item = remaining_items[0]
|
||||
|
||||
if remaining_items:
|
||||
ifcopenshell.api.geometry.add_boolean(file, preferred_item, remaining_items, "UNION")
|
||||
representation.Items = [i for i in representation.Items if i not in remaining_items]
|
||||
|
||||
representation.RepresentationType = ifcopenshell.util.representation.guess_type(representation.Items)
|
||||
if representation.RepresentationType == "CSG":
|
||||
return True
|
||||
return False
|
||||
Reference in New Issue
Block a user