Compare commits

..

11 Commits

Author SHA1 Message Date
Ryan Schultz 60ccd3ab7e Add ALT+Click unhide to select_group_elements
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 20:16:04 -05:00
Ryan Schultz f0d8a6f4f4 Add ALT+Click unhide to select_aggregate
When ALT is held, BIM_OT_select_aggregate now clears
hide_viewport and hide_set on matched objects before
selecting, consistent with the pattern used across other
selection operators on this branch.

Generated with the assistance of an AI coding tool.
2026-04-19 06:49:12 -05:00
Ryan Schultz 2ad30a00d7 Add container tools to spatial decomposition panel
- Add bim.select_similar_container button to the spatial
  decomposition panel, gated behind the new
  show_container_tools preference (renamed from
  container_hide_show_isolate)
- Fix select_similar_container to resolve container from
  the active list item rather than the active object
- Unhide individual objects when showing container
  visibility (set_container_visibility SHOW/ISOLATE)

Generated with the assistance of an AI coding tool.
2026-03-20 20:06:53 -05:00
Ryan Schultz e3d5e1d1fe Unhide objects when showing container visibility
When mode is SHOW or ISOLATE, also unhide individual
objects (hide_viewport and hide_set) within each
container, not just the collection-level visibility.

Generated with the assistance of an AI coding tool.
2026-03-20 19:38:57 -05:00
Ryan Schultz 15228ac8b5 Add ALT+Click unhide to select_decomposed_elements
Moves "select all listed elements" from ALT to SHIFT,
freeing ALT+Click to unhide hidden objects (viewport
and local hide) before selecting.

Generated with the assistance of an AI coding tool.
2026-03-20 19:20:43 -05:00
Ryan Schultz 011fb6660a Add ALT+Click unhide to select_linked_aggregates
ALT+Click now unhides hidden objects (viewport and local
hide) before selecting, matching the behavior added to
other select operators.

Generated with the assistance of an AI coding tool.
2026-03-20 19:06:04 -05:00
Ryan Schultz cd458780e6 Add ALT+Click unhide to select_by_material
ALT+Click now unhides hidden objects (viewport and local
hide) before selecting, matching the behavior added to
other select operators.

Generated with the assistance of an AI coding tool.
2026-03-20 19:00:47 -05:00
Ryan Schultz 873f9f061f Add ALT+Click unhide to select_similar_container
ALT+Click now unhides hidden objects (viewport and local
hide) before selecting. Also fixes select_products to set
hide_viewport in addition to hide_set when unhiding.

Generated with the assistance of an AI coding tool.
2026-03-20 18:54:43 -05:00
Ryan Schultz ea27b4f3ae Add ALT+Click unhide to select_similar_type
ALT+Click now unhides hidden objects (viewport and local
hide) before selecting, matching the behavior added to
select_ifc_class and select_similar.

Generated with the assistance of an AI coding tool.
2026-03-20 12:33:51 -05:00
Ryan Schultz 1ce62c948c Add ALT+Click unhide to select_similar
ALT+Click now unhides hidden objects (viewport and local
hide) before selecting, matching the same behavior added
to select_ifc_class.

Generated with the assistance of an AI coding tool.
2026-03-20 12:30:11 -05:00
Ryan Schultz f327a999d0 Closes #7780 - Add ALT+Click unhide to select_ifc_class
Clear hide_viewport and hide_set on matched objects
when the operator is invoked with Alt held, so hidden
objects of the target IFC class are revealed and selected.

Generated with the assistance of an AI coding tool.
2026-03-20 12:20:15 -05:00
149 changed files with 1295 additions and 2907 deletions
+1 -1
View File
@@ -53,7 +53,7 @@ jobs:
python ../nix/cache_dependencies.py unpack
- name: ccache
uses: hendrikmuhs/ccache-action@v1.2.21
uses: hendrikmuhs/ccache-action@v1.2.20
with:
key: mac-${{ matrix.arch }}
+1 -1
View File
@@ -29,7 +29,7 @@ jobs:
python ../IfcOpenShell/nix/cache_dependencies.py unpack
- name: ccache
uses: hendrikmuhs/ccache-action@v1.2.21
uses: hendrikmuhs/ccache-action@v1.2.20
with:
key: ubuntu-22.04-${{ runner.arch }}
+1 -1
View File
@@ -48,7 +48,7 @@ jobs:
python3 ../nix/cache_dependencies.py unpack
- name: ccache
uses: hendrikmuhs/ccache-action@v1.2.21
uses: hendrikmuhs/ccache-action@v1.2.20
with:
key: ubuntu-22.04-${{ runner.arch }}-rockylinux9
+1 -1
View File
@@ -48,7 +48,7 @@ jobs:
python3 ../nix/cache_dependencies.py unpack
- name: ccache
uses: hendrikmuhs/ccache-action@v1.2.21
uses: hendrikmuhs/ccache-action@v1.2.20
with:
key: ubuntu-22.04-${{ runner.arch }}-rockylinux9
+1 -1
View File
@@ -52,7 +52,7 @@ jobs:
}
- name: ccache
uses: hendrikmuhs/ccache-action@v1.2.21
uses: hendrikmuhs/ccache-action@v1.2.20
with:
key: win-${{ matrix.arch }}
# Windows ccache needs ~1GB
+1 -1
View File
@@ -35,7 +35,7 @@ jobs:
-
name: ccache
uses: hendrikmuhs/ccache-action@v1.2.21
uses: hendrikmuhs/ccache-action@v1.2.20
-
name: Build ifcopenshell
+1 -1
View File
@@ -79,7 +79,7 @@ jobs:
libhdf5-dev libcgal-dev libeigen3-dev
- name: ccache
uses: hendrikmuhs/ccache-action@v1.2.21
uses: hendrikmuhs/ccache-action@v1.2.20
with:
key: ubuntu-22.04-${{ runner.arch }}
+3 -5
View File
@@ -14,7 +14,6 @@
/src/ifcmax/out/
/src/ifcwrap/out/
/src/qtviewer/out/
/src/ifctester/webapp/public/pyodide/
/win/BuildDepsCache*.txt
@@ -86,11 +85,10 @@ src/bonsai/bonsai/translations.py
# bonsai external dependencies (cloned for just ty checks)
src/bonsai/external_dependencies/
# bonsai test temp/cache files
# bonsai test temp files
src/bonsai/test/files/temp
src/bonsai/test/files/*.cache.blend
src/bonsai/test/files/*.cache.json
src/bonsai/test/files/*.cache.sqlite
src/bonsai/test/files/basic.ifc.cache.blend
src/bonsai/test/files/basic.ifc.cache.sqlite
# bonsai data
src/bonsai/bonsai/bim/data/build/
+1 -1
View File
@@ -3,7 +3,7 @@ name = "IfcOpenShell"
version = "0.0.0"
dependencies = [
"black==26.3.1",
"ruff==0.15.7",
"ruff==0.15.6",
"poethepoet",
"gersemi==0.26.1",
]
+5 -8
View File
@@ -45,19 +45,16 @@ from bonsai.bim.module.nest.decorator import NestDecorator
cwd = os.path.dirname(os.path.realpath(__file__))
global_subscription_owner = object()
# Separate owner for per-object msgbus subscriptions (name, active_material_index).
# Using a dedicated owner allows clearing all per-object subscriptions at once
# during undo/redo without affecting other global subscriptions.
object_subscription_owner = object()
def name_callback(obj: Union[bpy.types.Object, bpy.types.Material], data: str) -> None:
try:
obj.name
except:
# The object is invalid but somehow still has a callback.
# This can occur during undo/redo when the Python wrapper is stale.
return
# The object is invalid but somehow still has a callback. Clear all
# msgbus subscriptions to prevent useless further triggers.
bpy.msgbus.clear_by_owner(obj)
return # In case the object RNA is gone during an undo / redo operation
# Blender names are up to 63 UTF-8 bytes
if len(bytes(obj.name, "utf-8")) >= 63:
return
@@ -192,7 +189,7 @@ def subscribe_to(obj: bpy.types.ID, data_path: str, callback: Callable[[bpy.type
return
bpy.msgbus.subscribe_rna(
key=subscribe_to,
owner=object_subscription_owner,
owner=obj,
args=(
obj,
data_path,
+9 -4
View File
@@ -316,8 +316,11 @@ class IfcStore:
del IfcStore.id_map[data["id"]]
if "guid" in data:
del IfcStore.guid_map[data["guid"]]
# Note: msgbus subscriptions are cleared globally during
# rebuild_element_maps which runs after every undo/redo.
obj = IfcStore.get_object_by_name(data["obj"])
if obj is None:
# obj was just created during this step and didn't existed before.
return
bpy.msgbus.clear_by_owner(obj)
@staticmethod
def commit_link_element(data: OperationData) -> None:
@@ -364,8 +367,10 @@ class IfcStore:
del IfcStore.id_map[data["id"]]
if "guid" in data:
del IfcStore.guid_map[data["guid"]]
# Note: msgbus subscriptions are cleared globally during
# rebuild_element_maps which runs after every undo/redo.
obj = IfcStore.get_object_by_name(data["obj"])
# obj might be removed after unlink.
if not obj:
bpy.msgbus.clear_by_owner(obj)
@staticmethod
def unlink_element(
+2 -2
View File
@@ -64,8 +64,8 @@ class MaterialCreator:
mesh: Union[OBJECT_DATA_TYPE, None],
shape_has_openings: bool,
) -> None:
if (((rep := getattr(element, "Representation", ...)) is not ... and not rep) or
((rep := getattr(element, "RepresentationMaps", ...)) is not ... and not rep)
if ((rep := getattr(element, "Representation", ...) is not ...) and not rep) or (
(rep := getattr(element, "RepresentationMaps", ...) is not ...) and not rep
):
return
@@ -266,17 +266,19 @@ class BIM_OT_select_aggregate(bpy.types.Operator):
one_level_deep: bpy.props.BoolProperty(
name="One Level Deep", description="Select only immediate children, not recursively", default=False
)
should_unhide: bpy.props.BoolProperty(default=False, options={"SKIP_SAVE"})
@classmethod
def description(cls, context, properties):
if properties.select_parts:
return "Select Aggregate and Parts.\n\nCtrl+click to select only one level deep"
return "Select Aggregate and Parts.\n\nCtrl+click to select only one level deep\nALT+Click to also unhide hidden objects (viewport and local hide)"
else:
return "Select Aggregate"
return "Select Aggregate\n\nALT+Click to also unhide hidden objects (viewport and local hide)"
def invoke(self, context, event):
if event.type == "LEFTMOUSE" and event.ctrl:
self.one_level_deep = True
self.should_unhide = event.alt
return self.execute(context)
def execute(self, context):
@@ -317,12 +319,18 @@ class BIM_OT_select_aggregate(bpy.types.Operator):
for element in set(selected_parts + all_parts):
obj = tool.Ifc.get_object(element)
if obj:
if self.should_unhide:
obj.hide_viewport = False
obj.hide_set(False)
obj.select_set(True)
else:
for aggregate_element in all_parts:
aggregate_obj = tool.Ifc.get_object(aggregate_element)
if aggregate_obj:
if self.should_unhide:
aggregate_obj.hide_viewport = False
aggregate_obj.hide_set(False)
aggregate_obj.select_set(True)
bpy.context.view_layer.objects.active = aggregate_obj
@@ -407,13 +415,18 @@ class BIM_OT_select_linked_aggregates(bpy.types.Operator):
bl_label = "Select linked aggregates"
bl_options = {"REGISTER", "UNDO"}
select_parts: bpy.props.BoolProperty(default=False)
should_unhide: bpy.props.BoolProperty(default=False, options={"SKIP_SAVE"})
@classmethod
def description(cls, context, properties):
if properties.select_parts:
return "Select all aggregates, subaggregates and all their parts"
return "Select all aggregates, subaggregates and all their parts\n\nALT+Click to also unhide hidden objects (viewport and local hide)"
else:
return "Select all aggregates"
return "Select all aggregates\n\nALT+Click to also unhide hidden objects (viewport and local hide)"
def invoke(self, context, event):
self.should_unhide = event.alt
return self.execute(context)
def execute(self, context):
for obj in context.selected_objects:
@@ -446,11 +459,18 @@ class BIM_OT_select_linked_aggregates(bpy.types.Operator):
for element in parts_objs:
obj = tool.Ifc.get_object(element)
if obj:
if self.should_unhide:
obj.hide_viewport = False
obj.hide_set(False)
obj.select_set(True)
else:
for element in parts:
obj = tool.Ifc.get_object(element)
obj.select_set(True)
if obj:
if self.should_unhide:
obj.hide_viewport = False
obj.hide_set(False)
obj.select_set(True)
return {"FINISHED"}
@@ -1426,7 +1426,6 @@ class CreateDrawing(bpy.types.Operator):
"/Pset_.*Common/.Status",
"EPset_Status.Status",
"EPset_Status.UserDefinedStatus",
"Material.Name",
]
group = root.find("{http://www.w3.org/2000/svg}g")
@@ -2289,7 +2289,7 @@ class OverrideModeSetEdit(bpy.types.Operator, tool.Ifc.Operator):
elif obj in pprops.clipping_planes_objs:
self.report({"ERROR"}, "Clipping planes cannot be edited")
elif element:
if not obj.data or obj.type not in ("MESH", "CURVE"):
if not obj.data:
self.report({"INFO"}, "No geometry to edit")
elif tool.Geometry.is_locked(element):
self.report({"ERROR"}, lock_error_message(obj.name))
@@ -196,18 +196,23 @@ class SelectGroupElements(bpy.types.Operator):
bl_label = "Select Group elements"
bl_options = {"REGISTER", "UNDO"}
bl_description = (
"Select objects assigned to the selected group and all nested groups\nALT + CLICK to exclude children"
"Select objects assigned to the selected group and all nested groups"
"\nCTRL + CLICK to exclude children"
"\nALT + CLICK to also unhide hidden objects (viewport and local hide)"
)
group: bpy.props.IntProperty()
is_recursive: bpy.props.BoolProperty(name="Is Recursive", default=True, options={"SKIP_SAVE"})
should_unhide: bpy.props.BoolProperty(default=False, options={"SKIP_SAVE"})
def invoke(self, context, event):
self.is_recursive = not event.alt
self.is_recursive = not event.ctrl
self.should_unhide = event.alt
return self.execute(context)
def execute(self, context):
tool.Spatial.select_products(
ifcopenshell.util.element.get_grouped_by(tool.Ifc.get().by_id(self.group), is_recursive=self.is_recursive)
ifcopenshell.util.element.get_grouped_by(tool.Ifc.get().by_id(self.group), is_recursive=self.is_recursive),
unhide=self.should_unhide,
)
return {"FINISHED"}
@@ -61,13 +61,18 @@ class DisableEditingMaterials(bpy.types.Operator):
class SelectByMaterial(bpy.types.Operator):
bl_idname = "bim.select_by_material"
bl_label = "Select By Material"
bl_description = "Select objects using the provided material"
bl_description = "Select objects using the provided material\n\nALT+Click to also unhide hidden objects (viewport and local hide)"
bl_options = {"REGISTER", "UNDO"}
material: bpy.props.IntProperty()
should_unhide: bpy.props.BoolProperty(default=False, options={"SKIP_SAVE"})
def invoke(self, context, event):
self.should_unhide = event.alt
return self.execute(context)
def execute(self, context):
material = tool.Ifc.get().by_id(self.material)
core.select_by_material(tool.Material, tool.Spatial, material=material)
core.select_by_material(tool.Material, tool.Spatial, material=material, should_unhide=self.should_unhide)
# copy selection query to clipboard
if material.is_a("IfcMaterialLayerSet"):
@@ -540,16 +540,6 @@ class AddBoolean(Operator, tool.Ifc.Operator):
booleans = ifcopenshell.api.geometry.add_boolean(tool.Ifc.get(), first_item, second_items, props.operator)
rep_obj = tool.Geometry.get_geometry_props().representation_obj
if booleans:
# Users typically select two top-level items and expect the
# operand to be absorbed into the boolean, not remain as a
# standalone item alongside it.
representation = tool.Geometry.get_active_representation(rep_obj)
representation = ifcopenshell.util.representation.resolve_representation(representation)
second_items_set = set(second_items)
new_items = [i for i in representation.Items if i not in second_items_set]
if new_items:
representation.Items = new_items
rep_element = tool.Ifc.get_entity(rep_obj)
tool.Model.mark_manual_booleans(rep_element, booleans)
tool.Geometry.reload_representation(rep_obj)
@@ -1264,15 +1264,17 @@ class SelectGlobalId(Operator):
class SelectIfcClass(Operator):
"""Click to select all objects that match with the given IFC class\nSHIFT + Click to also match Predefined Type"""
"""Click to select all objects that match with the given IFC class\nSHIFT + Click to also match Predefined Type\nALT + Click to also unhide hidden objects (viewport and local hide)"""
bl_idname = "bim.select_ifc_class"
bl_label = "Select IFC Class"
bl_options = {"REGISTER", "UNDO"}
should_filter_predefined_type: BoolProperty(default=False)
should_unhide: BoolProperty(default=False)
def invoke(self, context, event):
self.should_filter_predefined_type = event.shift
self.should_unhide = event.alt
return self.execute(context)
def execute(self, context):
@@ -1292,6 +1294,9 @@ class SelectIfcClass(Operator):
):
continue
if obj := tool.Ifc.get_object(element):
if self.should_unhide:
obj.hide_viewport = False
obj.hide_set(False)
tool.Blender.select_object(obj)
# copy selection query to clipboard
@@ -1451,10 +1456,11 @@ class SelectSimilar(Operator):
)
calculated_sum: bpy.props.FloatProperty(name="Calculated Sum", default=0.0)
remove_from_selection: bpy.props.BoolProperty(default=False, options={"SKIP_SAVE"})
should_unhide: bpy.props.BoolProperty(default=False, options={"SKIP_SAVE"})
@classmethod
def description(cls, context, properties):
base = "Select objects with a similar value\n\n" "SHIFT+CLICK remove from selection set."
base = "Select objects with a similar value\n\nSHIFT+CLICK remove from selection set.\nALT+CLICK also unhide hidden objects (viewport and local hide)."
key = getattr(properties, "key", None)
active = context.active_object
@@ -1481,6 +1487,7 @@ class SelectSimilar(Operator):
def invoke(self, context, event):
self.calculate_sum = event.ctrl and event.type == "LEFTMOUSE"
self.remove_from_selection = event.shift and event.type == "LEFTMOUSE"
self.should_unhide = event.alt
return self.execute(context)
def execute(self, context):
@@ -1538,11 +1545,15 @@ class SelectSimilar(Operator):
def _select_objects(self, context, key, reference_values, tolerance):
count = 0
for obj in context.visible_objects:
objects = context.scene.objects if self.should_unhide else context.visible_objects
for obj in objects:
obj_value = self._get_value(obj, key)
if obj_value is None:
continue
if any(self._compare_values(obj_value, ref_value, tolerance) for ref_value in reference_values):
if self.should_unhide:
obj.hide_viewport = False
obj.hide_set(False)
obj.select_set(not self.remove_from_selection)
count += 1
return count
@@ -284,22 +284,33 @@ class SelectContainer(bpy.types.Operator):
class SelectSimilarContainer(bpy.types.Operator):
bl_idname = "bim.select_similar_container"
bl_label = "Select Similar Container"
bl_description = "Recurvisevly selects all objects in the container.\n\nCtrl+click to select only one level deep"
bl_description = "Recursively selects all objects in the container.\n\nCtrl+click to select only one level deep\nAlt+click to also unhide hidden objects (viewport and local hide)"
bl_options = {"REGISTER", "UNDO"}
container: bpy.props.IntProperty(default=0)
is_recursive: bpy.props.BoolProperty(default=True)
should_unhide: bpy.props.BoolProperty(default=False, options={"SKIP_SAVE"})
def invoke(self, context, event):
if event.type == "LEFTMOUSE" and event.ctrl:
self.is_recursive = False
self.should_unhide = event.alt
return self.execute(context)
def execute(self, context):
if self.container:
container = tool.Ifc.get().by_id(self.container)
elif element := tool.Ifc.get_entity(context.active_object):
container = ifcopenshell.util.element.get_container(element)
else:
return {"CANCELLED"}
if not container:
return {"CANCELLED"}
core.select_similar_container(
tool.Ifc,
tool.Spatial,
obj=context.active_object,
container=container,
is_recursive=self.is_recursive,
should_unhide=self.should_unhide,
)
self.is_recursive = True # <-- forcibly reset
return {"FINISHED"}
@@ -425,24 +436,29 @@ class SelectDecomposedElements(bpy.types.Operator):
should_filter: bpy.props.BoolProperty(name="Should Filter", default=True, options={"SKIP_SAVE"})
container: bpy.props.IntProperty()
is_recursive: bpy.props.BoolProperty(default=True, options={"SKIP_SAVE"})
should_unhide: bpy.props.BoolProperty(default=False, options={"SKIP_SAVE"})
@classmethod
def description(cls, context, operator):
return (
"Select the active item"
+ "\nALT+CLICK to select all listed elements.\nCTRL + CLICK to select only one level deep"
+ "\nSHIFT+CLICK to select all listed elements.\nCTRL+CLICK to select only one level deep"
+ "\nALT+CLICK to also unhide hidden objects (viewport and local hide)"
)
def invoke(self, context, event):
if event.type == "LEFTMOUSE":
if event.alt:
if event.shift:
self.should_filter = False
if event.ctrl:
self.is_recursive = False
self.should_unhide = event.alt
return self.execute(context)
def execute(self, context):
tool.Spatial.select_products(tool.Spatial.get_filtered_elements(self.should_filter, self.is_recursive))
tool.Spatial.select_products(
tool.Spatial.get_filtered_elements(self.should_filter, self.is_recursive), unhide=self.should_unhide
)
# Make selected active element in list, the active object
props = tool.Spatial.get_spatial_props()
@@ -525,6 +541,11 @@ class SetContainerVisibility(bpy.types.Operator):
if obj := tool.Ifc.get_object(container):
if collection := tool.Blender.get_object_bim_props(obj).collection:
collection.hide_viewport = should_hide
if not should_hide:
for element in tool.Spatial.get_decomposed_elements(container, is_recursive=False):
if element_obj := tool.Ifc.get_object(element):
element_obj.hide_viewport = False
element_obj.hide_set(False)
if self.should_include_children:
queue.extend(ifcopenshell.util.element.get_parts(container))
return {"FINISHED"}
+5 -2
View File
@@ -139,7 +139,7 @@ class BIM_PT_spatial_decomposition(Panel):
op = col.operator("bim.set_default_container", icon="OUTLINER_COLLECTION", text="Set Default")
op.container = ifc_definition_id
if tool.Blender.get_addon_preferences().container_hide_show_isolate:
if tool.Blender.get_addon_preferences().show_container_tools:
op = row.operator("bim.set_container_visibility", icon="FULLSCREEN_EXIT", text="")
op.mode = "ISOLATE"
op.container = ifc_definition_id
@@ -152,7 +152,10 @@ class BIM_PT_spatial_decomposition(Panel):
# The only operator that's enabled for IfcProject.
col = row.column(align=True)
col.operator("bim.select_container", icon="OBJECT_DATA", text="").container = ifc_definition_id
row_ = col.row(align=True)
row_.operator("bim.select_container", icon="OBJECT_DATA", text="").container = ifc_definition_id
if tool.Blender.get_addon_preferences().show_container_tools:
row_.operator("bim.select_similar_container", icon="RESTRICT_SELECT_OFF", text="").container = ifc_definition_id
col = row.column(align=True)
op = col.operator("bim.delete_container", icon="X", text="")
+11 -1
View File
@@ -221,10 +221,17 @@ class SelectType(bpy.types.Operator):
class SelectSimilarType(bpy.types.Operator):
"""Select Similar Type\nALT+Click to also unhide hidden objects (viewport and local hide)"""
bl_idname = "bim.select_similar_type"
bl_label = "Select Similar Type"
bl_options = {"REGISTER", "UNDO"}
related_object: bpy.props.StringProperty()
should_unhide: bpy.props.BoolProperty(default=False, options={"SKIP_SAVE"})
def invoke(self, context, event):
self.should_unhide = event.alt
return self.execute(context)
def execute(self, context):
self.file = tool.Ifc.get()
@@ -246,7 +253,10 @@ class SelectSimilarType(bpy.types.Operator):
for element in related_objects:
obj = tool.Ifc.get_object(element)
if obj and obj in context.visible_objects:
if obj and (self.should_unhide or obj in context.visible_objects):
if self.should_unhide:
obj.hide_viewport = False
obj.hide_set(False)
obj.select_set(True)
# copy selection query to clipboard
+5 -5
View File
@@ -714,9 +714,9 @@ class BIM_ADDON_preferences(bpy.types.AddonPreferences):
description="Default parameters for BIM elements",
)
container_hide_show_isolate: BoolProperty(
name="Container hide/show/isolate",
description="Enable container hide/show/isolate feature in the UI",
show_container_tools: BoolProperty(
name="Show Container Tools (Select, Hide, Show, Isolate)",
description="Enable container select, hide/show/isolate tools in the UI",
default=False,
)
@@ -786,7 +786,7 @@ class BIM_ADDON_preferences(bpy.types.AddonPreferences):
pset_dir: str
doc: DocPreferences
default_parameters: DefaultParameters
container_hide_show_isolate: bool
show_container_tools: bool
chain_filter_with_set_operations: bool
save_metadata_blend_file: bool
metadata_blend_file_suffix: str
@@ -984,7 +984,7 @@ class BIM_ADDON_preferences(bpy.types.AddonPreferences):
layout.prop(self, "bsdd_baseurl")
def draw_extras_settings(self, layout: bpy.types.UILayout, context: bpy.types.Context) -> None:
layout.prop(self, "container_hide_show_isolate")
layout.prop(self, "show_container_tools")
row = layout.row(align=True)
row.prop(self, "chain_filter_with_set_operations")
row.operator("bim.open_uri", text="", icon="HELP").uri = "https://community.osarch.org/discussion/3270"
+61 -11
View File
@@ -51,11 +51,21 @@ def add_instance_flooring_covering_from_cursor(
if isinstance(space_polygon, str):
return
obj = spatial.create_object("Covering")
bm = spatial.get_bmesh_from_polygon(space_polygon, h=0, polygon_is_si=True)
name = "Covering"
mesh = spatial.get_named_mesh_from_bmesh(name=name, bmesh=bm)
obj = spatial.get_named_obj_from_mesh(name, mesh)
spatial.set_obj_origin_to_cursor_position_and_zero_elevation(obj)
spatial.translate_obj_to_z_location(obj, z)
points = spatial.get_2d_vertices_from_obj(obj)
points = spatial.get_scaled_2d_vertices(points)
spatial.assign_type_to_obj(obj)
spatial.set_covering_representation_from_polygon(obj, space_polygon, polygon_is_si=True)
spatial.assign_swept_area_outer_curve_from_2d_vertices(obj, vertices=points)
body = spatial.get_body_representation(obj)
spatial.regen_obj_representation(obj, body)
def add_instance_ceiling_covering_from_cursor(
@@ -85,11 +95,21 @@ def add_instance_ceiling_covering_from_cursor(
if isinstance(space_polygon, str):
return
obj = spatial.create_object("Covering")
bm = spatial.get_bmesh_from_polygon(space_polygon, h=0, polygon_is_si=True)
name = "Covering"
mesh = spatial.get_named_mesh_from_bmesh(name=name, bmesh=bm)
obj = spatial.get_named_obj_from_mesh(name, mesh)
spatial.set_obj_origin_to_cursor_position_and_zero_elevation(obj)
spatial.translate_obj_to_z_location(obj, z + ceiling_height)
points = spatial.get_2d_vertices_from_obj(obj)
points = spatial.get_scaled_2d_vertices(points)
spatial.assign_type_to_obj(obj)
spatial.set_covering_representation_from_polygon(obj, space_polygon, polygon_is_si=True)
spatial.assign_swept_area_outer_curve_from_2d_vertices(obj, vertices=points)
body = spatial.get_body_representation(obj)
spatial.regen_obj_representation(obj, body)
def regen_selected_covering_object(root: type[tool.Root], spatial: type[tool.Spatial]) -> None:
@@ -107,7 +127,19 @@ def regen_selected_covering_object(root: type[tool.Root], spatial: type[tool.Spa
if isinstance(space_polygon, str):
return
spatial.set_covering_representation_from_polygon(active_obj, space_polygon, polygon_is_si=True)
bm = spatial.get_bmesh_from_polygon(space_polygon, h=0, polygon_is_si=True)
name = "Aux"
mesh = spatial.get_named_mesh_from_bmesh(name=name, bmesh=bm)
mesh = spatial.get_transformed_mesh_from_local_to_global(mesh)
obj = spatial.get_named_obj_from_mesh(name, mesh)
points = spatial.get_2d_vertices_from_obj(obj)
points = spatial.get_scaled_2d_vertices(points)
spatial.assign_swept_area_outer_curve_from_2d_vertices(active_obj, vertices=points)
body = spatial.get_body_representation(active_obj)
spatial.regen_obj_representation(active_obj, body)
# TODO CHECK IF IT IS POSSIBLE TO CREATE ONLY ONE CORE FUNCTION FOR _FROM_WALLS
@@ -119,13 +151,22 @@ def add_instance_flooring_coverings_from_walls(root: type[tool.Root], spatial: t
union = spatial.get_union_shape_from_selected_objects()
for i, linear_ring in enumerate(union.interiors):
poly = spatial.get_buffered_poly_from_linear_ring(linear_ring)
bm = spatial.get_bmesh_from_polygon(poly, h=0, polygon_is_si=False)
name = "Covering" + str(i)
obj = spatial.create_object(name)
spatial.set_obj_origin_to_polygon_center(obj, poly, polygon_is_si=False)
obj = spatial.get_named_obj_from_bmesh(name, bmesh=bm)
spatial.set_obj_origin_to_bboxcenter(obj)
spatial.translate_obj_to_z_location(obj, z)
points = spatial.get_2d_vertices_from_obj(obj)
points = spatial.get_scaled_2d_vertices(points)
spatial.assign_type_to_obj(obj)
spatial.set_covering_representation_from_polygon(obj, poly, polygon_is_si=False)
spatial.assign_swept_area_outer_curve_from_2d_vertices(obj, vertices=points)
body = spatial.get_body_representation(obj)
spatial.regen_obj_representation(obj, body)
def add_instance_ceiling_coverings_from_walls(
@@ -138,13 +179,22 @@ def add_instance_ceiling_coverings_from_walls(
union = spatial.get_union_shape_from_selected_objects()
for i, linear_ring in enumerate(union.interiors):
poly = spatial.get_buffered_poly_from_linear_ring(linear_ring)
bm = spatial.get_bmesh_from_polygon(poly, h=0, polygon_is_si=False)
name = "Covering" + str(i)
obj = spatial.create_object(name)
spatial.set_obj_origin_to_polygon_center(obj, poly, polygon_is_si=False)
obj = spatial.get_named_obj_from_bmesh(name, bmesh=bm)
spatial.set_obj_origin_to_bboxcenter(obj)
spatial.translate_obj_to_z_location(obj, z)
points = spatial.get_2d_vertices_from_obj(obj)
points = spatial.get_scaled_2d_vertices(points)
spatial.assign_type_to_obj(obj)
spatial.set_covering_representation_from_polygon(obj, poly, polygon_is_si=False)
spatial.assign_swept_area_outer_curve_from_2d_vertices(obj, vertices=points)
body = spatial.get_body_representation(obj)
spatial.regen_obj_representation(obj, body)
class NoDefaultContainer(Exception):
+5 -2
View File
@@ -82,9 +82,12 @@ def disable_editing_materials(material: type[tool.Material]) -> None:
def select_by_material(
material_tool: type[tool.Material], spatial: type[tool.Spatial], material: ifcopenshell.entity_instance
material_tool: type[tool.Material],
spatial: type[tool.Spatial],
material: ifcopenshell.entity_instance,
should_unhide: bool = False,
) -> None:
spatial.select_products(material_tool.get_elements_by_material(material))
spatial.select_products(material_tool.get_elements_by_material(material), unhide=should_unhide)
def enable_editing_material(material_tool: type[tool.Material], material: ifcopenshell.entity_instance) -> None:
+15 -13
View File
@@ -120,14 +120,12 @@ def select_container(
def select_similar_container(
ifc: type[tool.Ifc],
spatial: type[tool.Spatial],
obj: bpy.types.Object,
container: ifcopenshell.entity_instance,
is_recursive: bool = True,
should_unhide: bool = False,
) -> None:
element = ifc.get_entity(obj)
if element:
spatial.select_products(spatial.get_decomposed_elements(spatial.get_container(element), is_recursive))
spatial.select_products(spatial.get_decomposed_elements(container, is_recursive), unhide=should_unhide)
def select_product(spatial: type[tool.Spatial], product: ifcopenshell.entity_instance) -> None:
@@ -219,8 +217,13 @@ def generate_space(
else:
assert space_polygon
bm = spatial.get_bmesh_from_polygon(space_polygon, h=h, polygon_is_si=True)
mesh = spatial.get_named_mesh_from_bmesh(name="Space", bmesh=bm)
if element and element.is_a("IfcSpace"):
spatial.set_space_representation_from_polygon(active_obj, element, space_polygon, h, polygon_is_si=True)
mesh = spatial.get_transformed_mesh_from_local_to_global(mesh)
spatial.edit_active_space_obj_from_mesh(mesh)
spatial.translate_obj_to_z_location(active_obj, z)
else:
if relating_type:
@@ -228,13 +231,12 @@ def generate_space(
else:
name = "Space"
obj = spatial.create_object(name)
obj = spatial.get_named_obj_from_mesh(name, mesh)
spatial.set_obj_origin_to_cursor_position_and_zero_elevation(obj)
spatial.translate_obj_to_z_location(obj, z)
spatial.assign_ifcspace_class_to_obj(obj)
element = ifc.get_entity(obj)
spatial.set_space_representation_from_polygon(obj, element, space_polygon, h, polygon_is_si=True)
if relating_type:
spatial.assign_relating_type_to_element(ifc, type, element, relating_type)
@@ -253,16 +255,16 @@ def generate_spaces_from_walls(
for i, linear_ring in enumerate(union.interiors):
poly = spatial.get_buffered_poly_from_linear_ring(linear_ring)
bm = spatial.get_bmesh_from_polygon(poly, h, polygon_is_si=False)
name = "Space" + str(i)
obj = spatial.create_object(name)
spatial.set_obj_origin_to_polygon_center(obj, poly, polygon_is_si=False)
obj = spatial.get_named_obj_from_bmesh(name, bmesh=bm)
spatial.set_obj_origin_to_bboxcenter_and_zero_elevation(obj)
spatial.translate_obj_to_z_location(obj, z)
spatial.assign_ifcspace_class_to_obj(obj)
element = ifc.get_entity(obj)
spatial.set_space_representation_from_polygon(obj, element, poly, h, polygon_is_si=False)
def toggle_space_visibility(ifc: type[tool.Ifc], spatial: type[tool.Spatial]) -> None:
model = ifc.get()
+13 -6
View File
@@ -999,12 +999,14 @@ class Spatial:
def get_purged_inner_holes_poly(cls, union_geom, min_area): pass
def get_poly_valid_interior_list(cls, poly, min_area, interiors_list): pass
def get_buffered_poly_from_linear_ring(cls, linear_ring): pass
def get_2d_vertices_from_polygon(cls, poly, obj, polygon_is_si=True): pass
def set_extrusion_representation_from_polygon(cls, obj, element, poly, depth_ifc, polygon_is_si=True): pass
def set_space_representation_from_polygon(cls, obj, element, poly, h, polygon_is_si=True): pass
def set_covering_representation_from_polygon(cls, obj, poly, polygon_is_si=True): pass
def create_object(cls, name): pass
def set_obj_origin_to_polygon_center(cls, obj, poly, polygon_is_si=True): pass
def get_bmesh_from_polygon(cls, poly, h, polygon_is_si=False): pass
def get_named_obj_from_bmesh(cls, name, bmesh): pass
def get_named_obj_from_mesh(cls, name, mesh): pass
def get_named_mesh_from_bmesh(cls, name, bmesh): pass
def get_transformed_mesh_from_local_to_global(cls, mesh): pass
def edit_active_space_obj_from_mesh(cls, mesh): pass
def set_obj_origin_to_bboxcenter(cls, obj): pass
def set_obj_origin_to_bboxcenter_and_zero_elevation(cls, obj): pass
def set_obj_origin_to_cursor_position_and_zero_elevation(cls, obj): pass
def get_selected_objects(cls): pass
def get_active_obj(cls): pass
@@ -1012,9 +1014,14 @@ class Spatial:
def get_active_obj_height(cls): pass
def get_relating_type_id(cls): pass
def translate_obj_to_z_location(cls, obj, z): pass
def get_2d_vertices_from_obj(cls, obj): pass
def get_scaled_2d_vertices(cls, points): pass
def assign_swept_area_outer_curve_from_2d_vertices(cls, obj, vertices): pass
def get_body_representation(cls, obj): pass
def assign_ifcspace_class_to_obj(cls, obj): pass
def assign_type_to_obj(cls, obj): pass
def assign_relating_type_to_element(cls, ifc, type, element, relating_type): pass
def regen_obj_representation(cls, obj, body): pass
def toggle_spaces_visibility_wired_and_textured(cls, spaces): pass
def toggle_hide_spaces(cls, spaces): pass
def set_default_container(cls, container): pass
+9 -28
View File
@@ -49,40 +49,21 @@ class Aggregate(bonsai.core.tool.Aggregate):
related_object = tool.Ifc.get_entity(related_obj)
if not relating_object or not related_object:
return False
if relating_object == related_object:
return False
is_compatible_class = False
if (relating_object.is_a("IfcElement") or relating_object.is_a("IfcElementType")) and related_object.is_a(
"IfcElement"
):
is_compatible_class = True
elif tool.Ifc.get_schema() == "IFC2X3":
return True
if tool.Ifc.get_schema() == "IFC2X3":
if relating_object.is_a("IfcSpatialStructureElement") and related_object.is_a("IfcSpatialStructureElement"):
is_compatible_class = True
elif relating_object.is_a("IfcProject") and related_object.is_a("IfcSpatialStructureElement"):
is_compatible_class = True
return True
if relating_object.is_a("IfcProject") and related_object.is_a("IfcSpatialStructureElement"):
return True
else:
if relating_object.is_a("IfcSpatialElement") and related_object.is_a("IfcSpatialElement"):
is_compatible_class = True
elif relating_object.is_a("IfcProject") and related_object.is_a("IfcSpatialElement"):
is_compatible_class = True
if not is_compatible_class:
return False
# Prevent cyclic references: walk up the full hierarchy from the
# proposed parent and reject if we encounter the proposed child.
ancestor = ifcopenshell.util.element.get_parent(relating_object)
seen = {relating_object}
while ancestor:
if ancestor == related_object:
return False
if ancestor in seen:
break
seen.add(ancestor)
ancestor = ifcopenshell.util.element.get_parent(ancestor)
return True
return True
if relating_object.is_a("IfcProject") and related_object.is_a("IfcSpatialElement"):
return True
return False
@classmethod
def has_physical_body_representation(cls, element: ifcopenshell.entity_instance) -> bool:
-2
View File
@@ -857,8 +857,6 @@ class Drawing(bonsai.core.tool.Drawing):
@classmethod
def edit_text_literals(cls, obj: bpy.types.Object, literal_attributes: dict) -> None:
if not literal_attributes:
return
assert (element := tool.Ifc.get_entity(obj))
assert (rep := cls.get_annotation_representation(element))
to_remove = [i for i in rep.Items if i.is_a("IfcTextLiteral")]
+3 -4
View File
@@ -1407,6 +1407,8 @@ class Geometry(bonsai.core.tool.Geometry):
:param representation_item: item to remove.
:param element: item's element. Is used to unmark manual booleans.
"""
# NOTE: we assume it's not the last representation item
# otherwise we probably would need to remove representation too
# NOTE: a lot of shared code with `geometry.remove_representation`
ifc_file = tool.Ifc.get()
shape_aspects: list[ifcopenshell.entity_instance] = []
@@ -1465,10 +1467,7 @@ class Geometry(bonsai.core.tool.Geometry):
cls.remove_representation_items_from_shape_aspect([representation_item], shape_aspect)
if representation:
new_items = tuple(set(representation.Items) - {representation_item})
if not new_items:
return
representation.Items = new_items
representation.Items = tuple(set(representation.Items) - {representation_item})
also_consider = list(consider_inverses)
ifcopenshell.util.element.remove_deep2(ifc_file, representation_item, also_consider=also_consider)
+4 -6
View File
@@ -197,16 +197,12 @@ class Ifc(bonsai.core.tool.Ifc):
if not cls.get():
return
# Clear all per-object msgbus subscriptions at once using the dedicated
# owner. After undo/redo, per-object Python wrappers have new
# identities so clearing by individual obj would miss stale
# subscriptions registered with the old wrappers.
bpy.msgbus.clear_by_owner(bonsai.bim.handler.object_subscription_owner)
for obj in bpy.data.objects:
if obj.library:
continue
bpy.msgbus.clear_by_owner(obj)
element = cls.get_entity(obj)
if not element:
continue
@@ -221,6 +217,8 @@ class Ifc(bonsai.core.tool.Ifc):
if obj.library:
continue
bpy.msgbus.clear_by_owner(obj)
style = cls.get_entity(obj)
if not style:
continue
+3 -17
View File
@@ -45,23 +45,9 @@ class Nest(bonsai.core.tool.Nest):
related_object = tool.Ifc.get_entity(related_obj)
if not relating_object or not related_object:
return False
if relating_object == related_object:
return False
is_compatible_class = relating_object.is_a("IfcElement") and related_object.is_a("IfcElement")
if not is_compatible_class:
return False
# Prevent cyclic references: walk up the full hierarchy from the
# proposed parent and reject if we encounter the proposed child.
ancestor = ifcopenshell.util.element.get_parent(relating_object)
seen = {relating_object}
while ancestor:
if ancestor == related_object:
return False
if ancestor in seen:
break
seen.add(ancestor)
ancestor = ifcopenshell.util.element.get_parent(ancestor)
return True
if relating_object.is_a("IfcElement") and related_object.is_a("IfcElement"):
return True
return False
@classmethod
def disable_editing(cls, obj: bpy.types.Object) -> None:
+152 -124
View File
@@ -21,13 +21,13 @@ from __future__ import annotations
import json
from collections import defaultdict
from collections.abc import Generator, Iterable
from math import pi
from typing import TYPE_CHECKING, Any, Literal, Optional, Union
import bmesh
import bpy
import ifcopenshell
import ifcopenshell.api.attribute
import ifcopenshell.api.geometry
import ifcopenshell.api.type
import ifcopenshell.geom
import ifcopenshell.util.classification
@@ -200,6 +200,7 @@ class Spatial(bonsai.core.tool.Spatial):
obj = tool.Ifc.get_object(product)
if obj and view_layer.objects.get(obj.name):
if unhide:
obj.hide_viewport = False
obj.hide_set(False)
obj.select_set(True)
@@ -991,107 +992,114 @@ class Spatial(bonsai.core.tool.Spatial):
return poly
@classmethod
def create_object(cls, name: str) -> bpy.types.Object:
mesh = bpy.data.meshes.new(name=name)
def get_bmesh_from_polygon(cls, poly: Polygon, h: float, polygon_is_si: bool = False) -> bmesh.types.BMesh:
"""
:param h: Height, in meters.
:param polygon_is_si: Should be True if `poly` is defined in meters.
"""
mat = Matrix()
bm = bmesh.new()
bm.verts.index_update()
bm.edges.index_update()
mat_invert = mat.inverted()
si_conversion = 1.0 if polygon_is_si else ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
new_verts = [
# bm.verts.new(mat_invert @ (Vector([v[0], v[1], 0]) * si_conversion)) for v in poly.exterior.coords[0:-1]
bm.verts.new(mat_invert @ (Vector([v[0], v[1], 0]) * si_conversion))
for v in shapely.get_exterior_ring(poly).coords[0:-1]
]
[bm.edges.new((new_verts[i], new_verts[i + 1])) for i in range(len(new_verts) - 1)]
bm.edges.new((new_verts[len(new_verts) - 1], new_verts[0]))
bm.verts.index_update()
bm.edges.index_update()
bmesh.ops.remove_doubles(bm, verts=bm.verts, dist=1e-5)
bmesh.ops.triangle_fill(bm, edges=bm.edges)
bmesh.ops.dissolve_limit(bm, angle_limit=pi / 180 * 5, verts=bm.verts, edges=bm.edges)
if h != 0:
extrusion = bmesh.ops.extrude_face_region(bm, geom=bm.faces)
extruded_verts = [g for g in extrusion["geom"] if isinstance(g, bmesh.types.BMVert)]
bmesh.ops.translate(bm, vec=[0.0, 0.0, h], verts=extruded_verts)
bmesh.ops.recalc_face_normals(bm, faces=bm.faces)
return bm
@classmethod
def get_named_obj_from_bmesh(cls, name: str, bmesh: bmesh.types.BMesh) -> bpy.types.Object:
mesh = cls.get_named_mesh_from_bmesh(name, bmesh)
obj = cls.get_named_obj_from_mesh(name, mesh)
return obj
@classmethod
def get_named_obj_from_mesh(cls, name: str, mesh: bpy.types.Mesh) -> bpy.types.Object:
obj = bpy.data.objects.new(name, mesh)
return obj
@classmethod
def set_obj_origin_to_polygon_center(
cls, obj: bpy.types.Object, poly: Polygon, polygon_is_si: bool = True
) -> None:
unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
centroid = poly.centroid
if polygon_is_si:
obj.location = Vector((centroid.x, centroid.y, 0))
else:
obj.location = Vector((centroid.x * unit_scale, centroid.y * unit_scale, 0))
def get_named_mesh_from_bmesh(cls, name: str, bmesh: bmesh.types.BMesh) -> bpy.types.Mesh:
mesh = bpy.data.meshes.new(name=name)
bmesh.to_mesh(mesh)
bmesh.free()
return mesh
@classmethod
def get_2d_vertices_from_polygon(
cls,
poly: Polygon,
obj: bpy.types.Object,
polygon_is_si: bool = True,
) -> list[list[float]]:
"""Convert a world-space shapely polygon to 2D vertices in obj's local space, in IFC file units.
:param poly: The polygon in world space.
:param obj: The Blender object whose local space is used.
:param polygon_is_si: True if polygon coords are in SI, False if in IFC file units.
:return: List of [x, y] coordinates (not closed).
"""
ifc_file = tool.Ifc.get()
unit_scale = ifcopenshell.util.unit.calculate_unit_scale(ifc_file)
bpy.context.view_layer.update()
mat_inv = obj.matrix_world.inverted()
coords_2d = []
for v in shapely.get_exterior_ring(poly).coords[:-1]:
world_si = Vector((v[0], v[1], 0))
if not polygon_is_si:
world_si = world_si * unit_scale
local_si = mat_inv @ world_si
coords_2d.append([local_si.x / unit_scale, local_si.y / unit_scale])
return coords_2d
def get_transformed_mesh_from_local_to_global(cls, mesh: bpy.types.Mesh) -> bpy.types.Mesh:
active_obj = cls.get_active_obj()
mat = active_obj.matrix_world
mesh.transform(mat.inverted())
mesh.update()
return mesh
@classmethod
def set_extrusion_representation_from_polygon(
cls,
obj: bpy.types.Object,
element: ifcopenshell.entity_instance,
poly: Polygon,
depth_ifc: float,
polygon_is_si: bool = True,
) -> None:
"""Create or replace the IFC body representation from a polygon extrusion.
:param obj: The Blender object.
:param element: The IFC product entity.
:param poly: The polygon in world space.
:param depth_ifc: The extrusion depth in IFC file units.
:param polygon_is_si: True if polygon coords are in SI, False if in IFC file units.
"""
ifc_file = tool.Ifc.get()
builder = ifcopenshell.util.shape_builder.ShapeBuilder(ifc_file)
coords_2d = cls.get_2d_vertices_from_polygon(poly, obj, polygon_is_si)
curve = builder.polyline(coords_2d, closed=True)
item = builder.extrude(curve, magnitude=depth_ifc)
old_body = ifcopenshell.util.representation.get_representation(element, "Model", "Body", "MODEL_VIEW")
if old_body:
context = old_body.ContextOfItems
ifcopenshell.api.geometry.unassign_representation(ifc_file, product=element, representation=old_body)
ifcopenshell.api.geometry.remove_representation(ifc_file, representation=old_body)
else:
context = ifcopenshell.util.representation.get_context(ifc_file, "Model", "Body", "MODEL_VIEW")
new_body = builder.get_representation(context, item)
ifcopenshell.api.geometry.assign_representation(ifc_file, product=element, representation=new_body)
bonsai.core.geometry.switch_representation(
tool.Ifc,
tool.Geometry,
obj=obj,
representation=new_body,
)
def edit_active_space_obj_from_mesh(cls, mesh: bpy.types.Mesh) -> None:
active_obj = bpy.context.active_object
old_mesh = active_obj.data
old_mesh_name = old_mesh.name
assert active_obj and isinstance(old_mesh, bpy.types.Mesh)
tool.Geometry.get_mesh_props(mesh).ifc_definition_id = tool.Geometry.get_mesh_props(old_mesh).ifc_definition_id
tool.Geometry.change_object_data(active_obj, mesh, is_global=True)
tool.Ifc.edit(active_obj)
tool.Blender.remove_data_block(old_mesh)
# Rename after old mesh is removed to avoid .001 suffix.
mesh.name = old_mesh_name
@classmethod
def set_space_representation_from_polygon(
cls,
obj: bpy.types.Object,
element: ifcopenshell.entity_instance,
poly: Polygon,
h: float,
polygon_is_si: bool = True,
) -> None:
"""Create or replace the IFC body representation of a space from a polygon.
def set_obj_origin_to_bboxcenter(cls, obj: bpy.types.Object) -> None:
mat = obj.matrix_world
inverted = mat.inverted()
local_bbox_center = 0.125 * sum((Vector(b) for b in obj.bound_box), Vector())
global_bbox_center = mat @ local_bbox_center
:param h: The height in SI (meters).
"""
unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
cls.set_extrusion_representation_from_polygon(obj, element, poly, h / unit_scale, polygon_is_si)
oldLoc = obj.location
newLoc = global_bbox_center
diff = newLoc - oldLoc
for vert in obj.data.vertices:
aux_vector = mat @ vert.co
aux_vector = aux_vector - diff
vert.co = inverted @ aux_vector
obj.location = newLoc
@classmethod
def set_obj_origin_to_bboxcenter_and_zero_elevation(cls, obj: bpy.types.Object) -> None:
mat = obj.matrix_world
inverted = mat.inverted()
local_bbox_center = 0.125 * sum((Vector(b) for b in obj.bound_box), Vector())
global_bbox_center = mat @ local_bbox_center
global_obj_origin = global_bbox_center
global_obj_origin.z = 0
oldLoc = obj.location
newLoc = global_obj_origin
diff = newLoc - oldLoc
for vert in obj.data.vertices:
aux_vector = mat @ vert.co
aux_vector = aux_vector - diff
vert.co = inverted @ aux_vector
obj.location = newLoc
@classmethod
def set_obj_origin_to_cursor_position_and_zero_elevation(cls, obj: bpy.types.Object) -> None:
@@ -1138,53 +1146,65 @@ class Spatial(bonsai.core.tool.Spatial):
if z != 0:
obj.location = obj.location + Vector((0, 0, z))
@classmethod
def get_2d_vertices_from_obj(cls, obj: bpy.types.Object) -> list[tuple]:
points = []
vectors = [v.co for v in obj.data.vertices.values()]
for vector in vectors:
points.append(vector.xy)
points.append(vectors[0].xy)
return points
@classmethod
def get_scaled_2d_vertices(cls, points: list[Vector]) -> list[tuple[float, float]]:
model = tool.Ifc.get()
unit_scale = ifcopenshell.util.unit.calculate_unit_scale(model)
_points = []
for p in points:
_p = list(p)
_p[0] /= unit_scale
_p[1] /= unit_scale
_points.append(_p)
return _points
@classmethod
def assign_swept_area_outer_curve_from_2d_vertices(cls, obj: bpy.types.Object, vertices: list[Vector]) -> None:
body = cls.get_body_representation(obj)
model = tool.Ifc.get()
extrusion = tool.Model.get_extrusion(body)
area = extrusion.SweptArea
old_area = area.OuterCurve
builder = ifcopenshell.util.shape_builder.ShapeBuilder(model)
outer_curve = builder.polyline(vertices, closed=True)
area.OuterCurve = outer_curve
ifcopenshell.util.element.remove_deep2(tool.Ifc.get(), old_area)
@classmethod
def get_body_representation(cls, obj: bpy.types.Object) -> Union[ifcopenshell.entity_instance, None]:
element = tool.Ifc.get_entity(obj)
return ifcopenshell.util.representation.get_representation(element, "Model", "Body", "MODEL_VIEW")
@classmethod
def assign_ifcspace_class_to_obj(cls, obj: bpy.types.Object) -> None:
bonsai.core.root.assign_class(
tool.Ifc,
tool.Collector,
tool.Root,
obj=obj,
ifc_class="IfcSpace",
should_add_representation=False,
)
bpy.ops.bim.assign_class(obj=obj.name, ifc_class="IfcSpace")
@classmethod
def assign_type_to_obj(cls, obj: bpy.types.Object) -> None:
# TODO this code looks in the wrong spot and suspicious
props = tool.Model.get_model_props()
ifc_file = tool.Ifc.get()
relating_type_id = props.relating_type_id
relating_type = ifc_file.by_id(int(relating_type_id))
relating_type = tool.Ifc.get().by_id(int(relating_type_id))
ifc_class = relating_type.is_a()
instance_class = ifcopenshell.util.type.get_applicable_entities(ifc_class, ifc_file.schema)[0]
bonsai.core.root.assign_class(
tool.Ifc,
tool.Collector,
tool.Root,
obj=obj,
ifc_class=instance_class,
should_add_representation=False,
)
bpy.ops.bim.assign_class(obj=obj.name, ifc_class=instance_class)
element = tool.Ifc.get_entity(obj)
assert element
ifcopenshell.api.type.assign_type(ifc_file, related_objects=[element], relating_type=relating_type)
@classmethod
def set_covering_representation_from_polygon(
cls,
obj: bpy.types.Object,
poly: Polygon,
polygon_is_si: bool = True,
) -> None:
"""Create the covering body representation from a polygon, extruded by the type's material layer thickness."""
element = tool.Ifc.get_entity(obj)
relating_type = ifcopenshell.util.element.get_type(element)
material = ifcopenshell.util.element.get_material(relating_type, should_skip_usage=True)
depth = 0.0
if material and material.is_a("IfcMaterialLayerSet"):
depth = sum(layer.LayerThickness for layer in material.MaterialLayers)
cls.set_extrusion_representation_from_polygon(obj, element, poly, depth, polygon_is_si)
@classmethod
def assign_relating_type_to_element(
cls,
@@ -1195,6 +1215,14 @@ class Spatial(bonsai.core.tool.Spatial):
) -> None:
bonsai.core.type.assign_type(ifc, tool.Model, type, element=element, type=relating_type)
@classmethod
def regen_obj_representation(cls, obj: bpy.types.Object, body: ifcopenshell.entity_instance) -> None:
bonsai.core.geometry.switch_representation(
tool.Ifc,
tool.Geometry,
obj=obj,
representation=body,
)
@classmethod
def set_space_visibility(cls, is_visible: bool) -> None:
+3 -108
View File
@@ -2,7 +2,7 @@
Feature: Covering
Covers covering tool.
Scenario: Add flooring from walls
Scenario: Execute generate flooring coverings from walls
Given an empty IFC project
And I load the demo construction library
And I set "scene.BIMModelProperties.ifc_class" to "IfcWallType"
@@ -24,9 +24,9 @@ Scenario: Add flooring from walls
And the cursor is at "0,2.0,0"
And I set "scene.BIMModelProperties.length" to "1.9"
And I press "bim.add_occurrence"
# Set COV30 predefined type to FLOORING.
# add_instance_flooring_coverings_from_walls is expecting FLOORING predefined type.
And the object "IfcCoveringType/COV30" is selected
And I look at the "Object Attributes" panel
And I look at the "Attributes" panel
And I click "Edit"
And I set the "PredefinedType" property to "FLOORING"
And I click "Save Attributes"
@@ -42,108 +42,3 @@ Scenario: Add flooring from walls
Then the object "IfcCovering/Covering0" exists
And the object "IfcCovering/Covering0" is at "1.8,1.05,0.0"
And the object "IfcCovering/Covering0" dimensions are "3.4,1.9,0.03"
Scenario: Add ceiling from walls
Given an empty IFC project
And I load the demo construction library
And I set "scene.BIMModelProperties.ifc_class" to "IfcWallType"
And the variable "element_type" is "[e for e in {ifc}.by_type('IfcWallType') if e.Name == 'WAL100'][0].id()"
And I set "scene.BIMModelProperties.relating_type_id" to "{element_type}"
And I press "bim.add_occurrence"
And the object "IfcWall/Wall" is selected
And I press "bim.change_layer_length(length=3.6)"
And the cursor is at "3.6,0.1,3"
And I set "scene.BIMModelProperties.length" to "2.0"
And I press "bim.add_occurrence"
And the cursor is at "3.5,2.1,3"
And I set "scene.BIMModelProperties.length" to "3.5"
And I press "bim.add_occurrence"
And the cursor is at "0,2.0,0"
And I set "scene.BIMModelProperties.length" to "1.9"
And I press "bim.add_occurrence"
# Set COV30 predefined type to CEILING.
And the object "IfcCoveringType/COV30" is selected
And I look at the "Object Attributes" panel
And I click "Edit"
And I set the "PredefinedType" property to "CEILING"
And I click "Save Attributes"
# Run the operator with ceiling height = 2.7 (default).
When the object "IfcWall/Wall" is selected
And additionally the object "IfcWall/Wall.001" is selected
And additionally the object "IfcWall/Wall.002" is selected
And additionally the object "IfcWall/Wall.003" is selected
And I set "scene.BIMModelProperties.ifc_class" to "IfcCoveringType"
And the variable "element_type" is "[e for e in {ifc}.by_type('IfcCoveringType') if e.Name == 'COV30'][0].id()"
And I set "scene.BIMModelProperties.relating_type_id" to "{element_type}"
And I press "bim.add_instance_ceiling_coverings_from_walls"
Then the object "IfcCovering/Covering0" exists
And the object "IfcCovering/Covering0" is at "1.8,1.05,2.7"
And the object "IfcCovering/Covering0" dimensions are "3.4,1.9,0.03"
Scenario: Add flooring from cursor
Given an empty IFC project
And I load the demo construction library
And I set "scene.BIMModelProperties.ifc_class" to "IfcWallType"
And the variable "element_type" is "[e for e in {ifc}.by_type('IfcWallType') if e.Name == 'WAL100'][0].id()"
And I set "scene.BIMModelProperties.relating_type_id" to "{element_type}"
And I press "bim.add_occurrence"
And the cursor is at "1.1,0,0"
And I press "bim.add_occurrence"
And the object "IfcWall/Wall.001" is selected
And I press "bim.hotkey(hotkey='S_R')"
And the cursor is at "0,.9,0"
And I press "bim.add_occurrence"
And the cursor is at "-1,0,0"
And I press "bim.add_occurrence"
And the object "IfcWall/Wall.003" is selected
And I press "bim.hotkey(hotkey='S_R')"
And the object "IfcWall/Wall.003" is moved to "0,0,0"
# Set COV30 predefined type to FLOORING.
And the object "IfcCoveringType/COV30" is selected
And I look at the "Object Attributes" panel
And I click "Edit"
And I set the "PredefinedType" property to "FLOORING"
And I click "Save Attributes"
# Generate covering from cursor inside the room.
When the cursor is at "0.5,0.5,0"
And I deselect all objects
And I set "scene.BIMModelProperties.ifc_class" to "IfcCoveringType"
And the variable "element_type" is "[e for e in {ifc}.by_type('IfcCoveringType') if e.Name == 'COV30'][0].id()"
And I set "scene.BIMModelProperties.relating_type_id" to "{element_type}"
And I press "bim.add_instance_flooring_covering_from_cursor"
Then the object "IfcCovering/Covering" exists
And the object "IfcCovering/Covering" dimensions are "1,0.8,0.03"
Scenario: Add ceiling from cursor
Given an empty IFC project
And I load the demo construction library
And I set "scene.BIMModelProperties.ifc_class" to "IfcWallType"
And the variable "element_type" is "[e for e in {ifc}.by_type('IfcWallType') if e.Name == 'WAL100'][0].id()"
And I set "scene.BIMModelProperties.relating_type_id" to "{element_type}"
And I press "bim.add_occurrence"
And the cursor is at "1.1,0,0"
And I press "bim.add_occurrence"
And the object "IfcWall/Wall.001" is selected
And I press "bim.hotkey(hotkey='S_R')"
And the cursor is at "0,.9,0"
And I press "bim.add_occurrence"
And the cursor is at "-1,0,0"
And I press "bim.add_occurrence"
And the object "IfcWall/Wall.003" is selected
And I press "bim.hotkey(hotkey='S_R')"
And the object "IfcWall/Wall.003" is moved to "0,0,0"
# Set COV30 predefined type to CEILING.
And the object "IfcCoveringType/COV30" is selected
And I look at the "Object Attributes" panel
And I click "Edit"
And I set the "PredefinedType" property to "CEILING"
And I click "Save Attributes"
# Generate covering from cursor inside the room.
When the cursor is at "0.5,0.5,0"
And I deselect all objects
And I set "scene.BIMModelProperties.ifc_class" to "IfcCoveringType"
And the variable "element_type" is "[e for e in {ifc}.by_type('IfcCoveringType') if e.Name == 'COV30'][0].id()"
And I set "scene.BIMModelProperties.relating_type_id" to "{element_type}"
And I press "bim.add_instance_ceiling_covering_from_cursor"
Then the object "IfcCovering/Covering" exists
And the object "IfcCovering/Covering" dimensions are "1,0.8,0.03"
-37
View File
@@ -18,7 +18,6 @@
import bpy
import ifcopenshell
import ifcopenshell.api.aggregate
import ifcopenshell.api.context
import ifcopenshell.api.geometry
import ifcopenshell.api.root
@@ -100,42 +99,6 @@ class TestCanAggregate(NewFile):
subelement_obj = bpy.data.objects.new("Object", None)
assert subject.can_aggregate(element_obj, subelement_obj) is False
def test_element_cannot_aggregate_to_itself(self):
ifc = ifcopenshell.file()
tool.Ifc.set(ifc)
element = ifc.createIfcElementAssembly()
element_obj = bpy.data.objects.new("Object", None)
tool.Ifc.link(element, element_obj)
assert subject.can_aggregate(element_obj, element_obj) is False
def test_cyclic_aggregation_is_prevented(self):
ifc = ifcopenshell.file()
tool.Ifc.set(ifc)
assembly_a = ifc.createIfcElementAssembly()
assembly_a_obj = bpy.data.objects.new("AssemblyA", None)
tool.Ifc.link(assembly_a, assembly_a_obj)
beam = ifc.createIfcBeam()
beam_obj = bpy.data.objects.new("Beam", None)
tool.Ifc.link(beam, beam_obj)
ifcopenshell.api.aggregate.assign_object(ifc, products=[beam], relating_object=assembly_a)
assert subject.can_aggregate(beam_obj, assembly_a_obj) is False
def test_deep_cyclic_aggregation_is_prevented(self):
ifc = ifcopenshell.file()
tool.Ifc.set(ifc)
assembly_a = ifc.createIfcElementAssembly()
assembly_a_obj = bpy.data.objects.new("AssemblyA", None)
tool.Ifc.link(assembly_a, assembly_a_obj)
assembly_b = ifc.createIfcElementAssembly()
assembly_b_obj = bpy.data.objects.new("AssemblyB", None)
tool.Ifc.link(assembly_b, assembly_b_obj)
beam = ifc.createIfcBeam()
beam_obj = bpy.data.objects.new("Beam", None)
tool.Ifc.link(beam, beam_obj)
ifcopenshell.api.aggregate.assign_object(ifc, products=[assembly_b], relating_object=assembly_a)
ifcopenshell.api.aggregate.assign_object(ifc, products=[beam], relating_object=assembly_b)
assert subject.can_aggregate(beam_obj, assembly_a_obj) is False
class TestHasPhysicalBodyRepresentation(NewFile):
def test_run(self):
-37
View File
@@ -19,7 +19,6 @@
import bpy
import ifcopenshell
import ifcopenshell.api
import ifcopenshell.api.nest
import ifcopenshell.api.spatial
import bonsai.core.tool
@@ -52,42 +51,6 @@ class TestCanNest(NewFile):
subelement_obj = bpy.data.objects.new("Object", None)
assert subject.can_nest(element_obj, subelement_obj) is False
def test_element_cannot_nest_to_itself(self):
ifc = ifcopenshell.file()
tool.Ifc.set(ifc)
element = ifc.createIfcWall()
element_obj = bpy.data.objects.new("Object", None)
tool.Ifc.link(element, element_obj)
assert subject.can_nest(element_obj, element_obj) is False
def test_cyclic_nesting_is_prevented(self):
ifc = ifcopenshell.file()
tool.Ifc.set(ifc)
wall_a = ifc.createIfcWall()
wall_a_obj = bpy.data.objects.new("WallA", None)
tool.Ifc.link(wall_a, wall_a_obj)
wall_b = ifc.createIfcWall()
wall_b_obj = bpy.data.objects.new("WallB", None)
tool.Ifc.link(wall_b, wall_b_obj)
ifcopenshell.api.nest.assign_object(ifc, related_objects=[wall_b], relating_object=wall_a)
assert subject.can_nest(wall_b_obj, wall_a_obj) is False
def test_deep_cyclic_nesting_is_prevented(self):
ifc = ifcopenshell.file()
tool.Ifc.set(ifc)
wall_a = ifc.createIfcWall()
wall_a_obj = bpy.data.objects.new("WallA", None)
tool.Ifc.link(wall_a, wall_a_obj)
wall_b = ifc.createIfcWall()
wall_b_obj = bpy.data.objects.new("WallB", None)
tool.Ifc.link(wall_b, wall_b_obj)
wall_c = ifc.createIfcWall()
wall_c_obj = bpy.data.objects.new("WallC", None)
tool.Ifc.link(wall_c, wall_c_obj)
ifcopenshell.api.nest.assign_object(ifc, related_objects=[wall_b], relating_object=wall_a)
ifcopenshell.api.nest.assign_object(ifc, related_objects=[wall_c], relating_object=wall_b)
assert subject.can_nest(wall_c_obj, wall_a_obj) is False
class TestDisableEditing(NewFile):
def test_run(self):
-1
View File
@@ -28,7 +28,6 @@ dist:
mkdir -p dist
cp -r $(PACKAGE_NAME) build/
cp pyproject.toml build/
if [ -f README.md ]; then cp README.md build/; fi
ifeq ($(IS_STABLE), TRUE)
$(SED) 's/version = "0.0.0"/version = "$(VERSION)"/' build/pyproject.toml
ifdef IS_MODULE
@@ -51,10 +51,8 @@ def remove_context(file: ifcopenshell.file, context: ifcopenshell.entity_instanc
new = context.ParentContext
for inverse in file.get_inverse(context):
if inverse.is_a("IfcCoordinateOperation"):
# Trick to make sure the coordinate operation is not referenced
# by a context so we can delete it safely
inverse.SourceCRS = inverse.TargetCRS
ifcopenshell.util.element.remove_deep2(file, inverse)
ifcopenshell.util.element.remove_deep(file, inverse)
else:
ifcopenshell.util.element.replace_attribute(inverse, context, new)
file.remove(context)
@@ -59,6 +59,6 @@ def edit_cost_value(
value["ValueComponent"],
)
value = file.create_entity("IfcMeasureWithUnit", value_component, value["UnitComponent"])
if old_unit_basis:
ifcopenshell.util.element.remove_deep2(file, old_unit_basis)
if old_unit_basis and file.get_total_inverses(old_unit_basis) == 0:
ifcopenshell.util.element.remove_deep(file, old_unit_basis)
setattr(cost_value, name, value)
@@ -22,13 +22,11 @@ import ifcopenshell.util.element
def remove_feature(file: ifcopenshell.file, feature: ifcopenshell.entity_instance) -> None:
"""Permanently delete a feature element and its void or projection relationship.
"""Remove a feature
The feature entity (e.g. IfcOpeningElement) is removed from the model
along with its IfcRelVoidsElement or IfcRelProjectsElement relationship.
The host element (wall, slab, etc.) is unaffected. Any fillings (windows,
doors) that occupied the opening become orphaned and must be separately
deleted via root.remove_product.
Fillings are retained as orphans. Featured elements remain. Features
cannot exist by themselves, so not only is the relationship removed, the
feature is also removed.
:param feature: The IfcFeatureElement to remove.
@@ -31,6 +31,17 @@ def add_boolean(
) -> list[ifcopenshell.entity_instance]:
"""Adds a boolean operation to two or more representation items
If an IfcBooleanOperand is part of the top level items in an
IfcShapeRepresentation, it will be removed from that level whilst being
added to the IfcBooleanResult. This is because it is generally intuitive
that an item is either participating in a boolean operation, or being an
item in its own right, but not both.
However, if an IfcBooleanOperand is part of another boolean operation
already, it will not be removed from the existing operation. A new
operation will be created, and therefore it will participate in two
operations.
This function protects against recursive booleans.
After a boolean operation is made, since the items of
@@ -90,6 +101,9 @@ def add_boolean(
booleans = []
for second_item in second_items:
for inverse in file.get_inverse(second_item):
if inverse.is_a("IfcShapeRepresentation"):
inverse.Items = list(set(inverse.Items) - {second_item})
if first.is_a("IfcTesselatedFaceSet"):
first.Closed = True # For now, trust the user to do the right thing.
if second_item.is_a("IfcTesselatedFaceSet"):
@@ -47,9 +47,7 @@ def add_wall_representation(
:param thickness: The thickness of the wall in meters.
:param x_angle: The slope angle along the wall's X-axis, in radians.
:param clippings: List of clipping definitions. Clippings can be `Clipping` objects
or dictionaries of arguments for `Clipping.parse`. Each clipping has a
``normal`` that points toward the removed material (the discarded side),
not toward the kept material; see :func:`clip_solid` for details.
or dictionaries of arguments for `Clipping.parse`.
:param booleans: List of any existing IfcBooleanResults.
:return: IfcShapeRepresentation.
"""
@@ -52,11 +52,10 @@ def edit_object_placement(
:param is_si: If True, the matrix is given in SI units. If false, in
project units.
:param should_transform_children: A child element is a nested element,
opening, filling, etc. If True, child elements move along with the
parent; pass True when moving an assembly (roof, furniture group, etc.)
and you want all children to follow. If False (default), child elements
keep their current world positions; their local placements are rewritten
to compensate for the parent move.
opening, filling, etc. If true, child elements will move along with the
parent. If false, child elements will stay where they are. Because most
placements in IFC are relative, this means that if a child moves, we
actually don't change their placement.
:return: The new or updated IfcLocalPlacement entity
"""
usecase = Usecase()
@@ -69,12 +69,6 @@ def regenerate_wall_representation(
additional extrusions are generated for each connection that boolean
difference the base extrusion.
Clippings applied via :func:`geometry.clip_solid` or
:func:`geometry.clip_solid_bounded` are preserved only if the ``element``
parameter was passed when creating them, which registers the result in the
``BBIM_Boolean`` property set. Clippings created without that parameter
are silently discarded during regeneration.
This will also update the axis line representation (e.g. trim the axis line
to any connections).
@@ -83,7 +83,6 @@ def validate_type(
if remaining_items:
ifcopenshell.api.geometry.add_boolean(file, preferred_item, remaining_items, "UNION")
representation.Items = [i for i in representation.Items if i not in remaining_items]
representation.RepresentationType = ifcopenshell.util.representation.guess_type(representation.Items)
if representation.RepresentationType == "CSG":
@@ -42,5 +42,7 @@ def remove_grid_axis(file: ifcopenshell.file, axis: ifcopenshell.entity_instance
ifcopenshell.api.grid.remove_grid_axis(model, axis=axis_2)
"""
axis_curve = axis.AxisCurve
if file.get_total_inverses(axis_curve) == 1:
ifcopenshell.util.element.remove_deep(file, axis_curve)
file.remove(axis_curve)
file.remove(axis)
ifcopenshell.util.element.remove_deep2(file, axis_curve)
@@ -19,9 +19,7 @@
from typing import Union
import ifcopenshell
import ifcopenshell.api.aggregate
import ifcopenshell.api.owner
import ifcopenshell.api.spatial
import ifcopenshell.guid
import ifcopenshell.util.element
@@ -139,10 +137,7 @@ def assign_object(
if not objects_to_change:
return is_nested_by
# Can be either only nested, aggregated, or contained at the same time.
possibly_contained = [o for o in objects_without_nests if hasattr(o, "ContainedInStructure")]
ifcopenshell.api.spatial.unassign_container(file, products=possibly_contained)
ifcopenshell.api.aggregate.unassign_object(file, products=objects_without_nests)
# NOTE: An object can both be nested and assigned to a container or an aggregate.
# unassign elements from previous nests
for nests in previous_nests_rels:
@@ -22,9 +22,9 @@ import ifcopenshell.util.element
def remove_prop_template(file: ifcopenshell.file, prop_template: ifcopenshell.entity_instance) -> None:
"""Removes a property template
Note that a property set template should always have at least one property
template to be valid. So a property set template will not be removed if it
is the only template ina a property ste template.
Note that a property set template should always have at least one
property template to be valid, so take care when removing property
templates.
:param prop_template: The IfcSimplePropertyTemplate to remove.
:return: None
@@ -43,8 +43,10 @@ def remove_prop_template(file: ifcopenshell.file, prop_template: ifcopenshell.en
ifcopenshell.api.pset_template.remove_prop_template(model, prop_template=prop2)
"""
for inverse in file.get_inverse(prop_template):
if len(inverse.HasPropertyTemplates) > 1:
if len(inverse.HasPropertyTemplates) == 1:
inverse.HasPropertyTemplates = []
else:
has_property_templates = list(inverse.HasPropertyTemplates)
has_property_templates.remove(prop_template)
inverse.HasPropertyTemplates = has_property_templates
ifcopenshell.util.element.remove_deep2(file, prop_template)
ifcopenshell.util.element.remove_deep(file, prop_template)
@@ -38,4 +38,4 @@ def remove_pset_template(file: ifcopenshell.file, pset_template: ifcopenshell.en
# Let's remove the template.
ifcopenshell.api.pset_template.remove_pset_template(model, pset_template=template)
"""
ifcopenshell.util.element.remove_deep2(file, pset_template)
ifcopenshell.util.element.remove_deep(file, pset_template)
@@ -79,5 +79,5 @@ def add_resource_quantity(
old_quantity = resource.BaseQuantity
resource.BaseQuantity = quantity
if old_quantity:
ifcopenshell.util.element.remove_deep2(file, old_quantity)
ifcopenshell.util.element.remove_deep(file, old_quantity)
return quantity
@@ -47,4 +47,4 @@ def remove_resource_quantity(file: ifcopenshell.file, resource: ifcopenshell.ent
old_quantity = resource.BaseQuantity
resource.BaseQuantity = None
if old_quantity:
ifcopenshell.util.element.remove_deep2(file, old_quantity)
ifcopenshell.util.element.remove_deep(file, old_quantity)
@@ -26,7 +26,7 @@ def assign_process(
relating_process: ifcopenshell.entity_instance,
related_object: ifcopenshell.entity_instance,
) -> ifcopenshell.entity_instance:
"""Assigns an object as an input, control, or resource of a process
"""Assigns an object to be related to a process, typically a construction task
Processes work using the ICOM (Input, Controls, Outputs, Mechanisms)
paradigm in IFC. This process model is commonly used in modeling
@@ -63,17 +63,6 @@ def assign_process(
For resources, any construction resource may be assigned to a task.
.. warning::
This function creates an **Input** relationship
(``IfcRelAssignsToProcess``), meaning the product is *consumed* or
*operated on* by the task the typical case is demolition or
maintenance.
If the task *constructs or installs* a product (e.g. erecting a wall
or fitting a window), use :func:`assign_product` instead, which
creates an **Output** relationship (``IfcRelAssignsToProduct``).
:param relating_process: The IfcProcess (typically IfcTask) that the
input, control, or resource is related to.
:param related_object: The IfcProduct (for input), IfcCostItem (for
@@ -88,7 +77,7 @@ def assign_process(
# need to be part of a work schedule.
schedule = ifcopenshell.api.sequence.add_work_schedule(model, name="Construction Schedule A")
# Let's create a demolition task. Note that the predefined type is
# Let's create a construction task. Note that the predefined type is
# important to distinguish types of tasks.
task = ifcopenshell.api.sequence.add_task(model,
work_schedule=schedule, name="Demolish existing", identification="A", predefined_type="DEMOLITION")
@@ -96,12 +85,8 @@ def assign_process(
# Let's say we have a wall somewhere.
wall = ifcopenshell.api.root.create_entity(model, ifc_class="IfcWall")
# The wall is an INPUT to the demolition task (it will be consumed).
# Let's demolish that wall!
ifcopenshell.api.sequence.assign_process(model, relating_process=task, related_object=wall)
# For a construction task that BUILDS a wall, use assign_product instead:
# build_task = ifcopenshell.api.sequence.add_task(model, ..., predefined_type="CONSTRUCTION")
# ifcopenshell.api.sequence.assign_product(model, relating_product=wall, related_object=build_task)
"""
if related_object.HasAssignments:
for assignment in related_object.HasAssignments:
@@ -47,5 +47,4 @@ def remove_unit(file: ifcopenshell.file, unit: ifcopenshell.entity_instance) ->
unit_assignment.Units = units
else:
file.remove(unit_assignment)
# TODO handle other possible unit inverses
ifcopenshell.util.element.remove_deep2(file, unit)
ifcopenshell.util.element.remove_deep(file, unit)
@@ -1,72 +0,0 @@
# IfcOpenShell - IFC toolkit and geometry engine
# Copyright (C) 2021 Dion Moult <dion@thinkmoult.com>
#
# This file is part of IfcOpenShell.
#
# IfcOpenShell is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# IfcOpenShell is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with IfcOpenShell. If not, see <http://www.gnu.org/licenses/>.
import ifcopenshell.api.cost
import ifcopenshell.api.unit
import test.bootstrap
class TestEditCostValue(test.bootstrap.IFC4):
def test_editing_applied_value(self):
schedule = ifcopenshell.api.cost.add_cost_schedule(self.file)
item = ifcopenshell.api.cost.add_cost_item(self.file, cost_schedule=schedule)
value = ifcopenshell.api.cost.add_cost_value(self.file, parent=item)
ifcopenshell.api.cost.edit_cost_value(self.file, cost_value=value, attributes={"AppliedValue": 42.0})
assert value.AppliedValue.wrappedValue == 42.0
def test_editing_unit_basis_removes_old_deeply(self):
schedule = ifcopenshell.api.cost.add_cost_schedule(self.file)
item = ifcopenshell.api.cost.add_cost_item(self.file, cost_schedule=schedule)
value = ifcopenshell.api.cost.add_cost_value(self.file, parent=item)
unit = ifcopenshell.api.unit.add_si_unit(self.file, unit_type="LENGTHUNIT")
ifcopenshell.api.cost.edit_cost_value(
self.file,
cost_value=value,
attributes={"UnitBasis": {"ValueComponent": 1.0, "UnitComponent": unit}},
)
old_basis = value.UnitBasis
assert old_basis is not None
old_basis_id = old_basis.id()
# Now change to a new unit basis — the old one should be deeply removed.
ifcopenshell.api.cost.edit_cost_value(
self.file,
cost_value=value,
attributes={"UnitBasis": {"ValueComponent": 2.0, "UnitComponent": unit}},
)
assert value.UnitBasis is not None
assert value.UnitBasis.id() != old_basis_id
def test_clearing_unit_basis(self):
schedule = ifcopenshell.api.cost.add_cost_schedule(self.file)
item = ifcopenshell.api.cost.add_cost_item(self.file, cost_schedule=schedule)
value = ifcopenshell.api.cost.add_cost_value(self.file, parent=item)
unit = ifcopenshell.api.unit.add_si_unit(self.file, unit_type="LENGTHUNIT")
ifcopenshell.api.cost.edit_cost_value(
self.file,
cost_value=value,
attributes={"UnitBasis": {"ValueComponent": 1.0, "UnitComponent": unit}},
)
assert value.UnitBasis is not None
ifcopenshell.api.cost.edit_cost_value(
self.file, cost_value=value, attributes={"UnitBasis": None}
)
assert value.UnitBasis is None
class TestEditCostValueIFC4X3(test.bootstrap.IFC4X3, TestEditCostValue):
pass
@@ -42,7 +42,7 @@ class TestAddBoolean(test.bootstrap.IFC4):
assert boolean.FirstOperand == first
assert boolean.SecondOperand == second
assert boolean.Operator == "DIFFERENCE"
assert set(rep.Items) == {boolean, second}
assert set(rep.Items) == {boolean}
def test_adding_multiple_booleans_from_three_top_level_items(self):
ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcProject")
@@ -58,14 +58,13 @@ class TestAddBoolean(test.bootstrap.IFC4):
booleans = ifcopenshell.api.geometry.add_boolean(self.file, first, [second1, second2])
assert len(booleans) == 2
final_boolean = booleans[-1]
assert final_boolean.FirstOperand.is_a("IfcBooleanResult")
assert final_boolean.SecondOperand == second2
assert final_boolean.Operator == "DIFFERENCE"
assert final_boolean.FirstOperand.FirstOperand == first
assert final_boolean.FirstOperand.SecondOperand == second1
assert final_boolean.FirstOperand.Operator == "DIFFERENCE"
assert set(rep.Items) == {final_boolean, second1, second2}
assert len(rep.Items) == 1
assert rep.Items[0].FirstOperand.is_a("IfcBooleanResult")
assert rep.Items[0].SecondOperand == second2
assert rep.Items[0].Operator == "DIFFERENCE"
assert rep.Items[0].FirstOperand.FirstOperand == first
assert rep.Items[0].FirstOperand.SecondOperand == second1
assert rep.Items[0].FirstOperand.Operator == "DIFFERENCE"
def test_adding_a_boolean_to_an_existing_operand_from_a_top_level_item(self):
ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcProject")
@@ -79,16 +78,14 @@ class TestAddBoolean(test.bootstrap.IFC4):
second2 = builder.block()
rep = builder.get_representation(body, [first, second1])
booleans = ifcopenshell.api.geometry.add_boolean(self.file, first, [second1])
# second1 stays in Items, add second2 as well
rep.Items = list(rep.Items) + [second2]
booleans = ifcopenshell.api.geometry.add_boolean(self.file, first, [second2])
assert len(booleans) == 1
final_boolean = booleans[0]
assert final_boolean.FirstOperand.is_a("IfcBooleanResult")
assert final_boolean.SecondOperand == second2
assert final_boolean.FirstOperand.FirstOperand == first
assert final_boolean.FirstOperand.SecondOperand == second1
assert set(rep.Items) == {final_boolean, second1, second2}
assert len(rep.Items) == 1
assert rep.Items[0].FirstOperand.is_a("IfcBooleanResult")
assert rep.Items[0].SecondOperand == second2
assert rep.Items[0].FirstOperand.FirstOperand == first
assert rep.Items[0].FirstOperand.SecondOperand == second1
def test_adding_a_boolean_to_an_existing_operand_from_another_operand(self):
ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcProject")
@@ -107,7 +104,7 @@ class TestAddBoolean(test.bootstrap.IFC4):
booleans = ifcopenshell.api.geometry.add_boolean(self.file, first1, [second2])
assert len(booleans) == 1
assert len(rep.Items) == 4
assert len(rep.Items) == 2
assert self.file.get_total_inverses(first1) == 1
result = next(iter(self.file.get_inverse(first1)))
@@ -135,15 +132,14 @@ class TestAddBoolean(test.bootstrap.IFC4):
rep = builder.get_representation(body, [first, second])
ifcopenshell.api.geometry.add_boolean(self.file, first, [second])
ifcopenshell.api.geometry.add_boolean(self.file, first, [second])
assert set(rep.Items) == {self.file.by_type("IfcBooleanResult")[0], second}
boolean = self.file.by_type("IfcBooleanResult")[0]
assert boolean.FirstOperand == first
assert boolean.SecondOperand == second
assert len(rep.Items) == 1
assert rep.Items[0].FirstOperand == first
assert rep.Items[0].SecondOperand == second
ifcopenshell.api.geometry.add_boolean(self.file, second, [second])
ifcopenshell.api.geometry.add_boolean(self.file, second, [first])
assert set(rep.Items) == {boolean, second}
assert boolean.FirstOperand == first
assert boolean.SecondOperand == second
assert len(rep.Items) == 1
assert rep.Items[0].FirstOperand == first
assert rep.Items[0].SecondOperand == second
assert len(self.file.by_type("IfcBooleanResult")) == 1
@@ -76,7 +76,7 @@ class TestValidateType(test.bootstrap.IFC4):
booleans = ifcopenshell.api.geometry.add_boolean(self.file, first, [second1])
assert len(booleans) == 1
assert len(rep.Items) == 4
assert len(rep.Items) == 3
assert ifcopenshell.api.geometry.validate_type(self.file, rep) is True
assert len(rep.Items) == 1
assert rep.RepresentationType == "CSG"
@@ -96,9 +96,9 @@ class TestValidateType(test.bootstrap.IFC4):
booleans = ifcopenshell.api.geometry.add_boolean(self.file, first, [second1])
assert len(booleans) == 1
assert len(rep.Items) == 3 # boolean replaced first, but second1 stays in Items
assert len(rep.Items) == 2
assert ifcopenshell.api.geometry.validate_type(self.file, rep) is False
assert len(rep.Items) == 2 # validate_type unioned second1 into the boolean
assert len(rep.Items) == 2
assert rep.RepresentationType is None
@@ -1,59 +0,0 @@
# IfcOpenShell - IFC toolkit and geometry engine
# Copyright (C) 2021 Dion Moult <dion@thinkmoult.com>
#
# This file is part of IfcOpenShell.
#
# IfcOpenShell is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# IfcOpenShell is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with IfcOpenShell. If not, see <http://www.gnu.org/licenses/>.
import ifcopenshell.api.grid
import test.bootstrap
class TestRemoveGridAxis(test.bootstrap.IFC4):
def test_removing_an_axis_removes_its_curve(self):
grid = self.file.createIfcGrid()
axis = ifcopenshell.api.grid.create_grid_axis(
self.file, axis_tag="A", same_sense=True, uvw_axes="UAxes", grid=grid
)
axis.AxisCurve = self.file.createIfcPolyline([self.file.createIfcCartesianPoint((0.0, 0.0, 0.0))])
axis2 = ifcopenshell.api.grid.create_grid_axis(
self.file, axis_tag="B", same_sense=True, uvw_axes="UAxes", grid=grid
)
axis2.AxisCurve = self.file.createIfcPolyline([self.file.createIfcCartesianPoint((1.0, 0.0, 0.0))])
ifcopenshell.api.grid.remove_grid_axis(self.file, axis=axis2)
assert grid.UAxes == (axis,)
assert len(self.file.by_type("IfcGridAxis")) == 1
# The curve should be removed since it was only used by the removed axis.
assert len(self.file.by_type("IfcPolyline")) == 1
def test_removing_an_axis_preserves_shared_curve(self):
grid = self.file.createIfcGrid()
shared_curve = self.file.createIfcPolyline([self.file.createIfcCartesianPoint((0.0, 0.0, 0.0))])
axis = ifcopenshell.api.grid.create_grid_axis(
self.file, axis_tag="A", same_sense=True, uvw_axes="UAxes", grid=grid
)
axis.AxisCurve = shared_curve
axis2 = ifcopenshell.api.grid.create_grid_axis(
self.file, axis_tag="B", same_sense=True, uvw_axes="UAxes", grid=grid
)
axis2.AxisCurve = shared_curve
ifcopenshell.api.grid.remove_grid_axis(self.file, axis=axis2)
assert grid.UAxes == (axis,)
# The shared curve should be preserved since it's still used by axis.
assert shared_curve in self.file
assert axis.AxisCurve == shared_curve
class TestRemoveGridAxisIFC2X3(test.bootstrap.IFC2X3, TestRemoveGridAxis):
pass
@@ -18,10 +18,8 @@
import pytest
import ifcopenshell.api.aggregate
import ifcopenshell.api.nest
import ifcopenshell.api.root
import ifcopenshell.api.spatial
import ifcopenshell.util.element
import test.bootstrap
@@ -84,24 +82,6 @@ class TestAssignObject(test.bootstrap.IFC4):
ifcopenshell.api.nest.assign_object(self.file, related_objects=subelements[2:3], relating_object=element2)
assert rel.RelatedObjects == tuple(subelements[:2] + subelements[3:])
def test_nesting_removes_spatial_containment(self):
element = ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcWall")
subelement = ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcWall")
storey = ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcBuildingStorey")
ifcopenshell.api.spatial.assign_container(self.file, products=[subelement], relating_structure=storey)
assert ifcopenshell.util.element.get_container(subelement) == storey
ifcopenshell.api.nest.assign_object(self.file, related_objects=[subelement], relating_object=element)
assert ifcopenshell.util.element.get_container(subelement) is None
def test_nesting_removes_aggregate(self):
element = ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcWall")
subelement = ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcWall")
assembly = ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcElementAssembly")
ifcopenshell.api.aggregate.assign_object(self.file, products=[subelement], relating_object=assembly)
assert ifcopenshell.util.element.get_aggregate(subelement) == assembly
ifcopenshell.api.nest.assign_object(self.file, related_objects=[subelement], relating_object=element)
assert ifcopenshell.util.element.get_aggregate(subelement) is None
class TestAssignObjectIFC2X3(test.bootstrap.IFC2X3, TestAssignObject):
pass
@@ -1,38 +0,0 @@
# IfcOpenShell - IFC toolkit and geometry engine
# Copyright (C) 2021 Dion Moult <dion@thinkmoult.com>
#
# This file is part of IfcOpenShell.
#
# IfcOpenShell is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# IfcOpenShell is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with IfcOpenShell. If not, see <http://www.gnu.org/licenses/>.
import ifcopenshell.api.pset_template
import test.bootstrap
class TestRemovePropTemplate(test.bootstrap.IFC4):
def test_removing_a_prop_template(self):
template = ifcopenshell.api.pset_template.add_pset_template(self.file, name="ABC_RiskFactors")
prop1 = ifcopenshell.api.pset_template.add_prop_template(self.file, pset_template=template)
prop2 = ifcopenshell.api.pset_template.add_prop_template(self.file, pset_template=template)
ifcopenshell.api.pset_template.remove_prop_template(self.file, prop_template=prop2)
assert len(self.file.by_type("IfcSimplePropertyTemplate")) == 1
assert template.HasPropertyTemplates == (prop1,)
def test_not_removing_the_last_prop_template(self):
template = ifcopenshell.api.pset_template.add_pset_template(self.file, name="ABC_RiskFactors")
prop = ifcopenshell.api.pset_template.add_prop_template(self.file, pset_template=template)
ifcopenshell.api.pset_template.remove_prop_template(self.file, prop_template=prop)
# The last prop template should not be removed to keep the pset template valid.
assert len(self.file.by_type("IfcSimplePropertyTemplate")) == 1
assert template.HasPropertyTemplates == (prop,)
@@ -1,35 +0,0 @@
# IfcOpenShell - IFC toolkit and geometry engine
# Copyright (C) 2021 Dion Moult <dion@thinkmoult.com>
#
# This file is part of IfcOpenShell.
#
# IfcOpenShell is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# IfcOpenShell is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with IfcOpenShell. If not, see <http://www.gnu.org/licenses/>.
import ifcopenshell.api.pset_template
import test.bootstrap
class TestRemovePsetTemplate(test.bootstrap.IFC4):
def test_removing_a_pset_template(self):
template = ifcopenshell.api.pset_template.add_pset_template(self.file, name="ABC_RiskFactors")
ifcopenshell.api.pset_template.remove_pset_template(self.file, pset_template=template)
assert len(self.file.by_type("IfcPropertySetTemplate")) == 0
def test_removing_a_pset_template_with_property_templates(self):
template = ifcopenshell.api.pset_template.add_pset_template(self.file, name="ABC_RiskFactors")
prop1 = ifcopenshell.api.pset_template.add_prop_template(self.file, pset_template=template)
prop2 = ifcopenshell.api.pset_template.add_prop_template(self.file, pset_template=template)
ifcopenshell.api.pset_template.remove_pset_template(self.file, pset_template=template)
assert len(self.file.by_type("IfcPropertySetTemplate")) == 0
assert len(self.file.by_type("IfcSimplePropertyTemplate")) == 0
@@ -1,44 +0,0 @@
# IfcOpenShell - IFC toolkit and geometry engine
# Copyright (C) 2021 Dion Moult <dion@thinkmoult.com>
#
# This file is part of IfcOpenShell.
#
# IfcOpenShell is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# IfcOpenShell is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with IfcOpenShell. If not, see <http://www.gnu.org/licenses/>.
import ifcopenshell.api.resource
import test.bootstrap
class TestRemoveResourceQuantity(test.bootstrap.IFC4):
def test_removing_a_resource_quantity(self):
self.file.create_entity("IfcProject")
resource = ifcopenshell.api.resource.add_resource(self.file, ifc_class="IfcLaborResource")
ifcopenshell.api.resource.add_resource_quantity(
self.file, resource=resource, ifc_class="IfcQuantityTime"
)
assert resource.BaseQuantity is not None
ifcopenshell.api.resource.remove_resource_quantity(self.file, resource=resource)
assert resource.BaseQuantity is None
assert len(self.file.by_type("IfcPhysicalSimpleQuantity")) == 0
def test_removing_a_resource_quantity_when_none_exists(self):
self.file.create_entity("IfcProject")
resource = ifcopenshell.api.resource.add_resource(self.file, ifc_class="IfcLaborResource")
# Should not raise.
ifcopenshell.api.resource.remove_resource_quantity(self.file, resource=resource)
assert resource.BaseQuantity is None
class TestRemoveResourceQuantityIFC2X3(test.bootstrap.IFC2X3, TestRemoveResourceQuantity):
pass
+10 -63
View File
@@ -25,19 +25,9 @@ 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
WORKER_BIN_DIR := $(WEBAPP_DIR)/public/worker/bin
IFCOPENSHELL_WASM_WHEEL := ifcopenshell-0.8.5+a51b2c5-cp313-cp313-pyodide_2025_0_wasm32.whl
IFCOPENSHELL_WASM_WHEEL_URL := https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-0.8.5%2Ba51b2c5-cp313-cp313-pyodide_2025_0_wasm32.whl
IFCOPENSHELL_WASM_WHEEL_PATH := $(WORKER_BIN_DIR)/$(IFCOPENSHELL_WASM_WHEEL)
WEBAPP_GENERATED_DIR := $(WEBAPP_DIR)/public/worker/generated
WEBAPP_IFCTESTER_MANIFEST := $(WEBAPP_GENERATED_DIR)/ifctester.json
WEBAPP_IFCTESTER_BUILD_DIR := build-webapp-wheel
PACKAGE_WEBAPP_DIR := $(PACKAGE_NAME)/webapp
PACKAGE_WEBAPP_WWW_DIR := $(PACKAGE_WEBAPP_DIR)/www
.PHONY: webapp-dev
webapp-dev: pyodide-download ifcopenshell-wasm-download webapp-stage-ifctester-wheel
cd $(WEBAPP_DIR) && npm install
webapp-dev:
cd $(WEBAPP_DIR) && npm run dev
.PHONY: pyodide-download
@@ -72,43 +62,8 @@ pyodide-download:
echo "Pyodide $(PYODIDE_VERSION) prepared in $(PYODIDE_DIR)"; \
fi
.PHONY: ifcopenshell-wasm-download
ifcopenshell-wasm-download:
@if [ -f "$(IFCOPENSHELL_WASM_WHEEL_PATH)" ]; then \
echo "IfcOpenShell wasm wheel already exists at $(IFCOPENSHELL_WASM_WHEEL_PATH), skipping download"; \
else \
echo "Downloading IfcOpenShell wasm wheel..."; \
mkdir -p $(WORKER_BIN_DIR); \
rm -f $(WORKER_BIN_DIR)/ifcopenshell-*.whl; \
curl -fL -o "$(IFCOPENSHELL_WASM_WHEEL_PATH)" "$(IFCOPENSHELL_WASM_WHEEL_URL)"; \
echo "IfcOpenShell wasm wheel prepared at $(IFCOPENSHELL_WASM_WHEEL_PATH)"; \
fi
.PHONY: webapp-stage-ifctester-wheel
webapp-stage-ifctester-wheel:
rm -rf $(WEBAPP_GENERATED_DIR)
rm -rf $(WEBAPP_IFCTESTER_BUILD_DIR)
mkdir -p $(WEBAPP_IFCTESTER_BUILD_DIR)
cp -r $(PACKAGE_NAME) $(WEBAPP_IFCTESTER_BUILD_DIR)/
rm -rf $(WEBAPP_IFCTESTER_BUILD_DIR)/$(PACKAGE_NAME)/webapp
cp pyproject.toml $(WEBAPP_IFCTESTER_BUILD_DIR)/
cp README.md $(WEBAPP_IFCTESTER_BUILD_DIR)/
ifeq ($(IS_STABLE), TRUE)
$(SED) 's/version = "0.0.0"/version = "$(VERSION)"/' $(WEBAPP_IFCTESTER_BUILD_DIR)/pyproject.toml
$(SED) 's/version = "0.0.0"/version = "$(VERSION)"/' $(WEBAPP_IFCTESTER_BUILD_DIR)/$(PACKAGE_NAME)/__init__.py
else
$(SED) 's/version = "0.0.0"/version = "$(VERSION)a$(VERSION_DATE)"/' $(WEBAPP_IFCTESTER_BUILD_DIR)/pyproject.toml
$(SED) 's/version = "0.0.0"/version = "$(VERSION)-alpha$(VERSION_DATE)"/' $(WEBAPP_IFCTESTER_BUILD_DIR)/$(PACKAGE_NAME)/__init__.py
endif
cd $(WEBAPP_IFCTESTER_BUILD_DIR) && $(PYTHON) -m venv env --system-site-packages && . env/$(VENV_ACTIVATE) && python -m pip install build && python -m build --wheel --no-isolation
mkdir -p $(WEBAPP_GENERATED_DIR)
wheel=$$(basename $(WEBAPP_IFCTESTER_BUILD_DIR)/dist/$(PACKAGE_NAME)-*.whl); \
cp "$(WEBAPP_IFCTESTER_BUILD_DIR)/dist/$$wheel" "$(WEBAPP_GENERATED_DIR)/$$wheel"; \
printf '{\n "wheel_url": "/worker/generated/%s"\n}\n' "$$wheel" > "$(WEBAPP_IFCTESTER_MANIFEST)"
rm -rf $(WEBAPP_IFCTESTER_BUILD_DIR)
.PHONY: webapp-build
webapp-build: pyodide-download ifcopenshell-wasm-download webapp-stage-ifctester-wheel
webapp-build: pyodide-download
cd $(WEBAPP_DIR) && npm install
cd $(WEBAPP_DIR) && npm run build
@@ -121,30 +76,22 @@ clean:
rm -rf $(WEBAPP_BUILD_DIR)
rm -rf $(WEBAPP_DIR)/node_modules
rm -rf $(PYODIDE_DIR)
rm -f $(WORKER_BIN_DIR)/ifcopenshell-*.whl
rm -rf $(WEBAPP_GENERATED_DIR)
rm -rf $(WEBAPP_IFCTESTER_BUILD_DIR)
rm -rf $(PACKAGE_NAME)/webapp
rm -rf dist
.PHONY: python-dist
python-dist:
rm -rf dist
# For some reason OS is not initalized when we call common.mk dist, which matters on Windows.
# So we pass it explicitly.
$(MAKE) -f ../common.mk dist PACKAGE_NAME=$(PACKAGE_NAME) OS=$(OS) IS_STABLE=$(IS_STABLE)
.PHONY: dist
dist: webapp-prepare
$(MAKE) python-dist OS=$(OS) IS_STABLE=$(IS_STABLE)
# For some reason OS is not initalized when we call common.mk dist, which matters on Windows.
# So we pass it explicitly.
$(MAKE) -f ../common.mk dist PACKAGE_NAME=$(PACKAGE_NAME) OS=$(OS)
.PHONY: webapp-prepare
webapp-prepare: webapp-build
rm -rf $(PACKAGE_WEBAPP_WWW_DIR)
mkdir -p $(PACKAGE_WEBAPP_WWW_DIR)
cp -r $(WEBAPP_BUILD_DIR)/* $(PACKAGE_WEBAPP_WWW_DIR)/
cp $(WEBAPP_DIR)/__init__.py $(PACKAGE_WEBAPP_DIR)/__init__.py
cp $(WEBAPP_DIR)/serve.py $(PACKAGE_WEBAPP_DIR)/serve.py
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:
+1 -3
View File
@@ -11,8 +11,6 @@ node_modules
dist
dist-ssr
*.local
public/worker/bin/ifcopenshell-*.whl
public/worker/generated
# Editor directories and files
.vscode/*
@@ -26,4 +24,4 @@ public/worker/generated
*.sw?
.claude
experiment/*
experiment/*
-31
View File
@@ -1,31 +0,0 @@
{
"$schema": "https://biomejs.dev/schemas/1.9.4/schema.json",
"files": {
"ignore": [
"dist",
"node_modules",
"public/pyodide",
"**/*.svelte"
]
},
"linter": {
"enabled": true,
"rules": {
"recommended": true
}
},
"overrides": [
{
"include": [
"src/modules/wasm/worker/**"
],
"linter": {
"rules": {
"suspicious": {
"noExplicitAny": "off"
}
}
}
}
]
}
+1 -1
View File
@@ -9,6 +9,6 @@
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.ts"></script>
<script type="module" src="/src/main.js"></script>
</body>
</html>
+39
View File
@@ -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"]
}
-258
View File
@@ -16,7 +16,6 @@
"svelte-spa-router": "^4.0.1"
},
"devDependencies": {
"@biomejs/biome": "^1.9.4",
"@internationalized/date": "^3.8.1",
"@lucide/svelte": "^0.515.0",
"@sveltejs/vite-plugin-svelte": "^5.0.3",
@@ -26,13 +25,11 @@
"mode-watcher": "^1.1.0",
"sass-embedded": "^1.89.0",
"svelte": "^5.53.6",
"svelte-check": "^4.0.0",
"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",
"typescript": "^5.8.3",
"vite": "^6.4.1"
}
},
@@ -50,170 +47,6 @@
"node": ">=6.0.0"
}
},
"node_modules/@biomejs/biome": {
"version": "1.9.4",
"resolved": "https://registry.npmjs.org/@biomejs/biome/-/biome-1.9.4.tgz",
"integrity": "sha512-1rkd7G70+o9KkTn5KLmDYXihGoTaIGO9PIIN2ZB7UJxFrWw04CZHPYiMRjYsaDvVV7hP1dYNRLxSANLaBFGpog==",
"dev": true,
"hasInstallScript": true,
"license": "MIT OR Apache-2.0",
"bin": {
"biome": "bin/biome"
},
"engines": {
"node": ">=14.21.3"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/biome"
},
"optionalDependencies": {
"@biomejs/cli-darwin-arm64": "1.9.4",
"@biomejs/cli-darwin-x64": "1.9.4",
"@biomejs/cli-linux-arm64": "1.9.4",
"@biomejs/cli-linux-arm64-musl": "1.9.4",
"@biomejs/cli-linux-x64": "1.9.4",
"@biomejs/cli-linux-x64-musl": "1.9.4",
"@biomejs/cli-win32-arm64": "1.9.4",
"@biomejs/cli-win32-x64": "1.9.4"
}
},
"node_modules/@biomejs/cli-darwin-arm64": {
"version": "1.9.4",
"resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-arm64/-/cli-darwin-arm64-1.9.4.tgz",
"integrity": "sha512-bFBsPWrNvkdKrNCYeAp+xo2HecOGPAy9WyNyB/jKnnedgzl4W4Hb9ZMzYNbf8dMCGmUdSavlYHiR01QaYR58cw==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT OR Apache-2.0",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">=14.21.3"
}
},
"node_modules/@biomejs/cli-darwin-x64": {
"version": "1.9.4",
"resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-x64/-/cli-darwin-x64-1.9.4.tgz",
"integrity": "sha512-ngYBh/+bEedqkSevPVhLP4QfVPCpb+4BBe2p7Xs32dBgs7rh9nY2AIYUL6BgLw1JVXV8GlpKmb/hNiuIxfPfZg==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT OR Apache-2.0",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">=14.21.3"
}
},
"node_modules/@biomejs/cli-linux-arm64": {
"version": "1.9.4",
"resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64/-/cli-linux-arm64-1.9.4.tgz",
"integrity": "sha512-fJIW0+LYujdjUgJJuwesP4EjIBl/N/TcOX3IvIHJQNsAqvV2CHIogsmA94BPG6jZATS4Hi+xv4SkBBQSt1N4/g==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT OR Apache-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=14.21.3"
}
},
"node_modules/@biomejs/cli-linux-arm64-musl": {
"version": "1.9.4",
"resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64-musl/-/cli-linux-arm64-musl-1.9.4.tgz",
"integrity": "sha512-v665Ct9WCRjGa8+kTr0CzApU0+XXtRgwmzIf1SeKSGAv+2scAlW6JR5PMFo6FzqqZ64Po79cKODKf3/AAmECqA==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT OR Apache-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=14.21.3"
}
},
"node_modules/@biomejs/cli-linux-x64": {
"version": "1.9.4",
"resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64/-/cli-linux-x64-1.9.4.tgz",
"integrity": "sha512-lRCJv/Vi3Vlwmbd6K+oQ0KhLHMAysN8lXoCI7XeHlxaajk06u7G+UsFSO01NAs5iYuWKmVZjmiOzJ0OJmGsMwg==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT OR Apache-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=14.21.3"
}
},
"node_modules/@biomejs/cli-linux-x64-musl": {
"version": "1.9.4",
"resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64-musl/-/cli-linux-x64-musl-1.9.4.tgz",
"integrity": "sha512-gEhi/jSBhZ2m6wjV530Yy8+fNqG8PAinM3oV7CyO+6c3CEh16Eizm21uHVsyVBEB6RIM8JHIl6AGYCv6Q6Q9Tg==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT OR Apache-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=14.21.3"
}
},
"node_modules/@biomejs/cli-win32-arm64": {
"version": "1.9.4",
"resolved": "https://registry.npmjs.org/@biomejs/cli-win32-arm64/-/cli-win32-arm64-1.9.4.tgz",
"integrity": "sha512-tlbhLk+WXZmgwoIKwHIHEBZUwxml7bRJgk0X2sPyNR3S93cdRq6XulAZRQJ17FYGGzWne0fgrXBKpl7l4M87Hg==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT OR Apache-2.0",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=14.21.3"
}
},
"node_modules/@biomejs/cli-win32-x64": {
"version": "1.9.4",
"resolved": "https://registry.npmjs.org/@biomejs/cli-win32-x64/-/cli-win32-x64-1.9.4.tgz",
"integrity": "sha512-8Y5wMhVIPaWe6jw2H+KlEm4wP/f7EW3810ZLmDlrEEy5KvBsb9ECEfu/kMWD484ijfQ8+nIi0giMgu9g1UAuuA==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT OR Apache-2.0",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=14.21.3"
}
},
"node_modules/@bufbuild/protobuf": {
"version": "2.5.1",
"resolved": "https://registry.npmjs.org/@bufbuild/protobuf/-/protobuf-2.5.1.tgz",
@@ -1635,22 +1468,6 @@
"dev": true,
"license": "MIT/X11"
},
"node_modules/chokidar": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz",
"integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==",
"dev": true,
"license": "MIT",
"dependencies": {
"readdirp": "^4.0.1"
},
"engines": {
"node": ">= 14.16.0"
},
"funding": {
"url": "https://paulmillr.com/funding/"
}
},
"node_modules/chownr": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz",
@@ -2309,16 +2126,6 @@
"svelte": "^5.7.0"
}
},
"node_modules/mri": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz",
"integrity": "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=4"
}
},
"node_modules/ms": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
@@ -2393,20 +2200,6 @@
"node": "^10 || ^12 || >=14"
}
},
"node_modules/readdirp": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz",
"integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 14.18.0"
},
"funding": {
"type": "individual",
"url": "https://paulmillr.com/funding/"
}
},
"node_modules/regexparam": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/regexparam/-/regexparam-2.0.2.tgz",
@@ -2488,19 +2281,6 @@
"tslib": "^2.1.0"
}
},
"node_modules/sade": {
"version": "1.8.1",
"resolved": "https://registry.npmjs.org/sade/-/sade-1.8.1.tgz",
"integrity": "sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==",
"dev": true,
"license": "MIT",
"dependencies": {
"mri": "^1.1.0"
},
"engines": {
"node": ">=6"
}
},
"node_modules/sass-embedded": {
"version": "1.89.0",
"resolved": "https://registry.npmjs.org/sass-embedded/-/sass-embedded-1.89.0.tgz",
@@ -3003,30 +2783,6 @@
"node": ">=18"
}
},
"node_modules/svelte-check": {
"version": "4.3.5",
"resolved": "https://registry.npmjs.org/svelte-check/-/svelte-check-4.3.5.tgz",
"integrity": "sha512-e4VWZETyXaKGhpkxOXP+B/d0Fp/zKViZoJmneZWe/05Y2aqSKj3YN2nLfYPJBQ87WEiY4BQCQ9hWGu9mPT1a1Q==",
"dev": true,
"license": "MIT",
"dependencies": {
"@jridgewell/trace-mapping": "^0.3.25",
"chokidar": "^4.0.1",
"fdir": "^6.2.0",
"picocolors": "^1.0.0",
"sade": "^1.7.4"
},
"bin": {
"svelte-check": "bin/svelte-check"
},
"engines": {
"node": ">= 18.0.0"
},
"peerDependencies": {
"svelte": "^4.0.0 || ^5.0.0-next.0",
"typescript": ">=5.0.0"
}
},
"node_modules/svelte-sonner": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/svelte-sonner/-/svelte-sonner-1.0.5.tgz",
@@ -3227,20 +2983,6 @@
"url": "https://github.com/sponsors/Wombosvideo"
}
},
"node_modules/typescript": {
"version": "5.9.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
"dev": true,
"license": "Apache-2.0",
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
},
"engines": {
"node": ">=14.17"
}
},
"node_modules/uuid": {
"version": "8.3.2",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
-6
View File
@@ -6,15 +6,11 @@
"scripts": {
"dev": "vite",
"build": "vite build",
"check": "tsc -p tsconfig.json --noEmit && svelte-check && biome lint .",
"lint": "biome lint .",
"lint:fix": "biome lint --write .",
"preview": "vite preview",
"deploy": "npm run build && npx wrangler pages deploy dist"
},
"devDependencies": {
"@internationalized/date": "^3.8.1",
"@biomejs/biome": "^1.9.4",
"@lucide/svelte": "^0.515.0",
"@sveltejs/vite-plugin-svelte": "^5.0.3",
"@tailwindcss/vite": "^4.0.0",
@@ -23,12 +19,10 @@
"mode-watcher": "^1.1.0",
"sass-embedded": "^1.89.0",
"svelte": "^5.53.6",
"svelte-check": "^4.0.0",
"svelte-sonner": "^1.0.5",
"tailwind-merge": "^3.3.0",
"tailwind-variants": "^1.0.0",
"tailwindcss": "^4.0.0",
"typescript": "^5.8.3",
"tw-animate-css": "^1.3.2",
"vite": "^6.4.1"
},
+3
View File
@@ -22,6 +22,9 @@ import os
import sys
bonsai_lib_path = os.environ.get("BONSAI_LIB_PATH")
print(os.environ)
print(bonsai_lib_path)
bonsai_version = os.environ.get("BONSAI_VERSION")
if bonsai_lib_path:
sys.path.insert(0, bonsai_lib_path)
+1 -1
View File
@@ -1,4 +1,4 @@
<script lang="ts">
<script>
import Router from 'svelte-spa-router';
import routes from './routes';
</script>
-4
View File
@@ -1,4 +0,0 @@
declare module "*.svelte" {
import type { SvelteComponent } from "svelte";
export default class Component extends SvelteComponent {}
}
@@ -1,11 +1,11 @@
<script lang="ts">
<script>
import * as Menubar from "$lib/components/ui/menubar";
import * as Dialog from "$lib/components/ui/dialog";
import * as IDS from "$src/modules/api/ids.svelte";
import * as API from "$src/modules/api/api.svelte";
import { error, success } from "$src/modules/utils/toast.svelte";
import * as IDS from "$src/modules/api/ids.svelte.js";
import * as API from "$src/modules/api/api.svelte.js";
import { error, success, info } from "$src/modules/utils/toast.svelte.js";
let { isOpen = false } : { isOpen?: boolean } = $props();
let { isOpen = false } = $props();
function openForum() {
window.open('https://community.osarch.org', '_blank');
@@ -23,9 +23,8 @@
try {
await IDS.openDocument();
} catch (err) {
const message = err instanceof Error ? err.message : String(err);
if (message !== 'File selection cancelled') {
error(`Error opening file: ${message}`);
if (err.message !== 'File selection cancelled') {
error('Error opening file: ' + err.message);
console.error(err);
}
}
@@ -51,7 +50,7 @@
success('Audit completed successfully');
} catch (err) {
console.error("Audit failed: ", err);
error("Audit failed: check console for details");
error(`Audit failed: check console for details`);
}
}
</script>
@@ -143,4 +142,4 @@
</Dialog.Close>
</Dialog.Footer>
</Dialog.Content>
</Dialog.Root>
</Dialog.Root>
@@ -1,5 +1,6 @@
<script lang="ts">
import { Module } from "$src/modules/api/ids.svelte";
<script>
import { onMount } from "svelte";
import { Module } from "$src/modules/api/ids.svelte.js";
</script>
<div class="app-ribbon">
@@ -19,4 +20,4 @@
<span>Error</span>
</div>
{/if}
</div>
</div>
@@ -1,16 +1,14 @@
<script lang="ts">
<script>
import * as Tooltip from "$lib/components/ui/tooltip";
import { IFCModels, openIfc, unloadIfc, runAudit } from "$src/modules/api/api.svelte";
import * as IDS from "$src/modules/api/ids.svelte";
import { error, success } from "$src/modules/utils/toast.svelte";
import { IFCModels, loadIfc, unloadIfc, auditIfc, openIfc, createAuditReport, clearIdsAuditReports, runAudit } from "$src/modules/api/api.svelte.js";
import * as IDS from "$src/modules/api/ids.svelte.js";
import { error, success } from "$src/modules/utils/toast.svelte.js";
import { ChevronRightIcon, LinkIcon, XIcon } from "@lucide/svelte";
import { Bonsai, connect, disconnect, runAudit as runBonsaiAudit } from "$src/modules/api/bonsai.svelte";
import { Bonsai, connect, disconnect, runAudit as runBonsaiAudit } from "$src/modules/api/bonsai.svelte.js";
import { onMount } from 'svelte';
import type { AuditReport } from "$src/types/report";
let isAuditing = $state(false);
let activeTab = $state<"home" | "bonsai">('home');
// biome-ignore lint/style/useConst: Svelte state uses assignment for updates.
let activeTab = $state('home');
let isMinimized = $state(false);
const handleLoadModel = async () => {
@@ -18,17 +16,15 @@
await openIfc();
success('IFC model loaded successfully');
} catch (err) {
const message = err instanceof Error ? err.message : String(err);
error(`Failed to load IFC model: ${message}`);
error(`Failed to load IFC model: ${err.message}`);
}
};
const handleUnloadModel = async (modelId: string) => {
const handleUnloadModel = async (modelId) => {
try {
await unloadIfc(modelId);
} catch (err) {
const message = err instanceof Error ? err.message : String(err);
error(`Failed to unload model: ${message}`);
error(`Failed to unload model: ${err.message}`);
}
};
@@ -39,14 +35,14 @@
success('Audit completed successfully');
} catch (err) {
console.error("Audit failed: ", err);
error("Audit failed: check console for details");
error(`Audit failed: check console for details`);
} finally {
isAuditing = false;
}
};
const handleViewAuditReport = (auditId: string) => {
const auditReport = IFCModels.audits.find(audit => audit.id === auditId) as AuditReport | undefined;
const handleViewAuditReport = (auditId) => {
const auditReport = IFCModels.audits.find(audit => audit.id === auditId);
if (!auditReport) return;
// Switch to the IDS document that was used for this audit
@@ -62,7 +58,7 @@
}
};
const formatFileSize = (bytes: number) => {
const formatFileSize = (bytes) => {
const units = ['B', 'KB', 'MB', 'GB'];
let size = bytes;
let unitIndex = 0;
@@ -95,7 +91,7 @@
<div class="buttons">
<Tooltip.Provider>
{#if isMinimized}
<Tooltip.Root disableHoverableContent>
<Tooltip.Root disableHoverableContent="true">
<Tooltip.Trigger>
<button class="tb-btn expand-btn" onclick={() => isMinimized = false} aria-label="Expand Toolbar">
<ChevronRightIcon size={24} />
@@ -106,7 +102,7 @@
</Tooltip.Content>
</Tooltip.Root>
{/if}
<Tooltip.Root disableHoverableContent>
<Tooltip.Root disableHoverableContent="true">
<Tooltip.Trigger>
<button class="tb-btn {activeTab === 'home' ? 'active' : ''}" onclick={() => activeTab = 'home'} aria-label="Home">
<svg class="w-6 h-6" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" viewBox="0 0 24 24">
@@ -118,7 +114,7 @@
<p>Home</p>
</Tooltip.Content>
</Tooltip.Root>
<Tooltip.Root disableHoverableContent>
<Tooltip.Root disableHoverableContent="true">
<Tooltip.Trigger>
<button class="tb-btn {activeTab === 'bonsai' ? 'active' : ''}" onclick={() => activeTab = 'bonsai'} aria-label="Bonsai Integration">
<svg style="height: 20px;" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="32mm" height="32mm" version="1.1" viewBox="0 0 32 32" xml:space="preserve">
@@ -696,4 +692,4 @@
opacity: 0.5;
}
}
</style>
</style>
@@ -1,9 +1,7 @@
<script lang="ts">
<script>
import * as DropdownMenu from "$lib/components/ui/dropdown-menu";
type FacetType = "entity" | "attribute" | "classification" | "partOf" | "property" | "material";
const { addFacet } : { addFacet: (facetType: FacetType) => void } = $props();
let { addFacet } = $props();
</script>
<DropdownMenu.Root>
@@ -65,4 +63,4 @@
Part Of
</DropdownMenu.Item>
</DropdownMenu.Content>
</DropdownMenu.Root>
</DropdownMenu.Root>
@@ -1,22 +1,13 @@
<script lang="ts">
import * as IDS from "$src/modules/api/ids.svelte";
import type { IdsDocument } from "$src/types/ids";
<script>
import * as IDS from "$src/modules/api/ids.svelte.js";
function switchDocument(docId: string) {
function switchDocument(docId) {
IDS.Module.activeDocument = docId;
}
function closeDocument(docId: string) {
function closeDocument(docId) {
IDS.deleteDocument(docId);
}
const handleActivation = (event: KeyboardEvent, action: () => void) => {
if (event.currentTarget !== event.target) return;
if (event.key === 'Enter' || event.key === ' ') {
event.preventDefault();
action();
}
};
</script>
<div class="ids-tabs">
@@ -24,13 +15,10 @@
<div
class="ids-tab"
class:active={IDS.Module.activeDocument === docId}
role="button"
tabindex="0"
onclick={() => switchDocument(docId)}
onkeydown={(event) => handleActivation(event, () => switchDocument(docId))}
aria-label={(doc as IdsDocument).info.title || "Untitled"}
aria-label={doc.info.title || "Untitled"}
>
<span class="tab-title">{(doc as IdsDocument).info.title || "Untitled"}</span>
<span class="tab-title">{doc.info.title || "Untitled"}</span>
<button
class="tab-close"
onclick={(e) => { e.stopPropagation(); closeDocument(docId); }}
@@ -43,4 +31,4 @@
</div>
{/each}
<div class="filler-tab"></div>
</div>
</div>
+4 -3
View File
@@ -1,7 +1,8 @@
{
"wasm": {
"wheel_url": "/worker/bin/ifcopenshell-0.8.5+a51b2c5-cp313-cp313-pyodide_2025_0_wasm32.whl",
"wheel_url": "/worker/bin/ifcopenshell-0.8.3+bb329af-cp313-cp313-emscripten_4_0_9_wasm32.whl",
"odfpy_url": "/worker/bin/odfpy-1.4.2-py2.py3-none-any.whl",
"api_py_url": "/worker/api.py"
"api_py_url": "/worker/api.py",
"pyodide_url": "https://cdn.jsdelivr.net/pyodide/v0.28.0/full/pyodide.js"
}
}
}
+1 -2
View File
@@ -576,8 +576,7 @@ html, body {
display: flex;
flex-direction: column;
label,
.form-label {
label {
margin-bottom: 4px;
font-size: 14px;
font-weight: 500;
@@ -1,11 +1,7 @@
<script lang="ts">
<script>
import { Dialog as DialogPrimitive } from "bits-ui";
type Props = {
ref?: HTMLElement | null;
} & Record<string, unknown>;
let { ref = $bindable(null), ...restProps } : Props = $props();
let { ref = $bindable(null), ...restProps } = $props();
</script>
<DialogPrimitive.Close bind:ref data-slot="dialog-close" {...restProps} />
<DialogPrimitive.Close bind:ref data-slot="dialog-close" {...restProps} />
@@ -1,17 +1,8 @@
<script lang="ts">
<script>
import { Dialog as DialogPrimitive } from "bits-ui";
import XIcon from "@lucide/svelte/icons/x";
import * as Dialog from "./index";
import { cn } from "$lib/utils";
import type { Snippet } from "svelte";
type Props = {
ref?: HTMLElement | null;
class?: string;
portalProps?: Record<string, unknown>;
children?: Snippet;
showCloseButton?: boolean;
} & Record<string, unknown>;
import * as Dialog from "./index.js";
import { cn } from "$lib/utils.js";
let {
ref = $bindable(null),
@@ -20,7 +11,7 @@
children,
showCloseButton = true,
...restProps
} : Props = $props();
} = $props();
</script>
<Dialog.Portal {...portalProps}>
@@ -44,4 +35,4 @@
</DialogPrimitive.Close>
{/if}
</DialogPrimitive.Content>
</Dialog.Portal>
</Dialog.Portal>
@@ -1,17 +1,12 @@
<script lang="ts">
<script>
import { Dialog as DialogPrimitive } from "bits-ui";
import { cn } from "$lib/utils";
type Props = {
ref?: HTMLElement | null;
class?: string;
} & Record<string, unknown>;
import { cn } from "$lib/utils.js";
let {
ref = $bindable(null),
class: className,
...restProps
} : Props = $props();
} = $props();
</script>
<DialogPrimitive.Description
@@ -19,4 +14,4 @@
data-slot="dialog-description"
class={cn("text-muted-foreground text-sm", className)}
{...restProps}
/>
/>
@@ -1,19 +1,11 @@
<script lang="ts">
import { cn } from "$lib/utils";
import type { Snippet } from "svelte";
type Props = {
ref?: HTMLElement | null;
class?: string;
children?: Snippet;
} & Record<string, unknown>;
<script>
import { cn } from "$lib/utils.js";
let {
ref = $bindable(null),
class: className,
children,
...restProps
} : Props = $props();
} = $props();
</script>
<div
@@ -23,4 +15,4 @@
{...restProps}
>
{@render children?.()}
</div>
</div>
@@ -1,19 +1,12 @@
<script lang="ts">
import { cn } from "$lib/utils";
import type { Snippet } from "svelte";
type Props = {
ref?: HTMLElement | null;
class?: string;
children?: Snippet;
} & Record<string, unknown>;
<script>
import { cn } from "$lib/utils.js";
let {
ref = $bindable(null),
class: className,
children,
...restProps
} : Props = $props();
} = $props();
</script>
<div
@@ -23,4 +16,4 @@
{...restProps}
>
{@render children?.()}
</div>
</div>
@@ -1,17 +1,12 @@
<script lang="ts">
<script>
import { Dialog as DialogPrimitive } from "bits-ui";
import { cn } from "$lib/utils";
type Props = {
ref?: HTMLElement | null;
class?: string;
} & Record<string, unknown>;
import { cn } from "$lib/utils.js";
let {
ref = $bindable(null),
class: className,
...restProps
} : Props = $props();
} = $props();
</script>
<DialogPrimitive.Overlay
@@ -22,4 +17,4 @@
className
)}
{...restProps}
/>
/>
@@ -1,17 +1,12 @@
<script lang="ts">
<script>
import { Dialog as DialogPrimitive } from "bits-ui";
import { cn } from "$lib/utils";
type Props = {
ref?: HTMLElement | null;
class?: string;
} & Record<string, unknown>;
import { cn } from "$lib/utils.js";
let {
ref = $bindable(null),
class: className,
...restProps
} : Props = $props();
} = $props();
</script>
<DialogPrimitive.Title
@@ -19,4 +14,4 @@
data-slot="dialog-title"
class={cn("text-lg leading-none", className)}
{...restProps}
/>
/>
@@ -1,11 +1,7 @@
<script lang="ts">
<script>
import { Dialog as DialogPrimitive } from "bits-ui";
type Props = {
ref?: HTMLElement | null;
} & Record<string, unknown>;
let { ref = $bindable(null), ...restProps } : Props = $props();
let { ref = $bindable(null), ...restProps } = $props();
</script>
<DialogPrimitive.Trigger bind:ref data-slot="dialog-trigger" {...restProps} />
<DialogPrimitive.Trigger bind:ref data-slot="dialog-trigger" {...restProps} />
@@ -1,16 +1,8 @@
<script lang="ts">
<script>
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
import CheckIcon from "@lucide/svelte/icons/check";
import MinusIcon from "@lucide/svelte/icons/minus";
import { cn } from "$lib/utils";
import type { Snippet } from "svelte";
type Props = {
ref?: HTMLElement | null;
checked?: boolean;
indeterminate?: boolean;
class?: string;
children?: Snippet;
} & Record<string, unknown>;
import { cn } from "$lib/utils.js";
let {
ref = $bindable(null),
checked = $bindable(false),
@@ -18,7 +10,7 @@
class: className,
children: childrenProp,
...restProps
} : Props = $props();
} = $props();
</script>
<DropdownMenuPrimitive.CheckboxItem
@@ -42,4 +34,4 @@
</span>
{@render childrenProp?.()}
{/snippet}
</DropdownMenuPrimitive.CheckboxItem>
</DropdownMenuPrimitive.CheckboxItem>
@@ -1,21 +1,14 @@
<script lang="ts">
import { cn } from "$lib/utils";
<script>
import { cn } from "$lib/utils.js";
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
type Props = {
ref?: HTMLElement | null;
sideOffset?: number;
portalProps?: Record<string, unknown>;
class?: string;
} & Record<string, unknown>;
let {
ref = $bindable(null),
sideOffset = 4,
portalProps,
class: className,
...restProps
} : Props = $props();
} = $props();
</script>
<DropdownMenuPrimitive.Portal {...portalProps}>
@@ -29,4 +22,4 @@
)}
{...restProps}
/>
</DropdownMenuPrimitive.Portal>
</DropdownMenuPrimitive.Portal>
@@ -1,17 +1,12 @@
<script lang="ts">
<script>
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
import { cn } from "$lib/utils";
type Props = {
ref?: HTMLElement | null;
class?: string;
inset?: boolean;
} & Record<string, unknown>;
import { cn } from "$lib/utils.js";
let {
ref = $bindable(null),
class: className,
inset,
...restProps
} : Props = $props();
} = $props();
</script>
<DropdownMenuPrimitive.GroupHeading
@@ -20,4 +15,4 @@
data-inset={inset}
class={cn("px-2 py-1.5 text-sm font-semibold data-[inset]:pl-8", className)}
{...restProps}
/>
/>
@@ -1,11 +1,7 @@
<script lang="ts">
<script>
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
type Props = {
ref?: HTMLElement | null;
} & Record<string, unknown>;
let { ref = $bindable(null), ...restProps } : Props = $props();
let { ref = $bindable(null), ...restProps } = $props();
</script>
<DropdownMenuPrimitive.Group bind:ref data-slot="dropdown-menu-group" {...restProps} />
<DropdownMenuPrimitive.Group bind:ref data-slot="dropdown-menu-group" {...restProps} />
@@ -1,21 +1,14 @@
<script lang="ts">
import { cn } from "$lib/utils";
<script>
import { cn } from "$lib/utils.js";
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
type Props = {
ref?: HTMLElement | null;
class?: string;
inset?: boolean;
variant?: string;
} & Record<string, unknown>;
let {
ref = $bindable(null),
class: className,
inset,
variant = "default",
...restProps
} : Props = $props();
} = $props();
</script>
<DropdownMenuPrimitive.Item
@@ -28,4 +21,4 @@
className
)}
{...restProps}
/>
/>
@@ -1,20 +1,12 @@
<script lang="ts">
import { cn } from "$lib/utils";
import type { Snippet } from "svelte";
type Props = {
ref?: HTMLElement | null;
inset?: boolean;
children?: Snippet;
class?: string;
} & Record<string, unknown>;
<script>
import { cn } from "$lib/utils.js";
let {
ref = $bindable(null),
class: className,
inset,
children,
...restProps
} : Props = $props();
} = $props();
</script>
<div
@@ -25,4 +17,4 @@
{...restProps}
>
{@render children?.()}
</div>
</div>
@@ -1,16 +1,11 @@
<script lang="ts">
<script>
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
type Props = {
ref?: HTMLElement | null;
value?: string;
} & Record<string, unknown>;
let {
ref = $bindable(null),
value = $bindable(),
...restProps
} : Props = $props();
} = $props();
</script>
<DropdownMenuPrimitive.RadioGroup
@@ -18,4 +13,4 @@
bind:value
data-slot="dropdown-menu-radio-group"
{...restProps}
/>
/>
@@ -1,22 +1,14 @@
<script lang="ts">
<script>
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
import CircleIcon from "@lucide/svelte/icons/circle";
import { cn } from "$lib/utils";
import type { Snippet } from "svelte";
type Props = {
ref?: HTMLElement | null;
class?: string;
value: string;
children?: Snippet<[ { checked: boolean } ]>;
} & Record<string, unknown>;
import { cn } from "$lib/utils.js";
let {
ref = $bindable(null),
class: className,
children: childrenProp,
...restProps
} : Props = $props();
} = $props();
</script>
<DropdownMenuPrimitive.RadioItem
@@ -36,4 +28,4 @@
</span>
{@render childrenProp?.({ checked })}
{/snippet}
</DropdownMenuPrimitive.RadioItem>
</DropdownMenuPrimitive.RadioItem>
@@ -1,17 +1,12 @@
<script lang="ts">
<script>
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
import { cn } from "$lib/utils";
type Props = {
ref?: HTMLElement | null;
class?: string;
} & Record<string, unknown>;
import { cn } from "$lib/utils.js";
let {
ref = $bindable(null),
class: className,
...restProps
} : Props = $props();
} = $props();
</script>
<DropdownMenuPrimitive.Separator
@@ -19,4 +14,4 @@
data-slot="dropdown-menu-separator"
class={cn("bg-border -mx-1 my-1 h-px", className)}
{...restProps}
/>
/>
@@ -1,19 +1,12 @@
<script lang="ts">
import { cn } from "$lib/utils";
import type { Snippet } from "svelte";
type Props = {
ref?: HTMLElement | null;
class?: string;
children?: Snippet;
} & Record<string, unknown>;
<script>
import { cn } from "$lib/utils.js";
let {
ref = $bindable(null),
class: className,
children,
...restProps
} : Props = $props();
} = $props();
</script>
<span
@@ -23,4 +16,4 @@
{...restProps}
>
{@render children?.()}
</span>
</span>
@@ -1,17 +1,12 @@
<script lang="ts">
<script>
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
import { cn } from "$lib/utils";
type Props = {
ref?: HTMLElement | null;
class?: string;
} & Record<string, unknown>;
import { cn } from "$lib/utils.js";
let {
ref = $bindable(null),
class: className,
...restProps
} : Props = $props();
} = $props();
</script>
<DropdownMenuPrimitive.SubContent
@@ -22,4 +17,4 @@
className
)}
{...restProps}
/>
/>
@@ -1,15 +1,7 @@
<script lang="ts">
<script>
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
import ChevronRightIcon from "@lucide/svelte/icons/chevron-right";
import { cn } from "$lib/utils";
import type { Snippet } from "svelte";
type Props = {
ref?: HTMLElement | null;
class?: string;
inset?: boolean;
children?: Snippet;
} & Record<string, unknown>;
import { cn } from "$lib/utils.js";
let {
ref = $bindable(null),
@@ -17,7 +9,7 @@
inset,
children,
...restProps
} : Props = $props();
} = $props();
</script>
<DropdownMenuPrimitive.SubTrigger
@@ -32,4 +24,4 @@
>
{@render children?.()}
<ChevronRightIcon class="ml-auto size-4" />
</DropdownMenuPrimitive.SubTrigger>
</DropdownMenuPrimitive.SubTrigger>

Some files were not shown because too many files have changed in this diff Show More