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 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"}
@@ -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")
@@ -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")
+65
View File
@@ -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):