From 34b6b8e60a1d235c8e48b50aaf344926c1e1cea4 Mon Sep 17 00:00:00 2001 From: Thomas Mansencal Date: Sun, 29 Oct 2023 16:03:20 +1300 Subject: [PATCH] Overhaul all the visual classes. --- colour_visuals/axes.py | 61 ++- colour_visuals/common.py | 1 + colour_visuals/diagrams.py | 527 ++++++++++++++++++---- colour_visuals/grid.py | 232 +++++++++- colour_visuals/pointer_gamut.py | 162 +++++-- colour_visuals/rgb_colourspace.py | 214 ++++++--- colour_visuals/rgb_scatter.py | 124 ++++-- colour_visuals/rosch_macadam.py | 114 +++-- colour_visuals/visual.py | 708 ++++++++++++++++++++++++++++++ docs/colour_visuals.rst | 28 ++ utilities/generate_plots.py | 8 +- 11 files changed, 1906 insertions(+), 273 deletions(-) create mode 100644 colour_visuals/visual.py diff --git a/colour_visuals/axes.py b/colour_visuals/axes.py index a5b80e3..2dfa752 100644 --- a/colour_visuals/axes.py +++ b/colour_visuals/axes.py @@ -13,17 +13,22 @@ import numpy as np import pygfx as gfx from colour.hints import LiteralColourspaceModel -from colour.models import COLOURSPACE_MODELS, COLOURSPACE_MODELS_AXIS_LABELS +from colour.models import COLOURSPACE_MODELS_AXIS_LABELS from colour.plotting import ( CONSTANTS_COLOUR_STYLE, colourspace_model_axis_reorder, ) -from colour.utilities import as_int_array, validate_method +from colour.utilities import as_int_array from colour_visuals.common import ( DEFAULT_FLOAT_DTYPE_WGPU, unlatexify, ) +from colour_visuals.visual import ( + MixinPropertyModel, + MixinPropertySize, + Visual, +) __author__ = "Colour Developers" __copyright__ = "Copyright 2023 Colour Developers" @@ -35,7 +40,7 @@ __all__ = ["VisualAxes"] -class VisualAxes(gfx.Group): +class VisualAxes(MixinPropertyModel, MixinPropertySize, Visual): """ Create an axes visual. @@ -47,6 +52,16 @@ class VisualAxes(gfx.Group): size Size of the axes. + Attributes + ---------- + - :attr:`~colour_visuals.VisualAxes.model` + - :attr:`~colour_visuals.VisualAxes.size` + + Methods + ------- + - :meth:`~colour_visuals.VisualAxes.__init__` + - :meth:`~colour_visuals.VisualAxes.update` + Examples -------- >>> import os @@ -80,8 +95,24 @@ def __init__( ): super().__init__() - size = int(size) - model = validate_method(model, tuple(COLOURSPACE_MODELS)) + self._axes_helper = None + self._x_text = None + self._y_text = None + self._z_text = None + + with self.block_update(): + self.model = model + self.size = size + + self.update() + + def update(self): + """Update the visual.""" + + if self._is_update_blocked: + return + + self.clear() axes_positions = np.array( [ @@ -94,7 +125,7 @@ def __init__( ], dtype=DEFAULT_FLOAT_DTYPE_WGPU, ) - axes_positions *= size + axes_positions *= int(self._size) axes_colours = np.array( [ @@ -114,8 +145,10 @@ def __init__( ) self.add(self._axes_helper) - labels = np.array(COLOURSPACE_MODELS_AXIS_LABELS[model])[ - as_int_array(colourspace_model_axis_reorder([0, 1, 2], model)) + labels = np.array(COLOURSPACE_MODELS_AXIS_LABELS[self._model])[ + as_int_array( + colourspace_model_axis_reorder([0, 1, 2], self._model) + ) ] self._x_text = gfx.Text( @@ -125,9 +158,9 @@ def __init__( screen_space=True, anchor="Middle-Center", ), - gfx.TextMaterial(color=np.array([1, 0, 0])), # pyright: ignore + gfx.TextMaterial(color=np.array([1, 0, 0])), ) - self._x_text.local.position = np.array([1.1, 0, 0]) + self._x_text.local.position = np.array([1 * self._size * 1.05, 0, 0]) self.add(self._x_text) self._y_text = gfx.Text( @@ -137,9 +170,9 @@ def __init__( screen_space=True, anchor="Middle-Center", ), - gfx.TextMaterial(color=np.array([0, 1, 0])), # pyright: ignore + gfx.TextMaterial(color=np.array([0, 1, 0])), ) - self._y_text.local.position = np.array([0, 1.1, 0]) + self._y_text.local.position = np.array([0, 1 * self._size * 1.05, 0]) self.add(self._y_text) self._z_text = gfx.Text( @@ -149,9 +182,9 @@ def __init__( screen_space=True, anchor="Middle-Center", ), - gfx.TextMaterial(color=np.array([0, 0, 1])), # pyright: ignore + gfx.TextMaterial(color=np.array([0, 0, 1])), ) - self._z_text.local.position = np.array([0, 0, 1.1]) + self._z_text.local.position = np.array([0, 0, 1 * self._size * 1.05]) self.add(self._z_text) diff --git a/colour_visuals/common.py b/colour_visuals/common.py index c8afaaa..2f7c175 100644 --- a/colour_visuals/common.py +++ b/colour_visuals/common.py @@ -42,6 +42,7 @@ "unlatexify", ] + DEFAULT_FLOAT_DTYPE_WGPU = np.float32 """Default int number dtype.""" diff --git a/colour_visuals/diagrams.py b/colour_visuals/diagrams.py index 1eb1ea2..1533ddf 100644 --- a/colour_visuals/diagrams.py +++ b/colour_visuals/diagrams.py @@ -34,15 +34,12 @@ METHODS_CHROMATICITY_DIAGRAM, XYZ_to_plotting_colourspace, colourspace_model_axis_reorder, - filter_cmfs, ) from colour.plotting.diagrams import lines_spectral_locus from colour.utilities import ( - first_item, full, optional, tstack, - validate_method, ) from scipy.spatial import Delaunay @@ -53,6 +50,20 @@ append_channel, as_contiguous_array, ) +from colour_visuals.visual import ( + MixinPropertyCMFS, + MixinPropertyColour, + MixinPropertyKwargs, + MixinPropertyMethod, + MixinPropertyModel, + MixinPropertyOpacity, + MixinPropertySamples, + MixinPropertyThickness, + MixinPropertyTypeMaterial, + MixinPropertyWireframe, + Visual, + visual_property, +) __author__ = "Colour Developers" __copyright__ = "Copyright 2023 Colour Developers" @@ -65,13 +76,22 @@ "VisualSpectralLocus2D", "VisualSpectralLocus3D", "VisualChromaticityDiagram", + "MixinPropertyKwargsVisualSpectralLocus", + "MixinPropertyKwargsVisualChromaticityDiagram", "VisualChromaticityDiagramCIE1931", "VisualChromaticityDiagramCIE1960UCS", "VisualChromaticityDiagramCIE1976UCS", ] -class VisualSpectralLocus2D(gfx.Group): +class VisualSpectralLocus2D( + MixinPropertyCMFS, + MixinPropertyColour, + MixinPropertyMethod, + MixinPropertyOpacity, + MixinPropertyThickness, + Visual, +): """ Create a 2D *Spectral Locus* visual. @@ -87,14 +107,28 @@ class VisualSpectralLocus2D(gfx.Group): Array of wavelength labels used to customise which labels will be drawn around the spectral locus. Passing an empty array will result in no wavelength labels being drawn. - colours - Colours of the visual, if *None*, the colours are computed from the - visual geometry. + colour + Colour of the visual, if *None*, the colour is computed from the visual + geometry. opacity Opacity of the visual. thickness Thickness of the visual lines. + Attributes + ---------- + - :attr:`~colour_visuals.VisualSpectralLocus2D.cmfs` + - :attr:`~colour_visuals.VisualSpectralLocus2D.method` + - :attr:`~colour_visuals.VisualSpectralLocus2D.labels` + - :attr:`~colour_visuals.VisualSpectralLocus2D.colour` + - :attr:`~colour_visuals.VisualSpectralLocus2D.opacity` + - :attr:`~colour_visuals.VisualSpectralLocus2D.thickness` + + Methods + ------- + - :meth:`~colour_visuals.VisualSpectralLocus2D.__init__` + - :meth:`~colour_visuals.VisualSpectralLocus2D.update` + Examples -------- >>> import os @@ -131,22 +165,67 @@ def __init__( method: Literal["CIE 1931", "CIE 1960 UCS", "CIE 1976 UCS"] | str = "CIE 1931", labels: Sequence | None = None, - colours: ArrayLike | None = None, + colour: ArrayLike | None = None, opacity: float = 1, thickness: float = 1, ): super().__init__() - cmfs = cast( - MultiSpectralDistributions, first_item(filter_cmfs(cmfs).values()) - ) - method = validate_method(method, tuple(METHODS_CHROMATICITY_DIAGRAM)) - labels = cast( + self._spectral_locus = None + self._wavelengths = None + self._wavelength_labels = None + self._points = None + + with self.block_update(): + self.cmfs = cmfs + self.method = method + self.labels = labels + self.colour = colour + self.opacity = opacity + self.thickness = thickness + + self.update() + + @visual_property + def labels( + self, + ) -> Sequence | None: + """ + Getter and setter property for the labels. + + Parameters + ---------- + value + Value to set the labels with. + + Returns + ------- + :class:`str` + Labels. + """ + + return self._labels + + @labels.setter + def labels(self, value: Sequence | None): + """Setter for the **self.labels** property.""" + + self._labels = cast( Sequence, - optional(labels, LABELS_CHROMATICITY_DIAGRAM_DEFAULT[method]), + optional(value, LABELS_CHROMATICITY_DIAGRAM_DEFAULT[self._method]), ) - lines_sl, lines_w = lines_spectral_locus(cmfs, labels, method) + def update(self): + """Update the visual.""" + + if self._is_update_blocked: + return + + self.clear() + + lines_sl, lines_w = lines_spectral_locus( + self._cmfs, self._labels, self._method + ) # Spectral Locus positions = np.concatenate( @@ -160,24 +239,29 @@ def __init__( ] ) - if colours is None: - colours_sl = np.concatenate( + if self._colour is None: + colour_sl = np.concatenate( [lines_sl["colour"][:-1], lines_sl["colour"][1:]], axis=1 ).reshape([-1, 3]) else: - colours_sl = np.tile(colours, (positions.shape[0], 1)) + colour_sl = np.tile(self._colour, (positions.shape[0], 1)) self._spectral_locus = gfx.Line( gfx.Geometry( positions=as_contiguous_array(positions), colors=as_contiguous_array( - append_channel(colours_sl, opacity) + append_channel(colour_sl, self._opacity) ), ), - gfx.LineSegmentMaterial(thickness=thickness, color_mode="vertex"), + gfx.LineSegmentMaterial( + thickness=self._thickness, color_mode="vertex" + ), ) self.add(self._spectral_locus) + if not self._labels: + return + # Wavelengths positions = lines_w["position"] positions = np.hstack( @@ -187,24 +271,32 @@ def __init__( ] ) - if colours is None: - colours_w = lines_w["colour"] + if self._colour is None: + colour_w = lines_w["colour"] else: - colours_w = np.tile(colours, (positions.shape[0], 1)) + colour_w = np.tile(self._colour, (positions.shape[0], 1)) self._wavelengths = gfx.Line( gfx.Geometry( positions=as_contiguous_array(positions), - colors=as_contiguous_array(append_channel(colours_w, opacity)), + colors=as_contiguous_array( + append_channel(colour_w, self._opacity) + ), + ), + gfx.LineSegmentMaterial( + thickness=self._thickness, color_mode="vertex" ), - gfx.LineSegmentMaterial(thickness=thickness, color_mode="vertex"), ) self.add(self._wavelengths) # Labels - self._labels = [] + self._wavelength_labels = [] for i, label in enumerate( - [label for label in labels if label in cmfs.wavelengths] + [ + label + for label in self._labels + if label in self._cmfs.wavelengths + ] ): positions = lines_w["position"][::2] normals = lines_w["normal"][::2] @@ -227,7 +319,7 @@ def __init__( 0, ] ) - self._labels.append(text) + self._wavelength_labels.append(text) self.add(text) positions = np.hstack( @@ -241,19 +333,21 @@ def __init__( ] ) - if colours is None: - colours_lp = lines_w["colour"][::2] + if self._colour is None: + colour_lp = lines_w["colour"][::2] else: - colours_lp = np.tile(colours, (positions.shape[0], 1)) + colour_lp = np.tile(self._colour, (positions.shape[0], 1)) self._points = gfx.Points( gfx.Geometry( positions=as_contiguous_array(positions), sizes=as_contiguous_array( - full(lines_w["position"][::2].shape[0], thickness * 3) + full( + lines_w["position"][::2].shape[0], self._thickness * 3 + ) ), colors=as_contiguous_array( - append_channel(colours_lp, opacity) + append_channel(colour_lp, self._opacity) ), ), gfx.PointsMaterial(color_mode="vertex", vertex_sizes=True), @@ -261,7 +355,15 @@ def __init__( self.add(self._points) -class VisualSpectralLocus3D(gfx.Line): +class VisualSpectralLocus3D( + MixinPropertyCMFS, + MixinPropertyColour, + MixinPropertyKwargs, + MixinPropertyModel, + MixinPropertyOpacity, + MixinPropertyThickness, + Visual, +): """ Create a 3D *Spectral Locus* visual. @@ -278,9 +380,9 @@ class VisualSpectralLocus3D(gfx.Line): Array of wavelength labels used to customise which labels will be drawn around the spectral locus. Passing an empty array will result in no wavelength labels being drawn. - colours - Colours of the visual, if *None*, the colours are computed from the - visual geometry. + colour + Colour of the visual, if *None*, the colour is computed from the visual + geometry. opacity Opacity of the visual. thickness @@ -291,6 +393,20 @@ class VisualSpectralLocus3D(gfx.Line): kwargs See the documentation of the supported conversion definitions. + Attributes + ---------- + - :attr:`~colour_visuals.VisualSpectralLocus3D.cmfs` + - :attr:`~colour_visuals.VisualSpectralLocus3D.model` + - :attr:`~colour_visuals.VisualSpectralLocus3D.labels` + - :attr:`~colour_visuals.VisualSpectralLocus3D.colour` + - :attr:`~colour_visuals.VisualSpectralLocus3D.opacity` + - :attr:`~colour_visuals.VisualSpectralLocus3D.thickness` + + Methods + ------- + - :meth:`~colour_visuals.VisualSpectralLocus3D.__init__` + - :meth:`~colour_visuals.VisualSpectralLocus3D.update` + Examples -------- >>> import os @@ -325,47 +441,80 @@ def __init__( MultiSpectralDistributions | str ] = "CIE 1931 2 Degree Standard Observer", model: LiteralColourspaceModel | str = "CIE xyY", - colours: ArrayLike | None = None, + colour: ArrayLike | None = None, opacity: float = 1, thickness: float = 1, **kwargs, ): super().__init__() - cmfs = cast( - MultiSpectralDistributions, first_item(filter_cmfs(cmfs).values()) - ) + self._spectral_locus = None + + with self.block_update(): + self.cmfs = cmfs + self.model = model + self.colour = colour + self.opacity = opacity + self.thickness = thickness + self.kwargs = kwargs + + self.update() + + def update(self): + """Update the visual.""" + + if self._is_update_blocked: + return + + self.clear() colourspace = CONSTANTS_COLOUR_STYLE.colour.colourspace positions = colourspace_model_axis_reorder( XYZ_to_colourspace_model( - cmfs.values, colourspace.whitepoint, model, **kwargs + self._cmfs.values, + colourspace.whitepoint, + self._model, + **self._kwargs, ), - model, + self._model, ) positions = np.concatenate( [positions[:-1], positions[1:]], axis=1 ).reshape([-1, 3]) - if colours is None: - colours = XYZ_to_RGB(cmfs.values, colourspace) - colours = np.concatenate( - [colours[:-1], colours[1:]], axis=1 - ).reshape([-1, 3]) + if self._colour is None: + colour = XYZ_to_RGB(self._cmfs.values, colourspace) + colour = np.concatenate([colour[:-1], colour[1:]], axis=1).reshape( + [-1, 3] + ) else: - colours = np.tile(colours, (positions.shape[0], 1)) + colours = np.tile(self._colour, (positions.shape[0], 1)) - super().__init__( + self._spectral_locus = gfx.Line( gfx.Geometry( positions=as_contiguous_array(positions), - colors=as_contiguous_array(append_channel(colours, opacity)), + colors=as_contiguous_array( + append_channel(colours, self._opacity) + ), + ), + gfx.LineSegmentMaterial( + thickness=self._thickness, color_mode="vertex" ), - gfx.LineSegmentMaterial(thickness=thickness, color_mode="vertex"), ) + self.add(self._spectral_locus) -class VisualChromaticityDiagram(gfx.Mesh): +class VisualChromaticityDiagram( + MixinPropertyCMFS, + MixinPropertyColour, + MixinPropertyTypeMaterial, + MixinPropertyMethod, + MixinPropertyOpacity, + MixinPropertySamples, + MixinPropertyWireframe, + Visual, +): """ Create a *Chromaticity Diagram* visual. @@ -378,18 +527,33 @@ class VisualChromaticityDiagram(gfx.Mesh): method *Chromaticity Diagram* method. colours - Colours of the visual, if *None*, the colours are computed from the + Colour of the visual, if *None*, the colours are computed from the visual geometry. opacity Opacity of the visual. material - Material used to surface the visual geomeetry. + Material used to surface the visual geometry. wireframe Whether to render the visual as a wireframe, i.e., only render edges. samples Samples count used for generating the *Chromaticity Diagram* Delaunay tesselation. + Attributes + ---------- + - :attr:`~colour_visuals.VisualChromaticityDiagram.cmfs` + - :attr:`~colour_visuals.VisualChromaticityDiagram.method` + - :attr:`~colour_visuals.VisualChromaticityDiagram.colours` + - :attr:`~colour_visuals.VisualChromaticityDiagram.opacity` + - :attr:`~colour_visuals.VisualChromaticityDiagram.type_material` + - :attr:`~colour_visuals.VisualChromaticityDiagram.wireframe` + - :attr:`~colour_visuals.VisualChromaticityDiagram.samples` + + Methods + ------- + - :meth:`~colour_visuals.VisualChromaticityDiagram.__init__` + - :meth:`~colour_visuals.VisualChromaticityDiagram.update` + Examples -------- >>> import os @@ -431,30 +595,49 @@ def __init__( wireframe: bool = False, samples: int = 64, ): - cmfs = cast( - MultiSpectralDistributions, first_item(filter_cmfs(cmfs).values()) - ) + super().__init__() + + self._chromaticity_diagram = None + + with self.block_update(): + self.cmfs = cmfs + self.method = method + self.colours = colours + self.opacity = opacity + self.type_material = material + self.wireframe = wireframe + self.samples = samples + + self.update() + + def update(self): + """Update the visual.""" + + if self._is_update_blocked: + return + + self.clear() illuminant = CONSTANTS_COLOUR_STYLE.colour.colourspace.whitepoint - XYZ_to_ij = METHODS_CHROMATICITY_DIAGRAM[method]["XYZ_to_ij"] - ij_to_XYZ = METHODS_CHROMATICITY_DIAGRAM[method]["ij_to_XYZ"] + XYZ_to_ij = METHODS_CHROMATICITY_DIAGRAM[self._method]["XYZ_to_ij"] + ij_to_XYZ = METHODS_CHROMATICITY_DIAGRAM[self._method]["ij_to_XYZ"] # CMFS - ij_l = XYZ_to_ij(cmfs.values, illuminant) + ij_l = XYZ_to_ij(self._cmfs.values, illuminant) # Line of Purples d = euclidean_distance(ij_l[0], ij_l[-1]) ij_p = tstack( [ - np.linspace(ij_l[0][0], ij_l[-1][0], int(d * samples)), - np.linspace(ij_l[0][1], ij_l[-1][1], int(d * samples)), + np.linspace(ij_l[0][0], ij_l[-1][0], int(d * self._samples)), + np.linspace(ij_l[0][1], ij_l[-1][1], int(d * self._samples)), ] ) # Grid triangulation = Delaunay(ij_l, qhull_options="QJ") - xi = np.linspace(0, 1, samples) + xi = np.linspace(0, 1, self._samples) ii_g, jj_g = np.meshgrid(xi, xi) ij_g = tstack([ii_g, jj_g]) ij_g = ij_g[triangulation.find_simplex(ij_g) > 0] @@ -465,7 +648,7 @@ def __init__( [ij, np.full((ij.shape[0], 1), 0, DEFAULT_FLOAT_DTYPE_WGPU)] ) - if colours is None: + if self._colour is None: colours = normalise_maximum( XYZ_to_plotting_colourspace( ij_to_XYZ(positions[..., :2], illuminant), illuminant @@ -473,25 +656,126 @@ def __init__( axis=-1, ) else: - colours = np.tile(colours, (positions.shape[0], 1)) + colours = np.tile(self._colour, (positions.shape[0], 1)) geometry = gfx.Geometry( positions=as_contiguous_array(positions), indices=as_contiguous_array( triangulation.simplices, DEFAULT_INT_DTYPE_WGPU ), - colors=as_contiguous_array(append_channel(colours, opacity)), + colors=as_contiguous_array(append_channel(colours, self._opacity)), ) - super().__init__( + self._chromaticity_diagram = gfx.Mesh( geometry, - material(color_mode="vertex", wireframe=wireframe) - if wireframe - else material(color_mode="vertex"), + self._type_material(color_mode="vertex", wireframe=self._wireframe) + if self._wireframe + else self._type_material(color_mode="vertex"), ) + self.add(self._chromaticity_diagram) -class VisualChromaticityDiagramCIE1931(gfx.Group): +class MixinPropertyKwargsVisualSpectralLocus: + """ + Define a mixin for keyword arguments for the + :class:`colour_visuals.VisualSpectralLocus2D` class. + + Attributes + ---------- + - :attr:`~colour_visuals.diagrams.MixinPropertyKwargsVisualSpectralLocus.\ +kwargs_visual_spectral_locus` + """ + + def __init__(self): + self._spectral_locus = None + self._kwargs_visual_spectral_locus = {} + + super().__init__() + + @property + def kwargs_visual_spectral_locus(self) -> dict: + """ + Getter and setter property for the visual kwargs for the + *Spectral Locus*. + + Parameters + ---------- + value + Value to set visual kwargs for the *Spectral Locus* with. + + Returns + ------- + :class:`dict` + Visual kwargs for the *Spectral Locus*. + """ + + return self._kwargs_visual_spectral_locus + + @kwargs_visual_spectral_locus.setter + def kwargs_visual_spectral_locus(self, value: dict): + """ + Setter for the **self.kwargs_visual_spectral_locus** property. + """ + + self._kwargs_visual_spectral_locus = value + + for key, value in self._kwargs_visual_spectral_locus.items(): + setattr(self._spectral_locus, key, value) + + +class MixinPropertyKwargsVisualChromaticityDiagram: + """ + Define a mixin for keyword arguments for the + :class:`colour_visuals.VisualChromaticityDiagram` class. + + Attributes + ---------- + - :attr:`~colour_visuals.diagrams.\ +MixinPropertyKwargsVisualChromaticityDiagram.kwargs_visual_chromaticity_diagram` + """ + + def __init__(self): + self._chromaticity_diagram = None + self._kwargs_visual_chromaticity_diagram = {} + + super().__init__() + + @property + def kwargs_visual_chromaticity_diagram(self) -> dict: + """ + Getter and setter property for the visual kwargs for the + *Chromaticity Diagram*. + + Parameters + ---------- + value + Value to set visual kwargs for the *Chromaticity Diagram* with. + + Returns + ------- + :class:`dict` + Visual kwargs for the *Chromaticity Diagram*. + """ + + return self._kwargs_visual_chromaticity_diagram + + @kwargs_visual_chromaticity_diagram.setter + def kwargs_visual_chromaticity_diagram(self, value: dict): + """ + Setter for the **self.kwargs_visual_chromaticity_diagram** property. + """ + + self._kwargs_visual_chromaticity_diagram = value + + for key, value in self._kwargs_visual_chromaticity_diagram.items(): + setattr(self._chromaticity_diagram, key, value) + + +class VisualChromaticityDiagramCIE1931( + MixinPropertyKwargsVisualSpectralLocus, + MixinPropertyKwargsVisualChromaticityDiagram, + Visual, +): """ Create the *CIE 1931* *Chromaticity Diagram* visual. @@ -504,6 +788,18 @@ class VisualChromaticityDiagramCIE1931(gfx.Group): Keyword arguments for the underlying :class:`colour_visuals.VisualChromaticityDiagram` class. + Attributes + ---------- + - :attr:`~colour_visuals.VisualChromaticityDiagramCIE1931.\ +kwargs_visual_spectral_locus` + - :attr:`~colour_visuals.VisualChromaticityDiagramCIE1931.\ +kwargs_visual_chromaticity_diagram` + + Methods + ------- + - :meth:`~colour_visuals.VisualChromaticityDiagramCIE1931.__init__` + - :meth:`~colour_visuals.VisualChromaticityDiagramCIE1931.update` + Examples -------- >>> import os @@ -539,19 +835,34 @@ def __init__( ): super().__init__() - self._spectral_locus = VisualSpectralLocus2D( - method="CIE 1931", **(optional(kwargs_visual_spectral_locus, {})) - ) + if kwargs_visual_spectral_locus is None: + kwargs_visual_spectral_locus = {} + + if kwargs_visual_chromaticity_diagram is None: + kwargs_visual_chromaticity_diagram = {} + + self._spectral_locus = VisualSpectralLocus2D(method="CIE 1931") self.add(self._spectral_locus) self._chromaticity_diagram = VisualChromaticityDiagram( - method="CIE 1931", - **(optional(kwargs_visual_chromaticity_diagram, {})), + method="CIE 1931" ) self.add(self._chromaticity_diagram) + self.kwargs_visual_spectral_locus = kwargs_visual_spectral_locus + self.kwargs_visual_chromaticity_diagram = ( + kwargs_visual_chromaticity_diagram + ) + + def update(self): + """Update the visual.""" -class VisualChromaticityDiagramCIE1960UCS(gfx.Group): + +class VisualChromaticityDiagramCIE1960UCS( + MixinPropertyKwargsVisualSpectralLocus, + MixinPropertyKwargsVisualChromaticityDiagram, + Visual, +): """ Create the *CIE 1960 UCS* *Chromaticity Diagram* visual. @@ -564,6 +875,18 @@ class VisualChromaticityDiagramCIE1960UCS(gfx.Group): Keyword arguments for the underlying :class:`colour_visuals.VisualChromaticityDiagram` class. + Attributes + ---------- + - :attr:`~colour_visuals.VisualChromaticityDiagramCIE1960UCS.\ +kwargs_visual_spectral_locus` + - :attr:`~colour_visuals.VisualChromaticityDiagramCIE1960UCS.\ +kwargs_visual_chromaticity_diagram` + + Methods + ------- + - :meth:`~colour_visuals.VisualChromaticityDiagramCIE1960UCS.__init__` + - :meth:`~colour_visuals.VisualChromaticityDiagramCIE1960UCS.update` + Examples -------- >>> import os @@ -599,6 +922,12 @@ def __init__( ): super().__init__() + if kwargs_visual_spectral_locus is None: + kwargs_visual_spectral_locus = {} + + if kwargs_visual_chromaticity_diagram is None: + kwargs_visual_chromaticity_diagram = {} + self._spectral_locus = VisualSpectralLocus2D( method="CIE 1960 UCS", **(optional(kwargs_visual_spectral_locus, {})), @@ -611,8 +940,20 @@ def __init__( ) self.add(self._chromaticity_diagram) + self.kwargs_visual_spectral_locus = kwargs_visual_spectral_locus + self.kwargs_visual_chromaticity_diagram = ( + kwargs_visual_chromaticity_diagram + ) -class VisualChromaticityDiagramCIE1976UCS(gfx.Group): + def update(self): + """Update the visual.""" + + +class VisualChromaticityDiagramCIE1976UCS( + MixinPropertyKwargsVisualSpectralLocus, + MixinPropertyKwargsVisualChromaticityDiagram, + Visual, +): """ Create the *CIE 1976 UCS* *Chromaticity Diagram* visual. @@ -625,6 +966,18 @@ class VisualChromaticityDiagramCIE1976UCS(gfx.Group): Keyword arguments for the underlying :class:`colour_visuals.VisualChromaticityDiagram` class. + Attributes + ---------- + - :attr:`~colour_visuals.VisualChromaticityDiagramCIE1976UCS.\ +kwargs_visual_spectral_locus` + - :attr:`~colour_visuals.VisualChromaticityDiagramCIE1976UCS.\ +kwargs_visual_chromaticity_diagram` + + Methods + ------- + - :meth:`~colour_visuals.VisualChromaticityDiagramCIE1976UCS.__init__` + - :meth:`~colour_visuals.VisualChromaticityDiagramCIE1976UCS.update` + Examples -------- >>> import os @@ -660,6 +1013,12 @@ def __init__( ): super().__init__() + if kwargs_visual_spectral_locus is None: + kwargs_visual_spectral_locus = {} + + if kwargs_visual_chromaticity_diagram is None: + kwargs_visual_chromaticity_diagram = {} + self._spectral_locus = VisualSpectralLocus2D( method="CIE 1976 UCS", **(optional(kwargs_visual_spectral_locus, {})), @@ -672,6 +1031,14 @@ def __init__( ) self.add(self._chromaticity_diagram) + self.kwargs_visual_spectral_locus = kwargs_visual_spectral_locus + self.kwargs_visual_chromaticity_diagram = ( + kwargs_visual_chromaticity_diagram + ) + + def update(self): + """Update the visual.""" + if __name__ == "__main__": scene = gfx.Scene() @@ -705,7 +1072,7 @@ def __init__( visual_5.local.position = np.array([4, 0, 0]) scene.add(visual_5) - visual_6 = VisualSpectralLocus2D(colours=[0.5, 0.5, 0.5]) + visual_6 = VisualSpectralLocus2D(colour=[0.5, 0.5, 0.5]) visual_6.local.position = np.array([5, 0, 0]) scene.add(visual_6) diff --git a/colour_visuals/grid.py b/colour_visuals/grid.py index c6a1e58..c7f2503 100644 --- a/colour_visuals/grid.py +++ b/colour_visuals/grid.py @@ -10,6 +10,8 @@ from __future__ import annotations +import math + import numpy as np import pygfx as gfx from colour.geometry import primitive_grid @@ -22,6 +24,7 @@ as_contiguous_array, conform_primitive_dtype, ) +from colour_visuals.visual import MixinPropertySize, Visual, visual_property __author__ = "Colour Developers" __copyright__ = "Copyright 2023 Colour Developers" @@ -33,7 +36,7 @@ __all__ = ["VisualGrid"] -class VisualGrid(gfx.Group): +class VisualGrid(MixinPropertySize, Visual): """ Create a 3D grid. @@ -42,17 +45,32 @@ class VisualGrid(gfx.Group): size Size of the grid. major_grid_colours - Colours of the major grid lines. + Colour of the major grid lines. minor_grid_colours - Colours of the minor grid lines. + Colour of the minor grid lines. major_tick_labels Whether to draw the major tick labels. major_tick_label_colours - Colours of the major tick labels. + Colour of the major tick labels. minor_tick_labels Whether to draw the minor tick labels. minor_tick_label_colours - Colours of the minor tick labels. + Colour of the minor tick labels. + + Attributes + ---------- + - :attr:`~colour_visuals.VisualGrid.size` + - :attr:`~colour_visuals.VisualGrid.major_grid_colours` + - :attr:`~colour_visuals.VisualGrid.minor_grid_colours` + - :attr:`~colour_visuals.VisualGrid.major_tick_labels` + - :attr:`~colour_visuals.VisualGrid.major_tick_label_colours` + - :attr:`~colour_visuals.VisualGrid.minor_tick_labels` + - :attr:`~colour_visuals.VisualGrid.minor_tick_label_colours` + + Methods + ------- + - :meth:`~colour_visuals.VisualGrid.__init__` + - :meth:`~colour_visuals.VisualGrid.update` Examples -------- @@ -82,7 +100,7 @@ class VisualGrid(gfx.Group): def __init__( self, - size: int = 20, + size: float = 20, major_grid_colours: ArrayLike = np.array([0.5, 0.5, 0.5]), minor_grid_colours: ArrayLike = np.array([0.25, 0.25, 0.25]), major_tick_labels=True, @@ -92,7 +110,177 @@ def __init__( ): super().__init__() - size = int(size) + self._major_grid_colours = np.array([0.5, 0.5, 0.5]) + self._minor_grid_colours = np.array([0.25, 0.25, 0.25]) + self._major_tick_labels = True + self._major_tick_label_colours = np.array([0.75, 0.75, 0.75]) + self._minor_tick_labels = True + self._minor_tick_label_colours = np.array([0.5, 0.5, 0.5]) + + with self.block_update(): + self.size = size + self.major_grid_colours = major_grid_colours + self.minor_grid_colours = minor_grid_colours + self.major_tick_labels = major_tick_labels + self.major_tick_label_colours = major_tick_label_colours + self.minor_tick_labels = minor_tick_labels + self.minor_tick_label_colours = minor_tick_label_colours + + self.update() + + @visual_property + def major_grid_colours(self) -> ArrayLike: + """ + Getter and setter property for the major grid colour. + + Parameters + ---------- + value + Value to set the major grid colour with. + + Returns + ------- + ArrayLike + Major grid colour. + """ + + return self._major_grid_colours + + @major_grid_colours.setter + def major_grid_colours(self, value: ArrayLike): + """Setter for the **self.major_grid_colours** property.""" + + self._major_grid_colours = value + + @visual_property + def minor_grid_colours(self) -> ArrayLike: + """ + Getter and setter property for the minor grid colour. + + Parameters + ---------- + value + Value to set the minor grid colour with. + + Returns + ------- + ArrayLike + Major grid colour. + """ + + return self._minor_grid_colours + + @minor_grid_colours.setter + def minor_grid_colours(self, value: ArrayLike): + """Setter for the **self.minor_grid_colours** property.""" + + self._minor_grid_colours = value + + @visual_property + def major_tick_labels(self) -> bool: + """ + Getter and setter property for the major tick labels state. + + Parameters + ---------- + value + Value to set major tick labels state with. + + Returns + ------- + :class:`bool` + Major tick labels state. + """ + + return self._major_tick_labels + + @major_tick_labels.setter + def major_tick_labels(self, value: bool): + """Setter for the **self.major_tick_labels** property.""" + + self._major_tick_labels = value + + @visual_property + def major_tick_label_colours(self) -> ArrayLike: + """ + Getter and setter property for the major tick label colour. + + Parameters + ---------- + value + Value to set the major tick label colour with. + + Returns + ------- + ArrayLike + Major tick label colour. + """ + + return self._major_tick_label_colours + + @major_tick_label_colours.setter + def major_tick_label_colours(self, value: ArrayLike): + """Setter for the **self.major_tick_label_colours** property.""" + + self._major_tick_label_colours = value + + @visual_property + def minor_tick_labels(self) -> bool: + """ + Getter and setter property for the minor tick labels state. + + Parameters + ---------- + value + Value to set minor tick labels state with. + + Returns + ------- + :class:`bool` + Major tick labels state. + """ + + return self._minor_tick_labels + + @minor_tick_labels.setter + def minor_tick_labels(self, value: bool): + """Setter for the **self.minor_tick_labels** property.""" + + self._minor_tick_labels = value + + @visual_property + def minor_tick_label_colours(self) -> ArrayLike: + """ + Getter and setter property for the minor tick label colour. + + Parameters + ---------- + value + Value to set the minor tick label colour with. + + Returns + ------- + ArrayLike + Major tick label colour. + """ + + return self._minor_tick_label_colours + + @minor_tick_label_colours.setter + def minor_tick_label_colours(self, value: ArrayLike): + """Setter for the **self.minor_tick_label_colours** property.""" + + self._minor_tick_label_colours = value + + def update(self): + """Update the visual.""" + + if self._is_update_blocked: + return + + self.clear() + + size = math.ceil(self._size / 2) * 2 vertices, faces, outline = conform_primitive_dtype( primitive_grid( @@ -108,7 +296,10 @@ def __init__( indices=outline[..., 1].reshape([-1, 4]), colors=as_contiguous_array( append_channel( - np.tile(major_grid_colours, (positions.shape[0], 1)), 1 + np.tile( + self._major_grid_colours, (positions.shape[0], 1) + ), + 1, ) ), ), @@ -131,7 +322,10 @@ def __init__( indices=outline[..., 1].reshape([-1, 4]), colors=as_contiguous_array( append_channel( - np.tile(minor_grid_colours, (positions.shape[0], 1)), 1 + np.tile( + self._minor_grid_colours, (positions.shape[0], 1) + ), + 1, ) ), ), @@ -168,7 +362,7 @@ def __init__( ) self.add(self._axes_helper) - if major_tick_labels: + if self._major_tick_labels: self._ticks_major_x, self._ticks_major_y = [], [] for i in np.arange(-size // 2, size // 2 + 1, 1): x_text = gfx.Text( @@ -178,9 +372,7 @@ def __init__( screen_space=True, anchor="Top-Right" if i == 0 else "Top-Center", ), - gfx.TextMaterial( - color=major_tick_label_colours # pyright: ignore - ), + gfx.TextMaterial(color=self._major_tick_label_colours), ) x_text.local.position = np.array([i, 0, 1e-3]) self.add(x_text) @@ -196,15 +388,13 @@ def __init__( screen_space=True, anchor="Center-Right", ), - gfx.TextMaterial( - color=major_tick_label_colours # pyright: ignore - ), + gfx.TextMaterial(color=self._major_tick_label_colours), ) y_text.local.position = np.array([0, i, 1e-3]) self.add(y_text) self._ticks_major_y.append(y_text) - if minor_tick_labels: + if self._minor_tick_labels: self._ticks_minor_x, self._ticks_minor_y = [], [] for i in np.arange(-size // 2, size // 2 + 0.1, 0.1): if np.around(i, 0) == np.around(i, 1): @@ -219,9 +409,7 @@ def __init__( screen_space=True, anchor="Top-Right" if i == 0 else "Top-Center", ), - gfx.TextMaterial( - color=minor_tick_label_colours # pyright: ignore - ), + gfx.TextMaterial(color=self._minor_tick_label_colours), ) x_text.local.position = np.array([i, 0, 1e-3]) self.add(x_text) @@ -237,9 +425,7 @@ def __init__( screen_space=True, anchor="Center-Right", ), - gfx.TextMaterial( - color=minor_tick_label_colours # pyright: ignore - ), + gfx.TextMaterial(color=self._minor_tick_label_colours), ) y_text.local.position = np.array([0, i, 1e-3]) self.add(y_text) diff --git a/colour_visuals/pointer_gamut.py b/colour_visuals/pointer_gamut.py index 7ad3b93..38e1e09 100644 --- a/colour_visuals/pointer_gamut.py +++ b/colour_visuals/pointer_gamut.py @@ -32,6 +32,15 @@ append_channel, as_contiguous_array, ) +from colour_visuals.visual import ( + MixinPropertyColour, + MixinPropertyKwargs, + MixinPropertyMethod, + MixinPropertyModel, + MixinPropertyOpacity, + MixinPropertyThickness, + Visual, +) __author__ = "Colour Developers" __copyright__ = "Copyright 2023 Colour Developers" @@ -43,7 +52,13 @@ __all__ = ["VisualPointerGamut2D", "VisualPointerGamut3D"] -class VisualPointerGamut2D(gfx.Group): +class VisualPointerGamut2D( + MixinPropertyColour, + MixinPropertyMethod, + MixinPropertyOpacity, + MixinPropertyThickness, + Visual, +): """ Create a 2D *Pointer's Gamut* visual. @@ -51,14 +66,26 @@ class VisualPointerGamut2D(gfx.Group): ---------- method *Chromaticity Diagram* method. - colours - Colours of the visual, if *None*, the colours are computed from the - visual geometry. + colour + Colour of the visual, if *None*, the colour is computed from the visual + geometry. opacity Opacity of the visual. thickness Thickness of the visual lines. + Attributes + ---------- + - :attr:`~colour_visuals.VisualPointerGamut2D.method` + - :attr:`~colour_visuals.VisualPointerGamut2D.colour` + - :attr:`~colour_visuals.VisualPointerGamut2D.opacity` + - :attr:`~colour_visuals.VisualPointerGamut2D.thickness` + + Methods + ------- + - :meth:`~colour_visuals.VisualPointerGamut2D.__init__` + - :meth:`~colour_visuals.VisualPointerGamut2D.update` + Examples -------- >>> import os @@ -89,13 +116,32 @@ def __init__( self, method: Literal["CIE 1931", "CIE 1960 UCS", "CIE 1976 UCS"] | str = "CIE 1931", - colours: ArrayLike | None = None, + colour: ArrayLike | None = None, opacity: float = 1, thickness: float = 1, ): super().__init__() - lines_b, lines_v = lines_pointer_gamut(method) + self._pointer_gamut_boundary = None + self._pointer_gamut_volume = None + + with self.block_update(): + self.method = method + self.colour = colour + self.opacity = opacity + self.thickness = thickness + + self.update() + + def update(self): + """Update the visual.""" + + if self._is_update_blocked: + return + + self.clear() + + lines_b, lines_v = lines_pointer_gamut(self._method) # Boundary positions = np.concatenate( @@ -108,19 +154,23 @@ def __init__( ] ) - if colours is None: - colours_b = np.concatenate( + if self._colour is None: + colour_b = np.concatenate( [lines_b["colour"][:-1], lines_b["colour"][1:]], axis=1 ).reshape([-1, 3]) else: - colours_b = np.tile(colours, (positions.shape[0], 1)) + colour_b = np.tile(self._colour, (positions.shape[0], 1)) self._pointer_gamut_boundary = gfx.Line( gfx.Geometry( positions=as_contiguous_array(positions), - colors=as_contiguous_array(append_channel(colours_b, opacity)), + colors=as_contiguous_array( + append_channel(colour_b, self._opacity) + ), + ), + gfx.LineSegmentMaterial( + thickness=self._thickness, color_mode="vertex" ), - gfx.LineSegmentMaterial(thickness=thickness, color_mode="vertex"), ) self.add(self._pointer_gamut_boundary) @@ -136,25 +186,34 @@ def __init__( ] ) - if colours is None: - colours_v = lines_v["colour"] + if self._colour is None: + colour_v = lines_v["colour"] else: - colours_v = np.tile(colours, (lines_v["colour"].shape[0], 1)) + colour_v = np.tile(self._colour, (lines_v["colour"].shape[0], 1)) self._pointer_gamut_volume = gfx.Points( gfx.Geometry( positions=as_contiguous_array(positions), sizes=as_contiguous_array( - np.full(positions.shape[0], thickness * 3) + np.full(positions.shape[0], self._thickness * 3) + ), + colors=as_contiguous_array( + append_channel(colour_v, self._opacity) ), - colors=as_contiguous_array(append_channel(colours_v, opacity)), ), gfx.PointsMaterial(color_mode="vertex", vertex_sizes=True), ) self.add(self._pointer_gamut_volume) -class VisualPointerGamut3D(gfx.Line): +class VisualPointerGamut3D( + MixinPropertyColour, + MixinPropertyKwargs, + MixinPropertyModel, + MixinPropertyOpacity, + MixinPropertyThickness, + Visual, +): """ Create a 3D *Pointer's Gamut* visual. @@ -163,14 +222,26 @@ class VisualPointerGamut3D(gfx.Line): model Colourspace model, see :attr:`colour.COLOURSPACE_MODELS` attribute for the list of supported colourspace models. - colours - Colours of the visual, if *None*, the colours are computed from the - visual geometry. + colour + Colour of the visual, if *None*, the colour is computed from the visual + geometry. opacity Opacity of the visual. thickness Thickness of the visual lines. + Attributes + ---------- + - :attr:`~colour_visuals.VisualPointerGamut3D.model` + - :attr:`~colour_visuals.VisualPointerGamut3D.colour` + - :attr:`~colour_visuals.VisualPointerGamut3D.opacity` + - :attr:`~colour_visuals.VisualPointerGamut3D.thickness` + + Methods + ------- + - :meth:`~colour_visuals.VisualPointerGamut3D.__init__` + - :meth:`~colour_visuals.VisualPointerGamut3D.update` + Other Parameters ---------------- kwargs @@ -205,13 +276,33 @@ class VisualPointerGamut3D(gfx.Line): def __init__( self, model: LiteralColourspaceModel | str = "CIE xyY", - colours: ArrayLike | None = None, + colour: ArrayLike | None = None, opacity: float = 0.5, thickness: float = 1, **kwargs, ): super().__init__() + self._pointer_gamut_boundary = None + self._pointer_gamut_volume = None + + with self.block_update(): + self.model = model + self.colour = colour + self.opacity = opacity + self.thickness = thickness + self.kwargs = kwargs + + self.update() + + def update(self): + """Update the visual.""" + + if self._is_update_blocked: + return + + self.clear() + illuminant = CONSTANTS_COLOUR_STYLE.colour.colourspace.whitepoint data_pointer_gamut = np.reshape( @@ -237,26 +328,31 @@ def __init__( XYZ_to_colourspace_model( sections, CCS_ILLUMINANT_POINTER_GAMUT, - model, - **kwargs, + self._model, + **self._kwargs, ), - model, + self._model, ).reshape([-1, 3]) - if colours is None: - colours = XYZ_to_plotting_colourspace( - sections, illuminant - ).reshape([-1, 3]) + if self._colour is None: + colour = XYZ_to_plotting_colourspace(sections, illuminant).reshape( + [-1, 3] + ) else: - colours = np.tile(colours, (positions.shape[0], 1)) + colour = np.tile(self._colour, (positions.shape[0], 1)) - super().__init__( + self._pointer_gamut = gfx.Line( gfx.Geometry( positions=as_contiguous_array(positions), - colors=as_contiguous_array(append_channel(colours, opacity)), + colors=as_contiguous_array( + append_channel(colour, self._opacity) + ), + ), + gfx.LineSegmentMaterial( + thickness=self._thickness, color_mode="vertex" ), - gfx.LineSegmentMaterial(thickness=thickness, color_mode="vertex"), ) + self.add(self._pointer_gamut) if __name__ == "__main__": @@ -276,7 +372,7 @@ def __init__( visual_2 = VisualPointerGamut2D() scene.add(visual_2) - visual_3 = VisualPointerGamut2D(colours=np.array([0.5, 0.5, 0.5])) + visual_3 = VisualPointerGamut2D(colour=np.array([0.5, 0.5, 0.5])) visual_3.local.position = np.array([1, 0, 0]) scene.add(visual_3) diff --git a/colour_visuals/rgb_colourspace.py b/colour_visuals/rgb_colourspace.py index 5f125e2..5dedf7b 100644 --- a/colour_visuals/rgb_colourspace.py +++ b/colour_visuals/rgb_colourspace.py @@ -22,16 +22,14 @@ LiteralRGBColourspace, Sequence, Type, - cast, ) from colour.models import RGB_Colourspace, RGB_to_XYZ, XYZ_to_RGB, xy_to_XYZ from colour.plotting import ( CONSTANTS_COLOUR_STYLE, METHODS_CHROMATICITY_DIAGRAM, colourspace_model_axis_reorder, - filter_RGB_colourspaces, ) -from colour.utilities import first_item, full +from colour.utilities import full from colour_visuals.common import ( XYZ_to_colourspace_model, @@ -39,6 +37,19 @@ as_contiguous_array, conform_primitive_dtype, ) +from colour_visuals.visual import ( + MixinPropertyColour, + MixinPropertyColourspace, + MixinPropertyKwargs, + MixinPropertyMethod, + MixinPropertyModel, + MixinPropertyOpacity, + MixinPropertySegments, + MixinPropertyThickness, + MixinPropertyTypeMaterial, + MixinPropertyWireframe, + Visual, +) __author__ = "Colour Developers" __copyright__ = "Copyright 2023 Colour Developers" @@ -53,7 +64,14 @@ ] -class VisualRGBColourspace2D(gfx.Group): +class VisualRGBColourspace2D( + MixinPropertyColourspace, + MixinPropertyMethod, + MixinPropertyColour, + MixinPropertyOpacity, + MixinPropertyThickness, + Visual, +): """ Create a 2D *RGB* colourspace gamut visual. @@ -65,14 +83,27 @@ class VisualRGBColourspace2D(gfx.Group): :func:`colour.plotting.common.filter_RGB_colourspaces` definition. method *Chromaticity Diagram* method. - colours - Colours of the visual, if *None*, the colours are computed from the - visual geometry. + colour + Colour of the visual, if *None*, the colour is computed from the visual + geometry. opacity Opacity of the visual. thickness Thickness of the visual lines. + Attributes + ---------- + - :attr:`~colour_visuals.VisualRGBColourspace2D.colourspace` + - :attr:`~colour_visuals.VisualRGBColourspace2D.method` + - :attr:`~colour_visuals.VisualRGBColourspace2D.colour` + - :attr:`~colour_visuals.VisualRGBColourspace2D.opacity` + - :attr:`~colour_visuals.VisualRGBColourspace2D.thickness` + + Methods + ------- + - :meth:`~colour_visuals.VisualRGBColourspace2D.__init__` + - :meth:`~colour_visuals.VisualRGBColourspace2D.update` + Examples -------- >>> import os @@ -106,23 +137,39 @@ def __init__( | Sequence[RGB_Colourspace | LiteralRGBColourspace | str] = "sRGB", method: Literal["CIE 1931", "CIE 1960 UCS", "CIE 1976 UCS"] | str = "CIE 1931", - colours: ArrayLike | None = None, + colour: ArrayLike | None = None, opacity: float = 1, thickness: float = 1, ): super().__init__() - colourspace = cast( - RGB_Colourspace, - first_item(filter_RGB_colourspaces(colourspace).values()), - ) + self._gamut = None + self._whitepoint = None + + with self.block_update(): + self.colourspace = colourspace + self.method = method + self.colour = colour + self.opacity = opacity + self.thickness = thickness + + self.update() + + def update(self): + """Update the visual.""" + + if self._is_update_blocked: + return + + self.clear() plotting_colourspace = CONSTANTS_COLOUR_STYLE.colour.colourspace - XYZ_to_ij = METHODS_CHROMATICITY_DIAGRAM[method]["XYZ_to_ij"] + XYZ_to_ij = METHODS_CHROMATICITY_DIAGRAM[self._method]["XYZ_to_ij"] ij = XYZ_to_ij( - xy_to_XYZ(colourspace.primaries), plotting_colourspace.whitepoint + xy_to_XYZ(self._colourspace.primaries), + plotting_colourspace.whitepoint, ) ij[np.isnan(ij)] = 0 @@ -130,52 +177,70 @@ def __init__( np.array([ij[0], ij[1], ij[1], ij[2], ij[2], ij[0]]), 0 ) - if colours is None: + if self._colour is None: RGB = XYZ_to_RGB( - xy_to_XYZ(colourspace.primaries), plotting_colourspace + xy_to_XYZ(self._colourspace.primaries), plotting_colourspace ) - colours_g = np.array( + colour_g = np.array( [RGB[0], RGB[1], RGB[1], RGB[2], RGB[2], RGB[0]] ) else: - colours_g = np.tile(colours, (positions.shape[0], 1)) + colour_g = np.tile(self._colour, (positions.shape[0], 1)) self._gamut = gfx.Line( gfx.Geometry( positions=as_contiguous_array(positions), - colors=as_contiguous_array(append_channel(colours_g, opacity)), + colors=as_contiguous_array( + append_channel(colour_g, self._opacity) + ), + ), + gfx.LineSegmentMaterial( + thickness=self._thickness, color_mode="vertex" ), - gfx.LineSegmentMaterial(thickness=thickness, color_mode="vertex"), ) self.add(self._gamut) ij = XYZ_to_ij( - xy_to_XYZ(colourspace.whitepoint), plotting_colourspace.whitepoint + xy_to_XYZ(self._colourspace.whitepoint), + plotting_colourspace.whitepoint, ) positions = append_channel(ij, 0).reshape([-1, 3]) - if colours is None: - colours_w = XYZ_to_RGB( - xy_to_XYZ(colourspace.whitepoint), plotting_colourspace + if self._colour is None: + colour_w = XYZ_to_RGB( + xy_to_XYZ(self._colourspace.whitepoint), plotting_colourspace ).reshape([-1, 3]) else: - colours_w = np.tile(colours, (positions.shape[0], 1)) + colour_w = np.tile(self._colour, (positions.shape[0], 1)) self._whitepoint = gfx.Points( gfx.Geometry( positions=as_contiguous_array(positions), sizes=as_contiguous_array( - full(positions.shape[0], thickness * 3) + full(positions.shape[0], self._thickness * 3) + ), + colors=as_contiguous_array( + append_channel(colour_w, self._opacity) ), - colors=as_contiguous_array(append_channel(colours_w, opacity)), ), gfx.PointsMaterial(color_mode="vertex", vertex_sizes=True), ) self.add(self._whitepoint) -class VisualRGBColourspace3D(gfx.Mesh): +class VisualRGBColourspace3D( + MixinPropertyColourspace, + MixinPropertyModel, + MixinPropertyColour, + MixinPropertyOpacity, + MixinPropertyThickness, + MixinPropertyTypeMaterial, + MixinPropertyWireframe, + MixinPropertySegments, + MixinPropertyKwargs, + Visual, +): """ Create a 3D *RGB* colourspace volume visual. @@ -188,15 +253,15 @@ class VisualRGBColourspace3D(gfx.Mesh): model Colourspace model, see :attr:`colour.COLOURSPACE_MODELS` attribute for the list of supported colourspace models. - colours - Colours of the visual, if *None*, the colours are computed from the - visual geometry. + colour + Colour of the visual, if *None*, the colour is computed from the visual + geometry. opacity Opacity of the visual. thickness Thickness of the visual lines. material - Material used to surface the visual geomeetry. + Material used to surface the visual geometry. wireframe Whether to render the visual as a wireframe, i.e., only render edges. segments @@ -207,6 +272,22 @@ class VisualRGBColourspace3D(gfx.Mesh): kwargs See the documentation of the supported conversion definitions. + Attributes + ---------- + - :attr:`~colour_visuals.VisualRGBColourspace3D.colourspace` + - :attr:`~colour_visuals.VisualRGBColourspace3D.model` + - :attr:`~colour_visuals.VisualRGBColourspace3D.colour` + - :attr:`~colour_visuals.VisualRGBColourspace3D.opacity` + - :attr:`~colour_visuals.VisualRGBColourspace3D.thickness` + - :attr:`~colour_visuals.VisualRGBColourspace3D.type_material` + - :attr:`~colour_visuals.VisualRGBColourspace3D.wireframe` + - :attr:`~colour_visuals.VisualRGBColourspace3D.segments` + + Methods + ------- + - :meth:`~colour_visuals.VisualRGBColourspace3D.__init__` + - :meth:`~colour_visuals.VisualRGBColourspace3D.update` + Examples -------- >>> import os @@ -243,23 +324,43 @@ def __init__( | str | Sequence[RGB_Colourspace | LiteralRGBColourspace | str] = "sRGB", model: LiteralColourspaceModel | str = "CIE xyY", - colours: ArrayLike | None = None, + colour: ArrayLike | None = None, opacity: float = 1, material: Type[gfx.MeshAbstractMaterial] = gfx.MeshBasicMaterial, wireframe: bool = False, segments: int = 16, **kwargs, ): - colourspace = cast( - RGB_Colourspace, - first_item(filter_RGB_colourspaces(colourspace).values()), - ) + super().__init__() + + self._gamut = None + self._whitepoint = None + + with self.block_update(): + self.colourspace = colourspace + self.model = model + self.colour = colour + self.opacity = opacity + self.type_material = material + self.wireframe = wireframe + self.segments = segments + self.kwargs = kwargs + + self.update() + + def update(self): + """Update the visual.""" + + if self._is_update_blocked: + return + + self.clear() vertices, faces, outline = conform_primitive_dtype( primitive_cube( - width_segments=segments, - height_segments=segments, - depth_segments=segments, + width_segments=self._segments, + height_segments=self._segments, + depth_segments=self._segments, ) ) @@ -267,32 +368,35 @@ def __init__( positions[positions == 0] = EPSILON - if colours is None: - colours = positions + if self._colour is None: + colour = positions else: - colours = np.tile(colours, (positions.shape[0], 1)) + colour = np.tile(self._colour, (positions.shape[0], 1)) positions = colourspace_model_axis_reorder( XYZ_to_colourspace_model( - RGB_to_XYZ(positions, colourspace), - colourspace.whitepoint, - model, - **kwargs, + RGB_to_XYZ(positions, self._colourspace), + self._colourspace.whitepoint, + self._model, + **self._kwargs, ), - model, + self._model, ) - super().__init__( + self._gamut = gfx.Mesh( gfx.Geometry( positions=as_contiguous_array(positions), normals=vertices["normal"], indices=outline[..., 1].reshape([-1, 4]), - colors=as_contiguous_array(append_channel(colours, opacity)), + colors=as_contiguous_array( + append_channel(colour, self._opacity) + ), ), - material(color_mode="vertex", wireframe=wireframe) - if wireframe - else material(color_mode="vertex"), + self._type_material(color_mode="vertex", wireframe=self._wireframe) + if self._wireframe + else self._type_material(color_mode="vertex"), ) + self.add(self._gamut) if __name__ == "__main__": @@ -324,7 +428,7 @@ def __init__( visual_4 = VisualRGBColourspace3D( model="CIE Lab", - colours=np.array([0.5, 0.5, 0.5]), + colour=np.array([0.5, 0.5, 0.5]), opacity=1, material=gfx.MeshStandardMaterial, ) @@ -337,7 +441,7 @@ def __init__( visual_6 = VisualRGBColourspace2D( method="CIE 1976 UCS", - colours=np.array([0.5, 0.5, 0.5]), + colour=np.array([0.5, 0.5, 0.5]), opacity=1, ) visual_6.local.position = np.array([4.5, 0, 0]) diff --git a/colour_visuals/rgb_scatter.py b/colour_visuals/rgb_scatter.py index 06659f1..c49fe16 100644 --- a/colour_visuals/rgb_scatter.py +++ b/colour_visuals/rgb_scatter.py @@ -18,21 +18,30 @@ ArrayLike, LiteralColourspaceModel, LiteralRGBColourspace, + NDArray, Sequence, - cast, ) from colour.models import RGB_Colourspace from colour.plotting import ( colourspace_model_axis_reorder, - filter_RGB_colourspaces, ) -from colour.utilities import as_float_array, first_item +from colour.utilities import as_float_array from colour_visuals.common import ( XYZ_to_colourspace_model, append_channel, as_contiguous_array, ) +from colour_visuals.visual import ( + MixinPropertyColour, + MixinPropertyColourspace, + MixinPropertyKwargs, + MixinPropertyModel, + MixinPropertyOpacity, + MixinPropertySize, + Visual, + visual_property, +) __author__ = "Colour Developers" __copyright__ = "Copyright 2023 Colour Developers" @@ -44,7 +53,15 @@ __all__ = ["VisualRGBScatter3D"] -class VisualRGBScatter3D(gfx.Points): +class VisualRGBScatter3D( + MixinPropertyColour, + MixinPropertyColourspace, + MixinPropertyKwargs, + MixinPropertyModel, + MixinPropertyOpacity, + MixinPropertySize, + Visual, +): """ Create a 3D *RGB* scatter visual. @@ -59,9 +76,9 @@ class VisualRGBScatter3D(gfx.Points): model Colourspace model, see :attr:`colour.COLOURSPACE_MODELS` attribute for the list of supported colourspace models. - colours - Colours of the visual, if *None*, the colours are computed from the - visual geometry. + colour + Colour of the visual, if *None*, the colour is computed from the visual + geometry. opacity Opacity of the visual. size @@ -72,6 +89,21 @@ class VisualRGBScatter3D(gfx.Points): kwargs See the documentation of the supported conversion definitions. + Attributes + ---------- + - :attr:`~colour_visuals.VisualRGBScatter3D.RGB` + - :attr:`~colour_visuals.VisualRGBScatter3D.colourspace` + - :attr:`~colour_visuals.VisualRGBScatter3D.model` + - :attr:`~colour_visuals.VisualRGBScatter3D.colour` + - :attr:`~colour_visuals.VisualRGBScatter3D.opacity` + - :attr:`~colour_visuals.VisualRGBScatter3D.size` + - :attr:`~colour_visuals.VisualRGBScatter3D.kwargs` + + Methods + ------- + - :meth:`~colour_visuals.VisualRGBScatter3D.__init__` + - :meth:`~colour_visuals.VisualRGBScatter3D.update` + Examples -------- >>> import os @@ -109,45 +141,85 @@ def __init__( | str | Sequence[RGB_Colourspace | LiteralRGBColourspace | str] = "sRGB", model: LiteralColourspaceModel | str = "CIE xyY", - colours: ArrayLike | None = None, + colour: ArrayLike | None = None, opacity: float = 1, size: float = 2, **kwargs, ): - colourspace = cast( - RGB_Colourspace, - first_item(filter_RGB_colourspaces(colourspace).values()), - ) + super().__init__() + + self._RGB = np.array([]) + self._scatter = None + + with self.block_update(): + self.RGB = RGB + self.colourspace = colourspace + self.model = model + self.colour = colour + self.opacity = opacity + self.size = size + self.kwargs = kwargs + + self.update() + + @visual_property + def RGB(self) -> NDArray: + """ + Getter and setter property for the *RGB* colourspace array. + + Parameters + ---------- + value + Value to set the *RGB* colourspace array with. + + Returns + ------- + :class:`numpy.ndarray` + *RGB* colourspace array. + """ + + return self._RGB + + @RGB.setter + def RGB(self, value: ArrayLike): + """Setter for the **self.RGB** property.""" - RGB = as_float_array(RGB).reshape(-1, 3) + self._RGB = as_float_array(value).reshape(-1, 3) + self._RGB[self._RGB == 0] = EPSILON - RGB[RGB == 0] = EPSILON + def update(self): + """Update the visual.""" - XYZ = RGB_to_XYZ(RGB, colourspace) + XYZ = RGB_to_XYZ(self._RGB, self._colourspace) positions = colourspace_model_axis_reorder( XYZ_to_colourspace_model( XYZ, - colourspace.whitepoint, - model, - **kwargs, + self._colourspace.whitepoint, + self._model, + **self._kwargs, ), - model, + self._model, ) - if colours is None: # noqa: SIM108 - colours = RGB + if self._colour is None: + colour = self._RGB else: - colours = np.tile(colours, (RGB.shape[0], 1)) + colour = np.tile(self._colour, (self._RGB.shape[0], 1)) - super().__init__( + self._scatter = gfx.Points( gfx.Geometry( positions=as_contiguous_array(positions), - sizes=as_contiguous_array(np.full(positions.shape[0], size)), - colors=as_contiguous_array(append_channel(colours, opacity)), + sizes=as_contiguous_array( + np.full(positions.shape[0], self._size) + ), + colors=as_contiguous_array( + append_channel(colour, self._opacity) + ), ), gfx.PointsMaterial(color_mode="vertex", vertex_sizes=True), ) + self.add(self._scatter) if __name__ == "__main__": @@ -168,7 +240,7 @@ def __init__( scene.add(visual_2) visual_3 = VisualRGBScatter3D( - np.random.random((64, 64, 3)), colours=np.array([0.5, 0.5, 0.5]) + np.random.random((64, 64, 3)), colour=np.array([0.5, 0.5, 0.5]) ) visual_3.local.position = np.array([0.5, 0, 0]) scene.add(visual_3) diff --git a/colour_visuals/rosch_macadam.py b/colour_visuals/rosch_macadam.py index 012a3e3..3f033dc 100644 --- a/colour_visuals/rosch_macadam.py +++ b/colour_visuals/rosch_macadam.py @@ -18,16 +18,11 @@ SpectralShape, ) from colour.constants import EPSILON -from colour.hints import ArrayLike, LiteralColourspaceModel, Sequence, cast +from colour.hints import ArrayLike, LiteralColourspaceModel, Sequence from colour.models import XYZ_to_RGB from colour.plotting import ( CONSTANTS_COLOUR_STYLE, colourspace_model_axis_reorder, - filter_cmfs, - filter_illuminants, -) -from colour.utilities import ( - first_item, ) from colour.volume import XYZ_outer_surface @@ -36,6 +31,16 @@ append_channel, as_contiguous_array, ) +from colour_visuals.visual import ( + MixinPropertyCMFS, + MixinPropertyColour, + MixinPropertyIlluminant, + MixinPropertyKwargs, + MixinPropertyModel, + MixinPropertyOpacity, + MixinPropertyThickness, + Visual, +) __author__ = "Colour Developers" __copyright__ = "Copyright 2023 Colour Developers" @@ -49,7 +54,16 @@ ] -class VisualRoschMacAdam(gfx.Line): +class VisualRoschMacAdam( + MixinPropertyCMFS, + MixinPropertyIlluminant, + MixinPropertyKwargs, + MixinPropertyModel, + MixinPropertyColour, + MixinPropertyOpacity, + MixinPropertyThickness, + Visual, +): """ Create a *Ròˆsch-MacAdam* visual. @@ -67,9 +81,9 @@ class VisualRoschMacAdam(gfx.Line): model Colourspace model, see :attr:`colour.COLOURSPACE_MODELS` attribute for the list of supported colourspace models. - colours - Colours of the visual, if *None*, the colours are computed from the - visual geometry. + colour + Colour of the visual, if *None*, the colour is computed from the visual + geometry. opacity Opacity of the visual. thickness @@ -80,6 +94,21 @@ class VisualRoschMacAdam(gfx.Line): kwargs See the documentation of the supported conversion definitions. + Attributes + ---------- + - :attr:`~colour_visuals.VisualRoschMacAdam.cmfs` + - :attr:`~colour_visuals.VisualRoschMacAdam.illuminant` + - :attr:`~colour_visuals.VisualRoschMacAdam.model` + - :attr:`~colour_visuals.VisualRoschMacAdam.colour` + - :attr:`~colour_visuals.VisualRoschMacAdam.opacity` + - :attr:`~colour_visuals.VisualRoschMacAdam.thickness` + - :attr:`~colour_visuals.VisualRoschMacAdam.kwargs` + + Methods + ------- + - :meth:`~colour_visuals.VisualRoschMacAdam.__init__` + - :meth:`~colour_visuals.VisualRoschMacAdam.update` + Examples -------- >>> import os @@ -121,28 +150,36 @@ def __init__( | str | Sequence[SpectralDistribution | str] = "E", model: LiteralColourspaceModel | str = "CIE xyY", - colours: ArrayLike | None = None, + colour: ArrayLike | None = None, opacity: float = 1, thickness: float = 1, **kwargs, ): super().__init__() - cmfs = cast( - MultiSpectralDistributions, first_item(filter_cmfs(cmfs).values()) - ) - illuminant = cast( - SpectralDistribution, - first_item(filter_illuminants(illuminant).values()), - ) + self._solid = None + + with self.block_update(): + self.cmfs = cmfs + self.illuminant = illuminant + self.model = model + self.colour = colour + self.opacity = opacity + self.thickness = thickness + self.kwargs = kwargs + + self.update() + + def update(self): + """Update the visual.""" colourspace = CONSTANTS_COLOUR_STYLE.colour.colourspace XYZ = XYZ_outer_surface( - cmfs.copy().align( - SpectralShape(cmfs.shape.start, cmfs.shape.end, 5) + self._cmfs.copy().align( + SpectralShape(self._cmfs.shape.start, self._cmfs.shape.end, 5) ), - illuminant, + self._illuminant, ) XYZ[XYZ == 0] = EPSILON @@ -151,30 +188,35 @@ def __init__( XYZ_to_colourspace_model( XYZ, colourspace.whitepoint, - model, - **kwargs, + self._model, + **self._kwargs, ), - model, + self._model, ) positions = np.concatenate( [positions[:-1], positions[1:]], axis=1 ).reshape([-1, 3]) - if colours is None: - colours = XYZ_to_RGB(XYZ, colourspace) - colours = np.concatenate( - [colours[:-1], colours[1:]], axis=1 - ).reshape([-1, 3]) + if self._colour is None: + colour = XYZ_to_RGB(XYZ, colourspace) + colour = np.concatenate([colour[:-1], colour[1:]], axis=1).reshape( + [-1, 3] + ) else: - colours = np.tile(colours, (positions.shape[0], 1)) + colour = np.tile(self._colour, (positions.shape[0], 1)) - super().__init__( + self._solid = gfx.Line( gfx.Geometry( positions=as_contiguous_array(positions), - colors=as_contiguous_array(append_channel(colours, opacity)), + colors=as_contiguous_array( + append_channel(colour, self._opacity) + ), + ), + gfx.LineSegmentMaterial( + thickness=self._thickness, color_mode="vertex" ), - gfx.LineSegmentMaterial(thickness=thickness, color_mode="vertex"), ) + self.add(self._solid) if __name__ == "__main__": @@ -190,19 +232,19 @@ def __init__( scene.add(visual_1) visual_2 = VisualRoschMacAdam( - model="CIE XYZ", colours=np.array([0.5, 0.5, 0.5]) + model="CIE XYZ", colour=np.array([0.5, 0.5, 0.5]) ) visual_2.local.position = np.array([1, 0, 0]) scene.add(visual_2) visual_3 = VisualRoschMacAdam( - model="JzAzBz", colours=np.array([0.5, 0.5, 0.5]) + model="JzAzBz", colour=np.array([0.5, 0.5, 0.5]) ) visual_3.local.position = np.array([3.5, 0, 0]) scene.add(visual_3) visual_4 = VisualRoschMacAdam( - model="ICtCp", colours=np.array([0.5, 0.5, 0.5]) + model="ICtCp", colour=np.array([0.5, 0.5, 0.5]) ) visual_4.local.position = np.array([6, 0, 0]) scene.add(visual_4) diff --git a/colour_visuals/visual.py b/colour_visuals/visual.py new file mode 100644 index 0000000..689356c --- /dev/null +++ b/colour_visuals/visual.py @@ -0,0 +1,708 @@ +""" +Visual Utilities +================ + +Defines the visual utilities. +""" + +from __future__ import annotations + +from abc import ABCMeta, abstractmethod +from contextlib import contextmanager + +import pygfx as gfx +from colour.colorimetry import ( + MSDS_CMFS, + SDS_ILLUMINANTS, + MultiSpectralDistributions, + SpectralDistribution, +) +from colour.hints import ( + ArrayLike, + Generator, + Literal, + LiteralColourspaceModel, + LiteralRGBColourspace, + Sequence, + Type, + cast, +) +from colour.models import ( + COLOURSPACE_MODELS, + RGB_Colourspace, + RGB_COLOURSPACE_sRGB, +) +from colour.plotting import ( + METHODS_CHROMATICITY_DIAGRAM, + filter_cmfs, + filter_illuminants, + filter_RGB_colourspaces, +) +from colour.utilities import first_item, validate_method + +__author__ = "Colour Developers" +__copyright__ = "Copyright 2023 Colour Developers" +__license__ = "BSD-3-Clause - https://opensource.org/licenses/BSD-3-Clause" +__maintainer__ = "Colour Developers" +__email__ = "colour-developers@colour-science.org" +__status__ = "Production" + +__all__ = [ + "visual_property", + "Visual", + "MixinPropertyCMFS", + "MixinPropertyColour", + "MixinPropertyColourspace", + "MixinPropertyIlluminant", + "MixinPropertyKwargs", + "MixinPropertyTypeMaterial", + "MixinPropertyMethod", + "MixinPropertyModel", + "MixinPropertyOpacity", + "MixinPropertySamples", + "MixinPropertySegments", + "MixinPropertySize", + "MixinPropertyThickness", + "MixinPropertyWireframe", +] + + +class visual_property(property): + """ + Define a :class:`property` sub-class calling the + :class:`colour_visuals.Visual.update` method. + """ + + def __set__(self, obj, value): + """Reimplement the :class:`property.__set__` method.""" + super().__set__(obj, value) + + obj.update() + + +class Visual(gfx.Group, metaclass=ABCMeta): + """Define the base class for the visuals.""" + + def __init__(self): + self._is_update_blocked = False + + super().__init__() + + @contextmanager + def block_update(self) -> Generator: + """Define a context manager that blocks the visual updates.""" + self._is_update_blocked = True + + yield + + self._is_update_blocked = False + + +class MixinPropertyCMFS: + """ + Define a mixin for a standard observer colour matching functions, + default to the *CIE 1931 2 Degree Standard Observer*. + + Attributes + ---------- + - :attr:`~colour_visuals.visual.MixinPropertyCMFS.cmfs` + """ + + def __init__(self): + self._cmfs = MSDS_CMFS["CIE 1931 2 Degree Standard Observer"] + + super().__init__() + + @visual_property + def cmfs( + self, + ) -> ( + MultiSpectralDistributions + | str + | Sequence[MultiSpectralDistributions | str] + ): + """ + Getter and setter property for the standard observer colour matching + functions. + + Parameters + ---------- + value + Value to set the standard observer colour matching functions with. + + Returns + ------- + :class:`colour.MultiSpectralDistributions` or :class:`str` or \ +:class:`Sequence` + Standard observer colour matching functions. + """ + + return self._cmfs + + @cmfs.setter + def cmfs( + self, + value: MultiSpectralDistributions + | str + | Sequence[MultiSpectralDistributions | str], + ): + """Setter for the **self.cmfs** property.""" + + self._cmfs = cast( + MultiSpectralDistributions, + first_item(filter_cmfs(value).values()), + ) + + +class MixinPropertyColour: + """ + Define a mixin for a colour. + + Attributes + ---------- + - :attr:`~colour_visuals.visual.MixinPropertyColour.colour` + """ + + def __init__(self): + self._colour = None + + super().__init__() + + @visual_property + def colour(self) -> ArrayLike | None: + """ + Getter and setter property for the colour. + + Parameters + ---------- + value + Value to set the colour with. + + Returns + ------- + ArrayLike or None + Visual colour. + """ + + return self._colour + + @colour.setter + def colour(self, value: ArrayLike | None): + """Setter for the **self.colour** property.""" + + self._colour = value + + +class MixinPropertyColourspace: + """ + Define a mixin for a *RGB* colourspace. + + Attributes + ---------- + - :attr:`~colour_visuals.visual.MixinPropertyColour.colour` + """ + + def __init__(self): + self._colourspace = RGB_COLOURSPACE_sRGB + + super().__init__() + + @visual_property + def colourspace( + self, + ) -> ( + RGB_Colourspace + | LiteralRGBColourspace + | str + | Sequence[RGB_Colourspace | LiteralRGBColourspace | str] + ): + """ + Getter and setter property for the *RGB* colourspace. + + Parameters + ---------- + value + Value to set the *RGB* colourspace with. + + Returns + ------- + :class:`colour.RGB_Colourspace` or :class:`str` or :class:`Sequence` + Colourspace. + """ + + return self._colourspace + + @colourspace.setter + def colourspace( + self, + value: ( + RGB_Colourspace + | LiteralRGBColourspace + | str + | Sequence[RGB_Colourspace | LiteralRGBColourspace | str] + ), + ): + """Setter for the **self.colourspace** property.""" + + self._colourspace = cast( + RGB_Colourspace, + first_item(filter_RGB_colourspaces(value).values()), + ) + + +class MixinPropertyIlluminant: + """ + Define a mixin for an illuminant spectral distribution. + + Attributes + ---------- + - :attr:`~colour_visuals.visual.MixinPropertyIlluminant.illuminant` + """ + + def __init__(self): + self._illuminant = SDS_ILLUMINANTS["E"] + + super().__init__() + + @visual_property + def illuminant( + self, + ) -> SpectralDistribution | str | Sequence[SpectralDistribution | str]: + """ + Getter and setter property for the illuminant spectral distribution. + + Parameters + ---------- + value + Value to set the illuminant spectral distribution with. + + Returns + ------- + :class:`colour.SpectralDistribution` or :class:`str` or \ +:class:`Sequence` + Illuminant spectral distribution. + """ + + return self._illuminant + + @illuminant.setter + def illuminant( + self, + value: ( + SpectralDistribution | str | Sequence[SpectralDistribution | str] + ) = "E", + ): + """Setter for the **self.illuminant** property.""" + + self._illuminant = cast( + SpectralDistribution, + first_item(filter_illuminants(value).values()), + ) + + +class MixinPropertyKwargs: + """ + Define a mixin for keyword arguments. + + Attributes + ---------- + - :attr:`~colour_visuals.visual.MixinPropertyKwargs.kwargs` + """ + + def __init__(self): + self._kwargs = {} + + super().__init__() + + @visual_property + def kwargs(self) -> dict: + """ + Getter and setter property for the keyword arguments. + + Parameters + ---------- + value + Value to set keyword arguments with. + + Returns + ------- + :class:`dict` + Keyword arguments. + """ + + return self._kwargs + + @kwargs.setter + def kwargs(self, value: dict): + """Setter for the **self.kwargs** property.""" + + self._kwargs = value + + +class MixinPropertyTypeMaterial: + """ + Define a mixin for a material type. + + Attributes + ---------- + - :attr:`~colour_visuals.visual.MixinPropertyTypeMaterial.type_material` + """ + + def __init__(self): + self._type_material = gfx.MeshBasicMaterial + + super().__init__() + + @visual_property + def type_material( + self, + ) -> Type[gfx.MeshAbstractMaterial]: + """ + Getter and setter property for the material type. + + Parameters + ---------- + value + Value to set the material type with. + + Returns + ------- + :class:`gfx.MeshAbstractMaterial` + Material type. + """ + + return self._type_material + + @type_material.setter + def type_material(self, value: Type[gfx.MeshAbstractMaterial]): + """Setter for the **self.material** property.""" + + self._type_material = value + + +class MixinPropertyMethod: + """ + Define a mixin for a *Chromaticity Diagram* method. + + Attributes + ---------- + - :attr:`~colour_visuals.visual.MixinPropertyMethod.method` + """ + + def __init__(self): + self._method = "CIE 1931" + + super().__init__() + + @visual_property + def method( + self, + ) -> Literal["CIE 1931", "CIE 1960 UCS", "CIE 1976 UCS"] | str: + """ + Getter and setter property for the *Chromaticity Diagram* method. + + Parameters + ---------- + value + Value to set the *Chromaticity Diagram* method with. + + Returns + ------- + :class:`str` + *Chromaticity Diagram* method. + """ + + return self._method + + @method.setter + def method( + self, value: Literal["CIE 1931", "CIE 1960 UCS", "CIE 1976 UCS"] | str + ): + """Setter for the **self.method** property.""" + + self._method = validate_method( + value, tuple(METHODS_CHROMATICITY_DIAGRAM) + ) + + +class MixinPropertyModel: + """ + Define a mixin for a colourspace model. + + Attributes + ---------- + - :attr:`~colour_visuals.visual.MixinPropertyModel.model` + """ + + def __init__(self): + self._model = "CIE xyY" + + super().__init__() + + @visual_property + def model(self) -> LiteralColourspaceModel | str: + """ + Getter and setter property for the colourspace model. + + Parameters + ---------- + value + Value to set the colourspace model with. + + Returns + ------- + :class:`str` + Colourspace model. + """ + + return self._model + + @model.setter + def model(self, value: LiteralColourspaceModel | str): + """Setter for the **self.model** property.""" + + self._model = validate_method(value, tuple(COLOURSPACE_MODELS)) + + @abstractmethod + def update(self): + """ + Update the visual. + + Notes + ----- + - Must be reimplemented by sub-classes. + """ + + +class MixinPropertyOpacity: + """ + Define a mixin for an opacity value. + + Attributes + ---------- + - :attr:`~colour_visuals.visual.MixinPropertyOpacity.opacity` + """ + + def __init__(self): + self._opacity = 1 + + super().__init__() + + @visual_property + def opacity(self) -> float: + """ + Getter and setter property for the opacity value. + + Parameters + ---------- + value + Value to set the opacity value with. + + Returns + ------- + :class:`float` + Visual opacity. + """ + + return self._opacity + + @opacity.setter + def opacity(self, value: float): + """Setter for the **self.opacity** property.""" + + self._opacity = value + + +class MixinPropertySamples: + """ + Define a mixin for a sample count. + + Attributes + ---------- + - :attr:`~colour_visuals.visual.MixinPropertySamples.samples` + """ + + def __init__(self): + self._samples = 1 + + super().__init__() + + @visual_property + def samples(self) -> int: + """ + Getter and setter property for the sample count. + + Parameters + ---------- + value + Value to set sample count with. + + Returns + ------- + :class:`int` + Samples count. + """ + + return self._samples + + @samples.setter + def samples(self, value: int): + """Setter for the **self.samples** property.""" + + self._samples = value + + +class MixinPropertySegments: + """ + Define a mixin for a segment count. + + Attributes + ---------- + - :attr:`~colour_visuals.visual.MixinPropertySegments.segments` + """ + + def __init__(self): + self._segments = 16 + + super().__init__() + + @visual_property + def segments(self) -> int: + """ + Getter and setter property for the segment count. + + Parameters + ---------- + value + Value to set segment count with. + + Returns + ------- + :class:`int` + Samples count. + """ + + return self._segments + + @segments.setter + def segments(self, value: int): + """Setter for the **self.segments** property.""" + + self._segments = value + + +class MixinPropertySize: + """ + Define a mixin for a size value. + + Attributes + ---------- + - :attr:`~colour_visuals.visual.MixinPropertySize.size` + """ + + def __init__(self): + self._size = 1 + + super().__init__() + + @visual_property + def size(self) -> float: + """ + Getter and setter property for the size value. + + Parameters + ---------- + value + Value to set size value with. + + Returns + ------- + :class:`int` + Size value. + """ + + return self._size + + @size.setter + def size(self, value: float): + """Setter for the **self.size** property.""" + + self._size = value + + +class MixinPropertyThickness: + """ + Define a mixin for a thickness value. + + Attributes + ---------- + - :attr:`~colour_visuals.visual.MixinPropertyThickness.thickness` + """ + + def __init__(self): + self._thickness = 1 + + super().__init__() + + @visual_property + def thickness(self) -> float: + """ + Getter and setter property for the thickness value. + + Parameters + ---------- + value + Value to set the thickness value with. + + Returns + ------- + :class:`float` + Thickness value. + """ + + return self._thickness + + @thickness.setter + def thickness(self, value: float): + """Setter for the **self.thickness** property.""" + + self._thickness = value + + +class MixinPropertyWireframe: + """ + Define a mixin for a wireframe state. + + Attributes + ---------- + - :attr:`~colour_visuals.visual.MixinPropertyWireframe.wireframe` + """ + + def __init__(self): + self._wireframe = False + + super().__init__() + + @visual_property + def wireframe(self) -> bool: + """ + Getter and setter property for the wireframe state. + + Parameters + ---------- + value + Value to set wireframe state with. + + Returns + ------- + :class:`bool` + Wireframe state. + """ + + return self._wireframe + + @wireframe.setter + def wireframe(self, value: bool): + """Setter for the **self.wireframe** property.""" + + self._wireframe = value diff --git a/docs/colour_visuals.rst b/docs/colour_visuals.rst index 4a286aa..37e7473 100644 --- a/docs/colour_visuals.rst +++ b/docs/colour_visuals.rst @@ -102,6 +102,34 @@ Patterns Visuals pattern_hue_stripes pattern_colour_wheel +Visual Utilities +---------------- + +``colour_visuals.visual`` + +.. currentmodule:: colour_visuals.visual + +.. autosummary:: + :toctree: generated/ + :template: class.rst + + visual_property + Visual + MixinPropertyCMFS + MixinPropertyColour + MixinPropertyColourspace + MixinPropertyIlluminant + MixinPropertyKwargs + MixinPropertyTypeMaterial + MixinPropertyMethod + MixinPropertyModel + MixinPropertyOpacity + MixinPropertySamples + MixinPropertySegments + MixinPropertySize + MixinPropertyThickness + MixinPropertyWireframe + Common Utilities ---------------- diff --git a/utilities/generate_plots.py b/utilities/generate_plots.py index 7fdc148..94a6873 100755 --- a/utilities/generate_plots.py +++ b/utilities/generate_plots.py @@ -168,9 +168,7 @@ def generate_documentation_plots(output_directory: str): kwargs_visual_chromaticity_diagram={"opacity": 0.25} ), VisualRGBColourspace2D("ACEScg"), - VisualRGBColourspace2D( - "Display P3", colours=np.array([0.5, 0.5, 0.5]) - ), + VisualRGBColourspace2D("Display P3", colour=np.array([0.5, 0.5, 0.5])), VisualRGBColourspace3D("Display P3", opacity=0.5, wireframe=True), VisualRGBScatter3D(np.random.random([24, 32, 3]), "ACEScg"), ] @@ -195,9 +193,7 @@ def generate_documentation_plots(output_directory: str): VisualSpectralLocus2D(), VisualSpectralLocus3D(), VisualRGBColourspace2D("ACEScg"), - VisualRGBColourspace2D( - "Display P3", colours=np.array([0.5, 0.5, 0.5]) - ), + VisualRGBColourspace2D("Display P3", colour=np.array([0.5, 0.5, 0.5])), VisualPointerGamut3D(), VisualRGBScatter3D(np.random.random([24, 32, 3]), "ACEScg"), ]