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 @@
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