[{"data":1,"prerenderedAt":1085},["ShallowReactive",2],{"tutorial-isocontours-on-a-triangle-mesh-in-python":3},{"id":4,"title":5,"author":6,"body":7,"date":1062,"description":1063,"extension":1064,"keywords":1065,"meta":1076,"navigation":136,"ogImage":1077,"path":1078,"published":136,"seo":1079,"stem":1080,"tags":1081,"__hash__":1084},"tutorials\u002Ftutorials\u002Fisocontours-on-a-triangle-mesh-in-python.md","Fast and Robust Isocontours on a Triangle Mesh in Python","Žiga Sajovic",{"type":8,"value":9,"toc":1051},"minimark",[10,28,40,44,51,56,81,91,95,162,172,281,285,292,295,315,321,327,442,456,460,480,486,500,562,565,587,591,594,641,647,650,694,697,701,708,759,765,776,779,813,816,844,848,851,881,887,927,930,945,967,971,1000,1004,1008,1020,1025,1031,1043,1047],[11,12,13],"note",{},[14,15,16,17,22,23,27],"p",{},"Also available in ",[18,19,21],"a",{"href":20},"\u002Ftutorials\u002Fisocontours-on-a-triangle-mesh-in-cpp","C++"," and ",[18,24,26],{"href":25},"\u002Ftutorials\u002Fisocontours-on-a-triangle-mesh-in-javascript","JavaScript",".",[14,29,30,31,35,36,39],{},"An isocontour is the curve where a scalar field on a mesh takes a given value. Height gives contour lines. Distance from a point gives rings. Curvature gives the borders between shapes. ",[32,33,34],"code",{},"trueform"," extracts isocontours of any per-vertex field, at one level or many, and cuts the mesh into isobands along them. One ",[32,37,38],{},"pip install",". NumPy arrays in, NumPy arrays out. This tutorial covers single and multiple levels, the curve format, and isobands.",[41,42],"article-cta",{":buttons":43},"[{\"label\":\"trueform\",\"to\":\"\u002Ftrueform\",\"icon\":\"i-lucide-cpu\",\"variant\":\"soft\",\"color\":\"neutral\"},{\"label\":\"GitHub\",\"to\":\"https:\u002F\u002Fgithub.com\u002Fpolydera\u002Ftrueform\",\"icon\":\"i-simple-icons-github\",\"variant\":\"soft\",\"color\":\"neutral\"},{\"label\":\"Documentation\",\"to\":\"https:\u002F\u002Ftrueform.polydera.com\u002Fpy\u002Fmodules\u002Fiso\",\"variant\":\"soft\",\"color\":\"neutral\"},{\"label\":\"Try it live\",\"to\":\"https:\u002F\u002Ftrueform.polydera.com\u002Flive-examples\u002Fslicing\",\"icon\":\"i-lucide-play\"}]",[14,45,46],{},[47,48],"img",{"alt":49,"src":50},"Height isobands on the Stanford dragon, alternating teal and orange, with the band boundaries as curves","\u002Fimg\u002Ftutorials\u002Fisocontours.png",[52,53,55],"h2",{"id":54},"install","Install",[57,58,63],"pre",{"className":59,"code":60,"language":61,"meta":62,"style":62},"language-bash shiki shiki-themes github-dark","pip install trueform\n","bash","",[32,64,65],{"__ignoreMap":62},[66,67,70,74,78],"span",{"class":68,"line":69},"line",1,[66,71,73],{"class":72},"svObZ","pip",[66,75,77],{"class":76},"sU2Wk"," install",[66,79,80],{"class":76}," trueform\n",[14,82,83,84,90],{},"See the ",[18,85,89],{"href":86,"rel":87},"https:\u002F\u002Ftrueform.polydera.com\u002Fpy\u002Fgetting-started\u002Finstallation",[88],"nofollow","installation guide"," for all options.",[52,92,94],{"id":93},"loading-the-mesh","Loading the mesh",[57,96,100],{"className":97,"code":98,"language":99,"meta":62,"style":62},"language-python shiki shiki-themes github-dark","import trueform as tf\nimport numpy as np\n\nmesh = tf.Mesh(*tf.read_stl(\"terrain.stl\"))\n","python",[32,101,102,118,131,138],{"__ignoreMap":62},[66,103,104,108,112,115],{"class":68,"line":69},[66,105,107],{"class":106},"snl16","import",[66,109,111],{"class":110},"s95oV"," trueform ",[66,113,114],{"class":106},"as",[66,116,117],{"class":110}," tf\n",[66,119,121,123,126,128],{"class":68,"line":120},2,[66,122,107],{"class":106},[66,124,125],{"class":110}," numpy ",[66,127,114],{"class":106},[66,129,130],{"class":110}," np\n",[66,132,134],{"class":68,"line":133},3,[66,135,137],{"emptyLinePlaceholder":136},true,"\n",[66,139,141,144,147,150,153,156,159],{"class":68,"line":140},4,[66,142,143],{"class":110},"mesh ",[66,145,146],{"class":106},"=",[66,148,149],{"class":110}," tf.Mesh(",[66,151,152],{"class":106},"*",[66,154,155],{"class":110},"tf.read_stl(",[66,157,158],{"class":76},"\"terrain.stl\"",[66,160,161],{"class":110},"))\n",[14,163,164,167,168,171],{},[32,165,166],{},"tf.read_obj"," works the same way. If you already have NumPy arrays, pass them to ",[32,169,170],{},"tf.Mesh(faces, points)",". To follow along without a file, make a small wavy terrain:",[57,173,175],{"className":97,"code":174,"language":99,"meta":62,"style":62},"faces, points = tf.make_plane_mesh(2.0, 2.0, 200, 200)\nx, y = points[:, 0], points[:, 1]\npoints[:, 2] = 0.2 * np.sin(3 * x) * np.cos(3 * y)\nmesh = tf.Mesh(faces, points)\n",[32,176,177,208,230,272],{"__ignoreMap":62},[66,178,179,182,184,187,191,194,196,198,201,203,205],{"class":68,"line":69},[66,180,181],{"class":110},"faces, points ",[66,183,146],{"class":106},[66,185,186],{"class":110}," tf.make_plane_mesh(",[66,188,190],{"class":189},"sDLfK","2.0",[66,192,193],{"class":110},", ",[66,195,190],{"class":189},[66,197,193],{"class":110},[66,199,200],{"class":189},"200",[66,202,193],{"class":110},[66,204,200],{"class":189},[66,206,207],{"class":110},")\n",[66,209,210,213,215,218,221,224,227],{"class":68,"line":120},[66,211,212],{"class":110},"x, y ",[66,214,146],{"class":106},[66,216,217],{"class":110}," points[:, ",[66,219,220],{"class":189},"0",[66,222,223],{"class":110},"], points[:, ",[66,225,226],{"class":189},"1",[66,228,229],{"class":110},"]\n",[66,231,232,235,238,241,243,246,249,252,255,257,260,262,265,267,269],{"class":68,"line":133},[66,233,234],{"class":110},"points[:, ",[66,236,237],{"class":189},"2",[66,239,240],{"class":110},"] ",[66,242,146],{"class":106},[66,244,245],{"class":189}," 0.2",[66,247,248],{"class":106}," *",[66,250,251],{"class":110}," np.sin(",[66,253,254],{"class":189},"3",[66,256,248],{"class":106},[66,258,259],{"class":110}," x) ",[66,261,152],{"class":106},[66,263,264],{"class":110}," np.cos(",[66,266,254],{"class":189},[66,268,248],{"class":106},[66,270,271],{"class":110}," y)\n",[66,273,274,276,278],{"class":68,"line":140},[66,275,143],{"class":110},[66,277,146],{"class":106},[66,279,280],{"class":110}," tf.Mesh(faces, points)\n",[52,282,284],{"id":283},"the-scalar-field","The scalar field",[14,286,287,288,291],{},"A scalar field is one value per point, as a NumPy array of shape ",[32,289,290],{},"(num_points,)",". It must have the mesh's dtype. Most meshes are float32, so most fields are float32.",[14,293,294],{},"Height is the simplest field:",[57,296,298],{"className":97,"code":297,"language":99,"meta":62,"style":62},"field = mesh.points[:, 2].copy()\n",[32,299,300],{"__ignoreMap":62},[66,301,302,305,307,310,312],{"class":68,"line":69},[66,303,304],{"class":110},"field ",[66,306,146],{"class":106},[66,308,309],{"class":110}," mesh.points[:, ",[66,311,237],{"class":189},[66,313,314],{"class":110},"].copy()\n",[14,316,317],{},[47,318],{"alt":319,"src":320},"The scalar field: height on the Nefertiti bust, scaled to the terrain's range, teal low to orange high","\u002Fimg\u002Ftutorials\u002Fsteps\u002Fisocontours-on-a-triangle-mesh-1-field.png",[14,322,323,324,326],{},"Any field works. A few that come from ",[32,325,34],{}," itself:",[57,328,330],{"className":97,"code":329,"language":99,"meta":62,"style":62},"# signed distance to a plane: x = 0\nplane = tf.Plane(np.array([1, 0, 0, 0], dtype=np.float32))\nacross = tf.distance(tf.Point(mesh.points), plane)\n\n# geodesic distance from a point\nalong = tf.geodesic_distances(mesh, [len(mesh.points) \u002F\u002F 2])\n\n# shape index, from curvature\nshape = tf.shape_index(mesh)\n",[32,331,332,338,374,384,388,394,420,425,431],{"__ignoreMap":62},[66,333,334],{"class":68,"line":69},[66,335,337],{"class":336},"sAwPA","# signed distance to a plane: x = 0\n",[66,339,340,343,345,348,350,352,354,356,358,360,362,365,369,371],{"class":68,"line":120},[66,341,342],{"class":110},"plane ",[66,344,146],{"class":106},[66,346,347],{"class":110}," tf.Plane(np.array([",[66,349,226],{"class":189},[66,351,193],{"class":110},[66,353,220],{"class":189},[66,355,193],{"class":110},[66,357,220],{"class":189},[66,359,193],{"class":110},[66,361,220],{"class":189},[66,363,364],{"class":110},"], ",[66,366,368],{"class":367},"s9osk","dtype",[66,370,146],{"class":106},[66,372,373],{"class":110},"np.float32))\n",[66,375,376,379,381],{"class":68,"line":133},[66,377,378],{"class":110},"across ",[66,380,146],{"class":106},[66,382,383],{"class":110}," tf.distance(tf.Point(mesh.points), plane)\n",[66,385,386],{"class":68,"line":140},[66,387,137],{"emptyLinePlaceholder":136},[66,389,391],{"class":68,"line":390},5,[66,392,393],{"class":336},"# geodesic distance from a point\n",[66,395,397,400,402,405,408,411,414,417],{"class":68,"line":396},6,[66,398,399],{"class":110},"along ",[66,401,146],{"class":106},[66,403,404],{"class":110}," tf.geodesic_distances(mesh, [",[66,406,407],{"class":189},"len",[66,409,410],{"class":110},"(mesh.points) ",[66,412,413],{"class":106},"\u002F\u002F",[66,415,416],{"class":189}," 2",[66,418,419],{"class":110},"])\n",[66,421,423],{"class":68,"line":422},7,[66,424,137],{"emptyLinePlaceholder":136},[66,426,428],{"class":68,"line":427},8,[66,429,430],{"class":336},"# shape index, from curvature\n",[66,432,434,437,439],{"class":68,"line":433},9,[66,435,436],{"class":110},"shape ",[66,438,146],{"class":106},[66,440,441],{"class":110}," tf.shape_index(mesh)\n",[14,443,444,445,448,449,452,453,27],{},"If your field is float64 and the mesh is float32, ",[32,446,447],{},"tf.isocontours"," raises a ",[32,450,451],{},"TypeError",". Convert with ",[32,454,455],{},"field.astype(mesh.points.dtype)",[52,457,459],{"id":458},"one-level","One level",[57,461,463],{"className":97,"code":462,"language":99,"meta":62,"style":62},"paths, curve_points = tf.isocontours(mesh, field, 0.0)\n",[32,464,465],{"__ignoreMap":62},[66,466,467,470,472,475,478],{"class":68,"line":69},[66,468,469],{"class":110},"paths, curve_points ",[66,471,146],{"class":106},[66,473,474],{"class":110}," tf.isocontours(mesh, field, ",[66,476,477],{"class":189},"0.0",[66,479,207],{"class":110},[14,481,482],{},[47,483],{"alt":484,"src":485},"One level: the height isocontour at 0.0 on the Nefertiti bust, a pink curve across the face","\u002Fimg\u002Ftutorials\u002Fsteps\u002Fisocontours-on-a-triangle-mesh-2-one-level.png",[14,487,488,491,492,495,496,499],{},[32,489,490],{},"curve_points"," is an ",[32,493,494],{},"(n, 3)"," array of points on the surface. ",[32,497,498],{},"paths"," holds one index list per curve:",[57,501,503],{"className":97,"code":502,"language":99,"meta":62,"style":62},"for path_ids in paths:\n    curve = curve_points[path_ids]        # (m, 3) points along one curve\n    closed = path_ids[0] == path_ids[-1]  # a closed loop repeats its first id\n",[32,504,505,519,532],{"__ignoreMap":62},[66,506,507,510,513,516],{"class":68,"line":69},[66,508,509],{"class":106},"for",[66,511,512],{"class":110}," path_ids ",[66,514,515],{"class":106},"in",[66,517,518],{"class":110}," paths:\n",[66,520,521,524,526,529],{"class":68,"line":120},[66,522,523],{"class":110},"    curve ",[66,525,146],{"class":106},[66,527,528],{"class":110}," curve_points[path_ids]        ",[66,530,531],{"class":336},"# (m, 3) points along one curve\n",[66,533,534,537,539,542,544,546,549,551,554,556,559],{"class":68,"line":133},[66,535,536],{"class":110},"    closed ",[66,538,146],{"class":106},[66,540,541],{"class":110}," path_ids[",[66,543,220],{"class":189},[66,545,240],{"class":110},[66,547,548],{"class":106},"==",[66,550,541],{"class":110},[66,552,553],{"class":106},"-",[66,555,226],{"class":189},[66,557,558],{"class":110},"]  ",[66,560,561],{"class":336},"# a closed loop repeats its first id\n",[14,563,564],{},"A curve that runs off the edge of an open mesh ends at the boundary. A curve on a closed region closes on itself.",[14,566,567,491,569,572,573,22,576,579,580,583,584,27],{},[32,568,498],{},[32,570,571],{},"OffsetBlockedArray",". If you want flat arrays, ",[32,574,575],{},"paths.offsets",[32,577,578],{},"paths.data"," give them directly. Curve ",[32,581,582],{},"i"," is ",[32,585,586],{},"paths.data[paths.offsets[i]:paths.offsets[i + 1]]",[52,588,590],{"id":589},"many-levels","Many levels",[14,592,593],{},"Pass an array of levels. One call extracts them all.",[57,595,597],{"className":97,"code":596,"language":99,"meta":62,"style":62},"levels = np.linspace(-0.15, 0.15, 7, dtype=np.float32)\npaths, curve_points = tf.isocontours(mesh, field, levels)\n",[32,598,599,632],{"__ignoreMap":62},[66,600,601,604,606,609,611,614,616,618,620,623,625,627,629],{"class":68,"line":69},[66,602,603],{"class":110},"levels ",[66,605,146],{"class":106},[66,607,608],{"class":110}," np.linspace(",[66,610,553],{"class":106},[66,612,613],{"class":189},"0.15",[66,615,193],{"class":110},[66,617,613],{"class":189},[66,619,193],{"class":110},[66,621,622],{"class":189},"7",[66,624,193],{"class":110},[66,626,368],{"class":367},[66,628,146],{"class":106},[66,630,631],{"class":110},"np.float32)\n",[66,633,634,636,638],{"class":68,"line":120},[66,635,469],{"class":110},[66,637,146],{"class":106},[66,639,640],{"class":110}," tf.isocontours(mesh, field, levels)\n",[14,642,643],{},[47,644],{"alt":645,"src":646},"Many levels: seven height isocontours from -0.15 to 0.15 on the Nefertiti bust, extracted in one call","\u002Fimg\u002Ftutorials\u002Fsteps\u002Fisocontours-on-a-triangle-mesh-3-many-levels.png",[14,648,649],{},"The result does not say which level each curve belongs to, and the curves are not in level order. When you need that, call once per level:",[57,651,653],{"className":97,"code":652,"language":99,"meta":62,"style":62},"by_level = {\n    float(t): tf.isocontours(mesh, field, float(t)) for t in levels\n}\n",[32,654,655,665,689],{"__ignoreMap":62},[66,656,657,660,662],{"class":68,"line":69},[66,658,659],{"class":110},"by_level ",[66,661,146],{"class":106},[66,663,664],{"class":110}," {\n",[66,666,667,670,673,676,679,681,684,686],{"class":68,"line":120},[66,668,669],{"class":189},"    float",[66,671,672],{"class":110},"(t): tf.isocontours(mesh, field, ",[66,674,675],{"class":189},"float",[66,677,678],{"class":110},"(t)) ",[66,680,509],{"class":106},[66,682,683],{"class":110}," t ",[66,685,515],{"class":106},[66,687,688],{"class":110}," levels\n",[66,690,691],{"class":68,"line":133},[66,692,693],{"class":110},"}\n",[14,695,696],{},"For height, you can also read the level from the curve itself: every point on a height contour has the level as its z.",[52,698,700],{"id":699},"isobands","Isobands",[14,702,703,704,707],{},"Isobands are the regions between levels. ",[32,705,706],{},"tf.isobands"," cuts the mesh along the isocontours, so the curves become edges, and labels each face by band.",[57,709,711],{"className":97,"code":710,"language":99,"meta":62,"style":62},"cuts = np.array([-0.1, 0.0, 0.1], dtype=np.float32)\n(band_faces, band_points), labels, face_labels = \\\n    tf.isobands(mesh, field, cuts)\n",[32,712,713,744,754],{"__ignoreMap":62},[66,714,715,718,720,723,725,728,730,732,734,736,738,740,742],{"class":68,"line":69},[66,716,717],{"class":110},"cuts ",[66,719,146],{"class":106},[66,721,722],{"class":110}," np.array([",[66,724,553],{"class":106},[66,726,727],{"class":189},"0.1",[66,729,193],{"class":110},[66,731,477],{"class":189},[66,733,193],{"class":110},[66,735,727],{"class":189},[66,737,364],{"class":110},[66,739,368],{"class":367},[66,741,146],{"class":106},[66,743,631],{"class":110},[66,745,746,749,751],{"class":68,"line":120},[66,747,748],{"class":110},"(band_faces, band_points), labels, face_labels ",[66,750,146],{"class":106},[66,752,753],{"class":110}," \\\n",[66,755,756],{"class":68,"line":133},[66,757,758],{"class":110},"    tf.isobands(mesh, field, cuts)\n",[14,760,761],{},[47,762],{"alt":763,"src":764},"Isobands: the Nefertiti bust cut at heights -0.1, 0.0 and 0.1 into four bands, each in its own color","\u002Fimg\u002Ftutorials\u002Fsteps\u002Fisocontours-on-a-triangle-mesh-4-isobands.png",[14,766,767,768,771,772,775],{},"Three cuts give four bands. Band 0 is below -0.1, band 3 is above 0.1. ",[32,769,770],{},"labels"," gives the band of each output face. ",[32,773,774],{},"face_labels"," gives the original face it came from.",[14,777,778],{},"Keep only some bands:",[57,780,782],{"className":97,"code":781,"language":99,"meta":62,"style":62},"(band_faces, band_points), labels, face_labels = \\\n    tf.isobands(mesh, field, cuts, selected_bands=[0, 3])\n",[32,783,784,792],{"__ignoreMap":62},[66,785,786,788,790],{"class":68,"line":69},[66,787,748],{"class":110},[66,789,146],{"class":106},[66,791,753],{"class":110},[66,793,794,797,800,802,805,807,809,811],{"class":68,"line":120},[66,795,796],{"class":110},"    tf.isobands(mesh, field, cuts, ",[66,798,799],{"class":367},"selected_bands",[66,801,146],{"class":106},[66,803,804],{"class":110},"[",[66,806,220],{"class":189},[66,808,193],{"class":110},[66,810,254],{"class":189},[66,812,419],{"class":110},[14,814,815],{},"Get the boundary curves in the same call:",[57,817,819],{"className":97,"code":818,"language":99,"meta":62,"style":62},"(band_faces, band_points), labels, face_labels, (paths, curve_points) = \\\n    tf.isobands(mesh, field, cuts, return_curves=True)\n",[32,820,821,830],{"__ignoreMap":62},[66,822,823,826,828],{"class":68,"line":69},[66,824,825],{"class":110},"(band_faces, band_points), labels, face_labels, (paths, curve_points) ",[66,827,146],{"class":106},[66,829,753],{"class":110},[66,831,832,834,837,839,842],{"class":68,"line":120},[66,833,796],{"class":110},[66,835,836],{"class":367},"return_curves",[66,838,146],{"class":106},[66,840,841],{"class":189},"True",[66,843,207],{"class":110},[52,845,847],{"id":846},"working-with-results","Working with results",[14,849,850],{},"Split the bands into separate meshes:",[57,852,854],{"className":97,"code":853,"language":99,"meta":62,"style":62},"band_mesh = tf.Mesh(band_faces, band_points)\nparts, part_labels = tf.split_into_components(band_mesh, labels)\n# parts[i] is a (faces, points) tuple for band part_labels[i]\n",[32,855,856,866,876],{"__ignoreMap":62},[66,857,858,861,863],{"class":68,"line":69},[66,859,860],{"class":110},"band_mesh ",[66,862,146],{"class":106},[66,864,865],{"class":110}," tf.Mesh(band_faces, band_points)\n",[66,867,868,871,873],{"class":68,"line":120},[66,869,870],{"class":110},"parts, part_labels ",[66,872,146],{"class":106},[66,874,875],{"class":110}," tf.split_into_components(band_mesh, labels)\n",[66,877,878],{"class":68,"line":133},[66,879,880],{"class":336},"# parts[i] is a (faces, points) tuple for band part_labels[i]\n",[14,882,883,884,886],{},"Carry per-face data from the input to the output with ",[32,885,774],{},":",[57,888,890],{"className":97,"code":889,"language":99,"meta":62,"style":62},"material = np.zeros(len(faces), dtype=np.int32)   # your per-face data\nband_material = material[face_labels]\n",[32,891,892,917],{"__ignoreMap":62},[66,893,894,897,899,902,904,907,909,911,914],{"class":68,"line":69},[66,895,896],{"class":110},"material ",[66,898,146],{"class":106},[66,900,901],{"class":110}," np.zeros(",[66,903,407],{"class":189},[66,905,906],{"class":110},"(faces), ",[66,908,368],{"class":367},[66,910,146],{"class":106},[66,912,913],{"class":110},"np.int32)   ",[66,915,916],{"class":336},"# your per-face data\n",[66,918,919,922,924],{"class":68,"line":120},[66,920,921],{"class":110},"band_material ",[66,923,146],{"class":106},[66,925,926],{"class":110}," material[face_labels]\n",[14,928,929],{},"Write to file:",[57,931,933],{"className":97,"code":932,"language":99,"meta":62,"style":62},"tf.write_obj((band_faces, band_points), \"bands.obj\")\n",[32,934,935],{"__ignoreMap":62},[66,936,937,940,943],{"class":68,"line":69},[66,938,939],{"class":110},"tf.write_obj((band_faces, band_points), ",[66,941,942],{"class":76},"\"bands.obj\"",[66,944,207],{"class":110},[14,946,947,948,952,953,957,958,962,963,27],{},"Contours of height are slices. See ",[18,949,951],{"href":950},"\u002Ftutorials\u002Fmesh-slicing-and-cross-sections-in-python","mesh slicing and cross-sections"," for layers and sections. Contours of ",[18,954,956],{"href":955},"\u002Ftutorials\u002Ffast-geodesic-distances-in-python","geodesic distance"," are rings. Contours of the ",[18,959,961],{"href":960},"\u002Ftutorials\u002Fprincipal-curvatures-of-a-mesh-in-python","shape index"," separate cups, saddles, and caps. To cut one mesh by another mesh instead of by a field, use ",[18,964,966],{"href":965},"\u002Ftutorials\u002Ffast-mesh-booleans-in-python","booleans",[52,968,970],{"id":969},"related-tutorials","Related tutorials",[972,973,974,981,987,993],"ul",{},[975,976,977,980],"li",{},[18,978,979],{"href":955},"Fast and Accurate Geodesic Distances in Python"," — distance along the surface from one seed or many.",[975,982,983,986],{},[18,984,985],{"href":950},"Fast and Robust Mesh Slicing and Cross-Sections in Python"," — plane sections, layers and slabs of a mesh.",[975,988,989,992],{},[18,990,991],{"href":960},"Fast and Robust Principal Curvatures of a Mesh in Python"," — curvature values and directions at every vertex.",[975,994,995,999],{},[18,996,998],{"href":997},"\u002Ftutorials\u002Fmesh-intersection-curves-in-python","Fast and Exact Mesh Intersection Curves in Python"," — exact curves where two or more meshes cross.",[52,1001,1003],{"id":1002},"performance","Performance",[1005,1006],"headline-numbers",{":items":1007},"[{\"value\":\"4.3ms\",\"label\":\"Isocontours\",\"detail\":\"1,025,766 polygons (VTK: 155 ms)\"},{\"value\":\"8.7ms\",\"label\":\"Many levels\",\"detail\":\"480,868 polygons (VTK: 212.5 ms)\"},{\"value\":\"32.9ms\",\"label\":\"Isobands\",\"detail\":\"1,025,766 polygons (VTK: 527.7 ms)\"}]",[14,1009,1010,1015,1016],{},[66,1011,1014],{"className":1012},[1013],"tag-pill","Apple M4 Max"," ",[66,1017,1019],{"className":1018},[1013],"16 threads",[1021,1022],"data-table",{":headers":1023,":highlight":237,":rows":1024},"[\"Operation\",\"Mesh\",\"trueform\",\"VTK\",\"Speedup\"]","[[\"Isocontours\",\"1,025,766 polygons\",\"4.3 ms\",\"155 ms\",\"36×\"],[\"Isocontours, many levels\",\"480,868 polygons\",\"8.7 ms\",\"212.5 ms\",\"24×\"],[\"Embedded isobands\",\"1,025,766 polygons\",\"32.9 ms\",\"527.7 ms\",\"16×\"]]",[14,1026,1027],{},[47,1028],{"alt":1029,"src":1030},"Benchmark chart: trueform against other Python libraries, isocontours","\u002Fimg\u002Falgorithms\u002Fbenchmark-isocontours-python-2026.png",[14,1032,1033,1037,1038],{},[18,1034,1036],{"href":1035},"\u002Ftrueform\u002Fbenchmarks","Full benchmarks"," · ",[18,1039,1042],{"href":1040,"rel":1041},"https:\u002F\u002Ftrueform.polydera.com\u002Fpy\u002Fmodules\u002Fiso",[88],"Documentation",[1044,1045],"cite-as",{"author":1046,"title":5},"Sajovic, {\\v{Z}}iga",[1048,1049,1050],"style",{},"html pre.shiki code .svObZ, html code.shiki .svObZ{--shiki-default:#B392F0}html pre.shiki code .sU2Wk, html code.shiki .sU2Wk{--shiki-default:#9ECBFF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html pre.shiki code .snl16, html code.shiki .snl16{--shiki-default:#F97583}html pre.shiki code .s95oV, html code.shiki .s95oV{--shiki-default:#E1E4E8}html pre.shiki code .sDLfK, html code.shiki .sDLfK{--shiki-default:#79B8FF}html pre.shiki code .sAwPA, html code.shiki .sAwPA{--shiki-default:#6A737D}html pre.shiki code .s9osk, html code.shiki .s9osk{--shiki-default:#FFAB70}",{"title":62,"searchDepth":120,"depth":120,"links":1052},[1053,1054,1055,1056,1057,1058,1059,1060,1061],{"id":54,"depth":120,"text":55},{"id":93,"depth":120,"text":94},{"id":283,"depth":120,"text":284},{"id":458,"depth":120,"text":459},{"id":589,"depth":120,"text":590},{"id":699,"depth":120,"text":700},{"id":846,"depth":120,"text":847},{"id":969,"depth":120,"text":970},{"id":1002,"depth":120,"text":1003},"2026-10-31","Extract isocontours (isolines) of any scalar field on a triangle mesh in Python. One level or many at once, plus isobands that cut the mesh along the curves. One pip install, NumPy arrays in and out.","md",[1066,1067,1068,1069,1070,1071,1072,1073,1074,1075],"isocontours mesh Python","isolines triangle mesh","contour lines on mesh Python","scalar field contours mesh","isobands Python","VTK contour filter alternative","PyVista contour alternative","trimesh contour alternative","level set curves mesh","numpy isocontours",{},"\u002Fimg\u002Fog\u002Ftutorials\u002Fisocontours-on-a-triangle-mesh-in-python.png","\u002Ftutorials\u002Fisocontours-on-a-triangle-mesh-in-python",{"title":5,"description":1063},"tutorials\u002Fisocontours-on-a-triangle-mesh-in-python",[99,1082,1083],"isocontours","tutorial","gyNBiP0wGLL-1jo-JVdPuUaUKrcz_gNwno5XofHW8C4",1793517812008]