[{"data":1,"prerenderedAt":1149},["ShallowReactive",2],{"tutorial-fast-closest-point-on-a-mesh-in-python":3},{"id":4,"title":5,"author":6,"body":7,"date":1126,"description":1127,"extension":1128,"keywords":1129,"meta":1139,"navigation":136,"ogImage":1140,"path":1141,"published":136,"seo":1142,"stem":1143,"tags":1144,"__hash__":1148},"tutorials\u002Ftutorials\u002Ffast-closest-point-on-a-mesh-in-python.md","Fast and Robust Closest Point on a Mesh in Python","Žiga Sajovic",{"type":8,"value":9,"toc":1114},"minimark",[10,28,40,44,51,56,81,91,95,162,184,188,194,259,265,283,300,304,315,410,416,429,432,441,451,469,473,484,538,544,555,599,602,606,615,649,655,670,674,677,797,802,806,809,862,868,878,899,918,1027,1031,1062,1066,1069,1073,1085,1088,1094,1106,1110],[11,12,13],"note",{},[14,15,16,17,22,23,27],"p",{},"Also available in ",[18,19,21],"a",{"href":20},"\u002Ftutorials\u002Ffast-closest-point-on-a-mesh-in-cpp","C++"," and ",[18,24,26],{"href":25},"\u002Ftutorials\u002Ffast-closest-point-on-a-mesh-in-javascript","JavaScript",".",[14,29,30,31,35,36,39],{},"A closest-point query takes a point in space and returns the nearest point on a mesh surface, the face it lies on, and the distance. It is the base of projection, snapping, distance checks, and scan-to-CAD comparison. ",[32,33,34],"code",{},"trueform"," does it with one ",[32,37,38],{},"pip install",". NumPy arrays in, NumPy arrays out. This page covers single queries, batch queries over many points, radius-limited search, k-nearest faces, and posed meshes.",[41,42],"article-cta",{":buttons":43},"[{\"label\":\"trueform\",\"to\":\"\u002Ftrueform\",\"icon\":\"i-lucide-cpu\",\"variant\":\"soft\",\"color\":\"neutral\"},{\"label\":\"GitHub\",\"to\":\"https:\u002F\u002Fgithub.com\u002Fpolydera\u002Ftrueform\",\"icon\":\"i-simple-icons-github\",\"variant\":\"soft\",\"color\":\"neutral\"},{\"label\":\"Documentation\",\"to\":\"https:\u002F\u002Ftrueform.polydera.com\u002Fpy\u002Fmodules\u002Fspatial#neighbor-search\",\"variant\":\"soft\",\"color\":\"neutral\"},{\"label\":\"Try it live\",\"to\":\"https:\u002F\u002Ftrueform.polydera.com\u002Flive-examples\u002Fclosest-points\",\"icon\":\"i-lucide-play\"}]",[14,45,46],{},[47,48],"img",{"alt":49,"src":50},"Query points around the Stanford dragon joined to their closest points on the surface","\u002Fimg\u002Ftutorials\u002Fclosest-point.png",[52,53,55],"h2",{"id":54},"install","Install",[57,58,63],"pre",{"className":59,"code":60,"language":61,"meta":62,"style":62},"language-bash shiki shiki-themes github-dark","pip install trueform\n","bash","",[32,64,65],{"__ignoreMap":62},[66,67,70,74,78],"span",{"class":68,"line":69},"line",1,[66,71,73],{"class":72},"svObZ","pip",[66,75,77],{"class":76},"sU2Wk"," install",[66,79,80],{"class":76}," trueform\n",[14,82,83,84,90],{},"See the ",[18,85,89],{"href":86,"rel":87},"https:\u002F\u002Ftrueform.polydera.com\u002Fpy\u002Fgetting-started\u002Finstallation",[88],"nofollow","installation guide"," for all options.",[52,92,94],{"id":93},"loading-the-mesh","Loading the mesh",[57,96,100],{"className":97,"code":98,"language":99,"meta":62,"style":62},"language-python shiki shiki-themes github-dark","import numpy as np\nimport trueform as tf\n\nmesh = tf.Mesh(*tf.read_stl(\"part.stl\"))\n","python",[32,101,102,118,131,138],{"__ignoreMap":62},[66,103,104,108,112,115],{"class":68,"line":69},[66,105,107],{"class":106},"snl16","import",[66,109,111],{"class":110},"s95oV"," numpy ",[66,113,114],{"class":106},"as",[66,116,117],{"class":110}," np\n",[66,119,121,123,126,128],{"class":68,"line":120},2,[66,122,107],{"class":106},[66,124,125],{"class":110}," trueform ",[66,127,114],{"class":106},[66,129,130],{"class":110}," tf\n",[66,132,134],{"class":68,"line":133},3,[66,135,137],{"emptyLinePlaceholder":136},true,"\n",[66,139,141,144,147,150,153,156,159],{"class":68,"line":140},4,[66,142,143],{"class":110},"mesh ",[66,145,146],{"class":106},"=",[66,148,149],{"class":110}," tf.Mesh(",[66,151,152],{"class":106},"*",[66,154,155],{"class":110},"tf.read_stl(",[66,157,158],{"class":76},"\"part.stl\"",[66,160,161],{"class":110},"))\n",[14,163,164,167,168,171,172,175,176,179,180,183],{},[32,165,166],{},"tf.read_stl"," returns ",[32,169,170],{},"(faces, points)"," as NumPy arrays. ",[32,173,174],{},"tf.read_obj"," works the same way. If you already have arrays, pass them to ",[32,177,178],{},"tf.Mesh(faces, points)"," directly. Points are ",[32,181,182],{},"float32"," by default.",[52,185,187],{"id":186},"one-point","One point",[14,189,190,193],{},[32,191,192],{},"tf.neighbor_search"," finds the nearest face of the mesh to a query point:",[57,195,197],{"className":97,"code":196,"language":99,"meta":62,"style":62},"query = tf.Point(np.array([0.0, 0.0, 2.0], dtype=np.float32))\n\nface_id, dist2, closest = tf.neighbor_search(mesh, query)\ndistance = np.sqrt(dist2)\n",[32,198,199,235,239,249],{"__ignoreMap":62},[66,200,201,204,206,209,213,216,218,220,223,226,230,232],{"class":68,"line":69},[66,202,203],{"class":110},"query ",[66,205,146],{"class":106},[66,207,208],{"class":110}," tf.Point(np.array([",[66,210,212],{"class":211},"sDLfK","0.0",[66,214,215],{"class":110},", ",[66,217,212],{"class":211},[66,219,215],{"class":110},[66,221,222],{"class":211},"2.0",[66,224,225],{"class":110},"], ",[66,227,229],{"class":228},"s9osk","dtype",[66,231,146],{"class":106},[66,233,234],{"class":110},"np.float32))\n",[66,236,237],{"class":68,"line":120},[66,238,137],{"emptyLinePlaceholder":136},[66,240,241,244,246],{"class":68,"line":133},[66,242,243],{"class":110},"face_id, dist2, closest ",[66,245,146],{"class":106},[66,247,248],{"class":110}," tf.neighbor_search(mesh, query)\n",[66,250,251,254,256],{"class":68,"line":140},[66,252,253],{"class":110},"distance ",[66,255,146],{"class":106},[66,257,258],{"class":110}," np.sqrt(dist2)\n",[14,260,261],{},[47,262],{"alt":263,"src":264},"Step: one point. A query point (orange) off the Stanford dragon, joined to its closest point on the surface (pink)","\u002Fimg\u002Ftutorials\u002Fsteps\u002Ffast-closest-point-on-a-mesh-1-one-point.png",[14,266,267,268,271,272,275,276,279,280,27],{},"It returns three values. ",[32,269,270],{},"face_id"," is the index of the nearest face. ",[32,273,274],{},"dist2"," is the squared distance. ",[32,277,278],{},"closest"," is the point on that face, shape ",[32,281,282],{},"(3,)",[14,284,285,286,289,290,293,294,296,297,27],{},"The query and the mesh must share a dtype. A Python list of floats becomes ",[32,287,288],{},"float64",", so build query points with ",[32,291,292],{},"dtype=np.float32"," when the mesh is ",[32,295,182],{},". A mismatch raises a ",[32,298,299],{},"TypeError",[52,301,303],{"id":302},"many-points-at-once","Many points at once",[14,305,306,307,310,311,314],{},"Wrap an ",[32,308,309],{},"(N, 3)"," array in ",[32,312,313],{},"tf.Point"," and pass it in one call. This is the fast form: one call, all queries run in parallel against the mesh's spatial tree.",[57,316,318],{"className":97,"code":317,"language":99,"meta":62,"style":62},"rng = np.random.default_rng(0)\nqueries = rng.uniform(-1.5, 1.5, size=(100_000, 3)).astype(np.float32)\n\nface_ids, dist2s, closest_pts = tf.neighbor_search(mesh, tf.Point(queries))\n# face_ids.shape    == (100000,)   int32\n# dist2s.shape      == (100000,)   float32\n# closest_pts.shape == (100000, 3) float32\n",[32,319,320,336,377,381,391,398,404],{"__ignoreMap":62},[66,321,322,325,327,330,333],{"class":68,"line":69},[66,323,324],{"class":110},"rng ",[66,326,146],{"class":106},[66,328,329],{"class":110}," np.random.default_rng(",[66,331,332],{"class":211},"0",[66,334,335],{"class":110},")\n",[66,337,338,341,343,346,349,352,354,356,358,361,363,366,369,371,374],{"class":68,"line":120},[66,339,340],{"class":110},"queries ",[66,342,146],{"class":106},[66,344,345],{"class":110}," rng.uniform(",[66,347,348],{"class":106},"-",[66,350,351],{"class":211},"1.5",[66,353,215],{"class":110},[66,355,351],{"class":211},[66,357,215],{"class":110},[66,359,360],{"class":228},"size",[66,362,146],{"class":106},[66,364,365],{"class":110},"(",[66,367,368],{"class":211},"100_000",[66,370,215],{"class":110},[66,372,373],{"class":211},"3",[66,375,376],{"class":110},")).astype(np.float32)\n",[66,378,379],{"class":68,"line":133},[66,380,137],{"emptyLinePlaceholder":136},[66,382,383,386,388],{"class":68,"line":140},[66,384,385],{"class":110},"face_ids, dist2s, closest_pts ",[66,387,146],{"class":106},[66,389,390],{"class":110}," tf.neighbor_search(mesh, tf.Point(queries))\n",[66,392,394],{"class":68,"line":393},5,[66,395,397],{"class":396},"sAwPA","# face_ids.shape    == (100000,)   int32\n",[66,399,401],{"class":68,"line":400},6,[66,402,403],{"class":396},"# dist2s.shape      == (100000,)   float32\n",[66,405,407],{"class":68,"line":406},7,[66,408,409],{"class":396},"# closest_pts.shape == (100000, 3) float32\n",[14,411,412],{},[47,413],{"alt":414,"src":415},"Step: many points at once. 100,000 queries around the dragon; their closest points mark the surface in pink, with a sample of queries (orange) joined to them","\u002Fimg\u002Ftutorials\u002Fsteps\u002Ffast-closest-point-on-a-mesh-2-many-points.png",[14,417,418,419,422,423,425,426,428],{},"Pass the batch as ",[32,420,421],{},"tf.Point(...)",". A bare ",[32,424,309],{}," array is rejected; a bare ",[32,427,282],{}," array is read as one point.",[14,430,431],{},"The spatial tree is built on the first query and cached on the mesh. To pay that cost at a time you choose, build it first:",[57,433,435],{"className":97,"code":434,"language":99,"meta":62,"style":62},"mesh.build_tree()\n",[32,436,437],{"__ignoreMap":62},[66,438,439],{"class":68,"line":69},[66,440,434],{"class":110},[14,442,443,444,22,447,450],{},"If you only need distances, ",[32,445,446],{},"tf.distance",[32,448,449],{},"tf.distance2"," take the same batch and return one array:",[57,452,454],{"className":97,"code":453,"language":99,"meta":62,"style":62},"distances = tf.distance(mesh, tf.Point(queries))   # shape (100000,)\n",[32,455,456],{"__ignoreMap":62},[66,457,458,461,463,466],{"class":68,"line":69},[66,459,460],{"class":110},"distances ",[66,462,146],{"class":106},[66,464,465],{"class":110}," tf.distance(mesh, tf.Point(queries))   ",[66,467,468],{"class":396},"# shape (100000,)\n",[52,470,472],{"id":471},"search-within-a-radius","Search within a radius",[14,474,475,476,479,480,483],{},"Pass ",[32,477,478],{},"radius"," to ignore anything farther away. A single query returns ",[32,481,482],{},"None"," when nothing is in range:",[57,485,487],{"className":97,"code":486,"language":99,"meta":62,"style":62},"result = tf.neighbor_search(mesh, query, radius=0.5)\nif result is not None:\n    face_id, dist2, closest = result\n",[32,488,489,508,528],{"__ignoreMap":62},[66,490,491,494,496,499,501,503,506],{"class":68,"line":69},[66,492,493],{"class":110},"result ",[66,495,146],{"class":106},[66,497,498],{"class":110}," tf.neighbor_search(mesh, query, ",[66,500,478],{"class":228},[66,502,146],{"class":106},[66,504,505],{"class":211},"0.5",[66,507,335],{"class":110},[66,509,510,513,516,519,522,525],{"class":68,"line":120},[66,511,512],{"class":106},"if",[66,514,515],{"class":110}," result ",[66,517,518],{"class":106},"is",[66,520,521],{"class":106}," not",[66,523,524],{"class":211}," None",[66,526,527],{"class":110},":\n",[66,529,530,533,535],{"class":68,"line":133},[66,531,532],{"class":110},"    face_id, dist2, closest ",[66,534,146],{"class":106},[66,536,537],{"class":110}," result\n",[14,539,540],{},[47,541],{"alt":542,"src":543},"Step: search within a radius. Queries inside the radius (orange) joined to their closest points; the rest (grey) come back as misses","\u002Fimg\u002Ftutorials\u002Fsteps\u002Ffast-closest-point-on-a-mesh-3-radius.png",[14,545,546,547,550,551,554],{},"In a batch, misses come back as ",[32,548,549],{},"-1"," in the ids and ",[32,552,553],{},"inf"," in the squared distances:",[57,556,558],{"className":97,"code":557,"language":99,"meta":62,"style":62},"face_ids, dist2s, closest_pts = tf.neighbor_search(\n    mesh, tf.Point(queries), radius=0.1)\nnear = face_ids >= 0\n",[32,559,560,569,583],{"__ignoreMap":62},[66,561,562,564,566],{"class":68,"line":69},[66,563,385],{"class":110},[66,565,146],{"class":106},[66,567,568],{"class":110}," tf.neighbor_search(\n",[66,570,571,574,576,578,581],{"class":68,"line":120},[66,572,573],{"class":110},"    mesh, tf.Point(queries), ",[66,575,478],{"class":228},[66,577,146],{"class":106},[66,579,580],{"class":211},"0.1",[66,582,335],{"class":110},[66,584,585,588,590,593,596],{"class":68,"line":133},[66,586,587],{"class":110},"near ",[66,589,146],{"class":106},[66,591,592],{"class":110}," face_ids ",[66,594,595],{"class":106},">=",[66,597,598],{"class":211}," 0\n",[14,600,601],{},"A tight radius also prunes the search. Use it when you know the scale you care about, as in snapping or contact checks.",[52,603,605],{"id":604},"k-nearest-faces","k nearest faces",[14,607,475,608,611,612,614],{},[32,609,610],{},"k"," to get the ",[32,613,610],{}," nearest faces per query, sorted by distance:",[57,616,618],{"className":97,"code":617,"language":99,"meta":62,"style":62},"ids, dist2s, pts, counts = tf.neighbor_search(mesh, tf.Point(queries), k=4)\n# ids.shape == (100000, 4), pts.shape == (100000, 4, 3)\n# counts[i] is the number of valid neighbors for query i\n",[32,619,620,639,644],{"__ignoreMap":62},[66,621,622,625,627,630,632,634,637],{"class":68,"line":69},[66,623,624],{"class":110},"ids, dist2s, pts, counts ",[66,626,146],{"class":106},[66,628,629],{"class":110}," tf.neighbor_search(mesh, tf.Point(queries), ",[66,631,610],{"class":228},[66,633,146],{"class":106},[66,635,636],{"class":211},"4",[66,638,335],{"class":110},[66,640,641],{"class":68,"line":120},[66,642,643],{"class":396},"# ids.shape == (100000, 4), pts.shape == (100000, 4, 3)\n",[66,645,646],{"class":68,"line":133},[66,647,648],{"class":396},"# counts[i] is the number of valid neighbors for query i\n",[14,650,651],{},[47,652],{"alt":653,"src":654},"Step: k nearest faces. Close-up of three queries next to the dragon, each with its four nearest faces in pink and joined to them","\u002Fimg\u002Ftutorials\u002Fsteps\u002Ffast-closest-point-on-a-mesh-4-k-nearest.png",[14,656,657,658,660,661,664,665,667,668,27],{},"With ",[32,659,478],{}," set, ",[32,662,663],{},"counts[i]"," can be less than ",[32,666,610],{},". Unused id slots hold ",[32,669,549],{},[52,671,673],{"id":672},"posed-meshes","Posed meshes",[14,675,676],{},"Set a 4×4 transformation on the mesh and queries run in world space. The tree is not rebuilt. A shared view carries its own pose over the same data:",[57,678,680],{"className":97,"code":679,"language":99,"meta":62,"style":62},"T = np.eye(4, dtype=np.float32)\nT[:3, 3] = [5.0, 0.0, 0.0]\n\nmoved = mesh.shared_view()\nmoved.transformation = T\n\nface_id, dist2, closest = tf.neighbor_search(\n    moved, tf.Point(np.array([5.0, 0.0, 2.0], dtype=np.float32)))\n",[32,681,682,703,736,740,750,760,764,772],{"__ignoreMap":62},[66,683,684,687,689,692,694,696,698,700],{"class":68,"line":69},[66,685,686],{"class":110},"T ",[66,688,146],{"class":106},[66,690,691],{"class":110}," np.eye(",[66,693,636],{"class":211},[66,695,215],{"class":110},[66,697,229],{"class":228},[66,699,146],{"class":106},[66,701,702],{"class":110},"np.float32)\n",[66,704,705,708,710,712,714,717,719,722,725,727,729,731,733],{"class":68,"line":120},[66,706,707],{"class":110},"T[:",[66,709,373],{"class":211},[66,711,215],{"class":110},[66,713,373],{"class":211},[66,715,716],{"class":110},"] ",[66,718,146],{"class":106},[66,720,721],{"class":110}," [",[66,723,724],{"class":211},"5.0",[66,726,215],{"class":110},[66,728,212],{"class":211},[66,730,215],{"class":110},[66,732,212],{"class":211},[66,734,735],{"class":110},"]\n",[66,737,738],{"class":68,"line":133},[66,739,137],{"emptyLinePlaceholder":136},[66,741,742,745,747],{"class":68,"line":140},[66,743,744],{"class":110},"moved ",[66,746,146],{"class":106},[66,748,749],{"class":110}," mesh.shared_view()\n",[66,751,752,755,757],{"class":68,"line":393},[66,753,754],{"class":110},"moved.transformation ",[66,756,146],{"class":106},[66,758,759],{"class":110}," T\n",[66,761,762],{"class":68,"line":400},[66,763,137],{"emptyLinePlaceholder":136},[66,765,766,768,770],{"class":68,"line":406},[66,767,243],{"class":110},[66,769,146],{"class":106},[66,771,568],{"class":110},[66,773,775,778,780,782,784,786,788,790,792,794],{"class":68,"line":774},8,[66,776,777],{"class":110},"    moved, tf.Point(np.array([",[66,779,724],{"class":211},[66,781,215],{"class":110},[66,783,212],{"class":211},[66,785,215],{"class":110},[66,787,222],{"class":211},[66,789,225],{"class":110},[66,791,229],{"class":228},[66,793,146],{"class":106},[66,795,796],{"class":110},"np.float32)))\n",[14,798,799,801],{},[32,800,278],{}," is in world space.",[52,803,805],{"id":804},"working-with-results","Working with results",[14,807,808],{},"The face ids index straight into per-face data. Face normals are one lookup:",[57,810,812],{"className":97,"code":811,"language":99,"meta":62,"style":62},"face_ids, dist2s, closest_pts = tf.neighbor_search(mesh, tf.Point(queries))\nnormals = mesh.normals[face_ids]          # (100000, 3)\ndistances = np.sqrt(dist2s)\noffsets = queries - closest_pts           # vector from surface to query\n",[32,813,814,822,835,844],{"__ignoreMap":62},[66,815,816,818,820],{"class":68,"line":69},[66,817,385],{"class":110},[66,819,146],{"class":106},[66,821,390],{"class":110},[66,823,824,827,829,832],{"class":68,"line":120},[66,825,826],{"class":110},"normals ",[66,828,146],{"class":106},[66,830,831],{"class":110}," mesh.normals[face_ids]          ",[66,833,834],{"class":396},"# (100000, 3)\n",[66,836,837,839,841],{"class":68,"line":133},[66,838,460],{"class":110},[66,840,146],{"class":106},[66,842,843],{"class":110}," np.sqrt(dist2s)\n",[66,845,846,849,851,854,856,859],{"class":68,"line":140},[66,847,848],{"class":110},"offsets ",[66,850,146],{"class":106},[66,852,853],{"class":110}," queries ",[66,855,348],{"class":106},[66,857,858],{"class":110}," closest_pts           ",[66,860,861],{"class":396},"# vector from surface to query\n",[14,863,864],{},[47,865],{"alt":866,"src":867},"Step: working with results. Face normals looked up at the closest points, drawn as short pink lines on the dragon","\u002Fimg\u002Ftutorials\u002Fsteps\u002Ffast-closest-point-on-a-mesh-5-normals.png",[14,869,870,871,874,875,27],{},"Project points onto the surface by replacing them with ",[32,872,873],{},"closest_pts",". Report scan deviation as ",[32,876,877],{},"distances",[14,879,880,881,886,887,167,890,893,894,898],{},"The distance here is unsigned. For inside and outside, use ",[18,882,885],{"href":883,"rel":884},"https:\u002F\u002Ftrueform.polydera.com\u002Fpy\u002Fmodules\u002Fspatial",[88],"signed distance",". For the closest pair between two meshes, pass a second mesh as the query: ",[32,888,889],{},"tf.neighbor_search(mesh_a, mesh_b)",[32,891,892],{},"((id_a, id_b), (dist2, pt_a, pt_b))",". The ",[18,895,897],{"href":896},"\u002Ftutorials\u002Ffast-mesh-collision-detection-in-python","collision detection"," page covers that case.",[14,900,901,902,905,906,909,910,913,914,917],{},"For single primitives there is ",[32,903,904],{},"tf.closest_metric_point(a, b)",". It takes primitives such as a ",[32,907,908],{},"Triangle"," or ",[32,911,912],{},"Segment",", not a mesh, and returns ",[32,915,916],{},"(dist2, point_on_a)",":",[57,919,921],{"className":97,"code":920,"language":99,"meta":62,"style":62},"tri = tf.Triangle(a=np.array([0, 0, 0], dtype=np.float32),\n                  b=np.array([1, 0, 0], dtype=np.float32),\n                  c=np.array([0, 1, 0], dtype=np.float32))\ndist2, pt = tf.closest_metric_point(tri, tf.Point(queries))   # batch works too\n",[32,922,923,959,987,1014],{"__ignoreMap":62},[66,924,925,928,930,933,935,937,940,942,944,946,948,950,952,954,956],{"class":68,"line":69},[66,926,927],{"class":110},"tri ",[66,929,146],{"class":106},[66,931,932],{"class":110}," tf.Triangle(",[66,934,18],{"class":228},[66,936,146],{"class":106},[66,938,939],{"class":110},"np.array([",[66,941,332],{"class":211},[66,943,215],{"class":110},[66,945,332],{"class":211},[66,947,215],{"class":110},[66,949,332],{"class":211},[66,951,225],{"class":110},[66,953,229],{"class":228},[66,955,146],{"class":106},[66,957,958],{"class":110},"np.float32),\n",[66,960,961,964,966,968,971,973,975,977,979,981,983,985],{"class":68,"line":120},[66,962,963],{"class":228},"                  b",[66,965,146],{"class":106},[66,967,939],{"class":110},[66,969,970],{"class":211},"1",[66,972,215],{"class":110},[66,974,332],{"class":211},[66,976,215],{"class":110},[66,978,332],{"class":211},[66,980,225],{"class":110},[66,982,229],{"class":228},[66,984,146],{"class":106},[66,986,958],{"class":110},[66,988,989,992,994,996,998,1000,1002,1004,1006,1008,1010,1012],{"class":68,"line":133},[66,990,991],{"class":228},"                  c",[66,993,146],{"class":106},[66,995,939],{"class":110},[66,997,332],{"class":211},[66,999,215],{"class":110},[66,1001,970],{"class":211},[66,1003,215],{"class":110},[66,1005,332],{"class":211},[66,1007,225],{"class":110},[66,1009,229],{"class":228},[66,1011,146],{"class":106},[66,1013,234],{"class":110},[66,1015,1016,1019,1021,1024],{"class":68,"line":140},[66,1017,1018],{"class":110},"dist2, pt ",[66,1020,146],{"class":106},[66,1022,1023],{"class":110}," tf.closest_metric_point(tri, tf.Point(queries))   ",[66,1025,1026],{"class":396},"# batch works too\n",[52,1028,1030],{"id":1029},"related-tutorials","Related tutorials",[1032,1033,1034,1041,1048,1055],"ul",{},[1035,1036,1037,1040],"li",{},[18,1038,1039],{"href":896},"Fast and Robust Mesh Collision Detection in Python"," — do two meshes touch, how far apart, and where.",[1035,1042,1043,1047],{},[18,1044,1046],{"href":1045},"\u002Ftutorials\u002Ffast-geodesic-distances-in-python","Fast and Accurate Geodesic Distances in Python"," — distance along the surface from one seed or many.",[1035,1049,1050,1054],{},[18,1051,1053],{"href":1052},"\u002Ftutorials\u002Ffast-connected-components-in-python","Fast Connected Components in Python"," — split a mesh into parts and find its boundary loops.",[1035,1056,1057,1061],{},[18,1058,1060],{"href":1059},"\u002Ftutorials\u002Ffast-stl-and-obj-reading-in-python","Fast STL and OBJ Reading in Python"," — load and write meshes, the first step of every pipeline.",[52,1063,1065],{"id":1064},"performance","Performance",[14,1067,1068],{},"The benchmark runs 100,000 queries against each of 100 meshes and compares with libigl's AABB tree. Both trees are built before the timer starts.",[1070,1071],"headline-numbers",{":items":1072},"[{\"value\":\"91.5ms\",\"label\":\"100,000 closest points\",\"detail\":\"Median over 100 meshes\"},{\"value\":\"285ms\",\"label\":\"libigl\",\"detail\":\"Same queries, same meshes\"},{\"value\":\"3.7×\",\"label\":\"Faster than libigl\",\"detail\":\"Median wall time\"}]",[14,1074,1075,1080,1081],{},[66,1076,1079],{"className":1077},[1078],"tag-pill","Apple M4 Max"," ",[66,1082,1084],{"className":1083},[1078],"16 threads",[14,1086,1087],{},"The meshes range from 100,368 to 938,940 triangles. The query points fall in each mesh's bounding box, grown by 20 %, so some lie far from the surface.",[14,1089,1090],{},[47,1091],{"alt":1092,"src":1093},"Benchmark chart: trueform against other Python libraries, closest point","\u002Fimg\u002Falgorithms\u002Fbenchmark-closest-point-python-2026.png",[14,1095,1096,1100,1101],{},[18,1097,1099],{"href":1098},"\u002Ftrueform\u002Fbenchmarks","Full benchmarks"," · ",[18,1102,1105],{"href":1103,"rel":1104},"https:\u002F\u002Ftrueform.polydera.com\u002Fpy\u002Fmodules\u002Fspatial#neighbor-search",[88],"Documentation",[1107,1108],"cite-as",{"author":1109,"title":5},"Sajovic, {\\v{Z}}iga",[1111,1112,1113],"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 .s95oV, html code.shiki .s95oV{--shiki-default:#E1E4E8}html pre.shiki code .sDLfK, html code.shiki .sDLfK{--shiki-default:#79B8FF}html pre.shiki code .s9osk, html code.shiki .s9osk{--shiki-default:#FFAB70}html pre.shiki code .sAwPA, html code.shiki .sAwPA{--shiki-default:#6A737D}",{"title":62,"searchDepth":120,"depth":120,"links":1115},[1116,1117,1118,1119,1120,1121,1122,1123,1124,1125],{"id":54,"depth":120,"text":55},{"id":93,"depth":120,"text":94},{"id":186,"depth":120,"text":187},{"id":302,"depth":120,"text":303},{"id":471,"depth":120,"text":472},{"id":604,"depth":120,"text":605},{"id":672,"depth":120,"text":673},{"id":804,"depth":120,"text":805},{"id":1029,"depth":120,"text":1030},{"id":1064,"depth":120,"text":1065},"2026-10-31","Find the closest point on a triangle mesh, the face it lies on, and the distance, for one point or a million, in Python. One pip install, NumPy arrays in and out.","md",[1130,1131,1132,1133,1134,1135,1136,1137,1138],"closest point on mesh Python","point to mesh distance Python","nearest point on triangle mesh","numpy mesh distance","batch closest point query","trimesh closest point alternative","libigl point_mesh_squared_distance alternative","Open3D RaycastingScene compute_closest_points alternative","PyVista find_closest_cell alternative",{},"\u002Fimg\u002Fog\u002Ftutorials\u002Ffast-closest-point-on-a-mesh-in-python.png","\u002Ftutorials\u002Ffast-closest-point-on-a-mesh-in-python",{"title":5,"description":1127},"tutorials\u002Ffast-closest-point-on-a-mesh-in-python",[99,1145,1146,1147],"spatial","closest-point","tutorial","0cXuPxVfxBCraPgTPI-9nib_Z-ZmnQ3rEYSaFvX01ZM",1793517812008]