diff --git a/src/bonsai/bonsai/bim/module/search/operator.py b/src/bonsai/bonsai/bim/module/search/operator.py index 927846792d..cf07f8298b 100644 --- a/src/bonsai/bonsai/bim/module/search/operator.py +++ b/src/bonsai/bonsai/bim/module/search/operator.py @@ -18,15 +18,16 @@ import bpy import json +import bisect import ifcopenshell import ifcopenshell.api import ifcopenshell.guid import ifcopenshell.util.element import ifcopenshell.util.selector import bonsai.tool as tool -from bonsai.bim.ifc import IfcStore import bonsai.core.search as core -from itertools import cycle +from bonsai.bim.ifc import IfcStore +from natsort import natsorted from bpy.types import PropertyGroup, Operator from bpy.props import ( PointerProperty, @@ -40,22 +41,6 @@ from bpy.props import ( ) -colour_list = [ - (0.651, 0.81, 0.892, 1), - (0.121, 0.471, 0.706, 1), - (0.699, 0.876, 0.54, 1), - (0.199, 0.629, 0.174, 1), - (0.983, 0.605, 0.602, 1), - (0.89, 0.101, 0.112, 1), - (0.989, 0.751, 0.427, 1), - (0.986, 0.497, 0.1, 1), - (0.792, 0.699, 0.839, 1), - (0.414, 0.239, 0.603, 1), - (0.993, 0.999, 0.6, 1), - (0.693, 0.349, 0.157, 1), -] - - class AddFilterGroup(Operator): bl_idname = "bim.add_filter_group" bl_label = "Add Filter Group" @@ -270,27 +255,74 @@ class ColourByProperty(Operator): self.report({"ERROR"}, "No Query Provided") return {"CANCELLED"} - colours = cycle(colour_list) + is_qualitative = props.pallette in ("tab10", "paired") + + if is_qualitative: + colours = tool.Search.get_qualitative_pallette(props.pallette) + colourscheme = {} - if len(props.colourscheme): + obj_values = {} + min_value = None + max_value = None + + if is_qualitative and len(props.colourscheme): colourscheme = {cs.name: {"colour": cs.colour[0:3], "total": 0} for cs in props.colourscheme} for obj in context.visible_objects: element = tool.Ifc.get_entity(obj) if not element: continue - value = str(ifcopenshell.util.selector.get_element_value(element, query)) - if value in colourscheme: - colourscheme[value]["total"] += 1 + value = ifcopenshell.util.selector.get_element_value(element, query) + if is_qualitative: + value = str(value) + if value in colourscheme: + colourscheme[value]["total"] += 1 + else: + colourscheme[value] = {"colour": next(colours)[0:3], "total": 1} + obj.color = (*colourscheme[value]["colour"], 1) else: - colourscheme[value] = {"colour": next(colours)[0:3], "total": 1} - obj.color = (*colourscheme[value]["colour"], 1) + if value is None: + obj.color = (0, 0, 0, 1) + if "None" in colourscheme: + colourscheme["None"]["total"] += 1 + else: + colourscheme["None"] = {"colour": (0, 0, 0), "total": 1} + else: + try: + value = float(value) + if min_value is None or value < min_value: + min_value = value + if max_value is None or value > max_value: + max_value = value + obj_values[obj] = value + except: + obj.color = (0, 0, 0, 1) + + if not is_qualitative: + steps = 10 + step_size = (max_value - min_value) / (steps - 1) + values = [] + for i in range(steps): + step_value = min_value + i * step_size + values.append(step_value) + colourscheme[str(step_value)] = { + "colour": tool.Search.get_quantitative_pallette("foo", step_value, min_value, max_value), + "total": 0, + } + + for obj, value in obj_values.items(): + index = bisect.bisect_right(values, value) + if index >= len(values): + index = -1 + colourscheme[str(values[index])]["total"] += 1 + obj.color = (*tool.Search.get_quantitative_pallette("foo", value, min_value, max_value), 1) + if areas := [a for a in context.screen.areas if a.type == "VIEW_3D"]: areas[0].spaces[0].shading.color_type = "OBJECT" props.colourscheme.clear() - for value in sorted(colourscheme.keys()): + for value in natsorted(colourscheme.keys()): data = colourscheme[value] new = props.colourscheme.add() new.name = str(value) @@ -331,13 +363,40 @@ class SelectByProperty(Operator): active_value = props.colourscheme[props.active_colourscheme_index].name + is_qualitative = props.pallette in ("tab10", "paired") + + if not is_qualitative: + values = [] + for colour in props.colourscheme: + try: + values.append(float(colour.name)) + except: + pass + values = sorted(values) + for obj in context.visible_objects: element = tool.Ifc.get_entity(obj) if not element: continue - value = str(ifcopenshell.util.selector.get_element_value(element, query)) - if value == active_value: - obj.select_set(True) + value = ifcopenshell.util.selector.get_element_value(element, query) + if is_qualitative: + if str(value) == active_value: + obj.select_set(True) + else: + if active_value == "None": + if value is None: + obj.select_set(True) + else: + try: + value = float(value) + index = bisect.bisect_right(values, value) + if index >= len(values): + index = -1 + if values[index] == float(active_value): + obj.select_set(True) + except: + pass + return {"FINISHED"} diff --git a/src/bonsai/bonsai/bim/module/search/prop.py b/src/bonsai/bonsai/bim/module/search/prop.py index 897ebc3771..0d6afb2369 100644 --- a/src/bonsai/bonsai/bim/module/search/prop.py +++ b/src/bonsai/bonsai/bim/module/search/prop.py @@ -123,6 +123,15 @@ class BIMSearchProperties(PropertyGroup): saved_searches: EnumProperty(items=get_saved_searches, name="Saved Searches") saved_colourschemes: EnumProperty(items=get_saved_colourschemes, name="Saved Colourschemes") colourscheme_query: StringProperty(name="Colourscheme Query", default="class") + pallette: EnumProperty( + items=[ + ("tab10", "Default", "Contrasting colours to distinguish categories"), + ("paired", "Paired", "Pairs of colours to distinguish categories"), + ("coolwarm", "CoolWarm", "A diverging linear range from blue to red with white in the middle"), + ("spectral", "Spectral", "A diverging spectral range from red to blue with white in the middle"), + ], + name="Pallette", + ) colourscheme: CollectionProperty(type=BIMColour) active_colourscheme_index: IntProperty(name="Active Colourscheme Index") filter_type: StringProperty(name="Filter Type") diff --git a/src/bonsai/bonsai/bim/module/search/ui.py b/src/bonsai/bonsai/bim/module/search/ui.py index 39336f3fc6..cdbfd11d10 100644 --- a/src/bonsai/bonsai/bim/module/search/ui.py +++ b/src/bonsai/bonsai/bim/module/search/ui.py @@ -85,6 +85,9 @@ class BIM_PT_colour_by_property(Panel): row = self.layout.row() row.prop(props, "colourscheme_query", text="Query") + row = self.layout.row() + row.prop(props, "pallette") + row = self.layout.row(align=True) row.operator("bim.colour_by_property", icon="BRUSH_DATA") row.operator("bim.reset_object_colours") diff --git a/src/bonsai/bonsai/tool/search.py b/src/bonsai/bonsai/tool/search.py index a1b8a1f083..f025950fb3 100644 --- a/src/bonsai/bonsai/tool/search.py +++ b/src/bonsai/bonsai/tool/search.py @@ -4,6 +4,7 @@ import lark import bonsai.core.tool import ifcopenshell.guid import ifcopenshell.util.selector +from itertools import cycle from bonsai.bim.prop import BIMFacet from typing import Union, Literal @@ -113,6 +114,70 @@ class Search(bonsai.core.tool.Search): return value return '"' + value.replace('"', '\\"') + '"' + @classmethod + def get_qualitative_pallette(cls, theme: str = "tab10") -> cycle: + if theme == "paired": + pass + return cycle( + [ + (0.651, 0.81, 0.892, 1), + (0.121, 0.471, 0.706, 1), + (0.699, 0.876, 0.54, 1), + (0.199, 0.629, 0.174, 1), + (0.983, 0.605, 0.602, 1), + (0.89, 0.101, 0.112, 1), + (0.989, 0.751, 0.427, 1), + (0.986, 0.497, 0.1, 1), + (0.792, 0.699, 0.839, 1), + (0.414, 0.239, 0.603, 1), + (0.993, 0.999, 0.6, 1), + (0.693, 0.349, 0.157, 1), + ] + ) + + @classmethod + def interpolate_color(cls, c1, c2, factor): + return tuple((1 - factor) * x + factor * y for x, y in zip(c1, c2)) + + @classmethod + def get_quantitative_pallette(cls, theme: str, value, min_val, max_val): + palette = [ + (0.227, 0.298, 0.753), + (0.282, 0.376, 0.820), + (0.345, 0.463, 0.886), + (0.412, 0.545, 0.937), + (0.482, 0.620, 0.973), + (0.553, 0.686, 0.992), + (0.616, 0.741, 0.996), + (0.686, 0.792, 0.984), + (0.753, 0.827, 0.961), + (0.812, 0.851, 0.918), + (0.865, 0.863, 0.863), + (0.910, 0.835, 0.792), + (0.945, 0.792, 0.714), + (0.965, 0.737, 0.635), + (0.965, 0.671, 0.553), + (0.957, 0.604, 0.482), + (0.929, 0.518, 0.404), + (0.890, 0.424, 0.329), + (0.839, 0.322, 0.263), + (0.773, 0.196, 0.200), + (0.702, 0.012, 0.149), + ] + + if value < min_val: + value = min_val + if value > max_val: + value = max_val + + scale = (value - min_val) / (max_val - min_val) * (len(palette) - 1) + index = int(scale) + fraction = scale - index + + if index >= len(palette) - 1: + return palette[-1] + return cls.interpolate_color(palette[index], palette[index + 1], fraction) + class ImportFilterQueryTransformer(lark.Transformer): def __init__(self, filter_groups):