Compare commits

...

34 Commits

Author SHA1 Message Date
Bruno Postle 07b53f38b4 regenerate_wall_representation: document BBIM_Boolean preservation requirement 2026-03-23 22:32:49 +00:00
Bruno Postle e80fd56d3f Doc clarification for api.geometry.add_wall_representation clippings normal 2026-03-23 22:32:27 +00:00
Bruno Postle 0f0ced593b Doc clarification for api.feature.remove_feature
Generated with the assistance of an AI coding tool.
2026-03-23 22:32:09 +00:00
Bruno Postle 05bd7df68f Doc clarification for api.geometry.edit_object_placement
Generated with the assistance of an AI coding tool.
2026-03-23 22:32:08 +00:00
Bruno Postle ee4681dc4c Doc clarification for api.sequence.assign_process
Generated with the assistance of an AI coding tool.
2026-03-23 22:32:08 +00:00
Sayan J. Das f679c63a18 Merge pull request #7808 from theseyan/ifctester-improvements-rebased
IfcTester webapp improvements
2026-03-23 15:48:34 +05:30
Thomas Krijnen e6cc0e7813 Initialize ncount_total #7834 2026-03-23 10:54:38 +01:00
Dion Moult cf2acfc649 Add covering feature tests for ceiling and cursor variants
Add tests for all four covering generation operators: flooring/ceiling
from walls and flooring/ceiling from cursor. Previously only flooring
from walls was tested.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-22 23:25:04 +11:00
Dion Moult 60ebb99fda Fix covering geometry not persisting to IFC, same root cause as #7055
The covering tool used bmesh as an intermediate and relied on
type.assign_type post-listeners (removed in 44a52863a) to generate
the IfcExtrudedAreaSolid body. With those listeners gone, coverings
had no body representation and assign_swept_area_outer_curve crashed.

Build covering representations from scratch using ShapeBuilder, reading
the extrusion depth from the type's IfcMaterialLayerSet. Also replace
bpy.ops.bim.assign_class with bonsai.core.root.assign_class using
should_add_representation=False, consistent with the space fix.

Refactored shared coordinate-conversion and extrusion-building logic
into get_2d_vertices_from_polygon and set_extrusion_representation_from_polygon,
used by both space and covering code paths. Removed all bmesh-dependent
dead code from the spatial tool.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-22 23:18:59 +11:00
Dion Moult ab96add772 Gitignore all test cache files 2026-03-22 22:26:23 +11:00
Dion Moult d8de623086 Fix space regen not saving geometry to IFC (#7055)
Space regeneration was only updating the Blender mesh and marking the
object as edited, but the IFC representation was never synced on save.
Replace the bmesh-based approach with ShapeBuilder to write geometry
directly to IFC as an IfcExtrudedAreaSolid, then reload via
switch_representation. This applies to both new space creation and
existing space regeneration.

Also changes assign_ifcspace_class_to_obj to call
bonsai.core.root.assign_class directly with
should_add_representation=False instead of bpy.ops.bim.assign_class.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-22 22:14:02 +11:00
dependabot[bot] a3f2e061fb Bump hendrikmuhs/ccache-action from 1.2.20 to 1.2.21
Bumps [hendrikmuhs/ccache-action](https://github.com/hendrikmuhs/ccache-action) from 1.2.20 to 1.2.21.
- [Release notes](https://github.com/hendrikmuhs/ccache-action/releases)
- [Commits](https://github.com/hendrikmuhs/ccache-action/compare/v1.2.20...v1.2.21)

---
updated-dependencies:
- dependency-name: hendrikmuhs/ccache-action
  dependency-version: 1.2.21
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-22 21:31:54 +11:00
dependabot[bot] f51a4673db Bump ruff from 0.15.6 to 0.15.7
Bumps [ruff](https://github.com/astral-sh/ruff) from 0.15.6 to 0.15.7.
- [Release notes](https://github.com/astral-sh/ruff/releases)
- [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md)
- [Commits](https://github.com/astral-sh/ruff/compare/0.15.6...0.15.7)

---
updated-dependencies:
- dependency-name: ruff
  dependency-version: 0.15.7
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-22 21:31:42 +11:00
Dion Moult 75b8d4f218 Remove spatial containment and aggregation when nesting
The nest assign_object API now removes existing spatial containment and
aggregate relationships before creating the nest, matching the behavior
documented in its docstring and consistent with aggregate.assign_object.

Fix #7248

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-22 15:28:47 +11:00
Dion Moult b8136d4762 Prevent cyclic references when assigning nesting or aggregation
Walk up the full hierarchy via get_parent() in can_nest() and
can_aggregate() to reject assignments that would create a cycle.
Also reject self-assignment.

Fix #7248

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-22 15:28:47 +11:00
Dion Moult ecde429d36 Fix crash after undo of assign_class on macOS (#7419)
After assigning an IFC class and undoing, msgbus subscriptions registered
with the old Python object wrapper survived (PERSISTENT flag) but could
not be cleared because: (1) rollback_link_element looked up objects by
their post-link name which no longer exists after undo, and (2) the
per-object clear_by_owner calls in rebuild_element_maps used new Python
wrappers that didn't match the old subscription owners.

Fix by using a dedicated stable object (object_subscription_owner) as
the msgbus owner for all per-object subscriptions, allowing
rebuild_element_maps to clear all stale subscriptions in one call
regardless of Python wrapper identity changes during undo/redo.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-22 14:42:55 +11:00
Dion Moult 1771b34449 Fix error when entering edit mode on camera objects
Fixes #7313.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-22 14:08:12 +11:00
Dion Moult 41469acbc8 Fix walrus operator precedence in MaterialCreator
The `is not ...` was being captured by the walrus assignment due to
missing parentheses, causing the condition to always evaluate incorrectly.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-22 13:58:17 +11:00
Ryan Schultz 547b22199f Without 'Material.Name' layers merge. (#7700) 2026-03-21 18:02:02 -05:00
Dion Moult 94c15213f6 Guard against emptying IfcShapeRepresentation Items
remove_representation_item now returns early if removing the item would
leave Items empty. edit_text_literals returns early on empty attributes.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-21 20:10:11 +11:00
Dion Moult fca258fb07 Fix add_boolean removing second operands from unrelated representations
add_boolean was removing second operands from ALL IfcShapeRepresentations
that referenced them, which could corrupt unrelated shapes and leave
representations with empty Items (bug #7803).

The API no longer modifies Items — callers manage this explicitly.
validate_type and Bonsai's AddBoolean operator now handle their own
item removal scoped to the correct representation.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-21 20:10:11 +11:00
Dion Moult bcfad8d96d Migrate remove_deep to remove_deep2 across API modules
remove_deep is deprecated and can silently delete elements still in use.
remove_deep2 requires zero inverses before removal, making it safer.
Also fixes a double-removal bug in remove_grid_axis and prevents
removing the last prop template from a pset template.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-21 20:10:11 +11:00
Sayan Jyoti Das aa5f5120e0 delete ifcopenshell wheel 2026-03-18 14:37:24 +05:30
Sayan Jyoti Das 81986bcbfb ifcopenshell wasm wheel should be dynamically fetched, not included in git 2026-03-18 14:35:55 +05:30
Sayan Jyoti Das 03de69814a fixes and cleanups from old branch 2026-03-18 11:22:41 +05:30
Sayan Jyoti Das 4dc6a0f2bc update ifcopenshell wheel to ifcopenshell-0.8.5+a51b2c5 2026-03-18 11:18:50 +05:30
Sayan Jyoti Das 47f058341b local ifctester wheel build 2026-03-18 10:29:09 +05:30
Sayan Jyoti Das 845a13ba83 some fixes and lint cleanups 2026-03-17 21:16:53 +05:30
Sayan Jyoti Das 530841967e Merge branch 'v0.8.0' into ifctester-improvements 2026-03-17 19:49:09 +05:30
Sayan Jyoti Das e95da857d6 convert codebase to typescript + introduce biome lint 2026-03-16 21:55:10 +05:30
Sayan Jyoti Das d587d1ac11 build step for pyodide 2026-03-16 21:46:46 +05:30
Sayan Jyoti Das 6117417b89 update webapp + bonsai integration 2026-03-16 15:54:52 +05:30
Thomas Krijnen 0398584c69 empty 2026-03-16 15:45:41 +05:30
Thomas Krijnen 0c69f85d5e Empty 2026-03-16 15:45:40 +05:30
140 changed files with 2873 additions and 1184 deletions
+1 -1
View File
@@ -53,7 +53,7 @@ jobs:
python ../nix/cache_dependencies.py unpack
- name: ccache
uses: hendrikmuhs/ccache-action@v1.2.20
uses: hendrikmuhs/ccache-action@v1.2.21
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.20
uses: hendrikmuhs/ccache-action@v1.2.21
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.20
uses: hendrikmuhs/ccache-action@v1.2.21
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.20
uses: hendrikmuhs/ccache-action@v1.2.21
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.20
uses: hendrikmuhs/ccache-action@v1.2.21
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.20
uses: hendrikmuhs/ccache-action@v1.2.21
-
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.20
uses: hendrikmuhs/ccache-action@v1.2.21
with:
key: ubuntu-22.04-${{ runner.arch }}
+5 -3
View File
@@ -14,6 +14,7 @@
/src/ifcmax/out/
/src/ifcwrap/out/
/src/qtviewer/out/
/src/ifctester/webapp/public/pyodide/
/win/BuildDepsCache*.txt
@@ -85,10 +86,11 @@ src/bonsai/bonsai/translations.py
# bonsai external dependencies (cloned for just ty checks)
src/bonsai/external_dependencies/
# bonsai test temp files
# bonsai test temp/cache files
src/bonsai/test/files/temp
src/bonsai/test/files/basic.ifc.cache.blend
src/bonsai/test/files/basic.ifc.cache.sqlite
src/bonsai/test/files/*.cache.blend
src/bonsai/test/files/*.cache.json
src/bonsai/test/files/*.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.6",
"ruff==0.15.7",
"poethepoet",
"gersemi==0.26.1",
]
+8 -5
View File
@@ -45,16 +45,19 @@ 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. 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
# The object is invalid but somehow still has a callback.
# This can occur during undo/redo when the Python wrapper is stale.
return
# Blender names are up to 63 UTF-8 bytes
if len(bytes(obj.name, "utf-8")) >= 63:
return
@@ -189,7 +192,7 @@ def subscribe_to(obj: bpy.types.ID, data_path: str, callback: Callable[[bpy.type
return
bpy.msgbus.subscribe_rna(
key=subscribe_to,
owner=obj,
owner=object_subscription_owner,
args=(
obj,
data_path,
+4 -9
View File
@@ -316,11 +316,8 @@ class IfcStore:
del IfcStore.id_map[data["id"]]
if "guid" in data:
del IfcStore.guid_map[data["guid"]]
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)
# Note: msgbus subscriptions are cleared globally during
# rebuild_element_maps which runs after every undo/redo.
@staticmethod
def commit_link_element(data: OperationData) -> None:
@@ -367,10 +364,8 @@ class IfcStore:
del IfcStore.id_map[data["id"]]
if "guid" in data:
del IfcStore.guid_map[data["guid"]]
obj = IfcStore.get_object_by_name(data["obj"])
# obj might be removed after unlink.
if not obj:
bpy.msgbus.clear_by_owner(obj)
# Note: msgbus subscriptions are cleared globally during
# rebuild_element_maps which runs after every undo/redo.
@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
@@ -1426,6 +1426,7 @@ 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:
if not obj.data or obj.type not in ("MESH", "CURVE"):
self.report({"INFO"}, "No geometry to edit")
elif tool.Geometry.is_locked(element):
self.report({"ERROR"}, lock_error_message(obj.name))
@@ -540,6 +540,16 @@ 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)
+11 -61
View File
@@ -51,21 +51,11 @@ def add_instance_flooring_covering_from_cursor(
if isinstance(space_polygon, str):
return
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)
obj = spatial.create_object("Covering")
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.assign_swept_area_outer_curve_from_2d_vertices(obj, vertices=points)
body = spatial.get_body_representation(obj)
spatial.regen_obj_representation(obj, body)
spatial.set_covering_representation_from_polygon(obj, space_polygon, polygon_is_si=True)
def add_instance_ceiling_covering_from_cursor(
@@ -95,21 +85,11 @@ def add_instance_ceiling_covering_from_cursor(
if isinstance(space_polygon, str):
return
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)
obj = spatial.create_object("Covering")
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.assign_swept_area_outer_curve_from_2d_vertices(obj, vertices=points)
body = spatial.get_body_representation(obj)
spatial.regen_obj_representation(obj, body)
spatial.set_covering_representation_from_polygon(obj, space_polygon, polygon_is_si=True)
def regen_selected_covering_object(root: type[tool.Root], spatial: type[tool.Spatial]) -> None:
@@ -127,19 +107,7 @@ def regen_selected_covering_object(root: type[tool.Root], spatial: type[tool.Spa
if isinstance(space_polygon, str):
return
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)
spatial.set_covering_representation_from_polygon(active_obj, space_polygon, polygon_is_si=True)
# TODO CHECK IF IT IS POSSIBLE TO CREATE ONLY ONE CORE FUNCTION FOR _FROM_WALLS
@@ -151,22 +119,13 @@ 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.get_named_obj_from_bmesh(name, bmesh=bm)
spatial.set_obj_origin_to_bboxcenter(obj)
obj = spatial.create_object(name)
spatial.set_obj_origin_to_polygon_center(obj, poly, polygon_is_si=False)
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.assign_swept_area_outer_curve_from_2d_vertices(obj, vertices=points)
body = spatial.get_body_representation(obj)
spatial.regen_obj_representation(obj, body)
spatial.set_covering_representation_from_polygon(obj, poly, polygon_is_si=False)
def add_instance_ceiling_coverings_from_walls(
@@ -179,22 +138,13 @@ 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.get_named_obj_from_bmesh(name, bmesh=bm)
spatial.set_obj_origin_to_bboxcenter(obj)
obj = spatial.create_object(name)
spatial.set_obj_origin_to_polygon_center(obj, poly, polygon_is_si=False)
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.assign_swept_area_outer_curve_from_2d_vertices(obj, vertices=points)
body = spatial.get_body_representation(obj)
spatial.regen_obj_representation(obj, body)
spatial.set_covering_representation_from_polygon(obj, poly, polygon_is_si=False)
class NoDefaultContainer(Exception):
+8 -12
View File
@@ -219,13 +219,8 @@ 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"):
mesh = spatial.get_transformed_mesh_from_local_to_global(mesh)
spatial.edit_active_space_obj_from_mesh(mesh)
spatial.set_space_representation_from_polygon(active_obj, element, space_polygon, h, polygon_is_si=True)
spatial.translate_obj_to_z_location(active_obj, z)
else:
if relating_type:
@@ -233,12 +228,13 @@ def generate_space(
else:
name = "Space"
obj = spatial.get_named_obj_from_mesh(name, mesh)
obj = spatial.create_object(name)
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)
@@ -257,16 +253,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.get_named_obj_from_bmesh(name, bmesh=bm)
spatial.set_obj_origin_to_bboxcenter_and_zero_elevation(obj)
obj = spatial.create_object(name)
spatial.set_obj_origin_to_polygon_center(obj, poly, polygon_is_si=False)
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()
+6 -13
View File
@@ -999,14 +999,12 @@ 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_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 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 set_obj_origin_to_cursor_position_and_zero_elevation(cls, obj): pass
def get_selected_objects(cls): pass
def get_active_obj(cls): pass
@@ -1014,14 +1012,9 @@ 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
+28 -9
View File
@@ -49,21 +49,40 @@ 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"
):
return True
if tool.Ifc.get_schema() == "IFC2X3":
is_compatible_class = True
elif tool.Ifc.get_schema() == "IFC2X3":
if relating_object.is_a("IfcSpatialStructureElement") and related_object.is_a("IfcSpatialStructureElement"):
return True
if relating_object.is_a("IfcProject") and related_object.is_a("IfcSpatialStructureElement"):
return True
is_compatible_class = True
elif relating_object.is_a("IfcProject") and related_object.is_a("IfcSpatialStructureElement"):
is_compatible_class = True
else:
if relating_object.is_a("IfcSpatialElement") and related_object.is_a("IfcSpatialElement"):
return True
if relating_object.is_a("IfcProject") and related_object.is_a("IfcSpatialElement"):
return True
return False
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
@classmethod
def has_physical_body_representation(cls, element: ifcopenshell.entity_instance) -> bool:
+2
View File
@@ -857,6 +857,8 @@ 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")]
+4 -3
View File
@@ -1407,8 +1407,6 @@ 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] = []
@@ -1467,7 +1465,10 @@ class Geometry(bonsai.core.tool.Geometry):
cls.remove_representation_items_from_shape_aspect([representation_item], shape_aspect)
if representation:
representation.Items = tuple(set(representation.Items) - {representation_item})
new_items = tuple(set(representation.Items) - {representation_item})
if not new_items:
return
representation.Items = new_items
also_consider = list(consider_inverses)
ifcopenshell.util.element.remove_deep2(ifc_file, representation_item, also_consider=also_consider)
+6 -4
View File
@@ -197,12 +197,16 @@ 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
@@ -217,8 +221,6 @@ 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
+17 -3
View File
@@ -45,9 +45,23 @@ 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.is_a("IfcElement") and related_object.is_a("IfcElement"):
return True
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
@classmethod
def disable_editing(cls, obj: bpy.types.Object) -> None:
+124 -151
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
@@ -991,114 +991,107 @@ class Spatial(bonsai.core.tool.Spatial):
return poly
@classmethod
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:
def create_object(cls, name: str) -> bpy.types.Object:
mesh = bpy.data.meshes.new(name=name)
obj = bpy.data.objects.new(name, mesh)
return obj
@classmethod
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
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))
@classmethod
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
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
@classmethod
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
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,
)
@classmethod
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
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.
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
: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)
@classmethod
def set_obj_origin_to_cursor_position_and_zero_elevation(cls, obj: bpy.types.Object) -> None:
@@ -1145,65 +1138,53 @@ 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:
bpy.ops.bim.assign_class(obj=obj.name, ifc_class="IfcSpace")
bonsai.core.root.assign_class(
tool.Ifc,
tool.Collector,
tool.Root,
obj=obj,
ifc_class="IfcSpace",
should_add_representation=False,
)
@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 = tool.Ifc.get().by_id(int(relating_type_id))
relating_type = ifc_file.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]
bpy.ops.bim.assign_class(obj=obj.name, ifc_class=instance_class)
bonsai.core.root.assign_class(
tool.Ifc,
tool.Collector,
tool.Root,
obj=obj,
ifc_class=instance_class,
should_add_representation=False,
)
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,
@@ -1214,14 +1195,6 @@ 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:
+108 -3
View File
@@ -2,7 +2,7 @@
Feature: Covering
Covers covering tool.
Scenario: Execute generate flooring coverings from walls
Scenario: Add flooring 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: Execute generate flooring coverings 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"
# add_instance_flooring_coverings_from_walls is expecting FLOORING predefined type.
# Set COV30 predefined type to FLOORING.
And the object "IfcCoveringType/COV30" is selected
And I look at the "Attributes" panel
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"
@@ -42,3 +42,108 @@ Scenario: Execute generate flooring coverings 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,6 +18,7 @@
import bpy
import ifcopenshell
import ifcopenshell.api.aggregate
import ifcopenshell.api.context
import ifcopenshell.api.geometry
import ifcopenshell.api.root
@@ -99,6 +100,42 @@ 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,6 +19,7 @@
import bpy
import ifcopenshell
import ifcopenshell.api
import ifcopenshell.api.nest
import ifcopenshell.api.spatial
import bonsai.core.tool
@@ -51,6 +52,42 @@ 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,6 +28,7 @@ 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,8 +51,10 @@ 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_deep(file, inverse)
ifcopenshell.util.element.remove_deep2(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 and file.get_total_inverses(old_unit_basis) == 0:
ifcopenshell.util.element.remove_deep(file, old_unit_basis)
if old_unit_basis:
ifcopenshell.util.element.remove_deep2(file, old_unit_basis)
setattr(cost_value, name, value)
@@ -22,11 +22,13 @@ import ifcopenshell.util.element
def remove_feature(file: ifcopenshell.file, feature: ifcopenshell.entity_instance) -> None:
"""Remove a feature
"""Permanently delete a feature element and its void or projection relationship.
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.
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.
:param feature: The IfcFeatureElement to remove.
@@ -31,17 +31,6 @@ 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
@@ -101,9 +90,6 @@ 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,7 +47,9 @@ 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`.
or dictionaries of arguments for `Clipping.parse`. Each clipping has a
``normal`` that points toward the removed material (the discarded side),
not toward the kept material; see :func:`clip_solid` for details.
:param booleans: List of any existing IfcBooleanResults.
:return: IfcShapeRepresentation.
"""
@@ -52,10 +52,11 @@ 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 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.
opening, filling, etc. If True, child elements move along with the
parent; pass True when moving an assembly (roof, furniture group, etc.)
and you want all children to follow. If False (default), child elements
keep their current world positions; their local placements are rewritten
to compensate for the parent move.
:return: The new or updated IfcLocalPlacement entity
"""
usecase = Usecase()
@@ -69,6 +69,12 @@ 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,6 +83,7 @@ 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,7 +42,5 @@ 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,7 +19,9 @@
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
@@ -137,7 +139,10 @@ def assign_object(
if not objects_to_change:
return is_nested_by
# NOTE: An object can both be nested and assigned to a container or an aggregate.
# 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)
# 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 take care when removing property
templates.
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.
:param prop_template: The IfcSimplePropertyTemplate to remove.
:return: None
@@ -43,10 +43,8 @@ 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:
inverse.HasPropertyTemplates = []
else:
if len(inverse.HasPropertyTemplates) > 1:
has_property_templates = list(inverse.HasPropertyTemplates)
has_property_templates.remove(prop_template)
inverse.HasPropertyTemplates = has_property_templates
ifcopenshell.util.element.remove_deep(file, prop_template)
ifcopenshell.util.element.remove_deep2(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_deep(file, pset_template)
ifcopenshell.util.element.remove_deep2(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_deep(file, old_quantity)
ifcopenshell.util.element.remove_deep2(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_deep(file, old_quantity)
ifcopenshell.util.element.remove_deep2(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 to be related to a process, typically a construction task
"""Assigns an object as an input, control, or resource of a process
Processes work using the ICOM (Input, Controls, Outputs, Mechanisms)
paradigm in IFC. This process model is commonly used in modeling
@@ -63,6 +63,17 @@ 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
@@ -77,7 +88,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 construction task. Note that the predefined type is
# Let's create a demolition 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")
@@ -85,8 +96,12 @@ def assign_process(
# Let's say we have a wall somewhere.
wall = ifcopenshell.api.root.create_entity(model, ifc_class="IfcWall")
# Let's demolish that wall!
# The wall is an INPUT to the demolition task (it will be consumed).
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,4 +47,5 @@ def remove_unit(file: ifcopenshell.file, unit: ifcopenshell.entity_instance) ->
unit_assignment.Units = units
else:
file.remove(unit_assignment)
ifcopenshell.util.element.remove_deep(file, unit)
# TODO handle other possible unit inverses
ifcopenshell.util.element.remove_deep2(file, unit)
@@ -0,0 +1,72 @@
# 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}
assert set(rep.Items) == {boolean, second}
def test_adding_multiple_booleans_from_three_top_level_items(self):
ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcProject")
@@ -58,13 +58,14 @@ class TestAddBoolean(test.bootstrap.IFC4):
booleans = ifcopenshell.api.geometry.add_boolean(self.file, first, [second1, second2])
assert len(booleans) == 2
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"
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}
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")
@@ -78,14 +79,16 @@ 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
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
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}
def test_adding_a_boolean_to_an_existing_operand_from_another_operand(self):
ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcProject")
@@ -104,7 +107,7 @@ class TestAddBoolean(test.bootstrap.IFC4):
booleans = ifcopenshell.api.geometry.add_boolean(self.file, first1, [second2])
assert len(booleans) == 1
assert len(rep.Items) == 2
assert len(rep.Items) == 4
assert self.file.get_total_inverses(first1) == 1
result = next(iter(self.file.get_inverse(first1)))
@@ -132,14 +135,15 @@ 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 len(rep.Items) == 1
assert rep.Items[0].FirstOperand == first
assert rep.Items[0].SecondOperand == 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
ifcopenshell.api.geometry.add_boolean(self.file, second, [second])
ifcopenshell.api.geometry.add_boolean(self.file, second, [first])
assert len(rep.Items) == 1
assert rep.Items[0].FirstOperand == first
assert rep.Items[0].SecondOperand == second
assert set(rep.Items) == {boolean, second}
assert boolean.FirstOperand == first
assert boolean.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) == 3
assert len(rep.Items) == 4
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) == 2
assert len(rep.Items) == 3 # boolean replaced first, but second1 stays in Items
assert ifcopenshell.api.geometry.validate_type(self.file, rep) is False
assert len(rep.Items) == 2
assert len(rep.Items) == 2 # validate_type unioned second1 into the boolean
assert rep.RepresentationType is None
@@ -0,0 +1,59 @@
# 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,8 +18,10 @@
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
@@ -82,6 +84,24 @@ 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
@@ -0,0 +1,38 @@
# 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,)
@@ -0,0 +1,35 @@
# 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
@@ -0,0 +1,44 @@
# 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
+63 -10
View File
@@ -25,9 +25,19 @@ 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:
webapp-dev: pyodide-download ifcopenshell-wasm-download webapp-stage-ifctester-wheel
cd $(WEBAPP_DIR) && npm install
cd $(WEBAPP_DIR) && npm run dev
.PHONY: pyodide-download
@@ -62,8 +72,43 @@ 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
webapp-build: pyodide-download ifcopenshell-wasm-download webapp-stage-ifctester-wheel
cd $(WEBAPP_DIR) && npm install
cd $(WEBAPP_DIR) && npm run build
@@ -76,22 +121,30 @@ 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: dist
dist: webapp-prepare
.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)
$(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)
.PHONY: webapp-prepare
webapp-prepare: webapp-build
rm -rf $(PACKAGE_NAME)/webapp/www/*
mkdir -p $(PACKAGE_NAME)/webapp/www
cp -r $(WEBAPP_BUILD_DIR)/* $(PACKAGE_NAME)/webapp/www/
cp $(WEBAPP_DIR)/__init__.py $(PACKAGE_NAME)/webapp/__init__.py
cp $(WEBAPP_DIR)/serve.py $(PACKAGE_NAME)/webapp/serve.py
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
.PHONY: test
test:
+3 -1
View File
@@ -11,6 +11,8 @@ node_modules
dist
dist-ssr
*.local
public/worker/bin/ifcopenshell-*.whl
public/worker/generated
# Editor directories and files
.vscode/*
@@ -24,4 +26,4 @@ dist-ssr
*.sw?
.claude
experiment/*
experiment/*
+31
View File
@@ -0,0 +1,31 @@
{
"$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.js"></script>
<script type="module" src="/src/main.ts"></script>
</body>
</html>
-39
View File
@@ -1,39 +0,0 @@
{
"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,6 +16,7 @@
"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",
@@ -25,11 +26,13 @@
"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"
}
},
@@ -47,6 +50,170 @@
"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",
@@ -1468,6 +1635,22 @@
"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",
@@ -2126,6 +2309,16 @@
"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",
@@ -2200,6 +2393,20 @@
"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",
@@ -2281,6 +2488,19 @@
"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",
@@ -2783,6 +3003,30 @@
"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",
@@ -2983,6 +3227,20 @@
"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,11 +6,15 @@
"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",
@@ -19,10 +23,12 @@
"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,9 +22,6 @@ 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>
<script lang="ts">
import Router from 'svelte-spa-router';
import routes from './routes';
</script>
+4
View File
@@ -0,0 +1,4 @@
declare module "*.svelte" {
import type { SvelteComponent } from "svelte";
export default class Component extends SvelteComponent {}
}
@@ -1,11 +1,11 @@
<script>
<script lang="ts">
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.js";
import * as API from "$src/modules/api/api.svelte.js";
import { error, success, info } from "$src/modules/utils/toast.svelte.js";
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";
let { isOpen = false } = $props();
let { isOpen = false } : { isOpen?: boolean } = $props();
function openForum() {
window.open('https://community.osarch.org', '_blank');
@@ -23,8 +23,9 @@
try {
await IDS.openDocument();
} catch (err) {
if (err.message !== 'File selection cancelled') {
error('Error opening file: ' + err.message);
const message = err instanceof Error ? err.message : String(err);
if (message !== 'File selection cancelled') {
error(`Error opening file: ${message}`);
console.error(err);
}
}
@@ -50,7 +51,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>
@@ -142,4 +143,4 @@
</Dialog.Close>
</Dialog.Footer>
</Dialog.Content>
</Dialog.Root>
</Dialog.Root>
@@ -1,6 +1,5 @@
<script>
import { onMount } from "svelte";
import { Module } from "$src/modules/api/ids.svelte.js";
<script lang="ts">
import { Module } from "$src/modules/api/ids.svelte";
</script>
<div class="app-ribbon">
@@ -20,4 +19,4 @@
<span>Error</span>
</div>
{/if}
</div>
</div>
@@ -1,14 +1,16 @@
<script>
<script lang="ts">
import * as Tooltip from "$lib/components/ui/tooltip";
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 { 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 { ChevronRightIcon, LinkIcon, XIcon } from "@lucide/svelte";
import { Bonsai, connect, disconnect, runAudit as runBonsaiAudit } from "$src/modules/api/bonsai.svelte.js";
import { Bonsai, connect, disconnect, runAudit as runBonsaiAudit } from "$src/modules/api/bonsai.svelte";
import { onMount } from 'svelte';
import type { AuditReport } from "$src/types/report";
let isAuditing = $state(false);
let activeTab = $state('home');
let activeTab = $state<"home" | "bonsai">('home');
// biome-ignore lint/style/useConst: Svelte state uses assignment for updates.
let isMinimized = $state(false);
const handleLoadModel = async () => {
@@ -16,15 +18,17 @@
await openIfc();
success('IFC model loaded successfully');
} catch (err) {
error(`Failed to load IFC model: ${err.message}`);
const message = err instanceof Error ? err.message : String(err);
error(`Failed to load IFC model: ${message}`);
}
};
const handleUnloadModel = async (modelId) => {
const handleUnloadModel = async (modelId: string) => {
try {
await unloadIfc(modelId);
} catch (err) {
error(`Failed to unload model: ${err.message}`);
const message = err instanceof Error ? err.message : String(err);
error(`Failed to unload model: ${message}`);
}
};
@@ -35,14 +39,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) => {
const auditReport = IFCModels.audits.find(audit => audit.id === auditId);
const handleViewAuditReport = (auditId: string) => {
const auditReport = IFCModels.audits.find(audit => audit.id === auditId) as AuditReport | undefined;
if (!auditReport) return;
// Switch to the IDS document that was used for this audit
@@ -58,7 +62,7 @@
}
};
const formatFileSize = (bytes) => {
const formatFileSize = (bytes: number) => {
const units = ['B', 'KB', 'MB', 'GB'];
let size = bytes;
let unitIndex = 0;
@@ -91,7 +95,7 @@
<div class="buttons">
<Tooltip.Provider>
{#if isMinimized}
<Tooltip.Root disableHoverableContent="true">
<Tooltip.Root disableHoverableContent>
<Tooltip.Trigger>
<button class="tb-btn expand-btn" onclick={() => isMinimized = false} aria-label="Expand Toolbar">
<ChevronRightIcon size={24} />
@@ -102,7 +106,7 @@
</Tooltip.Content>
</Tooltip.Root>
{/if}
<Tooltip.Root disableHoverableContent="true">
<Tooltip.Root disableHoverableContent>
<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">
@@ -114,7 +118,7 @@
<p>Home</p>
</Tooltip.Content>
</Tooltip.Root>
<Tooltip.Root disableHoverableContent="true">
<Tooltip.Root disableHoverableContent>
<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">
@@ -692,4 +696,4 @@
opacity: 0.5;
}
}
</style>
</style>
@@ -1,7 +1,9 @@
<script>
<script lang="ts">
import * as DropdownMenu from "$lib/components/ui/dropdown-menu";
let { addFacet } = $props();
type FacetType = "entity" | "attribute" | "classification" | "partOf" | "property" | "material";
const { addFacet } : { addFacet: (facetType: FacetType) => void } = $props();
</script>
<DropdownMenu.Root>
@@ -63,4 +65,4 @@
Part Of
</DropdownMenu.Item>
</DropdownMenu.Content>
</DropdownMenu.Root>
</DropdownMenu.Root>
@@ -1,13 +1,22 @@
<script>
import * as IDS from "$src/modules/api/ids.svelte.js";
<script lang="ts">
import * as IDS from "$src/modules/api/ids.svelte";
import type { IdsDocument } from "$src/types/ids";
function switchDocument(docId) {
function switchDocument(docId: string) {
IDS.Module.activeDocument = docId;
}
function closeDocument(docId) {
function closeDocument(docId: string) {
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">
@@ -15,10 +24,13 @@
<div
class="ids-tab"
class:active={IDS.Module.activeDocument === docId}
role="button"
tabindex="0"
onclick={() => switchDocument(docId)}
aria-label={doc.info.title || "Untitled"}
onkeydown={(event) => handleActivation(event, () => switchDocument(docId))}
aria-label={(doc as IdsDocument).info.title || "Untitled"}
>
<span class="tab-title">{doc.info.title || "Untitled"}</span>
<span class="tab-title">{(doc as IdsDocument).info.title || "Untitled"}</span>
<button
class="tab-close"
onclick={(e) => { e.stopPropagation(); closeDocument(docId); }}
@@ -31,4 +43,4 @@
</div>
{/each}
<div class="filler-tab"></div>
</div>
</div>
+3 -4
View File
@@ -1,8 +1,7 @@
{
"wasm": {
"wheel_url": "/worker/bin/ifcopenshell-0.8.3+bb329af-cp313-cp313-emscripten_4_0_9_wasm32.whl",
"wheel_url": "/worker/bin/ifcopenshell-0.8.5+a51b2c5-cp313-cp313-pyodide_2025_0_wasm32.whl",
"odfpy_url": "/worker/bin/odfpy-1.4.2-py2.py3-none-any.whl",
"api_py_url": "/worker/api.py",
"pyodide_url": "https://cdn.jsdelivr.net/pyodide/v0.28.0/full/pyodide.js"
"api_py_url": "/worker/api.py"
}
}
}
+2 -1
View File
@@ -576,7 +576,8 @@ html, body {
display: flex;
flex-direction: column;
label {
label,
.form-label {
margin-bottom: 4px;
font-size: 14px;
font-weight: 500;
@@ -1,7 +1,11 @@
<script>
<script lang="ts">
import { Dialog as DialogPrimitive } from "bits-ui";
let { ref = $bindable(null), ...restProps } = $props();
type Props = {
ref?: HTMLElement | null;
} & Record<string, unknown>;
let { ref = $bindable(null), ...restProps } : Props = $props();
</script>
<DialogPrimitive.Close bind:ref data-slot="dialog-close" {...restProps} />
<DialogPrimitive.Close bind:ref data-slot="dialog-close" {...restProps} />
@@ -1,8 +1,17 @@
<script>
<script lang="ts">
import { Dialog as DialogPrimitive } from "bits-ui";
import XIcon from "@lucide/svelte/icons/x";
import * as Dialog from "./index.js";
import { cn } from "$lib/utils.js";
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>;
let {
ref = $bindable(null),
@@ -11,7 +20,7 @@
children,
showCloseButton = true,
...restProps
} = $props();
} : Props = $props();
</script>
<Dialog.Portal {...portalProps}>
@@ -35,4 +44,4 @@
</DialogPrimitive.Close>
{/if}
</DialogPrimitive.Content>
</Dialog.Portal>
</Dialog.Portal>
@@ -1,12 +1,17 @@
<script>
<script lang="ts">
import { Dialog as DialogPrimitive } from "bits-ui";
import { cn } from "$lib/utils.js";
import { cn } from "$lib/utils";
type Props = {
ref?: HTMLElement | null;
class?: string;
} & Record<string, unknown>;
let {
ref = $bindable(null),
class: className,
...restProps
} = $props();
} : Props = $props();
</script>
<DialogPrimitive.Description
@@ -14,4 +19,4 @@
data-slot="dialog-description"
class={cn("text-muted-foreground text-sm", className)}
{...restProps}
/>
/>
@@ -1,11 +1,19 @@
<script>
import { cn } from "$lib/utils.js";
<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>;
let {
ref = $bindable(null),
class: className,
children,
...restProps
} = $props();
} : Props = $props();
</script>
<div
@@ -15,4 +23,4 @@
{...restProps}
>
{@render children?.()}
</div>
</div>
@@ -1,12 +1,19 @@
<script>
import { cn } from "$lib/utils.js";
<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>;
let {
ref = $bindable(null),
class: className,
children,
...restProps
} = $props();
} : Props = $props();
</script>
<div
@@ -16,4 +23,4 @@
{...restProps}
>
{@render children?.()}
</div>
</div>
@@ -1,12 +1,17 @@
<script>
<script lang="ts">
import { Dialog as DialogPrimitive } from "bits-ui";
import { cn } from "$lib/utils.js";
import { cn } from "$lib/utils";
type Props = {
ref?: HTMLElement | null;
class?: string;
} & Record<string, unknown>;
let {
ref = $bindable(null),
class: className,
...restProps
} = $props();
} : Props = $props();
</script>
<DialogPrimitive.Overlay
@@ -17,4 +22,4 @@
className
)}
{...restProps}
/>
/>
@@ -1,12 +1,17 @@
<script>
<script lang="ts">
import { Dialog as DialogPrimitive } from "bits-ui";
import { cn } from "$lib/utils.js";
import { cn } from "$lib/utils";
type Props = {
ref?: HTMLElement | null;
class?: string;
} & Record<string, unknown>;
let {
ref = $bindable(null),
class: className,
...restProps
} = $props();
} : Props = $props();
</script>
<DialogPrimitive.Title
@@ -14,4 +19,4 @@
data-slot="dialog-title"
class={cn("text-lg leading-none", className)}
{...restProps}
/>
/>
@@ -1,7 +1,11 @@
<script>
<script lang="ts">
import { Dialog as DialogPrimitive } from "bits-ui";
let { ref = $bindable(null), ...restProps } = $props();
type Props = {
ref?: HTMLElement | null;
} & Record<string, unknown>;
let { ref = $bindable(null), ...restProps } : Props = $props();
</script>
<DialogPrimitive.Trigger bind:ref data-slot="dialog-trigger" {...restProps} />
<DialogPrimitive.Trigger bind:ref data-slot="dialog-trigger" {...restProps} />
@@ -1,8 +1,16 @@
<script>
<script lang="ts">
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.js";
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>;
let {
ref = $bindable(null),
checked = $bindable(false),
@@ -10,7 +18,7 @@
class: className,
children: childrenProp,
...restProps
} = $props();
} : Props = $props();
</script>
<DropdownMenuPrimitive.CheckboxItem
@@ -34,4 +42,4 @@
</span>
{@render childrenProp?.()}
{/snippet}
</DropdownMenuPrimitive.CheckboxItem>
</DropdownMenuPrimitive.CheckboxItem>
@@ -1,14 +1,21 @@
<script>
import { cn } from "$lib/utils.js";
<script lang="ts">
import { cn } from "$lib/utils";
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}>
@@ -22,4 +29,4 @@
)}
{...restProps}
/>
</DropdownMenuPrimitive.Portal>
</DropdownMenuPrimitive.Portal>
@@ -1,12 +1,17 @@
<script>
<script lang="ts">
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
import { cn } from "$lib/utils.js";
import { cn } from "$lib/utils";
type Props = {
ref?: HTMLElement | null;
class?: string;
inset?: boolean;
} & Record<string, unknown>;
let {
ref = $bindable(null),
class: className,
inset,
...restProps
} = $props();
} : Props = $props();
</script>
<DropdownMenuPrimitive.GroupHeading
@@ -15,4 +20,4 @@
data-inset={inset}
class={cn("px-2 py-1.5 text-sm font-semibold data-[inset]:pl-8", className)}
{...restProps}
/>
/>
@@ -1,7 +1,11 @@
<script>
<script lang="ts">
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
let { ref = $bindable(null), ...restProps } = $props();
type Props = {
ref?: HTMLElement | null;
} & Record<string, unknown>;
let { ref = $bindable(null), ...restProps } : Props = $props();
</script>
<DropdownMenuPrimitive.Group bind:ref data-slot="dropdown-menu-group" {...restProps} />
<DropdownMenuPrimitive.Group bind:ref data-slot="dropdown-menu-group" {...restProps} />
@@ -1,14 +1,21 @@
<script>
import { cn } from "$lib/utils.js";
<script lang="ts">
import { cn } from "$lib/utils";
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
@@ -21,4 +28,4 @@
className
)}
{...restProps}
/>
/>
@@ -1,12 +1,20 @@
<script>
import { cn } from "$lib/utils.js";
<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>;
let {
ref = $bindable(null),
class: className,
inset,
children,
...restProps
} = $props();
} : Props = $props();
</script>
<div
@@ -17,4 +25,4 @@
{...restProps}
>
{@render children?.()}
</div>
</div>
@@ -1,11 +1,16 @@
<script>
<script lang="ts">
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
@@ -13,4 +18,4 @@
bind:value
data-slot="dropdown-menu-radio-group"
{...restProps}
/>
/>
@@ -1,14 +1,22 @@
<script>
<script lang="ts">
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
import CircleIcon from "@lucide/svelte/icons/circle";
import { cn } from "$lib/utils.js";
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>;
let {
ref = $bindable(null),
class: className,
children: childrenProp,
...restProps
} = $props();
} : Props = $props();
</script>
<DropdownMenuPrimitive.RadioItem
@@ -28,4 +36,4 @@
</span>
{@render childrenProp?.({ checked })}
{/snippet}
</DropdownMenuPrimitive.RadioItem>
</DropdownMenuPrimitive.RadioItem>
@@ -1,12 +1,17 @@
<script>
<script lang="ts">
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
import { cn } from "$lib/utils.js";
import { cn } from "$lib/utils";
type Props = {
ref?: HTMLElement | null;
class?: string;
} & Record<string, unknown>;
let {
ref = $bindable(null),
class: className,
...restProps
} = $props();
} : Props = $props();
</script>
<DropdownMenuPrimitive.Separator
@@ -14,4 +19,4 @@
data-slot="dropdown-menu-separator"
class={cn("bg-border -mx-1 my-1 h-px", className)}
{...restProps}
/>
/>
@@ -1,12 +1,19 @@
<script>
import { cn } from "$lib/utils.js";
<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>;
let {
ref = $bindable(null),
class: className,
children,
...restProps
} = $props();
} : Props = $props();
</script>
<span
@@ -16,4 +23,4 @@
{...restProps}
>
{@render children?.()}
</span>
</span>
@@ -1,12 +1,17 @@
<script>
<script lang="ts">
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
import { cn } from "$lib/utils.js";
import { cn } from "$lib/utils";
type Props = {
ref?: HTMLElement | null;
class?: string;
} & Record<string, unknown>;
let {
ref = $bindable(null),
class: className,
...restProps
} = $props();
} : Props = $props();
</script>
<DropdownMenuPrimitive.SubContent
@@ -17,4 +22,4 @@
className
)}
{...restProps}
/>
/>
@@ -1,7 +1,15 @@
<script>
<script lang="ts">
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
import ChevronRightIcon from "@lucide/svelte/icons/chevron-right";
import { cn } from "$lib/utils.js";
import { cn } from "$lib/utils";
import type { Snippet } from "svelte";
type Props = {
ref?: HTMLElement | null;
class?: string;
inset?: boolean;
children?: Snippet;
} & Record<string, unknown>;
let {
ref = $bindable(null),
@@ -9,7 +17,7 @@
inset,
children,
...restProps
} = $props();
} : Props = $props();
</script>
<DropdownMenuPrimitive.SubTrigger
@@ -24,4 +32,4 @@
>
{@render children?.()}
<ChevronRightIcon class="ml-auto size-4" />
</DropdownMenuPrimitive.SubTrigger>
</DropdownMenuPrimitive.SubTrigger>
@@ -1,7 +1,11 @@
<script>
<script lang="ts">
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
let { ref = $bindable(null), ...restProps } = $props();
type Props = {
ref?: HTMLElement | null;
} & Record<string, unknown>;
let { ref = $bindable(null), ...restProps } : Props = $props();
</script>
<DropdownMenuPrimitive.Trigger bind:ref data-slot="dropdown-menu-trigger" {...restProps} />
<DropdownMenuPrimitive.Trigger bind:ref data-slot="dropdown-menu-trigger" {...restProps} />
@@ -1,8 +1,16 @@
<script>
<script lang="ts">
import { Menubar as MenubarPrimitive } from "bits-ui";
import CheckIcon from "@lucide/svelte/icons/check";
import MinusIcon from "@lucide/svelte/icons/minus";
import { cn } from "$lib/utils.js";
import { cn } from "$lib/utils";
import type { Snippet } from "svelte";
type Props = {
ref?: HTMLElement | null;
class?: string;
checked?: boolean;
indeterminate?: boolean;
children?: Snippet;
} & Record<string, unknown>;
let {
ref = $bindable(null),
class: className,
@@ -10,7 +18,7 @@
indeterminate = $bindable(false),
children: childrenProp,
...restProps
} = $props();
} : Props = $props();
</script>
<MenubarPrimitive.CheckboxItem
@@ -34,4 +42,4 @@
</span>
{@render childrenProp?.()}
{/snippet}
</MenubarPrimitive.CheckboxItem>
</MenubarPrimitive.CheckboxItem>
@@ -1,6 +1,19 @@
<script>
<script lang="ts">
import { Menubar as MenubarPrimitive } from "bits-ui";
import { cn } from "$lib/utils.js";
import { cn } from "$lib/utils";
type Align = "start" | "center" | "end";
type Side = "top" | "bottom" | "left" | "right";
type Props = {
ref?: HTMLElement | null;
class?: string;
sideOffset?: number;
alignOffset?: number;
align?: Align;
side?: Side;
portalProps?: Record<string, unknown>;
} & Record<string, unknown>;
let {
ref = $bindable(null),
@@ -11,7 +24,7 @@
side = "bottom",
portalProps,
...restProps
} = $props();
} : Props = $props();
</script>
<MenubarPrimitive.Portal {...portalProps}>
@@ -28,4 +41,4 @@
)}
{...restProps}
/>
</MenubarPrimitive.Portal>
</MenubarPrimitive.Portal>
@@ -1,12 +1,17 @@
<script>
import { cn } from "$lib/utils.js";
<script lang="ts">
import { cn } from "$lib/utils";
import { Menubar as MenubarPrimitive } from "bits-ui";
type Props = {
ref?: HTMLElement | null;
inset?: boolean;
class?: string;
} & Record<string, unknown>;
let {
ref = $bindable(null),
inset,
class: className,
...restProps
} = $props();
} : Props = $props();
</script>
<MenubarPrimitive.GroupHeading
@@ -15,4 +20,4 @@
data-inset={inset}
class={cn("px-2 py-1.5 text-sm font-medium data-[inset]:pl-8", className)}
{...restProps}
/>
/>
@@ -1,10 +1,14 @@
<script>
<script lang="ts">
import { Menubar as MenubarPrimitive } from "bits-ui";
type Props = {
ref?: HTMLElement | null;
} & Record<string, unknown>;
let {
ref = $bindable(null),
...restProps
} = $props();
} : Props = $props();
</script>
<MenubarPrimitive.Group bind:ref data-slot="menubar-group" {...restProps} />
<MenubarPrimitive.Group bind:ref data-slot="menubar-group" {...restProps} />
@@ -1,6 +1,13 @@
<script>
<script lang="ts">
import { Menubar as MenubarPrimitive } from "bits-ui";
import { cn } from "$lib/utils.js";
import { cn } from "$lib/utils";
type Props = {
ref?: HTMLElement | null;
class?: string;
inset?: boolean;
variant?: string;
} & Record<string, unknown>;
let {
ref = $bindable(null),
@@ -8,7 +15,7 @@
inset = undefined,
variant = "default",
...restProps
} = $props();
} : Props = $props();
</script>
<MenubarPrimitive.Item
@@ -21,4 +28,4 @@
className
)}
{...restProps}
/>
/>
@@ -1,12 +1,19 @@
<script>
import { cn } from "$lib/utils.js";
<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>;
let {
ref = $bindable(null),
inset,
children,
class: className,
...restProps
} = $props();
} : Props = $props();
</script>
<div
@@ -17,4 +24,4 @@
{...restProps}
>
{@render children?.()}
</div>
</div>

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