[{"data":1,"prerenderedAt":1097},["ShallowReactive",2],{"algo-closest-point-on-a-mesh":3},{"id":4,"title":5,"author":6,"body":7,"date":1071,"description":1072,"extension":1073,"hub":299,"keywords":1074,"meta":1084,"navigation":299,"ogImage":1085,"path":1086,"published":299,"seo":1087,"stem":1090,"tags":1091,"updated":1095,"__hash__":1096},"algorithms\u002Falgorithms\u002Fclosest-point-on-a-mesh.md","Closest point on a mesh","Žiga Sajovic",{"type":8,"value":9,"toc":1059},"minimark",[10,21,25,34,39,42,48,51,54,77,80,84,87,90,96,99,102,108,111,114,117,150,154,157,161,164,167,171,175,178,181,188,192,195,201,207,210,213,231,235,238,241,244,248,259,335,342,346,349,355,365,371,385,389,413,417,420,968,971,975,990,1000,1010,1030,1047,1051,1055],[11,12,13,14,20],"p",{},"A scan holds two million points. The model holds a million triangles. The question is how far each point lies from the surface, and where on the surface it lands. That is a closest-point query, run two million times. This page is about running it at that scale: what it costs, how to pay the cost once, and how far to trust the answer. Move a probe around a dragon in the ",[15,16,19],"a",{"href":17,"external":18},"\u002Ftrueform\u002Flive\u002Fclosest-points","","live example",".",[22,23],"hub-language-cards",{":items":24},"[{\"lang\":\"C++\",\"icon\":\"i-simple-icons-cplusplus\",\"title\":\"Fast and Robust Closest Point on a Mesh in C++\",\"to\":\"\u002Ftutorials\u002Ffast-closest-point-on-a-mesh-in-cpp\",\"detail\":\"Header-only. A tree tagged onto your own mesh view, queries in your own parallel loop.\"},{\"lang\":\"Python\",\"icon\":\"i-simple-icons-python\",\"title\":\"Fast and Robust Closest Point on a Mesh in Python\",\"to\":\"\u002Ftutorials\u002Ffast-closest-point-on-a-mesh-in-python\",\"detail\":\"One pip install. An (N, 3) array in; ids, distances and points out.\"},{\"lang\":\"JavaScript\",\"icon\":\"i-simple-icons-typescript\",\"title\":\"Fast and Robust Closest Point on a Mesh in JavaScript\",\"to\":\"\u002Ftutorials\u002Ffast-closest-point-on-a-mesh-in-javascript\",\"detail\":\"WebAssembly. The same batch query in Node.js and the browser.\"}]",[11,26,27],{},[28,29],"img",{"alt":30,"height":31,"src":32,"width":33},"A grid of query points in front of the Stanford dragon, each joined to its closest point on the surface; the lines fan into the nearest features and are coloured by distance, teal near to orange far",800,"\u002Fimg\u002Falgorithms\u002Fhubs\u002Fclosest-point-hero.webp",1200,[35,36,38],"h2",{"id":37},"one-query-three-answers","One query, three answers",[11,40,41],{},"Give the query a point. It returns the nearest face of the mesh, the nearest point on that face, and the squared distance between them. The point can sit inside a face, on an edge or on a vertex. The distance comes squared because most uses compare distances, and a comparison needs no square root.",[11,43,44],{},[28,45],{"alt":46,"height":31,"src":47,"width":33},"One triangle on a plane that is split into seven regions: queries over the face land inside it, queries in the three bands beside the edges land on an edge, and queries in the three wedges past the corners all land on the same vertex","\u002Fimg\u002Falgorithms\u002Fhubs\u002Fclosest-point-regions.webp",[11,49,50],{},"Which of the three it is depends only on where the query sits. Every point in the plane above is coloured by where its closest point lands on the triangle. In the bands beside each edge, the answer slides along that edge. In the wedges past each corner, every query lands on the corner itself. The query works this out for each triangle it tests.",[11,52,53],{},"The operation goes by many names: point-to-mesh distance, nearest point on a mesh, projecting a point onto a surface, snapping. Each name points at one of three jobs.",[55,56,57,65,71],"ul",{},[58,59,60,64],"li",{},[61,62,63],"strong",{},"Deviation."," How far is each scan point from the model? Inspection, quality control, as-built against as-designed.",[58,66,67,70],{},[61,68,69],{},"Registration."," Which model point does each scan point pair with? Iterative alignment asks this for every point, every iteration.",[58,72,73,76],{},[61,74,75],{},"Snapping."," Where on the surface does a click, a probe or a sensor reading land? Picking, measuring, attaching annotations.",[11,78,79],{},"All three run the query in bulk, against a mesh that does not change between queries. That is the case this page is about.",[35,81,83],{"id":82},"build-the-tree-once","Build the tree once",[11,85,86],{},"A brute-force query tests every face. On a million-face mesh, a hundred thousand queries make a hundred billion point-triangle tests. Nobody runs that.",[11,88,89],{},"The fix is a spatial tree: faces grouped into boxes, boxes into larger boxes, up to one box around the mesh. A query walks down the tree, keeps the best distance found so far, and skips every box already farther than that. Most of the mesh is never touched.",[11,91,92],{},[28,93],{"alt":94,"height":31,"src":95,"width":33},"A closed curve of 160 segments covered by nested boxes, with one query point outside it; the query opens two small leaf boxes near its closest point, and every other box, including the large ones, is skipped whole","\u002Fimg\u002Falgorithms\u002Fhubs\u002Fclosest-point-traversal.webp",[11,97,98],{},"The diagram is in 2D, where every box can be seen. The curve stands in for the mesh; its segments stand in for faces. The boxes are a schematic tree, split down the middle; the answer is trueform's. The query opens the box nearest to it first and finds a good candidate there. From then on, the dotted circle is the distance to beat. Any box that lies wholly outside the circle cannot hold a closer segment, so it is skipped with everything inside it. Ten segments of 160 were tested. On a million faces the ratio is far smaller, because the tree is deeper and each skip drops more.",[11,100,101],{},"The tree has a cost. On the Stanford dragon at about one million faces, from a mesh in memory to a query-ready tree:",[103,104],"data-table",{":headers":105,":highlight":106,":rows":107},"[\"Language\",\"trueform tree build\",\"Compared against\"]","1","[[\"C++\",\"7.87 ms\",\"MeshLib 34.4 ms, CGAL 177 ms\"],[\"Python\",\"7.89 ms\",\"MeshLib 31.9 ms\"],[\"JavaScript (WebAssembly)\",\"10.8 ms\",\"three-mesh-bvh 252 ms\"]]",[11,109,110],{},"Apple M4 Max, 16 threads, trueform 0.10.10.",[11,112,113],{},"Pay it once. The tree depends only on the mesh, so every batch after the first runs against the same tree. In C++, the tree over the 1,025,766-face dragon takes 7.87 ms; a batch of 100,000 queries against a ~1M-face mesh takes 94.1 ms. The build is about a twelfth of the first batch, and nothing after that.",[11,115,116],{},"Three rules keep the tree alive:",[55,118,119,125,131],{},[58,120,121,124],{},[61,122,123],{},"Queries do not touch it."," A query only reads the tree. In C++, run queries from as many threads as you like. In Python and JavaScript, one call takes the whole batch.",[58,126,127,130],{},[61,128,129],{},"Moving the mesh does not rebuild it."," Give the mesh a 4×4 transformation and queries run in world space against the same tree. The points are not rewritten.",[58,132,133,136,137,141,142,145,146,149],{},[61,134,135],{},"Python and JavaScript cache it on the mesh."," The first query builds it. ",[138,139,140],"code",{},"build_tree()"," in Python, and ",[138,143,144],{},"buildTree()"," or ",[138,147,148],{},"tf.async.buildTree()"," in JavaScript, build it at a time you choose. Change the mesh data and the next query rebuilds it.",[35,151,153],{"id":152},"throughput","Throughput",[11,155,156],{},"The query benchmark runs 100,000 points against each of 160 Thingi10K meshes, 40 at each of ~100K, ~250K, ~500K and ~1M faces. The points fill each mesh's bounding box grown by 5 %, so some sit far from the surface.",[103,158],{":headers":159,":highlight":106,":rows":160},"[\"Language\",\"trueform, 100K queries\",\"MeshLib\",\"libigl\"]","[[\"C++\",\"94.1 ms at ~1M\",\"206.6 ms, 2.2× slower\",\"216.4 ms, 2.3× slower\"],[\"Python\",\"131 ms at ~1M\",\"187 ms, 1.4× slower\",\"220 ms, 1.7× slower\"],[\"JavaScript (WebAssembly)\",\"126 ms at ~1M, tree prebuilt\",\"no npm peer measured\",\"no npm peer measured\"]]",[11,162,163],{},"Apple M4 Max, 16 threads, trueform 0.10.10, 160 Thingi10K meshes.",[11,165,166],{},"Read the numbers as a rate. A 131 ms batch is about 760,000 closest points a second, from one Python call, on a ~1M-face mesh.",[168,169],"hub-benchmark-figures",{":categories":170},"[\"closest-point\",\"spatial-tree-build\"]",[35,172,174],{"id":173},"the-nearest-face-not-a-near-one","The nearest face, not a near one",[11,176,177],{},"Some fast methods trade accuracy for speed. They stop the search early, or search a sampled point cloud instead of the faces. trueform does neither.",[11,179,180],{},"A box is skipped only when it cannot hold a closer face than the best one found, so the face returned is the nearest face. The closest point is then computed on that triangle, wherever it falls: inside the face, on an edge or on a vertex. A point equally close to two faces gets one of them, with the same distance either way.",[11,182,183,184,187],{},"Precision is that of your coordinates. Meshes are float32 by default. In C++ and Python, a float64 mesh is queried in float64. Query points must match the mesh type; Python raises a ",[138,185,186],{},"TypeError"," on a mismatch instead of converting silently.",[35,189,191],{"id":190},"deviation-maps","Deviation maps",[11,193,194],{},"A deviation map colours each scan point by its distance to the model. The closest-point query gives the size of the deviation. The sign, out of the model or into it, needs one more step.",[11,196,197,200],{},[138,198,199],{},"signed_distance"," gives both at once: negative inside, positive outside. The size is the closest-point distance. The sign comes from the generalized winding number, which counts how much of the surface wraps around the query: about 1 inside, about 0 outside. It finds the side on meshes with holes and on triangle soups, so a scan or an export with gaps signs correctly.",[11,202,203],{},[28,204],{"alt":205,"height":31,"src":206,"width":33},"A synthetic scan of the Stanford dragon coloured by its signed distance to the model: the head, pushed outward, shows orange; a dent in the body shows teal; the rest is sand, within tolerance","\u002Fimg\u002Falgorithms\u002Fhubs\u002Fclosest-point-deviation.webp",[11,208,209],{},"The scan above is synthetic: the public Stanford dragon at a coarser tessellation, pushed out at the head, pushed in at the body, with fine noise added. Each of its vertices is one query against the 250K-face model. The colour is the signed distance: teal in, orange out, sand within the tolerance.",[11,211,212],{},"From the arrays to a report is NumPy:",[55,214,215,222,228],{},[58,216,217,218,221],{},"the largest deviation is ",[138,219,220],{},"abs(d).max()",";",[58,223,224,225,221],{},"the points out of tolerance are ",[138,226,227],{},"abs(d) > tol",[58,229,230],{},"the face ids index per-face labels, so deviation can be summed per feature of the model.",[35,232,234],{"id":233},"registration","Registration",[11,236,237],{},"Iterative closest point alignment moves the scan toward the model in steps. Each step pairs every scan point with its closest model point, then solves for the motion that brings the pairs together.",[11,239,240],{},"The model stays still, so its tree is built once and serves every iteration. Only the scan points move, and the scan points need no tree: they are the batch. Each iteration is one batch call.",[11,242,243],{},"To align the other way, give the model a transformation instead. The tree still does not rebuild.",[245,246],"image-grid",{":images":247},"[{\"src\":\"\u002Fimg\u002Falgorithms\u002Fhubs\u002Fclosest-point-registration-before.webp\",\"width\":1200,\"height\":800,\"alt\":\"The Stanford dragon in teal as the model and a coarser copy in orange as the scan, turned 35 degrees and moved away beside it\",\"label\":\"Before\",\"detail\":\"the scan, turned and moved away\"},{\"src\":\"\u002Fimg\u002Falgorithms\u002Fhubs\u002Fclosest-point-registration-after.webp\",\"width\":1200,\"height\":800,\"alt\":\"The orange scan aligned onto the teal model: the two surfaces coincide, so the dragon shows as a speckle of teal and orange\",\"label\":\"After\",\"detail\":\"the two surfaces coincide\"}]",[11,249,250,251,254,255,258],{},"In Python, two calls: ",[138,252,253],{},"fit_obb_alignment"," for the coarse pose, then ",[138,256,257],{},"fit_icp_alignment"," to refine it.",[260,261,265],"pre",{"className":262,"code":263,"language":264,"meta":18,"style":18},"language-python shiki shiki-themes github-dark","scan = tf.PointCloud(scan_points)\nmodel = tf.PointCloud(model_points)\n\nscan.transformation = tf.fit_obb_alignment(scan, model)                          # coarse\nscan.transformation = tf.fit_icp_alignment(scan, model) @ scan.transformation    # refine\n","python",[138,266,267,283,294,301,316],{"__ignoreMap":18},[268,269,272,276,280],"span",{"class":270,"line":271},"line",1,[268,273,275],{"class":274},"s95oV","scan ",[268,277,279],{"class":278},"snl16","=",[268,281,282],{"class":274}," tf.PointCloud(scan_points)\n",[268,284,286,289,291],{"class":270,"line":285},2,[268,287,288],{"class":274},"model ",[268,290,279],{"class":278},[268,292,293],{"class":274}," tf.PointCloud(model_points)\n",[268,295,297],{"class":270,"line":296},3,[268,298,300],{"emptyLinePlaceholder":299},true,"\n",[268,302,304,307,309,312],{"class":270,"line":303},4,[268,305,306],{"class":274},"scan.transformation ",[268,308,279],{"class":278},[268,310,311],{"class":274}," tf.fit_obb_alignment(scan, model)                          ",[268,313,315],{"class":314},"sAwPA","# coarse\n",[268,317,319,321,323,326,329,332],{"class":270,"line":318},5,[268,320,306],{"class":274},[268,322,279],{"class":278},[268,324,325],{"class":274}," tf.fit_icp_alignment(scan, model) ",[268,327,328],{"class":278},"@",[268,330,331],{"class":274}," scan.transformation    ",[268,333,334],{"class":314},"# refine\n",[11,336,337,338,20],{},"Drag one dragon away from another and align it back in the ",[15,339,341],{"href":340,"external":18},"\u002Ftrueform\u002Flive\u002Falignment","live registration example",[35,343,345],{"id":344},"snapping","Snapping",[11,347,348],{},"Snapping asks a smaller question: the surface point near a click or a probe, if one is near enough.",[11,350,351,354],{},[61,352,353],{},"Within a radius."," Pass a radius and the search ignores anything farther. A single query returns nothing when no face is in range. A batch marks misses with −1 for the face id and infinity for the distance, so the mask is one comparison. A tight radius also prunes the search.",[11,356,357,360,361,364],{},[61,358,359],{},"k nearest."," Pass ",[138,362,363],{},"k"," and each query returns its k nearest faces, sorted by distance. With a radius, a query can find fewer than k; Python returns the count per query.",[11,366,367,370],{},[61,368,369],{},"Per-face data."," The face id indexes anything stored per face. A normal, a material label or a colour at the snapped point is one array lookup.",[11,372,373,376,377,380,381,384],{},[61,374,375],{},"Distance only."," When the point and the face do not matter, ",[138,378,379],{},"distance"," and ",[138,382,383],{},"distance2"," take the same inputs and return one array.",[35,386,388],{"id":387},"tuning-the-query","Tuning the query",[55,390,391,397,407],{},[58,392,393,396],{},[61,394,395],{},"Points far from the surface."," Pass the largest distance that matters as the radius, and the search drops everything beyond it.",[58,398,399,402,403,406],{},[61,400,401],{},"A shape that changes."," Moving the mesh keeps the tree. Changing its points or faces rebuilds it on the next query; in C++, ",[138,404,405],{},"mod_tree"," updates only the faces that changed.",[58,408,409,412],{},[61,410,411],{},"Coordinates far from the origin."," A float32 mesh in survey coordinates, kilometres from the origin, keeps fewer digits near its surface. Subtract one offset from the mesh and the queries before you start, or use float64 in C++ and Python.",[35,414,416],{"id":415},"closest-point-in-code","Closest point in code",[11,418,419],{},"Each example builds the tree on the model and runs a batch of points against it.",[421,422,423,689,811],"code-group",{},[260,424,429],{"className":425,"code":426,"filename":427,"language":428,"meta":18,"style":18},"language-cpp shiki shiki-themes github-dark","#include \u003Ctrueform\u002Ftrueform.hpp>\n\nauto model_buffer = tf::read_stl(\"model.stl\");\nauto model = model_buffer.polygons();\ntf::aabb_tree\u003Cint, float, 3> tree(model, tf::config_tree(4, 4));\nauto form = model | tf::tag(tree);\n\n\u002F\u002F queries, face_ids, closest_pts: std::vectors of equal size\ntf::parallel_for_each(tf::zip(queries, face_ids, closest_pts), [&](auto row) {\n  auto &&[q, id, pt] = row;\n  auto [fid, mp] = tf::neighbor_search(form, q);\n  id = fid;\n  pt = mp.point;\n});\n","C++","cpp",[138,430,431,440,444,473,491,551,576,581,587,624,641,661,672,683],{"__ignoreMap":18},[268,432,433,436],{"class":270,"line":271},[268,434,435],{"class":278},"#include",[268,437,439],{"class":438},"sU2Wk"," \u003Ctrueform\u002Ftrueform.hpp>\n",[268,441,442],{"class":270,"line":285},[268,443,300],{"emptyLinePlaceholder":299},[268,445,446,449,452,454,458,461,464,467,470],{"class":270,"line":296},[268,447,448],{"class":278},"auto",[268,450,451],{"class":274}," model_buffer ",[268,453,279],{"class":278},[268,455,457],{"class":456},"svObZ"," tf",[268,459,460],{"class":274},"::",[268,462,463],{"class":456},"read_stl",[268,465,466],{"class":274},"(",[268,468,469],{"class":438},"\"model.stl\"",[268,471,472],{"class":274},");\n",[268,474,475,477,480,482,485,488],{"class":270,"line":303},[268,476,448],{"class":278},[268,478,479],{"class":274}," model ",[268,481,279],{"class":278},[268,483,484],{"class":274}," model_buffer.",[268,486,487],{"class":456},"polygons",[268,489,490],{"class":274},"();\n",[268,492,493,496,498,501,504,507,510,513,515,519,522,525,527,530,532,534,536,539,541,544,546,548],{"class":270,"line":318},[268,494,495],{"class":456},"tf",[268,497,460],{"class":274},[268,499,500],{"class":456},"aabb_tree",[268,502,503],{"class":274},"\u003C",[268,505,506],{"class":278},"int",[268,508,509],{"class":274},", ",[268,511,512],{"class":278},"float",[268,514,509],{"class":274},[268,516,518],{"class":517},"sDLfK","3",[268,520,521],{"class":274},"> ",[268,523,524],{"class":456},"tree",[268,526,466],{"class":274},[268,528,529],{"class":456},"model",[268,531,509],{"class":274},[268,533,495],{"class":456},[268,535,460],{"class":274},[268,537,538],{"class":456},"config_tree",[268,540,466],{"class":274},[268,542,543],{"class":517},"4",[268,545,509],{"class":274},[268,547,543],{"class":517},[268,549,550],{"class":274},"));\n",[268,552,554,556,559,561,563,566,568,570,573],{"class":270,"line":553},6,[268,555,448],{"class":278},[268,557,558],{"class":274}," form ",[268,560,279],{"class":278},[268,562,479],{"class":274},[268,564,565],{"class":278},"|",[268,567,457],{"class":456},[268,569,460],{"class":274},[268,571,572],{"class":456},"tag",[268,574,575],{"class":274},"(tree);\n",[268,577,579],{"class":270,"line":578},7,[268,580,300],{"emptyLinePlaceholder":299},[268,582,584],{"class":270,"line":583},8,[268,585,586],{"class":314},"\u002F\u002F queries, face_ids, closest_pts: std::vectors of equal size\n",[268,588,590,592,594,597,599,601,603,606,609,612,615,617,621],{"class":270,"line":589},9,[268,591,495],{"class":456},[268,593,460],{"class":274},[268,595,596],{"class":456},"parallel_for_each",[268,598,466],{"class":274},[268,600,495],{"class":456},[268,602,460],{"class":274},[268,604,605],{"class":456},"zip",[268,607,608],{"class":274},"(queries, face_ids, closest_pts), [",[268,610,611],{"class":278},"&",[268,613,614],{"class":274},"](",[268,616,448],{"class":278},[268,618,620],{"class":619},"s9osk"," row",[268,622,623],{"class":274},") {\n",[268,625,627,630,633,636,638],{"class":270,"line":626},10,[268,628,629],{"class":278},"  auto",[268,631,632],{"class":278}," &&",[268,634,635],{"class":274},"[q, id, pt] ",[268,637,279],{"class":278},[268,639,640],{"class":274}," row;\n",[268,642,644,646,649,651,653,655,658],{"class":270,"line":643},11,[268,645,629],{"class":278},[268,647,648],{"class":274}," [fid, mp] ",[268,650,279],{"class":278},[268,652,457],{"class":456},[268,654,460],{"class":274},[268,656,657],{"class":456},"neighbor_search",[268,659,660],{"class":274},"(form, q);\n",[268,662,664,667,669],{"class":270,"line":663},12,[268,665,666],{"class":274},"  id ",[268,668,279],{"class":278},[268,670,671],{"class":274}," fid;\n",[268,673,675,678,680],{"class":270,"line":674},13,[268,676,677],{"class":274},"  pt ",[268,679,279],{"class":278},[268,681,682],{"class":274}," mp.point;\n",[268,684,686],{"class":270,"line":685},14,[268,687,688],{"class":274},"});\n",[260,690,693],{"className":262,"code":691,"filename":692,"language":264,"meta":18,"style":18},"import numpy as np\nimport trueform as tf\n\nmodel = tf.Mesh(*tf.read_stl(\"model.stl\"))\nmodel.build_tree()\n\nscan = tf.Point(np.load(\"scan.npy\").astype(np.float32))   # (N, 3)\nface_ids, dist2, closest = tf.neighbor_search(model, scan)\ndeviation = tf.signed_distance(model, scan)               # negative inside\nout_of_tol = np.abs(deviation) > 0.1\n","Python",[138,694,695,709,721,725,745,750,754,772,782,795],{"__ignoreMap":18},[268,696,697,700,703,706],{"class":270,"line":271},[268,698,699],{"class":278},"import",[268,701,702],{"class":274}," numpy ",[268,704,705],{"class":278},"as",[268,707,708],{"class":274}," np\n",[268,710,711,713,716,718],{"class":270,"line":285},[268,712,699],{"class":278},[268,714,715],{"class":274}," trueform ",[268,717,705],{"class":278},[268,719,720],{"class":274}," tf\n",[268,722,723],{"class":270,"line":296},[268,724,300],{"emptyLinePlaceholder":299},[268,726,727,729,731,734,737,740,742],{"class":270,"line":303},[268,728,288],{"class":274},[268,730,279],{"class":278},[268,732,733],{"class":274}," tf.Mesh(",[268,735,736],{"class":278},"*",[268,738,739],{"class":274},"tf.read_stl(",[268,741,469],{"class":438},[268,743,744],{"class":274},"))\n",[268,746,747],{"class":270,"line":318},[268,748,749],{"class":274},"model.build_tree()\n",[268,751,752],{"class":270,"line":553},[268,753,300],{"emptyLinePlaceholder":299},[268,755,756,758,760,763,766,769],{"class":270,"line":578},[268,757,275],{"class":274},[268,759,279],{"class":278},[268,761,762],{"class":274}," tf.Point(np.load(",[268,764,765],{"class":438},"\"scan.npy\"",[268,767,768],{"class":274},").astype(np.float32))   ",[268,770,771],{"class":314},"# (N, 3)\n",[268,773,774,777,779],{"class":270,"line":583},[268,775,776],{"class":274},"face_ids, dist2, closest ",[268,778,279],{"class":278},[268,780,781],{"class":274}," tf.neighbor_search(model, scan)\n",[268,783,784,787,789,792],{"class":270,"line":589},[268,785,786],{"class":274},"deviation ",[268,788,279],{"class":278},[268,790,791],{"class":274}," tf.signed_distance(model, scan)               ",[268,793,794],{"class":314},"# negative inside\n",[268,796,797,800,802,805,808],{"class":270,"line":626},[268,798,799],{"class":274},"out_of_tol ",[268,801,279],{"class":278},[268,803,804],{"class":274}," np.abs(deviation) ",[268,806,807],{"class":278},">",[268,809,810],{"class":517}," 0.1\n",[260,812,817],{"className":813,"code":814,"filename":815,"language":816,"meta":18,"style":18},"language-ts shiki shiki-themes github-dark","import * as tf from \"@polydera\u002Ftrueform\";\nimport { readFileSync } from \"node:fs\";\n\nconst model = tf.readStl(readFileSync(\"model.stl\"));\nawait tf.async.buildTree(model);\n\nconst queries = tf.point(scanXYZ, 3);   \u002F\u002F Float32Array, 3 numbers per point\nconst { elementIds, points, distances } = tf.neighborSearch(model, queries);\n\u002F\u002F distances are squared\n","JavaScript","ts",[138,818,819,841,855,859,887,901,905,930,963],{"__ignoreMap":18},[268,820,821,823,826,829,832,835,838],{"class":270,"line":271},[268,822,699],{"class":278},[268,824,825],{"class":517}," *",[268,827,828],{"class":278}," as",[268,830,831],{"class":274}," tf ",[268,833,834],{"class":278},"from",[268,836,837],{"class":438}," \"@polydera\u002Ftrueform\"",[268,839,840],{"class":274},";\n",[268,842,843,845,848,850,853],{"class":270,"line":285},[268,844,699],{"class":278},[268,846,847],{"class":274}," { readFileSync } ",[268,849,834],{"class":278},[268,851,852],{"class":438}," \"node:fs\"",[268,854,840],{"class":274},[268,856,857],{"class":270,"line":296},[268,858,300],{"emptyLinePlaceholder":299},[268,860,861,864,867,870,873,876,878,881,883,885],{"class":270,"line":303},[268,862,863],{"class":278},"const",[268,865,866],{"class":517}," model",[268,868,869],{"class":278}," =",[268,871,872],{"class":274}," tf.",[268,874,875],{"class":456},"readStl",[268,877,466],{"class":274},[268,879,880],{"class":456},"readFileSync",[268,882,466],{"class":274},[268,884,469],{"class":438},[268,886,550],{"class":274},[268,888,889,892,895,898],{"class":270,"line":318},[268,890,891],{"class":278},"await",[268,893,894],{"class":274}," tf.async.",[268,896,897],{"class":456},"buildTree",[268,899,900],{"class":274},"(model);\n",[268,902,903],{"class":270,"line":553},[268,904,300],{"emptyLinePlaceholder":299},[268,906,907,909,912,914,916,919,922,924,927],{"class":270,"line":578},[268,908,863],{"class":278},[268,910,911],{"class":517}," queries",[268,913,869],{"class":278},[268,915,872],{"class":274},[268,917,918],{"class":456},"point",[268,920,921],{"class":274},"(scanXYZ, ",[268,923,518],{"class":517},[268,925,926],{"class":274},");   ",[268,928,929],{"class":314},"\u002F\u002F Float32Array, 3 numbers per point\n",[268,931,932,934,937,940,942,945,947,950,953,955,957,960],{"class":270,"line":583},[268,933,863],{"class":278},[268,935,936],{"class":274}," { ",[268,938,939],{"class":517},"elementIds",[268,941,509],{"class":274},[268,943,944],{"class":517},"points",[268,946,509],{"class":274},[268,948,949],{"class":517},"distances",[268,951,952],{"class":274}," } ",[268,954,279],{"class":278},[268,956,872],{"class":274},[268,958,959],{"class":456},"neighborSearch",[268,961,962],{"class":274},"(model, queries);\n",[268,964,965],{"class":270,"line":589},[268,966,967],{"class":314},"\u002F\u002F distances are squared\n",[11,969,970],{},"The tutorials go through single points, the radius, k nearest and posed meshes one step at a time, with pictures.",[35,972,974],{"id":973},"where-to-go-next","Where to go next",[11,976,977,980,981,509,984,509,987,20],{},[61,978,979],{},"Tutorials."," ",[15,982,427],{"href":983},"\u002Ftutorials\u002Ffast-closest-point-on-a-mesh-in-cpp",[15,985,692],{"href":986},"\u002Ftutorials\u002Ffast-closest-point-on-a-mesh-in-python",[15,988,815],{"href":989},"\u002Ftutorials\u002Ffast-closest-point-on-a-mesh-in-javascript",[11,991,992,995,996,20],{},[61,993,994],{},"Two meshes instead of points."," When the query is a whole second mesh, the questions change: do they touch, how far apart are they, which faces meet. That is ",[15,997,999],{"href":998},"\u002Falgorithms\u002Fmesh-collision-detection","mesh collision detection",[11,1001,1002,1005,1006,20],{},[61,1003,1004],{},"Distance along the surface."," A snapped point makes a good seed for ",[15,1007,1009],{"href":1008},"\u002Falgorithms\u002Fgeodesic-distances","geodesic distances",[11,1011,1012,1015,1016,509,1021,380,1025,20],{},[61,1013,1014],{},"Documentation."," Neighbor search, distance, signed distance and the rest of the spatial module, in ",[15,1017,427],{"href":1018,"rel":1019},"https:\u002F\u002Ftrueform.polydera.com\u002Fcpp\u002Fmodules\u002Fspatial",[1020],"nofollow",[15,1022,692],{"href":1023,"rel":1024},"https:\u002F\u002Ftrueform.polydera.com\u002Fpy\u002Fmodules\u002Fspatial#neighbor-search",[1020],[15,1026,1029],{"href":1027,"rel":1028},"https:\u002F\u002Ftrueform.polydera.com\u002Fts\u002Fmodules\u002Fspatial",[1020],"TypeScript",[11,1031,1032,1033,1037,1038,509,1042,1046],{},"Spatial queries are one module of ",[15,1034,1036],{"href":1035},"\u002Ftrueform","trueform",". The same engine does ",[15,1039,1041],{"href":1040},"\u002Falgorithms\u002Fmesh-booleans","mesh booleans",[15,1043,1045],{"href":1044},"\u002Falgorithms\u002Fprincipal-curvatures","curvature",", topology and remeshing, in C++, Python and the browser.",[1048,1049],"article-cta",{":buttons":1050},"[{\"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\u002Fspatial#neighbor-search\",\"variant\":\"soft\",\"color\":\"neutral\"},{\"label\":\"Try it live\",\"to\":\"\u002Ftrueform\u002Flive\u002Fclosest-points\",\"icon\":\"i-lucide-play\"}]",[1052,1053],"cite-as",{"author":1054,"title":5},"Sajovic, {\\v{Z}}iga",[1056,1057,1058],"style",{},"html pre.shiki code .s95oV, html code.shiki .s95oV{--shiki-default:#E1E4E8}html pre.shiki code .snl16, html code.shiki .snl16{--shiki-default:#F97583}html pre.shiki code .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 .sU2Wk, html code.shiki .sU2Wk{--shiki-default:#9ECBFF}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 .s9osk, html code.shiki .s9osk{--shiki-default:#FFAB70}",{"title":18,"searchDepth":285,"depth":285,"links":1060},[1061,1062,1063,1064,1065,1066,1067,1068,1069,1070],{"id":37,"depth":285,"text":38},{"id":82,"depth":285,"text":83},{"id":152,"depth":285,"text":153},{"id":173,"depth":285,"text":174},{"id":190,"depth":285,"text":191},{"id":233,"depth":285,"text":234},{"id":344,"depth":285,"text":345},{"id":387,"depth":285,"text":388},{"id":415,"depth":285,"text":416},{"id":973,"depth":285,"text":974},"2026-11-08","Point-to-mesh distance at scale: build the spatial tree once, then answer batches of closest-point queries for deviation maps, registration and snapping. Measured in C++, Python and JavaScript.","md",[1075,1076,1077,1078,1079,1080,1081,1082,1083],"closest point on mesh","point to mesh distance","nearest point on mesh","project point onto mesh","scan to CAD deviation","deviation map","aabb tree closest point","batch closest point query","k nearest faces",{},"\u002Fimg\u002Fog\u002Falgorithms\u002Fclosest-point-on-a-mesh.png","\u002Falgorithms\u002Fclosest-point-on-a-mesh",{"title":1088,"description":1089},"Closest Point on a Mesh: Point-to-Mesh Distance","Closest point on a triangle mesh for millions of points: build the tree once, query in parallel. Deviation maps, registration, snapping in C++, Python, JS.","algorithms\u002Fclosest-point-on-a-mesh",[1092,1093,1094],"spatial","queries","overview","2026-11-12T12:06:28.000Z","aeAV8TsQ2QcuVg23yN3jZYJJDZt6kZvPs42_IjjmY_M",1794486338888]