Skip to content

Commit 9052b90

Browse files
authored
Merge pull request #982 from compas-dev/use-colors
Cross platform use of colors
2 parents 011c777 + 1153e78 commit 9052b90

File tree

283 files changed

+2688
-2656
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

283 files changed

+2688
-2656
lines changed

CHANGELOG.md

Lines changed: 28 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,31 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
99

1010
### Added
1111

12+
* Added descriptor support to `compas.colors.Color`.
13+
* Added descriptor protocol metaclass to `compas.artists.Artist`.
14+
* Added `compas.artists.colordict.ColorDict` descriptor.
15+
* Added `allclose` to doctest fixtures.
16+
* Added `compas.colors.Color.coerce` to construct a color out og hex, RGB1, and RGB255 inputs.
17+
* Added `compas.datastructures.Network.from_pointcloud`.
18+
* Added `compas.datastructures.VolMesh.from_meshgrid`.
1219
* Added `vertices_where`, `vertices_where_predicate`, `edges_where`, `edges_where_predicate` to `compas.datastructures.HalfFace`.
1320
* Added `faces_where`, `faces_where_predicate`, `cells_where`, `cells_where_predicate` to `compas.datastructures.HalfFace`.
1421

1522
### Changed
1623

24+
* Changed `compas_rhino.artists.MeshArtist.draw` to draw the mesh only.
25+
* Changed `compas_blender.artists.MeshArtist.draw` to draw the mesh only.
26+
* Changed `compas_ghpython.artists.MeshArtist.draw` to draw the mesh only.
27+
* Changed `compas_rhino.artists.MeshArtist.draw_vertexlabels` to use the colors of the vertex color dict.
28+
* Changed `compas_rhino.artists.MeshArtist.draw_edgelabels` to use the colors of the edge color dict.
29+
* Changed `compas_rhino.artists.MeshArtist.draw_facelabels` to use the colors of the face color dict.
30+
* Changed `compas_blender.artists.MeshArtist.draw_vertexlabels` to use the colors of the vertex color dict.
31+
* Changed `compas_blender.artists.MeshArtist.draw_edgelabels` to use the colors of the edge color dict.
32+
* Changed `compas_blender.artists.MeshArtist.draw_facelabels` to use the colors of the face color dict.
33+
* Changed `compas_ghpython.artists.MeshArtist.draw_vertexlabels` to use the colors of the vertex color dict.
34+
* Changed `compas_ghpython.artists.MeshArtist.draw_edgelabels` to use the colors of the edge color dict.
35+
* Changed `compas_ghpython.artists.MeshArtist.draw_facelabels` to use the colors of the face color dict.
36+
1737
### Removed
1838

1939

@@ -88,14 +108,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
88108

89109
### Removed
90110

91-
* Removed `compas.geometry.Collection`
92-
* Removed `compas.geometry.CollectionNumpy`
93-
* Removed `compas.geometry.PointCollection`
94-
* Removed `compas.geometry.PointCollectionNumpy`
95-
* Removed `compas.interop`
111+
* Removed `compas.geometry.Collection`.
112+
* Removed `compas.geometry.CollectionNumpy`.
113+
* Removed `compas.geometry.PointCollection`.
114+
* Removed `compas.geometry.PointCollectionNumpy`.
115+
* Removed `compas.interop`.
96116
* Removed `numba`; `compas.numerical.drx` will be moved to a dedicated extension package.
97117
* Removed `ezdxf` (unused).
98118
* Removed `laspy` (unused).
119+
* Removed `compas_rhino.artists.MeshArtist.draw_mesh`.
120+
* Removed `compas_blender.artists.MeshArtist.draw_mesh`.
99121

100122
## [1.13.3] 2021-12-17
101123

@@ -474,6 +496,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
474496
* Added `kwargs` to all child classes of `compas.data.Data`.
475497
* Added grasshopper component for drawing a frame.
476498
* Added `draw_origin` and `draw_axes`.
499+
* Added `compas.PY2`.
477500

478501
### Changed
479502

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
from compas.datastructures import Mesh
2+
from compas.artists import Artist
3+
from compas.colors import Color
4+
5+
mesh = Mesh.from_meshgrid(10, 10)
6+
7+
Artist.clear()
8+
9+
artist = Artist(mesh)
10+
artist.draw_faces(color={face: Color.pink() for face in mesh.face_sample(size=17)})
11+
12+
Artist.redraw()
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
from compas.geometry import Pointcloud
2+
from compas.datastructures import Network
3+
from compas.artists import Artist
4+
from compas.colors import Color
5+
6+
network = Network.from_pointcloud(Pointcloud.from_bounds(8, 5, 3, 53))
7+
8+
node = network.node_sample(size=1)[0]
9+
nbrs = network.neighbors(node)
10+
edges = network.connected_edges(node)
11+
12+
Artist.clear()
13+
14+
artist = Artist(network)
15+
artist.draw(
16+
nodecolor={n: Color.pink() for n in [node] + nbrs},
17+
edgecolor={e: Color.pink() for e in edges}
18+
)
19+
20+
Artist.redraw()
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import random
2+
from compas.geometry import Box, Sphere, Cylinder, Cone, Capsule, Torus, Polyhedron
3+
from compas.geometry import Plane, Circle, Pointcloud
4+
from compas.geometry import Translation
5+
from compas.artists import Artist
6+
from compas.colors import Color
7+
8+
shapes = [
9+
Box.from_width_height_depth(1, 1, 1),
10+
Sphere([0, 0, 0], 0.3),
11+
Cylinder(Circle(Plane([0, 0, 0], [0, 0, 1]), 0.3), 1.0),
12+
Cone(Circle(Plane([0, 0, 0], [0, 0, 1]), 0.3), 1.0),
13+
Capsule([[0, 0, 0], [1, 0, 0]], 0.2),
14+
Torus(Plane([0, 0, 0], [0, 0, 1]), 1.0, 0.3),
15+
Polyhedron.from_platonicsolid(12)
16+
]
17+
18+
cloud = Pointcloud.from_bounds(8, 5, 3, len(shapes))
19+
20+
Artist.clear()
21+
22+
for point, shape in zip(cloud, shapes):
23+
shape.transform(Translation.from_vector(point))
24+
artist = Artist(shape)
25+
artist.draw(color=Color.from_i(random.random()))
26+
27+
Artist.redraw()
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
from compas.datastructures import VolMesh
2+
from compas.artists import Artist
3+
from compas.colors import Color
4+
5+
mesh = VolMesh.from_meshgrid(dx=10, nx=10)
6+
7+
Artist.clear()
8+
9+
artist = Artist(mesh)
10+
artist.draw_cells(color={cell: Color.pink() for cell in mesh.cell_sample(size=83)})
11+
12+
Artist.redraw()

src/compas/__init__.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,9 @@
7676
Float formatting (``'<x>f'``) and integer formatting (``'d'``) specifiers are supported.
7777
"""
7878

79+
PY2 = compas._os.PY2
80+
"""bool: True if the current Python version is 2.x, False otherwise."""
81+
7982
PY3 = compas._os.PY3
8083
"""bool: True if the current Python version is 3.x, False otherwise."""
8184

src/compas/_os.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
class NotADirectoryError(Exception):
1818
pass
1919

20+
PY2 = sys.version_info[0] == 2
2021
PY3 = sys.version_info[0] == 3
2122
SYMLINK_REGEX = re.compile(r"\n.*\<SYMLINKD\>\s(.*)\s\[(.*)\]\r")
2223

src/compas/artists/artist.py

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@
1111
from compas.plugins import pluggable
1212
from compas.plugins import PluginValidator
1313

14+
from .colordict import DescriptorProtocol
15+
1416

1517
@pluggable(category='drawing-utils')
1618
def clear():
@@ -68,13 +70,15 @@ class Artist(object):
6870
----------------
6971
AVAILABLE_CONTEXTS : list[str]
7072
The available visualization contexts.
71-
CONTEXT : str or None
73+
CONTEXT : str | None
7274
The current visualization context is one of :attr:`AVAILABLE_CONTEXTS`.
73-
ITEM_ARTIST : dict[str, dict[Type[:class:`compas.data.Data`], Type[:class:`compas.artists.Artist`]]]
75+
ITEM_ARTIST : dict[str, dict[Type[:class:`~compas.data.Data`], Type[:class:`~compas.artists.Artist`]]]
7476
Dictionary mapping data types to the corresponding artists types per visualization context.
7577
7678
"""
7779

80+
__metaclass__ = DescriptorProtocol
81+
7882
__ARTISTS_REGISTERED = False
7983

8084
AVAILABLE_CONTEXTS = ['Rhino', 'Grasshopper', 'Blender', 'Plotter']
@@ -101,7 +105,7 @@ def build(item, **kwargs):
101105
102106
Returns
103107
-------
104-
:class:`compas.artists.Artist`
108+
:class:`~compas.artists.Artist`
105109
An artist of the type matching the provided item according to the item-artist map :attr:`~Artist.ITEM_ARTIST`.
106110
The map is created by registering item-artist type pairs using :meth:`~Artist.register`.
107111
@@ -116,14 +120,14 @@ def build_as(item, artist_type, **kwargs):
116120
117121
Parameters
118122
----------
119-
artist_type : :class:`compas.artists.Artist`
123+
artist_type : :class:`~compas.artists.Artist`
120124
**kwargs : dict[str, Any], optional
121125
The keyword arguments (kwargs) collected in a dict.
122126
For relevant options, see the parameter lists of the matching artist type.
123127
124128
Returns
125129
-------
126-
:class:`compas.artists.Artist`
130+
:class:`~compas.artists.Artist`
127131
An artist of the given type.
128132
129133
"""
@@ -158,9 +162,9 @@ def register(item_type, artist_type, context=None):
158162
159163
Parameters
160164
----------
161-
item_type : :class:`compas.data.Data`
165+
item_type : :class:`~compas.data.Data`
162166
The type of data item.
163-
artist_type : :class:`compas.artists.Artist`
167+
artist_type : :class:`~compas.artists.Artist`
164168
The type of the corresponding/compatible artist.
165169
context : Literal['Rhino', 'Grasshopper', 'Blender', 'Plotter'], optional
166170
The visualization context in which the pair should be registered.

src/compas/artists/colordict.py

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
import compas
2+
from collections import defaultdict
3+
if compas.PY2:
4+
from collections import Mapping
5+
else:
6+
from collections.abc import Mapping
7+
from compas.colors import Color
8+
9+
10+
class DescriptorProtocol(type):
11+
"""Meta class to provide support for the descriptor protocol in Python versions lower than 3.6"""
12+
13+
def __init__(cls, name, bases, attrs):
14+
for k, v in iter(attrs.items()):
15+
if hasattr(v, '__set_name__'):
16+
v.__set_name__(cls, k)
17+
18+
19+
class ColorDict(object):
20+
"""Descriptor for color dictionaries.
21+
22+
To use this descriptor, some requirements need to be fulfilled.
23+
24+
The descriptor should be assigned to a class attribute
25+
that has a protected counterpart that will hold the actual dictionary values,
26+
and a corresponding attribute that defines the default value for missing colors.
27+
Both the protected attribute and the default attribute should follow a specific naming convention.
28+
29+
For example, to create the property ``vertex_color`` on a ``MeshArtist``,
30+
the ``MeshArtist`` should have ``self._vertex_color`` for storing the actual dictionary values,
31+
and ``self.default_vertexcolor`` for storing the replacement value for missing entries in the dict.
32+
33+
The descriptor will then ensure that all values assigned to ``vertex_color`` will result in the creation
34+
of a ``defaultdict`` that always returns instances of ``compas.colors.Color``
35+
such that colors can be reliably converted between color spaces as needed, regardless of the input.
36+
37+
"""
38+
39+
def __set_name__(self, owner, name):
40+
"""Record the name of the attribute this descriptor is assigned to.
41+
The attribute name is then used to identify the corresponding private attribute, and the attribute containing a default value.
42+
43+
Parameters
44+
----------
45+
owner : object
46+
The class owning the attribute.
47+
name : str
48+
The name of the attribute.
49+
50+
Returns
51+
-------
52+
None
53+
54+
Notes
55+
-----
56+
In Python 3.6+ this method is called automatically.
57+
For earlier versions it needs to be used with a custom metaclass (``DescriptorProtocol``).
58+
59+
"""
60+
self.public_name = name
61+
self.private_name = '_' + name
62+
self.default_name = 'default_' + ''.join(name.split('_'))
63+
64+
def __get__(self, obj, otype=None):
65+
"""Get the color dict stored in the private attribute corresponding to the public attribute name of the descriptor.
66+
67+
Parameters
68+
----------
69+
obj : object
70+
The instance owning the instance of the descriptor.
71+
otype : object, optional
72+
The type owning the instance of the descriptor.
73+
74+
Returns
75+
-------
76+
defaultdict
77+
A defaultdict with the value stored in the default attribute corresponding to the descriptor as a default value.
78+
79+
"""
80+
default = getattr(obj, self.default_name)
81+
return getattr(obj, self.private_name) or defaultdict(lambda: default)
82+
83+
def __set__(self, obj, value):
84+
"""Set a new value for the descriptor.
85+
86+
Parameters
87+
----------
88+
obj : object
89+
The owner of the descriptor.
90+
value : dict[Any, :class:`~compas.colors.Color`] | :class:`~compas.colors.Color`
91+
The new value for the descriptor.
92+
This value is stored in the corresponding private attribute in the form of a defaultdict.
93+
94+
Returns
95+
-------
96+
None
97+
98+
"""
99+
if not value:
100+
return
101+
102+
if isinstance(value, Mapping):
103+
default = getattr(obj, self.default_name)
104+
item_color = defaultdict(lambda: default)
105+
for item in value:
106+
color = Color.coerce(value[item])
107+
if color:
108+
item_color[item] = color
109+
setattr(obj, self.private_name, item_color)
110+
111+
else:
112+
color = Color.coerce(value)
113+
setattr(obj, self.private_name, defaultdict(lambda: color))

src/compas/artists/curveartist.py

Lines changed: 4 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -30,10 +30,12 @@ class CurveArtist(Artist):
3030
3131
"""
3232

33-
default_color = Color(0, 0, 0)
33+
default_color = Color.from_hex('#0092D2')
3434

3535
def __init__(self, curve, color=None, **kwargs):
3636
super(CurveArtist, self).__init__()
37+
self._default_color = None
38+
3739
self._curve = None
3840
self._color = None
3941
self.curve = curve
@@ -55,11 +57,4 @@ def color(self):
5557

5658
@color.setter
5759
def color(self, color):
58-
if not color:
59-
return
60-
if Color.is_rgb255(color):
61-
self._color = Color.from_rgb255(* list(color))
62-
elif Color.is_hex(color):
63-
self._color = Color.from_hex(color)
64-
else:
65-
self._color = Color(* list(color))
60+
self._color = Color.coerce(color)

0 commit comments

Comments
 (0)