From 1014b42f9fa97e57dc26ba8e698f1f8b50e0e2f7 Mon Sep 17 00:00:00 2001 From: Jelle Feringa Date: Wed, 19 Nov 2025 12:33:03 +0100 Subject: [PATCH 01/10] Adds support for rendering frames in the Notebook Viewer Introduces functionality to visualize frames as 3D objects in the Notebook Viewer. - Adds a `frame_to_threejs` conversion utility for transforming frames into PyThreeJS objects. - Implements `ThreeFrameObject` for rendering frames in scenes, with axes visualized in red, green, and blue. - Updates the registration process to include frame objects in the Notebook Viewer. - Enhances the example notebook to demonstrate frame rendering capabilities. - Fixes a type annotation issue in `line_to_threejs` and improves the `draw` method's return type annotations for consistency. These changes enhance the visualization capabilities of the Notebook Viewer and improve type safety in the codebase. --- notebooks/20_geometry.ipynb | 21 +++++++++--- src/compas_notebook/conversions/geometry.py | 38 ++++++++++++++++++++- src/compas_notebook/scene/__init__.py | 37 +++++++++++--------- src/compas_notebook/scene/frameobject.py | 25 ++++++++++++++ src/compas_notebook/scene/lineobject.py | 4 +-- 5 files changed, 100 insertions(+), 25 deletions(-) create mode 100644 src/compas_notebook/scene/frameobject.py diff --git a/notebooks/20_geometry.ipynb b/notebooks/20_geometry.ipynb index 1ec2557..04c8c7b 100644 --- a/notebooks/20_geometry.ipynb +++ b/notebooks/20_geometry.ipynb @@ -6,7 +6,7 @@ "metadata": {}, "outputs": [], "source": [ - "%pip install -q compas_notebook" + "# /%pip install -q compas_notebook" ] }, { @@ -20,6 +20,7 @@ "from compas.geometry import Point\n", "from compas.geometry import Pointcloud\n", "from compas.geometry import Polyline\n", + "from compas.geometry import Frame \n", "from compas_notebook.viewer import Viewer" ] }, @@ -33,7 +34,16 @@ "\n", "point = Point(-1, 2, 3)\n", "line = Line([0, 0, 0], point)\n", - "polyline = Polyline(cloud.points)" + "polyline = Polyline(cloud.points)\n", + "\n", + "\n", + "frame = Frame(point, [1, 0, 0], [0, 1, 0])\n", + "\n", + "# x_line = Line(frame.point, frame.point+frame.xaxis)\n", + "# y_line = Line(frame.point, frame.point+frame.yaxis)\n", + "# z_line = Line(frame.point, frame.point+frame.zaxis)\n", + "\n", + "\n" ] }, { @@ -44,7 +54,7 @@ { "data": { "application/vnd.jupyter.widget-view+json": { - "model_id": "a55820df515f4eb0a67494e16869aac2", + "model_id": "60cefc1deea3458c9e1a390cdd0ea3ba", "version_major": 2, "version_minor": 0 }, @@ -61,6 +71,7 @@ "\n", "viewer.scene.add(point, color=Color.red(), pointsize=0.3)\n", "viewer.scene.add(line)\n", + "viewer.scene.add(frame)\n", "viewer.scene.add(polyline, color=Color.blue())\n", "viewer.scene.add(cloud, color=Color.green(), pointsize=0.3)\n", "\n", @@ -70,7 +81,7 @@ ], "metadata": { "kernelspec": { - "display_name": "compas2", + "display_name": "compas_opzuid", "language": "python", "name": "python3" }, @@ -84,7 +95,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.12.1" + "version": "3.12.12" } }, "nbformat": 4, diff --git a/src/compas_notebook/conversions/geometry.py b/src/compas_notebook/conversions/geometry.py index b13b31f..5d2d1f5 100644 --- a/src/compas_notebook/conversions/geometry.py +++ b/src/compas_notebook/conversions/geometry.py @@ -1,3 +1,4 @@ +import re import numpy import pythreejs as three from compas.geometry import Box @@ -8,9 +9,11 @@ from compas.geometry import Polyline from compas.geometry import Sphere from compas.geometry import Torus +from compas.geometry import Frame +from compas.geometry import Line -def line_to_threejs(line: Point) -> three.BufferGeometry: +def line_to_threejs(line: Line) -> three.BufferGeometry: """Convert a COMPAS line to PyThreeJS. Parameters @@ -28,6 +31,39 @@ def line_to_threejs(line: Point) -> three.BufferGeometry: return geometry +def frame_to_threejs(frame: Frame) -> list[three.BufferGeometry]: + """Convert a COMPAS frame to PyThreeJS. + + Parameters + ---------- + frame : :class:`compas.geometry.Frame` + The frame to convert. + + Returns + ------- + list[three.BufferGeometry] + + """ + + # create lines for each axis + _x_line = Line(frame.point, frame.point + frame.xaxis) + _y_line = Line(frame.point, frame.point + frame.yaxis) + _z_line = Line(frame.point, frame.point + frame.zaxis) + + # convert lines to threejs vertex buffers + xline_verts = line_to_threejs(_x_line) + yline_verts = line_to_threejs(_y_line) + zline_verts = line_to_threejs(_z_line) + + # convert from vertex buffers to line objects + xline_lines = three.Line(xline_verts, three.LineBasicMaterial(color="red")) + yline_lines = three.Line(yline_verts, three.LineBasicMaterial(color="green")) + zline_lines = three.Line(zline_verts, three.LineBasicMaterial(color="blue")) + + result = [xline_lines, yline_lines, zline_lines] + return result + + def point_to_threejs(point: Point) -> three.SphereGeometry: """Convert a COMPAS point to PyThreeJS. diff --git a/src/compas_notebook/scene/__init__.py b/src/compas_notebook/scene/__init__.py index a1d967b..7c770e3 100644 --- a/src/compas_notebook/scene/__init__.py +++ b/src/compas_notebook/scene/__init__.py @@ -13,6 +13,7 @@ from compas.geometry import Cone from compas.geometry import Cylinder from compas.geometry import Line +from compas.geometry import Frame from compas.geometry import Point from compas.geometry import Pointcloud from compas.geometry import Polygon @@ -46,6 +47,7 @@ from .meshobject import ThreeMeshObject from .groupobject import ThreeGroupObject +from .frameobject import ThreeFrameObject @plugin(category="drawing-utils", pluggable_name="clear", requires=["pythreejs"]) @@ -75,6 +77,7 @@ def register_scene_objects(): register(Cone, ThreeConeObject, context="Notebook") register(Cylinder, ThreeCylinderObject, context="Notebook") register(Graph, ThreeGraphObject, context="Notebook") + register(Frame, ThreeFrameObject, context="Notebook") register(Line, ThreeLineObject, context="Notebook") register(Point, ThreePointObject, context="Notebook") register(Pointcloud, ThreePointcloudObject, context="Notebook") @@ -86,20 +89,20 @@ def register_scene_objects(): register(Mesh, ThreeMeshObject, context="Notebook") register(list, ThreeGroupObject, context="Notebook") - -__all__ = [ - "NotebookScene", - "ThreeBoxObject", - "ThreeCapsuleObject", - "ThreeConeObject", - "ThreeCylinderObject", - "ThreeGraphObject", - "ThreePointObject", - "ThreePointcloudObject", - "ThreePolygonObject", - "ThreePolyhedronObject", - "ThreePolylineObject", - "ThreeSceneObject", - "ThreeSphereObject", - "ThreeTorusObject", -] +# # yuck java +# __all__ = [ +# "NotebookScene", +# "ThreeBoxObject", +# "ThreeCapsuleObject", +# "ThreeConeObject", +# "ThreeCylinderObject", +# "ThreeGraphObject", +# "ThreePointObject", +# "ThreePointcloudObject", +# "ThreePolygonObject", +# "ThreePolyhedronObject", +# "ThreePolylineObject", +# "ThreeSceneObject", +# "ThreeSphereObject", +# "ThreeTorusObject", +# ] diff --git a/src/compas_notebook/scene/frameobject.py b/src/compas_notebook/scene/frameobject.py new file mode 100644 index 0000000..cf7e1b7 --- /dev/null +++ b/src/compas_notebook/scene/frameobject.py @@ -0,0 +1,25 @@ +import pythreejs as three +from compas.scene import GeometryObject + +from compas_notebook.conversions.geometry import frame_to_threejs + +from .sceneobject import ThreeSceneObject + + +class ThreeFrameObject(ThreeSceneObject, GeometryObject): + """Scene object for drawing frames""" + + def draw(self): + """Draw the frame associated with the scene object as a set of lines: x-axis in red, y-axis in green, z-axis in blue. + + Returns + ------- + list[three.Line] + List of pythreejs objects created. + + """ + + + self._guids = frame_to_threejs(self.geometry) + + return self.guids diff --git a/src/compas_notebook/scene/lineobject.py b/src/compas_notebook/scene/lineobject.py index 4b11224..b69f161 100644 --- a/src/compas_notebook/scene/lineobject.py +++ b/src/compas_notebook/scene/lineobject.py @@ -9,8 +9,8 @@ class ThreeLineObject(ThreeSceneObject, GeometryObject): """Scene object for drawing line.""" - def draw(self): - """Draw the line associated with the scene object. + def draw(self)-> list[three.Line]: + """Draw the frame associated with the scene object. Returns ------- From ac86bb545dc59c3a4543b6108c7207bc58ef57d8 Mon Sep 17 00:00:00 2001 From: Jelle Feringa Date: Wed, 19 Nov 2025 12:37:39 +0100 Subject: [PATCH 02/10] the initial cell: `%pip install -q compas_notebook` is problematic when working in dev mode and conflicts with `pip install -e .` --- notebooks/00_basics.ipynb | 9 --------- notebooks/10_scene.ipynb | 9 --------- notebooks/20_geometry.ipynb | 9 --------- notebooks/30_shapes.ipynb | 9 --------- notebooks/60_breps.ipynb | 17 ----------------- notebooks/70_graphs.ipynb | 9 --------- notebooks/80_groups.ipynb | 17 ----------------- 7 files changed, 79 deletions(-) diff --git a/notebooks/00_basics.ipynb b/notebooks/00_basics.ipynb index b3db2c3..8d3f982 100644 --- a/notebooks/00_basics.ipynb +++ b/notebooks/00_basics.ipynb @@ -1,14 +1,5 @@ { "cells": [ - { - "cell_type": "code", - "execution_count": 1, - "metadata": {}, - "outputs": [], - "source": [ - "%pip install -q compas_notebook" - ] - }, { "cell_type": "markdown", "metadata": {}, diff --git a/notebooks/10_scene.ipynb b/notebooks/10_scene.ipynb index a9aeab2..20156b7 100644 --- a/notebooks/10_scene.ipynb +++ b/notebooks/10_scene.ipynb @@ -1,14 +1,5 @@ { "cells": [ - { - "cell_type": "code", - "execution_count": 1, - "metadata": {}, - "outputs": [], - "source": [ - "%pip install -q compas_notebook" - ] - }, { "cell_type": "code", "execution_count": 2, diff --git a/notebooks/20_geometry.ipynb b/notebooks/20_geometry.ipynb index 04c8c7b..6fed711 100644 --- a/notebooks/20_geometry.ipynb +++ b/notebooks/20_geometry.ipynb @@ -1,14 +1,5 @@ { "cells": [ - { - "cell_type": "code", - "execution_count": 1, - "metadata": {}, - "outputs": [], - "source": [ - "# /%pip install -q compas_notebook" - ] - }, { "cell_type": "code", "execution_count": 2, diff --git a/notebooks/30_shapes.ipynb b/notebooks/30_shapes.ipynb index bec67ab..9436e87 100644 --- a/notebooks/30_shapes.ipynb +++ b/notebooks/30_shapes.ipynb @@ -1,14 +1,5 @@ { "cells": [ - { - "cell_type": "code", - "execution_count": 1, - "metadata": {}, - "outputs": [], - "source": [ - "%pip install -q compas_notebook" - ] - }, { "cell_type": "code", "execution_count": 2, diff --git a/notebooks/60_breps.ipynb b/notebooks/60_breps.ipynb index a47316a..f79cd0d 100644 --- a/notebooks/60_breps.ipynb +++ b/notebooks/60_breps.ipynb @@ -1,22 +1,5 @@ { "cells": [ - { - "cell_type": "code", - "execution_count": 1, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Note: you may need to restart the kernel to use updated packages.\n" - ] - } - ], - "source": [ - "%pip install -q compas_notebook" - ] - }, { "cell_type": "code", "execution_count": 2, diff --git a/notebooks/70_graphs.ipynb b/notebooks/70_graphs.ipynb index 2b8334f..5457cef 100644 --- a/notebooks/70_graphs.ipynb +++ b/notebooks/70_graphs.ipynb @@ -1,14 +1,5 @@ { "cells": [ - { - "cell_type": "code", - "execution_count": 1, - "metadata": {}, - "outputs": [], - "source": [ - "%pip install -q compas_notebook" - ] - }, { "cell_type": "code", "execution_count": 2, diff --git a/notebooks/80_groups.ipynb b/notebooks/80_groups.ipynb index 0e2ee94..41f68e8 100644 --- a/notebooks/80_groups.ipynb +++ b/notebooks/80_groups.ipynb @@ -1,22 +1,5 @@ { "cells": [ - { - "cell_type": "code", - "execution_count": 1, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Note: you may need to restart the kernel to use updated packages.\n" - ] - } - ], - "source": [ - "%pip install -q compas_notebook" - ] - }, { "cell_type": "code", "execution_count": 2, From 192c10a67a51342f95094b2601ade67f1b6e922c Mon Sep 17 00:00:00 2001 From: Jelle Feringa Date: Wed, 3 Dec 2025 17:40:06 +0100 Subject: [PATCH 03/10] add support for circle, ellipse, vector, plane, curve, surface primitives - converters use angular resolution for circles/ellipses - vector rendered as arrow with cone head - plane shown as grid - curve/surface discretization (requires nurbs plugin for creation) --- notebooks/20_geometry.ipynb | 85 ++++++- src/compas_notebook/conversions/geometry.py | 249 +++++++++++++++++++- src/compas_notebook/scene/__init__.py | 29 ++- src/compas_notebook/scene/circleobject.py | 26 ++ src/compas_notebook/scene/curveobject.py | 26 ++ src/compas_notebook/scene/ellipseobject.py | 26 ++ src/compas_notebook/scene/planeobject.py | 23 ++ src/compas_notebook/scene/surfaceobject.py | 23 ++ src/compas_notebook/scene/vectorobject.py | 23 ++ 9 files changed, 495 insertions(+), 15 deletions(-) create mode 100644 src/compas_notebook/scene/circleobject.py create mode 100644 src/compas_notebook/scene/curveobject.py create mode 100644 src/compas_notebook/scene/ellipseobject.py create mode 100644 src/compas_notebook/scene/planeobject.py create mode 100644 src/compas_notebook/scene/surfaceobject.py create mode 100644 src/compas_notebook/scene/vectorobject.py diff --git a/notebooks/20_geometry.ipynb b/notebooks/20_geometry.ipynb index 6fed711..91ba87d 100644 --- a/notebooks/20_geometry.ipynb +++ b/notebooks/20_geometry.ipynb @@ -2,22 +2,26 @@ "cells": [ { "cell_type": "code", - "execution_count": 2, + "execution_count": 1, "metadata": {}, "outputs": [], "source": [ "from compas.colors import Color\n", + "from compas.geometry import Circle\n", + "from compas.geometry import Ellipse\n", + "from compas.geometry import Frame\n", "from compas.geometry import Line\n", + "from compas.geometry import Plane\n", "from compas.geometry import Point\n", "from compas.geometry import Pointcloud\n", "from compas.geometry import Polyline\n", - "from compas.geometry import Frame \n", + "from compas.geometry import Vector\n", "from compas_notebook.viewer import Viewer" ] }, { "cell_type": "code", - "execution_count": 3, + "execution_count": 2, "metadata": {}, "outputs": [], "source": [ @@ -39,13 +43,13 @@ }, { "cell_type": "code", - "execution_count": 4, + "execution_count": 3, "metadata": {}, "outputs": [ { "data": { "application/vnd.jupyter.widget-view+json": { - "model_id": "60cefc1deea3458c9e1a390cdd0ea3ba", + "model_id": "0498e1e0a2e3459480997d957fbfc9da", "version_major": 2, "version_minor": 0 }, @@ -68,11 +72,78 @@ "\n", "viewer.show()" ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# New Primitives\n", + "\n", + "Demonstration of recently added primitive support: Circle, Ellipse, Vector, and Plane." + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [], + "source": [ + "# Create new primitives\n", + "circle = Circle(radius=1.5, frame=Frame([2, 0, 0], [0, 1, 0], [0, 0, 1]))\n", + "ellipse = Ellipse(major=2.0, minor=1.0, frame=Frame([5, 0, 0], [0, 1, 0], [0, 0, 1]))\n", + "vector = Vector(1, 1, 2)\n", + "plane = Plane([0, 0, -1], [0, 0, 1])" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [ + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "3cac0c93ba69483fb0534923124dbe15", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "VBox(children=(HBox(children=(Button(icon='search-plus', layout=Layout(height='32px', width='48px'), style=But…" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "viewer2 = Viewer()\n", + "\n", + "viewer2.scene.add(circle, color=Color.red())\n", + "viewer2.scene.add(ellipse, color=Color.green())\n", + "viewer2.scene.add(vector)\n", + "viewer2.scene.add(plane)\n", + "\n", + "viewer2.show()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] } ], "metadata": { "kernelspec": { - "display_name": "compas_opzuid", + "display_name": "Python 3 (ipykernel)", "language": "python", "name": "python3" }, @@ -90,5 +161,5 @@ } }, "nbformat": 4, - "nbformat_minor": 2 + "nbformat_minor": 4 } diff --git a/src/compas_notebook/conversions/geometry.py b/src/compas_notebook/conversions/geometry.py index 5d2d1f5..56f2bd8 100644 --- a/src/compas_notebook/conversions/geometry.py +++ b/src/compas_notebook/conversions/geometry.py @@ -2,15 +2,21 @@ import numpy import pythreejs as three from compas.geometry import Box +from compas.geometry import Circle from compas.geometry import Cone +from compas.geometry import Curve from compas.geometry import Cylinder +from compas.geometry import Ellipse +from compas.geometry import Frame +from compas.geometry import Line +from compas.geometry import Plane from compas.geometry import Point from compas.geometry import Pointcloud from compas.geometry import Polyline from compas.geometry import Sphere +from compas.geometry import Surface from compas.geometry import Torus -from compas.geometry import Frame -from compas.geometry import Line +from compas.geometry import Vector def line_to_threejs(line: Line) -> three.BufferGeometry: @@ -129,6 +135,245 @@ def polyline_to_threejs(polyline: Polyline) -> three.BufferGeometry: return geometry +def circle_to_threejs(circle: Circle, max_angle: float = 5.0) -> three.BufferGeometry: + """Convert a COMPAS circle to PyThreeJS. + + Parameters + ---------- + circle : :class:`compas.geometry.Circle` + The circle to convert. + max_angle : float, optional + Maximum angle in degrees between segments. + + Returns + ------- + :class:`three.BufferGeometry` + + """ + import math + + n = max(8, int(math.ceil(360.0 / max_angle))) + polyline = circle.to_polyline(n=n) + vertices = numpy.array(polyline.points, dtype=numpy.float32) + geometry = three.BufferGeometry(attributes={"position": three.BufferAttribute(vertices, normalized=False)}) + return geometry + + +def ellipse_to_threejs(ellipse: Ellipse, max_angle: float = 5.0) -> three.BufferGeometry: + """Convert a COMPAS ellipse to PyThreeJS. + + Parameters + ---------- + ellipse : :class:`compas.geometry.Ellipse` + The ellipse to convert. + max_angle : float, optional + Maximum angle in degrees between segments. + + Returns + ------- + :class:`three.BufferGeometry` + + """ + import math + + n = max(8, int(math.ceil(360.0 / max_angle))) + polyline = ellipse.to_polyline(n=n) + vertices = numpy.array(polyline.points, dtype=numpy.float32) + geometry = three.BufferGeometry(attributes={"position": three.BufferAttribute(vertices, normalized=False)}) + return geometry + + +def vector_to_threejs(vector: Vector, scale: float = 1.0) -> list[three.Object3D]: + """Convert a COMPAS vector to PyThreeJS as arrow. + + Parameters + ---------- + vector : :class:`compas.geometry.Vector` + The vector to convert. + scale : float, optional + Scale factor for vector length. + + Returns + ------- + list[three.Object3D] + Line and cone representing the arrow. + + """ + # Line from origin to vector * scale + line = Line([0, 0, 0], vector * scale) + line_geom = line_to_threejs(line) + line_obj = three.Line(line_geom, three.LineBasicMaterial(color="blue")) + + # Cone head at tip (10% of length, positioned at end) + length = vector.length * scale + cone_height = length * 0.1 + cone_radius = cone_height * 0.3 + + # Position cone at vector tip + cone_geom = three.CylinderGeometry( + radiusTop=0, + radiusBottom=cone_radius, + height=cone_height, + radialSegments=8, + ) + cone_obj = three.Mesh(cone_geom, three.MeshBasicMaterial(color="blue")) + + # Compute cone position and rotation + # Cone should point in vector direction + tip = vector * scale + cone_obj.position = (tip.x, tip.y, tip.z) + + # Align cone with vector direction + # Three.js cylinder default is Y-up, need to rotate to align with vector + normalized = vector.unitized() + cone_obj.quaternion = _vector_to_quaternion(normalized) + + return [line_obj, cone_obj] + + +def _vector_to_quaternion(vector): + """Helper to compute quaternion for aligning Y-axis with vector.""" + import math + + # Default direction is Y-up [0, 1, 0] + y_axis = Vector(0, 1, 0) + + # Compute rotation axis (cross product) + axis = y_axis.cross(vector) + + # Compute rotation angle + angle = math.acos(max(-1, min(1, y_axis.dot(vector)))) + + if axis.length < 1e-10: + # Vector is parallel or anti-parallel to Y-axis + if vector.y > 0: + return (0, 0, 0, 1) # No rotation + else: + return (0, 0, 1, 0) # 180 degree rotation around Z + + # Normalize axis + axis = axis.unitized() + + # Convert axis-angle to quaternion + half_angle = angle / 2 + s = math.sin(half_angle) + return (axis.x * s, axis.y * s, axis.z * s, math.cos(half_angle)) + + +def plane_to_threejs(plane: Plane, size: float = 1.0, grid: int = 10) -> list[three.Object3D]: + """Convert a COMPAS plane to PyThreeJS as grid. + + Parameters + ---------- + plane : :class:`compas.geometry.Plane` + The plane to convert. + size : float, optional + Size of the grid visualization. + grid : int, optional + Number of grid lines in each direction. + + Returns + ------- + list[three.Object3D] + Grid lines in the plane. + + """ + objects = [] + + # Get frame from plane to have xaxis and yaxis + frame = Frame.from_plane(plane) + + # Create grid lines along xaxis and yaxis directions + step = size / grid + half = size / 2 + + # Lines parallel to xaxis + for i in range(grid + 1): + offset = -half + i * step + start = frame.point + frame.yaxis * offset - frame.xaxis * half + end = frame.point + frame.yaxis * offset + frame.xaxis * half + line = Line(start, end) + line_geom = line_to_threejs(line) + line_obj = three.Line(line_geom, three.LineBasicMaterial(color="lightgray")) + objects.append(line_obj) + + # Lines parallel to yaxis + for i in range(grid + 1): + offset = -half + i * step + start = frame.point + frame.xaxis * offset - frame.yaxis * half + end = frame.point + frame.xaxis * offset + frame.yaxis * half + line = Line(start, end) + line_geom = line_to_threejs(line) + line_obj = three.Line(line_geom, three.LineBasicMaterial(color="lightgray")) + objects.append(line_obj) + + return objects + + +def curve_to_threejs(curve: Curve, resolution: int = 100) -> three.BufferGeometry: + """Convert a COMPAS curve to PyThreeJS. + + Parameters + ---------- + curve : :class:`compas.geometry.Curve` + The curve to convert. + resolution : int, optional + Number of points for discretization. + + Returns + ------- + :class:`three.BufferGeometry` + + """ + polyline = curve.to_polyline(n=resolution) + vertices = numpy.array(polyline.points, dtype=numpy.float32) + geometry = three.BufferGeometry(attributes={"position": three.BufferAttribute(vertices, normalized=False)}) + return geometry + + +def surface_to_threejs(surface: Surface, resolution_u: int = 20, resolution_v: int = 20): + """Convert a COMPAS surface to PyThreeJS. + + Parameters + ---------- + surface : :class:`compas.geometry.Surface` + The surface to convert. + resolution_u : int, optional + Number of divisions in U direction. + resolution_v : int, optional + Number of divisions in V direction. + + Returns + ------- + tuple[three.Mesh, three.LineSegments] + Mesh and edge visualization. + + """ + from compas_notebook.conversions.meshes import vertices_and_edges_to_threejs + from compas_notebook.conversions.meshes import vertices_and_faces_to_threejs + + vertices, faces = surface.to_vertices_and_faces(nu=resolution_u, nv=resolution_v) + + # Create faces + faces_geom = vertices_and_faces_to_threejs(vertices, faces) + mesh = three.Mesh(faces_geom, three.MeshBasicMaterial(color="lightblue", side="DoubleSide")) + + # Create edges + edges = [] + for face in faces: + n = len(face) + for i in range(n): + edge = (face[i], face[(i + 1) % n]) + # Add edge if not duplicate + if (edge[1], edge[0]) not in edges: + edges.append(edge) + + edges_geom = vertices_and_edges_to_threejs(vertices, edges) + lines = three.LineSegments(edges_geom, three.LineBasicMaterial(color="black")) + + return [mesh, lines] + + # ============================================================================= # Shapes # ============================================================================= diff --git a/src/compas_notebook/scene/__init__.py b/src/compas_notebook/scene/__init__.py index 7c770e3..9fc9196 100644 --- a/src/compas_notebook/scene/__init__.py +++ b/src/compas_notebook/scene/__init__.py @@ -10,17 +10,23 @@ from compas.geometry import Box from compas.geometry import Brep from compas.geometry import Capsule +from compas.geometry import Circle from compas.geometry import Cone +from compas.geometry import Curve from compas.geometry import Cylinder -from compas.geometry import Line +from compas.geometry import Ellipse from compas.geometry import Frame +from compas.geometry import Line +from compas.geometry import Plane from compas.geometry import Point from compas.geometry import Pointcloud from compas.geometry import Polygon from compas.geometry import Polyhedron from compas.geometry import Polyline from compas.geometry import Sphere +from compas.geometry import Surface from compas.geometry import Torus +from compas.geometry import Vector from compas.datastructures import Graph from compas.datastructures import Mesh @@ -32,23 +38,28 @@ from .boxobject import ThreeBoxObject from .brepobject import ThreeBrepObject from .capsuleobject import ThreeCapsuleObject +from .circleobject import ThreeCircleObject from .coneobject import ThreeConeObject +from .curveobject import ThreeCurveObject from .cylinderobject import ThreeCylinderObject +from .ellipseobject import ThreeEllipseObject +from .frameobject import ThreeFrameObject +from .groupobject import ThreeGroupObject from .lineobject import ThreeLineObject +from .planeobject import ThreePlaneObject from .pointobject import ThreePointObject from .pointcloudobject import ThreePointcloudObject from .polygonobject import ThreePolygonObject from .polyhedronobject import ThreePolyhedronObject from .polylineobject import ThreePolylineObject from .sphereobject import ThreeSphereObject +from .surfaceobject import ThreeSurfaceObject from .torusobject import ThreeTorusObject +from .vectorobject import ThreeVectorObject from .graphobject import ThreeGraphObject from .meshobject import ThreeMeshObject -from .groupobject import ThreeGroupObject -from .frameobject import ThreeFrameObject - @plugin(category="drawing-utils", pluggable_name="clear", requires=["pythreejs"]) def clear_pythreejs(guids=None): @@ -74,19 +85,25 @@ def register_scene_objects(): register(Box, ThreeBoxObject, context="Notebook") register(Brep, ThreeBrepObject, context="Notebook") register(Capsule, ThreeCapsuleObject, context="Notebook") + register(Circle, ThreeCircleObject, context="Notebook") register(Cone, ThreeConeObject, context="Notebook") + register(Curve, ThreeCurveObject, context="Notebook") register(Cylinder, ThreeCylinderObject, context="Notebook") - register(Graph, ThreeGraphObject, context="Notebook") + register(Ellipse, ThreeEllipseObject, context="Notebook") register(Frame, ThreeFrameObject, context="Notebook") + register(Graph, ThreeGraphObject, context="Notebook") register(Line, ThreeLineObject, context="Notebook") + register(Mesh, ThreeMeshObject, context="Notebook") + register(Plane, ThreePlaneObject, context="Notebook") register(Point, ThreePointObject, context="Notebook") register(Pointcloud, ThreePointcloudObject, context="Notebook") register(Polygon, ThreePolygonObject, context="Notebook") register(Polyhedron, ThreePolyhedronObject, context="Notebook") register(Polyline, ThreePolylineObject, context="Notebook") register(Sphere, ThreeSphereObject, context="Notebook") + register(Surface, ThreeSurfaceObject, context="Notebook") register(Torus, ThreeTorusObject, context="Notebook") - register(Mesh, ThreeMeshObject, context="Notebook") + register(Vector, ThreeVectorObject, context="Notebook") register(list, ThreeGroupObject, context="Notebook") # # yuck java diff --git a/src/compas_notebook/scene/circleobject.py b/src/compas_notebook/scene/circleobject.py new file mode 100644 index 0000000..3abf3ed --- /dev/null +++ b/src/compas_notebook/scene/circleobject.py @@ -0,0 +1,26 @@ +import pythreejs as three +from compas.scene import GeometryObject + +from compas_notebook.conversions.geometry import circle_to_threejs + +from .sceneobject import ThreeSceneObject + + +class ThreeCircleObject(ThreeSceneObject, GeometryObject): + """Scene object for drawing circles.""" + + def draw(self) -> list[three.Line]: + """Draw the circle as a discretized polyline. + + Returns + ------- + list[three.Line] + List of pythreejs objects created. + + """ + geometry = circle_to_threejs(self.geometry) + line = three.LineLoop(geometry, three.LineBasicMaterial(color=self.contrastcolor.hex)) + + self._guids = [line] + + return self.guids diff --git a/src/compas_notebook/scene/curveobject.py b/src/compas_notebook/scene/curveobject.py new file mode 100644 index 0000000..4831045 --- /dev/null +++ b/src/compas_notebook/scene/curveobject.py @@ -0,0 +1,26 @@ +import pythreejs as three +from compas.scene import GeometryObject + +from compas_notebook.conversions.geometry import curve_to_threejs + +from .sceneobject import ThreeSceneObject + + +class ThreeCurveObject(ThreeSceneObject, GeometryObject): + """Scene object for drawing curves.""" + + def draw(self) -> list[three.Line]: + """Draw the curve as discretized polyline. + + Returns + ------- + list[three.Line] + List of pythreejs objects created. + + """ + geometry = curve_to_threejs(self.geometry) + line = three.Line(geometry, three.LineBasicMaterial(color=self.contrastcolor.hex)) + + self._guids = [line] + + return self.guids diff --git a/src/compas_notebook/scene/ellipseobject.py b/src/compas_notebook/scene/ellipseobject.py new file mode 100644 index 0000000..10582a3 --- /dev/null +++ b/src/compas_notebook/scene/ellipseobject.py @@ -0,0 +1,26 @@ +import pythreejs as three +from compas.scene import GeometryObject + +from compas_notebook.conversions.geometry import ellipse_to_threejs + +from .sceneobject import ThreeSceneObject + + +class ThreeEllipseObject(ThreeSceneObject, GeometryObject): + """Scene object for drawing ellipses.""" + + def draw(self) -> list[three.Line]: + """Draw the ellipse as a discretized polyline. + + Returns + ------- + list[three.Line] + List of pythreejs objects created. + + """ + geometry = ellipse_to_threejs(self.geometry) + line = three.LineLoop(geometry, three.LineBasicMaterial(color=self.contrastcolor.hex)) + + self._guids = [line] + + return self.guids diff --git a/src/compas_notebook/scene/planeobject.py b/src/compas_notebook/scene/planeobject.py new file mode 100644 index 0000000..5ebad6d --- /dev/null +++ b/src/compas_notebook/scene/planeobject.py @@ -0,0 +1,23 @@ +import pythreejs as three +from compas.scene import GeometryObject + +from compas_notebook.conversions.geometry import plane_to_threejs + +from .sceneobject import ThreeSceneObject + + +class ThreePlaneObject(ThreeSceneObject, GeometryObject): + """Scene object for drawing planes as grids.""" + + def draw(self) -> list[three.Object3D]: + """Draw the plane as a grid. + + Returns + ------- + list[three.Object3D] + List of pythreejs objects created. + + """ + self._guids = plane_to_threejs(self.geometry) + + return self.guids diff --git a/src/compas_notebook/scene/surfaceobject.py b/src/compas_notebook/scene/surfaceobject.py new file mode 100644 index 0000000..b0d1edf --- /dev/null +++ b/src/compas_notebook/scene/surfaceobject.py @@ -0,0 +1,23 @@ +import pythreejs as three +from compas.scene import GeometryObject + +from compas_notebook.conversions.geometry import surface_to_threejs + +from .sceneobject import ThreeSceneObject + + +class ThreeSurfaceObject(ThreeSceneObject, GeometryObject): + """Scene object for drawing surfaces.""" + + def draw(self) -> list[three.Object3D]: + """Draw the surface as mesh with edges. + + Returns + ------- + list[three.Object3D] + List of pythreejs objects created. + + """ + self._guids = surface_to_threejs(self.geometry) + + return self.guids diff --git a/src/compas_notebook/scene/vectorobject.py b/src/compas_notebook/scene/vectorobject.py new file mode 100644 index 0000000..58c56e6 --- /dev/null +++ b/src/compas_notebook/scene/vectorobject.py @@ -0,0 +1,23 @@ +import pythreejs as three +from compas.scene import GeometryObject + +from compas_notebook.conversions.geometry import vector_to_threejs + +from .sceneobject import ThreeSceneObject + + +class ThreeVectorObject(ThreeSceneObject, GeometryObject): + """Scene object for drawing vectors as arrows.""" + + def draw(self) -> list[three.Object3D]: + """Draw the vector as arrow (line + cone head). + + Returns + ------- + list[three.Object3D] + List of pythreejs objects created. + + """ + self._guids = vector_to_threejs(self.geometry) + + return self.guids From 3aff457ca8bbfb285984b076de616b7ce6d091e5 Mon Sep 17 00:00:00 2001 From: Jelle Feringa Date: Wed, 3 Dec 2025 17:40:16 +0100 Subject: [PATCH 04/10] add surface examples using analytical surfaces spherical and cylindrical surfaces work without plugin --- notebooks/20_geometry.ipynb | 37 ++++++++++++++++++++++--------------- 1 file changed, 22 insertions(+), 15 deletions(-) diff --git a/notebooks/20_geometry.ipynb b/notebooks/20_geometry.ipynb index 91ba87d..1e615d6 100644 --- a/notebooks/20_geometry.ipynb +++ b/notebooks/20_geometry.ipynb @@ -2,22 +2,10 @@ "cells": [ { "cell_type": "code", - "execution_count": 1, + "execution_count": null, "metadata": {}, "outputs": [], - "source": [ - "from compas.colors import Color\n", - "from compas.geometry import Circle\n", - "from compas.geometry import Ellipse\n", - "from compas.geometry import Frame\n", - "from compas.geometry import Line\n", - "from compas.geometry import Plane\n", - "from compas.geometry import Point\n", - "from compas.geometry import Pointcloud\n", - "from compas.geometry import Polyline\n", - "from compas.geometry import Vector\n", - "from compas_notebook.viewer import Viewer" - ] + "source": "from compas.colors import Color\nfrom compas.geometry import Circle\nfrom compas.geometry import Ellipse\nfrom compas.geometry import Frame\nfrom compas.geometry import Line\nfrom compas.geometry import Plane\nfrom compas.geometry import Point\nfrom compas.geometry import Pointcloud\nfrom compas.geometry import Polyline\nfrom compas.geometry import Vector\nfrom compas.geometry import SphericalSurface\nfrom compas.geometry import CylindricalSurface\nfrom compas_notebook.viewer import Viewer" }, { "cell_type": "code", @@ -139,6 +127,25 @@ "metadata": {}, "outputs": [], "source": [] + }, + { + "cell_type": "markdown", + "source": "# Surfaces\n\nAnalytical surfaces work out of the box.", + "metadata": {} + }, + { + "cell_type": "code", + "source": "# Create analytical surfaces\nsphere_surface = SphericalSurface(radius=1.5, frame=Frame([0, 0, 0], [1, 0, 0], [0, 1, 0]))\ncylinder_surface = CylindricalSurface(radius=0.8, frame=Frame([3, 0, 0], [1, 0, 0], [0, 1, 0]))", + "metadata": {}, + "execution_count": null, + "outputs": [] + }, + { + "cell_type": "code", + "source": "viewer3 = Viewer()\n\nviewer3.scene.add(sphere_surface, color=Color.cyan())\nviewer3.scene.add(cylinder_surface, color=Color.magenta())\n\nviewer3.show()", + "metadata": {}, + "execution_count": null, + "outputs": [] } ], "metadata": { @@ -162,4 +169,4 @@ }, "nbformat": 4, "nbformat_minor": 4 -} +} \ No newline at end of file From c3d397e90fe909efd2ebb8e79c3fc5fd09465ebf Mon Sep 17 00:00:00 2001 From: Jelle Feringa Date: Fri, 5 Dec 2025 13:26:25 +0100 Subject: [PATCH 05/10] fix up following code review --- CHANGELOG.md | 2 + notebooks/20_geometry.ipynb | 140 ++++++-------------- src/compas_notebook/conversions/geometry.py | 27 ++-- src/compas_notebook/scene/__init__.py | 17 --- 4 files changed, 52 insertions(+), 134 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d61ffa5..fe0a381 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added +* Added support for `frames, circle, ellipse, vector, plane, curve, analystical surface primitives` + ### Changed ### Removed diff --git a/notebooks/20_geometry.ipynb b/notebooks/20_geometry.ipynb index 1e615d6..00a7d3b 100644 --- a/notebooks/20_geometry.ipynb +++ b/notebooks/20_geometry.ipynb @@ -2,42 +2,58 @@ "cells": [ { "cell_type": "code", - "execution_count": null, + "execution_count": 2, "metadata": {}, "outputs": [], - "source": "from compas.colors import Color\nfrom compas.geometry import Circle\nfrom compas.geometry import Ellipse\nfrom compas.geometry import Frame\nfrom compas.geometry import Line\nfrom compas.geometry import Plane\nfrom compas.geometry import Point\nfrom compas.geometry import Pointcloud\nfrom compas.geometry import Polyline\nfrom compas.geometry import Vector\nfrom compas.geometry import SphericalSurface\nfrom compas.geometry import CylindricalSurface\nfrom compas_notebook.viewer import Viewer" + "source": [ + "from compas.colors import Color\n", + "from compas.geometry import Circle\n", + "from compas.geometry import Ellipse\n", + "from compas.geometry import Frame\n", + "from compas.geometry import Line\n", + "from compas.geometry import Plane\n", + "from compas.geometry import Point\n", + "from compas.geometry import Pointcloud\n", + "from compas.geometry import Polyline\n", + "from compas.geometry import Vector\n", + "from compas.geometry import SphericalSurface\n", + "from compas.geometry import CylindricalSurface\n", + "from compas_notebook.viewer import Viewer" + ] }, { "cell_type": "code", - "execution_count": 2, + "execution_count": 5, "metadata": {}, "outputs": [], "source": [ - "cloud = Pointcloud.from_bounds(x=8, y=5, z=3, n=13)\n", - "\n", "point = Point(-1, 2, 3)\n", + "vector = Vector(1, 1, 2)\n", + "plane = Plane([0, 0, -1], [0, 0, 1])\n", + "frame = Frame(point, [1, 0, 0], [0, 1, 0])\n", + "\n", "line = Line([0, 0, 0], point)\n", + "cloud = Pointcloud.from_bounds(x=8, y=5, z=3, n=13)\n", "polyline = Polyline(cloud.points)\n", "\n", + "# quadric curves\n", + "circle = Circle(radius=1.5, frame=Frame([2, 0, 0], [0, 1, 0], [0, 0, 1]))\n", + "ellipse = Ellipse(major=2.0, minor=1.0, frame=Frame([5, 0, 0], [0, 1, 0], [0, 0, 1]))\n", "\n", - "frame = Frame(point, [1, 0, 0], [0, 1, 0])\n", - "\n", - "# x_line = Line(frame.point, frame.point+frame.xaxis)\n", - "# y_line = Line(frame.point, frame.point+frame.yaxis)\n", - "# z_line = Line(frame.point, frame.point+frame.zaxis)\n", - "\n", - "\n" + "# Create analytical surfaces\n", + "sphere_surface = SphericalSurface(radius=1.5, frame=Frame([0, 0, 0], [1, 0, 0], [0, 1, 0]))\n", + "cylinder_surface = CylindricalSurface(radius=0.8, frame=Frame([3, 0, 0], [1, 0, 0], [0, 1, 0]))" ] }, { "cell_type": "code", - "execution_count": 3, + "execution_count": 7, "metadata": {}, "outputs": [ { "data": { "application/vnd.jupyter.widget-view+json": { - "model_id": "0498e1e0a2e3459480997d957fbfc9da", + "model_id": "d32833ac778148c0920dfa0e9b672076", "version_major": 2, "version_minor": 0 }, @@ -57,100 +73,20 @@ "viewer.scene.add(frame)\n", "viewer.scene.add(polyline, color=Color.blue())\n", "viewer.scene.add(cloud, color=Color.green(), pointsize=0.3)\n", + "viewer.scene.add(circle, color=Color.red())\n", + "viewer.scene.add(ellipse, color=Color.green())\n", + "viewer.scene.add(vector)\n", + "viewer.scene.add(plane)\n", + "viewer.scene.add(sphere_surface, color=Color.cyan())\n", + "viewer.scene.add(cylinder_surface, color=Color.magenta())\n", "\n", "viewer.show()" ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# New Primitives\n", - "\n", - "Demonstration of recently added primitive support: Circle, Ellipse, Vector, and Plane." - ] - }, - { - "cell_type": "code", - "execution_count": 4, - "metadata": {}, - "outputs": [], - "source": [ - "# Create new primitives\n", - "circle = Circle(radius=1.5, frame=Frame([2, 0, 0], [0, 1, 0], [0, 0, 1]))\n", - "ellipse = Ellipse(major=2.0, minor=1.0, frame=Frame([5, 0, 0], [0, 1, 0], [0, 0, 1]))\n", - "vector = Vector(1, 1, 2)\n", - "plane = Plane([0, 0, -1], [0, 0, 1])" - ] - }, - { - "cell_type": "code", - "execution_count": 5, - "metadata": {}, - "outputs": [ - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "3cac0c93ba69483fb0534923124dbe15", - "version_major": 2, - "version_minor": 0 - }, - "text/plain": [ - "VBox(children=(HBox(children=(Button(icon='search-plus', layout=Layout(height='32px', width='48px'), style=But…" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "viewer2 = Viewer()\n", - "\n", - "viewer2.scene.add(circle, color=Color.red())\n", - "viewer2.scene.add(ellipse, color=Color.green())\n", - "viewer2.scene.add(vector)\n", - "viewer2.scene.add(plane)\n", - "\n", - "viewer2.show()" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [] - }, - { - "cell_type": "markdown", - "source": "# Surfaces\n\nAnalytical surfaces work out of the box.", - "metadata": {} - }, - { - "cell_type": "code", - "source": "# Create analytical surfaces\nsphere_surface = SphericalSurface(radius=1.5, frame=Frame([0, 0, 0], [1, 0, 0], [0, 1, 0]))\ncylinder_surface = CylindricalSurface(radius=0.8, frame=Frame([3, 0, 0], [1, 0, 0], [0, 1, 0]))", - "metadata": {}, - "execution_count": null, - "outputs": [] - }, - { - "cell_type": "code", - "source": "viewer3 = Viewer()\n\nviewer3.scene.add(sphere_surface, color=Color.cyan())\nviewer3.scene.add(cylinder_surface, color=Color.magenta())\n\nviewer3.show()", - "metadata": {}, - "execution_count": null, - "outputs": [] } ], "metadata": { "kernelspec": { - "display_name": "Python 3 (ipykernel)", + "display_name": "compas_opzuid", "language": "python", "name": "python3" }, @@ -169,4 +105,4 @@ }, "nbformat": 4, "nbformat_minor": 4 -} \ No newline at end of file +} diff --git a/src/compas_notebook/conversions/geometry.py b/src/compas_notebook/conversions/geometry.py index 56f2bd8..4bab5e2 100644 --- a/src/compas_notebook/conversions/geometry.py +++ b/src/compas_notebook/conversions/geometry.py @@ -1,4 +1,5 @@ import re +import math import numpy import pythreejs as three from compas.geometry import Box @@ -17,6 +18,8 @@ from compas.geometry import Surface from compas.geometry import Torus from compas.geometry import Vector +from compas_notebook.conversions.meshes import vertices_and_edges_to_threejs +from compas_notebook.conversions.meshes import vertices_and_faces_to_threejs def line_to_threejs(line: Line) -> three.BufferGeometry: @@ -32,7 +35,7 @@ def line_to_threejs(line: Line) -> three.BufferGeometry: :class:`three.BufferGeometry` """ - vertices = numpy.array([line.start, line.end], dtype=numpy.float32) + vertices = numpy.array([line.start, line.end], dtype=numpy.float64) geometry = three.BufferGeometry(attributes={"position": three.BufferAttribute(vertices, normalized=False)}) return geometry @@ -53,8 +56,8 @@ def frame_to_threejs(frame: Frame) -> list[three.BufferGeometry]: # create lines for each axis _x_line = Line(frame.point, frame.point + frame.xaxis) - _y_line = Line(frame.point, frame.point + frame.yaxis) - _z_line = Line(frame.point, frame.point + frame.zaxis) + _y_line = Line.from_point_and_vector(frame.point, frame.yaxis) + _z_line = Line.from_point_and_vector(frame.point, frame.zaxis) # convert lines to threejs vertex buffers xline_verts = line_to_threejs(_x_line) @@ -90,7 +93,7 @@ def point_to_threejs(point: Point) -> three.SphereGeometry: BufferGeometry(...) """ - vertices = numpy.array([point], dtype=numpy.float32) + vertices = numpy.array([point], dtype=numpy.float64) geometry = three.BufferGeometry(attributes={"position": three.BufferAttribute(vertices, normalized=False)}) return geometry @@ -112,7 +115,7 @@ def pointcloud_to_threejs(pointcloud: Pointcloud) -> three.SphereGeometry: >>> """ - vertices = numpy.array(pointcloud.points, dtype=numpy.float32) + vertices = numpy.array(pointcloud.points, dtype=numpy.float64) geometry = three.BufferGeometry(attributes={"position": three.BufferAttribute(vertices, normalized=False)}) return geometry @@ -130,7 +133,7 @@ def polyline_to_threejs(polyline: Polyline) -> three.BufferGeometry: :class:`three.BufferGeometry` """ - vertices = numpy.array(polyline.points, dtype=numpy.float32) + vertices = numpy.array(polyline.points, dtype=numpy.float64) geometry = three.BufferGeometry(attributes={"position": three.BufferAttribute(vertices, normalized=False)}) return geometry @@ -150,11 +153,10 @@ def circle_to_threejs(circle: Circle, max_angle: float = 5.0) -> three.BufferGeo :class:`three.BufferGeometry` """ - import math n = max(8, int(math.ceil(360.0 / max_angle))) polyline = circle.to_polyline(n=n) - vertices = numpy.array(polyline.points, dtype=numpy.float32) + vertices = numpy.array(polyline.points, dtype=numpy.float64) geometry = three.BufferGeometry(attributes={"position": three.BufferAttribute(vertices, normalized=False)}) return geometry @@ -174,11 +176,9 @@ def ellipse_to_threejs(ellipse: Ellipse, max_angle: float = 5.0) -> three.Buffer :class:`three.BufferGeometry` """ - import math - n = max(8, int(math.ceil(360.0 / max_angle))) polyline = ellipse.to_polyline(n=n) - vertices = numpy.array(polyline.points, dtype=numpy.float32) + vertices = numpy.array(polyline.points, dtype=numpy.float64) geometry = three.BufferGeometry(attributes={"position": three.BufferAttribute(vertices, normalized=False)}) return geometry @@ -326,7 +326,7 @@ def curve_to_threejs(curve: Curve, resolution: int = 100) -> three.BufferGeometr """ polyline = curve.to_polyline(n=resolution) - vertices = numpy.array(polyline.points, dtype=numpy.float32) + vertices = numpy.array(polyline.points, dtype=numpy.float64) geometry = three.BufferGeometry(attributes={"position": three.BufferAttribute(vertices, normalized=False)}) return geometry @@ -349,9 +349,6 @@ def surface_to_threejs(surface: Surface, resolution_u: int = 20, resolution_v: i Mesh and edge visualization. """ - from compas_notebook.conversions.meshes import vertices_and_edges_to_threejs - from compas_notebook.conversions.meshes import vertices_and_faces_to_threejs - vertices, faces = surface.to_vertices_and_faces(nu=resolution_u, nv=resolution_v) # Create faces diff --git a/src/compas_notebook/scene/__init__.py b/src/compas_notebook/scene/__init__.py index 9fc9196..a670d4e 100644 --- a/src/compas_notebook/scene/__init__.py +++ b/src/compas_notebook/scene/__init__.py @@ -106,20 +106,3 @@ def register_scene_objects(): register(Vector, ThreeVectorObject, context="Notebook") register(list, ThreeGroupObject, context="Notebook") -# # yuck java -# __all__ = [ -# "NotebookScene", -# "ThreeBoxObject", -# "ThreeCapsuleObject", -# "ThreeConeObject", -# "ThreeCylinderObject", -# "ThreeGraphObject", -# "ThreePointObject", -# "ThreePointcloudObject", -# "ThreePolygonObject", -# "ThreePolyhedronObject", -# "ThreePolylineObject", -# "ThreeSceneObject", -# "ThreeSphereObject", -# "ThreeTorusObject", -# ] From 900f32a5e2cc11e463c04427a4d54844dc396057 Mon Sep 17 00:00:00 2001 From: Jelle Feringa Date: Fri, 5 Dec 2025 13:56:00 +0100 Subject: [PATCH 06/10] Dot primitive that echo's Rhino dot command --- notebooks/20_geometry.ipynb | 63 ++---------- src/compas_notebook/conversions/__init__.py | 2 + src/compas_notebook/conversions/geometry.py | 46 +++++++-- src/compas_notebook/geometry/__init__.py | 1 + src/compas_notebook/geometry/dot.py | 108 ++++++++++++++++++++ src/compas_notebook/scene/__init__.py | 4 + src/compas_notebook/scene/dotobject.py | 30 ++++++ 7 files changed, 192 insertions(+), 62 deletions(-) create mode 100644 src/compas_notebook/geometry/__init__.py create mode 100644 src/compas_notebook/geometry/dot.py create mode 100644 src/compas_notebook/scene/dotobject.py diff --git a/notebooks/20_geometry.ipynb b/notebooks/20_geometry.ipynb index 00a7d3b..09ae476 100644 --- a/notebooks/20_geometry.ipynb +++ b/notebooks/20_geometry.ipynb @@ -2,7 +2,7 @@ "cells": [ { "cell_type": "code", - "execution_count": 2, + "execution_count": 1, "metadata": {}, "outputs": [], "source": [ @@ -18,70 +18,23 @@ "from compas.geometry import Vector\n", "from compas.geometry import SphericalSurface\n", "from compas.geometry import CylindricalSurface\n", + "from compas_notebook.geometry import Dot\n", "from compas_notebook.viewer import Viewer" ] }, { "cell_type": "code", - "execution_count": 5, + "execution_count": null, "metadata": {}, "outputs": [], - "source": [ - "point = Point(-1, 2, 3)\n", - "vector = Vector(1, 1, 2)\n", - "plane = Plane([0, 0, -1], [0, 0, 1])\n", - "frame = Frame(point, [1, 0, 0], [0, 1, 0])\n", - "\n", - "line = Line([0, 0, 0], point)\n", - "cloud = Pointcloud.from_bounds(x=8, y=5, z=3, n=13)\n", - "polyline = Polyline(cloud.points)\n", - "\n", - "# quadric curves\n", - "circle = Circle(radius=1.5, frame=Frame([2, 0, 0], [0, 1, 0], [0, 0, 1]))\n", - "ellipse = Ellipse(major=2.0, minor=1.0, frame=Frame([5, 0, 0], [0, 1, 0], [0, 0, 1]))\n", - "\n", - "# Create analytical surfaces\n", - "sphere_surface = SphericalSurface(radius=1.5, frame=Frame([0, 0, 0], [1, 0, 0], [0, 1, 0]))\n", - "cylinder_surface = CylindricalSurface(radius=0.8, frame=Frame([3, 0, 0], [1, 0, 0], [0, 1, 0]))" - ] + "source": "point = Point(-1, 2, 3)\nvector = Vector(1, 1, 2)\nplane = Plane([0, 0, -1], [0, 0, 1])\nframe = Frame(point, [1, 0, 0], [0, 1, 0])\n\nline = Line([0, 0, 0], point)\ncloud = Pointcloud.from_bounds(x=8, y=5, z=3, n=13)\npolyline = Polyline(cloud.points)\n\n# quadric curves\ncircle = Circle(radius=1.5, frame=Frame([2, 0, 0], [0, 1, 0], [0, 0, 1]))\nellipse = Ellipse(major=2.0, minor=1.0, frame=Frame([5, 0, 0], [0, 1, 0], [0, 0, 1]))\n\n# Create analytical surfaces\nsphere_surface = SphericalSurface(radius=1.5, frame=Frame([0, 0, 0], [1, 0, 0], [0, 1, 0]))\ncylinder_surface = CylindricalSurface(radius=0.8, frame=Frame([3, 0, 0], [1, 0, 0], [0, 1, 0]))\n\n# Dot: text label at a point (constant screen size)\ndot = Dot([8, 5, 3], \"Corner\")" }, { "cell_type": "code", - "execution_count": 7, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "d32833ac778148c0920dfa0e9b672076", - "version_major": 2, - "version_minor": 0 - }, - "text/plain": [ - "VBox(children=(HBox(children=(Button(icon='search-plus', layout=Layout(height='32px', width='48px'), style=But…" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "viewer = Viewer()\n", - "\n", - "viewer.scene.add(point, color=Color.red(), pointsize=0.3)\n", - "viewer.scene.add(line)\n", - "viewer.scene.add(frame)\n", - "viewer.scene.add(polyline, color=Color.blue())\n", - "viewer.scene.add(cloud, color=Color.green(), pointsize=0.3)\n", - "viewer.scene.add(circle, color=Color.red())\n", - "viewer.scene.add(ellipse, color=Color.green())\n", - "viewer.scene.add(vector)\n", - "viewer.scene.add(plane)\n", - "viewer.scene.add(sphere_surface, color=Color.cyan())\n", - "viewer.scene.add(cylinder_surface, color=Color.magenta())\n", - "\n", - "viewer.show()" - ] + "outputs": [], + "source": "viewer = Viewer()\n\nviewer.scene.add(point, color=Color.red(), pointsize=0.3)\nviewer.scene.add(line)\nviewer.scene.add(frame)\nviewer.scene.add(polyline, color=Color.blue())\nviewer.scene.add(cloud, color=Color.green(), pointsize=0.3)\nviewer.scene.add(circle, color=Color.red())\nviewer.scene.add(ellipse, color=Color.green())\nviewer.scene.add(vector)\nviewer.scene.add(plane)\nviewer.scene.add(sphere_surface, color=Color.cyan())\nviewer.scene.add(cylinder_surface, color=Color.magenta())\nviewer.scene.add(dot, color=Color.black())\n\nviewer.show()" } ], "metadata": { @@ -105,4 +58,4 @@ }, "nbformat": 4, "nbformat_minor": 4 -} +} \ No newline at end of file diff --git a/src/compas_notebook/conversions/__init__.py b/src/compas_notebook/conversions/__init__.py index 76d101a..a0d6802 100644 --- a/src/compas_notebook/conversions/__init__.py +++ b/src/compas_notebook/conversions/__init__.py @@ -3,6 +3,7 @@ from .geometry import box_to_threejs from .geometry import cone_to_threejs from .geometry import cylinder_to_threejs +from .geometry import dot_to_threejs from .geometry import line_to_threejs from .geometry import point_to_threejs from .geometry import pointcloud_to_threejs @@ -26,6 +27,7 @@ "color_to_threejs", "cone_to_threejs", "cylinder_to_threejs", + "dot_to_threejs", "line_to_threejs", "nodes_and_edges_to_threejs", "nodes_to_threejs", diff --git a/src/compas_notebook/conversions/geometry.py b/src/compas_notebook/conversions/geometry.py index 4bab5e2..2804b50 100644 --- a/src/compas_notebook/conversions/geometry.py +++ b/src/compas_notebook/conversions/geometry.py @@ -18,6 +18,7 @@ from compas.geometry import Surface from compas.geometry import Torus from compas.geometry import Vector +from compas_notebook.geometry import Dot from compas_notebook.conversions.meshes import vertices_and_edges_to_threejs from compas_notebook.conversions.meshes import vertices_and_faces_to_threejs @@ -35,7 +36,7 @@ def line_to_threejs(line: Line) -> three.BufferGeometry: :class:`three.BufferGeometry` """ - vertices = numpy.array([line.start, line.end], dtype=numpy.float64) + vertices = numpy.array([line.start, line.end], dtype=numpy.float32) geometry = three.BufferGeometry(attributes={"position": three.BufferAttribute(vertices, normalized=False)}) return geometry @@ -93,7 +94,7 @@ def point_to_threejs(point: Point) -> three.SphereGeometry: BufferGeometry(...) """ - vertices = numpy.array([point], dtype=numpy.float64) + vertices = numpy.array([point], dtype=numpy.float32) geometry = three.BufferGeometry(attributes={"position": three.BufferAttribute(vertices, normalized=False)}) return geometry @@ -115,7 +116,7 @@ def pointcloud_to_threejs(pointcloud: Pointcloud) -> three.SphereGeometry: >>> """ - vertices = numpy.array(pointcloud.points, dtype=numpy.float64) + vertices = numpy.array(pointcloud.points, dtype=numpy.float32) geometry = three.BufferGeometry(attributes={"position": three.BufferAttribute(vertices, normalized=False)}) return geometry @@ -133,7 +134,7 @@ def polyline_to_threejs(polyline: Polyline) -> three.BufferGeometry: :class:`three.BufferGeometry` """ - vertices = numpy.array(polyline.points, dtype=numpy.float64) + vertices = numpy.array(polyline.points, dtype=numpy.float32) geometry = three.BufferGeometry(attributes={"position": three.BufferAttribute(vertices, normalized=False)}) return geometry @@ -156,7 +157,7 @@ def circle_to_threejs(circle: Circle, max_angle: float = 5.0) -> three.BufferGeo n = max(8, int(math.ceil(360.0 / max_angle))) polyline = circle.to_polyline(n=n) - vertices = numpy.array(polyline.points, dtype=numpy.float64) + vertices = numpy.array(polyline.points, dtype=numpy.float32) geometry = three.BufferGeometry(attributes={"position": three.BufferAttribute(vertices, normalized=False)}) return geometry @@ -178,7 +179,7 @@ def ellipse_to_threejs(ellipse: Ellipse, max_angle: float = 5.0) -> three.Buffer """ n = max(8, int(math.ceil(360.0 / max_angle))) polyline = ellipse.to_polyline(n=n) - vertices = numpy.array(polyline.points, dtype=numpy.float64) + vertices = numpy.array(polyline.points, dtype=numpy.float32) geometry = three.BufferGeometry(attributes={"position": three.BufferAttribute(vertices, normalized=False)}) return geometry @@ -326,7 +327,7 @@ def curve_to_threejs(curve: Curve, resolution: int = 100) -> three.BufferGeometr """ polyline = curve.to_polyline(n=resolution) - vertices = numpy.array(polyline.points, dtype=numpy.float64) + vertices = numpy.array(polyline.points, dtype=numpy.float32) geometry = three.BufferGeometry(attributes={"position": three.BufferAttribute(vertices, normalized=False)}) return geometry @@ -505,3 +506,34 @@ def torus_to_threejs(torus: Torus) -> three.TorusGeometry: radialSegments=64, tubularSegments=32, ) + + +def dot_to_threejs(dot: Dot, fontsize: int = 48, color: str = "white") -> three.Sprite: + """Convert a COMPAS Dot to PyThreeJS Sprite. + + The sprite maintains constant screen size regardless of zoom level. + + Parameters + ---------- + dot : :class:`compas_notebook.geometry.Dot` + The dot to convert. + fontsize : int, optional + Font size for the text texture. + color : str, optional + Text color. + + Returns + ------- + :class:`three.Sprite` + A sprite with text texture positioned at the dot location. + + """ + texture = three.TextTexture(string=dot.text, size=fontsize, color=color, squareTexture=False) + material = three.SpriteMaterial(map=texture, sizeAttenuation=False, transparent=True) + sprite = three.Sprite(material=material) + sprite.position = [dot.point.x, dot.point.y, dot.point.z] + # scale based on text length - roughly 0.6 width per character + aspect = len(dot.text) * 0.6 + scale = 0.025 + sprite.scale = [scale * aspect, scale, 1] + return sprite diff --git a/src/compas_notebook/geometry/__init__.py b/src/compas_notebook/geometry/__init__.py new file mode 100644 index 0000000..517aad5 --- /dev/null +++ b/src/compas_notebook/geometry/__init__.py @@ -0,0 +1 @@ +from .dot import Dot diff --git a/src/compas_notebook/geometry/dot.py b/src/compas_notebook/geometry/dot.py new file mode 100644 index 0000000..b6358f8 --- /dev/null +++ b/src/compas_notebook/geometry/dot.py @@ -0,0 +1,108 @@ +from compas.geometry import Geometry +from compas.geometry import Point + + +class Dot(Geometry): + """A dot is text displayed at a point location. + + The dot maintains constant screen size regardless of zoom level, + similar to Rhino's Dot command. + + Parameters + ---------- + point : :class:`compas.geometry.Point` | list[float] + The location of the dot. + text : str + The text to display. + name : str, optional + The name of the dot. + + Attributes + ---------- + point : :class:`compas.geometry.Point` + The location of the dot. + text : str + The text to display. + + Examples + -------- + >>> from compas.geometry import Point + >>> from compas_notebook.geometry import Dot + >>> dot = Dot([0, 0, 0], "Hello") + >>> dot.point + Point(x=0.000, y=0.000, z=0.000) + >>> dot.text + 'Hello' + + """ + + DATASCHEMA = { + "type": "object", + "properties": { + "point": Point.DATASCHEMA, + "text": {"type": "string"}, + }, + "required": ["point", "text"], + } + + @property + def __data__(self): + return { + "point": self._point.__data__, + "text": self._text, + } + + @classmethod + def __from_data__(cls, data): + return cls( + point=Point.__from_data__(data["point"]), + text=data["text"], + ) + + def __init__(self, point, text, name=None): + super().__init__(name=name) + self._point = None + self._text = None + self.point = point + self.text = text + + def __repr__(self): + return f"Dot({self.point!r}, {self.text!r})" + + def __eq__(self, other): + if not isinstance(other, Dot): + return False + return self.point == other.point and self.text == other.text + + @property + def point(self): + return self._point + + @point.setter + def point(self, value): + if not isinstance(value, Point): + value = Point(*value) + self._point = value + + @property + def text(self): + return self._text + + @text.setter + def text(self, value): + self._text = str(value) + + def transform(self, transformation): + """Transform the dot. + + Parameters + ---------- + transformation : :class:`compas.geometry.Transformation` + The transformation to apply. + + Returns + ------- + None + + """ + self.point.transform(transformation) diff --git a/src/compas_notebook/scene/__init__.py b/src/compas_notebook/scene/__init__.py index a670d4e..0840db9 100644 --- a/src/compas_notebook/scene/__init__.py +++ b/src/compas_notebook/scene/__init__.py @@ -28,6 +28,8 @@ from compas.geometry import Torus from compas.geometry import Vector +from compas_notebook.geometry import Dot + from compas.datastructures import Graph from compas.datastructures import Mesh @@ -42,6 +44,7 @@ from .coneobject import ThreeConeObject from .curveobject import ThreeCurveObject from .cylinderobject import ThreeCylinderObject +from .dotobject import ThreeDotObject from .ellipseobject import ThreeEllipseObject from .frameobject import ThreeFrameObject from .groupobject import ThreeGroupObject @@ -89,6 +92,7 @@ def register_scene_objects(): register(Cone, ThreeConeObject, context="Notebook") register(Curve, ThreeCurveObject, context="Notebook") register(Cylinder, ThreeCylinderObject, context="Notebook") + register(Dot, ThreeDotObject, context="Notebook") register(Ellipse, ThreeEllipseObject, context="Notebook") register(Frame, ThreeFrameObject, context="Notebook") register(Graph, ThreeGraphObject, context="Notebook") diff --git a/src/compas_notebook/scene/dotobject.py b/src/compas_notebook/scene/dotobject.py new file mode 100644 index 0000000..b56d9a7 --- /dev/null +++ b/src/compas_notebook/scene/dotobject.py @@ -0,0 +1,30 @@ +from compas.colors import Color +from compas.scene import GeometryObject +from compas.scene.descriptors.color import ColorAttribute + +from compas_notebook.conversions import dot_to_threejs + +from .sceneobject import ThreeSceneObject + + +class ThreeDotObject(ThreeSceneObject, GeometryObject): + """Scene object for drawing a dot (text at a point).""" + + color = ColorAttribute(default=Color.black()) + + def __init__(self, fontsize=256, **kwargs): + super().__init__(**kwargs) + self.fontsize = fontsize + + def draw(self): + """Draw the dot associated with the scene object. + + Returns + ------- + list[three.Sprite] + List of pythreejs objects created. + + """ + sprite = dot_to_threejs(self.geometry, fontsize=self.fontsize, color=self.color.hex) + self._guids = [sprite] + return self.guids From 1041b1a5a3aa99c46494d1e07f5731f9112a1d7e Mon Sep 17 00:00:00 2001 From: Jelle Feringa Date: Fri, 5 Dec 2025 13:57:14 +0100 Subject: [PATCH 07/10] update changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index fe0a381..6a4274b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added * Added support for `frames, circle, ellipse, vector, plane, curve, analystical surface primitives` +* Dot command added ### Changed From 4019cd1b6a950057ad8b89096497817e9689d6f9 Mon Sep 17 00:00:00 2001 From: Jelle Feringa Date: Fri, 5 Dec 2025 15:15:59 +0100 Subject: [PATCH 08/10] life is ruff --- src/compas_notebook/geometry/__init__.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/compas_notebook/geometry/__init__.py b/src/compas_notebook/geometry/__init__.py index 517aad5..70efd2e 100644 --- a/src/compas_notebook/geometry/__init__.py +++ b/src/compas_notebook/geometry/__init__.py @@ -1 +1,3 @@ from .dot import Dot + +__all__ = ["Dot",] # under protest From 456a6a33601dabe1da3c8f0c9a8722bcf898c9c1 Mon Sep 17 00:00:00 2001 From: Jelle Feringa Date: Fri, 5 Dec 2025 16:21:05 +0100 Subject: [PATCH 09/10] ruff conformancy --- src/compas_notebook/scene/__init__.py | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/src/compas_notebook/scene/__init__.py b/src/compas_notebook/scene/__init__.py index 0840db9..1e04476 100644 --- a/src/compas_notebook/scene/__init__.py +++ b/src/compas_notebook/scene/__init__.py @@ -110,3 +110,29 @@ def register_scene_objects(): register(Vector, ThreeVectorObject, context="Notebook") register(list, ThreeGroupObject, context="Notebook") + +_ = [ + "Box", + "Brep", + "Capsule", + "Circle", + "Cone", + "Curve", + "Cylinder", + "Dot", + "Ellipse", + "Frame", + "Graph", + "Line", + "Mesh", + "Plane", + "Point", + "Pointcloud", + "Polygon", + "Polyhedron", + "Polyline", + "Sphere", + "Surface", + "Torus", + "Vector", +] From 2ff3694f0456ed8569482c3a832ae941e6fe0a4a Mon Sep 17 00:00:00 2001 From: Tom Van Mele Date: Fri, 5 Dec 2025 22:56:46 +0100 Subject: [PATCH 10/10] Clean up unused scene object imports Removed unused imports for various scene object types. --- src/compas_notebook/scene/__init__.py | 27 +-------------------------- 1 file changed, 1 insertion(+), 26 deletions(-) diff --git a/src/compas_notebook/scene/__init__.py b/src/compas_notebook/scene/__init__.py index 1e04476..ebf84a1 100644 --- a/src/compas_notebook/scene/__init__.py +++ b/src/compas_notebook/scene/__init__.py @@ -1,3 +1,4 @@ +# ruff: noqa: F401 """This package provides scene object plugins for visualising COMPAS objects in Jupyter Notebooks using three. When working in a notebook, :class:`compas.scene.SceneObject` will automatically use the corresponding PyThreeJS scene object for each COMPAS object type. @@ -110,29 +111,3 @@ def register_scene_objects(): register(Vector, ThreeVectorObject, context="Notebook") register(list, ThreeGroupObject, context="Notebook") - -_ = [ - "Box", - "Brep", - "Capsule", - "Circle", - "Cone", - "Curve", - "Cylinder", - "Dot", - "Ellipse", - "Frame", - "Graph", - "Line", - "Mesh", - "Plane", - "Point", - "Pointcloud", - "Polygon", - "Polyhedron", - "Polyline", - "Sphere", - "Surface", - "Torus", - "Vector", -]