[{"data":1,"prerenderedAt":1210},["ShallowReactive",2],{"algo-isocontours":3},{"id":4,"title":5,"author":6,"body":7,"date":1184,"description":1185,"extension":1186,"hub":300,"keywords":1187,"meta":1197,"navigation":300,"ogImage":1198,"path":1199,"published":300,"seo":1200,"stem":1203,"tags":1204,"updated":1208,"__hash__":1209},"algorithms\u002Falgorithms\u002Fisocontours.md","Isocontours on a mesh","Žiga Sajovic",{"type":8,"value":9,"toc":1171},"minimark",[10,26,35,38,42,47,50,54,57,60,65,68,71,75,78,90,93,120,123,127,130,136,139,153,161,164,204,207,211,214,261,268,272,275,1008,1022,1026,1046,1050,1053,1059,1063,1066,1070,1074,1082,1086,1103,1123,1136,1153,1159,1163,1167],[11,12,13,14,19,20,25],"p",{},"A contour map starts with one number per point: height on a terrain, stress on a part, temperature on a shell, distance from a seed. You choose the levels. ",[15,16,18],"a",{"href":17},"\u002Ftrueform","trueform"," returns the lines where the field crosses each level as connected polylines, and it can cut the mesh into bands between them. The bands come back as meshes, not as coloured pixels. Scroll a stack of slicing planes, the contours of height, through a dragon in the ",[15,21,24],{"href":22,"external":23},"\u002Ftrueform\u002Flive\u002Fslicing","","live example",".",[11,27,28],{},[29,30],"img",{"alt":31,"height":32,"src":33,"width":34},"A synthetic terrain coloured by height, teal low to orange high, with contour lines every 0.01 in dark teal and every fifth contour drawn heavier in pink",800,"\u002Fimg\u002Falgorithms\u002Fhubs\u002Fisocontours-terrain.webp",1200,[11,36,37],{},"The terrain above is synthetic: three hills, a saddle between two of them and a hollow, on a 126,000-triangle grid. Its 40 contour levels came from two calls, one for the minor lines and one for every fifth, the index contours. They make 75 curves. The 37 that close on themselves ring the hills and the hollow. The rest run off the edge of the grid.",[39,40],"hub-language-cards",{":items":41},"[{\"lang\":\"C++\",\"icon\":\"i-simple-icons-cplusplus\",\"title\":\"Fast and Robust Isocontours on a Triangle Mesh in C++\",\"to\":\"\u002Ftutorials\u002Fisocontours-on-a-triangle-mesh-in-cpp\",\"detail\":\"Header-only. The field is a range over your own buffer.\"},{\"lang\":\"Python\",\"icon\":\"i-simple-icons-python\",\"title\":\"Fast and Robust Isocontours on a Triangle Mesh in Python\",\"to\":\"\u002Ftutorials\u002Fisocontours-on-a-triangle-mesh-in-python\",\"detail\":\"One pip install. A NumPy field in, curves and bands out.\"},{\"lang\":\"JavaScript\",\"icon\":\"i-simple-icons-typescript\",\"title\":\"Fast and Robust Isocontours on a Triangle Mesh in JavaScript\",\"to\":\"\u002Ftutorials\u002Fisocontours-on-a-triangle-mesh-in-javascript\",\"detail\":\"WebAssembly. Sync and async calls, in Node.js and the browser.\"}]",[43,44,46],"h2",{"id":45},"how-a-level-crosses-a-triangle","How a level crosses a triangle",[11,48,49],{},"Inside one triangle the field is linear: it blends the three corner values. So a level crosses a triangle in a straight segment, or not at all. Walk the three edges. Where one end is below the level and the other above, the level crosses that edge once, at the fraction t = (level − low) \u002F (high − low) of the way from the low end. A triangle has two such edges or none.",[51,52],"image-grid",{":images":53},"[{\"src\":\"\u002Fimg\u002Falgorithms\u002Fhubs\u002Fisocontours-edge-crossing.webp\",\"width\":1200,\"height\":800,\"alt\":\"Two triangles sharing an edge, corner values 1, 4, 6 and 2, with the contour at level 3 drawn in pink through three orange crossing points, each labelled with its fraction along the edge\",\"label\":\"One level, two triangles\",\"detail\":\"one point per crossed edge\"},{\"src\":\"\u002Fimg\u002Falgorithms\u002Fhubs\u002Fisocontours-vertex-hit.webp\",\"width\":1200,\"height\":800,\"alt\":\"Six triangles around a centre vertex whose value is exactly 3; the contour at level 3 passes straight through the centre vertex as a single orange point\",\"label\":\"Level on a vertex\",\"detail\":\"one point, not six\"}]",[11,55,56],{},"Take the left picture, with made-up values. The top triangle has corners 1, 4 and 6; contour it at 3. The edge from 1 to 6 is crossed 2\u002F5 of the way up, the edge from 1 to 4 at 2\u002F3, and the edge from 4 to 6 not at all. The bottom triangle, with corners 1, 4 and 2, is crossed on the edge it shares with the top one, at the same 2\u002F3, and on the edge from 2 to 4, halfway. Two segments, three points, one curve.",[11,58,59],{},"This is marching squares cut down to triangles. On a square cell the four corners can leave a saddle, and the method must guess how the lines join. A linear triangle has no saddle, so there is nothing to guess.",[61,62,64],"h3",{"id":63},"when-the-level-hits-a-vertex","When the level hits a vertex",[11,66,67],{},"Terrain heights are often rounded, and simulation results often repeat. Then a level lands exactly on vertex values, and naive code breaks. The vertex is counted once per triangle around it, and the curve doubles, splits or leaves a gap there.",[11,69,70],{},"trueform counts an edge as crossing a level when the level lies in the half-open interval from the edge's lower value to its higher one. A level that lands on a vertex is recorded once, at the vertex, and every face around it uses that point, as in the right picture. A vertex that only touches the level, a peak or a pit exactly at it, starts no curve. Round the terrain above to steps of 0.02 and contour it at 0.1: every one of the curve's 804 points is a mesh vertex, and it still comes back as one closed curve.",[43,72,74],{"id":73},"curves-you-can-use","Curves you can use",[11,76,77],{},"A contour is only useful as a curve. You measure its length, offset it, export it to CAD, fill it or hand it to a toolpath. All of that needs ordered points, not a heap of loose segments.",[11,79,80,81,85,86,89],{},"The textbook method visits each triangle and emits its segment on its own. VTK's contour filter returns those segments, and a second filter, ",[82,83,84],"code",{},"vtkStripper",", joins them. libigl's ",[82,87,88],{},"isolines"," returns vertices and edges and leaves the joining to you. On a million faces that is a second problem.",[11,91,92],{},"trueform joins them inside the call. The result is one array of curve points and one index list per curve:",[94,95,96,108,114],"ul",{},[97,98,99,103,104,107],"li",{},[100,101,102],"strong",{},"Closed loops close."," A loop repeats its first index at the end, so ",[82,105,106],{},"path[0] == path[-1]"," tells you it is closed.",[97,109,110,113],{},[100,111,112],{},"Open curves end at the boundary."," On an open mesh, such as a terrain tile or a scanned patch, a curve that leaves the surface stops at its edge.",[97,115,116,119],{},[100,117,118],{},"Every crossing is one point."," A crossing point is keyed by its mesh edge and its level, so the two triangles on either side of an edge share it. Curves do not break between triangles.",[11,121,122],{},"Pass one level or an array; one call extracts them all, and repeated levels are dropped. Space levels evenly for a map or for layers, use the class limits for categories, and use the distances that matter for rings. A many-level call returns its curves in no particular level order, so when each curve's level matters, as for the heavier index contours above, make one call per level or per style.",[43,124,126],{"id":125},"isobands-are-meshes","Isobands are meshes",[11,128,129],{},"Isobands, or filled contours, are the regions between levels. Plotting tools paint them: each pixel gets the colour of its band, and the picture is all you get. VTK's banded contour filter returns polygons per band. trueform cuts the mesh itself. Every triangle the contours cross is split along them, so the contours become edges of the output mesh.",[11,131,132],{},[29,133],{"alt":134,"height":32,"src":135,"width":34},"The synthetic terrain cut into seven isobands along its height contours, each band lifted above the one below it so the separate pieces show, with the cut edges in pink","\u002Fimg\u002Falgorithms\u002Fhubs\u002Fisocontours-bands.webp",[11,137,138],{},"The same terrain, cut at six levels into seven bands. Each band was split off by its label and lifted to show that it is a mesh of its own. Every output triangle carries two numbers:",[94,140,141,147],{},[97,142,143,146],{},[100,144,145],{},"The band label."," Six cuts make seven bands: band 0 is below the first cut, band 6 above the last. Split by label to get one mesh per band.",[97,148,149,152],{},[100,150,151],{},"The input face."," The index of the triangle it was cut from. Use it to move any per-face data onto the bands: material, region, colour.",[11,154,155,156,160],{},"Each crossed face is re-triangulated by a ",[15,157,159],{"href":158},"\u002Falgorithms\u002Fconstrained-delaunay-triangulation","constrained Delaunay triangulation"," on an exact integer grid, with the contour pieces as its constraints. Because neighbouring faces share each crossing point, the bands fit back together without cracks.",[11,162,163],{},"You can keep only some bands, such as everything above a stress limit:",[165,166,170],"pre",{"className":167,"code":168,"language":169,"meta":23,"style":23},"language-python shiki shiki-themes github-dark","(faces, points), labels, face_labels = tf.isobands(mesh, stress, [limit], selected_bands=[1])\n","python",[82,171,172],{"__ignoreMap":23},[173,174,177,181,185,188,192,194,197,201],"span",{"class":175,"line":176},"line",1,[173,178,180],{"class":179},"s95oV","(faces, points), labels, face_labels ",[173,182,184],{"class":183},"snl16","=",[173,186,187],{"class":179}," tf.isobands(mesh, stress, [limit], ",[173,189,191],{"class":190},"s9osk","selected_bands",[173,193,184],{"class":183},[173,195,196],{"class":179},"[",[173,198,200],{"class":199},"sDLfK","1",[173,202,203],{"class":179},"])\n",[11,205,206],{},"You can ask for the contour curves in the same call as the bands, so both come from one pass.",[43,208,210],{"id":209},"fields-worth-contouring","Fields worth contouring",[11,212,213],{},"The field is a plain array, one value per vertex, in the mesh's float type. Anything that gives a number per vertex works:",[94,215,216,227,233,244,255],{},[97,217,218,221,222,226],{},[100,219,220],{},"Height."," Contour lines on terrain and scans. Height contours are also slices: see ",[15,223,225],{"href":224},"\u002Ftutorials\u002Fmesh-slicing-and-cross-sections-in-python","mesh slicing and cross-sections"," for layers and sections.",[97,228,229,232],{},[100,230,231],{},"Simulation results."," Stress, strain, temperature or pressure stored per vertex become contour plots, and isobands become the regions above or below a limit.",[97,234,235,238,239,243],{},[100,236,237],{},"Geodesic distance."," Contours of ",[15,240,242],{"href":241},"\u002Falgorithms\u002Fgeodesic-distances","geodesic distance"," are rings of equal distance along the surface. One band is the patch within a given reach.",[97,245,246,249,250,254],{},[100,247,248],{},"Curvature."," Contours of the shape index from ",[15,251,253],{"href":252},"\u002Falgorithms\u002Fprincipal-curvatures","principal curvatures"," separate cups, saddles and caps.",[97,256,257,260],{},[100,258,259],{},"Distance to a plane or a point."," Sections and spherical shells.",[11,262,263,264,25],{},"To cut one mesh by another mesh, not by a field, use ",[15,265,267],{"href":266},"\u002Falgorithms\u002Fmesh-booleans","mesh booleans",[43,269,271],{"id":270},"isocontours-in-code","Isocontours in code",[11,273,274],{},"Each example contours height at one level, then cuts the terrain into bands at three. The tutorials cover many levels, selected bands, curves with bands and splitting bands into parts.",[276,277,278,608,776],"code-group",{},[165,279,284],{"className":280,"code":281,"filename":282,"language":283,"meta":23,"style":23},"language-cpp shiki shiki-themes github-dark","#include \u003Ctrueform\u002Ftrueform.hpp>\n\nauto mesh_buffer = tf::read_stl(\"terrain.stl\");\nauto mesh = mesh_buffer.polygons();\ntf::buffer\u003Cfloat> field;                       \u002F\u002F height: the z of every point\nfield.allocate(mesh.points().size());\ntf::parallel_transform(mesh.points(), field, [](auto pt) { return pt[2]; });\n\nauto contour = tf::make_isocontours(mesh, tf::make_range(field), 0.f);\nfor (auto curve : contour.curves())\n  for (auto pt : curve) { \u002F* points along one curve *\u002F }\n\nstd::array\u003Cfloat, 3> cuts{-0.1f, 0.f, 0.1f};\nauto [bands, labels, face_labels] = tf::embedded_isocurves(\n    mesh, tf::make_range(field), tf::make_range(cuts));\n","C++","cpp",[82,285,286,295,302,332,351,370,394,431,436,474,494,513,518,565,585],{"__ignoreMap":23},[173,287,288,291],{"class":175,"line":176},[173,289,290],{"class":183},"#include",[173,292,294],{"class":293},"sU2Wk"," \u003Ctrueform\u002Ftrueform.hpp>\n",[173,296,298],{"class":175,"line":297},2,[173,299,301],{"emptyLinePlaceholder":300},true,"\n",[173,303,305,308,311,313,317,320,323,326,329],{"class":175,"line":304},3,[173,306,307],{"class":183},"auto",[173,309,310],{"class":179}," mesh_buffer ",[173,312,184],{"class":183},[173,314,316],{"class":315},"svObZ"," tf",[173,318,319],{"class":179},"::",[173,321,322],{"class":315},"read_stl",[173,324,325],{"class":179},"(",[173,327,328],{"class":293},"\"terrain.stl\"",[173,330,331],{"class":179},");\n",[173,333,335,337,340,342,345,348],{"class":175,"line":334},4,[173,336,307],{"class":183},[173,338,339],{"class":179}," mesh ",[173,341,184],{"class":183},[173,343,344],{"class":179}," mesh_buffer.",[173,346,347],{"class":315},"polygons",[173,349,350],{"class":179},"();\n",[173,352,354,357,360,363,366],{"class":175,"line":353},5,[173,355,356],{"class":315},"tf",[173,358,359],{"class":179},"::buffer",[173,361,362],{"class":183},"\u003Cfloat>",[173,364,365],{"class":179}," field;",[173,367,369],{"class":368},"sAwPA","                       \u002F\u002F height: the z of every point\n",[173,371,373,376,379,382,385,388,391],{"class":175,"line":372},6,[173,374,375],{"class":179},"field.",[173,377,378],{"class":315},"allocate",[173,380,381],{"class":179},"(mesh.",[173,383,384],{"class":315},"points",[173,386,387],{"class":179},"().",[173,389,390],{"class":315},"size",[173,392,393],{"class":179},"());\n",[173,395,397,399,401,404,406,408,411,413,416,419,422,425,428],{"class":175,"line":396},7,[173,398,356],{"class":315},[173,400,319],{"class":179},[173,402,403],{"class":315},"parallel_transform",[173,405,381],{"class":179},[173,407,384],{"class":315},[173,409,410],{"class":179},"(), field, [](",[173,412,307],{"class":183},[173,414,415],{"class":190}," pt",[173,417,418],{"class":179},") { ",[173,420,421],{"class":183},"return",[173,423,424],{"class":179}," pt[",[173,426,427],{"class":199},"2",[173,429,430],{"class":179},"]; });\n",[173,432,434],{"class":175,"line":433},8,[173,435,301],{"emptyLinePlaceholder":300},[173,437,439,441,444,446,448,450,453,456,458,460,463,466,469,472],{"class":175,"line":438},9,[173,440,307],{"class":183},[173,442,443],{"class":179}," contour ",[173,445,184],{"class":183},[173,447,316],{"class":315},[173,449,319],{"class":179},[173,451,452],{"class":315},"make_isocontours",[173,454,455],{"class":179},"(mesh, ",[173,457,356],{"class":315},[173,459,319],{"class":179},[173,461,462],{"class":315},"make_range",[173,464,465],{"class":179},"(field), ",[173,467,468],{"class":199},"0.",[173,470,471],{"class":183},"f",[173,473,331],{"class":179},[173,475,477,480,483,485,488,491],{"class":175,"line":476},10,[173,478,479],{"class":183},"for",[173,481,482],{"class":179}," (",[173,484,307],{"class":183},[173,486,487],{"class":179}," curve : contour.",[173,489,490],{"class":315},"curves",[173,492,493],{"class":179},"())\n",[173,495,497,500,502,504,507,510],{"class":175,"line":496},11,[173,498,499],{"class":183},"  for",[173,501,482],{"class":179},[173,503,307],{"class":183},[173,505,506],{"class":179}," pt : curve) {",[173,508,509],{"class":368}," \u002F* points along one curve *\u002F",[173,511,512],{"class":179}," }\n",[173,514,516],{"class":175,"line":515},12,[173,517,301],{"emptyLinePlaceholder":300},[173,519,521,524,527,530,533,536,539,542,545,548,550,552,554,556,558,560,562],{"class":175,"line":520},13,[173,522,523],{"class":315},"std",[173,525,526],{"class":179},"::array",[173,528,529],{"class":183},"\u003Cfloat",[173,531,532],{"class":179},", ",[173,534,535],{"class":199},"3",[173,537,538],{"class":183},">",[173,540,541],{"class":179}," cuts{",[173,543,544],{"class":183},"-",[173,546,547],{"class":199},"0.1",[173,549,471],{"class":183},[173,551,532],{"class":179},[173,553,468],{"class":199},[173,555,471],{"class":183},[173,557,532],{"class":179},[173,559,547],{"class":199},[173,561,471],{"class":183},[173,563,564],{"class":179},"};\n",[173,566,568,570,573,575,577,579,582],{"class":175,"line":567},14,[173,569,307],{"class":183},[173,571,572],{"class":179}," [bands, labels, face_labels] ",[173,574,184],{"class":183},[173,576,316],{"class":315},[173,578,319],{"class":179},[173,580,581],{"class":315},"embedded_isocurves",[173,583,584],{"class":179},"(\n",[173,586,588,591,593,595,597,599,601,603,605],{"class":175,"line":587},15,[173,589,590],{"class":179},"    mesh, ",[173,592,356],{"class":315},[173,594,319],{"class":179},[173,596,462],{"class":315},[173,598,465],{"class":179},[173,600,356],{"class":315},[173,602,319],{"class":179},[173,604,462],{"class":315},[173,606,607],{"class":179},"(cuts));\n",[165,609,612],{"className":167,"code":610,"filename":611,"language":169,"meta":23,"style":23},"import numpy as np\nimport trueform as tf\n\nmesh = tf.Mesh(*tf.read_stl(\"terrain.stl\"))\nfield = mesh.points[:, 2].copy()        # height, the mesh's dtype\n\npaths, curve_points = tf.isocontours(mesh, field, 0.0)\nfor path_ids in paths:\n    curve = curve_points[path_ids]      # (m, 3) points along one curve\n\ncuts = np.array([-0.1, 0.0, 0.1], dtype=np.float32)\n(band_faces, band_points), labels, face_labels = tf.isobands(mesh, field, cuts)\n","Python",[82,613,614,628,640,644,665,683,687,703,716,729,733,766],{"__ignoreMap":23},[173,615,616,619,622,625],{"class":175,"line":176},[173,617,618],{"class":183},"import",[173,620,621],{"class":179}," numpy ",[173,623,624],{"class":183},"as",[173,626,627],{"class":179}," np\n",[173,629,630,632,635,637],{"class":175,"line":297},[173,631,618],{"class":183},[173,633,634],{"class":179}," trueform ",[173,636,624],{"class":183},[173,638,639],{"class":179}," tf\n",[173,641,642],{"class":175,"line":304},[173,643,301],{"emptyLinePlaceholder":300},[173,645,646,649,651,654,657,660,662],{"class":175,"line":334},[173,647,648],{"class":179},"mesh ",[173,650,184],{"class":183},[173,652,653],{"class":179}," tf.Mesh(",[173,655,656],{"class":183},"*",[173,658,659],{"class":179},"tf.read_stl(",[173,661,328],{"class":293},[173,663,664],{"class":179},"))\n",[173,666,667,670,672,675,677,680],{"class":175,"line":353},[173,668,669],{"class":179},"field ",[173,671,184],{"class":183},[173,673,674],{"class":179}," mesh.points[:, ",[173,676,427],{"class":199},[173,678,679],{"class":179},"].copy()        ",[173,681,682],{"class":368},"# height, the mesh's dtype\n",[173,684,685],{"class":175,"line":372},[173,686,301],{"emptyLinePlaceholder":300},[173,688,689,692,694,697,700],{"class":175,"line":396},[173,690,691],{"class":179},"paths, curve_points ",[173,693,184],{"class":183},[173,695,696],{"class":179}," tf.isocontours(mesh, field, ",[173,698,699],{"class":199},"0.0",[173,701,702],{"class":179},")\n",[173,704,705,707,710,713],{"class":175,"line":433},[173,706,479],{"class":183},[173,708,709],{"class":179}," path_ids ",[173,711,712],{"class":183},"in",[173,714,715],{"class":179}," paths:\n",[173,717,718,721,723,726],{"class":175,"line":438},[173,719,720],{"class":179},"    curve ",[173,722,184],{"class":183},[173,724,725],{"class":179}," curve_points[path_ids]      ",[173,727,728],{"class":368},"# (m, 3) points along one curve\n",[173,730,731],{"class":175,"line":476},[173,732,301],{"emptyLinePlaceholder":300},[173,734,735,738,740,743,745,747,749,751,753,755,758,761,763],{"class":175,"line":496},[173,736,737],{"class":179},"cuts ",[173,739,184],{"class":183},[173,741,742],{"class":179}," np.array([",[173,744,544],{"class":183},[173,746,547],{"class":199},[173,748,532],{"class":179},[173,750,699],{"class":199},[173,752,532],{"class":179},[173,754,547],{"class":199},[173,756,757],{"class":179},"], ",[173,759,760],{"class":190},"dtype",[173,762,184],{"class":183},[173,764,765],{"class":179},"np.float32)\n",[173,767,768,771,773],{"class":175,"line":515},[173,769,770],{"class":179},"(band_faces, band_points), labels, face_labels ",[173,772,184],{"class":183},[173,774,775],{"class":179}," tf.isobands(mesh, field, cuts)\n",[165,777,782],{"className":778,"code":779,"filename":780,"language":781,"meta":23,"style":23},"language-ts shiki shiki-themes github-dark","import * as tf from \"@polydera\u002Ftrueform\";\nimport { readFileSync } from \"node:fs\";\n\nconst mesh = tf.readStl(readFileSync(\"terrain.stl\"));\nconst field = mesh.points.take(null, 2);      \u002F\u002F height: the z column\n\nconst curves = tf.isocontours(mesh, field, 0.0);\nfor (const pathIds of curves.paths) {\n  const ids = pathIds.data;   \u002F\u002F indices into curves.points\n}\n\nconst { mesh: bands, labels, faceLabels } =\n  tf.isobands(mesh, field, [-0.1, 0.0, 0.1]);\n","JavaScript","ts",[82,783,784,806,820,824,853,883,887,908,925,941,946,950,982],{"__ignoreMap":23},[173,785,786,788,791,794,797,800,803],{"class":175,"line":176},[173,787,618],{"class":183},[173,789,790],{"class":199}," *",[173,792,793],{"class":183}," as",[173,795,796],{"class":179}," tf ",[173,798,799],{"class":183},"from",[173,801,802],{"class":293}," \"@polydera\u002Ftrueform\"",[173,804,805],{"class":179},";\n",[173,807,808,810,813,815,818],{"class":175,"line":297},[173,809,618],{"class":183},[173,811,812],{"class":179}," { readFileSync } ",[173,814,799],{"class":183},[173,816,817],{"class":293}," \"node:fs\"",[173,819,805],{"class":179},[173,821,822],{"class":175,"line":304},[173,823,301],{"emptyLinePlaceholder":300},[173,825,826,829,832,835,838,841,843,846,848,850],{"class":175,"line":334},[173,827,828],{"class":183},"const",[173,830,831],{"class":199}," mesh",[173,833,834],{"class":183}," =",[173,836,837],{"class":179}," tf.",[173,839,840],{"class":315},"readStl",[173,842,325],{"class":179},[173,844,845],{"class":315},"readFileSync",[173,847,325],{"class":179},[173,849,328],{"class":293},[173,851,852],{"class":179},"));\n",[173,854,855,857,860,862,865,868,870,873,875,877,880],{"class":175,"line":353},[173,856,828],{"class":183},[173,858,859],{"class":199}," field",[173,861,834],{"class":183},[173,863,864],{"class":179}," mesh.points.",[173,866,867],{"class":315},"take",[173,869,325],{"class":179},[173,871,872],{"class":199},"null",[173,874,532],{"class":179},[173,876,427],{"class":199},[173,878,879],{"class":179},");      ",[173,881,882],{"class":368},"\u002F\u002F height: the z column\n",[173,884,885],{"class":175,"line":372},[173,886,301],{"emptyLinePlaceholder":300},[173,888,889,891,894,896,898,901,904,906],{"class":175,"line":396},[173,890,828],{"class":183},[173,892,893],{"class":199}," curves",[173,895,834],{"class":183},[173,897,837],{"class":179},[173,899,900],{"class":315},"isocontours",[173,902,903],{"class":179},"(mesh, field, ",[173,905,699],{"class":199},[173,907,331],{"class":179},[173,909,910,912,914,916,919,922],{"class":175,"line":433},[173,911,479],{"class":183},[173,913,482],{"class":179},[173,915,828],{"class":183},[173,917,918],{"class":199}," pathIds",[173,920,921],{"class":183}," of",[173,923,924],{"class":179}," curves.paths) {\n",[173,926,927,930,933,935,938],{"class":175,"line":438},[173,928,929],{"class":183},"  const",[173,931,932],{"class":199}," ids",[173,934,834],{"class":183},[173,936,937],{"class":179}," pathIds.data;   ",[173,939,940],{"class":368},"\u002F\u002F indices into curves.points\n",[173,942,943],{"class":175,"line":476},[173,944,945],{"class":179},"}\n",[173,947,948],{"class":175,"line":496},[173,949,301],{"emptyLinePlaceholder":300},[173,951,952,954,957,960,963,966,968,971,973,976,979],{"class":175,"line":515},[173,953,828],{"class":183},[173,955,956],{"class":179}," { ",[173,958,959],{"class":190},"mesh",[173,961,962],{"class":179},": ",[173,964,965],{"class":199},"bands",[173,967,532],{"class":179},[173,969,970],{"class":199},"labels",[173,972,532],{"class":179},[173,974,975],{"class":199},"faceLabels",[173,977,978],{"class":179}," } ",[173,980,981],{"class":183},"=\n",[173,983,984,987,990,993,995,997,999,1001,1003,1005],{"class":175,"line":520},[173,985,986],{"class":179},"  tf.",[173,988,989],{"class":315},"isobands",[173,991,992],{"class":179},"(mesh, field, [",[173,994,544],{"class":183},[173,996,547],{"class":199},[173,998,532],{"class":179},[173,1000,699],{"class":199},[173,1002,532],{"class":179},[173,1004,547],{"class":199},[173,1006,1007],{"class":179},"]);\n",[11,1009,1010,1011,1014,1015,1018,1019,25],{},"The field must have the mesh's type. In Python, a float64 field on a float32 mesh raises a ",[82,1012,1013],{},"TypeError","; convert it with ",[82,1016,1017],{},"astype"," first. In JavaScript, both calls also have an async form under ",[82,1020,1021],{},"tf.async",[43,1023,1025],{"id":1024},"preparing-the-field","Preparing the field",[94,1027,1028,1034,1040],{},[97,1029,1030,1033],{},[100,1031,1032],{},"Per-face values."," The field lives on the vertices and is linear across each face. A per-face result, such as element stress from a solver, goes to the vertices first: average it over the faces around each vertex.",[97,1035,1036,1039],{},[100,1037,1038],{},"Values from a coarse simulation mesh."," A contour is as fine as the mesh under it, one straight piece per triangle. Contour the mesh the values live on, not a finer display mesh with interpolated values.",[97,1041,1042,1045],{},[100,1043,1044],{},"The field's type."," It must match the mesh, float32 with float32, float64 with float64; see the code notes above.",[43,1047,1049],{"id":1048},"how-it-compares","How it compares",[11,1051,1052],{},"What each library hands back, from its documentation and, for VTK and libigl, from the benchmark harness below.",[1054,1055],"data-table",{":headers":1056,":highlight":1057,":rows":1058},"[\"Library\",\"Language\",\"Contour lines\",\"Isobands\"]","0","[[\"trueform\",\"C++, Python, JavaScript\",\"joined curves; closed loops repeat their first point\",\"triangle meshes, with a band label and the source face per triangle\"],[\"VTK\",\"C++, Python\",\"line segments; vtkStripper joins them\",\"polygons per band, with a band scalar\"],[\"libigl\",\"C++\",\"vertices and edges, not joined\",\"no band function\"],[\"matplotlib\",\"Python\",\"paths on a flat 2D triangulation, for plotting\",\"filled paths, for plotting\"]]",[43,1060,1062],{"id":1061},"speed","Speed",[11,1064,1065],{},"The 1.03M-face Stanford dragon, 16 levels. Apple M4 Max, 16 threads, trueform 0.10.10.",[1054,1067],{":headers":1068,":highlight":200,":rows":1069},"[\"Language\",\"Contour curves, 16 levels\",\"Isobands, 16 levels\"]","[[\"C++\",\"4.50 ms (VTK 211 ms, 47×; libigl 713 ms, 159×)\",\"8.17 ms (VTK 584 ms, 71×)\"],[\"Python\",\"4.37 ms (VTK 156 ms, 36×)\",\"18.4 ms (VTK 340 ms, 18×)\"],[\"JavaScript (WebAssembly)\",\"7.08 ms (no npm library found for fields on a mesh)\",\"11.8 ms (none found)\"]]",[1071,1072],"hub-benchmark-figures",{":categories":1073},"[\"isocontours\",\"embedded-isocontours\"]",[11,1075,1076,1077,1081],{},"Every trueform benchmark, by module and language, is on the ",[15,1078,1080],{"href":1079},"\u002Ftrueform\u002Fbenchmarks","trueform benchmarks"," page.",[43,1083,1085],{"id":1084},"further","Further",[11,1087,1088,1091,1092,532,1095,1098,1099,1102],{},[100,1089,1090],{},"Tutorials."," ",[15,1093,282],{"href":1094},"\u002Ftutorials\u002Fisocontours-on-a-triangle-mesh-in-cpp",[15,1096,611],{"href":1097},"\u002Ftutorials\u002Fisocontours-on-a-triangle-mesh-in-python"," and ",[15,1100,780],{"href":1101},"\u002Ftutorials\u002Fisocontours-on-a-triangle-mesh-in-javascript",": the field, one level, many levels, isobands and working with the results.",[11,1104,1105,1108,1109,532,1114,1098,1118,25],{},[100,1106,1107],{},"Documentation."," The iso module, in ",[15,1110,282],{"href":1111,"rel":1112},"https:\u002F\u002Ftrueform.polydera.com\u002Fcpp\u002Fmodules\u002Fiso",[1113],"nofollow",[15,1115,611],{"href":1116,"rel":1117},"https:\u002F\u002Ftrueform.polydera.com\u002Fpy\u002Fmodules\u002Fiso",[1113],[15,1119,1122],{"href":1120,"rel":1121},"https:\u002F\u002Ftrueform.polydera.com\u002Fts\u002Fmodules\u002Fiso",[1113],"TypeScript",[11,1124,1125,1091,1128,1098,1131,1135],{},[100,1126,1127],{},"Live.",[15,1129,1130],{"href":22,"external":23},"Slicing",[15,1132,1134],{"href":1133,"external":23},"\u002Ftrueform\u002Flive\u002Fcross-section","cross-section"," on the Stanford dragon.",[11,1137,1138,1091,1141,532,1144,532,1147,1149,1150,1152],{},[100,1139,1140],{},"Related.",[15,1142,1143],{"href":224},"Mesh slicing and cross-sections",[15,1145,1146],{"href":241},"geodesic distances",[15,1148,253],{"href":252},", and ",[15,1151,159],{"href":158},", which cuts the bands.",[11,1154,1155,1156,1158],{},"Isocontours are one module of ",[15,1157,18],{"href":17},". The same library does booleans, spatial queries, topology and remeshing, in C++, Python and the browser.",[1160,1161],"article-cta",{":buttons":1162},"[{\"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\":\"\u002Ftrueform\u002Flive\u002Fslicing\",\"icon\":\"i-lucide-play\"}]",[1164,1165],"cite-as",{"author":1166,"title":5},"Sajovic, {\\v{Z}}iga",[1168,1169,1170],"style",{},"html pre.shiki code .s95oV, html code.shiki .s95oV{--shiki-default:#E1E4E8}html pre.shiki code .snl16, html code.shiki .snl16{--shiki-default:#F97583}html pre.shiki code .s9osk, html code.shiki .s9osk{--shiki-default:#FFAB70}html pre.shiki code .sDLfK, html code.shiki .sDLfK{--shiki-default:#79B8FF}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 .sU2Wk, html code.shiki .sU2Wk{--shiki-default:#9ECBFF}html pre.shiki code .svObZ, html code.shiki .svObZ{--shiki-default:#B392F0}html pre.shiki code .sAwPA, html code.shiki .sAwPA{--shiki-default:#6A737D}",{"title":23,"searchDepth":297,"depth":297,"links":1172},[1173,1176,1177,1178,1179,1180,1181,1182,1183],{"id":45,"depth":297,"text":46,"children":1174},[1175],{"id":63,"depth":304,"text":64},{"id":73,"depth":297,"text":74},{"id":125,"depth":297,"text":126},{"id":209,"depth":297,"text":210},{"id":270,"depth":297,"text":271},{"id":1024,"depth":297,"text":1025},{"id":1048,"depth":297,"text":1049},{"id":1061,"depth":297,"text":1062},{"id":1084,"depth":297,"text":1085},"2026-11-08","Contour lines and isobands of any scalar field on a triangle mesh. Height, stress, temperature or distance in; closed, connected curves and band meshes out, in C++, Python and JavaScript.","md",[1188,1189,1190,989,1191,1192,1193,1194,1195,1196],"isocontours mesh","contour lines mesh","isolines triangle mesh","filled contours mesh","terrain contours","stress contour plot","level set mesh","marching triangles","VTK contour alternative",{},"\u002Fimg\u002Fog\u002Falgorithms\u002Fisocontours.png","\u002Falgorithms\u002Fisocontours",{"title":1201,"description":1202},"Isocontours on a Mesh: Contour Lines and Isobands","Contour lines and isobands of any scalar field on a triangle mesh: connected, closed curves, and bands as real meshes. C++, Python and JavaScript.","algorithms\u002Fisocontours",[1205,1206,1207],"iso","scalar-fields","overview","2026-11-12T12:06:28.000Z","6fLWFtOTTskBpRnVQfLntPrlImIpSFev_FAXwmRrHb4",1794486339528]