mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-08-09 09:21:46 +00:00
Merge remote-tracking branch 'origin/v0.8.0' into tfk-rocksdb-storage
This commit is contained in:
+17
-2
@@ -441,7 +441,19 @@ if(NOT MINIMAL_BUILD)
|
||||
# libxml2 is required for IFCXML (optional) and SVGFILL (mandatory)
|
||||
clear_wasm_sysroot()
|
||||
if(IFCXML_SUPPORT)
|
||||
find_package(LibXml2 REQUIRED)
|
||||
if((NOT LIBXML2_INCLUDE_DIR AND NOT LIBXML2_LIBRARIES))
|
||||
find_package(LibXml2 CONFIG REQUIRED)
|
||||
message(STATUS "Found LibXml2 config: ${LibXml2_DIR}")
|
||||
get_target_property(LIBXML2_INCLUDE_DIR LibXml2::LibXml2 INTERFACE_INCLUDE_DIRECTORIES)
|
||||
get_target_property(_libxml2_debug LibXml2::LibXml2 IMPORTED_LOCATION_DEBUG)
|
||||
get_target_property(_libxml2_release LibXml2::LibXml2 IMPORTED_LOCATION_RELEASE)
|
||||
set(LIBXML2_LIBRARIES
|
||||
optimized ${_libxml2_release}
|
||||
debug ${_libxml2_debug}
|
||||
)
|
||||
else()
|
||||
find_package(LibXml2 REQUIRED)
|
||||
endif()
|
||||
endif()
|
||||
restore_wasm_sysroot()
|
||||
endif()
|
||||
@@ -452,7 +464,7 @@ if(IFCXML_SUPPORT)
|
||||
endif()
|
||||
|
||||
if(BUILD_IFCGEOM)
|
||||
if(MSVC)
|
||||
if(MSVC AND NOT LibXml2_DIR)
|
||||
add_debug_variants(LIBXML2_LIBRARIES "${LIBXML2_LIBRARIES}" d)
|
||||
endif()
|
||||
|
||||
@@ -1097,6 +1109,9 @@ add_library(IfcParse ${IFCPARSE_FILES})
|
||||
target_link_libraries(IfcParse ${ROCKSDB_LIBRARY} ${ZSTD_LIBRARY} ${RPCRT_LIBRARIES} ${STDCPPFS})
|
||||
set_target_properties(IfcParse PROPERTIES COMPILE_FLAGS -DIFC_PARSE_EXPORTS VERSION "${PROJECT_VERSION}" SOVERSION "${PROJECT_VERSION_MAJOR}.${PROJECT_VERSION_MINOR}")
|
||||
|
||||
if(LibXml2_DIR)
|
||||
target_compile_definitions(IfcParse PRIVATE ${LIBXML2_DEFINITIONS})
|
||||
endif()
|
||||
if(WASM_BUILD)
|
||||
target_link_libraries(IfcParse ${BCRYPT_LIBRARIES} ${LIBXML2_LIBRARIES})
|
||||
else()
|
||||
|
||||
@@ -29,7 +29,7 @@ from ifcopenshell.util.doc import get_attribute_doc, get_predefined_type_doc, ge
|
||||
import bonsai.tool as tool
|
||||
from types import EllipsisType
|
||||
from typing import Optional, Any, Union, TYPE_CHECKING
|
||||
from collections.abc import Callable, Iterable
|
||||
from collections.abc import Callable, Iterable, Sequence
|
||||
|
||||
if TYPE_CHECKING:
|
||||
import bonsai.bim.prop
|
||||
@@ -51,7 +51,7 @@ if TYPE_CHECKING:
|
||||
|
||||
|
||||
def draw_attributes(
|
||||
props: bpy.types.bpy_prop_collection_idprop[Attribute],
|
||||
props: Union[bpy.types.bpy_prop_collection_idprop[Attribute], Sequence[Attribute]],
|
||||
layout: bpy.types.UILayout,
|
||||
copy_operator: Optional[str] = None,
|
||||
popup_active_attribute: Optional[bonsai.bim.prop.Attribute] = None,
|
||||
@@ -296,7 +296,13 @@ def add_attribute_enum_items_descriptions(
|
||||
new_enum_description.name = description
|
||||
|
||||
|
||||
def add_attribute_description(attribute_blender: bonsai.bim.prop.Attribute, attribute_ifc=None):
|
||||
def add_attribute_description(
|
||||
attribute_blender: bonsai.bim.prop.Attribute,
|
||||
attribute_ifc: Union[ifcopenshell.entity_instance, None] = None,
|
||||
) -> None:
|
||||
"""
|
||||
:param attribute_ifc: IFC Entity to use as a fallback source of description (using "Description" attribute).
|
||||
"""
|
||||
if not attribute_blender.name:
|
||||
return
|
||||
version = tool.Ifc.get_schema()
|
||||
|
||||
@@ -219,9 +219,7 @@ class IfcImporter:
|
||||
self.gross_elements: set[ifcopenshell.entity_instance] = set()
|
||||
self.element_types: set[ifcopenshell.entity_instance] = set()
|
||||
self.spatial_elements: set[ifcopenshell.entity_instance] = set()
|
||||
self.type_products = {}
|
||||
self.meshes: dict[str, OBJECT_DATA_TYPE] = {}
|
||||
self.mesh_shapes = {}
|
||||
self.time = 0
|
||||
self.unit_scale = 1.0
|
||||
# ifc definition ids to blender elements mapping
|
||||
@@ -463,6 +461,10 @@ class IfcImporter:
|
||||
return False
|
||||
|
||||
def calculate_model_offset(self) -> None:
|
||||
# TODO:
|
||||
if isinstance(self.file, ifcopenshell.sqlite):
|
||||
print("WARNING. Calculating model offset for IFCSQLite is not supported.")
|
||||
return
|
||||
props = tool.Georeference.get_georeference_props()
|
||||
if self.ifc_import_settings.false_origin_mode == "MANUAL":
|
||||
tool.Loader.set_manual_blender_offset(self.file)
|
||||
@@ -516,11 +518,52 @@ class IfcImporter:
|
||||
self.set_matrix_world(obj, tool.Loader.apply_blender_offset_to_matrix_world(obj, grid_placement))
|
||||
|
||||
def create_element_types(self):
|
||||
# TODO:
|
||||
if isinstance(self.file, ifcopenshell.sqlite):
|
||||
self.create_element_types_sqlite(self.element_types)
|
||||
return
|
||||
for element_type in self.element_types:
|
||||
if not element_type:
|
||||
continue
|
||||
self.create_element_type(element_type)
|
||||
|
||||
def create_element_types_sqlite(self, element_types: set[ifcopenshell.entity_instance]) -> None:
|
||||
assert isinstance(self.file, ifcopenshell.sqlite)
|
||||
geometry_cache = self.file.get_geometry([e.id() for e in element_types])
|
||||
geometry_meshes: dict[str, bpy.types.Mesh] = {}
|
||||
for geometry_id, geometry in geometry_cache["geometry"].items():
|
||||
verts = geometry["verts"]
|
||||
fake_geometry = type("Geometry", (), {"id": geometry_id})
|
||||
mesh_name = tool.Loader.get_mesh_name_from_shape(fake_geometry) # pyright: ignore[reportArgumentType]
|
||||
mesh = bpy.data.meshes.new(mesh_name)
|
||||
|
||||
if geometry["faces"].size:
|
||||
mesh = tool.Loader.create_mesh_from_shape(
|
||||
mesh=mesh, faces=geometry["faces"].reshape(-1, 3), verts=verts.reshape(-1, 3)
|
||||
)
|
||||
else:
|
||||
vertices = verts.reshape(-1, 3).tolist()
|
||||
edges = geometry["edges"].reshape(-1, 2).tolist()
|
||||
mesh.from_pydata(vertices, edges, [])
|
||||
tool.Loader.link_mesh(fake_geometry, mesh) # pyright: ignore[reportArgumentType]
|
||||
|
||||
mesh["ios_materials"] = geometry["materials"]
|
||||
mesh["ios_material_ids"] = geometry["material_ids"]
|
||||
self.meshes[mesh_name] = mesh
|
||||
geometry_meshes[geometry_id] = mesh
|
||||
|
||||
shapes = geometry_cache["shapes"]
|
||||
for element in element_types:
|
||||
# Allow missing element types to accomodate older ifcsqlite files
|
||||
# that didn't store element types geometry.
|
||||
shape = shapes.get(element.id())
|
||||
if shape:
|
||||
geometry_id = shapes[element.id()]["geometry"]
|
||||
else:
|
||||
geometry_id = None
|
||||
mesh = None if geometry_id is None else geometry_meshes[geometry_id]
|
||||
obj = bpy.data.objects.new(tool.Loader.get_name(element), mesh)
|
||||
self.link_element(element, obj)
|
||||
self.material_creator.create(element, obj, mesh, False)
|
||||
|
||||
def create_element_type(self, element: ifcopenshell.entity_instance) -> None:
|
||||
self.ifc_import_settings.logger.info("Creating object %s", element)
|
||||
mesh = None
|
||||
@@ -547,7 +590,6 @@ class IfcImporter:
|
||||
obj = bpy.data.objects.new(tool.Loader.get_name(element), mesh)
|
||||
self.link_element(element, obj)
|
||||
self.material_creator.create(element, obj, mesh, False)
|
||||
self.type_products[element.GlobalId] = obj
|
||||
|
||||
def create_native_elements(self):
|
||||
if not self.ifc_import_settings.should_load_geometry:
|
||||
@@ -631,15 +673,13 @@ class IfcImporter:
|
||||
verts = geometry["verts"]
|
||||
mesh["has_cartesian_point_offset"] = False
|
||||
|
||||
if geometry["faces"]:
|
||||
if geometry["faces"].size:
|
||||
mesh = tool.Loader.create_mesh_from_shape(
|
||||
mesh=mesh, faces=geometry["faces"].reshape(-1, 3), verts=verts.reshape(-1, 3)
|
||||
)
|
||||
else:
|
||||
e = geometry["edges"]
|
||||
v = verts
|
||||
vertices = [[v[i], v[i + 1], v[i + 2]] for i in range(0, len(v), 3)]
|
||||
edges = [[e[i], e[i + 1]] for i in range(0, len(e), 2)]
|
||||
vertices = verts.reshape(-1, 3).tolist()
|
||||
edges = geometry["edges"].reshape(-1, 2).tolist()
|
||||
mesh.from_pydata(vertices, edges, [])
|
||||
|
||||
mesh["ios_materials"] = geometry["materials"]
|
||||
|
||||
@@ -40,6 +40,7 @@ from mathutils import Vector, Matrix
|
||||
from bonsai.bim.ifc import IfcStore
|
||||
from bonsai.bim.module.model.decorator import ProfileDecorator
|
||||
from bonsai.bim.module.boundary.decorator import BoundaryDecorator
|
||||
from ifcopenshell.util.shape_builder import ShapeBuilder
|
||||
import bonsai.core
|
||||
import bonsai.core.geometry
|
||||
from typing import Union, Optional
|
||||
@@ -1058,15 +1059,11 @@ class AddBoundary(bpy.types.Operator, tool.Ifc.Operator):
|
||||
|
||||
self.unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
|
||||
tool.Model.unit_scale = self.unit_scale
|
||||
builder = ShapeBuilder(tool.Ifc.get())
|
||||
|
||||
surface = tool.Ifc.get().createIfcCurveBoundedPlane()
|
||||
surface.BasisSurface = tool.Ifc.get().createIfcPlane(
|
||||
tool.Ifc.get().createIfcAxis2Placement3D(
|
||||
tool.Ifc.get().createIfcCartesianPoint([o / self.unit_scale for o in p1]),
|
||||
tool.Ifc.get().createIfcDirection([float(o) for o in z_axis]),
|
||||
tool.Ifc.get().createIfcDirection([float(o) for o in x_axis]),
|
||||
)
|
||||
)
|
||||
placement = builder.create_axis2_placement_3d([o / self.unit_scale for o in p1], z_axis, x_axis)
|
||||
surface.BasisSurface = tool.Ifc.get().create_entity("IfcPlane", placement)
|
||||
|
||||
if tool.Ifc.get().schema != "IFC2X3":
|
||||
points = [tool.Model.convert_si_to_unit(list(co)) for co in polygon.exterior.coords]
|
||||
|
||||
@@ -56,6 +56,7 @@ class ClassificationsData:
|
||||
data = element.get_info()
|
||||
if tool.Ifc.get().schema == "IFC2X3" and element.EditionDate:
|
||||
data["EditionDate"] = ifcopenshell.util.date.ifc2datetime(data["EditionDate"])
|
||||
data["Name"] = data["Name"] or "Unnamed"
|
||||
results.append(data)
|
||||
return results
|
||||
|
||||
|
||||
@@ -17,7 +17,6 @@
|
||||
# along with Bonsai. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import bpy
|
||||
import json
|
||||
import ifcopenshell
|
||||
import ifcopenshell.api
|
||||
import ifcopenshell.api.pset
|
||||
@@ -192,21 +191,11 @@ class EnableEditingClassification(bpy.types.Operator):
|
||||
classification: bpy.props.IntProperty()
|
||||
|
||||
def execute(self, context):
|
||||
def callback(name, prop, data):
|
||||
if name == "ReferenceTokens":
|
||||
geo_props = tool.Georeference.get_georeference_props()
|
||||
new = geo_props.projected_crs.add()
|
||||
new.name = name
|
||||
new.data_type = "string"
|
||||
new.is_null = data[name] is None
|
||||
new.is_optional = True
|
||||
new.string_value = "" if new.is_null else json.dumps(data[name])
|
||||
return True
|
||||
|
||||
props = tool.Classification.get_classification_props()
|
||||
props.classification_attributes.clear()
|
||||
bonsai.bim.helper.import_attributes(
|
||||
tool.Ifc.get().by_id(self.classification), props.classification_attributes, callback
|
||||
tool.Ifc.get().by_id(self.classification),
|
||||
props.classification_attributes,
|
||||
)
|
||||
props.active_classification_id = self.classification
|
||||
return {"FINISHED"}
|
||||
@@ -248,13 +237,7 @@ class EditClassification(bpy.types.Operator, tool.Ifc.Operator):
|
||||
|
||||
def _execute(self, context):
|
||||
props = tool.Classification.get_classification_props()
|
||||
|
||||
def callback(attributes, prop):
|
||||
if prop.name == "ReferenceTokens":
|
||||
attributes[prop.name] = json.loads(prop.string_value)
|
||||
return True
|
||||
|
||||
attributes = bonsai.bim.helper.export_attributes(props.classification_attributes, callback=callback)
|
||||
attributes = bonsai.bim.helper.export_attributes(props.classification_attributes)
|
||||
ifc_file = tool.Ifc.get()
|
||||
ifcopenshell.api.classification.edit_classification(
|
||||
ifc_file,
|
||||
|
||||
@@ -73,6 +73,7 @@ class BIM_PT_classifications(Panel):
|
||||
self.draw_ui(classification)
|
||||
|
||||
def draw_add_manual_ui(self, context):
|
||||
assert self.layout
|
||||
if self.props.is_adding:
|
||||
bonsai.bim.helper.draw_attributes(self.props.classification_attributes, self.layout)
|
||||
row = self.layout.row(align=True)
|
||||
@@ -83,10 +84,12 @@ class BIM_PT_classifications(Panel):
|
||||
row.operator("bim.enable_adding_manual_classification", text="Add Classification", icon="ADD")
|
||||
|
||||
def draw_add_bsdd_ui(self, context):
|
||||
assert self.layout
|
||||
row = self.layout.row()
|
||||
row.operator("bim.add_classification_from_bsdd", icon="ADD")
|
||||
|
||||
def draw_add_file_ui(self, context):
|
||||
assert self.layout
|
||||
if ClassificationsData.data["has_classification_file"]:
|
||||
row = self.layout.row(align=True)
|
||||
row.prop(self.props, "available_classifications", text="")
|
||||
@@ -97,13 +100,15 @@ class BIM_PT_classifications(Panel):
|
||||
row.label(text="No Active Classification Library")
|
||||
row.operator("bim.load_classification_library", text="", icon="IMPORT")
|
||||
|
||||
def draw_editable_ui(self):
|
||||
def draw_editable_ui(self) -> None:
|
||||
assert self.layout
|
||||
row = self.layout.row(align=True)
|
||||
row.operator("bim.edit_classification", text="Save changes", icon="CHECKMARK")
|
||||
row.operator("bim.disable_editing_classification", text="", icon="CANCEL")
|
||||
bonsai.bim.helper.draw_attributes(self.props.classification_attributes, self.layout)
|
||||
|
||||
def draw_ui(self, classification):
|
||||
def draw_ui(self, classification: dict[str, Any]) -> None:
|
||||
assert self.layout
|
||||
row = self.layout.row(align=True)
|
||||
row.label(text=classification["Name"], icon="ASSET_MANAGER")
|
||||
if not self.props.active_classification_id:
|
||||
|
||||
@@ -332,7 +332,7 @@ class DecoratorData:
|
||||
returns font size in mm for current ifc text object"""
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
assert element
|
||||
props = tool.Drawing.get_text_props(obj)
|
||||
|
||||
# getting font size
|
||||
pset_data = ifcopenshell.util.element.get_pset(element, "EPset_Annotation") or {}
|
||||
# use `regular` as default
|
||||
@@ -353,20 +353,17 @@ class DecoratorData:
|
||||
newline_at = pset_data.get("Newline_At", 0)
|
||||
|
||||
# other attributes
|
||||
props_literals = props.literals
|
||||
props_literals_n = len(props.literals)
|
||||
literals = tool.Drawing.get_text_literal(obj, return_list=True)
|
||||
literals_data = []
|
||||
for i, literal in enumerate(literals):
|
||||
assert isinstance(literals, list)
|
||||
literals_data: list[dict[str, Any]] = []
|
||||
product = tool.Drawing.get_assigned_product(element) or element
|
||||
for literal in literals:
|
||||
literal_value = literal.Literal
|
||||
literal_data = {
|
||||
"Literal": literal.Literal,
|
||||
"Literal": literal_value,
|
||||
"BoxAlignment": literal.BoxAlignment,
|
||||
"CurrentValue": tool.Drawing.replace_text_literal_variables(literal_value, product),
|
||||
}
|
||||
if i < props_literals_n:
|
||||
literal_data["CurrentValue"] = props_literals[i].value
|
||||
else:
|
||||
literal_data["CurrentValue"] = literal.Literal
|
||||
|
||||
literals_data.append(literal_data)
|
||||
|
||||
return {"Literals": literals_data, "FontSize": font_size, "Symbol": symbol, "Newline_At": newline_at}
|
||||
|
||||
@@ -601,7 +601,7 @@ class BaseDecorator:
|
||||
props = tool.Drawing.get_text_props(obj)
|
||||
text_data = DecoratorData.data["text"].get(obj.name, None)
|
||||
if props.is_editing:
|
||||
text_data = text_data | props.get_text_edited_data()
|
||||
text_data = props.get_text_edited_data()
|
||||
literals_data = text_data["Literals"]
|
||||
symbol = text_data["Symbol"]
|
||||
newline_at = text_data["Newline_At"]
|
||||
|
||||
@@ -2971,47 +2971,9 @@ class EditTextPopup(bpy.types.Operator):
|
||||
first_run: bpy.props.BoolProperty(default=True)
|
||||
|
||||
def draw(self, context):
|
||||
# shares most of the code with BIM_PT_text.draw()
|
||||
# need to keep them in sync or move to some common function
|
||||
# NOTE: that `popup_active_attribute` is used here when it's not used in `BIM_PT_text.draw()`
|
||||
from bonsai.bim.module.drawing.ui import BIM_PT_text
|
||||
|
||||
assert self.layout
|
||||
obj = context.active_object
|
||||
assert obj
|
||||
props = tool.Drawing.get_text_props(obj)
|
||||
|
||||
row = self.layout.row(align=True)
|
||||
row.operator("bim.add_text_literal", icon="ADD", text="Add Literal")
|
||||
|
||||
row = self.layout.row(align=True)
|
||||
row.prop(props, "font_size")
|
||||
|
||||
for i, literal_props in enumerate(props.literals):
|
||||
box = self.layout.box()
|
||||
row = self.layout.row(align=True)
|
||||
|
||||
row = box.row(align=True)
|
||||
row.label(text=f"Literal[{i}]:")
|
||||
row.operator("bim.remove_text_literal", icon="X", text="").literal_prop_id = i
|
||||
|
||||
# skip BoxAlignment since we're going to format it ourselves
|
||||
attributes = [a for a in literal_props.attributes if a.name != "BoxAlignment"]
|
||||
bonsai.bim.helper.draw_attributes(attributes, box, popup_active_attribute=attributes[0])
|
||||
|
||||
row = box.row(align=True)
|
||||
cols = [row.column(align=True) for i in range(3)]
|
||||
for i in range(9):
|
||||
cols[i % 3].prop(
|
||||
literal_props,
|
||||
"box_alignment",
|
||||
text="",
|
||||
index=i,
|
||||
icon="RADIOBUT_ON" if literal_props.box_alignment[i] else "RADIOBUT_OFF",
|
||||
)
|
||||
|
||||
col = row.column(align=True)
|
||||
col.label(text=" Text box alignment:")
|
||||
col.label(text=f' {literal_props.attributes["BoxAlignment"].string_value}')
|
||||
BIM_PT_text.draw_text_editing_ui(self, context, popup_mode=True)
|
||||
|
||||
def cancel(self, context):
|
||||
# disable editing when dialog is closed
|
||||
@@ -3066,7 +3028,6 @@ class DisableEditingText(bpy.types.Operator, tool.Ifc.Operator):
|
||||
|
||||
# force update this object's font size for viewport display
|
||||
DecoratorData.data.pop(obj.name, None)
|
||||
tool.Drawing.update_text_value(obj)
|
||||
tool.Blender.update_viewport()
|
||||
|
||||
|
||||
@@ -3123,6 +3084,7 @@ class RemoveTextLiteral(bpy.types.Operator):
|
||||
assert obj
|
||||
props = tool.Drawing.get_text_props(obj)
|
||||
props.literals.remove(self.literal_prop_id)
|
||||
tool.Blender.update_viewport()
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
@@ -3140,6 +3102,7 @@ class OrderTextLiteralUp(bpy.types.Operator):
|
||||
assert obj
|
||||
props = tool.Drawing.get_text_props(obj)
|
||||
props.literals.move(self.literal_prop_id, self.literal_prop_id - 1)
|
||||
tool.Blender.update_viewport()
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
@@ -3157,6 +3120,7 @@ class OrderTextLiteralDown(bpy.types.Operator):
|
||||
assert obj
|
||||
props = tool.Drawing.get_text_props(obj)
|
||||
props.literals.move(self.literal_prop_id, self.literal_prop_id + 1)
|
||||
tool.Blender.update_viewport()
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
|
||||
@@ -267,12 +267,6 @@ def update_titleblocks(self, context):
|
||||
def update_should_draw_decorations(self, context: bpy.types.Context) -> None:
|
||||
if self.should_draw_decorations:
|
||||
# TODO: design a proper text variable templating renderer
|
||||
collection = tool.Blender.get_object_bim_props(context.scene.camera).collection
|
||||
for obj in collection.objects:
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
if not element or not tool.Drawing.is_annotation_object_type(element, ["TEXT", "TEXT_LEADER"]):
|
||||
continue
|
||||
tool.Drawing.update_text_value(obj)
|
||||
refresh_drawing_data()
|
||||
if bpy.app.background:
|
||||
return
|
||||
@@ -709,16 +703,12 @@ class LiteralProps(PropertyGroup):
|
||||
return self.get("box_alignment", DEFAULT_BOX_ALIGNMENT)
|
||||
|
||||
attributes: CollectionProperty(name="Attributes", type=Attribute)
|
||||
# Current text value with evaluated expressions stored in `value`.
|
||||
# The original (Literal) value stored in `attributes['Literal']`
|
||||
# and can be accessed with `get_text()`
|
||||
value: StringProperty(name="Value", default="TEXT")
|
||||
box_alignment: BoolVectorProperty(
|
||||
name="Box alignment", size=9, set=set_box_alignment, get=get_box_alignment, default=DEFAULT_BOX_ALIGNMENT
|
||||
)
|
||||
ifc_definition_id: IntProperty(name="IFC definition ID", default=0)
|
||||
|
||||
def get_literal_edited_data(self):
|
||||
def get_literal_edited_data(self) -> dict[str, str]:
|
||||
text_data = {
|
||||
"CurrentValue": self.attributes["Literal"].string_value,
|
||||
"Literal": self.attributes["Literal"].string_value,
|
||||
@@ -729,7 +719,7 @@ class LiteralProps(PropertyGroup):
|
||||
if TYPE_CHECKING:
|
||||
attributes: bpy.types.bpy_prop_collection_idprop[Attribute]
|
||||
value: str
|
||||
box_alignment: str
|
||||
box_alignment: tuple[bool, bool, bool, bool, bool, bool, bool, bool, bool]
|
||||
ifc_definition_id: int
|
||||
|
||||
|
||||
@@ -748,12 +738,43 @@ class BIMTextProperties(PropertyGroup):
|
||||
name="Font Size",
|
||||
)
|
||||
newline_at: IntProperty(name="Newline At")
|
||||
symbol: EnumProperty( # pyright: ignore[reportRedeclaration]
|
||||
name="Symbol",
|
||||
description="Symbol from symbols.svg to use for this text.",
|
||||
items=[(s, s, "") for s in ["NO SYMBOL", "CUSTOM SYMBOL"] + tool.Drawing.DEFAULT_SYMBOLS],
|
||||
default="NO SYMBOL",
|
||||
)
|
||||
custom_symbol: StringProperty( # pyright: ignore[reportRedeclaration]
|
||||
name="Custom Symbol",
|
||||
description="Non-default symbol to use for this text.",
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
is_editing: bool
|
||||
literals: bpy.types.bpy_prop_collection_idprop[LiteralProps]
|
||||
font_size: str
|
||||
newline_at: int
|
||||
symbol: Union[str, Literal["NO SYMBOL", "CUSTOM SYMBOL"]]
|
||||
custom_symbol: str
|
||||
|
||||
def get_symbol(self) -> Union[str, None]:
|
||||
if self.symbol == "NO SYMBOL":
|
||||
return None
|
||||
elif self.symbol == "CUSTOM SYMBOL":
|
||||
return self.custom_symbol or None
|
||||
else:
|
||||
return self.symbol
|
||||
|
||||
def set_symbol(self, symbol: Union[str, None]):
|
||||
if not symbol:
|
||||
self.property_unset("symbol")
|
||||
self.property_unset("custom_symbol")
|
||||
elif symbol in tool.Drawing.DEFAULT_SYMBOLS:
|
||||
self.symbol = symbol
|
||||
self.property_unset("custom_symbol")
|
||||
else:
|
||||
self.symbol = "CUSTOM SYMBOL"
|
||||
self.custom_symbol = symbol
|
||||
|
||||
def get_text_edited_data(self) -> dict[str, Any]:
|
||||
"""should be called only if `is_editing`
|
||||
@@ -768,6 +789,7 @@ class BIMTextProperties(PropertyGroup):
|
||||
"Literals": literals_data,
|
||||
"FontSize": float(self.font_size),
|
||||
"Newline_At": int(self.newline_at),
|
||||
"Symbol": self.get_symbol(),
|
||||
}
|
||||
return text_data
|
||||
|
||||
|
||||
@@ -823,6 +823,7 @@ class SvgWriter:
|
||||
x_offset = self.raw_width / 2
|
||||
y_offset = self.raw_height / 2
|
||||
element = tool.Ifc.get_entity(text_obj)
|
||||
assert element
|
||||
text_literals = tool.Drawing.get_text_literal(text_obj, return_list=True)
|
||||
product = tool.Drawing.get_assigned_product(element)
|
||||
|
||||
|
||||
@@ -29,7 +29,7 @@ from bonsai.bim.module.drawing.data import (
|
||||
ElementFiltersData,
|
||||
DecoratorData,
|
||||
)
|
||||
from typing import TYPE_CHECKING
|
||||
from typing import TYPE_CHECKING, Union
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from bonsai.bim.module.drawing.prop import DocProperties, Drawing, Sheet
|
||||
@@ -565,56 +565,77 @@ class BIM_PT_text(Panel):
|
||||
return
|
||||
return tool.Drawing.is_annotation_object_type(element, ["TEXT", "TEXT_LEADER"])
|
||||
|
||||
def draw_text_editing_ui(
|
||||
self: Union[bpy.types.Panel, bpy.types.Operator],
|
||||
context: bpy.types.Context,
|
||||
*,
|
||||
popup_mode: bool = False,
|
||||
) -> None:
|
||||
# The method is also used in EditTextPopup.draw().
|
||||
assert self.layout
|
||||
obj = context.active_object
|
||||
assert obj
|
||||
props = tool.Drawing.get_text_props(obj)
|
||||
|
||||
row = self.layout.row(align=True)
|
||||
|
||||
if popup_mode:
|
||||
row.operator("bim.add_text_literal", icon="ADD", text="Add Literal")
|
||||
else:
|
||||
row.operator("bim.edit_text", icon="CHECKMARK")
|
||||
row.operator("bim.add_text_literal", icon="ADD", text="")
|
||||
row.operator("bim.disable_editing_text", icon="CANCEL", text="")
|
||||
|
||||
row = self.layout.row(align=True)
|
||||
row.prop(props, "font_size")
|
||||
row = self.layout.row(align=True)
|
||||
row.prop(props, "newline_at")
|
||||
|
||||
row = self.layout.row(align=True)
|
||||
row.prop(props, "symbol")
|
||||
if props.symbol == "CUSTOM SYMBOL":
|
||||
row = self.layout.row(align=True)
|
||||
row.prop(props, "custom_symbol", text="")
|
||||
|
||||
for i, literal_props in enumerate(props.literals):
|
||||
box = self.layout.box()
|
||||
row = self.layout.row(align=True)
|
||||
|
||||
row = box.row(align=True)
|
||||
row.label(text=f"Literal[{i}]:")
|
||||
if i > 0:
|
||||
row.operator("bim.order_text_literal_up", icon="TRIA_UP", text="").literal_prop_id = i
|
||||
if i < len(props.literals) - 1:
|
||||
row.operator("bim.order_text_literal_down", icon="TRIA_DOWN", text="").literal_prop_id = i
|
||||
row.operator("bim.remove_text_literal", icon="X", text="").literal_prop_id = i
|
||||
|
||||
# skip BoxAlignment since we're going to format it ourselves
|
||||
attributes = [a for a in literal_props.attributes if a.name != "BoxAlignment"]
|
||||
popup_active_attribute = attributes[0] if popup_mode else None
|
||||
bonsai.bim.helper.draw_attributes(attributes, box, popup_active_attribute=popup_active_attribute)
|
||||
|
||||
row = box.row(align=True)
|
||||
cols = [row.column(align=True) for i in range(3)]
|
||||
for i in range(9):
|
||||
cols[i % 3].prop(
|
||||
literal_props,
|
||||
"box_alignment",
|
||||
text="",
|
||||
index=i,
|
||||
icon="RADIOBUT_ON" if literal_props.box_alignment[i] else "RADIOBUT_OFF",
|
||||
)
|
||||
|
||||
col = row.column(align=True)
|
||||
col.label(text=" Text box alignment:")
|
||||
col.label(text=f' {literal_props.attributes["BoxAlignment"].string_value}')
|
||||
|
||||
def draw(self, context):
|
||||
obj = context.active_object
|
||||
assert obj
|
||||
props = tool.Drawing.get_text_props(obj)
|
||||
|
||||
if props.is_editing:
|
||||
# shares most of the code with EditTextPopup.draw()
|
||||
# need to keep them in sync or move to some common function
|
||||
|
||||
row = self.layout.row(align=True)
|
||||
row.operator("bim.edit_text", icon="CHECKMARK")
|
||||
row.operator("bim.add_text_literal", icon="ADD", text="")
|
||||
row.operator("bim.disable_editing_text", icon="CANCEL", text="")
|
||||
|
||||
row = self.layout.row(align=True)
|
||||
row.prop(props, "font_size")
|
||||
row = self.layout.row(align=True)
|
||||
row.prop(props, "newline_at")
|
||||
|
||||
for i, literal_props in enumerate(props.literals):
|
||||
box = self.layout.box()
|
||||
row = self.layout.row(align=True)
|
||||
|
||||
row = box.row(align=True)
|
||||
row.label(text=f"Literal[{i}]:")
|
||||
if i > 0:
|
||||
row.operator("bim.order_text_literal_up", icon="TRIA_UP", text="").literal_prop_id = i
|
||||
if i < len(props.literals) - 1:
|
||||
row.operator("bim.order_text_literal_down", icon="TRIA_DOWN", text="").literal_prop_id = i
|
||||
row.operator("bim.remove_text_literal", icon="X", text="").literal_prop_id = i
|
||||
|
||||
# skip BoxAlignment since we're going to format it ourselves
|
||||
attributes = [a for a in literal_props.attributes if a.name != "BoxAlignment"]
|
||||
bonsai.bim.helper.draw_attributes(attributes, box)
|
||||
|
||||
row = box.row(align=True)
|
||||
cols = [row.column(align=True) for i in range(3)]
|
||||
for i in range(9):
|
||||
cols[i % 3].prop(
|
||||
literal_props,
|
||||
"box_alignment",
|
||||
text="",
|
||||
index=i,
|
||||
icon="RADIOBUT_ON" if literal_props.box_alignment[i] else "RADIOBUT_OFF",
|
||||
)
|
||||
|
||||
col = row.column(align=True)
|
||||
col.label(text=" Text box alignment:")
|
||||
col.label(text=f' {literal_props.attributes["BoxAlignment"].string_value}')
|
||||
|
||||
self.draw_text_editing_ui(context)
|
||||
else:
|
||||
text_data = DecoratorData.get_text_data(obj)
|
||||
|
||||
|
||||
@@ -28,6 +28,7 @@ import bonsai.tool as tool
|
||||
from math import pi, pow
|
||||
from mathutils import Vector, Matrix, geometry
|
||||
from typing import Union, Any, TypeVar, Optional
|
||||
from ifcopenshell.util.shape_builder import ShapeBuilder
|
||||
|
||||
T = TypeVar("T")
|
||||
|
||||
@@ -35,6 +36,7 @@ T = TypeVar("T")
|
||||
class Helper:
|
||||
def __init__(self, file: ifcopenshell.file):
|
||||
self.file = file
|
||||
self.builder = ShapeBuilder(file)
|
||||
self.unit_scale = ifcopenshell.util.unit.calculate_unit_scale(self.file)
|
||||
|
||||
# We can detect a rectangular extrusion by picking any face, then find an
|
||||
@@ -340,8 +342,10 @@ class Helper:
|
||||
def create_extruded_area_solid(
|
||||
self, mesh: bpy.types.Mesh, extrusion_indices: list[int], profile_def: dict[str, Any]
|
||||
) -> ifcopenshell.entity_instance:
|
||||
position = self.create_ifc_axis_2_placement_3d(
|
||||
profile_def["curve_ucs"]["center"], profile_def["curve_ucs"]["z_axis"], profile_def["curve_ucs"]["x_axis"]
|
||||
position = self.builder.create_axis2_placement_3d(
|
||||
self.convert_si_to_unit(profile_def["curve_ucs"]["center"]),
|
||||
profile_def["curve_ucs"]["z_axis"],
|
||||
profile_def["curve_ucs"]["x_axis"],
|
||||
)
|
||||
direction = self.get_extrusion_direction(mesh, extrusion_indices, profile_def["curve_ucs"])
|
||||
unit_direction = direction.normalized()
|
||||
@@ -505,12 +509,3 @@ class Helper:
|
||||
return self.file.createIfcAxis2Placement2D(
|
||||
self.create_cartesian_point(point.x, point.y), self.file.createIfcDirection((forward.x, forward.y))
|
||||
)
|
||||
|
||||
def create_ifc_axis_2_placement_3d(
|
||||
self, point: Vector, up: Vector, forward: Vector
|
||||
) -> ifcopenshell.entity_instance:
|
||||
return self.file.createIfcAxis2Placement3D(
|
||||
self.create_cartesian_point(point.x, point.y, point.z),
|
||||
self.file.createIfcDirection((up.x, up.y, up.z)),
|
||||
self.file.createIfcDirection((forward.x, forward.y, forward.z)),
|
||||
)
|
||||
|
||||
@@ -1452,16 +1452,20 @@ class Hotkey(bpy.types.Operator, tool.Ifc.Operator):
|
||||
self.report({"ERROR"}, "No LAYER2 objects selected")
|
||||
return
|
||||
|
||||
if layer2_bases and len(set(layer2_bases)) > 1:
|
||||
# --- tolerance check ---
|
||||
tolerance = 1e-5 # to provide a little wiggle room
|
||||
if layer2_bases and (max(layer2_bases) - min(layer2_bases)) > tolerance:
|
||||
min_base = min(layer2_bases)
|
||||
max_base = max(layer2_bases)
|
||||
self.report(
|
||||
{"ERROR"},
|
||||
f"Selected LAYER2 objects have different base heights ({min_base:.3f}m to {max_base:.3f}m). All objects must be at the exact same base level.",
|
||||
f"Selected LAYER2 objects have different base heights ({min_base:.3f}m to {max_base:.3f}m). "
|
||||
f"All objects must be at the exact same base level (tolerance {tolerance}).",
|
||||
)
|
||||
return
|
||||
|
||||
common_base = layer2_bases[0]
|
||||
# use the mean base as the "common" one to avoid floating-point mismatches
|
||||
common_base = sum(layer2_bases) / len(layer2_bases)
|
||||
new_height = cursor_z - common_base
|
||||
|
||||
if new_height > 0:
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
from __future__ import annotations
|
||||
import bpy
|
||||
import os
|
||||
import ifcopenshell
|
||||
import bonsai.bim
|
||||
import bonsai.tool as tool
|
||||
from bonsai.bim.helper import prop_with_search, draw_attributes
|
||||
@@ -259,10 +260,10 @@ class BIM_PT_project(Panel):
|
||||
def draw_editable_file_info(self, context):
|
||||
pprops = self.props
|
||||
|
||||
if tool.Ifc.get():
|
||||
if ifc_file := tool.Ifc.get():
|
||||
row = self.layout.row(align=True)
|
||||
row.label(text="IFC Schema", icon="FILE_CACHE")
|
||||
row.label(text=tool.Ifc.get().schema)
|
||||
row.label(text=ifc_file.schema)
|
||||
|
||||
if pprops.is_editing:
|
||||
row = self.layout.row(align=True)
|
||||
@@ -283,7 +284,10 @@ class BIM_PT_project(Panel):
|
||||
else:
|
||||
row = self.layout.row(align=True)
|
||||
row.label(text="IFC MVD", icon="FILE_HIDDEN")
|
||||
mvd = "".join(tool.Ifc.get().wrapped_data.header.file_description.description)
|
||||
if isinstance(ifc_file, ifcopenshell.sqlite):
|
||||
mvd = ifc_file.mvd_str
|
||||
else:
|
||||
mvd = "".join(ifc_file.wrapped_data.header.file_description.description)
|
||||
if "[" in mvd:
|
||||
mvd = mvd.split("[")[1][0:-1]
|
||||
row.label(text=mvd)
|
||||
|
||||
@@ -203,6 +203,7 @@ class AssignClass(bpy.types.Operator, tool.Ifc.Operator):
|
||||
predefined_type: str
|
||||
userdefined_type: str
|
||||
context_id: int
|
||||
props_to_pset: bool
|
||||
should_add_representation: bool
|
||||
ifc_representation_class: str
|
||||
|
||||
@@ -385,13 +386,8 @@ class AssignClass(bpy.types.Operator, tool.Ifc.Operator):
|
||||
# TODO: reload representation might lead to the object being replaced by object of the other type.
|
||||
# We probably should track it somehow and keep the original selection.
|
||||
|
||||
# Validation selection.
|
||||
new_selected_objects = list(filter(tool.Blender.is_valid_data_block, current_selection[2]))
|
||||
active_object = current_selection[1]
|
||||
if active_object and not tool.Blender.is_valid_data_block(active_object):
|
||||
active_object = None
|
||||
current_selection = (current_selection[0], active_object, new_selected_objects)
|
||||
|
||||
# Validate selection and reapply it.
|
||||
current_selection = tool.Blender.validate_object_selection(*current_selection)
|
||||
tool.Blender.set_objects_selection(*current_selection)
|
||||
|
||||
|
||||
|
||||
@@ -21,6 +21,9 @@ from . import ui, prop, operator
|
||||
|
||||
classes = (
|
||||
operator.ExecuteIfcTester,
|
||||
operator.StartIfcTesterWebapp,
|
||||
operator.StopIfcTesterWebapp,
|
||||
operator.OpenIfcTesterWebapp,
|
||||
operator.SelectRequirement,
|
||||
operator.SelectFailedEntities,
|
||||
operator.ExportBcf,
|
||||
|
||||
@@ -22,6 +22,13 @@ import time
|
||||
import tempfile
|
||||
import webbrowser
|
||||
import traceback
|
||||
import subprocess
|
||||
import socket
|
||||
import sys
|
||||
import threading
|
||||
import asyncio
|
||||
import socketio
|
||||
from aiohttp import web
|
||||
import ifctester
|
||||
import ifctester.ids
|
||||
import ifctester.reporter
|
||||
@@ -32,6 +39,167 @@ from bpy_extras.io_utils import ExportHelper
|
||||
from pathlib import Path
|
||||
from typing import Union
|
||||
|
||||
webapp_process = None
|
||||
websocket_server_thread = None
|
||||
websocket_app = None
|
||||
websocket_runner = None
|
||||
|
||||
|
||||
class IfcTesterWebSocketServer:
|
||||
def __init__(self, port):
|
||||
self.port = port
|
||||
self.sio = socketio.AsyncServer(cors_allowed_origins="*", async_mode="aiohttp")
|
||||
self.app = web.Application()
|
||||
self.sio.attach(self.app)
|
||||
self.runner = None
|
||||
self.site = None
|
||||
self.loop = None
|
||||
self.shutdown_event = None
|
||||
|
||||
# Register namespace
|
||||
self.sio.register_namespace(IfcTesterNamespace("/ifctester"))
|
||||
|
||||
# Add health check route
|
||||
self.app.router.add_get("/health", self.health_check)
|
||||
|
||||
async def health_check(self, request):
|
||||
return web.Response(text="OK", content_type="text/plain")
|
||||
|
||||
async def start_server(self):
|
||||
self.loop = asyncio.get_event_loop()
|
||||
self.shutdown_event = asyncio.Event()
|
||||
|
||||
self.runner = web.AppRunner(self.app)
|
||||
await self.runner.setup()
|
||||
self.site = web.TCPSite(self.runner, "127.0.0.1", self.port)
|
||||
await self.site.start()
|
||||
print(f"IfcTester WebSocket server started on 127.0.0.1:{self.port}")
|
||||
|
||||
try:
|
||||
# Wait for shutdown signal
|
||||
await self.shutdown_event.wait()
|
||||
except asyncio.CancelledError:
|
||||
print("WebSocket server received cancellation")
|
||||
finally:
|
||||
await self._cleanup()
|
||||
|
||||
async def _cleanup(self):
|
||||
try:
|
||||
# Disconnect all clients
|
||||
print("Shutting down SocketIO...")
|
||||
|
||||
try:
|
||||
await self.sio.shutdown()
|
||||
except Exception as e:
|
||||
print(f"Error shutting down socketio: {e}")
|
||||
|
||||
# Stop the web server
|
||||
if self.site:
|
||||
print("Stopping web server...")
|
||||
await asyncio.wait_for(self.site.stop(), timeout=2.0)
|
||||
self.site = None
|
||||
|
||||
# Clean up the runner
|
||||
if self.runner:
|
||||
print("Cleaning up runner...")
|
||||
await asyncio.wait_for(self.runner.cleanup(), timeout=2.0)
|
||||
self.runner = None
|
||||
|
||||
print("IfcTester WebSocket server stopped")
|
||||
except TimeoutError:
|
||||
print("Websocket server cleanup timed out, forcing shutdown")
|
||||
except Exception as e:
|
||||
print(f"Error during websocket cleanup: {e}")
|
||||
|
||||
def stop_server(self):
|
||||
if self.loop and self.shutdown_event and not self.shutdown_event.is_set():
|
||||
try:
|
||||
self.loop.call_soon_threadsafe(self.shutdown_event.set)
|
||||
except Exception as e:
|
||||
print(f"Error sending shutdown signal: {e}")
|
||||
|
||||
|
||||
class IfcTesterNamespace(socketio.AsyncNamespace):
|
||||
def __init__(self, namespace):
|
||||
super().__init__(namespace)
|
||||
|
||||
async def on_connect(self, sid, environ):
|
||||
print(f"IfcTester webapp client connected: {sid}")
|
||||
await self.emit("status", {"connected": True}, room=sid)
|
||||
|
||||
async def on_disconnect(self, sid):
|
||||
print(f"IfcTester webapp client disconnected: {sid}")
|
||||
|
||||
async def on_audit_ids(self, sid, data):
|
||||
request_id = None
|
||||
try:
|
||||
request_id = data.get("id")
|
||||
ids_string = data.get("ids")
|
||||
|
||||
if not request_id:
|
||||
await self.emit("error", {"error": "No request ID provided"}, room=sid)
|
||||
return
|
||||
|
||||
if not ids_string:
|
||||
await self.emit("error", {"id": request_id, "error": "No IDS XML string provided"}, room=sid)
|
||||
return
|
||||
|
||||
print(f"Processing IDS audit request {request_id}")
|
||||
|
||||
# Check if IFC is loaded in Bonsai
|
||||
ifc = tool.Ifc.get()
|
||||
if not ifc:
|
||||
await self.emit(
|
||||
"error", {"id": request_id, "error": "No IFC model is currently loaded in Bonsai"}, room=sid
|
||||
)
|
||||
return
|
||||
|
||||
# Parse IDS from string
|
||||
try:
|
||||
ids = ifctester.ids.from_string(ids_string)
|
||||
except Exception as e:
|
||||
await self.emit("error", {"id": request_id, "error": f"Failed to parse IDS XML: {str(e)}"}, room=sid)
|
||||
return
|
||||
|
||||
# Validate IFC against IDS
|
||||
try:
|
||||
ids.validate(ifc)
|
||||
except Exception as e:
|
||||
await self.emit("error", {"id": request_id, "error": f"Validation failed: {str(e)}"}, room=sid)
|
||||
return
|
||||
|
||||
# Generate reports
|
||||
try:
|
||||
# JSON report
|
||||
json_reporter = ifctester.reporter.Json(ids)
|
||||
json_reporter.report()
|
||||
json_report = json_reporter.to_string()
|
||||
|
||||
# HTML report
|
||||
html_reporter = ifctester.reporter.Html(ids)
|
||||
html_reporter.report()
|
||||
html_report = html_reporter.to_string()
|
||||
|
||||
# Send results back
|
||||
await self.emit(
|
||||
"audit_result", {"id": request_id, "json_report": json_report, "html_report": html_report}, room=sid
|
||||
)
|
||||
|
||||
print(f"Successfully processed IDS audit request {request_id}")
|
||||
|
||||
except Exception as e:
|
||||
await self.emit("error", {"id": request_id, "error": f"Failed to generate reports: {str(e)}"}, room=sid)
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error processing audit request: {str(e)}")
|
||||
import traceback
|
||||
|
||||
print(traceback.format_exc())
|
||||
await self.emit("error", {"id": request_id, "error": f"Internal server error: {str(e)}"}, room=sid)
|
||||
|
||||
async def on_ping(self, sid, data):
|
||||
await self.emit("pong", {"timestamp": data.get("timestamp")}, room=sid)
|
||||
|
||||
|
||||
class ExecuteIfcTester(bpy.types.Operator):
|
||||
bl_idname = "bim.execute_ifc_tester"
|
||||
@@ -124,6 +292,171 @@ class ExecuteIfcTester(bpy.types.Operator):
|
||||
new_spec.status = spec["status"]
|
||||
|
||||
|
||||
class StartIfcTesterWebapp(bpy.types.Operator):
|
||||
bl_idname = "bim.start_ifc_tester_webapp"
|
||||
bl_label = "Start IfcTester Webapp"
|
||||
bl_description = "Start the IfcTester webapp server and open it in the default browser"
|
||||
|
||||
def execute(self, context):
|
||||
global webapp_process, websocket_server_thread, websocket_app
|
||||
|
||||
props = tool.Tester.get_tester_props()
|
||||
|
||||
if webapp_process is not None or websocket_server_thread is not None:
|
||||
self.report({"WARNING"}, "IfcTester webapp is already running")
|
||||
return {"CANCELLED"}
|
||||
|
||||
try:
|
||||
import ifctester.webapp.serve
|
||||
except ImportError:
|
||||
self.report(
|
||||
{"ERROR"}, "IfcTester webapp not available. Please ensure the latest version of ifctester is installed."
|
||||
)
|
||||
return {"CANCELLED"}
|
||||
|
||||
webapp_port = self.find_free_port()
|
||||
websocket_port = self.find_free_port()
|
||||
|
||||
# Get the path to the serve.py module
|
||||
webapp_serve_path = ifctester.webapp.serve.__file__
|
||||
|
||||
try:
|
||||
# Start the websocket server in a thread
|
||||
websocket_app = IfcTesterWebSocketServer(websocket_port)
|
||||
|
||||
def run_websocket_server():
|
||||
loop = asyncio.new_event_loop()
|
||||
asyncio.set_event_loop(loop)
|
||||
try:
|
||||
loop.run_until_complete(websocket_app.start_server())
|
||||
except Exception as e:
|
||||
print(f"WebSocket server error: {e}")
|
||||
finally:
|
||||
loop.close()
|
||||
|
||||
websocket_server_thread = threading.Thread(target=run_websocket_server, daemon=True)
|
||||
websocket_server_thread.start()
|
||||
|
||||
# Start the Flask server as subprocess
|
||||
webapp_process = subprocess.Popen(
|
||||
[sys.executable, webapp_serve_path, "--host", "127.0.0.1", "--port", str(webapp_port)]
|
||||
)
|
||||
|
||||
# Update properties
|
||||
props.webapp_server_port = webapp_port
|
||||
props.websocket_server_port = websocket_port
|
||||
props.webapp_is_running = True
|
||||
|
||||
# Wait a moment for servers to start, then open browser
|
||||
def delayed_open_browser():
|
||||
import time
|
||||
|
||||
time.sleep(1.5)
|
||||
webbrowser.open(f"http://127.0.0.1:{webapp_port}?bonsai_server={websocket_port}")
|
||||
|
||||
browser_thread = threading.Thread(target=delayed_open_browser, daemon=True)
|
||||
browser_thread.start()
|
||||
|
||||
self.report(
|
||||
{"INFO"}, f"IfcTester webapp started at http://127.0.0.1:{webapp_port} (Websocket: {websocket_port})"
|
||||
)
|
||||
return {"FINISHED"}
|
||||
|
||||
except Exception as e:
|
||||
# Clean up on error
|
||||
if webapp_process:
|
||||
webapp_process.terminate()
|
||||
webapp_process = None
|
||||
if websocket_server_thread and websocket_app:
|
||||
# The websocket server will be cleaned up when the thread ends
|
||||
websocket_server_thread = None
|
||||
websocket_app = None
|
||||
|
||||
self.report({"ERROR"}, f"Failed to start servers: {str(e)}")
|
||||
return {"CANCELLED"}
|
||||
|
||||
def find_free_port(self):
|
||||
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
|
||||
s.bind(("", 0))
|
||||
s.listen(1)
|
||||
port = s.getsockname()[1]
|
||||
return port
|
||||
|
||||
|
||||
class StopIfcTesterWebapp(bpy.types.Operator):
|
||||
bl_idname = "bim.stop_ifc_tester_webapp"
|
||||
bl_label = "Stop IfcTester Webapp"
|
||||
bl_description = "Stop the IfcTester webapp server"
|
||||
|
||||
def execute(self, context):
|
||||
global webapp_process, websocket_server_thread, websocket_app
|
||||
|
||||
props = tool.Tester.get_tester_props()
|
||||
|
||||
if webapp_process is None and websocket_server_thread is None:
|
||||
self.report({"WARNING"}, "No IfcTester servers are running")
|
||||
return {"CANCELLED"}
|
||||
|
||||
errors = []
|
||||
|
||||
# Stop webapp server
|
||||
if webapp_process:
|
||||
try:
|
||||
webapp_process.terminate()
|
||||
webapp_process.wait(timeout=5)
|
||||
except subprocess.TimeoutExpired:
|
||||
webapp_process.kill()
|
||||
except Exception as e:
|
||||
errors.append(f"Error stopping webapp server: {str(e)}")
|
||||
finally:
|
||||
webapp_process = None
|
||||
|
||||
# Stop websocket server
|
||||
if websocket_app and websocket_server_thread:
|
||||
try:
|
||||
print("Stopping WebSocket server...")
|
||||
|
||||
# Signal shutdown using thread-safe method
|
||||
websocket_app.stop_server()
|
||||
|
||||
# Wait for the websocket thread to finish
|
||||
websocket_server_thread.join(timeout=5)
|
||||
|
||||
except Exception as e:
|
||||
errors.append(f"Error during websocket shutdown: {str(e)}")
|
||||
finally:
|
||||
websocket_app = None
|
||||
websocket_server_thread = None
|
||||
|
||||
# Update properties
|
||||
props.webapp_server_port = 0
|
||||
props.websocket_server_port = 0
|
||||
props.webapp_is_running = False
|
||||
|
||||
if errors:
|
||||
self.report({"WARNING"}, f"IfcTester webapp and server stopped with errors: {'; '.join(errors)}")
|
||||
else:
|
||||
self.report({"INFO"}, "IfcTester webapp and server stopped")
|
||||
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class OpenIfcTesterWebapp(bpy.types.Operator):
|
||||
bl_idname = "bim.open_ifc_tester_webapp"
|
||||
bl_label = "Open IfcTester Webapp"
|
||||
bl_description = "Open the IfcTester webapp in the default browser"
|
||||
|
||||
def execute(self, context):
|
||||
props = tool.Tester.get_tester_props()
|
||||
|
||||
if not props.webapp_is_running:
|
||||
self.report({"ERROR"}, "IfcTester webapp is not running. Please start it first.")
|
||||
return {"CANCELLED"}
|
||||
|
||||
webbrowser.open(f"http://127.0.0.1:{props.webapp_server_port}?bonsai_server={props.websocket_server_port}")
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class SelectRequirement(bpy.types.Operator):
|
||||
bl_idname = "bim.select_requirement"
|
||||
bl_label = "Select Specification"
|
||||
|
||||
@@ -77,6 +77,9 @@ class IfcTesterProperties(PropertyGroup):
|
||||
failed_entities: CollectionProperty(name="FailedEntities", type=FailedEntities)
|
||||
has_entities: BoolProperty(default=False, name="")
|
||||
n_entities: IntProperty(name="", default=0)
|
||||
webapp_server_port: IntProperty(name="Webapp Server Port", default=0)
|
||||
webapp_is_running: BoolProperty(default=False, name="Webapp Is Running", options=set())
|
||||
websocket_server_port: IntProperty(name="WebSocket Server Port", default=0)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
specs: MultipleFileSelect
|
||||
@@ -92,3 +95,6 @@ class IfcTesterProperties(PropertyGroup):
|
||||
failed_entities: bpy.types.bpy_prop_collection_idprop[FailedEntities]
|
||||
has_entities: bool
|
||||
n_entities: int
|
||||
webapp_server_port: int
|
||||
webapp_is_running: bool
|
||||
websocket_server_port: int
|
||||
|
||||
@@ -71,6 +71,20 @@ class BIM_PT_tester(Panel):
|
||||
row = self.layout.row()
|
||||
row.operator("bim.execute_ifc_tester")
|
||||
|
||||
self.layout.separator()
|
||||
|
||||
# IfcTester Webapp controls
|
||||
if props.webapp_is_running:
|
||||
row = self.layout.row()
|
||||
row.label(text=f"Webapp: {props.webapp_server_port} | Server: {props.websocket_server_port}")
|
||||
|
||||
row = self.layout.row(align=True)
|
||||
row.operator("bim.stop_ifc_tester_webapp")
|
||||
row.operator("bim.open_ifc_tester_webapp", icon="URL", text="")
|
||||
else:
|
||||
row = self.layout.row()
|
||||
row.operator("bim.start_ifc_tester_webapp")
|
||||
|
||||
if TesterData.data["has_report"]:
|
||||
self.layout.template_list(
|
||||
"BIM_UL_tester_specifications",
|
||||
|
||||
@@ -1057,7 +1057,10 @@ class BIM_OT_enum_property_search(bpy.types.Operator):
|
||||
self.layout.context_pointer_set(name="data", data=self.data)
|
||||
# NOTE: activate_init don't work with prop_search, so cannot activate field for typing,
|
||||
# though it would fit perfectly.
|
||||
self.layout.prop_search(self, "dummy_name", self, "collection_names")
|
||||
if self.dummy_name:
|
||||
self.layout.label(text=f"Current: {self.dummy_name}")
|
||||
|
||||
self.layout.prop_search(self, "dummy_name", self, "collection_names", text=self.prop_name)
|
||||
|
||||
def execute(self, context):
|
||||
return {"FINISHED"}
|
||||
|
||||
@@ -36,7 +36,8 @@ import bonsai.bim
|
||||
import bonsai.tool as tool
|
||||
from ifcopenshell.util.file import IfcHeaderExtractor
|
||||
from bonsai.bim.prop import Attribute
|
||||
from bonsai.bim.module.bsdd.prop import BIMBSDDProperties
|
||||
from bonsai.bim.module.bsdd.prop import BIMBSDDProperties, BSDDProperty
|
||||
from bonsai.bim.module.pset.prop import IfcProperty
|
||||
from typing import Optional, TYPE_CHECKING, Literal
|
||||
from natsort import natsorted
|
||||
|
||||
@@ -1424,8 +1425,20 @@ def draw_custom_context_menu(self: bpy.types.Menu, context: bpy.types.Context) -
|
||||
|
||||
if isinstance(prop_struct, Attribute):
|
||||
attr = prop_struct
|
||||
|
||||
# Hacky way to get Attribute containing description for enumerated values.
|
||||
pset_enum_identifier = ".enumerated_value.enumerated_values["
|
||||
attr_path = prop_struct.path_from_id()
|
||||
if pset_enum_identifier in attr_path:
|
||||
attr_path = attr_path.partition(pset_enum_identifier)[0]
|
||||
assert (data_block := prop_struct.id_data)
|
||||
attr = data_block.path_resolve(attr_path)
|
||||
assert isinstance(attr, IfcProperty)
|
||||
attr = attr.metadata
|
||||
|
||||
description = attr.description
|
||||
ifc_class = attr.ifc_class
|
||||
url = ""
|
||||
if ifc_class:
|
||||
try:
|
||||
url = get_entity_doc(version, ifc_class).get("spec_url", "")
|
||||
@@ -1445,10 +1458,13 @@ def draw_custom_context_menu(self: bpy.types.Menu, context: bpy.types.Context) -
|
||||
if attr_name:
|
||||
op = layout.operator("bim.copy_text_to_clipboard", text="Copy Attribute Name", icon="COPYDOWN")
|
||||
op.text = attr_name
|
||||
elif isinstance(prop_struct, BIMBSDDProperties) and hasattr(context, "active_bsdd_property"):
|
||||
elif isinstance(prop_struct, BIMBSDDProperties) and (
|
||||
active_bsdd_property := getattr(context, "active_bsdd_property", None)
|
||||
):
|
||||
# Context Menu for bSDD Properties
|
||||
assert isinstance(active_bsdd_property, BSDDProperty)
|
||||
op_description = layout.operator("bim.show_bsdd_description", text="bSDD Description", icon="INFO")
|
||||
op_description.url = context.active_bsdd_property.uri
|
||||
op_description.url = active_bsdd_property.uri
|
||||
else:
|
||||
# Basically context menu for any Blender property will end up here,
|
||||
# and will check 3 types of docs.
|
||||
@@ -1536,7 +1552,7 @@ class BIM_PT_snappping(Panel):
|
||||
return context.mode == "OBJECT"
|
||||
|
||||
def draw(self, context):
|
||||
prop = context.scene.BIMSnapProperties
|
||||
prop = tool.Snap.get_snap_props()
|
||||
layout = self.layout
|
||||
col = layout.column(align=True)
|
||||
col.prop(prop, "vertex", toggle=True, icon="SNAP_VERTEX")
|
||||
@@ -1544,7 +1560,7 @@ class BIM_PT_snappping(Panel):
|
||||
col.prop(prop, "edge_center", toggle=True, icon="SNAP_MIDPOINT")
|
||||
col.prop(prop, "edge_intersection", toggle=True, icon="SNAP_GRID")
|
||||
col.prop(prop, "face", toggle=True, icon="SNAP_FACE")
|
||||
groups = context.scene.BIMSnapGroups
|
||||
groups = tool.Snap.get_snap_groups()
|
||||
row = layout.row(align=True)
|
||||
row.label(text="Bonsai Target Selection")
|
||||
row = layout.row(align=True)
|
||||
|
||||
@@ -135,10 +135,8 @@ def assign_cost_item_type(
|
||||
List of found product types.
|
||||
"""
|
||||
product_types = list(spatial.get_selected_product_types())
|
||||
rels = [
|
||||
ifc.run("control.assign_control", relating_control=cost_item, related_object=product_type)
|
||||
for product_type in product_types
|
||||
]
|
||||
ifc.run("control.assign_control", relating_control=cost_item, related_objects=product_types)
|
||||
|
||||
cost.load_cost_item_types(cost_item)
|
||||
return product_types
|
||||
|
||||
@@ -156,10 +154,7 @@ def unassign_cost_item_type(
|
||||
"""
|
||||
if not product_types:
|
||||
product_types = list(spatial.get_selected_product_types())
|
||||
[
|
||||
ifc.run("control.unassign_control", relating_control=cost_item, related_object=product_type)
|
||||
for product_type in product_types
|
||||
]
|
||||
ifc.run("control.unassign_control", relating_control=cost_item, related_objects=product_types)
|
||||
cost.load_cost_item_types(cost_item)
|
||||
return product_types
|
||||
|
||||
@@ -173,7 +168,7 @@ def assign_cost_item_quantity(
|
||||
ifc: type[tool.Ifc],
|
||||
cost: type[tool.Cost],
|
||||
cost_item: ifcopenshell.entity_instance,
|
||||
related_object_type: type[tool.Cost].RELATED_OBJECT_TYPE,
|
||||
related_object_type: tool.Cost.RELATED_OBJECT_TYPE,
|
||||
prop_name: str,
|
||||
) -> bool:
|
||||
products = cost.get_products(related_object_type)
|
||||
@@ -213,10 +208,10 @@ def assign_cost_value(
|
||||
ifc.run("cost.assign_cost_value", cost_item=cost_item, cost_rate=cost_rate)
|
||||
existing_cost_rate = cost.get_assigned_rate_cost_item(cost_item)
|
||||
if existing_cost_rate is None:
|
||||
ifc.run("control.assign_control", relating_control=cost_rate, related_object=cost_item)
|
||||
ifc.run("control.assign_control", relating_control=cost_rate, related_objects=[cost_item])
|
||||
else:
|
||||
ifc.run("control.unassign_control", relating_control=existing_cost_rate, related_object=cost_item)
|
||||
ifc.run("control.assign_control", relating_control=cost_rate, related_object=cost_item)
|
||||
ifc.run("control.unassign_control", relating_control=existing_cost_rate, related_objects=[cost_item])
|
||||
ifc.run("control.assign_control", relating_control=cost_rate, related_objects=[cost_item])
|
||||
|
||||
|
||||
def load_schedule_of_rates(cost: type[tool.Cost], schedule_of_rates: ifcopenshell.entity_instance) -> None:
|
||||
|
||||
@@ -39,8 +39,7 @@ def disable_editing_text(drawing: type[tool.Drawing], obj: bpy.types.Object) ->
|
||||
def edit_text(drawing: type[tool.Drawing], obj: bpy.types.Object) -> None:
|
||||
drawing.synchronise_ifc_and_text_attributes(obj)
|
||||
drawing.update_text_size_pset(obj)
|
||||
drawing.update_newline_at(obj)
|
||||
drawing.update_text_value(obj)
|
||||
drawing.update_newline_at_and_symbol(obj)
|
||||
drawing.disable_editing_text(obj)
|
||||
|
||||
|
||||
@@ -67,8 +66,6 @@ def edit_assigned_product(
|
||||
ifc.run("drawing.unassign_product", relating_product=existing_product, related_object=element)
|
||||
if product:
|
||||
ifc.run("drawing.assign_product", relating_product=product, related_object=element)
|
||||
if drawing.is_annotation_object_type(element, ("TEXT", "TEXT_LEADER")):
|
||||
drawing.update_text_value(obj)
|
||||
|
||||
drawing.disable_editing_assigned_product(obj)
|
||||
|
||||
@@ -200,7 +197,7 @@ def disable_editing_references(drawing: type[tool.Drawing]) -> None:
|
||||
|
||||
|
||||
def add_document(
|
||||
ifc: type[tool.Ifc], drawing: type[tool.Drawing], document_type: type[tool.Drawing].DOCUMENT_TYPE, uri: str
|
||||
ifc: type[tool.Ifc], drawing: type[tool.Drawing], document_type: tool.Drawing.DOCUMENT_TYPE, uri: str
|
||||
) -> None:
|
||||
document = ifc.run("document.add_information")
|
||||
reference = ifc.run("document.add_reference", information=document)
|
||||
@@ -217,7 +214,7 @@ def add_document(
|
||||
def remove_document(
|
||||
ifc: type[tool.Ifc],
|
||||
drawing: type[tool.Drawing],
|
||||
document_type: type[tool.Drawing].DOCUMENT_TYPE,
|
||||
document_type: tool.Drawing.DOCUMENT_TYPE,
|
||||
document: ifcopenshell.entity_instance,
|
||||
) -> None:
|
||||
ifc.run("document.remove_information", information=document)
|
||||
|
||||
@@ -52,7 +52,7 @@ def add_pset(
|
||||
pset: type[tool.Pset],
|
||||
blender: type[tool.Blender],
|
||||
obj_name: str,
|
||||
obj_type: type[tool.Ifc].OBJECT_TYPE,
|
||||
obj_type: tool.Ifc.OBJECT_TYPE,
|
||||
) -> None:
|
||||
pset_name = pset.get_pset_name(obj_name, obj_type, pset_type="PSET")
|
||||
if obj_type == "Object":
|
||||
@@ -71,9 +71,9 @@ def enable_pset_editing(
|
||||
pset_tool: type[tool.Pset],
|
||||
pset: Union[ifcopenshell.entity_instance, None],
|
||||
pset_name: str,
|
||||
pset_type: type[tool.Pset].PSET_TYPE,
|
||||
pset_type: tool.Pset.PSET_TYPE,
|
||||
obj_name: str,
|
||||
obj_type: type[tool.Ifc].OBJECT_TYPE,
|
||||
obj_type: tool.Ifc.OBJECT_TYPE,
|
||||
) -> None:
|
||||
props = pset_tool.get_pset_props(obj_name, obj_type)
|
||||
pset_tool.clear_blender_pset_properties(props)
|
||||
@@ -87,14 +87,14 @@ def enable_pset_editing(
|
||||
has_template = False
|
||||
|
||||
if pset:
|
||||
pset_tool.import_pset_from_existing(pset, props)
|
||||
pset_tool.import_pset_from_existing(pset, props, pset_template)
|
||||
pset_tool.set_active_pset(props, pset, has_template)
|
||||
else:
|
||||
pset_tool.enable_proposed_pset(props, pset_name, pset_type, has_template)
|
||||
|
||||
|
||||
def add_proposed_prop(
|
||||
pset: type[tool.Pset], obj_name: str, obj_type: type[tool.Ifc].OBJECT_TYPE, name: str, value: Any
|
||||
pset: type[tool.Pset], obj_name: str, obj_type: tool.Ifc.OBJECT_TYPE, name: str, value: Any
|
||||
) -> Union[None, str]:
|
||||
props = pset.get_pset_props(obj_name, obj_type)
|
||||
res = pset.add_proposed_property(name, pset.cast_string_to_primitive(value), props)
|
||||
@@ -103,7 +103,7 @@ def add_proposed_prop(
|
||||
|
||||
|
||||
def unshare_pset(
|
||||
ifc: type[tool.Ifc], pset_tool: type[tool.Pset], obj_type: type[tool.Ifc].OBJECT_TYPE, obj_name: str, pset_id: int
|
||||
ifc: type[tool.Ifc], pset_tool: type[tool.Pset], obj_type: tool.Ifc.OBJECT_TYPE, obj_name: str, pset_id: int
|
||||
) -> None:
|
||||
elements: list[ifcopenshell.entity_instance]
|
||||
pset = ifc.get_entity_by_id(pset_id)
|
||||
|
||||
@@ -422,7 +422,7 @@ def edit_task_calendar(
|
||||
task: ifcopenshell.entity_instance,
|
||||
work_calendar: ifcopenshell.entity_instance,
|
||||
) -> None:
|
||||
ifc.run("control.assign_control", relating_control=work_calendar, related_object=task)
|
||||
ifc.run("control.assign_control", relating_control=work_calendar, related_objects=[task])
|
||||
ifc.run("sequence.cascade_schedule", task=task)
|
||||
sequence.load_task_properties()
|
||||
|
||||
@@ -433,7 +433,7 @@ def remove_task_calendar(
|
||||
task: ifcopenshell.entity_instance,
|
||||
work_calendar: ifcopenshell.entity_instance,
|
||||
) -> None:
|
||||
ifc.run("control.unassign_control", relating_control=work_calendar, related_object=task)
|
||||
ifc.run("control.unassign_control", relating_control=work_calendar, related_objects=[task])
|
||||
ifc.run("sequence.cascade_schedule", task=task)
|
||||
sequence.load_task_properties()
|
||||
|
||||
|
||||
@@ -307,7 +307,7 @@ class Document:
|
||||
@interface
|
||||
class Drawing:
|
||||
def activate_drawing(cls, camera): pass
|
||||
def add_literal_to_annotation(cls, obj, Literal='Literal', Path='RIGHT', BoxAlignment='bottom-left'): pass
|
||||
def add_literal(cls, **attributes): pass
|
||||
def copy_representation(cls, source, dest): pass
|
||||
def create_annotation_context(cls, target_view, object_type=None): pass
|
||||
def create_annotation_object(cls, drawing, object_type): pass
|
||||
@@ -382,7 +382,6 @@ class Drawing:
|
||||
def open_spreadsheet(cls, uri): pass
|
||||
def open_svg(cls, filepath): pass
|
||||
def reload_representation(cls, obj, representation): pass
|
||||
def remove_literal_from_annotation(cls, obj, literal): pass
|
||||
def run_drawing_activate_model(cls): pass
|
||||
def run_root_assign_class(cls, obj=None, ifc_class=None, predefined_type=None, should_add_representation=True, context=None, ifc_representation_class=None): pass
|
||||
def run_type_assign_type(cls, element=None, relating_type=None): pass
|
||||
@@ -395,9 +394,8 @@ class Drawing:
|
||||
def sync_object_placement(cls, obj): pass
|
||||
def synchronise_ifc_and_text_attributes(cls, obj): pass
|
||||
def update_embedded_svg_location(cls, uri, old_location, new_location): pass
|
||||
def update_newline_at(cls, obj): pass
|
||||
def update_newline_at_and_symbol(cls, obj): pass
|
||||
def update_text_size_pset(cls, obj): pass
|
||||
def update_text_value(cls, obj): pass
|
||||
|
||||
|
||||
@interface
|
||||
|
||||
@@ -56,7 +56,7 @@ from collections.abc import Iterable, Callable, Generator, Sequence, Sized
|
||||
if TYPE_CHECKING:
|
||||
from sun_position.properties import SunPosProperties
|
||||
import bpy.stub_internal.rna_enums as rna_enums
|
||||
from bonsai.bim.prop import BIMProperties, BIMObjectProperties
|
||||
from bonsai.bim.prop import BIMProperties, BIMObjectProperties, BIMSnapProperties
|
||||
from bonsai.bim.module.attribute.prop import BIMAttributeProperties
|
||||
from bonsai.bim.module.constraint.prop import BIMConstraintProperties, BIMObjectConstraintProperties
|
||||
from bonsai.bim.module.covetool.prop import CoveToolProperties
|
||||
@@ -690,6 +690,29 @@ class Blender(bonsai.core.tool.Blender):
|
||||
if active_object:
|
||||
active_object.select_set(True)
|
||||
|
||||
@classmethod
|
||||
def validate_object_selection(
|
||||
cls,
|
||||
context: bpy.types.Context,
|
||||
active_object: Union[bpy.types.Object, None] = None,
|
||||
selected_objects: Sequence[bpy.types.Object] = (),
|
||||
) -> tuple[bpy.types.Context, Union[bpy.types.Object, None], list[bpy.types.Object]]:
|
||||
"""Validate object selection and return only valid objects.
|
||||
|
||||
Can be used before ``set_objects_selection`` to avoid errors
|
||||
trying to select or set as active already removed objects
|
||||
or objects that are not in the current view layer (their collection is unchecked).
|
||||
"""
|
||||
assert context.view_layer
|
||||
view_layer_objects = set(context.view_layer.objects)
|
||||
|
||||
new_selected_objects = [o for o in selected_objects if cls.is_valid_data_block(o) and o in view_layer_objects]
|
||||
|
||||
if active_object and (not cls.is_valid_data_block(active_object) or active_object not in view_layer_objects):
|
||||
active_object = None
|
||||
|
||||
return context, active_object, new_selected_objects
|
||||
|
||||
@classmethod
|
||||
def clear_objects_selection(cls) -> None:
|
||||
"""Clear objects selection and active object."""
|
||||
@@ -1558,6 +1581,11 @@ class Blender(bonsai.core.tool.Blender):
|
||||
return repr(bpy_prop)
|
||||
return repr(bpy_struct)
|
||||
|
||||
@classmethod
|
||||
def get_props_attribute_name(cls, props: bpy.types.PropertyGroup) -> str:
|
||||
"""E.g. `bpy.data.objects['IfcAnnotation/TEXT'].BIMTextProperties` -> `BIMTextProperties`"""
|
||||
return repr(props).rpartition(".")[-1]
|
||||
|
||||
@classmethod
|
||||
def resolve_data_path_to_data_attr(cls, data_path: str) -> tuple[bpy.types.bpy_struct, str]:
|
||||
"""
|
||||
@@ -1568,8 +1596,8 @@ class Blender(bonsai.core.tool.Blender):
|
||||
|
||||
:return: Resolved tuple of Blender Struct and property name.
|
||||
Examples:
|
||||
- `(preferences.prop_group, "string_prop)`
|
||||
- `(scene, "string_prop)`
|
||||
- `(preferences.prop_group, "string_prop")`
|
||||
- `(scene, "string_prop")`
|
||||
|
||||
"""
|
||||
# Get data to modify.
|
||||
|
||||
@@ -58,6 +58,7 @@ from fractions import Fraction
|
||||
from typing import Optional, Union, Any, Literal, TYPE_CHECKING, NamedTuple
|
||||
from collections.abc import Iterable, Sequence
|
||||
from pathlib import Path
|
||||
from ifcopenshell.util.shape_builder import ShapeBuilder
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from bonsai.bim.module.drawing.prop import (
|
||||
@@ -109,6 +110,27 @@ class Drawing(bonsai.core.tool.Drawing):
|
||||
}
|
||||
# fmt: on
|
||||
|
||||
DEFAULT_SYMBOLS = [
|
||||
"rectangle-tag",
|
||||
"triangle-tag",
|
||||
"hexagon-tag",
|
||||
"capsule-tag",
|
||||
"circle-tag",
|
||||
"door-tag",
|
||||
"window-tag",
|
||||
"space-tag",
|
||||
"elevation-arrow",
|
||||
"elevation-tag",
|
||||
"section-arrow",
|
||||
"section-tag",
|
||||
"dot",
|
||||
"setout-tag",
|
||||
"setout-point",
|
||||
"control-point",
|
||||
"traverse-point",
|
||||
"spot-elevation",
|
||||
]
|
||||
|
||||
@classmethod
|
||||
def get_document_props(cls) -> DocProperties:
|
||||
assert (scene := bpy.context.scene)
|
||||
@@ -286,9 +308,6 @@ class Drawing(bonsai.core.tool.Drawing):
|
||||
ifc_file, relating_product=related_entity, related_object=obj_entity
|
||||
)
|
||||
|
||||
if object_type == "TEXT":
|
||||
tool.Drawing.update_text_value(obj)
|
||||
|
||||
@classmethod
|
||||
def is_annotation_object_type(
|
||||
cls, element: ifcopenshell.entity_instance, object_types: Union[str, Sequence[str]]
|
||||
@@ -436,7 +455,7 @@ class Drawing(bonsai.core.tool.Drawing):
|
||||
@classmethod
|
||||
def disable_editing_text(cls, obj: bpy.types.Object) -> None:
|
||||
props = tool.Drawing.get_text_props(obj)
|
||||
props.is_editing = False
|
||||
obj.property_unset(tool.Blender.get_props_attribute_name(props))
|
||||
|
||||
@classmethod
|
||||
def disable_editing_assigned_product(cls, obj: bpy.types.Object) -> None:
|
||||
@@ -505,7 +524,7 @@ class Drawing(bonsai.core.tool.Drawing):
|
||||
|
||||
@classmethod
|
||||
def export_text_literal_attributes(cls, obj: bpy.types.Object) -> list[dict[str, Any]]:
|
||||
literals = []
|
||||
literals: list[dict[str, Any]] = []
|
||||
props = tool.Drawing.get_text_props(obj)
|
||||
for literal_props in props.literals:
|
||||
literal_data = bonsai.bim.helper.export_attributes(literal_props.attributes)
|
||||
@@ -738,32 +757,25 @@ class Drawing(bonsai.core.tool.Drawing):
|
||||
props = tool.Drawing.get_document_props()
|
||||
return props.is_editing_sheets
|
||||
|
||||
@classmethod
|
||||
def remove_literal_from_annotation(cls, obj: bpy.types.Object, literal: ifcopenshell.entity_instance) -> None:
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
if not element:
|
||||
return
|
||||
|
||||
rep = cls.get_annotation_representation(element)
|
||||
if not rep:
|
||||
return
|
||||
|
||||
ifc_file = tool.Ifc.get()
|
||||
rep.Items = [l for l in rep.Items if l != literal]
|
||||
ifcopenshell.util.element.remove_deep2(ifc_file, literal)
|
||||
|
||||
@classmethod
|
||||
def synchronise_ifc_and_text_attributes(cls, obj: bpy.types.Object) -> None:
|
||||
literals = cls.get_text_literal(obj, return_list=True)
|
||||
assert (element := tool.Ifc.get_entity(obj))
|
||||
assert (rep := cls.get_annotation_representation(element))
|
||||
|
||||
old_literals = cls.get_text_literal(obj, return_list=True)
|
||||
assert isinstance(old_literals, list)
|
||||
literals_attributes = cls.export_text_literal_attributes(obj)
|
||||
props = cls.get_text_props(obj)
|
||||
defined_ifc_ids = [l.ifc_definition_id for l in props.literals]
|
||||
ifc_file = tool.Ifc.get()
|
||||
|
||||
added_literals: list[ifcopenshell.entity_instance] = []
|
||||
new_literals: list[ifcopenshell.entity_instance] = []
|
||||
for ifc_definition_id, attributes in zip(defined_ifc_ids, literals_attributes):
|
||||
# making sure all literals from text edit exist in ifc
|
||||
if ifc_definition_id == 0:
|
||||
literal = cls.add_literal_to_annotation(obj, **attributes)
|
||||
literal = cls.add_literal(**attributes)
|
||||
added_literals.append(literal)
|
||||
else:
|
||||
literal = ifc_file.by_id(ifc_definition_id)
|
||||
ifcopenshell.api.drawing.edit_text_literal(
|
||||
@@ -771,38 +783,32 @@ class Drawing(bonsai.core.tool.Drawing):
|
||||
text_literal=literal,
|
||||
attributes=attributes,
|
||||
)
|
||||
new_literals.append(literal)
|
||||
|
||||
# remove from ifc the literals that were removed during the edit
|
||||
for literal in literals:
|
||||
if literal.id() not in defined_ifc_ids:
|
||||
cls.remove_literal_from_annotation(obj, literal)
|
||||
removed_literals = set(old_literals) - set(new_literals)
|
||||
|
||||
# Add new literals and keep the order as defined in text props.
|
||||
items = [i for i in rep.Items if i not in removed_literals] + added_literals
|
||||
items.sort(key=lambda x: new_literals.index(x) if x in new_literals else -1)
|
||||
rep.Items = items
|
||||
|
||||
# Remove from ifc the literals that were removed during the edit.
|
||||
for literal in removed_literals:
|
||||
ifcopenshell.util.element.remove_deep2(ifc_file, literal)
|
||||
|
||||
@classmethod
|
||||
def add_literal_to_annotation(
|
||||
cls, obj: bpy.types.Object, Literal: str = "Literal", Path: str = "RIGHT", BoxAlignment: str = "bottom-left"
|
||||
) -> Union[ifcopenshell.entity_instance, None]:
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
if not element:
|
||||
return
|
||||
|
||||
rep = cls.get_annotation_representation(element)
|
||||
|
||||
if not rep:
|
||||
return
|
||||
|
||||
def add_literal(cls, **attributes: str) -> ifcopenshell.entity_instance:
|
||||
ifc_file = tool.Ifc.get()
|
||||
|
||||
origin = ifc_file.createIfcAxis2Placement3D(
|
||||
ifc_file.createIfcCartesianPoint((0.0, 0.0, 0.0)),
|
||||
ifc_file.createIfcDirection((0.0, 0.0, 1.0)),
|
||||
ifc_file.createIfcDirection((1.0, 0.0, 0.0)),
|
||||
builder = ShapeBuilder(ifc_file)
|
||||
origin = builder.create_axis2_placement_3d()
|
||||
ifc_literal = ifc_file.create_entity(
|
||||
"IfcTextLiteralWithExtent",
|
||||
attributes.get("Literal", "Literal"),
|
||||
origin,
|
||||
attributes.get("Path", "RIGHT"),
|
||||
ifc_file.create_entity("IfcPlanarExtent", 1000, 1000),
|
||||
attributes.get("BoxAlignment", "bottom-left"),
|
||||
)
|
||||
|
||||
ifc_literal = ifc_file.createIfcTextLiteralWithExtent(
|
||||
Literal, origin, Path, ifc_file.createIfcPlanarExtent(1000, 1000), BoxAlignment
|
||||
)
|
||||
|
||||
rep.Items = rep.Items + (ifc_literal,)
|
||||
return ifc_literal
|
||||
|
||||
@classmethod
|
||||
@@ -1073,14 +1079,17 @@ class Drawing(bonsai.core.tool.Drawing):
|
||||
props = cls.get_text_props(obj)
|
||||
props.literals.clear()
|
||||
|
||||
for ifc_literal in cls.get_text_literal(obj, return_list=True):
|
||||
ifc_literals = cls.get_text_literal(obj, return_list=True)
|
||||
assert isinstance(ifc_literals, list)
|
||||
for ifc_literal in ifc_literals:
|
||||
literal_props = props.literals.add()
|
||||
bonsai.bim.helper.import_attributes(ifc_literal, literal_props.attributes)
|
||||
|
||||
box_alignment_mask = [False] * 9
|
||||
position_string = literal_props.attributes["BoxAlignment"].string_value
|
||||
box_alignment_mask[BOX_ALIGNMENT_POSITIONS.index(position_string)] = True
|
||||
literal_props.box_alignment = box_alignment_mask
|
||||
|
||||
literal_props.box_alignment = box_alignment_mask # pyright: ignore[reportAttributeAccessIssue]
|
||||
literal_props.ifc_definition_id = ifc_literal.id()
|
||||
|
||||
from bonsai.bim.module.drawing.data import DecoratorData
|
||||
@@ -1088,6 +1097,7 @@ class Drawing(bonsai.core.tool.Drawing):
|
||||
text_data = DecoratorData.get_text_data(obj)
|
||||
props.font_size = str(text_data["FontSize"])
|
||||
props.newline_at = text_data["Newline_At"]
|
||||
props.set_symbol(text_data["Symbol"])
|
||||
|
||||
@classmethod
|
||||
def import_assigned_product(cls, obj: bpy.types.Object) -> None:
|
||||
@@ -1186,15 +1196,6 @@ class Drawing(bonsai.core.tool.Drawing):
|
||||
props = tool.Drawing.get_document_props()
|
||||
props.should_draw_decorations = True
|
||||
|
||||
@classmethod
|
||||
def update_text_value(cls, obj: bpy.types.Object) -> None:
|
||||
props = cls.get_text_props(obj)
|
||||
literals = cls.get_text_literal(obj, return_list=True)
|
||||
cls.import_text_attributes(obj)
|
||||
for i, literal in enumerate(literals):
|
||||
product = cls.get_assigned_product(tool.Ifc.get_entity(obj)) or tool.Ifc.get_entity(obj)
|
||||
props.literals[i].value = cls.replace_text_literal_variables(literal.Literal, product)
|
||||
|
||||
@classmethod
|
||||
def update_text_size_pset(cls, obj: bpy.types.Object) -> None:
|
||||
"""updates pset `EPset_Annotation.Classes` value
|
||||
@@ -1204,19 +1205,22 @@ class Drawing(bonsai.core.tool.Drawing):
|
||||
|
||||
props = cls.get_text_props(obj)
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
assert element
|
||||
# updating text font size in EPset_Annotation.Classes
|
||||
font_size = float(props.font_size)
|
||||
font_size_str = next((key for key in FONT_SIZES if FONT_SIZES[key] == font_size), None)
|
||||
classes = ifcopenshell.util.element.get_pset(element, "EPset_Annotation", "Classes")
|
||||
assert isinstance(classes, Union[str, None])
|
||||
classes_split = classes.split() if classes else []
|
||||
|
||||
different_font_sizes = [c for c in classes_split if c in FONT_SIZES and c != font_size_str]
|
||||
|
||||
# we do need to change pset value in ifc
|
||||
# only if there are different font sizes in classes already
|
||||
# We do need to change pset value in ifc,
|
||||
# but only if there are different font sizes in classes already
|
||||
# or if the current font size is not present in classes
|
||||
# (except regular font size because it's default)
|
||||
# (except regular font size because it's default).
|
||||
if different_font_sizes or (font_size_str not in classes_split and font_size_str != "regular"):
|
||||
assert font_size_str is not None
|
||||
classes_split = [c for c in classes_split if c not in FONT_SIZES] + [font_size_str]
|
||||
classes = " ".join(classes_split)
|
||||
|
||||
@@ -1231,10 +1235,12 @@ class Drawing(bonsai.core.tool.Drawing):
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def update_newline_at(cls, obj: bpy.types.Object) -> None:
|
||||
def update_newline_at_and_symbol(cls, obj: bpy.types.Object) -> None:
|
||||
props = cls.get_text_props(obj)
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
assert element
|
||||
newline_at = int(props.newline_at)
|
||||
symbol = props.get_symbol()
|
||||
ifc_file = tool.Ifc.get()
|
||||
pset = tool.Pset.get_element_pset(element, "EPset_Annotation")
|
||||
if not pset:
|
||||
@@ -1242,7 +1248,10 @@ class Drawing(bonsai.core.tool.Drawing):
|
||||
ifcopenshell.api.pset.edit_pset(
|
||||
ifc_file,
|
||||
pset=pset,
|
||||
properties={"Newline_At": newline_at},
|
||||
properties={
|
||||
"Newline_At": newline_at,
|
||||
"Symbol": symbol,
|
||||
},
|
||||
)
|
||||
|
||||
# TODO below this point is highly experimental prototype code with no tests
|
||||
|
||||
@@ -86,8 +86,8 @@ class Ifc(bonsai.core.tool.Ifc):
|
||||
|
||||
@classmethod
|
||||
def get_schema(cls) -> ifcopenshell.util.schema.IFC_SCHEMA:
|
||||
if IfcStore.get_file():
|
||||
return IfcStore.get_file().schema
|
||||
if ifc_file := IfcStore.get_file():
|
||||
return ifc_file.schema
|
||||
|
||||
@classmethod
|
||||
def clear_history(cls) -> None:
|
||||
|
||||
@@ -238,6 +238,8 @@ class Model(bonsai.core.tool.Model):
|
||||
|
||||
@classmethod
|
||||
def export_surface(cls, obj: bpy.types.Object) -> Union[ifcopenshell.entity_instance, None]:
|
||||
ifc_file = tool.Ifc.get()
|
||||
builder = ShapeBuilder(ifc_file)
|
||||
p1, p2, p3 = [v.co.copy() for v in obj.data.vertices[0:3]]
|
||||
|
||||
edge1 = p2 - p1
|
||||
@@ -271,13 +273,8 @@ class Model(bonsai.core.tool.Model):
|
||||
cls.bm.edges.ensure_lookup_table()
|
||||
|
||||
surface = tool.Ifc.get().createIfcCurveBoundedPlane()
|
||||
surface.BasisSurface = tool.Ifc.get().createIfcPlane(
|
||||
tool.Ifc.get().createIfcAxis2Placement3D(
|
||||
tool.Ifc.get().createIfcCartesianPoint([o / cls.unit_scale for o in p1]),
|
||||
tool.Ifc.get().createIfcDirection([float(o) for o in z_axis]),
|
||||
tool.Ifc.get().createIfcDirection([float(o) for o in x_axis]),
|
||||
)
|
||||
)
|
||||
placement = builder.create_axis2_placement_3d([o / cls.unit_scale for o in p1], z_axis, x_axis)
|
||||
surface.BasisSurface = ifc_file.create_entity("IfcPlane", placement)
|
||||
|
||||
surface.OuterBoundary = tool.Ifc.get().add(profile_def.OuterCurve)
|
||||
if profile_def.is_a("IfcArbitraryProfileDefWithVoids"):
|
||||
@@ -1088,6 +1085,9 @@ class Model(bonsai.core.tool.Model):
|
||||
# No need to preview to update, Blender will do it in background,
|
||||
# `preview.icon_id` doesn't change after `asset_generate_preview()`.
|
||||
obj.asset_generate_preview()
|
||||
# Avoid issues with sqlite files.
|
||||
elif type(tool.Ifc.get()) is not ifcopenshell.file:
|
||||
return
|
||||
else:
|
||||
unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
|
||||
size = 128
|
||||
@@ -2124,13 +2124,8 @@ class Model(bonsai.core.tool.Model):
|
||||
@classmethod
|
||||
def add_extrusion_position(cls, extrusion: ifcopenshell.entity_instance, position: Vector) -> None:
|
||||
ifc_file = tool.Ifc.get()
|
||||
|
||||
new_position = ifc_file.createIfcAxis2Placement3D(
|
||||
ifc_file.createIfcCartesianPoint(position),
|
||||
ifc_file.createIfcDirection((0.0, 0.0, 1.0)),
|
||||
ifc_file.createIfcDirection((1.0, 0.0, 0.0)),
|
||||
)
|
||||
|
||||
builder = ShapeBuilder(ifc_file)
|
||||
new_position = builder.create_axis2_placement_3d(position)
|
||||
extrusion.Position = new_position
|
||||
|
||||
@classmethod
|
||||
|
||||
@@ -30,6 +30,7 @@ from typing import Union, Literal, Any, TYPE_CHECKING, assert_never
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from bonsai.bim.prop import Attribute
|
||||
from bonsai.bim.module.pset.prop import (
|
||||
PsetProperties,
|
||||
GlobalPsetProperties,
|
||||
@@ -171,8 +172,16 @@ class Pset(bonsai.core.tool.Pset):
|
||||
return special_type
|
||||
|
||||
@classmethod
|
||||
def import_pset_from_existing(cls, pset: ifcopenshell.entity_instance, props: PsetProperties) -> None:
|
||||
pset_props = []
|
||||
def import_pset_from_existing(
|
||||
cls,
|
||||
pset: ifcopenshell.entity_instance,
|
||||
props: PsetProperties,
|
||||
pset_template: Union[ifcopenshell.entity_instance, None],
|
||||
) -> None:
|
||||
"""
|
||||
:param pset_template: Pset Template to use as a source for descriptions.
|
||||
"""
|
||||
pset_props: tuple[ifcopenshell.entity_instance, ...] = ()
|
||||
if pset.is_a("IfcElementQuantity"):
|
||||
pset_props = pset.Quantities
|
||||
elif pset.is_a("IfcPropertySet"):
|
||||
@@ -180,6 +189,16 @@ class Pset(bonsai.core.tool.Pset):
|
||||
elif pset.is_a("IfcMaterialProperties") or pset.is_a("IfcProfileProperties"):
|
||||
pset_props = pset.Properties
|
||||
|
||||
prop_templates: dict[str, ifcopenshell.entity_instance] = {}
|
||||
if pset_template:
|
||||
prop_templates = {prop.Name: prop for prop in pset_template.HasPropertyTemplates}
|
||||
|
||||
def process_prop_description(metadata: Attribute) -> None:
|
||||
prop_name = metadata.name
|
||||
if prop_name not in prop_templates:
|
||||
return
|
||||
bonsai.bim.helper.add_attribute_description(metadata, prop_templates[prop_name])
|
||||
|
||||
for prop in sorted(pset_props, key=lambda p: p.Name):
|
||||
if props.properties.get(prop.Name):
|
||||
continue # This property has already been added from a template
|
||||
@@ -191,6 +210,7 @@ class Pset(bonsai.core.tool.Pset):
|
||||
metadata.name = prop.Name
|
||||
metadata.is_null = len(simple_prop.enumerated_value.enumerated_values) == 0
|
||||
metadata.is_optional = True
|
||||
process_prop_description(metadata)
|
||||
enum_reference = prop.EnumerationReference
|
||||
selected_enum_items = [v.wrappedValue for v in (prop.EnumerationValues or ())]
|
||||
|
||||
@@ -230,6 +250,7 @@ class Pset(bonsai.core.tool.Pset):
|
||||
metadata.is_optional = True
|
||||
metadata.special_type = cls.get_special_type_for_prop(prop)
|
||||
metadata.set_value(metadata.get_value_default() if metadata.is_null else value)
|
||||
process_prop_description(metadata)
|
||||
|
||||
@classmethod
|
||||
def get_prop_template_primitive_type(cls, prop_template: ifcopenshell.entity_instance) -> str:
|
||||
@@ -277,6 +298,7 @@ class Pset(bonsai.core.tool.Pset):
|
||||
metadata.is_null = data.get(prop_template.Name, None) is None
|
||||
metadata.is_optional = True
|
||||
metadata.special_type = "URI" if prop_template.PrimaryMeasureType == "IfcURIReference" else ""
|
||||
bonsai.bim.helper.add_attribute_description(metadata, prop_template)
|
||||
|
||||
# Cute hack to abuse the metadata to find the Blender data_type
|
||||
metadata.set_value(enum_items[0])
|
||||
@@ -333,7 +355,7 @@ class Pset(bonsai.core.tool.Pset):
|
||||
|
||||
# For every prop we first ensure that existing prop value type matches the template value type
|
||||
# to prevent data loss and error casting data.
|
||||
# Property will be added later by import_pset_from_existing.
|
||||
# Existing property will be added later by import_pset_from_existing.
|
||||
for prop_template in sorted(pset_template.HasPropertyTemplates, key=lambda p: p.Name):
|
||||
if (
|
||||
not prop_template.is_a("IfcSimplePropertyTemplate")
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with Bonsai. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from __future__ import annotations
|
||||
import bpy
|
||||
import ifcopenshell
|
||||
import ifcopenshell.util.unit
|
||||
@@ -26,13 +27,26 @@ import math
|
||||
import mathutils
|
||||
from mathutils import Matrix, Vector
|
||||
from lark import Lark, Transformer
|
||||
from typing import Union, Any
|
||||
from typing import Union, Any, TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from bonsai.bim.prop import BIMSnapGroups, BIMSnapProperties
|
||||
|
||||
|
||||
class Snap(bonsai.core.tool.Snap):
|
||||
tool_state = None
|
||||
snap_plane_method = None
|
||||
|
||||
@classmethod
|
||||
def get_snap_props(cls) -> BIMSnapProperties:
|
||||
assert (scene := bpy.context.scene)
|
||||
return scene.BIMSnapProperties # pyright: ignore[reportAttributeAccessIssue]
|
||||
|
||||
@classmethod
|
||||
def get_snap_groups(cls) -> BIMSnapGroups:
|
||||
assert (scene := bpy.context.scene)
|
||||
return scene.BIMSnapGroups # pyright: ignore[reportAttributeAccessIssue]
|
||||
|
||||
@classmethod
|
||||
def set_snap_plane_method(cls, value=True):
|
||||
cls.snap_plane_method = value
|
||||
@@ -484,7 +498,7 @@ class Snap(bonsai.core.tool.Snap):
|
||||
def select_snapping_points(cls, context, event, tool_state, detected_snaps):
|
||||
def filter_snapping_points_by_type(snapping_points):
|
||||
options = ["Plane", "Axis"]
|
||||
props = context.scene.BIMSnapProperties
|
||||
props = tool.Snap.get_snap_props()
|
||||
for prop in props.__annotations__.keys():
|
||||
if getattr(props, prop):
|
||||
options.append(props.rna_type.properties[prop].name)
|
||||
@@ -494,7 +508,7 @@ class Snap(bonsai.core.tool.Snap):
|
||||
|
||||
def filter_snapping_points_by_group(detected_snaps):
|
||||
options = ["Wireframe", "Axis", "Plane"]
|
||||
props = context.scene.BIMSnapGroups
|
||||
props = tool.Snap.get_snap_groups()
|
||||
for prop in props.__annotations__.keys():
|
||||
if getattr(props, prop):
|
||||
options.append(props.rna_type.properties[prop].name)
|
||||
|
||||
@@ -37,8 +37,7 @@ class TestEditText:
|
||||
def test_run(self, drawing):
|
||||
drawing.synchronise_ifc_and_text_attributes("obj").should_be_called()
|
||||
drawing.update_text_size_pset("obj").should_be_called()
|
||||
drawing.update_newline_at("obj").should_be_called()
|
||||
drawing.update_text_value("obj").should_be_called()
|
||||
drawing.update_newline_at_and_symbol("obj").should_be_called()
|
||||
drawing.disable_editing_text("obj").should_be_called()
|
||||
subject.edit_text(drawing, obj="obj")
|
||||
|
||||
@@ -65,7 +64,6 @@ class TestEditAssignedProduct:
|
||||
).should_be_called()
|
||||
ifc.run("drawing.assign_product", relating_product="product", related_object="element").should_be_called()
|
||||
drawing.is_annotation_object_type("element", ("TEXT", "TEXT_LEADER")).should_be_called().will_return(True)
|
||||
drawing.update_text_value("obj").should_be_called()
|
||||
drawing.disable_editing_assigned_product("obj").should_be_called()
|
||||
subject.edit_assigned_product(ifc, drawing, obj="obj", product="product")
|
||||
|
||||
|
||||
@@ -622,16 +622,105 @@ class TestImportTextAttributes(NewFile):
|
||||
item = ifc.createIfcTextLiteralWithExtent(Literal="Literal", Path="RIGHT", BoxAlignment="bottom-left")
|
||||
representation = ifc.createIfcShapeRepresentation(ContextOfItems=context, Items=[item])
|
||||
element.Representation.Representations = [representation]
|
||||
element.ObjectType = "TEXT" # TODO: double check if it's valid to set this
|
||||
element.ObjectType = "TEXT"
|
||||
tool.Ifc.link(element, obj)
|
||||
|
||||
subject.import_text_attributes(obj)
|
||||
|
||||
props = tool.Drawing.get_text_props(obj)
|
||||
assert props.font_size == "2.5"
|
||||
|
||||
literal_props = props.literals[0]
|
||||
assert literal_props.ifc_definition_id == item.id()
|
||||
assert literal_props.box_alignment[:] == tuple([False] * 6 + [True] + [False] * 2)
|
||||
assert literal_props.attributes["Literal"].string_value == "Literal"
|
||||
assert literal_props.attributes["Path"].enum_value == "RIGHT"
|
||||
assert literal_props.attributes["BoxAlignment"].string_value == "bottom-left"
|
||||
|
||||
|
||||
class TestReplaceTextLiteralVariables(NewFile):
|
||||
def test_using_attribute_variables(self):
|
||||
ifc = ifcopenshell.file()
|
||||
wall = ifc.create_entity("IfcWall", Name="Baz")
|
||||
text = "Foo {{Name}} Bar"
|
||||
updated_text = subject.replace_text_literal_variables(text, wall)
|
||||
assert updated_text == "Foo Baz Bar"
|
||||
|
||||
def test_using_property_variables(self):
|
||||
ifc = ifcopenshell.file()
|
||||
|
||||
wall = ifc.createIfcWall()
|
||||
pset = ifcopenshell.api.pset.add_pset(ifc, name="Custom_Pset", product=wall)
|
||||
ifcopenshell.api.pset.edit_pset(ifc, pset=pset, properties={"Key": "Baz"})
|
||||
text = "Foo {{Custom_Pset.Key}} Bar"
|
||||
|
||||
updated_text = subject.replace_text_literal_variables(text, wall)
|
||||
assert updated_text == "Foo Baz Bar"
|
||||
|
||||
|
||||
class TestEditText(NewFile):
|
||||
def test_change_text_font_size(self):
|
||||
TestGetTextLiteral().test_run()
|
||||
obj = bpy.data.objects["Object"]
|
||||
assert DecoratorData.get_text_data(obj)["FontSize"] == 2.5
|
||||
|
||||
with bpy.context.temp_override(active_object=obj):
|
||||
bpy.ops.bim.enable_editing_text()
|
||||
props = tool.Drawing.get_text_props(obj)
|
||||
props.font_size = "7.0"
|
||||
bpy.ops.bim.edit_text()
|
||||
annotation_classes = ifcopenshell.util.element.get_pset(tool.Ifc.get_entity(obj), "EPset_Annotation", "Classes")
|
||||
assert "title" in annotation_classes
|
||||
assert DecoratorData.get_text_data(obj)["FontSize"] == 7.0
|
||||
|
||||
def test_add_second_literal(self, setup=True):
|
||||
if setup:
|
||||
TestGetTextLiteral().test_run()
|
||||
obj = bpy.data.objects["Object"]
|
||||
with bpy.context.temp_override(active_object=obj):
|
||||
bpy.ops.bim.enable_editing_text()
|
||||
bpy.ops.bim.add_text_literal()
|
||||
props = tool.Drawing.get_text_props(obj)
|
||||
literal = props.literals[1]
|
||||
literal.attributes["Literal"].string_value = "test_value"
|
||||
bpy.ops.bim.edit_text()
|
||||
|
||||
ifc = tool.Ifc.get()
|
||||
assert ifc.by_type("IfcTextLiteralWithExtent")[1].Literal == "test_value"
|
||||
|
||||
|
||||
class TestDisableTextEditing(NewFile):
|
||||
def test_disable_text_editing(self):
|
||||
# add second literal and change font size to test disable editing keeps those changes.
|
||||
TestEditText.test_change_text_font_size(self) # Set font size to "7.0".
|
||||
TestEditText.test_add_second_literal(self, setup=False)
|
||||
|
||||
obj = bpy.data.objects["Object"]
|
||||
props = tool.Drawing.get_text_props(obj)
|
||||
assert obj is not None, obj
|
||||
with bpy.context.temp_override(active_object=obj):
|
||||
bpy.ops.bim.enable_editing_text()
|
||||
bpy.ops.bim.remove_text_literal(literal_prop_id=1)
|
||||
props.literals[0].attributes["Literal"].string_value = "changed_value"
|
||||
props.font_size = "2.5"
|
||||
bpy.ops.bim.disable_editing_text()
|
||||
|
||||
ifc = tool.Ifc.get()
|
||||
# Test font size.
|
||||
annotation_classes = ifcopenshell.util.element.get_pset(tool.Ifc.get_entity(obj), "EPset_Annotation", "Classes")
|
||||
assert "title" in annotation_classes
|
||||
text_data = DecoratorData.get_text_data(obj)
|
||||
assert text_data["FontSize"] == 7.0
|
||||
|
||||
# Test second literal is present.
|
||||
assert text_data["Literals"][1]["Literal"] == "test_value"
|
||||
assert ifc.by_type("IfcTextLiteralWithExtent")[1].Literal == "test_value"
|
||||
|
||||
# Test first literal value is unchanged.
|
||||
assert text_data["Literals"][0]["Literal"] == "Literal"
|
||||
assert ifc.by_type("IfcTextLiteralWithExtent")[0].Literal == "Literal"
|
||||
|
||||
|
||||
class TestImportAssignedProduct(NewFile):
|
||||
def test_run(self):
|
||||
ifc = ifcopenshell.file()
|
||||
@@ -775,111 +864,6 @@ class TestDrawingMaintainingSheetPosition(NewFile):
|
||||
assert drawing_data["view-title"] == (30.0, 535.0, 50.22, 10.0)
|
||||
|
||||
|
||||
class TestUpdateTextValue(NewFile):
|
||||
def test_updating_arbitrary_strings(self):
|
||||
TestGetTextLiteral().test_run()
|
||||
ifc = tool.Ifc.get()
|
||||
|
||||
obj = bpy.data.objects["Object"]
|
||||
subject.update_text_value(obj)
|
||||
props = tool.Drawing.get_text_props(obj)
|
||||
literal = props.literals[0]
|
||||
|
||||
assert props.font_size == "2.5"
|
||||
assert literal.value == "Literal"
|
||||
assert literal.box_alignment[:] == tuple([False] * 6 + [True] + [False] * 2)
|
||||
assert literal.ifc_definition_id == ifc.by_type("IfcTextLiteralWithExtent")[0].id()
|
||||
|
||||
def test_using_attribute_variables(self):
|
||||
TestGetTextLiteral().test_run()
|
||||
|
||||
obj = bpy.data.objects["Object"]
|
||||
ifc = tool.Ifc.get()
|
||||
wall = ifc.createIfcWall(Name="Baz")
|
||||
label = ifc.by_type("IfcAnnotation")[0]
|
||||
ifcopenshell.api.drawing.assign_product(ifc, relating_product=wall, related_object=label)
|
||||
|
||||
ifc.by_type("IfcTextLiteralWithExtent")[0].Literal = "Foo {{Name}} Bar"
|
||||
|
||||
subject.update_text_value(obj)
|
||||
props = tool.Drawing.get_text_props(obj)
|
||||
assert props.literals[0].value == "Foo Baz Bar"
|
||||
|
||||
def test_using_property_variables(self):
|
||||
TestGetTextLiteral().test_run()
|
||||
|
||||
obj = bpy.data.objects["Object"]
|
||||
ifc = tool.Ifc.get()
|
||||
wall = ifc.createIfcWall()
|
||||
pset = ifcopenshell.api.pset.add_pset(ifc, name="Custom_Pset", product=wall)
|
||||
ifcopenshell.api.pset.edit_pset(ifc, pset=pset, properties={"Key": "Baz"})
|
||||
label = ifc.by_type("IfcAnnotation")[0]
|
||||
ifcopenshell.api.drawing.assign_product(ifc, relating_product=wall, related_object=label)
|
||||
|
||||
ifc.by_type("IfcTextLiteralWithExtent")[0].Literal = "Foo {{Custom_Pset.Key}} Bar"
|
||||
|
||||
subject.update_text_value(obj)
|
||||
props = tool.Drawing.get_text_props(obj)
|
||||
assert props.literals[0].value == "Foo Baz Bar"
|
||||
|
||||
def test_update_text_font_size(self):
|
||||
TestGetTextLiteral().test_run()
|
||||
obj = bpy.data.objects["Object"]
|
||||
with bpy.context.temp_override(active_object=obj):
|
||||
bpy.ops.bim.enable_editing_text()
|
||||
props = tool.Drawing.get_text_props(obj)
|
||||
props.font_size = "7.0"
|
||||
bpy.ops.bim.edit_text()
|
||||
annotation_classes = ifcopenshell.util.element.get_pset(tool.Ifc.get_entity(obj), "EPset_Annotation", "Classes")
|
||||
assert "title" in annotation_classes
|
||||
assert DecoratorData.get_text_data(obj)["FontSize"] == 7.0
|
||||
|
||||
def test_add_second_literal(self, setup=True):
|
||||
if setup:
|
||||
TestGetTextLiteral().test_run()
|
||||
obj = bpy.data.objects["Object"]
|
||||
with bpy.context.temp_override(active_object=obj):
|
||||
bpy.ops.bim.enable_editing_text()
|
||||
bpy.ops.bim.add_text_literal()
|
||||
props = tool.Drawing.get_text_props(obj)
|
||||
literal = props.literals[1]
|
||||
literal.attributes["Literal"].string_value = "test_value"
|
||||
bpy.ops.bim.edit_text()
|
||||
|
||||
ifc = tool.Ifc.get()
|
||||
assert ifc.by_type("IfcTextLiteralWithExtent")[1].Literal == "test_value"
|
||||
|
||||
def test_disable_text_editing(self):
|
||||
# add second literal and change font size to test changing them
|
||||
self.test_update_text_font_size() # sets font size to "7.0"
|
||||
self.test_add_second_literal(setup=False)
|
||||
|
||||
obj = bpy.data.objects["Object"]
|
||||
props = tool.Drawing.get_text_props(obj)
|
||||
assert obj is not None, obj
|
||||
with bpy.context.temp_override(active_object=obj):
|
||||
bpy.ops.bim.enable_editing_text()
|
||||
bpy.ops.bim.remove_text_literal(literal_prop_id=1)
|
||||
props.literals[0].attributes["Literal"].string_value = "changed_value"
|
||||
props.font_size = "2.5"
|
||||
bpy.ops.bim.disable_editing_text()
|
||||
|
||||
ifc = tool.Ifc.get()
|
||||
# test font size
|
||||
annotation_classes = ifcopenshell.util.element.get_pset(tool.Ifc.get_entity(obj), "EPset_Annotation", "Classes")
|
||||
assert props.font_size == "7.0"
|
||||
assert "title" in annotation_classes
|
||||
assert DecoratorData.get_text_data(obj)["FontSize"] == 7.0
|
||||
|
||||
# test second literal is present
|
||||
assert props.literals[1].attributes["Literal"].string_value == "test_value"
|
||||
assert ifc.by_type("IfcTextLiteralWithExtent")[1].Literal == "test_value"
|
||||
|
||||
# test first literal value is unchanged
|
||||
assert props.literals[0].attributes["Literal"].string_value == "Literal"
|
||||
assert ifc.by_type("IfcTextLiteralWithExtent")[0].Literal == "Literal"
|
||||
|
||||
|
||||
class TestDrawingStyles(NewFile):
|
||||
def setup_project_with_drawing(self):
|
||||
bpy.ops.bim.create_project()
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with Bonsai. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import contextlib
|
||||
import bpy
|
||||
import ifcopenshell
|
||||
import ifcopenshell.api.context
|
||||
@@ -25,6 +26,8 @@ import ifcopenshell.api.unit
|
||||
import bonsai.core.tool
|
||||
import bonsai.tool as tool
|
||||
import tempfile
|
||||
import ifcpatch
|
||||
from ifcpatch.recipes import Ifc2Sql
|
||||
from test.bim.bootstrap import NewFile
|
||||
from bonsai.tool.project import Project as subject
|
||||
from pathlib import Path
|
||||
@@ -348,3 +351,51 @@ class TestSaveLinkedModelsToIfc(NewFile):
|
||||
assert documents[0].id() == document_id
|
||||
assert documents[0].Name == "BBIM_Linked_Models"
|
||||
assert len(ifc.by_type("IfcDocumentReference")) == 0
|
||||
|
||||
|
||||
class TestLoadingIfcSqlite(NewFile):
|
||||
def test_run(self):
|
||||
filepath = Path("test/files/basic.ifc")
|
||||
ifc_file: ifcopenshell.file
|
||||
ifc_file = ifcopenshell.open(filepath)
|
||||
|
||||
patcher = Ifc2Sql.Patcher(
|
||||
ifc_file,
|
||||
sql_type="SQLite",
|
||||
)
|
||||
patcher.patch()
|
||||
tmp_file = Path(tempfile.mktemp(suffix=".ifcsqlite"))
|
||||
ifcpatch.write(patcher.get_output(), tmp_file)
|
||||
|
||||
elements_with_meshes = [
|
||||
# Types.
|
||||
"IfcSlabType/Slab",
|
||||
"IfcWallType/Wall",
|
||||
# Occurrences.
|
||||
"IfcBeam/Beam",
|
||||
"IfcWall/Wall",
|
||||
]
|
||||
|
||||
elements_without_meshes = (
|
||||
"IfcProject/My Project",
|
||||
"IfcSite/My Site",
|
||||
"IfcBuilding/My Building",
|
||||
"IfcBuildingStorey/Ground Floor",
|
||||
)
|
||||
|
||||
def clean_up() -> None:
|
||||
if isinstance(ifc_file := tool.Ifc.get(), ifcopenshell.sqlite):
|
||||
ifc_file.db.close()
|
||||
tmp_file.unlink(missing_ok=True)
|
||||
|
||||
with contextlib.ExitStack() as stack:
|
||||
stack.callback(clean_up)
|
||||
bpy.ops.bim.load_project(filepath=tmp_file.as_posix())
|
||||
assert isinstance(tool.Ifc.get(), ifcopenshell.sqlite)
|
||||
for element_name in elements_with_meshes:
|
||||
assert element_name in bpy.data.objects
|
||||
assert bpy.data.objects[element_name].data
|
||||
|
||||
for element_name in elements_without_meshes:
|
||||
assert element_name in bpy.data.objects
|
||||
assert not bpy.data.objects[element_name].data
|
||||
|
||||
+3
-1
@@ -11,11 +11,13 @@ ifeq ($(OS),Windows_NT)
|
||||
PYTHON:=python
|
||||
PIP:=pip
|
||||
VENV_BIN:=Scripts
|
||||
endif
|
||||
else
|
||||
UNAME_S:=$(shell uname -s)
|
||||
ifeq ($(UNAME_S),Darwin)
|
||||
SED:=sed -i '' -e
|
||||
PYTHON:=python3
|
||||
endif
|
||||
endif
|
||||
|
||||
VENV_ACTIVATE:=$(VENV_BIN)/activate
|
||||
|
||||
|
||||
@@ -261,6 +261,7 @@ class ScheduleIfcGenerator:
|
||||
wbs: Union[WBSEntry, None],
|
||||
work_schedule: Union[ifcopenshell.entity_instance, None],
|
||||
) -> None:
|
||||
assert self.file
|
||||
activity["ifc"] = ifcopenshell.api.sequence.add_task(
|
||||
self.file,
|
||||
work_schedule=None if wbs else work_schedule,
|
||||
@@ -283,7 +284,7 @@ class ScheduleIfcGenerator:
|
||||
ifcopenshell.api.control.assign_control(
|
||||
self.file,
|
||||
relating_control=calendar["ifc"],
|
||||
related_object=activity["ifc"],
|
||||
related_objects=[activity["ifc"]],
|
||||
)
|
||||
ifcopenshell.api.sequence.edit_task_time(
|
||||
self.file,
|
||||
|
||||
@@ -263,7 +263,7 @@ class MSP2Ifc:
|
||||
ifcopenshell.api.control.assign_control(
|
||||
self.file,
|
||||
relating_control=calendar,
|
||||
related_object=task["ifc"],
|
||||
related_objects=[task["ifc"]],
|
||||
)
|
||||
|
||||
ifcopenshell.api.sequence.edit_task(
|
||||
|
||||
@@ -20,11 +20,13 @@ from __future__ import annotations
|
||||
import csv
|
||||
import ifcopenshell
|
||||
import ifcopenshell.api
|
||||
import ifcopenshell.api.control
|
||||
import ifcopenshell.api.cost
|
||||
import ifcopenshell.api.root
|
||||
import ifcopenshell.util.unit
|
||||
import ifcopenshell.util.selector
|
||||
import ifcopenshell.util.element
|
||||
import ifcopenshell.util.cost
|
||||
import locale
|
||||
from pathlib import Path
|
||||
from typing import Union, Optional, TypedDict
|
||||
@@ -47,6 +49,10 @@ class CsvHeader(TypedDict):
|
||||
Property: NotRequired[int]
|
||||
Query: NotRequired[int]
|
||||
|
||||
# Assigning rates
|
||||
RateSchedule: NotRequired[str]
|
||||
RateID : NotRequired[str]
|
||||
|
||||
|
||||
# Currently we assume that if column is not part of the main header,
|
||||
# then it is a cost value category. So here we list any additional column
|
||||
@@ -98,6 +104,7 @@ class Csv2Ifc:
|
||||
units: dict[str, ifcopenshell.entity_instance]
|
||||
categories: dict[str, int]
|
||||
has_categories: bool
|
||||
has_rates: bool
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
@@ -152,7 +159,10 @@ class Csv2Ifc:
|
||||
# parse header
|
||||
if not self.headers:
|
||||
self.has_categories = True
|
||||
self.has_rates = False
|
||||
self.headers = {col: i for i, col in enumerate(row) if col}
|
||||
if "RateSchedule" in self.headers and "RateID" in self.headers:
|
||||
self.has_rates = True
|
||||
if "Value" in self.headers:
|
||||
self.has_categories = False
|
||||
else:
|
||||
@@ -219,6 +229,15 @@ class Csv2Ifc:
|
||||
assert "Value" in self.headers
|
||||
cost_values = row[self.headers["Value"]]
|
||||
cost_values = float(cost_values) if cost_values else None
|
||||
|
||||
if self.has_rates:
|
||||
cost_rate = {
|
||||
"Schedule": row[(self.headers["RateSchedule"])] if "RateSchedule" in self.headers else None,
|
||||
"RateID": row[(self.headers["RateID"])] if "RateID" in self.headers else None,
|
||||
}
|
||||
else:
|
||||
cost_rate = None
|
||||
|
||||
return {
|
||||
"Identification": str(identification) if identification else None,
|
||||
"Name": str(name) if name else None,
|
||||
@@ -228,6 +247,7 @@ class Csv2Ifc:
|
||||
"Property": property_name,
|
||||
"Query": query,
|
||||
"children": [],
|
||||
"CostRate": cost_rate,
|
||||
}
|
||||
|
||||
def create_ifc(self) -> None:
|
||||
@@ -297,6 +317,50 @@ class Csv2Ifc:
|
||||
|
||||
cost_value.UnitBasis = self.file.createIfcMeasureWithUnit(value_component, unit_component)
|
||||
|
||||
if self.has_rates:
|
||||
cost_rate = cost_item["CostRate"]
|
||||
assert isinstance(cost_rate, dict)
|
||||
|
||||
if cost_rate.get("Schedule") and cost_rate.get("RateID"):
|
||||
# if cost_rate["Schedule"] is not "":
|
||||
schedules = self.file.by_type("IfcCostSchedule")
|
||||
for schedule in schedules:
|
||||
if schedule.Name == cost_rate["Schedule"]:
|
||||
rate_cost_schedule = schedule
|
||||
break
|
||||
|
||||
if rate_cost_schedule:
|
||||
items = list(ifcopenshell.util.cost.get_schedule_cost_items(rate_cost_schedule))
|
||||
rate_cost_item = None
|
||||
for item in items:
|
||||
if item.Identification == cost_rate["RateID"]:
|
||||
rate_cost_item = item
|
||||
break
|
||||
if rate_cost_item:
|
||||
# next 9 lines are the same as core.cost.assign_cost_value
|
||||
ifcopenshell.api.cost.assign_cost_value(
|
||||
self.file, cost_item=cost_item["ifc"], cost_rate=rate_cost_item
|
||||
)
|
||||
existing_cost_rate = None
|
||||
for assignment in cost_item["ifc"].HasAssignments:
|
||||
if assignment.RelatingControl.is_a() == "IfcCostItem":
|
||||
existing_cost_rate = assignment.RelatingControl
|
||||
if existing_cost_rate is None:
|
||||
ifcopenshell.api.control.assign_control(
|
||||
self.file, relating_control=rate_cost_item, related_objects=[cost_item["ifc"]]
|
||||
)
|
||||
else:
|
||||
ifcopenshell.api.control.unassign_control(
|
||||
self.file, relating_control=existing_cost_rate, related_objects=[cost_item["ifc"]]
|
||||
)
|
||||
ifcopenshell.api.control.assign_control(
|
||||
self.file, relating_control=rate_cost_item, related_objects=[cost_item["ifc"]]
|
||||
)
|
||||
else:
|
||||
print(f"No cost item found with RateID={cost_rate['RateID']}")
|
||||
else:
|
||||
print(f"No cost schedule found with Name={cost_rate['Schedule']}")
|
||||
|
||||
quantity = None
|
||||
quantity_class = ifcopenshell.util.unit.get_symbol_quantity_class(cost_item["Unit"])
|
||||
prop_name = cost_item["Property"]
|
||||
|
||||
@@ -234,7 +234,7 @@ def step_impl(context, guid, number):
|
||||
site = util.assert_guid(IfcStore.file, guid)
|
||||
util.assert_type(site, "IfcSite")
|
||||
ref = util.assert_attribute(site, "RefLongitude")
|
||||
number = ifcopenshell.util.geolocation.dd2dms(number, use_ms=(len(ref) == 4))
|
||||
number = ifcopenshell.util.geolocation.dd2dms(number, use_us=(len(ref) == 4))
|
||||
util.assert_attribute(site, "RefLongitude", number)
|
||||
|
||||
|
||||
@@ -244,7 +244,7 @@ def step_impl(context, guid, number):
|
||||
site = util.assert_guid(IfcStore.file, guid)
|
||||
util.assert_type(site, "IfcSite")
|
||||
ref = util.assert_attribute(site, "RefLatitude")
|
||||
number = ifcopenshell.util.geolocation.dd2dms(number, use_ms=(len(ref) == 4))
|
||||
number = ifcopenshell.util.geolocation.dd2dms(number, use_us=(len(ref) == 4))
|
||||
util.assert_attribute(site, "RefLatitude", number)
|
||||
|
||||
|
||||
|
||||
@@ -53,6 +53,22 @@ pre_listeners: dict[str, dict] = {}
|
||||
post_listeners: dict[str, dict] = {}
|
||||
|
||||
|
||||
def batching_argument_deprecation(
|
||||
usecase_path: str, settings: dict, prev_argument: str, new_argument: str, replace_usecase: Optional[str] = None
|
||||
) -> tuple[str, dict]:
|
||||
if replace_usecase is not None:
|
||||
print(f"WARNING. `{usecase_path}` api method is deprecated and should be replaced with `{replace_usecase}`.")
|
||||
|
||||
if prev_argument in settings:
|
||||
print(
|
||||
f"WARNING. `{prev_argument}` argument is deprecated for API method "
|
||||
f'"{usecase_path}" and should be replaced with `{new_argument}`.'
|
||||
)
|
||||
settings = settings | {new_argument: [settings[prev_argument]]}
|
||||
settings.pop(prev_argument)
|
||||
return (replace_usecase or usecase_path, settings)
|
||||
|
||||
|
||||
def renamed_arguments_deprecation(
|
||||
usecase_path: str, settings: dict, arguments_remapped: dict[str, str]
|
||||
) -> tuple[str, dict]:
|
||||
@@ -71,7 +87,11 @@ def renamed_arguments_deprecation(
|
||||
# "group.add_group": partial(
|
||||
# renamed_arguments_deprecation, arguments_remapped={"Name": "name", "Description": "description"}
|
||||
# ),
|
||||
ARGUMENTS_DEPRECATION: dict[str, Callable[[str, dict[str, Any]], tuple[str, dict[str, Any]]]] = {}
|
||||
ARGUMENTS_DEPRECATION: dict[str, Callable[[str, dict[str, Any]], tuple[str, dict[str, Any]]]] = {
|
||||
"control.assign_control": partial(
|
||||
batching_argument_deprecation, prev_argument="related_object", new_argument="related_objects"
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
CACHED_USECASE_CLASSES: dict[str, Callable] = {}
|
||||
|
||||
@@ -91,43 +91,42 @@ def _add_segment_to_layout(file: ifcopenshell.file, layout: entity_instance, seg
|
||||
|
||||
segment_nest = ifcopenshell.api.alignment.get_alignment_segment_nest(layout)
|
||||
zero_length_segment = segment_nest.RelatedObjects[-1]
|
||||
# DesignParameters.StartPoint for IfcAlignmentHorizontalSegment is automatically updated when the
|
||||
# geometric representation is updated because the semantic and geometric data use the same IfcPoint.
|
||||
# This is not the case of IfcAlignmentVerticalSegment and IfcAlignmentCantSegment. For these
|
||||
# segment types, the design parameters of the zero length segment must be updated explicitly.
|
||||
if zero_length_segment.DesignParameters.is_a(
|
||||
"IfcAlignmentVerticalSegment"
|
||||
) or zero_length_segment.DesignParameters.is_a("IfcAlignmentCantSegment"):
|
||||
# get the geometric representation for the new segment
|
||||
mapped_segments = ifcopenshell.api.alignment.get_mapped_segments(segment)
|
||||
mapped_segment = mapped_segments[0] if mapped_segments[1] == None else mapped_segments[1]
|
||||
mapped_segments = ifcopenshell.api.alignment.get_mapped_segments(segment)
|
||||
mapped_segment = mapped_segments[0] if mapped_segments[1] == None else mapped_segments[1]
|
||||
|
||||
# compute the end point matrix
|
||||
settings = ifcopenshell.geom.settings()
|
||||
segment_fn = ifcopenshell_wrapper.map_shape(settings, mapped_segment.wrapped_data)
|
||||
segment_evaluator = ifcopenshell_wrapper.function_item_evaluator(settings, segment_fn)
|
||||
e = segment_evaluator.evaluate(segment_fn.end())
|
||||
end = np.array(e)
|
||||
# compute the end point matrix
|
||||
settings = ifcopenshell.geom.settings()
|
||||
segment_fn = ifcopenshell_wrapper.map_shape(settings, mapped_segment.wrapped_data)
|
||||
segment_evaluator = ifcopenshell_wrapper.function_item_evaluator(settings, segment_fn)
|
||||
e = segment_evaluator.evaluate(segment_fn.end())
|
||||
end = np.array(e)
|
||||
|
||||
# update the zero length segment semantic representation parameters
|
||||
if zero_length_segment.DesignParameters.is_a("IfcAlignmentVerticalSegment"):
|
||||
y = float(end[1, 3]) / unit_scale
|
||||
zero_length_segment.DesignParameters.StartHeight = y
|
||||
dx = float(end[0, 0])
|
||||
dy = float(end[1, 0])
|
||||
zero_length_segment.DesignParameters.StartGradient = dy / dx
|
||||
zero_length_segment.DesignParameters.EndGradient = zero_length_segment.DesignParameters.StartGradient
|
||||
else:
|
||||
z = float(end[2, 3]) / unit_scale
|
||||
dx = float(end[0, 1])
|
||||
dy = float(end[1, 1])
|
||||
dz = float(end[2, 1])
|
||||
ds = math.sqrt(dx * dx + dy * dy)
|
||||
slope = dz / ds
|
||||
railhead = layout.RailHeadDistance
|
||||
# update the zero length segment semantic representation parameters
|
||||
if zero_length_segment.DesignParameters.is_a("IfcAlignmentHorizontalSegment"):
|
||||
x = float(end[0, 3]) / unit_scale
|
||||
y = float(end[1, 3]) / unit_scale
|
||||
dx = float(end[0, 0])
|
||||
dy = float(end[1, 0])
|
||||
zero_length_segment.DesignParameters.StartPoint.Coordinates = (x,y)
|
||||
zero_length_segment.DesignParameters.StartDirection = dy / dx
|
||||
elif zero_length_segment.DesignParameters.is_a("IfcAlignmentVerticalSegment"):
|
||||
y = float(end[1, 3]) / unit_scale
|
||||
zero_length_segment.DesignParameters.StartHeight = y
|
||||
dx = float(end[0, 0])
|
||||
dy = float(end[1, 0])
|
||||
zero_length_segment.DesignParameters.StartGradient = dy / dx
|
||||
zero_length_segment.DesignParameters.EndGradient = zero_length_segment.DesignParameters.StartGradient
|
||||
else:
|
||||
z = float(end[2, 3]) / unit_scale
|
||||
dx = float(end[0, 1])
|
||||
dy = float(end[1, 1])
|
||||
dz = float(end[2, 1])
|
||||
ds = math.sqrt(dx * dx + dy * dy)
|
||||
slope = dz / ds
|
||||
railhead = layout.RailHeadDistance
|
||||
|
||||
zero_length_segment.DesignParameters.StartCantLeft = z + slope * railhead / 2.0
|
||||
zero_length_segment.DesignParameters.StartCantRight = z - slope * railhead / 2.0
|
||||
zero_length_segment.DesignParameters.StartCantLeft = z + slope * railhead / 2.0
|
||||
zero_length_segment.DesignParameters.StartCantRight = z - slope * railhead / 2.0
|
||||
|
||||
# updated the referent's name because the referent is now at a new station
|
||||
start_dist_along = 0.0
|
||||
|
||||
@@ -57,4 +57,4 @@ def get_mapped_segments(layout_segment: entity_instance) -> Sequence[entity_inst
|
||||
if segment_count == 1:
|
||||
return (curve.Segments[index - segment_count], None)
|
||||
else:
|
||||
return (curve.Segments[index - segment_count], curve.Segment[index])
|
||||
return (curve.Segments[index - segment_count], curve.Segments[index])
|
||||
|
||||
+5
-3
@@ -40,6 +40,8 @@ def layout_horizontal_alignment_by_pi_method(
|
||||
if not (len(hpoints) - 2 == len(radii)):
|
||||
raise ValueError("radii should have two fewer elements that hpoints")
|
||||
|
||||
angle_unit_scale = ifcopenshell.util.unit.calculate_unit_scale(file, "PLANEANGLEUNIT")
|
||||
|
||||
xBT, yBT = hpoints[0]
|
||||
xPI, yPI = hpoints[1]
|
||||
|
||||
@@ -84,7 +86,7 @@ def layout_horizontal_alignment_by_pi_method(
|
||||
StartTag=None,
|
||||
EndTag=None,
|
||||
StartPoint=pt,
|
||||
StartDirection=angleBT,
|
||||
StartDirection=angleBT / angle_unit_scale,
|
||||
StartRadiusOfCurvature=0.0,
|
||||
EndRadiusOfCurvature=0.0,
|
||||
SegmentLength=tangent_run,
|
||||
@@ -102,7 +104,7 @@ def layout_horizontal_alignment_by_pi_method(
|
||||
StartTag=None,
|
||||
EndTag=None,
|
||||
StartPoint=pc,
|
||||
StartDirection=angleBT,
|
||||
StartDirection=angleBT / angle_unit_scale,
|
||||
StartRadiusOfCurvature=float(radius),
|
||||
EndRadiusOfCurvature=float(radius),
|
||||
SegmentLength=lc,
|
||||
@@ -130,7 +132,7 @@ def layout_horizontal_alignment_by_pi_method(
|
||||
StartTag=None,
|
||||
EndTag=None,
|
||||
StartPoint=pt,
|
||||
StartDirection=angleBT,
|
||||
StartDirection=angleBT / angle_unit_scale,
|
||||
StartRadiusOfCurvature=0.0,
|
||||
EndRadiusOfCurvature=0.0,
|
||||
SegmentLength=tangent_run,
|
||||
|
||||
@@ -25,9 +25,9 @@ from typing import Union
|
||||
def assign_control(
|
||||
file: ifcopenshell.file,
|
||||
relating_control: ifcopenshell.entity_instance,
|
||||
related_object: ifcopenshell.entity_instance,
|
||||
related_objects: list[ifcopenshell.entity_instance],
|
||||
) -> Union[ifcopenshell.entity_instance, None]:
|
||||
"""Assigns a planning control or constraint to an object
|
||||
"""Assigns a planning control or constraint to a list of objects.
|
||||
|
||||
IFC can describe concepts that control other objects. For example, a
|
||||
planning calendar controls the availability of working days for
|
||||
@@ -42,7 +42,7 @@ def assign_control(
|
||||
|
||||
:param relating_control: The IfcControl entity that is creating the
|
||||
control or constraint
|
||||
:param related_object: The IfcObjectDefinition that is being controlled
|
||||
:param related_objects: The list of IfcObjectDefinition that is being controlled
|
||||
:return: The newly created IfcRelAssignsToControl. If relationship already
|
||||
existed before and wasn't changed then returns None.
|
||||
|
||||
@@ -59,7 +59,7 @@ def assign_control(
|
||||
# All subtasks will inherit this calendar, so assigning a single
|
||||
# calendar to the root task effectively defines a "default" calendar
|
||||
ifcopenshell.api.control.assign_control(model,
|
||||
relating_control=calendar, related_object=task)
|
||||
relating_control=calendar, related_objects=[task])
|
||||
|
||||
# Another common example might be relating a cost item and a product
|
||||
wall = ifcopenshell.api.root.create_entity(model, ifc_class="IfcWall")
|
||||
@@ -67,22 +67,33 @@ def assign_control(
|
||||
cost_item = ifcopenshell.api.cost.add_cost_item(model,
|
||||
cost_schedule=schedule)
|
||||
ifcopenshell.api.control.assign_control(model,
|
||||
relating_control=cost_item, related_object=wall)
|
||||
relating_control=cost_item, related_objects=[wall])
|
||||
"""
|
||||
if related_object.HasAssignments:
|
||||
for assignment in related_object.HasAssignments:
|
||||
if assignment.is_a("IfcRelAssignsToControl") and assignment.RelatingControl == relating_control:
|
||||
return
|
||||
# Filter out already assigned objects.
|
||||
related_objects_set = set(related_objects)
|
||||
objects_to_assign: set[ifcopenshell.entity_instance] = set()
|
||||
|
||||
control_assignments = set(relating_control.Controls)
|
||||
if control_assignments:
|
||||
for obj in related_objects_set:
|
||||
existing_assignment = next((a for a in obj.HasAssignments if a in control_assignments), None)
|
||||
# Skip objects already assigned to this control.
|
||||
if existing_assignment:
|
||||
continue
|
||||
objects_to_assign.add(obj)
|
||||
else:
|
||||
objects_to_assign = related_objects_set
|
||||
|
||||
if not objects_to_assign:
|
||||
return None
|
||||
|
||||
controls: Union[ifcopenshell.entity_instance, None]
|
||||
controls = next(iter(relating_control.Controls), None)
|
||||
|
||||
if controls:
|
||||
if related_object in controls.RelatedObjects:
|
||||
return
|
||||
related_objects = set(controls.RelatedObjects)
|
||||
related_objects.add(related_object)
|
||||
controls.RelatedObjects = list(related_objects)
|
||||
related_objects_new: list[ifcopenshell.entity_instance] = list(controls.RelatedObjects)
|
||||
related_objects_new.extend(objects_to_assign)
|
||||
controls.RelatedObjects = list(related_objects_new)
|
||||
ifcopenshell.api.owner.update_owner_history(file, element=controls)
|
||||
else:
|
||||
controls = file.create_entity(
|
||||
@@ -90,7 +101,7 @@ def assign_control(
|
||||
**{
|
||||
"GlobalId": ifcopenshell.guid.new(),
|
||||
"OwnerHistory": ifcopenshell.api.owner.create_owner_history(file),
|
||||
"RelatedObjects": [related_object],
|
||||
"RelatedObjects": list(objects_to_assign),
|
||||
"RelatingControl": relating_control,
|
||||
},
|
||||
)
|
||||
|
||||
@@ -19,21 +19,19 @@
|
||||
import ifcopenshell
|
||||
import ifcopenshell.api.owner
|
||||
import ifcopenshell.util.element
|
||||
from typing import Union
|
||||
|
||||
|
||||
def unassign_control(
|
||||
file: ifcopenshell.file,
|
||||
relating_control: ifcopenshell.entity_instance,
|
||||
related_object: ifcopenshell.entity_instance,
|
||||
) -> Union[ifcopenshell.entity_instance, None]:
|
||||
related_objects: list[ifcopenshell.entity_instance],
|
||||
) -> None:
|
||||
"""Unassigns a planning control or constraint to an object
|
||||
|
||||
:param relating_control: The IfcControl entity that is creating the
|
||||
control or constraint
|
||||
:param related_object: The IfcObjectDefinition that is being controlled
|
||||
:return: If the control still is related to other objects, the
|
||||
IfcRelAssignsToControl is returned, otherwise None.
|
||||
:param related_objects: The list IfcObjectDefinitions that is being controlled
|
||||
:return: None
|
||||
|
||||
Example:
|
||||
|
||||
@@ -45,23 +43,23 @@ def unassign_control(
|
||||
cost_item = ifcopenshell.api.cost.add_cost_item(model,
|
||||
cost_schedule=schedule)
|
||||
ifcopenshell.api.control.assign_control(model,
|
||||
relating_control=cost_item, related_object=wall)
|
||||
relating_control=cost_item, related_objects=[wall])
|
||||
|
||||
# And now let's change our mind
|
||||
ifcopenshell.api.control.unassign_control(model,
|
||||
relating_control=cost_item, related_object=wall)
|
||||
relating_control=cost_item, related_objects=[wall])
|
||||
"""
|
||||
for rel in related_object.HasAssignments or []:
|
||||
if not rel.is_a("IfcRelAssignsToControl") or rel.RelatingControl != relating_control:
|
||||
continue
|
||||
if len(rel.RelatedObjects) == 1:
|
||||
related_objects_set = set(related_objects)
|
||||
control_assignments = set(relating_control.Controls)
|
||||
rels = set(rel for obj in related_objects_set for rel in obj.HasAssignments if rel in control_assignments)
|
||||
|
||||
for rel in rels:
|
||||
related_objects_new = set(rel.RelatedObjects) - related_objects_set
|
||||
if related_objects_new:
|
||||
rel.RelatedObjects = list(related_objects_new)
|
||||
ifcopenshell.api.owner.update_owner_history(file, element=rel)
|
||||
else:
|
||||
history = rel.OwnerHistory
|
||||
file.remove(rel)
|
||||
if history:
|
||||
ifcopenshell.util.element.remove_deep2(file, history)
|
||||
return
|
||||
related_objects = list(rel.RelatedObjects)
|
||||
related_objects.remove(related_object)
|
||||
rel.RelatedObjects = related_objects
|
||||
ifcopenshell.api.owner.update_owner_history(file, element=rel)
|
||||
return rel
|
||||
|
||||
@@ -59,7 +59,7 @@ def add_cost_item(
|
||||
cost_item_ = ifcopenshell.api.root.create_entity(file, ifc_class="IfcCostItem")
|
||||
|
||||
if cost_schedule:
|
||||
ifcopenshell.api.control.assign_control(file, cost_schedule, cost_item_)
|
||||
ifcopenshell.api.control.assign_control(file, cost_schedule, [cost_item_])
|
||||
elif cost_item:
|
||||
ifcopenshell.api.nest.assign_object(file, related_objects=[cost_item_], relating_object=cost_item)
|
||||
return cost_item_
|
||||
|
||||
@@ -66,7 +66,7 @@ def add_cost_item_quantity(
|
||||
schedule = ifcopenshell.api.cost.add_cost_schedule(model)
|
||||
item = ifcopenshell.api.cost.add_cost_item(model, cost_schedule=schedule)
|
||||
ifcopenshell.api.control.assign_control(model,
|
||||
relating_control=item, related_object=chair)
|
||||
relating_control=item, related_objects=[chair])
|
||||
|
||||
# Let's assume we want to count the amount of chairs to calculate our cost item
|
||||
# Because this is an IfcQuantityCount the count will be automatically set to "1" chair
|
||||
|
||||
@@ -122,7 +122,7 @@ class Usecase:
|
||||
) -> ifcopenshell.entity_instance:
|
||||
return ifcopenshell.api.control.assign_control(
|
||||
self.file,
|
||||
related_object=related_object,
|
||||
related_objects=[related_object],
|
||||
relating_control=cost_item,
|
||||
)
|
||||
|
||||
|
||||
@@ -57,7 +57,7 @@ def calculate_cost_item_resource_value(file: ifcopenshell.file, cost_item: ifcop
|
||||
concrete = ifcopenshell.api.resource.add_resource(model,
|
||||
ifc_class="IfcConstructionMaterialResource", parent_resource=crew)
|
||||
ifcopenshell.api.control.assign_control(model,
|
||||
relating_control=item, related_object=concrete)
|
||||
relating_control=item, related_objects=[concrete])
|
||||
# ... which has a unit price of 42.0 per m3
|
||||
value = ifcopenshell.api.cost.add_cost_value(model, parent=concrete)
|
||||
ifcopenshell.api.cost.edit_cost_value(model, cost_value=value,
|
||||
@@ -72,7 +72,7 @@ def calculate_cost_item_resource_value(file: ifcopenshell.file, cost_item: ifcop
|
||||
equipment = ifcopenshell.api.resource.add_resource(model,
|
||||
ifc_class="IfcConstructionEquipmentResource", parent_resource=crew)
|
||||
ifcopenshell.api.control.assign_control(model,
|
||||
relating_control=item, related_object=equipment)
|
||||
relating_control=item, related_objects=[equipment])
|
||||
# ... with a fixed price of 50,000
|
||||
value = ifcopenshell.api.cost.add_cost_value(model, parent=concrete)
|
||||
ifcopenshell.api.cost.edit_cost_value(model, cost_value=value,
|
||||
|
||||
@@ -45,5 +45,5 @@ def copy_cost_schedule(
|
||||
if isinstance(duplicated_cost_item, list):
|
||||
# All other nested items are not connected to the cost schedule explicitly.
|
||||
duplicated_cost_item = duplicated_cost_item[0]
|
||||
ifcopenshell.api.control.assign_control(file, new_schedule, duplicated_cost_item)
|
||||
ifcopenshell.api.control.assign_control(file, new_schedule, [duplicated_cost_item])
|
||||
return new_schedule
|
||||
|
||||
@@ -86,7 +86,7 @@ class Usecase:
|
||||
for product in products:
|
||||
ifcopenshell.api.control.unassign_control(
|
||||
self.file,
|
||||
related_object=product,
|
||||
related_objects=[product],
|
||||
relating_control=cost_item,
|
||||
)
|
||||
self.update_cost_item_count(cost_item)
|
||||
|
||||
@@ -22,6 +22,7 @@ import ifcopenshell.api.owner
|
||||
import ifcopenshell.util.unit
|
||||
import ifcopenshell.util.element
|
||||
import ifcopenshell.util.placement
|
||||
from ifcopenshell.util.shape_builder import ShapeBuilder
|
||||
from typing import Optional, Union, Any
|
||||
|
||||
NPArrayOfFloats = npt.NDArray[np.float64]
|
||||
@@ -74,6 +75,7 @@ class Usecase:
|
||||
if not hasattr(self.settings["product"], "ObjectPlacement"):
|
||||
return
|
||||
self.unit_scale = ifcopenshell.util.unit.calculate_unit_scale(self.file)
|
||||
self.builder = ShapeBuilder(self.file)
|
||||
|
||||
if not self.settings["is_si"]:
|
||||
self.convert_matrix_to_si(self.settings["matrix"])
|
||||
@@ -183,25 +185,12 @@ class Usecase:
|
||||
o = np.array((m[0][3], m[1][3], m[2][3]))
|
||||
object_matrix = ifcopenshell.util.placement.a2p(o, z, x)
|
||||
relative_placement_matrix = np.linalg.inv(relating_object_matrix) @ object_matrix
|
||||
return self.create_ifc_axis_2_placement_3d(
|
||||
relative_placement_matrix[:, 3][0:3],
|
||||
return self.builder.create_axis2_placement_3d(
|
||||
self.convert_si_to_unit(relative_placement_matrix[:, 3][0:3]),
|
||||
relative_placement_matrix[:, 2][0:3],
|
||||
relative_placement_matrix[:, 0][0:3],
|
||||
)
|
||||
|
||||
def create_ifc_axis_2_placement_3d(
|
||||
self, point: NPArrayOfFloats, up: NPArrayOfFloats, forward: NPArrayOfFloats
|
||||
) -> ifcopenshell.entity_instance:
|
||||
return self.file.createIfcAxis2Placement3D(
|
||||
self.create_cartesian_point(point),
|
||||
self.file.createIfcDirection(up.tolist()),
|
||||
self.file.createIfcDirection(forward.tolist()),
|
||||
)
|
||||
|
||||
def create_cartesian_point(self, co: NPArrayOfFloats) -> ifcopenshell.entity_instance:
|
||||
co = self.convert_si_to_unit(co)
|
||||
return self.file.createIfcCartesianPoint(co.tolist())
|
||||
|
||||
def convert_si_to_unit(self, co: NPArrayOfFloats) -> NPArrayOfFloats:
|
||||
return co / self.unit_scale
|
||||
|
||||
|
||||
@@ -18,9 +18,9 @@
|
||||
|
||||
import ifcopenshell
|
||||
import ifcopenshell.util.element
|
||||
import ifcopenshell.util.geolocation
|
||||
import ifcopenshell.util.unit
|
||||
import numpy as np
|
||||
from ifcopenshell.util.shape_builder import ShapeBuilder
|
||||
from math import sin, cos, radians
|
||||
|
||||
|
||||
@@ -62,6 +62,7 @@ def edit_wcs(
|
||||
ifcopenshell.api.georeference.edit_wcs(model)
|
||||
"""
|
||||
unit_scale = ifcopenshell.util.unit.calculate_unit_scale(file)
|
||||
builder = ShapeBuilder(file)
|
||||
if np.isclose(rotation, 0):
|
||||
xaxis_x = 1.0
|
||||
xaxis_y = 0.0
|
||||
@@ -74,14 +75,10 @@ def edit_wcs(
|
||||
old_wcs = context.WorldCoordinateSystem
|
||||
if context.CoordinateSpaceDimension == 3:
|
||||
if is_si:
|
||||
point = file.createIfcCartesianPoint((x / unit_scale, y / unit_scale, z / unit_scale))
|
||||
xyz = (x / unit_scale, y / unit_scale, z / unit_scale)
|
||||
else:
|
||||
point = file.createIfcCartesianPoint((x, y, z))
|
||||
placement = file.createIfcAxis2Placement3D(
|
||||
point,
|
||||
file.createIfcDirection((0.0, 0.0, 1.0)),
|
||||
file.createIfcDirection((xaxis_x, xaxis_y, 0.0)),
|
||||
)
|
||||
xyz = (x, y, z)
|
||||
placement = builder.create_axis2_placement_3d(xyz, (0.0, 0.0, 1.0), (xaxis_x, xaxis_y, 0.0))
|
||||
elif context.CoordinateSpaceDimension == 2:
|
||||
if is_si:
|
||||
point = file.createIfcCartesianPoint((x / unit_scale, y / unit_scale))
|
||||
|
||||
@@ -138,7 +138,7 @@ def add_task(
|
||||
task.Identification = identification
|
||||
task.IsMilestone = False
|
||||
if work_schedule:
|
||||
ifcopenshell.api.control.assign_control(file, work_schedule, task)
|
||||
ifcopenshell.api.control.assign_control(file, work_schedule, [task])
|
||||
elif parent_task:
|
||||
rel = ifcopenshell.api.nest.assign_object(
|
||||
file,
|
||||
|
||||
@@ -75,7 +75,7 @@ def add_work_calendar(
|
||||
# We associate the calendar with the construction root task. All
|
||||
# subtasks underneath the construction work task will also inherit
|
||||
# this calendar by default (though you can override them).
|
||||
ifcopenshell.api.control.assign_control(model, relating_control=calendar, related_object=task)
|
||||
ifcopenshell.api.control.assign_control(model, relating_control=calendar, related_objects=[task])
|
||||
"""
|
||||
work_calendar = ifcopenshell.api.root.create_entity(
|
||||
file,
|
||||
|
||||
@@ -47,5 +47,5 @@ def copy_work_schedule(
|
||||
duplicated_tasks = ifcopenshell.api.sequence.duplicate_task(file, task)[1]
|
||||
# All other nested items are not connected to the work schedule explicitly.
|
||||
duplicated_task = duplicated_tasks[0]
|
||||
ifcopenshell.api.control.assign_control(file, new_schedule, duplicated_task)
|
||||
ifcopenshell.api.control.assign_control(file, new_schedule, [duplicated_task])
|
||||
return new_schedule
|
||||
|
||||
@@ -79,7 +79,7 @@ class Usecase:
|
||||
assert isinstance(res, list)
|
||||
current, duplicate = res
|
||||
ifcopenshell.api.control.assign_control(
|
||||
self.file, relating_control=baseline_work_schedule, related_object=duplicate[0]
|
||||
self.file, relating_control=baseline_work_schedule, related_objects=[duplicate[0]]
|
||||
)
|
||||
for i, task in enumerate(current):
|
||||
self.create_baseline_reference(task, duplicate[i])
|
||||
|
||||
@@ -54,7 +54,7 @@ def remove_work_calendar(file: ifcopenshell.file, work_calendar: ifcopenshell.en
|
||||
ifcopenshell.api.control.unassign_control(
|
||||
file,
|
||||
relating_control=work_calendar,
|
||||
related_object=related_object,
|
||||
related_objects=[related_object],
|
||||
)
|
||||
|
||||
# Currently in API work times are created already attached
|
||||
|
||||
@@ -18,11 +18,13 @@
|
||||
from __future__ import annotations
|
||||
import re
|
||||
import json
|
||||
import numpy as np
|
||||
import numpy.typing as npt
|
||||
import ifcopenshell
|
||||
import ifcopenshell.util.attribute
|
||||
import ifcopenshell.util.schema
|
||||
from pathlib import Path
|
||||
from typing import Any, NoReturn, Union, Optional, TYPE_CHECKING
|
||||
from typing import Any, NoReturn, Union, Optional, TYPE_CHECKING, TypedDict
|
||||
from . import ifcopenshell_wrapper
|
||||
from .file import file
|
||||
from .entity_instance import entity_instance
|
||||
@@ -36,8 +38,33 @@ except ImportError as e:
|
||||
print(f"No SQL support: {e}")
|
||||
|
||||
|
||||
class GeometryCache(TypedDict):
|
||||
shapes: dict[int, GeometryCacheShape]
|
||||
geometry: dict[str, GeometryCacheGeometry]
|
||||
|
||||
|
||||
class GeometryCacheShape(TypedDict):
|
||||
co: list[float]
|
||||
"""Object location."""
|
||||
matrix: npt.NDArray[np.float64]
|
||||
geometry: Union[str, None]
|
||||
"""Element's geometry id (same value as in ``Representation.id``).
|
||||
|
||||
Is set to ``None` when no geometry is available for the element.
|
||||
"""
|
||||
|
||||
|
||||
class GeometryCacheGeometry(TypedDict):
|
||||
verts: npt.NDArray[np.float64]
|
||||
edges: npt.NDArray[np.int32]
|
||||
faces: npt.NDArray[np.int32]
|
||||
material_ids: npt.NDArray[np.int32]
|
||||
materials: list[int]
|
||||
|
||||
|
||||
class sqlite(file):
|
||||
schema: ifcopenshell.util.schema.IFC_SCHEMA = "IFC4"
|
||||
mvd_str: str
|
||||
"""As in `header.file_description.description`."""
|
||||
|
||||
def __init__(self, filepath: str):
|
||||
"""
|
||||
@@ -53,7 +80,6 @@ class sqlite(file):
|
||||
if not Path(filepath).exists():
|
||||
raise FileNotFoundError(f"File doesn't exist: {filepath}")
|
||||
|
||||
self.wrapped_data = None
|
||||
self.history_size = 64
|
||||
self.history = []
|
||||
self.future = []
|
||||
@@ -81,7 +107,8 @@ class sqlite(file):
|
||||
except:
|
||||
assert False, "SQLite schema not supported."
|
||||
|
||||
self.schema = row[1]
|
||||
self._schema = row[1]
|
||||
self.mvd_str = row[2]
|
||||
self.ifc_schema = ifcopenshell.schema_by_name(self.schema)
|
||||
|
||||
self.cursor.execute("SELECT ifc_id, ifc_class FROM id_map")
|
||||
@@ -234,29 +261,30 @@ class sqlite(file):
|
||||
return True
|
||||
return False
|
||||
|
||||
def get_geometry(self, ids: list[int]) -> dict[str, dict]:
|
||||
def get_geometry(self, ids: list[int]) -> GeometryCache:
|
||||
import numpy as np
|
||||
|
||||
ids_csv = ",".join(map(str, ids))
|
||||
query = f"SELECT ifc_id, x, y, z, matrix, geometry, verts, edges, faces, material_ids, materials FROM shape LEFT JOIN geometry ON shape.geometry = geometry.id WHERE `ifc_id` IN ({ids_csv})"
|
||||
self.cursor.execute(query)
|
||||
rows = self.cursor.fetchall()
|
||||
shapes = {}
|
||||
geometry = {}
|
||||
shapes: dict[int, GeometryCacheShape] = {}
|
||||
geometry: dict[str, GeometryCacheGeometry] = {}
|
||||
for row in rows:
|
||||
if row["geometry"] and row["geometry"] not in geometry:
|
||||
# Same data types as in ifcopenshell.util.shape.
|
||||
geometry[row["geometry"]] = {
|
||||
"verts": np.frombuffer(row["verts"]).tolist() if row["verts"] else [],
|
||||
"edges": np.frombuffer(row["edges"], dtype=np.int64).tolist() if row["edges"] else [],
|
||||
"faces": np.frombuffer(row["faces"], dtype=np.int64).tolist() if row["faces"] else [],
|
||||
"verts": np.frombuffer(row["verts"], dtype="d") if row["verts"] else np.empty(0, dtype="d"),
|
||||
"edges": np.frombuffer(row["edges"], dtype="i") if row["edges"] else np.empty(0, dtype="i"),
|
||||
"faces": np.frombuffer(row["faces"], dtype="i") if row["faces"] else np.empty(0, dtype="i"),
|
||||
"material_ids": (
|
||||
np.frombuffer(row["material_ids"], dtype=np.int64).tolist() if row["material_ids"] else []
|
||||
np.frombuffer(row["material_ids"], dtype="i") if row["material_ids"] else np.empty(0, dtype="i")
|
||||
),
|
||||
"materials": json.loads(row["materials"]) if row["materials"] else [],
|
||||
}
|
||||
shapes[row["ifc_id"]] = {
|
||||
"co": [row["x"], row["y"], row["z"]],
|
||||
"matrix": np.copy(np.frombuffer(row["matrix"]).reshape((4, 4))),
|
||||
"matrix": np.copy(np.frombuffer(row["matrix"], dtype="d").reshape((4, 4))),
|
||||
"geometry": row["geometry"],
|
||||
}
|
||||
ids_without_geometry = set(ids) - set(shapes.keys())
|
||||
@@ -272,6 +300,21 @@ class sqlite(file):
|
||||
# Override to avoid clean up data unrelated to sqlite file.
|
||||
pass
|
||||
|
||||
def wrapped_data(self) -> NoReturn:
|
||||
class_name = str(type(self))
|
||||
raise Exception(
|
||||
f"No `wrapped_data` for {class_name}. `ifcopenshell.{class_name}` is probably confused with `ifcopenshell.file`."
|
||||
)
|
||||
|
||||
@property
|
||||
def schema(self) -> ifcopenshell.util.schema.IFC_SCHEMA:
|
||||
return self._schema
|
||||
|
||||
@property
|
||||
def schema_identifier(self) -> str:
|
||||
# The best option we've got for mimicing `file.schema_identifier`.
|
||||
return self._schema
|
||||
|
||||
|
||||
class sqlite_entity(entity_instance):
|
||||
sqlite_wrapper: sqlite_wrapper
|
||||
@@ -449,6 +492,10 @@ class sqlite_entity(entity_instance):
|
||||
info.update(self.sqlite_wrapper.attribute_cache)
|
||||
return info
|
||||
|
||||
@property
|
||||
def file(self) -> sqlite:
|
||||
return self.sqlite_wrapper.file
|
||||
|
||||
|
||||
class sqlite_wrapper:
|
||||
def __init__(self, id: int, ifc_class: str, file: sqlite):
|
||||
|
||||
@@ -87,10 +87,7 @@ try:
|
||||
return (int(items[0]), str(items[1]), items[2])
|
||||
|
||||
class stream(file):
|
||||
schema: ifcopenshell.util.schema.IFC_SCHEMA = "IFC4"
|
||||
|
||||
def __init__(self, filepath: str):
|
||||
self.wrapped_data = None
|
||||
self.history_size = 64
|
||||
self.history = []
|
||||
self.future = []
|
||||
@@ -109,9 +106,9 @@ try:
|
||||
# common.INT doesn't support negative integers.
|
||||
grammar = r"""
|
||||
start: "#" NUMBER "=" TYPE "(" args ")" ";"
|
||||
|
||||
|
||||
args: arg ("," arg)*
|
||||
|
||||
|
||||
arg: STRING -> string
|
||||
| FLOAT -> float
|
||||
| IFCINT -> ifcint
|
||||
@@ -121,21 +118,21 @@ try:
|
||||
| REFERENCE -> reference
|
||||
| list -> list
|
||||
| inline_type -> inline_type
|
||||
|
||||
|
||||
list: "(" arg? ("," arg)* ")"
|
||||
inline_type: TYPE "(" arg ")"
|
||||
REFERENCE: "#" /[0-9]+/
|
||||
|
||||
|
||||
TYPE: CNAME
|
||||
NUMBER: INT
|
||||
|
||||
|
||||
STRING: "'" /([^']|'')*/ "'"
|
||||
IFCINT: /-?[0-9]+/
|
||||
FLOAT: /-?[0-9]+\.[0-9]*([Ee]-?[0-9]+)?/
|
||||
NULL: "$"
|
||||
DERIVED: "*"
|
||||
ENUM: "." CNAME "."
|
||||
|
||||
|
||||
%import common.INT
|
||||
%import common.CNAME
|
||||
"""
|
||||
@@ -176,7 +173,7 @@ try:
|
||||
self.class_map.setdefault(ifc_class, []).append(step_id)
|
||||
self.id_offset[step_id] = offset
|
||||
elif line.startswith("FILE_SCHEMA"):
|
||||
self.schema = line.split("'")[1]
|
||||
self._schema = line.split("'")[1]
|
||||
self.ifc_schema = ifcopenshell.schema_by_name(self.schema)
|
||||
for ifc_class in exclude_classes:
|
||||
declaration = self.ifc_schema.declaration_by_name(ifc_class)
|
||||
@@ -290,6 +287,21 @@ try:
|
||||
# Override to avoid clean up unrelated to stream file.
|
||||
pass
|
||||
|
||||
def wrapped_data(self) -> NoReturn:
|
||||
class_name = str(type(self))
|
||||
raise Exception(
|
||||
f"No `wrapped_data` for {class_name}. `ifcopenshell.{class_name}` is probably confused with `ifcopenshell.file`."
|
||||
)
|
||||
|
||||
@property
|
||||
def schema(self) -> ifcopenshell.util.schema.IFC_SCHEMA:
|
||||
return self._schema
|
||||
|
||||
@property
|
||||
def schema_identifier(self) -> str:
|
||||
# The best option we've got for mimicing `file.schema_identifier`.
|
||||
return self._schema
|
||||
|
||||
class stream_entity(entity_instance):
|
||||
stream_wrapper: stream_wrapper
|
||||
|
||||
|
||||
@@ -21,6 +21,7 @@ import numpy as np
|
||||
import ifcopenshell
|
||||
from typing import Any, Union
|
||||
from dataclasses import dataclass
|
||||
from ifcopenshell.util.shape_builder import ShapeBuilder
|
||||
|
||||
|
||||
@dataclass
|
||||
@@ -78,9 +79,7 @@ class Clipping:
|
||||
|
||||
if not ifc_file:
|
||||
ifc_file = first_operand.file
|
||||
|
||||
location = ifc_file.createIfcCartesianPoint([i / unit_scale for i in self.location])
|
||||
direction = ifc_file.createIfcDirection(self.normal)
|
||||
builder = ShapeBuilder(ifc_file)
|
||||
|
||||
normal = np.array(self.normal)
|
||||
if np.allclose(normal, np.array([0.0, 0.0, 1.0]), atol=1e-2) or np.allclose(
|
||||
@@ -92,9 +91,9 @@ class Clipping:
|
||||
|
||||
x_axis = np.cross(normal, arbitrary_vector)
|
||||
x_axis /= np.linalg.norm(x_axis)
|
||||
x_axis = ifc_file.createIfcDirection(x_axis.tolist())
|
||||
|
||||
plane = ifc_file.createIfcPlane(ifc_file.createIfcAxis2Placement3D(location, direction, x_axis))
|
||||
placement = builder.create_axis2_placement_3d([i / unit_scale for i in self.location], self.normal, x_axis)
|
||||
plane = ifc_file.create_entity("IfcPlane", placement)
|
||||
|
||||
second_operand = ifc_file.createIfcHalfSpaceSolid(plane, False)
|
||||
return ifc_file.createIfcBooleanClippingResult("DIFFERENCE", first_operand, second_operand)
|
||||
|
||||
@@ -1169,6 +1169,25 @@ def get_groups(element: ifcopenshell.entity_instance) -> list[ifcopenshell.entit
|
||||
return groups
|
||||
|
||||
|
||||
def get_controls(element: ifcopenshell.entity_instance) -> Generator[ifcopenshell.entity_instance]:
|
||||
"""
|
||||
Retrieves the controls of an element.
|
||||
|
||||
:param element: The IFC element
|
||||
:return: Generator of IfcControl elements assigned to the element.
|
||||
|
||||
Example:
|
||||
|
||||
.. code:: python
|
||||
|
||||
task = file.by_type("IfcTask")[0]
|
||||
control = ifcopenshell.util.element.get_controls(task)[0]
|
||||
"""
|
||||
for rel in element.HasAssignments:
|
||||
if rel.is_a("IfcRelAssignsToControl"):
|
||||
yield rel.RelatingControl
|
||||
|
||||
|
||||
def get_parent(element: ifcopenshell.entity_instance) -> Union[ifcopenshell.entity_instance, None]:
|
||||
"""Get the parent in the spatial heirarchy
|
||||
|
||||
|
||||
@@ -23,6 +23,7 @@ import ifcopenshell.util.unit
|
||||
import ifcopenshell.util.element
|
||||
import ifcopenshell.util.placement
|
||||
from typing import NamedTuple, Optional, Union
|
||||
from decimal import Decimal, ROUND_HALF_UP
|
||||
|
||||
MatrixType = ifcopenshell.util.placement.MatrixType
|
||||
|
||||
@@ -39,39 +40,54 @@ class HelmertTransformation(NamedTuple):
|
||||
factor_z: float
|
||||
|
||||
|
||||
def dms2dd(degrees: int, minutes: int, seconds: int, ms: int = 0) -> float:
|
||||
"""Convert degrees, minutes, and (milli)seconds to decimal degrees
|
||||
def dms2dd(degrees: int, minutes: int, seconds: int, us: int = 0) -> float:
|
||||
"""Convert degrees, minutes, and (micro)seconds to decimal degrees
|
||||
All components must be either positive or negative.
|
||||
|
||||
:param degrees: The degrees component
|
||||
:param minutes: The minutes component
|
||||
:param seconds: The seconds component
|
||||
:param ms: The milliseconds component
|
||||
:param us: The microseconds component
|
||||
:return: The angle in decimal degrees.
|
||||
"""
|
||||
dd = float(degrees) + float(minutes) / 60.0 + float(seconds) / (3600.0) + float(ms / 3600000000.0)
|
||||
return dd
|
||||
all_positive_or_zero = degrees >= 0 and minutes >= 0 and seconds >= 0 and us >= 0
|
||||
all_negative_or_zero = degrees <= 0 and minutes <= 0 and seconds <= 0 and us <= 0
|
||||
assert all_positive_or_zero or all_negative_or_zero
|
||||
return degrees + minutes / 60.0 + seconds / 3600.0 + us / 3600000000.0
|
||||
|
||||
|
||||
def dd2dms(dd: float, use_ms: bool = False) -> Union[tuple[float, float, float, float], tuple[float, float, float]]:
|
||||
"""Convert decimal degrees to degrees, minutes, and (milli)seconds format
|
||||
def dd2dms(dd: float, use_us: bool = False) -> Union[tuple[int, int, int, int], tuple[int, int, float]]:
|
||||
"""Convert decimal degrees to degrees, minutes, and (micro)seconds format
|
||||
|
||||
:param dd: The decimal degrees
|
||||
:param use_ms: True if to include milliseconds and false otherwise. Defaults to false.
|
||||
:return: The angle in a tuple of either 3 or 4 values, being degrees,
|
||||
minutes, seconds, and optionally milliseconds.
|
||||
:param use_us: True if to include microseconds and false otherwise. Defaults to false.
|
||||
:return: The angle in a tuple of either 3 or 4 values,
|
||||
4 values: integer number of degrees, integer number of minutes, integer number of seconds and integer number of microseconds
|
||||
3 values: integer number of degrees, integer number of minutes, and a float number for seconds
|
||||
:note: the tuple follows the format of IfcCompoundPlaneAngleMeasure. Namely all of its components are either positive or negative.
|
||||
"""
|
||||
dd = float(dd)
|
||||
sign = 1 if dd >= 0 else -1
|
||||
dd = abs(dd)
|
||||
if use_ms:
|
||||
seconds, ms = divmod(dd * 60 * 60 * 1000000, 1000000)
|
||||
minutes, seconds = divmod(dd * 60 * 60, 60)
|
||||
degrees, minutes = divmod(minutes, 60)
|
||||
if dd < 0:
|
||||
degrees = -degrees
|
||||
if use_ms:
|
||||
return (int(degrees) * sign, int(minutes) * sign, int(seconds) * sign, int(ms) * sign)
|
||||
return (int(degrees) * sign, int(minutes) * sign, int(seconds) * sign)
|
||||
dd_decimal = Decimal(str(dd))
|
||||
|
||||
degrees = int(dd_decimal)
|
||||
degrees_decimal = Decimal(degrees)
|
||||
|
||||
fractional_part = dd_decimal - degrees_decimal
|
||||
|
||||
minutes_decimal = fractional_part * Decimal(60)
|
||||
minutes = int(minutes_decimal)
|
||||
minutes_decimal_int = Decimal(minutes)
|
||||
|
||||
seconds_decimal = (minutes_decimal - minutes_decimal_int) * Decimal(60)
|
||||
|
||||
if use_us:
|
||||
seconds = int(seconds_decimal)
|
||||
seconds_decimal_int = Decimal(seconds)
|
||||
microseconds_decimal = (seconds_decimal - seconds_decimal_int) * Decimal(1000000)
|
||||
microseconds = int(microseconds_decimal.quantize(Decimal(1), rounding=ROUND_HALF_UP))
|
||||
return (degrees, minutes, seconds, microseconds)
|
||||
else:
|
||||
seconds_float = float(seconds_decimal)
|
||||
return (degrees, minutes, seconds_float)
|
||||
|
||||
|
||||
def xyz2enh(
|
||||
|
||||
@@ -692,7 +692,8 @@ def calculate_unit_scale(ifc_file: ifcopenshell.file, unit_type: str = "LENGTHUN
|
||||
:returns: The scale factor
|
||||
"""
|
||||
if (
|
||||
unit_type
|
||||
type(ifc_file) is ifcopenshell.file
|
||||
and unit_type
|
||||
not in ifcopenshell.ifcopenshell_wrapper.schema_by_name(ifc_file.schema_identifier)
|
||||
.declaration_by_name("IfcUnitEnum")
|
||||
.enumeration_items()
|
||||
|
||||
@@ -27,23 +27,33 @@ class TestAssignControl(test.bootstrap.IFC4):
|
||||
control = ifcopenshell.api.cost.add_cost_schedule(self.file)
|
||||
|
||||
# simple assignment
|
||||
relation = ifcopenshell.api.control.assign_control(self.file, relating_control=control, related_object=wall)
|
||||
relation = ifcopenshell.api.control.assign_control(self.file, relating_control=control, related_objects=[wall])
|
||||
assert relation
|
||||
assert len(self.file.by_type("IfcRelAssignsToControl")) == 1
|
||||
assert relation.RelatingControl == control
|
||||
assert relation.RelatedObjects == (wall,)
|
||||
|
||||
# trying to establish existing relationship
|
||||
relation = ifcopenshell.api.control.assign_control(self.file, relating_control=control, related_object=wall)
|
||||
relation = ifcopenshell.api.control.assign_control(self.file, relating_control=control, related_objects=[wall])
|
||||
assert relation is None
|
||||
|
||||
# assigning same control to another object
|
||||
wall1 = self.file.createIfcWall()
|
||||
relation = ifcopenshell.api.control.assign_control(self.file, relating_control=control, related_object=wall1)
|
||||
relation = ifcopenshell.api.control.assign_control(self.file, relating_control=control, related_objects=[wall1])
|
||||
assert relation is not None
|
||||
assert len(self.file.by_type("IfcRelAssignsToControl")) == 1
|
||||
assert relation.RelatingControl == control
|
||||
assert set(relation.RelatedObjects) == set((wall, wall1))
|
||||
|
||||
def test_batch_assignment(self):
|
||||
walls = [self.file.createIfcWall() for _ in range(5)]
|
||||
control = ifcopenshell.api.cost.add_cost_schedule(self.file)
|
||||
relation = ifcopenshell.api.control.assign_control(self.file, relating_control=control, related_objects=walls)
|
||||
assert relation
|
||||
assert len(self.file.by_type("IfcRelAssignsToControl")) == 1
|
||||
assert relation.RelatingControl == control
|
||||
assert set(relation.RelatedObjects) == set(walls)
|
||||
|
||||
|
||||
class TestAssignControlIFC2X3(test.bootstrap.IFC2X3, TestAssignControl):
|
||||
pass
|
||||
|
||||
@@ -27,15 +27,17 @@ class TestUnassignControl(test.bootstrap.IFC4):
|
||||
control = ifcopenshell.api.cost.add_cost_schedule(self.file)
|
||||
|
||||
# assign and unassign
|
||||
relation = ifcopenshell.api.control.assign_control(self.file, relating_control=control, related_object=wall)
|
||||
ifcopenshell.api.control.unassign_control(self.file, relating_control=control, related_object=wall)
|
||||
relation = ifcopenshell.api.control.assign_control(self.file, relating_control=control, related_objects=[wall])
|
||||
assert relation
|
||||
ifcopenshell.api.control.unassign_control(self.file, relating_control=control, related_objects=[wall])
|
||||
assert len(self.file.by_type("IfcRelAssignsToControl")) == 0
|
||||
|
||||
# 1 control 2 related objects
|
||||
wall1 = self.file.createIfcWall()
|
||||
relation = ifcopenshell.api.control.assign_control(self.file, relating_control=control, related_object=wall)
|
||||
ifcopenshell.api.control.assign_control(self.file, relating_control=control, related_object=wall1)
|
||||
ifcopenshell.api.control.unassign_control(self.file, relating_control=control, related_object=wall1)
|
||||
relation = ifcopenshell.api.control.assign_control(self.file, relating_control=control, related_objects=[wall])
|
||||
assert relation
|
||||
ifcopenshell.api.control.assign_control(self.file, relating_control=control, related_objects=[wall1])
|
||||
ifcopenshell.api.control.unassign_control(self.file, relating_control=control, related_objects=[wall1])
|
||||
assert len(self.file.by_type("IfcRelAssignsToControl")) == 1
|
||||
assert relation.RelatedObjects == (wall,)
|
||||
|
||||
|
||||
@@ -30,7 +30,7 @@ class TestAddCostItemQuantity(test.bootstrap.IFC4):
|
||||
schedule = ifcopenshell.api.cost.add_cost_schedule(self.file)
|
||||
item = ifcopenshell.api.cost.add_cost_item(self.file, cost_schedule=schedule)
|
||||
wall = ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcWall")
|
||||
ifcopenshell.api.control.assign_control(self.file, relating_control=item, related_object=wall)
|
||||
ifcopenshell.api.control.assign_control(self.file, relating_control=item, related_objects=[wall])
|
||||
|
||||
quantities = []
|
||||
for quantity_type in quantity_types:
|
||||
|
||||
@@ -95,7 +95,7 @@ class TestEditTaskTime(test.bootstrap.IFC4):
|
||||
ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcProject")
|
||||
calendar = ifcopenshell.api.sequence.add_work_calendar(self.file)
|
||||
task = self.file.createIfcTask()
|
||||
ifcopenshell.api.control.assign_control(self.file, relating_control=calendar, related_object=task)
|
||||
ifcopenshell.api.control.assign_control(self.file, relating_control=calendar, related_objects=[task])
|
||||
task_time = ifcopenshell.api.sequence.add_task_time(self.file, task=task)
|
||||
ifcopenshell.api.sequence.edit_task_time(
|
||||
self.file,
|
||||
@@ -195,7 +195,7 @@ class TestEditTaskTime(test.bootstrap.IFC4):
|
||||
ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcProject")
|
||||
calendar = ifcopenshell.api.sequence.add_work_calendar(self.file)
|
||||
task = self.file.createIfcTask()
|
||||
ifcopenshell.api.control.assign_control(self.file, relating_control=calendar, related_object=task)
|
||||
ifcopenshell.api.control.assign_control(self.file, relating_control=calendar, related_objects=[task])
|
||||
task_time = ifcopenshell.api.sequence.add_task_time(self.file, task=task)
|
||||
ifcopenshell.api.sequence.edit_task_time(
|
||||
self.file,
|
||||
|
||||
@@ -38,7 +38,7 @@ class TestRemoveWorkCalendar(test.bootstrap.IFC4):
|
||||
|
||||
# Assign tasks.
|
||||
task = ifcopenshell.api.sequence.add_task(self.file)
|
||||
ifcopenshell.api.control.assign_control(self.file, work_calendar, task)
|
||||
ifcopenshell.api.control.assign_control(self.file, work_calendar, [task])
|
||||
|
||||
ifcopenshell.api.sequence.remove_work_calendar(self.file, work_calendar)
|
||||
|
||||
|
||||
@@ -16,19 +16,39 @@
|
||||
# You should have received a copy of the GNU Lesser General Public License
|
||||
# along with IfcOpenShell. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import ifcopenshell.api.cost
|
||||
import ifcopenshell.api.root
|
||||
import test.bootstrap
|
||||
import ifcopenshell.api
|
||||
import ifcopenshell.api.control
|
||||
import ifcopenshell.api.sequence
|
||||
import ifcopenshell.util.element
|
||||
from datetime import datetime
|
||||
from typing import Union
|
||||
|
||||
|
||||
def deprecation_check(test):
|
||||
def new_test(self):
|
||||
assert datetime.now().date() < datetime(2024, 8, 1).date(), "API arguments are completely deprecated"
|
||||
assert datetime.now().date() < datetime(2026, 1, 9).date(), "API arguments are completely deprecated"
|
||||
test(self)
|
||||
|
||||
return new_test
|
||||
|
||||
|
||||
class TestTemporarySupportForDeprecatedAPIArguments(test.bootstrap.IFC4):
|
||||
pass
|
||||
@deprecation_check
|
||||
def test_assigning_control(self):
|
||||
model = self.file
|
||||
element = ifcopenshell.api.root.create_entity(model, ifc_class="IfcWall")
|
||||
control = ifcopenshell.api.cost.add_cost_schedule(model)
|
||||
ifcopenshell.api.control.assign_control(model, relating_control=control, related_objects=[element])
|
||||
assert list(ifcopenshell.util.element.get_controls(element)) == [control]
|
||||
|
||||
@deprecation_check
|
||||
def test_unassigning_control(self):
|
||||
TestTemporarySupportForDeprecatedAPIArguments.test_assigning_control(self)
|
||||
model = self.file
|
||||
element = model.by_type("IfcWall")[0]
|
||||
control = model.by_type("IfcCostSchedule")[0]
|
||||
ifcopenshell.api.control.unassign_control(model, relating_control=control, related_objects=[element])
|
||||
assert list(ifcopenshell.util.element.get_controls(element)) == []
|
||||
|
||||
@@ -145,14 +145,20 @@ class TestAssignType(test.bootstrap.IFC4):
|
||||
This is because the type will have its own PredefinedType, and the element's PredefinedType
|
||||
will conflict with it. (See #7006)
|
||||
"""
|
||||
is_ifc2x3 = self.file.schema == "IFC2X3"
|
||||
element_type = ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcWallType")
|
||||
element_type.PredefinedType = "MOVABLE"
|
||||
element_type.PredefinedType = "POLYGONAL"
|
||||
|
||||
element = ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcWall")
|
||||
element.PredefinedType = "USERDEFINED"
|
||||
if not is_ifc2x3:
|
||||
# In IFC2X3, there seems to be no example when both type and occurence have PredefinedType.
|
||||
# So we just ignore it.
|
||||
element.PredefinedType = "USERDEFINED"
|
||||
element.ObjectType = "Test"
|
||||
ifcopenshell.api.type.assign_type(self.file, related_objects=[element], relating_type=element_type)
|
||||
assert element.PredefinedType is None
|
||||
|
||||
if not is_ifc2x3:
|
||||
assert element.PredefinedType is None
|
||||
assert element.ObjectType is None
|
||||
|
||||
def test_keep_predefined_type_if_type_assignment_is_notdefined(self):
|
||||
@@ -160,16 +166,26 @@ class TestAssignType(test.bootstrap.IFC4):
|
||||
if an element has a PredefinedType, it will be removed when assigning a type.(See #7006)
|
||||
This behavior needs to be blocked if the PredefinedType of the typing Entity is set to "NOTDEFINED". (See #7011)
|
||||
"""
|
||||
is_ifc2x3 = self.file.schema == "IFC2X3"
|
||||
element_type = ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcWallType")
|
||||
element_type.PredefinedType = "NOTDEFINED"
|
||||
|
||||
element = ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcWall")
|
||||
element.PredefinedType = "USERDEFINED"
|
||||
if not is_ifc2x3:
|
||||
# In IFC2X3, there seems to be no example when both type and occurence have PredefinedType.
|
||||
# So we just ignore it.
|
||||
element.PredefinedType = "USERDEFINED"
|
||||
element.ObjectType = "Test"
|
||||
ifcopenshell.api.type.assign_type(self.file, related_objects=[element], relating_type=element_type)
|
||||
assert element.PredefinedType == "USERDEFINED"
|
||||
|
||||
if not is_ifc2x3:
|
||||
assert element.PredefinedType == "USERDEFINED"
|
||||
assert element.ObjectType == "Test"
|
||||
|
||||
|
||||
class TestAssignTypeIFC2X3(test.bootstrap.IFC2X3, TestAssignType):
|
||||
pass
|
||||
|
||||
|
||||
class TestAssignTypeIFC4X3(test.bootstrap.IFC4X3, TestAssignType):
|
||||
pass
|
||||
|
||||
@@ -47,15 +47,20 @@ class TestPackageSupportedPlatforms:
|
||||
response = conn.getresponse()
|
||||
build_html = response.read().decode("utf-8")
|
||||
|
||||
def find_make_var(var_name: str) -> str:
|
||||
line = next(l for l in text.splitlines() if l.startswith(f"{var_name}:="))
|
||||
return line.partition(":=")[2]
|
||||
|
||||
BINARY_VERSION = find_make_var("BINARY_VERSION")
|
||||
URL_TYPES = ("IOS_URL", "IFCCONVERT_URL")
|
||||
|
||||
missing_urls: set[str] = set()
|
||||
for url_type in URL_TYPES:
|
||||
line = next(l for l in text.splitlines() if l.startswith(f"{url_type}:="))
|
||||
_, _, url_template = line.partition(":=")
|
||||
url_template = find_make_var(url_type)
|
||||
url_template = url_template.replace("$(", "{").replace(")", "}")
|
||||
for platform in SUPPORTED_PLATFORMS:
|
||||
for pyver in SUPPORTED_PY_VERSIONS:
|
||||
url = url_template.format(PYNUMBER=pyver, PLATFORM=platform)
|
||||
url = url_template.format(PYNUMBER=pyver, PLATFORM=platform, BINARY_VERSION=BINARY_VERSION)
|
||||
if url not in build_html:
|
||||
missing_urls.add(url)
|
||||
|
||||
|
||||
@@ -16,6 +16,8 @@
|
||||
# You should have received a copy of the GNU Lesser General Public License
|
||||
# along with IfcOpenShell. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import ifcopenshell.api.control
|
||||
import ifcopenshell.api.cost
|
||||
import ifcopenshell.api.profile
|
||||
import pytest
|
||||
import test.bootstrap
|
||||
@@ -926,6 +928,23 @@ class TestGetGroupsIFC2X3(test.bootstrap.IFC2X3, TestGetGroupsIFC4):
|
||||
pass
|
||||
|
||||
|
||||
class TestGetControls(test.bootstrap.IFC2X3):
|
||||
def test_run(self):
|
||||
model = self.file
|
||||
element = ifcopenshell.api.root.create_entity(model, ifc_class="IfcWall")
|
||||
control = ifcopenshell.api.cost.add_cost_schedule(model)
|
||||
ifcopenshell.api.control.assign_control(model, related_objects=[element], relating_control=control)
|
||||
assert list(subject.get_controls(element)) == [control]
|
||||
|
||||
|
||||
class TestGetControlsIFC4(test.bootstrap.IFC4, TestGetControls):
|
||||
pass
|
||||
|
||||
|
||||
class TestGetControlsIFC4X3(test.bootstrap.IFC4X3, TestGetControls):
|
||||
pass
|
||||
|
||||
|
||||
class TestGetAggregateIFC4(test.bootstrap.IFC4):
|
||||
def test_getting_the_containing_aggregate_of_a_subelement(self):
|
||||
element = ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcWall")
|
||||
|
||||
@@ -468,3 +468,29 @@ class TestAngle2YAxis(test.bootstrap.IFC4):
|
||||
assert np.allclose(subject.angle2yaxis(45), (-a, a))
|
||||
assert np.allclose(subject.angle2yaxis(-135), (a, -a))
|
||||
assert np.allclose(subject.angle2yaxis(135), (-a, -a))
|
||||
|
||||
|
||||
class TestDMS2DDandDD2DMS(test.bootstrap.IFC4):
|
||||
def test_dms2dd_and_dd2dms(self):
|
||||
test_cases_3tuple = [
|
||||
(35.41, (35, 24, 36.0)),
|
||||
(-116.89, (-116, -53, -24.0)),
|
||||
]
|
||||
test_cases_4tuple = [
|
||||
(40.431389, (40, 25, 53, 400)),
|
||||
(-4.248056, (-4, -14, -53, -1600)),
|
||||
(-35.401389, (-35, -24, -5, -400)),
|
||||
(148.981667, (148, 58, 54, 1200)),
|
||||
]
|
||||
|
||||
for dd, dms in test_cases_3tuple:
|
||||
d, m, s = subject.dd2dms(dd)
|
||||
assert (d, m, s) == dms
|
||||
dd_converted = subject.dms2dd(dms[0], dms[1], dms[2])
|
||||
assert dd_converted == dd
|
||||
|
||||
for dd, dms in test_cases_4tuple:
|
||||
d, m, s, us = subject.dd2dms(dd, use_us=True)
|
||||
assert (d, m, s, us) == dms
|
||||
dd_converted = subject.dms2dd(dms[0], dms[1], dms[2], dms[3])
|
||||
assert dd_converted == dd
|
||||
|
||||
@@ -33,6 +33,7 @@ import ifcopenshell.ifcopenshell_wrapper as W
|
||||
import ifcopenshell.util.attribute
|
||||
import ifcopenshell.util.element
|
||||
import ifcopenshell.util.placement
|
||||
import ifcopenshell.util.representation
|
||||
import ifcopenshell.util.schema
|
||||
import ifcopenshell.util.shape
|
||||
import ifcopenshell.util.unit
|
||||
@@ -140,7 +141,7 @@ class Patcher(ifcpatch.BasePatcher):
|
||||
self.should_skip_geometry_data = should_skip_geometry_data
|
||||
|
||||
geometry_rows: dict[str, tuple[str, bytes, bytes, bytes, bytes, str]]
|
||||
shape_rows: dict[int, tuple[int, list[float], list[float], list[float], bytes, str]]
|
||||
shape_rows: dict[int, tuple[int, float, float, float, bytes, Union[str, None]]]
|
||||
|
||||
def get_output(self) -> Union[str, None]:
|
||||
"""Return resulting database filepath for sqlite and ``None`` for mysql."""
|
||||
@@ -249,29 +250,29 @@ class Patcher(ifcpatch.BasePatcher):
|
||||
self.geometry_rows = {}
|
||||
|
||||
if self.file.schema in ("IFC2X3", "IFC4"):
|
||||
self.elements = self.file.by_type("IfcElement") + self.file.by_type("IfcProxy")
|
||||
elements = self.file.by_type("IfcElement") + self.file.by_type("IfcProxy")
|
||||
else:
|
||||
self.elements = self.file.by_type("IfcElement")
|
||||
elements = self.file.by_type("IfcElement")
|
||||
|
||||
self.settings = ifcopenshell.geom.settings()
|
||||
self.settings.set("apply-default-materials", False)
|
||||
|
||||
self.body_contexts = [
|
||||
body_contexts = [
|
||||
c.id()
|
||||
for c in self.file.by_type("IfcGeometricRepresentationSubContext")
|
||||
if c.ContextIdentifier in ["Body", "Facetation"]
|
||||
]
|
||||
# Ideally, all representations should be in a subcontext, but some BIM programs don't do this correctly
|
||||
self.body_contexts.extend(
|
||||
body_contexts.extend(
|
||||
[
|
||||
c.id()
|
||||
for c in self.file.by_type("IfcGeometricRepresentationContext", include_subtypes=False)
|
||||
if c.ContextType == "Model"
|
||||
]
|
||||
)
|
||||
self.settings.set("context-ids", self.body_contexts)
|
||||
self.settings.set("context-ids", body_contexts)
|
||||
|
||||
products = self.elements
|
||||
products = elements
|
||||
iterator = ifcopenshell.geom.iterator(self.settings, self.file, multiprocessing.cpu_count(), include=products)
|
||||
valid_file = iterator.initialize()
|
||||
if not valid_file:
|
||||
@@ -301,12 +302,7 @@ class Patcher(ifcpatch.BasePatcher):
|
||||
geometry = shape.geometry
|
||||
geometry_id = geometry.id
|
||||
if geometry_id not in self.geometry_rows:
|
||||
v = geometry.verts_buffer
|
||||
e = geometry.edges_buffer
|
||||
f = geometry.faces_buffer
|
||||
mids = geometry.material_ids_buffer
|
||||
m = json.dumps([m.instance_id() for m in geometry.materials])
|
||||
self.geometry_rows[geometry_id] = (geometry_id, v, e, f, mids, m)
|
||||
self.add_geometry_row(geometry_id, geometry)
|
||||
# Copy required since otherwise it is read-only
|
||||
m = ifcopenshell.util.shape.get_shape_matrix(shape).copy()
|
||||
m[:3, 3] /= self.unit_scale
|
||||
@@ -315,6 +311,37 @@ class Patcher(ifcpatch.BasePatcher):
|
||||
if not iterator.next():
|
||||
break
|
||||
|
||||
# Process element types geometry.
|
||||
element_types = self.file.by_type("IfcElementType")
|
||||
body_contexts = [self.file.by_id(i) for i in body_contexts]
|
||||
m_bytes = np.eye(4, dtype=np.float64).tobytes()
|
||||
for element_type in element_types:
|
||||
representation = None
|
||||
for context in body_contexts:
|
||||
representation = ifcopenshell.util.representation.get_representation(element_type, context)
|
||||
if representation:
|
||||
break
|
||||
geometry_id = None
|
||||
if representation:
|
||||
geometry_id_ = str(representation.id())
|
||||
if geometry_id_ in self.geometry_rows:
|
||||
geometry_id = geometry_id_
|
||||
elif geometry := ifcopenshell.geom.create_shape(self.settings, representation):
|
||||
geometry_id = geometry_id_
|
||||
assert isinstance(geometry, W.Triangulation)
|
||||
self.add_geometry_row(geometry_id, geometry)
|
||||
|
||||
shape_id = element_type.id()
|
||||
self.shape_rows[shape_id] = (shape_id, *(0.0, 0.0, 0.0), m_bytes, geometry_id)
|
||||
|
||||
def add_geometry_row(self, geometry_id: str, geometry: W.Triangulation) -> None:
|
||||
v = geometry.verts_buffer
|
||||
e = geometry.edges_buffer
|
||||
f = geometry.faces_buffer
|
||||
mids = geometry.material_ids_buffer
|
||||
m = json.dumps([m.instance_id() for m in geometry.materials])
|
||||
self.geometry_rows[geometry_id] = (geometry_id, v, e, f, mids, m)
|
||||
|
||||
def check_existing_ifc_database(self) -> None:
|
||||
if self.sql_type == "sqlite":
|
||||
cursor = self.c.execute("SELECT 1 FROM sqlite_master WHERE type='table' AND name='id_map'")
|
||||
@@ -576,7 +603,7 @@ class Patcher(ifcpatch.BasePatcher):
|
||||
if element.id() not in self.shape_rows and (placement := getattr(element, "ObjectPlacement", None)):
|
||||
m = ifcopenshell.util.placement.get_local_placement(placement)
|
||||
x, y, z = m[:, 3][0:3].tolist()
|
||||
self.shape_rows[element.id()] = [element.id(), x, y, z, m.tobytes(), None]
|
||||
self.shape_rows[element.id()] = (element.id(), x, y, z, m.tobytes(), None)
|
||||
|
||||
if self.sql_type == "sqlite":
|
||||
if rows:
|
||||
|
||||
@@ -21,6 +21,7 @@ import numpy as np
|
||||
import ifcopenshell
|
||||
import ifcopenshell.util.placement
|
||||
import typing
|
||||
from ifcopenshell.util.shape_builder import ShapeBuilder
|
||||
|
||||
|
||||
class Patcher:
|
||||
@@ -97,6 +98,7 @@ class Patcher:
|
||||
self.ax = None
|
||||
self.ay = None
|
||||
self.az = None
|
||||
self.builder = ShapeBuilder(file)
|
||||
|
||||
try:
|
||||
self.ax = float(ax)
|
||||
@@ -183,15 +185,4 @@ class Patcher:
|
||||
z = np.array((m[0][2], m[1][2], m[2][2]))
|
||||
o = np.array((m[0][3], m[1][3], m[2][3]))
|
||||
object_matrix = ifcopenshell.util.placement.a2p(o, z, x)
|
||||
return self.create_ifc_axis_2_placement_3d(
|
||||
object_matrix[:, 3][0:3],
|
||||
object_matrix[:, 2][0:3],
|
||||
object_matrix[:, 0][0:3],
|
||||
)
|
||||
|
||||
def create_ifc_axis_2_placement_3d(self, point, up, forward):
|
||||
return self.file.createIfcAxis2Placement3D(
|
||||
self.file.createIfcCartesianPoint(point.tolist()),
|
||||
self.file.createIfcDirection(up.tolist()),
|
||||
self.file.createIfcDirection(forward.tolist()),
|
||||
)
|
||||
return self.builder.create_axis2_placement_3d_from_matrix(object_matrix)
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
import ifcopenshell
|
||||
import ifcopenshell.api.alignment
|
||||
import ifcopenshell.util.element
|
||||
import ifcpatch
|
||||
from logging import Logger
|
||||
|
||||
import typing
|
||||
from typing import Union
|
||||
|
||||
|
||||
class Patcher(ifcpatch.BasePatcher):
|
||||
def __init__(self, file: ifcopenshell.file, logger: Union[Logger, None] = None):
|
||||
"""Adds a zero length segments to alignment layouts
|
||||
|
||||
Example:
|
||||
|
||||
.. code:: python
|
||||
|
||||
model = ifcpatch.execute({"input": "input.ifc", "file": model, "recipe": "PatchStationReferentPosition"})
|
||||
"""
|
||||
super().__init__(file, logger)
|
||||
self.file_patched: ifcopenshell.file
|
||||
|
||||
def patch(self):
|
||||
patched_file = ifcopenshell.file.from_string(self.file.wrapped_data.to_string())
|
||||
|
||||
alignments = patched_file.by_type("IfcAlignment")
|
||||
for alignment in alignments:
|
||||
basis_curve = ifcopenshell.api.alignment.get_basis_curve(alignment)
|
||||
|
||||
nests = alignment.IsNestedBy
|
||||
first_referent = nests[1].RelatedObjects[0]
|
||||
start_station = ifcopenshell.util.element.get_pset(first_referent,"Pset_Stationing","Station")
|
||||
#start_station = first_referent.IsDefinedBy[0].RelatingPropertyDefinition.HasProperties[0].NominalValue.wrapped_data # get station from first_referent
|
||||
for referent in nests[1].RelatedObjects:
|
||||
if referent.ObjectPlacement == None:
|
||||
# Need to get Station property from Pset_Stationing + Station property from the first referent... the DistanceAlong is the different in these values
|
||||
station = ifcopenshell.util.element.get_pset(referent,"Pset_Stationing","Station")
|
||||
#station = referent.IsDefinedBy[0].RelatingPropertyDefinition.HasProperties[0].NominalValue.wrapped_data # get station from current referent
|
||||
object_placement = patched_file.createIfcLinearPlacement(
|
||||
RelativePlacement=patched_file.createIfcAxis2PlacementLinear(
|
||||
Location=patched_file.createIfcPointByDistanceExpression(
|
||||
DistanceAlong=patched_file.createIfcLengthMeasure(station - start_station),
|
||||
OffsetLateral=None,
|
||||
OffsetVertical=None,
|
||||
OffsetLongitudinal=None,
|
||||
BasisCurve=basis_curve,
|
||||
)
|
||||
),
|
||||
)
|
||||
|
||||
referent.ObjectPlacement = object_placement
|
||||
|
||||
self.file_patched = patched_file
|
||||
@@ -19,6 +19,77 @@
|
||||
PACKAGE_NAME:=ifctester
|
||||
include ../common.mk
|
||||
|
||||
NODE_ENV ?= production
|
||||
WEBAPP_DIR := webapp
|
||||
WEBAPP_BUILD_DIR := $(WEBAPP_DIR)/dist
|
||||
PYODIDE_DIR := $(WEBAPP_DIR)/public/pyodide
|
||||
PYODIDE_VERSION := 0.28.0
|
||||
PYODIDE_URL := https://github.com/pyodide/pyodide/releases/download/$(PYODIDE_VERSION)/pyodide-$(PYODIDE_VERSION).tar.bz2
|
||||
|
||||
.PHONY: webapp-dev
|
||||
webapp-dev:
|
||||
cd $(WEBAPP_DIR) && npm run dev
|
||||
|
||||
.PHONY: pyodide-download
|
||||
pyodide-download:
|
||||
@if [ -d "$(PYODIDE_DIR)" ]; then \
|
||||
echo "Pyodide directory already exists at $(PYODIDE_DIR), skipping download"; \
|
||||
else \
|
||||
echo "Downloading and preparing Pyodide $(PYODIDE_VERSION)..."; \
|
||||
mkdir -p $(PYODIDE_DIR)/tmp; \
|
||||
curl -L -o $(PYODIDE_DIR)/tmp/pyodide-$(PYODIDE_VERSION).tar.bz2 $(PYODIDE_URL); \
|
||||
cd $(PYODIDE_DIR)/tmp && tar -xjf pyodide-$(PYODIDE_VERSION).tar.bz2 && cd -; \
|
||||
cp $(PYODIDE_DIR)/tmp/pyodide/pyodide.asm.js $(PYODIDE_DIR)/; \
|
||||
cp $(PYODIDE_DIR)/tmp/pyodide/pyodide.asm.wasm $(PYODIDE_DIR)/; \
|
||||
cp $(PYODIDE_DIR)/tmp/pyodide/pyodide.mjs $(PYODIDE_DIR)/; \
|
||||
cp $(PYODIDE_DIR)/tmp/pyodide/pyodide.mjs.map $(PYODIDE_DIR)/; \
|
||||
cp $(PYODIDE_DIR)/tmp/pyodide/python_stdlib.zip $(PYODIDE_DIR)/; \
|
||||
cp $(PYODIDE_DIR)/tmp/pyodide/pyodide-lock.json $(PYODIDE_DIR)/; \
|
||||
cp $(PYODIDE_DIR)/tmp/pyodide/certifi-*-py3-none-any.whl $(PYODIDE_DIR)/ 2>/dev/null || true; \
|
||||
cp $(PYODIDE_DIR)/tmp/pyodide/charset_normalizer-*-py3-none-any.whl $(PYODIDE_DIR)/ 2>/dev/null || true; \
|
||||
cp $(PYODIDE_DIR)/tmp/pyodide/idna-*-py3-none-any.whl $(PYODIDE_DIR)/ 2>/dev/null || true; \
|
||||
cp $(PYODIDE_DIR)/tmp/pyodide/micropip-*-py3-none-any.whl $(PYODIDE_DIR)/ 2>/dev/null || true; \
|
||||
cp $(PYODIDE_DIR)/tmp/pyodide/numpy-*-cp313-cp313-pyodide_2025_0_wasm32.whl $(PYODIDE_DIR)/ 2>/dev/null || true; \
|
||||
cp $(PYODIDE_DIR)/tmp/pyodide/python_dateutil-*-py2.py3-none-any.whl $(PYODIDE_DIR)/ 2>/dev/null || true; \
|
||||
cp $(PYODIDE_DIR)/tmp/pyodide/requests-*-py3-none-any.whl $(PYODIDE_DIR)/ 2>/dev/null || true; \
|
||||
cp $(PYODIDE_DIR)/tmp/pyodide/shapely-*-cp313-cp313-pyodide_2025_0_wasm32.whl $(PYODIDE_DIR)/ 2>/dev/null || true; \
|
||||
cp $(PYODIDE_DIR)/tmp/pyodide/six-*-py2.py3-none-any.whl $(PYODIDE_DIR)/ 2>/dev/null || true; \
|
||||
cp $(PYODIDE_DIR)/tmp/pyodide/typing_extensions-*-py3-none-any.whl $(PYODIDE_DIR)/ 2>/dev/null || true; \
|
||||
cp $(PYODIDE_DIR)/tmp/pyodide/urllib3-*-py3-none-any.whl $(PYODIDE_DIR)/ 2>/dev/null || true; \
|
||||
rm -rf /tmp/pyodide; \
|
||||
rm -f /tmp/pyodide-$(PYODIDE_VERSION).tar.bz2; \
|
||||
echo "Pyodide $(PYODIDE_VERSION) prepared in $(PYODIDE_DIR)"; \
|
||||
fi
|
||||
|
||||
.PHONY: webapp-build
|
||||
webapp-build: pyodide-download
|
||||
cd $(WEBAPP_DIR) && npm install
|
||||
cd $(WEBAPP_DIR) && npm run build
|
||||
|
||||
.PHONY: webapp-serve
|
||||
webapp-serve: webapp-build
|
||||
cd $(WEBAPP_DIR) && $(PYTHON) serve.py
|
||||
|
||||
.PHONY: clean
|
||||
clean:
|
||||
rm -rf $(WEBAPP_BUILD_DIR)
|
||||
rm -rf $(WEBAPP_DIR)/node_modules
|
||||
rm -rf $(PYODIDE_DIR)
|
||||
rm -rf $(PACKAGE_NAME)/webapp
|
||||
rm -rf dist
|
||||
|
||||
.PHONY: dist
|
||||
dist: webapp-prepare
|
||||
$(MAKE) -f ../common.mk dist PACKAGE_NAME=$(PACKAGE_NAME)
|
||||
|
||||
.PHONY: webapp-prepare
|
||||
webapp-prepare: webapp-build
|
||||
rm -rf $(PACKAGE_NAME)/webapp/www/*
|
||||
mkdir -p $(PACKAGE_NAME)/webapp/www
|
||||
cp -r $(WEBAPP_BUILD_DIR)/* $(PACKAGE_NAME)/webapp/www/
|
||||
cp $(WEBAPP_DIR)/__init__.py $(PACKAGE_NAME)/webapp/__init__.py
|
||||
cp $(WEBAPP_DIR)/serve.py $(PACKAGE_NAME)/webapp/serve.py
|
||||
|
||||
.PHONY: test
|
||||
test:
|
||||
pytest -p no:pytest-blender test
|
||||
|
||||
@@ -15,11 +15,10 @@ classifiers = [
|
||||
"Programming Language :: Python :: 3",
|
||||
"License :: OSI Approved :: GNU Lesser General Public License v3 or later (LGPLv3+)",
|
||||
]
|
||||
dependencies = ["ifcopenshell", "python-dateutil", "xmlschema", "numpy", "odfpy", "pystache", "bcf-client"]
|
||||
dependencies = ["ifcopenshell", "python-dateutil", "xmlschema", "numpy", "odfpy", "pystache", "bcf-client", "flask"]
|
||||
|
||||
[project.optional-dependencies]
|
||||
advanced = [
|
||||
"flask",
|
||||
]
|
||||
|
||||
[project.urls]
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
.DS_Store
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
|
||||
.claude
|
||||
experiment/*
|
||||
@@ -0,0 +1,2 @@
|
||||
# IfcTester (Next)
|
||||
The "next" version of IDS authoring and auditing on the web.
|
||||
@@ -0,0 +1,3 @@
|
||||
from .serve import app
|
||||
|
||||
__all__ = ["app"]
|
||||
@@ -1,83 +0,0 @@
|
||||
# IfcTester - IDS based model auditing
|
||||
# Copyright (C) 2022 Dion Moult <dion@thinkmoult.com>
|
||||
#
|
||||
# This file is part of IfcTester.
|
||||
#
|
||||
# IfcTester 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.
|
||||
#
|
||||
# IfcTester 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 IfcTester. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
|
||||
import os
|
||||
import time
|
||||
import ifctester
|
||||
import ifctester.reporter
|
||||
import ifcopenshell
|
||||
import ifcopenshell.guid
|
||||
from flask import Flask, request, send_from_directory
|
||||
|
||||
app = Flask(__name__)
|
||||
|
||||
|
||||
class Ifc:
|
||||
ifc = None
|
||||
filepath = None
|
||||
|
||||
@classmethod
|
||||
def get(cls, filepath=None):
|
||||
if filepath is None or filepath == cls.filepath:
|
||||
return cls.ifc
|
||||
cls.filepath = filepath
|
||||
cls.ifc = ifcopenshell.open(filepath)
|
||||
return cls.ifc
|
||||
|
||||
|
||||
@app.route("/")
|
||||
def index():
|
||||
with open("www/index.html") as template:
|
||||
return template.read()
|
||||
|
||||
|
||||
@app.route("/<path:asset>.<string:ext>")
|
||||
def get_asset(asset, ext):
|
||||
if ext in ("js", "css"):
|
||||
return send_from_directory("www", asset + "." + ext)
|
||||
|
||||
|
||||
@app.route("/audit", methods=["POST"])
|
||||
def audit():
|
||||
filename = ifcopenshell.guid.new()
|
||||
ids_filepath = os.path.join("uploads", filename + ".ids")
|
||||
ifc_filepath = os.path.join("uploads", filename + ".ifc")
|
||||
os.makedirs("uploads", exist_ok=True)
|
||||
request.files.get("ids").save(ids_filepath)
|
||||
request.files.get("ifc").save(ifc_filepath)
|
||||
|
||||
start = time.time()
|
||||
specs = ifctester.open(ids_filepath)
|
||||
ifc = Ifc.get(ifc_filepath)
|
||||
print("Finished loading:", time.time() - start)
|
||||
start = time.time()
|
||||
specs.validate(ifc)
|
||||
print("Finished validating:", time.time() - start)
|
||||
start = time.time()
|
||||
|
||||
os.remove(ids_filepath)
|
||||
os.remove(ifc_filepath)
|
||||
|
||||
engine = ifctester.reporter.Json(specs)
|
||||
engine.report()
|
||||
return engine.to_string()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
app.run(debug=False)
|
||||
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"$schema": "https://shadcn-svelte.com/schema.json",
|
||||
"tailwind": {
|
||||
"css": "src/css/app.scss",
|
||||
"baseColor": "gray"
|
||||
},
|
||||
"aliases": {
|
||||
"components": "$lib/components",
|
||||
"utils": "$lib/utils",
|
||||
"ui": "$lib/components/ui",
|
||||
"hooks": "$lib/hooks",
|
||||
"lib": "$lib"
|
||||
},
|
||||
"typescript": false,
|
||||
"registry": "https://shadcn-svelte.com/registry"
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<title>IFC Tester</title>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="color-scheme" content="dark" />
|
||||
<link rel="icon" type="image/svg+xml" href="/logo.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,39 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"moduleResolution": "bundler",
|
||||
"target": "ESNext",
|
||||
"module": "ESNext",
|
||||
/**
|
||||
* svelte-preprocess cannot figure out whether you have
|
||||
* a value or a type, so tell TypeScript to enforce using
|
||||
* `import type` instead of `import` for Types.
|
||||
*/
|
||||
"verbatimModuleSyntax": true,
|
||||
"isolatedModules": true,
|
||||
"resolveJsonModule": true,
|
||||
/**
|
||||
* To have warnings / errors of the Svelte compiler at the
|
||||
* correct position, enable source maps by default.
|
||||
*/
|
||||
"sourceMap": true,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
/**
|
||||
* Typecheck JS in `.svelte` and `.js` files by default.
|
||||
* Disable this if you'd like to use dynamic types.
|
||||
*/
|
||||
"checkJs": false,
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"$lib": ["./src/lib"],
|
||||
"$lib/*": ["./src/lib/*"],
|
||||
"$src": ["./src"],
|
||||
"$src/*": ["./src/*"]
|
||||
}
|
||||
},
|
||||
/**
|
||||
* Use global.d.ts instead of compilerOptions.types
|
||||
* to avoid limiting type declarations.
|
||||
*/
|
||||
"include": ["src/**/*.d.ts", "src/**/*.js", "src/**/*.svelte"]
|
||||
}
|
||||
Generated
+3023
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,37 @@
|
||||
{
|
||||
"name": "ifctester-next",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview",
|
||||
"deploy": "npm run build && npx wrangler pages deploy dist"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@internationalized/date": "^3.8.1",
|
||||
"@lucide/svelte": "^0.515.0",
|
||||
"@sveltejs/vite-plugin-svelte": "^5.0.3",
|
||||
"@tailwindcss/vite": "^4.0.0",
|
||||
"bits-ui": "^2.8.10",
|
||||
"clsx": "^2.1.1",
|
||||
"mode-watcher": "^1.1.0",
|
||||
"sass-embedded": "^1.89.0",
|
||||
"svelte": "^5.28.1",
|
||||
"svelte-sonner": "^1.0.5",
|
||||
"tailwind-merge": "^3.3.0",
|
||||
"tailwind-variants": "^1.0.0",
|
||||
"tailwindcss": "^4.0.0",
|
||||
"tw-animate-css": "^1.3.2",
|
||||
"vite": "^6.3.5"
|
||||
},
|
||||
"dependencies": {
|
||||
"eventemitter3": "^5.0.1",
|
||||
"hyperid": "^3.3.0",
|
||||
"lucide-svelte": "^0.542.0",
|
||||
"socket.io-client": "^4.8.1",
|
||||
"svelecte": "^5.2.0",
|
||||
"svelte-spa-router": "^4.0.1"
|
||||
}
|
||||
}
|
||||
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,63 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="800" height="783.031" viewBox="0 0 800 783.031" xmlns:xlink="http://www.w3.org/1999/xlink" role="img" artist="Katerina Limpitsouni" source="https://undraw.co/">
|
||||
<g id="Group_178" data-name="Group 178" transform="translate(-656 -242)">
|
||||
<g id="Group_177" data-name="Group 177" transform="translate(656 242)">
|
||||
<path id="Path_2918-215" data-name="Path 2918" d="M914.2,382.533q-.65-3.406-1.369-6.789c-1.077-5.056-2.259-10.1-3.535-15.1-.632-2.528-1.311-5.056-2-7.573v-.012A392.052,392.052,0,0,0,545.043,66.735h-.035q-7.532-.3-15.1-.293c-1.381,0-2.762.012-4.131.035A391.992,391.992,0,0,0,152.944,351.586q-1.089,3.827-2.072,7.678c-1.311,5.045-2.5,10.113-3.617,15.216v.012c-.492,2.282-.96,4.553-1.416,6.835a395.6,395.6,0,0,0-7.444,76.629c0,9.82.363,19.652,1.089,29.3.105,1.381.21,2.762.327,4.132.281,3.453.632,7.046,1.042,10.7a391.609,391.609,0,0,0,756.588,91.084q4.056-11.008,7.421-22.239a385.285,385.285,0,0,0,12.407-55.76q.965-6.531,1.7-13.086c.41-3.652.761-7.245,1.042-10.686.936-10.967,1.416-22.215,1.416-33.44a395.4,395.4,0,0,0-7.221-75.424Z" transform="translate(-121.423 -66.442)" fill="#fff"/>
|
||||
<path id="Path_2919-216" data-name="Path 2919" d="M506.048,137.05a1.028,1.028,0,1,1,0-2.057h68.076A32.535,32.535,0,0,1,605.6,110.127a45.245,45.245,0,0,1,82.18-9.652c1.012-.079,2-.119,2.958-.119a38.41,38.41,0,0,1,38.2,35.592,1.028,1.028,0,0,1-.954,1.1l-.073,0a1.028,1.028,0,0,1-1.025-.956Z" transform="translate(-5.811 -62.72)" fill="#e6e6e6"/>
|
||||
<path id="Path_2920-217" data-name="Path 2920" d="M705.856,133.47H537.193a1.028,1.028,0,1,1,0-2.057H705.856a1.028,1.028,0,1,1,0,2.057Z" transform="translate(4.01 -45.954)" fill="#e6e6e6"/>
|
||||
<rect id="Rectangle_663" data-name="Rectangle 663" width="46.814" height="108.524" transform="translate(88.233 423.738)" fill="#090814"/>
|
||||
<rect id="Rectangle_664" data-name="Rectangle 664" width="46.814" height="108.524" transform="translate(681.923 423.738)" fill="#090814"/>
|
||||
<path id="Path_2921-218" data-name="Path 2921" d="M889.646,466.426C834.857,615.354,691.711,721.583,523.764,721.583S212.671,615.354,157.882,466.426Z" transform="translate(-115.278 59.69)" fill="#1ca595"/>
|
||||
<circle id="Ellipse_470" data-name="Ellipse 470" cx="134.601" cy="134.601" r="134.601" transform="translate(0 62.619)" fill="#e6e6e6"/>
|
||||
<path id="Path_2922-219" data-name="Path 2922" d="M914.2,382.533q-.65-3.406-1.369-6.789c-1.077-5.056-2.259-10.1-3.535-15.1-.632-2.528-1.311-5.056-2-7.573v-.012A392.052,392.052,0,0,0,545.043,66.735h-.035q-7.532-.3-15.1-.293c-1.381,0-2.762.012-4.131.035A391.992,391.992,0,0,0,152.944,351.586q-1.089,3.827-2.072,7.678c-1.311,5.045-2.5,10.113-3.617,15.216v.012c-.492,2.282-.96,4.553-1.416,6.835a395.6,395.6,0,0,0-7.444,76.629c0,9.82.363,19.652,1.089,29.3.105,1.381.21,2.762.327,4.132.281,3.453.632,7.046,1.042,10.7a391.609,391.609,0,0,0,756.588,91.084q4.056-11.008,7.421-22.239a385.285,385.285,0,0,0,12.407-55.76q.965-6.531,1.7-13.086c.41-3.652.761-7.245,1.042-10.686.936-10.967,1.416-22.215,1.416-33.44a395.4,395.4,0,0,0-7.221-75.424ZM916.507,491.1c-.269,3.406-.62,6.976-1.03,10.581q-.737,6.514-1.673,12.992a386.684,386.684,0,0,1-12.3,55.268q-3.354,11.113-7.362,22C838.252,743.885,691.875,845.96,529.909,845.96S221.567,743.885,165.678,591.949a385.547,385.547,0,0,1-21.337-90.265c-.41-3.6-.761-7.175-1.042-10.593-.117-1.358-.222-2.715-.327-4.085-.7-9.562-1.065-19.324-1.065-29.05a387.49,387.49,0,0,1,8.4-80.5c.492-2.353,1.018-4.693,1.545-7.034v-.012q1.861-7.936,4.015-15.754c.2-.726.4-1.451.609-2.165q.28-1.036.6-2.072c.351-1.264.726-2.516,1.1-3.757C205.754,188.028,352.317,71.779,525.8,69.977c1.369-.012,2.739-.023,4.108-.023q7.515,0,14.982.281c169.27,6.461,311.117,121.89,357.221,277.981q1.159,3.933,2.247,7.912c1.4,5.15,2.7,10.358,3.9,15.59q.79,3.476,1.51,6.987a387.08,387.08,0,0,1,8.146,79.251C917.912,469.076,917.444,480.23,916.507,491.1Z" transform="translate(-121.423 -66.442)" fill="#090814"/>
|
||||
<rect id="Rectangle_665" data-name="Rectangle 665" width="36.175" height="68.093" transform="translate(93.554 360.965)" fill="#090814"/>
|
||||
<rect id="Rectangle_666" data-name="Rectangle 666" width="8.512" height="153.21" transform="translate(98.874 214.138)" fill="#090814"/>
|
||||
<rect id="Rectangle_667" data-name="Rectangle 667" width="8.512" height="153.21" transform="translate(115.897 214.138)" fill="#090814"/>
|
||||
<rect id="Rectangle_668" data-name="Rectangle 668" width="19.151" height="10.64" transform="translate(103.131 220.522)" fill="#090814"/>
|
||||
<rect id="Rectangle_669" data-name="Rectangle 669" width="19.151" height="10.64" transform="translate(103.131 263.08)" fill="#090814"/>
|
||||
<rect id="Rectangle_670" data-name="Rectangle 670" width="19.151" height="10.64" transform="translate(103.131 309.894)" fill="#090814"/>
|
||||
<rect id="Rectangle_671" data-name="Rectangle 671" width="4.256" height="10.64" transform="translate(101.001 207.754)" fill="#090814"/>
|
||||
<rect id="Rectangle_672" data-name="Rectangle 672" width="4.256" height="10.64" transform="translate(118.026 207.754)" fill="#090814"/>
|
||||
<rect id="Rectangle_673" data-name="Rectangle 673" width="36.175" height="68.093" transform="translate(687.243 360.965)" fill="#090814"/>
|
||||
<rect id="Rectangle_674" data-name="Rectangle 674" width="8.512" height="153.21" transform="translate(692.564 214.138)" fill="#090814"/>
|
||||
<rect id="Rectangle_675" data-name="Rectangle 675" width="8.512" height="153.21" transform="translate(709.586 214.138)" fill="#090814"/>
|
||||
<rect id="Rectangle_676" data-name="Rectangle 676" width="19.151" height="10.64" transform="translate(696.818 220.522)" fill="#090814"/>
|
||||
<rect id="Rectangle_677" data-name="Rectangle 677" width="19.151" height="10.64" transform="translate(696.818 263.08)" fill="#090814"/>
|
||||
<rect id="Rectangle_678" data-name="Rectangle 678" width="19.151" height="10.64" transform="translate(696.818 309.894)" fill="#090814"/>
|
||||
<rect id="Rectangle_679" data-name="Rectangle 679" width="4.256" height="10.64" transform="translate(694.691 207.754)" fill="#090814"/>
|
||||
<rect id="Rectangle_680" data-name="Rectangle 680" width="4.256" height="10.64" transform="translate(711.713 207.754)" fill="#090814"/>
|
||||
<path id="Path_2923-220" data-name="Path 2923" d="M917.505,389.4q-.439,5.355-1.041,10.639H141.839q-.6-5.285-1.041-10.639Z" transform="translate(-120.666 35.401)" fill="#090814"/>
|
||||
<path id="Path_2924-221" data-name="Path 2924" d="M515.482,424.693c-102.026,0-207.255-66.265-312.844-197.029l3.311-2.675C312.155,356.519,417.736,422.315,519.7,420.4c99.111-1.811,197.892-67.535,293.6-195.348l3.407,2.552C720.175,356.521,620.274,422.818,519.782,424.655Q517.633,424.694,515.482,424.693Z" transform="translate(-101.165 -16.445)" fill="#090814"/>
|
||||
<path id="Path_2925-222" data-name="Path 2925" d="M511.075,410.395c-96.371,0-195.758-61.448-295.485-182.712l3.287-2.7C319.169,346.93,418.815,407.834,515.151,406.1c93.523-1.716,186.738-62.634,277.056-181.061l3.384,2.581C704.445,347.134,610.117,408.616,515.229,410.357,513.846,410.382,512.459,410.395,511.075,410.395Z" transform="translate(-97.081 -16.448)" fill="#090814"/>
|
||||
<path id="Path_2926-223" data-name="Path 2926" d="M752.237,332.535c-1.475-1.264-2.961-2.54-4.436-3.827q-47.772-41.592-95.789-101.045l3.312-2.669q45.594,56.48,90.967,96.726,2.282,2.037,4.577,4.026Q751.587,329.135,752.237,332.535Z" transform="translate(40.542 -16.444)" fill="#090814"/>
|
||||
<path id="Path_2927-224" data-name="Path 2927" d="M743.25,310.653q-2.476-2.212-4.939-4.518a785.148,785.148,0,0,1-73.352-78.455l3.289-2.7a793.373,793.373,0,0,0,67.816,73.247c1.732,1.65,3.453,3.265,5.185,4.846v.012C741.939,305.6,742.618,308.125,743.25,310.653Z" transform="translate(44.625 -16.449)" fill="#090814"/>
|
||||
<rect id="Rectangle_681" data-name="Rectangle 681" width="4.256" height="184.538" transform="translate(146.992 243.456)" fill="#090814"/>
|
||||
<rect id="Rectangle_682" data-name="Rectangle 682" width="4.256" height="144.025" transform="translate(186.583 283.969)" fill="#090814"/>
|
||||
<rect id="Rectangle_683" data-name="Rectangle 683" width="4.256" height="100.05" transform="translate(240.515 329.008)" fill="#090814"/>
|
||||
<rect id="Rectangle_684" data-name="Rectangle 684" width="4.256" height="67.029" transform="translate(292.142 360.965)" fill="#090814"/>
|
||||
<rect id="Rectangle_685" data-name="Rectangle 685" width="4.256" height="51.089" transform="translate(334.981 379.033)" fill="#090814"/>
|
||||
<rect id="Rectangle_686" data-name="Rectangle 686" width="4.256" height="184.538" transform="translate(736.898 243.456)" fill="#090814"/>
|
||||
<rect id="Rectangle_687" data-name="Rectangle 687" width="4.256" height="144.025" transform="translate(776.489 283.969)" fill="#090814"/>
|
||||
<path id="Path_2928-225" data-name="Path 2928" d="M242.886,227.663q-22.191,27.476-44.348,51.172-2.142,2.265-4.249,4.506-17.7,18.592-35.336,34.762-2.141,1.966-4.26,3.874c-2.06,1.849-4.109,3.675-6.168,5.478-.48.421-.948.843-1.428,1.252-1.006.878-2.025,1.756-3.043,2.622.457-2.282.925-4.553,1.416-6.835v-.012c1.054-.9,2.1-1.826,3.137-2.762.491-.433.972-.866,1.463-1.3v-.012q2.317-2.054,4.623-4.167,2.141-1.931,4.26-3.921,17.645-16.345,35.336-35.2c1.416-1.51,2.832-3.02,4.249-4.553q20.506-22.139,41.036-47.579Z" transform="translate(-119.639 -16.444)" fill="#090814"/>
|
||||
<path id="Path_2929-226" data-name="Path 2929" d="M224.65,227.68Q210.762,244.57,196.9,259.9c-1.276,1.4-2.54,2.8-3.815,4.178q-17.856,19.47-35.687,36.342-1.967,1.861-3.909,3.675c-.2.2-.41.386-.609.562-.527.5-1.053.983-1.58,1.475-1.136,1.065-2.271,2.107-3.418,3.137q.983-3.845,2.072-7.678,1.808-1.65,3.593-3.371c.55-.515,1.089-1.03,1.639-1.557q18.4-17.469,36.834-37.735,14.66-16.117,29.343-33.955Z" transform="translate(-118.432 -16.449)" fill="#090814"/>
|
||||
<rect id="Rectangle_688" data-name="Rectangle 688" width="4.256" height="184.538" transform="translate(74.648 243.456)" fill="#090814"/>
|
||||
<rect id="Rectangle_689" data-name="Rectangle 689" width="4.256" height="144.025" transform="translate(35.057 283.969)" fill="#090814"/>
|
||||
<rect id="Rectangle_690" data-name="Rectangle 690" width="4.256" height="188.793" transform="translate(672.109 239.2)" fill="#090814"/>
|
||||
<rect id="Rectangle_691" data-name="Rectangle 691" width="4.256" height="144.025" transform="translate(632.515 283.969)" fill="#090814"/>
|
||||
<rect id="Rectangle_692" data-name="Rectangle 692" width="4.256" height="100.05" transform="translate(578.586 329.008)" fill="#090814"/>
|
||||
<rect id="Rectangle_693" data-name="Rectangle 693" width="4.256" height="67.029" transform="translate(526.959 360.965)" fill="#090814"/>
|
||||
<rect id="Rectangle_694" data-name="Rectangle 694" width="4.256" height="51.089" transform="translate(484.118 379.033)" fill="#090814"/>
|
||||
<rect id="Rectangle_695" data-name="Rectangle 695" width="4.256" height="38.53" transform="translate(401.616 391.592)" fill="#090814"/>
|
||||
<path id="Path_2930-227" data-name="Path 2930" d="M438.4,547.825H340.018a5.725,5.725,0,1,1,0-11.449H438.4a5.725,5.725,0,0,1,0,11.449Z" transform="translate(-59.648 81.748)" fill="#fff"/>
|
||||
<path id="Path_2931-228" data-name="Path 2931" d="M558.407,521.032H460.025a5.725,5.725,0,1,1,0-11.449h98.381a5.724,5.724,0,1,1,0,11.449Z" transform="translate(-21.805 73.299)" fill="#fff"/>
|
||||
<path id="Path_2932-229" data-name="Path 2932" d="M509.8,585.3H411.415a5.725,5.725,0,0,1,0-11.449H509.8a5.725,5.725,0,0,1,0,11.449Z" transform="translate(-37.134 93.564)" fill="#fff"/>
|
||||
<path id="Path_2933-230" data-name="Path 2933" d="M362.048,212.05a1.028,1.028,0,1,1,0-2.057h68.076A32.535,32.535,0,0,1,461.6,185.127a45.245,45.245,0,0,1,82.18-9.652c1.012-.079,2-.119,2.958-.119a38.41,38.41,0,0,1,38.2,35.592,1.028,1.028,0,0,1-.954,1.1l-.073,0a1.028,1.028,0,0,1-1.025-.956Z" transform="translate(-51.22 -39.069)" fill="#e6e6e6"/>
|
||||
<path id="Path_2934-231" data-name="Path 2934" d="M438.029,219.807H381.465a1.028,1.028,0,1,1,0-2.057h56.564a1.028,1.028,0,1,1,0,2.057Z" transform="translate(-45.097 -18.728)" fill="#e6e6e6"/>
|
||||
<path id="Path_2935-232" data-name="Path 2935" d="M451.665,113.7l8.553-6.841c-6.644-.733-9.374,2.891-10.492,5.759-5.191-2.155-10.842.669-10.842.669L456,119.5A12.949,12.949,0,0,0,451.665,113.7Z" transform="translate(-26.666 -53.727)" fill="#090814"/>
|
||||
<path id="Path_2936-233" data-name="Path 2936" d="M474.8,248.956l8.553-6.841c-6.644-.733-9.374,2.891-10.492,5.759-5.191-2.155-10.842.669-10.842.669l17.113,6.213a12.949,12.949,0,0,0-4.332-5.8Z" transform="translate(-19.371 -11.075)" fill="#090814"/>
|
||||
<path id="Path_2937-234" data-name="Path 2937" d="M626.963,198.235l8.553-6.841c-6.644-.733-9.374,2.891-10.492,5.759-5.191-2.155-10.842.669-10.842.669l17.113,6.213A12.95,12.95,0,0,0,626.963,198.235Z" transform="translate(28.613 -27.069)" fill="#090814"/>
|
||||
<rect id="Rectangle_696" data-name="Rectangle 696" width="83.102" height="14.045" transform="translate(67.887 512.07)" fill="#090814"/>
|
||||
<rect id="Rectangle_697" data-name="Rectangle 697" width="83.102" height="14.045" transform="translate(663.644 512.07)" fill="#090814"/>
|
||||
<rect id="Rectangle_698" data-name="Rectangle 698" width="778.085" height="3.511" transform="translate(19.376 414.524)" fill="#090814"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 13 KiB |
@@ -0,0 +1,2 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg width="32mm" height="32mm" version="1.1" viewBox="0 0 32 32" xml:space="preserve" xmlns="http://www.w3.org/2000/svg" xmlns:serif="http://www.serif.com/" xmlns:xlink="http://www.w3.org/1999/xlink"><defs><linearGradient id="d" x2="1" gradientTransform="matrix(388.04 -111.5 84.017 279.39 391.79 1018.9)" gradientUnits="userSpaceOnUse"><stop stop-color="#3bb549" offset="0"/><stop stop-color="#f5ee30" offset="1"/></linearGradient><linearGradient id="c" x2=".95791" y2=".0001803" gradientTransform="matrix(122.94 -266.68 378.61 181.95 228.98 392.44)" gradientUnits="userSpaceOnUse"><stop stop-color="#f5ee30" offset="0"/><stop stop-color="#3bb549" offset="1"/></linearGradient><linearGradient id="b" x2="1" gradientTransform="matrix(208.13 0 0 420.73 148.9 534.09)" gradientUnits="userSpaceOnUse"><stop stop-color="#f1592a" offset="0"/><stop stop-color="#f5e732" offset="1"/></linearGradient><linearGradient id="a" x2="1" gradientTransform="matrix(401.83 285.46 -279.32 410.66 391.79 578.6)" gradientUnits="userSpaceOnUse"><stop stop-color="#f5e732" offset="0"/><stop stop-color="#f1592a" offset="1"/></linearGradient></defs><g transform="translate(-1082.3 -67.993)"><g transform="matrix(.02877 0 0 .027964 1085.1 67.175)" clip-rule="evenodd" fill-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="2" stroke-width="8.4072" serif:id="Logo IfcOpenShell"><path d="m744.02 832.33c18.051 11.403 55.982 31.518 63.628 94.935l-0.12525 23.628c-8.342 104.33-183.41 185.58-415.74 186.93v-215.59c141.61-0.7966 269.74-30.595 352.23-89.901z" fill="url(#d)" fill-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="2" stroke-width="8.1335"/><path d="m181.03 442.16c-12.017-10.232-30.084-43.15-32.067-93.421l-0.0617-16.621c1.4066-109.27 91.01-193.98 208.13-195.34v214.31c-47.868 0.55282-122.9 15.206-176 91.069z" fill="url(#c)" fill-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="2" stroke-width="8.3203"/><path d="m148.9 334.48c1.4117 109.69 91.548 194.06 208.13 195.34v214.63c-116.74-1.3026-206.72-85.776-208.13-195.34z" fill="url(#b)" fill-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="2" stroke-width="8.3203"/><path d="m391.79 744.46v-214.64c235.46 1.3846 413.03 88.962 415.86 200.34v214.64c-2.8649-112.42-183.57-199-415.86-200.34z" fill="url(#a)" fill-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="2" stroke-width="8.3164"/><rect transform="matrix(4.3448 0 0 4.47 -94.827 29.251)" x="-8.2421e-7" y="-1.0518e-16" width="256" height="256" fill="none"/></g></g></svg>
|
||||
|
After Width: | Height: | Size: 2.5 KiB |
@@ -0,0 +1,194 @@
|
||||
import ifcopenshell
|
||||
import ifcopenshell.util
|
||||
import ifcopenshell.util.pset
|
||||
import ifcopenshell.util.schema
|
||||
import xml.etree.ElementTree as ET
|
||||
from xmlschema.validators.exceptions import XMLSchemaValidationError
|
||||
from ifctester.ids import Ids, IdsXmlValidationError, get_schema
|
||||
|
||||
# https://github.com/buildingSMART/IDS/blob/9914d568c7ac037acd97e58a0d16e9f93c3e3416/Schema/ids.xsd#L232
|
||||
ifc_schemas = ["IFC2X3", "IFC4", "IFC4X3_ADD2"]
|
||||
|
||||
|
||||
def get_predefined_types_for_entity(schema_name, entity_name):
|
||||
"""Get a list of predefined types for a given entity."""
|
||||
|
||||
schema = ifcopenshell.schema_by_name(schema_name)
|
||||
|
||||
try:
|
||||
entity = schema.declaration_by_name(entity_name)
|
||||
except:
|
||||
return []
|
||||
|
||||
if not entity or not entity.as_entity():
|
||||
print(f"Entity {entity_name} not found")
|
||||
return []
|
||||
|
||||
entity = entity.as_entity()
|
||||
predefined_type_attr = None
|
||||
|
||||
# Check all attributes for "PredefinedType"
|
||||
for attr in entity.all_attributes():
|
||||
if attr.name() == "PredefinedType":
|
||||
predefined_type_attr = attr
|
||||
break
|
||||
|
||||
if not predefined_type_attr:
|
||||
return []
|
||||
|
||||
param_type = predefined_type_attr.type_of_attribute()
|
||||
|
||||
if param_type.as_named_type():
|
||||
enum_decl = param_type.as_named_type().declared_type()
|
||||
if enum_decl.as_enumeration_type():
|
||||
return enum_decl.as_enumeration_type().enumeration_items()
|
||||
|
||||
return []
|
||||
|
||||
|
||||
def get_all_entity_classes(schema_name):
|
||||
"""Get all IFC entity classes in the given schema."""
|
||||
|
||||
schema = ifcopenshell.schema_by_name(schema_name)
|
||||
entities = []
|
||||
|
||||
for entity in schema.entities():
|
||||
entities.append(entity.name())
|
||||
|
||||
# Sort alphabetically
|
||||
entities.sort()
|
||||
return entities
|
||||
|
||||
|
||||
def get_all_data_types(schema_name):
|
||||
"""Get all data types in the given schema."""
|
||||
|
||||
schema = ifcopenshell.schema_by_name(schema_name)
|
||||
return {
|
||||
d.name(): ifcopenshell.util.attribute.get_primitive_type(d)
|
||||
for d in schema.declarations()
|
||||
if d.as_type_declaration()
|
||||
}
|
||||
|
||||
|
||||
def get_entity_attributes(schema_name, entity_name):
|
||||
"""Get all attributes for a given entity."""
|
||||
|
||||
schema = ifcopenshell.schema_by_name(schema_name)
|
||||
|
||||
try:
|
||||
entity = schema.declaration_by_name(entity_name)
|
||||
except:
|
||||
return []
|
||||
|
||||
if not entity or not entity.as_entity():
|
||||
print(f"Entity {entity_name} not found")
|
||||
return []
|
||||
|
||||
entity = entity.as_entity()
|
||||
attributes = []
|
||||
for attr in entity.all_attributes():
|
||||
attributes.append(
|
||||
{
|
||||
"name": attr.name(),
|
||||
# "type": attr.type_of_attribute() # TODO Types of attribute
|
||||
}
|
||||
)
|
||||
|
||||
return attributes
|
||||
|
||||
|
||||
def get_applicable_psets(schema_name, entity_name, predefined_type=""):
|
||||
"""Get all applicable property and quantity sets for a given entity."""
|
||||
|
||||
pset_qto = ifcopenshell.util.pset.PsetQto(schema_name)
|
||||
pset_names = pset_qto.get_applicable_names(entity_name, predefined_type)
|
||||
|
||||
return pset_names
|
||||
|
||||
|
||||
def get_all_psets(schema_name):
|
||||
"""Get all property sets and quantity sets defined in an IFC schema"""
|
||||
|
||||
pset_qto = ifcopenshell.util.pset.PsetQto(schema_name)
|
||||
result = {}
|
||||
|
||||
for template_file in pset_qto.templates:
|
||||
for pset_template in template_file.by_type("IfcPropertySetTemplate"):
|
||||
pset_name = pset_template.Name
|
||||
properties = []
|
||||
|
||||
# Get property templates for this pset
|
||||
if pset_template.HasPropertyTemplates:
|
||||
for prop_template in pset_template.HasPropertyTemplates:
|
||||
prop_info = {
|
||||
"name": prop_template.Name,
|
||||
# "description": prop_template.Description
|
||||
}
|
||||
|
||||
# Extract type information
|
||||
if prop_template.is_a("IfcSimplePropertyTemplate"):
|
||||
if prop_template.PrimaryMeasureType:
|
||||
prop_info["type"] = prop_template.PrimaryMeasureType
|
||||
else:
|
||||
prop_info["type"] = str(prop_template.TemplateType)
|
||||
elif prop_template.is_a("IfcComplexPropertyTemplate"):
|
||||
prop_info["type"] = None # Complex properties are not supported
|
||||
else:
|
||||
prop_info["type"] = None
|
||||
|
||||
properties.append(prop_info)
|
||||
result[pset_name] = properties
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def get_material_categories():
|
||||
return ["concrete", "steel", "aluminium", "block", "brick", "stone", "wood", "glass", "gypsum", "plastic", "earth"]
|
||||
|
||||
|
||||
def get_standard_classification_systems():
|
||||
return {
|
||||
"BB/SfB (3/4 cijfers)": {"source": "Regie der Gebouwen", "tokens": ["."]},
|
||||
"BIMTypeCode": {"source": "BIMStockholm", "tokens": None},
|
||||
"Common Arrangement of Work Sections (CAWS)": {"source": "NBS", "tokens": ["/"]},
|
||||
"CBI Classification - Level 2": {"source": "Masterspec", "tokens": None},
|
||||
"CBI Classification - Level 4": {"source": "Masterspec", "tokens": None},
|
||||
"Rumsfunktionskoder CC001 - 001": {"source": "BIMAlliance", "tokens": ["-"]},
|
||||
"CCS": {"source": "Molio", "tokens": None},
|
||||
"CCTB": {"source": "CCT-Bâtiments", "tokens": ["."]},
|
||||
"Funktionskoder Regionservice CD001 - 001": {"source": "BIMAlliance", "tokens": None},
|
||||
"Rumsfunktion Blekinge CD002 - 001": {"source": "BIMAlliance", "tokens": None},
|
||||
"EcoQuaestor Codetabel": {"source": "EcoQuaestor", "tokens": [".", "-"]},
|
||||
"GuBIMclass CA": {"source": "GuBIMClass", "tokens": ["."]},
|
||||
"GuBIMclass ES": {"source": "GuBIMClass", "tokens": ["."]},
|
||||
"MasterFormat": {"source": "CSI", "tokens": [" ", "."]},
|
||||
"NATSPEC Worksections": {"source": "NATSPEC", "tokens": None},
|
||||
"NBS Create": {"source": "NBS", "tokens": ["_", "/"]},
|
||||
"NL/SfB (4 cijfers)": {"source": "BIMLoket", "tokens": ["."]},
|
||||
"NS 3451 - Bygningsdelstabell": {"source": "Standard Norge", "tokens": None},
|
||||
"OmniClass": {"source": "OmniClass", "tokens": ["-", " "]},
|
||||
"ÖNORM 6241-2": {"source": "freeBIM 2", "tokens": None},
|
||||
"RICS NRM1": {"source": "RICS", "tokens": ["."]},
|
||||
"RICS NRM3": {"source": "RICS", "tokens": ["."]},
|
||||
"SFG20": {"source": "SFG20", "tokens": ["-"]},
|
||||
"SINAPI": {"source": "Caixa", "tokens": ["/"]},
|
||||
"STABU-Element": {"source": "STABU", "tokens": ["."]},
|
||||
"TALO 2000 Building Component Classification": {"source": "Rakennustieto", "tokens": ["."]},
|
||||
"TALO 2000 Hankenimikkeistö": {"source": "Rakennustieto", "tokens": ["."]},
|
||||
"Uniclass": {"source": "RIBA Enterprises Ltd", "tokens": ["_"]},
|
||||
"Uniclass 2015": {"source": "RIBA Enterprises Ltd", "tokens": ["_"]},
|
||||
"UniFormat": {"source": "UniFormat", "tokens": ["."]},
|
||||
"Uniformat": {"source": "UniFormat", "tokens": ["."]},
|
||||
"VMSW": {"source": "VMSW", "tokens": ["."]},
|
||||
}
|
||||
|
||||
|
||||
def ids_from_xml_string(xml: str, validate: bool = False) -> Ids:
|
||||
try:
|
||||
decode = get_schema().decode(
|
||||
xml, strip_namespaces=True, namespaces={"": "http://standards.buildingsmart.org/IDS"}
|
||||
)
|
||||
except XMLSchemaValidationError as e:
|
||||
raise IdsXmlValidationError(e, "Provided XML appears to be invalid. See details above.")
|
||||
return Ids().parse(decode)
|
||||
BIN
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,76 @@
|
||||
# IfcTester - IDS based model auditing
|
||||
# Copyright (C) 2022 Dion Moult <dion@thinkmoult.com>
|
||||
#
|
||||
# This file is part of IfcTester.
|
||||
#
|
||||
# IfcTester 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.
|
||||
#
|
||||
# IfcTester 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 IfcTester. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import os
|
||||
import argparse
|
||||
from flask import Flask, send_from_directory, send_file
|
||||
|
||||
app = Flask(__name__)
|
||||
|
||||
|
||||
def get_static_folder():
|
||||
base_dir = os.path.dirname(__file__)
|
||||
dist_dir = os.path.join(base_dir, "dist")
|
||||
www_dir = os.path.join(base_dir, "www")
|
||||
|
||||
if os.path.exists(dist_dir) and os.path.isdir(dist_dir):
|
||||
return dist_dir
|
||||
elif os.path.exists(www_dir) and os.path.isdir(www_dir):
|
||||
return www_dir
|
||||
else:
|
||||
return dist_dir
|
||||
|
||||
|
||||
STATIC_FOLDER = get_static_folder()
|
||||
|
||||
|
||||
@app.route("/")
|
||||
def index():
|
||||
return send_file(os.path.join(STATIC_FOLDER, "index.html"))
|
||||
|
||||
|
||||
@app.route("/<path:filename>")
|
||||
def static_files(filename):
|
||||
return send_from_directory(STATIC_FOLDER, filename)
|
||||
|
||||
|
||||
@app.route("/assets/<path:filename>")
|
||||
def assets(filename):
|
||||
return send_from_directory(os.path.join(STATIC_FOLDER, "assets"), filename)
|
||||
|
||||
|
||||
@app.errorhandler(404)
|
||||
def not_found(error):
|
||||
return send_file(os.path.join(STATIC_FOLDER, "index.html"))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
parser = argparse.ArgumentParser(description="Start IfcTester webapp")
|
||||
parser.add_argument("--host", default="127.0.0.1", help="Host to bind to (default: 127.0.0.1)")
|
||||
parser.add_argument("--port", type=int, default=5000, help="Port to bind to (default: 5000)")
|
||||
parser.add_argument("--debug", action="store_true", help="Enable debug mode")
|
||||
parser.add_argument("--dist-dir", default=STATIC_FOLDER, help="Directory containing built files")
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
STATIC_FOLDER = args.dist_dir
|
||||
|
||||
print(f"Serving IfcTester webapp from: {STATIC_FOLDER}")
|
||||
print(f"Server running at: http://{args.host}:{args.port}")
|
||||
|
||||
app.run(host=args.host, port=args.port, debug=args.debug)
|
||||
@@ -0,0 +1,6 @@
|
||||
<script>
|
||||
import Router from 'svelte-spa-router';
|
||||
import routes from './routes';
|
||||
</script>
|
||||
|
||||
<Router {routes} />
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user