Add support for colouring by quantitative colour legend, not just qualitative categories

This commit is contained in:
Dion Moult
2024-08-21 14:02:41 +10:00
parent e8848c9701
commit e4a4a33920
4 changed files with 165 additions and 29 deletions
+88 -29
View File
@@ -18,15 +18,16 @@
import bpy import bpy
import json import json
import bisect
import ifcopenshell import ifcopenshell
import ifcopenshell.api import ifcopenshell.api
import ifcopenshell.guid import ifcopenshell.guid
import ifcopenshell.util.element import ifcopenshell.util.element
import ifcopenshell.util.selector import ifcopenshell.util.selector
import bonsai.tool as tool import bonsai.tool as tool
from bonsai.bim.ifc import IfcStore
import bonsai.core.search as core 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.types import PropertyGroup, Operator
from bpy.props import ( from bpy.props import (
PointerProperty, 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): class AddFilterGroup(Operator):
bl_idname = "bim.add_filter_group" bl_idname = "bim.add_filter_group"
bl_label = "Add Filter Group" bl_label = "Add Filter Group"
@@ -270,27 +255,74 @@ class ColourByProperty(Operator):
self.report({"ERROR"}, "No Query Provided") self.report({"ERROR"}, "No Query Provided")
return {"CANCELLED"} 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 = {} 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} colourscheme = {cs.name: {"colour": cs.colour[0:3], "total": 0} for cs in props.colourscheme}
for obj in context.visible_objects: for obj in context.visible_objects:
element = tool.Ifc.get_entity(obj) element = tool.Ifc.get_entity(obj)
if not element: if not element:
continue continue
value = str(ifcopenshell.util.selector.get_element_value(element, query)) value = ifcopenshell.util.selector.get_element_value(element, query)
if value in colourscheme: if is_qualitative:
colourscheme[value]["total"] += 1 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: else:
colourscheme[value] = {"colour": next(colours)[0:3], "total": 1} if value is None:
obj.color = (*colourscheme[value]["colour"], 1) 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"]: if areas := [a for a in context.screen.areas if a.type == "VIEW_3D"]:
areas[0].spaces[0].shading.color_type = "OBJECT" areas[0].spaces[0].shading.color_type = "OBJECT"
props.colourscheme.clear() props.colourscheme.clear()
for value in sorted(colourscheme.keys()): for value in natsorted(colourscheme.keys()):
data = colourscheme[value] data = colourscheme[value]
new = props.colourscheme.add() new = props.colourscheme.add()
new.name = str(value) new.name = str(value)
@@ -331,13 +363,40 @@ class SelectByProperty(Operator):
active_value = props.colourscheme[props.active_colourscheme_index].name 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: for obj in context.visible_objects:
element = tool.Ifc.get_entity(obj) element = tool.Ifc.get_entity(obj)
if not element: if not element:
continue continue
value = str(ifcopenshell.util.selector.get_element_value(element, query)) value = ifcopenshell.util.selector.get_element_value(element, query)
if value == active_value: if is_qualitative:
obj.select_set(True) 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"} return {"FINISHED"}
@@ -123,6 +123,15 @@ class BIMSearchProperties(PropertyGroup):
saved_searches: EnumProperty(items=get_saved_searches, name="Saved Searches") saved_searches: EnumProperty(items=get_saved_searches, name="Saved Searches")
saved_colourschemes: EnumProperty(items=get_saved_colourschemes, name="Saved Colourschemes") saved_colourschemes: EnumProperty(items=get_saved_colourschemes, name="Saved Colourschemes")
colourscheme_query: StringProperty(name="Colourscheme Query", default="class") 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) colourscheme: CollectionProperty(type=BIMColour)
active_colourscheme_index: IntProperty(name="Active Colourscheme Index") active_colourscheme_index: IntProperty(name="Active Colourscheme Index")
filter_type: StringProperty(name="Filter Type") filter_type: StringProperty(name="Filter Type")
@@ -85,6 +85,9 @@ class BIM_PT_colour_by_property(Panel):
row = self.layout.row() row = self.layout.row()
row.prop(props, "colourscheme_query", text="Query") row.prop(props, "colourscheme_query", text="Query")
row = self.layout.row()
row.prop(props, "pallette")
row = self.layout.row(align=True) row = self.layout.row(align=True)
row.operator("bim.colour_by_property", icon="BRUSH_DATA") row.operator("bim.colour_by_property", icon="BRUSH_DATA")
row.operator("bim.reset_object_colours") row.operator("bim.reset_object_colours")
+65
View File
@@ -4,6 +4,7 @@ import lark
import bonsai.core.tool import bonsai.core.tool
import ifcopenshell.guid import ifcopenshell.guid
import ifcopenshell.util.selector import ifcopenshell.util.selector
from itertools import cycle
from bonsai.bim.prop import BIMFacet from bonsai.bim.prop import BIMFacet
from typing import Union, Literal from typing import Union, Literal
@@ -113,6 +114,70 @@ class Search(bonsai.core.tool.Search):
return value return value
return '"' + value.replace('"', '\\"') + '"' 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): class ImportFilterQueryTransformer(lark.Transformer):
def __init__(self, filter_groups): def __init__(self, filter_groups):