Skip to content

Commit c3fbb9f

Browse files
committed
add group object
1 parent 41bb424 commit c3fbb9f

File tree

6 files changed

+337
-2
lines changed

6 files changed

+337
-2
lines changed

CHANGELOG.md

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

1010
### Added
1111

12+
* Added `compas_notebook.conversions.shapes_to_edgesbuffer`.
13+
* Added `compas_notebook.conversions.shapes_to_facesbuffer`.
14+
* Added `compas_notebook.scene.ThreeGroupObject`.
15+
1216
### Changed
1317

1418
### Removed

notebooks/80_groups.ipynb

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
{
2+
"cells": [
3+
{
4+
"cell_type": "code",
5+
"execution_count": 1,
6+
"metadata": {},
7+
"outputs": [],
8+
"source": [
9+
"# %%capture\n",
10+
"# %pip install compas_notebook"
11+
]
12+
},
13+
{
14+
"cell_type": "code",
15+
"execution_count": 2,
16+
"metadata": {},
17+
"outputs": [],
18+
"source": [
19+
"import random\n",
20+
"from compas.geometry import Box, Frame, Pointcloud\n",
21+
"from compas.colors import Color\n",
22+
"from compas_notebook import Viewer"
23+
]
24+
},
25+
{
26+
"cell_type": "code",
27+
"execution_count": 8,
28+
"metadata": {},
29+
"outputs": [],
30+
"source": [
31+
"cloud = Pointcloud.from_bounds(10, 10, 10, 1000)\n",
32+
"shapes = []\n",
33+
"for point in cloud:\n",
34+
" shape = Box(random.random(), random.random(), random.random(), frame=Frame(point))\n",
35+
" shapes.append(shape)"
36+
]
37+
},
38+
{
39+
"cell_type": "code",
40+
"execution_count": 10,
41+
"metadata": {},
42+
"outputs": [
43+
{
44+
"data": {
45+
"application/vnd.jupyter.widget-view+json": {
46+
"model_id": "4a9300cce349439abca62bb8ebae76ce",
47+
"version_major": 2,
48+
"version_minor": 0
49+
},
50+
"text/plain": [
51+
"VBox(children=(HBox(children=(Button(icon='search-plus', layout=Layout(height='32px', width='48px'), style=But…"
52+
]
53+
},
54+
"metadata": {},
55+
"output_type": "display_data"
56+
}
57+
],
58+
"source": [
59+
"viewer = Viewer()\n",
60+
"viewer.scene.clear()\n",
61+
"viewer.scene.add(shapes, color=Color.grey().lightened(50))\n",
62+
"viewer.show()"
63+
]
64+
},
65+
{
66+
"cell_type": "code",
67+
"execution_count": null,
68+
"metadata": {},
69+
"outputs": [],
70+
"source": []
71+
}
72+
],
73+
"metadata": {
74+
"kernelspec": {
75+
"display_name": "compas2",
76+
"language": "python",
77+
"name": "python3"
78+
},
79+
"language_info": {
80+
"codemirror_mode": {
81+
"name": "ipython",
82+
"version": 3
83+
},
84+
"file_extension": ".py",
85+
"mimetype": "text/x-python",
86+
"name": "python",
87+
"nbconvert_exporter": "python",
88+
"pygments_lexer": "ipython3",
89+
"version": "3.12.1"
90+
}
91+
},
92+
"nbformat": 4,
93+
"nbformat_minor": 2
94+
}

src/compas_notebook/conversions/__init__.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,9 @@
1717
from .meshes import vertices_and_faces_to_threejs
1818
from .meshes import vertices_to_threejs
1919

20+
from .buffers import shapes_to_edgesbuffer
21+
from .buffers import shapes_to_facesbuffer
22+
2023

2124
__all__ = [
2225
"box_to_threejs",
@@ -29,6 +32,8 @@
2932
"point_to_threejs",
3033
"pointcloud_to_threejs",
3134
"polyline_to_threejs",
35+
"shapes_to_edgesbuffer",
36+
"shapes_to_facesbuffer",
3237
"sphere_to_threejs",
3338
"torus_to_threejs",
3439
"vertices_and_edges_to_threejs",
Lines changed: 181 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,181 @@
1+
from typing import Union
2+
3+
import numpy as np
4+
import pythreejs as three
5+
from compas.colors import Color
6+
from compas.datastructures import Mesh
7+
from compas.geometry import Polygon
8+
from compas.geometry import Shape
9+
from compas.geometry import earclip_polygon
10+
11+
12+
class MeshShape:
13+
14+
def __init__(self, mesh: Mesh):
15+
self.mesh = mesh
16+
self._vertices = None
17+
self._edges = None
18+
self._faces = None
19+
20+
@property
21+
def vertices(self):
22+
if not self._vertices:
23+
self._vertices = self.mesh.vertices_attributes("xyz")
24+
return self._vertices
25+
26+
@property
27+
def edges(self):
28+
if not self._edges:
29+
self._edges = list(self.mesh.edges())
30+
return self._edges
31+
32+
@property
33+
def faces(self):
34+
if not self._faces:
35+
self._faces = [self.mesh.face_vertices(face) for face in self.mesh.faces()]
36+
return self._faces
37+
38+
39+
def shapes_to_edgesbuffer(
40+
shapes: list[Mesh, Shape],
41+
color: Color,
42+
) -> three.LineSegments:
43+
"""Convert the combined edges of a collection of shapes to one line segment buffer.
44+
45+
Parameters
46+
----------
47+
shapes : list[:class:`compas.datastructures.Mesh`, :class:`compas.geometry.Shape`]
48+
The shape collection.
49+
color : :class:`compas.colors.Color`
50+
The color of the edges.
51+
52+
Returns
53+
-------
54+
pythreejs.LineSegments
55+
56+
"""
57+
positions = []
58+
colors = []
59+
60+
for shape in shapes:
61+
buffer = shape_to_edgesbuffer(shape, color)
62+
positions += buffer[0]
63+
colors += buffer[1]
64+
65+
positions = np.array(positions, dtype=np.float32)
66+
colors = np.array(colors, dtype=np.float32)
67+
68+
geometry = three.BufferGeometry(
69+
attributes={
70+
"position": three.BufferAttribute(positions, normalized=False),
71+
"color": three.BufferAttribute(colors, normalized=False, itemSize=3),
72+
}
73+
)
74+
75+
material = three.LineBasicMaterial(vertexColors="VertexColors")
76+
77+
return three.LineSegments(geometry, material)
78+
79+
80+
def shapes_to_facesbuffer(
81+
shapes: list[Mesh],
82+
color: Color,
83+
) -> three.Mesh:
84+
"""Convert the combined faces of a collection of shapes to one mesh buffer.
85+
86+
Parameters
87+
----------
88+
shapes : list[:class:`compas.datastructures.Mesh`, :class:`compas.geometry.Shape`]
89+
The shape collection.
90+
color : :class:`compas.colors.Color`
91+
The color of the faces.
92+
93+
Returns
94+
-------
95+
pythreejs.Mesh
96+
97+
"""
98+
positions = []
99+
colors = []
100+
101+
for shape in shapes:
102+
buffer = shape_to_facesbuffer(shape, color)
103+
positions += buffer[0]
104+
colors += buffer[1]
105+
106+
positions = np.array(positions, dtype=np.float32)
107+
colors = np.array(colors, dtype=np.float32)
108+
109+
geometry = three.BufferGeometry(
110+
attributes={
111+
"position": three.BufferAttribute(positions, normalized=False),
112+
"color": three.BufferAttribute(colors, normalized=False, itemSize=3),
113+
}
114+
)
115+
116+
material = three.MeshBasicMaterial(
117+
side="DoubleSide",
118+
vertexColors="VertexColors",
119+
)
120+
121+
return three.Mesh(geometry, material)
122+
123+
124+
def shape_to_edgesbuffer(shape: Union[Mesh, Shape], color: Color) -> tuple[list[list[float]], list[Color]]:
125+
positions = []
126+
colors = []
127+
128+
if isinstance(shape, Mesh):
129+
shape = MeshShape(shape)
130+
131+
for u, v in shape.edges:
132+
positions.append(shape.vertices[u])
133+
positions.append(shape.vertices[v])
134+
colors.append(color)
135+
colors.append(color)
136+
137+
return positions, colors
138+
139+
140+
def shape_to_facesbuffer(shape: Union[Mesh, Shape], color: Color) -> tuple[list[list[float]], list[Color]]:
141+
positions = []
142+
colors = []
143+
144+
if isinstance(shape, Mesh):
145+
shape = MeshShape(shape)
146+
147+
for face in shape.faces:
148+
149+
if len(face) == 3:
150+
positions.append(shape.vertices[face[0]])
151+
positions.append(shape.vertices[face[1]])
152+
positions.append(shape.vertices[face[2]])
153+
colors.append(color)
154+
colors.append(color)
155+
colors.append(color)
156+
157+
elif len(face) == 4:
158+
positions.append(shape.vertices[face[0]])
159+
positions.append(shape.vertices[face[1]])
160+
positions.append(shape.vertices[face[2]])
161+
colors.append(color)
162+
colors.append(color)
163+
colors.append(color)
164+
positions.append(shape.vertices[face[0]])
165+
positions.append(shape.vertices[face[2]])
166+
positions.append(shape.vertices[face[3]])
167+
colors.append(color)
168+
colors.append(color)
169+
colors.append(color)
170+
171+
else:
172+
ears = earclip_polygon(Polygon([shape.vertices[v] for v in face]))
173+
for ear in ears:
174+
positions.append(shape.vertices[ear[0]])
175+
positions.append(shape.vertices[ear[1]])
176+
positions.append(shape.vertices[ear[2]])
177+
colors.append(color)
178+
colors.append(color)
179+
colors.append(color)
180+
181+
return positions, colors

src/compas_notebook/scene/__init__.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,8 @@
4545
from .graphobject import ThreeGraphObject
4646
from .meshobject import ThreeMeshObject
4747

48+
from .groupobject import ThreeGroupObject
49+
4850

4951
@plugin(category="drawing-utils", pluggable_name="clear", requires=["pythreejs"])
5052
def clear_pythreejs(guids=None):
@@ -82,8 +84,7 @@ def register_scene_objects():
8284
register(Sphere, ThreeSphereObject, context="Notebook")
8385
register(Torus, ThreeTorusObject, context="Notebook")
8486
register(Mesh, ThreeMeshObject, context="Notebook")
85-
86-
# print("PyThreeJS SceneObjects registered.")
87+
register(list, ThreeGroupObject, context="Notebook")
8788

8889

8990
__all__ = [
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
from compas.colors import Color
2+
from compas.data import Data
3+
4+
from compas_notebook.conversions import shapes_to_edgesbuffer
5+
from compas_notebook.conversions import shapes_to_facesbuffer
6+
7+
from .sceneobject import SceneObject
8+
9+
10+
class Group(Data):
11+
"""A group of compas.data.Data items."""
12+
13+
def __init__(self, items: list[Data] = None):
14+
super().__init__()
15+
self.items = items or []
16+
17+
@property
18+
def __data__(self):
19+
return {"items": self.items}
20+
21+
22+
class ThreeGroupObject(SceneObject):
23+
"""A group of scene objects."""
24+
25+
item: Group
26+
27+
def __init__(self, item=None, **kwargs):
28+
super().__init__(item=Group(item), **kwargs)
29+
self.show = True
30+
self.is_selected = False
31+
self.opacity = 1.0
32+
self.bounding_box = None
33+
34+
@property
35+
def items(self) -> list:
36+
return self.item.items
37+
38+
def init(self, *args, **kwargs):
39+
pass
40+
41+
def draw(self, *args, **kwargs):
42+
self._guids = []
43+
edgesbuffer = shapes_to_edgesbuffer(self.items, Color(0.2, 0.2, 0.2))
44+
facesbuffer = shapes_to_facesbuffer(self.items, self.color)
45+
self._guids.append(edgesbuffer)
46+
self._guids.append(facesbuffer)
47+
return self._guids
48+
49+
def draw_instance(self, *args, **kwargs):
50+
pass

0 commit comments

Comments
 (0)