Skip to content

Commit 2a8410b

Browse files
Improved color class, moved logic for applying materials into the material classes
1 parent 8a480a4 commit 2a8410b

File tree

8 files changed

+278
-328
lines changed

8 files changed

+278
-328
lines changed

cadquery/materials.py

Lines changed: 165 additions & 134 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,27 @@
11
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+
39

410
RGB: TypeAlias = Tuple[float, float, float]
511
RGBA: TypeAlias = Tuple[float, float, float, float]
612

713

8-
@dataclass
14+
@dataclass(frozen=True)
915
class 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)
137154
class 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)
182185
class 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)
224220
class 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

Comments
 (0)