[{"data":1,"prerenderedAt":1921},["ShallowReactive",2],{"tutorial-fast-connected-components-in-cpp":3},{"id":4,"title":5,"author":6,"body":7,"date":1896,"description":1897,"extension":1898,"keywords":1899,"meta":1910,"navigation":148,"ogImage":1911,"path":1912,"published":148,"seo":1913,"stem":1914,"tags":1915,"__hash__":1920},"tutorials\u002Ftutorials\u002Ffast-connected-components-in-cpp.md","Fast Connected Components in C++","Žiga Sajovic",{"type":8,"value":9,"toc":1884},"minimark",[10,28,36,40,47,52,55,114,124,128,197,215,222,422,426,429,464,470,473,542,545,590,597,601,612,739,758,762,772,923,929,932,1032,1040,1044,1047,1297,1303,1309,1335,1339,1349,1553,1559,1570,1585,1738,1748,1752,1771,1796,1800,1827,1831,1834,1838,1850,1855,1858,1864,1876,1880],[11,12,13],"note",{},[14,15,16,17,22,23,27],"p",{},"Also available in ",[18,19,21],"a",{"href":20},"\u002Ftutorials\u002Ffast-connected-components-in-python","Python"," and ",[18,24,26],{"href":25},"\u002Ftutorials\u002Ffast-connected-components-in-javascript","JavaScript",".",[14,29,30,31,35],{},"A mesh file often holds more than one part. Scans carry floating debris. Exports merge bodies into one file. Open surfaces have holes. ",[32,33,34],"code",{},"trueform"," labels the connected components of a mesh, splits it into standalone parts, and finds its boundary loops. It is header-only C++17. This page covers labeling, splitting, keeping the largest part, open edges, and boundary loops.",[37,38],"article-cta",{":buttons":39},"[{\"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\u002Fcpp\u002Fmodules\u002Ftopology#connected-components\",\"variant\":\"soft\",\"color\":\"neutral\"}]",[14,41,42],{},[43,44],"img",{"alt":45,"src":46},"The Stanford dragon split into separate connected components, with their boundary loops","\u002Fimg\u002Ftutorials\u002Fconnected-components.png",[48,49,51],"h2",{"id":50},"install","Install",[14,53,54],{},"Header-only. It needs C++17 and oneTBB. Add it to your project via CMake FetchContent:",[56,57,62],"pre",{"className":58,"code":59,"language":60,"meta":61,"style":61},"language-cmake shiki shiki-themes github-dark","FetchContent_Declare(\n  trueform\n  GIT_REPOSITORY https:\u002F\u002Fgithub.com\u002Fpolydera\u002Ftrueform\n)\nFetchContent_MakeAvailable(trueform)\ntarget_link_libraries(my_app PRIVATE tf::trueform)\n","cmake","",[32,63,64,73,79,85,91,97],{"__ignoreMap":61},[65,66,69],"span",{"class":67,"line":68},"line",1,[65,70,72],{"class":71},"s95oV","FetchContent_Declare(\n",[65,74,76],{"class":67,"line":75},2,[65,77,78],{"class":71},"  trueform\n",[65,80,82],{"class":67,"line":81},3,[65,83,84],{"class":71},"  GIT_REPOSITORY https:\u002F\u002Fgithub.com\u002Fpolydera\u002Ftrueform\n",[65,86,88],{"class":67,"line":87},4,[65,89,90],{"class":71},")\n",[65,92,94],{"class":67,"line":93},5,[65,95,96],{"class":71},"FetchContent_MakeAvailable(trueform)\n",[65,98,100,104,107,111],{"class":67,"line":99},6,[65,101,103],{"class":102},"snl16","target_link_libraries",[65,105,106],{"class":71},"(my_app ",[65,108,110],{"class":109},"svObZ","PRIVATE",[65,112,113],{"class":71}," tf::trueform)\n",[14,115,116,117,123],{},"Also available via conan, nuget, and pip. See the ",[18,118,122],{"href":119,"rel":120},"https:\u002F\u002Ftrueform.polydera.com\u002Fcpp\u002Fgetting-started\u002Finstallation",[121],"nofollow","installation guide"," for all options.",[48,125,127],{"id":126},"loading-the-mesh","Loading the mesh",[56,129,133],{"className":130,"code":131,"language":132,"meta":61,"style":61},"language-cpp shiki shiki-themes github-dark","#include \u003Ctrueform\u002Ftrueform.hpp>\n\nauto mesh_buffer = tf::read_stl(\"parts.stl\");\nauto mesh = mesh_buffer.polygons();\n","cpp",[32,134,135,144,150,179],{"__ignoreMap":61},[65,136,137,140],{"class":67,"line":68},[65,138,139],{"class":102},"#include",[65,141,143],{"class":142},"sU2Wk"," \u003Ctrueform\u002Ftrueform.hpp>\n",[65,145,146],{"class":67,"line":75},[65,147,149],{"emptyLinePlaceholder":148},true,"\n",[65,151,152,155,158,161,164,167,170,173,176],{"class":67,"line":81},[65,153,154],{"class":102},"auto",[65,156,157],{"class":71}," mesh_buffer ",[65,159,160],{"class":102},"=",[65,162,163],{"class":109}," tf",[65,165,166],{"class":71},"::",[65,168,169],{"class":109},"read_stl",[65,171,172],{"class":71},"(",[65,174,175],{"class":142},"\"parts.stl\"",[65,177,178],{"class":71},");\n",[65,180,181,183,186,188,191,194],{"class":67,"line":87},[65,182,154],{"class":102},[65,184,185],{"class":71}," mesh ",[65,187,160],{"class":102},[65,189,190],{"class":71}," mesh_buffer.",[65,192,193],{"class":109},"polygons",[65,195,196],{"class":71},"();\n",[14,198,199,202,203,206,207,210,211,27],{},[32,200,201],{},"tf::read_stl"," merges corners at the same position, so faces share edges and the topology is there to read. ",[32,204,205],{},"tf::read_obj"," works the same way. If your data is already in memory, wrap it with ",[32,208,209],{},"tf::make_polygons",". See ",[18,212,214],{"href":213},"\u002Ftutorials\u002Ffast-stl-and-obj-reading-in-cpp","fast STL and OBJ reading",[14,216,217,218,221],{},"To build a two-part test file, join a sphere and a moved box. ",[32,219,220],{},"tf::concatenated"," applies the tagged transformation:",[56,223,225],{"className":130,"code":224,"language":132,"meta":61,"style":61},"auto sphere = tf::make_sphere_mesh(1.0f, 20, 20);\nauto box = tf::make_box_mesh(1.f, 1.f, 1.f);\nauto T = tf::make_transformation_from_translation(\n    tf::make_vector(3.f, 0.f, 0.f));\n\nauto parts = tf::concatenated(sphere.polygons(), box.polygons() | tf::tag(T));\ntf::write_stl(parts.polygons(), \"parts.stl\");\n",[32,226,227,264,301,320,353,357,399],{"__ignoreMap":61},[65,228,229,231,234,236,238,240,243,245,249,252,255,258,260,262],{"class":67,"line":68},[65,230,154],{"class":102},[65,232,233],{"class":71}," sphere ",[65,235,160],{"class":102},[65,237,163],{"class":109},[65,239,166],{"class":71},[65,241,242],{"class":109},"make_sphere_mesh",[65,244,172],{"class":71},[65,246,248],{"class":247},"sDLfK","1.0",[65,250,251],{"class":102},"f",[65,253,254],{"class":71},", ",[65,256,257],{"class":247},"20",[65,259,254],{"class":71},[65,261,257],{"class":247},[65,263,178],{"class":71},[65,265,266,268,271,273,275,277,280,282,285,287,289,291,293,295,297,299],{"class":67,"line":75},[65,267,154],{"class":102},[65,269,270],{"class":71}," box ",[65,272,160],{"class":102},[65,274,163],{"class":109},[65,276,166],{"class":71},[65,278,279],{"class":109},"make_box_mesh",[65,281,172],{"class":71},[65,283,284],{"class":247},"1.",[65,286,251],{"class":102},[65,288,254],{"class":71},[65,290,284],{"class":247},[65,292,251],{"class":102},[65,294,254],{"class":71},[65,296,284],{"class":247},[65,298,251],{"class":102},[65,300,178],{"class":71},[65,302,303,305,308,310,312,314,317],{"class":67,"line":81},[65,304,154],{"class":102},[65,306,307],{"class":71}," T ",[65,309,160],{"class":102},[65,311,163],{"class":109},[65,313,166],{"class":71},[65,315,316],{"class":109},"make_transformation_from_translation",[65,318,319],{"class":71},"(\n",[65,321,322,325,327,330,332,335,337,339,342,344,346,348,350],{"class":67,"line":87},[65,323,324],{"class":109},"    tf",[65,326,166],{"class":71},[65,328,329],{"class":109},"make_vector",[65,331,172],{"class":71},[65,333,334],{"class":247},"3.",[65,336,251],{"class":102},[65,338,254],{"class":71},[65,340,341],{"class":247},"0.",[65,343,251],{"class":102},[65,345,254],{"class":71},[65,347,341],{"class":247},[65,349,251],{"class":102},[65,351,352],{"class":71},"));\n",[65,354,355],{"class":67,"line":93},[65,356,149],{"emptyLinePlaceholder":148},[65,358,359,361,364,366,368,370,373,376,378,381,383,386,389,391,393,396],{"class":67,"line":99},[65,360,154],{"class":102},[65,362,363],{"class":71}," parts ",[65,365,160],{"class":102},[65,367,163],{"class":109},[65,369,166],{"class":71},[65,371,372],{"class":109},"concatenated",[65,374,375],{"class":71},"(sphere.",[65,377,193],{"class":109},[65,379,380],{"class":71},"(), box.",[65,382,193],{"class":109},[65,384,385],{"class":71},"() ",[65,387,388],{"class":102},"|",[65,390,163],{"class":109},[65,392,166],{"class":71},[65,394,395],{"class":109},"tag",[65,397,398],{"class":71},"(T));\n",[65,400,402,405,407,410,413,415,418,420],{"class":67,"line":401},7,[65,403,404],{"class":109},"tf",[65,406,166],{"class":71},[65,408,409],{"class":109},"write_stl",[65,411,412],{"class":71},"(parts.",[65,414,193],{"class":109},[65,416,417],{"class":71},"(), ",[65,419,175],{"class":142},[65,421,178],{"class":71},[48,423,425],{"id":424},"labeling-connected-components","Labeling connected components",[14,427,428],{},"Two faces belong to the same component when a chain of shared edges connects them. One call builds the connectivity and labels the faces:",[56,430,432],{"className":130,"code":431,"language":132,"meta":61,"style":61},"auto cl = tf::make_edge_connected_component_labels(mesh);\n\u002F\u002F cl.n_components == 2\n\u002F\u002F cl.labels: tf::buffer\u003Cint>, one label per face\n",[32,433,434,453,459],{"__ignoreMap":61},[65,435,436,438,441,443,445,447,450],{"class":67,"line":68},[65,437,154],{"class":102},[65,439,440],{"class":71}," cl ",[65,442,160],{"class":102},[65,444,163],{"class":109},[65,446,166],{"class":71},[65,448,449],{"class":109},"make_edge_connected_component_labels",[65,451,452],{"class":71},"(mesh);\n",[65,454,455],{"class":67,"line":75},[65,456,458],{"class":457},"sAwPA","\u002F\u002F cl.n_components == 2\n",[65,460,461],{"class":67,"line":81},[65,462,463],{"class":457},"\u002F\u002F cl.labels: tf::buffer\u003Cint>, one label per face\n",[14,465,466],{},[43,467],{"alt":468,"src":469},"Step: labeling connected components. The Utah teapot colored by component label: body, lid, spout and handle are four separate shells","\u002Fimg\u002Ftutorials\u002Fsteps\u002Ffast-connected-components-1-labels.png",[14,471,472],{},"Face counts per component are one pass over the labels:",[56,474,476],{"className":130,"code":475,"language":132,"meta":61,"style":61},"std::vector\u003Cint> sizes(cl.n_components, 0);\nfor (auto label : cl.labels) ++sizes[label];\n\u002F\u002F sizes == {760, 12}\n",[32,477,478,518,537],{"__ignoreMap":61},[65,479,480,483,485,488,491,494,497,500,502,505,507,511,513,516],{"class":67,"line":68},[65,481,482],{"class":109},"std",[65,484,166],{"class":71},[65,486,487],{"class":109},"vector",[65,489,490],{"class":71},"\u003C",[65,492,493],{"class":102},"int",[65,495,496],{"class":71},"> ",[65,498,499],{"class":109},"sizes",[65,501,172],{"class":71},[65,503,504],{"class":109},"cl",[65,506,27],{"class":71},[65,508,510],{"class":509},"s9osk","n_components",[65,512,254],{"class":71},[65,514,515],{"class":247},"0",[65,517,178],{"class":71},[65,519,520,523,526,528,531,534],{"class":67,"line":75},[65,521,522],{"class":102},"for",[65,524,525],{"class":71}," (",[65,527,154],{"class":102},[65,529,530],{"class":71}," label : cl.labels) ",[65,532,533],{"class":102},"++",[65,535,536],{"class":71},"sizes[label];\n",[65,538,539],{"class":67,"line":81},[65,540,541],{"class":457},"\u002F\u002F sizes == {760, 12}\n",[14,543,544],{},"Two other rules are one call each. The manifold variant separates parts at non-manifold edges. The vertex variant joins parts that only touch at a vertex, and its labels are per vertex:",[56,546,548],{"className":130,"code":547,"language":132,"meta":61,"style":61},"auto mcl = tf::make_manifold_edge_connected_component_labels(mesh);\nauto vcl = tf::make_vertex_connected_component_labels(mesh);  \u002F\u002F per vertex\n",[32,549,550,568],{"__ignoreMap":61},[65,551,552,554,557,559,561,563,566],{"class":67,"line":68},[65,553,154],{"class":102},[65,555,556],{"class":71}," mcl ",[65,558,160],{"class":102},[65,560,163],{"class":109},[65,562,166],{"class":71},[65,564,565],{"class":109},"make_manifold_edge_connected_component_labels",[65,567,452],{"class":71},[65,569,570,572,575,577,579,581,584,587],{"class":67,"line":75},[65,571,154],{"class":102},[65,573,574],{"class":71}," vcl ",[65,576,160],{"class":102},[65,578,163],{"class":109},[65,580,166],{"class":71},[65,582,583],{"class":109},"make_vertex_connected_component_labels",[65,585,586],{"class":71},"(mesh);",[65,588,589],{"class":457},"  \u002F\u002F per vertex\n",[14,591,592,593,596],{},"If a ",[32,594,595],{},"face_link"," or other topology is already tagged on the mesh, these calls reuse it.",[48,598,600],{"id":599},"custom-connectivity","Custom connectivity",[14,602,603,604,611],{},"For control over the rule, build the structures yourself and pass an applier to ",[18,605,608],{"href":606,"rel":607},"https:\u002F\u002Ftrueform.polydera.com\u002Fcpp\u002Fmodules\u002Ftopology#low-level-control",[121],[32,609,610],{},"tf::label_connected_components",":",[56,613,615],{"className":130,"code":614,"language":132,"meta":61,"style":61},"tf::face_membership\u003Cint> fm;\ntf::manifold_edge_link\u003Cint, 3> mel;\nfm.build(mesh);\nmel.build(mesh.faces(), fm);\n\ntf::buffer\u003Cint> labels;\nlabels.allocate(mesh.size());\nauto n = tf::label_connected_components(labels, tf::make_applier(mel));\n",[32,616,617,630,651,661,677,681,693,709],{"__ignoreMap":61},[65,618,619,621,624,627],{"class":67,"line":68},[65,620,404],{"class":109},[65,622,623],{"class":71},"::face_membership",[65,625,626],{"class":102},"\u003Cint>",[65,628,629],{"class":71}," fm;\n",[65,631,632,634,637,640,642,645,648],{"class":67,"line":75},[65,633,404],{"class":109},[65,635,636],{"class":71},"::manifold_edge_link",[65,638,639],{"class":102},"\u003Cint",[65,641,254],{"class":71},[65,643,644],{"class":247},"3",[65,646,647],{"class":102},">",[65,649,650],{"class":71}," mel;\n",[65,652,653,656,659],{"class":67,"line":81},[65,654,655],{"class":71},"fm.",[65,657,658],{"class":109},"build",[65,660,452],{"class":71},[65,662,663,666,668,671,674],{"class":67,"line":87},[65,664,665],{"class":71},"mel.",[65,667,658],{"class":109},[65,669,670],{"class":71},"(mesh.",[65,672,673],{"class":109},"faces",[65,675,676],{"class":71},"(), fm);\n",[65,678,679],{"class":67,"line":93},[65,680,149],{"emptyLinePlaceholder":148},[65,682,683,685,688,690],{"class":67,"line":99},[65,684,404],{"class":109},[65,686,687],{"class":71},"::buffer",[65,689,626],{"class":102},[65,691,692],{"class":71}," labels;\n",[65,694,695,698,701,703,706],{"class":67,"line":401},[65,696,697],{"class":71},"labels.",[65,699,700],{"class":109},"allocate",[65,702,670],{"class":71},[65,704,705],{"class":109},"size",[65,707,708],{"class":71},"());\n",[65,710,712,714,717,719,721,723,726,729,731,733,736],{"class":67,"line":711},8,[65,713,154],{"class":102},[65,715,716],{"class":71}," n ",[65,718,160],{"class":102},[65,720,163],{"class":109},[65,722,166],{"class":71},[65,724,725],{"class":109},"label_connected_components",[65,727,728],{"class":71},"(labels, ",[65,730,404],{"class":109},[65,732,166],{"class":71},[65,734,735],{"class":109},"make_applier",[65,737,738],{"class":71},"(mel));\n",[14,740,741,742,745,746,749,750,753,754,757],{},"An applier is any callable ",[32,743,744],{},"(id, callback)"," that calls ",[32,747,748],{},"callback(neighbor)"," for each neighbor of ",[32,751,752],{},"id",". Write your own to join only faces with similar normals, or faces with the same material. The labeling runs in parallel by default. On a graph with more than 500 components, pass the expected count as a third argument and it uses the sequential algorithm. There is also ",[32,755,756],{},"tf::label_connected_components_masked"," to leave chosen faces out.",[48,759,761],{"id":760},"splitting-into-parts","Splitting into parts",[14,763,764,771],{},[18,765,768],{"href":766,"rel":767},"https:\u002F\u002Ftrueform.polydera.com\u002Fcpp\u002Fmodules\u002Freindex#split-into-components",[121],[32,769,770],{},"tf::split_into_components"," returns one standalone mesh per label, with unused points dropped and indices renumbered:",[56,773,775],{"className":130,"code":774,"language":132,"meta":61,"style":61},"auto [components, component_labels] =\n    tf::split_into_components(mesh, cl.labels);\n\nfor (auto [component, label] : tf::zip(components, component_labels)) {\n  auto part = component.polygons();\n  std::cout \u003C\u003C label \u003C\u003C \": \" \u003C\u003C part.size() \u003C\u003C \" faces, \"\n            \u003C\u003C part.points().size() \u003C\u003C \" points\\n\";\n}\n\u002F\u002F 0: 760 faces, 382 points\n\u002F\u002F 1: 12 faces, 8 points\n",[32,776,777,787,799,803,824,841,875,906,911,917],{"__ignoreMap":61},[65,778,779,781,784],{"class":67,"line":68},[65,780,154],{"class":102},[65,782,783],{"class":71}," [components, component_labels] ",[65,785,786],{"class":102},"=\n",[65,788,789,791,793,796],{"class":67,"line":75},[65,790,324],{"class":109},[65,792,166],{"class":71},[65,794,795],{"class":109},"split_into_components",[65,797,798],{"class":71},"(mesh, cl.labels);\n",[65,800,801],{"class":67,"line":81},[65,802,149],{"emptyLinePlaceholder":148},[65,804,805,807,809,811,814,816,818,821],{"class":67,"line":87},[65,806,522],{"class":102},[65,808,525],{"class":71},[65,810,154],{"class":102},[65,812,813],{"class":71}," [component, label] : ",[65,815,404],{"class":109},[65,817,166],{"class":71},[65,819,820],{"class":109},"zip",[65,822,823],{"class":71},"(components, component_labels)) {\n",[65,825,826,829,832,834,837,839],{"class":67,"line":93},[65,827,828],{"class":102},"  auto",[65,830,831],{"class":71}," part ",[65,833,160],{"class":102},[65,835,836],{"class":71}," component.",[65,838,193],{"class":109},[65,840,196],{"class":71},[65,842,843,846,849,852,855,857,860,863,866,868,870,872],{"class":67,"line":99},[65,844,845],{"class":109},"  std",[65,847,848],{"class":71},"::cout ",[65,850,851],{"class":102},"\u003C\u003C",[65,853,854],{"class":71}," label ",[65,856,851],{"class":102},[65,858,859],{"class":142}," \": \"",[65,861,862],{"class":102}," \u003C\u003C",[65,864,865],{"class":71}," part.",[65,867,705],{"class":109},[65,869,385],{"class":71},[65,871,851],{"class":102},[65,873,874],{"class":142}," \" faces, \"\n",[65,876,877,880,882,885,888,890,892,894,897,900,903],{"class":67,"line":401},[65,878,879],{"class":102},"            \u003C\u003C",[65,881,865],{"class":71},[65,883,884],{"class":109},"points",[65,886,887],{"class":71},"().",[65,889,705],{"class":109},[65,891,385],{"class":71},[65,893,851],{"class":102},[65,895,896],{"class":142}," \" points",[65,898,899],{"class":247},"\\n",[65,901,902],{"class":142},"\"",[65,904,905],{"class":71},";\n",[65,907,908],{"class":67,"line":711},[65,909,910],{"class":71},"}\n",[65,912,914],{"class":67,"line":913},9,[65,915,916],{"class":457},"\u002F\u002F 0: 760 faces, 382 points\n",[65,918,920],{"class":67,"line":919},10,[65,921,922],{"class":457},"\u002F\u002F 1: 12 faces, 8 points\n",[14,924,925],{},[43,926],{"alt":927,"src":928},"Step: splitting into parts. The teapot split into four standalone meshes, one per label, pulled apart","\u002Fimg\u002Ftutorials\u002Fsteps\u002Ffast-connected-components-2-split.png",[14,930,931],{},"Keep only the largest part, a common clean-up for scans with floating debris:",[56,933,935],{"className":130,"code":934,"language":132,"meta":61,"style":61},"auto largest = std::max_element(sizes.begin(), sizes.end()) - sizes.begin();\nfor (auto [component, label] : tf::zip(components, component_labels))\n  if (label == largest)\n    tf::write_stl(component.polygons(), \"largest.stl\");\n",[32,936,937,979,998,1012],{"__ignoreMap":61},[65,938,939,941,944,946,949,951,954,957,960,963,966,969,972,975,977],{"class":67,"line":68},[65,940,154],{"class":102},[65,942,943],{"class":71}," largest ",[65,945,160],{"class":102},[65,947,948],{"class":109}," std",[65,950,166],{"class":71},[65,952,953],{"class":109},"max_element",[65,955,956],{"class":71},"(sizes.",[65,958,959],{"class":109},"begin",[65,961,962],{"class":71},"(), sizes.",[65,964,965],{"class":109},"end",[65,967,968],{"class":71},"()) ",[65,970,971],{"class":102},"-",[65,973,974],{"class":71}," sizes.",[65,976,959],{"class":109},[65,978,196],{"class":71},[65,980,981,983,985,987,989,991,993,995],{"class":67,"line":75},[65,982,522],{"class":102},[65,984,525],{"class":71},[65,986,154],{"class":102},[65,988,813],{"class":71},[65,990,404],{"class":109},[65,992,166],{"class":71},[65,994,820],{"class":109},[65,996,997],{"class":71},"(components, component_labels))\n",[65,999,1000,1003,1006,1009],{"class":67,"line":81},[65,1001,1002],{"class":102},"  if",[65,1004,1005],{"class":71}," (label ",[65,1007,1008],{"class":102},"==",[65,1010,1011],{"class":71}," largest)\n",[65,1013,1014,1016,1018,1020,1023,1025,1027,1030],{"class":67,"line":87},[65,1015,324],{"class":109},[65,1017,166],{"class":71},[65,1019,409],{"class":109},[65,1021,1022],{"class":71},"(component.",[65,1024,193],{"class":109},[65,1026,417],{"class":71},[65,1028,1029],{"class":142},"\"largest.stl\"",[65,1031,178],{"class":71},[14,1033,1034,1035,1039],{},"The labels do not have to come from connectivity. Any per-face label buffer works, for example the labels a ",[18,1036,1038],{"href":1037},"\u002Ftutorials\u002Ffast-mesh-booleans-in-cpp","mesh boolean"," returns.",[48,1041,1043],{"id":1042},"finding-holes-and-open-edges","Finding holes and open edges",[14,1045,1046],{},"A boundary edge belongs to only one face. On a closed mesh there are none. Cut a hole in a plane to see them:",[56,1048,1050],{"className":130,"code":1049,"language":132,"meta":61,"style":61},"auto plane = tf::make_plane_mesh(4.f, 4.f, 8, 8);\ntf::buffer\u003Cbool> keep;\nkeep.allocate(plane.polygons().size());\ntf::parallel_transform(plane.polygons(), keep, [](auto tri) {\n  auto c = tf::centroid(tri);\n  return c[0] * c[0] + c[1] * c[1] > 0.8f * 0.8f;  \u002F\u002F cut a hole in the middle\n});\nauto sheet_buffer = tf::reindexed_by_mask(plane.polygons(), keep);\nauto sheet = sheet_buffer.polygons();\n\nbool closed = tf::is_closed(sheet);  \u002F\u002F false\n",[32,1051,1052,1092,1104,1122,1146,1165,1225,1230,1253,1269,1273],{"__ignoreMap":61},[65,1053,1054,1056,1059,1061,1063,1065,1068,1070,1073,1075,1077,1079,1081,1083,1086,1088,1090],{"class":67,"line":68},[65,1055,154],{"class":102},[65,1057,1058],{"class":71}," plane ",[65,1060,160],{"class":102},[65,1062,163],{"class":109},[65,1064,166],{"class":71},[65,1066,1067],{"class":109},"make_plane_mesh",[65,1069,172],{"class":71},[65,1071,1072],{"class":247},"4.",[65,1074,251],{"class":102},[65,1076,254],{"class":71},[65,1078,1072],{"class":247},[65,1080,251],{"class":102},[65,1082,254],{"class":71},[65,1084,1085],{"class":247},"8",[65,1087,254],{"class":71},[65,1089,1085],{"class":247},[65,1091,178],{"class":71},[65,1093,1094,1096,1098,1101],{"class":67,"line":75},[65,1095,404],{"class":109},[65,1097,687],{"class":71},[65,1099,1100],{"class":102},"\u003Cbool>",[65,1102,1103],{"class":71}," keep;\n",[65,1105,1106,1109,1111,1114,1116,1118,1120],{"class":67,"line":81},[65,1107,1108],{"class":71},"keep.",[65,1110,700],{"class":109},[65,1112,1113],{"class":71},"(plane.",[65,1115,193],{"class":109},[65,1117,887],{"class":71},[65,1119,705],{"class":109},[65,1121,708],{"class":71},[65,1123,1124,1126,1128,1131,1133,1135,1138,1140,1143],{"class":67,"line":87},[65,1125,404],{"class":109},[65,1127,166],{"class":71},[65,1129,1130],{"class":109},"parallel_transform",[65,1132,1113],{"class":71},[65,1134,193],{"class":109},[65,1136,1137],{"class":71},"(), keep, [](",[65,1139,154],{"class":102},[65,1141,1142],{"class":509}," tri",[65,1144,1145],{"class":71},") {\n",[65,1147,1148,1150,1153,1155,1157,1159,1162],{"class":67,"line":93},[65,1149,828],{"class":102},[65,1151,1152],{"class":71}," c ",[65,1154,160],{"class":102},[65,1156,163],{"class":109},[65,1158,166],{"class":71},[65,1160,1161],{"class":109},"centroid",[65,1163,1164],{"class":71},"(tri);\n",[65,1166,1167,1170,1173,1175,1178,1181,1183,1185,1187,1190,1192,1195,1197,1199,1201,1203,1205,1207,1210,1212,1215,1217,1219,1222],{"class":67,"line":99},[65,1168,1169],{"class":102},"  return",[65,1171,1172],{"class":71}," c[",[65,1174,515],{"class":247},[65,1176,1177],{"class":71},"] ",[65,1179,1180],{"class":102},"*",[65,1182,1172],{"class":71},[65,1184,515],{"class":247},[65,1186,1177],{"class":71},[65,1188,1189],{"class":102},"+",[65,1191,1172],{"class":71},[65,1193,1194],{"class":247},"1",[65,1196,1177],{"class":71},[65,1198,1180],{"class":102},[65,1200,1172],{"class":71},[65,1202,1194],{"class":247},[65,1204,1177],{"class":71},[65,1206,647],{"class":102},[65,1208,1209],{"class":247}," 0.8",[65,1211,251],{"class":102},[65,1213,1214],{"class":102}," *",[65,1216,1209],{"class":247},[65,1218,251],{"class":102},[65,1220,1221],{"class":71},";",[65,1223,1224],{"class":457},"  \u002F\u002F cut a hole in the middle\n",[65,1226,1227],{"class":67,"line":401},[65,1228,1229],{"class":71},"});\n",[65,1231,1232,1234,1237,1239,1241,1243,1246,1248,1250],{"class":67,"line":711},[65,1233,154],{"class":102},[65,1235,1236],{"class":71}," sheet_buffer ",[65,1238,160],{"class":102},[65,1240,163],{"class":109},[65,1242,166],{"class":71},[65,1244,1245],{"class":109},"reindexed_by_mask",[65,1247,1113],{"class":71},[65,1249,193],{"class":109},[65,1251,1252],{"class":71},"(), keep);\n",[65,1254,1255,1257,1260,1262,1265,1267],{"class":67,"line":913},[65,1256,154],{"class":102},[65,1258,1259],{"class":71}," sheet ",[65,1261,160],{"class":102},[65,1263,1264],{"class":71}," sheet_buffer.",[65,1266,193],{"class":109},[65,1268,196],{"class":71},[65,1270,1271],{"class":67,"line":919},[65,1272,149],{"emptyLinePlaceholder":148},[65,1274,1276,1279,1282,1284,1286,1288,1291,1294],{"class":67,"line":1275},11,[65,1277,1278],{"class":102},"bool",[65,1280,1281],{"class":71}," closed ",[65,1283,160],{"class":102},[65,1285,163],{"class":109},[65,1287,166],{"class":71},[65,1289,1290],{"class":109},"is_closed",[65,1292,1293],{"class":71},"(sheet);",[65,1295,1296],{"class":457},"  \u002F\u002F false\n",[14,1298,1299],{},[43,1300],{"alt":1301,"src":1302},"Step: finding holes and open edges. The underside of the Stanford bunny, an open scan, with its boundary edges in pink","\u002Fimg\u002Ftutorials\u002Fsteps\u002Ffast-connected-components-3-holes.png",[14,1304,1305,1308],{},[32,1306,1307],{},"tf::make_boundary_edges"," returns the open edges as vertex pairs:",[56,1310,1312],{"className":130,"code":1311,"language":132,"meta":61,"style":61},"auto boundary_edges = tf::make_boundary_edges(sheet);  \u002F\u002F 48 edges\n",[32,1313,1314],{"__ignoreMap":61},[65,1315,1316,1318,1321,1323,1325,1327,1330,1332],{"class":67,"line":68},[65,1317,154],{"class":102},[65,1319,1320],{"class":71}," boundary_edges ",[65,1322,160],{"class":102},[65,1324,163],{"class":109},[65,1326,166],{"class":71},[65,1328,1329],{"class":109},"make_boundary_edges",[65,1331,1293],{"class":71},[65,1333,1334],{"class":457},"  \u002F\u002F 48 edges\n",[48,1336,1338],{"id":1337},"boundary-loops","Boundary loops",[14,1340,1341,1344,1345,1348],{},[32,1342,1343],{},"tf::make_boundary_paths"," chains the open edges into loops of vertex ids. ",[32,1346,1347],{},"tf::make_curves"," pairs them with the points:",[56,1350,1352],{"className":130,"code":1351,"language":132,"meta":61,"style":61},"auto paths = tf::make_boundary_paths(sheet);\nauto curves = tf::make_curves(paths, sheet.points());\n\u002F\u002F paths.size() == 2: the outer rim and the hole\n\nfor (const auto &curve : curves) {\n  float perimeter = 0;\n  for (auto seg : tf::make_slide_range\u003C2>(curve))\n    perimeter += tf::distance(seg[0], seg[1]);\n  std::cout \u003C\u003C curve.indices().size() - 1 \u003C\u003C \" edges, perimeter \" \u003C\u003C perimeter \u003C\u003C \"\\n\";\n}\n\u002F\u002F 32 edges, perimeter 16\n\u002F\u002F 16 edges, perimeter 9.65685\n",[32,1353,1354,1373,1396,1401,1405,1423,1438,1465,1493,1538,1542,1547],{"__ignoreMap":61},[65,1355,1356,1358,1361,1363,1365,1367,1370],{"class":67,"line":68},[65,1357,154],{"class":102},[65,1359,1360],{"class":71}," paths ",[65,1362,160],{"class":102},[65,1364,163],{"class":109},[65,1366,166],{"class":71},[65,1368,1369],{"class":109},"make_boundary_paths",[65,1371,1372],{"class":71},"(sheet);\n",[65,1374,1375,1377,1380,1382,1384,1386,1389,1392,1394],{"class":67,"line":75},[65,1376,154],{"class":102},[65,1378,1379],{"class":71}," curves ",[65,1381,160],{"class":102},[65,1383,163],{"class":109},[65,1385,166],{"class":71},[65,1387,1388],{"class":109},"make_curves",[65,1390,1391],{"class":71},"(paths, sheet.",[65,1393,884],{"class":109},[65,1395,708],{"class":71},[65,1397,1398],{"class":67,"line":81},[65,1399,1400],{"class":457},"\u002F\u002F paths.size() == 2: the outer rim and the hole\n",[65,1402,1403],{"class":67,"line":87},[65,1404,149],{"emptyLinePlaceholder":148},[65,1406,1407,1409,1411,1414,1417,1420],{"class":67,"line":93},[65,1408,522],{"class":102},[65,1410,525],{"class":71},[65,1412,1413],{"class":102},"const",[65,1415,1416],{"class":102}," auto",[65,1418,1419],{"class":102}," &",[65,1421,1422],{"class":71},"curve : curves) {\n",[65,1424,1425,1428,1431,1433,1436],{"class":67,"line":99},[65,1426,1427],{"class":102},"  float",[65,1429,1430],{"class":71}," perimeter ",[65,1432,160],{"class":102},[65,1434,1435],{"class":247}," 0",[65,1437,905],{"class":71},[65,1439,1440,1443,1445,1447,1450,1452,1454,1457,1459,1462],{"class":67,"line":401},[65,1441,1442],{"class":102},"  for",[65,1444,525],{"class":71},[65,1446,154],{"class":102},[65,1448,1449],{"class":71}," seg : ",[65,1451,404],{"class":109},[65,1453,166],{"class":71},[65,1455,1456],{"class":109},"make_slide_range",[65,1458,490],{"class":71},[65,1460,1461],{"class":247},"2",[65,1463,1464],{"class":71},">(curve))\n",[65,1466,1467,1470,1473,1475,1477,1480,1483,1485,1488,1490],{"class":67,"line":711},[65,1468,1469],{"class":71},"    perimeter ",[65,1471,1472],{"class":102},"+=",[65,1474,163],{"class":109},[65,1476,166],{"class":71},[65,1478,1479],{"class":109},"distance",[65,1481,1482],{"class":71},"(seg[",[65,1484,515],{"class":247},[65,1486,1487],{"class":71},"], seg[",[65,1489,1194],{"class":247},[65,1491,1492],{"class":71},"]);\n",[65,1494,1495,1497,1499,1501,1504,1507,1509,1511,1513,1515,1518,1520,1523,1525,1527,1529,1532,1534,1536],{"class":67,"line":913},[65,1496,845],{"class":109},[65,1498,848],{"class":71},[65,1500,851],{"class":102},[65,1502,1503],{"class":71}," curve.",[65,1505,1506],{"class":109},"indices",[65,1508,887],{"class":71},[65,1510,705],{"class":109},[65,1512,385],{"class":71},[65,1514,971],{"class":102},[65,1516,1517],{"class":247}," 1",[65,1519,862],{"class":102},[65,1521,1522],{"class":142}," \" edges, perimeter \"",[65,1524,862],{"class":102},[65,1526,1430],{"class":71},[65,1528,851],{"class":102},[65,1530,1531],{"class":142}," \"",[65,1533,899],{"class":247},[65,1535,902],{"class":142},[65,1537,905],{"class":71},[65,1539,1540],{"class":67,"line":919},[65,1541,910],{"class":71},[65,1543,1544],{"class":67,"line":1275},[65,1545,1546],{"class":457},"\u002F\u002F 32 edges, perimeter 16\n",[65,1548,1550],{"class":67,"line":1549},12,[65,1551,1552],{"class":457},"\u002F\u002F 16 edges, perimeter 9.65685\n",[14,1554,1555],{},[43,1556],{"alt":1557,"src":1558},"Step: boundary loops. The bunny's open edges chained into five loops, each in its own color","\u002Fimg\u002Ftutorials\u002Fsteps\u002Ffast-connected-components-4-loops.png",[14,1560,1561,1562,1565,1566,1569],{},"A closed loop repeats its first vertex at the end. ",[32,1563,1564],{},"curve.indices()"," gives the vertex ids; iterating ",[32,1567,1568],{},"curve"," gives the points. On a single sheet, the longest loop is usually the outer rim and the rest are holes. On a closed part, every loop is a hole.",[14,1571,1572,1573,1580,1581,1584],{},"For more detail per loop, ",[18,1574,1577],{"href":1575,"rel":1576},"https:\u002F\u002Ftrueform.polydera.com\u002Fcpp\u002Fmodules\u002Ftopology#boundary-rims",[121],[32,1578,1579],{},"tf::make_boundary_rims"," returns each rim's vertex walk, the face along each rim edge, and whether the rim closes. Tag a ",[32,1582,1583],{},"face_membership"," to share it between calls:",[56,1586,1588],{"className":130,"code":1587,"language":132,"meta":61,"style":61},"auto fm = tf::make_face_membership(sheet);\nauto tagged = sheet | tf::tag(fm);\nauto rims = tf::make_boundary_rims(tagged);\n\nfor (std::size_t i = 0; i \u003C rims.size(); ++i) {\n  auto vertices = rims.vertices[i];  \u002F\u002F the rim walked\n  auto faces = rims.faces[i];        \u002F\u002F the face along each rim edge\n  bool rim_closed = rims.closed[i];\n}\n",[32,1589,1590,1608,1630,1649,1653,1691,1706,1721,1734],{"__ignoreMap":61},[65,1591,1592,1594,1597,1599,1601,1603,1606],{"class":67,"line":68},[65,1593,154],{"class":102},[65,1595,1596],{"class":71}," fm ",[65,1598,160],{"class":102},[65,1600,163],{"class":109},[65,1602,166],{"class":71},[65,1604,1605],{"class":109},"make_face_membership",[65,1607,1372],{"class":71},[65,1609,1610,1612,1615,1617,1619,1621,1623,1625,1627],{"class":67,"line":75},[65,1611,154],{"class":102},[65,1613,1614],{"class":71}," tagged ",[65,1616,160],{"class":102},[65,1618,1259],{"class":71},[65,1620,388],{"class":102},[65,1622,163],{"class":109},[65,1624,166],{"class":71},[65,1626,395],{"class":109},[65,1628,1629],{"class":71},"(fm);\n",[65,1631,1632,1634,1637,1639,1641,1643,1646],{"class":67,"line":81},[65,1633,154],{"class":102},[65,1635,1636],{"class":71}," rims ",[65,1638,160],{"class":102},[65,1640,163],{"class":109},[65,1642,166],{"class":71},[65,1644,1645],{"class":109},"make_boundary_rims",[65,1647,1648],{"class":71},"(tagged);\n",[65,1650,1651],{"class":67,"line":87},[65,1652,149],{"emptyLinePlaceholder":148},[65,1654,1655,1657,1659,1661,1663,1666,1669,1671,1673,1676,1678,1681,1683,1686,1688],{"class":67,"line":93},[65,1656,522],{"class":102},[65,1658,525],{"class":71},[65,1660,482],{"class":109},[65,1662,166],{"class":71},[65,1664,1665],{"class":102},"size_t",[65,1667,1668],{"class":71}," i ",[65,1670,160],{"class":102},[65,1672,1435],{"class":247},[65,1674,1675],{"class":71},"; i ",[65,1677,490],{"class":102},[65,1679,1680],{"class":71}," rims.",[65,1682,705],{"class":109},[65,1684,1685],{"class":71},"(); ",[65,1687,533],{"class":102},[65,1689,1690],{"class":71},"i) {\n",[65,1692,1693,1695,1698,1700,1703],{"class":67,"line":99},[65,1694,828],{"class":102},[65,1696,1697],{"class":71}," vertices ",[65,1699,160],{"class":102},[65,1701,1702],{"class":71}," rims.vertices[i];",[65,1704,1705],{"class":457},"  \u002F\u002F the rim walked\n",[65,1707,1708,1710,1713,1715,1718],{"class":67,"line":401},[65,1709,828],{"class":102},[65,1711,1712],{"class":71}," faces ",[65,1714,160],{"class":102},[65,1716,1717],{"class":71}," rims.faces[i];",[65,1719,1720],{"class":457},"        \u002F\u002F the face along each rim edge\n",[65,1722,1723,1726,1729,1731],{"class":67,"line":711},[65,1724,1725],{"class":102},"  bool",[65,1727,1728],{"class":71}," rim_closed ",[65,1730,160],{"class":102},[65,1732,1733],{"class":71}," rims.closed[i];\n",[65,1735,1736],{"class":67,"line":913},[65,1737,910],{"class":71},[14,1739,1740,1741,22,1744,1747],{},"A closed rim does not repeat its first vertex: the rim of 32 edges has 32 vertices. ",[32,1742,1743],{},"tf::has_holes(tagged)",[32,1745,1746],{},"tf::is_open(tagged)"," answer the yes-or-no questions without building the loops.",[48,1749,1751],{"id":1750},"working-with-results","Working with results",[14,1753,1754,1755,1758,1759,1762,1763,1766,1767,1770],{},"Labels are a ",[32,1756,1757],{},"tf::buffer\u003Cint>",". Parts are ",[32,1760,1761],{},"tf::polygons_buffer","s that work with every other call in the library. Write them with ",[32,1764,1765],{},"tf::write_stl"," or ",[32,1768,1769],{},"tf::write_obj",". Common next steps:",[1772,1773,1774,1782,1790],"ul",{},[1775,1776,1777,1778,27],"li",{},"Find the closest point on the part you kept: ",[18,1779,1781],{"href":1780},"\u002Ftutorials\u002Ffast-closest-point-on-a-mesh-in-cpp","closest point on a mesh",[1775,1783,1784,1785,27],{},"Reduce a part's triangle count: ",[18,1786,1789],{"href":1787,"rel":1788},"https:\u002F\u002Ftrueform.polydera.com\u002Fcpp\u002Fmodules\u002Fremesh",[121],"mesh decimation",[1775,1791,1792,1793,27],{},"Join or cut the cleaned parts: ",[18,1794,1795],{"href":1037},"mesh booleans",[48,1797,1799],{"id":1798},"related-tutorials","Related tutorials",[1772,1801,1802,1808,1814,1820],{},[1775,1803,1804,1807],{},[18,1805,1806],{"href":1037},"Fast Mesh Booleans in C++"," — exact union, intersection and difference of two meshes.",[1775,1809,1810,1813],{},[18,1811,1812],{"href":213},"Fast STL and OBJ Reading in C++"," — load and write meshes, the first step of every pipeline.",[1775,1815,1816,1819],{},[18,1817,1818],{"href":1780},"Fast and Robust Closest Point on a Mesh in C++"," — nearest surface point for a batch of queries.",[1775,1821,1822,1826],{},[18,1823,1825],{"href":1824},"\u002Ftutorials\u002Fmesh-slicing-and-cross-sections-in-cpp","Fast and Robust Mesh Slicing and Cross-Sections in C++"," — plane sections, layers and slabs of a mesh.",[48,1828,1830],{"id":1829},"performance","Performance",[14,1832,1833],{},"The benchmark labels edge-connected faces on 2,000 production meshes of 100,368 to 938,940 triangles, in C++, against CGAL and libigl. Building the connectivity is inside the timer for all three.",[1835,1836],"headline-numbers",{":items":1837},"[{\"value\":\"3.6ms\",\"label\":\"Connected components\",\"detail\":\"Median, 2,000 meshes\"},{\"value\":\"7×\",\"label\":\"Faster than CGAL\",\"detail\":\"Geometric mean wall time\"},{\"value\":\"12×\",\"label\":\"Faster than libigl\",\"detail\":\"Geometric mean wall time\"}]",[14,1839,1840,1845,1846],{},[65,1841,1844],{"className":1842},[1843],"tag-pill","Apple M4 Max"," ",[65,1847,1849],{"className":1848},[1843],"16 threads",[1851,1852],"data-table",{":headers":1853,":highlight":515,":rows":1854},"[\"Library\",\"Median\",\"Geometric mean\",\"vs trueform\"]","[[\"trueform\",\"3.6 ms\",\"4.1 ms\",\"1.0×\"],[\"CGAL\",\"24.3 ms\",\"29.4 ms\",\"7.1×\"],[\"libigl\",\"40.9 ms\",\"48.0 ms\",\"11.7×\"]]",[14,1856,1857],{},"Median of fresh-process medians per mesh. Every result is checked: all 2,000 meshes are valid for all three libraries. There is no published benchmark for boundary loops, so that part of the page makes no speed claim.",[14,1859,1860],{},[43,1861],{"alt":1862,"src":1863},"Benchmark chart: trueform against other C++ libraries, connected components","\u002Fimg\u002Falgorithms\u002Fbenchmark-connected-components-cpp-2026.png",[14,1865,1866,1870,1871],{},[18,1867,1869],{"href":1868},"\u002Ftrueform\u002Fbenchmarks","Full benchmarks"," · ",[18,1872,1875],{"href":1873,"rel":1874},"https:\u002F\u002Ftrueform.polydera.com\u002Fcpp\u002Fmodules\u002Ftopology#connected-components",[121],"Documentation",[1877,1878],"cite-as",{"author":1879,"title":5},"Sajovic, {\\v{Z}}iga",[1881,1882,1883],"style",{},"html pre.shiki code .s95oV, html code.shiki .s95oV{--shiki-default:#E1E4E8}html pre.shiki code .snl16, html code.shiki .snl16{--shiki-default:#F97583}html pre.shiki code .svObZ, html code.shiki .svObZ{--shiki-default:#B392F0}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html pre.shiki code .sU2Wk, html code.shiki .sU2Wk{--shiki-default:#9ECBFF}html pre.shiki code .sDLfK, html code.shiki .sDLfK{--shiki-default:#79B8FF}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":61,"searchDepth":75,"depth":75,"links":1885},[1886,1887,1888,1889,1890,1891,1892,1893,1894,1895],{"id":50,"depth":75,"text":51},{"id":126,"depth":75,"text":127},{"id":424,"depth":75,"text":425},{"id":599,"depth":75,"text":600},{"id":760,"depth":75,"text":761},{"id":1042,"depth":75,"text":1043},{"id":1337,"depth":75,"text":1338},{"id":1750,"depth":75,"text":1751},{"id":1798,"depth":75,"text":1799},{"id":1829,"depth":75,"text":1830},"2026-10-31","Label the connected components of a triangle mesh, split it into separate parts, and find its holes and open edges as boundary loops, in C++. Header-only, C++17. Measured against CGAL and libigl on 2,000 meshes.","md",[1900,1901,1902,1903,1904,1905,1906,1907,1908,1909],"connected components mesh C++","mesh connected components C++","split mesh into parts C++","boundary loops mesh C++","find holes in mesh C++","boundary edges C++","CGAL connected_components alternative","libigl facet_components alternative","libigl boundary_loop alternative","mesh topology library C++",{},"\u002Fimg\u002Fog\u002Ftutorials\u002Ffast-connected-components-in-cpp.png","\u002Ftutorials\u002Ffast-connected-components-in-cpp",{"title":5,"description":1897},"tutorials\u002Ffast-connected-components-in-cpp",[1916,1917,1918,1919],"c++","topology","connected-components","tutorial","w6mMvRK3HHN0jzdox_0WYxxR-m-ZOMpaAqwx1p8TrAM",1793517812008]