🏛️ Bundle 2024-01
Object displays

Object displays

Every object inheriting from DessiaObject or PhysicalObject can be represented on the Dessia platform using a set of displays. For example, a display can be text, a graph, or even a 3D model. The generic objects in dessia_common offer a set of methods by default to define these different representations. More specifically, the available displays are:

  • Free text using the markdown formatting options
  • 2D graph
  • 2D diagram
  • 3D geometry
  • Workflow

PhysicalObject objects offer a default 3D geometric representation in addition to the previously introduced representations. In contrast, DessiaObject objects do not offer a 3D geometric representation by default. The dessia_common library provides a formalism for customizing each of these methods. In the following sections, we will detail the implementation of each of these methods."

Display 3D

display 3d

Customizing the 3D display requires detailing the "volmdlr_primitives" method. This method relies on the 3D geometric modeler "volmdlr" developed by Dessia (see chapter on volmdlr, we only briefly detail the use of this library here). It is a Python library that allows describing a 3D CAD from objects inheriting from the generic objects of dessia_common. Therefore, all volmdlr CADs can be treated similarly to DessiaObject or PhysicalObject objects and be present in the Dessia platform.

The "volmdlr_primitives" method describes how we want to construct the 3D geometry and must return a list of basic volmdlr 3D primitives (see chapter on volmdlr). The following code presents the syntax to define a 3D for Bearing and Ball objects:

from dessia_common.core import PhysicalObject
from volmdlr import Point2D, Point3D, OXYZ, Z3D
from volmdlr.primitives3d import Sphere, ExtrudedProfile,
from volmdlr.wires import Contour2D
from volmdlr.curves import Circle2D
 
class Ball(PhysicalObject):
    _standalone_in_db = False
 
    def __init__(self, diameter: float, name: str = ""):
        self.diameter = diameter
        PhysicalObject.__init__(self, name=name)
 
    def volmdlr_primitives(self, pos_x=0., pos_y=0., pos_z=0.,
                           distance=0., angle=0.):
        center = Point3D(pos_x + distance * cos(angle),
                         pos_y + distance * sin(angle),
                         pos_z)
        primitives = [Sphere(center=center, radius=self.diameter / 2)]
        return primitives
 
class Bearing(PhysicalObject):
    _standalone_in_db = True
 
    def __init__(self, ball: Ball,
                 internal_diameter: float,
                 external_diameter: float,
                 height: float,
                 thickness: float = 0.,
                 name: str = ""):
        self.ball = ball
        self.internal_diameter = internal_diameter
        self.external_diameter = external_diameter
        self.height = height
        self.thickness = thickness
        PhysicalObject.__init__(self, name=name)
 
    def volmdlr_primitives(self, pos_x=0., pos_y=0., pos_z=0.,
                           number_balls=1):
        # Setup circles for extrusions
        outer_circle = Circle2D.from_center_and_radius(
            center=Point2D(pos_x, pos_y),
            radius=self.external_diameter / 2)
        inner_outer_circle = Circle2D.from_center_and_radius(
            center=Point2D(pos_x, pos_y),
            radius=self.external_diameter / 2 - self.thickness)
        inner_circle = Circle2D.from_center_and_radius(
            center=Point2D(pos_x, pos_y),
            radius=self.internal_diameter / 2)
        outer_inner_circle = Circle2D.from_center_and_radius(
            center=Point2D(pos_x, pos_y),
            radius=self.internal_diameter / 2 + self.thickness)
        # Extrusions
        frame = OXYZ.copy()
        frame.origin += Z3D * (pos_z - self.height / 2)
        outer_extrusion = ExtrudedProfile(
            frame=frame,
            outer_contour2d=Contour2D.from_circle(outer_circle),
            inner_contours2d=[Contour2D.from_circle(inner_outer_circle)],
            extrusion_length=self.height)
        inner_extrusion = ExtrudedProfile(
            frame=frame,
            outer_contour2d=Contour2D.from_circle(outer_inner_circle),
            inner_contours2d=[Contour2D.from_circle(inner_circle)],
            extrusion_length=self.height)
        # Balls
        ball_primitives = []
        ball_distance = self.internal_diameter / 2 + \
            (self.external_diameter - self.inner_diameter) / 4
        for i in range(number_balls):
            ball_primitive = self.ball.volmdlr_primitives(
                pos_x=pos_x, pos_y=pos_y, pos_z=pos_z,
                distance=ball_distance,
                angle=i * 2 * pi / number_balls)[0]
            ball_primitives.append(ball_primitive)
        return [outer_extrusion, inner_extrusion] + ball_primitives

For example, to define the 3D representation of the Ball object, we use the Sphere object from the primitives3d module of volmdlr to declare an elementary primitive constituting a sphere. This primitive will be returned in a list to conform to the dessia_common format. When this object is opened from the Dessia platform, a 3D viewer will be integrated into the object's display to represent its geometry.

Display 2D

display 2d

Five types of 2D display are possible: a Scatter, an Histogram, a Draw, a Graph and a Parallel Plot. All five display modes require the use of the "plot_data" library developed by Dessia. This is a Python library that is structured in a similar way to "volmdlr" and whose components are decomposed from Python objects inheriting from DessiaObject. Similarly, all objects can be stored from the Dessia platform. For more details on the "plot_data" library, please refer to the 2D graphics chapter.

2D display from the "plot_data" library requires custom objects to implement the "plot_data_view" decorator. When a method is assigned this decorator, the Dessia platform will offer a graphical representation in the object's display area. It is important to realize that the term "plot_data" is used to define 3 different concepts:

  • Name of the 2D representation library
  • Name of the method allowing an object inheriting from DessiaObject to represent a 2D diagram or graph in its display
  • Name of the file format allowing to write all components of the plot_data library in a generic way

For ease of use, the volmdlr library provides methods for generating these plot_data objects for its 2D objects (which are required for 2D plotting). For more details, it is necessary to refer to the plot_data documentation (opens in a new tab).

The following code represents this method for generating a 2D representation of Ball and Bearing objects:

from math import cos, sin, pi
from dessia_common.core import PhysicalObject
from dessia_common.decorators import plot_data_view
from volmdlr import Point2D
from volmdlr.curves import Circle2D
from plot_data import PrimitiveGroup, EdgeStyle, SurfaceStyle
from plot_data.colors import BLACK, GREY
 
class Ball(PhysicalObject):
    _standalone_in_db = False
 
    def __init__(self, diameter: float, name: str = ''):
        self.diameter = diameter
        PhysicalObject.__init__(self, name=name)
 
    @plot_data_view("2D display for Ball")
    def display_2d(self, pos_x=0., pos_y=0., distance=0., angle=0.):
        # Color settings
        edge_style = EdgeStyle(color_stroke=BLACK)
        surface_style = SurfaceStyle(color_fill=GREY, opacity=0.5)
 
        ball_x = distance * cos(angle)
        ball_y = distance * sin(angle)
        center = Point2D(pos_x + ball_x, pos_y + ball_y)
        circle = Circle2D.from_center_and_radius(
            center=center, radius=self.diameter / 2)
        primitives = [circle.plot_data(
            edge_style=edge_style, surface_style=surface_style)]
        return PrimitiveGroup(primitives=primitives,
                              name='Circle')
 
class Bearing(PhysicalObject):
    _standalone_in_db = True
 
    def __init__(self, ball: Ball, internal_diameter: float,
                 external_diameter: float, height: float,
                 thickness: float = 0., name: str = ''):
        self.ball = ball
        self.internal_diameter = internal_diameter
        self.external_diameter = external_diameter
        self.height = height
        self.thickness = thickness
        PhysicalObject.__init__(self, name=name)
 
    @plot_data_view("2D display for Bearing")
    def display_2d(self, pos_x=0., pos_y=0., number_balls=1):
        # Color settings
        edge_style = EdgeStyle(color_stroke=BLACK)
        surface_style = SurfaceStyle(opacity=0)
 
        primitives = []
        center = Point2D(pos_x, pos_y)
        # External circle
        external_circle = Circle2D.from_center_and_radius(
            center=center, radius=self.external_diameter / 2)
        primitives.append(external_circle.plot_data(
            edge_style=edge_style, surface_style=surface_style))
        # Internal circle
        internal_circle = Circle2D.from_center_and_radius(
            center=center, radius=self.internal_diameter / 2)
        primitives.append(internal_circle.plot_data(
            edge_style=edge_style, surface_style=surface_style))
        # Balls
        ball_distance = self.internal_diameter / 2 + \
            (self.external_diameter - self.internal_diameter) / 4
        for i in range(number_balls):
            primitives.extend(self.ball.display_2d(
                pos_x=pos_x,
                pos_y=pos_y,
                distance=ball_distance,
                angle=i * 2 * pi / number_balls).primitives)
        return PrimitiveGroup(primitives=primitives)

For the Ball object, we start by using the volmdlr library to define the 2D geometric shapes. Then, from the "plot_data()" method of volmdlr objects, we generate these generic plot_data objects that can be used to fill PrimitiveGroups.

If you wish to include a scatter plot in the plot_data method, here is an example of a possible data input:

from dessia_common.core import PhysicalObject
import random
from plot_data import colors
import plot_data.core as plot_data
from dessia_common.decorators import plot_data_view
 
class Graph(PhysicalObject):
 
    def __init__(self, diameter: float,
                 name: str = ""):
        self.diameter = diameter
        PhysicalObject.__init__(self, name=name)
 
    @plot_data_view("ScatterPlot")
    def my_scatter_plot(self):
        elements = []
        SHAPES = ['round', 'square', 'triangle', 'ellipse']
        COLORS = [colors.RED, colors.BLUE, colors.GREEN, colors.YELLOW, colors.ORANGE, colors.VIOLET]
        for i in range(50):
            random_shape = SHAPES[random.randint(0, len(SHAPES) - 1)]
            random_color = COLORS[random.randint(0, len(SHAPES) - 1)]
            elements.append({'mass': random.uniform(0, 50),
                             'length': random.uniform(0, 100),
                             'shape': random_shape,
                             'color': random_color})
        return plot_data.Scatter(elements=elements,
                                 x_variable='mass,
                                 y_variable='length')

A large number of different graphs are possible and detailed in the plot_data documentation (opens in a new tab).

Display markdown

To include free text in the display, it is possible to write a "to_markdown" method in custom objects that inherit from DessiaObject or PhysicalObject. The "to_markdown" method returns a string formatted according to the markdown standard (# for a Heading 1, ## for a Heading 2, etc.). It is necessary to insert the '\n' character into the string when a line break is desired.

The following code presents an example of how to apply this to the Bearing object:

class Bearing(PhysicalObject):
    _standalone_in_db = True
 
    def __init__(self, ball: Ball,
                 internal_diameter: float,
                 external_diameter: float,
                 height: float,
                 name: str = ""):
        self.ball = ball
        self.internal_diameter = internal_diameter
        self.external_diameter = external_diameter
        self.height = height
        PhysicalObject.__init__(self, name=name)
 
    def to_markdown(self):
        infos = ''
        infos += '## Bearing Infos \n\n'
        infos += '|name|height|external_diameter|internal_diameter|' + '\n'
        infos += '|:---------------:|:---------------:|:---------------:|:-----------------:|' + '\n'
        infos += '|' + self.name + '|' + str(self.height) + '|' + str(self.external_diameter) \
                 + '|' + str(self.internal_diameter) + '|\n'
 
        return infos

Display customization

It is possible to customize the displays of an object that inherits from DessiaObject or PhysicalObject. Broadly speaking, an object's display on the platform is controlled by a list of DisplaySetting objects (in the dessia_common displays module). Each DisplaySetting describes an elementary display such as plot_data, 3D, or markdown. The 'display_settings()' method is used to display this list of DisplaySetting objects.

In : ball1.display_settings()
 
Out : [<dessia_common.displays.DisplaySetting at 0x147c6ac70>,
	   <dessia_common.displays.DisplaySetting at 0x144f51c10>,
	   <dessia_common.displays.DisplaySetting at 0x144f51730>]

By default, the 'display_settings()' method returns a list of elementary displays whose methods are present in the object. For example, if the 'plot_data', 'primitives_volmdlr', and 'to_markdown' methods are present in the object, then the 'display_settings()' method will return a list of three DisplaySetting objects, where the first describes the 'plot_data' display, the second the 3D display, and the third the markdown display.

If you wish to add elements, you simply need to include the 'display_settings()' method in your object in order to overwrite the default method and include the DisplaySetting elements you desire (such as multiple 'plot_data' displays). The following code provides a modification of a 'display_settings()' method that aims to display multiple plot_data, markdown, and 3D displays.

class Bearing(PhysicalObject):
    _standalone_in_db = True
 
    def __init__(self, ball: Ball,
                 internal_diameter: float,
                 external_diameter: float,
                 height: float,
                 name: str = ""):
        self.ball = ball
        self.internal_diameter = internal_diameter
        self.external_diameter = external_diameter
        self.height = height
        PhysicalObject.__init__(self, name=name)
 
    @plot_data_view("1st plot data")
    def plot_data1(self):
        ...
 
    @plot_data_view("2nd plot data")
    def plot_data2(self):
        ...
 
    def to_markdown1(self):
        ...
 
    def to_markdown2(self):
        ...
 
    def volmdlr_primitives(self):
        ...
 
    def primitives_volmdlr2(self):
        ...
 
    def volmdlr_volume_model1(self):
        primitives = self.primitives_volmdlr1()
        return volmdlr.core.VolumeModel(primitives)
 
    def volmdlr_volume_model2(self):
        primitives = self.primitives_volmdlr2()
        return volmdlr.core.VolumeModel(primitives)
 
	def display_settings(self):
		display1 = DisplaySetting(selector='cad_1', type_='babylon_data',
                              method='volmdlr_volume_model1().babylon_data',
                              serialize_data=True)
		display2 = DisplaySetting(selector='cad_2', type_='babylon_data',
                              method='volmdlr_volume_model2().babylon_data',
                              serialize_data=True)
		display3 = DisplaySetting(selector="markdown_1", type_="markdown",
                              method="to_markdown1")
		display4 = DisplaySetting(selector="markdown_2", type_="markdown",
                              method="to_markdown2")
		display5 = DisplaySetting(selector="plot_data_1", type_="plot_data",
                              method="plot_data1", serialize_data=True)
		display6 = DisplaySetting(selector="plot_data_2", type_="plot_data",
                              method="plot_data2", serialize_data=True)
		return [display1, display2, display3, display4, display5, display6]

It is important to follow the formalism for declaring DisplaySetting objects, as detailed in the previous example. The 'display_settings()' method will then be called by the '_displays()' method, which generates a JSON display that will be sent via the API to the platform.