[{"data":1,"prerenderedAt":1608},["ShallowReactive",2],{"tutorial-fast-geodesic-distances-in-javascript":3},{"id":4,"title":5,"author":6,"body":7,"date":1583,"description":1584,"extension":1585,"keywords":1586,"meta":1597,"navigation":155,"ogImage":1598,"path":1599,"published":155,"seo":1600,"stem":1601,"tags":1602,"__hash__":1607},"tutorials\u002Ftutorials\u002Ffast-geodesic-distances-in-javascript.md","Fast and Accurate Geodesic Distances in JavaScript","Žiga Sajovic",{"type":8,"value":9,"toc":1570},"minimark",[10,28,44,48,55,60,85,98,102,189,196,257,267,304,308,319,389,395,398,401,624,627,630,742,746,749,752,755,987,993,996,999,1033,1037,1044,1133,1139,1148,1152,1155,1212,1226,1229,1233,1239,1262,1266,1279,1285,1357,1363,1370,1373,1444,1450,1462,1466,1472,1476,1506,1510,1516,1520,1536,1541,1547,1550,1562,1566],[11,12,13],"note",{},[14,15,16,17,22,23,27],"p",{},"Also available in ",[18,19,21],"a",{"href":20},"\u002Ftutorials\u002Ffast-geodesic-distances-in-python","Python"," and ",[18,24,26],{"href":25},"\u002Ftutorials\u002Ffast-geodesic-distances-in-cpp","C++",".",[14,29,30,31,35,36,39,40,43],{},"A geodesic distance is the distance from a point to a seed, measured along the surface of a mesh. Straight-line distance cuts through the object. Geodesic distance walks over it. ",[32,33,34],"code",{},"trueform"," computes it with one call, ",[32,37,38],{},"tf.geodesicDistances",", new in version 0.10.8. One ",[32,41,42],{},"npm install",". It runs as WebAssembly in Node.js and the browser, with TypeScript types. This tutorial covers one seed, many seeds, a bounded radius, float32 and float64, and turning distances into rings and patches.",[45,46],"article-cta",{":buttons":47},"[{\"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\u002Fgeometry#geodesic-distances\",\"variant\":\"soft\",\"color\":\"neutral\"}]",[14,49,50],{},[51,52],"img",{"alt":53,"src":54},"Geodesic distance from one seed on the Stanford dragon, teal to orange, with iso-distance lines","\u002Fimg\u002Ftutorials\u002Fgeodesic-distances.png",[56,57,59],"h2",{"id":58},"install","Install",[61,62,67],"pre",{"className":63,"code":64,"language":65,"meta":66,"style":66},"language-bash shiki shiki-themes github-dark","npm install @polydera\u002Ftrueform\n","bash","",[32,68,69],{"__ignoreMap":66},[70,71,74,78,82],"span",{"class":72,"line":73},"line",1,[70,75,77],{"class":76},"svObZ","npm",[70,79,81],{"class":80},"sU2Wk"," install",[70,83,84],{"class":80}," @polydera\u002Ftrueform\n",[14,86,87,88,91,92,27],{},"You need version 0.10.8 or later. The WebAssembly binary loads on first use. In the browser, multithreading needs ",[32,89,90],{},"SharedArrayBuffer"," headers. See the ",[18,93,97],{"href":94,"rel":95},"https:\u002F\u002Ftrueform.polydera.com\u002Fts\u002Fgetting-started\u002Finstallation",[96],"nofollow","installation guide",[56,99,101],{"id":100},"loading-the-mesh","Loading the mesh",[61,103,107],{"className":104,"code":105,"language":106,"meta":66,"style":66},"language-typescript shiki shiki-themes github-dark","import * as tf from \"@polydera\u002Ftrueform\";\nimport { readFileSync } from \"node:fs\";\n\nconst mesh = tf.readStl(readFileSync(\"part.stl\"));\n","typescript",[32,108,109,135,150,157],{"__ignoreMap":66},[70,110,111,115,119,122,126,129,132],{"class":72,"line":73},[70,112,114],{"class":113},"snl16","import",[70,116,118],{"class":117},"sDLfK"," *",[70,120,121],{"class":113}," as",[70,123,125],{"class":124},"s95oV"," tf ",[70,127,128],{"class":113},"from",[70,130,131],{"class":80}," \"@polydera\u002Ftrueform\"",[70,133,134],{"class":124},";\n",[70,136,138,140,143,145,148],{"class":72,"line":137},2,[70,139,114],{"class":113},[70,141,142],{"class":124}," { readFileSync } ",[70,144,128],{"class":113},[70,146,147],{"class":80}," \"node:fs\"",[70,149,134],{"class":124},[70,151,153],{"class":72,"line":152},3,[70,154,156],{"emptyLinePlaceholder":155},true,"\n",[70,158,160,163,166,169,172,175,178,181,183,186],{"class":72,"line":159},4,[70,161,162],{"class":113},"const",[70,164,165],{"class":117}," mesh",[70,167,168],{"class":113}," =",[70,170,171],{"class":124}," tf.",[70,173,174],{"class":76},"readStl",[70,176,177],{"class":124},"(",[70,179,180],{"class":76},"readFileSync",[70,182,177],{"class":124},[70,184,185],{"class":80},"\"part.stl\"",[70,187,188],{"class":124},"));\n",[14,190,191,192,195],{},"In the browser, pass the bytes from ",[32,193,194],{},"fetch",":",[61,197,199],{"className":104,"code":198,"language":106,"meta":66,"style":66},"const bytes = new Uint8Array(await (await fetch(\"part.stl\")).arrayBuffer());\nconst mesh = tf.readStl(bytes);\n",[32,200,201,242],{"__ignoreMap":66},[70,202,203,205,208,210,213,216,218,221,224,226,229,231,233,236,239],{"class":72,"line":73},[70,204,162],{"class":113},[70,206,207],{"class":117}," bytes",[70,209,168],{"class":113},[70,211,212],{"class":113}," new",[70,214,215],{"class":76}," Uint8Array",[70,217,177],{"class":124},[70,219,220],{"class":113},"await",[70,222,223],{"class":124}," (",[70,225,220],{"class":113},[70,227,228],{"class":76}," fetch",[70,230,177],{"class":124},[70,232,185],{"class":80},[70,234,235],{"class":124},")).",[70,237,238],{"class":76},"arrayBuffer",[70,240,241],{"class":124},"());\n",[70,243,244,246,248,250,252,254],{"class":72,"line":137},[70,245,162],{"class":113},[70,247,165],{"class":117},[70,249,168],{"class":113},[70,251,171],{"class":124},[70,253,174],{"class":76},[70,255,256],{"class":124},"(bytes);\n",[14,258,259,262,263,266],{},[32,260,261],{},"tf.readObj"," works the same way. If you already have typed arrays, pass them to ",[32,264,265],{},"tf.mesh(faces, points)",". To follow along without a file, make a unit sphere:",[61,268,270],{"className":104,"code":269,"language":106,"meta":66,"style":66},"const mesh = tf.sphereMesh(1.0, 200, 400);\n",[32,271,272],{"__ignoreMap":66},[70,273,274,276,278,280,282,285,287,290,293,296,298,301],{"class":72,"line":73},[70,275,162],{"class":113},[70,277,165],{"class":117},[70,279,168],{"class":113},[70,281,171],{"class":124},[70,283,284],{"class":76},"sphereMesh",[70,286,177],{"class":124},[70,288,289],{"class":117},"1.0",[70,291,292],{"class":124},", ",[70,294,295],{"class":117},"200",[70,297,292],{"class":124},[70,299,300],{"class":117},"400",[70,302,303],{"class":124},");\n",[56,305,307],{"id":306},"distance-from-one-seed","Distance from one seed",[14,309,310,311,314,315,318],{},"A seed is a point id. Seeds go in as an ",[32,312,313],{},"Int32Array"," wrapped in an ",[32,316,317],{},"NDArray",". The result has one distance per point.",[61,320,322],{"className":104,"code":321,"language":106,"meta":66,"style":66},"const seeds = tf.ndarray(new Int32Array([0]), [1]);\nconst d = tf.geodesicDistances(mesh, seeds);\n\u002F\u002F d: NDArray, shape [numberOfPoints], the mesh's dtype\n\u002F\u002F d.data[0] === 0\n",[32,323,324,361,378,384],{"__ignoreMap":66},[70,325,326,328,331,333,335,338,340,343,346,349,352,355,358],{"class":72,"line":73},[70,327,162],{"class":113},[70,329,330],{"class":117}," seeds",[70,332,168],{"class":113},[70,334,171],{"class":124},[70,336,337],{"class":76},"ndarray",[70,339,177],{"class":124},[70,341,342],{"class":113},"new",[70,344,345],{"class":76}," Int32Array",[70,347,348],{"class":124},"([",[70,350,351],{"class":117},"0",[70,353,354],{"class":124},"]), [",[70,356,357],{"class":117},"1",[70,359,360],{"class":124},"]);\n",[70,362,363,365,368,370,372,375],{"class":72,"line":137},[70,364,162],{"class":113},[70,366,367],{"class":117}," d",[70,369,168],{"class":113},[70,371,171],{"class":124},[70,373,374],{"class":76},"geodesicDistances",[70,376,377],{"class":124},"(mesh, seeds);\n",[70,379,380],{"class":72,"line":152},[70,381,383],{"class":382},"sAwPA","\u002F\u002F d: NDArray, shape [numberOfPoints], the mesh's dtype\n",[70,385,386],{"class":72,"line":159},[70,387,388],{"class":382},"\u002F\u002F d.data[0] === 0\n",[14,390,391],{},[51,392],{"alt":393,"src":394},"Distance from one seed, computed on the Stanford bunny: geodesic distance from point 0 at the nose, teal near to orange far, with pink rings of equal distance","\u002Fimg\u002Ftutorials\u002Fsteps\u002Ffast-geodesic-distances-1-seed.png",[14,396,397],{},"The distance is zero at the seed. On the unit sphere, the farthest point is the opposite pole, at a distance close to π.",[14,399,400],{},"You can check the result on this sphere. The exact geodesic distance on a unit sphere is the great-circle angle:",[61,402,404],{"className":104,"code":403,"language":106,"meta":66,"style":66},"const p = mesh.points.data;\nlet maxErr = 0;\nfor (let i = 0; i \u003C mesh.numberOfPoints; i++) {\n  const c = p[3*i]*p[0] + p[3*i+1]*p[1] + p[3*i+2]*p[2];\n  const exact = Math.acos(Math.min(1, Math.max(-1, c)));\n  maxErr = Math.max(maxErr, Math.abs(d.data[i] - exact));\n}\n",[32,405,406,418,434,465,550,592,618],{"__ignoreMap":66},[70,407,408,410,413,415],{"class":72,"line":73},[70,409,162],{"class":113},[70,411,412],{"class":117}," p",[70,414,168],{"class":113},[70,416,417],{"class":124}," mesh.points.data;\n",[70,419,420,423,426,429,432],{"class":72,"line":137},[70,421,422],{"class":113},"let",[70,424,425],{"class":124}," maxErr ",[70,427,428],{"class":113},"=",[70,430,431],{"class":117}," 0",[70,433,134],{"class":124},[70,435,436,439,441,443,446,448,450,453,456,459,462],{"class":72,"line":152},[70,437,438],{"class":113},"for",[70,440,223],{"class":124},[70,442,422],{"class":113},[70,444,445],{"class":124}," i ",[70,447,428],{"class":113},[70,449,431],{"class":117},[70,451,452],{"class":124},"; i ",[70,454,455],{"class":113},"\u003C",[70,457,458],{"class":124}," mesh.numberOfPoints; i",[70,460,461],{"class":113},"++",[70,463,464],{"class":124},") {\n",[70,466,467,470,473,475,478,481,484,487,489,492,494,497,500,502,504,506,509,511,513,516,518,520,522,524,526,528,530,532,534,536,539,541,543,545,547],{"class":72,"line":159},[70,468,469],{"class":113},"  const",[70,471,472],{"class":117}," c",[70,474,168],{"class":113},[70,476,477],{"class":124}," p[",[70,479,480],{"class":117},"3",[70,482,483],{"class":113},"*",[70,485,486],{"class":124},"i]",[70,488,483],{"class":113},[70,490,491],{"class":124},"p[",[70,493,351],{"class":117},[70,495,496],{"class":124},"] ",[70,498,499],{"class":113},"+",[70,501,477],{"class":124},[70,503,480],{"class":117},[70,505,483],{"class":113},[70,507,508],{"class":124},"i",[70,510,499],{"class":113},[70,512,357],{"class":117},[70,514,515],{"class":124},"]",[70,517,483],{"class":113},[70,519,491],{"class":124},[70,521,357],{"class":117},[70,523,496],{"class":124},[70,525,499],{"class":113},[70,527,477],{"class":124},[70,529,480],{"class":117},[70,531,483],{"class":113},[70,533,508],{"class":124},[70,535,499],{"class":113},[70,537,538],{"class":117},"2",[70,540,515],{"class":124},[70,542,483],{"class":113},[70,544,491],{"class":124},[70,546,538],{"class":117},[70,548,549],{"class":124},"];\n",[70,551,553,555,558,560,563,566,569,572,574,576,579,582,584,587,589],{"class":72,"line":552},5,[70,554,469],{"class":113},[70,556,557],{"class":117}," exact",[70,559,168],{"class":113},[70,561,562],{"class":124}," Math.",[70,564,565],{"class":76},"acos",[70,567,568],{"class":124},"(Math.",[70,570,571],{"class":76},"min",[70,573,177],{"class":124},[70,575,357],{"class":117},[70,577,578],{"class":124},", Math.",[70,580,581],{"class":76},"max",[70,583,177],{"class":124},[70,585,586],{"class":113},"-",[70,588,357],{"class":117},[70,590,591],{"class":124},", c)));\n",[70,593,595,598,600,602,604,607,610,613,615],{"class":72,"line":594},6,[70,596,597],{"class":124},"  maxErr ",[70,599,428],{"class":113},[70,601,562],{"class":124},[70,603,581],{"class":76},[70,605,606],{"class":124},"(maxErr, Math.",[70,608,609],{"class":76},"abs",[70,611,612],{"class":124},"(d.data[i] ",[70,614,586],{"class":113},[70,616,617],{"class":124}," exact));\n",[70,619,621],{"class":72,"line":620},7,[70,622,623],{"class":124},"}\n",[14,625,626],{},"The difference is small, and it shrinks as the sphere gets finer.",[14,628,629],{},"Most of the time the seed is a position, not an id. Find the nearest mesh point first:",[61,631,633],{"className":104,"code":632,"language":106,"meta":66,"style":66},"const cloud = tf.pointCloud(mesh.points);\nconst { elementId: seed } = tf.neighborSearch(cloud, tf.point(0, 0, -1));\n\nconst fromSeed = tf.geodesicDistances(\n  mesh, tf.ndarray(new Int32Array([seed]), [1]));\n",[32,634,635,652,701,705,721],{"__ignoreMap":66},[70,636,637,639,642,644,646,649],{"class":72,"line":73},[70,638,162],{"class":113},[70,640,641],{"class":117}," cloud",[70,643,168],{"class":113},[70,645,171],{"class":124},[70,647,648],{"class":76},"pointCloud",[70,650,651],{"class":124},"(mesh.points);\n",[70,653,654,656,659,663,666,669,672,674,676,679,682,685,687,689,691,693,695,697,699],{"class":72,"line":137},[70,655,162],{"class":113},[70,657,658],{"class":124}," { ",[70,660,662],{"class":661},"s9osk","elementId",[70,664,665],{"class":124},": ",[70,667,668],{"class":117},"seed",[70,670,671],{"class":124}," } ",[70,673,428],{"class":113},[70,675,171],{"class":124},[70,677,678],{"class":76},"neighborSearch",[70,680,681],{"class":124},"(cloud, tf.",[70,683,684],{"class":76},"point",[70,686,177],{"class":124},[70,688,351],{"class":117},[70,690,292],{"class":124},[70,692,351],{"class":117},[70,694,292],{"class":124},[70,696,586],{"class":113},[70,698,357],{"class":117},[70,700,188],{"class":124},[70,702,703],{"class":72,"line":152},[70,704,156],{"emptyLinePlaceholder":155},[70,706,707,709,712,714,716,718],{"class":72,"line":159},[70,708,162],{"class":113},[70,710,711],{"class":117}," fromSeed",[70,713,168],{"class":113},[70,715,171],{"class":124},[70,717,374],{"class":76},[70,719,720],{"class":124},"(\n",[70,722,723,726,728,730,732,734,737,739],{"class":72,"line":552},[70,724,725],{"class":124},"  mesh, tf.",[70,727,337],{"class":76},[70,729,177],{"class":124},[70,731,342],{"class":113},[70,733,345],{"class":76},[70,735,736],{"class":124},"([seed]), [",[70,738,357],{"class":117},[70,740,741],{"class":124},"]));\n",[56,743,745],{"id":744},"distance-from-many-seeds","Distance from many seeds",[14,747,748],{},"Pass several ids. Each point gets the distance to its nearest seed.",[14,750,751],{},"The seeds can be a whole curve. Then the field is the distance from that curve, measured along the surface. That is the distance from an edge: an offset from a trim line, a falloff from a seam.",[14,753,754],{},"Take the bunny and cut it in half. The cut is the boundary of the half. Its points are the seeds:",[61,756,758],{"className":104,"code":757,"language":106,"meta":66,"style":66},"const bunny = tf.readStl(readFileSync(\"bunny.stl\"));\n\n\u002F\u002F cut it at the middle height; the bunny stands along y\nconst height = bunny.points.take(null, 1);\nlet lo = Infinity, hi = -Infinity;\nfor (const y of height.data) { lo = Math.min(lo, y); hi = Math.max(hi, y); }\nconst middle = 0.5 * (lo + hi);\nconst { mesh: half } = tf.isobands(\n  bunny, height, [middle], { selectedBands: [1] });\n\n\u002F\u002F the cut is the open boundary of the half\nconst cut = tf.boundaryPaths(half).data;   \u002F\u002F point ids on the cut\nconst fromCut = tf.geodesicDistances(half, cut);\n",[32,759,760,784,788,793,819,844,879,901,927,938,943,949,970],{"__ignoreMap":66},[70,761,762,764,767,769,771,773,775,777,779,782],{"class":72,"line":73},[70,763,162],{"class":113},[70,765,766],{"class":117}," bunny",[70,768,168],{"class":113},[70,770,171],{"class":124},[70,772,174],{"class":76},[70,774,177],{"class":124},[70,776,180],{"class":76},[70,778,177],{"class":124},[70,780,781],{"class":80},"\"bunny.stl\"",[70,783,188],{"class":124},[70,785,786],{"class":72,"line":137},[70,787,156],{"emptyLinePlaceholder":155},[70,789,790],{"class":72,"line":152},[70,791,792],{"class":382},"\u002F\u002F cut it at the middle height; the bunny stands along y\n",[70,794,795,797,800,802,805,808,810,813,815,817],{"class":72,"line":159},[70,796,162],{"class":113},[70,798,799],{"class":117}," height",[70,801,168],{"class":113},[70,803,804],{"class":124}," bunny.points.",[70,806,807],{"class":76},"take",[70,809,177],{"class":124},[70,811,812],{"class":117},"null",[70,814,292],{"class":124},[70,816,357],{"class":117},[70,818,303],{"class":124},[70,820,821,823,826,828,831,834,836,839,842],{"class":72,"line":552},[70,822,422],{"class":113},[70,824,825],{"class":124}," lo ",[70,827,428],{"class":113},[70,829,830],{"class":117}," Infinity",[70,832,833],{"class":124},", hi ",[70,835,428],{"class":113},[70,837,838],{"class":113}," -",[70,840,841],{"class":117},"Infinity",[70,843,134],{"class":124},[70,845,846,848,850,852,855,858,861,863,865,867,870,872,874,876],{"class":72,"line":594},[70,847,438],{"class":113},[70,849,223],{"class":124},[70,851,162],{"class":113},[70,853,854],{"class":117}," y",[70,856,857],{"class":113}," of",[70,859,860],{"class":124}," height.data) { lo ",[70,862,428],{"class":113},[70,864,562],{"class":124},[70,866,571],{"class":76},[70,868,869],{"class":124},"(lo, y); hi ",[70,871,428],{"class":113},[70,873,562],{"class":124},[70,875,581],{"class":76},[70,877,878],{"class":124},"(hi, y); }\n",[70,880,881,883,886,888,891,893,896,898],{"class":72,"line":620},[70,882,162],{"class":113},[70,884,885],{"class":117}," middle",[70,887,168],{"class":113},[70,889,890],{"class":117}," 0.5",[70,892,118],{"class":113},[70,894,895],{"class":124}," (lo ",[70,897,499],{"class":113},[70,899,900],{"class":124}," hi);\n",[70,902,904,906,908,911,913,916,918,920,922,925],{"class":72,"line":903},8,[70,905,162],{"class":113},[70,907,658],{"class":124},[70,909,910],{"class":661},"mesh",[70,912,665],{"class":124},[70,914,915],{"class":117},"half",[70,917,671],{"class":124},[70,919,428],{"class":113},[70,921,171],{"class":124},[70,923,924],{"class":76},"isobands",[70,926,720],{"class":124},[70,928,930,933,935],{"class":72,"line":929},9,[70,931,932],{"class":124},"  bunny, height, [middle], { selectedBands: [",[70,934,357],{"class":117},[70,936,937],{"class":124},"] });\n",[70,939,941],{"class":72,"line":940},10,[70,942,156],{"emptyLinePlaceholder":155},[70,944,946],{"class":72,"line":945},11,[70,947,948],{"class":382},"\u002F\u002F the cut is the open boundary of the half\n",[70,950,952,954,957,959,961,964,967],{"class":72,"line":951},12,[70,953,162],{"class":113},[70,955,956],{"class":117}," cut",[70,958,168],{"class":113},[70,960,171],{"class":124},[70,962,963],{"class":76},"boundaryPaths",[70,965,966],{"class":124},"(half).data;   ",[70,968,969],{"class":382},"\u002F\u002F point ids on the cut\n",[70,971,973,975,978,980,982,984],{"class":72,"line":972},13,[70,974,162],{"class":113},[70,976,977],{"class":117}," fromCut",[70,979,168],{"class":113},[70,981,171],{"class":124},[70,983,374],{"class":76},[70,985,986],{"class":124},"(half, cut);\n",[14,988,989],{},[51,990],{"alt":991,"src":992},"Distance from many seeds on the Stanford bunny: the bunny cut in half at mid-height, with every point on the cut as a seed. The half is colored by geodesic distance from the cut, teal at the cut to orange at the ear tips, with pink rings parallel to the cut and the cut itself as a thick pink line","\u002Fimg\u002Ftutorials\u002Fsteps\u002Ffast-geodesic-distances-2-seeds.png",[14,994,995],{},"Keep the top band. The bunny has holes in its base, so the top half has one boundary: the cut. The ids index the half's own points, so nothing maps back. The distance is zero on the cut and grows toward the ear tips.",[14,997,998],{},"One call handles all the seeds, here a few hundred. You do not loop over them. Isocontours of the field are curves parallel to the cut:",[61,1000,1002],{"className":104,"code":1001,"language":106,"meta":66,"style":66},"const rings = tf.isocontours(half, fromCut, [1, 2, 3]);\n",[32,1003,1004],{"__ignoreMap":66},[70,1005,1006,1008,1011,1013,1015,1018,1021,1023,1025,1027,1029,1031],{"class":72,"line":73},[70,1007,162],{"class":113},[70,1009,1010],{"class":117}," rings",[70,1012,168],{"class":113},[70,1014,171],{"class":124},[70,1016,1017],{"class":76},"isocontours",[70,1019,1020],{"class":124},"(half, fromCut, [",[70,1022,357],{"class":117},[70,1024,292],{"class":124},[70,1026,538],{"class":117},[70,1028,292],{"class":124},[70,1030,480],{"class":117},[70,1032,360],{"class":124},[56,1034,1036],{"id":1035},"bounded-by-a-radius","Bounded by a radius",[14,1038,1039,1040,1043],{},"Often you only care about the neighborhood of a seed. Pass ",[32,1041,1042],{},"radius",". The front stops there.",[61,1045,1047],{"className":104,"code":1046,"language":106,"meta":66,"style":66},"const near = tf.geodesicDistances(mesh, seeds, { radius: 0.5 });\n\nconst inside = near.lt(Infinity);  \u002F\u002F NDArray of bool\nconst region = tf.arange(\"int32\", mesh.numberOfPoints).booleanIndex(inside);\n\u002F\u002F region: point ids within 0.5 of the seed\n",[32,1048,1049,1071,1075,1100,1128],{"__ignoreMap":66},[70,1050,1051,1053,1056,1058,1060,1062,1065,1068],{"class":72,"line":73},[70,1052,162],{"class":113},[70,1054,1055],{"class":117}," near",[70,1057,168],{"class":113},[70,1059,171],{"class":124},[70,1061,374],{"class":76},[70,1063,1064],{"class":124},"(mesh, seeds, { radius: ",[70,1066,1067],{"class":117},"0.5",[70,1069,1070],{"class":124}," });\n",[70,1072,1073],{"class":72,"line":137},[70,1074,156],{"emptyLinePlaceholder":155},[70,1076,1077,1079,1082,1084,1087,1090,1092,1094,1097],{"class":72,"line":152},[70,1078,162],{"class":113},[70,1080,1081],{"class":117}," inside",[70,1083,168],{"class":113},[70,1085,1086],{"class":124}," near.",[70,1088,1089],{"class":76},"lt",[70,1091,177],{"class":124},[70,1093,841],{"class":117},[70,1095,1096],{"class":124},");  ",[70,1098,1099],{"class":382},"\u002F\u002F NDArray of bool\n",[70,1101,1102,1104,1107,1109,1111,1114,1116,1119,1122,1125],{"class":72,"line":159},[70,1103,162],{"class":113},[70,1105,1106],{"class":117}," region",[70,1108,168],{"class":113},[70,1110,171],{"class":124},[70,1112,1113],{"class":76},"arange",[70,1115,177],{"class":124},[70,1117,1118],{"class":80},"\"int32\"",[70,1120,1121],{"class":124},", mesh.numberOfPoints).",[70,1123,1124],{"class":76},"booleanIndex",[70,1126,1127],{"class":124},"(inside);\n",[70,1129,1130],{"class":72,"line":552},[70,1131,1132],{"class":382},"\u002F\u002F region: point ids within 0.5 of the seed\n",[14,1134,1135],{},[51,1136],{"alt":1137,"src":1138},"Bounded by a radius on the Stanford bunny: only points within geodesic distance 0.5 of the seed get a distance; the rest stay dark, and the pink ring marks the radius","\u002Fimg\u002Ftutorials\u002Fsteps\u002Ffast-geodesic-distances-3-radius.png",[14,1140,1141,1142,1144,1145,1147],{},"Points past the radius get ",[32,1143,841],{},". So do points on a piece of the mesh the seed is not connected to, with or without a radius. Test against ",[32,1146,841],{},", not against a large number.",[56,1149,1151],{"id":1150},"float32-or-float64","float32 or float64",[14,1153,1154],{},"The result has the mesh's dtype. A float32 mesh gives float32 distances. A float64 mesh gives float64 distances.",[61,1156,1158],{"className":104,"code":1157,"language":106,"meta":66,"style":66},"const mesh64 = tf.sphereMesh(1.0, 200, 400, { dtype: \"float64\" });\nconst d64 = tf.geodesicDistances(mesh64, seeds);  \u002F\u002F d64.dtype === \"float64\"\n",[32,1159,1160,1193],{"__ignoreMap":66},[70,1161,1162,1164,1167,1169,1171,1173,1175,1177,1179,1181,1183,1185,1188,1191],{"class":72,"line":73},[70,1163,162],{"class":113},[70,1165,1166],{"class":117}," mesh64",[70,1168,168],{"class":113},[70,1170,171],{"class":124},[70,1172,284],{"class":76},[70,1174,177],{"class":124},[70,1176,289],{"class":117},[70,1178,292],{"class":124},[70,1180,295],{"class":117},[70,1182,292],{"class":124},[70,1184,300],{"class":117},[70,1186,1187],{"class":124},", { dtype: ",[70,1189,1190],{"class":80},"\"float64\"",[70,1192,1070],{"class":124},[70,1194,1195,1197,1200,1202,1204,1206,1209],{"class":72,"line":137},[70,1196,162],{"class":113},[70,1198,1199],{"class":117}," d64",[70,1201,168],{"class":113},[70,1203,171],{"class":124},[70,1205,374],{"class":76},[70,1207,1208],{"class":124},"(mesh64, seeds);  ",[70,1210,1211],{"class":382},"\u002F\u002F d64.dtype === \"float64\"\n",[14,1213,1214,1217,1218,1221,1222,1225],{},[32,1215,1216],{},"tf.readStl"," and the mesh generators give float32 by default. Pass ",[32,1219,1220],{},"Float64Array"," points to ",[32,1223,1224],{},"tf.mesh"," for a double-precision mesh.",[14,1227,1228],{},"Distances are measured in the stored coordinates. A transformation set on the mesh does not change them.",[56,1230,1232],{"id":1231},"off-the-main-thread","Off the main thread",[14,1234,1235,1236,195],{},"In the browser, a large mesh can block the UI. The async variant runs off the main thread and returns a ",[32,1237,1238],{},"Promise",[61,1240,1242],{"className":104,"code":1241,"language":106,"meta":66,"style":66},"const d = await tf.async.geodesicDistances(mesh, seeds);\n",[32,1243,1244],{"__ignoreMap":66},[70,1245,1246,1248,1250,1252,1255,1258,1260],{"class":72,"line":73},[70,1247,162],{"class":113},[70,1249,367],{"class":117},[70,1251,168],{"class":113},[70,1253,1254],{"class":113}," await",[70,1256,1257],{"class":124}," tf.async.",[70,1259,374],{"class":76},[70,1261,377],{"class":124},[56,1263,1265],{"id":1264},"working-with-results","Working with results",[14,1267,1268,1269,1271,1272,1275,1276,1278],{},"The distance field is an ",[32,1270,317],{},", one value per point. ",[32,1273,1274],{},"d.data"," is the underlying typed array, ready to upload as a vertex attribute for coloring. Any scalar-field tool in ",[32,1277,34],{}," takes the field directly.",[14,1280,1281,1282,195],{},"Draw rings of equal distance with ",[18,1283,1017],{"href":1284},"\u002Ftutorials\u002Fisocontours-on-a-triangle-mesh-in-javascript",[61,1286,1288],{"className":104,"code":1287,"language":106,"meta":66,"style":66},"const rings = tf.isocontours(mesh, d, [0.25, 0.5, 1.0]);\n\nfor (const pathIds of rings.paths) {\n  const ids = pathIds.data;  \u002F\u002F Int32Array, indices into rings.points\n}\n",[32,1289,1290,1318,1322,1338,1353],{"__ignoreMap":66},[70,1291,1292,1294,1296,1298,1300,1302,1305,1308,1310,1312,1314,1316],{"class":72,"line":73},[70,1293,162],{"class":113},[70,1295,1010],{"class":117},[70,1297,168],{"class":113},[70,1299,171],{"class":124},[70,1301,1017],{"class":76},[70,1303,1304],{"class":124},"(mesh, d, [",[70,1306,1307],{"class":117},"0.25",[70,1309,292],{"class":124},[70,1311,1067],{"class":117},[70,1313,292],{"class":124},[70,1315,289],{"class":117},[70,1317,360],{"class":124},[70,1319,1320],{"class":72,"line":137},[70,1321,156],{"emptyLinePlaceholder":155},[70,1323,1324,1326,1328,1330,1333,1335],{"class":72,"line":152},[70,1325,438],{"class":113},[70,1327,223],{"class":124},[70,1329,162],{"class":113},[70,1331,1332],{"class":117}," pathIds",[70,1334,857],{"class":113},[70,1336,1337],{"class":124}," rings.paths) {\n",[70,1339,1340,1342,1345,1347,1350],{"class":72,"line":159},[70,1341,469],{"class":113},[70,1343,1344],{"class":117}," ids",[70,1346,168],{"class":113},[70,1348,1349],{"class":124}," pathIds.data;  ",[70,1351,1352],{"class":382},"\u002F\u002F Int32Array, indices into rings.points\n",[70,1354,1355],{"class":72,"line":552},[70,1356,623],{"class":124},[14,1358,1359],{},[51,1360],{"alt":1361,"src":1362},"Working with results on the Stanford bunny: pink rings at distances 0.25, 0.5 and 1.0, and the patch within 0.5 of the seeds cut out with isobands in orange","\u002Fimg\u002Ftutorials\u002Fsteps\u002Ffast-geodesic-distances-4-patch.png",[14,1364,1365,1366,1369],{},"The rings do not come back in level order. If you need to know which ring is which, call ",[32,1367,1368],{},"tf.isocontours"," once per level.",[14,1371,1372],{},"Cut out the patch within a distance as its own mesh with isobands:",[61,1374,1376],{"className":104,"code":1375,"language":106,"meta":66,"style":66},"const { mesh: patch, faceLabels } = tf.isobands(\n  mesh, d, [0.5], { selectedBands: [0] });\n\nconst objBytes = tf.writeObj(patch).data;  \u002F\u002F Int8Array, ready to save or download\n",[32,1377,1378,1406,1420,1424],{"__ignoreMap":66},[70,1379,1380,1382,1384,1386,1388,1391,1393,1396,1398,1400,1402,1404],{"class":72,"line":73},[70,1381,162],{"class":113},[70,1383,658],{"class":124},[70,1385,910],{"class":661},[70,1387,665],{"class":124},[70,1389,1390],{"class":117},"patch",[70,1392,292],{"class":124},[70,1394,1395],{"class":117},"faceLabels",[70,1397,671],{"class":124},[70,1399,428],{"class":113},[70,1401,171],{"class":124},[70,1403,924],{"class":76},[70,1405,720],{"class":124},[70,1407,1408,1411,1413,1416,1418],{"class":72,"line":137},[70,1409,1410],{"class":124},"  mesh, d, [",[70,1412,1067],{"class":117},[70,1414,1415],{"class":124},"], { selectedBands: [",[70,1417,351],{"class":117},[70,1419,937],{"class":124},[70,1421,1422],{"class":72,"line":152},[70,1423,156],{"emptyLinePlaceholder":155},[70,1425,1426,1428,1431,1433,1435,1438,1441],{"class":72,"line":159},[70,1427,162],{"class":113},[70,1429,1430],{"class":117}," objBytes",[70,1432,168],{"class":113},[70,1434,171],{"class":124},[70,1436,1437],{"class":76},"writeObj",[70,1439,1440],{"class":124},"(patch).data;  ",[70,1442,1443],{"class":382},"\u002F\u002F Int8Array, ready to save or download\n",[14,1445,1446,1447,1449],{},"Band 0 is everything below 0.5. The patch boundary is cut exactly at the 0.5 ring. ",[32,1448,1395],{}," maps each patch face back to its source face.",[14,1451,1452,1453,22,1457,1461],{},"The field also feeds the ",[18,1454,1456],{"href":1455},"\u002Ftutorials\u002Fprincipal-curvatures-of-a-mesh-in-javascript","curvature",[18,1458,1460],{"href":1459},"\u002Ftutorials\u002Ffast-mesh-booleans-in-javascript","boolean"," tools in the same library.",[56,1463,1465],{"id":1464},"about-other-methods","About other methods",[14,1467,1468,1469,1471],{},"People search for the heat method, fast marching, or exact MMP geodesics. They all answer the same question: distance along the surface from seeds. ",[32,1470,38],{}," runs a label-correcting front with a triangle-unfolding update. It is not the heat method.",[56,1473,1475],{"id":1474},"related-tutorials","Related tutorials",[1477,1478,1479,1486,1492,1499],"ul",{},[1480,1481,1482,1485],"li",{},[18,1483,1484],{"href":1284},"Fast and Robust Isocontours on a Triangle Mesh in JavaScript"," — curves and bands of equal value on a scalar field.",[1480,1487,1488,1491],{},[18,1489,1490],{"href":1455},"Fast and Robust Principal Curvatures of a Mesh in JavaScript"," — curvature values and directions at every vertex.",[1480,1493,1494,1498],{},[18,1495,1497],{"href":1496},"\u002Ftutorials\u002Fmesh-slicing-and-cross-sections-in-javascript","Fast and Robust Mesh Slicing and Cross-Sections in JavaScript"," — plane sections, layers and slabs of a mesh.",[1480,1500,1501,1505],{},[18,1502,1504],{"href":1503},"\u002Ftutorials\u002Ffast-closest-point-on-a-mesh-in-javascript","Fast and Robust Closest Point on a Mesh in JavaScript"," — nearest surface point for a batch of queries.",[56,1507,1509],{"id":1508},"performance","Performance",[14,1511,1512,1513,1515],{},"We measured the 28 Thingi10K meshes of the Python benchmark, 10K to 3.2M faces, and the dragon at five sizes, from one seed and from 32 seeds. There is no peer library in JavaScript: npm has no maintained geodesic-distance package for meshes, so ",[32,1514,34],{}," is measured alone. Building the mesh's topology is inside the timer.",[1517,1518],"headline-numbers",{":items":1519},"[{\"value\":\"99.0ms\",\"label\":\"Geodesic distances\",\"detail\":\"1,025,766 faces, one seed\"},{\"value\":\"196ms\",\"label\":\"Largest mesh\",\"detail\":\"3,154,110 faces, one seed\"},{\"value\":\"16.1ms\",\"label\":\"Geometric mean\",\"detail\":\"28 meshes, 10K–3.2M faces, one seed\"}]",[14,1521,1522,1527,1528,1527,1532],{},[70,1523,1526],{"className":1524},[1525],"tag-pill","Apple M4 Max"," ",[70,1529,1531],{"className":1530},[1525],"16 threads",[70,1533,1535],{"className":1534},[1525],"Node.js 25 · WebAssembly",[1537,1538],"data-table",{":headers":1539,":highlight":351,":rows":1540},"[\"Mesh\",\"trueform, one seed\",\"trueform, 32 seeds\"]","[[\"Dragon, 58,298 faces\",\"5.6 ms\",\"3.6 ms\"],[\"Dragon, 141,966 faces\",\"17.0 ms\",\"7.1 ms\"],[\"Dragon, 244,026 faces\",\"28.6 ms\",\"13.3 ms\"],[\"Dragon, 480,868 faces\",\"49.5 ms\",\"21.9 ms\"],[\"Dragon, 1,025,766 faces\",\"99.0 ms\",\"46.1 ms\"],[\"10 meshes, 10K–99K faces (geometric mean)\",\"3.6 ms\",\"2.3 ms\"],[\"10 meshes, 100K–939K faces (geometric mean)\",\"15.2 ms\",\"11.0 ms\"],[\"8 meshes, 1.0M–3.2M faces (geometric mean)\",\"114 ms\",\"77.0 ms\"],[\"All 28 meshes (geometric mean)\",\"16.1 ms\",\"11.0 ms\"]]",[14,1542,1543,1544,27],{},"Median of 9 runs after 2 warm-ups, in one Node.js process. The distances carry the same error against exact MMP geodesics as the native build on the ",[18,1545,1546],{"href":20},"Python page",[14,1548,1549],{},"Measured 2026-10-31 with trueform 0.10.8; harness and raw results: benchmarks\u002Ftutorials\u002Fjavascript-geodesic-distances.",[14,1551,1552,1556,1557],{},[18,1553,1555],{"href":1554},"\u002Ftrueform\u002Fbenchmarks","Full benchmarks"," · ",[18,1558,1561],{"href":1559,"rel":1560},"https:\u002F\u002Ftrueform.polydera.com\u002Fts\u002Fmodules\u002Fgeometry#geodesic-distances",[96],"Documentation",[1563,1564],"cite-as",{"author":1565,"title":5},"Sajovic, {\\v{Z}}iga",[1567,1568,1569],"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":66,"searchDepth":137,"depth":137,"links":1571},[1572,1573,1574,1575,1576,1577,1578,1579,1580,1581,1582],{"id":58,"depth":137,"text":59},{"id":100,"depth":137,"text":101},{"id":306,"depth":137,"text":307},{"id":744,"depth":137,"text":745},{"id":1035,"depth":137,"text":1036},{"id":1150,"depth":137,"text":1151},{"id":1231,"depth":137,"text":1232},{"id":1264,"depth":137,"text":1265},{"id":1464,"depth":137,"text":1465},{"id":1474,"depth":137,"text":1475},{"id":1508,"depth":137,"text":1509},"2026-10-31","Compute geodesic distances on a triangle mesh in JavaScript and TypeScript. Distance along the surface from one seed or many, bounded by a radius, in float32 or float64. WebAssembly, runs in Node.js and the browser.","md",[1587,1588,1589,1590,1591,1592,1593,1594,1595,1596],"geodesic distance JavaScript","geodesic distance TypeScript","geodesic distance mesh browser","surface distance triangle mesh JavaScript","WebAssembly geodesic distance","three.js geodesic distance","heat method geodesic JavaScript","npm geodesic distance","MeshLib geodesic alternative","browser mesh distance field",{},"\u002Fimg\u002Fog\u002Ftutorials\u002Ffast-geodesic-distances-in-javascript.png","\u002Ftutorials\u002Ffast-geodesic-distances-in-javascript",{"title":5,"description":1584},"tutorials\u002Ffast-geodesic-distances-in-javascript",[1603,106,1604,1605,1606],"javascript","geodesics","tutorial","webassembly","zeHrWsy4k17iww7HgPUZjqT2-AOooZFX9riYgPVNlU4",1793517812008]