From a744cef87f5fe00bc267ee480a4ce4224ca8426f Mon Sep 17 00:00:00 2001 From: Harshit Gupta Date: Wed, 24 Jun 2026 22:27:54 +0530 Subject: [PATCH 1/2] Fix: use official OTIO Color schema correctly Signed-off-by: Harshit Gupta --- editing.cpp | 42 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/editing.cpp b/editing.cpp index 6987c87..3d10930 100644 --- a/editing.cpp +++ b/editing.cpp @@ -7,6 +7,36 @@ #include #include +using otio = opentimelineio::OPENTIMELINEIO_VERSION; + +std::string ColorToName(const otio::Color& c) +{ + auto v = c.to_float_list(); + + double r = v[0], g = v[1], b = v[2]; + + auto close = [](double a, double b) { + return std::abs(a - b) < 0.01; + }; + + if (close(r,1.0) && close(g,1.0) && close(b,0.0)) return "YELLOW"; + if (close(r,1.0) && close(g,0.0) && close(b,0.0)) return "RED"; + if (close(r,0.0) && close(g,1.0) && close(b,0.0)) return "GREEN"; + if (close(r,0.0) && close(g,0.0) && close(b,1.0)) return "BLUE"; + + return "WHITE"; +} + +otio::Color NameToColor(const std::string& name) +{ + if (name == "YELLOW") return otio::Color::from_float_list({1.0,1.0,0.0,1.0}); + if (name == "RED") return otio::Color::from_float_list({1.0,0.0,0.0,1.0}); + if (name == "GREEN") return otio::Color::from_float_list({0.0,1.0,0.0,1.0}); + if (name == "BLUE") return otio::Color::from_float_list({0.0,0.0,1.0,1.0}); + + return otio::Color::from_float_list({1.0,1.0,1.0,1.0}); +} + void DeleteSelectedObject() { if (!appState.selected_object) { return; @@ -325,6 +355,14 @@ std::string GetItemColor(otio::Item* item) { std::string item_color = ""; + // ✅ Use official OTIO Color schema + auto c = item->color(); + if (c) + { + return ColorToName(*c); + } + + // ✅ Fallback to Raven legacy metadata if (item->metadata().has_key("raven") && item->metadata()["raven"].type() == typeid(otio::AnyDictionary)) { @@ -342,6 +380,10 @@ std::string GetItemColor(otio::Item* item) void SetItemColor(otio::Item* item, std::string color_name) { + auto c = NameToColor(color_name); + item->set_color(c); + + // Optionally keep Raven's legacy field in sync for backward compatibility otio::AnyDictionary raven_md; if (item->metadata().has_key("raven") && item->metadata()["raven"].type() == typeid(otio::AnyDictionary)) From 5824b97f449f5845d83bd85417fbb7f5dc4a924e Mon Sep 17 00:00:00 2001 From: Harshit Gupta Date: Sun, 12 Jul 2026 23:18:47 +0530 Subject: [PATCH 2/2] Preserve custom RGB colors for clips and tracks Signed-off-by: Harshit Gupta --- colors.cpp | 70 +++++++++++++++++++++++++++++++++++++++++++++++++-- colors.h | 15 ++++++++++- editing.cpp | 41 ++++++++---------------------- inspector.cpp | 41 +++++++++++++++++++++++++++--- timeline.cpp | 8 ++++++ 5 files changed, 138 insertions(+), 37 deletions(-) diff --git a/colors.cpp b/colors.cpp index c498e89..d92d2af 100644 --- a/colors.cpp +++ b/colors.cpp @@ -5,6 +5,18 @@ #include namespace otio = opentimelineio::OPENTIMELINEIO_VERSION; +ImU32 UIColorFromOTIOColor(const otio::Color& color) +{ + auto rgba = color.to_float_list(); + + int r = static_cast(rgba[0] * 255.0); + int g = static_cast(rgba[1] * 255.0); + int b = static_cast(rgba[2] * 255.0); + int a = static_cast(rgba[3] * 255.0); + + return IM_COL32(r, g, b, a); +} + ImU32 LerpColors(ImU32 col_a, ImU32 col_b, float t) { int r = ImLerp( (int)(col_a >> IM_COL32_R_SHIFT) & 0xFF, @@ -51,6 +63,61 @@ ImU32 UIColorFromName(std::string color) { return IM_COL32(0x88, 0x88, 0x88, 0xff); } +namespace { +struct NamedColor { + const char* name; + float r, g, b, a; +}; + +// Same 11 colors already supported by UIColorFromName(), given explicit +// float values so we can compare against otio::Color's float list. +const NamedColor kNamedColors[] = { + {otio::Marker::Color::pink, 1.00f, 0.44f, 0.44f, 1.00f}, + {otio::Marker::Color::red, 1.00f, 0.00f, 0.00f, 1.00f}, + {otio::Marker::Color::orange, 1.00f, 0.63f, 0.00f, 1.00f}, + {otio::Marker::Color::yellow, 1.00f, 1.00f, 0.00f, 1.00f}, + {otio::Marker::Color::green, 0.00f, 1.00f, 0.00f, 1.00f}, + {otio::Marker::Color::cyan, 0.00f, 1.00f, 1.00f, 1.00f}, + {otio::Marker::Color::blue, 0.00f, 0.00f, 1.00f, 1.00f}, + {otio::Marker::Color::purple, 0.63f, 0.00f, 0.82f, 1.00f}, + {otio::Marker::Color::magenta, 1.00f, 0.00f, 1.00f, 1.00f}, + {otio::Marker::Color::black, 0.00f, 0.00f, 0.00f, 1.00f}, + {otio::Marker::Color::white, 1.00f, 1.00f, 1.00f, 1.00f}, +}; + +bool FloatsClose(double a, double b) { + return std::abs(a - b) < 0.004; // ~1/255, one 8-bit step +} +} // namespace + +std::string NameFromOTIOColor(const otio::Color& color) +{ + auto v = color.to_float_list(); + for (const auto& named : kNamedColors) { + if (FloatsClose(v[0], named.r) && + FloatsClose(v[1], named.g) && + FloatsClose(v[2], named.b) && + FloatsClose(v[3], named.a)) { + return named.name; + } + } + // Not one of the known named colors - it's a custom RGB value. + // Callers must NOT fall back to a default name here, or the + // custom color will be silently lost on the next write. + return ""; +} + +otio::Color OTIOColorFromName(const std::string& name) +{ + for (const auto& named : kNamedColors) { + if (name == named.name) { + return otio::Color::from_float_list( + {named.r, named.g, named.b, named.a}); + } + } + return otio::Color::from_float_list({1.0f, 1.0f, 1.0f, 1.0f}); +} + bool ColorIsBright(ImU32 color) { int r = (int)(color >> IM_COL32_R_SHIFT) & 0xFF; @@ -73,5 +140,4 @@ ImU32 ColorInvert(ImU32 color) ImU32 TintedColorForUI(ImU32 color) { return LerpColors(color, IM_COL32(150,150,150,255), 0.3); -} - +} \ No newline at end of file diff --git a/colors.h b/colors.h index 8deb4ca..57bf0cd 100644 --- a/colors.h +++ b/colors.h @@ -1,8 +1,21 @@ #include #include "imgui.h" +#include + +namespace otio = opentimelineio::OPENTIMELINEIO_VERSION; + +ImU32 UIColorFromOTIOColor(const otio::Color& color); + +// Maps an otio::Color to one of the known named marker colors. +// Returns "" if the color does not exactly match a known name - +// callers must treat "" as "custom color, do not overwrite". +std::string NameFromOTIOColor(const otio::Color& color); + +// Maps one of the known named marker colors to an otio::Color. +otio::Color OTIOColorFromName(const std::string& name); ImU32 UIColorFromName(std::string color); ImU32 LerpColors(ImU32 col_a, ImU32 col_b, float t); bool ColorIsBright(ImU32 color); ImU32 ColorInvert(ImU32 color); -ImU32 TintedColorForUI(ImU32 color); +ImU32 TintedColorForUI(ImU32 color); \ No newline at end of file diff --git a/editing.cpp b/editing.cpp index 3d10930..0bf99ec 100644 --- a/editing.cpp +++ b/editing.cpp @@ -1,5 +1,6 @@ #include "editing.h" #include "app.h" +#include "colors.h" #include #include @@ -9,34 +10,6 @@ using otio = opentimelineio::OPENTIMELINEIO_VERSION; -std::string ColorToName(const otio::Color& c) -{ - auto v = c.to_float_list(); - - double r = v[0], g = v[1], b = v[2]; - - auto close = [](double a, double b) { - return std::abs(a - b) < 0.01; - }; - - if (close(r,1.0) && close(g,1.0) && close(b,0.0)) return "YELLOW"; - if (close(r,1.0) && close(g,0.0) && close(b,0.0)) return "RED"; - if (close(r,0.0) && close(g,1.0) && close(b,0.0)) return "GREEN"; - if (close(r,0.0) && close(g,0.0) && close(b,1.0)) return "BLUE"; - - return "WHITE"; -} - -otio::Color NameToColor(const std::string& name) -{ - if (name == "YELLOW") return otio::Color::from_float_list({1.0,1.0,0.0,1.0}); - if (name == "RED") return otio::Color::from_float_list({1.0,0.0,0.0,1.0}); - if (name == "GREEN") return otio::Color::from_float_list({0.0,1.0,0.0,1.0}); - if (name == "BLUE") return otio::Color::from_float_list({0.0,0.0,1.0,1.0}); - - return otio::Color::from_float_list({1.0,1.0,1.0,1.0}); -} - void DeleteSelectedObject() { if (!appState.selected_object) { return; @@ -359,7 +332,13 @@ std::string GetItemColor(otio::Item* item) auto c = item->color(); if (c) { - return ColorToName(*c); + // NameFromOTIOColor() returns "" for a color that isn't one of + // the known named colors (e.g. an arbitrary RGB value). That is + // intentional: we must never guess a name for a custom color, + // since doing so would silently overwrite it the next time + // SetItemColor() runs. "" tells callers "this item has a color, + // but it doesn't match a preset - leave it alone." + return NameFromOTIOColor(*c); } // ✅ Fallback to Raven legacy metadata @@ -380,7 +359,7 @@ std::string GetItemColor(otio::Item* item) void SetItemColor(otio::Item* item, std::string color_name) { - auto c = NameToColor(color_name); + auto c = OTIOColorFromName(color_name); item->set_color(c); // Optionally keep Raven's legacy field in sync for backward compatibility @@ -392,4 +371,4 @@ void SetItemColor(otio::Item* item, std::string color_name) } raven_md["color"] = color_name; item->metadata()["raven"] = raven_md; -} +} \ No newline at end of file diff --git a/inspector.cpp b/inspector.cpp index 5b4eadc..17b8796 100644 --- a/inspector.cpp +++ b/inspector.cpp @@ -265,7 +265,14 @@ void DrawNonEditableTextField(const char* label, const char* format, ...) { ImGui::PopStyleColor(); } -std::string DrawColorChooser(std::string current_color_name) +// custom_color_swatch: when the current color doesn't match a named +// preset (current_color_name == ""), pass the raw color here so we can +// still show the user *something* is set, instead of a blank combo that +// looks like "no color". Pass nullptr for callers (like Marker) that +// only ever deal in named colors. +std::string DrawColorChooser( + std::string current_color_name, + const otio::Color* custom_color_swatch = nullptr) { const char** color_choices = marker_color_names; int num_color_choices = IM_ARRAYSIZE(marker_color_names); @@ -277,6 +284,27 @@ std::string DrawColorChooser(std::string current_color_name) break; } } + + bool is_custom = (current_index == -1) && custom_color_swatch != nullptr; + if (is_custom) { + // Draw a small swatch of the actual color next to the combo so + // it's visible that a non-preset color is present. Selecting a + // named color below will replace it; leaving the combo alone + // preserves the original custom value untouched. + ImU32 swatch = UIColorFromOTIOColor(*custom_color_swatch); + ImGui::ColorButton( + "##custom_color_swatch", + ImGui::ColorConvertU32ToFloat4(swatch), + ImGuiColorEditFlags_NoTooltip, + ImVec2(16, 16)); + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip( + "Custom color (not one of the preset names).\n" + "Selecting a color below will replace it."); + } + ImGui::SameLine(); + } + if (ImGui::Combo("Color", ¤t_index, color_choices, num_color_choices)) { if (current_index >= 0 && current_index < num_color_choices) { return color_choices[current_index]; @@ -677,7 +705,14 @@ void DrawInspector() { if (!is_gap) { auto item_color = GetItemColor(item); - item_color = DrawColorChooser(item_color); + auto raw_color = item->color(); + // Only pass the swatch when there IS a color but it didn't + // match a named preset - i.e. it's genuinely custom, not + // simply absent. + bool is_custom_color = item_color == "" && raw_color.has_value(); + item_color = DrawColorChooser( + item_color, + is_custom_color ? &raw_color.value() : nullptr); if (item_color != "") { SetItemColor(item, item_color); } @@ -1231,4 +1266,4 @@ void DrawTreeInspector() { ImGui::EndTable(); } -} +} \ No newline at end of file diff --git a/timeline.cpp b/timeline.cpp index 7bda8b6..e5ac615 100644 --- a/timeline.cpp +++ b/timeline.cpp @@ -102,12 +102,20 @@ void DrawItem( auto hover_fill_color = appTheme.colors[AppThemeCol_ItemHovered]; bool fancy_corners = true; +if (auto color = item->color()) { + fill_color = UIColorFromOTIOColor(*color); + selected_fill_color = TintedColorForUI(fill_color); + hover_fill_color = TintedColorForUI(fill_color); +} else { auto item_color = GetItemColor(item); if (item_color != "") { fill_color = UIColorFromName(item_color); selected_fill_color = TintedColorForUI(fill_color); hover_fill_color = TintedColorForUI(fill_color); } +} + + if (auto gap = dynamic_cast(item)) { // different colors & style