Skip to content

Commit 1f607f1

Browse files
committed
Add box
1 parent c192535 commit 1f607f1

3 files changed

Lines changed: 145 additions & 2 deletions

File tree

docs/tutorial.md

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,67 @@ between `line.start` and `line.end`.
5858
Points (and mesh vertices and graph nodes) are drawn with a fixed on-screen size
5959
in points, so they do not grow or shrink when you zoom.
6060

61+
## 3D shapes
62+
63+
3D shapes are drawn as their projection onto the XY plane. A `Box`, for example,
64+
is projected face by face, so it reads as a solid wireframe regardless of how its
65+
frame is oriented.
66+
67+
```python
68+
from math import radians
69+
from compas.geometry import Box, Frame
70+
from compas_plotters import Plotter
71+
72+
plotter = Plotter(figsize=(8, 5))
73+
74+
# an axis-aligned box projects to a rectangle
75+
plotter.add(Box(3, 2, 1))
76+
77+
# a rotated box keeps its 3D silhouette
78+
frame = Frame([6, 0, 0]).rotated(radians(35), [1, 1, 1], point=[6, 0, 0])
79+
plotter.add(Box(2, 2, 2, frame=frame), facecolor=(0.9, 0.9, 1.0), alpha=0.3)
80+
81+
plotter.zoom_extents()
82+
plotter.show()
83+
```
84+
85+
Pass `fill=False` for a pure wireframe, or tune `alpha` and `facecolor` to shade
86+
the faces. Because a projection has no depth sorting, faces are not hidden behind
87+
one another — every edge stays visible.
88+
89+
### Rotating box animation
90+
91+
A good way to confirm the projection is spinning the box and watching the
92+
silhouette update every frame. Rotate the geometry inside a
93+
[`Plotter.on`][compas_plotters.Plotter.on] callback (see
94+
[Dynamic plots and animations](#dynamic-plots-and-animations)) and the plotter
95+
reprojects and redraws it:
96+
97+
```python
98+
from math import radians
99+
from compas.geometry import Box, Frame, Rotation
100+
from compas_plotters import Plotter
101+
102+
plotter = Plotter(figsize=(6, 6))
103+
104+
box = Box(2, 2, 2, frame=Frame([0, 0, 0]).rotated(radians(35), [1, 1, 1]))
105+
plotter.add(box, facecolor=(0.9, 0.9, 1.0), alpha=0.3)
106+
107+
# keep the view fixed so the box is seen to rotate, not the camera
108+
plotter.zoom_extents()
109+
110+
rotation = Rotation.from_axis_and_angle([0, 1, 0], radians(6), point=[0, 0, 0])
111+
112+
@plotter.on(interval=0.05, frames=60, record=True, recording="box.gif")
113+
def spin(frame):
114+
box.transform(rotation)
115+
116+
plotter.show()
117+
```
118+
119+
Each frame applies a small rotation about the Z axis; the six faces are
120+
re-projected onto the XY plane, so the wireframe appears to turn in 3D.
121+
61122
## Meshes
62123

63124
Meshes reuse the data layer of the COMPAS scene system, so the `show_vertices`,

src/compas_plotters/scene/__init__.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010

1111
from compas.datastructures import Graph
1212
from compas.datastructures import Mesh
13+
from compas.geometry import Box
1314
from compas.geometry import Circle
1415
from compas.geometry import Ellipse
1516
from compas.geometry import Frame
@@ -21,6 +22,7 @@
2122
from compas.plugins import plugin
2223
from compas.scene import register
2324

25+
from .boxobject import BoxObject
2426
from .circleobject import CircleObject
2527
from .ellipseobject import EllipseObject
2628
from .frameobject import FrameObject
@@ -46,6 +48,7 @@ def register_scene_objects():
4648
register(Circle, CircleObject, context="Plotter")
4749
register(Ellipse, EllipseObject, context="Plotter")
4850
register(Frame, FrameObject, context="Plotter")
51+
register(Box, BoxObject, context="Plotter")
4952
register(Mesh, MeshObject, context="Plotter")
5053
register(Graph, GraphObject, context="Plotter")
5154

@@ -54,7 +57,6 @@ def register_scene_objects():
5457
# keeps working until then, and lights up automatically when the objects (and
5558
# any optional dependencies, e.g. compas_occ for Breps) become available.
5659
try:
57-
from compas.geometry import Box
5860
from compas.geometry import Capsule
5961
from compas.geometry import Cone
6062
from compas.geometry import Cylinder
@@ -64,7 +66,7 @@ def register_scene_objects():
6466

6567
from .shapeobject import ShapeObject
6668

67-
for shape_cls in (Box, Sphere, Cylinder, Cone, Capsule, Torus, Polyhedron):
69+
for shape_cls in (Sphere, Cylinder, Cone, Capsule, Torus, Polyhedron):
6870
register(shape_cls, ShapeObject, context="Plotter")
6971
except ImportError:
7072
pass
@@ -86,6 +88,7 @@ def register_scene_objects():
8688
"CircleObject",
8789
"EllipseObject",
8890
"FrameObject",
91+
"BoxObject",
8992
"MeshObject",
9093
"GraphObject",
9194
]
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
from __future__ import annotations
2+
3+
from typing import Sequence
4+
5+
from compas.colors import Color
6+
from compas.geometry import Box
7+
from compas.scene import GeometryObject
8+
from matplotlib.patches import Polygon as PolygonPatch
9+
10+
from .plotterobject import PlotterSceneObject
11+
from .plotterobject import to_rgb
12+
13+
14+
class BoxObject(PlotterSceneObject, GeometryObject):
15+
"""Plotter scene object for :class:`compas.geometry.Box`.
16+
17+
The box is drawn as its projection onto the XY plane: each of the six faces
18+
is projected to a semi-transparent polygon and the twelve edges are drawn on
19+
top, so the box reads as a solid regardless of its orientation.
20+
21+
Parameters
22+
----------
23+
linewidth
24+
Width of the edges.
25+
linestyle
26+
Matplotlib line style (``"solid"``, ``"dotted"``, ``"dashed"``, ``"dashdot"``).
27+
facecolor
28+
Color of the faces.
29+
edgecolor
30+
Color of the edges.
31+
fill
32+
If True, fill the faces.
33+
alpha
34+
Transparency of the faces, between 0 and 1.
35+
"""
36+
37+
def __init__(
38+
self,
39+
linewidth: float = 1.0,
40+
linestyle: str = "solid",
41+
facecolor: Color | Sequence[float] = (0.9, 0.9, 1.0),
42+
edgecolor: Color | Sequence[float] = (0.0, 0.0, 0.0),
43+
fill: bool = True,
44+
alpha: float = 0.3,
45+
**kwargs,
46+
) -> None:
47+
super().__init__(zorder=1000, **kwargs)
48+
self.linewidth = linewidth
49+
self.linestyle = linestyle
50+
self.facecolor = facecolor
51+
self.edgecolor = edgecolor
52+
self.fill = fill
53+
self.alpha = alpha
54+
55+
@property
56+
def box(self) -> Box:
57+
return self.geometry # type: ignore[return-value]
58+
59+
def viewdata(self) -> list[list[float]]:
60+
return [list(point[:2]) for point in self.box.points]
61+
62+
def draw(self) -> list:
63+
points = self.box.points
64+
# Bake the transparency into the face color only, so the edges stay solid
65+
# even where projected faces overlap.
66+
facecolor = (*to_rgb(self.facecolor), self.alpha) if self.fill else "none"
67+
self._mpl_objects = []
68+
for face in self.box.faces:
69+
polygon = PolygonPatch(
70+
[list(points[index][:2]) for index in face],
71+
linewidth=self.linewidth,
72+
linestyle=self.linestyle,
73+
facecolor=facecolor,
74+
edgecolor=to_rgb(self.edgecolor),
75+
fill=self.fill,
76+
zorder=self.zorder,
77+
)
78+
self._mpl_objects.append(self.axes.add_patch(polygon))
79+
return self._mpl_objects

0 commit comments

Comments
 (0)