[{"data":1,"prerenderedAt":1526},["ShallowReactive",2],{"tutorial-isocontours-on-a-triangle-mesh-in-javascript":3},{"id":4,"title":5,"author":6,"body":7,"date":1502,"description":1503,"extension":1504,"keywords":1505,"meta":1516,"navigation":151,"ogImage":1517,"path":1518,"published":151,"seo":1519,"stem":1520,"tags":1521,"__hash__":1525},"tutorials\u002Ftutorials\u002Fisocontours-on-a-triangle-mesh-in-javascript.md","Fast and Robust Isocontours on a Triangle Mesh in JavaScript","Žiga Sajovic",{"type":8,"value":9,"toc":1490},"minimark",[10,28,40,44,51,56,81,94,98,185,200,400,404,415,418,455,461,467,632,651,655,682,688,704,781,784,803,861,865,868,922,928,931,989,992,996,1003,1073,1079,1088,1091,1129,1132,1183,1187,1190,1241,1245,1248,1288,1294,1345,1348,1373,1395,1399,1428,1432,1436,1456,1464,1470,1482,1486],[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-python","Python"," and ",[18,24,26],{"href":25},"\u002Ftutorials\u002Fisocontours-on-a-triangle-mesh-in-cpp","C++",".",[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],{},"npm install",". It runs as WebAssembly in Node.js and the browser, with TypeScript types. 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\u002Fts\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","npm install @polydera\u002Ftrueform\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","npm",[66,75,77],{"class":76},"sU2Wk"," install",[66,79,80],{"class":76}," @polydera\u002Ftrueform\n",[14,82,83,84,87,88,27],{},"The WebAssembly binary loads on first use. In the browser, multithreading needs ",[32,85,86],{},"SharedArrayBuffer"," headers. See the ",[18,89,93],{"href":90,"rel":91},"https:\u002F\u002Ftrueform.polydera.com\u002Fts\u002Fgetting-started\u002Finstallation",[92],"nofollow","installation guide",[52,95,97],{"id":96},"loading-the-mesh","Loading the mesh",[57,99,103],{"className":100,"code":101,"language":102,"meta":62,"style":62},"language-typescript shiki shiki-themes github-dark","import * as tf from \"@polydera\u002Ftrueform\";\nimport { readFileSync } from \"node:fs\";\n\nconst mesh = tf.readStl(readFileSync(\"terrain.stl\"));\n","typescript",[32,104,105,131,146,153],{"__ignoreMap":62},[66,106,107,111,115,118,122,125,128],{"class":68,"line":69},[66,108,110],{"class":109},"snl16","import",[66,112,114],{"class":113},"sDLfK"," *",[66,116,117],{"class":109}," as",[66,119,121],{"class":120},"s95oV"," tf ",[66,123,124],{"class":109},"from",[66,126,127],{"class":76}," \"@polydera\u002Ftrueform\"",[66,129,130],{"class":120},";\n",[66,132,134,136,139,141,144],{"class":68,"line":133},2,[66,135,110],{"class":109},[66,137,138],{"class":120}," { readFileSync } ",[66,140,124],{"class":109},[66,142,143],{"class":76}," \"node:fs\"",[66,145,130],{"class":120},[66,147,149],{"class":68,"line":148},3,[66,150,152],{"emptyLinePlaceholder":151},true,"\n",[66,154,156,159,162,165,168,171,174,177,179,182],{"class":68,"line":155},4,[66,157,158],{"class":109},"const",[66,160,161],{"class":113}," mesh",[66,163,164],{"class":109}," =",[66,166,167],{"class":120}," tf.",[66,169,170],{"class":72},"readStl",[66,172,173],{"class":120},"(",[66,175,176],{"class":72},"readFileSync",[66,178,173],{"class":120},[66,180,181],{"class":76},"\"terrain.stl\"",[66,183,184],{"class":120},"));\n",[14,186,187,188,191,192,195,196,199],{},"In the browser, pass the bytes from ",[32,189,190],{},"fetch"," instead. ",[32,193,194],{},"tf.readObj"," works the same way. If you already have typed arrays, pass them to ",[32,197,198],{},"tf.mesh(faces, points)",". To follow along without a file, make a small wavy terrain:",[57,201,203],{"className":100,"code":202,"language":102,"meta":62,"style":62},"const flat = tf.planeMesh(2.0, 2.0, 200, 200);\nconst points = flat.points.clone();\nconst q = points.data;\nfor (let i = 0; i \u003C q.length; i += 3) {\n  q[i + 2] = 0.2 * Math.sin(3 * q[i]) * Math.cos(3 * q[i + 1]);\n}\nconst mesh = tf.mesh(flat.faces, points);\n",[32,204,205,241,259,271,314,377,383],{"__ignoreMap":62},[66,206,207,209,212,214,216,219,221,224,227,229,231,234,236,238],{"class":68,"line":69},[66,208,158],{"class":109},[66,210,211],{"class":113}," flat",[66,213,164],{"class":109},[66,215,167],{"class":120},[66,217,218],{"class":72},"planeMesh",[66,220,173],{"class":120},[66,222,223],{"class":113},"2.0",[66,225,226],{"class":120},", ",[66,228,223],{"class":113},[66,230,226],{"class":120},[66,232,233],{"class":113},"200",[66,235,226],{"class":120},[66,237,233],{"class":113},[66,239,240],{"class":120},");\n",[66,242,243,245,248,250,253,256],{"class":68,"line":133},[66,244,158],{"class":109},[66,246,247],{"class":113}," points",[66,249,164],{"class":109},[66,251,252],{"class":120}," flat.points.",[66,254,255],{"class":72},"clone",[66,257,258],{"class":120},"();\n",[66,260,261,263,266,268],{"class":68,"line":148},[66,262,158],{"class":109},[66,264,265],{"class":113}," q",[66,267,164],{"class":109},[66,269,270],{"class":120}," points.data;\n",[66,272,273,276,279,282,285,288,291,294,297,300,303,305,308,311],{"class":68,"line":155},[66,274,275],{"class":109},"for",[66,277,278],{"class":120}," (",[66,280,281],{"class":109},"let",[66,283,284],{"class":120}," i ",[66,286,287],{"class":109},"=",[66,289,290],{"class":113}," 0",[66,292,293],{"class":120},"; i ",[66,295,296],{"class":109},"\u003C",[66,298,299],{"class":120}," q.",[66,301,302],{"class":113},"length",[66,304,293],{"class":120},[66,306,307],{"class":109},"+=",[66,309,310],{"class":113}," 3",[66,312,313],{"class":120},") {\n",[66,315,317,320,323,326,329,331,334,336,339,342,344,347,349,352,355,357,360,362,364,366,369,371,374],{"class":68,"line":316},5,[66,318,319],{"class":120},"  q[i ",[66,321,322],{"class":109},"+",[66,324,325],{"class":113}," 2",[66,327,328],{"class":120},"] ",[66,330,287],{"class":109},[66,332,333],{"class":113}," 0.2",[66,335,114],{"class":109},[66,337,338],{"class":120}," Math.",[66,340,341],{"class":72},"sin",[66,343,173],{"class":120},[66,345,346],{"class":113},"3",[66,348,114],{"class":109},[66,350,351],{"class":120}," q[i]) ",[66,353,354],{"class":109},"*",[66,356,338],{"class":120},[66,358,359],{"class":72},"cos",[66,361,173],{"class":120},[66,363,346],{"class":113},[66,365,114],{"class":109},[66,367,368],{"class":120}," q[i ",[66,370,322],{"class":109},[66,372,373],{"class":113}," 1",[66,375,376],{"class":120},"]);\n",[66,378,380],{"class":68,"line":379},6,[66,381,382],{"class":120},"}\n",[66,384,386,388,390,392,394,397],{"class":68,"line":385},7,[66,387,158],{"class":109},[66,389,161],{"class":113},[66,391,164],{"class":109},[66,393,167],{"class":120},[66,395,396],{"class":72},"mesh",[66,398,399],{"class":120},"(flat.faces, points);\n",[52,401,403],{"id":402},"the-scalar-field","The scalar field",[14,405,406,407,410,411,414],{},"A scalar field is one value per point, as an ",[32,408,409],{},"NDArray"," of shape ",[32,412,413],{},"[numberOfPoints]",". It must have the mesh's dtype. Most meshes are float32, so most fields are float32.",[14,416,417],{},"Height is the simplest field:",[57,419,421],{"className":100,"code":420,"language":102,"meta":62,"style":62},"const field = mesh.points.take(null, 2);  \u002F\u002F the z column\n",[32,422,423],{"__ignoreMap":62},[66,424,425,427,430,432,435,438,440,443,445,448,451],{"class":68,"line":69},[66,426,158],{"class":109},[66,428,429],{"class":113}," field",[66,431,164],{"class":109},[66,433,434],{"class":120}," mesh.points.",[66,436,437],{"class":72},"take",[66,439,173],{"class":120},[66,441,442],{"class":113},"null",[66,444,226],{"class":120},[66,446,447],{"class":113},"2",[66,449,450],{"class":120},");  ",[66,452,454],{"class":453},"sAwPA","\u002F\u002F the z column\n",[14,456,457],{},[47,458],{"alt":459,"src":460},"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,462,463,464,466],{},"Any field works. A few that come from ",[32,465,34],{}," itself:",[57,468,470],{"className":100,"code":469,"language":102,"meta":62,"style":62},"\u002F\u002F signed distance to the plane x = 0\nconst plane = tf.plane(tf.vector(1, 0, 0), 0);\nconst across = tf.distance(tf.point(mesh.points), plane);\n\n\u002F\u002F geodesic distance from a point\nconst seed = tf.ndarray(new Int32Array([mesh.numberOfPoints >> 1]), [1]);\nconst along = tf.geodesicDistances(mesh, seed);\n\n\u002F\u002F shape index, from curvature\nconst shape = tf.shapeIndex(mesh);\n",[32,471,472,477,518,540,544,549,586,603,608,614],{"__ignoreMap":62},[66,473,474],{"class":68,"line":69},[66,475,476],{"class":453},"\u002F\u002F signed distance to the plane x = 0\n",[66,478,479,481,484,486,488,491,494,497,499,502,504,507,509,511,514,516],{"class":68,"line":133},[66,480,158],{"class":109},[66,482,483],{"class":113}," plane",[66,485,164],{"class":109},[66,487,167],{"class":120},[66,489,490],{"class":72},"plane",[66,492,493],{"class":120},"(tf.",[66,495,496],{"class":72},"vector",[66,498,173],{"class":120},[66,500,501],{"class":113},"1",[66,503,226],{"class":120},[66,505,506],{"class":113},"0",[66,508,226],{"class":120},[66,510,506],{"class":113},[66,512,513],{"class":120},"), ",[66,515,506],{"class":113},[66,517,240],{"class":120},[66,519,520,522,525,527,529,532,534,537],{"class":68,"line":148},[66,521,158],{"class":109},[66,523,524],{"class":113}," across",[66,526,164],{"class":109},[66,528,167],{"class":120},[66,530,531],{"class":72},"distance",[66,533,493],{"class":120},[66,535,536],{"class":72},"point",[66,538,539],{"class":120},"(mesh.points), plane);\n",[66,541,542],{"class":68,"line":155},[66,543,152],{"emptyLinePlaceholder":151},[66,545,546],{"class":68,"line":316},[66,547,548],{"class":453},"\u002F\u002F geodesic distance from a point\n",[66,550,551,553,556,558,560,563,565,568,571,574,577,579,582,584],{"class":68,"line":379},[66,552,158],{"class":109},[66,554,555],{"class":113}," seed",[66,557,164],{"class":109},[66,559,167],{"class":120},[66,561,562],{"class":72},"ndarray",[66,564,173],{"class":120},[66,566,567],{"class":109},"new",[66,569,570],{"class":72}," Int32Array",[66,572,573],{"class":120},"([mesh.numberOfPoints ",[66,575,576],{"class":109},">>",[66,578,373],{"class":113},[66,580,581],{"class":120},"]), [",[66,583,501],{"class":113},[66,585,376],{"class":120},[66,587,588,590,593,595,597,600],{"class":68,"line":385},[66,589,158],{"class":109},[66,591,592],{"class":113}," along",[66,594,164],{"class":109},[66,596,167],{"class":120},[66,598,599],{"class":72},"geodesicDistances",[66,601,602],{"class":120},"(mesh, seed);\n",[66,604,606],{"class":68,"line":605},8,[66,607,152],{"emptyLinePlaceholder":151},[66,609,611],{"class":68,"line":610},9,[66,612,613],{"class":453},"\u002F\u002F shape index, from curvature\n",[66,615,617,619,622,624,626,629],{"class":68,"line":616},10,[66,618,158],{"class":109},[66,620,621],{"class":113}," shape",[66,623,164],{"class":109},[66,625,167],{"class":120},[66,627,628],{"class":72},"shapeIndex",[66,630,631],{"class":120},"(mesh);\n",[14,633,634,635,638,639,642,643,646,647,650],{},"If your field is float64 and the mesh is float32, ",[32,636,637],{},"tf.isocontours"," throws ",[32,640,641],{},"dtype mismatch",". Build the field as a ",[32,644,645],{},"Float32Array",", or the mesh with ",[32,648,649],{},"Float64Array"," points.",[52,652,654],{"id":653},"one-level","One level",[57,656,658],{"className":100,"code":657,"language":102,"meta":62,"style":62},"const curves = tf.isocontours(mesh, field, 0.0);\n",[32,659,660],{"__ignoreMap":62},[66,661,662,664,667,669,671,674,677,680],{"class":68,"line":69},[66,663,158],{"class":109},[66,665,666],{"class":113}," curves",[66,668,164],{"class":109},[66,670,167],{"class":120},[66,672,673],{"class":72},"isocontours",[66,675,676],{"class":120},"(mesh, field, ",[66,678,679],{"class":113},"0.0",[66,681,240],{"class":120},[14,683,684],{},[47,685],{"alt":686,"src":687},"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,689,690,693,694,410,696,699,700,703],{},[32,691,692],{},"curves.points"," is an ",[32,695,409],{},[32,697,698],{},"[n, 3]",", points on the surface. ",[32,701,702],{},"curves.paths"," holds one index list per curve:",[57,705,707],{"className":100,"code":706,"language":102,"meta":62,"style":62},"for (const pathIds of curves.paths) {\n  const ids = pathIds.data;                     \u002F\u002F Int32Array into curves.points\n  const closed = ids[0] === ids[ids.length - 1];  \u002F\u002F a closed loop repeats its first id\n}\n",[32,708,709,726,742,777],{"__ignoreMap":62},[66,710,711,713,715,717,720,723],{"class":68,"line":69},[66,712,275],{"class":109},[66,714,278],{"class":120},[66,716,158],{"class":109},[66,718,719],{"class":113}," pathIds",[66,721,722],{"class":109}," of",[66,724,725],{"class":120}," curves.paths) {\n",[66,727,728,731,734,736,739],{"class":68,"line":133},[66,729,730],{"class":109},"  const",[66,732,733],{"class":113}," ids",[66,735,164],{"class":109},[66,737,738],{"class":120}," pathIds.data;                     ",[66,740,741],{"class":453},"\u002F\u002F Int32Array into curves.points\n",[66,743,744,746,749,751,754,756,758,761,764,766,769,771,774],{"class":68,"line":148},[66,745,730],{"class":109},[66,747,748],{"class":113}," closed",[66,750,164],{"class":109},[66,752,753],{"class":120}," ids[",[66,755,506],{"class":113},[66,757,328],{"class":120},[66,759,760],{"class":109},"===",[66,762,763],{"class":120}," ids[ids.",[66,765,302],{"class":113},[66,767,768],{"class":109}," -",[66,770,373],{"class":113},[66,772,773],{"class":120},"];  ",[66,775,776],{"class":453},"\u002F\u002F a closed loop repeats its first id\n",[66,778,779],{"class":68,"line":155},[66,780,382],{"class":120},[14,782,783],{},"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,785,786,693,788,791,792,22,795,798,799,802],{},[32,787,702],{},[32,789,790],{},"OffsetBlockedBuffer",". For flat arrays, read ",[32,793,794],{},"offsets",[32,796,797],{},"data",". Curve ",[32,800,801],{},"i"," is the slice between two offsets:",[57,804,806],{"className":100,"code":805,"language":102,"meta":62,"style":62},"const offsets = curves.paths.offsets.data;  \u002F\u002F Int32Array\nconst data = curves.paths.data.data;        \u002F\u002F Int32Array\nconst curve = data.subarray(offsets[i], offsets[i + 1]);\n",[32,807,808,823,837],{"__ignoreMap":62},[66,809,810,812,815,817,820],{"class":68,"line":69},[66,811,158],{"class":109},[66,813,814],{"class":113}," offsets",[66,816,164],{"class":109},[66,818,819],{"class":120}," curves.paths.offsets.data;  ",[66,821,822],{"class":453},"\u002F\u002F Int32Array\n",[66,824,825,827,830,832,835],{"class":68,"line":133},[66,826,158],{"class":109},[66,828,829],{"class":113}," data",[66,831,164],{"class":109},[66,833,834],{"class":120}," curves.paths.data.data;        ",[66,836,822],{"class":453},[66,838,839,841,844,846,849,852,855,857,859],{"class":68,"line":148},[66,840,158],{"class":109},[66,842,843],{"class":113}," curve",[66,845,164],{"class":109},[66,847,848],{"class":120}," data.",[66,850,851],{"class":72},"subarray",[66,853,854],{"class":120},"(offsets[i], offsets[i ",[66,856,322],{"class":109},[66,858,373],{"class":113},[66,860,376],{"class":120},[52,862,864],{"id":863},"many-levels","Many levels",[14,866,867],{},"Pass an array of levels. One call extracts them all.",[57,869,871],{"className":100,"code":870,"language":102,"meta":62,"style":62},"const levels = tf.linspace(-0.15, 0.15, 7).data;\nconst curves = tf.isocontours(mesh, field, levels);\n",[32,872,873,907],{"__ignoreMap":62},[66,874,875,877,880,882,884,887,889,892,895,897,899,901,904],{"class":68,"line":69},[66,876,158],{"class":109},[66,878,879],{"class":113}," levels",[66,881,164],{"class":109},[66,883,167],{"class":120},[66,885,886],{"class":72},"linspace",[66,888,173],{"class":120},[66,890,891],{"class":109},"-",[66,893,894],{"class":113},"0.15",[66,896,226],{"class":120},[66,898,894],{"class":113},[66,900,226],{"class":120},[66,902,903],{"class":113},"7",[66,905,906],{"class":120},").data;\n",[66,908,909,911,913,915,917,919],{"class":68,"line":133},[66,910,158],{"class":109},[66,912,666],{"class":113},[66,914,164],{"class":109},[66,916,167],{"class":120},[66,918,673],{"class":72},[66,920,921],{"class":120},"(mesh, field, levels);\n",[14,923,924],{},[47,925],{"alt":926,"src":927},"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,929,930],{},"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,932,934],{"className":100,"code":933,"language":102,"meta":62,"style":62},"const byLevel = new Map(\n  [...levels].map((t) => [t, tf.isocontours(mesh, field, t)]));\n",[32,935,936,954],{"__ignoreMap":62},[66,937,938,940,943,945,948,951],{"class":68,"line":69},[66,939,158],{"class":109},[66,941,942],{"class":113}," byLevel",[66,944,164],{"class":109},[66,946,947],{"class":109}," new",[66,949,950],{"class":72}," Map",[66,952,953],{"class":120},"(\n",[66,955,956,959,962,965,968,971,975,978,981,984,986],{"class":68,"line":133},[66,957,958],{"class":120},"  [",[66,960,961],{"class":109},"...",[66,963,964],{"class":120},"levels].",[66,966,967],{"class":72},"map",[66,969,970],{"class":120},"((",[66,972,974],{"class":973},"s9osk","t",[66,976,977],{"class":120},") ",[66,979,980],{"class":109},"=>",[66,982,983],{"class":120}," [t, tf.",[66,985,673],{"class":72},[66,987,988],{"class":120},"(mesh, field, t)]));\n",[14,990,991],{},"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,993,995],{"id":994},"isobands","Isobands",[14,997,998,999,1002],{},"Isobands are the regions between levels. ",[32,1000,1001],{},"tf.isobands"," cuts the mesh along the isocontours, so the curves become edges, and labels each face by band.",[57,1004,1006],{"className":100,"code":1005,"language":102,"meta":62,"style":62},"const cuts = [-0.1, 0.0, 0.1];\nconst { mesh: bands, labels, faceLabels } = tf.isobands(mesh, field, cuts);\n",[32,1007,1008,1036],{"__ignoreMap":62},[66,1009,1010,1012,1015,1017,1020,1022,1025,1027,1029,1031,1033],{"class":68,"line":69},[66,1011,158],{"class":109},[66,1013,1014],{"class":113}," cuts",[66,1016,164],{"class":109},[66,1018,1019],{"class":120}," [",[66,1021,891],{"class":109},[66,1023,1024],{"class":113},"0.1",[66,1026,226],{"class":120},[66,1028,679],{"class":113},[66,1030,226],{"class":120},[66,1032,1024],{"class":113},[66,1034,1035],{"class":120},"];\n",[66,1037,1038,1040,1043,1045,1048,1051,1053,1056,1058,1061,1064,1066,1068,1070],{"class":68,"line":133},[66,1039,158],{"class":109},[66,1041,1042],{"class":120}," { ",[66,1044,396],{"class":973},[66,1046,1047],{"class":120},": ",[66,1049,1050],{"class":113},"bands",[66,1052,226],{"class":120},[66,1054,1055],{"class":113},"labels",[66,1057,226],{"class":120},[66,1059,1060],{"class":113},"faceLabels",[66,1062,1063],{"class":120}," } ",[66,1065,287],{"class":109},[66,1067,167],{"class":120},[66,1069,994],{"class":72},[66,1071,1072],{"class":120},"(mesh, field, cuts);\n",[14,1074,1075],{},[47,1076],{"alt":1077,"src":1078},"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,1080,1081,1082,1084,1085,1087],{},"Three cuts give four bands. Band 0 is below -0.1, band 3 is above 0.1. ",[32,1083,1055],{}," gives the band of each output face. ",[32,1086,1060],{}," gives the original face it came from.",[14,1089,1090],{},"Keep only some bands:",[57,1092,1094],{"className":100,"code":1093,"language":102,"meta":62,"style":62},"const { mesh: outer } = tf.isobands(mesh, field, cuts, { selectedBands: [0, 3] });\n",[32,1095,1096],{"__ignoreMap":62},[66,1097,1098,1100,1102,1104,1106,1109,1111,1113,1115,1117,1120,1122,1124,1126],{"class":68,"line":69},[66,1099,158],{"class":109},[66,1101,1042],{"class":120},[66,1103,396],{"class":973},[66,1105,1047],{"class":120},[66,1107,1108],{"class":113},"outer",[66,1110,1063],{"class":120},[66,1112,287],{"class":109},[66,1114,167],{"class":120},[66,1116,994],{"class":72},[66,1118,1119],{"class":120},"(mesh, field, cuts, { selectedBands: [",[66,1121,506],{"class":113},[66,1123,226],{"class":120},[66,1125,346],{"class":113},[66,1127,1128],{"class":120},"] });\n",[14,1130,1131],{},"Get the boundary curves in the same call:",[57,1133,1135],{"className":100,"code":1134,"language":102,"meta":62,"style":62},"const { mesh: bands, labels, faceLabels, curves } =\n  tf.isobands(mesh, field, cuts, { returnCurves: true });\n",[32,1136,1137,1167],{"__ignoreMap":62},[66,1138,1139,1141,1143,1145,1147,1149,1151,1153,1155,1157,1159,1162,1164],{"class":68,"line":69},[66,1140,158],{"class":109},[66,1142,1042],{"class":120},[66,1144,396],{"class":973},[66,1146,1047],{"class":120},[66,1148,1050],{"class":113},[66,1150,226],{"class":120},[66,1152,1055],{"class":113},[66,1154,226],{"class":120},[66,1156,1060],{"class":113},[66,1158,226],{"class":120},[66,1160,1161],{"class":113},"curves",[66,1163,1063],{"class":120},[66,1165,1166],{"class":109},"=\n",[66,1168,1169,1172,1174,1177,1180],{"class":68,"line":133},[66,1170,1171],{"class":120},"  tf.",[66,1173,994],{"class":72},[66,1175,1176],{"class":120},"(mesh, field, cuts, { returnCurves: ",[66,1178,1179],{"class":113},"true",[66,1181,1182],{"class":120}," });\n",[52,1184,1186],{"id":1185},"off-the-main-thread","Off the main thread",[14,1188,1189],{},"Both functions have async variants with the same arguments:",[57,1191,1193],{"className":100,"code":1192,"language":102,"meta":62,"style":62},"const curves = await tf.async.isocontours(mesh, field, levels);\nconst { mesh: bands, labels } = await tf.async.isobands(mesh, field, cuts);\n",[32,1194,1195,1213],{"__ignoreMap":62},[66,1196,1197,1199,1201,1203,1206,1209,1211],{"class":68,"line":69},[66,1198,158],{"class":109},[66,1200,666],{"class":113},[66,1202,164],{"class":109},[66,1204,1205],{"class":109}," await",[66,1207,1208],{"class":120}," tf.async.",[66,1210,673],{"class":72},[66,1212,921],{"class":120},[66,1214,1215,1217,1219,1221,1223,1225,1227,1229,1231,1233,1235,1237,1239],{"class":68,"line":133},[66,1216,158],{"class":109},[66,1218,1042],{"class":120},[66,1220,396],{"class":973},[66,1222,1047],{"class":120},[66,1224,1050],{"class":113},[66,1226,226],{"class":120},[66,1228,1055],{"class":113},[66,1230,1063],{"class":120},[66,1232,287],{"class":109},[66,1234,1205],{"class":109},[66,1236,1208],{"class":120},[66,1238,994],{"class":72},[66,1240,1072],{"class":120},[52,1242,1244],{"id":1243},"working-with-results","Working with results",[14,1246,1247],{},"Split the bands into separate meshes:",[57,1249,1251],{"className":100,"code":1250,"language":102,"meta":62,"style":62},"const { components, labels: partLabels } = tf.splitIntoComponents(bands, labels);\n\u002F\u002F components[i] is a Mesh for band partLabels.data[i]\n",[32,1252,1253,1283],{"__ignoreMap":62},[66,1254,1255,1257,1259,1262,1264,1266,1268,1271,1273,1275,1277,1280],{"class":68,"line":69},[66,1256,158],{"class":109},[66,1258,1042],{"class":120},[66,1260,1261],{"class":113},"components",[66,1263,226],{"class":120},[66,1265,1055],{"class":973},[66,1267,1047],{"class":120},[66,1269,1270],{"class":113},"partLabels",[66,1272,1063],{"class":120},[66,1274,287],{"class":109},[66,1276,167],{"class":120},[66,1278,1279],{"class":72},"splitIntoComponents",[66,1281,1282],{"class":120},"(bands, labels);\n",[66,1284,1285],{"class":68,"line":133},[66,1286,1287],{"class":453},"\u002F\u002F components[i] is a Mesh for band partLabels.data[i]\n",[14,1289,1290,1291,1293],{},"Carry per-face data from the input to the output with ",[32,1292,1060],{},":",[57,1295,1297],{"className":100,"code":1296,"language":102,"meta":62,"style":62},"const material = new Int32Array(mesh.numberOfFaces);  \u002F\u002F your per-face data\nconst bandMaterial = Int32Array.from(faceLabels.data, (f) => material[f]);\n",[32,1298,1299,1318],{"__ignoreMap":62},[66,1300,1301,1303,1306,1308,1310,1312,1315],{"class":68,"line":69},[66,1302,158],{"class":109},[66,1304,1305],{"class":113}," material",[66,1307,164],{"class":109},[66,1309,947],{"class":109},[66,1311,570],{"class":72},[66,1313,1314],{"class":120},"(mesh.numberOfFaces);  ",[66,1316,1317],{"class":453},"\u002F\u002F your per-face data\n",[66,1319,1320,1322,1325,1327,1330,1332,1335,1338,1340,1342],{"class":68,"line":133},[66,1321,158],{"class":109},[66,1323,1324],{"class":113}," bandMaterial",[66,1326,164],{"class":109},[66,1328,1329],{"class":120}," Int32Array.",[66,1331,124],{"class":72},[66,1333,1334],{"class":120},"(faceLabels.data, (",[66,1336,1337],{"class":973},"f",[66,1339,977],{"class":120},[66,1341,980],{"class":109},[66,1343,1344],{"class":120}," material[f]);\n",[14,1346,1347],{},"Write to file:",[57,1349,1351],{"className":100,"code":1350,"language":102,"meta":62,"style":62},"const objBytes = tf.writeObj(bands).data;  \u002F\u002F Int8Array, save or download\n",[32,1352,1353],{"__ignoreMap":62},[66,1354,1355,1357,1360,1362,1364,1367,1370],{"class":68,"line":69},[66,1356,158],{"class":109},[66,1358,1359],{"class":113}," objBytes",[66,1361,164],{"class":109},[66,1363,167],{"class":120},[66,1365,1366],{"class":72},"writeObj",[66,1368,1369],{"class":120},"(bands).data;  ",[66,1371,1372],{"class":453},"\u002F\u002F Int8Array, save or download\n",[14,1374,1375,1376,1380,1381,1385,1386,1390,1391,27],{},"Contours of height are slices. See ",[18,1377,1379],{"href":1378},"\u002Ftutorials\u002Fmesh-slicing-and-cross-sections-in-javascript","mesh slicing and cross-sections"," for layers and sections. Contours of ",[18,1382,1384],{"href":1383},"\u002Ftutorials\u002Ffast-geodesic-distances-in-javascript","geodesic distance"," are rings. Contours of the ",[18,1387,1389],{"href":1388},"\u002Ftutorials\u002Fprincipal-curvatures-of-a-mesh-in-javascript","shape index"," separate cups, saddles, and caps. To cut one mesh by another mesh instead of by a field, use ",[18,1392,1394],{"href":1393},"\u002Ftutorials\u002Ffast-mesh-booleans-in-javascript","booleans",[52,1396,1398],{"id":1397},"related-tutorials","Related tutorials",[1400,1401,1402,1409,1415,1421],"ul",{},[1403,1404,1405,1408],"li",{},[18,1406,1407],{"href":1383},"Fast and Accurate Geodesic Distances in JavaScript"," — distance along the surface from one seed or many.",[1403,1410,1411,1414],{},[18,1412,1413],{"href":1378},"Fast and Robust Mesh Slicing and Cross-Sections in JavaScript"," — plane sections, layers and slabs of a mesh.",[1403,1416,1417,1420],{},[18,1418,1419],{"href":1388},"Fast and Robust Principal Curvatures of a Mesh in JavaScript"," — curvature values and directions at every vertex.",[1403,1422,1423,1427],{},[18,1424,1426],{"href":1425},"\u002Ftutorials\u002Fmesh-intersection-curves-in-javascript","Fast and Exact Mesh Intersection Curves in JavaScript"," — exact curves where two or more meshes cross.",[52,1429,1431],{"id":1430},"performance","Performance",[1433,1434],"headline-numbers",{":items":1435},"[{\"value\":\"9.4ms\",\"label\":\"Isocontours\",\"detail\":\"1,025,766 polygons, JavaScript\"}]",[14,1437,1438,1443,1444,1443,1448,1443,1452],{},[66,1439,1442],{"className":1440},[1441],"tag-pill","Apple M4 Max"," ",[66,1445,1447],{"className":1446},[1441],"16 threads",[66,1449,1451],{"className":1450},[1441],"Node.js",[66,1453,1455],{"className":1454},[1441],"WebAssembly",[14,1457,1458,1459,22,1461,1463],{},"Isocontours on a 1,025,766-polygon mesh take 9.4 ms in Node.js. The JavaScript benchmark has no direct competitor on triangle surfaces, so there is no comparison here. The same core is measured against VTK on the ",[18,1460,21],{"href":20},[18,1462,26],{"href":25}," pages.",[14,1465,1466],{},[47,1467],{"alt":1468,"src":1469},"Benchmark chart: trueform against other TypeScript libraries, isocontours","\u002Fimg\u002Falgorithms\u002Fbenchmark-isocontours-typescript-2026.png",[14,1471,1472,1476,1477],{},[18,1473,1475],{"href":1474},"\u002Ftrueform\u002Fbenchmarks","Full benchmarks"," · ",[18,1478,1481],{"href":1479,"rel":1480},"https:\u002F\u002Ftrueform.polydera.com\u002Fts\u002Fmodules\u002Fiso",[92],"Documentation",[1483,1484],"cite-as",{"author":1485,"title":5},"Sajovic, {\\v{Z}}iga",[1487,1488,1489],"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 .sDLfK, html code.shiki .sDLfK{--shiki-default:#79B8FF}html pre.shiki code .s95oV, html code.shiki .s95oV{--shiki-default:#E1E4E8}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":133,"depth":133,"links":1491},[1492,1493,1494,1495,1496,1497,1498,1499,1500,1501],{"id":54,"depth":133,"text":55},{"id":96,"depth":133,"text":97},{"id":402,"depth":133,"text":403},{"id":653,"depth":133,"text":654},{"id":863,"depth":133,"text":864},{"id":994,"depth":133,"text":995},{"id":1185,"depth":133,"text":1186},{"id":1243,"depth":133,"text":1244},{"id":1397,"depth":133,"text":1398},{"id":1430,"depth":133,"text":1431},"2026-10-31","Extract isocontours (isolines) of any scalar field on a triangle mesh in JavaScript and TypeScript. One level or many at once, plus isobands that cut the mesh along the curves. WebAssembly, runs in Node.js and the browser.","md",[1506,1507,1508,1509,1510,1511,1512,1513,1514,1515],"isocontours mesh JavaScript","isolines triangle mesh TypeScript","contour lines on mesh browser","scalar field contours mesh JavaScript","isobands JavaScript","WebAssembly isocontours","three.js contour lines mesh","npm isolines mesh","VTK contour filter alternative","level set curves mesh JavaScript",{},"\u002Fimg\u002Fog\u002Ftutorials\u002Fisocontours-on-a-triangle-mesh-in-javascript.png","\u002Ftutorials\u002Fisocontours-on-a-triangle-mesh-in-javascript",{"title":5,"description":1503},"tutorials\u002Fisocontours-on-a-triangle-mesh-in-javascript",[1522,102,673,1523,1524],"javascript","tutorial","webassembly","0yLC5rWwRrGi_7xjJ44av6c-OXNLrixamjTgWKD8iBU",1793517812008]