Skip to content

Commit 828cf59

Browse files
thisistheplaceBen CannellBen Cannell
authored
Allow user input of json file or free text json (#5)
* Refactoring and started on sidepanel * Improved layout to include side panel * Re-worked endpoints and added restapi landing page * Updated home page * Removed unused directories * Added free text input * Added file upload * Homepage css update Co-authored-by: Ben Cannell <[email protected]> Co-authored-by: Ben Cannell <[email protected]>
1 parent c660f5a commit 828cf59

34 files changed

+720
-203
lines changed

docker-compose.yml

+6-2
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,10 @@ version: '3.7'
22

33
services:
44
fastapi:
5-
container_name: jointbuilder
5+
container_name: jointrestapi
6+
environment:
7+
- RESTAPI_URL=http://127.0.0.1:8000
8+
- VIEWER_URL=http://127.0.0.1:8050
69
restart: always
710
build: .
811
expose:
@@ -14,7 +17,8 @@ services:
1417
viewer:
1518
container_name: jointviewer
1619
environment:
17-
- VTK_MESHER_URL=http://host.docker.internal:8000
20+
- RESTAPI_URL=http://host.docker.internal:8000
21+
- VIEWER_URL=http://host.docker.internal:8050
1822
restart: always
1923
build: .
2024
expose:

model.json

+41
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
{
2+
"name": "TJoint",
3+
"joint": {
4+
"name": "TJoint",
5+
"tubes": [
6+
{
7+
"name": "Vertical",
8+
"axis": {
9+
"point": {
10+
"x": 0.0,
11+
"y": 0.0,
12+
"z": 0.0
13+
},
14+
"vector": {
15+
"x": 0.0,
16+
"y": 0.0,
17+
"z": 5.0
18+
}
19+
},
20+
"diameter": 0.5
21+
},
22+
{
23+
"name": "Horizontal",
24+
"axis": {
25+
"point": {
26+
"x": 0.0,
27+
"y": 0.0,
28+
"z": 3.0
29+
},
30+
"vector": {
31+
"x": 3.0,
32+
"y": 0.0,
33+
"z": 0.0
34+
}
35+
},
36+
"diameter": 0.25
37+
}
38+
],
39+
"origin": null
40+
}
41+
}

src/app/constants.py

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
# Set as an environment variable
2+
RESTAPI_URL = "RESTAPI_URL"
3+
VIEWER_URL = "VIEWER_URL"

src/app/viewer/models.py src/app/interfaces/examples/joints.py

+4-4
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
"""Demo models to use for testing"""
2-
from ..interfaces import *
2+
from ...interfaces import *
33

4-
DEMO_MODELS = {
4+
EXAMPLE_JOINTS = {
55
"TJoint": Joint(
66
name="TJoint",
77
tubes=[
@@ -60,5 +60,5 @@
6060
diameter=0.4,
6161
),
6262
],
63-
)
64-
}
63+
),
64+
}

src/app/interfaces/geometry.py

+12
Original file line numberDiff line numberDiff line change
@@ -16,3 +16,15 @@ class Vector3D(BaseModel):
1616
class Axis3D(BaseModel):
1717
point: Point3D = ...
1818
vector: Vector3D = ...
19+
20+
21+
class Tubular(BaseModel):
22+
name: str = ...
23+
axis: Axis3D = ...
24+
diameter: float = ...
25+
26+
27+
class Joint(BaseModel):
28+
name: str = ...
29+
tubes: list[Tubular] = ...
30+
origin: Point3D | None = None

src/app/interfaces/model.py

+9-10
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,14 @@
11
from pydantic import BaseModel
22

3-
from .geometry import Axis3D, Point3D
3+
from .geometry import Joint
4+
from .examples.joints import EXAMPLE_JOINTS
45

56

6-
class Tubular(BaseModel):
7+
class Model(BaseModel):
78
name: str = ...
8-
axis: Axis3D = ...
9-
diameter: float = ...
10-
11-
12-
class Joint(BaseModel):
13-
name: str = ...
14-
tubes: list[Tubular] = ...
15-
origin: Point3D | None = None
9+
joint: Joint = ...
10+
class Config:
11+
schema_extra = {
12+
"name": "TJoint",
13+
"joint": EXAMPLE_JOINTS["TJoint"].json()
14+
}

src/app/interfaces/validation.py

+10
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import json
2+
from typing import Any
3+
4+
def validate_json(json_str: str, check_type: Any) -> dict:
5+
if type(json_str) is str:
6+
json_data = json.loads(json_str)
7+
else:
8+
json_data = json_str
9+
check_type.validate(json_data)
10+
return json_data

src/app/modelling/mesher/cylinder.py

+4-2
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,14 @@
11
import gmsh
22
import math
33

4-
from app.interfaces.model import Tubular
4+
from app.interfaces import Tubular
55

66
from .vectors import angle_between
77
from ...interfaces.geometry import *
88

99
FACTORY = gmsh.model.occ
1010

11+
1112
def rotatexy(dimTags: list[tuple[int, int]], origin: Point3D, vector: Vector3D):
1213
xangle = angle_between([0, 0, 1], [0, vector.y, vector.z])
1314
FACTORY.rotate(dimTags, origin.x, origin.y, origin.z, 1, 0, 0, xangle)
@@ -19,6 +20,7 @@ def rotatexy(dimTags: list[tuple[int, int]], origin: Point3D, vector: Vector3D):
1920
print(vector)
2021
print(xangle, yangle)
2122

23+
2224
def add_tube(tube: Tubular) -> tuple[int, int]:
2325
origin = tube.axis.point
2426
vector = tube.axis.vector
@@ -40,7 +42,7 @@ def add_tube(tube: Tubular) -> tuple[int, int]:
4042
print(ring)
4143
rotatexy([(1, ring)], origin, vector)
4244
pipe = FACTORY.addPipe([(1, ring)], wire)
43-
45+
4446
# We delete the source surface, and increase the number of sub-edges for a
4547
# nicer display of the geometry:
4648
FACTORY.remove([(1, ring)])

src/app/modelling/mesher/mesh.py

+6-1
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,12 @@
99

1010
FACTORY = gmsh.model.occ
1111

12+
1213
def mesh_tubular(tube: Tubular) -> tuple[int, int]:
1314
"""Adds tubular geometry and returns tag id"""
1415
return add_cylinder(tube)
1516

17+
1618
def mesh_joint(joint: Joint) -> dict[str, tuple[int, int]]:
1719
joint_mesh = {tube.name: mesh_tubular(tube) for tube in joint.tubes}
1820
FACTORY.synchronize()
@@ -21,6 +23,7 @@ def mesh_joint(joint: Joint) -> dict[str, tuple[int, int]]:
2123
gmsh.model.setPhysicalName(dim, gid, k)
2224
return joint_mesh
2325

26+
2427
@contextmanager
2528
def mesh_model(joint: Joint) -> gmsh.model.mesh:
2629
try:
@@ -37,7 +40,9 @@ def mesh_model(joint: Joint) -> gmsh.model.mesh:
3740
FACTORY.synchronize()
3841
if len(intersect[0]) > 0:
3942
# if there is an intersection, do what you want to do.
40-
FACTORY.remove(intersect[0], True) # remove created intersection objects
43+
FACTORY.remove(
44+
intersect[0], True
45+
) # remove created intersection objects
4146
FACTORY.synchronize()
4247
else:
4348
pass

src/app/modelling/mesher/vectors.py

+11-9
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,24 @@
11
import numpy as np
22

3+
34
def unit_vector(vector):
4-
""" Returns the unit vector of the vector. """
5+
"""Returns the unit vector of the vector."""
56
return vector / np.linalg.norm(vector)
67

8+
79
def angle_between(v1, v2):
8-
""" Returns the angle in radians between vectors 'v1' and 'v2'::
10+
"""Returns the angle in radians between vectors 'v1' and 'v2'::
911
10-
>>> angle_between((1, 0, 0), (0, 1, 0))
11-
1.5707963267948966
12-
>>> angle_between((1, 0, 0), (1, 0, 0))
13-
0.0
14-
>>> angle_between((1, 0, 0), (-1, 0, 0))
15-
3.141592653589793
12+
>>> angle_between((1, 0, 0), (0, 1, 0))
13+
1.5707963267948966
14+
>>> angle_between((1, 0, 0), (1, 0, 0))
15+
0.0
16+
>>> angle_between((1, 0, 0), (-1, 0, 0))
17+
3.141592653589793
1618
"""
1719
v1_u = unit_vector(v1)
1820
v2_u = unit_vector(v2)
1921
angle = np.arccos(np.clip(np.dot(v1_u, v2_u), -1.0, 1.0))
2022
if np.isnan(angle):
2123
return 0.0
22-
return angle
24+
return angle

src/app/server/dependencies/security.py

Whitespace-only changes.

src/app/server/routers/home.py

+87
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
from fastapi import APIRouter, BackgroundTasks
2+
from fastapi.responses import HTMLResponse
3+
4+
import requests
5+
import os
6+
7+
from app.constants import VIEWER_URL, RESTAPI_URL
8+
9+
router = APIRouter()
10+
11+
def ping_viewer():
12+
requests.get(VIEWER_URL)
13+
14+
@router.get("/", response_class=HTMLResponse)
15+
async def root(background_tasks: BackgroundTasks):
16+
# Wake up viewer service by pinging it
17+
background_tasks.add_task(ping_viewer)
18+
19+
url_example = f"{os.environ[RESTAPI_URL]}/examples/TJoint"
20+
url_docs = f"{os.environ[RESTAPI_URL]}/docs"
21+
url_viewer = f"{os.environ[VIEWER_URL]}"
22+
html_content = f"""
23+
<html>
24+
<head>
25+
<title>Joint Meshing REST API</title>
26+
<!--using same theme as Dash Flatly-->
27+
<link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/flatly/bootstrap.min.css" rel="stylesheet">
28+
<style>
29+
body {{
30+
padding: 80px
31+
}}
32+
h1 {{
33+
padding-bottom: 40px
34+
}}
35+
.padbutton {{
36+
padding-top: 5px;
37+
padding-bottom: 5px;
38+
}}
39+
.credits {{
40+
display: block;
41+
position: absolute;
42+
right: 0px;
43+
bottom: 0px;
44+
padding: 40px;
45+
}}
46+
</style>
47+
</head>
48+
<body>
49+
<h1 sty>Joint Meshing REST API</h1>
50+
<div class="row">
51+
<div class="col-lg-4">
52+
<div class="card text-white bg-primary mb-3" style="max-width: 20rem; height: 100%;">
53+
<div class="card-header">RestAPI</div>
54+
<div class="card-body">
55+
<h4 class="card-title">Create joint meshes</h4>
56+
<p class="card-text">A REST API is available to generate joint meshes from json input.</p>
57+
<p class="card-text">This REST API is developed using <a href='https://fastapi.tiangolo.com'>FastAPI</a>.</p>
58+
<div class="padbutton">
59+
<button type="button" onclick="location.href='{url_docs}'" class="btn btn-success btn-lg">Docs</button>
60+
</div>
61+
<div class="padbutton">
62+
<button type="button" onclick="location.href='{url_example}'" class="btn btn-success btn-lg">Example response</button>
63+
</div>
64+
</div>
65+
</div>
66+
</div>
67+
<div class="col-lg-4">
68+
<div class="card text-white bg-primary mb-3" style="max-width: 20rem; height: 100%;">
69+
<div class="card-header">Viewer</div>
70+
<div class="card-body">
71+
<h4 class="card-title">View joint meshes</h4>
72+
<p class="card-text">You can create <b>and</b> view joint meshes using a user interface. This uses the REST API under the hood.</p>
73+
<p class="card-text">This viewer is developed using <a href='https://dash.plotly.com/vtk'>Dash VTK</a>.</p>
74+
<div class="padbutton">
75+
<button type="button" onclick="location.href='{url_viewer}'" class="btn btn-success btn-lg">Viewer</button>
76+
</div>
77+
</div>
78+
</div>
79+
</div>
80+
</div>
81+
<div class="credits">
82+
<span class="badge bg-light"><i>Templates by <a href='https://bootswatch.com/flatly'>https://bootswatch.com/flatly</a> :)</span>
83+
</div>
84+
</body>
85+
</html>
86+
"""
87+
return HTMLResponse(content=html_content, status_code=200)

src/app/server/routers/meshing.py

+45
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
from fastapi import APIRouter, HTTPException
2+
from fastapi.responses import StreamingResponse
3+
4+
import io
5+
import json
6+
7+
from app.converters.encoder import NpEncoder
8+
from app.interfaces.examples.joints import EXAMPLE_JOINTS
9+
from app.server.worker.worker import run_job, Worker
10+
from app.server.worker.job import Job
11+
12+
from ...interfaces import Joint, Model
13+
from ...interfaces.examples.joints import EXAMPLE_JOINTS
14+
15+
router = APIRouter()
16+
# get worker singleton
17+
worker = Worker()
18+
19+
def do_meshing(joint: Joint) -> StreamingResponse:
20+
job = Job(joint)
21+
try:
22+
job = run_job(worker, job)
23+
24+
def iterfile():
25+
with io.StringIO() as file_like:
26+
json.dump(job.mesh, file_like, cls=NpEncoder)
27+
file_like.seek(0)
28+
yield from file_like
29+
30+
return StreamingResponse(iterfile(), media_type="application/json")
31+
except Exception as e:
32+
raise HTTPException(status_code=500, detail=f"Error while generating mesh: {e}")
33+
34+
@router.get("/examples/{jointname}")
35+
def mesh_example(jointname: str):
36+
if jointname not in EXAMPLE_JOINTS:
37+
raise HTTPException(
38+
status_code=404, detail=f"Joint model {jointname} not found"
39+
)
40+
return do_meshing(EXAMPLE_JOINTS[jointname])
41+
42+
@router.post("/meshmodel")
43+
def mesh_model(model: Model):
44+
# do some validation here!
45+
return do_meshing(model.joint)
File renamed without changes.

0 commit comments

Comments
 (0)