[{"data":1,"prerenderedAt":1303},["ShallowReactive",2],{"tutorial-constrained-delaunay-triangulation-in-python":3},{"id":4,"title":5,"author":6,"body":7,"date":1278,"description":1279,"extension":1280,"keywords":1281,"meta":1293,"navigation":153,"ogImage":1294,"path":1295,"published":153,"seo":1296,"stem":1297,"tags":1298,"__hash__":1302},"tutorials\u002Ftutorials\u002Fconstrained-delaunay-triangulation-in-python.md","Fast and Exact Constrained Delaunay Triangulation in Python","Žiga Sajovic",{"type":8,"value":9,"toc":1267},"minimark",[10,28,44,48,55,60,85,95,99,112,210,224,228,231,268,274,285,289,292,564,570,573,577,580,634,640,655,806,813,860,864,871,927,933,945,975,984,988,1008,1011,1094,1117,1120,1158,1170,1174,1203,1207,1210,1214,1226,1231,1234,1237,1241,1247,1259,1263],[11,12,13],"note",{},[14,15,16,17,22,23,27],"p",{},"Also available in ",[18,19,21],"a",{"href":20},"\u002Ftutorials\u002Fconstrained-delaunay-triangulation-in-cpp","C++"," and ",[18,24,26],{"href":25},"\u002Ftutorials\u002Fconstrained-delaunay-triangulation-in-javascript","JavaScript",".",[14,29,30,31,35,36,39,40,43],{},"A constrained Delaunay triangulation (CDT) triangulates a set of 2D points and keeps chosen edges in the output. Those edges can be an outline, the rim of a hole, or a feature line. ",[32,33,34],"code",{},"trueform"," does it with ",[32,37,38],{},"tf.cdt",". Constraint edges may cross each other; the crossings are split for you. One ",[32,41,42],{},"pip install",". NumPy arrays in, NumPy arrays out. This tutorial covers plain Delaunay, outlines with holes, crossing constraints, region labels and index maps, then shows measured times against Triangle and SciPy.",[45,46],"article-cta",{":buttons":47},"[{\"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\u002Ftopology#constrained-delaunay-triangulation\",\"variant\":\"soft\",\"color\":\"neutral\"}]",[14,49,50],{},[51,52],"img",{"alt":53,"src":54},"A constrained Delaunay triangulation of a wavy outline with three holes and a constraint curve","\u002Fimg\u002Ftutorials\u002Fconstrained-delaunay.png",[56,57,59],"h2",{"id":58},"install","Install",[61,62,67],"pre",{"className":63,"code":64,"language":65,"meta":66,"style":66},"language-bash shiki shiki-themes github-dark","pip install trueform\n","bash","",[32,68,69],{"__ignoreMap":66},[70,71,74,78,82],"span",{"class":72,"line":73},"line",1,[70,75,77],{"class":76},"svObZ","pip",[70,79,81],{"class":80},"sU2Wk"," install",[70,83,84],{"class":80}," trueform\n",[14,86,87,88,94],{},"See the ",[18,89,93],{"href":90,"rel":91},"https:\u002F\u002Ftrueform.polydera.com\u002Fpy\u002Fgetting-started\u002Finstallation",[92],"nofollow","installation guide"," for all options.",[56,96,98],{"id":97},"loading-the-points","Loading the points",[14,100,101,103,104,107,108,111],{},[32,102,38],{}," takes an ",[32,105,106],{},"(N, 2)"," point array and, optionally, an ",[32,109,110],{},"(E, 2)"," int32 array of constraint edges that index into it. No mesh object is needed.",[61,113,117],{"className":114,"code":115,"language":116,"meta":66,"style":66},"language-python shiki shiki-themes github-dark","import numpy as np\nimport trueform as tf\n\nrng = np.random.default_rng(0)\npoints = rng.uniform(-1, 1, (1000, 2)).astype(np.float32)\n","python",[32,118,119,135,148,155,174],{"__ignoreMap":66},[70,120,121,125,129,132],{"class":72,"line":73},[70,122,124],{"class":123},"snl16","import",[70,126,128],{"class":127},"s95oV"," numpy ",[70,130,131],{"class":123},"as",[70,133,134],{"class":127}," np\n",[70,136,138,140,143,145],{"class":72,"line":137},2,[70,139,124],{"class":123},[70,141,142],{"class":127}," trueform ",[70,144,131],{"class":123},[70,146,147],{"class":127}," tf\n",[70,149,151],{"class":72,"line":150},3,[70,152,154],{"emptyLinePlaceholder":153},true,"\n",[70,156,158,161,164,167,171],{"class":72,"line":157},4,[70,159,160],{"class":127},"rng ",[70,162,163],{"class":123},"=",[70,165,166],{"class":127}," np.random.default_rng(",[70,168,170],{"class":169},"sDLfK","0",[70,172,173],{"class":127},")\n",[70,175,177,180,182,185,188,191,194,196,199,202,204,207],{"class":72,"line":176},5,[70,178,179],{"class":127},"points ",[70,181,163],{"class":123},[70,183,184],{"class":127}," rng.uniform(",[70,186,187],{"class":123},"-",[70,189,190],{"class":169},"1",[70,192,193],{"class":127},", ",[70,195,190],{"class":169},[70,197,198],{"class":127},", (",[70,200,201],{"class":169},"1000",[70,203,193],{"class":127},[70,205,206],{"class":169},"2",[70,208,209],{"class":127},")).astype(np.float32)\n",[14,211,212,213,223],{},"If your points come from a 3D file, read it with ",[18,214,216,219,220],{"href":215},"\u002Ftutorials\u002Ffast-stl-and-obj-reading-in-python",[32,217,218],{},"tf.read_stl"," or ",[32,221,222],{},"tf.read_obj"," and project to two coordinates first.",[56,225,227],{"id":226},"delaunay-triangulation","Delaunay triangulation",[14,229,230],{},"Without edges you get the Delaunay triangulation of the convex hull:",[61,232,234],{"className":114,"code":233,"language":116,"meta":66,"style":66},"faces, out_points = tf.cdt(points)\nprint(faces.shape, faces.dtype)     # (1980, 3) int32\nprint(out_points.shape)             # (1000, 2)\n",[32,235,236,246,258],{"__ignoreMap":66},[70,237,238,241,243],{"class":72,"line":73},[70,239,240],{"class":127},"faces, out_points ",[70,242,163],{"class":123},[70,244,245],{"class":127}," tf.cdt(points)\n",[70,247,248,251,254],{"class":72,"line":137},[70,249,250],{"class":169},"print",[70,252,253],{"class":127},"(faces.shape, faces.dtype)     ",[70,255,257],{"class":256},"sAwPA","# (1980, 3) int32\n",[70,259,260,262,265],{"class":72,"line":150},[70,261,250],{"class":169},[70,263,264],{"class":127},"(out_points.shape)             ",[70,266,267],{"class":256},"# (1000, 2)\n",[14,269,270],{},[51,271],{"alt":272,"src":273},"Delaunay triangulation: 1,000 random points triangulated over their convex hull","\u002Fimg\u002Ftutorials\u002Fsteps\u002Fconstrained-delaunay-triangulation-1-delaunay.png",[14,275,276,277,280,281,284],{},"The faces index ",[32,278,279],{},"out_points",", not your input array. The output order can differ from the input order. To map between them, use ",[32,282,283],{},"return_index_map=True"," (below).",[56,286,288],{"id":287},"an-outline-with-a-hole","An outline with a hole",[14,290,291],{},"Pass the outline and the hole as constraint edges. Every constraint edge is a wall by default. Triangles inside an odd number of walls are kept, so a ring of walls inside another ring is a hole.",[61,293,295],{"className":114,"code":294,"language":116,"meta":66,"style":66},"outer = np.array([[-1, -1], [1, -1], [1, 1], [-1, 1]], dtype=np.float32)\ninner = 0.4 * outer\nsteiner = rng.uniform(-1, 1, (500, 2)).astype(np.float32)\nring_points = np.vstack([outer, inner, steiner])\n\nring_edges = np.array([\n    [0, 1], [1, 2], [2, 3], [3, 0],     # outer square\n    [4, 5], [5, 6], [6, 7], [7, 4],     # inner square: the hole\n], dtype=np.int32)\n\nfaces, out_points = tf.cdt(ring_points, ring_edges)\nprint(faces.shape)                  # (858, 3)\nprint(tf.area((faces, out_points))) # 3.36... = 4 - 0.64\n",[32,296,297,358,374,402,412,416,427,470,514,527,532,542,553],{"__ignoreMap":66},[70,298,299,302,304,307,309,311,313,315,317,320,322,324,326,328,330,332,334,336,338,340,342,344,346,349,353,355],{"class":72,"line":73},[70,300,301],{"class":127},"outer ",[70,303,163],{"class":123},[70,305,306],{"class":127}," np.array([[",[70,308,187],{"class":123},[70,310,190],{"class":169},[70,312,193],{"class":127},[70,314,187],{"class":123},[70,316,190],{"class":169},[70,318,319],{"class":127},"], [",[70,321,190],{"class":169},[70,323,193],{"class":127},[70,325,187],{"class":123},[70,327,190],{"class":169},[70,329,319],{"class":127},[70,331,190],{"class":169},[70,333,193],{"class":127},[70,335,190],{"class":169},[70,337,319],{"class":127},[70,339,187],{"class":123},[70,341,190],{"class":169},[70,343,193],{"class":127},[70,345,190],{"class":169},[70,347,348],{"class":127},"]], ",[70,350,352],{"class":351},"s9osk","dtype",[70,354,163],{"class":123},[70,356,357],{"class":127},"np.float32)\n",[70,359,360,363,365,368,371],{"class":72,"line":137},[70,361,362],{"class":127},"inner ",[70,364,163],{"class":123},[70,366,367],{"class":169}," 0.4",[70,369,370],{"class":123}," *",[70,372,373],{"class":127}," outer\n",[70,375,376,379,381,383,385,387,389,391,393,396,398,400],{"class":72,"line":150},[70,377,378],{"class":127},"steiner ",[70,380,163],{"class":123},[70,382,184],{"class":127},[70,384,187],{"class":123},[70,386,190],{"class":169},[70,388,193],{"class":127},[70,390,190],{"class":169},[70,392,198],{"class":127},[70,394,395],{"class":169},"500",[70,397,193],{"class":127},[70,399,206],{"class":169},[70,401,209],{"class":127},[70,403,404,407,409],{"class":72,"line":157},[70,405,406],{"class":127},"ring_points ",[70,408,163],{"class":123},[70,410,411],{"class":127}," np.vstack([outer, inner, steiner])\n",[70,413,414],{"class":72,"line":176},[70,415,154],{"emptyLinePlaceholder":153},[70,417,419,422,424],{"class":72,"line":418},6,[70,420,421],{"class":127},"ring_edges ",[70,423,163],{"class":123},[70,425,426],{"class":127}," np.array([\n",[70,428,430,433,435,437,439,441,443,445,447,449,451,453,456,458,460,462,464,467],{"class":72,"line":429},7,[70,431,432],{"class":127},"    [",[70,434,170],{"class":169},[70,436,193],{"class":127},[70,438,190],{"class":169},[70,440,319],{"class":127},[70,442,190],{"class":169},[70,444,193],{"class":127},[70,446,206],{"class":169},[70,448,319],{"class":127},[70,450,206],{"class":169},[70,452,193],{"class":127},[70,454,455],{"class":169},"3",[70,457,319],{"class":127},[70,459,455],{"class":169},[70,461,193],{"class":127},[70,463,170],{"class":169},[70,465,466],{"class":127},"],     ",[70,468,469],{"class":256},"# outer square\n",[70,471,473,475,478,480,483,485,487,489,492,494,496,498,501,503,505,507,509,511],{"class":72,"line":472},8,[70,474,432],{"class":127},[70,476,477],{"class":169},"4",[70,479,193],{"class":127},[70,481,482],{"class":169},"5",[70,484,319],{"class":127},[70,486,482],{"class":169},[70,488,193],{"class":127},[70,490,491],{"class":169},"6",[70,493,319],{"class":127},[70,495,491],{"class":169},[70,497,193],{"class":127},[70,499,500],{"class":169},"7",[70,502,319],{"class":127},[70,504,500],{"class":169},[70,506,193],{"class":127},[70,508,477],{"class":169},[70,510,466],{"class":127},[70,512,513],{"class":256},"# inner square: the hole\n",[70,515,517,520,522,524],{"class":72,"line":516},9,[70,518,519],{"class":127},"], ",[70,521,352],{"class":351},[70,523,163],{"class":123},[70,525,526],{"class":127},"np.int32)\n",[70,528,530],{"class":72,"line":529},10,[70,531,154],{"emptyLinePlaceholder":153},[70,533,535,537,539],{"class":72,"line":534},11,[70,536,240],{"class":127},[70,538,163],{"class":123},[70,540,541],{"class":127}," tf.cdt(ring_points, ring_edges)\n",[70,543,545,547,550],{"class":72,"line":544},12,[70,546,250],{"class":169},[70,548,549],{"class":127},"(faces.shape)                  ",[70,551,552],{"class":256},"# (858, 3)\n",[70,554,556,558,561],{"class":72,"line":555},13,[70,557,250],{"class":169},[70,559,560],{"class":127},"(tf.area((faces, out_points))) ",[70,562,563],{"class":256},"# 3.36... = 4 - 0.64\n",[14,565,566],{},[51,567],{"alt":568,"src":569},"An outline with a hole: the outer square and the inner square as constraint walls; the triangles inside the hole are dropped","\u002Fimg\u002Ftutorials\u002Fsteps\u002Fconstrained-delaunay-triangulation-2-hole.png",[14,571,572],{},"The interior points inside the hole are dropped, and the area is the square minus the hole.",[56,574,576],{"id":575},"crossing-constraints-and-feature-lines","Crossing constraints and feature lines",[14,578,579],{},"Constraints may cross. The crossing point is added to the output:",[61,581,583],{"className":114,"code":582,"language":116,"meta":66,"style":66},"diagonals = np.array([[0, 2], [1, 3]], dtype=np.int32)\nfaces, out_points = tf.cdt(outer, diagonals)\nprint(out_points.shape)             # (5, 2): the four corners and the crossing at (0, 0)\n",[32,584,585,616,625],{"__ignoreMap":66},[70,586,587,590,592,594,596,598,600,602,604,606,608,610,612,614],{"class":72,"line":73},[70,588,589],{"class":127},"diagonals ",[70,591,163],{"class":123},[70,593,306],{"class":127},[70,595,170],{"class":169},[70,597,193],{"class":127},[70,599,206],{"class":169},[70,601,319],{"class":127},[70,603,190],{"class":169},[70,605,193],{"class":127},[70,607,455],{"class":169},[70,609,348],{"class":127},[70,611,352],{"class":351},[70,613,163],{"class":123},[70,615,526],{"class":127},[70,617,618,620,622],{"class":72,"line":137},[70,619,240],{"class":127},[70,621,163],{"class":123},[70,623,624],{"class":127}," tf.cdt(outer, diagonals)\n",[70,626,627,629,631],{"class":72,"line":150},[70,628,250],{"class":169},[70,630,264],{"class":127},[70,632,633],{"class":256},"# (5, 2): the four corners and the crossing at (0, 0)\n",[14,635,636],{},[51,637],{"alt":638,"src":639},"Crossing constraints and feature lines: two crossing diagonals as walls keep two triangles; with the outline as walls and the diagonals as feature lines the square stays solid in four quarters; the crossing point is added at the centre","\u002Fimg\u002Ftutorials\u002Fsteps\u002Fconstrained-delaunay-triangulation-3-crossing.png",[14,641,642,643,646,647,650,651,654],{},"To keep a constraint as an edge without making it a wall, pass an ",[32,644,645],{},"edge_mask",". ",[32,648,649],{},"True"," marks a wall, ",[32,652,653],{},"False"," a feature line that splits nothing:",[61,656,658],{"className":114,"code":657,"language":116,"meta":66,"style":66},"edges = np.array([\n    [0, 1], [1, 2], [2, 3], [3, 0],     # outline: walls\n    [0, 2], [1, 3],                     # diagonals: feature lines\n], dtype=np.int32)\nedge_mask = np.array([True, True, True, True, False, False])\n\nfaces, out_points = tf.cdt(outer, edges, edge_mask=edge_mask)\nprint(faces.shape)                  # (4, 3): four quarter triangles, square stays solid\n",[32,659,660,669,708,732,742,777,781,797],{"__ignoreMap":66},[70,661,662,665,667],{"class":72,"line":73},[70,663,664],{"class":127},"edges ",[70,666,163],{"class":123},[70,668,426],{"class":127},[70,670,671,673,675,677,679,681,683,685,687,689,691,693,695,697,699,701,703,705],{"class":72,"line":137},[70,672,432],{"class":127},[70,674,170],{"class":169},[70,676,193],{"class":127},[70,678,190],{"class":169},[70,680,319],{"class":127},[70,682,190],{"class":169},[70,684,193],{"class":127},[70,686,206],{"class":169},[70,688,319],{"class":127},[70,690,206],{"class":169},[70,692,193],{"class":127},[70,694,455],{"class":169},[70,696,319],{"class":127},[70,698,455],{"class":169},[70,700,193],{"class":127},[70,702,170],{"class":169},[70,704,466],{"class":127},[70,706,707],{"class":256},"# outline: walls\n",[70,709,710,712,714,716,718,720,722,724,726,729],{"class":72,"line":150},[70,711,432],{"class":127},[70,713,170],{"class":169},[70,715,193],{"class":127},[70,717,206],{"class":169},[70,719,319],{"class":127},[70,721,190],{"class":169},[70,723,193],{"class":127},[70,725,455],{"class":169},[70,727,728],{"class":127},"],                     ",[70,730,731],{"class":256},"# diagonals: feature lines\n",[70,733,734,736,738,740],{"class":72,"line":157},[70,735,519],{"class":127},[70,737,352],{"class":351},[70,739,163],{"class":123},[70,741,526],{"class":127},[70,743,744,747,749,752,754,756,758,760,762,764,766,768,770,772,774],{"class":72,"line":176},[70,745,746],{"class":127},"edge_mask ",[70,748,163],{"class":123},[70,750,751],{"class":127}," np.array([",[70,753,649],{"class":169},[70,755,193],{"class":127},[70,757,649],{"class":169},[70,759,193],{"class":127},[70,761,649],{"class":169},[70,763,193],{"class":127},[70,765,649],{"class":169},[70,767,193],{"class":127},[70,769,653],{"class":169},[70,771,193],{"class":127},[70,773,653],{"class":169},[70,775,776],{"class":127},"])\n",[70,778,779],{"class":72,"line":418},[70,780,154],{"emptyLinePlaceholder":153},[70,782,783,785,787,790,792,794],{"class":72,"line":429},[70,784,240],{"class":127},[70,786,163],{"class":123},[70,788,789],{"class":127}," tf.cdt(outer, edges, ",[70,791,645],{"class":351},[70,793,163],{"class":123},[70,795,796],{"class":127},"edge_mask)\n",[70,798,799,801,803],{"class":72,"line":472},[70,800,250],{"class":169},[70,802,549],{"class":127},[70,804,805],{"class":256},"# (4, 3): four quarter triangles, square stays solid\n",[14,807,808,809,812],{},"If crossings are not allowed in your data, pass ",[32,810,811],{},"split_constraints=False",". The call then returns empty arrays on the first crossing. The empty result is the answer to \"do these constraints cross\", not an error.",[61,814,816],{"className":114,"code":815,"language":116,"meta":66,"style":66},"faces, out_points = tf.cdt(outer, diagonals, split_constraints=False)\nprint(faces.shape[0] > 0)           # False: the diagonals cross\n",[32,817,818,836],{"__ignoreMap":66},[70,819,820,822,824,827,830,832,834],{"class":72,"line":73},[70,821,240],{"class":127},[70,823,163],{"class":123},[70,825,826],{"class":127}," tf.cdt(outer, diagonals, ",[70,828,829],{"class":351},"split_constraints",[70,831,163],{"class":123},[70,833,653],{"class":169},[70,835,173],{"class":127},[70,837,838,840,843,845,848,851,854,857],{"class":72,"line":137},[70,839,250],{"class":169},[70,841,842],{"class":127},"(faces.shape[",[70,844,170],{"class":169},[70,846,847],{"class":127},"] ",[70,849,850],{"class":123},">",[70,852,853],{"class":169}," 0",[70,855,856],{"class":127},")           ",[70,858,859],{"class":256},"# False: the diagonals cross\n",[56,861,863],{"id":862},"region-labels","Region labels",[14,865,866,867,870],{},"By default you get only the interior. Pass ",[32,868,869],{},"region_labels"," to get every triangle with a label per triangle:",[61,872,874],{"className":114,"code":873,"language":116,"meta":66,"style":66},"faces, out_points, labels = tf.cdt(ring_points, ring_edges, region_labels=\"nesting\")\nprint(np.unique(labels))            # [0 1]: 0 outside or in the hole, 1 interior\ninterior = faces[labels == 1]       # the 858 triangles of the default call\n",[32,875,876,895,905],{"__ignoreMap":66},[70,877,878,881,883,886,888,890,893],{"class":72,"line":73},[70,879,880],{"class":127},"faces, out_points, labels ",[70,882,163],{"class":123},[70,884,885],{"class":127}," tf.cdt(ring_points, ring_edges, ",[70,887,869],{"class":351},[70,889,163],{"class":123},[70,891,892],{"class":80},"\"nesting\"",[70,894,173],{"class":127},[70,896,897,899,902],{"class":72,"line":137},[70,898,250],{"class":169},[70,900,901],{"class":127},"(np.unique(labels))            ",[70,903,904],{"class":256},"# [0 1]: 0 outside or in the hole, 1 interior\n",[70,906,907,910,912,915,918,921,924],{"class":72,"line":150},[70,908,909],{"class":127},"interior ",[70,911,163],{"class":123},[70,913,914],{"class":127}," faces[labels ",[70,916,917],{"class":123},"==",[70,919,920],{"class":169}," 1",[70,922,923],{"class":127},"]       ",[70,925,926],{"class":256},"# the 858 triangles of the default call\n",[14,928,929],{},[51,930],{"alt":931,"src":932},"Region labels: every triangle of the outline-with-hole triangulation, coloured by its nesting label, interior 1 and hole 0","\u002Fimg\u002Ftutorials\u002Fsteps\u002Fconstrained-delaunay-triangulation-4-labels.png",[14,934,935,937,938,941,942,944],{},[32,936,892],{}," gives the parity of walls crossed from outside. ",[32,939,940],{},"\"components\""," gives an id per region cut out by the walls, with ",[32,943,170],{}," for the hull exterior, so an island inside a hole gets its own id:",[61,946,948],{"className":114,"code":947,"language":116,"meta":66,"style":66},"faces, out_points, labels = tf.cdt(ring_points, ring_edges, region_labels=\"components\")\nprint(np.unique(labels))            # [1 2]: the ring and the hole\n",[32,949,950,966],{"__ignoreMap":66},[70,951,952,954,956,958,960,962,964],{"class":72,"line":73},[70,953,880],{"class":127},[70,955,163],{"class":123},[70,957,885],{"class":127},[70,959,869],{"class":351},[70,961,163],{"class":123},[70,963,940],{"class":80},[70,965,173],{"class":127},[70,967,968,970,972],{"class":72,"line":137},[70,969,250],{"class":169},[70,971,901],{"class":127},[70,973,974],{"class":256},"# [1 2]: the ring and the hole\n",[14,976,977,978,980,981,27],{},"Labels are int32, one per output triangle. ",[32,979,869],{}," needs ",[32,982,983],{},"edges",[56,985,987],{"id":986},"working-with-results","Working with results",[14,989,990,993,994,997,998,993,1000,1003,1004,1007],{},[32,991,992],{},"faces"," is ",[32,995,996],{},"(K, 3)"," int32 and ",[32,999,279],{},[32,1001,1002],{},"(M, 2)"," in the input dtype. Pass float64 points for float64 output. The pair is a 2D mesh; ",[32,1005,1006],{},"tf.Mesh(faces, out_points)"," wraps it for further queries.",[14,1009,1010],{},"To trace input points through the triangulation, ask for the index map:",[61,1012,1014],{"className":114,"code":1013,"language":116,"meta":66,"style":66},"(faces, out_points), (f, kept_ids) = tf.cdt(ring_points, ring_edges, return_index_map=True)\n\nsentinel = f.size\ndropped = f == sentinel             # input points that fell outside the kept region\ncreated = kept_ids == sentinel      # output points made at constraint crossings\nprint(dropped.sum(), created.sum()) # 75 0\n",[32,1015,1016,1034,1038,1048,1066,1084],{"__ignoreMap":66},[70,1017,1018,1021,1023,1025,1028,1030,1032],{"class":72,"line":73},[70,1019,1020],{"class":127},"(faces, out_points), (f, kept_ids) ",[70,1022,163],{"class":123},[70,1024,885],{"class":127},[70,1026,1027],{"class":351},"return_index_map",[70,1029,163],{"class":123},[70,1031,649],{"class":169},[70,1033,173],{"class":127},[70,1035,1036],{"class":72,"line":137},[70,1037,154],{"emptyLinePlaceholder":153},[70,1039,1040,1043,1045],{"class":72,"line":150},[70,1041,1042],{"class":127},"sentinel ",[70,1044,163],{"class":123},[70,1046,1047],{"class":127}," f.size\n",[70,1049,1050,1053,1055,1058,1060,1063],{"class":72,"line":157},[70,1051,1052],{"class":127},"dropped ",[70,1054,163],{"class":123},[70,1056,1057],{"class":127}," f ",[70,1059,917],{"class":123},[70,1061,1062],{"class":127}," sentinel             ",[70,1064,1065],{"class":256},"# input points that fell outside the kept region\n",[70,1067,1068,1071,1073,1076,1078,1081],{"class":72,"line":176},[70,1069,1070],{"class":127},"created ",[70,1072,163],{"class":123},[70,1074,1075],{"class":127}," kept_ids ",[70,1077,917],{"class":123},[70,1079,1080],{"class":127}," sentinel      ",[70,1082,1083],{"class":256},"# output points made at constraint crossings\n",[70,1085,1086,1088,1091],{"class":72,"line":418},[70,1087,250],{"class":169},[70,1089,1090],{"class":127},"(dropped.sum(), created.sum()) ",[70,1092,1093],{"class":256},"# 75 0\n",[14,1095,1096,1099,1100,646,1103,1106,1107,1110,1111,1113,1114,27],{},[32,1097,1098],{},"f[i]"," is the output index of input point ",[32,1101,1102],{},"i",[32,1104,1105],{},"kept_ids[k]"," is the input index of output point ",[32,1108,1109],{},"k",". With ",[32,1112,869],{}," the return shape is ",[32,1115,1116],{},"((faces, out_points), labels, (f, kept_ids))",[14,1118,1119],{},"To lift a 2D triangulation into 3D, add a third coordinate and write it out:",[61,1121,1123],{"className":114,"code":1122,"language":116,"meta":66,"style":66},"points3 = np.column_stack([out_points, np.zeros(len(out_points), dtype=np.float32)])\ntf.write_obj((faces, points3), \"cdt.obj\")\n",[32,1124,1125,1148],{"__ignoreMap":66},[70,1126,1127,1130,1132,1135,1138,1141,1143,1145],{"class":72,"line":73},[70,1128,1129],{"class":127},"points3 ",[70,1131,163],{"class":123},[70,1133,1134],{"class":127}," np.column_stack([out_points, np.zeros(",[70,1136,1137],{"class":169},"len",[70,1139,1140],{"class":127},"(out_points), ",[70,1142,352],{"class":351},[70,1144,163],{"class":123},[70,1146,1147],{"class":127},"np.float32)])\n",[70,1149,1150,1153,1156],{"class":72,"line":137},[70,1151,1152],{"class":127},"tf.write_obj((faces, points3), ",[70,1154,1155],{"class":80},"\"cdt.obj\"",[70,1157,173],{"class":127},[14,1159,1160,1161,1165,1166,27],{},"The same CDT triangulates the cut surfaces in ",[18,1162,1164],{"href":1163},"\u002Ftutorials\u002Ffast-mesh-booleans-in-python","mesh booleans",". For slicing a 3D mesh into 2D outlines, see ",[18,1167,1169],{"href":1168},"\u002Ftutorials\u002Fmesh-slicing-and-cross-sections-in-python","mesh slicing and cross-sections",[56,1171,1173],{"id":1172},"related-tutorials","Related tutorials",[1175,1176,1177,1184,1190,1197],"ul",{},[1178,1179,1180,1183],"li",{},[18,1181,1182],{"href":1168},"Fast and Robust Mesh Slicing and Cross-Sections in Python"," — plane sections, layers and slabs of a mesh.",[1178,1185,1186,1189],{},[18,1187,1188],{"href":1163},"Fast Mesh Booleans in Python"," — exact union, intersection and difference of two meshes.",[1178,1191,1192,1196],{},[18,1193,1195],{"href":1194},"\u002Ftutorials\u002Fisocontours-on-a-triangle-mesh-in-python","Fast and Robust Isocontours on a Triangle Mesh in Python"," — curves and bands of equal value on a scalar field.",[1178,1198,1199,1202],{},[18,1200,1201],{"href":215},"Fast STL and OBJ Reading in Python"," — load and write meshes, the first step of every pipeline.",[56,1204,1206],{"id":1205},"performance","Performance",[14,1208,1209],{},"Delaunay triangulation of 1M random points, no constraints, for all three libraries. SciPy wraps Qhull, which computes an unconstrained Delaunay triangulation only.",[1211,1212],"headline-numbers",{":items":1213},"[{\"value\":\"19.6ms\",\"label\":\"trueform\",\"detail\":\"Delaunay, 1M points\"},{\"value\":\"22×\",\"label\":\"faster than Triangle\",\"detail\":\"433.5 ms\"},{\"value\":\"153×\",\"label\":\"faster than SciPy\u002FQhull\",\"detail\":\"3,004.8 ms\"}]",[14,1215,1216,1221,1222],{},[70,1217,1220],{"className":1218},[1219],"tag-pill","Apple M4 Max"," ",[70,1223,1225],{"className":1224},[1219],"16 threads",[1227,1228],"data-table",{":headers":1229,":highlight":170,":rows":1230},"[\"Library\",\"1M points\",\"vs trueform\"]","[[\"trueform\",\"19.6 ms\",\"1.0×\"],[\"Triangle\",\"433.5 ms\",\"22.1×\"],[\"SciPy\u002FQhull\",\"3,004.8 ms\",\"153×\"]]",[14,1232,1233],{},"Median of three fresh-process medians. Benchmark run with trueform 0.10.3, Triangle 20250106 and SciPy 1.17.1.",[14,1235,1236],{},"With constraints, on a wiggly closed contour of 100,000 vertices, the constrained triangulation takes 23.7 ms against Triangle's 39.7 ms (1.7×). SciPy has no constrained mode.",[1227,1238],{":headers":1239,":highlight":170,":rows":1240},"[\"Library\",\"Constrained, 100K-vertex contour\",\"vs trueform\"]","[[\"trueform\",\"23.7 ms\",\"1.0×\"],[\"Triangle\",\"39.7 ms\",\"1.7×\"]]",[14,1242,1243],{},[51,1244],{"alt":1245,"src":1246},"Benchmark chart: trueform against other Python libraries, cdt","\u002Fimg\u002Falgorithms\u002Fbenchmark-cdt-python-2026.png",[14,1248,1249,1253,1254],{},[18,1250,1252],{"href":1251},"\u002Ftrueform\u002Fbenchmarks","Full benchmarks"," · ",[18,1255,1258],{"href":1256,"rel":1257},"https:\u002F\u002Ftrueform.polydera.com\u002Fpy\u002Fmodules\u002Ftopology#constrained-delaunay-triangulation",[92],"Documentation",[1260,1261],"cite-as",{"author":1262,"title":5},"Sajovic, {\\v{Z}}iga",[1264,1265,1266],"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 .sAwPA, html code.shiki .sAwPA{--shiki-default:#6A737D}html pre.shiki code .s9osk, html code.shiki .s9osk{--shiki-default:#FFAB70}",{"title":66,"searchDepth":137,"depth":137,"links":1268},[1269,1270,1271,1272,1273,1274,1275,1276,1277],{"id":58,"depth":137,"text":59},{"id":97,"depth":137,"text":98},{"id":226,"depth":137,"text":227},{"id":287,"depth":137,"text":288},{"id":575,"depth":137,"text":576},{"id":862,"depth":137,"text":863},{"id":986,"depth":137,"text":987},{"id":1172,"depth":137,"text":1173},{"id":1205,"depth":137,"text":1206},"2026-10-31","Triangulate 2D points with constraint edges in Python. Keep outlines and holes, resolve crossing constraints, label regions. Delaunay of 1M points in 19.6 ms, measured against Triangle and SciPy. One pip install, NumPy arrays in and out.","md",[1282,1283,1284,1285,1286,1287,1288,1289,1290,1291,1292],"constrained Delaunay triangulation Python","CDT Python","Delaunay triangulation Python","triangulate polygon with holes Python","2D triangulation numpy","Triangle Python alternative","triangle library Python alternative","scipy Delaunay alternative","scipy.spatial.Delaunay constrained","CGAL constrained Delaunay Python","mapbox earcut alternative",{},"\u002Fimg\u002Fog\u002Ftutorials\u002Fconstrained-delaunay-triangulation-in-python.png","\u002Ftutorials\u002Fconstrained-delaunay-triangulation-in-python",{"title":5,"description":1279},"tutorials\u002Fconstrained-delaunay-triangulation-in-python",[116,1299,1300,1301],"delaunay","triangulation","tutorial","sggnLn_s01WqK58Wa5ok6nCv-RoWeQlKb_B2e3q4jhE",1793517811885]