Fix #4057. Data objects for the API are now in util.data instead of mixed into the shape builder utility and only depends on numpy.

This commit is contained in:
Dion Moult
2023-11-29 14:35:46 +11:00
parent a2a7d56691
commit f071c5746b
5 changed files with 107 additions and 63 deletions
@@ -18,7 +18,7 @@
import ifcopenshell.geom
import ifcopenshell.util.unit
from ifcopenshell.util.shape_builder import ClippingInfo
from ifcopenshell.util.data import Clipping
class Usecase:
@@ -29,8 +29,8 @@ class Usecase:
"profile": None,
"depth": 1.0,
"cardinal_point": 5,
# Planes are defined either by ClippingInfo objects
# or by dictionaries of arguments for `ClippingInfo.parse`
# Planes are defined either by Clipping objects
# or by dictionaries of arguments for `Clipping.parse`
"clippings": [], # A list of planes that define clipping half space solids
"placement_zx_axes": (None, None),
}
@@ -39,7 +39,7 @@ class Usecase:
def execute(self):
self.settings["unit_scale"] = ifcopenshell.util.unit.calculate_unit_scale(self.file)
self.settings["clippings"] = [ClippingInfo.parse(c) for c in self.settings["clippings"]]
self.settings["clippings"] = [Clipping.parse(c) for c in self.settings["clippings"]]
return self.file.createIfcShapeRepresentation(
self.settings["context"],
self.settings["context"].ContextIdentifier,
@@ -71,7 +71,7 @@ class Usecase:
new = ifcopenshell.util.element.copy(self.file, clipping)
new.FirstOperand = first_operand
first_operand = new
else: # ClippingInfo
else: # Clipping
first_operand = clipping.apply(self.file, first_operand, self.settings["unit_scale"])
return first_operand
@@ -27,8 +27,8 @@ class Usecase:
"context": None, # IfcGeometricRepresentationContext
"depth": 0.2,
"x_angle": 0, # Radians
# Planes are defined either by ClippingInfo objects
# or by dictionaries of arguments for `ClippingInfo.parse`
# Planes are defined either by Clipping objects
# or by dictionaries of arguments for `Clipping.parse`
"clippings": [], # A list of planes that define clipping half space solids
}
for key, value in settings.items():
@@ -83,7 +83,7 @@ class Usecase:
new = ifcopenshell.util.element.copy(self.file, clipping)
new.FirstOperand = first_operand
first_operand = new
else: # ClippingInfo
else: # Clipping
first_operand = clipping.apply(self.file, first_operand, self.settings["unit_scale"])
return first_operand
@@ -18,7 +18,7 @@
import ifcopenshell.util.unit
from math import sin, cos
from ifcopenshell.util.shape_builder import ClippingInfo
from ifcopenshell.util.data import Clipping
class Usecase:
@@ -32,8 +32,8 @@ class Usecase:
"thickness": 0.2,
# Sloped walls along the wall's X axis, provided in radians
"x_angle": 0,
# Planes are defined either by ClippingInfo objects
# or by dictionaries of arguments for `ClippingInfo.parse`
# Planes are defined either by Clipping objects
# or by dictionaries of arguments for `Clipping.parse`
"clippings": [], # A list of planes that define clipping half space solids
"booleans": [], # Any existing IfcBooleanResults
}
@@ -42,7 +42,7 @@ class Usecase:
def execute(self):
self.settings["unit_scale"] = ifcopenshell.util.unit.calculate_unit_scale(self.file)
self.settings["clippings"] = [ClippingInfo.parse(c) for c in self.settings["clippings"]]
self.settings["clippings"] = [Clipping.parse(c) for c in self.settings["clippings"]]
return self.file.createIfcShapeRepresentation(
self.settings["context"],
self.settings["context"].ContextIdentifier,
@@ -101,7 +101,7 @@ class Usecase:
new = ifcopenshell.util.element.copy(self.file, clipping)
new.FirstOperand = first_operand
first_operand = new
else: # ClippingInfo
else: # Clipping
first_operand = clipping.apply(self.file, first_operand, self.settings["unit_scale"])
return first_operand
@@ -0,0 +1,93 @@
# IfcOpenShell - IFC toolkit and geometry engine
# Copyright (C) 2023 Dion Moult <dion@thinkmoult.com>, @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 numpy as np
import ifcopenshell
from typing import Any, Union
from dataclasses import dataclass
@dataclass
class Clipping:
location: tuple[float, float, float]
normal: tuple[float, float, float]
type: str = "IfcBooleanClippingResult"
operand_type: str = "IfcHalfSpaceSolid"
@classmethod
def parse(cls, raw_data: Any) -> Union[ifcopenshell.entity_instance, Clipping, None]:
"""Parse various formats into a clipping object
`raw_data` can be either:
- IfcBooleanResult IFC entity
- `Clipping` instance
- dictionary to define `Clipping` - either `location` and `normal`
or a `matrix` where XY plane is the clipping boundary and +Z is removed.
`matrix` method will be soon to be deprecated completely.
"""
if isinstance(raw_data, ifcopenshell.entity_instance):
if not raw_data.is_a("IfcBooleanResult"):
raise Exception(f"Provided clipping of unexpected IFC class: {raw_data}")
return raw_data
elif isinstance(raw_data, Clipping):
return raw_data
elif isinstance(raw_data, dict):
if "matrix" in raw_data:
raw_data = raw_data.copy()
matrix = np.array(raw_data["matrix"])[:3]
raw_data["normal"] = matrix[:, 2].tolist()
raw_data["location"] = matrix[:, 3].tolist()
del raw_data["matrix"]
clipping_data = cls(**raw_data)
if clipping_data.type != "IfcBooleanClippingResult":
raise Exception(f'Provided clipping with unexpected result type "{clipping_data.type}"')
if clipping_data.operand_type != "IfcHalfSpaceSolid":
raise Exception(f'Provided clipping with unexpected operand type "{clipping_data.operand_type}"')
return clipping_data
raise Exception(f"Unexpected clipping type provided: {raw_data}")
def apply(
self, ifc_file: ifcopenshell.file, first_operand: ifcopenshell.entity_instance, unit_scale: float
) -> ifcopenshell.entity_instance:
"""Applies the clipping data as an IfcBooleanClippingResult to an operand
:param ifc_file: The model to create the entities in
:param first_operand: The representation item to apply the boolean clipping to.
:param unit_scale: The unit scale value to convert from the Clipping's SI units to project units
:return: An IfcBooleanClippingResult which uses an IfcHalfSpaceSolid to clip the first operand
"""
location = ifc_file.createIfcCartesianPoint([i / unit_scale for i in self.location])
direction = ifc_file.createIfcDirection(self.normal)
normal = np.array(self.normal)
if np.allclose(normal, np.array([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, arbitrary_vector)
x_axis /= np.linalg.norm(x_axis)
x_axis = ifc_file.createIfcDirection(x_axis.tolist())
plane = ifc_file.createIfcPlane(ifc_file.createIfcAxis2Placement3D(location, direction, x_axis))
second_operand = ifc_file.createIfcHalfSpaceSolid(plane, False)
return ifc_file.createIfcBooleanClippingResult("DIFFERENCE", first_operand, second_operand)
@@ -16,16 +16,14 @@
# 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 numpy as np
import collections
import ifcopenshell
import ifcopenshell.api
from math import cos, sin, pi, tan, radians, degrees, atan, sqrt, ceil
from typing import List, Any, Union
from typing import List
from itertools import chain
from mathutils import Vector, Matrix
from dataclasses import dataclass
V = lambda *x: Vector([float(i) for i in x])
sign = lambda x: x and (1, -1)[x < 0]
@@ -1476,50 +1474,3 @@ class ShapeBuilder:
"main_profile_dimension": profile_dim[lateral_axis],
}
return rep, bend_data
@dataclass
class ClippingInfo:
location: tuple[float, float, float]
normal: tuple[float, float, float]
type: str = "IfcBooleanClippingResult"
operand_type: str = "IfcHalfSpaceSolid"
@classmethod
def parse(cls, raw_data: Any) -> Union[ifcopenshell.entity_instance, ClippingInfo, None]:
"""`raw_data` can be either:
- IfcBooleanResult IFC entity
- `ClippingInfo` instance
- dictionary to define `ClippingInfo` - either `location` and `normal`
or a `matrix` where XY plane is the clipping boundary and +Z is removed.
`matrix` method will be soon to be deprecated completely.
"""
if isinstance(raw_data, ifcopenshell.entity_instance):
if not raw_data.is_a("IfcBooleanResult"):
raise Exception(f"Provided clipping of unexpected IFC class: {raw_data}")
return raw_data
elif isinstance(raw_data, ClippingInfo):
return raw_data
elif isinstance(raw_data, dict):
if "matrix" in raw_data:
raw_data = raw_data.copy()
matrix = np.array(raw_data["matrix"])[:3]
raw_data["normal"] = matrix[:, 2].tolist()
raw_data["location"] = matrix[:, 3].tolist()
del raw_data["matrix"]
clipping_data = cls(**raw_data)
if clipping_data.type != "IfcBooleanClippingResult":
raise Exception(f'Provided clipping with unexpected result type "{clipping_data.type}"')
if clipping_data.operand_type != "IfcHalfSpaceSolid":
raise Exception(f'Provided clipping with unexpected operand type "{clipping_data.operand_type}"')
return clipping_data
raise Exception(f"Unexpected clipping type provided: {raw_data}")
def apply(
self, file: ifcopenshell.file, first_operand: ifcopenshell.entity_instance, unit_scale: float
) -> ifcopenshell.entity_instance:
builder = ShapeBuilder(file)
plane = builder.plane(location=Vector([i / unit_scale for i in self.location]), normal=Vector(self.normal))
second_operand = file.createIfcHalfSpaceSolid(plane, False)
first_operand = file.createIfcBooleanClippingResult("DIFFERENCE", first_operand, second_operand)
return first_operand