@@ -1707,40 +1707,197 @@ std::string paint_by_density_percentage(double concentration, double max_conc, d
17071707 return colormap[ind];
17081708}
17091709
1710+ // Returns true if the string is a valid SVG/CSS color:
1711+ // - CSS named color (case-insensitive)
1712+ // - #rgb or #rrggbb hex notation
1713+ // - rgb(r,g,b) functional notation (integers 0-255)
1714+ static bool is_valid_svg_color ( const std::string& s )
1715+ {
1716+ if ( s.empty () ) return false ;
1717+
1718+ // #rgb / #rrggbb
1719+ if ( s[0 ] == ' #' )
1720+ {
1721+ if ( s.size () != 4 && s.size () != 7 ) return false ;
1722+ for ( size_t i = 1 ; i < s.size (); i++ )
1723+ {
1724+ char c = s[i];
1725+ if ( !( (c>=' 0' &&c<=' 9' ) || (c>=' a' &&c<=' f' ) || (c>=' A' &&c<=' F' ) ) )
1726+ return false ;
1727+ }
1728+ return true ;
1729+ }
1730+
1731+ // rgb(r,g,b) — accept optional whitespace, integers 0-255
1732+ if ( s.size () > 4 && s.substr (0 ,4 ) == " rgb(" && s.back () == ' )' )
1733+ {
1734+ std::string inner = s.substr (4 , s.size ()-5 );
1735+ // parse three comma-separated integers
1736+ int vals[3 ]; int nread;
1737+ char extra;
1738+ nread = std::sscanf (inner.c_str (), " %d , %d , %d %c" , &vals[0 ], &vals[1 ], &vals[2 ], &extra);
1739+ if ( nread != 3 ) return false ;
1740+ for ( int i = 0 ; i < 3 ; i++ )
1741+ if ( vals[i] < 0 || vals[i] > 255 ) return false ;
1742+ return true ;
1743+ }
1744+
1745+ // CSS named colors (standard 147 + rebeccapurple)
1746+ static const std::unordered_set<std::string> named = {
1747+ " aliceblue" ," antiquewhite" ," aqua" ," aquamarine" ," azure" ," beige" ," bisque" ," black" ,
1748+ " blanchedalmond" ," blue" ," blueviolet" ," brown" ," burlywood" ," cadetblue" ," chartreuse" ,
1749+ " chocolate" ," coral" ," cornflowerblue" ," cornsilk" ," crimson" ," cyan" ," darkblue" ," darkcyan" ,
1750+ " darkgoldenrod" ," darkgray" ," darkgreen" ," darkgrey" ," darkkhaki" ," darkmagenta" ,
1751+ " darkolivegreen" ," darkorange" ," darkorchid" ," darkred" ," darksalmon" ," darkseagreen" ,
1752+ " darkslateblue" ," darkslategray" ," darkslategrey" ," darkturquoise" ," darkviolet" ," deeppink" ,
1753+ " deepskyblue" ," dimgray" ," dimgrey" ," dodgerblue" ," firebrick" ," floralwhite" ," forestgreen" ,
1754+ " fuchsia" ," gainsboro" ," ghostwhite" ," gold" ," goldenrod" ," gray" ," green" ," greenyellow" ,
1755+ " grey" ," honeydew" ," hotpink" ," indianred" ," indigo" ," ivory" ," khaki" ," lavender" ,
1756+ " lavenderblush" ," lawngreen" ," lemonchiffon" ," lightblue" ," lightcoral" ," lightcyan" ,
1757+ " lightgoldenrodyellow" ," lightgray" ," lightgreen" ," lightgrey" ," lightpink" ," lightsalmon" ,
1758+ " lightseagreen" ," lightskyblue" ," lightslategray" ," lightslategrey" ," lightsteelblue" ,
1759+ " lightyellow" ," lime" ," limegreen" ," linen" ," magenta" ," maroon" ," mediumaquamarine" ,
1760+ " mediumblue" ," mediumorchid" ," mediumpurple" ," mediumseagreen" ," mediumslateblue" ,
1761+ " mediumspringgreen" ," mediumturquoise" ," mediumvioletred" ," midnightblue" ," mintcream" ,
1762+ " mistyrose" ," moccasin" ," navajowhite" ," navy" ," oldlace" ," olive" ," olivedrab" ," orange" ,
1763+ " orangered" ," orchid" ," palegoldenrod" ," palegreen" ," paleturquoise" ," palevioletred" ,
1764+ " papayawhip" ," peachpuff" ," peru" ," pink" ," plum" ," powderblue" ," purple" ," rebeccapurple" ,
1765+ " red" ," rosybrown" ," royalblue" ," saddlebrown" ," salmon" ," sandybrown" ," seagreen" ,
1766+ " seashell" ," sienna" ," silver" ," skyblue" ," slateblue" ," slategray" ," slategrey" ," snow" ,
1767+ " springgreen" ," steelblue" ," tan" ," teal" ," thistle" ," tomato" ," turquoise" ," violet" ,
1768+ " wheat" ," white" ," whitesmoke" ," yellow" ," yellowgreen"
1769+ };
1770+ // compare lower-case
1771+ std::string lower = s;
1772+ std::transform (lower.begin (), lower.end (), lower.begin (), ::tolower);
1773+ return named.count (lower) > 0 ;
1774+ }
1775+
1776+ // Convert HSV (h in [0,360), s and v in [0,1]) to an SVG "rgb(r,g,b)" string.
1777+ static std::string hsv_to_svg_color ( double h, double s, double v )
1778+ {
1779+ double c = v * s;
1780+ double x = c * ( 1.0 - std::fabs ( std::fmod ( h / 60.0 , 2.0 ) - 1.0 ) );
1781+ double m = v - c;
1782+ double r1, g1, b1;
1783+ if ( h < 60 ) { r1 = c; g1 = x; b1 = 0 ; }
1784+ else if ( h < 120 ) { r1 = x; g1 = c; b1 = 0 ; }
1785+ else if ( h < 180 ) { r1 = 0 ; g1 = c; b1 = x; }
1786+ else if ( h < 240 ) { r1 = 0 ; g1 = x; b1 = c; }
1787+ else if ( h < 300 ) { r1 = x; g1 = 0 ; b1 = c; }
1788+ else { r1 = c; g1 = 0 ; b1 = x; }
1789+ int r = (int )std::round ( (r1 + m) * 255 );
1790+ int g = (int )std::round ( (g1 + m) * 255 );
1791+ int b = (int )std::round ( (b1 + m) * 255 );
1792+ return " rgb(" + std::to_string (r) + " ," + std::to_string (g) + " ," + std::to_string (b) + " )" ;
1793+ }
1794+
17101795std::vector<std::string> paint_by_number_cell_coloring ( Cell* pCell )
17111796{
1712- static std::vector< std::string > colors ( 0 );
1713- static bool setup_done = false ;
1797+ static std::vector<std::string> colors;
1798+ static bool setup_done = false ;
17141799 if ( setup_done == false )
17151800 {
1716- colors.push_back ( " grey" ); // default color will be grey
1801+ // Built-in palette for the first 13 cell types.
1802+ static const std::vector<std::string> builtin = {
1803+ " grey" , " red" , " yellow" , " green" , " blue" ,
1804+ " magenta" , " orange" , " lime" , " cyan" ,
1805+ " hotpink" , " peachpuff" , " darkseagreen" , " lightskyblue"
1806+ };
17171807
1718- colors.push_back ( " red" );
1719- colors.push_back ( " yellow" );
1720- colors.push_back ( " green" );
1721- colors.push_back ( " blue" );
1722-
1723- colors.push_back ( " magenta" );
1724- colors.push_back ( " orange" );
1725- colors.push_back ( " lime" );
1726- colors.push_back ( " cyan" );
1727-
1728- colors.push_back ( " hotpink" );
1729- colors.push_back ( " peachpuff" );
1730- colors.push_back ( " darkseagreen" );
1731- colors.push_back ( " lightskyblue" );
1808+ int n = (int )cell_definitions_by_index.size ();
1809+
1810+ if ( PhysiCell_settings.svg_cell_colors_specified )
1811+ {
1812+ // Validate: every user-supplied name must match a cell definition.
1813+ for ( auto & kv : PhysiCell_settings.svg_cell_colors_by_name )
1814+ {
1815+ if ( cell_definitions_by_name.find (kv.first ) == cell_definitions_by_name.end () )
1816+ {
1817+ std::cerr << " ERROR (paint_by_number_cell_coloring): <cell_color name=\" "
1818+ << kv.first << " \" > does not match any cell definition." << std::endl;
1819+ exit (-1 );
1820+ }
1821+ }
1822+ // Validate: every supplied color value must be a recognized SVG color.
1823+ for ( auto & kv : PhysiCell_settings.svg_cell_colors_by_name )
1824+ {
1825+ if ( !is_valid_svg_color (kv.second ) )
1826+ {
1827+ std::cerr << " ERROR (paint_by_number_cell_coloring): <cell_color name=\" "
1828+ << kv.first << " \" > has unrecognized color value \" "
1829+ << kv.second << " \" ." << std::endl;
1830+ exit (-1 );
1831+ }
1832+ }
1833+ // Build the pool of default colors not already claimed by the user (case-insensitive).
1834+ std::unordered_set<std::string> claimed;
1835+ for ( auto & kv : PhysiCell_settings.svg_cell_colors_by_name )
1836+ {
1837+ std::string lc = kv.second ;
1838+ std::transform (lc.begin (), lc.end (), lc.begin (), ::tolower);
1839+ claimed.insert (lc);
1840+ }
1841+ std::vector<std::string> pool;
1842+ for ( auto & c : builtin )
1843+ {
1844+ if ( claimed.count (c) == 0 )
1845+ { pool.push_back (c); }
1846+ }
17321847
1733- setup_done = true ;
1848+ // Assign colors: user-specified first, then pool, then HSV-generated.
1849+ colors.resize (n);
1850+ int pool_idx = 0 ;
1851+ int extra_idx = 0 ;
1852+ for ( int i = 0 ; i < n; i++ )
1853+ {
1854+ const std::string& name = cell_definitions_by_index[i]->name ;
1855+ auto it = PhysiCell_settings.svg_cell_colors_by_name .find (name);
1856+ if ( it != PhysiCell_settings.svg_cell_colors_by_name .end () )
1857+ {
1858+ colors[i] = it->second ;
1859+ }
1860+ else if ( pool_idx < (int )pool.size () )
1861+ {
1862+ colors[i] = pool[pool_idx++];
1863+ }
1864+ else
1865+ {
1866+ double hue = std::fmod ( extra_idx * 137.508 , 360.0 );
1867+ colors[i] = hsv_to_svg_color ( hue, 0.65 , 0.85 );
1868+ extra_idx++;
1869+ }
1870+ }
1871+ }
1872+ else
1873+ {
1874+ // No user colors: use built-in palette; generate via golden-angle HSV for extras.
1875+ colors.resize (n);
1876+ int n_extra = 0 ;
1877+ for ( int i = 0 ; i < n; i++ )
1878+ {
1879+ if ( i < (int )builtin.size () )
1880+ { colors[i] = builtin[i]; }
1881+ else
1882+ {
1883+ double hue = std::fmod ( n_extra * 137.508 , 360.0 );
1884+ colors[i] = hsv_to_svg_color ( hue, 0.65 , 0.85 );
1885+ n_extra++;
1886+ }
1887+ }
1888+ }
1889+
1890+ setup_done = true ;
17341891 }
1735-
1736- // start all black
1737-
1738- std::vector<std::string> output = { " black" , " black" , " black" , " black" };
1739-
1740- // paint by number -- by cell type
1741-
1742- std::string interior_color = " white" ;
1743- if ( pCell->type < 13 )
1892+
1893+ // start all black
1894+
1895+ std::vector<std::string> output = { " black" , " black" , " black" , " black" };
1896+
1897+ // paint by number -- by cell type
1898+
1899+ std::string interior_color = " white" ;
1900+ if ( pCell->type >= 0 && pCell-> type < ( int )colors. size () )
17441901 { interior_color = colors[ pCell->type ]; }
17451902
17461903 output[0 ] = interior_color; // set cytoplasm color
0 commit comments