From c2e01dea8e49ffe6db575e2fc129b5ea6698b5b5 Mon Sep 17 00:00:00 2001 From: Dusan Maliarik Date: Tue, 22 Dec 2020 14:04:09 -0700 Subject: [PATCH] init --- .../product_extrusion-checkpoint.ipynb | 950 ++++++++++++++++++ Pipfile | 1 + Pipfile.lock | 112 ++- product_extrusion.ipynb | 74 +- 4 files changed, 1099 insertions(+), 38 deletions(-) create mode 100644 .ipynb_checkpoints/product_extrusion-checkpoint.ipynb diff --git a/.ipynb_checkpoints/product_extrusion-checkpoint.ipynb b/.ipynb_checkpoints/product_extrusion-checkpoint.ipynb new file mode 100644 index 0000000..20da75a --- /dev/null +++ b/.ipynb_checkpoints/product_extrusion-checkpoint.ipynb @@ -0,0 +1,950 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "\n", + "# Product application in 3D\n", + "\n", + "## Algorithm to generate 3D mesh from extrusion profile and arbitrary polygon\n", + "\n", + "The input polygon that we want to cover with 3D tiles needs to be:\n", + " - simple\n", + " - planar\n", + "\n", + "Base of the idea is in establishing TBN space, defined by (t)angent, (b)itangent and (n)ormal vector of the input polygon. In this space we can align the (repeated) 2D profile to the top (start) and bottom (end) edge of the polygon's bounding box. These two paths are then bridged to create a quad-mesh. This mesh is then clipped to the original polygon using quasi-CSG operations.\n", + "\n", + "Notebook requirements:\n", + " * Jupyter lab\n", + " * numpy, scipy, ipywidgets\n", + " * pythreejs (both the package and the lab extension)\n", + " * cython-csg\n", + " " + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Imports and basic tools" + ] + }, + { + "cell_type": "code", + "execution_count": 225, + "metadata": { + "jupyter": { + "source_hidden": true + } + }, + "outputs": [], + "source": [ + "import random\n", + "import numpy as np\n", + "from math import pi, ceil, floor\n", + "from scipy.linalg import expm, norm\n", + "from scipy.spatial import Delaunay\n", + "import ipywidgets as widgets\n", + "from ipywidgets import interact, interact_manual\n", + "\n", + "EPS = 1e-5\n", + "\n", + "def poly_normal(poly):\n", + " a, b, c = poly[:3]\n", + " x = np.linalg.det([[1,a[1],a[2]],\n", + " [1,b[1],b[2]],\n", + " [1,c[1],c[2]]])\n", + " y = np.linalg.det([[a[0],1,a[2]],\n", + " [b[0],1,b[2]],\n", + " [c[0],1,c[2]]])\n", + " z = np.linalg.det([[a[0],a[1],1],\n", + " [b[0],b[1],1],\n", + " [c[0],c[1],1]])\n", + " magnitude = (x**2 + y**2 + z**2)**.5\n", + " return (x/magnitude, y/magnitude, z/magnitude)\n", + "\n", + "def poly_area(poly):\n", + " if len(poly) < 3: # not a plane - no area\n", + " return 0\n", + " total = [0, 0, 0]\n", + " N = len(poly)\n", + " for i in range(N):\n", + " vi1 = poly[i]\n", + " vi2 = poly[(i+1) % N]\n", + " prod = np.cross(vi1, vi2)\n", + " total[0] += prod[0]\n", + " total[1] += prod[1]\n", + " total[2] += prod[2]\n", + " result = np.dot(total, poly_normal(poly[0], poly[1], poly[2]))\n", + " return result/2\n", + "\n", + "def normalize(v):\n", + " return v/np.linalg.norm(v)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Display functions" + ] + }, + { + "cell_type": "code", + "execution_count": 226, + "metadata": { + "jupyter": { + "source_hidden": true + } + }, + "outputs": [], + "source": [ + "from collections import namedtuple\n", + "from pythreejs import *\n", + "from IPython.display import display\n", + "import matplotlib.pyplot as plt\n", + "import matplotlib.patches as pltpat\n", + "from matplotlib.colors import to_hex\n", + "\n", + "Face3D = namedtuple('Face', [\n", + " 'a','b','c',\n", + " 'normal','color','materialIndex'\n", + "])\n", + "\n", + "Bounds = namedtuple('Bounds', [\n", + " 'min', 'max', 'size', 'center', 'mag'\n", + "])\n", + "\n", + "W = 600\n", + "H = 400\n", + "\n", + "def bounds(position):\n", + " bmin = np.min(position, axis=0)\n", + " bmax = np.max(position, axis=0)\n", + " bext = bmax - bmin\n", + " bmag = np.max(bext)\n", + " bctr = bmin + bext*0.5\n", + " return Bounds(\n", + " min=bmin, max=bmax, size=bext, center=bctr, mag=bmag)\n", + "\n", + "def make_scene(*args):\n", + " scene = Scene(children=[\n", + " DirectionalLight(\n", + " color=\"#ffffff\",\n", + " position=[3,5,1],\n", + " intensity=1.0),\n", + " AmbientLight(\n", + " color=\"#ffffff\",\n", + " intensity=0.3),\n", + " GridHelper(15,15),\n", + " AxesHelper(1)\n", + " ])\n", + " for x in args:\n", + " scene.add(x)\n", + " return scene\n", + "\n", + "def display_poly2D(poly):\n", + " p = pltpat.Polygon(poly, closed=False)\n", + " ax = plt.gca()\n", + " ax.add_patch(p)\n", + " bmin = np.min(poly, axis=0)\n", + " bmax = np.max(poly, axis=0)\n", + " ax.set_xlim(bmin[0]-1, bmax[0]+1)\n", + " ax.set_ylim(bmin[1]-1, bmax[1]+1)\n", + " plt.show()\n", + "\n", + "def poly_to_mesh(position, indices, color, opacity=1):\n", + " geom = Geometry()\n", + " geom.vertices = position\n", + " geom.faces = [\n", + " Face3D(a=a, b=b, c=c, \n", + " normal=poly_normal([\n", + " position[a],\n", + " position[b],\n", + " position[c]\n", + " ]), \n", + " color=(1,1,1), \n", + " materialIndex=0)\n", + " for a, b, c in indices]\n", + " mtl = MeshStandardMaterial(\n", + " color=to_hex(color),\n", + " metallicity=0,\n", + " roughness=1,\n", + " opacity=opacity,\n", + " transparent=True,\n", + " side='DoubleSide')\n", + " return Mesh(geom, mtl), bounds(position)\n", + "\n", + "def mesh_renderer(mesh, bbox):\n", + " scene = make_scene(mesh)\n", + " camera = PerspectiveCamera(\n", + " position=[1,1,1], up=[0,1,0], aspect=W/H)\n", + " camera.position = tuple(bbox.center + [0, bbox.mag/2, bbox.mag])\n", + " ctrl = OrbitControls(\n", + " controlling=camera, \n", + " target=tuple(bbox.center))\n", + " ctrl.exec_three_obj_method('update')\n", + " \n", + " return Renderer(\n", + " width=W, height=H,\n", + " camera=camera, scene=scene, controls=[ctrl])\n", + " \n", + "# display(\n", + "# mesh_renderer(\n", + "# *poly_to_mesh(\n", + "# [[0,0,0], [1,0,0], [1,0,-1], [0,0,-1]], \n", + "# [[0,1,2],[0,2,3]],\n", + "# (1,0,0))))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Define input polygon" + ] + }, + { + "cell_type": "code", + "execution_count": 227, + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "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\n", + "text/plain": [ + "
" + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + } + ], + "source": [ + "# Start in XZ plane\n", + "face_xz = np.array([\n", + " [-5, 0, 0],\n", + " [ 5, 0, 0],\n", + " [ 6, 0, -3],\n", + " [ 0, 0, -5]\n", + "])\n", + "face_xz_2D = face_xz[:,[0,2]]\n", + "\n", + "display_poly2D(face_xz_2D)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Bring it to 3D" + ] + }, + { + "cell_type": "code", + "execution_count": 228, + "metadata": {}, + "outputs": [], + "source": [ + "import random\n", + "\n", + "def rotate(axis, theta):\n", + " return expm(np.cross(np.eye(3), axis/norm(axis)*theta))\n", + "\n", + "tf = np.dot(\n", + " # rotate on Y axis by 90 deg.\n", + " rotate([0,1,0], random.uniform(-pi/2, pi/2)), \n", + " # rotate on X axis by n deg.\n", + " rotate([1,0,0], random.uniform(0, pi/4))\n", + ")\n", + "tf_inv = np.linalg.inv(tf)\n", + "\n", + "face = tf.dot(face_xz.T).T\n", + "face_indices = Delaunay(face_xz_2D).simplices" + ] + }, + { + "cell_type": "code", + "execution_count": 229, + "metadata": { + "jupyter": { + "source_hidden": true + } + }, + "outputs": [ + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "1bbfd79531d74a6aa21caffb2699a9f2", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "Renderer(camera=PerspectiveCamera(aspect=1.5, position=(0.28830735601264745, 6.947213875632741, 9.114960387348…" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "display(mesh_renderer(*poly_to_mesh(face.tolist(), face_indices, (1,0.5,1))))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Find basis for surface tangent space\n", + "\n", + "Following doesn't work well for polygons aligned to XZ plane, ie. when the normal is the same as the \"projection\" vector, which in our \"common\" case is the up-vector." + ] + }, + { + "cell_type": "code", + "execution_count": 230, + "metadata": {}, + "outputs": [], + "source": [ + "def poly_basis(p, poly):\n", + " n = poly_normal(poly)\n", + " t = np.cross(p, n)\n", + " if np.linalg.norm(t) < EPS:\n", + " # colinear normal with projection, this could be\n", + " # some \"base edge\" if known\n", + " t = np.array([1, 0, 0])\n", + " else:\n", + " t = normalize(t)\n", + " b = normalize(np.cross(t, n))\n", + " return [t, b, n]\n", + "\n", + "basis = poly_basis([0,1,0], face)" + ] + }, + { + "cell_type": "code", + "execution_count": 231, + "metadata": { + "jupyter": { + "source_hidden": true + } + }, + "outputs": [ + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "f7fce8d8be4a4387bcd6b5a65099adc2", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "Renderer(camera=PerspectiveCamera(aspect=1.5, position=(0.28830735601264745, 6.947213875632741, 9.114960387348…" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "RGB = [(1,0,0),(0,1,0),(0,0,1)]\n", + "\n", + "def basis_helpers_rgb(origin, t, b, n):\n", + " return [\n", + " ArrowHelper(\n", + " origin=origin, dir=tuple(v), \n", + " color=to_hex(RGB[i]), headWidth=0.2)\n", + " for i, v in enumerate([t, b, n])]\n", + "\n", + "def basis_helpers(origin, color, t, b, n):\n", + " return [\n", + " ArrowHelper(\n", + " origin=origin, dir=tuple(v), \n", + " color=color, headWidth=0.2)\n", + " for i, v in enumerate([t, b, n])]\n", + "\n", + "def basis_renderer(origin, opacity=1):\n", + " renderer = mesh_renderer(*poly_to_mesh(face.tolist(), face_indices, (1,0.5,1), opacity))\n", + " for h in basis_helpers_rgb(origin, *basis):\n", + " renderer.scene.add(h)\n", + " return renderer\n", + "\n", + "origin = tuple(tf.dot(bounds(face_xz).center+[0,0.01,0]))\n", + "display(basis_renderer(origin))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Find extrusion origin\n", + "\n", + "We need to find an origin point where we can place the 2D profile to be extruded, as well as extrusion axis (the profile will be oriented perpendicular to the extrusion axis).\n", + "\n", + "To do this, we transform the face into tangent space using TBN (tangent, bitangent, normal) matrix, and then take the min of its bounding box. This is then transformed using inverse TBN matrix back to world space." + ] + }, + { + "cell_type": "code", + "execution_count": 232, + "metadata": {}, + "outputs": [], + "source": [ + "TBN = np.array(basis)\n", + "TBN_inv = np.linalg.inv(TBN)\n", + "face_tbn = TBN.dot(face.T).T\n", + "ext_origin = TBN_inv.dot(np.min(face_tbn, axis=0))" + ] + }, + { + "cell_type": "code", + "execution_count": 233, + "metadata": { + "jupyter": { + "source_hidden": true + } + }, + "outputs": [ + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "ea3a6b17dede40f5ad0ff13bc85ec181", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "Renderer(camera=PerspectiveCamera(aspect=1.5, position=(0.28830735601264745, 6.947213875632741, 9.114960387348…" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "def tbn_and_origin_renderer():\n", + " mesh, _ = poly_to_mesh(face_tbn.tolist(), face_indices, (0,1,1), 0.5)\n", + " renderer = basis_renderer(ext_origin.tolist(), opacity=0.5)\n", + " renderer.scene.add(mesh)\n", + " return renderer\n", + "\n", + "display(tbn_and_origin_renderer())" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Define 2D profile curve\n", + "\n", + "This will be specified on input, such as a path from SVG source." + ] + }, + { + "cell_type": "code", + "execution_count": 234, + "metadata": {}, + "outputs": [], + "source": [ + "profile = np.array([\n", + " [0.0, 0.2],\n", + " [1.0, 0.2],\n", + " [1.1, 0.5],\n", + " [1.9, 0.5],\n", + " [2.0, 0.2]\n", + "], dtype=np.float32)\n", + "profile_bounds = bounds(profile)\n", + "unit_profile = (profile - profile_bounds.min) / profile_bounds.size[0]" + ] + }, + { + "cell_type": "code", + "execution_count": 235, + "metadata": { + "jupyter": { + "source_hidden": true + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "(-0.29999999701976776, 1.0)" + ] + }, + "execution_count": 235, + "metadata": {}, + "output_type": "execute_result" + }, + { + "data": { + "image/png": "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\n", + "text/plain": [ + "
" + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + } + ], + "source": [ + "plt.plot(profile[:,0], profile[:,1])\n", + "plt.plot(unit_profile[:,0], unit_profile[:,1])\n", + "plt.xlim(profile_bounds.min[0]-0.5, profile_bounds.max[0]+0.5)\n", + "plt.ylim(profile_bounds.min[1]-0.5, profile_bounds.max[1]+0.5)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Transform and extend the profile\n", + "\n", + "- Such that its origin aligns with extrusion origin and basis vectors.\n", + "- Such that the paths is repeated the cover the face in tangent direction." + ] + }, + { + "cell_type": "code", + "execution_count": 236, + "metadata": {}, + "outputs": [], + "source": [ + "unit_profile_3D = TBN_inv.dot(np.insert(unit_profile, 1, values=0, axis=1).T).T + ext_origin\n", + "nrep = ceil(bounds(face_tbn).size[0])\n", + "\n", + "def repeat_path(path, nrep):\n", + " step = bounds(path).size[0]\n", + " vertices = np.empty((0, path.shape[1]))\n", + " offset = 0\n", + " for i in range(nrep):\n", + " offset_vertices = path + [offset, 0]\n", + " if len(vertices) > 0 and np.linalg.norm(offset_vertices[0] - vertices[-1]) < EPS:\n", + " vertices = np.concatenate((vertices, offset_vertices[1:]), axis=0)\n", + " else:\n", + " vertices = np.concatenate((vertices, offset_vertices), axis=0)\n", + " offset += step\n", + " return np.array(vertices)\n", + "\n", + "profile_ext = TBN_inv.dot(\n", + " np.insert(\n", + " repeat_path(unit_profile, nrep), \n", + " 1, values=0, axis=1).T\n", + ").T + ext_origin" + ] + }, + { + "cell_type": "code", + "execution_count": 237, + "metadata": { + "jupyter": { + "source_hidden": true + } + }, + "outputs": [ + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "d853bc291ada4785ab9d61e7bff07504", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "HBox(children=(Label(value='Width :'), FloatSlider(value=1.0, max=3.0, min=0.25, step=0.01)))" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "48edac81872d404d89279aa8caf6d186", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "Renderer(camera=PerspectiveCamera(aspect=1.5, position=(0.28830735601264745, 6.947213875632741, 9.114960387348…" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "def profile_geom(path, width, offset=[0,0,0]):\n", + " nrep = ceil(bounds(face_tbn).size[0]/width)\n", + " single_profile = path*width\n", + " retpath = TBN_inv.dot(\n", + " np.insert(\n", + " repeat_path(single_profile, nrep), \n", + " 1, values=0, axis=1).T\n", + " ).T + ext_origin + offset\n", + " return BufferGeometry(\n", + " attributes={\n", + " 'position': BufferAttribute(retpath.astype(np.float32))\n", + " }, normalized=False)\n", + " \n", + "def profile_mesh(path, width, offset=[0,0,0], color=\"#ff0000\"):\n", + " geom = profile_geom(path, width, offset)\n", + " mtl = LineBasicMaterial(color=color)\n", + " return Line(geom, mtl)\n", + "\n", + "def profile_renderer(path, width):\n", + " mesh = profile_mesh(path, width)\n", + " renderer = mesh_renderer(\n", + " *poly_to_mesh(face.tolist(), face_indices, (1,0.5,1), \n", + " opacity=0.75))\n", + " for h in basis_helpers(ext_origin.tolist(), \"#cccccc\", *basis):\n", + " renderer.scene.add(h)\n", + " renderer.scene.add(mesh)\n", + " return renderer, mesh\n", + "\n", + "def run_profile_renderer():\n", + " renderer, mesh = profile_renderer(unit_profile, 1.0)\n", + " def update_profile_renderer(width):\n", + " mesh.geometry.exec_three_obj_method('dispose')\n", + " mesh.geometry = profile_geom(unit_profile, width['new'])\n", + " width = widgets.FloatSlider(min=0.25, max=3.0, step=0.01, value=1.0)\n", + " width.observe(update_profile_renderer, 'value')\n", + " display(widgets.HBox(children=[\n", + " widgets.Label(\"Width :\"),\n", + " width]))\n", + " display(renderer)\n", + "\n", + "run_profile_renderer()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Next, create the other end of the extrusion by offseting the base along bitangent vector, by the Y extent in TBN space:" + ] + }, + { + "cell_type": "code", + "execution_count": 238, + "metadata": {}, + "outputs": [], + "source": [ + "extrusion_length = bounds(face_tbn).size[1]\n", + "profile_ext_end = profile_ext + basis[1]*extrusion_length" + ] + }, + { + "cell_type": "code", + "execution_count": 239, + "metadata": { + "jupyter": { + "source_hidden": true + } + }, + "outputs": [ + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "239e8f7593324210860fb5c51704bbf6", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "HBox(children=(Label(value='Width :'), FloatSlider(value=1.0, max=3.0, min=0.25, step=0.01)))" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "f76f4a2c5c7e4a668be19be4aca8ee5b", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "Renderer(camera=PerspectiveCamera(aspect=1.5, position=(0.28830735601264745, 6.947213875632741, 9.114960387348…" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "def extrusion_end_renderer(path, width):\n", + " renderer, start = profile_renderer(path, width)\n", + " end = profile_mesh(path, width, basis[1]*extrusion_length, \"#0000ff\")\n", + " renderer.scene.add(end)\n", + " return renderer, start, end\n", + "\n", + "def run_extrusion_end_renderer(width):\n", + " renderer, start, end = extrusion_end_renderer(unit_profile, width)\n", + " def update_renderer(width):\n", + " start.geometry.exec_three_obj_method('dispose')\n", + " start.geometry = profile_geom(unit_profile, width['new'])\n", + " end.geometry.exec_three_obj_method('dispose')\n", + " end.geometry = profile_geom(unit_profile, width['new'], basis[1]*extrusion_length)\n", + " wwidth = widgets.FloatSlider(min=0.25, max=3.0, step=0.01, value=width)\n", + " wwidth.observe(update_renderer, 'value')\n", + " display(widgets.HBox(children=[\n", + " widgets.Label(\"Width :\"),\n", + " wwidth]))\n", + " display(renderer)\n", + "\n", + "run_extrusion_end_renderer(1.0)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Bridge start and end paths to create triangle mesh" + ] + }, + { + "cell_type": "code", + "execution_count": 240, + "metadata": {}, + "outputs": [], + "source": [ + "def edges(path):\n", + " \"\"\" Generate edges from path vertices \"\"\"\n", + " for i in range(0, len(path)-1):\n", + " yield path[i:i+2]\n", + "\n", + "def bridge_quads(start, end):\n", + " \"\"\" Bridge two paths, generating quads \"\"\"\n", + " for [s0, s1], [e0, e1] in zip(edges(start), edges(end)):\n", + " yield [s0, s1, e1, e0]\n", + "\n", + "def bridge_tris(start, end):\n", + " \"\"\" Bridge two paths, generating triangles \"\"\"\n", + " for a, b, c, d in bridge_quads(start, end):\n", + " yield [a, b, c]\n", + " yield [a, c, d]\n", + " \n", + "def bridge_faces():\n", + " return bridge_quads(profile_ext_end, profile_ext)" + ] + }, + { + "cell_type": "code", + "execution_count": 241, + "metadata": { + "jupyter": { + "source_hidden": true + } + }, + "outputs": [ + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "62d6f81da24649c0aa1a73421144a1fb", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "Renderer(camera=PerspectiveCamera(aspect=1.5, position=(0.28830735601264745, 6.947213875632741, 9.114960387348…" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "def faces_to_mesh(faces):\n", + " geom = BufferGeometry(\n", + " attributes={\n", + " 'position': BufferAttribute(faces.astype(np.float32))\n", + " })\n", + " geom.exec_three_obj_method('computeVertexNormals')\n", + " return geom\n", + "\n", + "def face_outline(path):\n", + " line = np.copy(path).tolist()\n", + " line.append(line[0])\n", + " return BufferGeometry(\n", + " attributes={\n", + " 'position': BufferAttribute(np.array(line, dtype=np.float32))\n", + " }, normalized=False)\n", + "\n", + "def bridge_renderer(start, end):\n", + " renderer, _, _ = extrusion_end_renderer(unit_profile, 1.0)\n", + " renderer.scene.remove(renderer.scene.children[4])\n", + " geom = faces_to_mesh(np.array(list(bridge_tris(end, start))))\n", + " mtl = MeshStandardMaterial(\n", + " color=\"#66ffff\", \n", + " opacity=0.8, \n", + " transparent=True,\n", + " side='DoubleSide')\n", + " mesh = Mesh(geom, mtl)\n", + " renderer.scene.add(mesh)\n", + " geom = face_outline(face)\n", + " mtl = LineBasicMaterial(color=\"#000000\")\n", + " mesh = Line(geom, mtl)\n", + " renderer.scene.add(mesh)\n", + " return renderer\n", + " \n", + "display(bridge_renderer(profile_ext, profile_ext_end))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Clip generated faces using input face" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Polygon clipping is a big problem on its own, we need to use external library to save time. Here we are using [Cython CSG](https://github.com/tomturner/cython-csg) library for Python, it's an optimized port if PyCSG library by Tim Knip, which is a port of [csg.js](https://evanw.github.io/csg.js/) library by Evan W. " + ] + }, + { + "cell_type": "code", + "execution_count": 242, + "metadata": {}, + "outputs": [], + "source": [ + "from _cython_csg import CSG, BSPNode, Polygon, Vertex" + ] + }, + { + "cell_type": "code", + "execution_count": 269, + "metadata": {}, + "outputs": [], + "source": [ + "def polys_to_csg(polys):\n", + " \"\"\" Convert sequence of polygons into CSG object \"\"\"\n", + " return CSG.fromPolygons([\n", + " Polygon([Vertex([x, y, z]) for x, y, z in poly])\n", + " for poly in polys\n", + " ])\n", + "\n", + "def poly_to_clipping_csg(poly, n, offset=100):\n", + " \"\"\" Create clipping CSG object by extruding sides of the polygon\n", + " along the vector n \"\"\"\n", + " noff = n*offset\n", + " def make_side(a, b):\n", + " return [\n", + " a - noff,\n", + " b - noff,\n", + " b + noff,\n", + " a + noff\n", + " ]\n", + " return polys_to_csg([\n", + " make_side(a, b) for a, b in edges(np.concatenate((poly, [poly[0]])))\n", + " ])\n", + "\n", + "def csg_to_polys(csg):\n", + " \"\"\" Convert CSG object to polygon sequence \"\"\"\n", + " return [\n", + " np.array([[p.pos.x, p.pos.y, p.pos.z] for p in poly.vertices])\n", + " for poly in csg.toPolygons()\n", + " ]\n", + "\n", + "def clip(A, B):\n", + " \"\"\" Custom clipping operation based on BSP tree node operations \"\"\"\n", + " a = BSPNode(A.clone().polygons)\n", + " b = BSPNode(B.clone().polygons)\n", + " b.invert()\n", + " a.clipTo(b)\n", + " return CSG.fromPolygons(a.allPolygons())\n", + "\n", + "bridge_csg = polys_to_csg(bridge_faces())\n", + "clipping_csg = poly_to_clipping_csg(face, TBN[2])\n", + "clipped_polys = csg_to_polys(clip(bridge_csg, clipping_csg))\n", + "\n", + "def triangulate3(poly):\n", + " \"\"\" Triangulate 3D polygon by first transforming it into XY plane,\n", + " and then computing Dealaunay triangulation. \"\"\"\n", + " tbn = np.array(poly_basis([0,0,1], poly))\n", + " poly_tbn = tbn.dot(poly.T).T[:,[0,1]]\n", + " indices = Delaunay(poly_tbn).simplices\n", + " return poly[indices]\n", + " \n", + "clipped_tris = np.concatenate(\n", + " [triangulate3(face) for face in clipped_polys], axis=0)\n" + ] + }, + { + "cell_type": "code", + "execution_count": 271, + "metadata": { + "jupyter": { + "source_hidden": true + } + }, + "outputs": [ + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "35a716badd8749edac90db265d88f4ff", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "Renderer(camera=PerspectiveCamera(aspect=1.5, position=(0.28830735601264745, 6.947213875632741, 9.114960387348…" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "def csg_result_renderer():\n", + " renderer, _, _ = extrusion_end_renderer(unit_profile, 1.0)\n", + " renderer.scene.remove(renderer.scene.children[4])\n", + " geom = faces_to_mesh(clipped_tris)\n", + " mtl = MeshStandardMaterial(\n", + " color=\"#66ffff\", \n", + " opacity=0.8, \n", + " transparent=True,\n", + " side='DoubleSide')\n", + " mesh = Mesh(geom, mtl)\n", + " renderer.scene.add(mesh)\n", + " geom = face_outline(face)\n", + " mtl = LineBasicMaterial(color=\"#000000\")\n", + " mesh = Line(geom, mtl)\n", + " renderer.scene.add(mesh)\n", + " return renderer\n", + " \n", + "display(csg_result_renderer())" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# The End" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.9.0" + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} diff --git a/Pipfile b/Pipfile index 5064b5c..3a22907 100644 --- a/Pipfile +++ b/Pipfile @@ -8,6 +8,7 @@ pythreejs = "*" cython = "*" cython-csg = "*" scipy = "*" +matplotlib = "*" [dev-packages] diff --git a/Pipfile.lock b/Pipfile.lock index 17943db..ae09eb4 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "0a1fcff58a6667fe9b4d5eba7dd0386cd77600895d6f1b1bbcab24df3f4a64f3" + "sha256": "4d4b7a6d326468a385a6e94ee7e1283eaa3a9403639d415f9ff571ac0e508e3c" }, "pipfile-spec": 6, "requires": { @@ -111,6 +111,13 @@ ], "version": "==1.14.4" }, + "cycler": { + "hashes": [ + "sha256:1d8a5ae1ff6c5cf9b93e8811e581232ad8920aeec647c37316ceac982b08cb2d", + "sha256:cd7b2d1018258d7247a71425e9f26463dfb444d411c39569972f4ce586b0c9d8" + ], + "version": "==0.10.0" + }, "cython": { "hashes": [ "sha256:0ac10bf476476a9f7ef61ec6e44c280ef434473124ad31d3132b720f7b0e8d2a", @@ -265,6 +272,44 @@ ], "version": "==0.1.2" }, + "kiwisolver": { + "hashes": [ + "sha256:0cd53f403202159b44528498de18f9285b04482bab2a6fc3f5dd8dbb9352e30d", + "sha256:1e1bc12fb773a7b2ffdeb8380609f4f8064777877b2225dec3da711b421fda31", + "sha256:225e2e18f271e0ed8157d7f4518ffbf99b9450fca398d561eb5c4a87d0986dd9", + "sha256:232c9e11fd7ac3a470d65cd67e4359eee155ec57e822e5220322d7b2ac84fbf0", + "sha256:31dfd2ac56edc0ff9ac295193eeaea1c0c923c0355bf948fbd99ed6018010b72", + "sha256:33449715e0101e4d34f64990352bce4095c8bf13bed1b390773fc0a7295967b3", + "sha256:401a2e9afa8588589775fe34fc22d918ae839aaaf0c0e96441c0fdbce6d8ebe6", + "sha256:44a62e24d9b01ba94ae7a4a6c3fb215dc4af1dde817e7498d901e229aaf50e4e", + "sha256:50af681a36b2a1dee1d3c169ade9fdc59207d3c31e522519181e12f1b3ba7000", + "sha256:563c649cfdef27d081c84e72a03b48ea9408c16657500c312575ae9d9f7bc1c3", + "sha256:5989db3b3b34b76c09253deeaf7fbc2707616f130e166996606c284395da3f18", + "sha256:5a7a7dbff17e66fac9142ae2ecafb719393aaee6a3768c9de2fd425c63b53e21", + "sha256:5c3e6455341008a054cccee8c5d24481bcfe1acdbc9add30aa95798e95c65621", + "sha256:5f6ccd3dd0b9739edcf407514016108e2280769c73a85b9e59aa390046dbf08b", + "sha256:72c99e39d005b793fb7d3d4e660aed6b6281b502e8c1eaf8ee8346023c8e03bc", + "sha256:78751b33595f7f9511952e7e60ce858c6d64db2e062afb325985ddbd34b5c131", + "sha256:834ee27348c4aefc20b479335fd422a2c69db55f7d9ab61721ac8cd83eb78882", + "sha256:8be8d84b7d4f2ba4ffff3665bcd0211318aa632395a1a41553250484a871d454", + "sha256:950a199911a8d94683a6b10321f9345d5a3a8433ec58b217ace979e18f16e248", + "sha256:a357fd4f15ee49b4a98b44ec23a34a95f1e00292a139d6015c11f55774ef10de", + "sha256:a53d27d0c2a0ebd07e395e56a1fbdf75ffedc4a05943daf472af163413ce9598", + "sha256:acef3d59d47dd85ecf909c359d0fd2c81ed33bdff70216d3956b463e12c38a54", + "sha256:b38694dcdac990a743aa654037ff1188c7a9801ac3ccc548d3341014bc5ca278", + "sha256:b9edd0110a77fc321ab090aaa1cfcaba1d8499850a12848b81be2222eab648f6", + "sha256:c08e95114951dc2090c4a630c2385bef681cacf12636fb0241accdc6b303fd81", + "sha256:c5518d51a0735b1e6cee1fdce66359f8d2b59c3ca85dc2b0813a8aa86818a030", + "sha256:c8fd0f1ae9d92b42854b2979024d7597685ce4ada367172ed7c09edf2cef9cb8", + "sha256:ca3820eb7f7faf7f0aa88de0e54681bddcb46e485beb844fcecbcd1c8bd01689", + "sha256:cf8b574c7b9aa060c62116d4181f3a1a4e821b2ec5cbfe3775809474113748d4", + "sha256:d3155d828dec1d43283bd24d3d3e0d9c7c350cdfcc0bd06c0ad1209c1bbc36d0", + "sha256:f8d6f8db88049a699817fd9178782867bf22283e3813064302ac59f61d95be05", + "sha256:fd34fbbfbc40628200730bc1febe30631347103fc8d3d4fa012c21ab9c11eca9" + ], + "markers": "python_version >= '3.6'", + "version": "==1.3.1" + }, "markupsafe": { "hashes": [ "sha256:00bc623926325b26bb9605ae9eae8a215691f33cae5df11ca5424f06f2d1f473", @@ -304,6 +349,37 @@ "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==1.1.1" }, + "matplotlib": { + "hashes": [ + "sha256:09225edca87a79815822eb7d3be63a83ebd4d9d98d5aa3a15a94f4eee2435954", + "sha256:0caa687fce6174fef9b27d45f8cc57cbc572e04e98c81db8e628b12b563d59a2", + "sha256:27c9393fada62bd0ad7c730562a0fecbd3d5aaa8d9ed80ba7d3ebb8abc4f0453", + "sha256:2c2c5041608cb75c39cbd0ed05256f8a563e144234a524c59d091abbfa7a868f", + "sha256:2d31aff0c8184b05006ad756b9a4dc2a0805e94d28f3abc3187e881b6673b302", + "sha256:3a4c3e9be63adf8e9b305aa58fb3ec40ecc61fd0f8fd3328ce55bc30e7a2aeb0", + "sha256:5111d6d47a0f5b8f3e10af7a79d5e7eb7e73a22825391834734274c4f312a8a0", + "sha256:5ed3d3342698c2b1f3651f8ea6c099b0f196d16ee00e33dc3a6fee8cb01d530a", + "sha256:6ffd2d80d76df2e5f9f0c0140b5af97e3b87dd29852dcdb103ec177d853ec06b", + "sha256:746897fbd72bd462b888c74ed35d812ca76006b04f717cd44698cdfc99aca70d", + "sha256:756ee498b9ba35460e4cbbd73f09018e906daa8537fff61da5b5bf8d5e9de5c7", + "sha256:7ad44f2c74c50567c694ee91c6fa16d67e7c8af6f22c656b80469ad927688457", + "sha256:83e6c895d93fdf93eeff1a21ee96778ba65ef258e5d284160f7c628fee40c38f", + "sha256:9b03722c89a43a61d4d148acfc89ec5bb54cd0fd1539df25b10eb9c5fa6c393a", + "sha256:a4fe54eab2c7129add75154823e6543b10261f9b65b2abe692d68743a4999f8c", + "sha256:b1b60c6476c4cfe9e5cf8ab0d3127476fd3d5f05de0f343a452badaad0e4bdec", + "sha256:b26c472847911f5a7eb49e1c888c31c77c4ddf8023c1545e0e8e0367ba74fb15", + "sha256:b2a5e1f637a92bb6f3526cc54cc8af0401112e81ce5cba6368a1b7908f9e18bc", + "sha256:b7b09c61a91b742cb5460b72efd1fe26ef83c1c704f666e0af0df156b046aada", + "sha256:b8ba2a1dbb4660cb469fe8e1febb5119506059e675180c51396e1723ff9b79d9", + "sha256:c092fc4673260b1446b8578015321081d5db73b94533fe4bf9b69f44e948d174", + "sha256:c586ac1d64432f92857c3cf4478cfb0ece1ae18b740593f8a39f2f0b27c7fda5", + "sha256:d082f77b4ed876ae94a9373f0db96bf8768a7cca6c58fc3038f94e30ffde1880", + "sha256:e71cdd402047e657c1662073e9361106c6981e9621ab8c249388dfc3ec1de07b", + "sha256:eb6b6700ea454bb88333d98601e74928e06f9669c1ea231b4c4c666c1d7701b4" + ], + "index": "pypi", + "version": "==3.3.3" + }, "mistune": { "hashes": [ "sha256:59a3429db53c50b5c6bcc8a07f8848cb00d7dc8bdb431a4ab41920d201d4756e", @@ -428,6 +504,40 @@ ], "version": "==0.7.5" }, + "pillow": { + "hashes": [ + "sha256:006de60d7580d81f4a1a7e9f0173dc90a932e3905cc4d47ea909bc946302311a", + "sha256:0a2e8d03787ec7ad71dc18aec9367c946ef8ef50e1e78c71f743bc3a770f9fae", + "sha256:0eeeae397e5a79dc088d8297a4c2c6f901f8fb30db47795113a4a605d0f1e5ce", + "sha256:11c5c6e9b02c9dac08af04f093eb5a2f84857df70a7d4a6a6ad461aca803fb9e", + "sha256:2fb113757a369a6cdb189f8df3226e995acfed0a8919a72416626af1a0a71140", + "sha256:4b0ef2470c4979e345e4e0cc1bbac65fda11d0d7b789dbac035e4c6ce3f98adb", + "sha256:59e903ca800c8cfd1ebe482349ec7c35687b95e98cefae213e271c8c7fffa021", + "sha256:5abd653a23c35d980b332bc0431d39663b1709d64142e3652890df4c9b6970f6", + "sha256:5f9403af9c790cc18411ea398a6950ee2def2a830ad0cfe6dc9122e6d528b302", + "sha256:6b4a8fd632b4ebee28282a9fef4c341835a1aa8671e2770b6f89adc8e8c2703c", + "sha256:6c1aca8231625115104a06e4389fcd9ec88f0c9befbabd80dc206c35561be271", + "sha256:795e91a60f291e75de2e20e6bdd67770f793c8605b553cb6e4387ce0cb302e09", + "sha256:7ba0ba61252ab23052e642abdb17fd08fdcfdbbf3b74c969a30c58ac1ade7cd3", + "sha256:7c9401e68730d6c4245b8e361d3d13e1035cbc94db86b49dc7da8bec235d0015", + "sha256:81f812d8f5e8a09b246515fac141e9d10113229bc33ea073fec11403b016bcf3", + "sha256:895d54c0ddc78a478c80f9c438579ac15f3e27bf442c2a9aa74d41d0e4d12544", + "sha256:8de332053707c80963b589b22f8e0229f1be1f3ca862a932c1bcd48dafb18dd8", + "sha256:92c882b70a40c79de9f5294dc99390671e07fc0b0113d472cbea3fde15db1792", + "sha256:95edb1ed513e68bddc2aee3de66ceaf743590bf16c023fb9977adc4be15bd3f0", + "sha256:b63d4ff734263ae4ce6593798bcfee6dbfb00523c82753a3a03cbc05555a9cc3", + "sha256:bd7bf289e05470b1bc74889d1466d9ad4a56d201f24397557b6f65c24a6844b8", + "sha256:cc3ea6b23954da84dbee8025c616040d9aa5eaf34ea6895a0a762ee9d3e12e11", + "sha256:cc9ec588c6ef3a1325fa032ec14d97b7309db493782ea8c304666fb10c3bd9a7", + "sha256:d3d07c86d4efa1facdf32aa878bd508c0dc4f87c48125cc16b937baa4e5b5e11", + "sha256:d8a96747df78cda35980905bf26e72960cba6d355ace4780d4bdde3b217cdf1e", + "sha256:e38d58d9138ef972fceb7aeec4be02e3f01d383723965bfcef14d174c8ccd039", + "sha256:eb472586374dc66b31e36e14720747595c2b265ae962987261f044e5cce644b5", + "sha256:fbd922f702582cb0d71ef94442bfca57624352622d75e3be7a1e7e9360b07e72" + ], + "markers": "python_version >= '3.6'", + "version": "==8.0.1" + }, "prometheus-client": { "hashes": [ "sha256:9da7b32f02439d8c04f7777021c304ed51d9ec180604700c1ba72a4d44dceb03", diff --git a/product_extrusion.ipynb b/product_extrusion.ipynb index 20da75a..c484b33 100644 --- a/product_extrusion.ipynb +++ b/product_extrusion.ipynb @@ -32,7 +32,7 @@ }, { "cell_type": "code", - "execution_count": 225, + "execution_count": 1, "metadata": { "jupyter": { "source_hidden": true @@ -92,7 +92,7 @@ }, { "cell_type": "code", - "execution_count": 226, + "execution_count": 2, "metadata": { "jupyter": { "source_hidden": true @@ -207,7 +207,7 @@ }, { "cell_type": "code", - "execution_count": 227, + "execution_count": 3, "metadata": {}, "outputs": [ { @@ -245,7 +245,7 @@ }, { "cell_type": "code", - "execution_count": 228, + "execution_count": 4, "metadata": {}, "outputs": [], "source": [ @@ -268,7 +268,7 @@ }, { "cell_type": "code", - "execution_count": 229, + "execution_count": 5, "metadata": { "jupyter": { "source_hidden": true @@ -278,12 +278,12 @@ { "data": { "application/vnd.jupyter.widget-view+json": { - "model_id": "1bbfd79531d74a6aa21caffb2699a9f2", + "model_id": "010954f6d4244659824a6d4dd54924d0", "version_major": 2, "version_minor": 0 }, "text/plain": [ - "Renderer(camera=PerspectiveCamera(aspect=1.5, position=(0.28830735601264745, 6.947213875632741, 9.114960387348…" + "Renderer(camera=PerspectiveCamera(aspect=1.5, position=(-1.8735587450117768, 6.894156878209674, 10.57629092906…" ] }, "metadata": {}, @@ -305,7 +305,7 @@ }, { "cell_type": "code", - "execution_count": 230, + "execution_count": 6, "metadata": {}, "outputs": [], "source": [ @@ -326,7 +326,7 @@ }, { "cell_type": "code", - "execution_count": 231, + "execution_count": 7, "metadata": { "jupyter": { "source_hidden": true @@ -336,12 +336,12 @@ { "data": { "application/vnd.jupyter.widget-view+json": { - "model_id": "f7fce8d8be4a4387bcd6b5a65099adc2", + "model_id": "8fa978bd932a47a98ed03de27c8c807c", "version_major": 2, "version_minor": 0 }, "text/plain": [ - "Renderer(camera=PerspectiveCamera(aspect=1.5, position=(0.28830735601264745, 6.947213875632741, 9.114960387348…" + "Renderer(camera=PerspectiveCamera(aspect=1.5, position=(-1.8735587450117768, 6.894156878209674, 10.57629092906…" ] }, "metadata": {}, @@ -388,7 +388,7 @@ }, { "cell_type": "code", - "execution_count": 232, + "execution_count": 8, "metadata": {}, "outputs": [], "source": [ @@ -400,7 +400,7 @@ }, { "cell_type": "code", - "execution_count": 233, + "execution_count": 9, "metadata": { "jupyter": { "source_hidden": true @@ -410,12 +410,12 @@ { "data": { "application/vnd.jupyter.widget-view+json": { - "model_id": "ea3a6b17dede40f5ad0ff13bc85ec181", + "model_id": "834c213ef2874bfd8aeb7be80ef5b59b", "version_major": 2, "version_minor": 0 }, "text/plain": [ - "Renderer(camera=PerspectiveCamera(aspect=1.5, position=(0.28830735601264745, 6.947213875632741, 9.114960387348…" + "Renderer(camera=PerspectiveCamera(aspect=1.5, position=(-1.8735587450117768, 6.894156878209674, 10.57629092906…" ] }, "metadata": {}, @@ -443,7 +443,7 @@ }, { "cell_type": "code", - "execution_count": 234, + "execution_count": 10, "metadata": {}, "outputs": [], "source": [ @@ -460,7 +460,7 @@ }, { "cell_type": "code", - "execution_count": 235, + "execution_count": 11, "metadata": { "jupyter": { "source_hidden": true @@ -473,7 +473,7 @@ "(-0.29999999701976776, 1.0)" ] }, - "execution_count": 235, + "execution_count": 11, "metadata": {}, "output_type": "execute_result" }, @@ -509,7 +509,7 @@ }, { "cell_type": "code", - "execution_count": 236, + "execution_count": 12, "metadata": {}, "outputs": [], "source": [ @@ -538,7 +538,7 @@ }, { "cell_type": "code", - "execution_count": 237, + "execution_count": 13, "metadata": { "jupyter": { "source_hidden": true @@ -548,7 +548,7 @@ { "data": { "application/vnd.jupyter.widget-view+json": { - "model_id": "d853bc291ada4785ab9d61e7bff07504", + "model_id": "983e9d761f2e4ccc935227422c9980ae", "version_major": 2, "version_minor": 0 }, @@ -562,12 +562,12 @@ { "data": { "application/vnd.jupyter.widget-view+json": { - "model_id": "48edac81872d404d89279aa8caf6d186", + "model_id": "cc2a16da0ffe46729f76035e46b44e05", "version_major": 2, "version_minor": 0 }, "text/plain": [ - "Renderer(camera=PerspectiveCamera(aspect=1.5, position=(0.28830735601264745, 6.947213875632741, 9.114960387348…" + "Renderer(camera=PerspectiveCamera(aspect=1.5, position=(-1.8735587450117768, 6.894156878209674, 10.57629092906…" ] }, "metadata": {}, @@ -627,7 +627,7 @@ }, { "cell_type": "code", - "execution_count": 238, + "execution_count": 14, "metadata": {}, "outputs": [], "source": [ @@ -637,7 +637,7 @@ }, { "cell_type": "code", - "execution_count": 239, + "execution_count": 15, "metadata": { "jupyter": { "source_hidden": true @@ -647,7 +647,7 @@ { "data": { "application/vnd.jupyter.widget-view+json": { - "model_id": "239e8f7593324210860fb5c51704bbf6", + "model_id": "4cd4d2f871a84c82b11437f74736390a", "version_major": 2, "version_minor": 0 }, @@ -661,12 +661,12 @@ { "data": { "application/vnd.jupyter.widget-view+json": { - "model_id": "f76f4a2c5c7e4a668be19be4aca8ee5b", + "model_id": "dc4ead2cd09e488994bced1715a71e62", "version_major": 2, "version_minor": 0 }, "text/plain": [ - "Renderer(camera=PerspectiveCamera(aspect=1.5, position=(0.28830735601264745, 6.947213875632741, 9.114960387348…" + "Renderer(camera=PerspectiveCamera(aspect=1.5, position=(-1.8735587450117768, 6.894156878209674, 10.57629092906…" ] }, "metadata": {}, @@ -706,7 +706,7 @@ }, { "cell_type": "code", - "execution_count": 240, + "execution_count": 16, "metadata": {}, "outputs": [], "source": [ @@ -732,7 +732,7 @@ }, { "cell_type": "code", - "execution_count": 241, + "execution_count": 17, "metadata": { "jupyter": { "source_hidden": true @@ -742,12 +742,12 @@ { "data": { "application/vnd.jupyter.widget-view+json": { - "model_id": "62d6f81da24649c0aa1a73421144a1fb", + "model_id": "8137f09aae734f8ea2e740cd866f4e2a", "version_major": 2, "version_minor": 0 }, "text/plain": [ - "Renderer(camera=PerspectiveCamera(aspect=1.5, position=(0.28830735601264745, 6.947213875632741, 9.114960387348…" + "Renderer(camera=PerspectiveCamera(aspect=1.5, position=(-1.8735587450117768, 6.894156878209674, 10.57629092906…" ] }, "metadata": {}, @@ -807,7 +807,7 @@ }, { "cell_type": "code", - "execution_count": 242, + "execution_count": 18, "metadata": {}, "outputs": [], "source": [ @@ -816,7 +816,7 @@ }, { "cell_type": "code", - "execution_count": 269, + "execution_count": 19, "metadata": {}, "outputs": [], "source": [ @@ -875,7 +875,7 @@ }, { "cell_type": "code", - "execution_count": 271, + "execution_count": 20, "metadata": { "jupyter": { "source_hidden": true @@ -885,12 +885,12 @@ { "data": { "application/vnd.jupyter.widget-view+json": { - "model_id": "35a716badd8749edac90db265d88f4ff", + "model_id": "e52242b1a57d4657bb2d07f2c5a2bc82", "version_major": 2, "version_minor": 0 }, "text/plain": [ - "Renderer(camera=PerspectiveCamera(aspect=1.5, position=(0.28830735601264745, 6.947213875632741, 9.114960387348…" + "Renderer(camera=PerspectiveCamera(aspect=1.5, position=(-1.8735587450117768, 6.894156878209674, 10.57629092906…" ] }, "metadata": {},