[{"data":1,"prerenderedAt":1047},["ShallowReactive",2],{"algo-geodesic-distances":3},{"id":4,"title":5,"author":6,"body":7,"date":1021,"description":1022,"extension":1023,"hub":219,"keywords":1024,"meta":1034,"navigation":219,"ogImage":1035,"path":1036,"published":219,"seo":1037,"stem":1040,"tags":1041,"updated":1045,"__hash__":1046},"algorithms\u002Falgorithms\u002Fgeodesic-distances.md","Geodesic distances","Žiga Sajovic",{"type":8,"value":9,"toc":1012},"minimark",[10,26,30,39,44,47,50,54,57,61,68,71,74,80,86,92,98,101,105,108,111,118,121,124,127,132,135,139,147,151,154,160,166,169,175,181,185,188,654,669,673,676,679,685,845,848,855,927,930,934,951,969,989,1000,1004,1008],[11,12,13,14,19,20,25],"p",{},"A geodesic distance is the length of the shortest path that stays on the surface. Every method that computes it on a mesh buys speed with error. Dijkstra on the edges is fast and runs long. The heat method is smooth and smears. Exact MMP is right and takes minutes. The useful question is not which method is fastest, but how far from exact each one lands for the time it takes. This page answers it with measurements against an exact reference, then shows how ",[15,16,18],"a",{"href":17},"\u002Ftrueform","trueform"," gets its place on that chart. Drop seeds on a dragon in the ",[15,21,24],{"href":22,"external":23},"\u002Ftrueform\u002Flive\u002Fgeodesic-rings","","live example",".",[27,28],"hub-language-cards",{":items":29},"[{\"lang\":\"C++\",\"icon\":\"i-simple-icons-cplusplus\",\"title\":\"Fast and Accurate Geodesic Distances in C++\",\"to\":\"\u002Ftutorials\u002Ffast-geodesic-distances-in-cpp\",\"detail\":\"Header-only. Views over your own buffers, float or double results.\"},{\"lang\":\"Python\",\"icon\":\"i-simple-icons-python\",\"title\":\"Fast and Accurate Geodesic Distances in Python\",\"to\":\"\u002Ftutorials\u002Ffast-geodesic-distances-in-python\",\"detail\":\"One pip install. NumPy arrays in, a NumPy array out.\"},{\"lang\":\"JavaScript\",\"icon\":\"i-simple-icons-typescript\",\"title\":\"Fast and Accurate Geodesic Distances in JavaScript\",\"to\":\"\u002Ftutorials\u002Ffast-geodesic-distances-in-javascript\",\"detail\":\"WebAssembly. The same call in Node.js and the browser.\"}]",[11,31,32],{},[33,34],"img",{"alt":35,"height":36,"src":37,"width":38},"Geodesic distance from three seeds on the Stanford dragon, teal near a seed to orange far from every seed, with pink rings of equal distance",800,"\u002Fimg\u002Falgorithms\u002Fhubs\u002Fgeodesics-hero.webp",1200,[40,41,43],"h2",{"id":42},"along-the-surface-not-through-it","Along the surface, not through it",[11,45,46],{},"Measured from a set of seeds, it gives one number per point: zero at a seed, growing as you walk away, and always to the nearest seed.",[11,48,49],{},"The difference from straight-line distance is easy to see. Both pictures below start from the dragon's snout. On the left, the rings are straight-line distance. They slice through the body like parallel planes, and the front claw counts as close because it sits under the jaw. On the right, the rings are geodesic. They wrap around the neck and run down the body, and the claw is far, because the only way there is the long way round.",[51,52],"image-grid",{":images":53},"[{\"src\":\"\u002Fimg\u002Falgorithms\u002Fhubs\u002Fgeodesics-euclidean.webp\",\"width\":1200,\"height\":800,\"alt\":\"Straight-line distance from the snout of the Stanford dragon: rings slice through the body as parallel bands and the front claw reads as near\",\"label\":\"Straight line\",\"detail\":\"rings cut through the body\"},{\"src\":\"\u002Fimg\u002Falgorithms\u002Fhubs\u002Fgeodesics-surface.webp\",\"width\":1200,\"height\":800,\"alt\":\"Geodesic distance from the snout of the Stanford dragon: rings wrap the neck and body, and the front claw reads as far\",\"label\":\"Geodesic\",\"detail\":\"rings follow the surface\"}]",[11,55,56],{},"People search for this as surface distance, distance along a mesh or a geodesic distance field. They also search by method: the heat method, fast marching, Dijkstra on a mesh, exact MMP. All of these answer the same question. They differ in how wrong the answer is and how long it takes.",[40,58,60],{"id":59},"the-methods-and-what-each-gets-wrong","The methods, and what each gets wrong",[11,62,63,67],{},[64,65,66],"strong",{},"Dijkstra on the edge graph."," The shortest path through mesh edges. It is simple and robust, but a path on a mesh does not follow edges. It cuts across triangles. On a flat grid, Dijkstra measures a zigzag instead of a straight line, so it always overestimates. VTK's geodesic field is this kind of method.",[11,69,70],{},"The flat case shows the error plainly, because there the true answer is the straight line. Both pictures are the same synthetic flat grid and the same seed. The edge path draws its rings as polygons whose corners follow the grid's edge directions. Across the diagonals it runs 41 % long, and its median point is 7.7 % long. trueform's rings are circles: on this grid every distance matches the straight line to rounding.",[51,72],{":images":73},"[{\"src\":\"\u002Fimg\u002Falgorithms\u002Fhubs\u002Fgeodesics-flat-edges.webp\",\"width\":1200,\"height\":800,\"alt\":\"Distance from one seed on a synthetic flat triangle grid along mesh edges: the rings are hexagon-like polygons aligned with the grid\",\"label\":\"Edge path (Dijkstra)\",\"detail\":\"rings follow the grid\"},{\"src\":\"\u002Fimg\u002Falgorithms\u002Fhubs\u002Fgeodesics-flat-trueform.webp\",\"width\":1200,\"height\":800,\"alt\":\"Distance from the same seed on the same synthetic flat grid with trueform: the rings are circles\",\"label\":\"trueform\",\"detail\":\"rings are circles\"}]",[11,75,76,79],{},[64,77,78],{},"The heat method."," Diffuse heat from the seeds for a short time, normalise the gradient, and solve a Poisson equation for the distance. Two sparse linear solves. The result is smooth, but it is smoothed: distances blur near the seeds and over fine detail, and how much depends on the time step. libigl, CGAL and potpourri3d ship it.",[11,81,82,85],{},[64,83,84],{},"Approximate fronts."," Fast marching and its relatives sweep a front out from the seeds and update each point from the triangle it lies in. They are fast and close, but the error depends on the update rule and on the triangles. MeshLib's surface distance is in this family.",[11,87,88,91],{},[64,89,90],{},"Exact window propagation (MMP)."," Track every interval of an edge that a shortest path can cross, and unfold it across the faces. The answer is exact up to floating point. It is the reference other methods are measured against. It is also slow: the exact answer for one mesh of up to 1.3M faces took up to 14 minutes.",[93,94],"data-table",{":headers":95,":highlight":96,":rows":97},"[\"Method\",\"Measured median error\",\"Where the time goes\"]","0","[[\"Dijkstra on edges (VTK)\",\"9.3 %\",\"One graph search; 32 seeds mean 32 runs\"],[\"Heat method (libigl, CGAL)\",\"13–36 %\",\"A sparse factorization, then two solves\"],[\"Approximate front (MeshLib)\",\"2.3–2.5 %\",\"One front\"],[\"Approximate (PyMeshLab)\",\"0.61 %\",\"About 20× trueform's time\"],[\"trueform\",\"0.57–0.61 %, 0.2 % from 32 seeds\",\"One front\"],[\"Exact MMP\",\"0 (the reference)\",\"Minutes per mesh\"]]",[11,99,100],{},"Median relative error per point against exact MMP, averaged over the meshes. The heat-method figures are from the C++ run, the rest from the Python run.",[40,102,104],{"id":103},"error-against-time","Error against time",[11,106,107],{},"A time without an error says nothing. A method that returns zeros is fast. So we measure both, on the same meshes and the same seeds.",[11,109,110],{},"The corpus is 28 closed meshes from Thingi10K, from 10K to 3.2M faces. Each library runs from the same seeds, one and 32. The reference is exact MMP, on every mesh small enough to compute it. Apple M4 Max, 16 threads, trueform 0.10.8.",[11,112,113],{},[33,114],{"alt":115,"height":36,"src":116,"width":38,"loading":117},"Median error against exact MMP geodesics versus geometric-mean time, in Python, one seed and 32 seeds: trueform at 5.8 ms and 0.61 %, PyMeshLab at the same error and 116 ms, MeshLib at 35 ms and 2.5 %, VTK at 29 ms and 9.3 %; from 32 seeds trueform at 4.1 ms and 0.20 %","\u002Fimg\u002Falgorithms\u002Fhubs\u002Fgeodesics-error-vs-time.webp","lazy",[11,119,120],{},"Lower left is better. In Python, on the 22 meshes where every library finished and an exact reference exists, trueform sits at 0.61 % median error in 5.8 ms. MeshLib is six times slower at four times the error. VTK's Dijkstra is five times slower at fifteen times the error, and from 32 seeds it has to run once per seed. PyMeshLab is the only peer that matches the error. It takes about twenty times as long.",[11,122,123],{},"Two things in the chart are worth reading twice. First, 32 seeds are faster than one. Each seed's front only has to reach the points nearer to it than to any other seed, so the fronts are short. Second, the error drops to 0.2 % from 32 seeds, because the paths are shorter and each crosses fewer faces.",[11,125,126],{},"The same story holds in every language. Times are geometric means over all 28 meshes.",[93,128],{":headers":129,":highlight":130,":rows":131},"[\"Language\",\"trueform, one seed \u002F 32 seeds\",\"Compared against\",\"Median error\"]","1","[[\"C++\",\"8.1 ms \u002F 6.0 ms\",\"MeshLib 4.4× and 6.5× slower; libigl and CGAL heat 47–99× slower below 1M faces\",\"0.57 %, MeshLib 2.3 %, heat 13–36 %\"],[\"Python\",\"8.9 ms \u002F 6.7 ms\",\"MeshLib 6.5× and 9.2× slower; PyMeshLab same error at 21× the time\",\"0.61 %, MeshLib 2.5 %, VTK 9.3 %\"],[\"JavaScript (WebAssembly)\",\"16.1 ms \u002F –\",\"No maintained npm package to compare\",\"the same algorithm\"]]",[11,133,134],{},"On the largest mesh, 3,154,110 faces, one seed takes 135 ms in C++, 119 ms in Python and 196 ms in WebAssembly.",[136,137],"hub-benchmark-figures",{"category":138},"geodesic-distances",[11,140,141,142,146],{},"Every trueform benchmark, by module and language, is on the ",[15,143,145],{"href":144},"\u002Ftrueform\u002Fbenchmarks","trueform benchmarks"," page.",[40,148,150],{"id":149},"how-trueform-lands-near-exact","How trueform lands near exact",[11,152,153],{},"trueform runs a label-correcting front with a triangle-unfolding update. It is not the heat method, and it is not exact. Here is the idea.",[11,155,156,159],{},[64,157,158],{},"Paths cross faces."," The points around each seed start at their straight-line distance. From there, a point is reached across a triangle, not along an edge: the two known corners of the triangle are unfolded into its plane, a virtual source is placed where the two distances meet, and the third corner reads its distance to that source. Where the source falls outside the triangle's angle at that corner, the path runs along an edge instead. On a flat mesh the result is the straight line. On a curved one it is shorter than any edge path.",[11,161,162],{},[33,163],{"alt":164,"height":36,"src":165,"width":38,"loading":117},"Diagram of the unfolding update: corners A and B of a triangle carry distances; the virtual source S stands at both distances on the far side of edge AB; corner C reads its distance on the straight pink line from S, which crosses the face; the dotted orange edge path through A is longer","\u002Fimg\u002Falgorithms\u002Fhubs\u002Fgeodesics-unfolding.webp",[11,167,168],{},"The update is local and cheap: a few lengths and one square root per triangle. Each point keeps only its distance, and each triangle rebuilds a source from two of them, so small errors add up over the faces a path crosses. That is the 0.57–0.61 % median error measured above, and 0.2 % from 32 seeds, where paths are shorter. Exact MMP keeps every window of an edge that a shortest path can cross, which is where its minutes go.",[11,170,171,174],{},[64,172,173],{},"Points are corrected."," A label-correcting front does not settle a point once. If a shorter path arrives later, the point is lowered and its neighbours are updated again.",[11,176,177,180],{},[64,178,179],{},"Clear values."," A seed is zero. A point no front reaches, on another piece of the mesh or past a given radius, is infinity, not a large number. Distances use the stored coordinates; a transformation set on the mesh does not enter them. The result comes back in the mesh's type, float or double.",[40,182,184],{"id":183},"one-seed-many-seeds-a-radius","One seed, many seeds, a radius",[11,186,187],{},"The call takes point ids. Pass one id for a field from a point. Pass many and each point gets the distance to its nearest seed. Pass a radius and the front stops there, which turns a whole-mesh solve into a local one.",[189,190,191,372,483],"code-group",{},[192,193,198],"pre",{"className":194,"code":195,"filename":196,"language":197,"meta":23,"style":23},"language-cpp shiki shiki-themes github-dark","#include \u003Ctrueform\u002Ftrueform.hpp>\n\nauto mesh_buffer = tf::read_stl(\"part.stl\");\nauto mesh = mesh_buffer.polygons();\n\nstd::vector\u003Cint> seeds{0, 120, 4711};\nauto d = tf::make_geodesic_distances(mesh, seeds);\n\u002F\u002F d: tf::buffer\u003Cfloat>, one distance per point, to the nearest seed\n\nauto near = tf::make_geodesic_distances(mesh, seeds, 0.5f);  \u002F\u002F within a radius\n","C++","cpp",[199,200,201,214,221,253,272,277,309,329,336,341],"code",{"__ignoreMap":23},[202,203,206,210],"span",{"class":204,"line":205},"line",1,[202,207,209],{"class":208},"snl16","#include",[202,211,213],{"class":212},"sU2Wk"," \u003Ctrueform\u002Ftrueform.hpp>\n",[202,215,217],{"class":204,"line":216},2,[202,218,220],{"emptyLinePlaceholder":219},true,"\n",[202,222,224,227,231,234,238,241,244,247,250],{"class":204,"line":223},3,[202,225,226],{"class":208},"auto",[202,228,230],{"class":229},"s95oV"," mesh_buffer ",[202,232,233],{"class":208},"=",[202,235,237],{"class":236},"svObZ"," tf",[202,239,240],{"class":229},"::",[202,242,243],{"class":236},"read_stl",[202,245,246],{"class":229},"(",[202,248,249],{"class":212},"\"part.stl\"",[202,251,252],{"class":229},");\n",[202,254,256,258,261,263,266,269],{"class":204,"line":255},4,[202,257,226],{"class":208},[202,259,260],{"class":229}," mesh ",[202,262,233],{"class":208},[202,264,265],{"class":229}," mesh_buffer.",[202,267,268],{"class":236},"polygons",[202,270,271],{"class":229},"();\n",[202,273,275],{"class":204,"line":274},5,[202,276,220],{"emptyLinePlaceholder":219},[202,278,280,283,286,289,292,295,298,301,303,306],{"class":204,"line":279},6,[202,281,282],{"class":236},"std",[202,284,285],{"class":229},"::vector",[202,287,288],{"class":208},"\u003Cint>",[202,290,291],{"class":229}," seeds{",[202,293,96],{"class":294},"sDLfK",[202,296,297],{"class":229},", ",[202,299,300],{"class":294},"120",[202,302,297],{"class":229},[202,304,305],{"class":294},"4711",[202,307,308],{"class":229},"};\n",[202,310,312,314,317,319,321,323,326],{"class":204,"line":311},7,[202,313,226],{"class":208},[202,315,316],{"class":229}," d ",[202,318,233],{"class":208},[202,320,237],{"class":236},[202,322,240],{"class":229},[202,324,325],{"class":236},"make_geodesic_distances",[202,327,328],{"class":229},"(mesh, seeds);\n",[202,330,332],{"class":204,"line":331},8,[202,333,335],{"class":334},"sAwPA","\u002F\u002F d: tf::buffer\u003Cfloat>, one distance per point, to the nearest seed\n",[202,337,339],{"class":204,"line":338},9,[202,340,220],{"emptyLinePlaceholder":219},[202,342,344,346,349,351,353,355,357,360,363,366,369],{"class":204,"line":343},10,[202,345,226],{"class":208},[202,347,348],{"class":229}," near ",[202,350,233],{"class":208},[202,352,237],{"class":236},[202,354,240],{"class":229},[202,356,325],{"class":236},[202,358,359],{"class":229},"(mesh, seeds, ",[202,361,362],{"class":294},"0.5",[202,364,365],{"class":208},"f",[202,367,368],{"class":229},");",[202,370,371],{"class":334},"  \u002F\u002F within a radius\n",[192,373,378],{"className":374,"code":375,"filename":376,"language":377,"meta":23,"style":23},"language-python shiki shiki-themes github-dark","import trueform as tf\n\nmesh = tf.Mesh(*tf.read_stl(\"part.stl\"))\n\nd = tf.geodesic_distances(mesh, [0, 120, 4711])\n# d: np.ndarray, shape (num_points,), the mesh's dtype\n\nnear = tf.geodesic_distances(mesh, [0], radius=0.5)  # inf past the radius\n","Python","python",[199,379,380,394,398,419,423,446,451,455],{"__ignoreMap":23},[202,381,382,385,388,391],{"class":204,"line":205},[202,383,384],{"class":208},"import",[202,386,387],{"class":229}," trueform ",[202,389,390],{"class":208},"as",[202,392,393],{"class":229}," tf\n",[202,395,396],{"class":204,"line":216},[202,397,220],{"emptyLinePlaceholder":219},[202,399,400,403,405,408,411,414,416],{"class":204,"line":223},[202,401,402],{"class":229},"mesh ",[202,404,233],{"class":208},[202,406,407],{"class":229}," tf.Mesh(",[202,409,410],{"class":208},"*",[202,412,413],{"class":229},"tf.read_stl(",[202,415,249],{"class":212},[202,417,418],{"class":229},"))\n",[202,420,421],{"class":204,"line":255},[202,422,220],{"emptyLinePlaceholder":219},[202,424,425,428,430,433,435,437,439,441,443],{"class":204,"line":274},[202,426,427],{"class":229},"d ",[202,429,233],{"class":208},[202,431,432],{"class":229}," tf.geodesic_distances(mesh, [",[202,434,96],{"class":294},[202,436,297],{"class":229},[202,438,300],{"class":294},[202,440,297],{"class":229},[202,442,305],{"class":294},[202,444,445],{"class":229},"])\n",[202,447,448],{"class":204,"line":279},[202,449,450],{"class":334},"# d: np.ndarray, shape (num_points,), the mesh's dtype\n",[202,452,453],{"class":204,"line":311},[202,454,220],{"emptyLinePlaceholder":219},[202,456,457,460,462,464,466,469,473,475,477,480],{"class":204,"line":331},[202,458,459],{"class":229},"near ",[202,461,233],{"class":208},[202,463,432],{"class":229},[202,465,96],{"class":294},[202,467,468],{"class":229},"], ",[202,470,472],{"class":471},"s9osk","radius",[202,474,233],{"class":208},[202,476,362],{"class":294},[202,478,479],{"class":229},")  ",[202,481,482],{"class":334},"# inf past the radius\n",[192,484,489],{"className":485,"code":486,"filename":487,"language":488,"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(\"part.stl\"));\n\nconst seeds = tf.ndarray(new Int32Array([0, 120, 4711]), [3]);\nconst d = tf.geodesicDistances(mesh, seeds);\n\u002F\u002F d: NDArray, shape [numberOfPoints]\n\nconst near = tf.geodesicDistances(mesh, seeds, { radius: 0.5 });\n","JavaScript","ts",[199,490,491,513,527,531,560,564,608,624,629,633],{"__ignoreMap":23},[202,492,493,495,498,501,504,507,510],{"class":204,"line":205},[202,494,384],{"class":208},[202,496,497],{"class":294}," *",[202,499,500],{"class":208}," as",[202,502,503],{"class":229}," tf ",[202,505,506],{"class":208},"from",[202,508,509],{"class":212}," \"@polydera\u002Ftrueform\"",[202,511,512],{"class":229},";\n",[202,514,515,517,520,522,525],{"class":204,"line":216},[202,516,384],{"class":208},[202,518,519],{"class":229}," { readFileSync } ",[202,521,506],{"class":208},[202,523,524],{"class":212}," \"node:fs\"",[202,526,512],{"class":229},[202,528,529],{"class":204,"line":223},[202,530,220],{"emptyLinePlaceholder":219},[202,532,533,536,539,542,545,548,550,553,555,557],{"class":204,"line":255},[202,534,535],{"class":208},"const",[202,537,538],{"class":294}," mesh",[202,540,541],{"class":208}," =",[202,543,544],{"class":229}," tf.",[202,546,547],{"class":236},"readStl",[202,549,246],{"class":229},[202,551,552],{"class":236},"readFileSync",[202,554,246],{"class":229},[202,556,249],{"class":212},[202,558,559],{"class":229},"));\n",[202,561,562],{"class":204,"line":274},[202,563,220],{"emptyLinePlaceholder":219},[202,565,566,568,571,573,575,578,580,583,586,589,591,593,595,597,599,602,605],{"class":204,"line":279},[202,567,535],{"class":208},[202,569,570],{"class":294}," seeds",[202,572,541],{"class":208},[202,574,544],{"class":229},[202,576,577],{"class":236},"ndarray",[202,579,246],{"class":229},[202,581,582],{"class":208},"new",[202,584,585],{"class":236}," Int32Array",[202,587,588],{"class":229},"([",[202,590,96],{"class":294},[202,592,297],{"class":229},[202,594,300],{"class":294},[202,596,297],{"class":229},[202,598,305],{"class":294},[202,600,601],{"class":229},"]), [",[202,603,604],{"class":294},"3",[202,606,607],{"class":229},"]);\n",[202,609,610,612,615,617,619,622],{"class":204,"line":311},[202,611,535],{"class":208},[202,613,614],{"class":294}," d",[202,616,541],{"class":208},[202,618,544],{"class":229},[202,620,621],{"class":236},"geodesicDistances",[202,623,328],{"class":229},[202,625,626],{"class":204,"line":331},[202,627,628],{"class":334},"\u002F\u002F d: NDArray, shape [numberOfPoints]\n",[202,630,631],{"class":204,"line":338},[202,632,220],{"emptyLinePlaceholder":219},[202,634,635,637,640,642,644,646,649,651],{"class":204,"line":343},[202,636,535],{"class":208},[202,638,639],{"class":294}," near",[202,641,541],{"class":208},[202,643,544],{"class":229},[202,645,621],{"class":236},[202,647,648],{"class":229},"(mesh, seeds, { radius: ",[202,650,362],{"class":294},[202,652,653],{"class":229}," });\n",[11,655,656,657,661,662,297,665,668],{},"When a seed is a position, such as a click, a probe or a landmark, snap it to the nearest mesh point first with a ",[15,658,660],{"href":659},"\u002Falgorithms\u002Fclosest-point-on-a-mesh","closest point query",". When a seed is a curve, pass all of its points. Install with ",[199,663,664],{},"pip install trueform",[199,666,667],{},"npm install @polydera\u002Ftrueform",", or CMake, conan or nuget for C++. Geodesic distances need version 0.10.8 or later.",[40,670,672],{"id":671},"what-the-field-is-for","What the field is for",[11,674,675],{},"A distance field is an input. Rings of equal distance are offsets on a curved surface: a margin around a trim line, a falloff from a seam, an allowance along a cut. The patch within a distance is a region to select, paint or remove. The point farthest from every seed is the spot the seeds cover least.",[11,677,678],{},"That last fact builds a sampler. Take the farthest point, add it to the seeds, and run again. Each call is one multi-seed field. Forty rounds spread forty samples evenly over the surface, by distance along it. Run one field per sample and take the nearest, and the surface splits into regions, one per sample.",[11,680,681],{},[33,682],{"alt":683,"height":36,"src":684,"width":38,"loading":117},"Forty farthest-point samples on the Stanford dragon, marked in pink, and the surface split into regions by nearest sample along the surface, in house colours","\u002Fimg\u002Falgorithms\u002Fhubs\u002Fgeodesics-sampling.webp",[192,686,688],{"className":374,"code":687,"language":377,"meta":23,"style":23},"import numpy as np\nimport trueform as tf\n\nmesh = tf.Mesh(*tf.read_stl(\"part.stl\"))\nseeds = [0]\nfor _ in range(39):\n    d = tf.geodesic_distances(mesh, seeds)\n    seeds.append(int(np.argmax(np.where(np.isfinite(d), d, -1))))\n\nfields = np.stack([tf.geodesic_distances(mesh, [s]) for s in seeds])\nregion = fields.argmin(axis=0)  # nearest sample, per point\n",[199,689,690,702,712,716,732,747,769,779,798,802,822],{"__ignoreMap":23},[202,691,692,694,697,699],{"class":204,"line":205},[202,693,384],{"class":208},[202,695,696],{"class":229}," numpy ",[202,698,390],{"class":208},[202,700,701],{"class":229}," np\n",[202,703,704,706,708,710],{"class":204,"line":216},[202,705,384],{"class":208},[202,707,387],{"class":229},[202,709,390],{"class":208},[202,711,393],{"class":229},[202,713,714],{"class":204,"line":223},[202,715,220],{"emptyLinePlaceholder":219},[202,717,718,720,722,724,726,728,730],{"class":204,"line":255},[202,719,402],{"class":229},[202,721,233],{"class":208},[202,723,407],{"class":229},[202,725,410],{"class":208},[202,727,413],{"class":229},[202,729,249],{"class":212},[202,731,418],{"class":229},[202,733,734,737,739,742,744],{"class":204,"line":274},[202,735,736],{"class":229},"seeds ",[202,738,233],{"class":208},[202,740,741],{"class":229}," [",[202,743,96],{"class":294},[202,745,746],{"class":229},"]\n",[202,748,749,752,755,758,761,763,766],{"class":204,"line":279},[202,750,751],{"class":208},"for",[202,753,754],{"class":229}," _ ",[202,756,757],{"class":208},"in",[202,759,760],{"class":294}," range",[202,762,246],{"class":229},[202,764,765],{"class":294},"39",[202,767,768],{"class":229},"):\n",[202,770,771,774,776],{"class":204,"line":311},[202,772,773],{"class":229},"    d ",[202,775,233],{"class":208},[202,777,778],{"class":229}," tf.geodesic_distances(mesh, seeds)\n",[202,780,781,784,787,790,793,795],{"class":204,"line":331},[202,782,783],{"class":229},"    seeds.append(",[202,785,786],{"class":294},"int",[202,788,789],{"class":229},"(np.argmax(np.where(np.isfinite(d), d, ",[202,791,792],{"class":208},"-",[202,794,130],{"class":294},[202,796,797],{"class":229},"))))\n",[202,799,800],{"class":204,"line":338},[202,801,220],{"emptyLinePlaceholder":219},[202,803,804,807,809,812,814,817,819],{"class":204,"line":343},[202,805,806],{"class":229},"fields ",[202,808,233],{"class":208},[202,810,811],{"class":229}," np.stack([tf.geodesic_distances(mesh, [s]) ",[202,813,751],{"class":208},[202,815,816],{"class":229}," s ",[202,818,757],{"class":208},[202,820,821],{"class":229}," seeds])\n",[202,823,825,828,830,833,836,838,840,842],{"class":204,"line":824},11,[202,826,827],{"class":229},"region ",[202,829,233],{"class":208},[202,831,832],{"class":229}," fields.argmin(",[202,834,835],{"class":471},"axis",[202,837,233],{"class":208},[202,839,96],{"class":294},[202,841,479],{"class":229},[202,843,844],{"class":334},"# nearest sample, per point\n",[51,846],{":images":847},"[{\"src\":\"\u002Fimg\u002Ftutorials\u002Fsteps\u002Ffast-geodesic-distances-2-seeds.webp\",\"width\":1200,\"height\":734,\"alt\":\"The Stanford bunny cut in half, coloured by geodesic distance from every point on the cut\",\"label\":\"A curve of seeds\",\"detail\":\"rings run parallel to the cut\"},{\"src\":\"\u002Fimg\u002Ftutorials\u002Fsteps\u002Ffast-geodesic-distances-4-patch.webp\",\"width\":1200,\"height\":734,\"alt\":\"Rings at three distances on the bunny and the patch within 0.5 of the seed cut out in orange\",\"label\":\"Rings and patch\",\"detail\":\"isocontours and isobands\"}]",[11,849,850,854],{},[15,851,853],{"href":852},"\u002Falgorithms\u002Fisocontours","Isocontours"," draw the rings at chosen distances. Isobands cut the patch out as its own mesh, with its boundary on the exact ring and each face mapped back to its source face.",[192,856,858],{"className":374,"code":857,"language":377,"meta":23,"style":23},"d = tf.geodesic_distances(mesh, seeds)\n\nrings, ring_points = tf.isocontours(mesh, d, [0.1, 0.2, 0.3])\n(faces, points), _, face_ids = tf.isobands(mesh, d, [0.5], selected_bands=[0])  # within 0.5\n",[199,859,860,868,872,897],{"__ignoreMap":23},[202,861,862,864,866],{"class":204,"line":205},[202,863,427],{"class":229},[202,865,233],{"class":208},[202,867,778],{"class":229},[202,869,870],{"class":204,"line":216},[202,871,220],{"emptyLinePlaceholder":219},[202,873,874,877,879,882,885,887,890,892,895],{"class":204,"line":223},[202,875,876],{"class":229},"rings, ring_points ",[202,878,233],{"class":208},[202,880,881],{"class":229}," tf.isocontours(mesh, d, [",[202,883,884],{"class":294},"0.1",[202,886,297],{"class":229},[202,888,889],{"class":294},"0.2",[202,891,297],{"class":229},[202,893,894],{"class":294},"0.3",[202,896,445],{"class":229},[202,898,899,902,904,907,909,911,914,916,919,921,924],{"class":204,"line":255},[202,900,901],{"class":229},"(faces, points), _, face_ids ",[202,903,233],{"class":208},[202,905,906],{"class":229}," tf.isobands(mesh, d, [",[202,908,362],{"class":294},[202,910,468],{"class":229},[202,912,913],{"class":471},"selected_bands",[202,915,233],{"class":208},[202,917,918],{"class":229},"[",[202,920,96],{"class":294},[202,922,923],{"class":229},"])  ",[202,925,926],{"class":334},"# within 0.5\n",[11,928,929],{},"The tutorials show both in each language.",[40,931,933],{"id":932},"where-to-go-next","Where to go next",[11,935,936,939,940,297,943,946,947,950],{},[64,937,938],{},"Tutorials."," ",[15,941,196],{"href":942},"\u002Ftutorials\u002Ffast-geodesic-distances-in-cpp",[15,944,376],{"href":945},"\u002Ftutorials\u002Ffast-geodesic-distances-in-python"," and ",[15,948,487],{"href":949},"\u002Ftutorials\u002Ffast-geodesic-distances-in-javascript",". Each covers one seed, many seeds, the radius, precision, and rings and patches.",[11,952,953,956,957,960,961,965,966,25],{},[64,954,955],{},"Related operations."," Rings and patches with ",[15,958,959],{"href":852},"isocontours",". The shape of the surface with ",[15,962,964],{"href":963},"\u002Falgorithms\u002Fprincipal-curvatures","principal curvatures",". Positions to seeds with ",[15,967,968],{"href":659},"closest point queries",[11,970,971,974,975,297,980,946,984,25],{},[64,972,973],{},"Documentation."," Geodesic distances in the geometry module, in ",[15,976,196],{"href":977,"rel":978},"https:\u002F\u002Ftrueform.polydera.com\u002Fcpp\u002Fmodules\u002Fgeometry",[979],"nofollow",[15,981,376],{"href":982,"rel":983},"https:\u002F\u002Ftrueform.polydera.com\u002Fpy\u002Fmodules\u002Fgeometry",[979],[15,985,988],{"href":986,"rel":987},"https:\u002F\u002Ftrueform.polydera.com\u002Fts\u002Fmodules\u002Fgeometry",[979],"TypeScript",[11,990,991,992,994,995,999],{},"Geodesics are one module of ",[15,993,18],{"href":17},". The same engine does ",[15,996,998],{"href":997},"\u002Falgorithms\u002Fmesh-booleans","mesh booleans",", spatial queries, topology and remeshing, in C++, Python and the browser.",[1001,1002],"article-cta",{":buttons":1003},"[{\"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\u002Fgeometry\",\"variant\":\"soft\",\"color\":\"neutral\"},{\"label\":\"Try it live\",\"to\":\"\u002Ftrueform\u002Flive\u002Fgeodesic-rings\",\"icon\":\"i-lucide-play\"}]",[1005,1006],"cite-as",{"author":1007,"title":5},"Sajovic, {\\v{Z}}iga",[1009,1010,1011],"style",{},"html pre.shiki code .snl16, html code.shiki .snl16{--shiki-default:#F97583}html pre.shiki code .sU2Wk, html code.shiki .sU2Wk{--shiki-default:#9ECBFF}html pre.shiki code .s95oV, html code.shiki .s95oV{--shiki-default:#E1E4E8}html pre.shiki code .svObZ, html code.shiki .svObZ{--shiki-default:#B392F0}html pre.shiki code .sDLfK, html code.shiki .sDLfK{--shiki-default:#79B8FF}html pre.shiki code .sAwPA, html code.shiki .sAwPA{--shiki-default:#6A737D}html .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 .s9osk, html code.shiki .s9osk{--shiki-default:#FFAB70}",{"title":23,"searchDepth":216,"depth":216,"links":1013},[1014,1015,1016,1017,1018,1019,1020],{"id":42,"depth":216,"text":43},{"id":59,"depth":216,"text":60},{"id":103,"depth":216,"text":104},{"id":149,"depth":216,"text":150},{"id":183,"depth":216,"text":184},{"id":671,"depth":216,"text":672},{"id":932,"depth":216,"text":933},"2026-11-08","Distance along the surface of a mesh, from one seed or many. The methods, what each gets wrong, and how far trueform lands from the exact answer and how fast, in C++, Python and JavaScript.","md",[1025,1026,1027,1028,1029,1030,1031,1032,1033],"geodesic distance","geodesic distance mesh","surface distance triangle mesh","geodesic distance python","geodesic distance c++","heat method alternative","fast marching mesh","MMP geodesic","exact geodesic",{},"\u002Fimg\u002Fog\u002Falgorithms\u002Fgeodesic-distances.png","\u002Falgorithms\u002Fgeodesic-distances",{"title":1038,"description":1039},"Geodesic Distance on a Mesh: Exact vs Fast Methods","Heat method, Dijkstra, fast fronts or exact MMP? Geodesic distance on a triangle mesh, measured for error against exact geodesics and for time.","algorithms\u002Fgeodesic-distances",[1042,1043,1044],"geodesics","geometry","overview","2026-11-12T12:06:28.000Z","-j4OmHrpY5ukqqjsfJSKexLdb6nXqji1Ox_fxkBPmng",1794486339528]