A 3D sketch in DASH

Hi DASH community

For an application I would like to sketch a box, ultimately proportioned as the user has specified.
I found : 3d mesh plots in Python – and at the bottom it is claimed all these figures can be passed to dcc.Graph.
Unfortunately, as I do this, I cannot get a proper 3D view of the box I am drawing. Here are some code snippets:

def go_plot_sketch():   # experimental

    obj = []            # Later this function should import Lx, Ly and Lz
    obj.append(         # x,y,z are then multiplied by their real dimensions
        go.Figure(data=[
            go.Mesh3d(
                x = [-0.5, -0.5, 0.5, 0.5, -0.5, -0.5, 0.5, 0.5],
                y = [0, 1, 1, 0, 0, 1, 1, 0],
                z = [1, 1, 1, 1, 0, 0, 0, 0],
                opacity=0.6,
                color='#DC143C',
                flatshading=True
            )
        ])
    )
    return obj

Later in the code:

   fig_sketch = go_plot_sketch()        # experimental
   plot_sketch = dcc.Graph(id='sketch',
                           style={'height':'600px','width':'900p'},
                           figure=fig_sketch
                          )             # experimental
   ...

   obj_sketch = html.Div([plot_sketch]) # experimental
   ...

   boxtabs.append(dcc.Tab(label='Sketch',id='obj_sketch',children=msg,   # experimental
   ...

In the callback:

              Output('obj_sketch','children'),         # experimental

I would like a kind of ‘isometric’ view. This is what it looks like right now:

No box. No way to rotate the view (a fixed view angle could be OK). Am I missing something, or does dcc.Graph not support mesh3d?

With kind regards,
Claus

OK, I can write executable code, which shows the box:

from dash import Dash, dcc, html
import plotly.graph_objects as go

fig = go.Figure(data=[
    go.Mesh3d(
        x = [-0.5, -0.5, 0.5, 0.5, -0.5, -0.5, 0.5, 0.5],
        y = [0, 1, 1, 0, 0, 1, 1, 0],
        z = [1, 1, 1, 1, 0, 0, 0, 0],
        opacity=0.6,
        color='#DC143C',
        flatshading=True
    )
])

app = Dash()
app.layout = html.Div([
    dcc.Graph(id='sketch',
              style={'height':'600px','width':'900p'},
              figure=fig)
])

app.run_server(debug=True)

This completely follows the example, no deviation. Output:

It seems the dcc.Graph needs its hands on the ‘fig’ itself, and cannot tolerate that it is passed in an object. (?)

/Claus

Another step in the right direction:

from dash import Dash, dcc, html
import plotly.graph_objects as go

Lx = 0.8  # width
Ly = 1.25 # height

fig = go.Figure(data=[
    go.Mesh3d(
        x = [-0.5*Lx, -0.5*Lx, 0.5*Lx, 0.5*Lx, -0.5*Lx, -0.5*Lx, 0.5*Lx, 0.5*Lx],
        y = [0, Ly, Ly, 0, 0, Ly, Ly, 0],
        z = [1, 1, 1, 1, 0, 0, 0, 0],
        opacity=0.6,
        color='#DC143C',
        # i, j and k give the vertices of triangles
        i = [7, 0, 0, 0, 4, 4, 6, 6, 4, 0, 3, 2],
        j = [3, 4, 1, 2, 5, 6, 5, 2, 0, 1, 6, 3],
        k = [0, 7, 2, 3, 6, 7, 1, 1, 5, 5, 7, 6],
        # without i,j,k the faces of the box are not visible
        flatshading=True
    )
])

app = Dash()
app.layout = html.Div([
    dcc.Graph(id='sketch',
              style={'height':'600px','width':'900p'},
              figure=fig)
])

app.run_server(debug=True)

Now I can see the faces of the box:

/Claus

Just to explain myself. The end goal is something like this:

/Claus

New problem. I thought that I should be able to draw shapes (e.g. filled circles) on the surface of the box, but when I add a shape, it draws itself in an unexpected way, like in a completely different 2D space:

from dash import Dash, dcc, html
import plotly.graph_objects as go

Lx = 0.8  # width
Ly = 1.25 # height

fig = go.Figure(data=[
    go.Mesh3d(
        x = [-0.5*Lx, -0.5*Lx, 0.5*Lx, 0.5*Lx, -0.5*Lx, -0.5*Lx, 0.5*Lx, 0.5*Lx],
        y = [0, Ly, Ly, 0, 0, Ly, Ly, 0],
        z = [0, 0, 0, 0, -1, -1, -1, -1],
        opacity=0.6,
        color='#DC143C',
        # i, j and k give the vertices of triangles
        i = [7, 0, 0, 0, 4, 4, 6, 6, 4, 0, 3, 2],
        j = [3, 4, 1, 2, 5, 6, 5, 2, 0, 1, 6, 3],
        k = [0, 7, 2, 3, 6, 7, 1, 1, 5, 5, 7, 6],
        # without i,j,k the faces of the box are not visible
        flatshading=True
    )
])
fig.add_shape(type="circle",
    xref="x", yref="y",
    fillcolor="PaleTurquoise",
    x0=0, y0=Ly/2, x1=0, y1=Ly,
    line_color="LightSeaGreen",
)

app = Dash()
app.layout = html.Div([
    dcc.Graph(id='sketch',
              style={'height':'600px','width':'900p'},
              figure=fig)
])

app.run_server(debug=True)

Resulting plot:

/Claus

Change of strategy. Instead of drawing a flat circle, I will draw a cylinder. Something like this (the code gets a bit long now…):

"""
sample3d.py code snippet shows how to use Mesh3d with DASH
"""
from dash import Dash, dcc, html
import plotly.graph_objects as go
import numpy as np

vb = 8*1e-3     # box volume 8 liter, in [m^3]
lxlz = 0.8      # Lx/Lz
lylz = 1.25     # Ly/Lz
ydrv = 0.5      # Yd/Ly
yprt = 0.2      # Yp/Ly
Lz = 100*(vb/lxlz/lylz)**(1/3)  # depth, 20 [cm]
Lx = lxlz*Lz    # width, 16 [cm]
Ly = lylz*Lz    # height, 25 [cm]
lp = 13.7       # length of port, 13.7 [cm]
dp = 1.25*2.54  # diameter of port, 1.25 inches = 3.2 [cm]

# Code example : https://community.plotly.com/t/basic-3d-cylinders/27990
def cylinder(r, h, a =0, nt=100, nv =50):
    """
    parametrize the cylinder of radius r, height h, base point a
    """
    theta = np.linspace(0, 2*np.pi, nt)
    v = np.linspace(a, a+h, nv )
    theta, v = np.meshgrid(theta, v)
    x = r*np.cos(theta)
    y = r*np.sin(theta)
    z = v
    return x, y, z

def boundary_circle(r, h, nt=100):
    """
    r - boundary circle radius
    h - height above xOy-plane where the circle is included
    returns the circle parameterization
    """
    theta = np.linspace(0, 2*np.pi, nt)
    x= r*np.cos(theta)
    y = r*np.sin(theta)
    z = h*np.ones(theta.shape)
    return x, y, z

r1 = dp/2            # radius of cylinder
a1 = -0.99*lp
h1 = lp              # length of cylinder

x1, y1, z1 = cylinder(r1, h1, a=a1)

colorscale = [[0, 'blue'],
             [1, 'blue']]

cyl1 = go.Surface(x=x1, y=y1, z=z1,
                 colorscale = colorscale,
                 showscale=False,
                 opacity=0.5)

xb_low, yb_low, zb_low = boundary_circle(r1, h=a1)
xb_up, yb_up, zb_up = boundary_circle(r1, h=a1+h1)

bcircles1 = go.Scatter3d(x = xb_low.tolist()+[None]+xb_up.tolist(),
                        y = yb_low.tolist()+[None]+yb_up.tolist(),
                        z = zb_low.tolist()+[None]+zb_up.tolist(),
                        mode ='lines',
                        line = dict(color='blue', width=2),
                        opacity =0.55, showlegend=False)

box1 = go.Mesh3d(
        x = [-0.5*Lx, -0.5*Lx, 0.5*Lx, 0.5*Lx, -0.5*Lx, -0.5*Lx, 0.5*Lx, 0.5*Lx],
        y = [0, Ly, Ly, 0, 0, Ly, Ly, 0],
        z = [0, 0, 0, 0, -Lz, -Lz, -Lz, -Lz],
        opacity=0.6,
        color='#DC143C',
        # i, j and k give the vertices of triangles
        i = [7, 0, 0, 0, 4, 4, 6, 6, 4, 0, 3, 2],
        j = [3, 4, 1, 2, 5, 6, 5, 2, 0, 1, 6, 3],
        k = [0, 7, 2, 3, 6, 7, 1, 1, 5, 5, 7, 6],
        # without i,j,k the faces of the box are not visible
        flatshading=True
    )

#layout = go.Layout(scene_xaxis_visible=False, scene_yaxis_visible=False, scene_zaxis_visible=False)
#fig =  go.Figure(data=[cyl1, bcircles1, box1], layout=layout)
fig =  go.Figure(data=[cyl1, bcircles1, box1])

fig.update_layout(scene_camera_eye_z= 0.55)
fig.layout.scene.camera.projection.type = "orthographic"

app = Dash()
app.layout = html.Div([
    dcc.Graph(id='sketch',
              style={'height':'600px','width':'900p'},
              figure=fig)
])

app.run_server(debug=True)

This is what it looks like - not finished, but I have to stop for the day:

/Claus

Dear DASH - Plotly community

I have pretty much finished my 3D sketch. One wish remains - in classical CAD software you can rotate the image, but you can always return to an isometric view (in DASH I can choose HOME and CAMERA views) but you can also always select top, side or front view. As an example with FreeCAD ( Manual:Navigating in the 3D view - FreeCAD Documentation) This is what it looks like:

image

Here is what my code looks now:

"""
sample3d.py code snippet shows how to use Mesh3d with DASH
"""
from dash import Dash, dcc, html
import plotly.graph_objects as go
import numpy as np

# ---------- BEGIN INPUT PARAMETERS ----------

dd = 11.5       # Driver Diameter, in [cm]
vb = 8*1e-3     # box volume 8 liter, in [m^3]
lxlz = 0.8      # Lx/Lz
lylz = 1.25     # Ly/Lz
ydrv = 0.6      # Yd/Ly
yprt = 0.2      # Yp/Ly
Lz = 100*(vb/lxlz/lylz)**(1/3)  # depth, 20 [cm]
Lx = lxlz*Lz    # width, 16 [cm]
Ly = lylz*Lz    # height, 25 [cm]
Yp = yprt*Ly
Yd = ydrv*Ly
lp = 13.7       # length of port, 13.7 [cm]
dp = 1.25*2.54  # diameter of port, 1.25 inches = 3.2 [cm]

# ---------- END INPUT PARAMETERS ----------

colorscale = [[0, 'blue'],
             [1, 'blue']]

r2 = dd/2           # radius of cylinder (from mm to cm)
a2 = -dd/20         # (make it shallow), half into the box, half out of the box
h2 = dd/10          # length of cylinder (driver)

r1 = dp/2           # radius of cylinder
a1 = -lp-a2         # a bit sticks out of the box (same as for the driver)
h1 = lp             # length of cylinder (port)

# Code example : https://community.plotly.com/t/basic-3d-cylinders/27990
def cylinder(r, h, a=0, Y=0, nt=100, nv=50):
    """
    parametrize the cylinder of radius r, height h, base point a
    """
    theta = np.linspace(0, 2*np.pi, nt)
    v = np.linspace(a, a+h, nv )
    theta, v = np.meshgrid(theta, v)
    x = r*np.cos(theta)
    y = r*np.sin(theta)+Y
    z = v
    return x, y, z

def boundary_circle(r, h, Y=0, nt=100):
    """
    r - boundary circle radius
    h - height above xOy-plane where the circle is included
    returns the circle parameterization
    """
    theta = np.linspace(0, 2*np.pi, nt)
    x = r*np.cos(theta)
    y = r*np.sin(theta)+Y
    z = h*np.ones(theta.shape)
    return x, y, z

def circle(x, y, z, r):
    """
    Create a circular mesh located at x, y, z with radius r
    """
    r_discr = np.linspace(0, r, 2)
    theta_discr = np.linspace(0, 2*np.pi, 36)
    r_grid, theta_grid = np.meshgrid(r_discr, theta_discr)
    x_circle = r_grid * np.cos(theta_grid) + x
    y_circle = r_grid * np.sin(theta_grid) + y
    z_circle = np.zeros_like(x_circle) + z
    return x_circle, y_circle, z_circle

x1, y1, z1 = cylinder(r1, h1, Y=Yp, a=a1)   # port

cyl1 = go.Surface(x=x1, y=y1, z=z1,
                 colorscale = colorscale,
                 showscale=False,
                 opacity=0.5)

xb_low, yb_low, zb_low = boundary_circle(r1, h=a1, Y=Yp)
xb_up, yb_up, zb_up = boundary_circle(r1, h=a1+h1, Y=Yp)

bcircles1 = go.Scatter3d(x = xb_low.tolist()+[None]+xb_up.tolist(),
                        y = yb_low.tolist()+[None]+yb_up.tolist(),
                        z = zb_low.tolist()+[None]+zb_up.tolist(),
                        mode ='lines',
                        line = dict(color='blue', width=2),
                        opacity =0.75, showlegend=False)

x2, y2, z2 = cylinder(r2, h2, Y=Yd, a=a2)  # driver, a flat disk

cyl2 = go.Surface(x=x2, y=y2, z=z2,
                 colorscale = colorscale,
                 showscale=False,
                 opacity=0.5)

xb_low, yb_low, zb_low = boundary_circle(r2, h=a2, Y=Yd)
xb_up, yb_up, zb_up = boundary_circle(r2, h=a2+h2, Y=Yd)

bcircles2 = go.Scatter3d(x = xb_low.tolist()+[None]+xb_up.tolist(),
                        y = yb_low.tolist()+[None]+yb_up.tolist(),
                        z = zb_low.tolist()+[None]+zb_up.tolist(),
                        mode ='lines',
                        line = dict(color='blue', width=2),
                        opacity =0.75, showlegend=False)

x_circle2, y_circle2, z_circle2 = circle(0, Yd, 0, r2)
endcap2 = go.Surface(x=x_circle2, y=y_circle2, z=z_circle2,
                     colorscale = colorscale,
                     showscale=False,
                     opacity=0.5)

box1 = go.Mesh3d(
        x = [-0.5*Lx, -0.5*Lx, 0.5*Lx, 0.5*Lx, -0.5*Lx, -0.5*Lx, 0.5*Lx, 0.5*Lx],
        y = [0, Ly, Ly, 0, 0, Ly, Ly, 0],
        z = [0, 0, 0, 0, -Lz, -Lz, -Lz, -Lz],
        opacity=0.6,
        color='#CCCCCC',        # light grey
        # i, j and k give the vertices of triangles for the box (cube)
        i = [7, 0, 0, 0, 4, 4, 6, 6, 4, 0, 3, 2],
        j = [3, 4, 1, 2, 5, 6, 5, 2, 0, 1, 6, 3],
        k = [0, 7, 2, 3, 6, 7, 1, 1, 5, 5, 7, 6],
        # without i,j,k the faces of the box are not visible
        flatshading=True
    )

#layout = go.Layout(scene_xaxis_visible=False, scene_yaxis_visible=False, scene_zaxis_visible=False)
#fig =  go.Figure(data=[box1, cyl1, bcircles1, cyl2, bcircles2, endcap2], layout=layout)
fig =  go.Figure(data=[box1, cyl1, bcircles1, cyl2, bcircles2, endcap2])

# Draw a smaller box in a larger coordinate system (make room for zoom), show axis text
fig.update_layout(scene=dict(
                      xaxis=dict(title='X Axis', range=[-0.75*Lx, 0.75*Lx]),
                      yaxis=dict(title='Y Axis', range=[-0.25*Ly, 1.25*Ly]),
                      zaxis=dict(title='Z Axis', range=[-1.25*Lz, 0.25*Lz])
                      )
                 )

camera = dict(                      # https://plotly.com/python/3d-camera-controls/
    up=dict(x=0, y=1, z=0),         # default z=1, we want y-axis to be up
    center=dict(x=0, y=0, z=0),     # default 0,0,0 (Origin)
    eye=dict(x=0.5, y=0.25, z=1.5)  # default 1.25 in all directions
)

fig.update_layout(scene_camera=camera,
                  yaxis = dict(scaleanchor = 'x'))  # keep x,y aspect ratio

fig.layout.scene.camera.projection.type = "orthographic"    # default "perspective"

app = Dash()
app.layout = html.Div([
    dcc.Graph(id='sketch',
              style={'height':'600px','width':'900p'},
              figure=fig)
])

app.run_server(debug=True)

This is the output:

In the upper right corner you see the icons for zoom, etc. and the HOME button as well as the CAMERA view button.

Adding FRONT view, SIDE view and TOP view would help users who are not familiar with rotating items using their computer mouse (people who are not fluent in CAD software). FreeCAD even has a Navigation Cube…

With kind regards,
Claus

Update to above (long) post. I understand the camera view can be modified as illustrated here:
https://plotly.com/python/3d-camera-controls/

My question is, how would I add face views (front, side and top) BUTTONS to my app?

With kind regards,
Claus

I guess asking yourself the right question (finding the right terminology) helps. I found code to add a drop-down menu and modify the view. Below is the code I have added, but ideally I’d like buttons up by the DASH Plotly upper right menu. To reduce the code, I only show the added code below (and a line above and below, to place the code in the previous code sample):

fig.layout.scene.camera.projection.type = "orthographic"    # default "perspective"

button0 = dict(method = "relayout",
               args = [{"scene.camera.projection.type": "orthographic",
                        "scene.camera.up": {'x':0, 'y':1, 'z':0},
                        "scene.camera.eye": {'x': 0.5, 'y': 0.25, 'z': 1.5}
                        }],
               label="Default view")

button1 = dict(method = "relayout",
               args = [{"scene.camera.projection.type": "orthographic",
                        "scene.camera.up": {'x':0, 'y':1, 'z':0},
                       "scene.camera.eye":{'x': 0, 'y': 0, 'z': 10}}], 
               label = "XY (Front) view")

button2 = dict(method = "relayout",
               args = [{"scene.camera.projection.type": "orthographic",
                        "scene.camera.up": {'x':0, 'y':1, 'z':0},
                       "scene.camera.eye":{'x': 10, 'y': 0, 'z': 0}}], 
               label = "YZ (Side) view")

fig.update_layout(width=700, height=700,
                   margin_b=80,
                   updatemenus=[dict(
                                 buttons=[button0, button1, button2])
                              ])

app = Dash()

As mentioned above, if you know how to get proper buttons instead of a drop down, or possibly something similar to the FreeCAD Navigation cube, I would be curious to hear about it.

With kind regards,
Claus

This is a difficult project, plotly is mainly a graphing library not designed for cad like capabilities. If I was in your position I would consider finding an existing react or javascript project and turning it into a dash component. Something like GitHub - jscad/OpenJSCAD.org: JSCAD is an open source set of modular, browser and command line tools for creating parametric 2D and 3D designs with JavaScript code. It provides a quick, precise and reproducible method for generating 3D models, and is especially useful for 3D printing applications.

Might be a good point to start, then look at:

1 Like

Hi PipInstallPython

Thank you for your response. Knowing there isn’t a solution is OK for me. At least now I can stop looking for it and accept the current drop-down menu is probably my best option.

With kind regards,
Claus

1 Like