1
0
Files
rw-product-mesh/product_extrusion.ipynb
2020-12-22 14:04:09 -07:00

951 lines
44 KiB
Plaintext

{
"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": 1,
"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": 2,
"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": 3,
"metadata": {},
"outputs": [
{
"data": {
"image/png": "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\n",
"text/plain": [
"<Figure size 432x288 with 1 Axes>"
]
},
"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": 4,
"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": 5,
"metadata": {
"jupyter": {
"source_hidden": true
}
},
"outputs": [
{
"data": {
"application/vnd.jupyter.widget-view+json": {
"model_id": "010954f6d4244659824a6d4dd54924d0",
"version_major": 2,
"version_minor": 0
},
"text/plain": [
"Renderer(camera=PerspectiveCamera(aspect=1.5, position=(-1.8735587450117768, 6.894156878209674, 10.57629092906…"
]
},
"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": 6,
"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": 7,
"metadata": {
"jupyter": {
"source_hidden": true
}
},
"outputs": [
{
"data": {
"application/vnd.jupyter.widget-view+json": {
"model_id": "8fa978bd932a47a98ed03de27c8c807c",
"version_major": 2,
"version_minor": 0
},
"text/plain": [
"Renderer(camera=PerspectiveCamera(aspect=1.5, position=(-1.8735587450117768, 6.894156878209674, 10.57629092906…"
]
},
"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": 8,
"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": 9,
"metadata": {
"jupyter": {
"source_hidden": true
}
},
"outputs": [
{
"data": {
"application/vnd.jupyter.widget-view+json": {
"model_id": "834c213ef2874bfd8aeb7be80ef5b59b",
"version_major": 2,
"version_minor": 0
},
"text/plain": [
"Renderer(camera=PerspectiveCamera(aspect=1.5, position=(-1.8735587450117768, 6.894156878209674, 10.57629092906…"
]
},
"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": 10,
"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": 11,
"metadata": {
"jupyter": {
"source_hidden": true
}
},
"outputs": [
{
"data": {
"text/plain": [
"(-0.29999999701976776, 1.0)"
]
},
"execution_count": 11,
"metadata": {},
"output_type": "execute_result"
},
{
"data": {
"image/png": "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\n",
"text/plain": [
"<Figure size 432x288 with 1 Axes>"
]
},
"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": 12,
"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": 13,
"metadata": {
"jupyter": {
"source_hidden": true
}
},
"outputs": [
{
"data": {
"application/vnd.jupyter.widget-view+json": {
"model_id": "983e9d761f2e4ccc935227422c9980ae",
"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": "cc2a16da0ffe46729f76035e46b44e05",
"version_major": 2,
"version_minor": 0
},
"text/plain": [
"Renderer(camera=PerspectiveCamera(aspect=1.5, position=(-1.8735587450117768, 6.894156878209674, 10.57629092906…"
]
},
"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": 14,
"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": 15,
"metadata": {
"jupyter": {
"source_hidden": true
}
},
"outputs": [
{
"data": {
"application/vnd.jupyter.widget-view+json": {
"model_id": "4cd4d2f871a84c82b11437f74736390a",
"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": "dc4ead2cd09e488994bced1715a71e62",
"version_major": 2,
"version_minor": 0
},
"text/plain": [
"Renderer(camera=PerspectiveCamera(aspect=1.5, position=(-1.8735587450117768, 6.894156878209674, 10.57629092906…"
]
},
"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": 16,
"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": 17,
"metadata": {
"jupyter": {
"source_hidden": true
}
},
"outputs": [
{
"data": {
"application/vnd.jupyter.widget-view+json": {
"model_id": "8137f09aae734f8ea2e740cd866f4e2a",
"version_major": 2,
"version_minor": 0
},
"text/plain": [
"Renderer(camera=PerspectiveCamera(aspect=1.5, position=(-1.8735587450117768, 6.894156878209674, 10.57629092906…"
]
},
"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": 18,
"metadata": {},
"outputs": [],
"source": [
"from _cython_csg import CSG, BSPNode, Polygon, Vertex"
]
},
{
"cell_type": "code",
"execution_count": 19,
"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": 20,
"metadata": {
"jupyter": {
"source_hidden": true
}
},
"outputs": [
{
"data": {
"application/vnd.jupyter.widget-view+json": {
"model_id": "e52242b1a57d4657bb2d07f2c5a2bc82",
"version_major": 2,
"version_minor": 0
},
"text/plain": [
"Renderer(camera=PerspectiveCamera(aspect=1.5, position=(-1.8735587450117768, 6.894156878209674, 10.57629092906…"
]
},
"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
}