11from dataclasses import dataclass
2- from typing import Optional , Tuple , TypeAlias , overload
2+ from typing import TYPE_CHECKING , Optional , Tuple , TypeAlias , overload
3+
4+ if TYPE_CHECKING :
5+ from OCP .Quantity import Quantity_Color , Quantity_ColorRGBA
6+ from OCP .XCAFDoc import XCAFDoc_Material , XCAFDoc_VisMaterial
7+ from vtkmodules .vtkRenderingCore import vtkActor
8+
39
410RGB : TypeAlias = Tuple [float , float , float ]
511RGBA : TypeAlias = Tuple [float , float , float , float ]
612
713
8- @dataclass
14+ @dataclass ( frozen = True )
915class Color :
1016 """
1117 Simple color representation with optional alpha channel.
1218 All values are in range [0.0, 1.0].
1319 """
1420
15- r : float # red component
16- g : float # green component
17- b : float # blue component
18- a : float = 1.0 # alpha component, defaults to opaque
21+ red : float
22+ green : float
23+ blue : float
24+ alpha : float = 1.0
1925
2026 @overload
2127 def __init__ (self ):
@@ -34,106 +40,118 @@ def __init__(self, name: str):
3440 ...
3541
3642 @overload
37- def __init__ (self , r : float , g : float , b : float , a : float = 1.0 ):
43+ def __init__ (self , red : float , green : float , blue : float , alpha : float = 1.0 ):
3844 """
3945 Construct a Color from RGB(A) values.
4046
41- :param r : red value, 0-1
42- :param g : green value, 0-1
43- :param b : blue value, 0-1
44- :param a : alpha value, 0-1 (default: 1.0)
47+ :param red : red value, 0-1
48+ :param green : green value, 0-1
49+ :param blue : blue value, 0-1
50+ :param alpha : alpha value, 0-1 (default: 1.0)
4551 """
4652 ...
4753
4854 def __init__ (self , * args , ** kwargs ):
49- if len (args ) == 0 :
55+ # Check for unknown kwargs
56+ valid_kwargs = {"red" , "green" , "blue" , "alpha" , "name" }
57+ unknown_kwargs = set (kwargs .keys ()) - valid_kwargs
58+ if unknown_kwargs :
59+ raise TypeError (f"Got unexpected keyword arguments: { unknown_kwargs } " )
60+
61+ number_of_args = len (args ) + len (kwargs )
62+ if number_of_args == 0 :
5063 # Handle no-args case (default yellow)
51- self .r = 1.0
52- self .g = 1.0
53- self .b = 0.0
54- self .a = 1.0
55- elif len (args ) == 1 and isinstance (args [0 ], str ):
56- from cadquery .occ_impl .assembly import color_from_name
64+ r , g , b , a = 1.0 , 1.0 , 0.0 , 1.0
65+ elif (number_of_args == 1 and isinstance (args [0 ], str )) or "name" in kwargs :
66+ from OCP .Quantity import Quantity_ColorRGBA
5767 from vtkmodules .vtkCommonColor import vtkNamedColors
5868
69+ color_name = args [0 ] if number_of_args == 1 else kwargs ["name" ]
70+
5971 # Try to get color from OCCT first, fall back to VTK if not found
6072 try :
6173 # Get color from OCCT
62- color = color_from_name (args [0 ])
63- self .r = color .r
64- self .g = color .g
65- self .b = color .b
66- self .a = color .a
74+ occ_rgba = Quantity_ColorRGBA ()
75+ exists = Quantity_ColorRGBA .ColorFromName_s (color_name , occ_rgba )
76+ if not exists :
77+ raise ValueError (f"Unknown color name: { color_name } " )
78+ occ_rgb = occ_rgba .GetRGB ()
79+ r , g , b , a = (
80+ occ_rgb .Red (),
81+ occ_rgb .Green (),
82+ occ_rgb .Blue (),
83+ occ_rgba .Alpha (),
84+ )
6785 except ValueError :
6886 # Check if color exists in VTK
6987 vtk_colors = vtkNamedColors ()
70- if not vtk_colors .ColorExists (args [ 0 ] ):
71- raise ValueError (f"Unsupported color name: { args [ 0 ] } " )
88+ if not vtk_colors .ColorExists (color_name ):
89+ raise ValueError (f"Unsupported color name: { color_name } " )
7290
7391 # Get color from VTK
74- color = vtk_colors .GetColor4d (args [0 ])
75- self .r = color .GetRed ()
76- self .g = color .GetGreen ()
77- self .b = color .GetBlue ()
78- self .a = color .GetAlpha ()
79-
80- elif len (args ) == 3 :
81- # Handle RGB case
82- r , g , b = args
83- a = kwargs .get ("a" , 1.0 )
84- self .r = r
85- self .g = g
86- self .b = b
87- self .a = a
88- elif len (args ) == 4 :
89- # Handle RGBA case
90- r , g , b , a = args
91- self .r = r
92- self .g = g
93- self .b = b
94- self .a = a
95- else :
96- raise ValueError (f"Unsupported arguments: { args } , { kwargs } " )
92+ vtk_rgba = vtk_colors .GetColor4d (color_name )
93+ r = vtk_rgba .GetRed ()
94+ g = vtk_rgba .GetGreen ()
95+ b = vtk_rgba .GetBlue ()
96+ a = vtk_rgba .GetAlpha ()
97+
98+ elif number_of_args <= 4 :
99+ r , g , b , a = args + (4 - len (args )) * (1.0 ,)
100+
101+ if "red" in kwargs :
102+ r = kwargs ["red" ]
103+ if "green" in kwargs :
104+ g = kwargs ["green" ]
105+ if "blue" in kwargs :
106+ b = kwargs ["blue" ]
107+ if "alpha" in kwargs :
108+ a = kwargs ["alpha" ]
109+
110+ elif number_of_args > 4 :
111+ raise ValueError ("Too many arguments" )
97112
98113 # Validate values
99- for name , value in [("r " , self . r ), ("g " , self . g ), ("b " , self . b ), ("a " , self . a )]:
114+ for name , value in [("red " , r ), ("green " , g ), ("blue " , b ), ("alpha " , a )]:
100115 if not 0.0 <= value <= 1.0 :
101116 raise ValueError (f"{ name } component must be between 0.0 and 1.0" )
102117
118+ # Set all attributes at once
119+ object .__setattr__ (self , "red" , r )
120+ object .__setattr__ (self , "green" , g )
121+ object .__setattr__ (self , "blue" , b )
122+ object .__setattr__ (self , "alpha" , a )
123+
103124 def rgb (self ) -> RGB :
104125 """Get RGB components as tuple."""
105- return (self .r , self .g , self .b )
126+ return (self .red , self .green , self .blue )
106127
107128 def rgba (self ) -> RGBA :
108129 """Get RGBA components as tuple."""
109- return (self .r , self .g , self .b , self .a )
130+ return (self .red , self .green , self .blue , self .alpha )
110131
111- def toTuple (self ) -> Tuple [float , float , float , float ]:
112- """
113- Convert Color to RGBA tuple.
114- """
115- return (self .r , self .g , self .b , self .a )
132+ def to_occ_rgb (self ) -> "Quantity_Color" :
133+ """Convert Color to an OCCT RGB color object."""
134+ from OCP .Quantity import Quantity_Color , Quantity_TOC_sRGB
135+
136+ return Quantity_Color (self .red , self .green , self .blue , Quantity_TOC_sRGB )
137+
138+ def to_occ_rgba (self ) -> "Quantity_ColorRGBA" :
139+ """Convert Color to an OCCT RGBA color object."""
140+ from OCP .Quantity import Quantity_ColorRGBA
141+
142+ rgb = self .to_occ_rgb ()
143+ return Quantity_ColorRGBA (rgb , self .alpha )
116144
117145 def __repr__ (self ) -> str :
118146 """String representation of the color."""
119- return f"Color(r={ self .r } , g={ self .g } , b={ self .b } , a={ self .a } )"
147+ return f"Color(r={ self .red } , g={ self .green } , b={ self .blue } , a={ self .alpha } )"
120148
121149 def __str__ (self ) -> str :
122150 """String representation of the color."""
123- return f"({ self .r } , { self .g } , { self .b } , { self .a } )"
124-
125- def __hash__ (self ) -> int :
126- """Make Color hashable."""
127- return hash ((self .r , self .g , self .b , self .a ))
151+ return f"({ self .red } , { self .green } , { self .blue } , { self .alpha } )"
128152
129- def __eq__ (self , other : object ) -> bool :
130- """Compare two Color objects."""
131- if not isinstance (other , Color ):
132- return False
133- return (self .r , self .g , self .b , self .a ) == (other .r , other .g , other .b , other .a )
134153
135-
136- @dataclass
154+ @dataclass (unsafe_hash = True )
137155class SimpleMaterial :
138156 """
139157 Traditional material model matching OpenCascade's XCAFDoc_VisMaterialCommon.
@@ -153,32 +171,18 @@ def __post_init__(self):
153171 if not 0.0 <= self .transparency <= 1.0 :
154172 raise ValueError ("Transparency must be between 0.0 and 1.0" )
155173
156- def __hash__ (self ) -> int :
157- """Make CommonMaterial hashable."""
158- return hash (
159- (
160- self .ambient_color ,
161- self .diffuse_color ,
162- self .specular_color ,
163- self .shininess ,
164- self .transparency ,
165- )
166- )
174+ def apply_to_vtk_actor (self , actor : "vtkActor" ) -> None :
175+ """Apply common material properties to a VTK actor."""
176+ prop = actor .GetProperty ()
177+ prop .SetInterpolationToPhong ()
178+ prop .SetAmbientColor (* self .ambient_color .rgb ())
179+ prop .SetDiffuseColor (* self .diffuse_color .rgb ())
180+ prop .SetSpecularColor (* self .specular_color .rgb ())
181+ prop .SetSpecular (self .shininess )
182+ prop .SetOpacity (1.0 - self .transparency )
167183
168- def __eq__ (self , other : object ) -> bool :
169- """Compare two CommonMaterial objects."""
170- if not isinstance (other , SimpleMaterial ):
171- return False
172- return (
173- self .ambient_color == other .ambient_color
174- and self .diffuse_color == other .diffuse_color
175- and self .specular_color == other .specular_color
176- and self .shininess == other .shininess
177- and self .transparency == other .transparency
178- )
179184
180-
181- @dataclass
185+ @dataclass (unsafe_hash = True )
182186class PbrMaterial :
183187 """
184188 PBR material definition matching OpenCascade's XCAFDoc_VisMaterialPBR.
@@ -202,25 +206,18 @@ def __post_init__(self):
202206 if not 1.0 <= self .refraction_index <= 3.0 :
203207 raise ValueError ("Refraction index must be between 1.0 and 3.0" )
204208
205- def __hash__ (self ) -> int :
206- """Make PbrMaterial hashable."""
207- return hash (
208- (self .base_color , self .metallic , self .roughness , self .refraction_index ,)
209- )
210-
211- def __eq__ (self , other : object ) -> bool :
212- """Compare two PbrMaterial objects."""
213- if not isinstance (other , PbrMaterial ):
214- return False
215- return (
216- self .base_color == other .base_color
217- and self .metallic == other .metallic
218- and self .roughness == other .roughness
219- and self .refraction_index == other .refraction_index
220- )
209+ def apply_to_vtk_actor (self , actor : "vtkActor" ) -> None :
210+ """Apply PBR material properties to a VTK actor."""
211+ prop = actor .GetProperty ()
212+ prop .SetInterpolationToPBR ()
213+ prop .SetColor (* self .base_color .rgb ())
214+ prop .SetOpacity (self .base_color .alpha )
215+ prop .SetMetallic (self .metallic )
216+ prop .SetRoughness (self .roughness )
217+ prop .SetBaseIOR (self .refraction_index )
221218
222219
223- @dataclass
220+ @dataclass ( unsafe_hash = True )
224221class Material :
225222 """
226223 Material class that can store multiple representation types simultaneously.
@@ -229,7 +226,8 @@ class Material:
229226
230227 name : str
231228 description : str
232- density : float # kg/m³
229+ density : float
230+ density_unit : str = "kg/m³"
233231
234232 # Material representations
235233 color : Optional [Color ] = None
@@ -241,28 +239,62 @@ def __post_init__(self):
241239 if not any ([self .color , self .simple , self .pbr ]):
242240 raise ValueError ("Material must have at least one representation defined" )
243241
244- def __hash__ (self ) -> int :
245- """Make Material hashable."""
246- return hash (
247- (
248- self .name ,
249- self .description ,
250- self .density ,
251- self .color ,
252- self .simple ,
253- self .pbr ,
254- )
242+ def apply_to_vtk_actor (self , actor : "vtkActor" ) -> None :
243+ """Apply material properties to a VTK actor."""
244+ prop = actor .GetProperty ()
245+ prop .SetMaterialName (self .name )
246+
247+ if self .pbr :
248+ self .pbr .apply_to_vtk_actor (actor )
249+ elif self .simple :
250+ self .simple .apply_to_vtk_actor (actor )
251+ elif self .color :
252+ r , g , b , a = self .color .rgba ()
253+ prop .SetColor (r , g , b )
254+ prop .SetOpacity (a )
255+
256+ def to_occ_material (self ) -> "XCAFDoc_Material" :
257+ """Convert to OCCT material object."""
258+ from OCP .XCAFDoc import XCAFDoc_Material
259+ from OCP .TCollection import TCollection_HAsciiString
260+
261+ occt_material = XCAFDoc_Material ()
262+ occt_material .Set (
263+ TCollection_HAsciiString (self .name ),
264+ TCollection_HAsciiString (self .description ),
265+ self .density ,
266+ TCollection_HAsciiString (self .density_unit ),
267+ TCollection_HAsciiString ("DENSITY" ),
255268 )
256-
257- def __eq__ (self , other : object ) -> bool :
258- """Compare two Material objects."""
259- if not isinstance (other , Material ):
260- return False
261- return (
262- self .name == other .name
263- and self .description == other .description
264- and self .density == other .density
265- and self .color == other .color
266- and self .simple == other .simple
267- and self .pbr == other .pbr
269+ return occt_material
270+
271+ def to_occ_vis_material (self ) -> "XCAFDoc_VisMaterial" :
272+ """Convert to OCCT visualization material object."""
273+ from OCP .XCAFDoc import (
274+ XCAFDoc_VisMaterial ,
275+ XCAFDoc_VisMaterialPBR ,
276+ XCAFDoc_VisMaterialCommon ,
268277 )
278+
279+ vis_mat = XCAFDoc_VisMaterial ()
280+
281+ # Set up PBR material if provided
282+ if self .pbr :
283+ pbr_mat = XCAFDoc_VisMaterialPBR ()
284+ pbr_mat .BaseColor = self .pbr .base_color .to_occ_rgba ()
285+ pbr_mat .Metallic = self .pbr .metallic
286+ pbr_mat .Roughness = self .pbr .roughness
287+ pbr_mat .RefractionIndex = self .pbr .refraction_index
288+ vis_mat .SetPbrMaterial (pbr_mat )
289+
290+ # Set up common material if provided
291+ if self .simple :
292+ common_mat = XCAFDoc_VisMaterialCommon ()
293+ common_mat .AmbientColor = self .simple .ambient_color .to_occ_rgb ()
294+ common_mat .DiffuseColor = self .simple .diffuse_color .to_occ_rgb ()
295+ common_mat .SpecularColor = self .simple .specular_color .to_occ_rgb ()
296+ common_mat .Shininess = self .simple .shininess
297+ common_mat .Transparency = self .simple .transparency
298+ vis_mat .SetCommonMaterial (common_mat )
299+
300+ return vis_mat
0 commit comments