193 lines
7.6 KiB
Python
193 lines
7.6 KiB
Python
"""Applies Noctalia's palette to Blender's UI chrome, run headless via `blender --background --python`.
|
|
|
|
Only structural chrome is themed: panel backgrounds, headers, widget fills/outlines/text.
|
|
Workflow-meaningful colors (mesh selection states, node categories, sequencer strip types,
|
|
syntax highlighting) are deliberately left at Blender's own defaults, since users rely on those
|
|
as functional conventions, not decoration.
|
|
"""
|
|
|
|
import bpy
|
|
|
|
COLORS = {
|
|
"surface": "{{colors.surface.default.hex}}",
|
|
"on_surface": "{{colors.on_surface.default.hex}}",
|
|
"surface_variant": "{{colors.surface_variant.default.hex}}",
|
|
"on_surface_variant": "{{colors.on_surface_variant.default.hex}}",
|
|
"surface_container": "{{colors.surface_container.default.hex}}",
|
|
"surface_container_low": "{{colors.surface_container_low.default.hex}}",
|
|
"surface_container_high": "{{colors.surface_container_high.default.hex}}",
|
|
"outline": "{{colors.outline.default.hex}}",
|
|
"outline_variant": "{{colors.outline_variant.default.hex}}",
|
|
"primary": "{{colors.primary.default.hex}}",
|
|
"on_primary": "{{colors.on_primary.default.hex}}",
|
|
"primary_container": "{{colors.primary_container.default.hex}}",
|
|
"on_primary_container": "{{colors.on_primary_container.default.hex}}",
|
|
"secondary": "{{colors.secondary.default.hex}}",
|
|
"secondary_container": "{{colors.secondary_container.default.hex}}",
|
|
"on_secondary_container": "{{colors.on_secondary_container.default.hex}}",
|
|
"tertiary": "{{colors.tertiary.default.hex}}",
|
|
"tertiary_container": "{{colors.tertiary_container.default.hex}}",
|
|
"on_tertiary_container": "{{colors.on_tertiary_container.default.hex}}",
|
|
"error": "{{colors.error.default.hex}}",
|
|
"on_error": "{{colors.on_error.default.hex}}",
|
|
"error_container": "{{colors.error_container.default.hex}}",
|
|
}
|
|
|
|
|
|
def hex_to_rgb(hex_str):
|
|
hex_str = hex_str.lstrip("#")
|
|
return tuple(int(hex_str[i : i + 2], 16) / 255.0 for i in (0, 2, 4))
|
|
|
|
|
|
RGB = {name: hex_to_rgb(value) for name, value in COLORS.items()}
|
|
|
|
|
|
def classify(path):
|
|
if path.endswith(".space.back"):
|
|
return "surface"
|
|
if path.endswith(".space.header"):
|
|
return "surface_container"
|
|
if path.endswith(".space.header_text"):
|
|
return "on_surface"
|
|
if path.endswith(".space.header_text_hi"):
|
|
return "primary"
|
|
if path.endswith(".space.text"):
|
|
return "on_surface"
|
|
if path.endswith(".space.text_hi"):
|
|
return "primary"
|
|
if path.endswith(".space.title"):
|
|
return "on_surface_variant"
|
|
if path.endswith(".space.tab_active"):
|
|
return "surface_container_high"
|
|
if path.endswith(".space.tab_back"):
|
|
return "surface_container_low"
|
|
if path.endswith(".space.tab_outline"):
|
|
return "outline"
|
|
if path.endswith(".space.gradients.gradient"):
|
|
return "surface"
|
|
if path.endswith(".space.gradients.high_gradient"):
|
|
return "surface_container"
|
|
if path.endswith("row_alternate"):
|
|
return "surface_container_low"
|
|
|
|
if ".wcol_" in path:
|
|
suffix = path.rsplit(".", 1)[-1]
|
|
wcol_map = {
|
|
"inner": "surface_container",
|
|
"inner_sel": "primary_container",
|
|
"inner_key": "tertiary_container",
|
|
"inner_key_sel": "tertiary_container",
|
|
"inner_anim": "secondary_container",
|
|
"inner_anim_sel": "secondary_container",
|
|
"inner_driven": "tertiary",
|
|
"inner_driven_sel": "tertiary",
|
|
"inner_changed": "primary",
|
|
"inner_changed_sel": "primary",
|
|
"inner_overridden": "error_container",
|
|
"inner_overridden_sel": "error_container",
|
|
"outline": "outline",
|
|
"outline_sel": "primary",
|
|
"item": "on_surface_variant",
|
|
"text": "on_surface",
|
|
"text_sel": "on_primary",
|
|
"error": "error",
|
|
"warning": "tertiary",
|
|
"success": "secondary",
|
|
"info": "primary",
|
|
}
|
|
if suffix in wcol_map:
|
|
return wcol_map[suffix]
|
|
|
|
if path.startswith("regions."):
|
|
if path.endswith(".back") or path.endswith(".header_back"):
|
|
return "surface_container"
|
|
if path.endswith(".tab_back"):
|
|
return "surface_container_low"
|
|
if path.endswith(".text"):
|
|
return "on_surface"
|
|
if path.endswith(".text_selected"):
|
|
return "primary"
|
|
|
|
direct = {
|
|
"text_editor.line_numbers": "on_surface_variant",
|
|
"text_editor.line_numbers_background": "surface_container_low",
|
|
"text_editor.cursor": "primary",
|
|
"text_editor.selected_text": "primary_container",
|
|
"image_editor.metadatabg": "surface_container",
|
|
"image_editor.metadatatext": "on_surface",
|
|
"sequence_editor.metadatabg": "surface_container",
|
|
"sequence_editor.metadatatext": "on_surface",
|
|
"sequence_editor.preview_back": "surface",
|
|
"clip_editor.metadatabg": "surface_container",
|
|
"clip_editor.metadatatext": "on_surface",
|
|
"node_editor.node_outline": "outline",
|
|
"user_interface.widget_text_cursor": "primary",
|
|
"user_interface.editor_border": "outline",
|
|
"user_interface.editor_outline": "outline_variant",
|
|
"user_interface.editor_outline_active": "primary",
|
|
"user_interface.panel_header": "surface_container",
|
|
"user_interface.panel_back": "surface_container_low",
|
|
"user_interface.panel_outline": "outline",
|
|
"user_interface.panel_title": "on_surface",
|
|
"user_interface.panel_text": "on_surface",
|
|
"user_interface.panel_active": "primary",
|
|
"outliner.selected_highlight": "primary_container",
|
|
"outliner.active": "primary",
|
|
"outliner.selected_object": "on_primary_container",
|
|
"outliner.active_object": "on_primary",
|
|
"outliner.match": "tertiary_container",
|
|
"info.info_error_text": "error",
|
|
"info.info_warning_text": "tertiary",
|
|
"info.info_info_text": "on_surface",
|
|
"info.info_property_text": "secondary",
|
|
"info.info_operator_text": "primary",
|
|
"info.info_debug_text": "outline",
|
|
"info.info_selected": "primary_container",
|
|
"info.info_selected_text": "on_primary_container",
|
|
"console.line_output": "on_surface",
|
|
"console.line_input": "primary",
|
|
"console.line_info": "secondary",
|
|
"console.line_error": "error",
|
|
"console.select": "primary_container",
|
|
"console.cursor": "primary",
|
|
"file_browser.selected_file": "primary_container",
|
|
"preferences.match": "tertiary_container",
|
|
"properties.match": "tertiary_container",
|
|
}
|
|
if path in direct:
|
|
return direct[path]
|
|
|
|
return None
|
|
|
|
|
|
def walk_and_apply(obj, path="", depth=0):
|
|
if depth > 4:
|
|
return 0
|
|
applied = 0
|
|
for prop in obj.bl_rna.properties:
|
|
if prop.identifier == "rna_type":
|
|
continue
|
|
try:
|
|
val = getattr(obj, prop.identifier)
|
|
except Exception:
|
|
continue
|
|
full = f"{path}.{prop.identifier}" if path else prop.identifier
|
|
if prop.type == "POINTER" and val is not None:
|
|
applied += walk_and_apply(val, full, depth + 1)
|
|
elif prop.type == "FLOAT" and getattr(prop, "array_length", 0) in (3, 4):
|
|
role = classify(full)
|
|
if role is None:
|
|
continue
|
|
rgb = RGB[role]
|
|
current = list(val)
|
|
current[0], current[1], current[2] = rgb
|
|
setattr(obj, prop.identifier, current)
|
|
applied += 1
|
|
return applied
|
|
|
|
|
|
theme = bpy.context.preferences.themes[0]
|
|
count = walk_and_apply(theme)
|
|
print(f"noctalia-theme: applied {count} properties")
|
|
bpy.ops.wm.save_userpref()
|