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