[{"data":1,"prerenderedAt":1496},["ShallowReactive",2],{"tutorial-fast-closest-point-on-a-mesh-in-javascript":3},{"id":4,"title":5,"author":6,"body":7,"date":1471,"description":1472,"extension":1473,"keywords":1474,"meta":1484,"navigation":151,"ogImage":1485,"path":1486,"published":151,"seo":1487,"stem":1488,"tags":1489,"__hash__":1495},"tutorials\u002Ftutorials\u002Ffast-closest-point-on-a-mesh-in-javascript.md","Fast and Robust Closest Point on a Mesh in JavaScript","Žiga Sajovic",{"type":8,"value":9,"toc":1459},"minimark",[10,28,40,44,51,56,81,94,98,185,188,249,268,272,278,369,375,379,394,576,582,592,595,628,635,660,664,679,748,754,763,814,817,821,830,876,882,897,901,904,993,1004,1008,1011,1087,1093,1110,1113,1205,1212,1334,1344,1366,1370,1401,1405,1408,1412,1428,1433,1436,1439,1451,1455],[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-python","Python"," and ",[18,24,26],{"href":25},"\u002Ftutorials\u002Ffast-closest-point-on-a-mesh-in-cpp","C++",".",[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 in JavaScript and TypeScript with one ",[32,37,38],{},"npm install",". It is WebAssembly-powered and runs in Node.js and the browser with the same API. Typed arrays in, typed 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\u002Fts\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","npm install @polydera\u002Ftrueform\n","bash","",[32,64,65],{"__ignoreMap":62},[66,67,70,74,78],"span",{"class":68,"line":69},"line",1,[66,71,73],{"class":72},"svObZ","npm",[66,75,77],{"class":76},"sU2Wk"," install",[66,79,80],{"class":76}," @polydera\u002Ftrueform\n",[14,82,83,84,87,88,27],{},"The package ships TypeScript types. The WebAssembly binary loads on first use. In the browser, multithreading needs ",[32,85,86],{},"SharedArrayBuffer"," headers; see the ",[18,89,93],{"href":90,"rel":91},"https:\u002F\u002Ftrueform.polydera.com\u002Fts\u002Fgetting-started\u002Finstallation",[92],"nofollow","installation guide",[52,95,97],{"id":96},"loading-the-mesh","Loading the mesh",[57,99,103],{"className":100,"code":101,"language":102,"meta":62,"style":62},"language-typescript shiki shiki-themes github-dark","import * as tf from \"@polydera\u002Ftrueform\";\nimport { readFileSync } from \"node:fs\";\n\nconst mesh = tf.readStl(readFileSync(\"part.stl\"));\n","typescript",[32,104,105,131,146,153],{"__ignoreMap":62},[66,106,107,111,115,118,122,125,128],{"class":68,"line":69},[66,108,110],{"class":109},"snl16","import",[66,112,114],{"class":113},"sDLfK"," *",[66,116,117],{"class":109}," as",[66,119,121],{"class":120},"s95oV"," tf ",[66,123,124],{"class":109},"from",[66,126,127],{"class":76}," \"@polydera\u002Ftrueform\"",[66,129,130],{"class":120},";\n",[66,132,134,136,139,141,144],{"class":68,"line":133},2,[66,135,110],{"class":109},[66,137,138],{"class":120}," { readFileSync } ",[66,140,124],{"class":109},[66,142,143],{"class":76}," \"node:fs\"",[66,145,130],{"class":120},[66,147,149],{"class":68,"line":148},3,[66,150,152],{"emptyLinePlaceholder":151},true,"\n",[66,154,156,159,162,165,168,171,174,177,179,182],{"class":68,"line":155},4,[66,157,158],{"class":109},"const",[66,160,161],{"class":113}," mesh",[66,163,164],{"class":109}," =",[66,166,167],{"class":120}," tf.",[66,169,170],{"class":72},"readStl",[66,172,173],{"class":120},"(",[66,175,176],{"class":72},"readFileSync",[66,178,173],{"class":120},[66,180,181],{"class":76},"\"part.stl\"",[66,183,184],{"class":120},"));\n",[14,186,187],{},"In the browser, fetch the bytes instead:",[57,189,191],{"className":100,"code":190,"language":102,"meta":62,"style":62},"const response = await fetch(\"part.stl\");\nconst mesh = tf.readStl(new Uint8Array(await response.arrayBuffer()));\n",[32,192,193,215],{"__ignoreMap":62},[66,194,195,197,200,202,205,208,210,212],{"class":68,"line":69},[66,196,158],{"class":109},[66,198,199],{"class":113}," response",[66,201,164],{"class":109},[66,203,204],{"class":109}," await",[66,206,207],{"class":72}," fetch",[66,209,173],{"class":120},[66,211,181],{"class":76},[66,213,214],{"class":120},");\n",[66,216,217,219,221,223,225,227,229,232,235,237,240,243,246],{"class":68,"line":133},[66,218,158],{"class":109},[66,220,161],{"class":113},[66,222,164],{"class":109},[66,224,167],{"class":120},[66,226,170],{"class":72},[66,228,173],{"class":120},[66,230,231],{"class":109},"new",[66,233,234],{"class":72}," Uint8Array",[66,236,173],{"class":120},[66,238,239],{"class":109},"await",[66,241,242],{"class":120}," response.",[66,244,245],{"class":72},"arrayBuffer",[66,247,248],{"class":120},"()));\n",[14,250,251,254,255,258,259,262,263,267],{},[32,252,253],{},"tf.readObj"," works the same way. If you already have arrays, build the mesh with ",[32,256,257],{},"tf.mesh(faces, points)",". Points are ",[32,260,261],{},"float32"," by default. See ",[18,264,266],{"href":265},"\u002Ftutorials\u002Ffast-stl-and-obj-reading-in-javascript","fast STL and OBJ reading"," for the details.",[52,269,271],{"id":270},"one-point","One point",[14,273,274,277],{},[32,275,276],{},"tf.neighborSearch"," finds the nearest face of the mesh to a query point:",[57,279,281],{"className":100,"code":280,"language":102,"meta":62,"style":62},"const r = tf.neighborSearch(mesh, tf.point(0, 0, 2));\n\nr.elementId;            \u002F\u002F index of the nearest face\nr.point;                \u002F\u002F NDArrayFloat32 [3], the point on that face\nr.distance2;            \u002F\u002F squared distance\nconst distance = Math.sqrt(r.distance2);\n",[32,282,283,320,324,333,341,350],{"__ignoreMap":62},[66,284,285,287,290,292,294,297,300,303,305,308,311,313,315,318],{"class":68,"line":69},[66,286,158],{"class":109},[66,288,289],{"class":113}," r",[66,291,164],{"class":109},[66,293,167],{"class":120},[66,295,296],{"class":72},"neighborSearch",[66,298,299],{"class":120},"(mesh, tf.",[66,301,302],{"class":72},"point",[66,304,173],{"class":120},[66,306,307],{"class":113},"0",[66,309,310],{"class":120},", ",[66,312,307],{"class":113},[66,314,310],{"class":120},[66,316,317],{"class":113},"2",[66,319,184],{"class":120},[66,321,322],{"class":68,"line":133},[66,323,152],{"emptyLinePlaceholder":151},[66,325,326,329],{"class":68,"line":148},[66,327,328],{"class":120},"r.elementId;            ",[66,330,332],{"class":331},"sAwPA","\u002F\u002F index of the nearest face\n",[66,334,335,338],{"class":68,"line":155},[66,336,337],{"class":120},"r.point;                ",[66,339,340],{"class":331},"\u002F\u002F NDArrayFloat32 [3], the point on that face\n",[66,342,344,347],{"class":68,"line":343},5,[66,345,346],{"class":120},"r.distance2;            ",[66,348,349],{"class":331},"\u002F\u002F squared distance\n",[66,351,353,355,358,360,363,366],{"class":68,"line":352},6,[66,354,158],{"class":109},[66,356,357],{"class":113}," distance",[66,359,164],{"class":109},[66,361,362],{"class":120}," Math.",[66,364,365],{"class":72},"sqrt",[66,367,368],{"class":120},"(r.distance2);\n",[14,370,371],{},[47,372],{"alt":373,"src":374},"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",[52,376,378],{"id":377},"many-points-at-once","Many points at once",[14,380,381,382,385,386,389,390,393],{},"Pack the queries into one flat ",[32,383,384],{},"Float32Array"," and wrap it with ",[32,387,388],{},"tf.point(data, 3)",". The ",[32,391,392],{},"3"," is the number of coordinates per point, not the number of points. One call runs all queries in parallel against the mesh's spatial tree.",[57,395,397],{"className":100,"code":396,"language":102,"meta":62,"style":62},"const n = 100_000;\nconst data = new Float32Array(3 * n);\nfor (let i = 0; i \u003C data.length; i++) data[i] = Math.random() * 3 - 1.5;\nconst queries = tf.point(data, 3);\n\nconst { elementIds, points, distances } = tf.neighborSearch(mesh, queries);\n\u002F\u002F elementIds: NDArrayInt32   [100000]\n\u002F\u002F points:     NDArrayFloat32 [100000, 3]\n\u002F\u002F distances:  NDArrayFloat32 [100000], squared distances\n",[32,398,399,413,437,502,522,526,558,564,570],{"__ignoreMap":62},[66,400,401,403,406,408,411],{"class":68,"line":69},[66,402,158],{"class":109},[66,404,405],{"class":113}," n",[66,407,164],{"class":109},[66,409,410],{"class":113}," 100_000",[66,412,130],{"class":120},[66,414,415,417,420,422,425,428,430,432,434],{"class":68,"line":133},[66,416,158],{"class":109},[66,418,419],{"class":113}," data",[66,421,164],{"class":109},[66,423,424],{"class":109}," new",[66,426,427],{"class":72}," Float32Array",[66,429,173],{"class":120},[66,431,392],{"class":113},[66,433,114],{"class":109},[66,435,436],{"class":120}," n);\n",[66,438,439,442,445,448,451,454,457,460,463,466,469,472,475,478,480,482,485,488,491,494,497,500],{"class":68,"line":148},[66,440,441],{"class":109},"for",[66,443,444],{"class":120}," (",[66,446,447],{"class":109},"let",[66,449,450],{"class":120}," i ",[66,452,453],{"class":109},"=",[66,455,456],{"class":113}," 0",[66,458,459],{"class":120},"; i ",[66,461,462],{"class":109},"\u003C",[66,464,465],{"class":120}," data.",[66,467,468],{"class":113},"length",[66,470,471],{"class":120},"; i",[66,473,474],{"class":109},"++",[66,476,477],{"class":120},") data[i] ",[66,479,453],{"class":109},[66,481,362],{"class":120},[66,483,484],{"class":72},"random",[66,486,487],{"class":120},"() ",[66,489,490],{"class":109},"*",[66,492,493],{"class":113}," 3",[66,495,496],{"class":109}," -",[66,498,499],{"class":113}," 1.5",[66,501,130],{"class":120},[66,503,504,506,509,511,513,515,518,520],{"class":68,"line":155},[66,505,158],{"class":109},[66,507,508],{"class":113}," queries",[66,510,164],{"class":109},[66,512,167],{"class":120},[66,514,302],{"class":72},[66,516,517],{"class":120},"(data, ",[66,519,392],{"class":113},[66,521,214],{"class":120},[66,523,524],{"class":68,"line":343},[66,525,152],{"emptyLinePlaceholder":151},[66,527,528,530,533,536,538,541,543,546,549,551,553,555],{"class":68,"line":352},[66,529,158],{"class":109},[66,531,532],{"class":120}," { ",[66,534,535],{"class":113},"elementIds",[66,537,310],{"class":120},[66,539,540],{"class":113},"points",[66,542,310],{"class":120},[66,544,545],{"class":113},"distances",[66,547,548],{"class":120}," } ",[66,550,453],{"class":109},[66,552,167],{"class":120},[66,554,296],{"class":72},[66,556,557],{"class":120},"(mesh, queries);\n",[66,559,561],{"class":68,"line":560},7,[66,562,563],{"class":331},"\u002F\u002F elementIds: NDArrayInt32   [100000]\n",[66,565,567],{"class":68,"line":566},8,[66,568,569],{"class":331},"\u002F\u002F points:     NDArrayFloat32 [100000, 3]\n",[66,571,573],{"class":68,"line":572},9,[66,574,575],{"class":331},"\u002F\u002F distances:  NDArrayFloat32 [100000], squared distances\n",[14,577,578],{},[47,579],{"alt":580,"src":581},"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,583,584,585,587,588,591],{},"In a batch, ",[32,586,545],{}," holds squared distances, the same as ",[32,589,590],{},"distance2"," for a single query.",[14,593,594],{},"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,596,598],{"className":100,"code":597,"language":102,"meta":62,"style":62},"mesh.buildTree();\n\u002F\u002F or off the main thread\nawait tf.async.buildTree(mesh);\n",[32,599,600,611,616],{"__ignoreMap":62},[66,601,602,605,608],{"class":68,"line":69},[66,603,604],{"class":120},"mesh.",[66,606,607],{"class":72},"buildTree",[66,609,610],{"class":120},"();\n",[66,612,613],{"class":68,"line":133},[66,614,615],{"class":331},"\u002F\u002F or off the main thread\n",[66,617,618,620,623,625],{"class":68,"line":148},[66,619,239],{"class":109},[66,621,622],{"class":120}," tf.async.",[66,624,607],{"class":72},[66,626,627],{"class":120},"(mesh);\n",[14,629,630,631,634],{},"If you only need distances, ",[32,632,633],{},"tf.distance"," takes the same batch and returns plain (not squared) distances:",[57,636,638],{"className":100,"code":637,"language":102,"meta":62,"style":62},"const d = tf.distance(mesh, queries);   \u002F\u002F NDArrayFloat32 [100000]\n",[32,639,640],{"__ignoreMap":62},[66,641,642,644,647,649,651,654,657],{"class":68,"line":69},[66,643,158],{"class":109},[66,645,646],{"class":113}," d",[66,648,164],{"class":109},[66,650,167],{"class":120},[66,652,653],{"class":72},"distance",[66,655,656],{"class":120},"(mesh, queries);   ",[66,658,659],{"class":331},"\u002F\u002F NDArrayFloat32 [100000]\n",[52,661,663],{"id":662},"search-within-a-radius","Search within a radius",[14,665,666,667,670,671,674,675,678],{},"Pass ",[32,668,669],{},"radius"," to ignore anything farther away. A miss comes back with ",[32,672,673],{},"elementId"," set to ",[32,676,677],{},"-1",":",[57,680,682],{"className":100,"code":681,"language":102,"meta":62,"style":62},"const near = tf.neighborSearch(mesh, tf.point(0, 0, 2), { radius: 0.5 });\nif (near.elementId >= 0) {\n  \u002F\u002F a face lies within 0.5\n}\n",[32,683,684,722,738,743],{"__ignoreMap":62},[66,685,686,688,691,693,695,697,699,701,703,705,707,709,711,713,716,719],{"class":68,"line":69},[66,687,158],{"class":109},[66,689,690],{"class":113}," near",[66,692,164],{"class":109},[66,694,167],{"class":120},[66,696,296],{"class":72},[66,698,299],{"class":120},[66,700,302],{"class":72},[66,702,173],{"class":120},[66,704,307],{"class":113},[66,706,310],{"class":120},[66,708,307],{"class":113},[66,710,310],{"class":120},[66,712,317],{"class":113},[66,714,715],{"class":120},"), { radius: ",[66,717,718],{"class":113},"0.5",[66,720,721],{"class":120}," });\n",[66,723,724,727,730,733,735],{"class":68,"line":133},[66,725,726],{"class":109},"if",[66,728,729],{"class":120}," (near.elementId ",[66,731,732],{"class":109},">=",[66,734,456],{"class":113},[66,736,737],{"class":120},") {\n",[66,739,740],{"class":68,"line":148},[66,741,742],{"class":331},"  \u002F\u002F a face lies within 0.5\n",[66,744,745],{"class":68,"line":155},[66,746,747],{"class":120},"}\n",[14,749,750],{},[47,751],{"alt":752,"src":753},"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,755,756,757,759,760,762],{},"In a batch, misses are ",[32,758,677],{}," in ",[32,761,535],{},". Test the ids, not the distances:",[57,764,766],{"className":100,"code":765,"language":102,"meta":62,"style":62},"const nb = tf.neighborSearch(mesh, queries, { radius: 0.1 });\nconst inRange = nb.elementIds.gte(0);   \u002F\u002F NDArrayBool [100000]\n",[32,767,768,789],{"__ignoreMap":62},[66,769,770,772,775,777,779,781,784,787],{"class":68,"line":69},[66,771,158],{"class":109},[66,773,774],{"class":113}," nb",[66,776,164],{"class":109},[66,778,167],{"class":120},[66,780,296],{"class":72},[66,782,783],{"class":120},"(mesh, queries, { radius: ",[66,785,786],{"class":113},"0.1",[66,788,721],{"class":120},[66,790,791,793,796,798,801,804,806,808,811],{"class":68,"line":133},[66,792,158],{"class":109},[66,794,795],{"class":113}," inRange",[66,797,164],{"class":109},[66,799,800],{"class":120}," nb.elementIds.",[66,802,803],{"class":72},"gte",[66,805,173],{"class":120},[66,807,307],{"class":113},[66,809,810],{"class":120},");   ",[66,812,813],{"class":331},"\u002F\u002F NDArrayBool [100000]\n",[14,815,816],{},"A tight radius also prunes the search. Use it when you know the scale you care about, as in snapping or contact checks.",[52,818,820],{"id":819},"k-nearest-faces","k nearest faces",[14,822,666,823,826,827,829],{},[32,824,825],{},"k"," to get the ",[32,828,825],{}," nearest faces per query:",[57,831,833],{"className":100,"code":832,"language":102,"meta":62,"style":62},"const knn = tf.neighborSearch(mesh, queries, { k: 4 });\n\u002F\u002F knn.elementIds: [100000, 4]\n\u002F\u002F knn.points:     [100000, 4, 3]\n\u002F\u002F knn.distances:  [100000, 4]\n\u002F\u002F knn.counts:     [100000], valid neighbors per query\n",[32,834,835,856,861,866,871],{"__ignoreMap":62},[66,836,837,839,842,844,846,848,851,854],{"class":68,"line":69},[66,838,158],{"class":109},[66,840,841],{"class":113}," knn",[66,843,164],{"class":109},[66,845,167],{"class":120},[66,847,296],{"class":72},[66,849,850],{"class":120},"(mesh, queries, { k: ",[66,852,853],{"class":113},"4",[66,855,721],{"class":120},[66,857,858],{"class":68,"line":133},[66,859,860],{"class":331},"\u002F\u002F knn.elementIds: [100000, 4]\n",[66,862,863],{"class":68,"line":148},[66,864,865],{"class":331},"\u002F\u002F knn.points:     [100000, 4, 3]\n",[66,867,868],{"class":68,"line":155},[66,869,870],{"class":331},"\u002F\u002F knn.distances:  [100000, 4]\n",[66,872,873],{"class":68,"line":343},[66,874,875],{"class":331},"\u002F\u002F knn.counts:     [100000], valid neighbors per query\n",[14,877,878],{},[47,879],{"alt":880,"src":881},"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,883,884,885,887,888,891,892,894,895,27],{},"With ",[32,886,669],{}," set, ",[32,889,890],{},"counts[i]"," can be less than ",[32,893,825],{},". Unused id slots hold ",[32,896,677],{},[52,898,900],{"id":899},"posed-meshes","Posed meshes",[14,902,903],{},"Set a 4×4 transformation on the mesh and queries run in world space. The tree is not rebuilt. A shallow copy shares the data and carries its own pose:",[57,905,907],{"className":100,"code":906,"language":102,"meta":62,"style":62},"const moved = mesh.shallowCopy();\nmoved.transformation = tf.makeTranslation(5, 0, 0);\n\nconst r = tf.neighborSearch(moved, tf.point(5, 0, 2));\n\u002F\u002F r.point is in world space\n",[32,908,909,926,953,957,988],{"__ignoreMap":62},[66,910,911,913,916,918,921,924],{"class":68,"line":69},[66,912,158],{"class":109},[66,914,915],{"class":113}," moved",[66,917,164],{"class":109},[66,919,920],{"class":120}," mesh.",[66,922,923],{"class":72},"shallowCopy",[66,925,610],{"class":120},[66,927,928,931,933,935,938,940,943,945,947,949,951],{"class":68,"line":133},[66,929,930],{"class":120},"moved.transformation ",[66,932,453],{"class":109},[66,934,167],{"class":120},[66,936,937],{"class":72},"makeTranslation",[66,939,173],{"class":120},[66,941,942],{"class":113},"5",[66,944,310],{"class":120},[66,946,307],{"class":113},[66,948,310],{"class":120},[66,950,307],{"class":113},[66,952,214],{"class":120},[66,954,955],{"class":68,"line":148},[66,956,152],{"emptyLinePlaceholder":151},[66,958,959,961,963,965,967,969,972,974,976,978,980,982,984,986],{"class":68,"line":155},[66,960,158],{"class":109},[66,962,289],{"class":113},[66,964,164],{"class":109},[66,966,167],{"class":120},[66,968,296],{"class":72},[66,970,971],{"class":120},"(moved, tf.",[66,973,302],{"class":72},[66,975,173],{"class":120},[66,977,942],{"class":113},[66,979,310],{"class":120},[66,981,307],{"class":113},[66,983,310],{"class":120},[66,985,317],{"class":113},[66,987,184],{"class":120},[66,989,990],{"class":68,"line":343},[66,991,992],{"class":331},"\u002F\u002F r.point is in world space\n",[14,994,995,996,999,1000,1003],{},"Matrices are row-major. Three.js uses column-major, so transpose with ",[32,997,998],{},".T"," when you pass a ",[32,1001,1002],{},"Matrix4"," in.",[52,1005,1007],{"id":1006},"working-with-results","Working with results",[14,1009,1010],{},"The face ids index straight into per-face data. Face normals are one gather:",[57,1012,1014],{"className":100,"code":1013,"language":102,"meta":62,"style":62},"const normals = mesh.normals.take(elementIds);                 \u002F\u002F [100000, 3]\nconst offsets = tf.ndarray(data, [n, 3]).sub(points);          \u002F\u002F surface to query\nconst dist = tf.sqrt(distances);                               \u002F\u002F [100000]\n",[32,1015,1016,1037,1068],{"__ignoreMap":62},[66,1017,1018,1020,1023,1025,1028,1031,1034],{"class":68,"line":69},[66,1019,158],{"class":109},[66,1021,1022],{"class":113}," normals",[66,1024,164],{"class":109},[66,1026,1027],{"class":120}," mesh.normals.",[66,1029,1030],{"class":72},"take",[66,1032,1033],{"class":120},"(elementIds);                 ",[66,1035,1036],{"class":331},"\u002F\u002F [100000, 3]\n",[66,1038,1039,1041,1044,1046,1048,1051,1054,1056,1059,1062,1065],{"class":68,"line":133},[66,1040,158],{"class":109},[66,1042,1043],{"class":113}," offsets",[66,1045,164],{"class":109},[66,1047,167],{"class":120},[66,1049,1050],{"class":72},"ndarray",[66,1052,1053],{"class":120},"(data, [n, ",[66,1055,392],{"class":113},[66,1057,1058],{"class":120},"]).",[66,1060,1061],{"class":72},"sub",[66,1063,1064],{"class":120},"(points);          ",[66,1066,1067],{"class":331},"\u002F\u002F surface to query\n",[66,1069,1070,1072,1075,1077,1079,1081,1084],{"class":68,"line":148},[66,1071,158],{"class":109},[66,1073,1074],{"class":113}," dist",[66,1076,164],{"class":109},[66,1078,167],{"class":120},[66,1080,365],{"class":72},[66,1082,1083],{"class":120},"(distances);                               ",[66,1085,1086],{"class":331},"\u002F\u002F [100000]\n",[14,1088,1089],{},[47,1090],{"alt":1091,"src":1092},"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,1094,1095,1096,1098,1099,1102,1103,1106,1107,27],{},"Project points onto the surface by replacing them with ",[32,1097,540],{},". Report scan deviation as ",[32,1100,1101],{},"dist",". Every array exposes its typed array as ",[32,1104,1105],{},".data",", ready for a Three.js ",[32,1108,1109],{},"BufferAttribute",[14,1111,1112],{},"The distance here is unsigned. For the closest pair between two meshes, pass a second mesh as the query:",[57,1114,1116],{"className":100,"code":1115,"language":102,"meta":62,"style":62},"const other = mesh.shallowCopy();\nother.transformation = tf.makeTranslation(3, 0, 0);\n\nconst { elementId0, elementId1, point0, point1, distance2 } =\n  tf.neighborSearch(mesh, other);\n",[32,1117,1118,1133,1158,1162,1195],{"__ignoreMap":62},[66,1119,1120,1122,1125,1127,1129,1131],{"class":68,"line":69},[66,1121,158],{"class":109},[66,1123,1124],{"class":113}," other",[66,1126,164],{"class":109},[66,1128,920],{"class":120},[66,1130,923],{"class":72},[66,1132,610],{"class":120},[66,1134,1135,1138,1140,1142,1144,1146,1148,1150,1152,1154,1156],{"class":68,"line":133},[66,1136,1137],{"class":120},"other.transformation ",[66,1139,453],{"class":109},[66,1141,167],{"class":120},[66,1143,937],{"class":72},[66,1145,173],{"class":120},[66,1147,392],{"class":113},[66,1149,310],{"class":120},[66,1151,307],{"class":113},[66,1153,310],{"class":120},[66,1155,307],{"class":113},[66,1157,214],{"class":120},[66,1159,1160],{"class":68,"line":148},[66,1161,152],{"emptyLinePlaceholder":151},[66,1163,1164,1166,1168,1171,1173,1176,1178,1181,1183,1186,1188,1190,1192],{"class":68,"line":155},[66,1165,158],{"class":109},[66,1167,532],{"class":120},[66,1169,1170],{"class":113},"elementId0",[66,1172,310],{"class":120},[66,1174,1175],{"class":113},"elementId1",[66,1177,310],{"class":120},[66,1179,1180],{"class":113},"point0",[66,1182,310],{"class":120},[66,1184,1185],{"class":113},"point1",[66,1187,310],{"class":120},[66,1189,590],{"class":113},[66,1191,548],{"class":120},[66,1193,1194],{"class":109},"=\n",[66,1196,1197,1200,1202],{"class":68,"line":343},[66,1198,1199],{"class":120},"  tf.",[66,1201,296],{"class":72},[66,1203,1204],{"class":120},"(mesh, other);\n",[14,1206,1207,1208,1211],{},"For single primitives such as a triangle or a segment, ",[32,1209,1210],{},"tf.closestPointPair"," returns the closest point on each side:",[57,1213,1215],{"className":100,"code":1214,"language":102,"meta":62,"style":62},"const tri = tf.triangle(tf.point(0, 0, 0), tf.point(1, 0, 0), tf.point(0, 1, 0));\nconst { point0, point1, distance2 } = tf.closestPointPair(tf.point(0.2, 0.2, 1), tri);\n\u002F\u002F point1 is on the triangle\n",[32,1216,1217,1284,1329],{"__ignoreMap":62},[66,1218,1219,1221,1224,1226,1228,1231,1234,1236,1238,1240,1242,1244,1246,1248,1251,1253,1255,1258,1260,1262,1264,1266,1268,1270,1272,1274,1276,1278,1280,1282],{"class":68,"line":69},[66,1220,158],{"class":109},[66,1222,1223],{"class":113}," tri",[66,1225,164],{"class":109},[66,1227,167],{"class":120},[66,1229,1230],{"class":72},"triangle",[66,1232,1233],{"class":120},"(tf.",[66,1235,302],{"class":72},[66,1237,173],{"class":120},[66,1239,307],{"class":113},[66,1241,310],{"class":120},[66,1243,307],{"class":113},[66,1245,310],{"class":120},[66,1247,307],{"class":113},[66,1249,1250],{"class":120},"), tf.",[66,1252,302],{"class":72},[66,1254,173],{"class":120},[66,1256,1257],{"class":113},"1",[66,1259,310],{"class":120},[66,1261,307],{"class":113},[66,1263,310],{"class":120},[66,1265,307],{"class":113},[66,1267,1250],{"class":120},[66,1269,302],{"class":72},[66,1271,173],{"class":120},[66,1273,307],{"class":113},[66,1275,310],{"class":120},[66,1277,1257],{"class":113},[66,1279,310],{"class":120},[66,1281,307],{"class":113},[66,1283,184],{"class":120},[66,1285,1286,1288,1290,1292,1294,1296,1298,1300,1302,1304,1306,1309,1311,1313,1315,1318,1320,1322,1324,1326],{"class":68,"line":133},[66,1287,158],{"class":109},[66,1289,532],{"class":120},[66,1291,1180],{"class":113},[66,1293,310],{"class":120},[66,1295,1185],{"class":113},[66,1297,310],{"class":120},[66,1299,590],{"class":113},[66,1301,548],{"class":120},[66,1303,453],{"class":109},[66,1305,167],{"class":120},[66,1307,1308],{"class":72},"closestPointPair",[66,1310,1233],{"class":120},[66,1312,302],{"class":72},[66,1314,173],{"class":120},[66,1316,1317],{"class":113},"0.2",[66,1319,310],{"class":120},[66,1321,1317],{"class":113},[66,1323,310],{"class":120},[66,1325,1257],{"class":113},[66,1327,1328],{"class":120},"), tri);\n",[66,1330,1331],{"class":68,"line":148},[66,1332,1333],{"class":331},"\u002F\u002F point1 is on the triangle\n",[14,1335,1336,1337,1340,1341,678],{},"Every query has an async twin under ",[32,1338,1339],{},"tf.async"," that runs off the main thread and returns a ",[32,1342,1343],{},"Promise",[57,1345,1347],{"className":100,"code":1346,"language":102,"meta":62,"style":62},"const res = await tf.async.neighborSearch(mesh, queries);\n",[32,1348,1349],{"__ignoreMap":62},[66,1350,1351,1353,1356,1358,1360,1362,1364],{"class":68,"line":69},[66,1352,158],{"class":109},[66,1354,1355],{"class":113}," res",[66,1357,164],{"class":109},[66,1359,204],{"class":109},[66,1361,622],{"class":120},[66,1363,296],{"class":72},[66,1365,557],{"class":120},[52,1367,1369],{"id":1368},"related-tutorials","Related tutorials",[1371,1372,1373,1381,1388,1395],"ul",{},[1374,1375,1376,1380],"li",{},[18,1377,1379],{"href":1378},"\u002Ftutorials\u002Ffast-mesh-collision-detection-in-javascript","Fast and Robust Mesh Collision Detection in JavaScript"," — do two meshes touch, how far apart, and where.",[1374,1382,1383,1387],{},[18,1384,1386],{"href":1385},"\u002Ftutorials\u002Ffast-geodesic-distances-in-javascript","Fast and Accurate Geodesic Distances in JavaScript"," — distance along the surface from one seed or many.",[1374,1389,1390,1394],{},[18,1391,1393],{"href":1392},"\u002Ftutorials\u002Ffast-connected-components-in-javascript","Fast Connected Components in JavaScript"," — split a mesh into parts and find its boundary loops.",[1374,1396,1397,1400],{},[18,1398,1399],{"href":265},"Fast STL and OBJ Reading in JavaScript"," — load and write meshes, the first step of every pipeline.",[52,1402,1404],{"id":1403},"performance","Performance",[14,1406,1407],{},"We measured one batch of 100,000 query points against one mesh, on 100 Thingi10K meshes (100,368 to 938,940 faces) and the dragon at five sizes. The points fill the mesh's bounding box grown by 20 percent, so some lie far from the surface. Each query returns the distance to the closest point.",[1409,1410],"headline-numbers",{":items":1411},"[{\"value\":\"78ms\",\"label\":\"100,000 closest points\",\"detail\":\"1,025,766 faces, tree build included\"},{\"value\":\"67ms\",\"label\":\"Queries only\",\"detail\":\"1,025,766 faces, tree already built\"},{\"value\":\"73ms\",\"label\":\"Typical, 100 meshes\",\"detail\":\"Geometric mean, tree build included\"}]",[14,1413,1414,1419,1420,1419,1424],{},[66,1415,1418],{"className":1416},[1417],"tag-pill","Apple M4 Max"," ",[66,1421,1423],{"className":1422},[1417],"16 threads",[66,1425,1427],{"className":1426},[1417],"Node.js 25 · WebAssembly",[1429,1430],"data-table",{":headers":1431,":highlight":307,":rows":1432},"[\"Faces\",\"Meshes\",\"Build + query\",\"Query only\"]","[[\"100,368–145,418\",\"40\",\"62.3 ms\",\"60.7 ms\"],[\"153,248–242,390\",\"26\",\"76.8 ms\",\"75.2 ms\"],[\"255,508–498,974\",\"16\",\"72.9 ms\",\"69.2 ms\"],[\"587,688–938,940\",\"18\",\"94.9 ms\",\"88.5 ms\"],[\"Dragon, 58,298\",\"1\",\"23.8 ms\",\"22.4 ms\"],[\"Dragon, 141,966\",\"1\",\"35.9 ms\",\"33.3 ms\"],[\"Dragon, 244,026\",\"1\",\"38.8 ms\",\"35.9 ms\"],[\"Dragon, 480,868\",\"1\",\"59.2 ms\",\"53.3 ms\"],[\"Dragon, 1,025,766\",\"1\",\"78.4 ms\",\"66.9 ms\"]]",[14,1434,1435],{},"The Thingi10K rows are geometric means over the meshes in each size range. Build + query starts from the vertex and index arrays and includes building the mesh and its tree. Query only starts with the tree built. Times differ between meshes of the same size because the points fall nearer to or farther from the surface. trueform ran on its WebAssembly thread pool of 16 workers. No peer library in JavaScript was timed.",[14,1437,1438],{},"Best of the timed runs after warm-ups, measured 2026-11-01 with trueform 0.10.8; harness and raw results: benchmarks\u002Ftutorials\u002Fjavascript-closest-point.",[14,1440,1441,1445,1446],{},[18,1442,1444],{"href":1443},"\u002Ftrueform\u002Fbenchmarks","Full benchmarks"," · ",[18,1447,1450],{"href":1448,"rel":1449},"https:\u002F\u002Ftrueform.polydera.com\u002Fts\u002Fmodules\u002Fspatial#neighbor-search",[92],"Documentation",[1452,1453],"cite-as",{"author":1454,"title":5},"Sajovic, {\\v{Z}}iga",[1456,1457,1458],"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}",{"title":62,"searchDepth":133,"depth":133,"links":1460},[1461,1462,1463,1464,1465,1466,1467,1468,1469,1470],{"id":54,"depth":133,"text":55},{"id":96,"depth":133,"text":97},{"id":270,"depth":133,"text":271},{"id":377,"depth":133,"text":378},{"id":662,"depth":133,"text":663},{"id":819,"depth":133,"text":820},{"id":899,"depth":133,"text":900},{"id":1006,"depth":133,"text":1007},{"id":1368,"depth":133,"text":1369},{"id":1403,"depth":133,"text":1404},"2026-10-31","Find the closest point on a triangle mesh, the face it lies on, and the distance, for one point or a hundred thousand, in JavaScript and TypeScript. WebAssembly-powered, runs in Node.js and the browser.","md",[1475,1476,1477,1478,1479,1480,1481,1482,1483],"closest point on mesh JavaScript","closest point on mesh TypeScript","point to mesh distance JavaScript","nearest point on triangle mesh browser","WebAssembly closest point","npm closest point mesh","three-mesh-bvh closestPointToPoint alternative","three.js closest point on mesh","BVH nearest point JavaScript",{},"\u002Fimg\u002Fog\u002Ftutorials\u002Ffast-closest-point-on-a-mesh-in-javascript.png","\u002Ftutorials\u002Ffast-closest-point-on-a-mesh-in-javascript",{"title":5,"description":1472},"tutorials\u002Ffast-closest-point-on-a-mesh-in-javascript",[1490,102,1491,1492,1493,1494],"javascript","spatial","closest-point","tutorial","webassembly","I287IaDkMc8KNYEQ8e2FCxRTRJy-rFYoSCPPWzZsJz0",1793517812008]