From 5616367a03ea397885e93523e55788da82238742 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Tue, 10 Sep 2024 12:15:29 +0500 Subject: [PATCH 01/56] don't deep copy psets for optimization Related to #5291 pset.edit_pset should cover it since it does support unsharing shared properties --- .../ifcopenshell/api/profile/copy_profile.py | 2 +- src/ifcopenshell-python/ifcopenshell/api/pset/unshare_pset.py | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/ifcopenshell-python/ifcopenshell/api/profile/copy_profile.py b/src/ifcopenshell-python/ifcopenshell/api/profile/copy_profile.py index 61d3c7d6cc..0da06f2beb 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/profile/copy_profile.py +++ b/src/ifcopenshell-python/ifcopenshell/api/profile/copy_profile.py @@ -42,6 +42,6 @@ def copy_profile(file: ifcopenshell.file, profile: ifcopenshell.entity_instance) inverses = file.get_inverse(profile) psets = [i for i in inverses if i.is_a("IfcProfileProperties")] for pset in psets: - new_pset = ifcopenshell.util.element.copy_deep(file, pset, exclude=["IfcProfileDef"]) + new_pset = ifcopenshell.util.element.copy(file, pset) new_pset.ProfileDefinition = new_profile return new_profile diff --git a/src/ifcopenshell-python/ifcopenshell/api/pset/unshare_pset.py b/src/ifcopenshell-python/ifcopenshell/api/pset/unshare_pset.py index acf955879a..051905b2b7 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/pset/unshare_pset.py +++ b/src/ifcopenshell-python/ifcopenshell/api/pset/unshare_pset.py @@ -87,7 +87,8 @@ def unshare_pset( for product in products: # No need to consider about profile/material properties since # they are assigned to 1 element directly and therefore cannot be shared. - pset_copy = ifcopenshell.util.element.copy_deep(file, pset) + # Don't copy_deep to keep it light - edit_pset supports unsharing shared props. + pset_copy = ifcopenshell.util.element.copy(file, pset) pset_copies.append(pset_copy) ifcopenshell.api.pset.assign_pset(file, [product], pset_copy) From 765b5c2f5c633a2ca9dc11d0bce43c2a16e2fa73 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Tue, 10 Sep 2024 17:45:45 +0500 Subject: [PATCH 02/56] black format --- src/bonsai/bonsai/bim/module/spatial/operator.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/bonsai/bonsai/bim/module/spatial/operator.py b/src/bonsai/bonsai/bim/module/spatial/operator.py index 5352ff144c..f0c117528b 100644 --- a/src/bonsai/bonsai/bim/module/spatial/operator.py +++ b/src/bonsai/bonsai/bim/module/spatial/operator.py @@ -414,7 +414,7 @@ class ToggleGrids(bpy.types.Operator, tool.Ifc.Operator): is_visible: bpy.props.BoolProperty(name="Is Visible", default=False, options={"SKIP_SAVE"}) def _execute(self, context): - for element in (tool.Ifc.get().by_type("IfcGrid") + tool.Ifc.get().by_type("IfcGridAxis")): + for element in tool.Ifc.get().by_type("IfcGrid") + tool.Ifc.get().by_type("IfcGridAxis"): if obj := tool.Ifc.get_object(element): obj.hide_set(not self.is_visible) From 9a6fc58949f8383d50378a87733ad78357373b7c Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Mon, 9 Sep 2024 18:32:33 +0500 Subject: [PATCH 03/56] Fix typo --- src/bonsai/bonsai/core/profile.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/bonsai/bonsai/core/profile.py b/src/bonsai/bonsai/core/profile.py index d8cf537345..a0ee00b6de 100644 --- a/src/bonsai/bonsai/core/profile.py +++ b/src/bonsai/bonsai/core/profile.py @@ -26,7 +26,7 @@ if TYPE_CHECKING: def purge_unused_profiles(ifc: tool.Ifc, profile: tool.Profile) -> int: - """Purge profiles that have no invserses. + """Purge profiles that have no inverses. :return: Number of removed profiles. """ From ddf92ab8fb0b98f9401bc0f45d07515e09b64cc3 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Tue, 10 Sep 2024 12:05:07 +0500 Subject: [PATCH 04/56] typing --- src/ifc5d/ifc5d/qto.py | 1 + .../ifcopenshell/api/pset/edit_pset.py | 5 ++++- .../ifcopenshell/util/sequence.py | 13 ++++++++++++- 3 files changed, 17 insertions(+), 2 deletions(-) diff --git a/src/ifc5d/ifc5d/qto.py b/src/ifc5d/ifc5d/qto.py index f73d5870c1..8fe9380518 100644 --- a/src/ifc5d/ifc5d/qto.py +++ b/src/ifc5d/ifc5d/qto.py @@ -24,6 +24,7 @@ import ifcopenshell.api.pset import ifcopenshell.util.unit import ifcopenshell.util.element import ifcopenshell.util.selector +import ifcopenshell.util.shape import ifcopenshell.util.representation import multiprocessing from collections import namedtuple diff --git a/src/ifcopenshell-python/ifcopenshell/api/pset/edit_pset.py b/src/ifcopenshell-python/ifcopenshell/api/pset/edit_pset.py index 797f0afba3..6de7ca5f44 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/pset/edit_pset.py +++ b/src/ifcopenshell-python/ifcopenshell/api/pset/edit_pset.py @@ -167,6 +167,9 @@ def edit_pset( class Usecase: + file: ifcopenshell.file + settings: dict[str, Any] + def execute(self) -> None: self.update_pset_name() self.load_pset_template() @@ -378,7 +381,7 @@ class Usecase: properties.append(self.file.create_entity("IfcPropertySingleValue", **args)) return properties - def assign_new_properties(self, props: ifcopenshell.entity_instance) -> None: + def assign_new_properties(self, props: list[ifcopenshell.entity_instance]) -> None: if hasattr(self.settings["pset"], "HasProperties"): self.settings["pset"].HasProperties = props diff --git a/src/ifcopenshell-python/ifcopenshell/util/sequence.py b/src/ifcopenshell-python/ifcopenshell/util/sequence.py index f476df5792..166a3c3674 100644 --- a/src/ifcopenshell-python/ifcopenshell/util/sequence.py +++ b/src/ifcopenshell-python/ifcopenshell/util/sequence.py @@ -36,7 +36,18 @@ RECURRENCE_TYPE = Literal[ ] -def derive_date(task, attribute_name, date=None, is_earliest=False, is_latest=False): +def derive_date( + task: ifcopenshell.entity_instance, + attribute_name: str, + date=None, + is_earliest: bool = False, + is_latest: bool = False, +): + """ + + :param task: IfcTask. + + """ if task.TaskTime: current_date = ( ifcopenshell.util.date.ifc2datetime(getattr(task.TaskTime, attribute_name)) From 2d68d344bd49caa98c34feb69d7b883790af184b Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Tue, 10 Sep 2024 14:57:22 +0500 Subject: [PATCH 05/56] Profiles UI - update displayed psets when user is changing active profile previously it would get stuck until some other operator would refresh ui --- src/bonsai/bonsai/bim/module/pset/data.py | 15 ++++++++++++--- src/bonsai/bonsai/bim/module/pset/ui.py | 10 +++++++++- src/bonsai/bonsai/tool/profile.py | 8 ++++++++ 3 files changed, 29 insertions(+), 4 deletions(-) diff --git a/src/bonsai/bonsai/bim/module/pset/data.py b/src/bonsai/bonsai/bim/module/pset/data.py index c22ee88053..e9768ff0ab 100644 --- a/src/bonsai/bonsai/bim/module/pset/data.py +++ b/src/bonsai/bonsai/bim/module/pset/data.py @@ -290,9 +290,18 @@ class ProfilePsetsData(Data): @classmethod def load(cls): - pprops = bpy.context.scene.BIMProfileProperties - ifc_definition_id = pprops.profiles[pprops.active_profile_index].ifc_definition_id - cls.data = {"psets": cls.psetqtos(tool.Ifc.get().by_id(ifc_definition_id), psets_only=True)} + active_profile = tool.Profile.get_active_profile_ui() + if active_profile: + ifc_definition_id = active_profile.ifc_definition_id + psets_data = cls.psetqtos(tool.Ifc.get().by_id(ifc_definition_id), psets_only=True) + else: + ifc_definition_id = 0 + psets_data = [] + + cls.data = { + "ifc_definition_id": ifc_definition_id, + "psets": psets_data, + } cls.is_loaded = True diff --git a/src/bonsai/bonsai/bim/module/pset/ui.py b/src/bonsai/bonsai/bim/module/pset/ui.py index fd01a3b6af..3177a9ae31 100644 --- a/src/bonsai/bonsai/bim/module/pset/ui.py +++ b/src/bonsai/bonsai/bim/module/pset/ui.py @@ -664,7 +664,15 @@ class BIM_PT_profile_psets(Panel): return False def draw(self, context): - if not ProfilePsetsData.is_loaded: + active_profile = tool.Profile.get_active_profile_ui() + + if not active_profile: + return + + if ( + not ProfilePsetsData.is_loaded + or active_profile.ifc_definition_id != ProfilePsetsData.data["ifc_definition_id"] + ): ProfilePsetsData.load() props = context.scene.ProfilePsetProperties diff --git a/src/bonsai/bonsai/tool/profile.py b/src/bonsai/bonsai/tool/profile.py index 859f8e4a3b..cc7e91f0b3 100644 --- a/src/bonsai/bonsai/tool/profile.py +++ b/src/bonsai/bonsai/tool/profile.py @@ -16,6 +16,7 @@ # You should have received a copy of the GNU General Public License # along with Bonsai. If not, see . +import bpy import ifcopenshell import ifcopenshell.api.profile import ifcopenshell.geom @@ -104,3 +105,10 @@ class Profile(bonsai.core.tool.Profile): # In UI unnamed profiles are not available, so we don't handle them. new_profile.ProfileName = profile.ProfileName + "_copy" return new_profile + + @classmethod + def get_active_profile_ui(cls) -> Union[bpy.types.PropertyGroup, None]: + props = bpy.context.scene.BIMProfileProperties + index = props.active_profile_index + if len(props.profiles) > index >= 0: + return props.profiles[index] From 3bb0901e3e6a7ade239d538faa88ce9238bc4503 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Tue, 10 Sep 2024 15:20:51 +0500 Subject: [PATCH 06/56] Profiles UI - better handling for invalid profiles Previously it would be just console errors possibly breaking UI, now there will be UI error message - https://imgur.com/1o9teJ0 --- src/bonsai/bonsai/bim/module/profile/data.py | 1 + src/bonsai/bonsai/bim/module/profile/prop.py | 14 +++++++++++-- src/bonsai/bonsai/bim/module/profile/ui.py | 21 +++++++++++--------- src/bonsai/bonsai/tool/profile.py | 4 ++++ 4 files changed, 29 insertions(+), 11 deletions(-) diff --git a/src/bonsai/bonsai/bim/module/profile/data.py b/src/bonsai/bonsai/bim/module/profile/data.py index 2402192aad..2ea97a9a20 100644 --- a/src/bonsai/bonsai/bim/module/profile/data.py +++ b/src/bonsai/bonsai/bim/module/profile/data.py @@ -29,6 +29,7 @@ def refresh(): class ProfileData: data = {} + failed_previews: set[int] = set() preview_collection = bpy.utils.previews.new() is_loaded = False diff --git a/src/bonsai/bonsai/bim/module/profile/prop.py b/src/bonsai/bonsai/bim/module/profile/prop.py index afe21b1446..9fa4f5e360 100644 --- a/src/bonsai/bonsai/bim/module/profile/prop.py +++ b/src/bonsai/bonsai/bim/module/profile/prop.py @@ -81,14 +81,24 @@ def generate_thumbnail_for_active_profile(): if not props.profiles: bpy.ops.bim.load_profiles() - profile_id = props.profiles[props.active_profile_index].ifc_definition_id + active_profile = tool.Profile.get_active_profile_ui() + assert active_profile + profile_id = active_profile.ifc_definition_id profile = ifc_file.by_id(profile_id) # generate image size = 128 img = Image.new("RGBA", (size, size)) draw = ImageDraw.Draw(img) - tool.Profile.draw_image_for_ifc_profile(draw, profile, size) + + try: + tool.Profile.draw_image_for_ifc_profile(draw, profile, size) + except RuntimeError as e: + print(f"Failed to generate preview image for profile '{profile}': '{e}'.") + ProfileData.failed_previews.add(profile_id) + return + + ProfileData.failed_previews.discard(profile_id) pixels = [item for sublist in img.getdata() for item in sublist] # save generated image to preview collection diff --git a/src/bonsai/bonsai/bim/module/profile/ui.py b/src/bonsai/bonsai/bim/module/profile/ui.py index 56c5aefe96..23eecc0037 100644 --- a/src/bonsai/bonsai/bim/module/profile/ui.py +++ b/src/bonsai/bonsai/bim/module/profile/ui.py @@ -43,19 +43,22 @@ class BIM_PT_profiles(Panel): self.props = context.scene.BIMProfileProperties active_profile = None - if self.props.is_editing and self.props.profiles and self.props.active_profile_index < len(self.props.profiles): + if self.props.is_editing and (active_profile := tool.Profile.get_active_profile_ui()): preview_collection = ProfileData.preview_collection box = self.layout.box() - active_profile = self.props.profiles[self.props.active_profile_index] profile_id = active_profile.ifc_definition_id - profile_id_str = str(profile_id) - if profile_id_str in preview_collection: - preview_image = preview_collection[profile_id_str] - else: - preview_image = preview_collection.new(profile_id_str) - generate_thumbnail_for_active_profile() - box.template_icon(icon_value=preview_image.icon_id, scale=5) + if profile_id in ProfileData.failed_previews: + box.label(text="Failed to load preview (invalid profile).", icon="ERROR") + else: + profile_id_str = str(profile_id) + if profile_id_str in preview_collection: + preview_image = preview_collection[profile_id_str] + else: + preview_image = preview_collection.new(profile_id_str) + generate_thumbnail_for_active_profile() + + box.template_icon(icon_value=preview_image.icon_id, scale=5) row = self.layout.row(align=True) row.label(text=f"{ProfileData.data['total_profiles']} Named Profiles", icon="ITALIC") diff --git a/src/bonsai/bonsai/tool/profile.py b/src/bonsai/bonsai/tool/profile.py index cc7e91f0b3..ae1d9a45de 100644 --- a/src/bonsai/bonsai/tool/profile.py +++ b/src/bonsai/bonsai/tool/profile.py @@ -40,7 +40,11 @@ class Profile(bonsai.core.tool.Profile): settings = ifcopenshell.geom.settings() settings.set("dimensionality", ifcopenshell.ifcopenshell_wrapper.CURVES_SURFACES_AND_SOLIDS) shape = ifcopenshell.geom.create_shape(settings, profile) + verts = shape.verts + if not verts: + raise RuntimeError("Profile shape has no vertices, it probably is invalid.") + edges = shape.edges grouped_verts = [[verts[i], verts[i + 1]] for i in range(0, len(verts), 3)] From dc92a8fd2784a1837f6f5b4491278f86a9d81496 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Tue, 10 Sep 2024 16:10:35 +0500 Subject: [PATCH 07/56] Bonsai - setup default values for new patametric profiles So there will be less invalid IFC and user will get a preview right away after creating a new profile. Example - https://imgur.com/a/GjkCql4 --- .../bonsai/bim/module/profile/operator.py | 1 + src/bonsai/bonsai/tool/profile.py | 101 ++++++++++++++++++ 2 files changed, 102 insertions(+) diff --git a/src/bonsai/bonsai/bim/module/profile/operator.py b/src/bonsai/bonsai/bim/module/profile/operator.py index 2c0a7faee8..459f9bf1ce 100644 --- a/src/bonsai/bonsai/bim/module/profile/operator.py +++ b/src/bonsai/bonsai/bim/module/profile/operator.py @@ -149,6 +149,7 @@ class AddProfileDef(bpy.types.Operator, tool.Ifc.Operator): profile = ifcopenshell.api.run("profile.add_arbitrary_profile", tool.Ifc.get(), profile=points) else: profile = ifcopenshell.api.run("profile.add_parameterized_profile", tool.Ifc.get(), ifc_class=profile_class) + tool.Profile.set_default_profile_attrs(profile) profile.ProfileName = "New Profile" bpy.ops.bim.load_profiles() diff --git a/src/bonsai/bonsai/tool/profile.py b/src/bonsai/bonsai/tool/profile.py index ae1d9a45de..6a8d0a3b6d 100644 --- a/src/bonsai/bonsai/tool/profile.py +++ b/src/bonsai/bonsai/tool/profile.py @@ -116,3 +116,104 @@ class Profile(bonsai.core.tool.Profile): index = props.active_profile_index if len(props.profiles) > index >= 0: return props.profiles[index] + + # Lengths are in meters. + DEFAULT_PROFILE_ATTRS = { + "IfcCircleProfileDef": { + "Radius": 0.05, + }, + # TODO: test after debug + "IfcAsymmetricIShapeProfileDef": { + "BottomFlangeWidth": 0.1, + "BottomFlangeThickness": 0.01, + "BottomFlangeFilletRadius": 0.01, + "OverallDepth": 0.1, + "WebThickness": 0.005, + "TopFlangeWidth": 0.075, + "TopFlangeThickness": 0.01, + "TopFlangeFilletRadius": 0.01, + }, + "IfcCShapeProfileDef": { + "Depth": 0.1, + "Width": 0.05, + "WallThickness": 0.01, + "Girth": 0.01, + }, + # 101.6-10.0 + "IfcCircleHollowProfileDef": { + "WallThickness": 0.01, + }, + # TODO: check shape after fixing crash + # "IfcEllipseProfileDef": { + # "SemiAxis1": 0.1, + # "SemiAxis2": 0.1, + # }, + # HEA100 + "IfcIShapeProfileDef": { + "OverallWidth": 0.1, + "OverallDepth": 0.1, + "WebThickness": 0.005, + "FlangeThickness": 0.01, + "FilletRadius": 0.01, + }, + # LNP100x10 + "IfcLShapeProfileDef": { + "Depth": 0.1, + "Thickness": 0.01, + "FilletRadius": 0.012, + "EdgeRadius": 0.01, + }, + "IfcRectangleProfileDef": { + "XDim": 0.1, + "YDim": 0.1, + }, + "IfcRoundedRectangleProfileDef": { + "RoundingRadius": 0.01, + }, + # 100-10.0 + "IfcRectangleHollowProfileDef": { + "WallThickness": 0.01, + "InnerFilletRadius": 0.01, + "OuterFilletRadius": 0.01, + }, + "IfcTShapeProfileDef": { + "Depth": 0.1, + "FlangeWidth": 0.05, + "WebThickness": 0.005, + "FlangeThickness": 0.009, + }, + "IfcTrapeziumProfileDef": { + "BottomXDim": 0.1, + "TopXDim": 0.08, + "YDim": 0.05, + "TopXOffset": 0.01, + }, + # UAP100 + "IfcUShapeProfileDef": { + "Depth": 0.1, + "FlangeWidth": 0.05, + "WebThickness": 0.005, + "FlangeThickness": 0.009, + }, + # ZNP100 + "IfcZShapeProfileDef": { + "Depth": 0.1, + "FlangeWidth": 0.05, + "WebThickness": 0.007, + "FlangeThickness": 0.01, + }, + } + + @classmethod + def set_default_profile_attrs(cls, profile: ifcopenshell.entity_instance) -> None: + """Set default profile attributes to keep profile valid.""" + class_match = False + si_conversion = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get()) + for ifc_class, params in cls.DEFAULT_PROFILE_ATTRS.items(): + if profile.is_a(ifc_class): + class_match = True + for key, value in params.items(): + setattr(profile, key, value / si_conversion) + + if not class_match: + raise ValueError(f"Unable to set default profile parameters for {profile.is_a()}.") From a4f669e5492705522dfb6c3c9e4017a1a5a68873 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Tue, 10 Sep 2024 16:34:29 +0500 Subject: [PATCH 08/56] ifc5d.qto - some clarifications for arguments --- src/ifc5d/ifc5d/qto.py | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/src/ifc5d/ifc5d/qto.py b/src/ifc5d/ifc5d/qto.py index 8fe9380518..6673997ce2 100644 --- a/src/ifc5d/ifc5d/qto.py +++ b/src/ifc5d/ifc5d/qto.py @@ -28,19 +28,25 @@ import ifcopenshell.util.shape import ifcopenshell.util.representation import multiprocessing from collections import namedtuple -from typing import Any +from typing import Any, Literal, get_args Function = namedtuple("Function", ["measure", "name", "description"]) -rules = {} +RULE_SET = Literal["IFC4QtoBaseQuantities", "IFC4QtoBaseQuantitiesBlender"] +rules: dict[RULE_SET, dict[str, Any]] = {} cwd = os.path.dirname(os.path.realpath(__file__)) -for name in ("IFC4QtoBaseQuantities", "IFC4QtoBaseQuantitiesBlender"): +for name in get_args(RULE_SET): with open(os.path.join(cwd, name + ".json"), "r") as f: rules[name] = json.load(f) def quantify(ifc_file: ifcopenshell.file, elements: set[ifcopenshell.entity_instance], rules: dict) -> dict: + """ + + :param rules: Set of rules from `ifc5d.qto.rules`. + + """ results = {} for calculator, queries in rules["calculators"].items(): calculator = calculators[calculator] @@ -52,6 +58,11 @@ def quantify(ifc_file: ifcopenshell.file, elements: set[ifcopenshell.entity_inst def edit_qtos(ifc_file: ifcopenshell.file, results: dict[ifcopenshell.entity_instance, Any]) -> None: + """ + + :param results: Results from `ifc5d.qto.quantify`. + + """ for element, qtos in results.items(): for name, quantities in qtos.items(): qto = ifcopenshell.util.element.get_pset(element, name, should_inherit=False) From 2d0b262351b1fcec67891f5461fb8c71f54a9be8 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Tue, 10 Sep 2024 17:01:05 +0500 Subject: [PATCH 09/56] get_nested_tasks to discard non-ifctask elements #4911 --- src/ifcopenshell-python/ifcopenshell/util/sequence.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ifcopenshell-python/ifcopenshell/util/sequence.py b/src/ifcopenshell-python/ifcopenshell/util/sequence.py index 166a3c3674..6d905766df 100644 --- a/src/ifcopenshell-python/ifcopenshell/util/sequence.py +++ b/src/ifcopenshell-python/ifcopenshell/util/sequence.py @@ -279,7 +279,7 @@ def get_task_work_schedule(task: ifcopenshell.entity_instance) -> Union[ifcopens def get_nested_tasks(task: ifcopenshell.entity_instance) -> list[ifcopenshell.entity_instance]: - return [object for rel in task.IsNestedBy or [] for object in rel.RelatedObjects] + return [object for rel in task.IsNestedBy or [] for object in rel.RelatedObjects if object.is_a("IfcTask")] def get_parent_task(task: ifcopenshell.entity_instance) -> Union[ifcopenshell.entity_instance, None]: From 6fd4d8b1bac6b1fba1115f2a200062d26a558c5d Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Tue, 10 Sep 2024 17:27:12 +0500 Subject: [PATCH 10/56] Fix sequence.get_related_products bug after 845d772 --- .../ifcopenshell/util/sequence.py | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/src/ifcopenshell-python/ifcopenshell/util/sequence.py b/src/ifcopenshell-python/ifcopenshell/util/sequence.py index 6d905766df..720bebfb22 100644 --- a/src/ifcopenshell-python/ifcopenshell/util/sequence.py +++ b/src/ifcopenshell-python/ifcopenshell/util/sequence.py @@ -459,12 +459,9 @@ def get_related_products( """Gets the related products being output by a task :param relating_product: One of the products already output by the task. - :type relating_product: ifcopenshell.entity_instance, optional :param related_object: The IfcTask that you want to get all the related products for. - :type related_object: ifcopenshell.entity_instance, optional :return: A set of IfcProducts output by the IfcTask. - :rtype: set[ifcopenshell.entity_instance] Example: @@ -489,17 +486,18 @@ def get_related_products( products = ifcopenshell.util.sequence.get_related_products(related_object=task) """ + assert relating_product or related_object, "Either relating_product or related_object must be provided." + products = set() - related_object = None - if related_object: - related_object = related_object - elif relating_product: + if not related_object and relating_product: for reference in relating_product.ReferencedBy: if reference.is_a("IfcRelAssignsToProduct"): related_object = reference.RelatedObjects[0] + if related_object: assignments = related_object.HasAssignments for assignment in assignments: if assignment.is_a("IfcRelAssignsToProduct"): products.add(assignment.RelatingProduct.id()) + return products From 34aed93b7bd6f60241e7f13c5042bab5386be78b Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Tue, 10 Sep 2024 17:33:47 +0500 Subject: [PATCH 11/56] Fix 2 segfaults creating IfcEllipseProfileDef 1) taxonomy::ellipse was missing matrix so create_shape was segfaulting either was if Position was set or was not (segfaulting on line - https://github.com/IfcOpenShell/IfcOpenShell/blob/5616367a03ea397885e93523e55788da82238742/src/ifcgeom/kernels/opencascade/loop.cpp#L92) 2) was segfaulting when there was no default matrix Removed fc->matrix assignment as matrix is already assigned to the curve. --- src/ifcgeom/mapping/IfcEllipseProfileDef.cpp | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/ifcgeom/mapping/IfcEllipseProfileDef.cpp b/src/ifcgeom/mapping/IfcEllipseProfileDef.cpp index d07e3e80fb..5608b01cdf 100644 --- a/src/ifcgeom/mapping/IfcEllipseProfileDef.cpp +++ b/src/ifcgeom/mapping/IfcEllipseProfileDef.cpp @@ -39,6 +39,9 @@ taxonomy::ptr mapping::map_impl(const IfcSchema::IfcEllipseProfileDef* inst) { #endif if (has_position) { m4 = taxonomy::cast(map(inst->Position())); + } else { + // matrix needs to be set on elementary curves. + m4 = taxonomy::make(); } if (ry > rx) { @@ -58,9 +61,9 @@ taxonomy::ptr mapping::map_impl(const IfcSchema::IfcEllipseProfileDef* inst) { auto el = taxonomy::make(); el->radius = rx; el->radius2 = ry; + el->matrix = m4; ed->basis = el; lp->children.push_back(ed); fc->children.push_back(lp); - fc->matrix = m4; return fc; } From 3abe5edd21c19d553a11320a8e60a6dee7064daf Mon Sep 17 00:00:00 2001 From: myoualid Date: Tue, 10 Sep 2024 17:16:09 +0100 Subject: [PATCH 12/56] Fix generating 3D view drawing when IFC elements don't exist in the Blender scene ( such as IfcDistributionPort) --- src/bonsai/bonsai/tool/drawing.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/bonsai/bonsai/tool/drawing.py b/src/bonsai/bonsai/tool/drawing.py index de4c983495..cdf5fbc49d 100644 --- a/src/bonsai/bonsai/tool/drawing.py +++ b/src/bonsai/bonsai/tool/drawing.py @@ -1855,6 +1855,8 @@ class Drawing(bonsai.core.tool.Drawing): element_obj_names = set() for element in filtered_elements: obj = tool.Ifc.get_object(element) + if not obj: + continue current_representation = tool.Geometry.get_active_representation(obj) if current_representation: subcontext = current_representation.ContextOfItems From 67a7e713b2b443131883b3eb78e562c844ec0bd4 Mon Sep 17 00:00:00 2001 From: myoualid Date: Tue, 10 Sep 2024 17:17:42 +0100 Subject: [PATCH 13/56] fix util.get_elements_by_pset to account for quantity sets --- src/ifcopenshell-python/ifcopenshell/util/element.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ifcopenshell-python/ifcopenshell/util/element.py b/src/ifcopenshell-python/ifcopenshell/util/element.py index 8804586acd..61d20457fd 100644 --- a/src/ifcopenshell-python/ifcopenshell/util/element.py +++ b/src/ifcopenshell-python/ifcopenshell/util/element.py @@ -445,7 +445,7 @@ def get_elements_by_pset(pset: ifcopenshell.entity_instance) -> set[ifcopenshell """Retrieve the elements (or element types) that are using the provided property set.""" is_ifc2x3 = pset.file.schema == "IFC2X3" elements = set() - if pset.is_a("IfcPropertySet"): + if pset.is_a("IfcPropertySet") or pset.is_a("IfcQuantitySet"): rels = pset.PropertyDefinitionOf if is_ifc2x3 else pset.DefinesOccurrence for rel in rels: elements.update(rel.RelatedObjects) From cc7171060f376601d1bcfb7944c96fa5a8fcf0ed Mon Sep 17 00:00:00 2001 From: Richard Brice <37087370+RickBrice@users.noreply.github.com> Date: Tue, 10 Sep 2024 10:11:17 -0700 Subject: [PATCH 14/56] Decouples evaluation of a piecewise_function from the function itself (#5344) --- src/ifcgeom/AbstractKernel.cpp | 4 +- src/ifcgeom/mapping/IfcCompositeCurve.cpp | 2 +- src/ifcgeom/mapping/IfcCurveSegment.cpp | 2 +- .../IfcFixedReferenceSweptAreaSolid.cpp | 19 +--- src/ifcgeom/mapping/IfcGradientCurve.cpp | 12 +- .../mapping/IfcOffsetCurveByDistance.cpp | 12 +- .../mapping/IfcPointByDistanceExpression.cpp | 4 +- .../mapping/IfcSectionedSolidHorizontal.cpp | 6 +- .../mapping/IfcSegmentedReferenceCurve.cpp | 13 ++- src/ifcgeom/piecewise_function_evaluator.cpp | 103 ++++++++++++++++++ src/ifcgeom/piecewise_function_evaluator.h | 58 ++++++++++ src/ifcgeom/piecewise_function_impl.cpp | 101 ++++------------- src/ifcgeom/piecewise_function_impl.h | 74 ++----------- src/ifcgeom/taxonomy.cpp | 13 +-- src/ifcgeom/taxonomy.h | 35 +----- src/ifcwrap/IfcGeomWrapper.i | 1 + src/ifcwrap/IfcPython.i | 2 + 17 files changed, 240 insertions(+), 221 deletions(-) create mode 100644 src/ifcgeom/piecewise_function_evaluator.cpp create mode 100644 src/ifcgeom/piecewise_function_evaluator.h diff --git a/src/ifcgeom/AbstractKernel.cpp b/src/ifcgeom/AbstractKernel.cpp index a82e7c1641..a86a542895 100644 --- a/src/ifcgeom/AbstractKernel.cpp +++ b/src/ifcgeom/AbstractKernel.cpp @@ -3,6 +3,7 @@ #include "../ifcgeom/IfcGeomElement.h" #include "../ifcgeom/ConversionSettings.h" #include "../ifcgeom/abstract_mapping.h" +#include "../ifcgeom/piecewise_function_evaluator.h" #ifdef IFOPSH_WITH_OPENCASCADE #include "../ifcgeom/kernels/opencascade/OpenCascadeKernel.h" @@ -220,7 +221,8 @@ bool ifcopenshell::geometry::kernels::AbstractKernel::convert_impl(const taxonom } bool ifcopenshell::geometry::kernels::AbstractKernel::convert_impl(const taxonomy::piecewise_function::ptr item, IfcGeom::ConversionResults& cs) { - auto expl = item->evaluate(); + piecewise_function_evaluator evaluator(item); + auto expl = evaluator.evaluate(); expl->instance = item->instance; return convert(expl, cs); } diff --git a/src/ifcgeom/mapping/IfcCompositeCurve.cpp b/src/ifcgeom/mapping/IfcCompositeCurve.cpp index b9f2aaaf4d..568b861ad7 100644 --- a/src/ifcgeom/mapping/IfcCompositeCurve.cpp +++ b/src/ifcgeom/mapping/IfcCompositeCurve.cpp @@ -92,7 +92,7 @@ taxonomy::ptr mapping::map_impl(const IfcSchema::IfcCompositeCurve* inst) { return loop; } else { - auto pwf = taxonomy::make(0.0,pwfs,&settings_,inst); + auto pwf = taxonomy::make(0.0,pwfs,inst); return pwf; } } diff --git a/src/ifcgeom/mapping/IfcCurveSegment.cpp b/src/ifcgeom/mapping/IfcCurveSegment.cpp index 343cb8300d..e2f38135e3 100644 --- a/src/ifcgeom/mapping/IfcCurveSegment.cpp +++ b/src/ifcgeom/mapping/IfcCurveSegment.cpp @@ -961,7 +961,7 @@ taxonomy::ptr mapping::map_impl(const IfcSchema::IfcCurveSegment* inst) { taxonomy::piecewise_function::spans_t spans; spans.emplace_back(fabs(length), fn); - auto pwf = taxonomy::make(0.0, spans,&settings_,inst); + auto pwf = taxonomy::make(0.0, spans,inst); return pwf; } diff --git a/src/ifcgeom/mapping/IfcFixedReferenceSweptAreaSolid.cpp b/src/ifcgeom/mapping/IfcFixedReferenceSweptAreaSolid.cpp index 7267f61b2a..79632b9f01 100644 --- a/src/ifcgeom/mapping/IfcFixedReferenceSweptAreaSolid.cpp +++ b/src/ifcgeom/mapping/IfcFixedReferenceSweptAreaSolid.cpp @@ -18,6 +18,7 @@ ********************************************************************************/ #include "mapping.h" +#include "../piecewise_function_evaluator.h" #define mapping POSTFIX_SCHEMA(mapping) using namespace ifcopenshell::geometry; @@ -34,6 +35,7 @@ taxonomy::ptr mapping::map_impl(const IfcSchema::IfcFixedReferenceSweptAreaSolid // @todo currently only the case is handled where directrix returns a piecewise_function if (auto pwf = taxonomy::dcast(dir)) { + piecewise_function_evaluator evaluator(pwf,&settings_); double start = 0; double end = pwf->length(); #ifdef SCHEMA_HAS_IfcDirectrixCurveSweptAreaSolid @@ -53,20 +55,9 @@ taxonomy::ptr mapping::map_impl(const IfcSchema::IfcFixedReferenceSweptAreaSolid } } #endif - auto curve_length = end - start; - auto param_type = settings_.get().get(); - auto param = settings_.get().get(); - size_t num_steps = 0; - if (param_type == ifcopenshell::geometry::settings::PiecewiseStepMethod::MAXSTEPSIZE) { - // parameter is max step size - num_steps = (size_t) std::ceil(curve_length / param); - } else { - // parameter is minimum number of steps - num_steps = (size_t) std::ceil(param); - } - for (size_t i = 0; i <= num_steps; ++i) { - auto distalong = start + curve_length / num_steps * i; - auto m4 = pwf->evaluate(distalong); + auto evaluation_points = evaluator.evaluation_points(); + for (const auto& dist_along : evaluation_points) { + auto m4 = evaluator.evaluate(dist_along); /* std::stringstream ss; diff --git a/src/ifcgeom/mapping/IfcGradientCurve.cpp b/src/ifcgeom/mapping/IfcGradientCurve.cpp index 3c5b95635a..6217b81a0e 100644 --- a/src/ifcgeom/mapping/IfcGradientCurve.cpp +++ b/src/ifcgeom/mapping/IfcGradientCurve.cpp @@ -18,6 +18,7 @@ ********************************************************************************/ #include "mapping.h" +#include "../piecewise_function_evaluator.h" #define mapping POSTFIX_SCHEMA(mapping) using namespace ifcopenshell::geometry; @@ -55,7 +56,7 @@ taxonomy::ptr mapping::map_impl(const IfcSchema::IfcGradientCurve* inst) { double gradient_start = m(0, 3); // start of vertical (row 0, col 3) - "Distance Along" horizontal curve // create the vertical pwf - auto vertical = taxonomy::make(gradient_start, pwfs, &settings_); + auto vertical = taxonomy::make(gradient_start, pwfs); // Determine the valid domain of the PWF... the valid domain is where both // the base curve and gradient curves are defined @@ -69,11 +70,12 @@ taxonomy::ptr mapping::map_impl(const IfcSchema::IfcGradientCurve* inst) { } // define the callback function for the gradient curve - auto composition = [horizontal, vertical](double u)->Eigen::Matrix4d { + piecewise_function_evaluator horizontal_evaluator(horizontal, &settings_), vertical_evaluator(vertical, &settings_); + auto composition = [horizontal_evaluator, vertical_evaluator,start=vertical->start()](double u) -> Eigen::Matrix4d { // u is distance from start of gradient curve (vertical) // add vertical->start() to u to get distance from start of horizontal - auto xy = horizontal->evaluate(u + vertical->start()); - auto uz = vertical->evaluate(u); + auto xy = horizontal_evaluator.evaluate(u + start); + auto uz = vertical_evaluator.evaluate(u); uz.col(3)(0) = 0.0; // x is distance along. zero it out so it doesn't add to the x from horizontal uz.col(1).swap(uz.col(2)); // uz is 2D in distance along - y plane, swap y and z so elevations become z @@ -86,7 +88,7 @@ taxonomy::ptr mapping::map_impl(const IfcSchema::IfcGradientCurve* inst) { taxonomy::piecewise_function::spans_t spans; spans.emplace_back(length, composition); - auto pwf = taxonomy::make(start, spans, &settings_, inst); + auto pwf = taxonomy::make(start, spans, inst); return pwf; } diff --git a/src/ifcgeom/mapping/IfcOffsetCurveByDistance.cpp b/src/ifcgeom/mapping/IfcOffsetCurveByDistance.cpp index 7af8b42bd8..1ed4d05e8b 100644 --- a/src/ifcgeom/mapping/IfcOffsetCurveByDistance.cpp +++ b/src/ifcgeom/mapping/IfcOffsetCurveByDistance.cpp @@ -19,6 +19,7 @@ #include "mapping.h" #include "../profile_helper.h" +#include "../piecewise_function_evaluator.h" #define mapping POSTFIX_SCHEMA(mapping) using namespace ifcopenshell::geometry; @@ -144,11 +145,12 @@ taxonomy::ptr mapping::map_impl(const IfcSchema::IfcOffsetCurveByDistances* inst offset_spans.emplace_back(l, fn); } - auto offsets = taxonomy::make(start,offset_spans,&settings_); + auto offsets = taxonomy::make(start,offset_spans); - auto composition = [pw_curve, offsets](double u) -> Eigen::Matrix4d { - auto p = pw_curve->evaluate(u); - auto offset = offsets->evaluate(u); + piecewise_function_evaluator pw_evaluator(pw_curve, &settings_), offsets_evaluator(offsets, &settings_); + auto composition = [pw_evaluator, offsets_evaluator](double u) -> Eigen::Matrix4d { + auto p = pw_evaluator.evaluate(u); + auto offset = offsets_evaluator.evaluate(u); Eigen::Matrix4d m = p * offset; return m; }; @@ -157,7 +159,7 @@ taxonomy::ptr mapping::map_impl(const IfcSchema::IfcOffsetCurveByDistances* inst // this may change depending on decisions in the bSI-IF taxonomy::piecewise_function::spans_t spans; spans.emplace_back(basis_curve_length, composition); - auto pwf = taxonomy::make(start,spans,&settings_,inst); + auto pwf = taxonomy::make(start,spans,inst); return pwf; } diff --git a/src/ifcgeom/mapping/IfcPointByDistanceExpression.cpp b/src/ifcgeom/mapping/IfcPointByDistanceExpression.cpp index 601765e1a3..6097344eb3 100644 --- a/src/ifcgeom/mapping/IfcPointByDistanceExpression.cpp +++ b/src/ifcgeom/mapping/IfcPointByDistanceExpression.cpp @@ -19,6 +19,7 @@ #include "mapping.h" #include "../profile_helper.h" +#include "../piecewise_function_evaluator.h" #define mapping POSTFIX_SCHEMA(mapping) using namespace ifcopenshell::geometry; @@ -31,7 +32,8 @@ taxonomy::ptr mapping::map_impl(const IfcSchema::IfcPointByDistanceExpression* i //auto item = map(basis_curve); //auto pw_curve = ifcopenshell::geometry::piecewise_from_item(item); auto pw_curve = taxonomy::dcast(map(inst->BasisCurve())); - auto m = pw_curve->evaluate(u); + piecewise_function_evaluator evaluator(pw_curve,&settings_); + auto m = evaluator.evaluate(u); auto o = m.col(3).head<3>(); auto z = m.col(2).head<3>(); diff --git a/src/ifcgeom/mapping/IfcSectionedSolidHorizontal.cpp b/src/ifcgeom/mapping/IfcSectionedSolidHorizontal.cpp index 74e6e4cd89..95b750f82f 100644 --- a/src/ifcgeom/mapping/IfcSectionedSolidHorizontal.cpp +++ b/src/ifcgeom/mapping/IfcSectionedSolidHorizontal.cpp @@ -22,6 +22,7 @@ using namespace ifcopenshell::geometry; #include "../../ifcgeom/profile_helper.h" +#include "../piecewise_function_evaluator.h" #include @@ -114,7 +115,8 @@ taxonomy::ptr mapping::map_impl(const IfcSchema::IfcSectionedSolidHorizontal* in // @todo currently only the case is handled where directrix returns a piecewise_function // @todo this "if" statement is not really required because the function returns at the start if the Directrix is not a piecewise function if (pwf) { - double start = std::max(0., cross_sections.front().dist_along); + piecewise_function_evaluator evaluator(pwf, &settings_); + double start = std::max(0., cross_sections.front().dist_along); double end = std::min(pwf->length(), cross_sections.back().dist_along); if (end - start < 1.e-9) { @@ -232,7 +234,7 @@ taxonomy::ptr mapping::map_impl(const IfcSchema::IfcSectionedSolidHorizontal* in } } - auto m4 = pwf->evaluate(dist_along); + auto m4 = evaluator.evaluate(dist_along); /* { std::wcout << "#" << pwf->instance->data().id() << " " << dist_along << ": " << m4.col(3).row(2).value() << std::endl; }*/ diff --git a/src/ifcgeom/mapping/IfcSegmentedReferenceCurve.cpp b/src/ifcgeom/mapping/IfcSegmentedReferenceCurve.cpp index 2ab56482c1..d1ab88fbe5 100644 --- a/src/ifcgeom/mapping/IfcSegmentedReferenceCurve.cpp +++ b/src/ifcgeom/mapping/IfcSegmentedReferenceCurve.cpp @@ -21,6 +21,8 @@ #define mapping POSTFIX_SCHEMA(mapping) using namespace ifcopenshell::geometry; +#include "../piecewise_function_evaluator.h" + #ifdef SCHEMA_HAS_IfcSegmentedReferenceCurve taxonomy::ptr mapping::map_impl(const IfcSchema::IfcSegmentedReferenceCurve* inst) { @@ -53,7 +55,7 @@ taxonomy::ptr mapping::map_impl(const IfcSchema::IfcSegmentedReferenceCurve* ins const Eigen::Matrix4d& m = p->ccomponents(); double cant_start = m(0, 3); // start of cant curve - auto cant = taxonomy::make(cant_start,pwfs,&settings_); + auto cant = taxonomy::make(cant_start,pwfs); // Determine the valid domain of the PWF... the valid domain is where // horizontal, gradient and cant curves are defined @@ -68,11 +70,12 @@ taxonomy::ptr mapping::map_impl(const IfcSchema::IfcSegmentedReferenceCurve* ins } // define the callback function for the segmented reference curve - auto composition = [gradient, cant](double u)->Eigen::Matrix4d { + piecewise_function_evaluator gradient_evaluator(gradient, &settings_), cant_evaluator(cant, &settings_); + auto composition = [gradient_evaluator, cant_evaluator, start = cant->start()](double u) -> Eigen::Matrix4d { // u is distance from start of cant curve // add cant->start() to u to get the distance from start of gradient curve - auto g = gradient->evaluate(u+cant->start()); - auto c = cant->evaluate(u); + auto g = gradient_evaluator.evaluate(u + start); + auto c = cant_evaluator.evaluate(u); // Need to multiply g and c so the axis vectors // from cant have the correct rotation applied so @@ -105,7 +108,7 @@ taxonomy::ptr mapping::map_impl(const IfcSchema::IfcSegmentedReferenceCurve* ins taxonomy::piecewise_function::spans_t spans; spans.emplace_back(length, composition); - auto pwf = taxonomy::make(start, spans, &settings_, inst); + auto pwf = taxonomy::make(start, spans, inst); return pwf; } diff --git a/src/ifcgeom/piecewise_function_evaluator.cpp b/src/ifcgeom/piecewise_function_evaluator.cpp new file mode 100644 index 0000000000..e4a03511ca --- /dev/null +++ b/src/ifcgeom/piecewise_function_evaluator.cpp @@ -0,0 +1,103 @@ +#include "piecewise_function_evaluator.h" +#include "profile_helper.h" + +using namespace ifcopenshell::geometry; + + +piecewise_function_evaluator::piecewise_function_evaluator(taxonomy::piecewise_function::const_ptr pwf, ifcopenshell::geometry::Settings* settings) : pwf_(pwf) { + if (settings) { + settings_ = *settings; + } +} + +std::vector piecewise_function_evaluator::evaluation_points() const { + if (!eval_points_.has_value()) { + double curve_length = pwf_->length(); + + auto param_type = settings_.get().get(); + auto param = settings_.get().get(); + unsigned num_steps = 0; + if (param_type == ifcopenshell::geometry::settings::PiecewiseStepMethod::MAXSTEPSIZE) { + // parameter is max step size + num_steps = (unsigned)std::ceil(curve_length / param); + } else { + // parameter is minimum number of steps + num_steps = (unsigned)std::ceil(param); + } + + eval_points_ = evaluation_points(pwf_->start(), pwf_->start() + curve_length, num_steps); + } + return *eval_points_; +} + +std::vector piecewise_function_evaluator::evaluation_points(double ustart, double uend, unsigned nsteps) const { + double curve_length = pwf_->length(); + ustart = std::max(pwf_->start(), ustart); + uend = std::min(uend, pwf_->start() + curve_length); + + nsteps = std::max(1u, nsteps); // never have fewer than 1 step + + auto resolution = (uend - ustart) / nsteps; + + std::vector u_values; + u_values.reserve(nsteps); + + for (unsigned i = 0; i <= nsteps; ++i) { + auto u = resolution * i + ustart; + u_values.push_back(u); + } + + return u_values; +} + +taxonomy::item::ptr piecewise_function_evaluator::evaluate() const { + return evaluate(evaluation_points()); +} + +taxonomy::item::ptr piecewise_function_evaluator::evaluate(double ustart, double uend, unsigned nsteps) const { + return evaluate(evaluation_points(ustart, uend, nsteps)); +} + +Eigen::Matrix4d piecewise_function_evaluator::evaluate(double u) const { + // assume monotonic evaluation and store last evaluated segment + if (current_span_fn_ == nullptr || (u < current_span_start_ || current_span_end_ < u)) { + // there isn't a current span or u is outside the range of the current span + // get a new "current span" + std::tie(current_span_start_, current_span_end_, current_span_fn_) = get_span(u); + } + + u -= current_span_start_; // make u relative to start of span + return (*current_span_fn_)(u); +} + +taxonomy::item::ptr piecewise_function_evaluator::evaluate(const std::vector& dist) const { + std::vector polygon; + polygon.reserve(dist.size()); + for (auto& u : dist) { + Eigen::Matrix4d m = evaluate(u); + polygon.push_back(taxonomy::make(m(0, 3), m(1, 3), m(2, 3))); + } + + return polygon_from_points(polygon); +} + +std::tuple*> piecewise_function_evaluator::get_span(double u) const { + // force u to be within bounds of the curve + double s = pwf_->start(); + double e = pwf_->end(); + u = std::max(s, u); + u = std::min(u, e); + + double span_start = s; + for (auto& [length, fn] : pwf_->spans()) { + double span_end = span_start + length; + auto tolerance = settings_.get().get(); + if (span_start <= u && u < span_end + tolerance) { + return {span_start, span_end, &fn}; + } + span_start += length; + } + + Logger::Error("piecewise_function_impl::get_span span not found."); + return {0, 0, nullptr}; +} diff --git a/src/ifcgeom/piecewise_function_evaluator.h b/src/ifcgeom/piecewise_function_evaluator.h new file mode 100644 index 0000000000..ca1bddce54 --- /dev/null +++ b/src/ifcgeom/piecewise_function_evaluator.h @@ -0,0 +1,58 @@ +#ifndef ITERATOR_PWF_EVALUATOR_H +#define ITERATOR_PWF_EVALUATOR_H + +#include "../ifcgeom/taxonomy.h" + +#include + +namespace ifcopenshell { namespace geometry { + +/// @brief utility class to evaluate piecewise_function objects +class piecewise_function_evaluator { + public: + piecewise_function_evaluator(taxonomy::piecewise_function::const_ptr pwf, ifcopenshell::geometry::Settings* settings=nullptr); + + /// @brief returns a vector of "distance along" points where the evaluate function computes loop points + std::vector evaluation_points() const; + + /// @brief returns a vector of "distance along" points between ustart and uend + /// @param ustart starting location + /// @param uend ending location + /// @param nsteps number of steps to evaluate + std::vector evaluation_points(double ustart, double uend, unsigned nsteps) const; + + /// @brief evaluates the piecewise function between start and end + /// evaluation point step size is taken from the settings object + taxonomy::item::ptr evaluate() const; + + /// @brief evaluates the piecewise function between ustart and uend + /// if ustart and uend are out of range, the range of values evaluated + /// are constrained to start_ and start_+length_ + /// @param ustart starting location + /// @param uend ending location + /// @param nsteps number of steps to evaluate + /// @return taxonomy::loop::ptr + taxonomy::item::ptr evaluate(double ustart, double uend, unsigned nsteps) const; + + /// @brief evaluates the piecewise function at u + /// @param u u is constrained to be between start_ and start_+length + /// @return 4x4 placement matrix + Eigen::Matrix4d evaluate(double u) const; + + private: + taxonomy::item::ptr evaluate(const std::vector& dist) const; + std::tuple*> get_span(double u) const; + + taxonomy::piecewise_function::const_ptr pwf_; + + ifcopenshell::geometry::Settings settings_; + + mutable double current_span_start_ = 0; + mutable double current_span_end_ = 0; + mutable const std::function* current_span_fn_ = nullptr; + mutable boost::optional> eval_points_; +}; + +}} + +#endif \ No newline at end of file diff --git a/src/ifcgeom/piecewise_function_impl.cpp b/src/ifcgeom/piecewise_function_impl.cpp index ecd2762057..6fa29fb649 100644 --- a/src/ifcgeom/piecewise_function_impl.cpp +++ b/src/ifcgeom/piecewise_function_impl.cpp @@ -7,97 +7,40 @@ namespace geometry { namespace taxonomy { -std::vector ifcopenshell::geometry::taxonomy::piecewise_function_impl::evaluation_points() const { - if (!eval_points_.has_value()) { - double curve_length = length(); +piecewise_function_impl::piecewise_function_impl(double start, const spans_t& s) : start_(start), spans_(s) { +} - auto param_type = settings_ ? settings_->get().get() : ifcopenshell::geometry::settings::PiecewiseStepMethod::MAXSTEPSIZE; - auto param = settings_ ? settings_->get().get() : 0.5; - unsigned num_steps = 0; - if (param_type == ifcopenshell::geometry::settings::PiecewiseStepMethod::MAXSTEPSIZE) { - // parameter is max step size - num_steps = (unsigned)std::ceil(curve_length / param); - } else { - // parameter is minimum number of steps - num_steps = (unsigned)std::ceil(param); - } - - eval_points_ = evaluation_points(start_, start_ + curve_length, num_steps); +piecewise_function_impl::piecewise_function_impl(double start, const std::vector& pwfs) : start_(start) { + for (auto& pwf : pwfs) { + spans_.insert(spans_.end(), pwf->spans().begin(), pwf->spans().end()); } - return *eval_points_; } -std::vector ifcopenshell::geometry::taxonomy::piecewise_function_impl::evaluation_points(double ustart, double uend, unsigned nsteps) const { - double curve_length = length(); - ustart = std::max(start_, ustart); - uend = std::min(uend, start_ + curve_length); +const piecewise_function_impl::spans_t& piecewise_function_impl::spans() const { return spans_; } - nsteps = std::max(1u, nsteps); // never have fewer than 1 step +bool piecewise_function_impl::is_empty() const { return spans_.empty(); } - auto resolution = (uend - ustart) / nsteps; - - std::vector u_values; - u_values.reserve(nsteps); - - for (unsigned i = 0; i <= nsteps; ++i) { - auto u = resolution * i + ustart; - u_values.push_back(u); - } - - return u_values; +double piecewise_function_impl::start() const { + return start_; } -ifcopenshell::geometry::taxonomy::item::ptr ifcopenshell::geometry::taxonomy::piecewise_function_impl::evaluate() const { - return evaluate(evaluation_points()); +double piecewise_function_impl::end() const { + return start_ + length(); } -item::ptr ifcopenshell::geometry::taxonomy::piecewise_function_impl::evaluate(double ustart, double uend, unsigned nsteps) const { - return evaluate(evaluation_points(ustart, uend, nsteps)); +double piecewise_function_impl::length() const { + return std::accumulate(spans_.begin(), spans_.end(), 0.0, [](const auto& v, const auto& s) { return v + s.first; }); + + // this is a secondary option where we only compute length once and cache it. + // mutex is needed to prevent interruption of the accumulation if there is multi-threading + // skipping this detail for now and just adding up the span lengths every time + //if (!length_.has_value()) { + // length_ = std::accumulate(spans_.begin(), spans_.end(), 0.0, [](const auto& v, const auto& s) { return v + s.first; }); + //} + //return *length_; } -item::ptr ifcopenshell::geometry::taxonomy::piecewise_function_impl::evaluate(const std::vector& dist) const { - std::vector polygon; - polygon.reserve(dist.size()); - for (auto& u : dist) { - Eigen::Matrix4d m = evaluate(u); - polygon.push_back(taxonomy::make(m.col(3)(0), m.col(3)(1), m.col(3)(2))); - } - - return polygon_from_points(polygon); -} - -Eigen::Matrix4d ifcopenshell::geometry::taxonomy::piecewise_function_impl::evaluate(double u) const { - // assume monotonic evaluation and store last evaluated segment - if (current_span_fn_ == nullptr || (u < current_span_start_ || current_span_end_ < u)) { - // there isn't a current span or u is outside the range of the current span - // get a new "current span" - std::tie(current_span_start_, current_span_end_, current_span_fn_) = get_span(u); - } - - u -= current_span_start_; // make u relative to start of span - return (*current_span_fn_)(u); -} - -std::tuple*> ifcopenshell::geometry::taxonomy::piecewise_function_impl::get_span(double u) const { - // force u to be within bounds of the curve - double s = start(); - double e = end(); - u = std::max(s, u); - u = std::min(u, e); - - double span_start = s; - for (auto& [length, fn] : spans_) { - double span_end = span_start + length; - auto tolerance = settings_ ? settings_->get().get() : 0.001; - if (span_start <= u && u < span_end + tolerance) { - return {span_start, span_end, &fn}; - } - span_start += length; - } - - Logger::Error("piecewise_function_impl::get_span span not found."); - return {0, 0, nullptr}; -} +piecewise_function_impl* piecewise_function_impl::clone_() const { return new piecewise_function_impl(*this); } } // namespace taxonomy diff --git a/src/ifcgeom/piecewise_function_impl.h b/src/ifcgeom/piecewise_function_impl.h index 0c2f306ac5..daf8636feb 100644 --- a/src/ifcgeom/piecewise_function_impl.h +++ b/src/ifcgeom/piecewise_function_impl.h @@ -12,79 +12,23 @@ namespace taxonomy { struct piecewise_function_impl { using spans_t = std::vector>>; - piecewise_function_impl(double start, const spans_t& s, ifcopenshell::geometry::Settings* settings = nullptr) : start_(start), - settings_(settings), - spans_(s){}; - piecewise_function_impl(double start, const std::vector& pwfs, ifcopenshell::geometry::Settings* settings = nullptr) : start_(start), - settings_(settings) { - for (auto& pwf : pwfs) { - spans_.insert(spans_.end(), pwf->spans().begin(), pwf->spans().end()); - } - }; + piecewise_function_impl(double start, const spans_t& s); + piecewise_function_impl(double start, const std::vector& pwfs); piecewise_function_impl(piecewise_function_impl&&) = default; piecewise_function_impl(const piecewise_function_impl&) = default; - const ifcopenshell::geometry::Settings* settings_ = nullptr; - - const spans_t& spans() const { return spans_; } - - bool is_empty() const { return spans_.empty(); } - - double start() const { - return start_; - } - - double end() const { - return start_ + length(); - } - - double length() const { - if (!length_.has_value()) { - length_ = std::accumulate(spans_.begin(), spans_.end(), 0.0, [](const auto& v, const auto& s) { return v + s.first; }); - } - return *length_; - } - - piecewise_function_impl* clone_() const { return new piecewise_function_impl(*this); } - - /// @brief returns a vector of "distance along" points where the evaluate function computes loop points - std::vector evaluation_points() const; - - /// @brief returns a vector of "distance along" points between ustart and uend - /// @param ustart starting location - /// @param uend ending location - /// @param nsteps number of steps to evaluate - std::vector evaluation_points(double ustart, double uend, unsigned nsteps) const; - - /// @brief evaluates the piecewise function between start and end - /// evaluation point step size is taken from the settings object - item::ptr evaluate() const; - - /// @brief evaluates the piecewise function between ustart and uend - /// if ustart and uend are out of range, the range of values evaluated - /// are constrained to start_ and start_+length_ - /// @param ustart starting location - /// @param uend ending location - /// @param nsteps number of steps to evaluate - /// @return taxonomy::loop::ptr - item::ptr evaluate(double ustart, double uend, unsigned nsteps) const; - - /// @brief evaluates the piecewise function at u - /// @param u u is constrained to be between start_ and start_+length - /// @return 4x4 placement matrix - Eigen::Matrix4d evaluate(double u) const; + const spans_t& spans() const; + bool is_empty() const; + double start() const; + double end() const; + double length() const; + piecewise_function_impl* clone_() const; private: - item::ptr evaluate(const std::vector& dist) const; - std::tuple*> get_span(double u) const; double start_ = 0.0; // starting value of the pwf spans_t spans_; - mutable double current_span_start_ = 0; - mutable double current_span_end_ = 0; - mutable const std::function* current_span_fn_ = nullptr; - mutable boost::optional length_; - mutable boost::optional> eval_points_; + //mutable boost::optional length_; // used for length() method }; } // namespace taxonomy diff --git a/src/ifcgeom/taxonomy.cpp b/src/ifcgeom/taxonomy.cpp index 9b4f5043aa..1dd61d72b0 100644 --- a/src/ifcgeom/taxonomy.cpp +++ b/src/ifcgeom/taxonomy.cpp @@ -464,12 +464,12 @@ ifcopenshell::geometry::taxonomy::solid::ptr ifcopenshell::geometry::create_box( } /////////////////// -piecewise_function::piecewise_function(double start, const spans_t& s, ifcopenshell::geometry::Settings* settings, const IfcUtil::IfcBaseInterface* instance) : implicit_item(instance) { - impl_ = new piecewise_function_impl(start, s, settings); +piecewise_function::piecewise_function(double start, const spans_t& s, const IfcUtil::IfcBaseInterface* instance) : implicit_item(instance) { + impl_ = new piecewise_function_impl(start, s); } -piecewise_function::piecewise_function(double start, const std::vector& pwfs, ifcopenshell::geometry::Settings* settings, const IfcUtil::IfcBaseInterface* instance) : implicit_item(instance) { - impl_ = new piecewise_function_impl(start, pwfs, settings); +piecewise_function::piecewise_function(double start, const std::vector& pwfs, const IfcUtil::IfcBaseInterface* instance) : implicit_item(instance) { + impl_ = new piecewise_function_impl(start, pwfs); }; piecewise_function::piecewise_function(const piecewise_function& other) : implicit_item(other) { @@ -486,11 +486,6 @@ double piecewise_function::start() const { return impl_->start(); } double piecewise_function::end() const { return impl_->end(); } double piecewise_function::length() const { return impl_->length(); } -std::vector piecewise_function::evaluation_points() const { return impl_->evaluation_points(); } -std::vector piecewise_function::evaluation_points(double ustart, double uend, unsigned nsteps) const { return impl_->evaluation_points(ustart, uend, nsteps); } -item::ptr piecewise_function::evaluate() const { return impl_->evaluate(); } -item::ptr piecewise_function::evaluate(double ustart, double uend, unsigned nsteps) const { return impl_->evaluate(ustart, uend, nsteps); } -Eigen::Matrix4d piecewise_function::evaluate(double u) const { return impl_->evaluate(u); } ifcopenshell::geometry::taxonomy::collection::ptr ifcopenshell::geometry::flatten(const taxonomy::collection::ptr& deep) { auto flat = make(); diff --git a/src/ifcgeom/taxonomy.h b/src/ifcgeom/taxonomy.h index 3b77273240..db9c97863e 100644 --- a/src/ifcgeom/taxonomy.h +++ b/src/ifcgeom/taxonomy.h @@ -347,8 +347,6 @@ typedef item const* ptr; struct implicit_item : public geom_item { DECLARE_PTR(implicit_item) using geom_item::geom_item; - - virtual item::ptr evaluate() const = 0; }; struct piecewise_function_impl; // forward declaration @@ -357,14 +355,12 @@ typedef item const* ptr; using spans_t = std::vector>>; - piecewise_function(double start, const spans_t& s, ifcopenshell::geometry::Settings* settings = nullptr, const IfcUtil::IfcBaseInterface* instance = nullptr); - piecewise_function(double start, const std::vector& pwfs, ifcopenshell::geometry::Settings* settings = nullptr, const IfcUtil::IfcBaseInterface* instance = nullptr); + piecewise_function(double start, const spans_t& s, const IfcUtil::IfcBaseInterface* instance = nullptr); + piecewise_function(double start, const std::vector& pwfs, const IfcUtil::IfcBaseInterface* instance = nullptr); piecewise_function(piecewise_function&&) = default; piecewise_function(const piecewise_function&); virtual ~piecewise_function(); - const ifcopenshell::geometry::Settings* settings_ = nullptr; - const spans_t& spans() const; bool is_empty() const; double start() const; @@ -379,33 +375,6 @@ typedef item const* ptr; return boost::hash{}(v); } - /// @brief returns a vector of "distance along" points where the evaluate function computes loop points - std::vector evaluation_points() const; - - /// @brief returns a vector of "distance along" points between ustart and uend - /// @param ustart starting location - /// @param uend ending location - /// @param nsteps number of steps to evaluate - std::vector evaluation_points(double ustart, double uend, unsigned nsteps) const; - - /// @brief evaluates the piecewise function between start and end - /// evaluation point step size is taken from the settings object - item::ptr evaluate() const override; - - /// @brief evaluates the piecewise function between ustart and uend - /// if ustart and uend are out of range, the range of values evaluated - /// are constrained to start_ and start_+length_ - /// @param ustart starting location - /// @param uend ending location - /// @param nsteps number of steps to evaluate - /// @return taxonomy::loop::ptr - item::ptr evaluate(double ustart, double uend, unsigned nsteps) const; - - /// @brief evaluates the piecewise function at u - /// @param u u is constrained to be between start_ and start_+length - /// @return 4x4 placement matrix - Eigen::Matrix4d evaluate(double u) const; - private: // note: it would be better if this were a std::unique_ptr, but that requires having the full definition // of piecewise_function_impl in this header file, which defeats the purpose of the PIMPL idiom. diff --git a/src/ifcwrap/IfcGeomWrapper.i b/src/ifcwrap/IfcGeomWrapper.i index 0f26e28db7..2b2178e125 100644 --- a/src/ifcwrap/IfcGeomWrapper.i +++ b/src/ifcwrap/IfcGeomWrapper.i @@ -246,6 +246,7 @@ namespace { %include "../ifcgeom/Iterator.h" %include "../ifcgeom/GeometrySerializer.h" %include "../ifcgeom/taxonomy.h" +%include "../ifcgeom/piecewise_function_evaluator.h" %include "../serializers/SvgSerializer.h" %include "../serializers/HdfSerializer.h" diff --git a/src/ifcwrap/IfcPython.i b/src/ifcwrap/IfcPython.i index b3b6cc3a85..f70388663d 100644 --- a/src/ifcwrap/IfcPython.i +++ b/src/ifcwrap/IfcPython.i @@ -84,6 +84,7 @@ %{ #include "../ifcgeom/Iterator.h" #include "../ifcgeom/taxonomy.h" + #include "../ifcgeom/piecewise_function_evaluator.h" #ifdef IFOPSH_WITH_OPENCASCADE #include "../ifcgeom/Serialization/Serialization.h" #include "../ifcgeom/kernels/opencascade/IfcGeomTree.h" @@ -159,6 +160,7 @@ %module ifcopenshell_wrapper %{ #include "../ifcgeom/Converter.h" #include "../ifcgeom/taxonomy.h" + #include "../ifcgeom/piecewise_function_evaluator.h" #ifdef IFOPSH_WITH_OPENCASCADE #include "../ifcgeom/Serialization/Serialization.h" #include "../ifcgeom/kernels/opencascade/IfcGeomTree.h" From 8dc77735a58fad2f54386a2044b42b393f6d8db8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bruno=20Perdig=C3=A3o?= Date: Sun, 8 Sep 2024 12:10:01 -0300 Subject: [PATCH 15/56] fix rebase conflict --- .../bonsai/bim/module/model/decorator.py | 222 ++------------ src/bonsai/bonsai/bim/module/model/wall.py | 74 ++--- .../bonsai/bim/module/project/operator.py | 65 ++-- src/bonsai/bonsai/core/tool.py | 3 + src/bonsai/bonsai/tool/__init__.py | 1 + src/bonsai/bonsai/tool/polyline.py | 284 ++++++++++++++++++ src/bonsai/bonsai/tool/snap.py | 14 +- 7 files changed, 388 insertions(+), 275 deletions(-) create mode 100644 src/bonsai/bonsai/tool/polyline.py diff --git a/src/bonsai/bonsai/bim/module/model/decorator.py b/src/bonsai/bonsai/bim/module/model/decorator.py index f3160453a8..b14878a690 100644 --- a/src/bonsai/bonsai/bim/module/model/decorator.py +++ b/src/bonsai/bonsai/bim/module/model/decorator.py @@ -302,8 +302,8 @@ class PolylineDecorator: is_installed = False handlers = [] mouse_pos = None - input_panel = None input_type = None + input_ui = None angle_snap_mat = None angle_snap_loc = None use_default_container = False @@ -319,7 +319,7 @@ class PolylineDecorator: SpaceView3D.draw_handler_add(handler.draw_on_screen_menu, (context,), "WINDOW", "POST_PIXEL") ) cls.handlers.append(SpaceView3D.draw_handler_add(handler.draw_measurements, (context,), "WINDOW", "POST_PIXEL")) - cls.handlers.append(SpaceView3D.draw_handler_add(handler.draw_input_panel, (context,), "WINDOW", "POST_PIXEL")) + cls.handlers.append(SpaceView3D.draw_handler_add(handler.draw_input_ui, (context,), "WINDOW", "POST_PIXEL")) cls.handlers.append(SpaceView3D.draw_handler_add(handler, (context,), "WINDOW", "POST_VIEW")) cls.is_installed = True @@ -337,8 +337,8 @@ class PolylineDecorator: cls.mouse_pos = event.mouse_region_x, event.mouse_region_y @classmethod - def set_input_panel(cls, input_panel, input_type): - cls.input_panel = input_panel + def set_input_ui(cls, input_ui, input_type): + cls.input_ui = input_ui cls.input_type = input_type @classmethod @@ -354,11 +354,6 @@ class PolylineDecorator: def set_use_default_container(cls, value=False): cls.use_default_container = value - @classmethod - def set_plane(cls, plane_origin, plane_normal): - cls.plane_origin = plane_origin - cls.plane_normal = plane_normal - @classmethod def set_instructions(cls, instructions): cls.instructions = instructions @@ -367,169 +362,6 @@ class PolylineDecorator: def set_snap_info(cls, snap_info): cls.snap_info = snap_info - @classmethod - def calculate_distance_and_angle(cls, context, is_input_on): - - try: - polyline_data = context.scene.BIMModelProperties.polyline_point - default_container_elevation = tool.Ifc.get_object(tool.Root.get_default_container()).location.z - last_point_data = polyline_data[len(polyline_data) - 1] - except: - default_container_elevation = 0 - last_point_data = None - - snap_prop = context.scene.BIMModelProperties.snap_mouse_point[0] - - if last_point_data: - last_point = Vector((last_point_data.x, last_point_data.y, last_point_data.z)) - else: - last_point = Vector((0, 0, 0)) - - if is_input_on: - if cls.use_default_container: - snap_vector = Vector( - (float(cls.input_panel["X"]), float(cls.input_panel["Y"]), default_container_elevation) - ) - else: - snap_vector = Vector( - (float(cls.input_panel["X"]), float(cls.input_panel["Y"]), float(cls.input_panel["Z"])) - ) - else: - if cls.use_default_container: - snap_vector = Vector((snap_prop.x, snap_prop.y, default_container_elevation)) - else: - snap_vector = Vector((snap_prop.x, snap_prop.y, snap_prop.z)) - - second_to_last_point = None - if len(polyline_data) > 1: - second_to_last_point_data = polyline_data[len(polyline_data) - 2] - second_to_last_point = Vector( - (second_to_last_point_data.x, second_to_last_point_data.y, second_to_last_point_data.z) - ) - else: - # Creates a fake "second to last" point away from the first point but in the same x axis - # this allows to calculate the angle relative to x axis when there is only one point - second_to_last_point = Vector((last_point.x + 1000, last_point.y, last_point.z)) - - distance = (snap_vector - last_point).length - if distance > 0: - angle = tool.Cad.angle_3_vectors( - second_to_last_point, last_point, snap_vector, new_angle=None, degrees=True - ) - - # Round angle to the nearest 0.05 - angle = round(angle / 0.05) * 0.05 - - if cls.input_panel: - cls.input_panel["X"] = str(round(snap_vector.x, 3)) - cls.input_panel["Y"] = str(round(snap_vector.y, 3)) - if "Z" in list(cls.input_panel.keys()): - cls.input_panel["Z"] = str(round(snap_vector.z, 3)) - cls.input_panel["D"] = str(round(distance, 3)) - cls.input_panel["A"] = str(round(angle, 3)) - - return cls.input_panel - - return cls.input_panel - - @classmethod - def calculate_area(cls, context): - try: - polyline_data = context.scene.BIMModelProperties.polyline_point - except: - return cls.input_panel - - if len(polyline_data) < 3: - return cls.input_panel - - points = [] - for data in polyline_data: - points.append(Vector((data.x, data.y, data.z))) - - if points[0] == points[-1]: - points = points[1:] - - # TODO move this to CAD - # Calculate the normal vector of the plane formed by the first three vertices - v1, v2, v3 = points[:3] - normal = (v2 - v1).cross(v3 - v1).normalized() - - # Check if all points are coplanar - is_coplanar = True - tolerance = 1e-6 # Adjust this value as needed - for v in points: - if abs((v - v1).dot(normal)) > tolerance: - is_coplanar = False - - if is_coplanar: - area = 0 - for i in range(len(points)): - j = (i + 1) % len(points) - area += points[i].cross(points[j]).dot(normal) - - area = abs(area) / 2 - else: - area = 0 - - if "AREA" in list(cls.input_panel.keys()): - cls.input_panel["AREA"] = str(round(area, 4)) - return cls.input_panel - - @classmethod - def calculate_x_y_and_z(cls, context): - try: - polyline_data = context.scene.BIMModelProperties.polyline_point - default_container_elevation = tool.Ifc.get_object(tool.Root.get_default_container()).location.z - last_point_data = polyline_data[len(polyline_data) - 1] - last_point = Vector((last_point_data.x, last_point_data.y, last_point_data.z)) - except: - default_container_elevation = 0 - last_point = Vector((0, 0, 0)) - - snap_prop = context.scene.BIMModelProperties.snap_mouse_point[0] - snap_vector = Vector((snap_prop.x, snap_prop.y, snap_prop.z)) - - if cls.use_default_container: - snap_vector = Vector((snap_prop.x, snap_prop.y, default_container_elevation)) - else: - snap_vector = Vector((snap_prop.x, snap_prop.y, snap_prop.z)) - - if len(polyline_data) > 1: - second_to_last_point_data = polyline_data[len(polyline_data) - 2] - second_to_last_point = Vector( - (second_to_last_point_data.x, second_to_last_point_data.y, second_to_last_point_data.z) - ) - else: - # Creates a fake "second to last" point away from the first point but in the same x axis - # this allows to calculate the angle relative to x axis when there is only one point - second_to_last_point = Vector((last_point.x + 1000, last_point.y, last_point.z)) - - distance = float(cls.input_panel["D"]) - - if distance < 0 or distance > 0: - angle = radians(float(cls.input_panel["A"])) - - rot_vector = tool.Cad.angle_3_vectors(second_to_last_point, last_point, snap_vector, angle, degrees=True) - - coords = rot_vector * distance + last_point - - x = coords[0] - y = coords[1] - z = coords[2] - if cls.input_panel: - cls.input_panel["X"] = str(round(x, 3)) - cls.input_panel["Y"] = str(round(y, 3)) - if "Z" in list(cls.input_panel.keys()): - cls.input_panel["Z"] = str(round(z, 3)) - - return cls.input_panel - - cls.input_panel["X"] = str(round(last_point.x, 3)) - cls.input_panel["Y"] = str(round(last_point.y, 3)) - if "Z" in list(cls.input_panel.keys()): - cls.input_panel["Z"] = str(round(last_point.z, 3)) - - return cls.input_panel def draw_batch(self, shader_type, content_pos, color, indices=None): shader = self.line_shader if shader_type == "LINES" else self.shader @@ -537,21 +369,8 @@ class PolylineDecorator: shader.uniform_float("color", color) batch.draw(shader) - @classmethod - def format_input_panel_units(cls, context, value): - unit_system = tool.Drawing.get_unit_system() - if unit_system == "IMPERIAL": - precision = context.scene.DocProperties.imperial_precision - factor = 3.28084 - else: - precision = None - factor = 1 - if context.scene.unit_settings.length_unit == "MILLIMETERS": - factor = 1000 - return format_distance(value * factor, precision=precision, suppress_zero_inches=True, in_unit_length=True) - - def draw_input_panel(self, context): + def draw_input_ui(self, context): texts = { "D": "Distance: ", "A": "Angle: ", @@ -571,22 +390,22 @@ class PolylineDecorator: color_highlight = self.addon_prefs.decorator_color_special offset = 20 new_line = 20 - for i, (key, value) in enumerate(self.input_panel.items()): + for i, (key, field_name) in enumerate(texts.items()): - if key != "A" and key != self.input_type: - value = float(value) - formatted_value = self.format_input_panel_units(context, value) + if key != self.input_type: + formatted_value = self.input_ui.get_formatted_value(key) else: - formatted_value = value + formatted_value = self.input_ui.get_text_value(key) - if key not in list(texts.keys()): + if formatted_value is None: continue if key == self.input_type: blf.color(self.font_id, *color_highlight) else: blf.color(self.font_id, *color) blf.position(self.font_id, self.mouse_pos[0] + offset, self.mouse_pos[1] - (new_line * i), 0) - blf.draw(self.font_id, texts[key] + formatted_value) + blf.draw(self.font_id, field_name + formatted_value) + def draw_measurements(self, context): region = context.region @@ -608,9 +427,7 @@ class PolylineDecorator: pos_dim = (Vector(measurement_prop[i].position) + Vector(measurement_prop[i - 1].position)) / 2 coords_dim = view3d_utils.location_3d_to_region_2d(region, rv3d, pos_dim) - value = measurement_prop[i].dim - value = float(value) - formatted_value = self.format_input_panel_units(context, value) + formatted_value = measurement_prop[i].dim blf.position(self.font_id, coords_dim[0], coords_dim[1], 0) blf.draw(self.font_id, "d: " + formatted_value) @@ -622,6 +439,7 @@ class PolylineDecorator: blf.position(self.font_id, coords_angle[0], coords_angle[1], 0) blf.draw(self.font_id, "a: " + measurement_prop[i].angle) + def draw_on_screen_menu(self, context): region = context.region @@ -708,12 +526,12 @@ class PolylineDecorator: pass # Area highlight - if "AREA" in list(self.input_panel.keys()): - if self.input_panel["AREA"] and float(self.input_panel["AREA"]) > 0: - edges = [] - for i in range(1, len(polyline_points) - 1): - edges.append((0, i, i + 1)) - self.draw_batch("TRIS", polyline_points, (0, 1, 0, 0.1), edges) + # if "AREA" in list(self.input_panel.keys()): # TODO Change to input_ui + # if self.input_panel["AREA"] and float(self.input_panel["AREA"]) > 0: # TODO Change to input_ui + # edges = [] + # for i in range(1, len(polyline_points) - 1): + # edges.append((0, i, i + 1)) + # self.draw_batch("TRIS", polyline_points, (0, 1, 0, 0.1), edges) # Mouse points if snap_prop.snap_type in ["Face", "Plane"]: diff --git a/src/bonsai/bonsai/bim/module/model/wall.py b/src/bonsai/bonsai/bim/module/model/wall.py index 21b57f8f4f..5dbc53351a 100644 --- a/src/bonsai/bonsai/bim/module/model/wall.py +++ b/src/bonsai/bonsai/bim/module/model/wall.py @@ -324,9 +324,9 @@ class DrawPolylineWall(bpy.types.Operator): self.number_is_negative = False self.is_input_on = False self.input_options = ["D", "A", "X", "Y"] - self.input_type = "OFF" + self.input_type = None self.input_value_xy = [None, None] - self.input_panel = {"D": "", "A": "", "X": "", "Y": ""} + self.input_ui = tool.Polyline.create_input_ui() self.snap_angle = None self.snapping_points = [] self.instructions = """TAB: Cycle Input @@ -340,18 +340,18 @@ class DrawPolylineWall(bpy.types.Operator): def recalculate_inputs(self, context): if self.number_input: is_valid, self.number_output = tool.Snap.validate_input(self.number_output, self.input_type) - self.input_panel[self.input_type] = self.number_output + self.input_ui.set_value(self.input_type, self.number_output) if not is_valid: self.report({"WARNING"}, "The number typed is not valid.") return is_valid else: if self.input_type in {"X", "Y"}: - self.input_panel = PolylineDecorator.calculate_distance_and_angle(context, self.is_input_on) + tool.Polyline.calculate_distance_and_angle(context, self.is_input_on, self.input_ui) elif self.input_type in {"D", "A"}: - self.input_panel = PolylineDecorator.calculate_x_y_and_z(context) - self.input_panel = PolylineDecorator.calculate_distance_and_angle(context, self.is_input_on) + tool.Polyline.calculate_x_y_and_z(context, self.input_ui) + tool.Polyline.calculate_distance_and_angle(context, self.is_input_on, self.input_ui) else: - self.input_panel[self.input_type] = self.number_output + self.input_ui.set_value(self.input_type, self.number_output) tool.Blender.update_viewport() return is_valid @@ -387,8 +387,8 @@ class DrawPolylineWall(bpy.types.Operator): if event.type == "MOUSEMOVE" or event.type == "INBETWEEN_MOUSEMOVE": self.mousemove_count += 1 self.is_input_on = False - self.input_type = "OFF" - PolylineDecorator.set_input_panel(self.input_panel, self.input_type) + self.input_type = None + PolylineDecorator.set_input_ui(self.input_ui, self.input_type) tool.Snap.clear_snapping_ref() tool.Blender.update_viewport() else: @@ -403,7 +403,7 @@ class DrawPolylineWall(bpy.types.Operator): detected_snaps = tool.Snap.detect_snapping_points(context, event, self.objs_2d_bbox) self.snapping_points = tool.Snap.select_snapping_points(context, event, detected_snaps) PolylineDecorator.set_mouse_position(event) - self.input_panel = PolylineDecorator.calculate_distance_and_angle(context, self.is_input_on) + tool.Polyline.calculate_distance_and_angle(context, self.is_input_on, self.input_ui) tool.Blender.update_viewport() return {"RUNNING_MODAL"} @@ -412,7 +412,7 @@ class DrawPolylineWall(bpy.types.Operator): tool.Blender.update_viewport() if event.value == "RELEASE" and event.type == "LEFTMOUSE": - tool.Snap.insert_polyline_point(self.input_panel) + tool.Snap.insert_polyline_point(self.input_ui) tool.Blender.update_viewport() if event.value == "PRESS" and event.type == "X": @@ -425,7 +425,7 @@ class DrawPolylineWall(bpy.types.Operator): if event.value == "PRESS" and event.type == "C": tool.Snap.close_polyline() - PolylineDecorator.set_input_panel(self.input_panel, self.input_type) + PolylineDecorator.set_input_ui(self.input_ui, self.input_type) tool.Blender.update_viewport() if self.is_input_on and event.value == "PRESS" and event.type == "TAB": @@ -434,15 +434,15 @@ class DrawPolylineWall(bpy.types.Operator): size = len(self.input_options) self.input_type = self.input_options[((index + 1) % size)] - self.number_input = self.input_panel[self.input_type] + self.number_input = self.input_ui.get_text_value(self.input_type) self.number_input = list(self.number_input) self.number_output = "".join(self.number_input) if self.input_type != "A": - self.number_output = PolylineDecorator.format_input_panel_units(context, float(self.number_output)) + self.number_output = self.input_ui.get_formatted_value(self.input_type) - self.input_panel[self.input_type] = self.number_output + self.input_ui.set_value(self.input_type, self.number_output) - PolylineDecorator.set_input_panel(self.input_panel, self.input_type) + PolylineDecorator.set_input_ui(self.input_ui, self.input_type) tool.Blender.update_viewport() if not self.is_input_on and event.value == "RELEASE" and event.type == "TAB": @@ -450,13 +450,14 @@ class DrawPolylineWall(bpy.types.Operator): self.is_input_on = True self.input_type = "D" - self.number_input = self.input_panel[self.input_type] + self.number_input = self.input_ui.get_text_value(self.input_type) + print("NI", self.number_input) self.number_input = list(self.number_input) self.number_output = "".join(self.number_input) - self.number_output = PolylineDecorator.format_input_panel_units(context, float(self.number_output)) - self.input_panel[self.input_type] = self.number_output + self.number_output = self.input_ui.get_formatted_value(self.input_type) + self.input_ui.set_value(self.input_type, self.number_output) - PolylineDecorator.set_input_panel(self.input_panel, self.input_type) + PolylineDecorator.set_input_ui(self.input_ui, self.input_type) tool.Blender.update_viewport() if not self.is_input_on and event.ascii in self.number_options: @@ -464,7 +465,8 @@ class DrawPolylineWall(bpy.types.Operator): self.is_input_on = True self.input_type = "D" self.number_input = [] - PolylineDecorator.set_input_panel(self.input_panel, self.input_type) + print("WALL", self.input_type) + PolylineDecorator.set_input_ui(self.input_ui, self.input_type) tool.Blender.update_viewport() if event.value == "RELEASE" and event.type in {"D", "A"}: @@ -472,8 +474,8 @@ class DrawPolylineWall(bpy.types.Operator): self.is_input_on = True self.input_type = event.type self.number_input = [] - self.input_panel[self.input_type] = "" - PolylineDecorator.set_input_panel(self.input_panel, self.input_type) + self.input_ui.set_value(self.input_type, "") + PolylineDecorator.set_input_ui(self.input_ui, self.input_type) tool.Blender.update_viewport() if self.input_type in self.input_options: @@ -486,16 +488,16 @@ class DrawPolylineWall(bpy.types.Operator): self.number_output = "".join(self.number_input) if not self.number_input: self.number_output = "0" - self.input_panel[self.input_type] = self.number_output - PolylineDecorator.set_input_panel(self.input_panel, self.input_type) + self.input_ui.set_value(self.input_type, self.number_output) + PolylineDecorator.set_input_ui(self.input_ui, self.input_type) tool.Blender.update_viewport() else: self.number_input.append(event.ascii) self.number_output = "".join(self.number_input) if self.number_input: - self.input_panel[self.input_type] = self.number_output - PolylineDecorator.set_input_panel(self.input_panel, self.input_type) + self.input_ui.set_value(self.input_type, self.number_output) + PolylineDecorator.set_input_ui(self.input_ui, self.input_type) tool.Blender.update_viewport() if not self.is_input_on and event.value == "RELEASE" and event.type in {"RET", "NUMPAD_ENTER", "RIGHTMOUSE"}: @@ -508,18 +510,18 @@ class DrawPolylineWall(bpy.types.Operator): if self.is_input_on and event.value == "RELEASE" and event.type in {"RET", "NUMPAD_ENTER", "RIGHTMOUSE"}: is_valid = self.recalculate_inputs(context) if is_valid: - tool.Snap.insert_polyline_point(self.input_panel) + tool.Snap.insert_polyline_point(self.input_ui) self.is_input_on = False - self.input_type = "OFF" + self.input_type = None self.number_input = [] self.number_output = "" - PolylineDecorator.set_input_panel(self.input_panel, self.input_type) + PolylineDecorator.set_input_ui(self.input_ui, self.input_type) tool.Blender.update_viewport() if event.value == "PRESS" and event.type == "M": self.snapping_points = tool.Snap.modify_snapping_point_selection(self.snapping_points) PolylineDecorator.set_mouse_position(event) - self.input_panel = PolylineDecorator.calculate_distance_and_angle(context, self.is_input_on) + tool.Polyline.calculate_distance_and_angle(context, self.is_input_on, self.input_ui) tool.Blender.update_viewport() if event.type in {"MIDDLEMOUSE", "WHEELUPMOUSE", "WHEELDOWNMOUSE"}: @@ -529,8 +531,8 @@ class DrawPolylineWall(bpy.types.Operator): if event.value == "RELEASE" and event.type in {"ESC"}: self.recalculate_inputs(context) self.is_input_on = False - self.input_type = "OFF" - PolylineDecorator.set_input_panel(self.input_panel, self.input_type) + self.input_type = None + PolylineDecorator.set_input_ui(self.input_ui, self.input_type) tool.Blender.update_viewport() else: if event.value == "RELEASE" and event.type in {"ESC"}: @@ -547,16 +549,18 @@ class DrawPolylineWall(bpy.types.Operator): PolylineDecorator.install(context) tool.Snap.set_use_default_container(True) PolylineDecorator.set_use_default_container(True) + tool.Polyline.set_use_default_container(True) # <--- tool.Snap.set_snap_plane_method("XY") PolylineDecorator.set_instructions(self.instructions) - PolylineDecorator.set_input_panel(self.input_panel, self.input_type) + PolylineDecorator.set_input_ui(self.input_ui, self.input_type) self.visible_objs = tool.Raycast.get_visible_objects(context) for obj in self.visible_objs: self.objs_2d_bbox.append(tool.Raycast.get_on_screen_2d_bounding_boxes(context, obj)) detected_snaps = tool.Snap.detect_snapping_points(context, event, self.objs_2d_bbox) self.snapping_points = tool.Snap.select_snapping_points(context, event, detected_snaps) PolylineDecorator.set_mouse_position(event) - self.input_panel = PolylineDecorator.calculate_distance_and_angle(context, self.is_input_on) + tool.Polyline.calculate_distance_and_angle(context, self.is_input_on, self.input_ui) + tool.Blender.update_viewport() context.window_manager.modal_handler_add(self) return {"RUNNING_MODAL"} diff --git a/src/bonsai/bonsai/bim/module/project/operator.py b/src/bonsai/bonsai/bim/module/project/operator.py index f8acb4d7e1..0758348d04 100644 --- a/src/bonsai/bonsai/bim/module/project/operator.py +++ b/src/bonsai/bonsai/bim/module/project/operator.py @@ -2310,7 +2310,7 @@ class MeasureTool(bpy.types.Operator): self.input_options = ["D", "A", "X", "Y", "Z"] self.input_type = None self.input_value_xy = [None, None] - self.input_panel = {"D": "", "A": "", "X": "", "Y": "", "Z": ""} + self.input_ui = tool.Polyline.create_input_ui(init_z=True) self.snap_angle = None self.snapping_points = [] self.instructions = """TAB: Cycle Input @@ -2325,18 +2325,18 @@ class MeasureTool(bpy.types.Operator): def recalculate_inputs(self, context): if self.number_input: is_valid, self.number_output = tool.Snap.validate_input(self.number_output, self.input_type) - self.input_panel[self.input_type] = self.number_output + self.input_ui.set_value(self.input_type, self.number_output) if not is_valid: self.report({"WARNING"}, "The number typed is not valid.") return is_valid else: if self.input_type in {"X", "Y", "Z"}: - self.input_panel = PolylineDecorator.calculate_distance_and_angle(context, self.is_input_on) + tool.Polyline.calculate_distance_and_angle(context, self.is_input_on, self.input_ui) elif self.input_type in {"D", "A"}: - self.input_panel = PolylineDecorator.calculate_x_y_and_z(context) - # self.input_panel = PolylineDecorator.calculate_distance_and_angle(context, self.is_input_on) + tool.Polyline.calculate_x_y_and_z(context, self.input_ui) + tool.Polyline.calculate_distance_and_angle(context, self.is_input_on, self.input_ui) else: - self.input_panel[self.input_type] = self.number_output + self.input_ui.set_value(self.input_type, self.number_output) tool.Blender.update_viewport() return is_valid @@ -2347,7 +2347,7 @@ class MeasureTool(bpy.types.Operator): self.mousemove_count += 1 self.is_input_on = False self.input_type = None - PolylineDecorator.set_input_panel(self.input_panel, self.input_type) + PolylineDecorator.set_input_ui(self.input_ui, self.input_type) tool.Snap.clear_snapping_ref() tool.Blender.update_viewport() else: @@ -2362,7 +2362,7 @@ class MeasureTool(bpy.types.Operator): detected_snaps = tool.Snap.detect_snapping_points(context, event, self.objs_2d_bbox) self.snapping_points = tool.Snap.select_snapping_points(context, event, detected_snaps) PolylineDecorator.set_mouse_position(event) - self.input_panel = PolylineDecorator.calculate_distance_and_angle(context, self.is_input_on) + tool.Polyline.calculate_distance_and_angle(context, self.is_input_on, self.input_ui) tool.Blender.update_viewport() return {"RUNNING_MODAL"} @@ -2371,7 +2371,7 @@ class MeasureTool(bpy.types.Operator): tool.Blender.update_viewport() if event.value == "RELEASE" and event.type == "LEFTMOUSE": - tool.Snap.insert_polyline_point(self.input_panel) + tool.Snap.insert_polyline_point(self.input_ui) tool.Blender.update_viewport() if event.value == "PRESS" and event.type == "X": @@ -2388,7 +2388,7 @@ class MeasureTool(bpy.types.Operator): if event.value == "PRESS" and event.type == "C": tool.Snap.close_polyline() - PolylineDecorator.set_input_panel(self.input_panel, self.input_type) + PolylineDecorator.set_input_ui(self.input_ui, self.input_type) tool.Blender.update_viewport() if self.is_input_on and event.value == "PRESS" and event.type == "TAB": @@ -2397,15 +2397,15 @@ class MeasureTool(bpy.types.Operator): size = len(self.input_options) self.input_type = self.input_options[((index + 1) % size)] - self.number_input = self.input_panel[self.input_type] + self.number_input = self.input_ui.get_text_value(self.input_type) self.number_input = list(self.number_input) self.number_output = "".join(self.number_input) if self.input_type != "A": - self.number_output = PolylineDecorator.format_input_panel_units(context, float(self.number_output)) + self.number_output = self.input_ui.get_formatted_value(self.input_type) - self.input_panel[self.input_type] = self.number_output + self.input_ui.set_value(self.input_type, self.number_output) - PolylineDecorator.set_input_panel(self.input_panel, self.input_type) + PolylineDecorator.set_input_ui(self.input_ui, self.input_type) tool.Blender.update_viewport() if not self.is_input_on and event.value == "RELEASE" and event.type == "TAB": @@ -2413,13 +2413,13 @@ class MeasureTool(bpy.types.Operator): self.is_input_on = True self.input_type = "D" - self.number_input = self.input_panel[self.input_type] + self.number_input = self.input_ui.get_text_value(self.input_type) self.number_input = list(self.number_input) self.number_output = "".join(self.number_input) - self.number_output = PolylineDecorator.format_input_panel_units(context, float(self.number_output)) - self.input_panel[self.input_type] = self.number_output + self.number_output = self.input_ui.get_formatted_value(self.input_type) + self.input_ui.set_value(self.input_type, self.number_output) - PolylineDecorator.set_input_panel(self.input_panel, self.input_type) + PolylineDecorator.set_input_ui(self.input_ui, self.input_type) tool.Blender.update_viewport() if not self.is_input_on and event.ascii in self.number_options: @@ -2427,7 +2427,7 @@ class MeasureTool(bpy.types.Operator): self.is_input_on = True self.input_type = "D" self.number_input = [] - PolylineDecorator.set_input_panel(self.input_panel, self.input_type) + PolylineDecorator.set_input_ui(self.input_ui, self.input_type) tool.Blender.update_viewport() if event.value == "RELEASE" and event.type in {"D", "A"}: @@ -2435,8 +2435,8 @@ class MeasureTool(bpy.types.Operator): self.is_input_on = True self.input_type = event.type self.number_input = [] - self.input_panel[self.input_type] = "" - PolylineDecorator.set_input_panel(self.input_panel, self.input_type) + self.input_ui.set_value(self.input_type, "") + PolylineDecorator.set_input_ui(self.input_ui, self.input_type) tool.Blender.update_viewport() if self.input_type in self.input_options: @@ -2447,16 +2447,18 @@ class MeasureTool(bpy.types.Operator): else: self.number_input = self.number_input[:-1] self.number_output = "".join(self.number_input) - self.input_panel[self.input_type] = self.number_output - PolylineDecorator.set_input_panel(self.input_panel, self.input_type) + if not self.number_input: + self.number_output = "0" + self.input_ui.set_value(self.input_type, self.number_output) + PolylineDecorator.set_input_ui(self.input_ui, self.input_type) tool.Blender.update_viewport() else: self.number_input.append(event.ascii) self.number_output = "".join(self.number_input) if self.number_input: - self.input_panel[self.input_type] = self.number_output - PolylineDecorator.set_input_panel(self.input_panel, self.input_type) + self.input_ui.set_value(self.input_type, self.number_output) + PolylineDecorator.set_input_ui(self.input_ui, self.input_type) tool.Blender.update_viewport() if not self.is_input_on and event.value == "RELEASE" and event.type in {"RET", "NUMPAD_ENTER", "RIGHTMOUSE"}: @@ -2468,18 +2470,18 @@ class MeasureTool(bpy.types.Operator): if self.is_input_on and event.value == "RELEASE" and event.type in {"RET", "NUMPAD_ENTER", "RIGHTMOUSE"}: is_valid = self.recalculate_inputs(context) if is_valid: - tool.Snap.insert_polyline_point(self.input_panel) + tool.Snap.insert_polyline_point(self.input_ui) self.is_input_on = False self.input_type = None self.number_input = [] self.number_output = "" - PolylineDecorator.set_input_panel(self.input_panel, self.input_type) + PolylineDecorator.set_input_ui(self.input_ui, self.input_type) tool.Blender.update_viewport() if event.value == "PRESS" and event.type == "M": self.snapping_points = tool.Snap.modify_snapping_point_selection(self.snapping_points) PolylineDecorator.set_mouse_position(event) - self.input_panel = PolylineDecorator.calculate_distance_and_angle(context, self.is_input_on) + tool.Polyline.calculate_distance_and_angle(context, self.is_input_on, self.input_ui) tool.Blender.update_viewport() if event.shift and event.value == "PRESS" and event.type == "X": @@ -2511,7 +2513,7 @@ class MeasureTool(bpy.types.Operator): self.recalculate_inputs(context) self.is_input_on = False self.input_type = None - PolylineDecorator.set_input_panel(self.input_panel, self.input_type) + PolylineDecorator.set_input_ui(self.input_ui, self.input_type) tool.Blender.update_viewport() else: if event.value == "RELEASE" and event.type in {"ESC"}: @@ -2529,17 +2531,18 @@ class MeasureTool(bpy.types.Operator): PolylineDecorator.install(context) tool.Snap.set_use_default_container(False) PolylineDecorator.set_use_default_container(False) + tool.Polyline.set_use_default_container(False) # <--- tool.Snap.set_snap_plane_method(None) tool.Snap.set_snap_axis_method(None) PolylineDecorator.set_instructions(self.instructions) - PolylineDecorator.set_input_panel(self.input_panel, self.input_type) + PolylineDecorator.set_input_ui(self.input_ui, self.input_type) self.visible_objs = tool.Raycast.get_visible_objects(context) for obj in self.visible_objs: self.objs_2d_bbox.append(tool.Raycast.get_on_screen_2d_bounding_boxes(context, obj)) detected_snaps = tool.Snap.detect_snapping_points(context, event, self.objs_2d_bbox) self.snapping_points = tool.Snap.select_snapping_points(context, event, detected_snaps) PolylineDecorator.set_mouse_position(event) - self.input_panel = PolylineDecorator.calculate_distance_and_angle(context, self.is_input_on) + tool.Polyline.calculate_distance_and_angle(context, self.is_input_on, self.input_ui) tool.Blender.update_viewport() context.window_manager.modal_handler_add(self) return {"RUNNING_MODAL"} diff --git a/src/bonsai/bonsai/core/tool.py b/src/bonsai/bonsai/core/tool.py index e3566b72cf..0c675f684d 100644 --- a/src/bonsai/bonsai/core/tool.py +++ b/src/bonsai/bonsai/core/tool.py @@ -580,6 +580,9 @@ class Nest: class Patch: def run_migrate_patch(cls, infile, outfile, schema): pass +@interface +class Polyline: + pass @interface class Owner: diff --git a/src/bonsai/bonsai/tool/__init__.py b/src/bonsai/bonsai/tool/__init__.py index a7d28cab44..16a64716e4 100644 --- a/src/bonsai/bonsai/tool/__init__.py +++ b/src/bonsai/bonsai/tool/__init__.py @@ -44,6 +44,7 @@ from bonsai.tool.model import Model from bonsai.tool.nest import Nest from bonsai.tool.owner import Owner from bonsai.tool.patch import Patch +from bonsai.tool.polyline import Polyline from bonsai.tool.project import Project from bonsai.tool.profile import Profile from bonsai.tool.pset import Pset diff --git a/src/bonsai/bonsai/tool/polyline.py b/src/bonsai/bonsai/tool/polyline.py new file mode 100644 index 0000000000..8934ffe4c7 --- /dev/null +++ b/src/bonsai/bonsai/tool/polyline.py @@ -0,0 +1,284 @@ +# Bonsai - OpenBIM Blender Add-on +# Copyright (C) 2022 Cyril Waechter +# +# This file is part of Bonsai. +# +# Bonsai is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Bonsai 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Bonsai. If not, see . + +import bpy +import bonsai.core.tool +import bonsai.tool as tool +from bonsai.bim.module.drawing.helper import format_distance +from dataclasses import dataclass, field +from math import sin, cos, radians, degrees, atan2, acos +from mathutils import Vector, Matrix +from typing import Optional + + +@dataclass +class PolylineUI: + _D: str = "" + _A: str = "" + _X: str = "" + _Y: str = "" + _Z: Optional[str] = None + _AREA: Optional[str] = None + init_z: bool = False + init_area: bool = False + + def __post_init__(self): + if self.init_z: + self._Z = "" + if self.init_area: + self._AREA = "" + + def set_value(self, attribute_name, value): + if isinstance(value, float): + value = round(value, 3) + value = str(value) + setattr(self, f"_{attribute_name}", value) + + def get_text_value(self, attribute_name): + return getattr(self, f"_{attribute_name}") + + def get_number_value(self, attribute_name): + value = getattr(self, f"_{attribute_name}") + if value: + return float(value) + else: + return value + + def get_formatted_value(self, attribute_name): + value = self.get_number_value(attribute_name) + context = bpy.context + if value is None: + return None + if attribute_name == 'A': + return self.get_text_value(attribute_name) + else: + return self.format_input_ui_units(context, value) + + def format_input_ui_units(cls, context, value): + unit_system = tool.Drawing.get_unit_system() + if unit_system == "IMPERIAL": + precision = context.scene.DocProperties.imperial_precision + factor = 3.28084 + else: + precision = None + factor = 1 + if context.scene.unit_settings.length_unit == "MILLIMETERS": + factor = 1000 + + return format_distance(value * factor, precision=precision, suppress_zero_inches=True, in_unit_length=True) + + +class Polyline(bonsai.core.tool.Polyline): + + @classmethod + def create_input_ui(cls, init_z=False, init_area=False): + return PolylineUI(init_z=init_z, init_area=init_area) + + @classmethod + def set_mouse_position(cls, event): + cls.mouse_pos = event.mouse_region_x, event.mouse_region_y + + @classmethod + def set_angle_axis_line(cls, start, end): + cls.axis_start = start + cls.axis_end = end + + @classmethod + def set_axis_rectangle(cls, corners): + cls.axis_rectangle = [*corners] + + @classmethod + def set_use_default_container(cls, value=False): + cls.use_default_container = value + + @classmethod + def set_plane(cls, plane_origin, plane_normal): + cls.plane_origin = plane_origin + cls.plane_normal = plane_normal + + @classmethod + def set_instructions(cls, instructions): + cls.instructions = instructions + + @classmethod + def set_snap_info(cls, snap_info): + cls.snap_info = snap_info + + @classmethod + def calculate_distance_and_angle(cls, context, is_input_on, input_ui): + + try: + polyline_data = context.scene.BIMModelProperties.polyline_point + default_container_elevation = tool.Ifc.get_object(tool.Root.get_default_container()).location.z + last_point_data = polyline_data[len(polyline_data) - 1] + except: + default_container_elevation = 0 + last_point_data = None + + snap_prop = context.scene.BIMModelProperties.snap_mouse_point[0] + + if last_point_data: + last_point = Vector((last_point_data.x, last_point_data.y, last_point_data.z)) + else: + last_point = Vector((0, 0, 0)) + + if is_input_on: + if cls.use_default_container: + snap_vector = Vector( + (input_ui.get_number_value("X"), input_ui.get_number_value("Y"), default_container_elevation) + ) + else: + snap_vector = Vector( + (input_ui.get_number_value("X"), input_ui.get_number_value("Y"), input_ui.get_number_value("Z")) + ) + else: + if cls.use_default_container: + snap_vector = Vector((snap_prop.x, snap_prop.y, default_container_elevation)) + else: + snap_vector = Vector((snap_prop.x, snap_prop.y, snap_prop.z)) + + second_to_last_point = None + if len(polyline_data) > 1: + second_to_last_point_data = polyline_data[len(polyline_data) - 2] + second_to_last_point = Vector( + (second_to_last_point_data.x, second_to_last_point_data.y, second_to_last_point_data.z) + ) + else: + # Creates a fake "second to last" point away from the first point but in the same x axis + # this allows to calculate the angle relative to x axis when there is only one point + second_to_last_point = Vector((last_point.x + 1000, last_point.y, last_point.z)) + + distance = (snap_vector - last_point).length + if distance > 0: + angle = tool.Cad.angle_3_vectors( + second_to_last_point, last_point, snap_vector, new_angle=None, degrees=True + ) + + # Round angle to the nearest 0.05 + angle = round(angle / 0.05) * 0.05 + + if input_ui: + input_ui.set_value("X", snap_vector.x) + input_ui.set_value("Y", snap_vector.y) + if input_ui.get_number_value("Z") is not None: + input_ui.set_value("Z", snap_vector.z) + + input_ui.set_value("D", distance) + input_ui.set_value("A", angle) + return + + return + + @classmethod + def calculate_area(cls, context, input_ui): + try: + polyline_data = context.scene.BIMModelProperties.polyline_point + except: + return input_ui + + if len(polyline_data) < 3: + return input_ui + + points = [] + for data in polyline_data: + points.append(Vector((data.x, data.y, data.z))) + + if points[0] == points[-1]: + points = points[1:] + + # TODO move this to CAD + # Calculate the normal vector of the plane formed by the first three vertices + v1, v2, v3 = points[:3] + normal = (v2 - v1).cross(v3 - v1).normalized() + + # Check if all points are coplanar + is_coplanar = True + tolerance = 1e-6 # Adjust this value as needed + for v in points: + if abs((v - v1).dot(normal)) > tolerance: + is_coplanar = False + + if is_coplanar: + area = 0 + for i in range(len(points)): + j = (i + 1) % len(points) + area += points[i].cross(points[j]).dot(normal) + + area = abs(area) / 2 + else: + area = 0 + + if input_ui.get_text_value("A") is not None: + input_ui.set_value("A", area) + return + + @classmethod + def calculate_x_y_and_z(cls, context, input_ui): + try: + polyline_data = context.scene.BIMModelProperties.polyline_point + default_container_elevation = tool.Ifc.get_object(tool.Root.get_default_container()).location.z + last_point_data = polyline_data[len(polyline_data) - 1] + last_point = Vector((last_point_data.x, last_point_data.y, last_point_data.z)) + except: + default_container_elevation = 0 + last_point = Vector((0, 0, 0)) + + snap_prop = context.scene.BIMModelProperties.snap_mouse_point[0] + snap_vector = Vector((snap_prop.x, snap_prop.y, snap_prop.z)) + + if cls.use_default_container: + snap_vector = Vector((snap_prop.x, snap_prop.y, default_container_elevation)) + else: + snap_vector = Vector((snap_prop.x, snap_prop.y, snap_prop.z)) + + if len(polyline_data) > 1: + second_to_last_point_data = polyline_data[len(polyline_data) - 2] + second_to_last_point = Vector( + (second_to_last_point_data.x, second_to_last_point_data.y, second_to_last_point_data.z) + ) + else: + # Creates a fake "second to last" point away from the first point but in the same x axis + # this allows to calculate the angle relative to x axis when there is only one point + second_to_last_point = Vector((last_point.x + 1000, last_point.y, last_point.z)) + + distance = input_ui.get_number_value("D") + + if distance < 0 or distance > 0: + angle = radians(input_ui.get_number_value("A")) + + rot_vector = tool.Cad.angle_3_vectors(second_to_last_point, last_point, snap_vector, angle, degrees=True) + + coords = rot_vector * distance + last_point + + x = coords[0] + y = coords[1] + z = coords[2] + if input_ui: + input_ui.set_value("X", x) + input_ui.set_value("Y", y) + if input_ui.get_number_value("Z") is not None: + input_ui.set_value("Z", z) + + return + + input_ui.set_value("X", last_point.x) + input_ui.set_value("Y", last_point.y) + if input_ui.get_number_value("Z") is not None: + input_ui.set_value("Z", last_point.z) + + return diff --git a/src/bonsai/bonsai/tool/snap.py b/src/bonsai/bonsai/tool/snap.py index 4f51e85ca6..9f0cc5f9bf 100644 --- a/src/bonsai/bonsai/tool/snap.py +++ b/src/bonsai/bonsai/tool/snap.py @@ -70,6 +70,7 @@ class Snap(bonsai.core.tool.Snap): return snap_point + # TODO Remove this function @classmethod def select_snap_point(cls, snap_points, hit, threshold): shortest_distance = None @@ -125,15 +126,15 @@ class Snap(bonsai.core.tool.Snap): bpy.context.scene.BIMModelProperties.snap_mouse_ref.clear() @classmethod - def insert_polyline_point(cls, input_panel): - x = float(input_panel["X"]) - y = float(input_panel["Y"]) + def insert_polyline_point(cls, input_ui): + x = input_ui.get_number_value("X") + y = input_ui.get_number_value("Y") try: - z = float(input_panel["Z"]) + z = input_ui.get_number_value("Z") except: z = Vector((0, 0, 0)) - d = input_panel["D"] - a = input_panel["A"] + d = input_ui.get_formatted_value("D") + a = input_ui.get_formatted_value("A") snap_vertex = bpy.context.scene.BIMModelProperties.snap_mouse_point[0] if cls.use_default_container: @@ -405,7 +406,6 @@ class Snap(bonsai.core.tool.Snap): elevation = tool.Ifc.get_object(tool.Root.get_default_container()).location.z plane_origin, plane_normal = select_plane_method() - PolylineDecorator.set_plane(plane_origin, plane_normal) intersection = tool.Raycast.ray_cast_to_plane(context, event, plane_origin, plane_normal) axis_start = None From e546971ada72454e27ac2a3efb45d9d00fbbeb9f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bruno=20Perdig=C3=A3o?= Date: Fri, 6 Sep 2024 14:26:31 -0300 Subject: [PATCH 16/56] Changed input validation to tool/polyline.py --- src/bonsai/bonsai/bim/module/model/wall.py | 2 +- .../bonsai/bim/module/project/operator.py | 2 +- src/bonsai/bonsai/tool/polyline.py | 130 ++++++++++++++++++ src/bonsai/bonsai/tool/snap.py | 127 ----------------- 4 files changed, 132 insertions(+), 129 deletions(-) diff --git a/src/bonsai/bonsai/bim/module/model/wall.py b/src/bonsai/bonsai/bim/module/model/wall.py index 5dbc53351a..d06323ae95 100644 --- a/src/bonsai/bonsai/bim/module/model/wall.py +++ b/src/bonsai/bonsai/bim/module/model/wall.py @@ -339,7 +339,7 @@ class DrawPolylineWall(bpy.types.Operator): def recalculate_inputs(self, context): if self.number_input: - is_valid, self.number_output = tool.Snap.validate_input(self.number_output, self.input_type) + is_valid, self.number_output = tool.Polyline.validate_input(self.number_output, self.input_type) self.input_ui.set_value(self.input_type, self.number_output) if not is_valid: self.report({"WARNING"}, "The number typed is not valid.") diff --git a/src/bonsai/bonsai/bim/module/project/operator.py b/src/bonsai/bonsai/bim/module/project/operator.py index 0758348d04..b862a9cee2 100644 --- a/src/bonsai/bonsai/bim/module/project/operator.py +++ b/src/bonsai/bonsai/bim/module/project/operator.py @@ -2324,7 +2324,7 @@ class MeasureTool(bpy.types.Operator): def recalculate_inputs(self, context): if self.number_input: - is_valid, self.number_output = tool.Snap.validate_input(self.number_output, self.input_type) + is_valid, self.number_output = tool.Polyline.validate_input(self.number_output, self.input_type) self.input_ui.set_value(self.input_type, self.number_output) if not is_valid: self.report({"WARNING"}, "The number typed is not valid.") diff --git a/src/bonsai/bonsai/tool/polyline.py b/src/bonsai/bonsai/tool/polyline.py index 8934ffe4c7..9095f99fdb 100644 --- a/src/bonsai/bonsai/tool/polyline.py +++ b/src/bonsai/bonsai/tool/polyline.py @@ -21,6 +21,7 @@ import bonsai.core.tool import bonsai.tool as tool from bonsai.bim.module.drawing.helper import format_distance from dataclasses import dataclass, field +from lark import Lark, Transformer from math import sin, cos, radians, degrees, atan2, acos from mathutils import Vector, Matrix from typing import Optional @@ -282,3 +283,132 @@ class Polyline(bonsai.core.tool.Polyline): input_ui.set_value("Z", last_point.z) return + + + @classmethod + def validate_input(cls, input_number, input_type): + + grammar_imperial = """ + start: (FORMULA dim expr) | dim + dim: imperial + + FORMULA: "=" + + imperial: feet? "-"? inches? + feet: NUMBER? "-"? fraction? "'" + inches: NUMBER? "-"? fraction? "\\"" + fraction: NUMBER "/" NUMBER + + expr: (ADD | SUB) dim | (MUL | DIV) NUMBER + + NUMBER: /-?\\d+(?:\\.\\d+)?/ + ADD: "+" + SUB: "-" + MUL: "*" + DIV: "/" + + %ignore " " + """ + + grammar_metric = """ + start: FORMULA? dim expr? + dim: metric + + FORMULA: "=" + + metric: NUMBER + + expr: (ADD | SUB | MUL | DIV) dim + + NUMBER: /-?\\d+(?:\\.\\d+)?/ + ADD: "+" + SUB: "-" + MUL: "*" + DIV: "/" + + %ignore " " + """ + + class InputTransform(Transformer): + def NUMBER(self, n): + return float(n) + + def fraction(self, numbers): + return numbers[0] / numbers[1] + + def inches(self, args): + if len(args) > 1: + result = args[0] + args[1] + else: + result = args[0] + return result / 12 + + def feet(self, args): + return args[0] + + def imperial(self, args): + if len(args) > 1: + if args[0] <= 0: + result = args[0] - args[1] + else: + result = args[0] + args[1] + else: + result = args[0] + return result + + def metric(self, args): + return args[0] + + def dim(self, args): + return args[0] + + def expr(self, args): + op = args[0] + value = float(args[1]) + if op == "+": + return lambda x: x + value + elif op == "-": + return lambda x: x - value + elif op == "*": + return lambda x: x * value + elif op == "/": + return lambda x: x / value + + def FORMULA(cls, args): + return args[0] + + def start(self, args): + i = 0 + if args[0] == "=": + i += 1 + else: + if len(args) > 1: + raise ValueError("Invalid input.") + dimension = args[i] + if len(args) > i + 1: + expression = args[i + 1] + return expression(dimension) * factor + else: + return dimension * factor + + try: + if bpy.context.scene.unit_settings.system == "IMPERIAL": + parser = Lark(grammar_imperial) + factor = 0.3048 + else: + parser = Lark(grammar_metric) + factor = 1 + if bpy.context.scene.unit_settings.length_unit == "MILLIMETERS": + factor = 0.001 + + if input_type == "A": + parser = Lark(grammar_metric) + factor = 1 + + parse_tree = parser.parse(input_number) + + transformer = InputTransform() + result = transformer.transform(parse_tree) + return True, str(result) + except: + return False, "0" diff --git a/src/bonsai/bonsai/tool/snap.py b/src/bonsai/bonsai/tool/snap.py index 9f0cc5f9bf..e75db3aadc 100644 --- a/src/bonsai/bonsai/tool/snap.py +++ b/src/bonsai/bonsai/tool/snap.py @@ -509,130 +509,3 @@ class Snap(bonsai.core.tool.Snap): cls.update_snapping_point(shifted_list[0][0], shifted_list[0][1]) return shifted_list - @classmethod - def validate_input(cls, input_number, input_type): - - grammar_imperial = """ - start: (FORMULA dim expr) | dim - dim: imperial - - FORMULA: "=" - - imperial: feet? "-"? inches? - feet: NUMBER? "-"? fraction? "'" - inches: NUMBER? "-"? fraction? "\\"" - fraction: NUMBER "/" NUMBER - - expr: (ADD | SUB) dim | (MUL | DIV) NUMBER - - NUMBER: /-?\\d+(?:\\.\\d+)?/ - ADD: "+" - SUB: "-" - MUL: "*" - DIV: "/" - - %ignore " " - """ - - grammar_metric = """ - start: FORMULA? dim expr? - dim: metric - - FORMULA: "=" - - metric: NUMBER - - expr: (ADD | SUB | MUL | DIV) dim - - NUMBER: /-?\\d+(?:\\.\\d+)?/ - ADD: "+" - SUB: "-" - MUL: "*" - DIV: "/" - - %ignore " " - """ - - class InputTransform(Transformer): - def NUMBER(self, n): - return float(n) - - def fraction(self, numbers): - return numbers[0] / numbers[1] - - def inches(self, args): - if len(args) > 1: - result = args[0] + args[1] - else: - result = args[0] - return result / 12 - - def feet(self, args): - return args[0] - - def imperial(self, args): - if len(args) > 1: - if args[0] <= 0: - result = args[0] - args[1] - else: - result = args[0] + args[1] - else: - result = args[0] - return result - - def metric(self, args): - return args[0] - - def dim(self, args): - return args[0] - - def expr(self, args): - op = args[0] - value = float(args[1]) - if op == "+": - return lambda x: x + value - elif op == "-": - return lambda x: x - value - elif op == "*": - return lambda x: x * value - elif op == "/": - return lambda x: x / value - - def FORMULA(cls, args): - return args[0] - - def start(self, args): - i = 0 - if args[0] == "=": - i += 1 - else: - if len(args) > 1: - raise ValueError("Invalid input.") - dimension = args[i] - if len(args) > i + 1: - expression = args[i + 1] - return expression(dimension) * factor - else: - return dimension * factor - - try: - if bpy.context.scene.unit_settings.system == "IMPERIAL": - parser = Lark(grammar_imperial) - factor = 0.3048 - else: - parser = Lark(grammar_metric) - factor = 1 - if bpy.context.scene.unit_settings.length_unit == "MILLIMETERS": - factor = 0.001 - - if input_type == "A": - parser = Lark(grammar_metric) - factor = 1 - - parse_tree = parser.parse(input_number) - - transformer = InputTransform() - result = transformer.transform(parse_tree) - return True, str(result) - except: - return False, "0" From 2008081d9ac80b2211b11bf4f8a64220a92eac5f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bruno=20Perdig=C3=A3o?= Date: Fri, 6 Sep 2024 14:43:01 -0300 Subject: [PATCH 17/56] Implementation of #5317. The number in the input UI will be replaced if the user presses a number. This behavior will be different if the user press "=" or backspace. --- src/bonsai/bonsai/bim/module/model/wall.py | 50 +++++++++---------- .../bonsai/bim/module/project/operator.py | 48 +++++++++--------- 2 files changed, 46 insertions(+), 52 deletions(-) diff --git a/src/bonsai/bonsai/bim/module/model/wall.py b/src/bonsai/bonsai/bim/module/model/wall.py index d06323ae95..8c1a61a5a0 100644 --- a/src/bonsai/bonsai/bim/module/model/wall.py +++ b/src/bonsai/bonsai/bim/module/model/wall.py @@ -327,6 +327,7 @@ class DrawPolylineWall(bpy.types.Operator): self.input_type = None self.input_value_xy = [None, None] self.input_ui = tool.Polyline.create_input_ui() + self.is_typing = False self.snap_angle = None self.snapping_points = [] self.instructions = """TAB: Cycle Input @@ -433,15 +434,11 @@ class DrawPolylineWall(bpy.types.Operator): index = self.input_options.index(self.input_type) size = len(self.input_options) self.input_type = self.input_options[((index + 1) % size)] - - self.number_input = self.input_ui.get_text_value(self.input_type) + self.is_typing = False + self.number_input = self.input_ui.get_formatted_value(self.input_type) self.number_input = list(self.number_input) self.number_output = "".join(self.number_input) - if self.input_type != "A": - self.number_output = self.input_ui.get_formatted_value(self.input_type) - self.input_ui.set_value(self.input_type, self.number_output) - PolylineDecorator.set_input_ui(self.input_ui, self.input_type) tool.Blender.update_viewport() @@ -449,14 +446,11 @@ class DrawPolylineWall(bpy.types.Operator): self.recalculate_inputs(context) self.is_input_on = True self.input_type = "D" - - self.number_input = self.input_ui.get_text_value(self.input_type) - print("NI", self.number_input) + self.is_typing = False + self.number_input = self.input_ui.get_formatted_value(self.input_type) self.number_input = list(self.number_input) self.number_output = "".join(self.number_input) - self.number_output = self.input_ui.get_formatted_value(self.input_type) self.input_ui.set_value(self.input_type, self.number_output) - PolylineDecorator.set_input_ui(self.input_ui, self.input_type) tool.Blender.update_viewport() @@ -464,8 +458,6 @@ class DrawPolylineWall(bpy.types.Operator): self.recalculate_inputs(context) self.is_input_on = True self.input_type = "D" - self.number_input = [] - print("WALL", self.input_type) PolylineDecorator.set_input_ui(self.input_ui, self.input_type) tool.Blender.update_viewport() @@ -473,32 +465,36 @@ class DrawPolylineWall(bpy.types.Operator): self.recalculate_inputs(context) self.is_input_on = True self.input_type = event.type - self.number_input = [] self.input_ui.set_value(self.input_type, "") PolylineDecorator.set_input_ui(self.input_ui, self.input_type) tool.Blender.update_viewport() if self.input_type in self.input_options: if (event.ascii in self.number_options) or (event.value == "RELEASE" and event.type == "BACK_SPACE"): + if not self.is_typing and event.ascii != "=": + self.number_input = [] + if event.type == "BACK_SPACE": if len(self.number_input) <= 1: self.number_input = [] else: - self.number_input = self.number_input[:-1] - self.number_output = "".join(self.number_input) - if not self.number_input: - self.number_output = "0" - self.input_ui.set_value(self.input_type, self.number_output) - PolylineDecorator.set_input_ui(self.input_ui, self.input_type) - tool.Blender.update_viewport() + self.number_input.pop(-1) + elif event.ascii == "=": + if self.number_input[0] == "=": + self.number_input.pop(0) + else: + self.number_input.insert(0, "=") else: self.number_input.append(event.ascii) - self.number_output = "".join(self.number_input) - if self.number_input: - self.input_ui.set_value(self.input_type, self.number_output) - PolylineDecorator.set_input_ui(self.input_ui, self.input_type) - tool.Blender.update_viewport() + if not self.number_input: + self.number_output = "0" + + self.is_typing = True + self.number_output = "".join(self.number_input) + self.input_ui.set_value(self.input_type, self.number_output) + PolylineDecorator.set_input_ui(self.input_ui, self.input_type) + tool.Blender.update_viewport() if not self.is_input_on and event.value == "RELEASE" and event.type in {"RET", "NUMPAD_ENTER", "RIGHTMOUSE"}: self.create_walls_from_polyline(context) @@ -549,7 +545,7 @@ class DrawPolylineWall(bpy.types.Operator): PolylineDecorator.install(context) tool.Snap.set_use_default_container(True) PolylineDecorator.set_use_default_container(True) - tool.Polyline.set_use_default_container(True) # <--- + tool.Polyline.set_use_default_container(True) tool.Snap.set_snap_plane_method("XY") PolylineDecorator.set_instructions(self.instructions) PolylineDecorator.set_input_ui(self.input_ui, self.input_type) diff --git a/src/bonsai/bonsai/bim/module/project/operator.py b/src/bonsai/bonsai/bim/module/project/operator.py index b862a9cee2..94d3527854 100644 --- a/src/bonsai/bonsai/bim/module/project/operator.py +++ b/src/bonsai/bonsai/bim/module/project/operator.py @@ -2311,6 +2311,7 @@ class MeasureTool(bpy.types.Operator): self.input_type = None self.input_value_xy = [None, None] self.input_ui = tool.Polyline.create_input_ui(init_z=True) + self.is_typing = False self.snap_angle = None self.snapping_points = [] self.instructions = """TAB: Cycle Input @@ -2396,15 +2397,11 @@ class MeasureTool(bpy.types.Operator): index = self.input_options.index(self.input_type) size = len(self.input_options) self.input_type = self.input_options[((index + 1) % size)] - - self.number_input = self.input_ui.get_text_value(self.input_type) + self.is_typing = False + self.number_input = self.input_ui.get_formatted_value(self.input_type) self.number_input = list(self.number_input) self.number_output = "".join(self.number_input) - if self.input_type != "A": - self.number_output = self.input_ui.get_formatted_value(self.input_type) - self.input_ui.set_value(self.input_type, self.number_output) - PolylineDecorator.set_input_ui(self.input_ui, self.input_type) tool.Blender.update_viewport() @@ -2412,13 +2409,11 @@ class MeasureTool(bpy.types.Operator): self.recalculate_inputs(context) self.is_input_on = True self.input_type = "D" - - self.number_input = self.input_ui.get_text_value(self.input_type) + self.is_typing = False + self.number_input = self.input_ui.get_formatted_value(self.input_type) self.number_input = list(self.number_input) self.number_output = "".join(self.number_input) - self.number_output = self.input_ui.get_formatted_value(self.input_type) self.input_ui.set_value(self.input_type, self.number_output) - PolylineDecorator.set_input_ui(self.input_ui, self.input_type) tool.Blender.update_viewport() @@ -2426,7 +2421,6 @@ class MeasureTool(bpy.types.Operator): self.recalculate_inputs(context) self.is_input_on = True self.input_type = "D" - self.number_input = [] PolylineDecorator.set_input_ui(self.input_ui, self.input_type) tool.Blender.update_viewport() @@ -2434,32 +2428,36 @@ class MeasureTool(bpy.types.Operator): self.recalculate_inputs(context) self.is_input_on = True self.input_type = event.type - self.number_input = [] self.input_ui.set_value(self.input_type, "") PolylineDecorator.set_input_ui(self.input_ui, self.input_type) tool.Blender.update_viewport() if self.input_type in self.input_options: if (event.ascii in self.number_options) or (event.value == "RELEASE" and event.type == "BACK_SPACE"): + if not self.is_typing and event.ascii != "=": + self.number_input = [] + if event.type == "BACK_SPACE": if len(self.number_input) <= 1: self.number_input = [] else: - self.number_input = self.number_input[:-1] - self.number_output = "".join(self.number_input) - if not self.number_input: - self.number_output = "0" - self.input_ui.set_value(self.input_type, self.number_output) - PolylineDecorator.set_input_ui(self.input_ui, self.input_type) - tool.Blender.update_viewport() + self.number_input.pop(-1) + elif event.ascii == "=": + if self.number_input[0] == "=": + self.number_input.pop(0) + else: + self.number_input.insert(0, "=") else: self.number_input.append(event.ascii) - self.number_output = "".join(self.number_input) - if self.number_input: - self.input_ui.set_value(self.input_type, self.number_output) - PolylineDecorator.set_input_ui(self.input_ui, self.input_type) - tool.Blender.update_viewport() + if not self.number_input: + self.number_output = "0" + + self.is_typing = True + self.number_output = "".join(self.number_input) + self.input_ui.set_value(self.input_type, self.number_output) + PolylineDecorator.set_input_ui(self.input_ui, self.input_type) + tool.Blender.update_viewport() if not self.is_input_on and event.value == "RELEASE" and event.type in {"RET", "NUMPAD_ENTER", "RIGHTMOUSE"}: PolylineDecorator.uninstall() @@ -2531,7 +2529,7 @@ class MeasureTool(bpy.types.Operator): PolylineDecorator.install(context) tool.Snap.set_use_default_container(False) PolylineDecorator.set_use_default_container(False) - tool.Polyline.set_use_default_container(False) # <--- + tool.Polyline.set_use_default_container(False) tool.Snap.set_snap_plane_method(None) tool.Snap.set_snap_axis_method(None) PolylineDecorator.set_instructions(self.instructions) From aaa3d5c44343e217c1fe95c058a036da84c758b3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bruno=20Perdig=C3=A3o?= Date: Fri, 6 Sep 2024 14:59:59 -0300 Subject: [PATCH 18/56] Small fix on previos commit --- src/bonsai/bonsai/bim/module/model/wall.py | 2 +- src/bonsai/bonsai/bim/module/project/operator.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/bonsai/bonsai/bim/module/model/wall.py b/src/bonsai/bonsai/bim/module/model/wall.py index 8c1a61a5a0..a50573f20d 100644 --- a/src/bonsai/bonsai/bim/module/model/wall.py +++ b/src/bonsai/bonsai/bim/module/model/wall.py @@ -471,7 +471,7 @@ class DrawPolylineWall(bpy.types.Operator): if self.input_type in self.input_options: if (event.ascii in self.number_options) or (event.value == "RELEASE" and event.type == "BACK_SPACE"): - if not self.is_typing and event.ascii != "=": + if not self.is_typing and not (event.ascii == "=" or event.type == "BACK_SPACE"): self.number_input = [] if event.type == "BACK_SPACE": diff --git a/src/bonsai/bonsai/bim/module/project/operator.py b/src/bonsai/bonsai/bim/module/project/operator.py index 94d3527854..07a83d439b 100644 --- a/src/bonsai/bonsai/bim/module/project/operator.py +++ b/src/bonsai/bonsai/bim/module/project/operator.py @@ -2434,7 +2434,7 @@ class MeasureTool(bpy.types.Operator): if self.input_type in self.input_options: if (event.ascii in self.number_options) or (event.value == "RELEASE" and event.type == "BACK_SPACE"): - if not self.is_typing and event.ascii != "=": + if not self.is_typing and not (event.ascii == "=" or event.type == "BACK_SPACE"): self.number_input = [] if event.type == "BACK_SPACE": From edf0804b0e5423e02449539829526fbdbf85d924 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bruno=20Perdig=C3=A3o?= Date: Fri, 6 Sep 2024 15:00:29 -0300 Subject: [PATCH 19/56] Small fix on how the input UI rounds numbers --- src/bonsai/bonsai/tool/polyline.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/bonsai/bonsai/tool/polyline.py b/src/bonsai/bonsai/tool/polyline.py index 9095f99fdb..24024958f3 100644 --- a/src/bonsai/bonsai/tool/polyline.py +++ b/src/bonsai/bonsai/tool/polyline.py @@ -45,13 +45,16 @@ class PolylineUI: self._AREA = "" def set_value(self, attribute_name, value): - if isinstance(value, float): - value = round(value, 3) value = str(value) setattr(self, f"_{attribute_name}", value) def get_text_value(self, attribute_name): - return getattr(self, f"_{attribute_name}") + value = getattr(self, f"_{attribute_name}") + try: + value = f"{float(value):.3g}" + except: + pass + return value def get_number_value(self, attribute_name): value = getattr(self, f"_{attribute_name}") From d63961d840de94785b28673f753e4fcb133b277e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bruno=20Perdig=C3=A3o?= Date: Fri, 6 Sep 2024 15:17:57 -0300 Subject: [PATCH 20/56] Small fix on previous commit --- src/bonsai/bonsai/tool/polyline.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/src/bonsai/bonsai/tool/polyline.py b/src/bonsai/bonsai/tool/polyline.py index 24024958f3..f107d7ece5 100644 --- a/src/bonsai/bonsai/tool/polyline.py +++ b/src/bonsai/bonsai/tool/polyline.py @@ -50,10 +50,6 @@ class PolylineUI: def get_text_value(self, attribute_name): value = getattr(self, f"_{attribute_name}") - try: - value = f"{float(value):.3g}" - except: - pass return value def get_number_value(self, attribute_name): @@ -69,7 +65,8 @@ class PolylineUI: if value is None: return None if attribute_name == 'A': - return self.get_text_value(attribute_name) + value = float(self.get_text_value(attribute_name)) + return f"{value:.2f}" else: return self.format_input_ui_units(context, value) From e48050af4bf464a4d84498312df76e291c27e331 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bruno=20Perdig=C3=A3o?= Date: Fri, 6 Sep 2024 17:23:20 -0300 Subject: [PATCH 21/56] Fix #5336 --- src/bonsai/bonsai/bim/module/model/wall.py | 2 ++ src/bonsai/bonsai/bim/module/project/operator.py | 1 + src/bonsai/bonsai/tool/snap.py | 5 +++++ 3 files changed, 8 insertions(+) diff --git a/src/bonsai/bonsai/bim/module/model/wall.py b/src/bonsai/bonsai/bim/module/model/wall.py index a50573f20d..b1b9680c26 100644 --- a/src/bonsai/bonsai/bim/module/model/wall.py +++ b/src/bonsai/bonsai/bim/module/model/wall.py @@ -543,9 +543,11 @@ class DrawPolylineWall(bpy.types.Operator): def invoke(self, context, event): if context.space_data.type == "VIEW_3D": PolylineDecorator.install(context) + tool.Snap.clear_snapping_point() tool.Snap.set_use_default_container(True) PolylineDecorator.set_use_default_container(True) tool.Polyline.set_use_default_container(True) + tool.Snap.set_snap_axis_method(None) tool.Snap.set_snap_plane_method("XY") PolylineDecorator.set_instructions(self.instructions) PolylineDecorator.set_input_ui(self.input_ui, self.input_type) diff --git a/src/bonsai/bonsai/bim/module/project/operator.py b/src/bonsai/bonsai/bim/module/project/operator.py index 07a83d439b..dff315ec52 100644 --- a/src/bonsai/bonsai/bim/module/project/operator.py +++ b/src/bonsai/bonsai/bim/module/project/operator.py @@ -2527,6 +2527,7 @@ class MeasureTool(bpy.types.Operator): def invoke(self, context, event): if context.space_data.type == "VIEW_3D": PolylineDecorator.install(context) + tool.Snap.clear_snapping_point() tool.Snap.set_use_default_container(False) PolylineDecorator.set_use_default_container(False) tool.Polyline.set_use_default_container(False) diff --git a/src/bonsai/bonsai/tool/snap.py b/src/bonsai/bonsai/tool/snap.py index e75db3aadc..ccad90a2e6 100644 --- a/src/bonsai/bonsai/tool/snap.py +++ b/src/bonsai/bonsai/tool/snap.py @@ -109,6 +109,10 @@ class Snap(bonsai.core.tool.Snap): snap_vertex.z = snap_point[2] snap_vertex.snap_type = snap_type + @classmethod + def clear_snapping_point(cls): + bpy.context.scene.BIMModelProperties.snap_mouse_point.clear() + @classmethod def update_snapping_ref(cls, snap_point, snap_type): try: @@ -413,6 +417,7 @@ class Snap(bonsai.core.tool.Snap): # TODO It only work for XY plane. Make it work also for None plane_method rot_intersection = None + cls.snap_angle = None if not cls.snap_plane_method: if cls.snap_axis_method == "X": cls.snap_angle = 180 From ce8e104b0b23f986be1fe7757a6f2b01e781d3ee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bruno=20Perdig=C3=A3o?= Date: Fri, 6 Sep 2024 17:24:03 -0300 Subject: [PATCH 22/56] Change snap axis to 15 angles increments. --- src/bonsai/bonsai/tool/snap.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/bonsai/bonsai/tool/snap.py b/src/bonsai/bonsai/tool/snap.py index ccad90a2e6..716b67a037 100644 --- a/src/bonsai/bonsai/tool/snap.py +++ b/src/bonsai/bonsai/tool/snap.py @@ -236,8 +236,8 @@ class Snap(bonsai.core.tool.Snap): translated_intersection = intersection - last_point snap_axis = [] if not lock_axis: - for i in range(1, 13): - angle = 30 * i + for i in range(1, 25): + angle = 15 * i snap_axis.append(angle) else: snap_axis = [lock_axis] @@ -426,6 +426,7 @@ class Snap(bonsai.core.tool.Snap): if cls.snap_axis_method == "Z": cls.snap_angle = 90 if cls.snap_axis_method: + # Doesn't update snap_angle so that it keeps in the same axis rot_intersection, _, axis_start, axis_end = cls.snap_on_axis(intersection, cls.snap_angle) if cls.snap_plane_method: @@ -482,9 +483,6 @@ class Snap(bonsai.core.tool.Snap): snapping_points.append(op) break - if "Plane" in list(origin.keys()): - intersection = origin["Plane"] - snapping_points.append((intersection, "Plane")) for origin in detected_snaps: if "Axis" in list(origin.keys()): @@ -493,6 +491,10 @@ class Snap(bonsai.core.tool.Snap): axis_end = intersection[2] snapping_points.append((intersection[0], "Axis")) + if "Plane" in list(origin.keys()): + intersection = origin["Plane"] + snapping_points.append((intersection, "Plane")) + # Make Axis first priority if event.shift or cls.snap_axis_method in {"X", "Y", "Z"}: cls.update_snapping_ref(snapping_points[0][0], snapping_points[0][1]) From 2e192034f87773ab7f1829270520c1f42fff0204 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bruno=20Perdig=C3=A3o?= Date: Sat, 7 Sep 2024 10:21:41 -0300 Subject: [PATCH 23/56] Small tweak in 8032e4851 --- src/bonsai/bonsai/bim/module/model/wall.py | 1 + src/bonsai/bonsai/tool/snap.py | 5 ++++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/src/bonsai/bonsai/bim/module/model/wall.py b/src/bonsai/bonsai/bim/module/model/wall.py index b1b9680c26..cb273177e5 100644 --- a/src/bonsai/bonsai/bim/module/model/wall.py +++ b/src/bonsai/bonsai/bim/module/model/wall.py @@ -545,6 +545,7 @@ class DrawPolylineWall(bpy.types.Operator): PolylineDecorator.install(context) tool.Snap.clear_snapping_point() tool.Snap.set_use_default_container(True) + tool.Snap.clear_snap_angle() PolylineDecorator.set_use_default_container(True) tool.Polyline.set_use_default_container(True) tool.Snap.set_snap_axis_method(None) diff --git a/src/bonsai/bonsai/tool/snap.py b/src/bonsai/bonsai/tool/snap.py index 716b67a037..f0668f2320 100644 --- a/src/bonsai/bonsai/tool/snap.py +++ b/src/bonsai/bonsai/tool/snap.py @@ -41,6 +41,10 @@ class Snap(bonsai.core.tool.Snap): def set_snap_plane_method(cls, value=True): cls.snap_plane_method = value + @classmethod + def clear_snap_angle(cls): + cls.snap_angle = None + @classmethod def cycle_snap_plane_method(cls, value=True): if cls.snap_plane_method == value: @@ -417,7 +421,6 @@ class Snap(bonsai.core.tool.Snap): # TODO It only work for XY plane. Make it work also for None plane_method rot_intersection = None - cls.snap_angle = None if not cls.snap_plane_method: if cls.snap_axis_method == "X": cls.snap_angle = 180 From be8b325a83e1a41eceb5e9350b68a6a98134353d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bruno=20Perdig=C3=A3o?= Date: Sat, 7 Sep 2024 15:23:43 -0300 Subject: [PATCH 24/56] WIP - Polyline tool refactor --- .../bonsai/bim/module/model/decorator.py | 48 +++++++------- src/bonsai/bonsai/bim/module/model/wall.py | 66 +++++++++++-------- src/bonsai/bonsai/tool/polyline.py | 62 ++++++++--------- src/bonsai/bonsai/tool/snap.py | 50 +++++++------- 4 files changed, 116 insertions(+), 110 deletions(-) diff --git a/src/bonsai/bonsai/bim/module/model/decorator.py b/src/bonsai/bonsai/bim/module/model/decorator.py index b14878a690..790cd9740b 100644 --- a/src/bonsai/bonsai/bim/module/model/decorator.py +++ b/src/bonsai/bonsai/bim/module/model/decorator.py @@ -301,7 +301,8 @@ class ProfileDecorator: class PolylineDecorator: is_installed = False handlers = [] - mouse_pos = None + event = None + input_type = None input_ui = None angle_snap_mat = None @@ -333,30 +334,27 @@ class PolylineDecorator: cls.is_installed = False @classmethod - def set_mouse_position(cls, event): - cls.mouse_pos = event.mouse_region_x, event.mouse_region_y + def update(cls, event, tool_state, input_ui): + cls.event = event + cls.tool_state = tool_state + cls.input_ui = input_ui @classmethod - def set_input_ui(cls, input_ui, input_type): + def set_input_ui(cls, input_ui): cls.input_ui = input_ui - cls.input_type = input_type @classmethod def set_angle_axis_line(cls, start, end): cls.axis_start = start cls.axis_end = end - @classmethod - def set_axis_rectangle(cls, corners): - cls.axis_rectangle = [*corners] + # @classmethod + # def set_axis_rectangle(cls, corners): + # cls.axis_rectangle = [*corners] @classmethod - def set_use_default_container(cls, value=False): - cls.use_default_container = value - - @classmethod - def set_instructions(cls, instructions): - cls.instructions = instructions + def set_tool_state(cls, tool_state): + cls.tool_state = tool_state @classmethod def set_snap_info(cls, snap_info): @@ -379,6 +377,8 @@ class PolylineDecorator: "Z": "Z coord:", "AREA": "Area: ", } + mouse_pos = self.event.mouse_region_x, self.event.mouse_region_y + self.addon_prefs = tool.Blender.get_addon_preferences() self.font_id = 0 @@ -392,18 +392,18 @@ class PolylineDecorator: new_line = 20 for i, (key, field_name) in enumerate(texts.items()): - if key != self.input_type: + if key != self.tool_state.input_type: formatted_value = self.input_ui.get_formatted_value(key) else: formatted_value = self.input_ui.get_text_value(key) if formatted_value is None: continue - if key == self.input_type: + if key == self.tool_state.input_type: blf.color(self.font_id, *color_highlight) else: blf.color(self.font_id, *color) - blf.position(self.font_id, self.mouse_pos[0] + offset, self.mouse_pos[1] - (new_line * i), 0) + blf.position(self.font_id, mouse_pos[0] + offset, mouse_pos[1] - (new_line * i), 0) blf.draw(self.font_id, field_name + formatted_value) @@ -452,10 +452,10 @@ class PolylineDecorator: color = self.addon_prefs.decorations_colour blf.color(self.font_id, *color) - text_w, text_h = blf.dimensions(0, self.instructions) + text_w, text_h = blf.dimensions(0, self.tool_state.instructions) position = (region.width / 2) - (text_w / 2) blf.position(self.font_id, position, 10, 0) - blf.draw(self.font_id, self.instructions) + blf.draw(self.font_id, self.tool_state.instructions) text_w, text_h = blf.dimensions(0, self.snap_info) position = (region.width / 2) - (text_w / 2) @@ -494,7 +494,7 @@ class PolylineDecorator: default_container_elevation = tool.Ifc.get_object(tool.Root.get_default_container()).location.z projection_point = [] - if self.use_default_container: + if self.tool_state.use_default_container: # When a point is above the plane it projects the point # to the plane and creates a line if snap_prop.snap_type != "Plane" and snap_prop.z != 0: @@ -520,10 +520,10 @@ class PolylineDecorator: self.line_shader.uniform_float("lineWidth", 0.75) self.draw_batch("LINES", [self.axis_start, self.axis_end], decorator_color_unselected, [(0, 1)]) - try: - self.draw_batch("TRIS", self.axis_rectangle, (1, 1, 1, 0.1), [(0, 1, 3), (0, 2, 3)]) - except: - pass + # try: + # self.draw_batch("TRIS", self.axis_rectangle, (1, 1, 1, 0.1), [(0, 1, 3), (0, 2, 3)]) + # except: + # pass # Area highlight # if "AREA" in list(self.input_panel.keys()): # TODO Change to input_ui diff --git a/src/bonsai/bonsai/bim/module/model/wall.py b/src/bonsai/bonsai/bim/module/model/wall.py index cb273177e5..50cd35de0f 100644 --- a/src/bonsai/bonsai/bim/module/model/wall.py +++ b/src/bonsai/bonsai/bim/module/model/wall.py @@ -325,6 +325,7 @@ class DrawPolylineWall(bpy.types.Operator): self.is_input_on = False self.input_options = ["D", "A", "X", "Y"] self.input_type = None + self.input_type = None self.input_value_xy = [None, None] self.input_ui = tool.Polyline.create_input_ui() self.is_typing = False @@ -337,6 +338,7 @@ class DrawPolylineWall(bpy.types.Operator): X Y: Axis Shift: Lock axis """ + self.tool_state = tool.Polyline.create_tool_state() def recalculate_inputs(self, context): if self.number_input: @@ -347,10 +349,10 @@ class DrawPolylineWall(bpy.types.Operator): return is_valid else: if self.input_type in {"X", "Y"}: - tool.Polyline.calculate_distance_and_angle(context, self.is_input_on, self.input_ui) + tool.Polyline.calculate_distance_and_angle(context, self.input_ui, self.tool_state) elif self.input_type in {"D", "A"}: - tool.Polyline.calculate_x_y_and_z(context, self.input_ui) - tool.Polyline.calculate_distance_and_angle(context, self.is_input_on, self.input_ui) + tool.Polyline.calculate_x_y_and_z(context, self.input_ui, self.tool_state) + tool.Polyline.calculate_distance_and_angle(context, self.input_ui, self.tool_state) else: self.input_ui.set_value(self.input_type, self.number_output) tool.Blender.update_viewport() @@ -388,8 +390,10 @@ class DrawPolylineWall(bpy.types.Operator): if event.type == "MOUSEMOVE" or event.type == "INBETWEEN_MOUSEMOVE": self.mousemove_count += 1 self.is_input_on = False + self.tool_state.is_input_on = False self.input_type = None - PolylineDecorator.set_input_ui(self.input_ui, self.input_type) + self.tool_state.input_type = None + PolylineDecorator.update(event, self.tool_state, self.input_ui) tool.Snap.clear_snapping_ref() tool.Blender.update_viewport() else: @@ -401,10 +405,9 @@ class DrawPolylineWall(bpy.types.Operator): self.objs_2d_bbox.append(tool.Raycast.get_on_screen_2d_bounding_boxes(context, obj)) if self.mousemove_count > 3: - detected_snaps = tool.Snap.detect_snapping_points(context, event, self.objs_2d_bbox) + detected_snaps = tool.Snap.detect_snapping_points(context, event, self.objs_2d_bbox, self.tool_state) self.snapping_points = tool.Snap.select_snapping_points(context, event, detected_snaps) - PolylineDecorator.set_mouse_position(event) - tool.Polyline.calculate_distance_and_angle(context, self.is_input_on, self.input_ui) + tool.Polyline.calculate_distance_and_angle(context, self.input_ui, self.tool_state) tool.Blender.update_viewport() return {"RUNNING_MODAL"} @@ -426,7 +429,7 @@ class DrawPolylineWall(bpy.types.Operator): if event.value == "PRESS" and event.type == "C": tool.Snap.close_polyline() - PolylineDecorator.set_input_ui(self.input_ui, self.input_type) + PolylineDecorator.update(event, self.tool_state, self.input_ui) tool.Blender.update_viewport() if self.is_input_on and event.value == "PRESS" and event.type == "TAB": @@ -434,39 +437,48 @@ class DrawPolylineWall(bpy.types.Operator): index = self.input_options.index(self.input_type) size = len(self.input_options) self.input_type = self.input_options[((index + 1) % size)] + self.tool_state.input_type = self.input_options[((index + 1) % size)] self.is_typing = False self.number_input = self.input_ui.get_formatted_value(self.input_type) self.number_input = list(self.number_input) self.number_output = "".join(self.number_input) self.input_ui.set_value(self.input_type, self.number_output) - PolylineDecorator.set_input_ui(self.input_ui, self.input_type) + PolylineDecorator.update(event, self.tool_state, self.input_ui) tool.Blender.update_viewport() if not self.is_input_on and event.value == "RELEASE" and event.type == "TAB": self.recalculate_inputs(context) self.is_input_on = True + self.tool_state.is_input_on = True self.input_type = "D" + self.tool_state.input_type = "D" self.is_typing = False self.number_input = self.input_ui.get_formatted_value(self.input_type) self.number_input = list(self.number_input) self.number_output = "".join(self.number_input) self.input_ui.set_value(self.input_type, self.number_output) - PolylineDecorator.set_input_ui(self.input_ui, self.input_type) + PolylineDecorator.update(event, self.tool_state, self.input_ui) tool.Blender.update_viewport() if not self.is_input_on and event.ascii in self.number_options: self.recalculate_inputs(context) self.is_input_on = True + self.tool_state.is_input_on = True self.input_type = "D" - PolylineDecorator.set_input_ui(self.input_ui, self.input_type) + self.tool_state.input_type = "D" + # PolylineDecorator.set_input_ui(self.input_ui) + # PolylineDecorator.set_tool_state(self.tool_state) + PolylineDecorator.update(event, self.tool_state, self.input_ui) tool.Blender.update_viewport() if event.value == "RELEASE" and event.type in {"D", "A"}: self.recalculate_inputs(context) self.is_input_on = True + self.tool_state.is_input_on = True self.input_type = event.type + self.tool_state.input_type = event.type self.input_ui.set_value(self.input_type, "") - PolylineDecorator.set_input_ui(self.input_ui, self.input_type) + PolylineDecorator.update(event, self.tool_state, self.input_ui) tool.Blender.update_viewport() if self.input_type in self.input_options: @@ -493,7 +505,7 @@ class DrawPolylineWall(bpy.types.Operator): self.is_typing = True self.number_output = "".join(self.number_input) self.input_ui.set_value(self.input_type, self.number_output) - PolylineDecorator.set_input_ui(self.input_ui, self.input_type) + PolylineDecorator.update(event, self.tool_state, self.input_ui) tool.Blender.update_viewport() if not self.is_input_on and event.value == "RELEASE" and event.type in {"RET", "NUMPAD_ENTER", "RIGHTMOUSE"}: @@ -508,16 +520,18 @@ class DrawPolylineWall(bpy.types.Operator): if is_valid: tool.Snap.insert_polyline_point(self.input_ui) self.is_input_on = False + self.tool_state.is_input_on = False self.input_type = None + self.tool_state.input_type = None self.number_input = [] self.number_output = "" - PolylineDecorator.set_input_ui(self.input_ui, self.input_type) + PolylineDecorator.update(event, self.tool_state, self.input_ui) tool.Blender.update_viewport() if event.value == "PRESS" and event.type == "M": self.snapping_points = tool.Snap.modify_snapping_point_selection(self.snapping_points) - PolylineDecorator.set_mouse_position(event) - tool.Polyline.calculate_distance_and_angle(context, self.is_input_on, self.input_ui) + tool.Polyline.calculate_distance_and_angle(context, self.input_ui, self.tool_state) + PolylineDecorator.update(event, self.tool_state, self.input_ui) tool.Blender.update_viewport() if event.type in {"MIDDLEMOUSE", "WHEELUPMOUSE", "WHEELDOWNMOUSE"}: @@ -527,8 +541,10 @@ class DrawPolylineWall(bpy.types.Operator): if event.value == "RELEASE" and event.type in {"ESC"}: self.recalculate_inputs(context) self.is_input_on = False + self.tool_state.is_input_on = False self.input_type = None - PolylineDecorator.set_input_ui(self.input_ui, self.input_type) + self.tool_state.input_type = None + PolylineDecorator.update(event, self.tool_state, self.input_ui) tool.Blender.update_viewport() else: if event.value == "RELEASE" and event.type in {"ESC"}: @@ -544,21 +560,19 @@ class DrawPolylineWall(bpy.types.Operator): if context.space_data.type == "VIEW_3D": PolylineDecorator.install(context) tool.Snap.clear_snapping_point() - tool.Snap.set_use_default_container(True) - tool.Snap.clear_snap_angle() - PolylineDecorator.set_use_default_container(True) - tool.Polyline.set_use_default_container(True) + + self.tool_state.use_default_container = True + tool.Snap.set_tool_state(self.tool_state) + tool.Snap.set_snap_axis_method(None) tool.Snap.set_snap_plane_method("XY") - PolylineDecorator.set_instructions(self.instructions) - PolylineDecorator.set_input_ui(self.input_ui, self.input_type) + PolylineDecorator.update(event, self.tool_state, self.input_ui) self.visible_objs = tool.Raycast.get_visible_objects(context) for obj in self.visible_objs: self.objs_2d_bbox.append(tool.Raycast.get_on_screen_2d_bounding_boxes(context, obj)) - detected_snaps = tool.Snap.detect_snapping_points(context, event, self.objs_2d_bbox) + detected_snaps = tool.Snap.detect_snapping_points(context, event, self.objs_2d_bbox, self.tool_state) self.snapping_points = tool.Snap.select_snapping_points(context, event, detected_snaps) - PolylineDecorator.set_mouse_position(event) - tool.Polyline.calculate_distance_and_angle(context, self.is_input_on, self.input_ui) + tool.Polyline.calculate_distance_and_angle(context, self.input_ui, self.tool_state) tool.Blender.update_viewport() context.window_manager.modal_handler_add(self) diff --git a/src/bonsai/bonsai/tool/polyline.py b/src/bonsai/bonsai/tool/polyline.py index f107d7ece5..5b43a9fd7e 100644 --- a/src/bonsai/bonsai/tool/polyline.py +++ b/src/bonsai/bonsai/tool/polyline.py @@ -83,6 +83,26 @@ class PolylineUI: return format_distance(value * factor, precision=precision, suppress_zero_inches=True, in_unit_length=True) +@dataclass +class ToolState: + use_default_container: bool = None + snap_angle: float = None + is_input_on: bool = None + # angle_axis_start: Vector + # angle_axis_end: Vector + axis_method: str = None + plane_method: str = None + instructions: str = """TAB: Cycle Input + M: Modify Snap Point + C: Close + Backspace: Remove + X Y: Axis + Shift: Lock axis +""" + snap_info: str = None + # input_state: str = None | "Select" | "Edit" + input_type: str = None + class Polyline(bonsai.core.tool.Polyline): @@ -91,37 +111,11 @@ class Polyline(bonsai.core.tool.Polyline): return PolylineUI(init_z=init_z, init_area=init_area) @classmethod - def set_mouse_position(cls, event): - cls.mouse_pos = event.mouse_region_x, event.mouse_region_y + def create_tool_state(cls): + return ToolState() @classmethod - def set_angle_axis_line(cls, start, end): - cls.axis_start = start - cls.axis_end = end - - @classmethod - def set_axis_rectangle(cls, corners): - cls.axis_rectangle = [*corners] - - @classmethod - def set_use_default_container(cls, value=False): - cls.use_default_container = value - - @classmethod - def set_plane(cls, plane_origin, plane_normal): - cls.plane_origin = plane_origin - cls.plane_normal = plane_normal - - @classmethod - def set_instructions(cls, instructions): - cls.instructions = instructions - - @classmethod - def set_snap_info(cls, snap_info): - cls.snap_info = snap_info - - @classmethod - def calculate_distance_and_angle(cls, context, is_input_on, input_ui): + def calculate_distance_and_angle(cls, context, input_ui, tool_state): try: polyline_data = context.scene.BIMModelProperties.polyline_point @@ -138,8 +132,8 @@ class Polyline(bonsai.core.tool.Polyline): else: last_point = Vector((0, 0, 0)) - if is_input_on: - if cls.use_default_container: + if tool_state.is_input_on: + if tool_state.use_default_container: snap_vector = Vector( (input_ui.get_number_value("X"), input_ui.get_number_value("Y"), default_container_elevation) ) @@ -148,7 +142,7 @@ class Polyline(bonsai.core.tool.Polyline): (input_ui.get_number_value("X"), input_ui.get_number_value("Y"), input_ui.get_number_value("Z")) ) else: - if cls.use_default_container: + if tool_state.use_default_container: snap_vector = Vector((snap_prop.x, snap_prop.y, default_container_elevation)) else: snap_vector = Vector((snap_prop.x, snap_prop.y, snap_prop.z)) @@ -229,7 +223,7 @@ class Polyline(bonsai.core.tool.Polyline): return @classmethod - def calculate_x_y_and_z(cls, context, input_ui): + def calculate_x_y_and_z(cls, context, input_ui, tool_state): try: polyline_data = context.scene.BIMModelProperties.polyline_point default_container_elevation = tool.Ifc.get_object(tool.Root.get_default_container()).location.z @@ -242,7 +236,7 @@ class Polyline(bonsai.core.tool.Polyline): snap_prop = context.scene.BIMModelProperties.snap_mouse_point[0] snap_vector = Vector((snap_prop.x, snap_prop.y, snap_prop.z)) - if cls.use_default_container: + if tool_state.use_default_container: snap_vector = Vector((snap_prop.x, snap_prop.y, default_container_elevation)) else: snap_vector = Vector((snap_prop.x, snap_prop.y, snap_prop.z)) diff --git a/src/bonsai/bonsai/tool/snap.py b/src/bonsai/bonsai/tool/snap.py index f0668f2320..3164fccabc 100644 --- a/src/bonsai/bonsai/tool/snap.py +++ b/src/bonsai/bonsai/tool/snap.py @@ -27,15 +27,14 @@ from lark import Lark, Transformer class Snap(bonsai.core.tool.Snap): - mouse_pos = None snap_angle = None - use_default_container = False + tool_state = None snap_plane_method = None snap_axis_method = None @classmethod - def set_use_default_container(cls, value=True): - cls.use_default_container = value + def set_tool_state(cls, tool_state): + cls.tool_state = tool_state @classmethod def set_snap_plane_method(cls, value=True): @@ -145,7 +144,7 @@ class Snap(bonsai.core.tool.Snap): a = input_ui.get_formatted_value("A") snap_vertex = bpy.context.scene.BIMModelProperties.snap_mouse_point[0] - if cls.use_default_container: + if cls.tool_state.use_default_container: z = tool.Ifc.get_object(tool.Root.get_default_container()).location.z if x is None and y is None: @@ -289,11 +288,10 @@ class Snap(bonsai.core.tool.Snap): return sorted_intersections[0], "Mix" @classmethod - def detect_snapping_points(cls, context, event, objs_2d_bbox): - region = context.region + def detect_snapping_points(cls, context, event, objs_2d_bbox, tool_state): rv3d = context.region_data space = context.space_data - cls.mouse_pos = event.mouse_region_x, event.mouse_region_y + mouse_pos = event.mouse_region_x, event.mouse_region_y detected_snaps = [] snap_threshold = 0.3 @@ -322,7 +320,7 @@ class Snap(bonsai.core.tool.Snap): plane_normal = view_direction.normalized() if cls.snap_plane_method == "XY" or (not cls.snap_plane_method and cls.snap_axis_method in {"X", "Y"}): - if cls.use_default_container: + if cls.tool_state.use_default_container: plane_origin = Vector((0, 0, elevation)) elif not last_polyline_point: plane_origin = Vector((0, 0, 0)) @@ -342,7 +340,7 @@ class Snap(bonsai.core.tool.Snap): return plane_origin, plane_normal - def cast_rays_and_get_best_object(objs_to_raycast): + def cast_rays_and_get_best_object(objs_to_raycast, mouse_pos): best_length_squared = 1.0 best_obj = None best_hit = None @@ -352,13 +350,13 @@ class Snap(bonsai.core.tool.Snap): hit, normal, face_index = tool.Raycast.obj_ray_cast(context, event, obj) if hit is None: # Tried original mouse position. Now it will try the offsets. - original_mouse_pos = cls.mouse_pos + original_mouse_pos = mouse_pos for value in mouse_offset: - cls.mouse_pos = tuple(x + y for x, y in zip(original_mouse_pos, value)) - hit, normal, face_index = tool.Raycast.obj_ray_cast(context, event, obj, cls.mouse_pos) + mouse_pos = tuple(x + y for x, y in zip(original_mouse_pos, value)) + hit, normal, face_index = tool.Raycast.obj_ray_cast(context, event, obj, mouse_pos) if hit: break - cls.mouse_pos = original_mouse_pos + mouse_pos = original_mouse_pos if hit is not None: hit_world = obj.original.matrix_world @ hit @@ -380,14 +378,14 @@ class Snap(bonsai.core.tool.Snap): objs_to_raycast = [] for obj, bbox_2d in objs_2d_bbox: if obj.type == "MESH" and bbox_2d: - if tool.Raycast.intersect_mouse_2d_bounding_box(cls.mouse_pos, bbox_2d, offset): + if tool.Raycast.intersect_mouse_2d_bounding_box(mouse_pos, bbox_2d, offset): if space.local_view: if obj.local_view_get(context.space_data): objs_to_raycast.append(obj) else: objs_to_raycast.append(obj) # Obj - snap_obj, hit, face_index = cast_rays_and_get_best_object(objs_to_raycast) + snap_obj, hit, face_index = cast_rays_and_get_best_object(objs_to_raycast, mouse_pos) if hit is not None: detected_snaps.append({"Object": (snap_obj, hit, face_index)}) @@ -423,29 +421,29 @@ class Snap(bonsai.core.tool.Snap): rot_intersection = None if not cls.snap_plane_method: if cls.snap_axis_method == "X": - cls.snap_angle = 180 + tool_state.snap_angle = 180 if cls.snap_axis_method == "Y": - cls.snap_angle = 90 + tool_state.snap_angle = 90 if cls.snap_axis_method == "Z": - cls.snap_angle = 90 + tool_state.snap_angle = 90 if cls.snap_axis_method: # Doesn't update snap_angle so that it keeps in the same axis - rot_intersection, _, axis_start, axis_end = cls.snap_on_axis(intersection, cls.snap_angle) + rot_intersection, _, axis_start, axis_end = cls.snap_on_axis(intersection, tool_state.snap_angle) if cls.snap_plane_method: if cls.snap_plane_method in {"XY", "XZ"} and cls.snap_axis_method == "X": - cls.snap_angle = 180 + tool_state.snap_angle = 180 if cls.snap_plane_method in {"XY", "YZ"} and cls.snap_axis_method == "Y": - cls.snap_angle = 90 + tool_state.snap_angle = 90 if cls.snap_plane_method in {"YZ"} and cls.snap_axis_method == "Z": - cls.snap_angle = 180 + tool_state.snap_angle = 180 if cls.snap_plane_method in {"XZ"} and cls.snap_axis_method == "Z": - cls.snap_angle = 90 + tool_state.snap_angle = 90 if event.shift or cls.snap_axis_method: # Doesn't update snap_angle so that it keeps in the same axis - rot_intersection, _, axis_start, axis_end = cls.snap_on_axis(intersection, cls.snap_angle) + rot_intersection, _, axis_start, axis_end = cls.snap_on_axis(intersection, tool_state.snap_angle) else: - rot_intersection, cls.snap_angle, axis_start, axis_end = cls.snap_on_axis(intersection, None) + rot_intersection, tool_state.snap_angle, axis_start, axis_end = cls.snap_on_axis(intersection, None) if rot_intersection: detected_snaps.append({"Axis": (rot_intersection, axis_start, axis_end)}) From 55df9a42e65223657726c28246fd65381c62325f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bruno=20Perdig=C3=A3o?= Date: Sat, 7 Sep 2024 17:30:18 -0300 Subject: [PATCH 25/56] WIP - More refactor --- .../bonsai/bim/module/model/decorator.py | 8 +- src/bonsai/bonsai/bim/module/model/wall.py | 75 ++++++++--------- src/bonsai/bonsai/tool/polyline.py | 2 +- src/bonsai/bonsai/tool/snap.py | 80 +++++++------------ 4 files changed, 75 insertions(+), 90 deletions(-) diff --git a/src/bonsai/bonsai/bim/module/model/decorator.py b/src/bonsai/bonsai/bim/module/model/decorator.py index 790cd9740b..c330602467 100644 --- a/src/bonsai/bonsai/bim/module/model/decorator.py +++ b/src/bonsai/bonsai/bim/module/model/decorator.py @@ -334,10 +334,13 @@ class PolylineDecorator: cls.is_installed = False @classmethod - def update(cls, event, tool_state, input_ui): + def update(cls, event, tool_state, input_ui, snapping_point): cls.event = event cls.tool_state = tool_state cls.input_ui = input_ui + cls.snap_info = f"""Snap: {snapping_point[1]} + Axis:{tool_state.axis_method} + Plane: {tool_state.plane_method}""" @classmethod def set_input_ui(cls, input_ui): @@ -356,9 +359,6 @@ class PolylineDecorator: def set_tool_state(cls, tool_state): cls.tool_state = tool_state - @classmethod - def set_snap_info(cls, snap_info): - cls.snap_info = snap_info def draw_batch(self, shader_type, content_pos, color, indices=None): diff --git a/src/bonsai/bonsai/bim/module/model/wall.py b/src/bonsai/bonsai/bim/module/model/wall.py index 50cd35de0f..3b6a1d2ee0 100644 --- a/src/bonsai/bonsai/bim/module/model/wall.py +++ b/src/bonsai/bonsai/bim/module/model/wall.py @@ -322,7 +322,6 @@ class DrawPolylineWall(bpy.types.Operator): self.number_input = [] self.number_output = "" self.number_is_negative = False - self.is_input_on = False self.input_options = ["D", "A", "X", "Y"] self.input_type = None self.input_type = None @@ -386,14 +385,14 @@ class DrawPolylineWall(bpy.types.Operator): def modal(self, context, event): - if not self.is_input_on: + if not self.tool_state.is_input_on: if event.type == "MOUSEMOVE" or event.type == "INBETWEEN_MOUSEMOVE": self.mousemove_count += 1 - self.is_input_on = False + self.tool_state.mode = "Mouse" self.tool_state.is_input_on = False self.input_type = None self.tool_state.input_type = None - PolylineDecorator.update(event, self.tool_state, self.input_ui) + PolylineDecorator.update(event, self.tool_state, self.input_ui, self.snapping_points[0]) tool.Snap.clear_snapping_ref() tool.Blender.update_viewport() else: @@ -406,7 +405,8 @@ class DrawPolylineWall(bpy.types.Operator): if self.mousemove_count > 3: detected_snaps = tool.Snap.detect_snapping_points(context, event, self.objs_2d_bbox, self.tool_state) - self.snapping_points = tool.Snap.select_snapping_points(context, event, detected_snaps) + self.snapping_points = tool.Snap.select_snapping_points(context, event, self.tool_state, detected_snaps) + print(self.snapping_points) tool.Polyline.calculate_distance_and_angle(context, self.input_ui, self.tool_state) tool.Blender.update_viewport() return {"RUNNING_MODAL"} @@ -420,35 +420,38 @@ class DrawPolylineWall(bpy.types.Operator): tool.Blender.update_viewport() if event.value == "PRESS" and event.type == "X": - tool.Snap.set_snap_axis_method("X") + self.tool_state.axis_method = "X" if self.tool_state.axis_method != event.type else None + PolylineDecorator.update(event, self.tool_state, self.input_ui, self.snapping_points[0]) tool.Blender.update_viewport() if event.value == "PRESS" and event.type == "Y": - tool.Snap.set_snap_axis_method("Y") + self.tool_state.axis_method = "Y" if self.tool_state.axis_method != event.type else None + PolylineDecorator.update(event, self.tool_state, self.input_ui, self.snapping_points[0]) tool.Blender.update_viewport() if event.value == "PRESS" and event.type == "C": tool.Snap.close_polyline() - PolylineDecorator.update(event, self.tool_state, self.input_ui) + PolylineDecorator.update(event, self.tool_state, self.input_ui, self.snapping_points[0]) tool.Blender.update_viewport() - if self.is_input_on and event.value == "PRESS" and event.type == "TAB": + if self.tool_state.is_input_on and event.value == "PRESS" and event.type == "TAB": self.recalculate_inputs(context) index = self.input_options.index(self.input_type) size = len(self.input_options) self.input_type = self.input_options[((index + 1) % size)] self.tool_state.input_type = self.input_options[((index + 1) % size)] + self.tool_state.mode = "Select" self.is_typing = False self.number_input = self.input_ui.get_formatted_value(self.input_type) self.number_input = list(self.number_input) self.number_output = "".join(self.number_input) self.input_ui.set_value(self.input_type, self.number_output) - PolylineDecorator.update(event, self.tool_state, self.input_ui) + PolylineDecorator.update(event, self.tool_state, self.input_ui, self.snapping_points[0]) tool.Blender.update_viewport() - if not self.is_input_on and event.value == "RELEASE" and event.type == "TAB": + if not self.tool_state.is_input_on and event.value == "RELEASE" and event.type == "TAB": self.recalculate_inputs(context) - self.is_input_on = True + self.tool_state.mode = "Select" self.tool_state.is_input_on = True self.input_type = "D" self.tool_state.input_type = "D" @@ -457,33 +460,31 @@ class DrawPolylineWall(bpy.types.Operator): self.number_input = list(self.number_input) self.number_output = "".join(self.number_input) self.input_ui.set_value(self.input_type, self.number_output) - PolylineDecorator.update(event, self.tool_state, self.input_ui) + PolylineDecorator.update(event, self.tool_state, self.input_ui, self.snapping_points[0]) tool.Blender.update_viewport() - if not self.is_input_on and event.ascii in self.number_options: + if not self.tool_state.is_input_on and event.ascii in self.number_options: self.recalculate_inputs(context) - self.is_input_on = True + self.tool_state.mode = "Edit" self.tool_state.is_input_on = True self.input_type = "D" self.tool_state.input_type = "D" - # PolylineDecorator.set_input_ui(self.input_ui) - # PolylineDecorator.set_tool_state(self.tool_state) - PolylineDecorator.update(event, self.tool_state, self.input_ui) + PolylineDecorator.update(event, self.tool_state, self.input_ui, self.snapping_points[0]) tool.Blender.update_viewport() if event.value == "RELEASE" and event.type in {"D", "A"}: self.recalculate_inputs(context) - self.is_input_on = True + self.tool_state.mode = "Edit" self.tool_state.is_input_on = True self.input_type = event.type self.tool_state.input_type = event.type self.input_ui.set_value(self.input_type, "") - PolylineDecorator.update(event, self.tool_state, self.input_ui) + PolylineDecorator.update(event, self.tool_state, self.input_ui, self.snapping_points[0]) tool.Blender.update_viewport() if self.input_type in self.input_options: if (event.ascii in self.number_options) or (event.value == "RELEASE" and event.type == "BACK_SPACE"): - if not self.is_typing and not (event.ascii == "=" or event.type == "BACK_SPACE"): + if not self.tool_state.mode == "Edit" and not (event.ascii == "=" or event.type == "BACK_SPACE"): self.number_input = [] if event.type == "BACK_SPACE": @@ -502,58 +503,60 @@ class DrawPolylineWall(bpy.types.Operator): if not self.number_input: self.number_output = "0" + self.tool_state.mode = "Edit" self.is_typing = True self.number_output = "".join(self.number_input) self.input_ui.set_value(self.input_type, self.number_output) - PolylineDecorator.update(event, self.tool_state, self.input_ui) + PolylineDecorator.update(event, self.tool_state, self.input_ui, self.snapping_points[0]) tool.Blender.update_viewport() - if not self.is_input_on and event.value == "RELEASE" and event.type in {"RET", "NUMPAD_ENTER", "RIGHTMOUSE"}: + if not self.tool_state.is_input_on and event.value == "RELEASE" and event.type in {"RET", "NUMPAD_ENTER", "RIGHTMOUSE"}: self.create_walls_from_polyline(context) PolylineDecorator.uninstall() tool.Snap.clear_polyline() tool.Blender.update_viewport() return {"FINISHED"} - if self.is_input_on and event.value == "RELEASE" and event.type in {"RET", "NUMPAD_ENTER", "RIGHTMOUSE"}: + if self.tool_state.is_input_on and event.value == "RELEASE" and event.type in {"RET", "NUMPAD_ENTER", "RIGHTMOUSE"}: is_valid = self.recalculate_inputs(context) if is_valid: tool.Snap.insert_polyline_point(self.input_ui) - self.is_input_on = False + self.tool_state.mode = "Mouse" self.tool_state.is_input_on = False self.input_type = None self.tool_state.input_type = None self.number_input = [] self.number_output = "" - PolylineDecorator.update(event, self.tool_state, self.input_ui) + PolylineDecorator.update(event, self.tool_state, self.input_ui, self.snapping_points[0]) tool.Blender.update_viewport() if event.value == "PRESS" and event.type == "M": self.snapping_points = tool.Snap.modify_snapping_point_selection(self.snapping_points) tool.Polyline.calculate_distance_and_angle(context, self.input_ui, self.tool_state) - PolylineDecorator.update(event, self.tool_state, self.input_ui) + PolylineDecorator.update(event, self.tool_state, self.input_ui, self.snapping_points[0]) tool.Blender.update_viewport() if event.type in {"MIDDLEMOUSE", "WHEELUPMOUSE", "WHEELDOWNMOUSE"}: return {"PASS_THROUGH"} - if self.is_input_on: + if self.tool_state.is_input_on: if event.value == "RELEASE" and event.type in {"ESC"}: self.recalculate_inputs(context) - self.is_input_on = False + self.tool_state.mode = "Mouse" self.tool_state.is_input_on = False self.input_type = None self.tool_state.input_type = None - PolylineDecorator.update(event, self.tool_state, self.input_ui) + PolylineDecorator.update(event, self.tool_state, self.input_ui, self.snapping_points[0]) tool.Blender.update_viewport() else: if event.value == "RELEASE" and event.type in {"ESC"}: - tool.Snap.set_snap_axis_method(None) + self.tool_state.axis_method = None PolylineDecorator.uninstall() tool.Snap.clear_polyline() tool.Blender.update_viewport() return {"CANCELLED"} + print(">>> ", self.tool_state.mode) return {"RUNNING_MODAL"} def invoke(self, context, event): @@ -562,18 +565,18 @@ class DrawPolylineWall(bpy.types.Operator): tool.Snap.clear_snapping_point() self.tool_state.use_default_container = True + self.tool_state.axis_method = None + self.tool_state.plane_method = "XY" + self.tool_state.mode = "Mouse" tool.Snap.set_tool_state(self.tool_state) - - tool.Snap.set_snap_axis_method(None) - tool.Snap.set_snap_plane_method("XY") - PolylineDecorator.update(event, self.tool_state, self.input_ui) self.visible_objs = tool.Raycast.get_visible_objects(context) for obj in self.visible_objs: self.objs_2d_bbox.append(tool.Raycast.get_on_screen_2d_bounding_boxes(context, obj)) detected_snaps = tool.Snap.detect_snapping_points(context, event, self.objs_2d_bbox, self.tool_state) - self.snapping_points = tool.Snap.select_snapping_points(context, event, detected_snaps) + self.snapping_points = tool.Snap.select_snapping_points(context, event, self.tool_state, detected_snaps) tool.Polyline.calculate_distance_and_angle(context, self.input_ui, self.tool_state) + PolylineDecorator.update(event, self.tool_state, self.input_ui, self.snapping_points[0]) tool.Blender.update_viewport() context.window_manager.modal_handler_add(self) return {"RUNNING_MODAL"} diff --git a/src/bonsai/bonsai/tool/polyline.py b/src/bonsai/bonsai/tool/polyline.py index 5b43a9fd7e..3b3feeaf7b 100644 --- a/src/bonsai/bonsai/tool/polyline.py +++ b/src/bonsai/bonsai/tool/polyline.py @@ -100,7 +100,7 @@ class ToolState: Shift: Lock axis """ snap_info: str = None - # input_state: str = None | "Select" | "Edit" + mode: str = None input_type: str = None diff --git a/src/bonsai/bonsai/tool/snap.py b/src/bonsai/bonsai/tool/snap.py index 3164fccabc..137bad0b52 100644 --- a/src/bonsai/bonsai/tool/snap.py +++ b/src/bonsai/bonsai/tool/snap.py @@ -27,10 +27,8 @@ from lark import Lark, Transformer class Snap(bonsai.core.tool.Snap): - snap_angle = None tool_state = None snap_plane_method = None - snap_axis_method = None @classmethod def set_tool_state(cls, tool_state): @@ -40,10 +38,6 @@ class Snap(bonsai.core.tool.Snap): def set_snap_plane_method(cls, value=True): cls.snap_plane_method = value - @classmethod - def clear_snap_angle(cls): - cls.snap_angle = None - @classmethod def cycle_snap_plane_method(cls, value=True): if cls.snap_plane_method == value: @@ -51,13 +45,6 @@ class Snap(bonsai.core.tool.Snap): return cls.snap_plane_method = value - @classmethod - def set_snap_axis_method(cls, value=True): - if cls.snap_axis_method == value: - cls.snap_axis_method = None - return - cls.snap_axis_method = value - @classmethod def get_snap_points_on_raycasted_face(cls, context, event, obj, face_index): matrix = obj.matrix_world.copy() @@ -102,11 +89,6 @@ class Snap(bonsai.core.tool.Snap): except: snap_vertex = bpy.context.scene.BIMModelProperties.snap_mouse_point.add() - info = f"""Snap: {snap_type} - Axis:{cls.snap_axis_method} - Plane:{cls.snap_plane_method} -""" - PolylineDecorator.set_snap_info(info) snap_vertex.x = snap_point[0] snap_vertex.y = snap_point[1] snap_vertex.z = snap_point[2] @@ -194,12 +176,12 @@ class Snap(bonsai.core.tool.Snap): polyline_measurement.remove(len(polyline_measurement) - 1) @classmethod - def snap_on_axis(cls, intersection, lock_axis=None): + def snap_on_axis(cls, intersection, tool_state, lock_angle=False): def create_axis_line_data(rot_mat, origin): length = 1000 direction = Vector((1, 0, 0)) - if cls.snap_plane_method == "YZ" or (not cls.snap_plane_method and cls.snap_axis_method == "Z"): + if tool_state.plane_method == "YZ" or (not tool_state.plane_method and tool_state.axis_method == "Z"): direction = Vector((0, 0, 1)) rot_dir = rot_mat.inverted() @ direction start = origin + rot_dir * length @@ -210,13 +192,13 @@ class Snap(bonsai.core.tool.Snap): def create_axis_rectangle_data(origin): size = 0.5 direction = Vector((1, 0, 0)) - if cls.snap_plane_method == "YZ": + if tool_state.plane_method == "YZ": direction = Vector((0, 0, 1)) rot_mat = Matrix.Rotation(math.radians(360), 3, pivot_axis) rot_dir = rot_mat.inverted() @ direction v1 = origin + rot_dir * 0 v2 = origin + rot_dir * size - if cls.snap_plane_method == "XY": + if tool_state.plane_method == "XY": angle = 270 else: angle = 90 @@ -238,17 +220,17 @@ class Snap(bonsai.core.tool.Snap): # Translates intersection point based on last_point translated_intersection = intersection - last_point snap_axis = [] - if not lock_axis: + if not tool_state.snap_angle: for i in range(1, 25): angle = 15 * i snap_axis.append(angle) else: - snap_axis = [lock_axis] + snap_axis = [tool_state.snap_angle] pivot_axis = "Z" - if cls.snap_plane_method == "XZ": + if tool_state.plane_method == "XZ": pivot_axis = "Y" - if cls.snap_plane_method == "YZ": + if tool_state.plane_method == "YZ": pivot_axis = "X" for axis in snap_axis: @@ -256,10 +238,10 @@ class Snap(bonsai.core.tool.Snap): start, end = create_axis_line_data(rot_mat, last_point) rot_intersection = rot_mat @ translated_intersection proximity = rot_intersection.y - if cls.snap_plane_method == "XZ": + if tool_state.plane_method == "XZ": proximity = rot_intersection.z PolylineDecorator.set_angle_axis_line(start, end) - if lock_axis: + if lock_angle: is_on_rot_axis = True else: is_on_rot_axis = abs(proximity) <= 0.15 @@ -267,7 +249,7 @@ class Snap(bonsai.core.tool.Snap): if is_on_rot_axis: # Snap to axis rot_intersection = Vector((rot_intersection.x, 0, rot_intersection.z)) - if cls.snap_plane_method == "XZ": + if tool_state.plane_method == "XZ": rot_intersection = Vector((rot_intersection.x, rot_intersection.y, 0)) # Convert it back snap_intersection = rot_mat.inverted() @ rot_intersection + last_point @@ -313,13 +295,13 @@ class Snap(bonsai.core.tool.Snap): plane_origin = Vector((0, 0, 0)) plane_normal = Vector((0, 0, 1)) - if not cls.snap_plane_method: + if not tool_state.plane_method: camera_rotation = rv3d.view_rotation plane_origin = Vector((0, 0, 0)) view_direction = Vector((0, 0, -1)) @ camera_rotation.to_matrix().transposed() plane_normal = view_direction.normalized() - if cls.snap_plane_method == "XY" or (not cls.snap_plane_method and cls.snap_axis_method in {"X", "Y"}): + if tool_state.plane_method == "XY" or (not tool_state.plane_method and tool_state.axis_method in {"X", "Y"}): if cls.tool_state.use_default_container: plane_origin = Vector((0, 0, elevation)) elif not last_polyline_point: @@ -328,12 +310,12 @@ class Snap(bonsai.core.tool.Snap): plane_origin = Vector((last_polyline_point.x, last_polyline_point.y, last_polyline_point.z)) plane_normal = Vector((0, 0, 1)) - elif cls.snap_plane_method == "XZ" or (not cls.snap_plane_method and cls.snap_axis_method == "Z"): + elif tool_state.plane_method == "XZ" or (not tool_state.plane_method and tool_state.axis_method == "Z"): if last_polyline_point: plane_origin = Vector((last_polyline_point.x, last_polyline_point.y, last_polyline_point.z)) plane_normal = Vector((0, 1, 0)) - elif cls.snap_plane_method == "YZ": + elif tool_state.plane_method == "YZ": if last_polyline_point: plane_origin = Vector((last_polyline_point.x, last_polyline_point.y, last_polyline_point.z)) plane_normal = Vector((1, 0, 0)) @@ -419,31 +401,31 @@ class Snap(bonsai.core.tool.Snap): # TODO It only work for XY plane. Make it work also for None plane_method rot_intersection = None - if not cls.snap_plane_method: - if cls.snap_axis_method == "X": + if not tool_state.plane_method: + if tool_state.axis_method == "X": tool_state.snap_angle = 180 - if cls.snap_axis_method == "Y": + if tool_state.axis_method == "Y": tool_state.snap_angle = 90 - if cls.snap_axis_method == "Z": + if tool_state.axis_method == "Z": tool_state.snap_angle = 90 - if cls.snap_axis_method: + if tool_state.axis_method: # Doesn't update snap_angle so that it keeps in the same axis - rot_intersection, _, axis_start, axis_end = cls.snap_on_axis(intersection, tool_state.snap_angle) + rot_intersection, _, axis_start, axis_end = cls.snap_on_axis(intersection, tool_state, True) - if cls.snap_plane_method: - if cls.snap_plane_method in {"XY", "XZ"} and cls.snap_axis_method == "X": + if tool_state.plane_method: + if tool_state.plane_method in {"XY", "XZ"} and tool_state.axis_method == "X": tool_state.snap_angle = 180 - if cls.snap_plane_method in {"XY", "YZ"} and cls.snap_axis_method == "Y": + if tool_state.plane_method in {"XY", "YZ"} and tool_state.axis_method == "Y": tool_state.snap_angle = 90 - if cls.snap_plane_method in {"YZ"} and cls.snap_axis_method == "Z": + if tool_state.plane_method in {"YZ"} and tool_state.axis_method == "Z": tool_state.snap_angle = 180 - if cls.snap_plane_method in {"XZ"} and cls.snap_axis_method == "Z": + if tool_state.plane_method in {"XZ"} and tool_state.axis_method == "Z": tool_state.snap_angle = 90 - if event.shift or cls.snap_axis_method: + if event.shift or tool_state.axis_method: # Doesn't update snap_angle so that it keeps in the same axis - rot_intersection, _, axis_start, axis_end = cls.snap_on_axis(intersection, tool_state.snap_angle) + rot_intersection, _, axis_start, axis_end = cls.snap_on_axis(intersection, tool_state, True) else: - rot_intersection, tool_state.snap_angle, axis_start, axis_end = cls.snap_on_axis(intersection, None) + rot_intersection, tool_state.snap_angle, axis_start, axis_end = cls.snap_on_axis(intersection, tool_state, False) if rot_intersection: detected_snaps.append({"Axis": (rot_intersection, axis_start, axis_end)}) @@ -452,7 +434,7 @@ class Snap(bonsai.core.tool.Snap): return detected_snaps @classmethod - def select_snapping_points(cls, context, event, detected_snaps): + def select_snapping_points(cls, context, event, tool_state, detected_snaps): snapping_points = [] for origin in detected_snaps: if "Object" in list(origin.keys()): @@ -497,7 +479,7 @@ class Snap(bonsai.core.tool.Snap): snapping_points.append((intersection, "Plane")) # Make Axis first priority - if event.shift or cls.snap_axis_method in {"X", "Y", "Z"}: + if event.shift or tool_state.axis_method in {"X", "Y", "Z"}: cls.update_snapping_ref(snapping_points[0][0], snapping_points[0][1]) for point in snapping_points: if point[1] == "Axis": From 1a587dea7ddbf2a683c56d51c436504034d66533 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bruno=20Perdig=C3=A3o?= Date: Sun, 8 Sep 2024 14:30:03 -0300 Subject: [PATCH 26/56] Minor refactor of tool/polyline --- src/bonsai/bonsai/core/tool.py | 13 ++- src/bonsai/bonsai/tool/polyline.py | 165 ++++++++++++++--------------- 2 files changed, 94 insertions(+), 84 deletions(-) diff --git a/src/bonsai/bonsai/core/tool.py b/src/bonsai/bonsai/core/tool.py index 0c675f684d..dc9a8a81a2 100644 --- a/src/bonsai/bonsai/core/tool.py +++ b/src/bonsai/bonsai/core/tool.py @@ -580,9 +580,16 @@ class Nest: class Patch: def run_migrate_patch(cls, infile, outfile, schema): pass + @interface class Polyline: - pass + def create_input_ui(cls, init_z=False, init_area=False): pass + def create_tool_state(cls): pass + def calculate_distance_and_angle(cls, context, input_ui, tool_state): pass + def calculate_area(cls, context, input_ui): pass + def calculate_x_y_and_z(cls, context, input_ui, tool_state): pass + def validate_input(cls, input_number, input_type): pass + @interface class Owner: @@ -671,10 +678,12 @@ class Qto: def get_rounded_value(cls, new_quantity): pass def set_qto_result(cls, result): pass + @interface class Raycast: pass + @interface class Resource: def clear_productivity_data(cls, props): pass @@ -948,10 +957,12 @@ class Spatial: class Covering: def get_z_from_ceiling_height(cls): pass + @interface class Snap: pass + @interface class Structural: def disable_editing_structural_analysis_model(cls): pass diff --git a/src/bonsai/bonsai/tool/polyline.py b/src/bonsai/bonsai/tool/polyline.py index 3b3feeaf7b..758931e7f1 100644 --- a/src/bonsai/bonsai/tool/polyline.py +++ b/src/bonsai/bonsai/tool/polyline.py @@ -1,5 +1,5 @@ # Bonsai - OpenBIM Blender Add-on -# Copyright (C) 2022 Cyril Waechter +# Copyright (C) 2024 Bruno Perdigão # # This file is part of Bonsai. # @@ -20,99 +20,98 @@ import bpy import bonsai.core.tool import bonsai.tool as tool from bonsai.bim.module.drawing.helper import format_distance -from dataclasses import dataclass, field +from dataclasses import dataclass from lark import Lark, Transformer -from math import sin, cos, radians, degrees, atan2, acos -from mathutils import Vector, Matrix +from math import radians +from mathutils import Vector from typing import Optional -@dataclass -class PolylineUI: - _D: str = "" - _A: str = "" - _X: str = "" - _Y: str = "" - _Z: Optional[str] = None - _AREA: Optional[str] = None - init_z: bool = False - init_area: bool = False - - def __post_init__(self): - if self.init_z: - self._Z = "" - if self.init_area: - self._AREA = "" - - def set_value(self, attribute_name, value): - value = str(value) - setattr(self, f"_{attribute_name}", value) - - def get_text_value(self, attribute_name): - value = getattr(self, f"_{attribute_name}") - return value - - def get_number_value(self, attribute_name): - value = getattr(self, f"_{attribute_name}") - if value: - return float(value) - else: - return value - - def get_formatted_value(self, attribute_name): - value = self.get_number_value(attribute_name) - context = bpy.context - if value is None: - return None - if attribute_name == 'A': - value = float(self.get_text_value(attribute_name)) - return f"{value:.2f}" - else: - return self.format_input_ui_units(context, value) - - def format_input_ui_units(cls, context, value): - unit_system = tool.Drawing.get_unit_system() - if unit_system == "IMPERIAL": - precision = context.scene.DocProperties.imperial_precision - factor = 3.28084 - else: - precision = None - factor = 1 - if context.scene.unit_settings.length_unit == "MILLIMETERS": - factor = 1000 - - return format_distance(value * factor, precision=precision, suppress_zero_inches=True, in_unit_length=True) - -@dataclass -class ToolState: - use_default_container: bool = None - snap_angle: float = None - is_input_on: bool = None - # angle_axis_start: Vector - # angle_axis_end: Vector - axis_method: str = None - plane_method: str = None - instructions: str = """TAB: Cycle Input - M: Modify Snap Point - C: Close - Backspace: Remove - X Y: Axis - Shift: Lock axis -""" - snap_info: str = None - mode: str = None - input_type: str = None - - class Polyline(bonsai.core.tool.Polyline): + @dataclass + class PolylineUI: + _D: str = "" + _A: str = "" + _X: str = "" + _Y: str = "" + _Z: Optional[str] = None + _AREA: Optional[str] = None + init_z: bool = False + init_area: bool = False + + def __post_init__(self): + if self.init_z: + self._Z = "" + if self.init_area: + self._AREA = "" + + def set_value(self, attribute_name, value): + value = str(value) + setattr(self, f"_{attribute_name}", value) + + def get_text_value(self, attribute_name): + value = getattr(self, f"_{attribute_name}") + return value + + def get_number_value(self, attribute_name): + value = getattr(self, f"_{attribute_name}") + if value: + return float(value) + else: + return value + + def get_formatted_value(self, attribute_name): + value = self.get_number_value(attribute_name) + context = bpy.context + if value is None: + return None + if attribute_name == 'A': + value = float(self.get_text_value(attribute_name)) + return f"{value:.2f}" + else: + return self.format_input_ui_units(context, value) + + def format_input_ui_units(cls, context, value): + unit_system = tool.Drawing.get_unit_system() + if unit_system == "IMPERIAL": + precision = context.scene.DocProperties.imperial_precision + factor = 3.28084 + else: + precision = None + factor = 1 + if context.scene.unit_settings.length_unit == "MILLIMETERS": + factor = 1000 + + return format_distance(value * factor, precision=precision, suppress_zero_inches=True, in_unit_length=True) + + @dataclass + class ToolState: + use_default_container: bool = None + snap_angle: float = None + is_input_on: bool = None + # angle_axis_start: Vector + # angle_axis_end: Vector + axis_method: str = None + plane_method: str = None + instructions: str = """TAB: Cycle Input + M: Modify Snap Point + C: Close + Backspace: Remove + X Y: Axis + Shift: Lock axis + """ + snap_info: str = None + mode: str = None + input_type: str = None + @classmethod def create_input_ui(cls, init_z=False, init_area=False): - return PolylineUI(init_z=init_z, init_area=init_area) + return cls.PolylineUI(init_z=init_z, init_area=init_area) @classmethod def create_tool_state(cls): - return ToolState() + return cls.ToolState() @classmethod def calculate_distance_and_angle(cls, context, input_ui, tool_state): From c06929e1c77ad0f23e231b5998ab32657c916961 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bruno=20Perdig=C3=A3o?= Date: Sun, 8 Sep 2024 15:47:51 -0300 Subject: [PATCH 27/56] Major refactor in polyline tool to use a base class for other operators. Polyline Wall and Measure tool now share most of the logic by inheriting from a base class PolylineOperator. This should facilitate the implementation of other tools that use polyline. --- .../bonsai/bim/module/model/polyline.py | 337 ++++++++++++++++++ src/bonsai/bonsai/bim/module/model/wall.py | 262 +------------- .../bonsai/bim/module/project/operator.py | 262 +------------- src/bonsai/bonsai/tool/snap.py | 6 +- 4 files changed, 375 insertions(+), 492 deletions(-) create mode 100644 src/bonsai/bonsai/bim/module/model/polyline.py diff --git a/src/bonsai/bonsai/bim/module/model/polyline.py b/src/bonsai/bonsai/bim/module/model/polyline.py new file mode 100644 index 0000000000..89986961aa --- /dev/null +++ b/src/bonsai/bonsai/bim/module/model/polyline.py @@ -0,0 +1,337 @@ +# Bonsai - OpenBIM Blender Add-on +# Copyright (C) 2024 Bruno Perdigão +# +# This file is part of Bonsai. +# +# Bonsai is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Bonsai 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Bonsai. If not, see . + +import bpy +import copy +import math +import bmesh +import ifcopenshell +import ifcopenshell.api +import ifcopenshell.util.unit +import ifcopenshell.util.element +import ifcopenshell.util.placement +import ifcopenshell.util.representation +import ifcopenshell.util.type +import mathutils.geometry +import bonsai.core.type +import bonsai.core.root +import bonsai.core.geometry +import bonsai.core.model as core +import bonsai.tool as tool +from bonsai.bim.ifc import IfcStore +from math import pi, sin, cos, degrees +from mathutils import Vector, Matrix +from bonsai.bim.module.model.opening import FilledOpeningGenerator +from bonsai.bim.module.model.decorator import PolylineDecorator +from typing import Optional +from lark import Lark, Transformer + + +class PolylineOperator: + # TODO Fill doc strings + """ """ + + @classmethod + def poll(cls, context): + return context.space_data.type == "VIEW_3D" + + def __init__(self): + self.mousemove_count = 0 + self.action_count = 0 + self.visible_objs = [] + self.objs_2d_bbox = [] + self.number_options = { + "0", + "1", + "2", + "3", + "4", + "5", + "6", + "7", + "8", + "9", + " ", + ".", + "+", + "-", + "*", + "/", + "'", + '"', + "=", + } + self.number_input = [] + self.number_output = "" + self.number_is_negative = False + self.input_options = ["D", "A", "X", "Y"] + self.input_type = None + self.input_type = None + self.input_value_xy = [None, None] + self.input_ui = tool.Polyline.create_input_ui() + self.is_typing = False + self.snap_angle = None + self.snapping_points = [] + self.instructions = """TAB: Cycle Input + M: Modify Snap Point + C: Close + Backspace: Remove + X Y: Axis + Shift: Lock axis +""" + self.tool_state = tool.Polyline.create_tool_state() + + def recalculate_inputs(self, context): + if self.number_input: + is_valid, self.number_output = tool.Polyline.validate_input(self.number_output, self.input_type) + self.input_ui.set_value(self.input_type, self.number_output) + if not is_valid: + self.report({"WARNING"}, "The number typed is not valid.") + return is_valid + else: + if self.input_type in {"X", "Y"}: + tool.Polyline.calculate_distance_and_angle(context, self.input_ui, self.tool_state) + elif self.input_type in {"D", "A"}: + tool.Polyline.calculate_x_y_and_z(context, self.input_ui, self.tool_state) + tool.Polyline.calculate_distance_and_angle(context, self.input_ui, self.tool_state) + else: + self.input_ui.set_value(self.input_type, self.number_output) + tool.Blender.update_viewport() + return is_valid + + def choose_axis(self, event, x=True, y=True, z=False): + if x: + if event.value == "PRESS" and event.type == "X": + self.tool_state.axis_method = "X" if self.tool_state.axis_method != event.type else None + PolylineDecorator.update(event, self.tool_state, self.input_ui, self.snapping_points[0]) + tool.Blender.update_viewport() + + if y: + if event.value == "PRESS" and event.type == "Y": + self.tool_state.axis_method = "Y" if self.tool_state.axis_method != event.type else None + PolylineDecorator.update(event, self.tool_state, self.input_ui, self.snapping_points[0]) + tool.Blender.update_viewport() + if z: + if event.value == "PRESS" and event.type == "Z": + self.tool_state.axis_method = "Z" if self.tool_state.axis_method != event.type else None + PolylineDecorator.update(event, self.tool_state, self.input_ui, self.snapping_points[0]) + tool.Blender.update_viewport() + + def choose_plane(self, event, x=True, y=True, z=True): + if x: + if event.shift and event.value == "PRESS" and event.type == "X": + self.tool_state.use_default_container = False + self.tool_state.plane_method = "YZ" + self.tool_state.axis_method = None + tool.Blender.update_viewport() + + if y: + if event.shift and event.value == "PRESS" and event.type == "Y": + self.tool_state.use_default_container = False + self.tool_state.plane_method = "XZ" + self.tool_state.axis_method = None + tool.Blender.update_viewport() + + if z: + if event.shift and event.value == "PRESS" and event.type == "Z": + self.tool_state.use_default_container = False + self.tool_state.plane_method = "XY" + self.tool_state.axis_method = None + tool.Blender.update_viewport() + + def handle_keyboard_input(self, context, event): + + if self.tool_state.is_input_on and event.value == "PRESS" and event.type == "TAB": + self.recalculate_inputs(context) + index = self.input_options.index(self.input_type) + size = len(self.input_options) + self.input_type = self.input_options[((index + 1) % size)] + self.tool_state.input_type = self.input_options[((index + 1) % size)] + self.tool_state.mode = "Select" + self.is_typing = False + self.number_input = self.input_ui.get_formatted_value(self.input_type) + self.number_input = list(self.number_input) + self.number_output = "".join(self.number_input) + self.input_ui.set_value(self.input_type, self.number_output) + PolylineDecorator.update(event, self.tool_state, self.input_ui, self.snapping_points[0]) + tool.Blender.update_viewport() + + if not self.tool_state.is_input_on and event.value == "RELEASE" and event.type == "TAB": + self.recalculate_inputs(context) + self.tool_state.mode = "Select" + self.tool_state.is_input_on = True + self.input_type = "D" + self.tool_state.input_type = "D" + self.is_typing = False + self.number_input = self.input_ui.get_formatted_value(self.input_type) + self.number_input = list(self.number_input) + self.number_output = "".join(self.number_input) + self.input_ui.set_value(self.input_type, self.number_output) + PolylineDecorator.update(event, self.tool_state, self.input_ui, self.snapping_points[0]) + tool.Blender.update_viewport() + + if not self.tool_state.is_input_on and event.ascii in self.number_options: + self.recalculate_inputs(context) + self.tool_state.mode = "Edit" + self.tool_state.is_input_on = True + self.input_type = "D" + self.tool_state.input_type = "D" + PolylineDecorator.update(event, self.tool_state, self.input_ui, self.snapping_points[0]) + tool.Blender.update_viewport() + + if event.value == "RELEASE" and event.type in {"D", "A"}: + self.recalculate_inputs(context) + self.tool_state.mode = "Edit" + self.tool_state.is_input_on = True + self.input_type = event.type + self.tool_state.input_type = event.type + self.input_ui.set_value(self.input_type, "") + PolylineDecorator.update(event, self.tool_state, self.input_ui, self.snapping_points[0]) + tool.Blender.update_viewport() + + if self.input_type in self.input_options: + if (event.ascii in self.number_options) or (event.value == "RELEASE" and event.type == "BACK_SPACE"): + if not self.tool_state.mode == "Edit" and not (event.ascii == "=" or event.type == "BACK_SPACE"): + self.number_input = [] + + if event.type == "BACK_SPACE": + if len(self.number_input) <= 1: + self.number_input = [] + else: + self.number_input.pop(-1) + elif event.ascii == "=": + if self.number_input[0] == "=": + self.number_input.pop(0) + else: + self.number_input.insert(0, "=") + else: + self.number_input.append(event.ascii) + + if not self.number_input: + self.number_output = "0" + + self.tool_state.mode = "Edit" + self.is_typing = True + self.number_output = "".join(self.number_input) + self.input_ui.set_value(self.input_type, self.number_output) + PolylineDecorator.update(event, self.tool_state, self.input_ui, self.snapping_points[0]) + tool.Blender.update_viewport() + + def handle_inserting_polyline(self, context, event): + if event.value == "RELEASE" and event.type == "LEFTMOUSE": + tool.Snap.insert_polyline_point(self.input_ui) + tool.Blender.update_viewport() + + if event.value == "PRESS" and event.type == "C": + tool.Snap.close_polyline() + PolylineDecorator.update(event, self.tool_state, self.input_ui, self.snapping_points[0]) + tool.Blender.update_viewport() + + if ( + self.tool_state.is_input_on + and event.value == "RELEASE" + and event.type in {"RET", "NUMPAD_ENTER", "RIGHTMOUSE"} + ): + is_valid = self.recalculate_inputs(context) + if is_valid: + tool.Snap.insert_polyline_point(self.input_ui) + self.tool_state.mode = "Mouse" + self.tool_state.is_input_on = False + self.input_type = None + self.tool_state.input_type = None + self.number_input = [] + self.number_output = "" + PolylineDecorator.update(event, self.tool_state, self.input_ui, self.snapping_points[0]) + tool.Blender.update_viewport() + + def handle_snap_selection(self, context, event): + if event.value == "PRESS" and event.type == "M": + self.snapping_points = tool.Snap.modify_snapping_point_selection(self.snapping_points) + tool.Polyline.calculate_distance_and_angle(context, self.input_ui, self.tool_state) + PolylineDecorator.update(event, self.tool_state, self.input_ui, self.snapping_points[0]) + tool.Blender.update_viewport() + + def handle_cancelation(self, context, event): + if self.tool_state.is_input_on: + if event.value == "RELEASE" and event.type in {"ESC"}: + self.recalculate_inputs(context) + self.tool_state.mode = "Mouse" + self.tool_state.is_input_on = False + self.input_type = None + self.tool_state.input_type = None + PolylineDecorator.update(event, self.tool_state, self.input_ui, self.snapping_points[0]) + tool.Blender.update_viewport() + else: + if event.value == "RELEASE" and event.type in {"ESC"}: + self.tool_state.axis_method = None + PolylineDecorator.uninstall() + tool.Snap.clear_polyline() + tool.Blender.update_viewport() + return {"CANCELLED"} + + def handle_mouse_move(self, context, event): + if not self.tool_state.is_input_on: + if event.type == "MOUSEMOVE" or event.type == "INBETWEEN_MOUSEMOVE": + self.mousemove_count += 1 + self.tool_state.mode = "Mouse" + self.tool_state.is_input_on = False + self.input_type = None + self.tool_state.input_type = None + PolylineDecorator.update(event, self.tool_state, self.input_ui, self.snapping_points[0]) + tool.Snap.clear_snapping_ref() + tool.Blender.update_viewport() + else: + self.mousemove_count = 0 + + if self.mousemove_count == 2: + self.objs_2d_bbox = [] + for obj in self.visible_objs: + self.objs_2d_bbox.append(tool.Raycast.get_on_screen_2d_bounding_boxes(context, obj)) + + if self.mousemove_count > 3: + detected_snaps = tool.Snap.detect_snapping_points(context, event, self.objs_2d_bbox, self.tool_state) + self.snapping_points = tool.Snap.select_snapping_points(context, event, self.tool_state, detected_snaps) + tool.Polyline.calculate_distance_and_angle(context, self.input_ui, self.tool_state) + tool.Blender.update_viewport() + return {"RUNNING_MODAL"} + + if event.value == "RELEASE" and event.type == "BACK_SPACE": + tool.Snap.remove_last_polyline_point() + tool.Blender.update_viewport() + + + def invoke(self, context, event): + PolylineDecorator.install(context) + tool.Snap.clear_snapping_point() + + self.tool_state.use_default_container = False + self.tool_state.axis_method = None + self.tool_state.plane_method = None + self.tool_state.mode = "Mouse" + tool.Snap.set_tool_state(self.tool_state) + self.visible_objs = tool.Raycast.get_visible_objects(context) + for obj in self.visible_objs: + self.objs_2d_bbox.append(tool.Raycast.get_on_screen_2d_bounding_boxes(context, obj)) + detected_snaps = tool.Snap.detect_snapping_points(context, event, self.objs_2d_bbox, self.tool_state) + self.snapping_points = tool.Snap.select_snapping_points(context, event, self.tool_state, detected_snaps) + tool.Polyline.calculate_distance_and_angle(context, self.input_ui, self.tool_state) + + PolylineDecorator.update(event, self.tool_state, self.input_ui, self.snapping_points[0]) + tool.Blender.update_viewport() + context.window_manager.modal_handler_add(self) + # return {"RUNNING_MODAL"} diff --git a/src/bonsai/bonsai/bim/module/model/wall.py b/src/bonsai/bonsai/bim/module/model/wall.py index 3b6a1d2ee0..0244f4a173 100644 --- a/src/bonsai/bonsai/bim/module/model/wall.py +++ b/src/bonsai/bonsai/bim/module/model/wall.py @@ -38,6 +38,7 @@ from math import pi, sin, cos, degrees from mathutils import Vector, Matrix from bonsai.bim.module.model.opening import FilledOpeningGenerator from bonsai.bim.module.model.decorator import PolylineDecorator +from bonsai.bim.module.model.polyline import PolylineOperator from typing import Optional from lark import Lark, Transformer @@ -284,7 +285,7 @@ def recalculate_dumb_wall_origin(wall, new_origin=None): child.matrix_parent_inverse = wall.matrix_world.inverted() -class DrawPolylineWall(bpy.types.Operator): +class DrawPolylineWall(bpy.types.Operator, PolylineOperator): bl_idname = "bim.draw_polyline_wall" bl_label = "Draw Polyline Wall" bl_options = {"REGISTER", "UNDO"} @@ -294,68 +295,7 @@ class DrawPolylineWall(bpy.types.Operator): return context.space_data.type == "VIEW_3D" def __init__(self): - self.mousemove_count = 0 - self.action_count = 0 - self.visible_objs = [] - self.objs_2d_bbox = [] - self.number_options = { - "0", - "1", - "2", - "3", - "4", - "5", - "6", - "7", - "8", - "9", - " ", - ".", - "+", - "-", - "*", - "/", - "'", - '"', - "=", - } - self.number_input = [] - self.number_output = "" - self.number_is_negative = False - self.input_options = ["D", "A", "X", "Y"] - self.input_type = None - self.input_type = None - self.input_value_xy = [None, None] - self.input_ui = tool.Polyline.create_input_ui() - self.is_typing = False - self.snap_angle = None - self.snapping_points = [] - self.instructions = """TAB: Cycle Input - M: Modify Snap Point - C: Close - Backspace: Remove - X Y: Axis - Shift: Lock axis -""" - self.tool_state = tool.Polyline.create_tool_state() - - def recalculate_inputs(self, context): - if self.number_input: - is_valid, self.number_output = tool.Polyline.validate_input(self.number_output, self.input_type) - self.input_ui.set_value(self.input_type, self.number_output) - if not is_valid: - self.report({"WARNING"}, "The number typed is not valid.") - return is_valid - else: - if self.input_type in {"X", "Y"}: - tool.Polyline.calculate_distance_and_angle(context, self.input_ui, self.tool_state) - elif self.input_type in {"D", "A"}: - tool.Polyline.calculate_x_y_and_z(context, self.input_ui, self.tool_state) - tool.Polyline.calculate_distance_and_angle(context, self.input_ui, self.tool_state) - else: - self.input_ui.set_value(self.input_type, self.number_output) - tool.Blender.update_viewport() - return is_valid + super().__init__() # TODO This is creating a hack in generate function from DumbWallGenerator # Come up with a better solution @@ -384,131 +324,14 @@ class DrawPolylineWall(bpy.types.Operator): DumbWallJoiner().join_V(wall1["obj"], wall2["obj"]) def modal(self, context, event): + if event.type in {"MIDDLEMOUSE", "WHEELUPMOUSE", "WHEELDOWNMOUSE"}: + return {"PASS_THROUGH"} - if not self.tool_state.is_input_on: - if event.type == "MOUSEMOVE" or event.type == "INBETWEEN_MOUSEMOVE": - self.mousemove_count += 1 - self.tool_state.mode = "Mouse" - self.tool_state.is_input_on = False - self.input_type = None - self.tool_state.input_type = None - PolylineDecorator.update(event, self.tool_state, self.input_ui, self.snapping_points[0]) - tool.Snap.clear_snapping_ref() - tool.Blender.update_viewport() - else: - self.mousemove_count = 0 + self.handle_mouse_move(context, event) - if self.mousemove_count == 2: - self.objs_2d_bbox = [] - for obj in self.visible_objs: - self.objs_2d_bbox.append(tool.Raycast.get_on_screen_2d_bounding_boxes(context, obj)) + self.choose_axis(event) - if self.mousemove_count > 3: - detected_snaps = tool.Snap.detect_snapping_points(context, event, self.objs_2d_bbox, self.tool_state) - self.snapping_points = tool.Snap.select_snapping_points(context, event, self.tool_state, detected_snaps) - print(self.snapping_points) - tool.Polyline.calculate_distance_and_angle(context, self.input_ui, self.tool_state) - tool.Blender.update_viewport() - return {"RUNNING_MODAL"} - - if event.value == "RELEASE" and event.type == "BACK_SPACE": - tool.Snap.remove_last_polyline_point() - tool.Blender.update_viewport() - - if event.value == "RELEASE" and event.type == "LEFTMOUSE": - tool.Snap.insert_polyline_point(self.input_ui) - tool.Blender.update_viewport() - - if event.value == "PRESS" and event.type == "X": - self.tool_state.axis_method = "X" if self.tool_state.axis_method != event.type else None - PolylineDecorator.update(event, self.tool_state, self.input_ui, self.snapping_points[0]) - tool.Blender.update_viewport() - - if event.value == "PRESS" and event.type == "Y": - self.tool_state.axis_method = "Y" if self.tool_state.axis_method != event.type else None - PolylineDecorator.update(event, self.tool_state, self.input_ui, self.snapping_points[0]) - tool.Blender.update_viewport() - - if event.value == "PRESS" and event.type == "C": - tool.Snap.close_polyline() - PolylineDecorator.update(event, self.tool_state, self.input_ui, self.snapping_points[0]) - tool.Blender.update_viewport() - - if self.tool_state.is_input_on and event.value == "PRESS" and event.type == "TAB": - self.recalculate_inputs(context) - index = self.input_options.index(self.input_type) - size = len(self.input_options) - self.input_type = self.input_options[((index + 1) % size)] - self.tool_state.input_type = self.input_options[((index + 1) % size)] - self.tool_state.mode = "Select" - self.is_typing = False - self.number_input = self.input_ui.get_formatted_value(self.input_type) - self.number_input = list(self.number_input) - self.number_output = "".join(self.number_input) - self.input_ui.set_value(self.input_type, self.number_output) - PolylineDecorator.update(event, self.tool_state, self.input_ui, self.snapping_points[0]) - tool.Blender.update_viewport() - - if not self.tool_state.is_input_on and event.value == "RELEASE" and event.type == "TAB": - self.recalculate_inputs(context) - self.tool_state.mode = "Select" - self.tool_state.is_input_on = True - self.input_type = "D" - self.tool_state.input_type = "D" - self.is_typing = False - self.number_input = self.input_ui.get_formatted_value(self.input_type) - self.number_input = list(self.number_input) - self.number_output = "".join(self.number_input) - self.input_ui.set_value(self.input_type, self.number_output) - PolylineDecorator.update(event, self.tool_state, self.input_ui, self.snapping_points[0]) - tool.Blender.update_viewport() - - if not self.tool_state.is_input_on and event.ascii in self.number_options: - self.recalculate_inputs(context) - self.tool_state.mode = "Edit" - self.tool_state.is_input_on = True - self.input_type = "D" - self.tool_state.input_type = "D" - PolylineDecorator.update(event, self.tool_state, self.input_ui, self.snapping_points[0]) - tool.Blender.update_viewport() - - if event.value == "RELEASE" and event.type in {"D", "A"}: - self.recalculate_inputs(context) - self.tool_state.mode = "Edit" - self.tool_state.is_input_on = True - self.input_type = event.type - self.tool_state.input_type = event.type - self.input_ui.set_value(self.input_type, "") - PolylineDecorator.update(event, self.tool_state, self.input_ui, self.snapping_points[0]) - tool.Blender.update_viewport() - - if self.input_type in self.input_options: - if (event.ascii in self.number_options) or (event.value == "RELEASE" and event.type == "BACK_SPACE"): - if not self.tool_state.mode == "Edit" and not (event.ascii == "=" or event.type == "BACK_SPACE"): - self.number_input = [] - - if event.type == "BACK_SPACE": - if len(self.number_input) <= 1: - self.number_input = [] - else: - self.number_input.pop(-1) - elif event.ascii == "=": - if self.number_input[0] == "=": - self.number_input.pop(0) - else: - self.number_input.insert(0, "=") - else: - self.number_input.append(event.ascii) - - if not self.number_input: - self.number_output = "0" - - self.tool_state.mode = "Edit" - self.is_typing = True - self.number_output = "".join(self.number_input) - self.input_ui.set_value(self.input_type, self.number_output) - PolylineDecorator.update(event, self.tool_state, self.input_ui, self.snapping_points[0]) - tool.Blender.update_viewport() + self.handle_snap_selection(context, event) if not self.tool_state.is_input_on and event.value == "RELEASE" and event.type in {"RET", "NUMPAD_ENTER", "RIGHTMOUSE"}: self.create_walls_from_polyline(context) @@ -517,72 +340,21 @@ class DrawPolylineWall(bpy.types.Operator): tool.Blender.update_viewport() return {"FINISHED"} - if self.tool_state.is_input_on and event.value == "RELEASE" and event.type in {"RET", "NUMPAD_ENTER", "RIGHTMOUSE"}: - is_valid = self.recalculate_inputs(context) - if is_valid: - tool.Snap.insert_polyline_point(self.input_ui) - self.tool_state.mode = "Mouse" - self.tool_state.is_input_on = False - self.input_type = None - self.tool_state.input_type = None - self.number_input = [] - self.number_output = "" - PolylineDecorator.update(event, self.tool_state, self.input_ui, self.snapping_points[0]) - tool.Blender.update_viewport() + self.handle_keyboard_input(context, event) - if event.value == "PRESS" and event.type == "M": - self.snapping_points = tool.Snap.modify_snapping_point_selection(self.snapping_points) - tool.Polyline.calculate_distance_and_angle(context, self.input_ui, self.tool_state) - PolylineDecorator.update(event, self.tool_state, self.input_ui, self.snapping_points[0]) - tool.Blender.update_viewport() + self.handle_inserting_polyline(context, event) - if event.type in {"MIDDLEMOUSE", "WHEELUPMOUSE", "WHEELDOWNMOUSE"}: - return {"PASS_THROUGH"} + result = self.handle_cancelation(context, event) + if result is not None: + return result - if self.tool_state.is_input_on: - if event.value == "RELEASE" and event.type in {"ESC"}: - self.recalculate_inputs(context) - self.tool_state.mode = "Mouse" - self.tool_state.is_input_on = False - self.input_type = None - self.tool_state.input_type = None - PolylineDecorator.update(event, self.tool_state, self.input_ui, self.snapping_points[0]) - tool.Blender.update_viewport() - else: - if event.value == "RELEASE" and event.type in {"ESC"}: - self.tool_state.axis_method = None - PolylineDecorator.uninstall() - tool.Snap.clear_polyline() - tool.Blender.update_viewport() - return {"CANCELLED"} - - print(">>> ", self.tool_state.mode) return {"RUNNING_MODAL"} def invoke(self, context, event): - if context.space_data.type == "VIEW_3D": - PolylineDecorator.install(context) - tool.Snap.clear_snapping_point() - - self.tool_state.use_default_container = True - self.tool_state.axis_method = None - self.tool_state.plane_method = "XY" - self.tool_state.mode = "Mouse" - tool.Snap.set_tool_state(self.tool_state) - self.visible_objs = tool.Raycast.get_visible_objects(context) - for obj in self.visible_objs: - self.objs_2d_bbox.append(tool.Raycast.get_on_screen_2d_bounding_boxes(context, obj)) - detected_snaps = tool.Snap.detect_snapping_points(context, event, self.objs_2d_bbox, self.tool_state) - self.snapping_points = tool.Snap.select_snapping_points(context, event, self.tool_state, detected_snaps) - tool.Polyline.calculate_distance_and_angle(context, self.input_ui, self.tool_state) - - PolylineDecorator.update(event, self.tool_state, self.input_ui, self.snapping_points[0]) - tool.Blender.update_viewport() - context.window_manager.modal_handler_add(self) - return {"RUNNING_MODAL"} - else: - self.report({"WARNING"}, "Active space must be a View3d") - return {"CANCELLED"} + super().invoke(context, event) + self.tool_state.use_default_container = True + self.tool_state.plane_method = "XY" + return {"RUNNING_MODAL"} class DumbWallAligner: diff --git a/src/bonsai/bonsai/bim/module/project/operator.py b/src/bonsai/bonsai/bim/module/project/operator.py index dff315ec52..4eef869eff 100644 --- a/src/bonsai/bonsai/bim/module/project/operator.py +++ b/src/bonsai/bonsai/bim/module/project/operator.py @@ -53,6 +53,7 @@ from ifcopenshell.geom import ShapeElementType from bonsai.bim.module.project.data import LinksData from bonsai.bim.module.project.decorator import ProjectDecorator, ClippingPlaneDecorator from bonsai.bim.module.model.decorator import PolylineDecorator +from bonsai.bim.module.model.polyline import PolylineOperator from typing import Union @@ -2268,7 +2269,7 @@ if bpy.app.version >= (4, 1, 0): return True -class MeasureTool(bpy.types.Operator): +class MeasureTool(bpy.types.Operator, PolylineOperator): bl_idname = "bim.measure_tool" bl_options = {"REGISTER", "UNDO"} bl_label = "Measure Tool" @@ -2278,42 +2279,9 @@ class MeasureTool(bpy.types.Operator): return context.space_data.type == "VIEW_3D" def __init__(self): - self.mousemove_count = 0 - self.action_count = 0 - self.visible_objs = [] - self.objs_2d_bbox = [] - self.number_options = { - "0", - "1", - "2", - "3", - "4", - "5", - "6", - "7", - "8", - "9", - " ", - ".", - "+", - "-", - "*", - "/", - "'", - '"', - "=", - } - self.number_input = [] - self.number_output = "" - self.number_is_negative = False - self.is_input_on = False - self.input_options = ["D", "A", "X", "Y", "Z"] - self.input_type = None - self.input_value_xy = [None, None] + super().__init__() self.input_ui = tool.Polyline.create_input_ui(init_z=True) - self.is_typing = False - self.snap_angle = None - self.snapping_points = [] + self.input_options = ["D", "A", "X", "Y", "Z"] self.instructions = """TAB: Cycle Input M: Modify Snap Point C: Close @@ -2323,228 +2291,34 @@ class MeasureTool(bpy.types.Operator): Shift: Lock axis """ - def recalculate_inputs(self, context): - if self.number_input: - is_valid, self.number_output = tool.Polyline.validate_input(self.number_output, self.input_type) - self.input_ui.set_value(self.input_type, self.number_output) - if not is_valid: - self.report({"WARNING"}, "The number typed is not valid.") - return is_valid - else: - if self.input_type in {"X", "Y", "Z"}: - tool.Polyline.calculate_distance_and_angle(context, self.is_input_on, self.input_ui) - elif self.input_type in {"D", "A"}: - tool.Polyline.calculate_x_y_and_z(context, self.input_ui) - tool.Polyline.calculate_distance_and_angle(context, self.is_input_on, self.input_ui) - else: - self.input_ui.set_value(self.input_type, self.number_output) - tool.Blender.update_viewport() - return is_valid - def modal(self, context, event): + if event.type in {"MIDDLEMOUSE", "WHEELUPMOUSE", "WHEELDOWNMOUSE"}: + return {"PASS_THROUGH"} - if not self.is_input_on: - if event.type == "MOUSEMOVE" or event.type == "INBETWEEN_MOUSEMOVE": - self.mousemove_count += 1 - self.is_input_on = False - self.input_type = None - PolylineDecorator.set_input_ui(self.input_ui, self.input_type) - tool.Snap.clear_snapping_ref() - tool.Blender.update_viewport() - else: - self.mousemove_count = 0 + self.handle_mouse_move(context, event) - if self.mousemove_count == 2: - self.objs_2d_bbox = [] - for obj in self.visible_objs: - self.objs_2d_bbox.append(tool.Raycast.get_on_screen_2d_bounding_boxes(context, obj)) + self.choose_axis(event, z=True) - if self.mousemove_count > 3: - detected_snaps = tool.Snap.detect_snapping_points(context, event, self.objs_2d_bbox) - self.snapping_points = tool.Snap.select_snapping_points(context, event, detected_snaps) - PolylineDecorator.set_mouse_position(event) - tool.Polyline.calculate_distance_and_angle(context, self.is_input_on, self.input_ui) - tool.Blender.update_viewport() - return {"RUNNING_MODAL"} + self.choose_plane(event) - if event.value == "RELEASE" and event.type == "BACK_SPACE": - tool.Snap.remove_last_polyline_point() - tool.Blender.update_viewport() + self.handle_snap_selection(context, event) - if event.value == "RELEASE" and event.type == "LEFTMOUSE": - tool.Snap.insert_polyline_point(self.input_ui) - tool.Blender.update_viewport() - - if event.value == "PRESS" and event.type == "X": - tool.Snap.set_snap_axis_method("X") - tool.Blender.update_viewport() - - if event.value == "PRESS" and event.type == "Y": - tool.Snap.set_snap_axis_method("Y") - tool.Blender.update_viewport() - - if event.value == "PRESS" and event.type == "Z": - tool.Snap.set_snap_axis_method("Z") - tool.Blender.update_viewport() - - if event.value == "PRESS" and event.type == "C": - tool.Snap.close_polyline() - PolylineDecorator.set_input_ui(self.input_ui, self.input_type) - tool.Blender.update_viewport() - - if self.is_input_on and event.value == "PRESS" and event.type == "TAB": - self.recalculate_inputs(context) - index = self.input_options.index(self.input_type) - size = len(self.input_options) - self.input_type = self.input_options[((index + 1) % size)] - self.is_typing = False - self.number_input = self.input_ui.get_formatted_value(self.input_type) - self.number_input = list(self.number_input) - self.number_output = "".join(self.number_input) - self.input_ui.set_value(self.input_type, self.number_output) - PolylineDecorator.set_input_ui(self.input_ui, self.input_type) - tool.Blender.update_viewport() - - if not self.is_input_on and event.value == "RELEASE" and event.type == "TAB": - self.recalculate_inputs(context) - self.is_input_on = True - self.input_type = "D" - self.is_typing = False - self.number_input = self.input_ui.get_formatted_value(self.input_type) - self.number_input = list(self.number_input) - self.number_output = "".join(self.number_input) - self.input_ui.set_value(self.input_type, self.number_output) - PolylineDecorator.set_input_ui(self.input_ui, self.input_type) - tool.Blender.update_viewport() - - if not self.is_input_on and event.ascii in self.number_options: - self.recalculate_inputs(context) - self.is_input_on = True - self.input_type = "D" - PolylineDecorator.set_input_ui(self.input_ui, self.input_type) - tool.Blender.update_viewport() - - if event.value == "RELEASE" and event.type in {"D", "A"}: - self.recalculate_inputs(context) - self.is_input_on = True - self.input_type = event.type - self.input_ui.set_value(self.input_type, "") - PolylineDecorator.set_input_ui(self.input_ui, self.input_type) - tool.Blender.update_viewport() - - if self.input_type in self.input_options: - if (event.ascii in self.number_options) or (event.value == "RELEASE" and event.type == "BACK_SPACE"): - if not self.is_typing and not (event.ascii == "=" or event.type == "BACK_SPACE"): - self.number_input = [] - - if event.type == "BACK_SPACE": - if len(self.number_input) <= 1: - self.number_input = [] - else: - self.number_input.pop(-1) - elif event.ascii == "=": - if self.number_input[0] == "=": - self.number_input.pop(0) - else: - self.number_input.insert(0, "=") - else: - self.number_input.append(event.ascii) - - if not self.number_input: - self.number_output = "0" - - self.is_typing = True - self.number_output = "".join(self.number_input) - self.input_ui.set_value(self.input_type, self.number_output) - PolylineDecorator.set_input_ui(self.input_ui, self.input_type) - tool.Blender.update_viewport() - - if not self.is_input_on and event.value == "RELEASE" and event.type in {"RET", "NUMPAD_ENTER", "RIGHTMOUSE"}: + if not self.tool_state.is_input_on and event.value == "RELEASE" and event.type in {"RET", "NUMPAD_ENTER", "RIGHTMOUSE"}: PolylineDecorator.uninstall() tool.Snap.clear_polyline() tool.Blender.update_viewport() return {"FINISHED"} - if self.is_input_on and event.value == "RELEASE" and event.type in {"RET", "NUMPAD_ENTER", "RIGHTMOUSE"}: - is_valid = self.recalculate_inputs(context) - if is_valid: - tool.Snap.insert_polyline_point(self.input_ui) - self.is_input_on = False - self.input_type = None - self.number_input = [] - self.number_output = "" - PolylineDecorator.set_input_ui(self.input_ui, self.input_type) - tool.Blender.update_viewport() + self.handle_keyboard_input(context, event) - if event.value == "PRESS" and event.type == "M": - self.snapping_points = tool.Snap.modify_snapping_point_selection(self.snapping_points) - PolylineDecorator.set_mouse_position(event) - tool.Polyline.calculate_distance_and_angle(context, self.is_input_on, self.input_ui) - tool.Blender.update_viewport() + self.handle_inserting_polyline(context, event) - if event.shift and event.value == "PRESS" and event.type == "X": - tool.Snap.set_use_default_container(False) - PolylineDecorator.set_use_default_container(False) - tool.Snap.cycle_snap_plane_method("YZ") - tool.Snap.set_snap_axis_method(None) - tool.Blender.update_viewport() - - if event.shift and event.value == "PRESS" and event.type == "Y": - tool.Snap.set_use_default_container(False) - PolylineDecorator.set_use_default_container(False) - tool.Snap.cycle_snap_plane_method("XZ") - tool.Snap.set_snap_axis_method(None) - tool.Blender.update_viewport() - - if event.shift and event.value == "PRESS" and event.type == "Z": - tool.Snap.set_use_default_container(False) - PolylineDecorator.set_use_default_container(False) - tool.Snap.cycle_snap_plane_method("XY") - tool.Snap.set_snap_axis_method(None) - tool.Blender.update_viewport() - - if event.type in {"MIDDLEMOUSE", "WHEELUPMOUSE", "WHEELDOWNMOUSE"}: - return {"PASS_THROUGH"} - - if self.is_input_on: - if event.value == "RELEASE" and event.type in {"ESC"}: - self.recalculate_inputs(context) - self.is_input_on = False - self.input_type = None - PolylineDecorator.set_input_ui(self.input_ui, self.input_type) - tool.Blender.update_viewport() - else: - if event.value == "RELEASE" and event.type in {"ESC"}: - tool.Snap.set_snap_plane_method(None) - tool.Snap.set_snap_axis_method(None) - PolylineDecorator.uninstall() - tool.Snap.clear_polyline() - tool.Blender.update_viewport() - return {"CANCELLED"} + result = self.handle_cancelation(context, event) + if result is not None: + return result return {"RUNNING_MODAL"} def invoke(self, context, event): - if context.space_data.type == "VIEW_3D": - PolylineDecorator.install(context) - tool.Snap.clear_snapping_point() - tool.Snap.set_use_default_container(False) - PolylineDecorator.set_use_default_container(False) - tool.Polyline.set_use_default_container(False) - tool.Snap.set_snap_plane_method(None) - tool.Snap.set_snap_axis_method(None) - PolylineDecorator.set_instructions(self.instructions) - PolylineDecorator.set_input_ui(self.input_ui, self.input_type) - self.visible_objs = tool.Raycast.get_visible_objects(context) - for obj in self.visible_objs: - self.objs_2d_bbox.append(tool.Raycast.get_on_screen_2d_bounding_boxes(context, obj)) - detected_snaps = tool.Snap.detect_snapping_points(context, event, self.objs_2d_bbox) - self.snapping_points = tool.Snap.select_snapping_points(context, event, detected_snaps) - PolylineDecorator.set_mouse_position(event) - tool.Polyline.calculate_distance_and_angle(context, self.is_input_on, self.input_ui) - tool.Blender.update_viewport() - context.window_manager.modal_handler_add(self) - return {"RUNNING_MODAL"} - else: - self.report({"WARNING"}, "Active space must be a View3d") - return {"CANCELLED"} + super().invoke(context, event) + return {"RUNNING_MODAL"} diff --git a/src/bonsai/bonsai/tool/snap.py b/src/bonsai/bonsai/tool/snap.py index 137bad0b52..ff9a9d5b41 100644 --- a/src/bonsai/bonsai/tool/snap.py +++ b/src/bonsai/bonsai/tool/snap.py @@ -118,10 +118,10 @@ class Snap(bonsai.core.tool.Snap): def insert_polyline_point(cls, input_ui): x = input_ui.get_number_value("X") y = input_ui.get_number_value("Y") - try: + if input_ui.get_number_value("Z") is not None: z = input_ui.get_number_value("Z") - except: - z = Vector((0, 0, 0)) + else: + z = 0 d = input_ui.get_formatted_value("D") a = input_ui.get_formatted_value("A") From 794154f6f5b4bf89c699aa1452406a2c47719227 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bruno=20Perdig=C3=A3o?= Date: Mon, 9 Sep 2024 22:27:24 -0300 Subject: [PATCH 28/56] Instructions in polyline tools were moved to the status bar. Previously the instructions were shown on screen with decorators. Now it is handled by the modal function of the polyline tools. --- .../bonsai/bim/module/model/decorator.py | 27 ----------------- .../bonsai/bim/module/model/polyline.py | 29 +++++++++++++++---- src/bonsai/bonsai/bim/module/model/wall.py | 3 ++ .../bonsai/bim/module/project/operator.py | 19 +++++++----- 4 files changed, 38 insertions(+), 40 deletions(-) diff --git a/src/bonsai/bonsai/bim/module/model/decorator.py b/src/bonsai/bonsai/bim/module/model/decorator.py index c330602467..59f242b36f 100644 --- a/src/bonsai/bonsai/bim/module/model/decorator.py +++ b/src/bonsai/bonsai/bim/module/model/decorator.py @@ -316,9 +316,6 @@ class PolylineDecorator: if cls.is_installed: cls.uninstall() handler = cls() - cls.handlers.append( - SpaceView3D.draw_handler_add(handler.draw_on_screen_menu, (context,), "WINDOW", "POST_PIXEL") - ) cls.handlers.append(SpaceView3D.draw_handler_add(handler.draw_measurements, (context,), "WINDOW", "POST_PIXEL")) cls.handlers.append(SpaceView3D.draw_handler_add(handler.draw_input_ui, (context,), "WINDOW", "POST_PIXEL")) cls.handlers.append(SpaceView3D.draw_handler_add(handler, (context,), "WINDOW", "POST_VIEW")) @@ -338,9 +335,6 @@ class PolylineDecorator: cls.event = event cls.tool_state = tool_state cls.input_ui = input_ui - cls.snap_info = f"""Snap: {snapping_point[1]} - Axis:{tool_state.axis_method} - Plane: {tool_state.plane_method}""" @classmethod def set_input_ui(cls, input_ui): @@ -440,27 +434,6 @@ class PolylineDecorator: blf.draw(self.font_id, "a: " + measurement_prop[i].angle) - def draw_on_screen_menu(self, context): - region = context.region - - self.addon_prefs = tool.Blender.get_addon_preferences() - self.font_id = 2 - font_size = tool.Blender.scale_font_size(12) - blf.size(self.font_id, font_size) - blf.enable(self.font_id, blf.SHADOW) - blf.shadow(self.font_id, 6, 0, 0, 0, 1) - color = self.addon_prefs.decorations_colour - blf.color(self.font_id, *color) - - text_w, text_h = blf.dimensions(0, self.tool_state.instructions) - position = (region.width / 2) - (text_w / 2) - blf.position(self.font_id, position, 10, 0) - blf.draw(self.font_id, self.tool_state.instructions) - - text_w, text_h = blf.dimensions(0, self.snap_info) - position = (region.width / 2) - (text_w / 2) - blf.position(self.font_id, position, 30, 0) - blf.draw(self.font_id, self.snap_info) def __call__(self, context): diff --git a/src/bonsai/bonsai/bim/module/model/polyline.py b/src/bonsai/bonsai/bim/module/model/polyline.py index 89986961aa..be52340745 100644 --- a/src/bonsai/bonsai/bim/module/model/polyline.py +++ b/src/bonsai/bonsai/bim/module/model/polyline.py @@ -88,12 +88,20 @@ class PolylineOperator: self.snap_angle = None self.snapping_points = [] self.instructions = """TAB: Cycle Input - M: Modify Snap Point - C: Close - Backspace: Remove - X Y: Axis - Shift: Lock axis -""" + D: Distance Input + A: Angle Input + M: Modify Snap Point + C: Close Polyline + BACKSPACE: Remove Point + X, Y: Choose Axis + SHIFT: Lock axis + """ + self.snap_info = """ + Snap: + Axis: + Plane: + """ + self.tool_state = tool.Polyline.create_tool_state() def recalculate_inputs(self, context): @@ -154,6 +162,14 @@ class PolylineOperator: self.tool_state.axis_method = None tool.Blender.update_viewport() + def handle_instructions(self, context): + self.snap_info = f"""| + Axis: {self.tool_state.axis_method} + Plane: {self.tool_state.plane_method} + Snap: {self.snapping_points[0][1]} + """ + context.workspace.status_text_set(self.instructions + self.snap_info) + def handle_keyboard_input(self, context, event): if self.tool_state.is_input_on and event.value == "PRESS" and event.type == "TAB": @@ -279,6 +295,7 @@ class PolylineOperator: else: if event.value == "RELEASE" and event.type in {"ESC"}: self.tool_state.axis_method = None + context.workspace.status_text_set(text=None) PolylineDecorator.uninstall() tool.Snap.clear_polyline() tool.Blender.update_viewport() diff --git a/src/bonsai/bonsai/bim/module/model/wall.py b/src/bonsai/bonsai/bim/module/model/wall.py index 0244f4a173..53b3dccc7b 100644 --- a/src/bonsai/bonsai/bim/module/model/wall.py +++ b/src/bonsai/bonsai/bim/module/model/wall.py @@ -327,6 +327,8 @@ class DrawPolylineWall(bpy.types.Operator, PolylineOperator): if event.type in {"MIDDLEMOUSE", "WHEELUPMOUSE", "WHEELDOWNMOUSE"}: return {"PASS_THROUGH"} + self.handle_instructions(context) + self.handle_mouse_move(context, event) self.choose_axis(event) @@ -335,6 +337,7 @@ class DrawPolylineWall(bpy.types.Operator, PolylineOperator): if not self.tool_state.is_input_on and event.value == "RELEASE" and event.type in {"RET", "NUMPAD_ENTER", "RIGHTMOUSE"}: self.create_walls_from_polyline(context) + context.workspace.status_text_set(text=None) PolylineDecorator.uninstall() tool.Snap.clear_polyline() tool.Blender.update_viewport() diff --git a/src/bonsai/bonsai/bim/module/project/operator.py b/src/bonsai/bonsai/bim/module/project/operator.py index 4eef869eff..97190f1b94 100644 --- a/src/bonsai/bonsai/bim/module/project/operator.py +++ b/src/bonsai/bonsai/bim/module/project/operator.py @@ -2283,18 +2283,22 @@ class MeasureTool(bpy.types.Operator, PolylineOperator): self.input_ui = tool.Polyline.create_input_ui(init_z=True) self.input_options = ["D", "A", "X", "Y", "Z"] self.instructions = """TAB: Cycle Input - M: Modify Snap Point - C: Close - Backspace: Remove - X Y Z: Axis - S-(X Y Z): Plane - Shift: Lock axis -""" + D: Distance Input + A: Angle Input + M: Modify Snap Point + C: Close Polyline + BACKSPACE: Remove Point + X, Y, Z: Choose Axis + S-X, S-Y, S-Z: Choose Plane + SHIFT: Lock axis + """ def modal(self, context, event): if event.type in {"MIDDLEMOUSE", "WHEELUPMOUSE", "WHEELDOWNMOUSE"}: return {"PASS_THROUGH"} + self.handle_instructions(context) + self.handle_mouse_move(context, event) self.choose_axis(event, z=True) @@ -2304,6 +2308,7 @@ class MeasureTool(bpy.types.Operator, PolylineOperator): self.handle_snap_selection(context, event) if not self.tool_state.is_input_on and event.value == "RELEASE" and event.type in {"RET", "NUMPAD_ENTER", "RIGHTMOUSE"}: + context.workspace.status_text_set(text=None) PolylineDecorator.uninstall() tool.Snap.clear_polyline() tool.Blender.update_viewport() From 158f11d58c56911d4a8b0b2f0686b2159a9807ce Mon Sep 17 00:00:00 2001 From: myoualid Date: Tue, 10 Sep 2024 22:48:22 +0100 Subject: [PATCH 29/56] Cost web ui improvements: - hovering cost item row displays quick actions - clicking the quantity cells and cost cells prompts form to edit values - further style improvements & refactoring --- src/bonsai/bonsai/bim/data/webui/sioserver.py | 6 +- .../bonsai/bim/data/webui/static/css/cost.css | 37 ++- .../bonsai/bim/data/webui/static/js/cost.js | 70 ++--- .../data/webui/static/js/utilities/costui.js | 262 +++++++++++++----- src/bonsai/bonsai/tool/web.py | 12 +- 5 files changed, 265 insertions(+), 122 deletions(-) diff --git a/src/bonsai/bonsai/bim/data/webui/sioserver.py b/src/bonsai/bonsai/bim/data/webui/sioserver.py index 91cec745e5..e80173c474 100644 --- a/src/bonsai/bonsai/bim/data/webui/sioserver.py +++ b/src/bonsai/bonsai/bim/data/webui/sioserver.py @@ -183,10 +183,10 @@ class BlenderNamespace(socketio.AsyncNamespace): blender_messages[sid]["predefined_types"] = data await sio.emit("predefined_types", {"blenderId": sid, "data": data}, namespace="/web") - async def on_selected_products(self, sid, data): + async def on_quantities(self, sid, data): print(f"Selected products from Blender client {sid}") - blender_messages[sid]["selected_products"] = data - await sio.emit("selected_products", {"blenderId": sid, "data": data}, namespace="/web") + blender_messages[sid]["quantities"] = data + await sio.emit("quantities", {"blenderId": sid, "data": data}, namespace="/web") async def schedules(request): with open("templates/index.html", "r") as f: diff --git a/src/bonsai/bonsai/bim/data/webui/static/css/cost.css b/src/bonsai/bonsai/bim/data/webui/static/css/cost.css index bf2a85a9df..c0ad9a8e16 100644 --- a/src/bonsai/bonsai/bim/data/webui/static/css/cost.css +++ b/src/bonsai/bonsai/bim/data/webui/static/css/cost.css @@ -53,7 +53,6 @@ body { margin-bottom: 10px; } - .form-container { padding: var(--padding-medium); overflow-y: auto; @@ -155,7 +154,6 @@ body { table-layout: fixed; } - [id^="selected-products"] .form-container { height: 50vh; } @@ -186,7 +184,6 @@ tbody { td, th { - border-left: 1px solid #ddd; border-right: 1px solid #ddd; text-align: center; } @@ -304,8 +301,38 @@ form table { max-height: 50%; } - .subtotal-row { background-color: var(--button-hover-background); font-weight: bold; -} \ No newline at end of file +} + +[id^="cost-items"] th:nth-child(1), +[id^="cost-items"] td:nth-child(1) { + width: auto; +} +[id^="cost-items"] th:not(:nth-child(1)), +[id^="cost-items"] td:not(:nth-child(1)) { + width: 10%; +} + +[id^="cost-items"] th:last-child, +[id^="cost-items"] td:last-child { + width: fit-content; + min-width: fit-content; + max-width: 50%; +} + +.actions-column { + transition: opacity 0.3s ease-in-out; + opacity: 0; +} + +[id^="cost-items"] tr:hover .actions-column { + opacity: 1; +} + +.clickable-cell:hover { + border: 1px solid #28a746; + border-radius: 6px; + cursor: pointer; +} diff --git a/src/bonsai/bonsai/bim/data/webui/static/js/cost.js b/src/bonsai/bonsai/bim/data/webui/static/js/cost.js index 22fca33d5a..74943c9426 100644 --- a/src/bonsai/bonsai/bim/data/webui/static/js/cost.js +++ b/src/bonsai/bonsai/bim/data/webui/static/js/cost.js @@ -28,53 +28,25 @@ function connectSocket() { socket.on("cost_items", handleCostItemsData); socket.on("cost_values", handleCostValuesData); socket.on("cost_value", handleCostValueData); - socket.on("selected_products", handleSelectedProducts); + socket.on("quantities", handleEditQuantities); } -function handleSelectedProducts(data) { - const costItemId = data.data["selected_products"]["cost_item_id"]; - const products = data.data["selected_products"]["selected_products"]; - const assigned_products = data.data["selected_products"]["assigned_products"]; - const quantityNames = - data.data["selected_products"]["product_quantity_names"]; - const formId = "selected-products-" + costItemId; - const form = CostUI.Form({ - id: formId, - name: "Edit Product Assignments for: " + CostUI.getCostItemName(costItemId), - icon: "fa-solid fa-box", - }); - const numberOfProducts = CostUI.Text( - "Selection basket : " + products.length + " products", - "fa-solid fa-cart-shopping", - "large" - ); - form.appendChild(numberOfProducts); - CostUI.highlightElement(costItemId); - const selectedProductsTable = CostUI.createProductTable({ - form, - products, - quantityNames, - costItemId, +function handleEditQuantities(data) { + const costItemId = data.data["quantities"]["cost_item_id"]; + const products = data.data["quantities"]["selected_products"]; + const assigned_products = data.data["quantities"]["assigned_products"]; + const quantityNames = data.data["quantities"]["product_quantity_names"]; + + CostUI.editQuantities({ + costItemId: costItemId, + selectedProducts: products, + assignedProducts: assigned_products, + quantityNames: quantityNames, callbacks: { addProductAssignments: addProductAssignments, - getSelectedProducts: getSelectedProducts, + enableEditingQuantities: enableEditingQuantities, }, }); - - if (assigned_products.length > 0) { - const assignedProductsText = CostUI.Text( - "Assigned Products", - "fa-solid fa-solid fa-paperclip", - "large" - ); - form.appendChild(assignedProductsText); - const assignmentsTable = CostUI.createAssignmentsTable({ - form, - products: assigned_products, - quantityNames, - costItemId, - }); - } } function handlePredefinedTypes(data) { @@ -322,9 +294,6 @@ function handleCostSchedulesData(data) { if (costSchedules.length === 0) { return; } - const currency = data.data["cost_schedules"]["currency"] - ? data.data["cost_schedules"]["currency"]["name"] - : "Undefined"; const costScheduleDiv = document.getElementById("cost-schedules"); costScheduleDiv.innerHTML = ""; costSchedules.forEach((costSchedule) => { @@ -368,8 +337,13 @@ function handleCostItemsData(data) { loadedSchedules[data.blenderId] = costScheduleId; CostUI.highlightElement("schedule-" + costScheduleId); + const currency = data.data["cost_items"]["currency"] + ? data.data["cost_items"]["currency"]["name"] + : "Undefined"; + CostUI.createCostSchedule({ - data: data.data["cost_items"]["cost_items"], + costItems: data.data["cost_items"]["cost_items"], + currency: currency, costScheduleId: costScheduleId, blenderID: data.blenderId, callbacks: { @@ -380,13 +354,13 @@ function handleCostItemsData(data) { editCostItemName: editCostItemName, enableEditingCostValues: enableEditingCostValues, addSummaryCostItem: addSummaryCostItem, - getSelectedProducts: getSelectedProducts, + enableEditingQuantities: enableEditingQuantities, }, }); } -function getSelectedProducts(costItemId) { - executeOperator({ type: "getSelectedProducts", costItemId: costItemId }); +function enableEditingQuantities(costItemId) { + executeOperator({ type: "enableEditingQuantities", costItemId: costItemId }); } function duplicateCostItem(costItemId) { diff --git a/src/bonsai/bonsai/bim/data/webui/static/js/utilities/costui.js b/src/bonsai/bonsai/bim/data/webui/static/js/utilities/costui.js index a3f6c76882..0a94fb4d50 100644 --- a/src/bonsai/bonsai/bim/data/webui/static/js/utilities/costui.js +++ b/src/bonsai/bonsai/bim/data/webui/static/js/utilities/costui.js @@ -9,7 +9,7 @@ export class CostUI { static removeCostSchedule(id) { document.getElementById("cost-items-" + id).remove(); } - static createTable(id, callbacks) { + static createCostTable(id, currency, callbacks) { CostUI.isCostScheduleLoaded(id) ? CostUI.removeCostSchedule(id) : null; const table = document.createElement("table"); @@ -21,9 +21,9 @@ export class CostUI { "Name", "Quantity", "Unit", - "Cost", - "Total Cost", - "Action", + "Cost (" + currency + ")", + "Total Cost (" + currency + ")", + "Actions", ]; const thead = document.createElement("thead"); const tr = document.createElement("tr"); @@ -47,7 +47,6 @@ export class CostUI { table.appendChild(tbody); document.getElementById("cost-items").appendChild(table); - CostUI.addTableStyles(id); CostUI.createContextMenu(callbacks); table.get_blender_id = function () { return this.getAttribute("id").split("-")[2]; @@ -56,20 +55,12 @@ export class CostUI { return [table, tbody]; } - static addTableStyles(id) { - const style = document.createElement("style"); - style.textContent = ` - #cost-items-${id} th:nth-child(1), - #cost-items-${id} td:nth-child(1) { - width: auto; - } - #cost-items-${id} th:not(:nth-child(1)), - #cost-items-${id} td:not(:nth-child(1)) { - width: 100px; /* Set a fixed width for other columns */ - } - - `; - document.head.appendChild(style); + static deleteCostItem(costItemId, callback) { + const costItemRow = document.getElementById(costItemId); + let expandedState = JSON.parse(localStorage.getItem("expandedState")) || {}; + expandedState = CostUI.deleteCostItemRow(costItemRow, expandedState); + localStorage.setItem("expandedState", JSON.stringify(expandedState)); + callback(costItemId); } static createContextMenu(callbacks) { @@ -139,21 +130,17 @@ export class CostUI { addButton.dataset.listenerAdded = "true"; } - const deleteCostItem = document.getElementById("delete-cost-item"); - if (deleteCostItem && !deleteCostItem.hasListener) { - deleteCostItem.addEventListener("click", function () { + const deleteCostItemButton = document.getElementById("delete-cost-item"); + if (deleteCostItemButton && !deleteCostItemButton.hasListener) { + deleteCostItemButton.addEventListener("click", function () { const targetRow = document.getElementById("context-menu").targetRow; if (targetRow) { const costItemId = parseInt(targetRow.getAttribute("id")); - let expandedState = - JSON.parse(localStorage.getItem("expandedState")) || {}; - expandedState = CostUI.deleteCostItemRow(targetRow, expandedState); - localStorage.setItem("expandedState", JSON.stringify(expandedState)); - callbacks.deleteCostItem(costItemId); + CostUI.deleteCostItem(costItemId, callbacks.deleteCostItem); } document.getElementById("context-menu").style.display = "none"; }); - deleteCostItem.hasListener = true; + deleteCostItemButton.hasListener = true; } const duplicateButton = document.getElementById("duplicate-button"); @@ -175,7 +162,7 @@ export class CostUI { const targetRow = document.getElementById("context-menu").targetRow; if (targetRow) { const costItemId = parseInt(targetRow.getAttribute("id")); - callbacks.getSelectedProducts(costItemId); + callbacks.enableEditingQuantities(costItemId); } document.getElementById("context-menu").style.display = "none"; }); @@ -192,7 +179,7 @@ export class CostUI { document .getElementById("cost-items") - .addEventListener("dblclick", function (event) { + .addEventListener("click", function (event) { const targetRow = event.target.closest("tr"); const targetCell = event.target.closest("td"); if (targetRow && targetCell) { @@ -202,11 +189,16 @@ export class CostUI { const costItemId = parseInt(targetRow.getAttribute("id")); const columnName = getColumnNames("cost-items")[columnIndex]; - if (columnName === "Cost") { + if (columnName.includes("Cost") && !columnName.includes("Total")) { callbacks.enableEditingCostValues ? callbacks.enableEditingCostValues(costItemId) : null; } + if (columnName === "Quantity") { + callbacks.enableEditingQuantities + ? callbacks.enableEditingQuantities(costItemId) + : null; + } } }); } @@ -363,13 +355,18 @@ export class CostUI { } static createCostSchedule({ - data, + costItems, + currency, costScheduleId, blenderID, callbacks = {}, }) { - const [table, tbody] = CostUI.createTable(blenderID, callbacks); - if (data.length === 0) { + const [table, tbody] = CostUI.createCostTable( + blenderID, + currency, + callbacks + ); + if (costItems.length === 0) { const tr = document.createElement("tr"); const td = document.createElement("td"); td.colSpan = 6; @@ -387,24 +384,29 @@ export class CostUI { td.appendChild(addSummaryCostItemButton); addSummaryCostItemButton.classList.add("action-button"); } else { - CostUI.createCostItem(data, tbody, 0, null, callbacks); + CostUI.createCostTree(costItems, tbody, 0, null, callbacks); CostUI.applyExpandedState(); } } - static createCostItem( - data, + static createCostTree( + costItems, container, nestingLevel = 0, parentID = null, callbacks = {} ) { - data.forEach((costItem) => { - const row = CostUI.createRow(costItem, nestingLevel, parentID, callbacks); + costItems.forEach((costItem) => { + const row = CostUI.addCostItemRow( + costItem, + nestingLevel, + parentID, + callbacks + ); container.appendChild(row); if (costItem.is_nested_by && costItem.is_nested_by.length > 0) { - CostUI.createCostItem( + CostUI.createCostTree( costItem.is_nested_by, container, nestingLevel + 1, @@ -415,7 +417,7 @@ export class CostUI { }); } - static createRow(costItem, nestingLevel, parentID, callbacks = {}) { + static addCostItemRow(costItem, nestingLevel, parentID, callbacks = {}) { const totalQuantity = costItem.TotalCostQuantity ? parseFloat(costItem.TotalCostQuantity).toFixed(2) : "-"; @@ -431,20 +433,21 @@ export class CostUI { callbacks ); const totalCostQuantityCell = CostUI.createTableCell(totalQuantity); + totalCostQuantityCell.classList.add("clickable-cell"); const unitSymbolCell = CostUI.createTableCell(costItem.UnitSymbol); const totalAppliedValueCell = CostUI.createTableCell(appliedValue); + totalAppliedValueCell.classList.add("clickable-cell"); const totalCostCell = CostUI.createTotalCostCell(costItem); - const flexContainerCell = CostUI.createFlexContainerCell( - costItem, - callbacks - ); + const actionsCell = CostUI.costItemActions(costItem, callbacks); + + actionsCell.classList.add("actions-column"); row.appendChild(nameCell); row.appendChild(totalCostQuantityCell); row.appendChild(unitSymbolCell); row.appendChild(totalAppliedValueCell); row.appendChild(totalCostCell); - row.appendChild(flexContainerCell); + row.appendChild(actionsCell); row.get_id = function () { return this.getAttribute("id"); @@ -636,30 +639,92 @@ export class CostUI { return totalCostCell; } - static createFlexContainerCell(costItem, callbacks) { + static costItemActions(costItem, callbacks) { const divFlex = document.createElement("div"); divFlex.classList.add("row-container"); - const selectButton = CostUI.createSelectButton(costItem, callbacks); - divFlex.appendChild(selectButton); + const addCostItem = CostUI.addCostItemButton( + costItem.id, + callbacks.addCostItem + ); + const selectButton = CostUI.createSelectButton( + costItem.id, + callbacks.selectAssignedElements + ); + const deleteButton = CostUI.deleteCostItemButton( + costItem.id, + callbacks.deleteCostItem + ); + const duplicateButton = CostUI.duplicateCostItemButton( + costItem.id, + callbacks.duplicateCostItem + ); - const flexContainerCell = document.createElement("td"); - flexContainerCell.appendChild(divFlex); - return flexContainerCell; + [addCostItem, duplicateButton, selectButton, deleteButton].forEach( + (button) => { + divFlex.appendChild(button); + } + ); + + const actionsCell = document.createElement("td"); + actionsCell.appendChild(divFlex); + return actionsCell; } - static createSelectButton(costItem, callbacks) { - const selectButton = document.createElement("button"); - selectButton.classList.add("action-button"); - selectButton.textContent = "Select"; - selectButton.addEventListener("click", function (e) { - e.stopPropagation(); - callbacks.selectAssignedElements - ? callbacks.selectAssignedElements(costItem.id) - : null; - }); + static duplicateCostItemButton(costItemId, duplicateCostItem) { + const duplicateButton = CostUI.createButton( + "Duplicate", + "fa-solid fa-copy" + ); + duplicateButton.addEventListener( + "click", + duplicateCostItem.bind(null, costItemId) + ); + return duplicateButton; + } + + static addCostItemButton(costItemId, addCostItem) { + const addCostItemButton = CostUI.createButton( + "Add Sub-Cost", + "fa-solid fa-plus" + ); + addCostItemButton.addEventListener( + "click", + addCostItem.bind(null, costItemId) + ); + return addCostItemButton; + } + + static createSelectButton(costItemId, selectAssignedElements) { + const selectButton = CostUI.createButton( + "Select", + "fa-solid fa-arrow-pointer" + ); + selectButton.addEventListener( + "click", + selectAssignedElements.bind(null, costItemId) + ); return selectButton; } + static deleteCostItemButton(costItemId, deleteCostItem) { + const deleteButton = CostUI.createButton("Delete", "fa-solid fa-trash"); + deleteButton.addEventListener( + "click", + CostUI.deleteCostItem.bind(null, costItemId, deleteCostItem) + ); + return deleteButton; + } + + static createButton(text, icon) { + const button = document.createElement("button"); + !icon ? (button.textContent = text) : null; + icon + ? button.classList.add("action-button", ...icon.split(" ")) + : button.classList.add("action-button"); + button.dataset.tooltip = text; + return button; + } + static highlightElement(id) { const element = document.getElementById(id); if (element) { @@ -1479,6 +1544,7 @@ export class CostUI { const picker = CostUI.createColorPicker(); const tableFontSize = CostUI.createFontSizePicker(); + const currencyPicker = CostUI.createCurrencyPicker(); settingsMenu.appendChild(picker); settingsMenu.appendChild(tableFontSize); document.getElementById("settings-menu").style.display = "none"; @@ -1521,6 +1587,32 @@ export class CostUI { return div; } + static createCurrencyPicker() { + const currency = CostUI.getCurrency(); + const div = document.createElement("div"); + const currencyText = document.createElement("p"); + currencyText.textContent = "Select a currency for the table:"; + div.appendChild(currencyText); + const currencyPicker = document.createElement("input"); + currencyPicker.type = "text"; + currencyPicker.id = "currency-picker"; + currencyPicker.value = currency; + div.appendChild(currencyPicker); + currencyPicker.addEventListener("input", function () { + const currency = currencyPicker.value; + CostUI.saveCurrency(currency); + }); + return div; + } + + static saveCurrency(currency) { + localStorage.setItem("tableCurrency", currency); + } + + static getCurrency() { + return localStorage.getItem("tableCurrency"); + } + static saveFontSize(fontSize) { localStorage.setItem("tableFontSize", fontSize); } @@ -1637,4 +1729,50 @@ export class CostUI { }, }); } + + static editQuantities({ + costItemId, + selectedProducts, + quantityNames, + assignedProducts, + callbacks, + }) { + const formId = "selected-products-" + costItemId; + const form = CostUI.Form({ + id: formId, + name: + "Edit Product Assignments for: " + CostUI.getCostItemName(costItemId), + icon: "fa-solid fa-box", + }); + const numberOfProducts = CostUI.Text( + "Selection basket : " + selectedProducts.length + " products", + "fa-solid fa-cart-shopping", + "large" + ); + form.appendChild(numberOfProducts); + CostUI.highlightElement(costItemId); + const selectedProductsTable = CostUI.createProductTable({ + form, + products: selectedProducts, + quantityNames, + costItemId, + callbacks, + }); + + if (assignedProducts.length > 0) { + const assignedProductsText = CostUI.Text( + "Assigned Products", + "fa-solid fa-solid fa-paperclip", + "large" + ); + form.appendChild(assignedProductsText); + const assignmentsTable = CostUI.createAssignmentsTable({ + form, + products: assignedProducts, + quantityNames, + costItemId, + callbacks, + }); + } + } } diff --git a/src/bonsai/bonsai/tool/web.py b/src/bonsai/bonsai/tool/web.py index 5408efd898..5a4df5e2b2 100644 --- a/src/bonsai/bonsai/tool/web.py +++ b/src/bonsai/bonsai/tool/web.py @@ -425,7 +425,7 @@ class Web(bonsai.core.tool.Web): cost_item = tool.Ifc.get().by_id(operator_data["costItemId"]) products = tool.Cost.get_cost_item_products(cost_item, is_deep=True) tool.Spatial.select_products(products, unhide=True) - if operator_data["type"] == "getSelectedProducts": + if operator_data["type"] == "enableEditingQuantities": cost_item_id = operator_data["costItemId"] cost_item = ifc_file.by_id(cost_item_id) if not cost_item: @@ -446,8 +446,8 @@ class Web(bonsai.core.tool.Web): "product_quantity_names": names, "cost_item_id": cost_item_id, }, - data_key="selected_products", - event="selected_products", + data_key="quantities", + event="quantities", ) if operator_data["type"] == "addSummaryCostItem": @@ -531,7 +531,11 @@ class Web(bonsai.core.tool.Web): def load_cost_schedule_web_ui(cls, cost_schedule): json_data = tool.Cost.create_cost_schedule_json(cost_schedule) cls.send_webui_data( - data={"cost_items": json_data, "cost_schedule_id": cost_schedule.id()}, + data={ + "cost_items": json_data, + "cost_schedule_id": cost_schedule.id(), + "currency": tool.Cost.currency(), + }, data_key="cost_items", event="cost_items", ) From bad6931a84a1e0c7a578fc5b66b4a9ba35fced73 Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Wed, 11 Sep 2024 12:26:39 +1000 Subject: [PATCH 30/56] Fix bug in tessellate elements --- src/ifcpatch/ifcpatch/recipes/TessellateElements.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/ifcpatch/ifcpatch/recipes/TessellateElements.py b/src/ifcpatch/ifcpatch/recipes/TessellateElements.py index 0769e2027e..06929cea59 100644 --- a/src/ifcpatch/ifcpatch/recipes/TessellateElements.py +++ b/src/ifcpatch/ifcpatch/recipes/TessellateElements.py @@ -92,7 +92,7 @@ class Patcher: ) -> None: geometry = getattr(shape, "geometry", shape) v = [[x.tolist() for x in ifcopenshell.util.shape.get_vertices(geometry)]] - f = [ifcopenshell.util.shape.get_faces(geometry)] + f = [ifcopenshell.util.shape.get_faces(geometry).tolist()] replacements[element] = (v, f) iterator = ifcopenshell.geom.iterator(settings, self.file, multiprocessing.cpu_count(), include=products) @@ -112,6 +112,8 @@ class Patcher: # Do the replacements outside the iterator to prevent messing up iterator state. for element, geometry in replacements.items(): v, f = geometry + if not v or not f: + continue mesh = ifcopenshell.api.run( "geometry.add_mesh_representation", self.file, From f8f3519a68dfb99cdf842f5c7c1767ef9680ef18 Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Wed, 11 Sep 2024 13:45:11 +1000 Subject: [PATCH 31/56] Minor bugfix to allow unaggregation of hidden elements --- src/bonsai/bonsai/bim/module/aggregate/operator.py | 2 +- src/bonsai/bonsai/tool/spatial.py | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/bonsai/bonsai/bim/module/aggregate/operator.py b/src/bonsai/bonsai/bim/module/aggregate/operator.py index 187d1cb648..d25343fad8 100644 --- a/src/bonsai/bonsai/bim/module/aggregate/operator.py +++ b/src/bonsai/bonsai/bim/module/aggregate/operator.py @@ -76,7 +76,7 @@ class BIM_OT_aggregate_unassign_object(bpy.types.Operator, tool.Ifc.Operator): bl_options = {"REGISTER", "UNDO"} def _execute(self, context): - for obj in bpy.context.selected_objects: + for obj in tool.Blender.get_selected_objects(): element = tool.Ifc.get_entity(obj) if not element: continue diff --git a/src/bonsai/bonsai/tool/spatial.py b/src/bonsai/bonsai/tool/spatial.py index e4147c61bc..d9442bd356 100644 --- a/src/bonsai/bonsai/tool/spatial.py +++ b/src/bonsai/bonsai/tool/spatial.py @@ -48,8 +48,7 @@ from natsort import natsorted class Spatial(bonsai.core.tool.Spatial): @classmethod def can_contain(cls, container: ifcopenshell.entity_instance, element_obj: Union[bpy.types.Object, None]) -> bool: - element = tool.Ifc.get_entity(element_obj) - if not element: + if not (element := tool.Ifc.get_entity(element_obj)): return False if tool.Ifc.get_schema() == "IFC2X3": if not container.is_a("IfcSpatialStructureElement"): From 5fe85e71bbe795efe5f447bc2e1f420f4c691033 Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Wed, 11 Sep 2024 18:03:46 +1000 Subject: [PATCH 32/56] Work in progress example to import multiple representation items as an object-based breakdown --- .../bonsai/bim/module/geometry/__init__.py | 2 + .../bonsai/bim/module/geometry/decorator.py | 138 ++++++++++++++++++ .../bonsai/bim/module/geometry/operator.py | 93 ++++++++++++ src/bonsai/bonsai/bim/module/geometry/prop.py | 13 +- .../bonsai/bim/module/project/operator.py | 1 + src/bonsai/bonsai/tool/root.py | 7 + 6 files changed, 253 insertions(+), 1 deletion(-) create mode 100644 src/bonsai/bonsai/bim/module/geometry/decorator.py diff --git a/src/bonsai/bonsai/bim/module/geometry/__init__.py b/src/bonsai/bonsai/bim/module/geometry/__init__.py index f07aa0b19f..1caba354ba 100644 --- a/src/bonsai/bonsai/bim/module/geometry/__init__.py +++ b/src/bonsai/bonsai/bim/module/geometry/__init__.py @@ -36,6 +36,7 @@ classes = ( operator.EnableEditingRepresentationItems, operator.FlipObject, operator.GetRepresentationIfcParameters, + operator.ImportRepresentationItems, operator.OverrideDelete, operator.OverrideDuplicateMove, operator.OverrideDuplicateMoveLinked, @@ -60,6 +61,7 @@ classes = ( operator.UpdateParametricRepresentation, operator.UpdateRepresentation, prop.RepresentationItem, + prop.RepresentationItemObject, prop.ShapeAspect, prop.BIMObjectGeometryProperties, prop.BIMGeometryProperties, diff --git a/src/bonsai/bonsai/bim/module/geometry/decorator.py b/src/bonsai/bonsai/bim/module/geometry/decorator.py new file mode 100644 index 0000000000..4db123c04e --- /dev/null +++ b/src/bonsai/bonsai/bim/module/geometry/decorator.py @@ -0,0 +1,138 @@ +# Bonsai - OpenBIM Blender Add-on +# Copyright (C) 2024 Dion Moult +# +# This file is part of Bonsai. +# +# Bonsai is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Bonsai 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Bonsai. If not, see . + +import blf +import gpu +import json +import bmesh +import bonsai.tool as tool +from bpy.types import SpaceView3D +from mathutils import Vector +from gpu_extras.batch import batch_for_shader +from bpy_extras.view3d_utils import location_3d_to_region_2d + + +class ItemDecorator: + is_installed = False + handlers = [] + + @classmethod + def install(cls, context): + if cls.is_installed: + cls.uninstall() + handler = cls() + cls.handlers.append(SpaceView3D.draw_handler_add(handler.draw_text, (context,), "WINDOW", "POST_PIXEL")) + cls.handlers.append(SpaceView3D.draw_handler_add(handler.draw, (context,), "WINDOW", "POST_VIEW")) + cls.is_installed = True + + @classmethod + def uninstall(cls): + for handler in cls.handlers: + try: + SpaceView3D.draw_handler_remove(handler, "WINDOW") + except ValueError: + pass + cls.is_installed = False + + def draw_batch(self, shader_type, content_pos, color, indices=None): + shader = self.line_shader if shader_type == "LINES" else self.shader + batch = batch_for_shader(shader, shader_type, {"pos": content_pos}, indices=indices) + shader.uniform_float("color", color) + batch.draw(shader) + + def draw_text(self, context): + self.addon_prefs = tool.Blender.get_addon_preferences() + selected_elements_color = self.addon_prefs.decorator_color_selected + unselected_elements_color = self.addon_prefs.decorator_color_unselected + special_elements_color = self.addon_prefs.decorator_color_special + + font_id = 0 + blf.size(font_id, 12) + blf.enable(font_id, blf.SHADOW) + color = selected_elements_color + blf.color(font_id, *color) + + for item in context.scene.BIMGeometryProperties.item_objs: + if (obj := item.obj) and obj.hide_get() == False: + if obj.select_get(): + centroid = obj.matrix_world @ Vector(obj.bound_box[0]).lerp(Vector(obj.bound_box[6]), 0.5) + tag = obj.name.split("/")[1] + coords_2d = location_3d_to_region_2d(context.region, context.region_data, centroid) + if coords_2d: + w, h = blf.dimensions(font_id, tag) + coords_2d -= Vector((w * 0.5, h * 0.5)) + blf.position(font_id, coords_2d[0], coords_2d[1], 0) + blf.draw(font_id, tag) + + def draw(self, context): + def transparent_color(color, alpha=0.3): + color = [i for i in color] + color[3] = alpha + return color + + self.addon_prefs = tool.Blender.get_addon_preferences() + selected_elements_color = self.addon_prefs.decorator_color_selected + unselected_elements_color = self.addon_prefs.decorator_color_unselected + special_elements_color = self.addon_prefs.decorator_color_special + + gpu.state.point_size_set(6) + gpu.state.blend_set("ALPHA") + + self.line_shader = gpu.shader.from_builtin("POLYLINE_UNIFORM_COLOR") + self.line_shader.bind() # required to be able to change uniforms of the shader + # POLYLINE_UNIFORM_COLOR specific uniforms + self.line_shader.uniform_float("viewportSize", (context.region.width, context.region.height)) + self.line_shader.uniform_float("lineWidth", 2.0) + + # general shader + self.shader = gpu.shader.from_builtin("UNIFORM_COLOR") + + selected_verts = [] + selected_edges = [] + selected_tris = [] + unselected_verts = [] + unselected_edges = [] + unselected_tris = [] + for item in context.scene.BIMGeometryProperties.item_objs: + if (obj := item.obj) and obj.hide_get() == False: + if obj.select_get(): + if context.mode != "OBJECT": + continue + edges = selected_edges + verts = selected_verts + offset = len(selected_verts) + selected_verts.extend([tuple(obj.matrix_world @ v.co) for v in obj.data.vertices]) + selected_tris.extend([tuple([i + offset for i in t.vertices]) for t in obj.data.loop_triangles]) + else: + offset = len(unselected_verts) + unselected_verts.extend([tuple(obj.matrix_world @ v.co) for v in obj.data.vertices]) + unselected_tris.extend([tuple([i + offset for i in t.vertices]) for t in obj.data.loop_triangles]) + edges = unselected_edges + verts = unselected_verts + i = len(verts) + edges.extend([[ei + i for ei in e] for e in json.loads(item.edges)]) + matrix_world = obj.matrix_world + verts.extend([matrix_world @ Vector(v) for v in json.loads(item.verts)]) + + if unselected_verts: + self.draw_batch("LINES", unselected_verts, transparent_color(unselected_elements_color), unselected_edges) + self.draw_batch("TRIS", unselected_verts, transparent_color(special_elements_color), unselected_tris) + + if selected_verts: + self.draw_batch("LINES", selected_verts, selected_elements_color, selected_edges) + self.draw_batch("TRIS", selected_verts, transparent_color(selected_elements_color), selected_tris) diff --git a/src/bonsai/bonsai/bim/module/geometry/operator.py b/src/bonsai/bonsai/bim/module/geometry/operator.py index 4991f18978..cd154017e9 100644 --- a/src/bonsai/bonsai/bim/module/geometry/operator.py +++ b/src/bonsai/bonsai/bim/module/geometry/operator.py @@ -2214,3 +2214,96 @@ class RemoveRepresentationItemFromShapeAspect(bpy.types.Operator, tool.Ifc.Opera if not styled_item.Styles: ifc_file.remove(styled_item) + + +class ImportRepresentationItems(bpy.types.Operator, tool.Ifc.Operator): + bl_idname = "bim.xxx_import_representation_items" + bl_label = "Import Representation Items" + bl_options = {"REGISTER", "UNDO"} + + def _execute(self, context): + obj = context.active_object + tool.Geometry.apply_item_ids_as_vertex_groups(obj) + tool.Geometry.dissolve_triangulated_edges(obj) + bm_dict = self.separate_faces_by_vertex_group(obj) + # bm_dict = self.separate_faces_by_id(obj) + props = context.scene.BIMGeometryProperties + props.item_objs.clear() + + for item_id, bm in bm_dict.items(): + item_mesh = bpy.data.meshes.new(f"mesh_id_{item_id}") + bm.to_mesh(item_mesh) + bm.free() + + item = tool.Ifc.get().by_id(item_id) + item_obj = bpy.data.objects.new(f"Item/{item.is_a()}/{item_id}", item_mesh) + item_obj.matrix_world = obj.matrix_world + item_obj.show_in_front = True + bpy.context.collection.objects.link(item_obj) + + new = props.item_objs.add() + new.obj = item_obj + + verts = [list(co) for co in item_obj.bound_box] + edges = [(0, 3), (3, 7), (7, 4), (4, 0), (0, 1), (3, 2), (7, 6), (4, 5), (1, 2), (2, 6), (6, 5), (5, 1)] + new.verts = json.dumps(verts) + new.edges = json.dumps(edges) + + def separate_faces_by_id(self, obj): + mesh = obj.data + bm = bmesh.new() + bm.from_mesh(mesh) + results = {} + face_ids = mesh["ios_item_ids"] + unique_ids = set(face_ids) + + for item_id in unique_ids: + item_bm = bmesh.new() + for face, face_id in zip(bm.faces, face_ids): + if face_id == item_id: + # Copy the face and its vertices into the new BMesh + new_face_verts = [item_bm.verts.new(v.co) for v in face.verts] + item_bm.faces.new(new_face_verts) + item_bm.verts.ensure_lookup_table() + item_bm.faces.ensure_lookup_table() + results[item_id] = item_bm + + return results + + def separate_faces_by_vertex_group(self, obj): + mesh = obj.data + bm = bmesh.new() + bm.from_mesh(mesh) + + # Ensure the bmesh has up-to-date vertex weights (vertex groups) + bm.verts.layers.deform.verify() + + results = {} + for vgroup in obj.vertex_groups: + group_bm = bmesh.new() + deform_layer = bm.verts.layers.deform.active + + # Iterate over the faces and check if all vertices of the face belong to the vertex group + for face in bm.faces: + face_in_group = True + for vert in face.verts: + # Get the vertex groups the vertex belongs to + deform = vert[deform_layer] + + # If this vertex does not belong to the current vertex group, mark the face as outside the group + if vgroup.index not in deform: + face_in_group = False + break + + # If all vertices of the face belong to the current vertex group, add the face to the new BMesh + if face_in_group: + new_face_verts = [group_bm.verts.new(v.co) for v in face.verts] + group_bm.faces.new(new_face_verts) + + # Ensure the mesh is valid and doesn't have duplicate elements + group_bm.verts.ensure_lookup_table() + group_bm.faces.ensure_lookup_table() + + results[int(vgroup.name.split("_")[3])] = group_bm + + return results diff --git a/src/bonsai/bonsai/bim/module/geometry/prop.py b/src/bonsai/bonsai/bim/module/geometry/prop.py index 641695a16e..1bb06837d8 100644 --- a/src/bonsai/bonsai/bim/module/geometry/prop.py +++ b/src/bonsai/bonsai/bim/module/geometry/prop.py @@ -18,7 +18,7 @@ import bpy import bonsai.tool as tool -from bonsai.bim.prop import StrProperty, Attribute +from bonsai.bim.prop import StrProperty, Attribute, ObjProperty from bonsai.bim.module.geometry.data import RepresentationsData, ViewportData from bpy.types import PropertyGroup from bpy.props import ( @@ -100,6 +100,16 @@ class RepresentationItem(PropertyGroup): tags: StringProperty(name="Tags") +class RepresentationItemObject(PropertyGroup): + name: StringProperty(name="Name") + ifc_definition_id: IntProperty(name="IFC Definition ID") + obj: PointerProperty(type=bpy.types.Object) + verts: StringProperty(name="Verts") + edges: StringProperty(name="Edges") + special_verts: StringProperty(name="Special Verts") + special_edges: StringProperty(name="Special Edges") + + class ShapeAspect(PropertyGroup): name: StringProperty( name="Name", @@ -137,6 +147,7 @@ class BIMGeometryProperties(PropertyGroup): should_force_triangulation: BoolProperty(name="Force Triangulation", default=False) is_changing_mode: BoolProperty(name="Is Changing Mode", default=False) mode: EnumProperty(items=get_mode, name="IFC Interaction Mode", update=update_mode) + item_objs: CollectionProperty(name="Item Objects", type=RepresentationItemObject) def is_object_valid_for_representation_copy(self, obj: bpy.types.Object) -> bool: return bool(obj != bpy.context.active_object and obj.data) diff --git a/src/bonsai/bonsai/bim/module/project/operator.py b/src/bonsai/bonsai/bim/module/project/operator.py index 97190f1b94..3ea324506e 100644 --- a/src/bonsai/bonsai/bim/module/project/operator.py +++ b/src/bonsai/bonsai/bim/module/project/operator.py @@ -820,6 +820,7 @@ class LoadProjectElements(bpy.types.Operator): tool.Project.set_default_context() tool.Project.set_default_modeling_dimensions() tool.Root.reload_grid_decorator() + tool.Root.reload_item_decorator() return {"FINISHED"} def get_decomposition_elements(self): diff --git a/src/bonsai/bonsai/tool/root.py b/src/bonsai/bonsai/tool/root.py index 13a47b386c..0522ab6ea0 100644 --- a/src/bonsai/bonsai/tool/root.py +++ b/src/bonsai/bonsai/tool/root.py @@ -28,6 +28,7 @@ import bonsai.core.geometry import bonsai.tool as tool from typing import Union, Optional, Any from bonsai.bim.module.spatial.decorator import GridDecorator +from bonsai.bim.module.geometry.decorator import ItemDecorator class Root(bonsai.core.tool.Root): @@ -214,6 +215,12 @@ class Root(bonsai.core.tool.Root): new.obj = obj GridDecorator.install(bpy.context) + @classmethod + def reload_item_decorator(cls) -> None: + item_objs = bpy.context.scene.BIMGeometryProperties.item_objs + item_objs.clear() + ItemDecorator.install(bpy.context) + @classmethod def link_object_data(cls, source_obj: bpy.types.Object, destination_obj: bpy.types.Object) -> None: destination_obj.data = source_obj.data From c63f1cfa88b5b00836c5c8b002aae63257850958 Mon Sep 17 00:00:00 2001 From: Thomas Krijnen Date: Wed, 11 Sep 2024 11:18:09 +0200 Subject: [PATCH 33/56] Iterator: Don't crash on calling get() before initialize() --- src/ifcgeom/Iterator.h | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/ifcgeom/Iterator.h b/src/ifcgeom/Iterator.h index 8bf862def1..242cdb7418 100644 --- a/src/ifcgeom/Iterator.h +++ b/src/ifcgeom/Iterator.h @@ -701,6 +701,10 @@ namespace IfcGeom { /// Gets the representation of the current geometrical entity. Element* get() { + if (!initialization_outcome_) { + throw std::runtime_error("Iterator not initialized"); + } + auto ret = *task_result_iterator_; // If we want to organize the element considering their hierarchy From 32c6ea9363700dadc434c5ffd688585a3e72dd19 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Wed, 11 Sep 2024 15:38:09 +0500 Subject: [PATCH 34/56] shapebuilder.profile - fix bug in ifc2x3 assigning non existing Position attr confused arbitrary profiles with parametric profiles in 640320f --- src/ifcopenshell-python/ifcopenshell/util/shape_builder.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/ifcopenshell-python/ifcopenshell/util/shape_builder.py b/src/ifcopenshell-python/ifcopenshell/util/shape_builder.py index 57868109ba..2d059c1a7c 100644 --- a/src/ifcopenshell-python/ifcopenshell/util/shape_builder.py +++ b/src/ifcopenshell-python/ifcopenshell/util/shape_builder.py @@ -372,10 +372,6 @@ class ShapeBuilder: "ProfileType": profile_type, "OuterCurve": outer_curve, } - if self.file.schema == "IFC2X3": - kwargs["Position"] = self.file.create_entity( - "IfcAxis2Placement2D", self.file.create_entity("IfcCartesianPoint", [0.0, 0.0]) - ) if inner_curves: if not isinstance(inner_curves, collections.abc.Iterable): From f4c3ceb69370a7c6b64761fcc5fdf4f68d1a6a4b Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Wed, 11 Sep 2024 10:08:03 +0500 Subject: [PATCH 35/56] black format --- src/bonsai/bonsai/bim/module/model/decorator.py | 7 ------- src/bonsai/bonsai/bim/module/model/polyline.py | 3 +-- src/bonsai/bonsai/bim/module/model/wall.py | 6 +++++- src/bonsai/bonsai/bim/module/project/operator.py | 6 +++++- src/bonsai/bonsai/tool/polyline.py | 7 +++---- src/bonsai/bonsai/tool/snap.py | 10 ++++++---- src/bonsai/bonsai/tool/web.py | 2 +- 7 files changed, 21 insertions(+), 20 deletions(-) diff --git a/src/bonsai/bonsai/bim/module/model/decorator.py b/src/bonsai/bonsai/bim/module/model/decorator.py index 59f242b36f..e96bd74450 100644 --- a/src/bonsai/bonsai/bim/module/model/decorator.py +++ b/src/bonsai/bonsai/bim/module/model/decorator.py @@ -353,15 +353,12 @@ class PolylineDecorator: def set_tool_state(cls, tool_state): cls.tool_state = tool_state - - def draw_batch(self, shader_type, content_pos, color, indices=None): shader = self.line_shader if shader_type == "LINES" else self.shader batch = batch_for_shader(shader, shader_type, {"pos": content_pos}, indices=indices) shader.uniform_float("color", color) batch.draw(shader) - def draw_input_ui(self, context): texts = { "D": "Distance: ", @@ -373,7 +370,6 @@ class PolylineDecorator: } mouse_pos = self.event.mouse_region_x, self.event.mouse_region_y - self.addon_prefs = tool.Blender.get_addon_preferences() self.font_id = 0 font_size = tool.Blender.scale_font_size(12) @@ -400,7 +396,6 @@ class PolylineDecorator: blf.position(self.font_id, mouse_pos[0] + offset, mouse_pos[1] - (new_line * i), 0) blf.draw(self.font_id, field_name + formatted_value) - def draw_measurements(self, context): region = context.region rv3d = region.data @@ -433,8 +428,6 @@ class PolylineDecorator: blf.position(self.font_id, coords_angle[0], coords_angle[1], 0) blf.draw(self.font_id, "a: " + measurement_prop[i].angle) - - def __call__(self, context): self.addon_prefs = tool.Blender.get_addon_preferences() diff --git a/src/bonsai/bonsai/bim/module/model/polyline.py b/src/bonsai/bonsai/bim/module/model/polyline.py index be52340745..a828309696 100644 --- a/src/bonsai/bonsai/bim/module/model/polyline.py +++ b/src/bonsai/bonsai/bim/module/model/polyline.py @@ -169,7 +169,7 @@ class PolylineOperator: Snap: {self.snapping_points[0][1]} """ context.workspace.status_text_set(self.instructions + self.snap_info) - + def handle_keyboard_input(self, context, event): if self.tool_state.is_input_on and event.value == "PRESS" and event.type == "TAB": @@ -331,7 +331,6 @@ class PolylineOperator: tool.Snap.remove_last_polyline_point() tool.Blender.update_viewport() - def invoke(self, context, event): PolylineDecorator.install(context) tool.Snap.clear_snapping_point() diff --git a/src/bonsai/bonsai/bim/module/model/wall.py b/src/bonsai/bonsai/bim/module/model/wall.py index 53b3dccc7b..a5ddd83a7a 100644 --- a/src/bonsai/bonsai/bim/module/model/wall.py +++ b/src/bonsai/bonsai/bim/module/model/wall.py @@ -335,7 +335,11 @@ class DrawPolylineWall(bpy.types.Operator, PolylineOperator): self.handle_snap_selection(context, event) - if not self.tool_state.is_input_on and event.value == "RELEASE" and event.type in {"RET", "NUMPAD_ENTER", "RIGHTMOUSE"}: + if ( + not self.tool_state.is_input_on + and event.value == "RELEASE" + and event.type in {"RET", "NUMPAD_ENTER", "RIGHTMOUSE"} + ): self.create_walls_from_polyline(context) context.workspace.status_text_set(text=None) PolylineDecorator.uninstall() diff --git a/src/bonsai/bonsai/bim/module/project/operator.py b/src/bonsai/bonsai/bim/module/project/operator.py index 3ea324506e..178eaa49e0 100644 --- a/src/bonsai/bonsai/bim/module/project/operator.py +++ b/src/bonsai/bonsai/bim/module/project/operator.py @@ -2308,7 +2308,11 @@ class MeasureTool(bpy.types.Operator, PolylineOperator): self.handle_snap_selection(context, event) - if not self.tool_state.is_input_on and event.value == "RELEASE" and event.type in {"RET", "NUMPAD_ENTER", "RIGHTMOUSE"}: + if ( + not self.tool_state.is_input_on + and event.value == "RELEASE" + and event.type in {"RET", "NUMPAD_ENTER", "RIGHTMOUSE"} + ): context.workspace.status_text_set(text=None) PolylineDecorator.uninstall() tool.Snap.clear_polyline() diff --git a/src/bonsai/bonsai/tool/polyline.py b/src/bonsai/bonsai/tool/polyline.py index 758931e7f1..4c66316bd4 100644 --- a/src/bonsai/bonsai/tool/polyline.py +++ b/src/bonsai/bonsai/tool/polyline.py @@ -23,7 +23,7 @@ from bonsai.bim.module.drawing.helper import format_distance from dataclasses import dataclass from lark import Lark, Transformer from math import radians -from mathutils import Vector +from mathutils import Vector from typing import Optional @@ -66,8 +66,8 @@ class Polyline(bonsai.core.tool.Polyline): context = bpy.context if value is None: return None - if attribute_name == 'A': - value = float(self.get_text_value(attribute_name)) + if attribute_name == "A": + value = float(self.get_text_value(attribute_name)) return f"{value:.2f}" else: return self.format_input_ui_units(context, value) @@ -277,7 +277,6 @@ class Polyline(bonsai.core.tool.Polyline): return - @classmethod def validate_input(cls, input_number, input_type): diff --git a/src/bonsai/bonsai/tool/snap.py b/src/bonsai/bonsai/tool/snap.py index ff9a9d5b41..0249b6ece6 100644 --- a/src/bonsai/bonsai/tool/snap.py +++ b/src/bonsai/bonsai/tool/snap.py @@ -301,7 +301,9 @@ class Snap(bonsai.core.tool.Snap): view_direction = Vector((0, 0, -1)) @ camera_rotation.to_matrix().transposed() plane_normal = view_direction.normalized() - if tool_state.plane_method == "XY" or (not tool_state.plane_method and tool_state.axis_method in {"X", "Y"}): + if tool_state.plane_method == "XY" or ( + not tool_state.plane_method and tool_state.axis_method in {"X", "Y"} + ): if cls.tool_state.use_default_container: plane_origin = Vector((0, 0, elevation)) elif not last_polyline_point: @@ -425,7 +427,9 @@ class Snap(bonsai.core.tool.Snap): # Doesn't update snap_angle so that it keeps in the same axis rot_intersection, _, axis_start, axis_end = cls.snap_on_axis(intersection, tool_state, True) else: - rot_intersection, tool_state.snap_angle, axis_start, axis_end = cls.snap_on_axis(intersection, tool_state, False) + rot_intersection, tool_state.snap_angle, axis_start, axis_end = cls.snap_on_axis( + intersection, tool_state, False + ) if rot_intersection: detected_snaps.append({"Axis": (rot_intersection, axis_start, axis_end)}) @@ -466,7 +470,6 @@ class Snap(bonsai.core.tool.Snap): snapping_points.append(op) break - for origin in detected_snaps: if "Axis" in list(origin.keys()): intersection = origin["Axis"] @@ -498,4 +501,3 @@ class Snap(bonsai.core.tool.Snap): shifted_list = snapping_points[1:] + snapping_points[:1] cls.update_snapping_point(shifted_list[0][0], shifted_list[0][1]) return shifted_list - diff --git a/src/bonsai/bonsai/tool/web.py b/src/bonsai/bonsai/tool/web.py index 5a4df5e2b2..858c2d2c5f 100644 --- a/src/bonsai/bonsai/tool/web.py +++ b/src/bonsai/bonsai/tool/web.py @@ -535,7 +535,7 @@ class Web(bonsai.core.tool.Web): "cost_items": json_data, "cost_schedule_id": cost_schedule.id(), "currency": tool.Cost.currency(), - }, + }, data_key="cost_items", event="cost_items", ) From 9be392e2f1fd9dc3694468aab3a472e054d28533 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Wed, 11 Sep 2024 14:39:07 +0500 Subject: [PATCH 36/56] test TesselateElements #5199 --- src/ifcpatch/test/test_TesselateElements.py | 84 +++++++++++++++++++++ 1 file changed, 84 insertions(+) create mode 100644 src/ifcpatch/test/test_TesselateElements.py diff --git a/src/ifcpatch/test/test_TesselateElements.py b/src/ifcpatch/test/test_TesselateElements.py new file mode 100644 index 0000000000..83ea349137 --- /dev/null +++ b/src/ifcpatch/test/test_TesselateElements.py @@ -0,0 +1,84 @@ +# IfcOpenShell - IFC toolkit and geometry engine +# Copyright (C) 2022 Dion Moult +# +# 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 . + +import ifcopenshell.util.representation +import ifcpatch +import ifcopenshell +import ifcopenshell.api.context +import ifcopenshell.api.geometry +import ifcopenshell.api.profile +import ifcopenshell.api.root +import ifcopenshell.api.unit +import test.bootstrap +import ifcopenshell.geom +import ifcopenshell.util.element +from ifcopenshell.util.shape_builder import ShapeBuilder + + +class TestTesselateElements(test.bootstrap.IFC4): + def test_run(self): + is_ifc2x3 = self.file.schema == "IFC2X3" + ifcopenshell.api.root.create_entity(self.file, "IfcProject") + wall = ifcopenshell.api.root.create_entity(self.file, "IfcWall") + model = ifcopenshell.api.context.add_context(self.file, "Model") + body = ifcopenshell.api.context.add_context( + self.file, + context_type="Model", + context_identifier="Body", + target_view="MODEL_VIEW", + parent=model, + ) + + # Add a length unit just for tests. + unit = ifcopenshell.api.unit.add_si_unit(self.file, unit_type="LENGTHUNIT") + ifcopenshell.api.unit.assign_unit(self.file, units=[unit]) + + builder = ShapeBuilder(self.file) + rect = builder.polyline(builder.get_rectangle_coords(), closed=True) + extrusion = builder.extrude(rect, 1.0) + rep = builder.get_representation(body, extrusion) + ifcopenshell.api.geometry.assign_representation(self.file, wall, rep) + original_rep_id = rep.id() + + ifcpatch.execute({"file": self.file, "recipe": "TessellateElements", "arguments": ["IfcWall"]}) + + # Original representation still exists. + assert self.file.by_id(original_rep_id) + + other_reps = [r for r in ifcopenshell.util.representation.get_representations_iter(wall) if r != rep] + assert len(other_reps) == 1 + new_rep = other_reps[0] + new_rep.ContextOfItems = body + new_rep.RepresentationIdentifier = "Body" + new_rep.RepresentationType = "Tesselation" + assert len(items := new_rep.Items) == 1 + item = items[0] + if is_ifc2x3: + assert item.is_a("IfcFacetedBrep") + assert len(faces := item.Outer.CfsFaces) == 12 + for face in faces: + assert len(face.Bounds[0].Bound.Polygon) == 3 + else: + assert item.is_a("IfcPolygonalFaceSet") + assert len(faces := item.Faces) == 12 + for face in faces: + assert len(face.CoordIndex) == 3 + + +class TestTesselateElementsIFC2X3(test.bootstrap.IFC2X3, TestTesselateElements): + pass From 97393eb173c79aa0b07ce97eabf95f44430c21cc Mon Sep 17 00:00:00 2001 From: myoualid Date: Wed, 11 Sep 2024 18:48:59 +0100 Subject: [PATCH 37/56] prevent spatial elements selection from being assigned to cost control --- .../ifcopenshell/api/cost/assign_cost_item_quantity.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/ifcopenshell-python/ifcopenshell/api/cost/assign_cost_item_quantity.py b/src/ifcopenshell-python/ifcopenshell/api/cost/assign_cost_item_quantity.py index a1c0b9a706..542347fa11 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/cost/assign_cost_item_quantity.py +++ b/src/ifcopenshell-python/ifcopenshell/api/cost/assign_cost_item_quantity.py @@ -104,12 +104,14 @@ class Usecase: if self.settings["prop_name"]: self.quantities = set(self.settings["cost_item"].CostQuantities or []) for product in self.settings["products"]: + if not product.is_a("IfcElement"): + continue self.assign_cost_control(related_object=product, cost_item=self.settings["cost_item"]) if self.settings["prop_name"]: if ( self.settings["cost_item"].CostQuantities and self.settings["cost_item"].CostQuantities[0].Name.lower() != self.settings["prop_name"].lower() - ) or not product.is_a("IfcObject"): + ): continue self.add_quantity_from_related_object(product) if self.settings["prop_name"]: From cc2017518b724f25ee8ef87df0c187c46c6c192f Mon Sep 17 00:00:00 2001 From: Thomas Krijnen Date: Wed, 11 Sep 2024 19:58:52 +0200 Subject: [PATCH 38/56] Update build-all.py : _ifcopenshell_wrapper now called ifcopenshell_wrapper? --- nix/build-all.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nix/build-all.py b/nix/build-all.py index 8f63612434..756b6b3c1b 100644 --- a/nix/build-all.py +++ b/nix/build-all.py @@ -966,7 +966,7 @@ if "IfcOpenShell-Python" in targets: logger.info(f"\rBuilding python {python_version} wrapper... ") - run([make, f"-j{IFCOS_NUM_BUILD_PROCS}", "_ifcopenshell_wrapper"], cwd=python_dir) + run([make, f"-j{IFCOS_NUM_BUILD_PROCS}", "ifcopenshell_wrapper"], cwd=python_dir) run([make, "install/local"], cwd=os.path.join(python_dir, "ifcwrap")) if python_executable: From 1964def3cae50763d4a32d4ab3ebd856440bece3 Mon Sep 17 00:00:00 2001 From: myoualid Date: Wed, 11 Sep 2024 19:00:15 +0100 Subject: [PATCH 39/56] web ui cost module features: - improve UI for editing cost quantites - add, edit, delete manual quantities --- .../bonsai/bim/data/webui/static/css/cost.css | 129 +++-- .../bonsai/bim/data/webui/static/js/cost.js | 34 +- .../data/webui/static/js/utilities/costui.js | 457 ++++++++++++++++-- src/bonsai/bonsai/tool/cost.py | 37 +- src/bonsai/bonsai/tool/web.py | 170 ++++--- 5 files changed, 683 insertions(+), 144 deletions(-) diff --git a/src/bonsai/bonsai/bim/data/webui/static/css/cost.css b/src/bonsai/bonsai/bim/data/webui/static/css/cost.css index c0ad9a8e16..eac13d1945 100644 --- a/src/bonsai/bonsai/bim/data/webui/static/css/cost.css +++ b/src/bonsai/bonsai/bim/data/webui/static/css/cost.css @@ -20,6 +20,22 @@ body { box-sizing: border-box; } +.floating-form { + width: 50vw; + max-height: 80vh; + position: absolute; + background-color: var(--background-color); + border: 1px solid black; + z-index: 9999; + cursor: move; + user-select: none; + padding-bottom: var(--padding-medium); +} + +[id^="enable-editing-quantities"].floating-form { + height: 70vh; +} + .form-header { display: flex; justify-content: flex-start; @@ -30,36 +46,44 @@ body { font-size: large; border-bottom: 1px solid #ddd; border-radius: 6px 6px 0 0; + height: 5%; } -.floating-form span { - padding: var(--padding-small); - margin-left: var(--margin-small); +.form-container { + height: 95%; + padding-left: var(--padding-medium); + padding-right: var(--padding-medium); } -.floating-form { - width: 50vw; - max-height: 80vh; - position: absolute; - background-color: var(--background-color); - border: 1px solid black; - z-index: 9999; - cursor: move; - user-select: none; - resize: both; +.form-section { + margin-bottom: 1em; + height: 65%; + overflow-y: auto; + overflow-x: hidden; } +.form-section h3 { + margin-bottom: 0.5em; +} + +.summary-section { + margin-top: 1em; +} + +.summary-table { + width: 100%; + border-collapse: collapse; + font-size: 14px; + text-align: left; +} + + .floating-form table tr { margin-bottom: 10px; } -.form-container { - padding: var(--padding-medium); - overflow-y: auto; - overflow-x: hidden; -} - .action-button { + position:relative; background-color: var(--button-background); border: none; color: #fff; @@ -72,6 +96,32 @@ body { transition: background-color 0.3s ease; width: fit-content; margin: 0.5em; + z-index: 11000; +} + +.action-button::after { + content: attr(data-tooltip); + position: absolute; + bottom: -30px; + background-color: #333; + color: #fff; + padding: 5px 10px; + border-radius: 5px; + white-space: nowrap; + opacity: 0; + pointer-events: none; + transition: opacity 0.3s; + font-size: 12px; +} + +.action-button:hover::after { + opacity: 1; +} + +.active-btn { + background-color: #887821; + /* make a nic shadow */ + box-shadow: 0 0 10px #887821; } .action-button:hover { @@ -141,7 +191,7 @@ body { } .floating-form .table-container { - max-height: 70%; + max-height: 80%; } #cost-items { @@ -149,7 +199,6 @@ body { } [id^="cost-values-form"] table { - /* fixed */ max-height: 50%; table-layout: fixed; } @@ -186,6 +235,7 @@ td, th { border-right: 1px solid #ddd; text-align: center; + height: 100%; } th { @@ -221,15 +271,7 @@ select { color: var(--blender-button-text, var(--text-color)); border-color: var(--blender-button-border, var(--border-color)); transition: filter 0.2s ease; - border-radius: 6px; - padding: 6px 8px; - font-size: 14px; - line-height: 20px; transition: border-color 0.2s cubic-bezier(0.3, 0, 0.5, 1); - margin-left: 0.5em; - margin-right: 0.5em; - font-size: var(--fontSize); - min-width: 50px; } input:focus { @@ -239,6 +281,31 @@ input:focus { color: #ddd; } +#cost-items input, #cost-items +select { + border-radius: 6px; + padding: 6px 8px; + font-size: 14px; + line-height: 20px; + margin-left: 0.5em; + margin-right: 0.5em; + font-size: var(--fontSize); + min-width: 50px; +} + +form input { + border-radius: 6px; + padding: 3px 4px; + font-size: 12px; + line-height: 10px; + margin-left: 0.2em; + margin-right: 0.2em; + font-size: inherit; + height: inherit; + width: inherit; + box-sizing: border-box; +} + #cost-items tr:hover, .highlighted { background-color: #28a74657; @@ -332,7 +399,7 @@ form table { } .clickable-cell:hover { - border: 1px solid #28a746; + border: 1px solid #94a728; border-radius: 6px; cursor: pointer; -} +} \ No newline at end of file diff --git a/src/bonsai/bonsai/bim/data/webui/static/js/cost.js b/src/bonsai/bonsai/bim/data/webui/static/js/cost.js index 74943c9426..be078a8d3c 100644 --- a/src/bonsai/bonsai/bim/data/webui/static/js/cost.js +++ b/src/bonsai/bonsai/bim/data/webui/static/js/cost.js @@ -36,19 +36,51 @@ function handleEditQuantities(data) { const products = data.data["quantities"]["selected_products"]; const assigned_products = data.data["quantities"]["assigned_products"]; const quantityNames = data.data["quantities"]["product_quantity_names"]; + const costQuantities = data.data["quantities"]["cost_quantities"]; - CostUI.editQuantities({ + CostUI.enableEditingQuantities({ costItemId: costItemId, selectedProducts: products, assignedProducts: assigned_products, quantityNames: quantityNames, + costQuantities: costQuantities, callbacks: { addProductAssignments: addProductAssignments, enableEditingQuantities: enableEditingQuantities, + addQuantity: addQuantity, + editQuantity: editQuantity, + deleteQuantity: deleteQuantity, }, }); } +function addQuantity(costItemId, ifcClass) { + executeOperator({ + type: "AddCostItemQuantity", + costItemId: costItemId, + ifcClass: ifcClass, + }); + + //CostUI.addQuantity(costItemId, quantityName); +} + +function editQuantity(costItemId, quantityId, attributes) { + executeOperator({ + type: "editCostItemQuantity", + costItemId: costItemId, + quantityId: quantityId, + attributes: attributes, + }); +} + +function deleteQuantity(costItemId, quantityId) { + executeOperator({ + type: "deleteCostItemQuantity", + costItemId: costItemId, + quantityId: quantityId, + }); +} + function handlePredefinedTypes(data) { CostUI.enableAddingCostSchedule( data.data["predefined_types"], diff --git a/src/bonsai/bonsai/bim/data/webui/static/js/utilities/costui.js b/src/bonsai/bonsai/bim/data/webui/static/js/utilities/costui.js index 0a94fb4d50..7a99b57c8a 100644 --- a/src/bonsai/bonsai/bim/data/webui/static/js/utilities/costui.js +++ b/src/bonsai/bonsai/bim/data/webui/static/js/utilities/costui.js @@ -793,11 +793,15 @@ export class CostUI { } static getRowNameCell(costItemId) { - return document.getElementById(costItemId).querySelector("td input"); + return document.getElementById(costItemId) + ? document.getElementById(costItemId).querySelector("td input") + : null; } static getCostItemName(costItemId) { - return CostUI.getRowNameCell(costItemId).value; + return CostUI.getRowNameCell(costItemId) + ? CostUI.getRowNameCell(costItemId).value + : "Unnamed"; } static createCostValuesForm({ costItemId, costValues, callbacks }) { @@ -840,7 +844,6 @@ export class CostUI { static addNewCostValueRow(costItemId, costValueId, costValueCallbacks) { const table = CostUI.getCostValuesTable(costItemId); if (!table) { - console.log("Cost values table not found for cost item ID:", costItemId); return; } const tr = CostUI.createCostvaluesRow( @@ -908,8 +911,8 @@ export class CostUI { } else if (costValue.applied_value) { costType = "FIXED"; } - - const typeCell = CostUI.createTableDropdown("type", costType); + const options = ["FIXED", "CATEGORY", "SUM"]; + const typeCell = CostUI.createTableDropdown("type", options, costType); const dropdown = typeCell.querySelector("select"); dropdown.addEventListener("change", function () { const selectedType = this.value; @@ -1012,12 +1015,11 @@ export class CostUI { } } - static createTableDropdown(name, value = "FIXED") { + static createTableDropdown(name, options, value = "FIXED") { const cell = document.createElement("td"); const dropdown = document.createElement("select"); dropdown.name = name; - const options = ["FIXED", "CATEGORY", "SUM"]; options.forEach((optionValue) => { const option = document.createElement("option"); option.value = optionValue; @@ -1306,7 +1308,7 @@ export class CostUI { } static createProductTable({ - form, + container, products, quantityNames, costItemId, @@ -1316,7 +1318,7 @@ export class CostUI { const noProductsMessage = document.createElement("p"); noProductsMessage.textContent = "Your Blender Selection is empty! Select objects first."; - form.appendChild(noProductsMessage); + container.appendChild(noProductsMessage); return; } const { tableContainer, table } = CostUI.addTable({ @@ -1324,7 +1326,7 @@ export class CostUI { className: "", id: "cost-values-table-" + costItemId, }); - form.appendChild(tableContainer); + container.appendChild(tableContainer); const tbody = document.createElement("tbody"); table.appendChild(tbody); @@ -1432,7 +1434,7 @@ export class CostUI { const productIds = products.map((product) => product.info.id); callbacks.emptyForm = () => { - form.innerHTML = ""; + container.innerHTML = ""; }; const addProductAssignmentsButton = CostUI.addProductAssignmentsButton( costItemId, @@ -1441,13 +1443,13 @@ export class CostUI { callbacks ); - form.appendChild(quantitySelect); - form.appendChild(addProductAssignmentsButton); + container.appendChild(quantitySelect); + container.appendChild(addProductAssignmentsButton); return table; } static createAssignmentsTable({ - form, + container, products, costItemId, quantityNames, @@ -1459,7 +1461,7 @@ export class CostUI { id: "cost-values-table-" + costItemId, }); - form.appendChild(tableContainer); + container.appendChild(tableContainer); const tbody = document.createElement("tbody"); table.appendChild(tbody); @@ -1512,8 +1514,10 @@ export class CostUI { quantitySelect, callbacks ) { - const addProductAssignmentsButton = document.createElement("button"); - addProductAssignmentsButton.textContent = "Add Product Assignments"; + const addProductAssignmentsButton = CostUI.createButton( + "Add Product Assignments", + "fa-solid fa-plus" + ); addProductAssignmentsButton.addEventListener("click", (event) => { event.preventDefault(); let propName = quantitySelect.value; @@ -1528,7 +1532,7 @@ export class CostUI { }) : null; callbacks.emptyForm ? callbacks.emptyForm() : null; - callbacks.getSelectedProducts(costItemId); + callbacks.enableEditingQuantities(costItemId); }); addProductAssignmentsButton.classList.add("action-button"); return addProductAssignmentsButton; @@ -1730,49 +1734,424 @@ export class CostUI { }); } - static editQuantities({ + static enableEditingQuantities({ costItemId, selectedProducts, - quantityNames, assignedProducts, + quantityNames, + costQuantities, callbacks, }) { - const formId = "selected-products-" + costItemId; + CostUI.highlightElement(costItemId); const form = CostUI.Form({ - id: formId, - name: - "Edit Product Assignments for: " + CostUI.getCostItemName(costItemId), + id: "enable-editing-quantities-" + costItemId, + name: "Editing Quantities for: " + CostUI.getCostItemName(costItemId), icon: "fa-solid fa-box", }); - const numberOfProducts = CostUI.Text( - "Selection basket : " + selectedProducts.length + " products", - "fa-solid fa-cart-shopping", - "large" - ); - form.appendChild(numberOfProducts); - CostUI.highlightElement(costItemId); - const selectedProductsTable = CostUI.createProductTable({ - form, + + const ribbonBar = CostUI.createRibbonBar(); + form.appendChild(ribbonBar); + + const selectedProductsSection = CostUI.selectedProductsSection({ products: selectedProducts, quantityNames, costItemId, callbacks, }); - if (assignedProducts.length > 0) { + const assignedProductsSection = CostUI.assignedProductsSection({ + products: assignedProducts, + quantityNames, + costItemId, + callbacks, + }); + + const manualQuantitiesSection = CostUI.manualQuantitiesSection({ + costItemId, + costQuantities, + has_assigned_products: assignedProducts.length > 0, + callbacks, + }); + + form.appendChild(selectedProductsSection); + form.appendChild(assignedProductsSection); + form.appendChild(manualQuantitiesSection); + + const summarySection = CostUI.createSummarySection({ + selectedProducts, + assignedProducts, + costQuantities, + }); + form.appendChild(summarySection); + + CostUI.addEventListeners({ + switchBar: ribbonBar, + costItemId, + selectedProductsSection, + assignedProductsSection, + manualQuantitiesSection, + }); + + const lastActiveSection = + localStorage.getItem("lastActiveSection") || "selected-products"; + const lastActiveButton = document.getElementById( + `${lastActiveSection}-btn` + ); + if (lastActiveButton) { + lastActiveButton.click(); + } + } + + static createRibbonBar() { + const ribbonBar = document.createElement("div"); + ribbonBar.className = "switch-bar"; + ribbonBar.innerHTML = ` + + + + `; + return ribbonBar; + } + + static createSummarySection({ + selectedProducts, + assignedProducts, + costQuantities, + }) { + const summarySection = document.createElement("div"); + summarySection.classList.add("summary-section"); + + const manualQuantities = costQuantities.quantities.filter( + (quantity) => !quantity.fromProduct + ); + const paramaterQuantities = costQuantities.quantities.filter( + (quantity) => quantity.fromProduct + ); + + const table = document.createElement("table"); + table.classList.add("summary-table"); + + const thead = document.createElement("thead"); + const headerRow = document.createElement("tr"); + const headers = ["Category", "Count"]; + headers.forEach((headerText) => { + const th = document.createElement("th"); + th.textContent = headerText; + headerRow.appendChild(th); + }); + thead.appendChild(headerRow); + table.appendChild(thead); + const tbody = document.createElement("tbody"); + + const data = [ + { category: "Assigned Products", count: assignedProducts.length }, + { category: "Manual Quantities", count: manualQuantities.length }, + { + category: "Product derived Quantities", + count: paramaterQuantities.length, + }, + ]; + + data.forEach((item) => { + const row = document.createElement("tr"); + const categoryCell = document.createElement("td"); + categoryCell.textContent = item.category; + const countCell = document.createElement("td"); + countCell.textContent = item.count; + row.appendChild(categoryCell); + row.appendChild(countCell); + tbody.appendChild(row); + }); + + table.appendChild(tbody); + summarySection.appendChild(table); + + return summarySection; + } + + static addEventListeners({ + switchBar, + costItemId, + selectedProductsSection, + assignedProductsSection, + manualQuantitiesSection, + }) { + const buttons = switchBar.querySelectorAll(".action-button"); + + buttons.forEach((button) => { + button.addEventListener("click", (e) => { + e.preventDefault(); + const sectionName = button.id.replace("-btn", ""); + const sectionId = sectionName + "-section-" + costItemId; + + if (selectedProductsSection) + selectedProductsSection.style.display = "none"; + if (assignedProductsSection) + assignedProductsSection.style.display = "none"; + if (manualQuantitiesSection) + manualQuantitiesSection.style.display = "none"; + + const section = document.getElementById(sectionId); + if (section) { + section.style.display = "block"; + } + buttons.forEach((btn) => btn.classList.remove("active-btn")); + button.classList.add("active-btn"); + + localStorage.setItem("lastActiveSection", sectionName); + }); + }); + } + + static getSection(costItemId, sectionName) { + return document.getElementById(`${sectionName}-${costItemId}`); + } + + static selectedProductsSection({ + products, + quantityNames, + costItemId, + callbacks, + }) { + const selectedProductsSection = document.createElement("div"); + selectedProductsSection.id = "selected-products-section-" + costItemId; + selectedProductsSection.classList.add("form-section"); + const numberOfProducts = CostUI.Text( + "Selection basket : " + products.length + " products", + "fa-solid fa-cart-shopping", + "medium" + ); + selectedProductsSection.appendChild(numberOfProducts); + + const selectedProductsTable = CostUI.createProductTable({ + container: selectedProductsSection, + products: products, + quantityNames, + costItemId, + callbacks, + }); + return selectedProductsSection; + } + + static assignedProductsSection({ + products, + quantityNames, + costItemId, + callbacks, + }) { + const assignedProductsSection = document.createElement("div"); + assignedProductsSection.id = "assigned-products-section-" + costItemId; + assignedProductsSection.style.display = "none"; + assignedProductsSection.classList.add("form-section"); + if (products.length > 0) { + let text = " Assigned Products : " + products.length; const assignedProductsText = CostUI.Text( - "Assigned Products", + text, "fa-solid fa-solid fa-paperclip", - "large" + "medium" ); - form.appendChild(assignedProductsText); + assignedProductsSection.appendChild(assignedProductsText); const assignmentsTable = CostUI.createAssignmentsTable({ - form, - products: assignedProducts, + container: assignedProductsSection, + products: products, quantityNames, costItemId, callbacks, }); } + + return assignedProductsSection; + } + + static manualQuantitiesSection({ + costItemId, + costQuantities, + has_assigned_products, + callbacks, + }) { + const manualQuantitiesSection = document.createElement("div"); + manualQuantitiesSection.classList.add("form-section"); + manualQuantitiesSection.id = "manual-quantities-section-" + costItemId; + manualQuantitiesSection.style.display = "none"; + + const title = CostUI.Text( + "Manual Quantities", + "fa-solid fa-ruler", + "medium" + ); + manualQuantitiesSection.appendChild(title); + const unitSymbol = costQuantities.unit_symbol; + + let quantityType = costQuantities.quantity_type; + + if (quantityType) { + const text = CostUI.Text( + quantityType + " (" + unitSymbol + " )", + "fa-solid fa-ruler", + "small" + ); + manualQuantitiesSection.appendChild(text); + } else { + // add dropdown to chose quantity type , from "IfcQuantityArea", "IfcQuantityLength", "IfcQuantityVolume", "IfcQuantityCount", "IfcQuantityWeight " + const quantityTypes = [ + "IfcQuantityArea", + "IfcQuantityLength", + "IfcQuantityVolume", + "IfcQuantityCount", + "IfcQuantityWeight", + ]; + const dropdown = CostUI.createTableDropdown( + "type", + quantityTypes, + "IfcQuantityArea" + ); + dropdown.id = "quantity-type-dropdown-" + costItemId; + manualQuantitiesSection.appendChild(dropdown); + } + + // if quantity is ty IfcQuantityCount, and the costQuantities + if (quantityType === "IfcQuantityCount" && has_assigned_products) { + // write text that one of the quantities is assigned to the product selection + const text = CostUI.Text( + " One of the quantities is assigned to the product selection", + "fa-solid fa-warning", + "small" + ); + manualQuantitiesSection.appendChild(text); + } + + let paramaterQuantities = costQuantities.quantities.filter( + (quantity) => quantity.fromProduct + ); + let manualQuantities = costQuantities.quantities.filter( + (quantity) => !quantity.fromProduct + ); + + let headerNames = ["Name", "Value" + " (" + unitSymbol + " )", "Actions"]; + + if (manualQuantities.length > 0) { + headerNames = []; + Object.entries(manualQuantities[0]).forEach(([key, value]) => { + if (key !== "id" && key !== "fromProduct") { + headerNames.push(key); + } + }); + headerNames.push("Actions"); + } + + const { tableContainer, table } = CostUI.addTable({ + headers: headerNames, + className: "manual-quantities-table", + id: "manual-quantities-table-" + costItemId, + }); + + manualQuantities.forEach((quantity) => { + const tr = CostUI.createManualQuantityRow( + costItemId, + quantity, + callbacks + ); + table.appendChild(tr); + }); + const addButton = CostUI.createAddManualQuantityButton( + costItemId, + quantityType, + callbacks + ); + + manualQuantitiesSection.appendChild(tableContainer); + // manualQuantitiesSection.appendChild(tableContainer2); + manualQuantitiesSection.appendChild(addButton); + return manualQuantitiesSection; + } + + static createManualQuantityRow(costItemId, quantity, callbacks) { + const tr = document.createElement("tr"); + tr.id = quantity.id; + + // get quantity keys and values to create the table row cells + // Label cell + + Object.entries(quantity).forEach(([key, value]) => { + if (key !== "id" && key !== "fromProduct") { + if (key === "Name") { + // create input + const cell = document.createElement("td"); + const input = document.createElement("input"); + input.type = "text"; + input.value = value; + cell.appendChild(input); + tr.appendChild(cell); + + input.addEventListener("keydown", function (e) { + if (e.key === "Enter") { + e.preventDefault(); + callbacks.editQuantity(costItemId, quantity.id, { + [key]: input.value, + }); + } + }); + } + + // if key contains value + else if (key.toLowerCase().includes("value")) { + const cell = document.createElement("td"); + const input = document.createElement("input"); + input.type = "number"; + input.value = value; + cell.appendChild(input); + + input.addEventListener("keydown", function (e) { + if (e.key === "Enter") { + e.preventDefault(); + callbacks.editQuantity(costItemId, quantity.id, { + [key]: parseFloat(input.value), // Dynamically set the key and parse the value as a double + }); + } + }); + tr.appendChild(cell); + } else { + const cell = document.createElement("td"); + cell.textContent = value; + tr.appendChild(cell); + } + } + }); + + // Add delete button + const deleteButton = CostUI.createButton("Delete", "fa-solid fa-trash"); + const deleteCell = document.createElement("td"); + deleteButton.addEventListener("click", function (e) { + e.preventDefault(); + callbacks.deleteQuantity(costItemId, quantity.id); + tr.remove(); + }); + deleteCell.appendChild(deleteButton); + tr.appendChild(deleteCell); + + return tr; + } + + static createAddManualQuantityButton(costItemId, quantityType, callbacks) { + const addButton = CostUI.createButton( + "Add Manual Quantity", + "fa-solid fa-plus" + ); + + addButton.addEventListener("click", function (e) { + e.preventDefault(); + let type = quantityType; + if (!type) { + // get quantityType from dropdown + const qtoSection = document.getElementById( + "manual-quantities-section-" + costItemId + ); + const dropdown = qtoSection.querySelector("select"); + type = dropdown ? dropdown.value : null; + } + callbacks.addQuantity(costItemId, type); + }); + + return addButton; } } diff --git a/src/bonsai/bonsai/tool/cost.py b/src/bonsai/bonsai/tool/cost.py index 8d9945a4ad..4c68d31d12 100644 --- a/src/bonsai/bonsai/tool/cost.py +++ b/src/bonsai/bonsai/tool/cost.py @@ -295,6 +295,15 @@ class Cost(bonsai.core.tool.Cost): unit = cls.get_quantity_unit_symbol(quantity) return selected_quantitites, unit + + @classmethod + def get_assigned_product(cls, cost_item, quantity): + assigned_products = cls.get_cost_item_assignments(cost_item, filter_by_type="PRODUCT", is_deep=False) + for product in assigned_products: + assigned_quantities, _ = cls.get_assigned_quantities(cost_item, product) + if quantity in assigned_quantities: + return product + @classmethod def get_products(cls, related_object_type: RELATED_OBJECT_TYPE) -> list[ifcopenshell.entity_instance]: if related_object_type == "PRODUCT": @@ -859,7 +868,33 @@ class Cost(bonsai.core.tool.Cost): bpy.ops.bim.connect_websocket_server(page="costing") cost_schedule_data = cls.create_cost_schedule_json(cost_chedule) tool.Web.send_webui_data( - data={"cost_items": cost_schedule_data, "cost_schedule_id": cost_chedule.id()}, + data={ + "cost_items": cost_schedule_data, + "cost_schedule_id": cost_chedule.id(), + "currency": cls.currency() + }, data_key="cost_items", event="cost_items", ) + + @classmethod + def get_cost_quantities(cls, cost_item: ifcopenshell.entity_instance) -> dict: + results = { + "quantities": [], + "unit_symbol": None, + } + if not cost_item: + return results + results["quantity_type"] = cost_item.CostQuantities[0].is_a() if cost_item.CostQuantities else None + unit = ifcopenshell.util.unit.get_property_unit(cost_item.CostQuantities[0], tool.Ifc.get()) if cost_item.CostQuantities else None + if unit: + results["unit_symbol"] = ifcopenshell.util.unit.get_unit_symbol(unit) + for quantity in cost_item.CostQuantities or []: + assigned_product = cls.get_assigned_product(cost_item, quantity) + info = quantity.get_info() + info["fromProduct"] = assigned_product.get_info(recursive=True) if assigned_product else None + results["quantities"].append(info) + if results["quantity_type"] == "IfcQuantityCount": + results["unit_symbol"] = "U" + return results + diff --git a/src/bonsai/bonsai/tool/web.py b/src/bonsai/bonsai/tool/web.py index 858c2d2c5f..c8d3f1fb82 100644 --- a/src/bonsai/bonsai/tool/web.py +++ b/src/bonsai/bonsai/tool/web.py @@ -344,52 +344,6 @@ class Web(bonsai.core.tool.Web): operator_data (dict): A dictionary containing the operator data. """ - def selection_data(cost_item, elements): - print(cost_item, elements) - if not elements: - return [] - if not cost_item: - return [ - { - "info": { - "id": element.id(), - "name": element.Name, - "class": element.is_a(), - "type": get_type(element).Name if get_type(element) else None, - }, - "qtos": get_psets(element, qtos_only=True), - } - for element in elements or [] - ] - data = [] - for element in elements or []: - psets = get_psets(element, qtos_only=True) - element_type = get_type(element) - quantities, unit = tool.Cost.get_assigned_quantities(cost_item, element) - data.append( - { - "info": { - "id": element.id(), - "name": element.Name, - "class": element.is_a(), - "type": element_type.Name if element_type else None, - }, - "qtos": psets, - "assigned_quantities": [ - { - "cost_item_id": cost_item.id(), - "product_id": element.id(), - "name": q.Name, - "value": q[3], - "type": q.Unit, - } - for q in quantities or [] - ], - "unit": unit, - } - ) - return data - ifc_file = tool.Ifc.get() if operator_data["type"] == "getPredefinedTypes": print("getting predefined types") @@ -425,31 +379,6 @@ class Web(bonsai.core.tool.Web): cost_item = tool.Ifc.get().by_id(operator_data["costItemId"]) products = tool.Cost.get_cost_item_products(cost_item, is_deep=True) tool.Spatial.select_products(products, unhide=True) - if operator_data["type"] == "enableEditingQuantities": - cost_item_id = operator_data["costItemId"] - cost_item = ifc_file.by_id(cost_item_id) - if not cost_item: - print("---> cost item not found") - return - - selected_products = list(tool.Spatial.get_selected_products()) - selected_products_data = selection_data(cost_item, selected_products) - - assigned_products = tool.Cost.get_cost_item_products(cost_item, is_deep=False) - assigned_products_data = selection_data(cost_item, assigned_products) - - names = ifcopenshell.util.cost.get_product_quantity_names(selected_products) - cls.send_webui_data( - data={ - "selected_products": selected_products_data, - "assigned_products": assigned_products_data, - "product_quantity_names": names, - "cost_item_id": cost_item_id, - }, - data_key="quantities", - event="quantities", - ) - if operator_data["type"] == "addSummaryCostItem": cost_schedule = ifc_file.by_id(operator_data["costScheduleId"]) bonsai.core.cost.add_summary_cost_item(tool.Ifc, tool.Cost, cost_schedule=cost_schedule) @@ -520,12 +449,109 @@ class Web(bonsai.core.tool.Web): prop_name = operator_data["propName"] if prop_name == "count": prop_name = "" - print(prop_name) bpy.ops.bim.assign_cost_item_quantity( cost_item=operator_data["costItemId"], related_object_type="PRODUCT", prop_name=prop_name ) cost_schedule = tool.Cost.get_cost_schedule(cost_item=tool.Ifc.get().by_id(operator_data["costItemId"])) cls.load_cost_schedule_web_ui(cost_schedule) + if operator_data["type"] == "enableEditingQuantities": + cost_item_id = operator_data["costItemId"] + cost_item = ifc_file.by_id(cost_item_id) + cls.enableEditingCostItemQuantities(cost_item) + if operator_data["type"] == "AddCostItemQuantity": + cost_item_id = operator_data["costItemId"] + cost_item = ifc_file.by_id(cost_item_id) + cost_schedule = tool.Cost.get_cost_schedule(cost_item) + bpy.ops.bim.add_cost_item_quantity(cost_item=cost_item_id, ifc_class=operator_data["ifcClass"]) + cls.load_cost_schedule_web_ui(cost_schedule) + cls.enableEditingCostItemQuantities(cost_item) + if operator_data["type"] == "editCostItemQuantity": + cost_item_id = operator_data["costItemId"] + cost_item = ifc_file.by_id(cost_item_id) + cost_schedule = tool.Cost.get_cost_schedule(cost_item) + physical_quantity = tool.Ifc.get().by_id(operator_data["quantityId"]) + attributes = operator_data["attributes"] + tool.Ifc.run("cost.edit_cost_item_quantity", physical_quantity=physical_quantity, attributes=attributes) + tool.Cost.load_cost_item_quantities(ifc_file.by_id(operator_data["costItemId"])) + cls.load_cost_schedule_web_ui(cost_schedule) + cls.enableEditingCostItemQuantities(cost_item) + if operator_data["type"] == "deleteCostItemQuantity": + bpy.ops.bim.remove_cost_item_quantity(cost_item=operator_data["costItemId"], physical_quantity=operator_data["quantityId"]) + cost_item_id = operator_data["costItemId"] + cost_item = ifc_file.by_id(cost_item_id) + cost_schedule = tool.Cost.get_cost_schedule(cost_item) + cls.load_cost_schedule_web_ui(cost_schedule) + cls.enableEditingCostItemQuantities(cost_item) + + @classmethod + def enableEditingCostItemQuantities(cls, cost_item): + if not cost_item: + return + selected_products = list(tool.Spatial.get_selected_products()) + selected_products_data = cls.selection_data(cost_item, selected_products) + + assigned_products = tool.Cost.get_cost_item_products(cost_item, is_deep=False) + assigned_products_data = cls.selection_data(cost_item, assigned_products) + + names = ifcopenshell.util.cost.get_product_quantity_names(selected_products) + cls.send_webui_data( + data={ + "selected_products": selected_products_data, + "assigned_products": assigned_products_data, + "product_quantity_names": names, + "cost_item_id": cost_item.id(), + "cost_quantities": tool.Cost.get_cost_quantities(cost_item), + }, + data_key="quantities", + event="quantities", + ) + + + @classmethod + def selection_data(cls, cost_item, elements): + if not elements: + return [] + if not cost_item: + return [ + { + "info": { + "id": element.id(), + "name": element.Name, + "class": element.is_a(), + "type": get_type(element).Name if get_type(element) else None, + }, + "qtos": get_psets(element, qtos_only=True), + } + for element in elements or [] + ] + data = [] + for element in elements or []: + psets = get_psets(element, qtos_only=True) + element_type = get_type(element) + quantities, unit = tool.Cost.get_assigned_quantities(cost_item, element) + data.append( + { + "info": { + "id": element.id(), + "name": element.Name, + "class": element.is_a(), + "type": element_type.Name if element_type else None, + }, + "qtos": psets, + "assigned_quantities": [ + { + "cost_item_id": cost_item.id(), + "product_id": element.id(), + "name": q.Name, + "value": q[3], + "type": q.Unit, + } + for q in quantities or [] + ], + "unit": unit, + } + ) + return data @classmethod def load_cost_schedule_web_ui(cls, cost_schedule): From 5a44a8e32546af6f93ddd3cd02ed7c3c71a14047 Mon Sep 17 00:00:00 2001 From: Thomas Krijnen Date: Wed, 11 Sep 2024 20:13:50 +0200 Subject: [PATCH 40/56] Defensiveness against invalid data --- src/ifcparse/IfcParse.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ifcparse/IfcParse.cpp b/src/ifcparse/IfcParse.cpp index 0ea64c2f78..a922775977 100644 --- a/src/ifcparse/IfcParse.cpp +++ b/src/ifcparse/IfcParse.cpp @@ -709,7 +709,7 @@ void IfcParse::IfcFile::load(unsigned entity_instance_name, const IfcParse::enti load(entity_instance_name, entity, context.push(), attribute_index == -1 ? (int) attribute_index_within_data : attribute_index); } else { return_value++; - if (TokenFunc::isIdentifier(next)) { + if (TokenFunc::isIdentifier(next) && entity) { register_inverse(entity_instance_name, entity, next, attribute_index == -1 ? attribute_index_within_data : attribute_index); } From c7183a58eaef1cb66f95a897be15d5323c943f73 Mon Sep 17 00:00:00 2001 From: Thomas Krijnen Date: Wed, 11 Sep 2024 20:15:14 +0200 Subject: [PATCH 41/56] aggregate of int/real compatibility in parsing #5302 --- src/ifcparse/IfcFile.cpp | 43 ++++++++++++++++++++++++++++++++++++++-- 1 file changed, 41 insertions(+), 2 deletions(-) diff --git a/src/ifcparse/IfcFile.cpp b/src/ifcparse/IfcFile.cpp index 830754444f..79c3d25f6f 100644 --- a/src/ifcparse/IfcFile.cpp +++ b/src/ifcparse/IfcFile.cpp @@ -130,10 +130,48 @@ namespace { if (aggregate_storage.which() == 0) { aggregate_storage = std::vector>{ v }; } else { - auto* vec_ptr = boost::get>>(&aggregate_storage); - if (vec_ptr) { + if (auto* vec_ptr = boost::get>>(&aggregate_storage)) { vec_ptr->push_back(v); } else { + if constexpr (std::is_same_v, int>) { + auto* vec_ptr2 = boost::get>(&aggregate_storage); + if (vec_ptr2) { + // double[] + int + vec_ptr2->push_back((double) v); + } + } + if constexpr (std::is_same_v, double>) { + auto* vec_ptr2 = boost::get>(&aggregate_storage); + if (vec_ptr2) { + // int[] -> double[] + double + std::vector ps(vec_ptr2->begin(), vec_ptr2->end()); + ps.push_back(v); + aggregate_storage = ps; + } + } + + if constexpr (std::is_same_v, std::vector>) { + auto* vec_ptr2 = boost::get>>(&aggregate_storage); + if (vec_ptr2) { + // double[][] + int[] + std::vector vd(v.begin(), v.end()); + vec_ptr2->push_back(vd); + } + } + if constexpr (std::is_same_v, std::vector>) { + auto* vec_ptr2 = boost::get>>(&aggregate_storage); + if (vec_ptr2) { + // int[][] -> double[][] + double[] + std::vector> vvd; + for (auto& vv : *vec_ptr2) { + std::vector vd(vv.begin(), vv.end()); + vvd.push_back(vd); + } + vvd.push_back(v); + aggregate_storage = vvd; + } + } + // @todo would be cool if we can trace this back to file offset auto current = boost::apply_visitor([](auto v) { if constexpr (!std::is_same_v) { @@ -145,6 +183,7 @@ namespace { return std::string{}; } }, aggregate_storage); + Logger::Error("Inconsistent aggregate valuation while attempting to append " + std::string(typeid(decltype(v)).name()) + " to an aggregate of " + current); // @todo boolean -> logical upgrade From 35a4d87a658bfb2041b20af420608daa4b35e6a8 Mon Sep 17 00:00:00 2001 From: Ryan Schultz Date: Wed, 11 Sep 2024 23:06:43 -0500 Subject: [PATCH 42/56] give schedules a css style called 'schedule' to help with styling --- src/bonsai/bonsai/bim/module/drawing/scheduler.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/bonsai/bonsai/bim/module/drawing/scheduler.py b/src/bonsai/bonsai/bim/module/drawing/scheduler.py index abe44ffd7a..7b2b6c23b6 100644 --- a/src/bonsai/bonsai/bim/module/drawing/scheduler.py +++ b/src/bonsai/bonsai/bim/module/drawing/scheduler.py @@ -554,6 +554,8 @@ class Scheduler: text_params["font-style"] = "italic" if text_color: text_params["fill"] = text_color + + text_params["class_"] = "schedule" if len(text_lines) == 1 and not wrap_text: text_params.update(box_alignment_params) From 2545769f67c1cf3da68ef8f73766ef6f62c15e32 Mon Sep 17 00:00:00 2001 From: Thomas Krijnen Date: Thu, 12 Sep 2024 11:20:30 +0200 Subject: [PATCH 43/56] Add support for IfcSectionedSurface --- src/ifcgeom/infra_sweep_helper.cpp | 203 ++++++++++++++++++ src/ifcgeom/infra_sweep_helper.h | 26 +++ src/ifcgeom/kernels/opencascade/loft.cpp | 41 ++-- .../mapping/IfcSectionedSolidHorizontal.cpp | 179 +-------------- src/ifcgeom/mapping/IfcSectionedSurface.cpp | 93 ++++++++ src/ifcgeom/mapping/mapping.i | 3 + src/ifcgeom/piecewise_function_evaluator.cpp | 2 +- src/ifcgeom/piecewise_function_evaluator.h | 2 +- src/ifcgeom/taxonomy.cpp | 2 +- src/ifcgeom/taxonomy.h | 2 +- src/ifcwrap/IfcGeomWrapper.i | 2 +- 11 files changed, 361 insertions(+), 194 deletions(-) create mode 100644 src/ifcgeom/infra_sweep_helper.cpp create mode 100644 src/ifcgeom/infra_sweep_helper.h create mode 100644 src/ifcgeom/mapping/IfcSectionedSurface.cpp diff --git a/src/ifcgeom/infra_sweep_helper.cpp b/src/ifcgeom/infra_sweep_helper.cpp new file mode 100644 index 0000000000..5456a14fc9 --- /dev/null +++ b/src/ifcgeom/infra_sweep_helper.cpp @@ -0,0 +1,203 @@ +#include "profile_helper.h" +#include "infra_sweep_helper.h" +#include "piecewise_function_evaluator.h" + +#include + +using namespace ifcopenshell::geometry; + +namespace { + // std::lerp when upgrading to C++ 20 + template + T lerp(const T& a, const T& b, double t) { + return a + t * (b - a); + } +} + +taxonomy::loft::ptr ifcopenshell::geometry::make_loft(const Settings& settings_, const IfcUtil::IfcBaseClass* inst, const taxonomy::piecewise_function::ptr& pwf, std::vector& cross_sections) +{ + std::sort(cross_sections.begin(), cross_sections.end()); + + auto loft = taxonomy::make(); + // @todo intialize as default + loft->axis = nullptr; + + // @todo currently only the case is handled where directrix returns a piecewise_function + // @todo this "if" statement is not really required because the function returns at the start if the Directrix is not a piecewise function + if (pwf) { + piecewise_function_evaluator evaluator(pwf, &settings_); + double start = std::max(0., cross_sections.front().dist_along); + double end = std::min(pwf->length(), cross_sections.back().dist_along); + + if (end - start < 1.e-9) { + Logger::Warning("Empty sweep domain with start at " + std::to_string(cross_sections.front().dist_along) + " end at " + std::to_string(cross_sections.back().dist_along) + " and curve domain length " + std::to_string(pwf->length()), inst); + return nullptr; + } + + auto curve_length = end - start; + auto param_type = settings_.get().get(); + auto param = settings_.get().get(); + size_t num_steps = 0; + if (param_type == ifcopenshell::geometry::settings::PiecewiseStepMethod::MAXSTEPSIZE) { + // parameter is max step size + num_steps = (size_t)std::ceil(curve_length / param); + } else { + // parameter is minimum number of steps + num_steps = (size_t)std::ceil(param); + } + std::vector longitudes; + for (auto& x : cross_sections) { + longitudes.push_back(x.dist_along); + } + longitudes.push_back(std::numeric_limits::infinity()); + auto profile_index = longitudes.begin(); + for (size_t i = 0; i <= num_steps; ++i) { + auto dist_along = start + curve_length / num_steps * i; + while (dist_along > *(profile_index + 1)) { + profile_index++; + if (profile_index == longitudes.end()) { + // @todo handle this? + } + } + + auto relative_dist_along = (dist_along - *profile_index) / (*(profile_index + 1) - *profile_index); + const auto& profile_a = cross_sections[std::distance(longitudes.begin(), profile_index)].section_geometry; + const auto& offset_a = cross_sections[std::distance(longitudes.begin(), profile_index)].offset; + + taxonomy::geom_item::ptr interpolated = nullptr; + + // Only interpolate if: + // - there is a profile ahead of us, and + // - we're not exactly at the location of the current profile or whether there is an offset involved. + bool should_interpolate = + (profile_index + 1 < longitudes.end()) && + (relative_dist_along >= 1.e-9 || offset_a.cwiseAbs().maxCoeff() > 0.); + + if (should_interpolate) { + taxonomy::geom_item::ptr profile_b; + Eigen::Vector3d offset_b; + if ((profile_index + 1 < longitudes.end())) { + profile_b = cross_sections[std::distance(longitudes.begin(), profile_index) + 1].section_geometry; + offset_b = cross_sections[std::distance(longitudes.begin(), profile_index) + 1].offset; + } else { + profile_b = profile_a; + offset_b = offset_a; + } + + // Only interpolate if the profiles are different or either of the offsets is non-zero + bool should_interpolate2 = + (profile_a->instance != profile_b->instance) || + (offset_a.cwiseAbs().maxCoeff() > 0. || offset_b.cwiseAbs().maxCoeff() > 0.); + + if (should_interpolate2) { + + std::vector loops_a, loops_b; + + if (profile_a->kind() == taxonomy::FACE) { + interpolated = taxonomy::make(); + + auto profile_a_f = std::static_pointer_cast(profile_a); + auto profile_b_f = std::static_pointer_cast(profile_b); + + if (profile_a_f->children.size() != profile_b_f->children.size()) { + Logger::Warning("Mismatching number of face boundaries: " + + std::to_string(profile_a_f->children.size()) + " vs " + + std::to_string(profile_b_f->children.size()), + inst + ); + return nullptr; + } + loops_a = profile_a_f->children; + loops_b = profile_b_f->children; + } else { + loops_a = { std::static_pointer_cast(profile_a) }; + loops_b = { std::static_pointer_cast(profile_b) }; + interpolated = taxonomy::make(); + } + + // @todo should_interpolate should also be informed based by different face matrices. + if (profile_a->matrix || profile_b->matrix) { + interpolated->matrix = taxonomy::make(); + Eigen::Matrix4d m4a = Eigen::Matrix4d::Identity(); + Eigen::Matrix4d m4b = Eigen::Matrix4d::Identity(); + if (profile_a->matrix) { + m4a = profile_a->matrix->ccomponents(); + } + if (profile_b->matrix) { + m4b = profile_b->matrix->ccomponents(); + } + interpolated->matrix->components() = lerp(m4a, m4b, relative_dist_along); + } + + auto interpolated_offset = lerp(offset_a, offset_b, relative_dist_along); + taxonomy::loop::ptr w1, w2; + taxonomy::edge::ptr e1, e2; + for (auto tmp_ : boost::combine(loops_a, loops_b)) { + boost::tie(w1, w2) = tmp_; + if (w1->children.size() != w2->children.size()) { + Logger::Warning("Mismatching number of edges: " + + std::to_string(w1->children.size()) + " vs " + + std::to_string(w2->children.size()), + inst + ); + return nullptr; + } + std::vector points; + for (auto tmp__ : boost::combine(w1->children, w2->children)) { + boost::tie(e1, e2) = tmp__; + auto& p1 = boost::get(e1->start); + auto& p2 = boost::get(e2->start); + + auto p3 = (lerp(p1->ccomponents(), p2->ccomponents(), relative_dist_along) + interpolated_offset).eval(); + points.push_back(taxonomy::make(p3)); + } + if (!points.empty()) { + // close polygon by referencing first point + // @todo add a closed=true|false to polygon_from_points()? + points.push_back(points.front()); + } + + auto interpolated_loop = polygon_from_points(points); + if (interpolated->kind() == taxonomy::FACE) { + std::static_pointer_cast(interpolated)->children.push_back(interpolated_loop); + } else { + std::static_pointer_cast(interpolated)->children = interpolated_loop->children; + } + } + } + } + + auto m4 = evaluator.evaluate(dist_along); + /* { + std::wcout << "#" << pwf->instance->data().id() << " " << dist_along << ": " << m4.col(3).row(2).value() << std::endl; + }*/ + + Eigen::Matrix4d m4b = Eigen::Matrix4d::Identity(); + m4b.col(0).head<3>() = m4.col(1).head<3>().normalized(); + m4b.col(1).head<3>() = m4.col(2).head<3>().normalized(); + m4b.col(2).head<3>() = m4.col(0).head<3>().normalized(); + m4b.col(3).head<3>() = m4.col(3).head<3>(); + + if (interpolated) { + loft->children.push_back(interpolated); + } else { + if (profile_a->kind() == taxonomy::FACE) { + loft->children.push_back(std::static_pointer_cast(taxonomy::item::ptr(profile_a->clone_()))); + } else { + loft->children.push_back(std::static_pointer_cast(taxonomy::item::ptr(profile_a->clone_()))); + } + if (profile_a->matrix) { + loft->children.back()->matrix = taxonomy::matrix4::ptr(profile_a->matrix->clone_()); + } + } + if (!loft->children.back()->matrix) { + // @todo should this not be initialized by default? matrix4 already has a 'lazy identity' mechanism. + loft->children.back()->matrix = taxonomy::make(); + } + auto m = (m4b * loft->children.back()->matrix->ccomponents()).eval(); + loft->children.back()->matrix->components() = m; + } + } + + return loft; +} diff --git a/src/ifcgeom/infra_sweep_helper.h b/src/ifcgeom/infra_sweep_helper.h new file mode 100644 index 0000000000..41b38e6c9a --- /dev/null +++ b/src/ifcgeom/infra_sweep_helper.h @@ -0,0 +1,26 @@ +#ifndef LINEAR_SWEEP_HELPER_H +#define LINEAR_SWEEP_HELPER_H + +#include "taxonomy.h" +#include "ConversionSettings.h" + +namespace ifcopenshell { + + namespace geometry { + + struct cross_section { + double dist_along; + taxonomy::geom_item::ptr section_geometry; + Eigen::Vector3d offset; + + bool operator <(const cross_section& other) const { + return dist_along < other.dist_along; + } + }; + + taxonomy::loft::ptr make_loft(const Settings& settings_, const IfcUtil::IfcBaseClass* inst, const taxonomy::piecewise_function::ptr& directrix, std::vector& cross_sections); + } + +} + +#endif \ No newline at end of file diff --git a/src/ifcgeom/kernels/opencascade/loft.cpp b/src/ifcgeom/kernels/opencascade/loft.cpp index b418675fbd..70e174fe01 100644 --- a/src/ifcgeom/kernels/opencascade/loft.cpp +++ b/src/ifcgeom/kernels/opencascade/loft.cpp @@ -48,26 +48,43 @@ bool OpenCascadeKernel::convert(const taxonomy::loft::ptr loft, TopoDS_Shape& re for (auto it = loft->children.begin(); it < loft->children.end() - 1; ++it) { auto jt = it + 1; - std::array fa = { *it, *jt }; + std::array fa = { *it, *jt }; std::array shps; std::array ws; for (int i = 0; i < 2; ++i) { - if (!convert(fa[i], shps[i])) { - return false; + if (fa[i]->kind() == taxonomy::FACE) { + if (!convert(std::static_pointer_cast(fa[i]), shps[i])) { + return false; + } } - if (shps[i].ShapeType() != TopAbs_FACE) { + if (fa[i]->kind() == taxonomy::LOOP) { + TopoDS_Wire w; + if (!convert(std::static_pointer_cast(fa[i]), w)) { + return false; + } + shps[i] = w; + } + if (shps[i].ShapeType() != TopAbs_FACE && shps[i].ShapeType() != TopAbs_WIRE) { return false; } // @todo this is only outer wire - ws[i] = BRepTools::OuterWire(TopoDS::Face(shps[i])); + if (shps[i].ShapeType() == TopAbs_FACE) { + ws[i] = BRepTools::OuterWire(TopoDS::Face(shps[i])); + } else { + ws[i] = TopoDS::Wire(shps[i]); + } } - if (it == loft->children.begin()) { - // faces.Append(shps[0]); - BB.Add(comp, shps[0]); - } - if (jt == loft->children.end() - 1) { - // faces.Append(shps[1]); - BB.Add(comp, shps[1]); + if (shps[0].ShapeType() == TopAbs_FACE) { + // When processing a sectioned *surface* there are no + // begin and end caps that need to be added. + if (it == loft->children.begin()) { + // faces.Append(shps[0]); + BB.Add(comp, shps[0]); + } + if (jt == loft->children.end() - 1) { + // faces.Append(shps[1]); + BB.Add(comp, shps[1]); + } } BRepTools_WireExplorer a(ws[0]); BRepTools_WireExplorer b(ws[1]); diff --git a/src/ifcgeom/mapping/IfcSectionedSolidHorizontal.cpp b/src/ifcgeom/mapping/IfcSectionedSolidHorizontal.cpp index 95b750f82f..14ee9899ad 100644 --- a/src/ifcgeom/mapping/IfcSectionedSolidHorizontal.cpp +++ b/src/ifcgeom/mapping/IfcSectionedSolidHorizontal.cpp @@ -22,29 +22,10 @@ using namespace ifcopenshell::geometry; #include "../../ifcgeom/profile_helper.h" -#include "../piecewise_function_evaluator.h" - -#include +#include "../../ifcgeom/infra_sweep_helper.h" #ifdef SCHEMA_HAS_IfcSectionedSolidHorizontal -namespace { - // std::lerp when upgrading to C++ 20 - template - T lerp(const T& a, const T& b, double t) { - return a + t * (b - a); - } - - struct cross_section { - double dist_along; - taxonomy::face::ptr section_geometry; - Eigen::Vector3d offset; - - bool operator <(const cross_section& other) const { - return dist_along < other.dist_along; - } - }; -} taxonomy::ptr mapping::map_impl(const IfcSchema::IfcSectionedSolidHorizontal* inst) { std::vector cross_sections; @@ -106,163 +87,7 @@ taxonomy::ptr mapping::map_impl(const IfcSchema::IfcSectionedSolidHorizontal* in } } - std::sort(cross_sections.begin(), cross_sections.end()); - - auto loft = taxonomy::make(); - // @todo intialize as default - loft->axis = nullptr; - - // @todo currently only the case is handled where directrix returns a piecewise_function - // @todo this "if" statement is not really required because the function returns at the start if the Directrix is not a piecewise function - if (pwf) { - piecewise_function_evaluator evaluator(pwf, &settings_); - double start = std::max(0., cross_sections.front().dist_along); - double end = std::min(pwf->length(), cross_sections.back().dist_along); - - if (end - start < 1.e-9) { - Logger::Warning("Empty sweep domain with start at " + std::to_string(cross_sections.front().dist_along) + " end at " + std::to_string(cross_sections.back().dist_along) + " and curve domain length " + std::to_string(pwf->length()), inst); - return nullptr; - } - - auto curve_length = end - start; - auto param_type = settings_.get().get(); - auto param = settings_.get().get(); - size_t num_steps = 0; - if (param_type == ifcopenshell::geometry::settings::PiecewiseStepMethod::MAXSTEPSIZE) { - // parameter is max step size - num_steps = (size_t) std::ceil(curve_length / param); - } else { - // parameter is minimum number of steps - num_steps = (size_t) std::ceil(param); - } - std::vector longitudes; - for (auto& x : cross_sections) { - longitudes.push_back(x.dist_along); - } - longitudes.push_back(std::numeric_limits::infinity()); - auto profile_index = longitudes.begin(); - for (size_t i = 0; i <= num_steps; ++i) { - auto dist_along = start + curve_length / num_steps * i; - while (dist_along > *(profile_index+1)) { - profile_index++; - if (profile_index == longitudes.end()) { - // @todo handle this? - } - } - - auto relative_dist_along = (dist_along - *profile_index) / (*(profile_index+1) - *profile_index); - const auto& profile_a = cross_sections[std::distance(longitudes.begin(), profile_index)].section_geometry; - const auto& offset_a = cross_sections[std::distance(longitudes.begin(), profile_index)].offset; - - taxonomy::face::ptr interpolated = nullptr; - - // Only interpolate if: - // - there is a profile ahead of us, and - // - we're not exactly at the location of the current profile or whether there is an offset involved. - bool should_interpolate = - (profile_index + 1 < longitudes.end()) && - (relative_dist_along >= 1.e-9 || offset_a.cwiseAbs().maxCoeff() > 0.); - - if (should_interpolate) { - taxonomy::face::ptr profile_b; - Eigen::Vector3d offset_b; - if ((profile_index + 1 < longitudes.end())) { - profile_b = cross_sections[std::distance(longitudes.begin(), profile_index) + 1].section_geometry; - offset_b = cross_sections[std::distance(longitudes.begin(), profile_index) + 1].offset; - } else { - profile_b = profile_a; - offset_b = offset_a; - } - - // Only interpolate if the profiles are different or either of the offsets is non-zero - bool should_interpolate2 = - (profile_a->instance != profile_b->instance) || - (offset_a.cwiseAbs().maxCoeff() > 0. || offset_b.cwiseAbs().maxCoeff() > 0.); - - if (should_interpolate2) { - if (profile_a->children.size() != profile_b->children.size()) { - Logger::Warning("Mismatching number of face boundaries: " + - std::to_string(profile_a->children.size()) + " vs " + - std::to_string(profile_b->children.size()), - inst - ); - return nullptr; - } - interpolated = taxonomy::make(); - // @todo should_interpolate should also be informed based by different face matrices. - if (profile_a->matrix || profile_b->matrix) { - interpolated->matrix = taxonomy::make(); - Eigen::Matrix4d m4a = Eigen::Matrix4d::Identity(); - Eigen::Matrix4d m4b = Eigen::Matrix4d::Identity(); - if (profile_a->matrix) { - m4a = profile_a->matrix->ccomponents(); - } - if (profile_b->matrix) { - m4b = profile_b->matrix->ccomponents(); - } - interpolated->matrix->components() = lerp(m4a, m4b, relative_dist_along); - } - auto interpolated_offset = lerp(offset_a, offset_b, relative_dist_along); - taxonomy::loop::ptr w1, w2; - taxonomy::edge::ptr e1, e2; - for (auto tmp_ : boost::combine(profile_a->children, profile_b->children)) { - boost::tie(w1, w2) = tmp_; - if (w1->children.size() != w2->children.size()) { - Logger::Warning("Mismatching number of edges for face boundary: " + - std::to_string(w1->children.size()) + " vs " + - std::to_string(w2->children.size()), - inst - ); - return nullptr; - } - std::vector points; - for (auto tmp__ : boost::combine(w1->children, w2->children)) { - boost::tie(e1, e2) = tmp__; - auto& p1 = boost::get(e1->start); - auto& p2 = boost::get(e2->start); - - auto p3 = (lerp(p1->ccomponents(), p2->ccomponents(), relative_dist_along) + interpolated_offset).eval(); - points.push_back(taxonomy::make(p3)); - } - if (!points.empty()) { - // close polygon by referencing first point - // @todo add a closed=true|false to polygon_from_points()? - points.push_back(points.front()); - } - interpolated->children.push_back(polygon_from_points(points)); - } - } - } - - auto m4 = evaluator.evaluate(dist_along); - /* { - std::wcout << "#" << pwf->instance->data().id() << " " << dist_along << ": " << m4.col(3).row(2).value() << std::endl; - }*/ - - Eigen::Matrix4d m4b = Eigen::Matrix4d::Identity(); - m4b.col(0).head<3>() = m4.col(1).head<3>().normalized(); - m4b.col(1).head<3>() = m4.col(2).head<3>().normalized(); - m4b.col(2).head<3>() = m4.col(0).head<3>().normalized(); - m4b.col(3).head<3>() = m4.col(3).head<3>(); - - if (interpolated) { - loft->children.push_back(interpolated); - } else { - loft->children.push_back(taxonomy::face::ptr(profile_a->clone_())); - if (profile_a->matrix) { - loft->children.back()->matrix = taxonomy::matrix4::ptr(profile_a->matrix->clone_()); - } - } - if (!loft->children.back()->matrix) { - // @todo should this not be initialized by default? matrix4 already has a 'lazy identity' mechanism. - loft->children.back()->matrix = taxonomy::make(); - } - auto m = (m4b * loft->children.back()->matrix->ccomponents()).eval(); - loft->children.back()->matrix->components() = m; - } - } - - return loft; + return make_loft(settings_, inst, pwf, cross_sections); } #endif diff --git a/src/ifcgeom/mapping/IfcSectionedSurface.cpp b/src/ifcgeom/mapping/IfcSectionedSurface.cpp new file mode 100644 index 0000000000..3edcde7946 --- /dev/null +++ b/src/ifcgeom/mapping/IfcSectionedSurface.cpp @@ -0,0 +1,93 @@ +/******************************************************************************** + * * + * This file is part of IfcOpenShell. * + * * + * IfcOpenShell is free software: you can redistribute it and/or modify * + * it under the terms of the Lesser GNU General Public License as published by * + * the Free Software Foundation, either version 3.0 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 * + * Lesser GNU General Public License for more details. * + * * + * You should have received a copy of the Lesser GNU General Public License * + * along with this program. If not, see . * + * * + ********************************************************************************/ + +#include "mapping.h" +#define mapping POSTFIX_SCHEMA(mapping) +using namespace ifcopenshell::geometry; + +#include "../../ifcgeom/profile_helper.h" +#include "../../ifcgeom/infra_sweep_helper.h" + +#ifdef SCHEMA_HAS_IfcSectionedSurface + + +taxonomy::ptr mapping::map_impl(const IfcSchema::IfcSectionedSurface* inst) { + std::vector cross_sections; + + auto dir = map(inst->Directrix()); + auto pwf = taxonomy::dcast(dir); + if (!pwf) { + // Only implement on alignment curves + Logger::Warning("IfcSectionedSurface is only implemented for piecewise function Directrix curves", inst); + return nullptr; + } + + { + auto css = inst->CrossSections(); + auto csps = inst->CrossSectionPositions(); + std::vector faces; + + // The PointByDistanceExpressesions are factored out into (a) a cartesian offset relative to the + // reference frame along a certain curve location (b) the longitude. + + // The longitudes determine the range of the sweep and the offsets are interpolated in between + // sweep segments. + std::vector profile_offsets; + std::vector longitudes; + + for (auto& cs : *css) { + faces.push_back(std::move(taxonomy::cast(map(cs)))); + } +#ifdef SCHEMA_HAS_IfcPointByDistanceExpression + for (auto& csp : *csps) { + auto pbde = csp->Location()->as(true); + + longitudes.push_back(*pbde->DistanceAlong()->as(true) * length_unit_); + + // Corresponds to the profile X, Y directions (hopefully). + Eigen::Vector3d po( + pbde->OffsetLateral().get_value_or(0.), + // @todo I don't understand whether vertical is an offset relative to the tangent plane or to the global XY plane + pbde->OffsetVertical().get_value_or(0.), + 0. + ); + + profile_offsets.push_back(po); + } +#else + return nullptr; +#endif + if (faces.size() != profile_offsets.size()) { + Logger::Warning("Expected CrossSections and CrossSectionPositions to be equal length, but got " + std::to_string(faces.size()) + " and " + std::to_string(profile_offsets.size()) + " respectively", inst); + return nullptr; + } + if (faces.size() < 2) { + Logger::Warning("Expected at least two cross sections, but got " + std::to_string(faces.size()), inst); + return nullptr; + } + + for (size_t i = 0; i < faces.size(); ++i) { + cross_sections.push_back({ longitudes[i], faces[i], profile_offsets[i] }); + } + } + + return make_loft(settings_, inst, pwf, cross_sections); +} + +#endif diff --git a/src/ifcgeom/mapping/mapping.i b/src/ifcgeom/mapping/mapping.i index c6ed7a11f9..51e001f05d 100644 --- a/src/ifcgeom/mapping/mapping.i +++ b/src/ifcgeom/mapping/mapping.i @@ -135,6 +135,9 @@ BIND(IfcFixedReferenceSweptAreaSolid) #ifdef SCHEMA_HAS_IfcSectionedSolidHorizontal BIND(IfcSectionedSolidHorizontal) #endif +#ifdef SCHEMA_HAS_IfcSectionedSurface +BIND(IfcSectionedSurface) +#endif BIND(IfcCircle); BIND(IfcEllipse); diff --git a/src/ifcgeom/piecewise_function_evaluator.cpp b/src/ifcgeom/piecewise_function_evaluator.cpp index e4a03511ca..b85b165f21 100644 --- a/src/ifcgeom/piecewise_function_evaluator.cpp +++ b/src/ifcgeom/piecewise_function_evaluator.cpp @@ -4,7 +4,7 @@ using namespace ifcopenshell::geometry; -piecewise_function_evaluator::piecewise_function_evaluator(taxonomy::piecewise_function::const_ptr pwf, ifcopenshell::geometry::Settings* settings) : pwf_(pwf) { +piecewise_function_evaluator::piecewise_function_evaluator(taxonomy::piecewise_function::const_ptr pwf, const ifcopenshell::geometry::Settings* settings) : pwf_(pwf) { if (settings) { settings_ = *settings; } diff --git a/src/ifcgeom/piecewise_function_evaluator.h b/src/ifcgeom/piecewise_function_evaluator.h index ca1bddce54..3d91547268 100644 --- a/src/ifcgeom/piecewise_function_evaluator.h +++ b/src/ifcgeom/piecewise_function_evaluator.h @@ -10,7 +10,7 @@ namespace ifcopenshell { namespace geometry { /// @brief utility class to evaluate piecewise_function objects class piecewise_function_evaluator { public: - piecewise_function_evaluator(taxonomy::piecewise_function::const_ptr pwf, ifcopenshell::geometry::Settings* settings=nullptr); + piecewise_function_evaluator(taxonomy::piecewise_function::const_ptr pwf, const ifcopenshell::geometry::Settings* settings=nullptr); /// @brief returns a vector of "distance along" points where the evaluate function computes loop points std::vector evaluation_points() const; diff --git a/src/ifcgeom/taxonomy.cpp b/src/ifcgeom/taxonomy.cpp index 1dd61d72b0..2feacfae15 100644 --- a/src/ifcgeom/taxonomy.cpp +++ b/src/ifcgeom/taxonomy.cpp @@ -313,7 +313,7 @@ namespace { } bool compare(const loft& a, const loft& b) { - return compare_collection(a, b); + return compare_collection(a, b); } bool compare(const collection& a, const collection& b) { diff --git a/src/ifcgeom/taxonomy.h b/src/ifcgeom/taxonomy.h index db9c97863e..c1ced08786 100644 --- a/src/ifcgeom/taxonomy.h +++ b/src/ifcgeom/taxonomy.h @@ -810,7 +810,7 @@ typedef item const* ptr; } }; - struct loft : public collection_base { + struct loft : public collection_base { DECLARE_PTR(loft) item::ptr axis; diff --git a/src/ifcwrap/IfcGeomWrapper.i b/src/ifcwrap/IfcGeomWrapper.i index 2b2178e125..ca0851324f 100644 --- a/src/ifcwrap/IfcGeomWrapper.i +++ b/src/ifcwrap/IfcGeomWrapper.i @@ -316,7 +316,7 @@ assign_children_access(loop, edge); assign_children_access(face, loop); assign_children_access(shell, face); assign_children_access(solid, shell); -assign_children_access(loft, face); +assign_children_access(loft, geom_item); assign_children_access(boolean_result, geom_item); %define assign_matrix_access(item_name) From 2168b3b7bd8adc26d403482c63a667e422ac90b7 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Thu, 12 Sep 2024 18:40:14 +0500 Subject: [PATCH 44/56] black format --- src/bonsai/bonsai/bim/module/drawing/scheduler.py | 2 +- src/bonsai/bonsai/tool/cost.py | 14 ++++++-------- src/bonsai/bonsai/tool/web.py | 5 +++-- 3 files changed, 10 insertions(+), 11 deletions(-) diff --git a/src/bonsai/bonsai/bim/module/drawing/scheduler.py b/src/bonsai/bonsai/bim/module/drawing/scheduler.py index 7b2b6c23b6..568c3ca294 100644 --- a/src/bonsai/bonsai/bim/module/drawing/scheduler.py +++ b/src/bonsai/bonsai/bim/module/drawing/scheduler.py @@ -554,7 +554,7 @@ class Scheduler: text_params["font-style"] = "italic" if text_color: text_params["fill"] = text_color - + text_params["class_"] = "schedule" if len(text_lines) == 1 and not wrap_text: diff --git a/src/bonsai/bonsai/tool/cost.py b/src/bonsai/bonsai/tool/cost.py index 4c68d31d12..8b38502c14 100644 --- a/src/bonsai/bonsai/tool/cost.py +++ b/src/bonsai/bonsai/tool/cost.py @@ -295,7 +295,6 @@ class Cost(bonsai.core.tool.Cost): unit = cls.get_quantity_unit_symbol(quantity) return selected_quantitites, unit - @classmethod def get_assigned_product(cls, cost_item, quantity): assigned_products = cls.get_cost_item_assignments(cost_item, filter_by_type="PRODUCT", is_deep=False) @@ -868,11 +867,7 @@ class Cost(bonsai.core.tool.Cost): bpy.ops.bim.connect_websocket_server(page="costing") cost_schedule_data = cls.create_cost_schedule_json(cost_chedule) tool.Web.send_webui_data( - data={ - "cost_items": cost_schedule_data, - "cost_schedule_id": cost_chedule.id(), - "currency": cls.currency() - }, + data={"cost_items": cost_schedule_data, "cost_schedule_id": cost_chedule.id(), "currency": cls.currency()}, data_key="cost_items", event="cost_items", ) @@ -886,7 +881,11 @@ class Cost(bonsai.core.tool.Cost): if not cost_item: return results results["quantity_type"] = cost_item.CostQuantities[0].is_a() if cost_item.CostQuantities else None - unit = ifcopenshell.util.unit.get_property_unit(cost_item.CostQuantities[0], tool.Ifc.get()) if cost_item.CostQuantities else None + unit = ( + ifcopenshell.util.unit.get_property_unit(cost_item.CostQuantities[0], tool.Ifc.get()) + if cost_item.CostQuantities + else None + ) if unit: results["unit_symbol"] = ifcopenshell.util.unit.get_unit_symbol(unit) for quantity in cost_item.CostQuantities or []: @@ -897,4 +896,3 @@ class Cost(bonsai.core.tool.Cost): if results["quantity_type"] == "IfcQuantityCount": results["unit_symbol"] = "U" return results - diff --git a/src/bonsai/bonsai/tool/web.py b/src/bonsai/bonsai/tool/web.py index c8d3f1fb82..d459ac1e3c 100644 --- a/src/bonsai/bonsai/tool/web.py +++ b/src/bonsai/bonsai/tool/web.py @@ -476,7 +476,9 @@ class Web(bonsai.core.tool.Web): cls.load_cost_schedule_web_ui(cost_schedule) cls.enableEditingCostItemQuantities(cost_item) if operator_data["type"] == "deleteCostItemQuantity": - bpy.ops.bim.remove_cost_item_quantity(cost_item=operator_data["costItemId"], physical_quantity=operator_data["quantityId"]) + bpy.ops.bim.remove_cost_item_quantity( + cost_item=operator_data["costItemId"], physical_quantity=operator_data["quantityId"] + ) cost_item_id = operator_data["costItemId"] cost_item = ifc_file.by_id(cost_item_id) cost_schedule = tool.Cost.get_cost_schedule(cost_item) @@ -506,7 +508,6 @@ class Web(bonsai.core.tool.Web): event="quantities", ) - @classmethod def selection_data(cls, cost_item, elements): if not elements: From aa97ff7f167a9d96446cab053f9e67423b98facd Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Wed, 11 Sep 2024 18:34:56 +0500 Subject: [PATCH 45/56] typing --- src/bonsai/bonsai/bim/module/profile/operator.py | 1 + src/bonsai/bonsai/bim/module/spatial/prop.py | 1 + src/bonsai/bonsai/tool/blender.py | 2 +- src/bonsai/bonsai/tool/pset.py | 1 - .../api/cost/calculate_cost_item_resource_value.py | 7 ++++--- src/ifcopenshell-python/ifcopenshell/util/resource.py | 6 +++--- 6 files changed, 10 insertions(+), 8 deletions(-) diff --git a/src/bonsai/bonsai/bim/module/profile/operator.py b/src/bonsai/bonsai/bim/module/profile/operator.py index 459f9bf1ce..ba5c0f168a 100644 --- a/src/bonsai/bonsai/bim/module/profile/operator.py +++ b/src/bonsai/bonsai/bim/module/profile/operator.py @@ -18,6 +18,7 @@ import bpy import ifcopenshell.api +import ifcopenshell.util.element import bonsai.bim.helper import bonsai.tool as tool import bonsai.bim.module.model.profile as model_profile diff --git a/src/bonsai/bonsai/bim/module/spatial/prop.py b/src/bonsai/bonsai/bim/module/spatial/prop.py index c2b1def5b6..10b5ec1111 100644 --- a/src/bonsai/bonsai/bim/module/spatial/prop.py +++ b/src/bonsai/bonsai/bim/module/spatial/prop.py @@ -34,6 +34,7 @@ import bonsai.tool as tool import bonsai.core.geometry import ifcopenshell import ifcopenshell.util.element +import ifcopenshell.util.unit def get_subelement_class(self, context): diff --git a/src/bonsai/bonsai/tool/blender.py b/src/bonsai/bonsai/tool/blender.py index 5ea0cb05e8..7d15d5772c 100644 --- a/src/bonsai/bonsai/tool/blender.py +++ b/src/bonsai/bonsai/tool/blender.py @@ -803,7 +803,7 @@ class Blender(bonsai.core.tool.Blender): return collections_mapping @classmethod - def is_editable(cls, obj): + def is_editable(cls, obj: bpy.types.Object) -> bool: if obj.type not in cls.OBJECT_TYPES_THAT_SUPPORT_EDIT_MODE: return False if not (element := tool.Ifc.get_entity(obj)): diff --git a/src/bonsai/bonsai/tool/pset.py b/src/bonsai/bonsai/tool/pset.py index b4d9e936d6..e5436513db 100644 --- a/src/bonsai/bonsai/tool/pset.py +++ b/src/bonsai/bonsai/tool/pset.py @@ -327,4 +327,3 @@ class Pset(bonsai.core.tool.Pset): return value except: return value - return value diff --git a/src/ifcopenshell-python/ifcopenshell/api/cost/calculate_cost_item_resource_value.py b/src/ifcopenshell-python/ifcopenshell/api/cost/calculate_cost_item_resource_value.py index c58bd7e657..b160803d0a 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/cost/calculate_cost_item_resource_value.py +++ b/src/ifcopenshell-python/ifcopenshell/api/cost/calculate_cost_item_resource_value.py @@ -102,9 +102,10 @@ def calculate_cost_item_resource_value(file: ifcopenshell.file, cost_item: ifcop for resource in resources: cost, unit = ifcopenshell.util.resource.get_cost(resource) if not cost: - cost, unit = ifcopenshell.util.resource.get_parent_cost( - resource - ) # Concept to standardise - Not defined in schema, but this makes manual scheduling of resources 10x faster and less duplicate data. + # Concept to standardise - Not defined in schema, but this makes manual scheduling of resources 10x faster and less duplicate data. + parent_cost = ifcopenshell.util.resource.get_parent_cost(resource) + assert parent_cost + cost, unit = parent_cost quantity = ifcopenshell.util.resource.get_quantity(resource) if not cost or not quantity: continue diff --git a/src/ifcopenshell-python/ifcopenshell/util/resource.py b/src/ifcopenshell-python/ifcopenshell/util/resource.py index adce5bfa3f..8a444e6371 100644 --- a/src/ifcopenshell-python/ifcopenshell/util/resource.py +++ b/src/ifcopenshell-python/ifcopenshell/util/resource.py @@ -166,9 +166,9 @@ def get_quantity(resource: ifcopenshell.entity_instance) -> float: return duration.total_seconds() / 3600 -def get_parent_cost(resource: ifcopenshell.entity_instance) -> Union[None, tuple[float, Union[str, None]]]: - if not resource.Nests: +def get_parent_cost(resource: ifcopenshell.entity_instance) -> Union[tuple[float, Union[str, None]], None]: + if not (nests := resource.Nests): return else: - cost = get_cost(resource.Nests[0].RelatingObject) + cost = get_cost(nests[0].RelatingObject) return cost From e81fe6021885cb0126dce6eeee159f76cfca4252 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Wed, 11 Sep 2024 21:48:29 +0500 Subject: [PATCH 46/56] CMakeLists - fix issue on msvc >= 14.40 #5158 --- cmake/CMakeLists.txt | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/cmake/CMakeLists.txt b/cmake/CMakeLists.txt index f77ffaa117..75d7fe5fa6 100644 --- a/cmake/CMakeLists.txt +++ b/cmake/CMakeLists.txt @@ -730,6 +730,10 @@ if(MSVC) # endif() add_definitions(-D_ENABLE_EXTENDED_ALIGNED_STORAGE) + # See #5158. + if(CMAKE_CXX_COMPILER_VERSION VERSION_GREATER_EQUAL 19.40) + add_definitions(-D_DISABLE_CONSTEXPR_MUTEX_CONSTRUCTOR) + endif() else() add_definitions(-Wall -Wextra) From cc90c92dec4bf79ed18eb45f01f65ebd4eed947a Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Thu, 12 Sep 2024 11:08:17 +0500 Subject: [PATCH 47/56] bim.add_proposed_prop to show error message if property already exists --- src/bonsai/bonsai/bim/module/pset/operator.py | 5 ++++- src/bonsai/bonsai/core/pset.py | 7 +++++-- src/bonsai/bonsai/tool/pset.py | 4 ++-- 3 files changed, 11 insertions(+), 5 deletions(-) diff --git a/src/bonsai/bonsai/bim/module/pset/operator.py b/src/bonsai/bonsai/bim/module/pset/operator.py index 5870547bce..24662bee04 100644 --- a/src/bonsai/bonsai/bim/module/pset/operator.py +++ b/src/bonsai/bonsai/bim/module/pset/operator.py @@ -440,5 +440,8 @@ class AddProposedProp(bpy.types.Operator): prop_value: bpy.props.StringProperty() def execute(self, context): - core.add_proposed_prop(tool.Pset, self.obj, self.obj_type, self.prop_name, self.prop_value) + res = core.add_proposed_prop(tool.Pset, self.obj, self.obj_type, self.prop_name, self.prop_value) + if res: + self.report({"ERROR"}, res) + return {"CANCELLED"} return {"FINISHED"} diff --git a/src/bonsai/bonsai/core/pset.py b/src/bonsai/bonsai/core/pset.py index c2b87836df..c96e5177ff 100644 --- a/src/bonsai/bonsai/core/pset.py +++ b/src/bonsai/bonsai/core/pset.py @@ -89,9 +89,12 @@ def enable_pset_editing( pset_tool.enable_proposed_pset(props, pset_name, pset_type, has_template) -def add_proposed_prop(pset: tool.Pset, obj_name: str, obj_type: tool.Ifc.OBJECT_TYPE, name: str, value: Any) -> None: +def add_proposed_prop( + pset: tool.Pset, obj_name: str, obj_type: tool.Ifc.OBJECT_TYPE, name: str, value: Any +) -> Union[None, str]: props = pset.get_pset_props(obj_name, obj_type) - pset.add_proposed_property(name, pset.cast_string_to_primitive(value), props) + res = pset.add_proposed_property(name, pset.cast_string_to_primitive(value), props) + return res def unshare_pset( diff --git a/src/bonsai/bonsai/tool/pset.py b/src/bonsai/bonsai/tool/pset.py index e5436513db..dcfa111872 100644 --- a/src/bonsai/bonsai/tool/pset.py +++ b/src/bonsai/bonsai/tool/pset.py @@ -297,9 +297,9 @@ class Pset(bonsai.core.tool.Pset): return bonsai.bim.schema.ifc.psetqto.get_by_name(name) @classmethod - def add_proposed_property(cls, name: str, value: Any, props: bpy.types.PropertyGroup) -> None: + def add_proposed_property(cls, name: str, value: Any, props: bpy.types.PropertyGroup) -> Union[None, str]: if props.properties.get(name): - return + return f"Property '{name}' already exists." prop = props.properties.add() prop.name = name metadata = prop.metadata From 0f6cc7326c5d79c27dddb15fbe6816ca279cb88b Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Thu, 12 Sep 2024 11:38:08 +0500 Subject: [PATCH 48/56] bim.add_proposed_prop - document possible property types --- src/bonsai/bonsai/bim/module/pset/operator.py | 8 ++++++++ src/bonsai/bonsai/bim/module/pset/prop.py | 1 + 2 files changed, 9 insertions(+) diff --git a/src/bonsai/bonsai/bim/module/pset/operator.py b/src/bonsai/bonsai/bim/module/pset/operator.py index 24662bee04..fe08585701 100644 --- a/src/bonsai/bonsai/bim/module/pset/operator.py +++ b/src/bonsai/bonsai/bim/module/pset/operator.py @@ -433,6 +433,14 @@ class BIM_OT_bulk_remove_psets(bpy.types.Operator, tool.Ifc.Operator): class AddProposedProp(bpy.types.Operator): bl_idname = "bim.add_proposed_prop" bl_label = "Add Proposed Prop" + bl_description = ( + "Add proposed property to the custom property set.\n\n" + "Property type will be deduced from the provided value. Possible types:\n" + "- provide an integer or a float to create integer/real property\n" + "- 'true', 'false' to add a boolean property\n" + "- 'null' or '' (empty value) to add a null property\n" + "- any other value will be added as a string property" + ) bl_options = {"REGISTER", "UNDO"} obj: bpy.props.StringProperty() obj_type: bpy.props.StringProperty() diff --git a/src/bonsai/bonsai/bim/module/pset/prop.py b/src/bonsai/bonsai/bim/module/pset/prop.py index 074ac3d73f..7804521221 100644 --- a/src/bonsai/bonsai/bim/module/pset/prop.py +++ b/src/bonsai/bonsai/bim/module/pset/prop.py @@ -243,6 +243,7 @@ class PsetProperties(PropertyGroup): properties: CollectionProperty(name="Properties", type=IfcProperty) pset_name: EnumProperty(items=get_pset_name, name="Pset Name") qto_name: EnumProperty(items=get_qto_name, name="Qto Name") + # Proposed property. prop_name: StringProperty(name="Property Name", default="MyProperty") prop_value: StringProperty(name="Property Value", default="Some Value") From 094bbe6913019fd4b2c2976e1778afe40462d0d3 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Thu, 12 Sep 2024 14:57:25 +0500 Subject: [PATCH 49/56] bim.override_origin_set to reuse descriptions from blender operator --- src/bonsai/bonsai/bim/module/geometry/operator.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/bonsai/bonsai/bim/module/geometry/operator.py b/src/bonsai/bonsai/bim/module/geometry/operator.py index cd154017e9..1016c8a636 100644 --- a/src/bonsai/bonsai/bim/module/geometry/operator.py +++ b/src/bonsai/bonsai/bim/module/geometry/operator.py @@ -98,10 +98,19 @@ class OverrideMeshSeparate(bpy.types.Operator, tool.Ifc.Operator): class OverrideOriginSet(bpy.types.Operator, tool.Ifc.Operator): bl_idname = "bim.override_origin_set" + blender_op = bpy.ops.object.origin_set.get_rna_type() bl_label = "IFC Origin Set" + bl_description = ( + blender_op.description + ".\nAlso makes sure changes are in sync with IFC (opeartor works only on IFC objects)" + ) bl_options = {"REGISTER", "UNDO"} obj: bpy.props.StringProperty() - origin_type: bpy.props.StringProperty() + blender_type_prop = blender_op.properties["type"] + origin_type: bpy.props.EnumProperty( + name=blender_type_prop.name, + default=blender_type_prop.default, + items=[(i.identifier, i.name, i.description) for i in blender_type_prop.enum_items], + ) def _execute(self, context): objs = [bpy.data.objects.get(self.obj)] if self.obj else context.selected_objects From 3bb42438504ff7ccfaea042d84a75c17bdc99602 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Thu, 12 Sep 2024 17:31:48 +0500 Subject: [PATCH 50/56] Purge unused styles and materials #3914 Example - https://imgur.com/a/JXEofa1 --- .../bonsai/bim/module/debug/__init__.py | 1 + .../bonsai/bim/module/debug/operator.py | 53 +++++++++++++++++++ .../bonsai/bim/module/profile/__init__.py | 1 - .../bonsai/bim/module/profile/operator.py | 15 ------ src/bonsai/bonsai/bim/module/project/ui.py | 6 ++- .../bonsai/bim/module/style/operator.py | 3 +- src/bonsai/bonsai/bim/module/type/__init__.py | 1 - src/bonsai/bonsai/bim/module/type/operator.py | 10 ---- src/bonsai/bonsai/tool/debug.py | 12 +++++ 9 files changed, 72 insertions(+), 30 deletions(-) diff --git a/src/bonsai/bonsai/bim/module/debug/__init__.py b/src/bonsai/bonsai/bim/module/debug/__init__.py index 629bb05dc6..1e19ff678a 100644 --- a/src/bonsai/bonsai/bim/module/debug/__init__.py +++ b/src/bonsai/bonsai/bim/module/debug/__init__.py @@ -36,6 +36,7 @@ classes = ( operator.ProfileImportIFC, operator.PurgeHdf5Cache, operator.PurgeUnusedElementsByClass, + operator.PurgeUnusedObjects, operator.RestartBlender, operator.RewindInspector, operator.SelectExpressFile, diff --git a/src/bonsai/bonsai/bim/module/debug/operator.py b/src/bonsai/bonsai/bim/module/debug/operator.py index 44c4eef5da..83ee47d07f 100644 --- a/src/bonsai/bonsai/bim/module/debug/operator.py +++ b/src/bonsai/bonsai/bim/module/debug/operator.py @@ -32,6 +32,8 @@ import ifcopenshell.util.representation import ifcopenshell.util.unit import bonsai.tool as tool import bonsai.core.debug as core +import bonsai.core.profile +import bonsai.core.type import bonsai.bim.handler import bonsai.bim.import_ifc as import_ifc from pathlib import Path @@ -608,6 +610,57 @@ class PurgeUnusedElementsByClass(bpy.types.Operator, tool.Ifc.Operator): tool.Ifc.get().write(self.filepath) +class PurgeUnusedObjects(bpy.types.Operator, tool.Ifc.Operator): + bl_idname = "bim.purge_unused_objects" + bl_label = "Purge Unused Objects" + bl_options = {"REGISTER", "UNDO"} + + object_type: bpy.props.EnumProperty( + name="Object Type", + items=( + ("TYPE", "Type", ""), + ("PROFILE", "Profile", ""), + ("STYLE", "Style", ""), + ("MATERIAL", "Material", ""), + ), + ) + + def _execute(self, context): + object_type = self.object_type + if object_type == "TYPE": + purged = bonsai.core.type.purge_unused_types(tool.Ifc, tool.Type, tool.Geometry) + elif object_type == "PROFILE": + purged = bonsai.core.profile.purge_unused_profiles(tool.Ifc, tool.Profile) + elif object_type == "STYLE": + purged = tool.Debug.purge_unused_class("IfcPresentationStyle") + elif object_type == "MATERIAL": + ifc_file = tool.Ifc.get() + is_ifc2x3 = ifc_file.schema == "IFC2X3" + if is_ifc2x3: + purged = tool.Debug.purge_unused_class("IfcMaterial") + else: + purged = tool.Debug.purge_unused_class("IfcMaterialDefinition") + else: + self.report({"ERROR"}, f"Invalid object type {object_type}.") + return {"CANCELLED"} + + self.report({"INFO"}, f"{purged} unused {object_type.lower()}s were purged.") + + if purged == 0: + return + + scene = context.scene + if object_type == "PROFILE": + if scene.BIMProfileProperties.is_editing: + bpy.ops.bim.load_profiles() + elif object_type == "STYLE": + if scene.BIMStylesProperties.is_editing: + bpy.ops.bim.load_styles() + elif object_type == "MATERIAL": + if scene.BIMMaterialProperties.is_editing: + bpy.ops.bim.load_materials() + + class PipInstall(bpy.types.Operator): bl_idname = "bim.pip_install" bl_label = "Pip Install" diff --git a/src/bonsai/bonsai/bim/module/profile/__init__.py b/src/bonsai/bonsai/bim/module/profile/__init__.py index d06bc1c78c..a9a4895884 100644 --- a/src/bonsai/bonsai/bim/module/profile/__init__.py +++ b/src/bonsai/bonsai/bim/module/profile/__init__.py @@ -30,7 +30,6 @@ classes = ( operator.EnableEditingArbitraryProfile, operator.EnableEditingProfile, operator.LoadProfiles, - operator.PurgeUnusedProfiles, operator.RemoveProfileDef, prop.Profile, prop.BIMProfileProperties, diff --git a/src/bonsai/bonsai/bim/module/profile/operator.py b/src/bonsai/bonsai/bim/module/profile/operator.py index ba5c0f168a..15e4b9ea7a 100644 --- a/src/bonsai/bonsai/bim/module/profile/operator.py +++ b/src/bonsai/bonsai/bim/module/profile/operator.py @@ -264,18 +264,3 @@ class EditArbitraryProfile(bpy.types.Operator, tool.Ifc.Operator): props.active_arbitrary_profile_id = 0 model_profile.DumbProfileRegenerator().regenerate_from_profile_def(profile) - - -class PurgeUnusedProfiles(bpy.types.Operator, tool.Ifc.Operator): - bl_idname = "bim.purge_unused_profiles" - bl_label = "Purge Unused Profiles" - bl_options = {"REGISTER", "UNDO"} - - def _execute(self, context): - props = context.scene.BIMProfileProperties - purged_profiles = core.purge_unused_profiles(tool.Ifc, tool.Profile) - self.report({"INFO"}, f"{purged_profiles} profiles were purged.") - - if props.is_editing: - refresh() - bpy.ops.bim.load_profiles() diff --git a/src/bonsai/bonsai/bim/module/project/ui.py b/src/bonsai/bonsai/bim/module/project/ui.py index 1b476ba1e4..9004f5a84d 100644 --- a/src/bonsai/bonsai/bim/module/project/ui.py +++ b/src/bonsai/bonsai/bim/module/project/ui.py @@ -519,5 +519,7 @@ class BIM_PT_purge(Panel): def draw(self, context): layout = self.layout - layout.operator("bim.purge_unused_profiles") - layout.operator("bim.purge_unused_types") + layout.operator("bim.purge_unused_objects", text="Purge Unused Profiles").object_type = "PROFILE" + layout.operator("bim.purge_unused_objects", text="Purge Unused Types").object_type = "TYPE" + layout.operator("bim.purge_unused_objects", text="Purge Unused Styles").object_type = "STYLE" + layout.operator("bim.purge_unused_objects", text="Purge Unused Materials").object_type = "MATERIAL" diff --git a/src/bonsai/bonsai/bim/module/style/operator.py b/src/bonsai/bonsai/bim/module/style/operator.py index 22dd98b7ba..ebfbec53af 100644 --- a/src/bonsai/bonsai/bim/module/style/operator.py +++ b/src/bonsai/bonsai/bim/module/style/operator.py @@ -460,7 +460,8 @@ class LoadStyles(bpy.types.Operator, tool.Ifc.Operator): style_type: bpy.props.StringProperty() def _execute(self, context): - core.load_styles(tool.Style, style_type=self.style_type) + style_type = self.style_type if self.style_type else context.scene.BIMStylesProperties.style_type + core.load_styles(tool.Style, style_type=style_type) class SelectByStyle(bpy.types.Operator, tool.Ifc.Operator): diff --git a/src/bonsai/bonsai/bim/module/type/__init__.py b/src/bonsai/bonsai/bim/module/type/__init__.py index 445c9e1e28..f0e5546740 100644 --- a/src/bonsai/bonsai/bim/module/type/__init__.py +++ b/src/bonsai/bonsai/bim/module/type/__init__.py @@ -26,7 +26,6 @@ classes = ( operator.DisableEditingType, operator.DuplicateType, operator.EnableEditingType, - operator.PurgeUnusedTypes, operator.RemoveType, operator.RenameType, operator.SelectSimilarType, diff --git a/src/bonsai/bonsai/bim/module/type/operator.py b/src/bonsai/bonsai/bim/module/type/operator.py index c72cebcf71..be652058ab 100644 --- a/src/bonsai/bonsai/bim/module/type/operator.py +++ b/src/bonsai/bonsai/bim/module/type/operator.py @@ -555,13 +555,3 @@ class DuplicateType(bpy.types.Operator, tool.Ifc.Operator): context.scene.BIMModelProperties.ifc_class = new.is_a() context.scene.BIMModelProperties.relating_type_id = str(new_obj.BIMObjectProperties.ifc_definition_id) return {"FINISHED"} - - -class PurgeUnusedTypes(bpy.types.Operator, tool.Ifc.Operator): - bl_idname = "bim.purge_unused_types" - bl_label = "Purge Unused Types" - bl_options = {"REGISTER", "UNDO"} - - def _execute(self, context): - purged_types = core.purge_unused_types(tool.Ifc, tool.Type, tool.Geometry) - self.report({"INFO"}, f"{purged_types} types were purged.") diff --git a/src/bonsai/bonsai/tool/debug.py b/src/bonsai/bonsai/tool/debug.py index 99bf884aa9..5451cae5b6 100644 --- a/src/bonsai/bonsai/tool/debug.py +++ b/src/bonsai/bonsai/tool/debug.py @@ -97,3 +97,15 @@ class Debug(bonsai.core.tool.Debug): print(f"{class_string: <50} {unused[ifc_class]: >5}") return sum(unused.values()) + + @classmethod + def purge_unused_class(cls, ifc_class: str) -> int: + ifc_file = tool.Ifc.get() + elements = ifc_file.by_type(ifc_class) + i = 0 + for element in elements: + if ifc_file.get_total_inverses(element) != 0: + continue + ifcopenshell.util.element.remove_deep(ifc_file, element) + i += 1 + return i From e5a59d0bea1167854a11159af86d5cf275dfbea1 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Thu, 12 Sep 2024 17:49:24 +0500 Subject: [PATCH 51/56] remove unnecessary ifc operators --- .../bonsai/bim/module/material/operator.py | 30 +++++++++++-------- .../bonsai/bim/module/profile/operator.py | 17 ++++++----- .../bonsai/bim/module/style/operator.py | 25 +++++++++------- src/bonsai/bonsai/bim/module/type/operator.py | 5 ++-- 4 files changed, 46 insertions(+), 31 deletions(-) diff --git a/src/bonsai/bonsai/bim/module/material/operator.py b/src/bonsai/bonsai/bim/module/material/operator.py index 2575e62c6c..1e6094e5fb 100644 --- a/src/bonsai/bonsai/bim/module/material/operator.py +++ b/src/bonsai/bonsai/bim/module/material/operator.py @@ -32,43 +32,47 @@ from bonsai.bim.module.material.prop import purge as material_prop_purge from bonsai.bim.ifc import IfcStore -class LoadMaterials(bpy.types.Operator, tool.Ifc.Operator): +class LoadMaterials(bpy.types.Operator): bl_idname = "bim.load_materials" bl_label = "Load Materials" bl_description = "Display list of named materials" bl_options = {"REGISTER", "UNDO"} - def _execute(self, context): + def execute(self, context): core.load_materials(tool.Material, context.scene.BIMMaterialProperties.material_type) + return {"FINISHED"} -class DisableEditingMaterials(bpy.types.Operator, tool.Ifc.Operator): +class DisableEditingMaterials(bpy.types.Operator): bl_idname = "bim.disable_editing_materials" bl_label = "Disable Editing Materials" bl_options = {"REGISTER", "UNDO"} - def _execute(self, context): + def execute(self, context): core.disable_editing_materials(tool.Material) + return {"FINISHED"} -class SelectByMaterial(bpy.types.Operator, tool.Ifc.Operator): +class SelectByMaterial(bpy.types.Operator): bl_idname = "bim.select_by_material" bl_label = "Select By Material" bl_options = {"REGISTER", "UNDO"} material: bpy.props.IntProperty() - def _execute(self, context): + def execute(self, context): core.select_by_material(tool.Material, tool.Spatial, material=tool.Ifc.get().by_id(self.material)) + return {"FINISHED"} -class EnableEditingMaterial(bpy.types.Operator, tool.Ifc.Operator): +class EnableEditingMaterial(bpy.types.Operator): bl_idname = "bim.enable_editing_material" bl_label = "Enable Editing Material" bl_options = {"REGISTER", "UNDO"} material: bpy.props.IntProperty() - def _execute(self, context): + def execute(self, context): core.enable_editing_material(tool.Material, material=tool.Ifc.get().by_id(self.material)) + return {"FINISHED"} class EditMaterial(bpy.types.Operator, tool.Ifc.Operator): @@ -81,14 +85,15 @@ class EditMaterial(bpy.types.Operator, tool.Ifc.Operator): core.edit_material(tool.Ifc, tool.Material, material=tool.Ifc.get().by_id(self.material)) -class DisableEditingMaterial(bpy.types.Operator, tool.Ifc.Operator): +class DisableEditingMaterial(bpy.types.Operator): bl_idname = "bim.disable_editing_material" bl_label = "Disable Editing Material" bl_options = {"REGISTER", "UNDO"} material: bpy.props.IntProperty() - def _execute(self, context): + def execute(self, context): core.disable_editing_material(tool.Material) + return {"FINISHED"} class AssignParameterizedProfile(bpy.types.Operator, tool.Ifc.Operator): @@ -776,13 +781,13 @@ class ContractMaterialCategory(bpy.types.Operator): return {"FINISHED"} -class EnableEditingMaterialStyle(bpy.types.Operator, tool.Ifc.Operator): +class EnableEditingMaterialStyle(bpy.types.Operator): bl_idname = "bim.enable_editing_material_style" bl_label = "Enable Editing Material Style" bl_options = {"REGISTER", "UNDO"} material: bpy.props.IntProperty() - def _execute(self, context): + def execute(self, context): props = bpy.context.scene.BIMMaterialProperties props.active_material_id = self.material props.editing_material_type = "STYLE" @@ -800,6 +805,7 @@ class EnableEditingMaterialStyle(bpy.types.Operator, tool.Ifc.Operator): style = rep.Items[0].Styles[0] if style.Name: # props.styles only has named styles props.styles = str(rep.Items[0].Styles[0].id()) + return {"FINISHED"} class EditMaterialStyle(bpy.types.Operator, tool.Ifc.Operator): diff --git a/src/bonsai/bonsai/bim/module/profile/operator.py b/src/bonsai/bonsai/bim/module/profile/operator.py index 15e4b9ea7a..cb7cd27491 100644 --- a/src/bonsai/bonsai/bim/module/profile/operator.py +++ b/src/bonsai/bonsai/bim/module/profile/operator.py @@ -98,17 +98,18 @@ class RemoveProfileDef(bpy.types.Operator, tool.Ifc.Operator): props.active_profile_index = min(current_index, len(props.profiles) - 1) -class EnableEditingProfile(bpy.types.Operator, tool.Ifc.Operator): +class EnableEditingProfile(bpy.types.Operator): bl_idname = "bim.enable_editing_profile" bl_label = "Enable Editing Profile" bl_options = {"REGISTER", "UNDO"} profile: bpy.props.IntProperty() - def _execute(self, context): + def execute(self, context): props = context.scene.BIMProfileProperties props.profile_attributes.clear() bonsai.bim.helper.import_attributes2(tool.Ifc.get().by_id(self.profile), props.profile_attributes) props.active_profile_id = self.profile + return {"FINISHED"} class DisableEditingProfile(bpy.types.Operator): @@ -176,12 +177,12 @@ class DuplicateProfileDef(bpy.types.Operator, tool.Ifc.Operator): bpy.ops.bim.load_profiles() -class EnableEditingArbitraryProfile(bpy.types.Operator, tool.Ifc.Operator): +class EnableEditingArbitraryProfile(bpy.types.Operator): bl_idname = "bim.enable_editing_arbitrary_profile" bl_label = "Enable Editing Arbitrary Profile" bl_options = {"REGISTER", "UNDO"} - def _execute(self, context): + def execute(self, context): props = context.scene.BIMProfileProperties active_profile = props.profiles[props.active_profile_index] profile_id = active_profile.ifc_definition_id @@ -194,6 +195,7 @@ class EnableEditingArbitraryProfile(bpy.types.Operator, tool.Ifc.Operator): bpy.ops.object.mode_set(mode="EDIT") ProfileDecorator.install(context, exit_edit_mode_callback=lambda: disable_editing_arbitrary_profile(context)) tool.Blender.set_viewport_tool("bim.cad_tool") + return {"FINISHED"} def disable_editing_arbitrary_profile(context): @@ -212,13 +214,14 @@ def disable_editing_arbitrary_profile(context): refresh() -class DisableEditingArbitraryProfile(bpy.types.Operator, tool.Ifc.Operator): +class DisableEditingArbitraryProfile(bpy.types.Operator): bl_idname = "bim.disable_editing_arbitrary_profile" bl_label = "Disable Editing Arbitrary Profile" bl_options = {"REGISTER", "UNDO"} - def _execute(self, context): - return disable_editing_arbitrary_profile(context) + def execute(self, context): + disable_editing_arbitrary_profile(context) + return {"FINISHED"} class EditArbitraryProfile(bpy.types.Operator, tool.Ifc.Operator): diff --git a/src/bonsai/bonsai/bim/module/style/operator.py b/src/bonsai/bonsai/bim/module/style/operator.py index ebfbec53af..0a1bc04598 100644 --- a/src/bonsai/bonsai/bim/module/style/operator.py +++ b/src/bonsai/bonsai/bim/module/style/operator.py @@ -356,14 +356,14 @@ class BrowseExternalStyle(bpy.types.Operator): return {"FINISHED"} -class ActivateExternalStyle(bpy.types.Operator, tool.Ifc.Operator): +class ActivateExternalStyle(bpy.types.Operator): bl_idname = "bim.activate_external_style" bl_label = "Activate External Style" bl_options = {"REGISTER", "UNDO", "INTERNAL"} material_name: bpy.props.StringProperty(name="Material Name", default="") - def _execute(self, context): + def execute(self, context): if not self.material_name: material = context.active_object.active_material else: @@ -403,6 +403,7 @@ class ActivateExternalStyle(bpy.types.Operator, tool.Ifc.Operator): if material.use_nodes: tool.Blender.copy_node_graph(material, db["data_block"]) bpy.data.materials.remove(db["data_block"]) + return {"FINISHED"} def copy_material_attributes(self, source, target): ID_properties = bpy.types.ID.bl_rna.properties @@ -444,34 +445,37 @@ class ActivateExternalStyle(bpy.types.Operator, tool.Ifc.Operator): set_prop(prop_name) -class DisableEditingStyles(bpy.types.Operator, tool.Ifc.Operator): +class DisableEditingStyles(bpy.types.Operator): bl_idname = "bim.disable_editing_styles" bl_options = {"REGISTER", "UNDO"} bl_label = "Disable Editing Styles" - def _execute(self, context): + def execute(self, context): core.disable_editing_styles(tool.Style) + return {"FINISHED"} -class LoadStyles(bpy.types.Operator, tool.Ifc.Operator): +class LoadStyles(bpy.types.Operator): bl_idname = "bim.load_styles" bl_label = "Load Styles" bl_options = {"REGISTER", "UNDO"} style_type: bpy.props.StringProperty() - def _execute(self, context): + def execute(self, context): style_type = self.style_type if self.style_type else context.scene.BIMStylesProperties.style_type core.load_styles(tool.Style, style_type=style_type) + return {"FINISHED"} -class SelectByStyle(bpy.types.Operator, tool.Ifc.Operator): +class SelectByStyle(bpy.types.Operator): bl_idname = "bim.select_by_style" bl_label = "Select By Style" bl_options = {"REGISTER", "UNDO"} style: bpy.props.IntProperty() - def _execute(self, context): + def execute(self, context): core.select_by_style(tool.Style, tool.Spatial, style=tool.Ifc.get().by_id(self.style)) + return {"FINISHED"} class ChooseTextureMapPath(bpy.types.Operator): @@ -634,14 +638,14 @@ class AddPresentationStyle(bpy.types.Operator, tool.Ifc.Operator): core.load_styles(tool.Style, style_type=props.style_type) -class EnableEditingSurfaceStyle(bpy.types.Operator, tool.Ifc.Operator): +class EnableEditingSurfaceStyle(bpy.types.Operator): bl_idname = "bim.enable_editing_surface_style" bl_label = "Enable Editing Surface Style" bl_options = {"REGISTER", "UNDO"} style: bpy.props.IntProperty(default=0) ifc_class: bpy.props.StringProperty(default="") - def _execute(self, context): + def execute(self, context): props = bpy.context.scene.BIMStylesProperties style = tool.Ifc.get().by_id(self.style) props.is_editing_style = self.style @@ -679,6 +683,7 @@ class EnableEditingSurfaceStyle(bpy.types.Operator, tool.Ifc.Operator): and active_style_type != "Shading" ): tool.Style.switch_shading(material, "Shading") + return {"FINISHED"} class EditSurfaceStyle(bpy.types.Operator, tool.Ifc.Operator): diff --git a/src/bonsai/bonsai/bim/module/type/operator.py b/src/bonsai/bonsai/bim/module/type/operator.py index be652058ab..605ab97c25 100644 --- a/src/bonsai/bonsai/bim/module/type/operator.py +++ b/src/bonsai/bonsai/bim/module/type/operator.py @@ -515,12 +515,12 @@ class RenameType(bpy.types.Operator, tool.Ifc.Operator): self.layout.prop(self, "name") -class AutoRenameOccurrences(bpy.types.Operator, tool.Ifc.Operator): +class AutoRenameOccurrences(bpy.types.Operator): bl_idname = "bim.auto_rename_occurrences" bl_label = "Auto Rename Occurrences" bl_options = {"REGISTER", "UNDO"} - def _execute(self, context): + def execute(self, context): obj = context.active_object element_type = tool.Ifc.get_entity(obj) if element_type and element_type.is_a("IfcTypeObject"): @@ -529,6 +529,7 @@ class AutoRenameOccurrences(bpy.types.Operator, tool.Ifc.Operator): occurrence.Name = tool.Model.generate_occurrence_name(element_type, occurrence.is_a()) if obj: tool.Root.set_object_name(obj, occurrence) + return {"FINISHED"} class DuplicateType(bpy.types.Operator, tool.Ifc.Operator): From 845402ea9cdd8fb0b04ff1fc8d17c4f7ef5231ff Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Thu, 12 Sep 2024 18:15:15 +0500 Subject: [PATCH 52/56] Replace grids/space elements visibility two toggles with one See - https://i.imgur.com/5GssIDp.png (same for grids) --- src/bonsai/bonsai/bim/module/spatial/prop.py | 34 ++++++++++++++++++-- src/bonsai/bonsai/bim/module/spatial/ui.py | 18 ++++++----- 2 files changed, 42 insertions(+), 10 deletions(-) diff --git a/src/bonsai/bonsai/bim/module/spatial/prop.py b/src/bonsai/bonsai/bim/module/spatial/prop.py index 10b5ec1111..6959f24992 100644 --- a/src/bonsai/bonsai/bim/module/spatial/prop.py +++ b/src/bonsai/bonsai/bim/module/spatial/prop.py @@ -127,6 +127,14 @@ def update_spatial_is_locked(self, context): tool.Geometry.unlock_object(obj) +def update_spatial_is_visible(self: "BIMSpatialDecompositionProperties", context: bpy.types.Context) -> None: + bpy.ops.bim.toggle_spatial_elements(is_visible=self.is_visible) + + +def update_grid_is_visible(self: "BIMGridProperties", context: bpy.types.Context) -> None: + bpy.ops.bim.toggle_grids(is_visible=self.is_visible) + + def poll_container_obj(self, obj): return obj is None or tool.Ifc.get_entity(obj) @@ -174,7 +182,18 @@ class Element(PropertyGroup): class BIMSpatialDecompositionProperties(PropertyGroup): - is_locked: BoolProperty(name="Is Locked", default=True, update=update_spatial_is_locked) + is_locked: BoolProperty( + name="Is Locked", + description="Prevent all spatial elements from being edited, removed, duplicated", + default=True, + update=update_spatial_is_locked, + ) + is_visible: BoolProperty( + name="Is Visible", + description="Show or hide spatial elements, such as buildings, sites, etc", + default=True, + update=update_spatial_is_visible, + ) container_filter: StringProperty(name="Container Filter", default="", options={"TEXTEDIT_UPDATE"}) containers: CollectionProperty(name="Containers", type=BIMContainer) contracted_containers: StringProperty(name="Contracted containers", default="[]") @@ -211,5 +230,16 @@ class BIMSpatialDecompositionProperties(PropertyGroup): class BIMGridProperties(PropertyGroup): - is_locked: BoolProperty(name="Is Locked", default=True, update=update_grid_is_locked) + is_locked: BoolProperty( + name="Is Locked", + description="Prevent all grids and grid axes from being edited, removed, duplicated", + default=True, + update=update_grid_is_locked, + ) + is_visible: BoolProperty( + name="Is Visible", + description="Show or hide grids and grid axes", + default=True, + update=update_grid_is_visible, + ) grid_axes: CollectionProperty(name="Grid Axes", type=ObjProperty) diff --git a/src/bonsai/bonsai/bim/module/spatial/ui.py b/src/bonsai/bonsai/bim/module/spatial/ui.py index 39e810ed8d..a1d25b8c6f 100644 --- a/src/bonsai/bonsai/bim/module/spatial/ui.py +++ b/src/bonsai/bonsai/bim/module/spatial/ui.py @@ -103,12 +103,13 @@ class BIM_PT_spatial_decomposition(Panel): return tool.Ifc.get() def draw_header(self, context): + props = context.scene.BIMSpatialDecompositionProperties row = self.layout.row(align=True) row.label(text="") # empty text occupies the left of the row - row.operator("bim.toggle_spatial_elements", text="", icon="HIDE_OFF").is_visible = True - row.operator("bim.toggle_spatial_elements", text="", icon="HIDE_ON").is_visible = False - icon = "VIEW_LOCKED" if context.scene.BIMSpatialDecompositionProperties.is_locked else "VIEW_UNLOCKED" - row.prop(context.scene.BIMSpatialDecompositionProperties, "is_locked", text="", icon=icon) + icon = "HIDE_OFF" if props.is_visible else "HIDE_ON" + row.prop(props, "is_visible", text="", icon=icon) + icon = "VIEW_LOCKED" if props.is_locked else "VIEW_UNLOCKED" + row.prop(props, "is_locked", text="", icon=icon) def draw(self, context): if not SpatialDecompositionData.is_loaded: @@ -218,12 +219,13 @@ class BIM_PT_grids(Panel): self.layout.row().operator("mesh.add_grid", icon="ADD", text="Add Grids") def draw_header(self, context): + props = context.scene.BIMGridProperties row = self.layout.row(align=True) row.label(text="") # empty text occupies the left of the row - row.operator("bim.toggle_grids", text="", icon="HIDE_OFF").is_visible = True - row.operator("bim.toggle_grids", text="", icon="HIDE_ON").is_visible = False - icon = "VIEW_LOCKED" if context.scene.BIMGridProperties.is_locked else "VIEW_UNLOCKED" - row.prop(context.scene.BIMGridProperties, "is_locked", text="", icon=icon) + icon = "HIDE_OFF" if props.is_visible else "HIDE_ON" + row.prop(props, "is_visible", text="", icon=icon) + icon = "VIEW_LOCKED" if props.is_locked else "VIEW_UNLOCKED" + row.prop(props, "is_locked", text="", icon=icon) class BIM_UL_containers_manager(UIList): From d34d3312ba505e7be9560b3a704eab695f3d690d Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Thu, 12 Sep 2024 18:34:15 +0500 Subject: [PATCH 53/56] Add messages for operations with locked elements Otherwise they just silently failing leaving user confused --- src/bonsai/bonsai/bim/module/geometry/operator.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/bonsai/bonsai/bim/module/geometry/operator.py b/src/bonsai/bonsai/bim/module/geometry/operator.py index 1016c8a636..6a7100347f 100644 --- a/src/bonsai/bonsai/bim/module/geometry/operator.py +++ b/src/bonsai/bonsai/bim/module/geometry/operator.py @@ -644,6 +644,7 @@ class OverrideDelete(bpy.types.Operator): element = tool.Ifc.get_entity(obj) if element: if tool.Geometry.is_locked(element): + self.report({"ERROR"}, f"Element '{obj.name}' is locked and cannot be deleted.") continue if ifcopenshell.util.element.get_pset(element, "BBIM_Array"): self.report({"INFO"}, "Elements that are part of an array cannot be deleted.") @@ -775,6 +776,7 @@ class OverrideOutlinerDelete(bpy.types.Operator): for obj in objects_to_delete: if element := tool.Ifc.get_entity(obj): if tool.Geometry.is_locked(element): + self.report({"ERROR"}, f"Element '{obj.name}' is locked and cannot be deleted.") if collection := obj.BIMObjectProperties.collection: collections_to_delete.discard(collection) continue @@ -891,6 +893,7 @@ class OverrideDuplicateMove(bpy.types.Operator): continue # For now, don't copy drawings until we stabilise a bit more. It's tricky. elif tool.Geometry.is_locked(element): obj.select_set(False) + self.report({"ERROR"}, f"Element '{obj.name}' is locked and cannot be duplicated.") continue linked_non_ifc_object = linked and not element @@ -1609,6 +1612,7 @@ class OverrideModeSetEdit(bpy.types.Operator, tool.Ifc.Operator): if not element: continue if tool.Geometry.is_locked(element): + self.report({"ERROR"}, f"Element '{obj.name}' is locked and cannot be edited.") obj.select_set(False) continue representation = tool.Geometry.get_active_representation(obj) From e3c23072a57017f74c6ba93f9aa8de51046d2505 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Thu, 12 Sep 2024 18:32:44 +0500 Subject: [PATCH 54/56] Hide edit mode for locked elements #5371 --- src/bonsai/bonsai/bim/module/geometry/data.py | 6 +++++- src/bonsai/bonsai/bim/module/spatial/prop.py | 3 +++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/src/bonsai/bonsai/bim/module/geometry/data.py b/src/bonsai/bonsai/bim/module/geometry/data.py index 5f84838e14..0c8ba8eec1 100644 --- a/src/bonsai/bonsai/bim/module/geometry/data.py +++ b/src/bonsai/bonsai/bim/module/geometry/data.py @@ -52,7 +52,11 @@ class ViewportData: ("OBJECT", "IFC Object Mode", "", "OBJECT_DATAMODE", 0), ("EDIT", "IFC Edit Mode", "", "EDITMODE_HLT", 1), ] - if not obj or not tool.Blender.is_editable(obj): + if ( + not obj + or not tool.Blender.is_editable(obj) + or ((element := tool.Ifc.get_entity(obj)) and tool.Geometry.is_locked(element)) + ): return obj_mode return mesh_modes diff --git a/src/bonsai/bonsai/bim/module/spatial/prop.py b/src/bonsai/bonsai/bim/module/spatial/prop.py index 6959f24992..161130b877 100644 --- a/src/bonsai/bonsai/bim/module/spatial/prop.py +++ b/src/bonsai/bonsai/bim/module/spatial/prop.py @@ -31,6 +31,7 @@ from bpy.props import ( CollectionProperty, ) import bonsai.tool as tool +import bonsai.bim.handler import bonsai.core.geometry import ifcopenshell import ifcopenshell.util.element @@ -125,6 +126,8 @@ def update_spatial_is_locked(self, context): tool.Geometry.lock_object(obj) else: tool.Geometry.unlock_object(obj) + # Need to update ViewportData.mode. + bonsai.bim.handler.refresh_ui_data() def update_spatial_is_visible(self: "BIMSpatialDecompositionProperties", context: bpy.types.Context) -> None: From 806975c4b97e6f60b015c419b01e71e08cffdfa2 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Thu, 12 Sep 2024 19:07:13 +0500 Subject: [PATCH 55/56] bump ifcopenshell build for bonsai --- src/bonsai/Makefile | 2 +- src/ifcopenshell-python/Makefile | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/bonsai/Makefile b/src/bonsai/Makefile index 524ac9f91c..8a67f787de 100644 --- a/src/bonsai/Makefile +++ b/src/bonsai/Makefile @@ -81,7 +81,7 @@ BLENDER_PLATFORM:=windows-x64 endif # Current build commit hash. -OLD:=03935a9 +OLD:=f5e02d1 .PHONY: bump bump: cd . && $(SED) -b "s/$(OLD)/$(NEW)/" Makefile diff --git a/src/ifcopenshell-python/Makefile b/src/ifcopenshell-python/Makefile index 7cfadb02dd..b1f8d458bb 100644 --- a/src/ifcopenshell-python/Makefile +++ b/src/ifcopenshell-python/Makefile @@ -52,8 +52,8 @@ ifeq ($(PLATFORM), win64) PLATFORMTAG:=win_amd64 endif -IOS_URL:=https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-$(PYNUMBER)-v0.7.11-03935a9-$(PLATFORM).zip -IFCCONVERT_URL:=https://s3.amazonaws.com/ifcopenshell-builds/IfcConvert-v0.7.11-03935a9-$(PLATFORM).zip +IOS_URL:=https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-$(PYNUMBER)-v0.7.11-f5e02d1-$(PLATFORM).zip +IFCCONVERT_URL:=https://s3.amazonaws.com/ifcopenshell-builds/IfcConvert-v0.7.11-f5e02d1-$(PLATFORM).zip .PHONY: test test: From 51211dc5275da21c2eb35f7bcd1be93d7bbf9b2c Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Thu, 12 Sep 2024 19:19:04 +0500 Subject: [PATCH 56/56] ifcopenshell makefile - use VERSION --- src/ifcopenshell-python/Makefile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/ifcopenshell-python/Makefile b/src/ifcopenshell-python/Makefile index b1f8d458bb..6239a44476 100644 --- a/src/ifcopenshell-python/Makefile +++ b/src/ifcopenshell-python/Makefile @@ -52,8 +52,8 @@ ifeq ($(PLATFORM), win64) PLATFORMTAG:=win_amd64 endif -IOS_URL:=https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-$(PYNUMBER)-v0.7.11-f5e02d1-$(PLATFORM).zip -IFCCONVERT_URL:=https://s3.amazonaws.com/ifcopenshell-builds/IfcConvert-v0.7.11-f5e02d1-$(PLATFORM).zip +IOS_URL:=https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-$(PYNUMBER)-v$(VERSION)-f5e02d1-$(PLATFORM).zip +IFCCONVERT_URL:=https://s3.amazonaws.com/ifcopenshell-builds/IfcConvert-v$(VERSION)-f5e02d1-$(PLATFORM).zip .PHONY: test test: