1
1
from 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
+
3
9
4
10
RGB : TypeAlias = Tuple [float , float , float ]
5
11
RGBA : TypeAlias = Tuple [float , float , float , float ]
6
12
7
13
8
- @dataclass
14
+ @dataclass ( frozen = True )
9
15
class Color :
10
16
"""
11
17
Simple color representation with optional alpha channel.
12
18
All values are in range [0.0, 1.0].
13
19
"""
14
20
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
19
25
20
26
@overload
21
27
def __init__ (self ):
@@ -34,106 +40,117 @@ def __init__(self, name: str):
34
40
...
35
41
36
42
@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 ):
38
44
"""
39
45
Construct a Color from RGB(A) values.
40
46
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)
45
51
"""
46
52
...
47
53
48
54
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 :
50
63
# 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
57
67
from vtkmodules .vtkCommonColor import vtkNamedColors
58
68
69
+ color_name = args [0 ] if number_of_args == 1 else kwargs ["name" ]
70
+
59
71
# Try to get color from OCCT first, fall back to VTK if not found
60
72
try :
61
73
# 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
+ )
67
85
except ValueError :
68
86
# Check if color exists in VTK
69
87
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 } " )
72
90
73
91
# 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" )
97
112
98
113
# 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 )]:
100
115
if not 0.0 <= value <= 1.0 :
101
116
raise ValueError (f"{ name } component must be between 0.0 and 1.0" )
102
117
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
+
103
124
def rgb (self ) -> RGB :
104
125
"""Get RGB components as tuple."""
105
- return (self .r , self .g , self .b )
126
+ return (self .red , self .green , self .blue )
106
127
107
128
def rgba (self ) -> RGBA :
108
129
"""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 )
110
131
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 )
116
143
117
144
def __repr__ (self ) -> str :
118
145
"""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 } )"
120
147
121
148
def __str__ (self ) -> str :
122
149
"""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 } )"
128
151
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 )
134
152
135
-
136
- @dataclass
153
+ @dataclass (unsafe_hash = True )
137
154
class SimpleMaterial :
138
155
"""
139
156
Traditional material model matching OpenCascade's XCAFDoc_VisMaterialCommon.
@@ -153,32 +170,18 @@ def __post_init__(self):
153
170
if not 0.0 <= self .transparency <= 1.0 :
154
171
raise ValueError ("Transparency must be between 0.0 and 1.0" )
155
172
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 )
167
182
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
- )
179
183
180
-
181
- @dataclass
184
+ @dataclass (unsafe_hash = True )
182
185
class PbrMaterial :
183
186
"""
184
187
PBR material definition matching OpenCascade's XCAFDoc_VisMaterialPBR.
@@ -202,25 +205,18 @@ def __post_init__(self):
202
205
if not 1.0 <= self .refraction_index <= 3.0 :
203
206
raise ValueError ("Refraction index must be between 1.0 and 3.0" )
204
207
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 )
221
217
222
218
223
- @dataclass
219
+ @dataclass ( unsafe_hash = True )
224
220
class Material :
225
221
"""
226
222
Material class that can store multiple representation types simultaneously.
@@ -229,7 +225,8 @@ class Material:
229
225
230
226
name : str
231
227
description : str
232
- density : float # kg/m³
228
+ density : float
229
+ density_unit : str = "kg/m³"
233
230
234
231
# Material representations
235
232
color : Optional [Color ] = None
@@ -241,28 +238,62 @@ def __post_init__(self):
241
238
if not any ([self .color , self .simple , self .pbr ]):
242
239
raise ValueError ("Material must have at least one representation defined" )
243
240
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" ),
255
267
)
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 ,
268
276
)
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