Skip to content

Commit

Permalink
Merge pull request #945 from compas-dev/artist-context
Browse files Browse the repository at this point in the history
Register all available artists and load based on context.
  • Loading branch information
tomvanmele authored Dec 10, 2021
2 parents eb6b5dc + aad90e1 commit cbe6bfa
Show file tree
Hide file tree
Showing 20 changed files with 187 additions and 311 deletions.
10 changes: 9 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

* Added `compas_rhino.DEFAULT_VERSION`.
* Added `clean` option to `compas_rhino.install` to remove existing symlinks if they cannot be imported from the current environment.
* Added `compas.is_grasshopper`.
* Added `compas.GH`.
* Added `compas.artists.Artist.CONTEXT`.
* Added `compas.artists.Artist.AVAILABLE_CONTEXTS`.
* Added `compas.artists.artist.register_artists` pluggable.

### Changed

Expand All @@ -19,15 +24,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
* Fixed error in parameter list of `compas_rhino.geometry.curves.new_nurbscurve`.
* Fixed error in parameter list of `compas_rhino.geometry.curves.new_nurbscurve_from_interpolation`.
* Fixed error in parameter list of `compas_rhino.geometry.curves.new_nurbscurve_from_step`.

* Changed `compas_rhino.install` to remove broken symlinks.
* Changed `compas_rhino.install` to reinstall broken symlinks if they can be imported from the current environment.
* Changed `compas_rhino.uninstall` to remove broken symlinks.
* Changed `compas_rhino.install_plugin` to remove broken symlinks.
* Changed default Rhino version for installation to `7.0`.
* Fixed bug in `compas_ghpython` related to importing `Grasshopper` prematurely.
* Changed `compas.artists.Artist.ITAM_ARTIST` to context-based dict.

### Removed

* Removed `compas.artists.artist.new_artist` pluggable.


## [1.12.2] 2021-11-30

Expand Down
53 changes: 1 addition & 52 deletions src/compas/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,57 +21,6 @@
compas.topology
compas.utilities
Utility functions
=================
JSON handling
-------------
.. autofunction:: json_dump
.. autofunction:: json_dumps
.. autofunction:: json_load
.. autofunction:: json_loads
Precision
---------
.. autofunction:: set_precision
.. autodata:: PRECISION
Execution context
-----------------
.. autofunction:: is_windows
.. autofunction:: is_linux
.. autofunction:: is_osx
.. autofunction:: is_mono
.. autofunction:: is_ironpython
.. autofunction:: is_rhino
.. autofunction:: is_blender
.. autodata:: WINDOWS
.. autodata:: LINUX
.. autodata:: OSX
.. autodata:: MONO
.. autodata:: IPY
.. autodata:: RHINO
.. autodata:: BLENDER
"""
from __future__ import print_function
Expand All @@ -82,7 +31,7 @@
from distutils.version import LooseVersion

import compas._os
from compas._os import is_windows, is_linux, is_osx, is_mono, is_ironpython, is_rhino, is_blender
from compas._os import is_windows, is_linux, is_osx, is_mono, is_ironpython, is_rhino, is_blender, is_grasshopper # noqa: F401
from compas.data import json_dump, json_dumps, json_load, json_loads


Expand Down
18 changes: 13 additions & 5 deletions src/compas/_os.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,8 @@ class NotADirectoryError(Exception):
'is_mono',
'is_ironpython',
'is_rhino',
'is_blender'
'is_blender',
'is_grasshopper'
]


Expand Down Expand Up @@ -100,17 +101,24 @@ def is_rhino():
import Rhino # noqa : F401
except ImportError:
return False
else:
return True
return True


def is_grasshopper():
try:
import Rhino
import scriptcontext
except ImportError:
return False
return not isinstance(scriptcontext.doc, Rhino.RhinoDoc)


def is_blender():
try:
import bpy # noqa : F401
except ImportError:
return False
else:
return True
return True


if is_windows():
Expand Down
1 change: 1 addition & 0 deletions src/compas/artists/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@

BaseRobotModelArtist = RobotModelArtist


__all__ = [
'DataArtistNotRegistered',
'Artist',
Expand Down
75 changes: 53 additions & 22 deletions src/compas/artists/artist.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,12 @@

import inspect
from abc import abstractmethod
from collections import defaultdict

import compas
from compas.artists import DataArtistNotRegistered
from compas.plugins import pluggable
from compas.plugins import PluginValidator


@pluggable(category='drawing-utils')
Expand All @@ -19,19 +22,62 @@ def redraw():
raise NotImplementedError


@pluggable(category='factories')
def new_artist(cls, *args, **kwargs):
@pluggable(category='factories', selector='collect_all')
def register_artists():
raise NotImplementedError


def identify_context():
if compas.is_grasshopper():
return 'Grasshopper'
if compas.is_rhino():
return 'Rhino'
if compas.is_blender():
return 'Blender'
return None


def _get_artist_cls(data, **kwargs):
if 'context' in kwargs:
Artist.CONTEXT = kwargs['context']
else:
Artist.CONTEXT = identify_context()

dtype = type(data)
cls = None

if 'artist_type' in kwargs:
cls = kwargs['artist_type']
else:
context = Artist.ITEM_ARTIST[Artist.CONTEXT]
for type_ in inspect.getmro(dtype):
cls = context.get(type_, None)
if cls is not None:
break

if cls is None:
raise DataArtistNotRegistered('No artist is registered for this data type: {} in this context: {}'.format(dtype, Artist.CONTEXT))

return cls


class Artist(object):
"""Base class for all artists.
"""

ITEM_ARTIST = {}
__ARTISTS_REGISTERED = False

AVAILABLE_CONTEXTS = ['Rhino', 'Grasshopper', 'Blender', 'Plotter']
CONTEXT = None
ITEM_ARTIST = defaultdict(dict)

def __new__(cls, *args, **kwargs):
return new_artist(cls, *args, **kwargs)
if not Artist.__ARTISTS_REGISTERED:
register_artists()
Artist.__ARTISTS_REGISTERED = True
cls = _get_artist_cls(args[0], **kwargs)
PluginValidator.ensure_implementations(cls)
return super(Artist, cls).__new__(cls)

@staticmethod
def build(item, **kwargs):
Expand All @@ -49,7 +95,7 @@ def build(item, **kwargs):
An artist of the type matching the provided item according to the item-artist map ``~Artist.ITEM_ARTIST``.
The map is created by registering item-artist type pairs using ``~Artist.register``.
"""
artist_type = Artist.ITEM_ARTIST[type(item)]
artist_type = _get_artist_cls(item, **kwargs)
artist = artist_type(item, **kwargs)
return artist

Expand All @@ -58,21 +104,6 @@ def build_as(item, artist_type, **kwargs):
artist = artist_type(item, **kwargs)
return artist

@staticmethod
def get_artist_cls(data, **kwargs):
dtype = type(data)
cls = None
if 'artist_type' in kwargs:
cls = kwargs['artist_type']
else:
for type_ in inspect.getmro(dtype):
cls = Artist.ITEM_ARTIST.get(type_)
if cls is not None:
break
if cls is None:
raise DataArtistNotRegistered('No artist is registered for this data type: {}'.format(dtype))
return cls

@staticmethod
def clear():
return clear()
Expand All @@ -82,8 +113,8 @@ def redraw():
return redraw()

@staticmethod
def register(item_type, artist_type):
Artist.ITEM_ARTIST[item_type] = artist_type
def register(item_type, artist_type, context=None):
Artist.ITEM_ARTIST[context][item_type] = artist_type

@abstractmethod
def draw(self):
Expand Down
57 changes: 21 additions & 36 deletions src/compas_blender/artists/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,6 @@
import compas_blender

from compas.plugins import plugin
from compas.plugins import PluginValidator
from compas.artists import Artist

from compas.geometry import Box
Expand Down Expand Up @@ -115,6 +114,7 @@

@plugin(category='drawing-utils', pluggable_name='clear', requires=['bpy'])
def clear_blender():
print('doing it')
compas_blender.clear()


Expand All @@ -123,41 +123,26 @@ def redraw_blender():
compas_blender.redraw()


artists_registered = False


@plugin(category='factories', pluggable_name='new_artist', tryfirst=True, requires=['bpy'])
def new_artist_blender(cls, *args, **kwargs):
# "lazy registration" seems necessary to avoid item-artist pairs to be overwritten unintentionally
global artists_registered

if not artists_registered:
BlenderArtist.register(Box, BoxArtist)
BlenderArtist.register(Capsule, CapsuleArtist)
BlenderArtist.register(Circle, CircleArtist)
BlenderArtist.register(Cone, ConeArtist)
BlenderArtist.register(Cylinder, CylinderArtist)
BlenderArtist.register(Frame, FrameArtist)
BlenderArtist.register(Line, LineArtist)
BlenderArtist.register(Mesh, MeshArtist)
BlenderArtist.register(Network, NetworkArtist)
BlenderArtist.register(Point, PointArtist)
BlenderArtist.register(Polygon, PolygonArtist)
BlenderArtist.register(Polyhedron, PolyhedronArtist)
BlenderArtist.register(Polyline, PolylineArtist)
BlenderArtist.register(RobotModel, RobotModelArtist)
BlenderArtist.register(Sphere, SphereArtist)
BlenderArtist.register(Torus, TorusArtist)
BlenderArtist.register(Vector, VectorArtist)
artists_registered = True

data = args[0]

cls = Artist.get_artist_cls(data, **kwargs)

PluginValidator.ensure_implementations(cls)

return super(Artist, cls).__new__(cls)
@plugin(category='factories', requires=['bpy'])
def register_artists():
Artist.register(Box, BoxArtist, context='Blender')
Artist.register(Capsule, CapsuleArtist, context='Blender')
Artist.register(Circle, CircleArtist, context='Blender')
Artist.register(Cone, ConeArtist, context='Blender')
Artist.register(Cylinder, CylinderArtist, context='Blender')
Artist.register(Frame, FrameArtist, context='Blender')
Artist.register(Line, LineArtist, context='Blender')
Artist.register(Mesh, MeshArtist, context='Blender')
Artist.register(Network, NetworkArtist, context='Blender')
Artist.register(Point, PointArtist, context='Blender')
Artist.register(Polygon, PolygonArtist, context='Blender')
Artist.register(Polyhedron, PolyhedronArtist, context='Blender')
Artist.register(Polyline, PolylineArtist, context='Blender')
Artist.register(RobotModel, RobotModelArtist, context='Blender')
Artist.register(Sphere, SphereArtist, context='Blender')
Artist.register(Torus, TorusArtist, context='Blender')
Artist.register(Vector, VectorArtist, context='Blender')
print('Blender Artists registered.')


__all__ = [
Expand Down
4 changes: 0 additions & 4 deletions src/compas_blender/artists/artist.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,3 @@ def collection(self, value: Union[str, bpy.types.Collection]):
self._collection = compas_blender.create_collection(value)
else:
raise Exception('Collection must be of type `str` or `bpy.types.Collection`.')

def clear(self) -> None:
"""Delete all objects created by the artist."""
compas_blender.delete_objects(self.collection.objects)
9 changes: 3 additions & 6 deletions src/compas_ghpython/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,11 @@
import compas
import compas_rhino

# TODO: I believe this should be removed, as it pulls all utilities funcs
# into first-level instead of the usual second-level namespace
if compas.RHINO:
from .utilities import * # noqa: F401 F403


__version__ = '1.12.2'

if compas.is_rhino():
from .utilities import * # noqa: F401 F403


def get_grasshopper_plugin_path(version):
version = compas_rhino._check_rhino_version(version)
Expand Down
Loading

0 comments on commit cbe6bfa

Please sign in to comment.