[{"data":1,"prerenderedAt":1386},["ShallowReactive",2],{"tutorial-fast-mesh-collision-detection-in-javascript":3},{"id":4,"title":5,"author":6,"body":7,"date":1361,"description":1362,"extension":1363,"keywords":1364,"meta":1375,"navigation":151,"ogImage":1376,"path":1377,"published":151,"seo":1378,"stem":1379,"tags":1380,"__hash__":1385},"tutorials\u002Ftutorials\u002Ffast-mesh-collision-detection-in-javascript.md","Fast and Robust Mesh Collision Detection in JavaScript","Žiga Sajovic",{"type":8,"value":9,"toc":1350},"minimark",[10,28,40,44,51,56,81,94,98,211,218,297,300,304,321,378,384,391,425,429,443,527,533,547,561,565,575,630,636,647,713,716,844,848,855,914,921,925,928,1057,1064,1197,1200,1239,1243,1273,1277,1287,1291,1307,1312,1315,1318,1324,1330,1342,1346],[11,12,13],"note",{},[14,15,16,17,22,23,27],"p",{},"Also available in ",[18,19,21],"a",{"href":20},"\u002Ftutorials\u002Ffast-mesh-collision-detection-in-python","Python"," and ",[18,24,26],{"href":25},"\u002Ftutorials\u002Ffast-mesh-collision-detection-in-cpp","C++",".",[14,29,30,31,35,36,39],{},"Collision detection asks three questions about two meshes. Do they touch? If not, how far apart are they? Where is the contact? ",[32,33,34],"code",{},"trueform"," answers all three on triangle meshes, with each mesh in its own pose. One ",[32,37,38],{},"npm install",". It runs as WebAssembly in Node.js and the browser, with TypeScript types. This tutorial covers the test, the clearance, the closest pair, the contact curves, and moving a mesh without rebuilding anything.",[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#intersection\",\"variant\":\"soft\",\"color\":\"neutral\"},{\"label\":\"Try it live\",\"to\":\"https:\u002F\u002Ftrueform.polydera.com\u002Flive-examples\u002Fcollision\",\"icon\":\"i-lucide-play\"}]",[14,45,46],{},[47,48],"img",{"alt":49,"src":50},"A sphere near the Stanford dragon with the closest pair of points between them","\u002Fimg\u002Ftutorials\u002Fcollision-detection.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 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-meshes","Loading the meshes",[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 fixture = tf.readStl(readFileSync(\"fixture.stl\"));\nconst part = tf.readObj(readFileSync(\"part.obj\"));\n","typescript",[32,104,105,131,146,153,185],{"__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}," fixture",[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},"\"fixture.stl\"",[66,183,184],{"class":120},"));\n",[66,186,188,190,193,195,197,200,202,204,206,209],{"class":68,"line":187},5,[66,189,158],{"class":109},[66,191,192],{"class":113}," part",[66,194,164],{"class":109},[66,196,167],{"class":120},[66,198,199],{"class":72},"readObj",[66,201,173],{"class":120},[66,203,176],{"class":72},[66,205,173],{"class":120},[66,207,208],{"class":76},"\"part.obj\"",[66,210,184],{"class":120},[14,212,213,214,217],{},"In the browser, pass the bytes from ",[32,215,216],{},"fetch"," instead. To follow along without files, use a subdivided box as the fixed mesh and a small sphere as the moving one:",[57,219,221],{"className":100,"code":220,"language":102,"meta":62,"style":62},"const fixed = tf.boxMesh(2.0, 2.0, 2.0, 10, 10, 10);\nconst moving = tf.sphereMesh(0.5, 30, 30);\n",[32,222,223,267],{"__ignoreMap":62},[66,224,225,227,230,232,234,237,239,242,245,247,249,251,253,256,258,260,262,264],{"class":68,"line":69},[66,226,158],{"class":109},[66,228,229],{"class":113}," fixed",[66,231,164],{"class":109},[66,233,167],{"class":120},[66,235,236],{"class":72},"boxMesh",[66,238,173],{"class":120},[66,240,241],{"class":113},"2.0",[66,243,244],{"class":120},", ",[66,246,241],{"class":113},[66,248,244],{"class":120},[66,250,241],{"class":113},[66,252,244],{"class":120},[66,254,255],{"class":113},"10",[66,257,244],{"class":120},[66,259,255],{"class":113},[66,261,244],{"class":120},[66,263,255],{"class":113},[66,265,266],{"class":120},");\n",[66,268,269,271,274,276,278,281,283,286,288,291,293,295],{"class":68,"line":133},[66,270,158],{"class":109},[66,272,273],{"class":113}," moving",[66,275,164],{"class":109},[66,277,167],{"class":120},[66,279,280],{"class":72},"sphereMesh",[66,282,173],{"class":120},[66,284,285],{"class":113},"0.5",[66,287,244],{"class":120},[66,289,290],{"class":113},"30",[66,292,244],{"class":120},[66,294,290],{"class":113},[66,296,266],{"class":120},[14,298,299],{},"The box spans −1 to 1 on each axis. The sphere has radius 0.5.",[52,301,303],{"id":302},"moving-a-mesh","Moving a mesh",[14,305,306,307,314,315,320],{},"Each ",[18,308,311],{"href":309,"rel":310},"https:\u002F\u002Ftrueform.polydera.com\u002Fts\u002Fmodules\u002Fcore#mesh",[92],[32,312,313],{},"Mesh"," can carry a 4×4 ",[18,316,319],{"href":317,"rel":318},"https:\u002F\u002Ftrueform.polydera.com\u002Fts\u002Fmodules\u002Fspatial#transformations-on-forms",[92],"transformation",". Every query applies it on the fly. The points are not rewritten and the spatial tree stays valid.",[57,322,324],{"className":100,"code":323,"language":102,"meta":62,"style":62},"moving.transformation = tf.makeTranslation(3.0, 0, 0);\nmoving.transformation = tf.makeRotation(45, \"z\");\n",[32,325,326,355],{"__ignoreMap":62},[66,327,328,331,334,336,339,341,344,346,349,351,353],{"class":68,"line":69},[66,329,330],{"class":120},"moving.transformation ",[66,332,333],{"class":109},"=",[66,335,167],{"class":120},[66,337,338],{"class":72},"makeTranslation",[66,340,173],{"class":120},[66,342,343],{"class":113},"3.0",[66,345,244],{"class":120},[66,347,348],{"class":113},"0",[66,350,244],{"class":120},[66,352,348],{"class":113},[66,354,266],{"class":120},[66,356,357,359,361,363,366,368,371,373,376],{"class":68,"line":133},[66,358,330],{"class":120},[66,360,333],{"class":109},[66,362,167],{"class":120},[66,364,365],{"class":72},"makeRotation",[66,367,173],{"class":120},[66,369,370],{"class":113},"45",[66,372,244],{"class":120},[66,374,375],{"class":76},"\"z\"",[66,377,266],{"class":120},[14,379,380],{},[47,381],{"alt":382,"src":383},"Step: moving a mesh. A Thingi10K screw moved away from a Thingi10K part by its transformation; the faint copy is where it started","\u002Fimg\u002Ftutorials\u002Fsteps\u002Ffast-mesh-collision-detection-1-moving.png",[14,385,386,387,390],{},"Matrices are row-major. A Three.js ",[32,388,389],{},"Matrix4"," is column-major, so transpose it:",[57,392,394],{"className":100,"code":393,"language":102,"meta":62,"style":62},"moving.transformation = tf.ndarray(object.matrixWorld.elements, [4, 4]).T;\n",[32,395,396],{"__ignoreMap":62},[66,397,398,400,402,404,407,410,413,415,417,420,423],{"class":68,"line":69},[66,399,330],{"class":120},[66,401,333],{"class":109},[66,403,167],{"class":120},[66,405,406],{"class":72},"ndarray",[66,408,409],{"class":120},"(object.matrixWorld.elements, [",[66,411,412],{"class":113},"4",[66,414,244],{"class":120},[66,416,412],{"class":113},[66,418,419],{"class":120},"]).",[66,421,422],{"class":113},"T",[66,424,130],{"class":120},[52,426,428],{"id":427},"do-the-meshes-collide","Do the meshes collide?",[14,430,431,438,439,442],{},[18,432,435],{"href":433,"rel":434},"https:\u002F\u002Ftrueform.polydera.com\u002Fts\u002Fmodules\u002Fspatial#intersection",[92],[32,436,437],{},"tf.intersects"," returns a ",[32,440,441],{},"boolean",":",[57,444,446],{"className":100,"code":445,"language":102,"meta":62,"style":62},"moving.transformation = tf.makeTranslation(3.0, 0, 0);\ntf.intersects(fixed, moving);  \u002F\u002F false\n\nmoving.transformation = tf.makeTranslation(1.3, 0, 0);\ntf.intersects(fixed, moving);  \u002F\u002F true\n",[32,447,448,472,487,491,516],{"__ignoreMap":62},[66,449,450,452,454,456,458,460,462,464,466,468,470],{"class":68,"line":69},[66,451,330],{"class":120},[66,453,333],{"class":109},[66,455,167],{"class":120},[66,457,338],{"class":72},[66,459,173],{"class":120},[66,461,343],{"class":113},[66,463,244],{"class":120},[66,465,348],{"class":113},[66,467,244],{"class":120},[66,469,348],{"class":113},[66,471,266],{"class":120},[66,473,474,477,480,483],{"class":68,"line":133},[66,475,476],{"class":120},"tf.",[66,478,479],{"class":72},"intersects",[66,481,482],{"class":120},"(fixed, moving);  ",[66,484,486],{"class":485},"sAwPA","\u002F\u002F false\n",[66,488,489],{"class":68,"line":148},[66,490,152],{"emptyLinePlaceholder":151},[66,492,493,495,497,499,501,503,506,508,510,512,514],{"class":68,"line":155},[66,494,330],{"class":120},[66,496,333],{"class":109},[66,498,167],{"class":120},[66,500,338],{"class":72},[66,502,173],{"class":120},[66,504,505],{"class":113},"1.3",[66,507,244],{"class":120},[66,509,348],{"class":113},[66,511,244],{"class":120},[66,513,348],{"class":113},[66,515,266],{"class":120},[66,517,518,520,522,524],{"class":68,"line":187},[66,519,476],{"class":120},[66,521,479],{"class":72},[66,523,482],{"class":120},[66,525,526],{"class":485},"\u002F\u002F true\n",[14,528,529],{},[47,530],{"alt":531,"src":532},"Step: do the meshes collide? The screw in two poses: grey, clear of the part (False), and pink, pushed into it (True)","\u002Fimg\u002Ftutorials\u002Fsteps\u002Ffast-mesh-collision-detection-2-collide.png",[14,534,535,536,539,540,543,544,27],{},"Touching counts. With the sphere at ",[32,537,538],{},"x = 1.5",", its surface just meets the box face at ",[32,541,542],{},"x = 1",", and the answer is ",[32,545,546],{},"true",[14,548,549,550,552,553,556,557,560],{},"The test is on surfaces. A sphere entirely inside the box does not cross any box face, so ",[32,551,437],{}," returns ",[32,554,555],{},"false",", and ",[32,558,559],{},"tf.distance"," returns the gap between the two surfaces, 0.5 here.",[52,562,564],{"id":563},"how-far-apart-are-they","How far apart are they?",[14,566,567,568,574],{},"For clearance, use ",[18,569,572],{"href":570,"rel":571},"https:\u002F\u002Ftrueform.polydera.com\u002Fts\u002Fmodules\u002Fspatial#distance",[92],[32,573,559],{},". It returns 0 when the meshes touch.",[57,576,578],{"className":100,"code":577,"language":102,"meta":62,"style":62},"moving.transformation = tf.makeTranslation(1.55, 0, 0);\ntf.distance(fixed, moving);   \u002F\u002F 0.05\ntf.distance2(fixed, moving);  \u002F\u002F 0.0025, squared\n",[32,579,580,605,618],{"__ignoreMap":62},[66,581,582,584,586,588,590,592,595,597,599,601,603],{"class":68,"line":69},[66,583,330],{"class":120},[66,585,333],{"class":109},[66,587,167],{"class":120},[66,589,338],{"class":72},[66,591,173],{"class":120},[66,593,594],{"class":113},"1.55",[66,596,244],{"class":120},[66,598,348],{"class":113},[66,600,244],{"class":120},[66,602,348],{"class":113},[66,604,266],{"class":120},[66,606,607,609,612,615],{"class":68,"line":133},[66,608,476],{"class":120},[66,610,611],{"class":72},"distance",[66,613,614],{"class":120},"(fixed, moving);   ",[66,616,617],{"class":485},"\u002F\u002F 0.05\n",[66,619,620,622,625,627],{"class":68,"line":148},[66,621,476],{"class":120},[66,623,624],{"class":72},"distance2",[66,626,482],{"class":120},[66,628,629],{"class":485},"\u002F\u002F 0.0025, squared\n",[14,631,632],{},[47,633],{"alt":634,"src":635},"Step: how far apart are they? The screw just short of the part, with the closest pair of points joined in pink","\u002Fimg\u002Ftutorials\u002Fsteps\u002Ffast-mesh-collision-detection-3-distance.png",[14,637,638,639,646],{},"To get the closest pair itself, call ",[18,640,643],{"href":641,"rel":642},"https:\u002F\u002Ftrueform.polydera.com\u002Fts\u002Fmodules\u002Fspatial#neighbor-search",[92],[32,644,645],{},"tf.neighborSearch"," with two meshes:",[57,648,650],{"className":100,"code":649,"language":102,"meta":62,"style":62},"const { elementId0, elementId1, point0, point1, distance2 } =\n  tf.neighborSearch(fixed, moving);\n\u002F\u002F elementId0, elementId1: closest face on each mesh\n\u002F\u002F point0, point1:         closest points, in world coordinates\n\u002F\u002F distance2:              squared distance between them\n",[32,651,652,687,698,703,708],{"__ignoreMap":62},[66,653,654,656,659,662,664,667,669,672,674,677,679,681,684],{"class":68,"line":69},[66,655,158],{"class":109},[66,657,658],{"class":120}," { ",[66,660,661],{"class":113},"elementId0",[66,663,244],{"class":120},[66,665,666],{"class":113},"elementId1",[66,668,244],{"class":120},[66,670,671],{"class":113},"point0",[66,673,244],{"class":120},[66,675,676],{"class":113},"point1",[66,678,244],{"class":120},[66,680,624],{"class":113},[66,682,683],{"class":120}," } ",[66,685,686],{"class":109},"=\n",[66,688,689,692,695],{"class":68,"line":133},[66,690,691],{"class":120},"  tf.",[66,693,694],{"class":72},"neighborSearch",[66,696,697],{"class":120},"(fixed, moving);\n",[66,699,700],{"class":68,"line":148},[66,701,702],{"class":485},"\u002F\u002F elementId0, elementId1: closest face on each mesh\n",[66,704,705],{"class":68,"line":155},[66,706,707],{"class":485},"\u002F\u002F point0, point1:         closest points, in world coordinates\n",[66,709,710],{"class":68,"line":187},[66,711,712],{"class":485},"\u002F\u002F distance2:              squared distance between them\n",[14,714,715],{},"The same calls take primitives such as points and segments. A common use is a capsule, a segment with a radius, as a stand-in for a tool or a robot link:",[57,717,719],{"className":100,"code":718,"language":102,"meta":62,"style":62},"const axis = tf.segment(tf.point(1.3, -0.5, 0), tf.point(1.3, 0.5, 0));\nconst radius = 0.2;\n\nconst clearance = tf.distance(fixed, axis) - radius;  \u002F\u002F 0.1\nconst { elementId, point } = tf.neighborSearch(fixed, axis);  \u002F\u002F closest box face and point\n",[32,720,721,775,789,793,817],{"__ignoreMap":62},[66,722,723,725,728,730,732,735,738,741,743,745,747,750,752,754,756,759,761,763,765,767,769,771,773],{"class":68,"line":69},[66,724,158],{"class":109},[66,726,727],{"class":113}," axis",[66,729,164],{"class":109},[66,731,167],{"class":120},[66,733,734],{"class":72},"segment",[66,736,737],{"class":120},"(tf.",[66,739,740],{"class":72},"point",[66,742,173],{"class":120},[66,744,505],{"class":113},[66,746,244],{"class":120},[66,748,749],{"class":109},"-",[66,751,285],{"class":113},[66,753,244],{"class":120},[66,755,348],{"class":113},[66,757,758],{"class":120},"), tf.",[66,760,740],{"class":72},[66,762,173],{"class":120},[66,764,505],{"class":113},[66,766,244],{"class":120},[66,768,285],{"class":113},[66,770,244],{"class":120},[66,772,348],{"class":113},[66,774,184],{"class":120},[66,776,777,779,782,784,787],{"class":68,"line":133},[66,778,158],{"class":109},[66,780,781],{"class":113}," radius",[66,783,164],{"class":109},[66,785,786],{"class":113}," 0.2",[66,788,130],{"class":120},[66,790,791],{"class":68,"line":148},[66,792,152],{"emptyLinePlaceholder":151},[66,794,795,797,800,802,804,806,809,811,814],{"class":68,"line":155},[66,796,158],{"class":109},[66,798,799],{"class":113}," clearance",[66,801,164],{"class":109},[66,803,167],{"class":120},[66,805,611],{"class":72},[66,807,808],{"class":120},"(fixed, axis) ",[66,810,749],{"class":109},[66,812,813],{"class":120}," radius;  ",[66,815,816],{"class":485},"\u002F\u002F 0.1\n",[66,818,819,821,823,826,828,830,832,834,836,838,841],{"class":68,"line":187},[66,820,158],{"class":109},[66,822,658],{"class":120},[66,824,825],{"class":113},"elementId",[66,827,244],{"class":120},[66,829,740],{"class":113},[66,831,683],{"class":120},[66,833,333],{"class":109},[66,835,167],{"class":120},[66,837,694],{"class":72},[66,839,840],{"class":120},"(fixed, axis);  ",[66,842,843],{"class":485},"\u002F\u002F closest box face and point\n",[52,845,847],{"id":846},"where-is-the-contact","Where is the contact?",[14,849,850,851,442],{},"When the meshes overlap, the contact is the curve where their surfaces cross. Compute it with ",[18,852,854],{"href":853},"\u002Ftutorials\u002Fmesh-intersection-curves-in-javascript","intersection curves",[57,856,858],{"className":100,"code":857,"language":102,"meta":62,"style":62},"moving.transformation = tf.makeTranslation(1.3, 0, 0);\nconst contact = tf.intersectionCurves(fixed, moving);\ncontact.length;  \u002F\u002F 1, a closed loop on the box face\n",[32,859,860,884,900],{"__ignoreMap":62},[66,861,862,864,866,868,870,872,874,876,878,880,882],{"class":68,"line":69},[66,863,330],{"class":120},[66,865,333],{"class":109},[66,867,167],{"class":120},[66,869,338],{"class":72},[66,871,173],{"class":120},[66,873,505],{"class":113},[66,875,244],{"class":120},[66,877,348],{"class":113},[66,879,244],{"class":120},[66,881,348],{"class":113},[66,883,266],{"class":120},[66,885,886,888,891,893,895,898],{"class":68,"line":133},[66,887,158],{"class":109},[66,889,890],{"class":113}," contact",[66,892,164],{"class":109},[66,894,167],{"class":120},[66,896,897],{"class":72},"intersectionCurves",[66,899,697],{"class":120},[66,901,902,905,908,911],{"class":68,"line":148},[66,903,904],{"class":120},"contact.",[66,906,907],{"class":113},"length",[66,909,910],{"class":120},";  ",[66,912,913],{"class":485},"\u002F\u002F 1, a closed loop on the box face\n",[14,915,916,917,920],{},"The curve is exact. Use it to draw the contact, to measure it, or to decide whether a ",[18,918,441],{"href":919},"\u002Ftutorials\u002Ffast-mesh-booleans-in-javascript"," is needed.",[52,922,924],{"id":923},"working-with-results","Working with results",[14,926,927],{},"In a motion loop, build the trees once and only change the transformation:",[57,929,931],{"className":100,"code":930,"language":102,"meta":62,"style":62},"fixed.buildTree();\nmoving.buildTree();\n\nfor (let x = 3.0; x >= 0; x -= 0.5) {\n  moving.transformation = tf.makeTranslation(x, 0, 0);\n  const hit = tf.intersects(fixed, moving);\n  const gap = tf.distance(fixed, moving);\n}\n",[32,932,933,944,953,957,996,1018,1035,1051],{"__ignoreMap":62},[66,934,935,938,941],{"class":68,"line":69},[66,936,937],{"class":120},"fixed.",[66,939,940],{"class":72},"buildTree",[66,942,943],{"class":120},"();\n",[66,945,946,949,951],{"class":68,"line":133},[66,947,948],{"class":120},"moving.",[66,950,940],{"class":72},[66,952,943],{"class":120},[66,954,955],{"class":68,"line":148},[66,956,152],{"emptyLinePlaceholder":151},[66,958,959,962,965,968,971,973,976,979,982,985,987,990,993],{"class":68,"line":155},[66,960,961],{"class":109},"for",[66,963,964],{"class":120}," (",[66,966,967],{"class":109},"let",[66,969,970],{"class":120}," x ",[66,972,333],{"class":109},[66,974,975],{"class":113}," 3.0",[66,977,978],{"class":120},"; x ",[66,980,981],{"class":109},">=",[66,983,984],{"class":113}," 0",[66,986,978],{"class":120},[66,988,989],{"class":109},"-=",[66,991,992],{"class":113}," 0.5",[66,994,995],{"class":120},") {\n",[66,997,998,1001,1003,1005,1007,1010,1012,1014,1016],{"class":68,"line":187},[66,999,1000],{"class":120},"  moving.transformation ",[66,1002,333],{"class":109},[66,1004,167],{"class":120},[66,1006,338],{"class":72},[66,1008,1009],{"class":120},"(x, ",[66,1011,348],{"class":113},[66,1013,244],{"class":120},[66,1015,348],{"class":113},[66,1017,266],{"class":120},[66,1019,1021,1024,1027,1029,1031,1033],{"class":68,"line":1020},6,[66,1022,1023],{"class":109},"  const",[66,1025,1026],{"class":113}," hit",[66,1028,164],{"class":109},[66,1030,167],{"class":120},[66,1032,479],{"class":72},[66,1034,697],{"class":120},[66,1036,1038,1040,1043,1045,1047,1049],{"class":68,"line":1037},7,[66,1039,1023],{"class":109},[66,1041,1042],{"class":113}," gap",[66,1044,164],{"class":109},[66,1046,167],{"class":120},[66,1048,611],{"class":72},[66,1050,697],{"class":120},[66,1052,1054],{"class":68,"line":1053},8,[66,1055,1056],{"class":120},"}\n",[14,1058,1059,1060,1063],{},"To test many copies of one part in different poses, use ",[32,1061,1062],{},"shallowCopy()",". Each copy shares the points and faces, and carries its own transformation:",[57,1065,1067],{"className":100,"code":1066,"language":102,"meta":62,"style":62},"const copies = [1.2, 2.2, 3.2].map((x) => {\n  const view = moving.shallowCopy();\n  view.transformation = tf.makeTranslation(x, 0, 0);\n  return view;\n});\n\ncopies.map((view) => tf.intersects(fixed, view));  \u002F\u002F [true, false, false]\n",[32,1068,1069,1116,1133,1154,1162,1167,1171],{"__ignoreMap":62},[66,1070,1071,1073,1076,1078,1081,1084,1086,1089,1091,1094,1097,1100,1103,1107,1110,1113],{"class":68,"line":69},[66,1072,158],{"class":109},[66,1074,1075],{"class":113}," copies",[66,1077,164],{"class":109},[66,1079,1080],{"class":120}," [",[66,1082,1083],{"class":113},"1.2",[66,1085,244],{"class":120},[66,1087,1088],{"class":113},"2.2",[66,1090,244],{"class":120},[66,1092,1093],{"class":113},"3.2",[66,1095,1096],{"class":120},"].",[66,1098,1099],{"class":72},"map",[66,1101,1102],{"class":120},"((",[66,1104,1106],{"class":1105},"s9osk","x",[66,1108,1109],{"class":120},") ",[66,1111,1112],{"class":109},"=>",[66,1114,1115],{"class":120}," {\n",[66,1117,1118,1120,1123,1125,1128,1131],{"class":68,"line":133},[66,1119,1023],{"class":109},[66,1121,1122],{"class":113}," view",[66,1124,164],{"class":109},[66,1126,1127],{"class":120}," moving.",[66,1129,1130],{"class":72},"shallowCopy",[66,1132,943],{"class":120},[66,1134,1135,1138,1140,1142,1144,1146,1148,1150,1152],{"class":68,"line":148},[66,1136,1137],{"class":120},"  view.transformation ",[66,1139,333],{"class":109},[66,1141,167],{"class":120},[66,1143,338],{"class":72},[66,1145,1009],{"class":120},[66,1147,348],{"class":113},[66,1149,244],{"class":120},[66,1151,348],{"class":113},[66,1153,266],{"class":120},[66,1155,1156,1159],{"class":68,"line":155},[66,1157,1158],{"class":109},"  return",[66,1160,1161],{"class":120}," view;\n",[66,1163,1164],{"class":68,"line":187},[66,1165,1166],{"class":120},"});\n",[66,1168,1169],{"class":68,"line":1020},[66,1170,152],{"emptyLinePlaceholder":151},[66,1172,1173,1176,1178,1180,1183,1185,1187,1189,1191,1194],{"class":68,"line":1037},[66,1174,1175],{"class":120},"copies.",[66,1177,1099],{"class":72},[66,1179,1102],{"class":120},[66,1181,1182],{"class":1105},"view",[66,1184,1109],{"class":120},[66,1186,1112],{"class":109},[66,1188,167],{"class":120},[66,1190,479],{"class":72},[66,1192,1193],{"class":120},"(fixed, view));  ",[66,1195,1196],{"class":485},"\u002F\u002F [true, false, false]\n",[14,1198,1199],{},"In the browser, the async variants run off the main thread and keep the frame loop free:",[57,1201,1203],{"className":100,"code":1202,"language":102,"meta":62,"style":62},"const hit = await tf.async.intersects(fixed, moving);\nconst gap = await tf.async.distance(fixed, moving);\n",[32,1204,1205,1223],{"__ignoreMap":62},[66,1206,1207,1209,1211,1213,1216,1219,1221],{"class":68,"line":69},[66,1208,158],{"class":109},[66,1210,1026],{"class":113},[66,1212,164],{"class":109},[66,1214,1215],{"class":109}," await",[66,1217,1218],{"class":120}," tf.async.",[66,1220,479],{"class":72},[66,1222,697],{"class":120},[66,1224,1225,1227,1229,1231,1233,1235,1237],{"class":68,"line":133},[66,1226,158],{"class":109},[66,1228,1042],{"class":113},[66,1230,164],{"class":109},[66,1232,1215],{"class":109},[66,1234,1218],{"class":120},[66,1236,611],{"class":72},[66,1238,697],{"class":120},[52,1240,1242],{"id":1241},"related-tutorials","Related tutorials",[1244,1245,1246,1254,1260,1266],"ul",{},[1247,1248,1249,1253],"li",{},[18,1250,1252],{"href":1251},"\u002Ftutorials\u002Ffast-closest-point-on-a-mesh-in-javascript","Fast and Robust Closest Point on a Mesh in JavaScript"," — nearest surface point for a batch of queries.",[1247,1255,1256,1259],{},[18,1257,1258],{"href":853},"Fast and Exact Mesh Intersection Curves in JavaScript"," — exact curves where two or more meshes cross.",[1247,1261,1262,1265],{},[18,1263,1264],{"href":919},"Fast Mesh Booleans in JavaScript"," — exact union, intersection and difference of two meshes.",[1247,1267,1268,1272],{},[18,1269,1271],{"href":1270},"\u002Ftutorials\u002Ffast-stl-and-obj-reading-in-javascript","Fast STL and OBJ Reading in JavaScript"," — load and write meshes, the first step of every pipeline.",[52,1274,1276],{"id":1275},"performance","Performance",[14,1278,1279,1280,22,1283,1286],{},"We measured mesh–mesh queries between the dragon and a transformed copy of it, at five sizes, against three-mesh-bvh's ",[32,1281,1282],{},"closestPointToGeometry",[32,1284,1285],{},"intersectsGeometry",". The closest pair is taken across a gap; the intersection test on a pose where the surfaces cross.",[1288,1289],"headline-numbers",{":items":1290},"[{\"value\":\"0.19ms\",\"label\":\"Mesh–mesh closest pair\",\"detail\":\"1,025,766 faces each, vs 80.2 ms three-mesh-bvh\"},{\"value\":\"413×\",\"label\":\"Faster than three-mesh-bvh\",\"detail\":\"Closest pair, trees built\"},{\"value\":\"26×\",\"label\":\"Faster than three-mesh-bvh\",\"detail\":\"Closest pair, trees built inside the timer\"}]",[14,1292,1293,1298,1299,1298,1303],{},[66,1294,1297],{"className":1295},[1296],"tag-pill","Apple M4 Max"," ",[66,1300,1302],{"className":1301},[1296],"16 threads",[66,1304,1306],{"className":1305},[1296],"Node.js 25 · WebAssembly",[1308,1309],"data-table",{":headers":1310,":highlight":348,":rows":1311},"[\"Faces each\",\"Closest pair: trueform\",\"three-mesh-bvh\",\"Intersects: trueform\",\"three-mesh-bvh\"]","[[\"58,298\",\"0.045 ms\",\"7.46 ms\",\"0.107 ms\",\"0.386 ms\"],[\"141,966\",\"0.098 ms\",\"12.8 ms\",\"0.065 ms\",\"0.504 ms\"],[\"244,026\",\"0.088 ms\",\"24.1 ms\",\"0.091 ms\",\"0.747 ms\"],[\"480,868\",\"0.184 ms\",\"61.3 ms\",\"0.046 ms\",\"0.802 ms\"],[\"1,025,766\",\"0.194 ms\",\"80.2 ms\",\"0.043 ms\",\"2.68 ms\"],[\"1,025,766, both trees built inside the timer\",\"22.2 ms\",\"573 ms\",\"23.4 ms\",\"466 ms\"]]",[14,1313,1314],{},"Both trees are built before the timer, except in the last row. Both libraries return the same answer on every input. Median of 9 runs after 2 warm-ups, each library in its own Node.js process. three-mesh-bvh runs on one JavaScript thread.",[14,1316,1317],{},"Measured 2026-10-31 with trueform 0.10.8; harness and raw results: benchmarks\u002Ftutorials\u002Fjavascript-collision-detection.",[14,1319,1320,1321,1323],{},"Every collision query runs on a spatial tree. On a 1,025,766-polygon mesh, ",[32,1322,34],{}," builds it in 15.8 ms in Node.js, against 259.9 ms for three-mesh-bvh. The tree is built once. Moving a mesh by its transformation does not rebuild it.",[14,1325,1326],{},[47,1327],{"alt":1328,"src":1329},"Benchmark chart: trueform against other TypeScript libraries, tree build","\u002Fimg\u002Falgorithms\u002Fbenchmark-tree-build-typescript-2026.png",[14,1331,1332,1336,1337],{},[18,1333,1335],{"href":1334},"\u002Ftrueform\u002Fbenchmarks","Full benchmarks"," · ",[18,1338,1341],{"href":1339,"rel":1340},"https:\u002F\u002Ftrueform.polydera.com\u002Fts\u002Fmodules\u002Fspatial",[92],"Documentation",[1343,1344],"cite-as",{"author":1345,"title":5},"Sajovic, {\\v{Z}}iga",[1347,1348,1349],"style",{},"html pre.shiki code .svObZ, html code.shiki .svObZ{--shiki-default:#B392F0}html pre.shiki code .sU2Wk, html code.shiki .sU2Wk{--shiki-default:#9ECBFF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html pre.shiki code .snl16, html code.shiki .snl16{--shiki-default:#F97583}html pre.shiki code .sDLfK, html code.shiki .sDLfK{--shiki-default:#79B8FF}html pre.shiki code .s95oV, html code.shiki .s95oV{--shiki-default:#E1E4E8}html pre.shiki code .sAwPA, html code.shiki .sAwPA{--shiki-default:#6A737D}html pre.shiki code .s9osk, html code.shiki .s9osk{--shiki-default:#FFAB70}",{"title":62,"searchDepth":133,"depth":133,"links":1351},[1352,1353,1354,1355,1356,1357,1358,1359,1360],{"id":54,"depth":133,"text":55},{"id":96,"depth":133,"text":97},{"id":302,"depth":133,"text":303},{"id":427,"depth":133,"text":428},{"id":563,"depth":133,"text":564},{"id":846,"depth":133,"text":847},{"id":923,"depth":133,"text":924},{"id":1241,"depth":133,"text":1242},{"id":1275,"depth":133,"text":1276},"2026-10-31","Test whether two triangle meshes collide in JavaScript and TypeScript, measure their clearance, find the closest pair, and move meshes without rebuilding anything. WebAssembly, runs in Node.js and the browser.","md",[1365,1366,1367,1368,1369,1370,1371,1372,1373,1374],"mesh collision detection JavaScript","triangle mesh collision TypeScript","collision detection browser","mesh mesh distance JavaScript","WebAssembly collision detection","three.js mesh collision","three-mesh-bvh alternative","npm collision detection mesh","BVH JavaScript","FCL alternative JavaScript",{},"\u002Fimg\u002Fog\u002Ftutorials\u002Ffast-mesh-collision-detection-in-javascript.png","\u002Ftutorials\u002Ffast-mesh-collision-detection-in-javascript",{"title":5,"description":1362},"tutorials\u002Ffast-mesh-collision-detection-in-javascript",[1381,102,1382,1383,1384],"javascript","collision","tutorial","webassembly","mkp6Esy-Md66Tqzy_Wnz3e9hvW9yOMob26e9X6gOQ6s",1793517812008]