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,117 @@ 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_RGB
135+
136+ return Quantity_Color (self .red , self .green , self .blue , Quantity_TOC_RGB )
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+ return Quantity_ColorRGBA (self .red , self .green , self .blue , self .alpha )
116143
117144 def __repr__ (self ) -> str :
118145 """String representation of the color."""
119- return f"Color(r={ self .r } , g={ self .g } , b={ self .b } , a={ self .a } )"
146+ return f"Color(r={ self .red } , g={ self .green } , b={ self .blue } , a={ self .alpha } )"
120147
121148 def __str__ (self ) -> str :
122149 """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 ))
150+ return f"({ self .red } , { self .green } , { self .blue } , { self .alpha } )"
128151
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 )
134152
135-
136- @dataclass
153+ @dataclass (unsafe_hash = True )
137154class SimpleMaterial :
138155 """
139156 Traditional material model matching OpenCascade's XCAFDoc_VisMaterialCommon.
@@ -153,32 +170,18 @@ def __post_init__(self):
153170 if not 0.0 <= self .transparency <= 1.0 :
154171 raise ValueError ("Transparency must be between 0.0 and 1.0" )
155172
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- )
173+ def apply_to_vtk_actor (self , actor : "vtkActor" ) -> None :
174+ """Apply common material properties to a VTK actor."""
175+ prop = actor .GetProperty ()
176+ prop .SetInterpolationToPhong ()
177+ prop .SetAmbientColor (* self .ambient_color .rgb ())
178+ prop .SetDiffuseColor (* self .diffuse_color .rgb ())
179+ prop .SetSpecularColor (* self .specular_color .rgb ())
180+ prop .SetSpecular (self .shininess )
181+ prop .SetOpacity (1.0 - self .transparency )
167182
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- )
179183
180-
181- @dataclass
184+ @dataclass (unsafe_hash = True )
182185class PbrMaterial :
183186 """
184187 PBR material definition matching OpenCascade's XCAFDoc_VisMaterialPBR.
@@ -202,25 +205,18 @@ def __post_init__(self):
202205 if not 1.0 <= self .refraction_index <= 3.0 :
203206 raise ValueError ("Refraction index must be between 1.0 and 3.0" )
204207
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- )
208+ def apply_to_vtk_actor (self , actor : "vtkActor" ) -> None :
209+ """Apply PBR material properties to a VTK actor."""
210+ prop = actor .GetProperty ()
211+ prop .SetInterpolationToPBR ()
212+ prop .SetColor (* self .base_color .rgb ())
213+ prop .SetOpacity (self .base_color .alpha )
214+ prop .SetMetallic (self .metallic )
215+ prop .SetRoughness (self .roughness )
216+ prop .SetBaseIOR (self .refraction_index )
221217
222218
223- @dataclass
219+ @dataclass ( unsafe_hash = True )
224220class Material :
225221 """
226222 Material class that can store multiple representation types simultaneously.
@@ -229,7 +225,8 @@ class Material:
229225
230226 name : str
231227 description : str
232- density : float # kg/m³
228+ density : float
229+ density_unit : str = "kg/m³"
233230
234231 # Material representations
235232 color : Optional [Color ] = None
@@ -241,28 +238,62 @@ def __post_init__(self):
241238 if not any ([self .color , self .simple , self .pbr ]):
242239 raise ValueError ("Material must have at least one representation defined" )
243240
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- )
241+ def apply_to_vtk_actor (self , actor : "vtkActor" ) -> None :
242+ """Apply material properties to a VTK actor."""
243+ prop = actor .GetProperty ()
244+ prop .SetMaterialName (self .name )
245+
246+ if self .pbr :
247+ self .pbr .apply_to_vtk_actor (actor )
248+ elif self .simple :
249+ self .simple .apply_to_vtk_actor (actor )
250+ elif self .color :
251+ r , g , b , a = self .color .rgba ()
252+ prop .SetColor (r , g , b )
253+ prop .SetOpacity (a )
254+
255+ def to_occ_material (self ) -> "XCAFDoc_Material" :
256+ """Convert to OCCT material object."""
257+ from OCP .XCAFDoc import XCAFDoc_Material
258+ from OCP .TCollection import TCollection_HAsciiString
259+
260+ occt_material = XCAFDoc_Material ()
261+ occt_material .Set (
262+ TCollection_HAsciiString (self .name ),
263+ TCollection_HAsciiString (self .description ),
264+ self .density ,
265+ TCollection_HAsciiString (self .density_unit ),
266+ TCollection_HAsciiString ("DENSITY" ),
255267 )
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
268+ return occt_material
269+
270+ def to_occ_vis_material (self ) -> "XCAFDoc_VisMaterial" :
271+ """Convert to OCCT visualization material object."""
272+ from OCP .XCAFDoc import (
273+ XCAFDoc_VisMaterial ,
274+ XCAFDoc_VisMaterialPBR ,
275+ XCAFDoc_VisMaterialCommon ,
268276 )
277+
278+ vis_mat = XCAFDoc_VisMaterial ()
279+
280+ # Set up PBR material if provided
281+ if self .pbr :
282+ pbr_mat = XCAFDoc_VisMaterialPBR ()
283+ pbr_mat .BaseColor = self .pbr .base_color .to_occ_rgba ()
284+ pbr_mat .Metallic = self .pbr .metallic
285+ pbr_mat .Roughness = self .pbr .roughness
286+ pbr_mat .RefractionIndex = self .pbr .refraction_index
287+ vis_mat .SetPbrMaterial (pbr_mat )
288+
289+ # Set up common material if provided
290+ if self .simple :
291+ common_mat = XCAFDoc_VisMaterialCommon ()
292+ common_mat .AmbientColor = self .simple .ambient_color .to_occ_rgb ()
293+ common_mat .DiffuseColor = self .simple .diffuse_color .to_occ_rgb ()
294+ common_mat .SpecularColor = self .simple .specular_color .to_occ_rgb ()
295+ common_mat .Shininess = self .simple .shininess
296+ common_mat .Transparency = self .simple .transparency
297+ vis_mat .SetCommonMaterial (common_mat )
298+
299+ return vis_mat
0 commit comments