[{"data":1,"prerenderedAt":1188},["ShallowReactive",2],{"tutorial-fast-mesh-collision-detection-in-python":3},{"id":4,"title":5,"author":6,"body":7,"date":1164,"description":1165,"extension":1166,"keywords":1167,"meta":1179,"navigation":136,"ogImage":1180,"path":1181,"published":136,"seo":1182,"stem":1183,"tags":1184,"__hash__":1187},"tutorials\u002Ftutorials\u002Ffast-mesh-collision-detection-in-python.md","Fast and Robust Mesh Collision Detection in Python","Žiga Sajovic",{"type":8,"value":9,"toc":1153},"minimark",[10,28,40,44,51,56,81,91,95,182,185,271,274,278,295,392,398,408,412,426,492,498,512,527,531,542,584,590,601,631,642,773,786,790,804,877,883,886,889,946,950,953,1038,1045,1052,1067,1071,1102,1106,1110,1122,1128,1134,1145,1149],[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-cpp","C++"," and ",[18,24,26],{"href":25},"\u002Ftutorials\u002Ffast-mesh-collision-detection-in-javascript","JavaScript",".",[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? If they do, which faces touch? ",[32,33,34],"code",{},"trueform"," answers all three on triangle meshes, with each mesh in its own pose. One ",[32,37,38],{},"pip install",". NumPy arrays in, NumPy arrays out. This tutorial covers the test, the clearance, the contact faces, 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\u002Fpy\u002Fmodules\u002Fspatial#intersection-testing\",\"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","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-meshes","Loading the meshes",[57,96,100],{"className":97,"code":98,"language":99,"meta":62,"style":62},"language-python shiki shiki-themes github-dark","import trueform as tf\nimport numpy as np\n\nfixture = tf.Mesh(*tf.read_stl(\"fixture.stl\"))\npart = tf.Mesh(*tf.read_obj(\"part.obj\"))\n","python",[32,101,102,118,131,138,162],{"__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"," trueform ",[66,113,114],{"class":106},"as",[66,116,117],{"class":110}," tf\n",[66,119,121,123,126,128],{"class":68,"line":120},2,[66,122,107],{"class":106},[66,124,125],{"class":110}," numpy ",[66,127,114],{"class":106},[66,129,130],{"class":110}," np\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},"fixture ",[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},"\"fixture.stl\"",[66,160,161],{"class":110},"))\n",[66,163,165,168,170,172,174,177,180],{"class":68,"line":164},5,[66,166,167],{"class":110},"part ",[66,169,146],{"class":106},[66,171,149],{"class":110},[66,173,152],{"class":106},[66,175,176],{"class":110},"tf.read_obj(",[66,178,179],{"class":76},"\"part.obj\"",[66,181,161],{"class":110},[14,183,184],{},"To follow along without files, use a subdivided box as the static mesh and a small sphere as the moving one:",[57,186,188],{"className":97,"code":187,"language":99,"meta":62,"style":62},"static = tf.Mesh(*tf.make_box_mesh(2.0, 2.0, 2.0, 10, 10, 10))\nmoving = tf.Mesh(*tf.make_sphere_mesh(0.5, stacks=30, segments=30))\n",[32,189,190,232],{"__ignoreMap":62},[66,191,192,195,197,199,201,204,208,211,213,215,217,219,222,224,226,228,230],{"class":68,"line":69},[66,193,194],{"class":110},"static ",[66,196,146],{"class":106},[66,198,149],{"class":110},[66,200,152],{"class":106},[66,202,203],{"class":110},"tf.make_box_mesh(",[66,205,207],{"class":206},"sDLfK","2.0",[66,209,210],{"class":110},", ",[66,212,207],{"class":206},[66,214,210],{"class":110},[66,216,207],{"class":206},[66,218,210],{"class":110},[66,220,221],{"class":206},"10",[66,223,210],{"class":110},[66,225,221],{"class":206},[66,227,210],{"class":110},[66,229,221],{"class":206},[66,231,161],{"class":110},[66,233,234,237,239,241,243,246,249,251,255,257,260,262,265,267,269],{"class":68,"line":120},[66,235,236],{"class":110},"moving ",[66,238,146],{"class":106},[66,240,149],{"class":110},[66,242,152],{"class":106},[66,244,245],{"class":110},"tf.make_sphere_mesh(",[66,247,248],{"class":206},"0.5",[66,250,210],{"class":110},[66,252,254],{"class":253},"s9osk","stacks",[66,256,146],{"class":106},[66,258,259],{"class":206},"30",[66,261,210],{"class":110},[66,263,264],{"class":253},"segments",[66,266,146],{"class":106},[66,268,259],{"class":206},[66,270,161],{"class":110},[14,272,273],{},"The box spans −1 to 1 on each axis. The sphere has radius 0.5.",[52,275,277],{"id":276},"moving-a-mesh","Moving a mesh",[14,279,280,281,288,289,294],{},"Each ",[18,282,285],{"href":283,"rel":284},"https:\u002F\u002Ftrueform.polydera.com\u002Fpy\u002Fmodules\u002Fspatial#mesh",[88],[32,286,287],{},"tf.Mesh"," can carry a 4×4 ",[18,290,293],{"href":291,"rel":292},"https:\u002F\u002Ftrueform.polydera.com\u002Fpy\u002Fmodules\u002Fspatial#transformations-on-forms",[88],"transformation",". Every query applies it on the fly. The points are not rewritten and the spatial tree stays valid.",[57,296,298],{"className":97,"code":297,"language":99,"meta":62,"style":62},"def translation(x, y, z):\n    T = np.eye(4, dtype=np.float32)\n    T[:3, 3] = [x, y, z]\n    return T\n\nmoving.transformation = translation(3.0, 0.0, 0.0)\n",[32,299,300,311,334,354,362,366],{"__ignoreMap":62},[66,301,302,305,308],{"class":68,"line":69},[66,303,304],{"class":106},"def",[66,306,307],{"class":72}," translation",[66,309,310],{"class":110},"(x, y, z):\n",[66,312,313,316,318,321,324,326,329,331],{"class":68,"line":120},[66,314,315],{"class":110},"    T ",[66,317,146],{"class":106},[66,319,320],{"class":110}," np.eye(",[66,322,323],{"class":206},"4",[66,325,210],{"class":110},[66,327,328],{"class":253},"dtype",[66,330,146],{"class":106},[66,332,333],{"class":110},"np.float32)\n",[66,335,336,339,342,344,346,349,351],{"class":68,"line":133},[66,337,338],{"class":110},"    T[:",[66,340,341],{"class":206},"3",[66,343,210],{"class":110},[66,345,341],{"class":206},[66,347,348],{"class":110},"] ",[66,350,146],{"class":106},[66,352,353],{"class":110}," [x, y, z]\n",[66,355,356,359],{"class":68,"line":140},[66,357,358],{"class":106},"    return",[66,360,361],{"class":110}," T\n",[66,363,364],{"class":68,"line":164},[66,365,137],{"emptyLinePlaceholder":136},[66,367,369,372,374,377,380,382,385,387,389],{"class":68,"line":368},6,[66,370,371],{"class":110},"moving.transformation ",[66,373,146],{"class":106},[66,375,376],{"class":110}," translation(",[66,378,379],{"class":206},"3.0",[66,381,210],{"class":110},[66,383,384],{"class":206},"0.0",[66,386,210],{"class":110},[66,388,384],{"class":206},[66,390,391],{"class":110},")\n",[14,393,394],{},[47,395],{"alt":396,"src":397},"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,399,400,401,404,405,27],{},"The matrix dtype matches the mesh: ",[32,402,403],{},"float32"," here. Rotations go in ",[32,406,407],{},"T[:3, :3]",[52,409,411],{"id":410},"do-the-meshes-collide","Do the meshes collide?",[14,413,414,421,422,425],{},[18,415,418],{"href":416,"rel":417},"https:\u002F\u002Ftrueform.polydera.com\u002Fpy\u002Fmodules\u002Fspatial#intersection-testing",[88],[32,419,420],{},"tf.intersects"," returns a ",[32,423,424],{},"bool",":",[57,427,429],{"className":97,"code":428,"language":99,"meta":62,"style":62},"moving.transformation = translation(3.0, 0.0, 0.0)\ntf.intersects(static, moving)  # False\n\nmoving.transformation = translation(1.3, 0.0, 0.0)\ntf.intersects(static, moving)  # True\n",[32,430,431,451,460,464,485],{"__ignoreMap":62},[66,432,433,435,437,439,441,443,445,447,449],{"class":68,"line":69},[66,434,371],{"class":110},[66,436,146],{"class":106},[66,438,376],{"class":110},[66,440,379],{"class":206},[66,442,210],{"class":110},[66,444,384],{"class":206},[66,446,210],{"class":110},[66,448,384],{"class":206},[66,450,391],{"class":110},[66,452,453,456],{"class":68,"line":120},[66,454,455],{"class":110},"tf.intersects(static, moving)  ",[66,457,459],{"class":458},"sAwPA","# False\n",[66,461,462],{"class":68,"line":133},[66,463,137],{"emptyLinePlaceholder":136},[66,465,466,468,470,472,475,477,479,481,483],{"class":68,"line":140},[66,467,371],{"class":110},[66,469,146],{"class":106},[66,471,376],{"class":110},[66,473,474],{"class":206},"1.3",[66,476,210],{"class":110},[66,478,384],{"class":206},[66,480,210],{"class":110},[66,482,384],{"class":206},[66,484,391],{"class":110},[66,486,487,489],{"class":68,"line":164},[66,488,455],{"class":110},[66,490,491],{"class":458},"# True\n",[14,493,494],{},[47,495],{"alt":496,"src":497},"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,499,500,501,504,505,508,509,27],{},"Touching counts. With the sphere at ",[32,502,503],{},"x = 1.5",", its surface just meets the box face at ",[32,506,507],{},"x = 1",", and the answer is ",[32,510,511],{},"True",[14,513,514,515,517,518,521,522,27],{},"The test is on surfaces. A sphere entirely inside the box does not cross any box face, so ",[32,516,420],{}," returns ",[32,519,520],{},"False",". To catch containment, test one vertex of the moving mesh with a ",[18,523,526],{"href":524,"rel":525},"https:\u002F\u002Ftrueform.polydera.com\u002Fpy\u002Fmodules\u002Fspatial",[88],"point-in-mesh test",[52,528,530],{"id":529},"how-far-apart-are-they","How far apart are they?",[14,532,533,534,541],{},"For clearance, use ",[18,535,538],{"href":536,"rel":537},"https:\u002F\u002Ftrueform.polydera.com\u002Fpy\u002Fmodules\u002Fspatial#distance",[88],[32,539,540],{},"tf.distance",". It returns 0.0 when the meshes touch.",[57,543,545],{"className":97,"code":544,"language":99,"meta":62,"style":62},"moving.transformation = translation(1.55, 0.0, 0.0)\ntf.distance(static, moving)   # 0.05\ntf.distance2(static, moving)  # 0.0025, squared\n",[32,546,547,568,576],{"__ignoreMap":62},[66,548,549,551,553,555,558,560,562,564,566],{"class":68,"line":69},[66,550,371],{"class":110},[66,552,146],{"class":106},[66,554,376],{"class":110},[66,556,557],{"class":206},"1.55",[66,559,210],{"class":110},[66,561,384],{"class":206},[66,563,210],{"class":110},[66,565,384],{"class":206},[66,567,391],{"class":110},[66,569,570,573],{"class":68,"line":120},[66,571,572],{"class":110},"tf.distance(static, moving)   ",[66,574,575],{"class":458},"# 0.05\n",[66,577,578,581],{"class":68,"line":133},[66,579,580],{"class":110},"tf.distance2(static, moving)  ",[66,582,583],{"class":458},"# 0.0025, squared\n",[14,585,586],{},[47,587],{"alt":588,"src":589},"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,591,592,593,600],{},"To get the closest pair itself, call ",[18,594,597],{"href":595,"rel":596},"https:\u002F\u002Ftrueform.polydera.com\u002Fpy\u002Fmodules\u002Fspatial#neighbor-search",[88],[32,598,599],{},"tf.neighbor_search"," with two meshes:",[57,602,604],{"className":97,"code":603,"language":99,"meta":62,"style":62},"(face0, face1), (dist2, pt0, pt1) = tf.neighbor_search(static, moving)\n# face0, face1: closest face on each mesh\n# pt0, pt1:     closest points, in world coordinates\n# dist2:        squared distance between them\n",[32,605,606,616,621,626],{"__ignoreMap":62},[66,607,608,611,613],{"class":68,"line":69},[66,609,610],{"class":110},"(face0, face1), (dist2, pt0, pt1) ",[66,612,146],{"class":106},[66,614,615],{"class":110}," tf.neighbor_search(static, moving)\n",[66,617,618],{"class":68,"line":120},[66,619,620],{"class":458},"# face0, face1: closest face on each mesh\n",[66,622,623],{"class":68,"line":133},[66,624,625],{"class":458},"# pt0, pt1:     closest points, in world coordinates\n",[66,627,628],{"class":68,"line":140},[66,629,630],{"class":458},"# dist2:        squared distance between them\n",[14,632,633,634,641],{},"For a primitive against a primitive, use ",[18,635,638],{"href":636,"rel":637},"https:\u002F\u002Ftrueform.polydera.com\u002Fpy\u002Fmodules\u002Fspatial#closest-point",[88],[32,639,640],{},"tf.closest_metric_point_pair",". It takes points, segments, triangles, polygons, rays, lines, planes and boxes, not meshes. A common use is a capsule: a segment with a radius, as a stand-in for a tool or a robot link.",[57,643,645],{"className":97,"code":644,"language":99,"meta":62,"style":62},"axis = tf.Segment(np.array([[1.3, -0.5, 0.0], [1.3, 0.5, 0.0]], dtype=np.float32))\nradius = 0.2\n\n# closest box face to the capsule axis\nface_id, d2, pt = tf.neighbor_search(static, axis)\nclearance = np.sqrt(d2) - radius   # 0.1\n\n# closest points between the axis and that face\ntri = tf.Triangle(static.points[static.faces[face_id]])\nd2, on_axis, on_face = tf.closest_metric_point_pair(axis, tri)\n",[32,646,647,693,703,707,712,722,740,745,751,762],{"__ignoreMap":62},[66,648,649,652,654,657,659,661,664,666,668,670,673,675,677,679,681,683,686,688,690],{"class":68,"line":69},[66,650,651],{"class":110},"axis ",[66,653,146],{"class":106},[66,655,656],{"class":110}," tf.Segment(np.array([[",[66,658,474],{"class":206},[66,660,210],{"class":110},[66,662,663],{"class":106},"-",[66,665,248],{"class":206},[66,667,210],{"class":110},[66,669,384],{"class":206},[66,671,672],{"class":110},"], [",[66,674,474],{"class":206},[66,676,210],{"class":110},[66,678,248],{"class":206},[66,680,210],{"class":110},[66,682,384],{"class":206},[66,684,685],{"class":110},"]], ",[66,687,328],{"class":253},[66,689,146],{"class":106},[66,691,692],{"class":110},"np.float32))\n",[66,694,695,698,700],{"class":68,"line":120},[66,696,697],{"class":110},"radius ",[66,699,146],{"class":106},[66,701,702],{"class":206}," 0.2\n",[66,704,705],{"class":68,"line":133},[66,706,137],{"emptyLinePlaceholder":136},[66,708,709],{"class":68,"line":140},[66,710,711],{"class":458},"# closest box face to the capsule axis\n",[66,713,714,717,719],{"class":68,"line":164},[66,715,716],{"class":110},"face_id, d2, pt ",[66,718,146],{"class":106},[66,720,721],{"class":110}," tf.neighbor_search(static, axis)\n",[66,723,724,727,729,732,734,737],{"class":68,"line":368},[66,725,726],{"class":110},"clearance ",[66,728,146],{"class":106},[66,730,731],{"class":110}," np.sqrt(d2) ",[66,733,663],{"class":106},[66,735,736],{"class":110}," radius   ",[66,738,739],{"class":458},"# 0.1\n",[66,741,743],{"class":68,"line":742},7,[66,744,137],{"emptyLinePlaceholder":136},[66,746,748],{"class":68,"line":747},8,[66,749,750],{"class":458},"# closest points between the axis and that face\n",[66,752,754,757,759],{"class":68,"line":753},9,[66,755,756],{"class":110},"tri ",[66,758,146],{"class":106},[66,760,761],{"class":110}," tf.Triangle(static.points[static.faces[face_id]])\n",[66,763,765,768,770],{"class":68,"line":764},10,[66,766,767],{"class":110},"d2, on_axis, on_face ",[66,769,146],{"class":106},[66,771,772],{"class":110}," tf.closest_metric_point_pair(axis, tri)\n",[14,774,775,776,779,780,782,783,27],{},"Both operands must share a dtype. A ",[32,777,778],{},"float64"," segment against a ",[32,781,403],{}," mesh raises a ",[32,784,785],{},"TypeError",[52,787,789],{"id":788},"which-faces-touch","Which faces touch?",[14,791,792,799,800,803],{},[18,793,796],{"href":794,"rel":795},"https:\u002F\u002Ftrueform.polydera.com\u002Fpy\u002Fmodules\u002Fspatial#gathering-element-ids",[88],[32,797,798],{},"tf.gather_intersecting_ids"," returns every pair of intersecting faces, as an ",[32,801,802],{},"(M, 2)"," array:",[57,805,807],{"className":97,"code":806,"language":99,"meta":62,"style":62},"moving.transformation = translation(1.3, 0.0, 0.0)\n\npairs = tf.gather_intersecting_ids(static, moving)\npairs.shape              # (120, 2)\nnp.unique(pairs[:, 0])   # faces of the box in contact\nnp.unique(pairs[:, 1])   # faces of the sphere in contact\n",[32,808,809,829,833,843,851,865],{"__ignoreMap":62},[66,810,811,813,815,817,819,821,823,825,827],{"class":68,"line":69},[66,812,371],{"class":110},[66,814,146],{"class":106},[66,816,376],{"class":110},[66,818,474],{"class":206},[66,820,210],{"class":110},[66,822,384],{"class":206},[66,824,210],{"class":110},[66,826,384],{"class":206},[66,828,391],{"class":110},[66,830,831],{"class":68,"line":120},[66,832,137],{"emptyLinePlaceholder":136},[66,834,835,838,840],{"class":68,"line":133},[66,836,837],{"class":110},"pairs ",[66,839,146],{"class":106},[66,841,842],{"class":110}," tf.gather_intersecting_ids(static, moving)\n",[66,844,845,848],{"class":68,"line":140},[66,846,847],{"class":110},"pairs.shape              ",[66,849,850],{"class":458},"# (120, 2)\n",[66,852,853,856,859,862],{"class":68,"line":164},[66,854,855],{"class":110},"np.unique(pairs[:, ",[66,857,858],{"class":206},"0",[66,860,861],{"class":110},"])   ",[66,863,864],{"class":458},"# faces of the box in contact\n",[66,866,867,869,872,874],{"class":68,"line":368},[66,868,855],{"class":110},[66,870,871],{"class":206},"1",[66,873,861],{"class":110},[66,875,876],{"class":458},"# faces of the sphere in contact\n",[14,878,879],{},[47,880],{"alt":881,"src":882},"Step: which faces touch? The screw pushed into the part, with the intersecting faces colored pink on both meshes","\u002Fimg\u002Ftutorials\u002Fsteps\u002Ffast-mesh-collision-detection-4-touching-faces.png",[14,884,885],{},"Column 0 indexes faces of the first mesh, column 1 faces of the second. Use them to color the contact, to transfer attributes, or to restrict a finer check.",[14,887,888],{},"For near misses, gather the pairs within a distance:",[57,890,892],{"className":97,"code":891,"language":99,"meta":62,"style":62},"moving.transformation = translation(1.55, 0.0, 0.0)\n\nnear = tf.gather_ids_within_distance(static, moving, distance=0.1)\nnear.shape  # (430, 2)\n",[32,893,894,914,918,938],{"__ignoreMap":62},[66,895,896,898,900,902,904,906,908,910,912],{"class":68,"line":69},[66,897,371],{"class":110},[66,899,146],{"class":106},[66,901,376],{"class":110},[66,903,557],{"class":206},[66,905,210],{"class":110},[66,907,384],{"class":206},[66,909,210],{"class":110},[66,911,384],{"class":206},[66,913,391],{"class":110},[66,915,916],{"class":68,"line":120},[66,917,137],{"emptyLinePlaceholder":136},[66,919,920,923,925,928,931,933,936],{"class":68,"line":133},[66,921,922],{"class":110},"near ",[66,924,146],{"class":106},[66,926,927],{"class":110}," tf.gather_ids_within_distance(static, moving, ",[66,929,930],{"class":253},"distance",[66,932,146],{"class":106},[66,934,935],{"class":206},"0.1",[66,937,391],{"class":110},[66,939,940,943],{"class":68,"line":140},[66,941,942],{"class":110},"near.shape  ",[66,944,945],{"class":458},"# (430, 2)\n",[52,947,949],{"id":948},"working-with-results","Working with results",[14,951,952],{},"In a motion loop, build the trees once and only change the transformation:",[57,954,956],{"className":97,"code":955,"language":99,"meta":62,"style":62},"static.build_tree()\nmoving.build_tree()\n\nfor x in np.linspace(3.0, 0.0, 7):\n    moving.transformation = translation(x, 0.0, 0.0)\n    hit = tf.intersects(static, moving)\n    gap = tf.distance(static, moving)\n",[32,957,958,963,968,972,1000,1018,1028],{"__ignoreMap":62},[66,959,960],{"class":68,"line":69},[66,961,962],{"class":110},"static.build_tree()\n",[66,964,965],{"class":68,"line":120},[66,966,967],{"class":110},"moving.build_tree()\n",[66,969,970],{"class":68,"line":133},[66,971,137],{"emptyLinePlaceholder":136},[66,973,974,977,980,983,986,988,990,992,994,997],{"class":68,"line":140},[66,975,976],{"class":106},"for",[66,978,979],{"class":110}," x ",[66,981,982],{"class":106},"in",[66,984,985],{"class":110}," np.linspace(",[66,987,379],{"class":206},[66,989,210],{"class":110},[66,991,384],{"class":206},[66,993,210],{"class":110},[66,995,996],{"class":206},"7",[66,998,999],{"class":110},"):\n",[66,1001,1002,1005,1007,1010,1012,1014,1016],{"class":68,"line":164},[66,1003,1004],{"class":110},"    moving.transformation ",[66,1006,146],{"class":106},[66,1008,1009],{"class":110}," translation(x, ",[66,1011,384],{"class":206},[66,1013,210],{"class":110},[66,1015,384],{"class":206},[66,1017,391],{"class":110},[66,1019,1020,1023,1025],{"class":68,"line":368},[66,1021,1022],{"class":110},"    hit ",[66,1024,146],{"class":106},[66,1026,1027],{"class":110}," tf.intersects(static, moving)\n",[66,1029,1030,1033,1035],{"class":68,"line":742},[66,1031,1032],{"class":110},"    gap ",[66,1034,146],{"class":106},[66,1036,1037],{"class":110}," tf.distance(static, moving)\n",[14,1039,1040,1041,1044],{},"To test many copies of one part in different poses, use ",[32,1042,1043],{},"part.shared_view()",". Each view shares the data and the tree, and carries its own transformation.",[14,1046,1047,1048,425],{},"When you need the contact as geometry, not just face pairs, compute the ",[18,1049,1051],{"href":1050},"\u002Ftutorials\u002Fmesh-intersection-curves-in-python","intersection curves",[57,1053,1055],{"className":97,"code":1054,"language":99,"meta":62,"style":62},"paths, curve_points = tf.intersection_curves(static, moving)\n",[32,1056,1057],{"__ignoreMap":62},[66,1058,1059,1062,1064],{"class":68,"line":69},[66,1060,1061],{"class":110},"paths, curve_points ",[66,1063,146],{"class":106},[66,1065,1066],{"class":110}," tf.intersection_curves(static, moving)\n",[52,1068,1070],{"id":1069},"related-tutorials","Related tutorials",[1072,1073,1074,1082,1088,1095],"ul",{},[1075,1076,1077,1081],"li",{},[18,1078,1080],{"href":1079},"\u002Ftutorials\u002Ffast-closest-point-on-a-mesh-in-python","Fast and Robust Closest Point on a Mesh in Python"," — nearest surface point for a batch of queries.",[1075,1083,1084,1087],{},[18,1085,1086],{"href":1050},"Fast and Exact Mesh Intersection Curves in Python"," — exact curves where two or more meshes cross.",[1075,1089,1090,1094],{},[18,1091,1093],{"href":1092},"\u002Ftutorials\u002Ffast-mesh-booleans-in-python","Fast Mesh Booleans in Python"," — exact union, intersection and difference of two meshes.",[1075,1096,1097,1101],{},[18,1098,1100],{"href":1099},"\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,1103,1105],{"id":1104},"performance","Performance",[1107,1108],"headline-numbers",{":items":1109},"[{\"value\":\"\u003C 1 ms\",\"label\":\"Mesh–mesh closest point\",\"detail\":\"1,025,766 polygons, vs 370 ms FCL\"},{\"value\":\"28.5 ms\",\"label\":\"Spatial tree build\",\"detail\":\"1,025,766 polygons, vs 302.7 ms MeshLib\"},{\"value\":\"3.5 ms\",\"label\":\"Intersection curves\",\"detail\":\"2×500K polygons\"}]",[14,1111,1112,1117,1118],{},[66,1113,1116],{"className":1114},[1115],"tag-pill","Apple M4 Max"," ",[66,1119,1121],{"className":1120},[1115],"16 threads",[14,1123,1124,1125,1127],{},"On a 1,025,766-polygon mesh, the closest point between two meshes takes under 1 ms in ",[32,1126,34],{},", against 370 ms in FCL. The spatial tree on the same mesh builds in 28.5 ms from Python, against 302.7 ms in MeshLib. The tree is built once. Moving a mesh by its transformation does not rebuild it.",[14,1129,1130],{},[47,1131],{"alt":1132,"src":1133},"Benchmark chart: trueform against other Python libraries, tree build","\u002Fimg\u002Falgorithms\u002Fbenchmark-tree-build-python-2026.png",[14,1135,1136,1140,1141],{},[18,1137,1139],{"href":1138},"\u002Ftrueform\u002Fbenchmarks","Full benchmarks"," · ",[18,1142,1144],{"href":524,"rel":1143},[88],"Documentation",[1146,1147],"cite-as",{"author":1148,"title":5},"Sajovic, {\\v{Z}}iga",[1150,1151,1152],"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":1154},[1155,1156,1157,1158,1159,1160,1161,1162,1163],{"id":54,"depth":120,"text":55},{"id":93,"depth":120,"text":94},{"id":276,"depth":120,"text":277},{"id":410,"depth":120,"text":411},{"id":529,"depth":120,"text":530},{"id":788,"depth":120,"text":789},{"id":948,"depth":120,"text":949},{"id":1069,"depth":120,"text":1070},{"id":1104,"depth":120,"text":1105},"2026-10-31","Test whether two triangle meshes collide, measure the clearance between them, and find which faces touch, in Python. Move meshes with a 4×4 transformation, no rebuilds. One pip install, NumPy in and out.","md",[1168,1169,1170,1171,1172,1173,1174,1175,1176,1177,1178],"mesh collision detection Python","collision detection Python","triangle mesh collision","mesh mesh distance Python","clearance check mesh Python","numpy collision detection","FCL alternative Python","python-fcl alternative","trimesh collision manager alternative","Open3D collision alternative","MeshLib collision alternative",{},"\u002Fimg\u002Fog\u002Ftutorials\u002Ffast-mesh-collision-detection-in-python.png","\u002Ftutorials\u002Ffast-mesh-collision-detection-in-python",{"title":5,"description":1165},"tutorials\u002Ffast-mesh-collision-detection-in-python",[99,1185,1186],"collision","tutorial","X5VzHcFFnHSybI1Lw7oeQq0ApPR0WCdaQtBWQj_OG9M",1793517812008]