mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-08-21 04:32:23 +00:00
Compare commits
31 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 1cea3d21e8 | |||
| 640320f70d | |||
| 29352092aa | |||
| 5b877b549a | |||
| 721466c429 | |||
| cce2fee1fa | |||
| 087d55f02a | |||
| f1037de14d | |||
| 4711672170 | |||
| c0b4e099af | |||
| f41fbf24c3 | |||
| e14d96e6ee | |||
| 716a33f347 | |||
| c5f084a4af | |||
| 606a0b46f2 | |||
| 1c729a74d1 | |||
| db31574103 | |||
| d03b47e1e6 | |||
| ae62ac2777 | |||
| 998f9c6a1e | |||
| 3ff31b577c | |||
| cd013079f4 | |||
| b8a9674483 | |||
| 8beb1bef98 | |||
| 377676f881 | |||
| 4efe284338 | |||
| 34a4e3f37a | |||
| 889c9a9e9f | |||
| 9e79499532 | |||
| 589b98053e | |||
| fc5bb230a9 |
@@ -34,6 +34,7 @@ import ifcopenshell.util.element
|
||||
import ifcopenshell.util.geolocation
|
||||
import ifcopenshell.util.placement
|
||||
import ifcopenshell.util.representation
|
||||
import ifcopenshell.util.shape
|
||||
import blenderbim.tool as tool
|
||||
import ifcopenshell.ifcopenshell_wrapper as ifcopenshell_wrapper
|
||||
from itertools import chain, accumulate
|
||||
@@ -199,7 +200,7 @@ class MaterialCreator:
|
||||
|
||||
|
||||
class IfcImporter:
|
||||
def __init__(self, ifc_import_settings):
|
||||
def __init__(self, ifc_import_settings: IfcImportSettings):
|
||||
self.ifc_import_settings = ifc_import_settings
|
||||
self.diff = None
|
||||
self.file: ifcopenshell.file = None
|
||||
@@ -430,7 +431,7 @@ class IfcImporter:
|
||||
self.annotations = set([a for a in self.file.by_type("IfcAnnotation")])
|
||||
self.annotations -= drawing_annotations
|
||||
|
||||
self.elements = [e for e in self.elements if not e.is_a("IfcFeatureElement")]
|
||||
self.elements = [e for e in self.elements if not e.is_a("IfcFeatureElement") or e.is_a("IfcSurfaceFeature")]
|
||||
if self.ifc_import_settings.is_coordinating:
|
||||
self.elements = [e for e in self.elements if e.Representation]
|
||||
|
||||
@@ -725,7 +726,7 @@ class IfcImporter:
|
||||
if len(subelement.Coordinates) == 3 and self.is_point_far_away(subelement, is_meters=False):
|
||||
return True
|
||||
|
||||
def apply_blender_offset_to_matrix_world(self, obj, matrix):
|
||||
def apply_blender_offset_to_matrix_world(self, obj: bpy.types.Object, matrix: np.ndarray) -> mathutils.Matrix:
|
||||
props = bpy.context.scene.BIMGeoreferenceProperties
|
||||
if props.has_blender_offset:
|
||||
if obj.data and obj.data.get("has_cartesian_point_offset", None):
|
||||
@@ -862,7 +863,6 @@ class IfcImporter:
|
||||
else:
|
||||
self.create_generic_elements(self.spatial_elements, unselectable=False)
|
||||
|
||||
|
||||
def create_elements(self) -> None:
|
||||
self.create_generic_elements(self.elements)
|
||||
tmp = self.context_settings
|
||||
@@ -953,7 +953,9 @@ class IfcImporter:
|
||||
self.create_product(element, mesh=mesh)
|
||||
|
||||
def create_products(
|
||||
self, products, settings: Optional[ifcopenshell.geom.main.settings] = None
|
||||
self,
|
||||
products: set[ifcopenshell.entity_instance],
|
||||
settings: Optional[ifcopenshell.geom.main.settings] = None,
|
||||
) -> set[ifcopenshell.entity_instance]:
|
||||
results = set()
|
||||
if not products:
|
||||
@@ -1627,30 +1629,34 @@ class IfcImporter:
|
||||
if self.ifc_import_settings.has_filter:
|
||||
rel_aggregates = set()
|
||||
for element in self.elements:
|
||||
if element.IsDecomposedBy:
|
||||
rel_aggregates.add(element.IsDecomposedBy[0])
|
||||
elif element.Decomposes:
|
||||
rel_aggregates.add(element.Decomposes[0])
|
||||
elif getattr(element, "IsNestedBy", []): # IFC2X3 does not have IsNestedBy
|
||||
if [e for e in element.IsNestedBy[0].RelatedObjects if not e.is_a("IfcPort")]:
|
||||
rel_aggregates.add(element.IsNestedBy[0])
|
||||
elif getattr(element, "Nests", []):
|
||||
rel_aggregates.add(element.Nests[0])
|
||||
if decomposed_by := element.IsDecomposedBy:
|
||||
rel_aggregates.add(decomposed_by[0])
|
||||
elif decomposes := element.Decomposes:
|
||||
rel_aggregates.add(decomposes[0])
|
||||
elif nested_by := getattr(element, "IsNestedBy", []): # IFC2X3 does not have IsNestedBy
|
||||
if next((e for e in nested_by[0].RelatedObjects if not e.is_a("IfcPort")), None):
|
||||
rel_aggregates.add(nested_by[0])
|
||||
elif nests := getattr(element, "Nests", []):
|
||||
rel_aggregates.add(nests[0])
|
||||
elif element.is_a("IfcSurfaceFeature") and self.file.schema == "IFC4X3":
|
||||
rel_aggregates.add(element.AdheresToElement[0])
|
||||
else:
|
||||
rel_aggregates = [
|
||||
r
|
||||
for r in self.file.by_type("IfcRelAggregates")
|
||||
if r.RelatingObject.is_a("IfcElement") or r.RelatingObject.is_a("IfcElementType")
|
||||
if (relating_obj := r.RelatingObject).is_a("IfcElement") or relating_obj.is_a("IfcElementType")
|
||||
] + [
|
||||
r
|
||||
for r in self.file.by_type("IfcRelNests")
|
||||
if (
|
||||
r.RelatingObject.is_a("IfcElement")
|
||||
or r.RelatingObject.is_a("IfcElementType")
|
||||
or (r.RelatingObject.is_a("IfcPositioningElement") and not r.RelatingObject.is_a("IfcGrid"))
|
||||
(relating_obj := r.RelatingObject).is_a("IfcElement")
|
||||
or relating_obj.is_a("IfcElementType")
|
||||
or (relating_obj.is_a("IfcPositioningElement") and not relating_obj.is_a("IfcGrid"))
|
||||
)
|
||||
and [e for e in r.RelatedObjects if not e.is_a("IfcPort")]
|
||||
]
|
||||
if self.file.schema == "IFC4X3":
|
||||
rel_aggregates += [r for r in self.file.by_type("IfcRelAdheresToElement")]
|
||||
|
||||
if len(rel_aggregates) > 10000:
|
||||
# More than 10,000 collections makes Blender unhappy
|
||||
@@ -1660,7 +1666,9 @@ class IfcImporter:
|
||||
|
||||
aggregates: dict[str, dict] = {}
|
||||
for rel_aggregate in rel_aggregates:
|
||||
element: ifcopenshell.entity_instance = rel_aggregate.RelatingObject
|
||||
element: ifcopenshell.entity_instance = getattr(rel_aggregate, "RelatingObject", None) or getattr(
|
||||
rel_aggregate, "RelatingElement"
|
||||
)
|
||||
collection = bpy.data.collections.new(tool.Loader.get_name(element))
|
||||
aggregates[element.GlobalId] = {"element": element, "collection": collection}
|
||||
self.collections[element.GlobalId] = collection
|
||||
@@ -1761,6 +1769,9 @@ class IfcImporter:
|
||||
elif getattr(element, "Nests", None) and not element.is_a("IfcPort"):
|
||||
nest = ifcopenshell.util.element.get_nest(element)
|
||||
return self.collections[nest.GlobalId].objects.link(obj)
|
||||
elif element.is_a("IfcSurfaceFeature") and self.file.schema == "IFC4X3":
|
||||
adherend = element.AdheresToElement[0].RelatingElement
|
||||
return self.collections[adherend.GlobalId].objects.link(obj)
|
||||
|
||||
return self.place_object_in_spatial_decomposition_collection(element, obj)
|
||||
|
||||
@@ -1810,7 +1821,7 @@ class IfcImporter:
|
||||
if rel.is_a("IfcRelAssignsToGroup") and rel.RelatingGroup.ObjectType == "DRAWING":
|
||||
return rel.RelatingGroup
|
||||
|
||||
def get_element_matrix(self, element: ifcopenshell.entity_instance) -> np.array:
|
||||
def get_element_matrix(self, element: ifcopenshell.entity_instance) -> np.ndarray:
|
||||
if isinstance(element, ifcopenshell.sqlite_entity):
|
||||
result = self.geometry_cache["shapes"][element.id()]["matrix"]
|
||||
else:
|
||||
@@ -1932,7 +1943,9 @@ class IfcImporter:
|
||||
|
||||
# See bug 3546
|
||||
# ios_edges holds true edges that aren't triangulated.
|
||||
mesh["ios_edges"] = list(set(tuple(e) for e in ifcopenshell.util.shape.get_edges(geometry)))
|
||||
#
|
||||
# we do `.tolist()` because Blender can't assign `np.int32` to it's custom attributes
|
||||
mesh["ios_edges"] = list(set(tuple(e) for e in ifcopenshell.util.shape.get_edges(geometry).tolist()))
|
||||
|
||||
mesh.vertices.add(num_vertices)
|
||||
mesh.vertices.foreach_set("co", verts)
|
||||
@@ -1959,14 +1972,14 @@ class IfcImporter:
|
||||
|
||||
print(traceback.format_exc())
|
||||
|
||||
def a2p(self, o, z, x):
|
||||
def a2p(self, o: mathutils.Vector, z: mathutils.Vector, x: mathutils.Vector) -> mathutils.Matrix:
|
||||
y = z.cross(x)
|
||||
r = mathutils.Matrix((x, y, z, o))
|
||||
r.resize_4x4()
|
||||
r.transpose()
|
||||
return r
|
||||
|
||||
def get_axis2placement(self, plc):
|
||||
def get_axis2placement(self, plc: ifcopenshell.entity_instance) -> mathutils.Matrix:
|
||||
if plc.is_a("IfcAxis2Placement3D"):
|
||||
z = mathutils.Vector(plc.Axis.DirectionRatios if plc.Axis else (0, 0, 1))
|
||||
x = mathutils.Vector(plc.RefDirection.DirectionRatios if plc.RefDirection else (1, 0, 0))
|
||||
@@ -1986,7 +1999,7 @@ class IfcImporter:
|
||||
o = plc.LocalOrigin.Coordinates
|
||||
return self.a2p(o, z, x)
|
||||
|
||||
def get_local_placement(self, plc):
|
||||
def get_local_placement(self, plc: Optional[ifcopenshell.entity_instance] = None) -> mathutils.Matrix:
|
||||
if plc is None:
|
||||
return mathutils.Matrix()
|
||||
if plc.PlacementRelTo is None:
|
||||
@@ -2001,11 +2014,11 @@ class IfcImporter:
|
||||
bpy.context.scene.BIMRootProperties.contexts = str(subcontext.id())
|
||||
break
|
||||
|
||||
def link_element(self, element, obj):
|
||||
def link_element(self, element: ifcopenshell.entity_instance, obj: IFC_CONNECTED_TYPE) -> None:
|
||||
self.added_data[element.id()] = obj
|
||||
tool.Ifc.link(element, obj)
|
||||
|
||||
def set_matrix_world(self, obj, matrix_world):
|
||||
def set_matrix_world(self, obj: bpy.types.Object, matrix_world: mathutils.Matrix) -> None:
|
||||
obj.matrix_world = matrix_world
|
||||
tool.Geometry.record_object_position(obj)
|
||||
|
||||
|
||||
@@ -20,6 +20,7 @@ import bpy
|
||||
import bmesh
|
||||
import logging
|
||||
import shapely
|
||||
import shapely.ops
|
||||
import mathutils
|
||||
import numpy as np
|
||||
import multiprocessing
|
||||
|
||||
@@ -1241,12 +1241,14 @@ class AddDrawingToSheet(bpy.types.Operator, Operator):
|
||||
return
|
||||
|
||||
reference = tool.Ifc.run("document.add_reference", information=sheet)
|
||||
id_attr = "ItemReference" if tool.Ifc.get_schema() == "IFC2X3" else "Identification"
|
||||
attributes = {
|
||||
id_attr: str(len([r for r in references if r.Description in ("DRAWING", "SCHEDULE")]) + 1),
|
||||
"Location": drawing_reference.Location,
|
||||
"Description": "DRAWING",
|
||||
}
|
||||
attributes = tool.Drawing.generate_reference_attributes(
|
||||
reference,
|
||||
Identification=str(
|
||||
len([r for r in references if tool.Drawing.get_reference_description(r) in ("DRAWING", "SCHEDULE")]) + 1
|
||||
),
|
||||
Location=drawing_reference.Location,
|
||||
Description="DRAWING",
|
||||
)
|
||||
tool.Ifc.run("document.edit_reference", reference=reference, attributes=attributes)
|
||||
sheet_builder = sheeter.SheetBuilder()
|
||||
sheet_builder.data_dir = context.scene.BIMProperties.data_dir
|
||||
@@ -1314,9 +1316,10 @@ class CreateSheets(bpy.types.Operator, Operator):
|
||||
|
||||
has_sheet_reference = False
|
||||
for reference in tool.Drawing.get_document_references(sheet):
|
||||
if reference.Description == "SHEET":
|
||||
reference_description = tool.Drawing.get_reference_description(reference)
|
||||
if reference_description == "SHEET":
|
||||
has_sheet_reference = True
|
||||
elif reference.Description == "RASTER":
|
||||
elif reference_description == "RASTER":
|
||||
if reference.Location in raster_references:
|
||||
raster_references.remove(reference.Location)
|
||||
else:
|
||||
@@ -1327,7 +1330,9 @@ class CreateSheets(bpy.types.Operator, Operator):
|
||||
tool.Ifc.run(
|
||||
"document.edit_reference",
|
||||
reference=reference,
|
||||
attributes={"Location": tool.Ifc.get_relative_uri(svg), "Description": "SHEET"},
|
||||
attributes=tool.Drawing.generate_reference_attributes(
|
||||
reference, Location=tool.Ifc.get_relative_uri(svg), Description="SHEET"
|
||||
),
|
||||
)
|
||||
|
||||
for raster_reference in raster_references:
|
||||
@@ -1335,7 +1340,9 @@ class CreateSheets(bpy.types.Operator, Operator):
|
||||
tool.Ifc.run(
|
||||
"document.edit_reference",
|
||||
reference=reference,
|
||||
attributes={"Location": tool.Ifc.get_relative_uri(raster_reference), "Description": "RASTER"},
|
||||
attributes=tool.Drawing.generate_reference_attributes(
|
||||
reference, Location=tool.Ifc.get_relative_uri(raster_reference), Description="RASTER"
|
||||
),
|
||||
)
|
||||
|
||||
svg2pdf_command = context.preferences.addons["blenderbim"].preferences.svg2pdf_command
|
||||
@@ -1988,12 +1995,14 @@ class AddScheduleToSheet(bpy.types.Operator, Operator):
|
||||
return
|
||||
|
||||
reference = tool.Ifc.run("document.add_reference", information=sheet)
|
||||
id_attr = "ItemReference" if tool.Ifc.get_schema() == "IFC2X3" else "Identification"
|
||||
attributes = {
|
||||
id_attr: str(len([r for r in references if r.Description in ("DRAWING", "SCHEDULE")]) + 1),
|
||||
"Location": schedule_location,
|
||||
"Description": "SCHEDULE",
|
||||
}
|
||||
attributes = tool.Drawing.generate_reference_attributes(
|
||||
reference,
|
||||
Identification=str(
|
||||
len([r for r in references if tool.Drawing.get_reference_description(r) in ("DRAWING", "SCHEDULE")]) + 1
|
||||
),
|
||||
Location=schedule_location,
|
||||
Description="SCHEDULE",
|
||||
)
|
||||
tool.Ifc.run("document.edit_reference", reference=reference, attributes=attributes)
|
||||
|
||||
sheet_builder = sheeter.SheetBuilder()
|
||||
@@ -2042,12 +2051,15 @@ class AddReferenceToSheet(bpy.types.Operator, Operator):
|
||||
return
|
||||
|
||||
reference = tool.Ifc.run("document.add_reference", information=sheet)
|
||||
id_attr = "ItemReference" if tool.Ifc.get_schema() == "IFC2X3" else "Identification"
|
||||
attributes = {
|
||||
id_attr: str(len([r for r in references if r.Description in ("DRAWING", "REFERENCE")]) + 1),
|
||||
"Location": extref_location,
|
||||
"Description": "REFERENCE",
|
||||
}
|
||||
attributes = tool.Drawing.generate_reference_attributes(
|
||||
reference,
|
||||
Identification=str(
|
||||
len([r for r in references if tool.Drawing.get_reference_description(r) in ("DRAWING", "REFERENCE")])
|
||||
+ 1
|
||||
),
|
||||
Location=extref_location,
|
||||
Description="REFERENCE",
|
||||
)
|
||||
tool.Ifc.run("document.edit_reference", reference=reference, attributes=attributes)
|
||||
|
||||
sheet_builder = sheeter.SheetBuilder()
|
||||
@@ -2392,8 +2404,8 @@ class EditSheet(bpy.types.Operator, Operator):
|
||||
if sheet.is_a("IfcDocumentInformation"):
|
||||
self.document_type = "SHEET"
|
||||
self.name = sheet.Name
|
||||
self.identification = sheet.Identification
|
||||
elif sheet.is_a("IfcDocumentReference") and sheet.Description == "TITLEBLOCK":
|
||||
self.identification = sheet.DocumentId if tool.Ifc.get_schema() == "IFC2X3" else sheet.Identification
|
||||
elif sheet.is_a("IfcDocumentReference") and tool.Drawing.get_reference_description(sheet) == "TITLEBLOCK":
|
||||
self.document_type = "TITLEBLOCK"
|
||||
else:
|
||||
self.document_type = "EMBEDDED"
|
||||
@@ -2419,7 +2431,7 @@ class EditSheet(bpy.types.Operator, Operator):
|
||||
if self.document_type == "SHEET":
|
||||
core.rename_sheet(tool.Ifc, tool.Drawing, sheet=sheet, identification=self.identification, name=self.name)
|
||||
elif self.document_type == "EMBEDDED":
|
||||
core.rename_reference(tool.Ifc, reference=sheet, identification=self.identification)
|
||||
core.rename_reference(tool.Ifc, tool.Drawing, reference=sheet, identification=self.identification)
|
||||
elif self.document_type == "TITLEBLOCK":
|
||||
titleblock = self.props.titleblock
|
||||
reference = sheet
|
||||
|
||||
@@ -41,7 +41,7 @@ class SheetBuilder:
|
||||
self.data_dir = None
|
||||
self.scale = "NTS"
|
||||
|
||||
def create(self, layout_path, titleblock_name):
|
||||
def create(self, layout_path: str, titleblock_name: str) -> None:
|
||||
root = ET.Element("svg")
|
||||
root.attrib["xmlns"] = "http://www.w3.org/2000/svg"
|
||||
root.attrib["xmlns:xlink"] = "http://www.w3.org/1999/xlink"
|
||||
@@ -76,7 +76,12 @@ class SheetBuilder:
|
||||
with open(layout_path, "w") as f:
|
||||
f.write(minidom.parseString(ET.tostring(root)).toprettyxml(indent=" "))
|
||||
|
||||
def add_drawing(self, reference, drawing, sheet):
|
||||
def add_drawing(
|
||||
self,
|
||||
reference: ifcopenshell.entity_instance,
|
||||
drawing: ifcopenshell.entity_instance,
|
||||
sheet: ifcopenshell.entity_instance,
|
||||
) -> None:
|
||||
filename = drawing.Name
|
||||
layout_path = tool.Drawing.get_document_uri(sheet, "LAYOUT")
|
||||
layout_dir = os.path.dirname(layout_path)
|
||||
@@ -131,7 +136,7 @@ class SheetBuilder:
|
||||
)
|
||||
layout_tree.write(layout_path)
|
||||
|
||||
def update_sheet_drawing_sizes(self, sheet):
|
||||
def update_sheet_drawing_sizes(self, sheet: ifcopenshell.entity_instance) -> None:
|
||||
ET.register_namespace("", "http://www.w3.org/2000/svg")
|
||||
|
||||
layout_path = tool.Drawing.get_document_uri(sheet, "LAYOUT")
|
||||
@@ -171,7 +176,7 @@ class SheetBuilder:
|
||||
|
||||
layout_tree.write(layout_path)
|
||||
|
||||
def remove_drawing(self, reference, sheet):
|
||||
def remove_drawing(self, reference: ifcopenshell.entity_instance, sheet: ifcopenshell.entity_instance) -> None:
|
||||
ET.register_namespace("", "http://www.w3.org/2000/svg")
|
||||
|
||||
layout_path = tool.Drawing.get_document_uri(sheet, "LAYOUT")
|
||||
@@ -187,7 +192,12 @@ class SheetBuilder:
|
||||
|
||||
layout_tree.write(layout_path)
|
||||
|
||||
def add_document(self, reference, document, sheet):
|
||||
def add_document(
|
||||
self,
|
||||
reference: ifcopenshell.entity_instance,
|
||||
document: ifcopenshell.entity_instance,
|
||||
sheet: ifcopenshell.entity_instance,
|
||||
) -> None:
|
||||
view_path = tool.Drawing.get_path_with_ext(tool.Drawing.get_document_uri(document), "svg")
|
||||
if not os.path.exists(view_path):
|
||||
tool.Drawing.create_svg_document(document)
|
||||
@@ -224,7 +234,7 @@ class SheetBuilder:
|
||||
)
|
||||
layout_tree.write(layout_path)
|
||||
|
||||
def add_view_title(self, x, y, parent, layout_dir):
|
||||
def add_view_title(self, x: float, y: float, parent: ET.Element, layout_dir: str) -> None:
|
||||
title_path = os.path.join(layout_dir, "assets", "view-title.svg")
|
||||
os.makedirs(os.path.dirname(title_path), exist_ok=True)
|
||||
if not os.path.exists(title_path):
|
||||
@@ -241,7 +251,7 @@ class SheetBuilder:
|
||||
title.attrib["width"] = str(self.convert_to_mm(title_root.attrib.get("width")))
|
||||
title.attrib["height"] = str(self.convert_to_mm(title_root.attrib.get("height")))
|
||||
|
||||
def build(self, sheet):
|
||||
def build(self, sheet: ifcopenshell.entity_instance) -> dict:
|
||||
self.references = {"SHEET": None, "RASTER": []}
|
||||
|
||||
layout_path = tool.Drawing.get_document_uri(sheet, "LAYOUT")
|
||||
@@ -272,7 +282,7 @@ class SheetBuilder:
|
||||
|
||||
return self.references
|
||||
|
||||
def build_titleblock(self, root, sheet):
|
||||
def build_titleblock(self, root: ET.Element, sheet: ifcopenshell.entity_instance) -> None:
|
||||
titleblock = root.findall('{http://www.w3.org/2000/svg}g[@data-type="titleblock"]')[0]
|
||||
image = titleblock.findall("{http://www.w3.org/2000/svg}image")[0]
|
||||
g = self.parse_embedded_svg(image, sheet.get_info())
|
||||
@@ -285,7 +295,7 @@ class SheetBuilder:
|
||||
titleblock.append(g)
|
||||
titleblock.remove(image)
|
||||
|
||||
def ensure_drawing_unique_styles(self, svg, drawing_id):
|
||||
def ensure_drawing_unique_styles(self, svg: ET.Element, drawing_id: int) -> ET.Element:
|
||||
"""ensures all drawing's classes and ids will be unique for the whole sheet
|
||||
by adding `drawing_id` based prefix
|
||||
"""
|
||||
@@ -313,7 +323,7 @@ class SheetBuilder:
|
||||
brackets_level -= 1
|
||||
text += l
|
||||
|
||||
def replace_urls(text):
|
||||
def replace_urls(text: str) -> str:
|
||||
"""replace urls `url(#marker)` with `url(#prefix-marker)`
|
||||
since `url(#marker.prefix)` doesn't seem to work
|
||||
"""
|
||||
@@ -343,7 +353,7 @@ class SheetBuilder:
|
||||
|
||||
return svg
|
||||
|
||||
def build_drawings(self, root, sheet):
|
||||
def build_drawings(self, root: ET.Element, sheet: ifcopenshell.entity_instance):
|
||||
for view in root.findall('{http://www.w3.org/2000/svg}g[@data-type="drawing"]'):
|
||||
drawing_id = int(view.attrib["data-id"])
|
||||
try:
|
||||
@@ -390,7 +400,7 @@ class SheetBuilder:
|
||||
for image in images:
|
||||
view.remove(image)
|
||||
|
||||
def build_documents(self, root, sheet):
|
||||
def build_documents(self, root: ET.Element, sheet: ifcopenshell.entity_instance) -> None:
|
||||
schedules = root.findall('{http://www.w3.org/2000/svg}g[@data-type="schedule"]')
|
||||
references = root.findall('{http://www.w3.org/2000/svg}g[@data-type="reference"]')
|
||||
documents = schedules + references
|
||||
@@ -427,10 +437,10 @@ class SheetBuilder:
|
||||
for image in images:
|
||||
view.remove(image)
|
||||
|
||||
def get_href(self, element):
|
||||
return urllib.parse.unquote(element.attrib.get("{http://www.w3.org/1999/xlink}href")).replace('\\','/')
|
||||
def get_href(self, element: ET.Element) -> str:
|
||||
return urllib.parse.unquote(element.attrib.get("{http://www.w3.org/1999/xlink}href")).replace("\\", "/")
|
||||
|
||||
def parse_embedded_svg(self, image, data):
|
||||
def parse_embedded_svg(self, image: ET.Element, data: dict) -> ET.Element:
|
||||
group = ET.Element("g")
|
||||
group.attrib["transform"] = "translate({},{})".format(
|
||||
self.convert_to_mm(image.attrib.get("x")), self.convert_to_mm(image.attrib.get("y"))
|
||||
@@ -466,7 +476,7 @@ class SheetBuilder:
|
||||
group.append(child)
|
||||
return group
|
||||
|
||||
def change_titleblock(self, sheet, titleblock_name):
|
||||
def change_titleblock(self, sheet: ifcopenshell.entity_instance, titleblock_name: str) -> None:
|
||||
ootb_titleblock_path = os.path.join(self.data_dir, "templates", "titleblocks", titleblock_name + ".svg")
|
||||
titleblock_path = tool.Drawing.get_default_titleblock_path(titleblock_name)
|
||||
sheet_path = tool.Drawing.get_document_uri(sheet, "LAYOUT")
|
||||
@@ -499,7 +509,7 @@ class SheetBuilder:
|
||||
|
||||
sheet_tree.write(sheet_path)
|
||||
|
||||
def convert_to_mm(self, value):
|
||||
def convert_to_mm(self, value: str) -> float:
|
||||
# CSS is what defines these possibilities
|
||||
# https://www.w3.org/TR/SVG/refs.html#ref-css-values-3
|
||||
# https://www.w3.org/TR/css-values-3/#absolute-lengths
|
||||
@@ -520,5 +530,5 @@ class SheetBuilder:
|
||||
return float(value[0:-2]) * (1 / 96) * 2.54 * 10
|
||||
return float(value)
|
||||
|
||||
def mm_to_px(self, value):
|
||||
def mm_to_px(self, value: float) -> float:
|
||||
return (value / 25.4) * 96
|
||||
|
||||
@@ -726,7 +726,8 @@ class SvgWriter:
|
||||
reference = tool.Drawing.get_drawing_reference(drawing)
|
||||
if reference:
|
||||
for sheet_reference in tool.Ifc.get().by_type("IfcDocumentReference"):
|
||||
if sheet_reference.Description != "DRAWING" or sheet_reference.Location != reference.Location:
|
||||
reference_description = tool.Drawing.get_reference_description(reference)
|
||||
if reference_description != "DRAWING" or sheet_reference.Location != reference.Location:
|
||||
continue
|
||||
sheet = tool.Drawing.get_reference_document(sheet_reference)
|
||||
if sheet:
|
||||
|
||||
@@ -538,7 +538,7 @@ class Helper:
|
||||
)
|
||||
position = None
|
||||
if self.file.schema == "IFC2X3":
|
||||
position = self.file.createIfcAxis2Placement2D(self.file.createIfcCartesianPoint([0.0, 0.0, 0.0]))
|
||||
position = self.file.createIfcAxis2Placement2D(self.file.createIfcCartesianPoint([0.0, 0.0]))
|
||||
curve = self.file.createIfcRectangleProfileDef("AREA", None, position, xdim, ydim)
|
||||
return {"curve_ucs": curve_ucs, "curve": curve}
|
||||
|
||||
|
||||
@@ -24,6 +24,7 @@ classes = (
|
||||
operator.AddLayer,
|
||||
operator.AddListItem,
|
||||
operator.AddMaterial,
|
||||
operator.DuplicateMaterial,
|
||||
operator.AddMaterialSet,
|
||||
operator.AddProfile,
|
||||
operator.AssignMaterial,
|
||||
|
||||
@@ -134,6 +134,19 @@ class AddMaterial(bpy.types.Operator, tool.Ifc.Operator):
|
||||
material_prop_purge()
|
||||
|
||||
|
||||
class DuplicateMaterial(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_idname = "bim.duplicate_material"
|
||||
bl_label = "Diplicate Material"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
material: bpy.props.IntProperty(name="Material ID")
|
||||
|
||||
def _execute(self, context):
|
||||
ifc_file = tool.Ifc.get()
|
||||
tool.Material.duplicate_material(ifc_file.by_id(self.material))
|
||||
material_prop_purge()
|
||||
bpy.ops.bim.load_materials()
|
||||
|
||||
|
||||
class AddMaterialSet(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_idname = "bim.add_material_set"
|
||||
bl_label = "Add Material Set"
|
||||
|
||||
@@ -61,6 +61,8 @@ class BIM_PT_materials(Panel):
|
||||
if self.props.materials and self.props.active_material_index < len(self.props.materials):
|
||||
material = self.props.materials[self.props.active_material_index]
|
||||
if material.ifc_definition_id:
|
||||
op = row.operator("bim.duplicate_material", text="", icon="DUPLICATE")
|
||||
op.material = material.ifc_definition_id
|
||||
op = row.operator("bim.select_by_material", text="", icon="RESTRICT_SELECT_OFF")
|
||||
op.material = material.ifc_definition_id
|
||||
op = row.operator("bim.enable_editing_material", text="", icon="GREASEPENCIL")
|
||||
|
||||
@@ -21,6 +21,7 @@ from . import ui, prop, operator, data
|
||||
|
||||
classes = (
|
||||
operator.AddProfileDef,
|
||||
operator.DuplicateProfileDef,
|
||||
operator.DisableEditingArbitraryProfile,
|
||||
operator.DisableEditingProfile,
|
||||
operator.DisableProfileEditingUI,
|
||||
|
||||
@@ -66,9 +66,15 @@ class RemoveProfileDef(bpy.types.Operator, tool.Ifc.Operator):
|
||||
profile: bpy.props.IntProperty()
|
||||
|
||||
def _execute(self, context):
|
||||
props = context.scene.BIMProfileProperties
|
||||
current_index = props.active_profile_index
|
||||
ifcopenshell.api.run("profile.remove_profile", tool.Ifc.get(), profile=tool.Ifc.get().by_id(self.profile))
|
||||
bpy.ops.bim.load_profiles()
|
||||
|
||||
# preserve selected index if possible
|
||||
if props.profiles:
|
||||
props.active_profile_index = min(current_index, len(props.profiles) - 1)
|
||||
|
||||
|
||||
class EnableEditingProfile(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_idname = "bim.enable_editing_profile"
|
||||
@@ -126,6 +132,27 @@ class AddProfileDef(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bpy.ops.bim.load_profiles()
|
||||
|
||||
|
||||
class DuplicateProfileDef(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_idname = "bim.duplicate_profile_def"
|
||||
bl_label = "Duplicate Profile"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
props = context.scene.BIMProfileProperties
|
||||
if len(props.profiles) > props.active_profile_index:
|
||||
return True
|
||||
cls.poll_message_set("No profile selected to duplicate.")
|
||||
return False
|
||||
|
||||
def _execute(self, context):
|
||||
props = context.scene.BIMProfileProperties
|
||||
ifc_file = tool.Ifc.get()
|
||||
profile = ifc_file.by_id(props.profiles[props.active_profile_index].ifc_definition_id)
|
||||
tool.Profile.duplicate_profile(profile)
|
||||
bpy.ops.bim.load_profiles()
|
||||
|
||||
|
||||
class EnableEditingArbitraryProfile(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_idname = "bim.enable_editing_arbitrary_profile"
|
||||
bl_label = "Enable Editing Arbitrary Profile"
|
||||
|
||||
@@ -70,6 +70,7 @@ class BIM_PT_profiles(Panel):
|
||||
row = self.layout.row(align=True)
|
||||
row.prop(self.props, "profile_classes", text="")
|
||||
row.operator("bim.add_profile_def", text="", icon="ADD")
|
||||
row.operator("bim.duplicate_profile_def", icon="DUPLICATE", text="")
|
||||
|
||||
self.layout.template_list(
|
||||
"BIM_UL_profiles",
|
||||
|
||||
@@ -30,6 +30,7 @@ classes = (
|
||||
operator.DisableAddingPresentationStyle,
|
||||
operator.DisableEditingStyle,
|
||||
operator.DisableEditingStyles,
|
||||
operator.DuplicateStyle,
|
||||
operator.EditStyle,
|
||||
operator.EditSurfaceStyle,
|
||||
operator.EnableAddingPresentationStyle,
|
||||
|
||||
@@ -493,6 +493,22 @@ class EnableAddingPresentationStyle(bpy.types.Operator, tool.Ifc.Operator):
|
||||
props.is_adding = True
|
||||
|
||||
|
||||
class DuplicateStyle(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_idname = "bim.duplicate_style"
|
||||
bl_label = "Duplicate Style"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
style: bpy.props.IntProperty(name="Style ID")
|
||||
|
||||
def _execute(self, context):
|
||||
style_type = context.scene.BIMStylesProperties.style_type
|
||||
ifc_file = tool.Ifc.get()
|
||||
style = ifc_file.by_id(self.style)
|
||||
tool.Style.duplicate_style(style)
|
||||
bpy.ops.bim.disable_editing_styles()
|
||||
bpy.ops.bim.load_styles(style_type=style_type)
|
||||
|
||||
|
||||
class DisableAddingPresentationStyle(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_idname = "bim.disable_adding_presentation_style"
|
||||
bl_label = "Disable Add Presentation Style"
|
||||
|
||||
@@ -44,19 +44,33 @@ class BIM_PT_styles(Panel):
|
||||
|
||||
self.props = context.scene.BIMStylesProperties
|
||||
|
||||
if self.props.is_editing:
|
||||
row = self.layout.row(align=True)
|
||||
row.label(text="{} {}s".format(len(self.props.styles), self.props.style_type), icon="SHADING_RENDERED")
|
||||
if not self.props.is_adding:
|
||||
row.operator("bim.enable_adding_presentation_style", text="", icon="ADD")
|
||||
row.operator("bim.disable_editing_styles", text="", icon="CANCEL")
|
||||
else:
|
||||
if not self.props.is_editing:
|
||||
row = self.layout.row(align=True)
|
||||
row.label(text="{} Styles".format(StylesData.data["total_styles"]), icon="SHADING_RENDERED")
|
||||
blenderbim.bim.helper.prop_with_search(row, self.props, "style_type", text="")
|
||||
row.operator("bim.load_styles", text="", icon="IMPORT").style_type = self.props.style_type
|
||||
return
|
||||
|
||||
active_style = self.props.styles and self.props.active_style_index < len(self.props.styles)
|
||||
row = self.layout.row(align=True)
|
||||
row.label(text="{} {}s".format(len(self.props.styles), self.props.style_type), icon="SHADING_RENDERED")
|
||||
row.operator("bim.disable_editing_styles", text="", icon="CANCEL")
|
||||
|
||||
row = self.layout.row(align=True)
|
||||
row.alignment = "RIGHT"
|
||||
if not self.props.is_adding:
|
||||
row.operator("bim.enable_adding_presentation_style", text="", icon="ADD")
|
||||
if active_style:
|
||||
style = self.props.styles[self.props.active_style_index]
|
||||
material_name = StylesData.data["styles_to_blender_material_names"][self.props.active_style_index]
|
||||
material = bpy.data.materials[material_name]
|
||||
|
||||
row.operator("bim.duplicate_style", text="", icon="DUPLICATE").style = style.ifc_definition_id
|
||||
row.operator("bim.select_by_style", text="", icon="RESTRICT_SELECT_OFF").style = style.ifc_definition_id
|
||||
op = row.operator("bim.enable_editing_style", text="", icon="GREASEPENCIL")
|
||||
op.style = style.ifc_definition_id
|
||||
row.operator("bim.remove_style", text="", icon="X").style = style.ifc_definition_id
|
||||
|
||||
self.layout.template_list("BIM_UL_styles", "", self.props, "styles", self.props, "active_style_index")
|
||||
|
||||
# adding a new IfcSurfaceStyle
|
||||
@@ -77,17 +91,7 @@ class BIM_PT_styles(Panel):
|
||||
row.operator("bim.disable_adding_presentation_style", text="", icon="CANCEL")
|
||||
|
||||
# style ui tools
|
||||
if self.props.styles and self.props.active_style_index < len(self.props.styles):
|
||||
row = self.layout.row(align=True)
|
||||
style = self.props.styles[self.props.active_style_index]
|
||||
material_name = StylesData.data["styles_to_blender_material_names"][self.props.active_style_index]
|
||||
material = bpy.data.materials[material_name]
|
||||
|
||||
op = row.operator("bim.enable_editing_style", text="Edit Style", icon="GREASEPENCIL")
|
||||
op.style = style.ifc_definition_id
|
||||
row.operator("bim.select_by_style", text="", icon="RESTRICT_SELECT_OFF").style = style.ifc_definition_id
|
||||
row.operator("bim.remove_style", text="", icon="X").style = style.ifc_definition_id
|
||||
|
||||
if active_style:
|
||||
row = self.layout.row(align=True)
|
||||
row.prop(material.BIMStyleProperties, "active_style_type", icon="SHADING_RENDERED", text="")
|
||||
op = row.operator("bim.update_current_style", icon="FILE_REFRESH", text="")
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
# along with BlenderBIM Add-on. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from pathlib import Path
|
||||
import ifcopenshell
|
||||
|
||||
|
||||
def enable_editing_text(drawing, obj=None):
|
||||
@@ -66,7 +67,7 @@ def disable_editing_sheets(drawing):
|
||||
drawing.disable_editing_sheets()
|
||||
|
||||
|
||||
def add_sheet(ifc, drawing, titleblock=None):
|
||||
def add_sheet(ifc, drawing, titleblock: ifcopenshell.entity_instance):
|
||||
sheet = ifc.run("document.add_information")
|
||||
layout = ifc.run("document.add_reference", information=sheet)
|
||||
titleblock_reference = ifc.run("document.add_reference", information=sheet)
|
||||
@@ -77,16 +78,17 @@ def add_sheet(ifc, drawing, titleblock=None):
|
||||
else:
|
||||
attributes = {"Identification": identification, "Name": "UNTITLED", "Scope": "SHEET"}
|
||||
ifc.run("document.edit_information", information=sheet, attributes=attributes)
|
||||
ifc.run(
|
||||
"document.edit_reference",
|
||||
reference=layout,
|
||||
attributes={"Location": drawing.get_default_layout_path(identification, "UNTITLED"), "Description": "LAYOUT"},
|
||||
|
||||
attributes = drawing.generate_reference_attributes(
|
||||
layout, Location=drawing.get_default_layout_path(identification, "UNTITLED"), Description="LAYOUT"
|
||||
)
|
||||
ifc.run(
|
||||
"document.edit_reference",
|
||||
reference=titleblock_reference,
|
||||
attributes={"Location": drawing.get_default_titleblock_path(titleblock), "Description": "TITLEBLOCK"},
|
||||
ifc.run("document.edit_reference", reference=layout, attributes=attributes)
|
||||
|
||||
attributes = drawing.generate_reference_attributes(
|
||||
layout, Location=drawing.get_default_titleblock_path(titleblock), Description="TITLEBLOCK"
|
||||
)
|
||||
ifc.run("document.edit_reference", reference=titleblock_reference, attributes=attributes)
|
||||
|
||||
drawing.create_svg_sheet(sheet, titleblock)
|
||||
drawing.import_sheets()
|
||||
|
||||
@@ -94,7 +96,12 @@ def add_sheet(ifc, drawing, titleblock=None):
|
||||
def regenerate_sheet(drawing, sheet=None):
|
||||
titleblock_uri = drawing.get_document_uri(sheet, "TITLEBLOCK")
|
||||
drawing.create_svg_sheet(sheet, drawing.sanitise_filename(Path(titleblock_uri).stem))
|
||||
drawing.add_drawings(sheet)
|
||||
try:
|
||||
drawing.add_drawings(sheet)
|
||||
except FileNotFoundError:
|
||||
path_layout = drawing.get_document_uri(sheet, "LAYOUT")
|
||||
if drawing.does_file_exist(path_layout):
|
||||
drawing.delete_file(path_layout)
|
||||
|
||||
|
||||
def open_sheet(drawing, sheet=None):
|
||||
@@ -111,8 +118,12 @@ def remove_sheet(ifc, drawing, sheet=None):
|
||||
drawing.import_sheets()
|
||||
|
||||
|
||||
def rename_sheet(ifc, drawing, sheet=None, identification=None, name=None):
|
||||
ifc.run("document.edit_information", information=sheet, attributes={"Identification": identification, "Name": name})
|
||||
def rename_sheet(ifc, drawing, sheet: ifcopenshell.entity_instance, identification: str, name: str) -> None:
|
||||
if ifc.get_schema() == "IFC2X3":
|
||||
attributes = {"DocumentId": identification, "Name": name}
|
||||
else:
|
||||
attributes = {"Identification": identification, "Name": name}
|
||||
ifc.run("document.edit_information", information=sheet, attributes=attributes)
|
||||
for reference in drawing.get_document_references(sheet):
|
||||
description = drawing.get_reference_description(reference)
|
||||
if description == "SHEET":
|
||||
@@ -133,8 +144,9 @@ def rename_sheet(ifc, drawing, sheet=None, identification=None, name=None):
|
||||
drawing.move_file(old_location, ifc.resolve_uri(new_location))
|
||||
|
||||
|
||||
def rename_reference(ifc, reference=None, identification=None):
|
||||
ifc.run("document.edit_reference", reference=reference, attributes={"Identification": identification})
|
||||
def rename_reference(ifc, drawing, reference=None, identification=None):
|
||||
attributes = drawing.generate_reference_attributes(reference, Identifiaction=identification)
|
||||
ifc.run("document.edit_reference", reference=reference, attributes=attributes)
|
||||
|
||||
|
||||
def load_schedules(drawing):
|
||||
|
||||
@@ -329,6 +329,7 @@ class Drawing:
|
||||
def get_name(cls, element): pass
|
||||
def get_path_filename(cls, uri): pass
|
||||
def get_reference_description(cls, reference): pass
|
||||
def generate_reference_attributes(cls, reference, **attributes): pass
|
||||
def get_reference_document(cls, reference): pass
|
||||
def get_reference_location(cls, reference): pass
|
||||
def get_references_with_location(cls, location): pass
|
||||
|
||||
@@ -187,6 +187,9 @@ class Collector(blenderbim.core.tool.Collector):
|
||||
if any(e for e in element.IsNestedBy[0].RelatedObjects if not e.is_a("IfcPort")):
|
||||
return cls._create_own_collection(obj)
|
||||
|
||||
if getattr(element, "HasSurfaceFeatures", None):
|
||||
return cls._create_own_collection(obj)
|
||||
|
||||
@classmethod
|
||||
def _get_collection(cls, element: ifcopenshell.entity_instance, obj: bpy.types.Object) -> bpy.types.Collection:
|
||||
"""get or create collection for the element based on it's type"""
|
||||
@@ -247,6 +250,13 @@ class Collector(blenderbim.core.tool.Collector):
|
||||
if collection:
|
||||
return collection
|
||||
|
||||
if element.is_a("IfcSurfaceFeature") and element.file.schema == "IFC4X3":
|
||||
adherend = element.AdheresToElement[0].RelatingElement
|
||||
adherend_obj = tool.Ifc.get_object(adherend)
|
||||
collection = adherend_obj.BIMObjectProperties.collection
|
||||
if collection:
|
||||
return collection
|
||||
|
||||
if element.is_a("IfcProject"):
|
||||
return bpy.context.scene.collection
|
||||
|
||||
|
||||
@@ -240,7 +240,7 @@ class Drawing(blenderbim.core.tool.Drawing):
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def create_svg_sheet(cls, document, titleblock):
|
||||
def create_svg_sheet(cls, document: ifcopenshell.entity_instance, titleblock: str) -> str:
|
||||
sheet_builder = sheeter.SheetBuilder()
|
||||
sheet_builder.data_dir = bpy.context.scene.BIMProperties.data_dir
|
||||
uri = cls.get_document_uri(document, "LAYOUT")
|
||||
@@ -251,16 +251,16 @@ class Drawing(blenderbim.core.tool.Drawing):
|
||||
def add_drawings(cls, sheet):
|
||||
sheet_builder = sheeter.SheetBuilder()
|
||||
sheet_builder.data_dir = bpy.context.scene.BIMProperties.data_dir
|
||||
sheet_reference = None
|
||||
drawing_references = {}
|
||||
drawing_names = []
|
||||
for reference in cls.get_document_references(sheet):
|
||||
if reference.Description == "LAYOUT":
|
||||
sheet_reference = reference
|
||||
elif reference.Description == "DRAWING":
|
||||
reference_description = cls.get_reference_description(reference)
|
||||
if reference_description == "DRAWING":
|
||||
drawing_references[Path(reference.Location).stem] = reference
|
||||
drawing_names.append(Path(reference.Location).stem)
|
||||
for annotation in [e for e in tool.Ifc.get().by_type("IfcAnnotation") if e.ObjectType == "DRAWING"]:
|
||||
if annotation.Name in drawing_names:
|
||||
sheet_builder.add_drawing(sheet_reference, annotation, sheet)
|
||||
for drawing_annotation in [e for e in tool.Ifc.get().by_type("IfcAnnotation") if e.ObjectType == "DRAWING"]:
|
||||
if drawing_annotation.Name in drawing_names:
|
||||
sheet_builder.add_drawing(drawing_references[drawing_annotation.Name], drawing_annotation, sheet)
|
||||
|
||||
@classmethod
|
||||
def delete_collection(cls, collection):
|
||||
@@ -421,7 +421,7 @@ class Drawing(blenderbim.core.tool.Drawing):
|
||||
else:
|
||||
references = document.HasDocumentReferences
|
||||
for reference in references:
|
||||
if description and reference.Description != description:
|
||||
if description and cls.get_reference_description(reference) != description:
|
||||
continue
|
||||
location = cls.get_document_uri(reference)
|
||||
if location:
|
||||
@@ -823,7 +823,8 @@ class Drawing(blenderbim.core.tool.Drawing):
|
||||
continue
|
||||
|
||||
for reference in cls.get_document_references(sheet):
|
||||
if reference.Description in ("SHEET", "LAYOUT", "RASTER"):
|
||||
reference_description = cls.get_reference_description(reference)
|
||||
if reference_description in ("SHEET", "LAYOUT", "RASTER"):
|
||||
# These references are an internal detail and should not be visible to users
|
||||
continue
|
||||
new = props.sheets.add()
|
||||
@@ -836,7 +837,7 @@ class Drawing(blenderbim.core.tool.Drawing):
|
||||
new.identification = reference.Identification or ""
|
||||
|
||||
new.name = os.path.basename(reference.Location)
|
||||
new.reference_type = reference.Description
|
||||
new.reference_type = reference_description
|
||||
|
||||
@classmethod
|
||||
def get_active_sheet(cls, context):
|
||||
@@ -1568,9 +1569,28 @@ class Drawing(blenderbim.core.tool.Drawing):
|
||||
tree.write(uri, pretty_print=True, xml_declaration=True, encoding="utf-8")
|
||||
|
||||
@classmethod
|
||||
def get_reference_description(cls, reference):
|
||||
def get_reference_description(cls, reference: ifcopenshell.entity_instance) -> Union[str, None]:
|
||||
if reference.file.schema == "IFC2X3":
|
||||
return reference.Name
|
||||
return reference.Description
|
||||
|
||||
@classmethod
|
||||
def generate_reference_attributes(cls, reference: ifcopenshell.entity_instance, **attributes: Any) -> dict[str, Any]:
|
||||
"""will automatically convert attributes below for IFC2X3 compatibility:
|
||||
|
||||
- Identification -> ItemReference
|
||||
|
||||
- Description -> Name
|
||||
"""
|
||||
if reference.file.schema == "IFC2X3":
|
||||
if "Description" in attributes:
|
||||
attributes["Name"] = attributes["Description"]
|
||||
del attributes["Description"]
|
||||
if "Identification" in attributes:
|
||||
attributes["ItemReference"] = attributes["Identification"]
|
||||
del attributes["Identification"]
|
||||
return attributes
|
||||
|
||||
@classmethod
|
||||
def get_reference_location(cls, reference):
|
||||
return reference.Location
|
||||
|
||||
@@ -622,7 +622,7 @@ class Geometry(blenderbim.core.tool.Geometry):
|
||||
obj.data.BIMMeshProperties.material_checksum = str([s.id() for s in cls.get_styles(obj) if s])
|
||||
|
||||
@classmethod
|
||||
def record_object_position(cls, obj):
|
||||
def record_object_position(cls, obj: bpy.types.Object) -> None:
|
||||
# These are recorded separately because they have different numerical tolerances
|
||||
obj.BIMObjectProperties.location_checksum = repr(np.array(obj.matrix_world.translation).tobytes())
|
||||
obj.BIMObjectProperties.rotation_checksum = repr(np.array(obj.matrix_world.to_3x3()).tobytes())
|
||||
|
||||
@@ -38,6 +38,12 @@ class Material(blenderbim.core.tool.Material):
|
||||
def disable_editing_materials(cls):
|
||||
bpy.context.scene.BIMMaterialProperties.is_editing = False
|
||||
|
||||
@classmethod
|
||||
def duplicate_material(cls, material: ifcopenshell.entity_instance) -> ifcopenshell.entity_instance:
|
||||
new_material = ifcopenshell.util.element.copy_deep(tool.Ifc.get(), material)
|
||||
new_material.Name = material.Name + "_copy"
|
||||
return new_material
|
||||
|
||||
@classmethod
|
||||
def enable_editing_materials(cls):
|
||||
bpy.context.scene.BIMMaterialProperties.is_editing = True
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
# along with BlenderBIM Add-on. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import ifcopenshell
|
||||
import ifcopenshell.util.element
|
||||
import ifcopenshell.util.unit
|
||||
import ifcopenshell.util.placement
|
||||
import ifcopenshell.util.representation
|
||||
@@ -75,3 +76,9 @@ class Profile(blenderbim.core.tool.Profile):
|
||||
@classmethod
|
||||
def get_model_profiles(cls):
|
||||
return tool.Ifc.get().by_type("IfcProfileDef")
|
||||
|
||||
@classmethod
|
||||
def duplicate_profile(cls, profile: ifcopenshell.entity_instance) -> ifcopenshell.entity_instance:
|
||||
new_profile = ifcopenshell.util.element.copy_deep(tool.Ifc.get(), profile)
|
||||
new_profile.ProfileName = profile.ProfileName + "_copy"
|
||||
return new_profile
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
import bpy
|
||||
import numpy as np
|
||||
import ifcopenshell
|
||||
import ifcopenshell.util.element
|
||||
import blenderbim.core.tool
|
||||
import blenderbim.tool as tool
|
||||
import blenderbim.bim.helper
|
||||
@@ -59,6 +60,12 @@ class Style(blenderbim.core.tool.Style):
|
||||
def disable_editing_styles(cls):
|
||||
bpy.context.scene.BIMStylesProperties.is_editing = False
|
||||
|
||||
@classmethod
|
||||
def duplicate_style(cls, style: ifcopenshell.entity_instance) -> ifcopenshell.entity_instance:
|
||||
new_style = ifcopenshell.util.element.copy_deep(tool.Ifc.get(), style)
|
||||
new_style.Name = style.Name + "_copy"
|
||||
return new_style
|
||||
|
||||
@classmethod
|
||||
def enable_editing(cls, obj):
|
||||
obj.BIMStyleProperties.is_editing = True
|
||||
|
||||
@@ -103,6 +103,7 @@ For Linux or Mac:
|
||||
# Remove and link other IfcOpenShell utilities
|
||||
$ rm -r $BLENDER_ADDON_PATH/libs/site/packages/ifccsv.py
|
||||
$ rm -r $BLENDER_ADDON_PATH/libs/site/packages/ifcdiff.py
|
||||
$ rm -r $BLENDER_ADDON_PATH/libs/site/packages/bsdd.py
|
||||
$ rm -r $BLENDER_ADDON_PATH/libs/site/packages/ifc4d
|
||||
$ rm -r $BLENDER_ADDON_PATH/libs/site/packages/ifc5d
|
||||
$ rm -r $BLENDER_ADDON_PATH/libs/site/packages/ifccityjson
|
||||
@@ -110,9 +111,11 @@ For Linux or Mac:
|
||||
$ rm -r $BLENDER_ADDON_PATH/libs/site/packages/ifcpatch
|
||||
$ rm -r $BLENDER_ADDON_PATH/libs/site/packages/ifctester
|
||||
$ rm -r $BLENDER_ADDON_PATH/libs/site/packages/ifcfm
|
||||
$ rm -r $BLENDER_ADDON_PATH/libs/Desktop
|
||||
|
||||
$ ln -s $PWD/src/ifccsv/ifccsv.py $BLENDER_ADDON_PATH/libs/site/packages/ifccsv.py
|
||||
$ ln -s $PWD/src/ifcdiff/ifcdiff.py $BLENDER_ADDON_PATH/libs/site/packages/ifcdiff.py
|
||||
$ ln -s $PWD/src/bsdd/bsdd.py $BLENDER_ADDON_PATH/libs/site/packages/bsdd.py
|
||||
$ ln -s $PWD/src/ifc4d/ifc4d $BLENDER_ADDON_PATH/libs/site/packages/ifc4d
|
||||
$ ln -s $PWD/src/ifc5d/ifc5d $BLENDER_ADDON_PATH/libs/site/packages/ifc5d
|
||||
$ ln -s $PWD/src/ifccityjson/ifccityjson $BLENDER_ADDON_PATH/libs/site/packages/ifccityjson
|
||||
@@ -120,6 +123,7 @@ For Linux or Mac:
|
||||
$ ln -s $PWD/src/ifcpatch/ifcpatch $BLENDER_ADDON_PATH/libs/site/packages/ifcpatch
|
||||
$ ln -s $PWD/src/ifctester/ifctester $BLENDER_ADDON_PATH/libs/site/packages/ifctester
|
||||
$ ln -s $PWD/src/ifcfm/ifcfm $BLENDER_ADDON_PATH/libs/site/packages/ifcfm
|
||||
$ ln -s $PWD/src/blenderbim/blenderbim/libs/desktop $BLENDER_ADDON_PATH/libs/Desktop
|
||||
|
||||
# Manually download some third party dependencies
|
||||
$ cd $BLENDER_ADDON_PATH/bim/data/gantt
|
||||
@@ -168,6 +172,7 @@ Before running it follow the instructions descibed after `rem` tags.
|
||||
echo Remove and link other IfcOpenShell utilities...
|
||||
del "%blenderbim%\libs\site\packages\ifccsv.py"
|
||||
del "%blenderbim%\libs\site\packages\ifcdiff.py"
|
||||
del "%blenderbim%\libs\site\packages\bsdd.py"
|
||||
rd /S /Q "%blenderbim%\libs\site\packages\ifc4d"
|
||||
rd /S /Q "%blenderbim%\libs\site\packages\ifc5d"
|
||||
rd /S /Q "%blenderbim%\libs\site\packages\ifccityjson"
|
||||
@@ -175,9 +180,11 @@ Before running it follow the instructions descibed after `rem` tags.
|
||||
rd /S /Q "%blenderbim%\libs\site\packages\ifcpatch"
|
||||
rd /S /Q "%blenderbim%\libs\site\packages\ifctester"
|
||||
rd /S /Q "%blenderbim%\libs\site\packages\ifcfm"
|
||||
rd /S /Q "%blenderbim%\libs\desktop"
|
||||
|
||||
mklink "%blenderbim%\libs\site\packages\ifccsv.py" "%cd%\src\ifccsv\ifccsv.py"
|
||||
mklink "%blenderbim%\libs\site\packages\ifcdiff.py" "%cd%\src\ifcdiff\ifcdiff.py"
|
||||
mklink "%blenderbim%\libs\site\packages\bsdd.py" "%cd%\src\bsdd\bsdd.py"
|
||||
mklink /D "%blenderbim%\libs\site\packages\ifc4d" "%cd%\src\ifc4d\ifc4d"
|
||||
mklink /D "%blenderbim%\libs\site\packages\ifc5d" "%cd%\src\ifc5d\ifc5d"
|
||||
mklink /D "%blenderbim%\libs\site\packages\ifccityjson" "%cd%\src\ifccityjson\ifccityjson"
|
||||
@@ -185,6 +192,7 @@ Before running it follow the instructions descibed after `rem` tags.
|
||||
mklink /D "%blenderbim%\libs\site\packages\ifcpatch" "%cd%\src\ifcpatch\ifcpatch"
|
||||
mklink /D "%blenderbim%\libs\site\packages\ifctester" "%cd%\src\ifctester\ifctester"
|
||||
mklink /D "%blenderbim%\libs\site\packages\ifcfm" "%cd%\src\ifcfm\ifcfm"
|
||||
mklink /D "%blenderbim%\libs\desktop" "%cd%\src\blenderbim\blenderbim\libs\desktop"
|
||||
|
||||
echo Manually downloading some third party dependencies...
|
||||
curl https://raw.githubusercontent.com/jsGanttImproved/jsgantt-improved/master/dist/jsgantt.js -o "%blenderbim%\bim\data\gantt\jsgantt.js"
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
import json
|
||||
import pytest
|
||||
import blenderbim.core.tool
|
||||
from typing import Any, Self, Optional
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
@@ -241,12 +242,12 @@ def voider():
|
||||
class Prophecy:
|
||||
def __init__(self, cls):
|
||||
self.subject = cls
|
||||
self.predictions = []
|
||||
self.calls = []
|
||||
self.return_values = {}
|
||||
self.should_call = None
|
||||
self.predictions: list[dict] = []
|
||||
self.calls: list[dict] = []
|
||||
self.return_values: dict[str, Any] = {}
|
||||
self.should_call: Optional[dict] = None
|
||||
|
||||
def __getattr__(self, attr):
|
||||
def __getattr__(self, attr: str):
|
||||
if not hasattr(self.subject, attr):
|
||||
raise AttributeError(f"Prophecy {self.subject} has no attribute {attr}")
|
||||
|
||||
@@ -270,12 +271,12 @@ class Prophecy:
|
||||
self.predictions.append({"type": "SHOULD_BE_CALLED", "number": number, "call": self.should_call})
|
||||
return self
|
||||
|
||||
def will_return(self, value):
|
||||
def will_return(self, value: Any) -> Self:
|
||||
key = json.dumps(self.should_call, sort_keys=True)
|
||||
self.return_values[key] = value
|
||||
return self
|
||||
|
||||
def verify(self):
|
||||
def verify(self) -> None:
|
||||
predicted_calls = []
|
||||
for prediction in self.predictions:
|
||||
predicted_calls.append(prediction["call"])
|
||||
@@ -285,7 +286,7 @@ class Prophecy:
|
||||
if call not in predicted_calls:
|
||||
raise Exception(f"Unpredicted call: {call}")
|
||||
|
||||
def verify_should_be_called(self, prediction):
|
||||
def verify_should_be_called(self, prediction: dict) -> None:
|
||||
if prediction["number"]:
|
||||
count = self.calls.count(prediction["call"])
|
||||
if count != prediction["number"]:
|
||||
|
||||
@@ -95,15 +95,21 @@ class TestAddSheet:
|
||||
information="sheet",
|
||||
attributes={"Identification": "u_identification", "Name": "UNTITLED", "Scope": "SHEET"},
|
||||
).should_be_called()
|
||||
drawing.generate_reference_attributes(
|
||||
"reference", Location="layout_path", Description="LAYOUT"
|
||||
).should_be_called().will_return("attributes")
|
||||
ifc.run(
|
||||
"document.edit_reference",
|
||||
reference="reference",
|
||||
attributes={"Location": "layout_path", "Description": "LAYOUT"},
|
||||
attributes="attributes",
|
||||
).should_be_called()
|
||||
drawing.generate_reference_attributes(
|
||||
"reference", Location="titleblock_path", Description="TITLEBLOCK"
|
||||
).should_be_called().will_return("attributes2")
|
||||
ifc.run(
|
||||
"document.edit_reference",
|
||||
reference="reference",
|
||||
attributes={"Location": "titleblock_path", "Description": "TITLEBLOCK"},
|
||||
attributes="attributes2",
|
||||
).should_be_called()
|
||||
drawing.create_svg_sheet("sheet", "titleblock").should_be_called()
|
||||
drawing.import_sheets().should_be_called()
|
||||
@@ -122,15 +128,21 @@ class TestAddSheet:
|
||||
information="sheet",
|
||||
attributes={"DocumentId": "u_identification", "Name": "UNTITLED", "Scope": "SHEET"},
|
||||
).should_be_called()
|
||||
drawing.generate_reference_attributes(
|
||||
"reference", Location="layout_path", Description="LAYOUT"
|
||||
).should_be_called().will_return("attributes")
|
||||
ifc.run(
|
||||
"document.edit_reference",
|
||||
reference="reference",
|
||||
attributes={"Location": "layout_path", "Description": "LAYOUT"},
|
||||
attributes="attributes",
|
||||
).should_be_called()
|
||||
drawing.generate_reference_attributes(
|
||||
"reference", Location="titleblock_path", Description="TITLEBLOCK"
|
||||
).should_be_called().will_return("attributes2")
|
||||
ifc.run(
|
||||
"document.edit_reference",
|
||||
reference="reference",
|
||||
attributes={"Location": "titleblock_path", "Description": "TITLEBLOCK"},
|
||||
attributes="attributes2",
|
||||
).should_be_called()
|
||||
drawing.create_svg_sheet("sheet", "titleblock").should_be_called()
|
||||
drawing.import_sheets().should_be_called()
|
||||
@@ -490,7 +502,9 @@ class TestUpdateDrawingName:
|
||||
)
|
||||
ifc.resolve_uri("relative_layout_uri").should_be_called().will_return("absolute_layout_uri")
|
||||
drawing.does_file_exist("absolute_layout_uri").should_be_called().will_return(True)
|
||||
drawing.update_embedded_svg_location("absolute_layout_uri", "reference_with_old_location", "new_uri").should_be_called()
|
||||
drawing.update_embedded_svg_location(
|
||||
"absolute_layout_uri", "reference_with_old_location", "new_uri"
|
||||
).should_be_called()
|
||||
|
||||
drawing.is_editing_sheets().should_be_called().will_return(True)
|
||||
drawing.import_sheets().should_be_called()
|
||||
|
||||
@@ -62,7 +62,9 @@
|
||||
#include <STEPConstruct_PointHasher.hxx>
|
||||
#include "clash_utils.h"
|
||||
|
||||
#ifdef WITH_HDF5
|
||||
#include "H5Cpp.h"
|
||||
#endif
|
||||
|
||||
|
||||
namespace IfcGeom {
|
||||
@@ -1504,6 +1506,7 @@ namespace IfcGeom {
|
||||
}
|
||||
}
|
||||
|
||||
#ifdef WITH_HDF5
|
||||
void write_h5() {
|
||||
H5::H5File file("filename.h5", H5F_ACC_TRUNC);
|
||||
H5::Group shapes = file.createGroup("/shapes");
|
||||
@@ -1714,6 +1717,7 @@ namespace IfcGeom {
|
||||
colours_dataset.write(flat_colours.data(), H5::PredType::NATIVE_FLOAT);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
template <typename T>
|
||||
void apply_matrix_to_flat_verts(const std::vector<T>& flat_list, const std::vector<T>& matrix, std::vector<T>& result) {
|
||||
|
||||
@@ -39,6 +39,7 @@ import sys
|
||||
import tempfile
|
||||
import zipfile
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
|
||||
import ifcopenshell.util.file
|
||||
|
||||
@@ -197,12 +198,14 @@ def register_schema(schema):
|
||||
register_schema_attributes(schema.schema)
|
||||
|
||||
|
||||
def schema_by_name(schema=None, schema_version=None):
|
||||
def schema_by_name(
|
||||
schema: Optional[str] = None, schema_version: Optional[tuple[int, ...]] = None
|
||||
) -> ifcopenshell_wrapper.schema_definition:
|
||||
"""Returns an object allowing you to query the IFC schema itself
|
||||
|
||||
:param schema: Which IFC schema to use, chosen from "IFC2X3", "IFC4",
|
||||
or "IFC4X3". These refer to the ISO approved versions of IFC.
|
||||
:type schema: string
|
||||
:type schema: string, optional
|
||||
:param schema_version: If you want to specify an exact version of IFC
|
||||
that may not be an ISO approved version, use this argument instead
|
||||
of ``schema``. IFC versions on technical.buildingsmart.org are
|
||||
@@ -211,13 +214,15 @@ def schema_by_name(schema=None, schema_version=None):
|
||||
ADD2 TC1, which is the official version approved by ISO when people
|
||||
refer to "IFC4". Generally you should not use this argument unless
|
||||
you are testing non-ISO IFC releases.
|
||||
:type schema_version: tuple[int]
|
||||
:type schema_version: tuple[int, ...], optional
|
||||
:return: Schema definition object.
|
||||
:rtype: ifocpenshell_wrapper.schema_definition
|
||||
"""
|
||||
if schema_version:
|
||||
prefixes = ("IFC", "X", "_ADD", "_TC")
|
||||
schema = "".join("".join(map(str, t)) if t[1] else "" for t in zip(prefixes, schema_version))
|
||||
else:
|
||||
schema = {"IFC4X3": "IFC4X3_ADD1"}.get(schema, schema)
|
||||
schema = {"IFC4X3": "IFC4X3_ADD2"}.get(schema, schema)
|
||||
return ifcopenshell_wrapper.schema_by_name(schema)
|
||||
|
||||
|
||||
|
||||
@@ -15,10 +15,17 @@
|
||||
#
|
||||
# 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
|
||||
from typing import Any, Optional
|
||||
|
||||
|
||||
class Usecase:
|
||||
def __init__(self, file, information=None, attributes=None):
|
||||
def __init__(
|
||||
self,
|
||||
file: ifcopenshell.file,
|
||||
information: ifcopenshell.entity_instance,
|
||||
attributes: Optional[dict[str, Any]] = None,
|
||||
):
|
||||
"""Edits the attributes of an IfcDocumentInformation
|
||||
|
||||
For more information about the attributes and data types of an
|
||||
@@ -44,6 +51,6 @@ class Usecase:
|
||||
self.file = file
|
||||
self.settings = {"information": information, "attributes": attributes or {}}
|
||||
|
||||
def execute(self):
|
||||
def execute(self) -> None:
|
||||
for name, value in self.settings["attributes"].items():
|
||||
setattr(self.settings["information"], name, value)
|
||||
|
||||
@@ -15,10 +15,17 @@
|
||||
#
|
||||
# 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
|
||||
from typing import Any, Optional
|
||||
|
||||
|
||||
class Usecase:
|
||||
def __init__(self, file, reference=None, attributes=None):
|
||||
def __init__(
|
||||
self,
|
||||
file: ifcopenshell.file,
|
||||
reference: ifcopenshell.entity_instance,
|
||||
attributes: Optional[dict[str, Any]] = None,
|
||||
):
|
||||
"""Edits the attributes of an IfcDocumentReference
|
||||
|
||||
For more information about the attributes and data types of an
|
||||
@@ -47,6 +54,6 @@ class Usecase:
|
||||
self.file = file
|
||||
self.settings = {"reference": reference, "attributes": attributes or {}}
|
||||
|
||||
def execute(self):
|
||||
def execute(self) -> None:
|
||||
for name, value in self.settings["attributes"].items():
|
||||
setattr(self.settings["reference"], name, value)
|
||||
|
||||
@@ -17,12 +17,19 @@
|
||||
# along with IfcOpenShell. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import datetime
|
||||
import ifcopenshell.util.constraint
|
||||
import ifcopenshell.util.date
|
||||
import ifcopenshell.util.sequence
|
||||
from typing import Any, Optional
|
||||
|
||||
|
||||
class Usecase:
|
||||
def __init__(self, file, task_time=None, attributes=None):
|
||||
def __init__(
|
||||
self,
|
||||
file: ifcopenshell.file,
|
||||
task_time: ifcopenshell.entity_instance,
|
||||
attributes: Optional[dict[str, Any]] = None,
|
||||
):
|
||||
"""Edits the attributes of an IfcTaskTime
|
||||
|
||||
For more information about the attributes and data types of an
|
||||
@@ -55,7 +62,7 @@ class Usecase:
|
||||
self.file = file
|
||||
self.settings = {"task_time": task_time, "attributes": attributes or {}}
|
||||
|
||||
def execute(self):
|
||||
def execute(self) -> None:
|
||||
self.task = self.get_task()
|
||||
self.calendar = ifcopenshell.util.sequence.derive_calendar(self.task)
|
||||
|
||||
@@ -169,12 +176,12 @@ class Usecase:
|
||||
duration, "IfcDuration"
|
||||
)
|
||||
|
||||
def get_task(self):
|
||||
return [
|
||||
def get_task(self) -> ifcopenshell.entity_instance:
|
||||
return next(
|
||||
e
|
||||
for e in self.file.get_inverse(self.settings["task_time"])
|
||||
if e.is_a("IfcTask")
|
||||
][0]
|
||||
)
|
||||
|
||||
def handle_resource_calculation(self):
|
||||
resources = ifcopenshell.util.sequence.get_task_resources(self.task, is_deep=False)
|
||||
|
||||
@@ -17,10 +17,16 @@
|
||||
# along with IfcOpenShell. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import ifcopenshell.util.date
|
||||
from typing import Any, Optional
|
||||
|
||||
|
||||
class Usecase:
|
||||
def __init__(self, file, work_time=None, attributes=None):
|
||||
def __init__(
|
||||
self,
|
||||
file: ifcopenshell.file,
|
||||
work_time: ifcopenshell.entity_instance,
|
||||
attributes: Optional[dict[str, Any]] = None,
|
||||
):
|
||||
"""Edits the attributes of an IfcWorkTime
|
||||
|
||||
For more information about the attributes and data types of an
|
||||
@@ -53,13 +59,15 @@ class Usecase:
|
||||
self.file = file
|
||||
self.settings = {"work_time": work_time, "attributes": attributes or {}}
|
||||
|
||||
def execute(self):
|
||||
def execute(self) -> None:
|
||||
for name, value in self.settings["attributes"].items():
|
||||
if name in ("Start", "StartDate"):
|
||||
value = ifcopenshell.util.date.datetime2ifc(value, "IfcDate")
|
||||
# 4 IfcWorktime Start
|
||||
self.settings["work_time"][4] = value
|
||||
elif name in ("Finish", "FinishDate"):
|
||||
value = ifcopenshell.util.date.datetime2ifc(value, "IfcDate")
|
||||
# 5 IfcWorktime Finish
|
||||
self.settings["work_time"][5] = value
|
||||
else:
|
||||
setattr(self.settings["work_time"], name, value)
|
||||
|
||||
@@ -18,10 +18,11 @@
|
||||
|
||||
import ifcopenshell
|
||||
import ifcopenshell.util.unit
|
||||
from typing import Optional
|
||||
|
||||
|
||||
class Usecase:
|
||||
def __init__(self, file, name="foot", conversion_offset=None):
|
||||
def __init__(self, file: ifcopenshell.file, name: str = "foot", conversion_offset: Optional[float] = None):
|
||||
"""Add a conversion based unit
|
||||
|
||||
If you're in one of those countries who don't use SI units, you're
|
||||
@@ -41,7 +42,7 @@ class Usecase:
|
||||
that this is just an example and you don't actually need to specify
|
||||
that for fahrenheit as it's built into this API function. For
|
||||
advanced users only.
|
||||
:type conversion_offset: float
|
||||
:type conversion_offset: float, optional
|
||||
:return: The new IfcConversionBasedUnit or
|
||||
IfcConversionBasedUnitWithOffset
|
||||
:rtype: ifcopenshell.entity_instance.entity_instance
|
||||
@@ -60,7 +61,7 @@ class Usecase:
|
||||
self.file = file
|
||||
self.settings = {"name": name, "conversion_offset": conversion_offset}
|
||||
|
||||
def execute(self):
|
||||
def execute(self) -> ifcopenshell.entity_instance:
|
||||
unit_type = ifcopenshell.util.unit.imperial_types.get(self.settings["name"], "USERDEFINED")
|
||||
dimensions = ifcopenshell.util.unit.named_dimensions[unit_type]
|
||||
exponents = self.file.createIfcDimensionalExponents(*dimensions)
|
||||
|
||||
@@ -17,10 +17,11 @@
|
||||
# along with IfcOpenShell. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import ifcopenshell.util.unit
|
||||
from typing import Optional
|
||||
|
||||
|
||||
class Usecase:
|
||||
def __init__(self, file, unit_type="LENGTHUNIT", prefix=None):
|
||||
def __init__(self, file: ifcopenshell.file, unit_type: str = "LENGTHUNIT", prefix: Optional[str] = None):
|
||||
"""Add a new SI unit
|
||||
|
||||
The supported types are ABSORBEDDOSEUNIT, AMOUNTOFSUBSTANCEUNIT,
|
||||
@@ -59,7 +60,7 @@ class Usecase:
|
||||
self.file = file
|
||||
self.settings = {"unit_type": unit_type, "prefix": prefix}
|
||||
|
||||
def execute(self):
|
||||
def execute(self) -> ifcopenshell.entity_instance:
|
||||
name = ifcopenshell.util.unit.si_type_names.get(self.settings["unit_type"], None)
|
||||
return self.file.create_entity(
|
||||
"IfcSIUnit", UnitType=self.settings["unit_type"], Name=name, Prefix=self.settings["prefix"]
|
||||
|
||||
@@ -18,10 +18,18 @@
|
||||
|
||||
import ifcopenshell
|
||||
import ifcopenshell.util.unit
|
||||
from typing import Optional
|
||||
|
||||
|
||||
class Usecase:
|
||||
def __init__(self, file, units=None, length=None, area=None, volume=None):
|
||||
def __init__(
|
||||
self,
|
||||
file: ifcopenshell.file,
|
||||
units: Optional[list[ifcopenshell.entity_instance]] = None,
|
||||
length: Optional[dict] = None,
|
||||
area: Optional[dict] = None,
|
||||
volume: Optional[dict] = None,
|
||||
):
|
||||
"""Assign default project units
|
||||
|
||||
Whenever a unitised quantity is specified, such as a length, area,
|
||||
@@ -67,7 +75,7 @@ class Usecase:
|
||||
self.settings["area"] = area or {"is_metric": True, "raw": "METERS"}
|
||||
self.settings["volume"] = volume or {"is_metric": True, "raw": "METERS"}
|
||||
|
||||
def execute(self):
|
||||
def execute(self) -> ifcopenshell.entity_instance:
|
||||
# We're going to refactor this to split unit creation and assignment
|
||||
if self.settings["units"]:
|
||||
units = self.settings["units"]
|
||||
@@ -84,7 +92,7 @@ class Usecase:
|
||||
self.assign_units(unit_assignment, units)
|
||||
return unit_assignment
|
||||
|
||||
def get_unit_assignment(self):
|
||||
def get_unit_assignment(self) -> ifcopenshell.entity_instance:
|
||||
unit_assignment = self.file.by_type("IfcUnitAssignment")
|
||||
if unit_assignment:
|
||||
unit_assignment = unit_assignment[0]
|
||||
@@ -97,13 +105,15 @@ class Usecase:
|
||||
self.file.by_type("IfcContext")[0].UnitsInContext = unit_assignment
|
||||
return unit_assignment
|
||||
|
||||
def assign_units(self, unit_assignment, new_units):
|
||||
def assign_units(
|
||||
self, unit_assignment: ifcopenshell.entity_instance, new_units: list[ifcopenshell.entity_instance]
|
||||
) -> None:
|
||||
units = set(unit_assignment.Units or [])
|
||||
for unit in new_units:
|
||||
units.add(unit)
|
||||
unit_assignment.Units = list(units)
|
||||
|
||||
def create_metric_unit(self, unit_type, data):
|
||||
def create_metric_unit(self, unit_type: str, data: dict) -> ifcopenshell.entity_instance:
|
||||
type_prefix = ""
|
||||
if unit_type == "area":
|
||||
type_prefix = "SQUARE_"
|
||||
@@ -116,7 +126,7 @@ class Usecase:
|
||||
type_prefix + ifcopenshell.util.unit.get_unit_name(data["raw"]),
|
||||
)
|
||||
|
||||
def create_imperial_unit(self, unit_type, data):
|
||||
def create_imperial_unit(self, unit_type: str, data: dict) -> ifcopenshell.entity_instance:
|
||||
if unit_type == "length":
|
||||
dimensional_exponents = self.file.createIfcDimensionalExponents(1, 0, 0, 0, 0, 0, 0)
|
||||
name_prefix = ""
|
||||
|
||||
@@ -15,10 +15,12 @@
|
||||
#
|
||||
# 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
|
||||
from typing import Optional
|
||||
|
||||
|
||||
class Usecase:
|
||||
def __init__(self, file, units=None):
|
||||
def __init__(self, file: ifcopenshell.file, units: Optional[list[ifcopenshell.entity_instance]] = None):
|
||||
"""Unassigns units as default units for the project
|
||||
|
||||
:param units: A list of units to assign as project defaults.
|
||||
|
||||
@@ -276,7 +276,15 @@ class entity_instance(object):
|
||||
|
||||
def __setattr__(self, key: str, value: Any) -> None:
|
||||
index = self.wrapped_data.get_argument_index(key)
|
||||
self[index] = value
|
||||
try:
|
||||
self[index] = value
|
||||
except IndexError as e:
|
||||
# get_argument_index returns 0xFFFFFFFF if attribute is not found
|
||||
if index == 0xFFFFFFFF:
|
||||
raise AttributeError(
|
||||
"entity instance of type '%s' has no attribute '%s'" % (self.wrapped_data.is_a(True), key)
|
||||
)
|
||||
raise e
|
||||
|
||||
def __getitem__(self, key: int) -> Any:
|
||||
if key < 0 or key >= len(self):
|
||||
|
||||
@@ -28,7 +28,7 @@ import numbers
|
||||
import zipfile
|
||||
import functools
|
||||
from pathlib import Path
|
||||
from typing import List, Optional
|
||||
from typing import Optional, Any
|
||||
|
||||
import ifcopenshell.util.element
|
||||
import ifcopenshell.util.file
|
||||
@@ -52,7 +52,7 @@ class Transaction:
|
||||
self.batch_delete_ids = set()
|
||||
self.batch_inverses = []
|
||||
|
||||
def serialise_entity_instance(self, element):
|
||||
def serialise_entity_instance(self, element: ifcopenshell.entity_instance) -> dict[str, Any]:
|
||||
info = element.get_info()
|
||||
for key, value in info.items():
|
||||
info[key] = self.serialise_value(element, value)
|
||||
@@ -103,7 +103,7 @@ class Transaction:
|
||||
}
|
||||
)
|
||||
|
||||
def store_delete(self, element):
|
||||
def store_delete(self, element: ifcopenshell.entity_instance) -> None:
|
||||
inverses = {}
|
||||
if self.is_batched:
|
||||
if element.id() not in self.batch_delete_ids:
|
||||
@@ -259,7 +259,7 @@ class file(object):
|
||||
self.history_size = 64
|
||||
self.history = []
|
||||
self.future = []
|
||||
self.transaction = None
|
||||
self.transaction: Optional[Transaction] = None
|
||||
|
||||
import weakref
|
||||
|
||||
|
||||
@@ -20,8 +20,10 @@ import json
|
||||
from pathlib import Path
|
||||
import copy
|
||||
import ifcopenshell
|
||||
import ifcopenshell.ifcopenshell_wrapper as ifcopenshell_wrapper
|
||||
import ifcopenshell.util.attribute
|
||||
import ifcopenshell.util.schema
|
||||
from typing import Optional, Literal
|
||||
|
||||
try:
|
||||
import glob
|
||||
@@ -58,8 +60,8 @@ IFC4x3_SPEC_URL_TEMPLATE = "https://ifc43-docs.standards.buildingsmart.org/IFC/R
|
||||
# child -> description
|
||||
# note: in IFC4x3 there is no children[] for properties
|
||||
|
||||
|
||||
SCHEMA_FILES = {
|
||||
SUPPORTED_SCHEMA = Literal["IFC2X3", "IFC4", "IFC4X3"]
|
||||
SCHEMA_FILES: dict[SUPPORTED_SCHEMA, dict] = {
|
||||
"IFC2X3": {
|
||||
"entities": BASE_MODULE_PATH / "schema/ifc2x3_entities.json",
|
||||
"properties": BASE_MODULE_PATH / "schema/ifc2x3_properties.json",
|
||||
@@ -81,7 +83,11 @@ SCHEMA_FILES = {
|
||||
}
|
||||
|
||||
db = None
|
||||
schema_by_name = {"IFC2X3": None, "IFC4": None, "IFC4X3": None}
|
||||
schema_by_name: dict[SUPPORTED_SCHEMA, Optional[ifcopenshell_wrapper.schema_definition]] = {
|
||||
"IFC2X3": None,
|
||||
"IFC4": None,
|
||||
"IFC4X3": None,
|
||||
}
|
||||
|
||||
|
||||
def get_db(version):
|
||||
@@ -103,11 +109,12 @@ def get_db(version):
|
||||
return db.get(version)
|
||||
|
||||
|
||||
def get_schema_by_name(version: str):
|
||||
def get_schema_by_name(version: str) -> ifcopenshell_wrapper.schema_definition:
|
||||
global schema_by_name
|
||||
version = ifcopenshell.util.schema.get_fallback_schema(version)
|
||||
if not schema_by_name[version]:
|
||||
schema_by_name[version] = ifcopenshell.ifcopenshell_wrapper.schema_by_name(version)
|
||||
schema_name = "IFC4X3_ADD2" if version == "IFC4X3" else version
|
||||
schema_by_name[version] = ifcopenshell_wrapper.schema_by_name(schema_name)
|
||||
return schema_by_name[version]
|
||||
|
||||
|
||||
|
||||
@@ -17,10 +17,13 @@
|
||||
# along with IfcOpenShell. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import numpy as np
|
||||
import numpy.typing as npt
|
||||
import ifcopenshell
|
||||
from typing import Literal, Iterable
|
||||
|
||||
MatrixType = np.ndarray[np.ndarray[float]]
|
||||
|
||||
MatrixType = npt.NDArray[np.float64]
|
||||
"""`npt.NDArray[np.float64]`"""
|
||||
|
||||
|
||||
def a2p(o: Iterable[float], z: Iterable[float], x: Iterable[float]) -> MatrixType:
|
||||
@@ -36,7 +39,7 @@ def a2p(o: Iterable[float], z: Iterable[float], x: Iterable[float]) -> MatrixTyp
|
||||
:param x: The +X vector / axis of the matrix
|
||||
:type x: iterable[float]
|
||||
:return: A 4x4 numpy matrix
|
||||
:rtype: np.ndarray[np.ndarray[float]]
|
||||
:rtype: MatrixType
|
||||
"""
|
||||
x = x / np.linalg.norm(x)
|
||||
z = z / np.linalg.norm(z)
|
||||
@@ -59,7 +62,7 @@ def get_axis2placement(placement: ifcopenshell.entity_instance) -> MatrixType:
|
||||
:param placement: The IfcLocalPlacement enitity
|
||||
:type placement: ifcopenshell.entity_instance.entity_instance
|
||||
:return: A 4x4 numpy matrix
|
||||
:rtype: np.ndarray[np.ndarray[float]]
|
||||
:rtype: MatrixType
|
||||
"""
|
||||
ifc_class = placement.is_a()
|
||||
if ifc_class in ("IfcAxis2Placement3D", "IfcAxis2PlacementLinear"):
|
||||
@@ -69,10 +72,10 @@ def get_axis2placement(placement: ifcopenshell.entity_instance) -> MatrixType:
|
||||
if coordinates := getattr(location, "Coordinates", None):
|
||||
o = coordinates
|
||||
else:
|
||||
ifc_class = location.is_a("IfcPointByDistanceExpression")
|
||||
ifc_class = location.is_a()
|
||||
print(
|
||||
f'WARNING. Placement location of type "{ifc_class}" '
|
||||
f'is not yet supported and placement {placement} may be placed incorrectly.'
|
||||
f"is not yet supported and placement {placement} may be placed incorrectly."
|
||||
)
|
||||
o = (0.0, 0.0, 0.0)
|
||||
|
||||
@@ -117,14 +120,14 @@ def get_local_placement(placement: ifcopenshell.entity_instance) -> MatrixType:
|
||||
:param placement: The IfcLocalPlacement entity
|
||||
:type placement: ifcopenshell.entity_instance.entity_instance
|
||||
:return: A 4x4 numpy matrix
|
||||
:rtype: np.ndarray[np.ndarray[float]]
|
||||
:rtype: MatrixType
|
||||
"""
|
||||
if placement is None:
|
||||
return np.eye(4)
|
||||
if placement.PlacementRelTo is None:
|
||||
if (rel_to := placement.PlacementRelTo) is None:
|
||||
parent = np.eye(4)
|
||||
else:
|
||||
parent = get_local_placement(placement.PlacementRelTo)
|
||||
parent = get_local_placement(rel_to)
|
||||
return np.dot(parent, get_axis2placement(placement.RelativePlacement))
|
||||
|
||||
|
||||
@@ -137,7 +140,7 @@ def get_cartesiantransformationoperator3d(inst: ifcopenshell.entity_instance) ->
|
||||
:param item: The IfcCartesianTransformationOperator entity
|
||||
:type item: ifcopenshell.entity_instance.entity_instance
|
||||
:return: A 4x4 numpy transformation matrix
|
||||
:rtype: np.ndarray[np.ndarray[float]]
|
||||
:rtype: MatrixType
|
||||
"""
|
||||
origin = np.array(inst.LocalOrigin.Coordinates)
|
||||
axis1 = np.array((1.0, 0.0, 0.0))
|
||||
@@ -183,7 +186,7 @@ def get_mappeditem_transformation(item: ifcopenshell.entity_instance) -> MatrixT
|
||||
:param item: The IfcMappedItem entity
|
||||
:type item: ifcopenshell.entity_instance.entity_instance
|
||||
:return: A 4x4 numpy transformation matrix
|
||||
:rtype: np.ndarray[np.ndarray[float]]
|
||||
:rtype: MatrixType
|
||||
"""
|
||||
m4 = get_axis2placement(item.MappingSource.MappingOrigin)
|
||||
# TODO 2d
|
||||
@@ -219,7 +222,7 @@ def rotation(angle: float, axis: Literal["X", "Y", "Z"], is_degrees=True) -> Mat
|
||||
radians. Defaults to true (i.e. degrees).
|
||||
:type is_degrees: bool
|
||||
:return: A 4x4 numpy rotation matrix
|
||||
:rtype: np.ndarray[np.ndarray[float]]
|
||||
:rtype: MatrixType
|
||||
"""
|
||||
theta = np.radians(angle) if is_degrees else angle
|
||||
cos, sin = np.cos(theta), np.sin(theta)
|
||||
|
||||
@@ -20,7 +20,20 @@ import datetime
|
||||
import ifcopenshell.util.date
|
||||
from math import floor
|
||||
from functools import lru_cache
|
||||
from collections import namedtuple
|
||||
from typing import Union, Literal, Optional, Iterator
|
||||
|
||||
|
||||
DURATION_TYPE = Literal["ELAPSEDTIME", "WORKTIME", "NOTDEFINED"]
|
||||
RECURRENCE_TYPE = Literal[
|
||||
"BY_DAY_COUNT",
|
||||
"BY_WEEKDAY_COUNT",
|
||||
"DAILY",
|
||||
"MONTHLY_BY_DAY_OF_MONTH",
|
||||
"MONTHLY_BY_POSITION",
|
||||
"WEEKLY",
|
||||
"YEARLY_BY_DAY_OF_MONTH",
|
||||
"YEARLY_BY_POSITION",
|
||||
]
|
||||
|
||||
|
||||
def derive_date(task, attribute_name, date=None, is_earliest=False, is_latest=False):
|
||||
@@ -49,7 +62,7 @@ def derive_date(task, attribute_name, date=None, is_earliest=False, is_latest=Fa
|
||||
return date
|
||||
|
||||
|
||||
def derive_calendar(task):
|
||||
def derive_calendar(task: ifcopenshell.entity_instance) -> Union[ifcopenshell.entity_instance, None]:
|
||||
calendar = get_calendar(task)
|
||||
if calendar:
|
||||
return calendar
|
||||
@@ -57,7 +70,7 @@ def derive_calendar(task):
|
||||
return derive_calendar(rel.RelatingObject)
|
||||
|
||||
|
||||
def get_calendar(task):
|
||||
def get_calendar(task: ifcopenshell.entity_instance) -> Union[ifcopenshell.entity_instance, None]:
|
||||
calendar = [
|
||||
rel.RelatingControl
|
||||
for rel in task.HasAssignments or []
|
||||
@@ -68,7 +81,7 @@ def get_calendar(task):
|
||||
return calendar[0]
|
||||
|
||||
|
||||
def count_working_days(start, finish, calendar):
|
||||
def count_working_days(start, finish, calendar: ifcopenshell.entity_instance) -> int:
|
||||
result = 0
|
||||
if start == finish:
|
||||
return 0
|
||||
@@ -88,7 +101,11 @@ def count_working_days(start, finish, calendar):
|
||||
|
||||
|
||||
def get_start_or_finish_date(
|
||||
start, duration, duration_type, calendar, date_type="FINISH"
|
||||
start,
|
||||
duration,
|
||||
duration_type: DURATION_TYPE,
|
||||
calendar: ifcopenshell.entity_instance,
|
||||
date_type: Literal["START", "FINISH"] = "FINISH",
|
||||
):
|
||||
if not duration.days:
|
||||
# Typically a milestone will have zero duration, so the start == finish
|
||||
@@ -107,7 +124,7 @@ def get_start_or_finish_date(
|
||||
return datetime.datetime.combine(result, datetime.time(17))
|
||||
|
||||
|
||||
def offset_date(start, duration, duration_type, calendar):
|
||||
def offset_date(start, duration, duration_type: DURATION_TYPE, calendar: ifcopenshell.entity_instance):
|
||||
current_date = start
|
||||
months = getattr(duration, "months", 0)
|
||||
years = getattr(duration, "years", 0)
|
||||
@@ -129,7 +146,7 @@ def offset_date(start, duration, duration_type, calendar):
|
||||
return current_date
|
||||
|
||||
|
||||
def get_soonest_working_day(start, duration_type, calendar):
|
||||
def get_soonest_working_day(start, duration_type: DURATION_TYPE, calendar: ifcopenshell.entity_instance):
|
||||
if duration_type == "ELAPSEDTIME" or not is_calendar_applicable(start, calendar):
|
||||
return start
|
||||
while not is_working_day(start, calendar):
|
||||
@@ -139,7 +156,7 @@ def get_soonest_working_day(start, duration_type, calendar):
|
||||
return start
|
||||
|
||||
|
||||
def get_recent_working_day(start, duration_type, calendar):
|
||||
def get_recent_working_day(start, duration_type: DURATION_TYPE, calendar: ifcopenshell.entity_instance):
|
||||
if duration_type == "ELAPSEDTIME" or not is_calendar_applicable(start, calendar):
|
||||
return start
|
||||
while not is_working_day(start, calendar):
|
||||
@@ -150,7 +167,7 @@ def get_recent_working_day(start, duration_type, calendar):
|
||||
|
||||
|
||||
@lru_cache(maxsize=None)
|
||||
def is_working_day(day, calendar):
|
||||
def is_working_day(day, calendar: ifcopenshell.entity_instance) -> bool:
|
||||
is_working_day = False
|
||||
for work_time in calendar.WorkingTimes or []:
|
||||
if is_work_time_applicable_to_day(work_time, day):
|
||||
@@ -166,7 +183,7 @@ def is_working_day(day, calendar):
|
||||
|
||||
|
||||
@lru_cache(maxsize=None)
|
||||
def is_calendar_applicable(day, calendar):
|
||||
def is_calendar_applicable(day, calendar: ifcopenshell.entity_instance) -> bool:
|
||||
if not calendar or not calendar.WorkingTimes:
|
||||
return False
|
||||
is_applicable = False
|
||||
@@ -177,18 +194,20 @@ def is_calendar_applicable(day, calendar):
|
||||
return is_applicable
|
||||
|
||||
|
||||
def is_day_in_work_time(day, work_time):
|
||||
def is_day_in_work_time(day, work_time: ifcopenshell.entity_instance) -> bool:
|
||||
is_day_in_work_time = True
|
||||
if isinstance(day, datetime.datetime):
|
||||
day = datetime.date(day.year, day.month, day.day)
|
||||
if work_time[4]:
|
||||
start = ifcopenshell.util.date.ifc2datetime(work_time[4])
|
||||
# 4 IfcWorktime Start
|
||||
if start := work_time[4]:
|
||||
start = ifcopenshell.util.date.ifc2datetime(start)
|
||||
if day > start:
|
||||
is_day_in_work_time = True
|
||||
else:
|
||||
is_day_in_work_time = False
|
||||
if work_time[5]:
|
||||
finish = ifcopenshell.util.date.ifc2datetime(work_time[5])
|
||||
# 5 IfcWorktime Finish
|
||||
if finish := work_time[5]:
|
||||
finish = ifcopenshell.util.date.ifc2datetime(finish)
|
||||
if day < finish:
|
||||
is_day_in_work_time = True
|
||||
else:
|
||||
@@ -196,7 +215,7 @@ def is_day_in_work_time(day, work_time):
|
||||
return is_day_in_work_time
|
||||
|
||||
|
||||
def is_work_time_applicable_to_day(work_time, day):
|
||||
def is_work_time_applicable_to_day(work_time: ifcopenshell.entity_instance, day) -> bool:
|
||||
if not is_day_in_work_time(day, work_time):
|
||||
return False
|
||||
if not work_time.RecurrencePattern:
|
||||
@@ -205,36 +224,39 @@ def is_work_time_applicable_to_day(work_time, day):
|
||||
if isinstance(day, datetime.datetime):
|
||||
day = datetime.date(day.year, day.month, day.day)
|
||||
recurrence = work_time.RecurrencePattern
|
||||
if recurrence.RecurrenceType == "DAILY":
|
||||
recurrence_type: RECURRENCE_TYPE = recurrence.RecurrenceType
|
||||
if recurrence_type == "DAILY":
|
||||
if not recurrence.Interval and not recurrence.Occurrences:
|
||||
return True
|
||||
# 4 IfcWorktime Start
|
||||
if not work_time[4]:
|
||||
return False
|
||||
return False # TODO
|
||||
elif recurrence.RecurrenceType == "WEEKLY":
|
||||
elif recurrence_type == "WEEKLY":
|
||||
if not recurrence.Interval and not recurrence.Occurrences:
|
||||
return (day.weekday() + 1) in recurrence.WeekdayComponent
|
||||
# 4 IfcWorktime Start
|
||||
if not work_time[4]:
|
||||
return False
|
||||
return False # TODO
|
||||
elif recurrence.RecurrenceType == "MONTHLY_BY_DAY_OF_MONTH":
|
||||
elif recurrence_type == "MONTHLY_BY_DAY_OF_MONTH":
|
||||
if not recurrence.Interval and not recurrence.Occurrences:
|
||||
return day.day in recurrence.DayComponent
|
||||
return False # TODO
|
||||
elif recurrence.RecurrenceType == "MONTHLY_BY_POSITION":
|
||||
elif recurrence_type == "MONTHLY_BY_POSITION":
|
||||
if not recurrence.Interval and not recurrence.Occurrences:
|
||||
return (day.weekday() + 1) in recurrence.WeekdayComponent and floor(
|
||||
day.day / 7
|
||||
) + 1 == recurrence["Position"]
|
||||
return False # TODO
|
||||
elif recurrence.RecurrenceType == "YEARLY_BY_DAY_OF_MONTH":
|
||||
elif recurrence_type == "YEARLY_BY_DAY_OF_MONTH":
|
||||
if not recurrence.Interval and not recurrence.Occurrences:
|
||||
return (
|
||||
day.month in recurrence.MonthComponent
|
||||
and day.day in recurrence.DayComponent
|
||||
)
|
||||
return False # TODO
|
||||
elif recurrence.RecurrenceType == "YEARLY_BY_POSITION":
|
||||
elif recurrence_type == "YEARLY_BY_POSITION":
|
||||
if not recurrence.Interval and not recurrence.Occurrences:
|
||||
return (
|
||||
day.month in recurrence.MonthComponent
|
||||
@@ -244,7 +266,7 @@ def is_work_time_applicable_to_day(work_time, day):
|
||||
return False # TODO
|
||||
|
||||
|
||||
def get_task_work_schedule(task):
|
||||
def get_task_work_schedule(task: ifcopenshell.entity_instance) -> Union[ifcopenshell.entity_instance, None]:
|
||||
parent_task = get_parent_task(task)
|
||||
if parent_task:
|
||||
return get_task_work_schedule(parent_task) or get_task_work_schedule(task)
|
||||
@@ -257,25 +279,23 @@ def get_task_work_schedule(task):
|
||||
return None
|
||||
|
||||
|
||||
def get_nested_tasks(task):
|
||||
def get_nested_tasks(task: ifcopenshell.entity_instance) -> list[ifcopenshell.entity_instance]:
|
||||
return [object for rel in task.IsNestedBy or [] for object in rel.RelatedObjects]
|
||||
|
||||
|
||||
def get_parent_task(task):
|
||||
return (
|
||||
task.Nests[0].RelatingObject
|
||||
if task.Nests and task.Nests[0].RelatingObject.is_a("IfcTask")
|
||||
else None
|
||||
)
|
||||
def get_parent_task(task: ifcopenshell.entity_instance) -> Union[ifcopenshell.entity_instance, None]:
|
||||
nests = task.Nests
|
||||
if nests and (obj := nests[0].RelatingObject).is_a("IfcTask"):
|
||||
return obj
|
||||
|
||||
|
||||
def get_all_nested_tasks(task):
|
||||
def get_all_nested_tasks(task: ifcopenshell.entity_instance) -> Iterator[ifcopenshell.entity_instance]:
|
||||
for nested_task in get_nested_tasks(task):
|
||||
yield nested_task
|
||||
yield from get_all_nested_tasks(nested_task)
|
||||
|
||||
|
||||
def get_work_schedule_tasks(work_schedule):
|
||||
def get_work_schedule_tasks(work_schedule: ifcopenshell.entity_instance) -> list[ifcopenshell.entity_instance]:
|
||||
tasks = []
|
||||
for root_task in get_root_tasks(work_schedule):
|
||||
nested_tasks = get_all_nested_tasks(root_task)
|
||||
@@ -283,7 +303,7 @@ def get_work_schedule_tasks(work_schedule):
|
||||
return tasks
|
||||
|
||||
|
||||
def get_root_tasks(work_schedule):
|
||||
def get_root_tasks(work_schedule: ifcopenshell.entity_instance) -> list[ifcopenshell.entity_instance]:
|
||||
return [
|
||||
obj
|
||||
for rel in work_schedule.Controls
|
||||
@@ -292,7 +312,7 @@ def get_root_tasks(work_schedule):
|
||||
]
|
||||
|
||||
|
||||
def get_root_tasks_ids(work_schedule):
|
||||
def get_root_tasks_ids(work_schedule: ifcopenshell.entity_instance) -> list[int]:
|
||||
return [
|
||||
obj.id()
|
||||
for rel in work_schedule.Controls
|
||||
@@ -301,7 +321,7 @@ def get_root_tasks_ids(work_schedule):
|
||||
]
|
||||
|
||||
|
||||
def guess_date_range(work_schedule):
|
||||
def guess_date_range(work_schedule: ifcopenshell.entity_instance):
|
||||
earliest = None
|
||||
latest = None
|
||||
root_tasks = get_root_tasks(work_schedule)
|
||||
@@ -323,7 +343,7 @@ def guess_date_range(work_schedule):
|
||||
return earliest, latest
|
||||
|
||||
|
||||
def get_direct_task_outputs(task):
|
||||
def get_direct_task_outputs(task: ifcopenshell.entity_instance) -> list[ifcopenshell.entity_instance]:
|
||||
return [
|
||||
rel.RelatingProduct
|
||||
for rel in task.HasAssignments
|
||||
@@ -331,7 +351,7 @@ def get_direct_task_outputs(task):
|
||||
]
|
||||
|
||||
|
||||
def get_task_outputs(task, is_deep=False):
|
||||
def get_task_outputs(task: ifcopenshell.entity_instance, is_deep=False):
|
||||
if not is_deep:
|
||||
return get_direct_task_outputs(task)
|
||||
else:
|
||||
@@ -342,7 +362,7 @@ def get_task_outputs(task, is_deep=False):
|
||||
]
|
||||
|
||||
|
||||
def get_task_inputs(task, is_deep=False):
|
||||
def get_task_inputs(task: ifcopenshell.entity_instance, is_deep=False):
|
||||
if not is_deep:
|
||||
return [
|
||||
object
|
||||
@@ -365,7 +385,7 @@ def get_task_inputs(task, is_deep=False):
|
||||
]
|
||||
|
||||
|
||||
def get_task_resources(task, is_deep=False):
|
||||
def get_task_resources(task: ifcopenshell.entity_instance, is_deep=False):
|
||||
if not is_deep:
|
||||
return [
|
||||
object
|
||||
@@ -388,15 +408,17 @@ def get_task_resources(task, is_deep=False):
|
||||
]
|
||||
|
||||
|
||||
def has_task_outputs(task):
|
||||
def has_task_outputs(task: ifcopenshell.entity_instance) -> bool:
|
||||
return len(get_task_outputs(task)) > 0
|
||||
|
||||
|
||||
def has_task_inputs(task):
|
||||
def has_task_inputs(task: ifcopenshell.entity_instance) -> bool:
|
||||
return len(get_task_inputs(task)) > 0
|
||||
|
||||
|
||||
def get_tasks_for_product(product, schedule=None):
|
||||
def get_tasks_for_product(
|
||||
product: ifcopenshell.entity_instance, schedule: Optional[ifcopenshell.entity_instance] = None
|
||||
) -> tuple[list[ifcopenshell.entity_instance], list[ifcopenshell.entity_instance]]:
|
||||
"""
|
||||
Get all tasks assigned to or referenced by the given product.
|
||||
|
||||
@@ -438,7 +460,7 @@ def get_tasks_for_product(product, schedule=None):
|
||||
return inputs, outputs
|
||||
|
||||
|
||||
def get_sequence_assignment(task, sequence="successor"):
|
||||
def get_sequence_assignment(task: ifcopenshell.entity_instance, sequence="successor"):
|
||||
if sequence == "successor":
|
||||
relationship_attr = "IsPredecessorTo"
|
||||
elif sequence == "predecessor":
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
import shapely
|
||||
import shapely.ops
|
||||
import numpy as np
|
||||
import numpy.typing as npt
|
||||
import ifcopenshell.util.element
|
||||
import ifcopenshell.util.placement
|
||||
import ifcopenshell.util.representation
|
||||
@@ -28,6 +29,9 @@ tol = 1e-6
|
||||
AXIS_LITERAL = Literal["X", "Y", "Z"]
|
||||
VECTOR_3D = tuple[float, float, float]
|
||||
|
||||
MatrixType = npt.NDArray[np.float64]
|
||||
"""`npt.NDArray[np.float64]`"""
|
||||
|
||||
|
||||
def is_x(value: float, x: float, tolerance: Optional[float] = None) -> bool:
|
||||
"""Checks whether a value is equivalent to X given a tolerance
|
||||
@@ -113,19 +117,19 @@ def get_z(geometry) -> float:
|
||||
return max(z_values) - min(z_values)
|
||||
|
||||
|
||||
def get_shape_matrix(shape) -> np.ndarray:
|
||||
def get_shape_matrix(shape) -> MatrixType:
|
||||
"""Formats the transformation matrix of a shape as a 4x4 numpy array
|
||||
|
||||
:param shape: Shape output calculated by IfcOpenShell
|
||||
:type shape: shape
|
||||
:return: A 4x4 numpy array representing the transformation matrix
|
||||
:rtype: np.array
|
||||
:rtype: MatrixType
|
||||
"""
|
||||
m = shape.transformation.matrix.data
|
||||
return np.array(([m[0], m[3], m[6], m[9]], [m[1], m[4], m[7], m[10]], [m[2], m[5], m[8], m[11]], [0, 0, 0, 1]))
|
||||
|
||||
|
||||
def get_bbox_centroid(geometry) -> tuple[float]:
|
||||
def get_bbox_centroid(geometry) -> tuple[float, float, float]:
|
||||
"""Calculates the bounding box centroid of the geometry
|
||||
|
||||
The centroid is in local coordinates relative to the object's placement.
|
||||
@@ -133,11 +137,14 @@ def get_bbox_centroid(geometry) -> tuple[float]:
|
||||
:param geometry: Geometry output calculated by IfcOpenShell
|
||||
:type geometry: geometry
|
||||
:return: A tuple representing the XYZ centroid
|
||||
:rtype: tuple[float]
|
||||
:rtype: tuple[float, float, float]
|
||||
"""
|
||||
x_values = [geometry.verts[i] for i in range(0, len(geometry.verts), 3)]
|
||||
y_values = [geometry.verts[i + 1] for i in range(0, len(geometry.verts), 3)]
|
||||
z_values = [geometry.verts[i + 2] for i in range(0, len(geometry.verts), 3)]
|
||||
x_values: list[float]
|
||||
y_values: list[float]
|
||||
z_values: list[float]
|
||||
minx = min(x_values)
|
||||
maxx = max(x_values)
|
||||
miny = min(y_values)
|
||||
@@ -147,7 +154,7 @@ def get_bbox_centroid(geometry) -> tuple[float]:
|
||||
return (minx + ((maxx - minx) / 2), miny + ((maxy - miny) / 2), minz + ((maxz - minz) / 2))
|
||||
|
||||
|
||||
def get_element_bbox_centroid(element: ifcopenshell.entity_instance, geometry) -> tuple[float]:
|
||||
def get_element_bbox_centroid(element: ifcopenshell.entity_instance, geometry) -> npt.NDArray[np.float64]:
|
||||
"""Calculates the element's bounding box centroid
|
||||
|
||||
The centroid is in global coordinates. Note that if you have the shape, it
|
||||
@@ -158,16 +165,16 @@ def get_element_bbox_centroid(element: ifcopenshell.entity_instance, geometry) -
|
||||
:param geometry: Geometry output calculated by IfcOpenShell
|
||||
:type geometry: geometry
|
||||
:return: A tuple representing the XYZ centroid
|
||||
:rtype: tuple[float]
|
||||
:rtype: npt.NDArray[np.float64]
|
||||
"""
|
||||
centroid = get_bbox_centroid(geometry)
|
||||
if not element.ObjectPlacement or not element.ObjectPlacement.is_a("IfcLocalPlacement"):
|
||||
return centroid
|
||||
return np.array(centroid)
|
||||
mat = ifcopenshell.util.placement.get_local_placement(element.ObjectPlacement)
|
||||
return (mat @ np.array([*centroid, 1.0]))[0:3]
|
||||
|
||||
|
||||
def get_shape_bbox_centroid(shape, geometry) -> tuple[float]:
|
||||
def get_shape_bbox_centroid(shape, geometry) -> npt.NDArray[np.float64]:
|
||||
"""Calculates the shape's bounding box centroid
|
||||
|
||||
The centroid is in global coordinates. Note that if you do not have the
|
||||
@@ -178,13 +185,13 @@ def get_shape_bbox_centroid(shape, geometry) -> tuple[float]:
|
||||
:param geometry: Geometry output calculated by IfcOpenShell
|
||||
:type geometry: geometry
|
||||
:return: A tuple representing the XYZ centroid
|
||||
:rtype: tuple[float]
|
||||
:rtype: npt.NDArray[np.float64]
|
||||
"""
|
||||
centroid = get_bbox_centroid(geometry)
|
||||
return (get_shape_matrix(shape) @ np.array([*centroid, 1.0]))[0:3]
|
||||
|
||||
|
||||
def get_vertices(geometry) -> np.ndarray[np.ndarray[float]]:
|
||||
def get_vertices(geometry) -> npt.NDArray[np.float64]:
|
||||
"""Get all the vertices as a numpy array
|
||||
|
||||
Vertices are in local coordinates.
|
||||
@@ -200,7 +207,7 @@ def get_vertices(geometry) -> np.ndarray[np.ndarray[float]]:
|
||||
return np.array([np.array([verts[i], verts[i + 1], verts[i + 2]]) for i in range(0, len(verts), 3)])
|
||||
|
||||
|
||||
def get_edges(geometry) -> np.ndarray[np.ndarray[int]]:
|
||||
def get_edges(geometry) -> npt.NDArray[np.int32]:
|
||||
"""Get all the edges as a numpy array
|
||||
|
||||
Results are a nested numpy array e.g. [[e1v1, e1v2], [e2v1, e2v2], ...]
|
||||
@@ -215,10 +222,10 @@ def get_edges(geometry) -> np.ndarray[np.ndarray[int]]:
|
||||
:rtype: np.array[np.array[int]]
|
||||
"""
|
||||
edges = geometry.edges
|
||||
return [[edges[i], edges[i + 1]] for i in range(0, len(edges), 2)]
|
||||
return np.array([[edges[i], edges[i + 1]] for i in range(0, len(edges), 2)])
|
||||
|
||||
|
||||
def get_faces(geometry) -> np.ndarray[np.ndarray[int]]:
|
||||
def get_faces(geometry) -> npt.NDArray[np.int32]:
|
||||
"""Get all the faces as a numpy array
|
||||
|
||||
Faces are always triangulated. If the shape is a BRep and you want to get
|
||||
@@ -232,10 +239,10 @@ def get_faces(geometry) -> np.ndarray[np.ndarray[int]]:
|
||||
:rtype: np.array[np.array[int]]
|
||||
"""
|
||||
faces = geometry.faces
|
||||
return [[faces[i], faces[i + 1], faces[i + 2]] for i in range(0, len(faces), 3)]
|
||||
return np.array([[faces[i], faces[i + 1], faces[i + 2]] for i in range(0, len(faces), 3)])
|
||||
|
||||
|
||||
def get_shape_vertices(shape, geometry) -> np.ndarray[np.ndarray[float]]:
|
||||
def get_shape_vertices(shape, geometry) -> npt.NDArray[np.float64]:
|
||||
"""Get the shape's vertices as a numpy array
|
||||
|
||||
Vertices are in global coordinates. If you do not have the shape, you can
|
||||
@@ -255,7 +262,7 @@ def get_shape_vertices(shape, geometry) -> np.ndarray[np.ndarray[float]]:
|
||||
return np.delete((mat @ np.hstack((verts, np.ones((len(verts), 1)))).T).T, -1, axis=1)
|
||||
|
||||
|
||||
def get_element_vertices(element: ifcopenshell.entity_instance, geometry) -> np.ndarray[np.ndarray[float]]:
|
||||
def get_element_vertices(element: ifcopenshell.entity_instance, geometry) -> npt.NDArray[np.float64]:
|
||||
"""Get the element's vertices as a numpy array
|
||||
|
||||
Vertices are in global coordinates. Note that if you have the shape, it is
|
||||
@@ -365,7 +372,7 @@ def get_element_top_elevation(element: ifcopenshell.entity_instance, geometry) -
|
||||
return max([v[2] for v in get_element_vertices(element, geometry)])
|
||||
|
||||
|
||||
def get_bbox(vertices: Iterable[VECTOR_3D]) -> tuple[np.ndarray[float]]:
|
||||
def get_bbox(vertices: Iterable[VECTOR_3D]) -> tuple[npt.NDArray[np.float64], npt.NDArray[np.float64]]:
|
||||
"""Gets the bounding box of vertices
|
||||
|
||||
:param vertices: An iterable of vertices
|
||||
@@ -388,7 +395,7 @@ def get_bbox(vertices: Iterable[VECTOR_3D]) -> tuple[np.ndarray[float]]:
|
||||
return (np.array([minx, miny, minz]), np.array([maxx, maxy, maxz]))
|
||||
|
||||
|
||||
def get_area_vf(vertices: np.ndarray[VECTOR_3D], faces: np.ndarray[Iterable[int]]) -> float:
|
||||
def get_area_vf(vertices: npt.NDArray[np.float64], faces: npt.NDArray[np.int32]) -> float:
|
||||
"""Calculates the surface area given a list of vertices and triangulated faces
|
||||
|
||||
:param vertices: A list of 3D vertices, such as returned from get_vertices.
|
||||
|
||||
@@ -17,9 +17,14 @@
|
||||
# along with IfcOpenShell. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import numpy as np
|
||||
import numpy.typing as npt
|
||||
import collections
|
||||
import collections.abc
|
||||
import ifcopenshell
|
||||
import ifcopenshell.api
|
||||
import ifcopenshell.util.element
|
||||
import ifcopenshell.util.representation
|
||||
import ifcopenshell.util.unit
|
||||
from math import cos, sin, pi, tan, radians, degrees, atan, sqrt, ceil
|
||||
from typing import List, Tuple, Type, Union
|
||||
from itertools import chain
|
||||
@@ -339,6 +344,16 @@ class ShapeBuilder:
|
||||
"Ref: https://ifc43-docs.standards.buildingsmart.org/IFC/RELEASE/IFC4x3/HTML/lexical/IfcArbitraryClosedProfileDef.htm#8.15.3.1.4-Formal-propositions"
|
||||
)
|
||||
|
||||
kwargs = {
|
||||
"ProfileName": name,
|
||||
"ProfileType": profile_type,
|
||||
"OuterCurve": outer_curve,
|
||||
}
|
||||
if self.file.schema == "IFC2X3":
|
||||
kwargs["Position"] = self.file.create_entity(
|
||||
"IfcAxis2Placement2D", self.file.create_entity("IfcCartesianPoint", [0.0, 0.0])
|
||||
)
|
||||
|
||||
if inner_curves:
|
||||
if not isinstance(inner_curves, collections.abc.Iterable):
|
||||
inner_curves = [inner_curves]
|
||||
@@ -349,13 +364,9 @@ class ShapeBuilder:
|
||||
"Ref: https://ifc43-docs.standards.buildingsmart.org/IFC/RELEASE/IFC4x3/HTML/lexical/IfcArbitraryClosedProfileDef.htm#8.15.3.1.4-Formal-propositions"
|
||||
)
|
||||
|
||||
profile = self.file.createIfcArbitraryProfileDefWithVoids(
|
||||
ProfileName=name, ProfileType=profile_type, OuterCurve=outer_curve, InnerCurves=inner_curves
|
||||
)
|
||||
profile = self.file.create_entity("IfcArbitraryProfileDefWithVoids", InnerCurves=inner_curves, **kwargs)
|
||||
else:
|
||||
profile = self.file.createIfcArbitraryClosedProfileDef(
|
||||
ProfileName=name, ProfileType=profile_type, OuterCurve=outer_curve
|
||||
)
|
||||
profile = self.file.create_entity("IfcArbitraryClosedProfileDef", **kwargs)
|
||||
return profile
|
||||
|
||||
def translate(self, curve_or_item, translation: Vector, create_copy=False):
|
||||
@@ -529,13 +540,13 @@ class ShapeBuilder:
|
||||
|
||||
def create_axis2_placement_3d_from_matrix(
|
||||
self,
|
||||
matrix: Union[np.ndarray, None] = None,
|
||||
matrix: Union[npt.NDArray[np.float64], None] = None,
|
||||
) -> ifcopenshell.entity_instance:
|
||||
"""
|
||||
Create IfcAxis2Placement3D from numpy matrix.
|
||||
|
||||
:param matrix: 4x4 transformation matrix, defaults to `np.eye(4)`
|
||||
:type matrix: np.array[np.array[float]], optional
|
||||
:type matrix: npt.NDArray[np.float64], optional
|
||||
:return: IfcAxis2Placement3D
|
||||
:rtype: ifcopenshell.entity_instance
|
||||
"""
|
||||
|
||||
@@ -21,6 +21,7 @@ from math import pi
|
||||
from typing import Iterable, Any, Union, Literal, Optional
|
||||
|
||||
import ifcopenshell
|
||||
import ifcopenshell.ifcopenshell_wrapper as ifcopenshell_wrapper
|
||||
import ifcopenshell.api
|
||||
|
||||
prefixes = {
|
||||
@@ -353,13 +354,32 @@ def get_prefix_multiplier(text):
|
||||
return 1
|
||||
|
||||
|
||||
def get_unit_name(text):
|
||||
def get_unit_name(text: str) -> Union[str, None]:
|
||||
"""Get unit name from str, if unit is in SI."""
|
||||
text = text.upper().replace("METER", "METRE")
|
||||
for name in unit_names:
|
||||
if name.replace("_", " ") in text:
|
||||
return name
|
||||
|
||||
|
||||
def get_unit_name_universal(text: str) -> Union[str, None]:
|
||||
"""Get unit name from str, supports both SI and imperial system.
|
||||
|
||||
Can be used to provide units for `convert()`"""
|
||||
text = text.upper().replace("METER", "METRE")
|
||||
for name in unit_names:
|
||||
if name.replace("_", " ") in text:
|
||||
return name
|
||||
for name in imperial_types:
|
||||
if name.upper() in text:
|
||||
return name
|
||||
|
||||
|
||||
def get_full_unit_name(unit: ifcopenshell.entity_instance) -> str:
|
||||
prefix = getattr(unit, "Prefix", None) or ""
|
||||
return prefix + unit.Name.upper()
|
||||
|
||||
|
||||
def get_si_dimensions(name):
|
||||
return si_dimensions.get(name, si_dimensions["OTHERWISE"])
|
||||
|
||||
@@ -368,13 +388,13 @@ def get_named_dimensions(name):
|
||||
return named_dimensions.get(name, (0, 0, 0, 0, 0, 0, 0))
|
||||
|
||||
|
||||
def get_unit_assignment(ifc_file):
|
||||
def get_unit_assignment(ifc_file: ifcopenshell.file) -> Union[ifcopenshell.entity_instance, None]:
|
||||
unit_assignments = ifc_file.by_type("IfcUnitAssignment")
|
||||
if unit_assignments:
|
||||
return unit_assignments[0]
|
||||
|
||||
|
||||
def get_project_unit(ifc_file, unit_type):
|
||||
def get_project_unit(ifc_file: ifcopenshell.file, unit_type: str) -> Union[ifcopenshell.entity_instance, None]:
|
||||
"""Get the default project unit of a particular unit type
|
||||
|
||||
:param ifc_file: The IFC file.
|
||||
@@ -384,7 +404,7 @@ def get_project_unit(ifc_file, unit_type):
|
||||
:type unit_type: str
|
||||
:return: The IFC unit entity, or nothing if there is no default project unit
|
||||
defined.
|
||||
:rtype: ifcopenshell.entity_instance,None
|
||||
:rtype: Union[ifcopenshell.entity_instance, None]
|
||||
"""
|
||||
unit_assignment = get_unit_assignment(ifc_file)
|
||||
if unit_assignment:
|
||||
@@ -393,7 +413,9 @@ def get_project_unit(ifc_file, unit_type):
|
||||
return unit
|
||||
|
||||
|
||||
def get_property_unit(prop, ifc_file):
|
||||
def get_property_unit(
|
||||
prop: ifcopenshell.entity_instance, ifc_file: ifcopenshell.file
|
||||
) -> Union[ifcopenshell.entity_instance, None]:
|
||||
unit = getattr(prop, "Unit", None)
|
||||
if unit:
|
||||
return unit
|
||||
@@ -446,14 +468,14 @@ def get_property_unit(prop, ifc_file):
|
||||
return units[0]
|
||||
|
||||
|
||||
def get_unit_measure_class(unit_type):
|
||||
def get_unit_measure_class(unit_type: str) -> str:
|
||||
if unit_type == "USERDEFINED":
|
||||
# See https://github.com/buildingSMART/IFC4.3.x-development/issues/71
|
||||
return "IfcNumericMeasure"
|
||||
return "Ifc" + unit_type[0:-4].lower().capitalize() + "Measure"
|
||||
|
||||
|
||||
def get_measure_unit_type(measure_class):
|
||||
def get_measure_unit_type(measure_class: str) -> str:
|
||||
if measure_class == "IfcNumericMeasure":
|
||||
# See https://github.com/buildingSMART/IFC4.3.x-development/issues/71
|
||||
return "USERDEFINED"
|
||||
@@ -462,7 +484,7 @@ def get_measure_unit_type(measure_class):
|
||||
return measure_class.upper() + "UNIT"
|
||||
|
||||
|
||||
def get_symbol_measure_class(symbol):
|
||||
def get_symbol_measure_class(symbol: Optional[str] = None) -> str:
|
||||
# Dumb, but everybody gets it, unlike regex golf
|
||||
if not symbol:
|
||||
return "IfcNumericMeasure"
|
||||
@@ -480,7 +502,7 @@ def get_symbol_measure_class(symbol):
|
||||
return "IfcNumericMeasure"
|
||||
|
||||
|
||||
def get_symbol_quantity_class(symbol):
|
||||
def get_symbol_quantity_class(symbol: Optional[str] = None) -> str:
|
||||
# Dumb, but everybody gets it, unlike regex golf
|
||||
if not symbol:
|
||||
return "IfcQuantityCount"
|
||||
@@ -498,7 +520,7 @@ def get_symbol_quantity_class(symbol):
|
||||
return "IfcQuantityCount"
|
||||
|
||||
|
||||
def get_unit_symbol(unit):
|
||||
def get_unit_symbol(unit: ifcopenshell.entity_instance) -> str:
|
||||
symbol = ""
|
||||
if unit.is_a("IfcSIUnit"):
|
||||
symbol += prefix_symbols.get(unit.Prefix, "")
|
||||
@@ -508,7 +530,7 @@ def get_unit_symbol(unit):
|
||||
return symbol
|
||||
|
||||
|
||||
def convert_unit(value, from_unit, to_unit):
|
||||
def convert_unit(value: float, from_unit: ifcopenshell.entity_instance, to_unit: ifcopenshell.entity_instance) -> float:
|
||||
"""Convert from one unit to another unit
|
||||
|
||||
:param value: The numeric value you want to convert
|
||||
@@ -668,9 +690,9 @@ def format_length(
|
||||
|
||||
|
||||
def is_attr_type(
|
||||
content_type: Union[ifcopenshell.ifcopenshell_wrapper.named_type, ifcopenshell.ifcopenshell_wrapper.type_declaration],
|
||||
content_type: ifcopenshell_wrapper.parameter_type,
|
||||
ifc_unit_type_name: str,
|
||||
) -> Union[ifcopenshell.ifcopenshell_wrapper.type_declaration, None]:
|
||||
) -> Union[ifcopenshell_wrapper.type_declaration, None]:
|
||||
cur_decl = content_type
|
||||
while hasattr(cur_decl, "declared_type") is True:
|
||||
cur_decl = cur_decl.declared_type()
|
||||
@@ -679,9 +701,15 @@ def is_attr_type(
|
||||
if cur_decl.name() == ifc_unit_type_name:
|
||||
return cur_decl
|
||||
|
||||
if isinstance(cur_decl, ifcopenshell.ifcopenshell_wrapper.aggregation_type):
|
||||
res = cur_decl.type_of_element()
|
||||
cur_decl = res.declared_type()
|
||||
if isinstance(cur_decl, ifcopenshell_wrapper.aggregation_type):
|
||||
# support aggregate of aggregates, as in IfcCartesianPointList3D.CoordList
|
||||
def get_declared_type_from_aggregate(cur_decl):
|
||||
cur_decl = cur_decl.type_of_element()
|
||||
if not isinstance(cur_decl, ifcopenshell_wrapper.aggregation_type):
|
||||
return cur_decl.declared_type()
|
||||
return get_declared_type_from_aggregate(cur_decl)
|
||||
|
||||
cur_decl = get_declared_type_from_aggregate(cur_decl)
|
||||
if hasattr(cur_decl, "name") and cur_decl.name() == ifc_unit_type_name:
|
||||
return cur_decl
|
||||
while hasattr(cur_decl, "declared_type") is True:
|
||||
@@ -696,13 +724,13 @@ def is_attr_type(
|
||||
|
||||
def iter_element_and_attributes_per_type(
|
||||
ifc_file: ifcopenshell.file, attr_type_name: str
|
||||
) -> Iterable[tuple[ifcopenshell.entity_instance, ifcopenshell.ifcopenshell_wrapper.attribute, Any, str]]:
|
||||
schema = ifcopenshell.ifcopenshell_wrapper.schema_by_name(ifc_file.schema)
|
||||
) -> Iterable[tuple[ifcopenshell.entity_instance, ifcopenshell_wrapper.attribute, Any]]:
|
||||
schema: ifcopenshell_wrapper.schema_definition = ifcopenshell_wrapper.schema_by_name(ifc_file.schema)
|
||||
|
||||
for element in ifc_file:
|
||||
entity = schema.declaration_by_name(element.is_a())
|
||||
attrs = entity.all_attributes()
|
||||
for i, (attr, val, is_derived) in enumerate(zip(attrs, list(element), entity.derived())):
|
||||
for attr, val, is_derived in zip(attrs, list(element), entity.derived()):
|
||||
if is_derived:
|
||||
continue
|
||||
|
||||
@@ -718,26 +746,38 @@ def iter_element_and_attributes_per_type(
|
||||
yield element, attr, val
|
||||
|
||||
|
||||
def convert_file_length_units(ifc_file: ifcopenshell.file, target_units: str) -> ifcopenshell.file:
|
||||
def convert_file_length_units(ifc_file: ifcopenshell.file, target_units: str = "METER") -> ifcopenshell.file:
|
||||
"""Converts all units in an IFC file to the specified target units. Returns a new file."""
|
||||
prefix = "MILLI" if target_units == "MILLIMETERS" else None
|
||||
prefix = get_prefix(target_units)
|
||||
si_unit = get_unit_name(target_units)
|
||||
|
||||
# Copy all elements from the original file to the patched file
|
||||
file_patched = ifcopenshell.file.from_string(ifc_file.wrapped_data.to_string())
|
||||
|
||||
unit_assignment = ifcopenshell.util.unit.get_unit_assignment(file_patched)
|
||||
unit_assignment = get_unit_assignment(file_patched)
|
||||
|
||||
old_length = [u for u in unit_assignment.Units if getattr(u, "UnitType", None) == "LENGTHUNIT"][0]
|
||||
new_length = ifcopenshell.api.run("unit.add_si_unit", file_patched, unit_type="LENGTHUNIT", prefix=prefix)
|
||||
old_length = next(u for u in unit_assignment.Units if getattr(u, "UnitType", None) == "LENGTHUNIT")
|
||||
if si_unit:
|
||||
new_length = ifcopenshell.api.run("unit.add_si_unit", file_patched, unit_type="LENGTHUNIT", prefix=prefix)
|
||||
else:
|
||||
target_units = target_units.lower()
|
||||
if imperial_types.get(target_units) != "LENGTHUNIT":
|
||||
raise Exception(
|
||||
f'Couldn\'t identify target units "{target_units}". '
|
||||
'The method supports singular unit names like "CENTIMETER", "METER", "FOOT", etc.'
|
||||
)
|
||||
new_length = ifcopenshell.api.run("unit.add_conversion_based_unit", file_patched, name=target_units)
|
||||
|
||||
# support tuple of tuples, as in IfcCartesianPointList3D.CoordList
|
||||
def convert_value(value):
|
||||
if not isinstance(value, tuple):
|
||||
return convert_unit(value, old_length, new_length)
|
||||
return tuple(convert_value(v) for v in value)
|
||||
|
||||
# Traverse all elements and their nested attributes in the file and convert them
|
||||
for element, attr, val in iter_element_and_attributes_per_type(file_patched, "IfcLengthMeasure"):
|
||||
if isinstance(val, tuple):
|
||||
new_value = [ifcopenshell.util.unit.convert_unit(v, old_length, new_length) for v in val]
|
||||
setattr(element, attr.name(), tuple(new_value))
|
||||
else:
|
||||
new_value = ifcopenshell.util.unit.convert_unit(val, old_length, new_length)
|
||||
setattr(element, attr.name(), new_value)
|
||||
new_value = convert_value(val)
|
||||
setattr(element, attr.name(), new_value)
|
||||
|
||||
file_patched.remove(old_length)
|
||||
unit_assignment.Units = tuple([new_length, *unit_assignment.Units])
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
ISO-10303-21;
|
||||
HEADER;
|
||||
FILE_DESCRIPTION(('ViewDefinition[DesignTransferView]'),'2;1');
|
||||
FILE_NAME('mill.ifc','2024-04-23T16:52:20+05:00',(),(),'IfcOpenShell v0.7.0-f7c03db75','BlenderBIM 0.0.240422-998f9c6','Nobody');
|
||||
FILE_SCHEMA(('IFC4'));
|
||||
ENDSEC;
|
||||
DATA;
|
||||
#1=IFCPROJECT('3Q4HLVId9C$OJq2NqKnvVO',$,'My Project',$,$,$,$,(#14,#26),#9);
|
||||
#2=IFCSIUNIT(*,.LENGTHUNIT.,.MILLI.,.METRE.);
|
||||
#3=IFCSIUNIT(*,.AREAUNIT.,$,.SQUARE_METRE.);
|
||||
#4=IFCSIUNIT(*,.VOLUMEUNIT.,$,.CUBIC_METRE.);
|
||||
#5=IFCDIMENSIONALEXPONENTS(0,0,0,0,0,0,0);
|
||||
#6=IFCSIUNIT(*,.PLANEANGLEUNIT.,$,.RADIAN.);
|
||||
#7=IFCMEASUREWITHUNIT(IFCREAL(0.0174532925199433),#6);
|
||||
#8=IFCCONVERSIONBASEDUNIT(#5,.PLANEANGLEUNIT.,'degree',#7);
|
||||
#9=IFCUNITASSIGNMENT((#4,#2,#8,#3));
|
||||
#10=IFCCARTESIANPOINT((0.,0.,0.));
|
||||
#11=IFCDIRECTION((0.,0.,1.));
|
||||
#12=IFCDIRECTION((1.,0.,0.));
|
||||
#13=IFCAXIS2PLACEMENT3D(#10,#11,#12);
|
||||
#14=IFCGEOMETRICREPRESENTATIONCONTEXT($,'Model',3,1.E-05,#13,$);
|
||||
#15=IFCGEOMETRICREPRESENTATIONSUBCONTEXT('Body','Model',*,*,*,*,#14,$,.MODEL_VIEW.,$);
|
||||
#16=IFCGEOMETRICREPRESENTATIONSUBCONTEXT('Axis','Model',*,*,*,*,#14,$,.GRAPH_VIEW.,$);
|
||||
#17=IFCGEOMETRICREPRESENTATIONSUBCONTEXT('Box','Model',*,*,*,*,#14,$,.MODEL_VIEW.,$);
|
||||
#18=IFCGEOMETRICREPRESENTATIONSUBCONTEXT('Annotation','Model',*,*,*,*,#14,$,.SECTION_VIEW.,$);
|
||||
#19=IFCGEOMETRICREPRESENTATIONSUBCONTEXT('Annotation','Model',*,*,*,*,#14,$,.ELEVATION_VIEW.,$);
|
||||
#20=IFCGEOMETRICREPRESENTATIONSUBCONTEXT('Annotation','Model',*,*,*,*,#14,$,.MODEL_VIEW.,$);
|
||||
#21=IFCGEOMETRICREPRESENTATIONSUBCONTEXT('Annotation','Model',*,*,*,*,#14,$,.PLAN_VIEW.,$);
|
||||
#22=IFCGEOMETRICREPRESENTATIONSUBCONTEXT('Profile','Model',*,*,*,*,#14,$,.ELEVATION_VIEW.,$);
|
||||
#23=IFCCARTESIANPOINT((0.,0.));
|
||||
#24=IFCDIRECTION((1.,0.));
|
||||
#25=IFCAXIS2PLACEMENT2D(#23,#24);
|
||||
#26=IFCGEOMETRICREPRESENTATIONCONTEXT($,'Plan',2,1.E-05,#25,$);
|
||||
#27=IFCGEOMETRICREPRESENTATIONSUBCONTEXT('Axis','Plan',*,*,*,*,#26,$,.GRAPH_VIEW.,$);
|
||||
#28=IFCGEOMETRICREPRESENTATIONSUBCONTEXT('Body','Plan',*,*,*,*,#26,$,.PLAN_VIEW.,$);
|
||||
#29=IFCGEOMETRICREPRESENTATIONSUBCONTEXT('Annotation','Plan',*,*,*,*,#26,$,.PLAN_VIEW.,$);
|
||||
#30=IFCSITE('1T8u08wvPCnhfVRFeG_Je7',$,'My Site',$,$,#53,$,$,$,$,$,$,$,$);
|
||||
#36=IFCBUILDING('0AGAIai5z9HOTud5I6CjMh',$,'My Building',$,$,#59,$,$,$,$,$,$);
|
||||
#42=IFCBUILDINGSTOREY('2lMUoFnMv5KOSnzZ9_MPZv',$,'My Storey',$,$,#65,$,$,$,$);
|
||||
#48=IFCRELAGGREGATES('0cjacslobAjw8I_uTLtTuK',$,$,$,#1,(#30));
|
||||
#49=IFCCARTESIANPOINT((0.,0.,0.));
|
||||
#50=IFCDIRECTION((0.,0.,1.));
|
||||
#51=IFCDIRECTION((1.,0.,0.));
|
||||
#52=IFCAXIS2PLACEMENT3D(#49,#50,#51);
|
||||
#53=IFCLOCALPLACEMENT($,#52);
|
||||
#54=IFCRELAGGREGATES('126VmVXPD0pAP2uJz6QYat',$,$,$,#30,(#36));
|
||||
#55=IFCCARTESIANPOINT((0.,0.,0.));
|
||||
#56=IFCDIRECTION((0.,0.,1.));
|
||||
#57=IFCDIRECTION((1.,0.,0.));
|
||||
#58=IFCAXIS2PLACEMENT3D(#55,#56,#57);
|
||||
#59=IFCLOCALPLACEMENT(#53,#58);
|
||||
#60=IFCRELAGGREGATES('1KLZsOP9zAivCjkiTn31bt',$,$,$,#36,(#42));
|
||||
#61=IFCCARTESIANPOINT((0.,0.,0.));
|
||||
#62=IFCDIRECTION((0.,0.,1.));
|
||||
#63=IFCDIRECTION((1.,0.,0.));
|
||||
#64=IFCAXIS2PLACEMENT3D(#61,#62,#63);
|
||||
#65=IFCLOCALPLACEMENT(#59,#64);
|
||||
#66=IFCACTUATOR('3RT$GBDjj6VhZQ2OYV3P13',$,'Cube',$,$,#95,#84,$,.ELECTRICACTUATOR.);
|
||||
#72=IFCINDEXEDPOLYGONALFACE((1,2,4,3));
|
||||
#73=IFCINDEXEDPOLYGONALFACE((3,4,8,7));
|
||||
#74=IFCINDEXEDPOLYGONALFACE((7,8,6,5));
|
||||
#75=IFCINDEXEDPOLYGONALFACE((5,6,2,1));
|
||||
#76=IFCINDEXEDPOLYGONALFACE((3,7,5,1));
|
||||
#77=IFCINDEXEDPOLYGONALFACE((8,4,2,6));
|
||||
#78=IFCCARTESIANPOINTLIST3D(((-999.999938964844,-999.999938964844,-999.999938964844),(-999.999938964844,-999.999938964844,999.999938964844),(-999.999938964844,999.999938964844,-999.999938964844),(-999.999938964844,999.999938964844,999.999938964844),(999.999938964844,-999.999938964844,-999.999938964844),(999.999938964844,-999.999938964844,999.999938964844),(999.999938964844,999.999938964844,-999.999938964844),(999.999938964844,999.999938964844,999.999938964844)));
|
||||
#79=IFCPOLYGONALFACESET(#78,.T.,(#72,#73,#74,#75,#76,#77),$);
|
||||
#80=IFCSHAPEREPRESENTATION(#15,'Body','Tessellation',(#79));
|
||||
#81=IFCCARTESIANPOINT((-1000.,-1000.,-1000.));
|
||||
#82=IFCBOUNDINGBOX(#81,2000.,2000.,2000.);
|
||||
#83=IFCSHAPEREPRESENTATION(#17,'Box','BoundingBox',(#82));
|
||||
#84=IFCPRODUCTDEFINITIONSHAPE($,$,(#83,#80));
|
||||
#85=IFCRELCONTAINEDINSPATIALSTRUCTURE('107zrsI95BsfGJXI83megf',$,$,$,(#66),#42);
|
||||
#91=IFCCARTESIANPOINT((0.,5000.,0.));
|
||||
#92=IFCDIRECTION((0.,0.,1.));
|
||||
#93=IFCDIRECTION((1.,0.,0.));
|
||||
#94=IFCAXIS2PLACEMENT3D(#91,#92,#93);
|
||||
#95=IFCLOCALPLACEMENT(#65,#94);
|
||||
ENDSEC;
|
||||
END-ISO-10303-21;
|
||||
@@ -16,34 +16,52 @@
|
||||
# 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 pathlib
|
||||
|
||||
import pytest
|
||||
|
||||
import ifcopenshell.util.unit
|
||||
import numpy as np
|
||||
|
||||
UNITS_FIXTURE_DIR = pathlib.Path(__file__).parent.parent / "fixtures" / "units"
|
||||
|
||||
|
||||
@pytest.mark.parametrize("ifc_file", UNITS_FIXTURE_DIR.glob("*.ifc"))
|
||||
def test_file_units_length_convert(ifc_file):
|
||||
def test_file_units_length_convert(ifc_file: str):
|
||||
f = ifcopenshell.open(ifc_file)
|
||||
project_unit = ifcopenshell.util.unit.get_project_unit(f, "LENGTHUNIT")
|
||||
if project_unit.Prefix == "MILLI":
|
||||
target_units = "METERS"
|
||||
scale = 0.001
|
||||
else:
|
||||
target_units = "MILLIMETERS"
|
||||
scale = 1000
|
||||
|
||||
new_f = ifcopenshell.util.unit.convert_file_length_units(f, target_units)
|
||||
def get_project_unit(f: ifcopenshell.file) -> str:
|
||||
unit = ifcopenshell.util.unit.get_project_unit(f, "LENGTHUNIT")
|
||||
return ifcopenshell.util.unit.get_full_unit_name(unit)
|
||||
|
||||
base_project_unit = get_project_unit(f)
|
||||
target_units = ["MILLIMETRE", "METRE", "CENTIMETRE", "INCH", "FOOT"]
|
||||
|
||||
def convert_file_and_test(f: ifcopenshell.file, project_unit: str, target_unit: str) -> ifcopenshell.file:
|
||||
scale = ifcopenshell.util.unit.convert(
|
||||
value=1,
|
||||
from_prefix=ifcopenshell.util.unit.get_prefix(project_unit),
|
||||
from_unit=ifcopenshell.util.unit.get_unit_name_universal(project_unit),
|
||||
to_prefix=ifcopenshell.util.unit.get_prefix(target_unit),
|
||||
to_unit=ifcopenshell.util.unit.get_unit_name_universal(target_unit),
|
||||
)
|
||||
new_f = ifcopenshell.util.unit.convert_file_length_units(f, target_unit)
|
||||
|
||||
for element, attr, val in ifcopenshell.util.unit.iter_element_and_attributes_per_type(
|
||||
new_f, "IfcLengthMeasure"
|
||||
):
|
||||
elem_id = element.id()
|
||||
original_element = f.by_id(elem_id)
|
||||
original_val = getattr(original_element, attr.name())
|
||||
|
||||
def convert_value(value):
|
||||
if not isinstance(value, tuple):
|
||||
return value * scale
|
||||
return tuple(convert_value(v) for v in value)
|
||||
|
||||
for element, attr, val in ifcopenshell.util.unit.iter_element_and_attributes_per_type(new_f, "IfcLengthMeasure"):
|
||||
elem_id = element.id()
|
||||
original_element = f.by_id(elem_id)
|
||||
original_val = getattr(original_element, attr.name())
|
||||
if isinstance(original_val, tuple):
|
||||
# assert element is equal to original element times scale
|
||||
assert val == tuple([v * scale for v in original_val])
|
||||
else:
|
||||
# assert element is equal to original element times scale
|
||||
assert val == original_val * scale
|
||||
assert np.allclose([val], [convert_value(original_val)])
|
||||
|
||||
assert get_project_unit(new_f) == target_unit
|
||||
return new_f
|
||||
|
||||
for target_unit in target_units:
|
||||
new_f = convert_file_and_test(f, base_project_unit, target_unit)
|
||||
convert_file_and_test(new_f, target_unit, base_project_unit)
|
||||
|
||||
@@ -26,13 +26,19 @@ from logging import Logger
|
||||
|
||||
|
||||
class Patcher:
|
||||
def __init__(self, src: str, file: ifcopenshell.file, logger: Logger, unit: str = "METERS"):
|
||||
def __init__(
|
||||
self,
|
||||
src: str,
|
||||
file: ifcopenshell.file,
|
||||
logger: Logger,
|
||||
unit: str = "METER",
|
||||
):
|
||||
"""Converts the length unit of a model to the specified unit
|
||||
|
||||
Allowed metric units include METERS, MILLIMETERS, CENTIMETERS, etc.
|
||||
Allowed imperial units include INCHES, FEET, MILES.
|
||||
Allowed metric units include METER, MILLIMETER, CENTIMETER, etc.
|
||||
Allowed imperial units include INCH, FOOT, MILE.
|
||||
|
||||
:param unit: The name of the desired unit, defaults to "METERS"
|
||||
:param unit: The name of the desired unit, defaults to "METER"
|
||||
:type unit: str
|
||||
|
||||
Example:
|
||||
@@ -40,10 +46,10 @@ class Patcher:
|
||||
.. code:: python
|
||||
|
||||
# Convert to millimeters
|
||||
ifcpatch.execute({"input": "input.ifc", "file": model, "recipe": "ConvertLengthUnit", "arguments": ["MILLIMETERS"]})
|
||||
ifcpatch.execute({"input": "input.ifc", "file": model, "recipe": "ConvertLengthUnit", "arguments": ["MILLIMETER"]})
|
||||
|
||||
# Convert to feet
|
||||
ifcpatch.execute({"input": "input.ifc", "file": model, "recipe": "ConvertLengthUnit", "arguments": ["FEET"]})
|
||||
ifcpatch.execute({"input": "input.ifc", "file": model, "recipe": "ConvertLengthUnit", "arguments": ["FOOT"]})
|
||||
"""
|
||||
self.src = src
|
||||
self.file = file
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
|
||||
import ifcopenshell
|
||||
import ifcopenshell.util.element
|
||||
import ifcopenshell.util.unit
|
||||
from typing import Union
|
||||
from logging import Logger
|
||||
|
||||
@@ -30,6 +31,9 @@ class Patcher:
|
||||
further processing will be done. This means that you may end up with
|
||||
duplicate spatial hierarchies (i.e. 2 sites, 2 buildings, etc).
|
||||
|
||||
Will automatically convert length units in the second model to the main
|
||||
model's unit before merging.
|
||||
|
||||
:param filepath: The filepath of the second IFC model to merge into the
|
||||
first. The first model is already specified as the input to
|
||||
IfcPatch.
|
||||
@@ -48,6 +52,10 @@ class Patcher:
|
||||
|
||||
def patch(self):
|
||||
source = ifcopenshell.open(self.filepath)
|
||||
# make sure models units will match
|
||||
if (main_unit := self.get_unit_name(self.file)) != self.get_unit_name(source):
|
||||
source = ifcopenshell.util.unit.convert_file_length_units(source, main_unit)
|
||||
|
||||
self.existing_contexts: list[ifcopenshell.entity_instance] = self.file.by_type(
|
||||
"IfcGeometricRepresentationContext"
|
||||
)
|
||||
@@ -69,6 +77,10 @@ class Patcher:
|
||||
|
||||
self.reuse_existing_contexts()
|
||||
|
||||
def get_unit_name(self, ifc_file: ifcopenshell.file) -> str:
|
||||
length_unit = ifcopenshell.util.unit.get_project_unit(ifc_file, "LENGTHUNIT")
|
||||
return ifcopenshell.util.unit.get_full_unit_name(length_unit)
|
||||
|
||||
def reuse_existing_contexts(self):
|
||||
to_delete = set()
|
||||
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
# 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 test.bootstrap
|
||||
import ifcopenshell.api
|
||||
import ifcopenshell.util.unit
|
||||
import ifcpatch
|
||||
|
||||
|
||||
class TestConvertLengthUnit(test.bootstrap.IFC4):
|
||||
# NOTE: conversion itself is covered by `ifcopenshell.util.unit.convert_file_length_units` tests
|
||||
def test_run(self):
|
||||
project = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcProject")
|
||||
unit = ifcopenshell.api.run("unit.add_si_unit", self.file, unit_type="LENGTHUNIT", prefix="MILLI")
|
||||
ifcopenshell.api.run("unit.assign_unit", self.file, units=[unit])
|
||||
output = ifcpatch.execute(
|
||||
{"input": "input.ifc", "file": self.file, "recipe": "ConvertLengthUnit", "arguments": ["METER"]}
|
||||
)
|
||||
unit = ifcopenshell.util.unit.get_project_unit(output, "LENGTHUNIT")
|
||||
assert ifcopenshell.util.unit.get_full_unit_name(unit) == "METRE"
|
||||
|
||||
|
||||
class TestConvertLengthUnitIFC2X3(test.bootstrap.IFC2X3, TestConvertLengthUnit):
|
||||
pass
|
||||
@@ -0,0 +1,66 @@
|
||||
# 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 ifcpatch
|
||||
import ifcopenshell
|
||||
import ifcopenshell.api
|
||||
import ifcopenshell.util.placement
|
||||
import test.bootstrap
|
||||
import tempfile
|
||||
import numpy as np
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
|
||||
|
||||
class TestMergeProject(test.bootstrap.IFC4):
|
||||
def setup_project(self, ifc_file: Optional[ifcopenshell.file] = None):
|
||||
prefix = None if ifc_file else "MILLI"
|
||||
if ifc_file is None:
|
||||
ifc_file = ifcopenshell.file(schema=self.file.schema)
|
||||
|
||||
project = ifcopenshell.api.run("root.create_entity", ifc_file, ifc_class="IfcProject")
|
||||
unit = ifcopenshell.api.run("unit.add_si_unit", ifc_file, unit_type="LENGTHUNIT", prefix=prefix)
|
||||
ifcopenshell.api.run("unit.assign_unit", ifc_file, units=[unit])
|
||||
|
||||
matrix = np.eye(4)
|
||||
matrix[:, 3] = (1, 2, 3, 1)
|
||||
wall = ifcopenshell.api.run("root.create_entity", ifc_file, ifc_class="IfcWall")
|
||||
ifcopenshell.api.run("geometry.edit_object_placement", ifc_file, product=wall, matrix=matrix, is_si=True)
|
||||
return ifc_file
|
||||
|
||||
def test_run(self):
|
||||
self.file = self.setup_project(self.file)
|
||||
second_file = self.setup_project()
|
||||
temp_path = Path(tempfile.gettempdir()) / "second.ifc"
|
||||
second_file.write(temp_path)
|
||||
output = ifcpatch.execute({"file": self.file, "recipe": "MergeProject", "arguments": [str(temp_path)]})
|
||||
|
||||
assert len(output.by_type("IfcWall")) == 2
|
||||
wall1, wall2 = output.by_type("IfcWall")
|
||||
|
||||
# test that units are converted
|
||||
placement1 = ifcopenshell.util.placement.get_local_placement(wall1.ObjectPlacement)
|
||||
placement2 = ifcopenshell.util.placement.get_local_placement(wall2.ObjectPlacement)
|
||||
to_tuple = lambda arr: tuple(map(tuple, arr))
|
||||
matrix = np.eye(4)
|
||||
matrix[:, 3] = (1, 2, 3, 1)
|
||||
assert to_tuple(placement1) == to_tuple(placement2) == to_tuple(matrix)
|
||||
|
||||
|
||||
class TestMergeProjectIFC2X3(test.bootstrap.IFC2X3, TestMergeProject):
|
||||
pass
|
||||
@@ -72,9 +72,16 @@
|
||||
|
||||
template <>
|
||||
std::string cast_pyobject(PyObject* element) {
|
||||
#if SWIG_VERSION >= 0x040200
|
||||
PyObject *pbytes = NULL;
|
||||
const char* str_data = SWIG_PyUnicode_AsUTF8AndSize(element, NULL, &pbytes);
|
||||
std::string str = str_data;
|
||||
Py_XDECREF(pbytes);
|
||||
#else
|
||||
char* str_data = SWIG_Python_str_AsChar(element);
|
||||
std::string str = str_data;
|
||||
SWIG_Python_str_DelForPy3(str_data);
|
||||
#endif
|
||||
return str;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user