From 6a82cbb0c758726d99dec924a905bef41bcdb273 Mon Sep 17 00:00:00 2001 From: Daniel Bergman Date: Mon, 18 May 2026 09:31:05 -0400 Subject: [PATCH] Add support for custom SVG cell colors and validation in PhysiCell settings - Introduced a new XML structure for specifying cell colors in PhysiCell_settings.xml. - Implemented validation for SVG/CSS color formats in PhysiCell_pathology.cpp. - Updated PhysiCell_Settings to read custom colors from XML and store them in a map. - Enhanced color assignment logic in paint_by_number_cell_coloring to accommodate user-defined colors. --- config/PhysiCell_settings.xml | 12 ++ modules/PhysiCell_pathology.cpp | 211 ++++++++++++++++++++++++++++---- modules/PhysiCell_pathology.h | 4 +- modules/PhysiCell_settings.cpp | 17 ++- modules/PhysiCell_settings.h | 8 +- 5 files changed, 220 insertions(+), 32 deletions(-) diff --git a/config/PhysiCell_settings.xml b/config/PhysiCell_settings.xml index e3cd03b4a..6f41e17ac 100644 --- a/config/PhysiCell_settings.xml +++ b/config/PhysiCell_settings.xml @@ -35,6 +35,18 @@ 60 true + false diff --git a/modules/PhysiCell_pathology.cpp b/modules/PhysiCell_pathology.cpp index eef368b1e..bc1b00e4b 100644 --- a/modules/PhysiCell_pathology.cpp +++ b/modules/PhysiCell_pathology.cpp @@ -1707,40 +1707,197 @@ std::string paint_by_density_percentage(double concentration, double max_conc, d return colormap[ind]; } +// Returns true if the string is a valid SVG/CSS color: +// - CSS named color (case-insensitive) +// - #rgb or #rrggbb hex notation +// - rgb(r,g,b) functional notation (integers 0-255) +static bool is_valid_svg_color( const std::string& s ) +{ + if( s.empty() ) return false; + + // #rgb / #rrggbb + if( s[0] == '#' ) + { + if( s.size() != 4 && s.size() != 7 ) return false; + for( size_t i = 1; i < s.size(); i++ ) + { + char c = s[i]; + if( !( (c>='0'&&c<='9') || (c>='a'&&c<='f') || (c>='A'&&c<='F') ) ) + return false; + } + return true; + } + + // rgb(r,g,b) — accept optional whitespace, integers 0-255 + if( s.size() > 4 && s.substr(0,4) == "rgb(" && s.back() == ')' ) + { + std::string inner = s.substr(4, s.size()-5); + // parse three comma-separated integers + int vals[3]; int nread; + char extra; + nread = std::sscanf(inner.c_str(), " %d , %d , %d %c", &vals[0], &vals[1], &vals[2], &extra); + if( nread != 3 ) return false; + for( int i = 0; i < 3; i++ ) + if( vals[i] < 0 || vals[i] > 255 ) return false; + return true; + } + + // CSS named colors (standard 147 + rebeccapurple) + static const std::unordered_set named = { + "aliceblue","antiquewhite","aqua","aquamarine","azure","beige","bisque","black", + "blanchedalmond","blue","blueviolet","brown","burlywood","cadetblue","chartreuse", + "chocolate","coral","cornflowerblue","cornsilk","crimson","cyan","darkblue","darkcyan", + "darkgoldenrod","darkgray","darkgreen","darkgrey","darkkhaki","darkmagenta", + "darkolivegreen","darkorange","darkorchid","darkred","darksalmon","darkseagreen", + "darkslateblue","darkslategray","darkslategrey","darkturquoise","darkviolet","deeppink", + "deepskyblue","dimgray","dimgrey","dodgerblue","firebrick","floralwhite","forestgreen", + "fuchsia","gainsboro","ghostwhite","gold","goldenrod","gray","green","greenyellow", + "grey","honeydew","hotpink","indianred","indigo","ivory","khaki","lavender", + "lavenderblush","lawngreen","lemonchiffon","lightblue","lightcoral","lightcyan", + "lightgoldenrodyellow","lightgray","lightgreen","lightgrey","lightpink","lightsalmon", + "lightseagreen","lightskyblue","lightslategray","lightslategrey","lightsteelblue", + "lightyellow","lime","limegreen","linen","magenta","maroon","mediumaquamarine", + "mediumblue","mediumorchid","mediumpurple","mediumseagreen","mediumslateblue", + "mediumspringgreen","mediumturquoise","mediumvioletred","midnightblue","mintcream", + "mistyrose","moccasin","navajowhite","navy","oldlace","olive","olivedrab","orange", + "orangered","orchid","palegoldenrod","palegreen","paleturquoise","palevioletred", + "papayawhip","peachpuff","peru","pink","plum","powderblue","purple","rebeccapurple", + "red","rosybrown","royalblue","saddlebrown","salmon","sandybrown","seagreen", + "seashell","sienna","silver","skyblue","slateblue","slategray","slategrey","snow", + "springgreen","steelblue","tan","teal","thistle","tomato","turquoise","violet", + "wheat","white","whitesmoke","yellow","yellowgreen" + }; + // compare lower-case + std::string lower = s; + std::transform(lower.begin(), lower.end(), lower.begin(), ::tolower); + return named.count(lower) > 0; +} + +// Convert HSV (h in [0,360), s and v in [0,1]) to an SVG "rgb(r,g,b)" string. +static std::string hsv_to_svg_color( double h, double s, double v ) +{ + double c = v * s; + double x = c * ( 1.0 - std::fabs( std::fmod( h / 60.0, 2.0 ) - 1.0 ) ); + double m = v - c; + double r1, g1, b1; + if ( h < 60 ) { r1 = c; g1 = x; b1 = 0; } + else if( h < 120 ) { r1 = x; g1 = c; b1 = 0; } + else if( h < 180 ) { r1 = 0; g1 = c; b1 = x; } + else if( h < 240 ) { r1 = 0; g1 = x; b1 = c; } + else if( h < 300 ) { r1 = x; g1 = 0; b1 = c; } + else { r1 = c; g1 = 0; b1 = x; } + int r = (int)std::round( (r1 + m) * 255 ); + int g = (int)std::round( (g1 + m) * 255 ); + int b = (int)std::round( (b1 + m) * 255 ); + return "rgb(" + std::to_string(r) + "," + std::to_string(g) + "," + std::to_string(b) + ")"; +} + std::vector paint_by_number_cell_coloring( Cell* pCell ) { - static std::vector< std::string > colors(0); - static bool setup_done = false; + static std::vector colors; + static bool setup_done = false; if( setup_done == false ) { - colors.push_back( "grey" ); // default color will be grey + // Built-in palette for the first 13 cell types. + static const std::vector builtin = { + "grey", "red", "yellow", "green", "blue", + "magenta", "orange", "lime", "cyan", + "hotpink", "peachpuff", "darkseagreen", "lightskyblue" + }; - colors.push_back( "red" ); - colors.push_back( "yellow" ); - colors.push_back( "green" ); - colors.push_back( "blue" ); - - colors.push_back( "magenta" ); - colors.push_back( "orange" ); - colors.push_back( "lime" ); - colors.push_back( "cyan" ); - - colors.push_back( "hotpink" ); - colors.push_back( "peachpuff" ); - colors.push_back( "darkseagreen" ); - colors.push_back( "lightskyblue" ); + int n = (int)cell_definitions_by_index.size(); + + if( PhysiCell_settings.svg_cell_colors_specified ) + { + // Validate: every user-supplied name must match a cell definition. + for( auto& kv : PhysiCell_settings.svg_cell_colors_by_name ) + { + if( cell_definitions_by_name.find(kv.first) == cell_definitions_by_name.end() ) + { + std::cerr << "ERROR (paint_by_number_cell_coloring): does not match any cell definition." << std::endl; + exit(-1); + } + } + // Validate: every supplied color value must be a recognized SVG color. + for( auto& kv : PhysiCell_settings.svg_cell_colors_by_name ) + { + if( !is_valid_svg_color(kv.second) ) + { + std::cerr << "ERROR (paint_by_number_cell_coloring): has unrecognized color value \"" + << kv.second << "\"." << std::endl; + exit(-1); + } + } + // Build the pool of default colors not already claimed by the user (case-insensitive). + std::unordered_set claimed; + for( auto& kv : PhysiCell_settings.svg_cell_colors_by_name ) + { + std::string lc = kv.second; + std::transform(lc.begin(), lc.end(), lc.begin(), ::tolower); + claimed.insert(lc); + } + std::vector pool; + for( auto& c : builtin ) + { + if( claimed.count(c) == 0 ) + { pool.push_back(c); } + } - setup_done = true; + // Assign colors: user-specified first, then pool, then HSV-generated. + colors.resize(n); + int pool_idx = 0; + int extra_idx = 0; + for( int i = 0; i < n; i++ ) + { + const std::string& name = cell_definitions_by_index[i]->name; + auto it = PhysiCell_settings.svg_cell_colors_by_name.find(name); + if( it != PhysiCell_settings.svg_cell_colors_by_name.end() ) + { + colors[i] = it->second; + } + else if( pool_idx < (int)pool.size() ) + { + colors[i] = pool[pool_idx++]; + } + else + { + double hue = std::fmod( extra_idx * 137.508, 360.0 ); + colors[i] = hsv_to_svg_color( hue, 0.65, 0.85 ); + extra_idx++; + } + } + } + else + { + // No user colors: use built-in palette; generate via golden-angle HSV for extras. + colors.resize(n); + int n_extra = 0; + for( int i = 0; i < n; i++ ) + { + if( i < (int)builtin.size() ) + { colors[i] = builtin[i]; } + else + { + double hue = std::fmod( n_extra * 137.508, 360.0 ); + colors[i] = hsv_to_svg_color( hue, 0.65, 0.85 ); + n_extra++; + } + } + } + + setup_done = true; } - - // start all black - - std::vector output = { "black", "black", "black", "black" }; - - // paint by number -- by cell type - - std::string interior_color = "white"; - if( pCell->type < 13 ) + + // start all black + + std::vector output = { "black", "black", "black", "black" }; + + // paint by number -- by cell type + + std::string interior_color = "white"; + if( pCell->type >= 0 && pCell->type < (int)colors.size() ) { interior_color = colors[ pCell->type ]; } output[0] = interior_color; // set cytoplasm color diff --git a/modules/PhysiCell_pathology.h b/modules/PhysiCell_pathology.h index fae47f212..279530409 100644 --- a/modules/PhysiCell_pathology.h +++ b/modules/PhysiCell_pathology.h @@ -65,8 +65,10 @@ ############################################################################### */ -#include +#include #include +#include +#include #ifndef __PhysiCell_pathology__ #define __PhysiCell_pathology__ diff --git a/modules/PhysiCell_settings.cpp b/modules/PhysiCell_settings.cpp index a588383d1..db41d87f6 100644 --- a/modules/PhysiCell_settings.cpp +++ b/modules/PhysiCell_settings.cpp @@ -211,9 +211,22 @@ void PhysiCell_Settings::read_from_pugixml( void ) enable_full_saves = xml_get_bool_value( node , "enable" ); node = node.parent(); - node = xml_find_node( node , "SVG" ); + node = xml_find_node( node , "SVG" ); SVG_save_interval = xml_get_double_value( node , "interval" ); - enable_SVG_saves = xml_get_bool_value( node , "enable" ); + enable_SVG_saves = xml_get_bool_value( node , "enable" ); + + pugi::xml_node node_cell_colors = node.child("cell_colors"); + if( node_cell_colors ) + { + svg_cell_colors_specified = true; + for( pugi::xml_node cc = node_cell_colors.child("cell_color"); cc; cc = cc.next_sibling("cell_color") ) + { + std::string cname = cc.attribute("name").as_string(); + std::string color = cc.child_value(); + if( !cname.empty() && !color.empty() ) + { svg_cell_colors_by_name[cname] = color; } + } + } pugi::xml_node node_plot_substrate; node_plot_substrate = xml_find_node( node , "plot_substrate" ); diff --git a/modules/PhysiCell_settings.h b/modules/PhysiCell_settings.h index 86afb2939..33204c817 100644 --- a/modules/PhysiCell_settings.h +++ b/modules/PhysiCell_settings.h @@ -75,6 +75,7 @@ #include #include #include +#include #include #include "./PhysiCell_pugixml.h" @@ -115,8 +116,11 @@ class PhysiCell_Settings bool disable_automated_spring_adhesions = false; - double SVG_save_interval = 60; - bool enable_SVG_saves = true; + double SVG_save_interval = 60; + bool enable_SVG_saves = true; + + bool svg_cell_colors_specified = false; + std::map svg_cell_colors_by_name; bool enable_substrate_plot = false; std::string substrate_to_monitor = "oxygen";