[{"data":1,"prerenderedAt":1697},["ShallowReactive",2],{"algo-constrained-delaunay-triangulation":3},{"id":4,"title":5,"author":6,"body":7,"date":1670,"description":1671,"extension":1672,"hub":395,"keywords":1673,"meta":1684,"navigation":395,"ogImage":1685,"path":1686,"published":395,"seo":1687,"stem":1690,"tags":1691,"updated":1695,"__hash__":1696},"algorithms\u002Falgorithms\u002Fconstrained-delaunay-triangulation.md","Constrained Delaunay triangulation","Žiga Sajovic",{"type":8,"value":9,"toc":1660},"minimark",[10,26,31,36,39,42,45,53,65,69,73,76,83,89,95,101,107,169,175,178,184,188,191,194,197,201,225,324,328,331,335,338,342,345,349,352,356,364,368,371,1571,1578,1582,1599,1622,1642,1648,1652,1656],[11,12,13,14,19,20,25],"p",{},"Most triangulators want clean input: closed rings that never touch and constraint edges that never cross. Real outlines are not clean. Slices of a scanned part overlap. GIS layers share edges and cross at their seams. A drawing has a hatch line running through a hole. ",[15,16,18],"a",{"href":17},"\u002Ftrueform","trueform"," takes that input as it comes, splits every crossing with exact arithmetic, and returns a constrained Delaunay triangulation (CDT) of what you drew. Move holes through a shape in the ",[15,21,24],{"href":22,"external":23},"\u002Ftrueform\u002Flive\u002Fcdt","","live example",".",[27,28],"image-grid",{":images":29,":eager":30},"[{\"src\":\"\u002Fimg\u002Falgorithms\u002Fhubs\u002Fcdt-input.webp\",\"width\":1200,\"height\":800,\"alt\":\"Synthetic input: a wavy pink outline, two pink hole rims that overlap each other, a third round hole, three white feature lines that cross each other, the rims and the outline, and loose points scattered across the whole box\",\"label\":\"Input\",\"detail\":\"rims and lines that cross\"},{\"src\":\"\u002Fimg\u002Falgorithms\u002Fhubs\u002Fcdt-output.webp\",\"width\":1200,\"height\":800,\"alt\":\"The constrained Delaunay triangulation of that input: the shape filled with triangles in teal, the three holes empty, the lens where the two holes overlap filled in sand, the feature lines kept as edges, and an orange dot at every point created where two constraints cross\",\"label\":\"Output\",\"detail\":\"one call, crossings split\"}]","true",[32,33,35],"h2",{"id":34},"what-a-cdt-is","What a CDT is",[11,37,38],{},"A Delaunay triangulation joins 2D points into triangles so that no point lies inside any triangle's circumcircle. That rule avoids thin slivers as far as the points allow. It covers the convex hull and picks its own edges.",[11,40,41],{},"So a Delaunay triangulation ignores your lines. Draw a breakline through a point set, and the edges it picks run straight across it.",[27,43],{":images":44},"[{\"src\":\"\u002Fimg\u002Falgorithms\u002Fhubs\u002Fcdt-delaunay.webp\",\"width\":1200,\"height\":800,\"alt\":\"Synthetic scattered points triangulated by plain Delaunay, with a three-segment constraint line dashed in pink; the Delaunay edges that cross the line are drawn in orange\",\"label\":\"Delaunay\",\"detail\":\"orange edges cross the line\"},{\"src\":\"\u002Fimg\u002Falgorithms\u002Fhubs\u002Fcdt-constrained.webp\",\"width\":1200,\"height\":800,\"alt\":\"The same points as a constrained Delaunay triangulation: the pink line is now three edges of the mesh; one triangle on the line is shaded, and its dashed circumcircle holds three points, all on the far side of the line\",\"label\":\"Constrained Delaunay\",\"detail\":\"the line is kept as edges\"}]",[11,46,47,48,52],{},"A constrained Delaunay triangulation keeps the edges you choose: an outline, the rim of a hole, a breakline, a fold. Every other edge is as Delaunay as those edges allow. The rule becomes: no point ",[49,50,51],"em",{},"visible"," from inside a triangle lies in its circumcircle, and a constraint blocks the view. On the right, the shaded triangle's circle holds three points. All three sit behind the pink line, so the triangle stays. With outlines and holes, a CDT also decides which triangles are inside.",[11,54,55,56,60,61,25],{},"Readers come to it from many sides: polygon triangulation with holes, PSLG meshing, a base mesh for finite elements, a TIN with breaklines, caps for cut surfaces, letters and logos. Inside trueform, the same CDT cuts the surfaces of ",[15,57,59],{"href":58},"\u002Falgorithms\u002Fmesh-booleans","mesh booleans"," and re-triangulates the faces cut by ",[15,62,64],{"href":63},"\u002Falgorithms\u002Fisocontours","isobands",[66,67],"hub-language-cards",{":items":68},"[{\"lang\":\"C++\",\"icon\":\"i-simple-icons-cplusplus\",\"title\":\"Fast and Exact Constrained Delaunay Triangulation in C++\",\"to\":\"\u002Ftutorials\u002Fconstrained-delaunay-triangulation-in-cpp\",\"detail\":\"Header-only. Points and edges as views over your own arrays.\"},{\"lang\":\"Python\",\"icon\":\"i-simple-icons-python\",\"title\":\"Fast and Exact Constrained Delaunay Triangulation in Python\",\"to\":\"\u002Ftutorials\u002Fconstrained-delaunay-triangulation-in-python\",\"detail\":\"One pip install. NumPy points and edges in, triangles out.\"},{\"lang\":\"JavaScript\",\"icon\":\"i-simple-icons-typescript\",\"title\":\"Fast and Exact Constrained Delaunay Triangulation in JavaScript\",\"to\":\"\u002Ftutorials\u002Fconstrained-delaunay-triangulation-in-javascript\",\"detail\":\"WebAssembly. Typed arrays in, triangles out, in Node.js and the browser.\"}]",[32,70,72],{"id":71},"what-happens-to-messy-input","What happens to messy input",[11,74,75],{},"The input above is synthetic and built to be awkward. It has 1,186 points and 483 constraint edges. Here is what one call does with each part of it.",[11,77,78,82],{},[79,80,81],"strong",{},"Crossing constraints are split."," Two hole rims cross each other. Three feature lines cross each other, both rims, the third hole and the outline. Before the triangulation, every crossing between constraints is found: edge through edge, a vertex on an edge, two vertices at one spot. A point is added at each one, orange in the figure, and the constraints are split there. You do not pre-process anything.",[11,84,85,88],{},[79,86,87],{},"Walls decide what is inside."," By default every constraint is a wall. Walking in from outside, each wall you cross flips inside to outside. Triangles behind an odd number of walls are kept. A ring inside a ring is a hole. Where the two holes overlap, the walk crosses three walls, so the lens is solid again, in sand above. That is the even-odd rule. Each copy of a wall counts, so a rim given twice cancels; give a wall once to keep it. If you mean the union of the two holes, merge them before you triangulate.",[11,90,91,94],{},[79,92,93],{},"Feature lines split nothing."," The white lines are constraints with their wall flag off. They appear as edges of the output but leave inside and outside alone. Where they run outside the outline, they are dropped with the outside.",[11,96,97,100],{},[79,98,99],{},"Loose points outside are dropped."," Points in the holes and beyond the outline fall away with their triangles. An index map gives the output index of every input point, says which input points fell away, and marks the output points that were made at crossings.",[11,102,103,106],{},[79,104,105],{},"You can refuse crossings instead."," When your data must not cross, say so. The call then keeps every constraint whole and returns empty arrays on the first crossing. The empty result is the answer to \"do these constraints cross\", not an error. It makes the CDT a validity check for outlines.",[108,109,113],"pre",{"className":110,"code":111,"language":112,"meta":23,"style":23},"language-python shiki shiki-themes github-dark","faces, points = tf.cdt(points2d, edges, split_constraints=False)\nvalid = len(faces) > 0      # empty: two constraints cross\n","python",[114,115,116,145],"code",{"__ignoreMap":23},[117,118,121,125,129,132,136,138,142],"span",{"class":119,"line":120},"line",1,[117,122,124],{"class":123},"s95oV","faces, points ",[117,126,128],{"class":127},"snl16","=",[117,130,131],{"class":123}," tf.cdt(points2d, edges, ",[117,133,135],{"class":134},"s9osk","split_constraints",[117,137,128],{"class":127},[117,139,141],{"class":140},"sDLfK","False",[117,143,144],{"class":123},")\n",[117,146,148,151,153,156,159,162,165],{"class":119,"line":147},2,[117,149,150],{"class":123},"valid ",[117,152,128],{"class":127},[117,154,155],{"class":140}," len",[117,157,158],{"class":123},"(faces) ",[117,160,161],{"class":127},">",[117,163,164],{"class":140}," 0",[117,166,168],{"class":167},"sAwPA","      # empty: two constraints cross\n",[11,170,171,174],{},[79,172,173],{},"Regions get labels."," Instead of the interior alone, ask for every triangle with a label: its nesting parity, or the id of the region the walls cut it into. Each hole, the lens and the solid then has its own id.",[11,176,177],{},"In short:",[179,180],"data-table",{":headers":181,":highlight":182,":rows":183},"[\"Input\",\"Default\",\"With crossings refused\"]","0","[[\"Two constraints cross\",\"a point is added and both are split\",\"empty result\"],[\"A constraint end lies on another constraint\",\"the other is split there\",\"empty result\"],[\"Two points weld to one grid point\",\"one output point\",\"one output point\"],[\"A wall given twice\",\"the two cancel\",\"the two cancel\"],[\"A point outside the outline or in a hole\",\"dropped, with its triangles\",\"dropped, with its triangles\"]]",[32,185,187],{"id":186},"exact-and-what-exact-means-here","Exact, and what exact means here",[11,189,190],{},"A triangulator rests on a few yes-or-no tests. Is this point left of that line? Is it inside that circle? Do these two segments cross? Near-collinear and near-cocircular points make floating-point answers inconsistent, and inconsistent answers give crossed edges, holes in the mesh or a loop that never ends.",[11,192,193],{},"trueform welds the input onto an integer grid over its bounding box: 32-bit for float32 input, 64-bit for float64. On that grid, the orientation, in-circle and crossing tests are exact integer arithmetic. Whether a crossing lies inside a segment is decided on the exact ratio, not on a rounded value. A crossing point is then placed on the same grid and written out in your float type.",[11,195,196],{},"Two consequences follow. Input points that weld to the same grid point become one output point. And a crossing point in the output is rounded, because the output is a float array; no float array can hold an exact rational point. The decisions that build the mesh are exact, so the mesh is valid.",[32,198,200],{"id":199},"in-a-pipeline","In a pipeline",[202,203,204,215],"ul",{},[205,206,207,210,211,214],"li",{},[79,208,209],{},"A quality floor."," The CDT adds points only at crossings, so thin triangles along a long constraint stay thin. In C++, refine the result with ",[114,212,213],{},"tf::cdt_refiner",", Ruppert's method, which splits encroached constraints at their midpoints. It takes constraints that do not cross.",[205,216,217,220,221,224],{},[79,218,219],{},"A planar cut in 3D."," Points are ",[114,222,223],{},"(x, y)",". Project the outline onto its plane, triangulate, and lift the result back.",[108,226,228],{"className":110,"code":227,"language":112,"meta":23,"style":23},"# outline: (N, 3) points on a plane through o, with unit in-plane axes u and v\nuv = np.c_[(outline - o) @ u, (outline - o) @ v].astype(np.float32)\nfaces, points2d = tf.cdt(uv, edges)\npoints = o + points2d[:, :1] * u + points2d[:, 1:] * v     # back in 3D\n",[114,229,230,235,266,277],{"__ignoreMap":23},[117,231,232],{"class":119,"line":120},[117,233,234],{"class":167},"# outline: (N, 3) points on a plane through o, with unit in-plane axes u and v\n",[117,236,237,240,242,245,248,251,254,257,259,261,263],{"class":119,"line":147},[117,238,239],{"class":123},"uv ",[117,241,128],{"class":127},[117,243,244],{"class":123}," np.c_[(outline ",[117,246,247],{"class":127},"-",[117,249,250],{"class":123}," o) ",[117,252,253],{"class":127},"@",[117,255,256],{"class":123}," u, (outline ",[117,258,247],{"class":127},[117,260,250],{"class":123},[117,262,253],{"class":127},[117,264,265],{"class":123}," v].astype(np.float32)\n",[117,267,269,272,274],{"class":119,"line":268},3,[117,270,271],{"class":123},"faces, points2d ",[117,273,128],{"class":127},[117,275,276],{"class":123}," tf.cdt(uv, edges)\n",[117,278,280,283,285,288,291,294,297,300,303,306,308,311,313,316,318,321],{"class":119,"line":279},4,[117,281,282],{"class":123},"points ",[117,284,128],{"class":127},[117,286,287],{"class":123}," o ",[117,289,290],{"class":127},"+",[117,292,293],{"class":123}," points2d[:, :",[117,295,296],{"class":140},"1",[117,298,299],{"class":123},"] ",[117,301,302],{"class":127},"*",[117,304,305],{"class":123}," u ",[117,307,290],{"class":127},[117,309,310],{"class":123}," points2d[:, ",[117,312,296],{"class":140},[117,314,315],{"class":123},":] ",[117,317,302],{"class":127},[117,319,320],{"class":123}," v     ",[117,322,323],{"class":167},"# back in 3D\n",[32,325,327],{"id":326},"how-it-compares","How it compares",[11,329,330],{},"Libraries split into three groups on crossing constraints: those that resolve them, those that refuse them, and those that take points only. One more group is the dangerous one: libraries that accept crossing input and quietly lose a constraint. This table is from trueform's own benchmark harness, which declares each library's capabilities before it runs.",[179,332],{":headers":333,":highlight":182,":rows":334},"[\"Library\",\"Language\",\"Constraints\",\"Crossing constraints\"]","[[\"trueform\",\"C++, Python, JavaScript\",\"yes\",\"resolved; or refused on request\"],[\"Triangle\",\"C, Python\",\"yes\",\"resolved\"],[\"CGAL\",\"C++\",\"yes\",\"resolved with an exact-predicates tag; the default tag throws\"],[\"Geogram\",\"C++\",\"yes\",\"resolved\"],[\"artem-ogre\u002FCDT\",\"C++\",\"yes\",\"resolved\"],[\"detria\",\"C++\",\"yes\",\"refused, with a clear error\"],[\"poly2tri\",\"C++\",\"yes\",\"refused\"],[\"cdt2d\",\"JavaScript\",\"yes\",\"in our test, a crossing constraint was dropped with no error\"],[\"Delaunay32\",\"C++\",\"no\",\"points only\"],[\"SciPy (Qhull)\",\"Python\",\"no\",\"points only\"],[\"Delaunator\",\"JavaScript\",\"no\",\"points only\"]]",[11,336,337],{},"Refusing is correct behaviour; a library that refuses tells you so. The cdt2d case is the one to watch: the unit square with both diagonals came back as two triangles, one diagonal gone.",[32,339,341],{"id":340},"benchmarks","Benchmarks",[11,343,344],{},"Points in, triangles out. Apple M4 Max, 16 threads, trueform 0.10.10.",[179,346],{":headers":347,":highlight":182,":rows":348},"[\"C++, 1M points\",\"Delaunay\",\"With closed-contour constraints (≈0.1N)\"]","[[\"trueform 0.10.10\",\"18.0 ms\",\"50.1 ms\"],[\"Delaunay32\",\"27.7 ms (1.54×)\",\"no constraints\"],[\"Triangle 1.6\",\"455 ms (25.3×)\",\"470 ms (9.4×)\"],[\"CGAL 6.2, EPICK\",\"472 ms (26.3×)\",\"592 ms (11.8×)\"],[\"detria\",\"768 ms (42.7×)\",\"810 ms (16.2×)\"],[\"Geogram, floating-point CDT\",\"775 ms (43.1×)\",\"773 ms (15.4×)\"],[\"artem-ogre\u002FCDT 1.4.5\",\"833 ms (46.3×)\",\"828 ms (16.5×)\"]]",[11,350,351],{},"In Python, trueform 0.10.10 triangulates 1M points in 18.1 ms, against 427 ms for Triangle (24×) and 3,052 ms for SciPy's Qhull (169×). On a 100,000-vertex closed boundary it takes 23.9 ms, against Triangle's 35.8 ms (1.5×); at 200,000, 47.0 ms against 79.0 ms (1.7×); at 10,000 the margin is 1.3×. In Node.js, trueform npm 0.10.10 triangulates 1M points in 30.2 ms, against 456 ms for Delaunator (15×); at 10,000 points the margin is 1.1×. cdt2d tops out at 10,000 points.",[353,354],"hub-benchmark-figures",{"category":355},"delaunay-cdt",[11,357,358,359,363],{},"Every trueform benchmark, by module and language, is on the ",[15,360,362],{"href":361},"\u002Ftrueform\u002Fbenchmarks","trueform benchmarks"," page.",[32,365,367],{"id":366},"constrained-delaunay-triangulation-in-code","Constrained Delaunay triangulation in code",[11,369,370],{},"Each example triangulates a square outline with a square hole and 500 loose points. The interior points inside the hole are dropped, and the area is the square minus the hole. The tutorials cover plain Delaunay, crossings, feature lines, region labels and index maps.",[372,373,374,825,1147],"code-group",{},[108,375,380],{"className":376,"code":377,"filename":378,"language":379,"meta":23,"style":23},"language-cpp shiki shiki-themes github-dark","#include \u003Ctrueform\u002Ftrueform.hpp>\n\nstd::vector\u003Cfloat> xy = {\n    -1, -1,  1, -1,  1,  1,  -1,  1,                        \u002F\u002F outer square\n    -0.4f, -0.4f, 0.4f, -0.4f, 0.4f, 0.4f, -0.4f, 0.4f};    \u002F\u002F inner square\nfor (int i = 0; i \u003C 500; ++i) {                             \u002F\u002F loose points\n  xy.push_back(tf::random(-1.f, 1.f));\n  xy.push_back(tf::random(-1.f, 1.f));\n}\nstd::vector\u003Cint> ring = {0, 1, 1, 2, 2, 3, 3, 0,   \u002F\u002F outer square\n                         4, 5, 5, 6, 6, 7, 7, 4};  \u002F\u002F inner square: the hole\n\nauto points = tf::make_points\u003C2>(xy);\nauto edges = tf::make_edges(tf::make_range(ring));\nauto polys = tf::make_cdt(points, edges);\n","C++","cpp",[114,381,382,391,397,417,466,531,571,610,641,647,702,745,750,776,805],{"__ignoreMap":23},[117,383,384,387],{"class":119,"line":120},[117,385,386],{"class":127},"#include",[117,388,390],{"class":389},"sU2Wk"," \u003Ctrueform\u002Ftrueform.hpp>\n",[117,392,393],{"class":119,"line":147},[117,394,396],{"emptyLinePlaceholder":395},true,"\n",[117,398,399,403,406,409,412,414],{"class":119,"line":268},[117,400,402],{"class":401},"svObZ","std",[117,404,405],{"class":123},"::vector",[117,407,408],{"class":127},"\u003Cfloat>",[117,410,411],{"class":123}," xy ",[117,413,128],{"class":127},[117,415,416],{"class":123}," {\n",[117,418,419,422,424,427,429,431,434,436,438,440,442,444,446,448,450,452,454,456,458,460,463],{"class":119,"line":279},[117,420,421],{"class":127},"    -",[117,423,296],{"class":140},[117,425,426],{"class":123},", ",[117,428,247],{"class":127},[117,430,296],{"class":140},[117,432,433],{"class":123},",  ",[117,435,296],{"class":140},[117,437,426],{"class":123},[117,439,247],{"class":127},[117,441,296],{"class":140},[117,443,433],{"class":123},[117,445,296],{"class":140},[117,447,433],{"class":123},[117,449,296],{"class":140},[117,451,433],{"class":123},[117,453,247],{"class":127},[117,455,296],{"class":140},[117,457,433],{"class":123},[117,459,296],{"class":140},[117,461,462],{"class":123},",",[117,464,465],{"class":167},"                        \u002F\u002F outer square\n",[117,467,469,471,474,477,479,481,483,485,487,489,491,493,495,497,499,501,503,505,507,509,511,513,515,517,519,521,523,525,528],{"class":119,"line":468},5,[117,470,421],{"class":127},[117,472,473],{"class":140},"0.4",[117,475,476],{"class":127},"f",[117,478,426],{"class":123},[117,480,247],{"class":127},[117,482,473],{"class":140},[117,484,476],{"class":127},[117,486,426],{"class":123},[117,488,473],{"class":140},[117,490,476],{"class":127},[117,492,426],{"class":123},[117,494,247],{"class":127},[117,496,473],{"class":140},[117,498,476],{"class":127},[117,500,426],{"class":123},[117,502,473],{"class":140},[117,504,476],{"class":127},[117,506,426],{"class":123},[117,508,473],{"class":140},[117,510,476],{"class":127},[117,512,426],{"class":123},[117,514,247],{"class":127},[117,516,473],{"class":140},[117,518,476],{"class":127},[117,520,426],{"class":123},[117,522,473],{"class":140},[117,524,476],{"class":127},[117,526,527],{"class":123},"};",[117,529,530],{"class":167},"    \u002F\u002F inner square\n",[117,532,534,537,540,543,546,548,550,553,556,559,562,565,568],{"class":119,"line":533},6,[117,535,536],{"class":127},"for",[117,538,539],{"class":123}," (",[117,541,542],{"class":127},"int",[117,544,545],{"class":123}," i ",[117,547,128],{"class":127},[117,549,164],{"class":140},[117,551,552],{"class":123},"; i ",[117,554,555],{"class":127},"\u003C",[117,557,558],{"class":140}," 500",[117,560,561],{"class":123},"; ",[117,563,564],{"class":127},"++",[117,566,567],{"class":123},"i) {",[117,569,570],{"class":167},"                             \u002F\u002F loose points\n",[117,572,574,577,580,583,586,589,592,594,596,599,601,603,605,607],{"class":119,"line":573},7,[117,575,576],{"class":123},"  xy.",[117,578,579],{"class":401},"push_back",[117,581,582],{"class":123},"(",[117,584,585],{"class":401},"tf",[117,587,588],{"class":123},"::",[117,590,591],{"class":401},"random",[117,593,582],{"class":123},[117,595,247],{"class":127},[117,597,598],{"class":140},"1.",[117,600,476],{"class":127},[117,602,426],{"class":123},[117,604,598],{"class":140},[117,606,476],{"class":127},[117,608,609],{"class":123},"));\n",[117,611,613,615,617,619,621,623,625,627,629,631,633,635,637,639],{"class":119,"line":612},8,[117,614,576],{"class":123},[117,616,579],{"class":401},[117,618,582],{"class":123},[117,620,585],{"class":401},[117,622,588],{"class":123},[117,624,591],{"class":401},[117,626,582],{"class":123},[117,628,247],{"class":127},[117,630,598],{"class":140},[117,632,476],{"class":127},[117,634,426],{"class":123},[117,636,598],{"class":140},[117,638,476],{"class":127},[117,640,609],{"class":123},[117,642,644],{"class":119,"line":643},9,[117,645,646],{"class":123},"}\n",[117,648,650,652,654,657,660,662,665,667,669,671,673,675,677,680,682,684,686,689,691,693,695,697,699],{"class":119,"line":649},10,[117,651,402],{"class":401},[117,653,405],{"class":123},[117,655,656],{"class":127},"\u003Cint>",[117,658,659],{"class":123}," ring ",[117,661,128],{"class":127},[117,663,664],{"class":123}," {",[117,666,182],{"class":140},[117,668,426],{"class":123},[117,670,296],{"class":140},[117,672,426],{"class":123},[117,674,296],{"class":140},[117,676,426],{"class":123},[117,678,679],{"class":140},"2",[117,681,426],{"class":123},[117,683,679],{"class":140},[117,685,426],{"class":123},[117,687,688],{"class":140},"3",[117,690,426],{"class":123},[117,692,688],{"class":140},[117,694,426],{"class":123},[117,696,182],{"class":140},[117,698,462],{"class":123},[117,700,701],{"class":167},"   \u002F\u002F outer square\n",[117,703,705,708,710,713,715,717,719,722,724,726,728,731,733,735,737,740,742],{"class":119,"line":704},11,[117,706,707],{"class":140},"                         4",[117,709,426],{"class":123},[117,711,712],{"class":140},"5",[117,714,426],{"class":123},[117,716,712],{"class":140},[117,718,426],{"class":123},[117,720,721],{"class":140},"6",[117,723,426],{"class":123},[117,725,721],{"class":140},[117,727,426],{"class":123},[117,729,730],{"class":140},"7",[117,732,426],{"class":123},[117,734,730],{"class":140},[117,736,426],{"class":123},[117,738,739],{"class":140},"4",[117,741,527],{"class":123},[117,743,744],{"class":167},"  \u002F\u002F inner square: the hole\n",[117,746,748],{"class":119,"line":747},12,[117,749,396],{"emptyLinePlaceholder":395},[117,751,753,756,759,761,764,766,769,771,773],{"class":119,"line":752},13,[117,754,755],{"class":127},"auto",[117,757,758],{"class":123}," points ",[117,760,128],{"class":127},[117,762,763],{"class":401}," tf",[117,765,588],{"class":123},[117,767,768],{"class":401},"make_points",[117,770,555],{"class":123},[117,772,679],{"class":140},[117,774,775],{"class":123},">(xy);\n",[117,777,779,781,784,786,788,790,793,795,797,799,802],{"class":119,"line":778},14,[117,780,755],{"class":127},[117,782,783],{"class":123}," edges ",[117,785,128],{"class":127},[117,787,763],{"class":401},[117,789,588],{"class":123},[117,791,792],{"class":401},"make_edges",[117,794,582],{"class":123},[117,796,585],{"class":401},[117,798,588],{"class":123},[117,800,801],{"class":401},"make_range",[117,803,804],{"class":123},"(ring));\n",[117,806,808,810,813,815,817,819,822],{"class":119,"line":807},15,[117,809,755],{"class":127},[117,811,812],{"class":123}," polys ",[117,814,128],{"class":127},[117,816,763],{"class":401},[117,818,588],{"class":123},[117,820,821],{"class":401},"make_cdt",[117,823,824],{"class":123},"(points, edges);\n",[108,826,829],{"className":110,"code":827,"filename":828,"language":112,"meta":23,"style":23},"import numpy as np\nimport trueform as tf\n\nouter = np.array([[-1, -1], [1, -1], [1, 1], [-1, 1]], dtype=np.float32)\ninner = 0.4 * outer\nloose = np.random.default_rng(0).uniform(-1, 1, (500, 2)).astype(np.float32)\npoints = np.vstack([outer, inner, loose])\nedges = np.array([[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(points, edges)\nprint(tf.area((faces, out_points)))   # 3.36 = 4 - 0.64\n\nfaces, _ = tf.cdt(points, edges, split_constraints=False)\nprint(len(faces) > 0)                 # True: no two constraints cross\n","Python",[114,830,831,845,857,861,921,937,973,982,1027,1068,1078,1082,1092,1103,1107,1125],{"__ignoreMap":23},[117,832,833,836,839,842],{"class":119,"line":120},[117,834,835],{"class":127},"import",[117,837,838],{"class":123}," numpy ",[117,840,841],{"class":127},"as",[117,843,844],{"class":123}," np\n",[117,846,847,849,852,854],{"class":119,"line":147},[117,848,835],{"class":127},[117,850,851],{"class":123}," trueform ",[117,853,841],{"class":127},[117,855,856],{"class":123}," tf\n",[117,858,859],{"class":119,"line":268},[117,860,396],{"emptyLinePlaceholder":395},[117,862,863,866,868,871,873,875,877,879,881,884,886,888,890,892,894,896,898,900,902,904,906,908,910,913,916,918],{"class":119,"line":279},[117,864,865],{"class":123},"outer ",[117,867,128],{"class":127},[117,869,870],{"class":123}," np.array([[",[117,872,247],{"class":127},[117,874,296],{"class":140},[117,876,426],{"class":123},[117,878,247],{"class":127},[117,880,296],{"class":140},[117,882,883],{"class":123},"], [",[117,885,296],{"class":140},[117,887,426],{"class":123},[117,889,247],{"class":127},[117,891,296],{"class":140},[117,893,883],{"class":123},[117,895,296],{"class":140},[117,897,426],{"class":123},[117,899,296],{"class":140},[117,901,883],{"class":123},[117,903,247],{"class":127},[117,905,296],{"class":140},[117,907,426],{"class":123},[117,909,296],{"class":140},[117,911,912],{"class":123},"]], ",[117,914,915],{"class":134},"dtype",[117,917,128],{"class":127},[117,919,920],{"class":123},"np.float32)\n",[117,922,923,926,928,931,934],{"class":119,"line":468},[117,924,925],{"class":123},"inner ",[117,927,128],{"class":127},[117,929,930],{"class":140}," 0.4",[117,932,933],{"class":127}," *",[117,935,936],{"class":123}," outer\n",[117,938,939,942,944,947,949,952,954,956,958,960,963,966,968,970],{"class":119,"line":533},[117,940,941],{"class":123},"loose ",[117,943,128],{"class":127},[117,945,946],{"class":123}," np.random.default_rng(",[117,948,182],{"class":140},[117,950,951],{"class":123},").uniform(",[117,953,247],{"class":127},[117,955,296],{"class":140},[117,957,426],{"class":123},[117,959,296],{"class":140},[117,961,962],{"class":123},", (",[117,964,965],{"class":140},"500",[117,967,426],{"class":123},[117,969,679],{"class":140},[117,971,972],{"class":123},")).astype(np.float32)\n",[117,974,975,977,979],{"class":119,"line":573},[117,976,282],{"class":123},[117,978,128],{"class":127},[117,980,981],{"class":123}," np.vstack([outer, inner, loose])\n",[117,983,984,987,989,991,993,995,997,999,1001,1003,1005,1007,1009,1011,1013,1015,1017,1019,1021,1024],{"class":119,"line":612},[117,985,986],{"class":123},"edges ",[117,988,128],{"class":127},[117,990,870],{"class":123},[117,992,182],{"class":140},[117,994,426],{"class":123},[117,996,296],{"class":140},[117,998,883],{"class":123},[117,1000,296],{"class":140},[117,1002,426],{"class":123},[117,1004,679],{"class":140},[117,1006,883],{"class":123},[117,1008,679],{"class":140},[117,1010,426],{"class":123},[117,1012,688],{"class":140},[117,1014,883],{"class":123},[117,1016,688],{"class":140},[117,1018,426],{"class":123},[117,1020,182],{"class":140},[117,1022,1023],{"class":123},"],     ",[117,1025,1026],{"class":167},"# outer square\n",[117,1028,1029,1032,1034,1036,1038,1040,1042,1044,1046,1048,1050,1052,1054,1056,1058,1060,1062,1065],{"class":119,"line":643},[117,1030,1031],{"class":123},"                  [",[117,1033,739],{"class":140},[117,1035,426],{"class":123},[117,1037,712],{"class":140},[117,1039,883],{"class":123},[117,1041,712],{"class":140},[117,1043,426],{"class":123},[117,1045,721],{"class":140},[117,1047,883],{"class":123},[117,1049,721],{"class":140},[117,1051,426],{"class":123},[117,1053,730],{"class":140},[117,1055,883],{"class":123},[117,1057,730],{"class":140},[117,1059,426],{"class":123},[117,1061,739],{"class":140},[117,1063,1064],{"class":123},"]],    ",[117,1066,1067],{"class":167},"# inner square: the hole\n",[117,1069,1070,1073,1075],{"class":119,"line":649},[117,1071,1072],{"class":134},"                 dtype",[117,1074,128],{"class":127},[117,1076,1077],{"class":123},"np.int32)\n",[117,1079,1080],{"class":119,"line":704},[117,1081,396],{"emptyLinePlaceholder":395},[117,1083,1084,1087,1089],{"class":119,"line":747},[117,1085,1086],{"class":123},"faces, out_points ",[117,1088,128],{"class":127},[117,1090,1091],{"class":123}," tf.cdt(points, edges)\n",[117,1093,1094,1097,1100],{"class":119,"line":752},[117,1095,1096],{"class":140},"print",[117,1098,1099],{"class":123},"(tf.area((faces, out_points)))   ",[117,1101,1102],{"class":167},"# 3.36 = 4 - 0.64\n",[117,1104,1105],{"class":119,"line":778},[117,1106,396],{"emptyLinePlaceholder":395},[117,1108,1109,1112,1114,1117,1119,1121,1123],{"class":119,"line":807},[117,1110,1111],{"class":123},"faces, _ ",[117,1113,128],{"class":127},[117,1115,1116],{"class":123}," tf.cdt(points, edges, ",[117,1118,135],{"class":134},[117,1120,128],{"class":127},[117,1122,141],{"class":140},[117,1124,144],{"class":123},[117,1126,1128,1130,1132,1135,1137,1139,1141,1144],{"class":119,"line":1127},16,[117,1129,1096],{"class":140},[117,1131,582],{"class":123},[117,1133,1134],{"class":140},"len",[117,1136,158],{"class":123},[117,1138,161],{"class":127},[117,1140,164],{"class":140},[117,1142,1143],{"class":123},")                 ",[117,1145,1146],{"class":167},"# True: no two constraints cross\n",[108,1148,1153],{"className":1149,"code":1150,"filename":1151,"language":1152,"meta":23,"style":23},"language-ts shiki shiki-themes github-dark","import * as tf from \"@polydera\u002Ftrueform\";\n\nconst n = 500;\nconst xy = new Float32Array(2 * (8 + n));\nxy.set([-1, -1, 1, -1, 1, 1, -1, 1,                      \u002F\u002F outer square\n        -0.4, -0.4, 0.4, -0.4, 0.4, 0.4, -0.4, 0.4]);    \u002F\u002F inner square\nfor (let i = 16; i \u003C xy.length; i++) xy[i] = Math.random() * 2 - 1;\nconst points = tf.ndarray(xy, [8 + n, 2]);\n\nconst edges = tf.ndarray(new Int32Array([\n  0, 1, 1, 2, 2, 3, 3, 0,   \u002F\u002F outer square\n  4, 5, 5, 6, 6, 7, 7, 4,   \u002F\u002F inner square: the hole\n]), [8, 2]);\n\nconst { faces, points: outPoints } = tf.cdt(points, { edges });\n","JavaScript","ts",[114,1154,1155,1176,1180,1195,1227,1282,1329,1386,1416,1420,1444,1482,1520,1533,1537],{"__ignoreMap":23},[117,1156,1157,1159,1161,1164,1167,1170,1173],{"class":119,"line":120},[117,1158,835],{"class":127},[117,1160,933],{"class":140},[117,1162,1163],{"class":127}," as",[117,1165,1166],{"class":123}," tf ",[117,1168,1169],{"class":127},"from",[117,1171,1172],{"class":389}," \"@polydera\u002Ftrueform\"",[117,1174,1175],{"class":123},";\n",[117,1177,1178],{"class":119,"line":147},[117,1179,396],{"emptyLinePlaceholder":395},[117,1181,1182,1185,1188,1191,1193],{"class":119,"line":268},[117,1183,1184],{"class":127},"const",[117,1186,1187],{"class":140}," n",[117,1189,1190],{"class":127}," =",[117,1192,558],{"class":140},[117,1194,1175],{"class":123},[117,1196,1197,1199,1202,1204,1207,1210,1212,1214,1216,1218,1221,1224],{"class":119,"line":279},[117,1198,1184],{"class":127},[117,1200,1201],{"class":140}," xy",[117,1203,1190],{"class":127},[117,1205,1206],{"class":127}," new",[117,1208,1209],{"class":401}," Float32Array",[117,1211,582],{"class":123},[117,1213,679],{"class":140},[117,1215,933],{"class":127},[117,1217,539],{"class":123},[117,1219,1220],{"class":140},"8",[117,1222,1223],{"class":127}," +",[117,1225,1226],{"class":123}," n));\n",[117,1228,1229,1232,1235,1238,1240,1242,1244,1246,1248,1250,1252,1254,1256,1258,1260,1262,1264,1266,1268,1270,1272,1274,1276,1279],{"class":119,"line":468},[117,1230,1231],{"class":123},"xy.",[117,1233,1234],{"class":401},"set",[117,1236,1237],{"class":123},"([",[117,1239,247],{"class":127},[117,1241,296],{"class":140},[117,1243,426],{"class":123},[117,1245,247],{"class":127},[117,1247,296],{"class":140},[117,1249,426],{"class":123},[117,1251,296],{"class":140},[117,1253,426],{"class":123},[117,1255,247],{"class":127},[117,1257,296],{"class":140},[117,1259,426],{"class":123},[117,1261,296],{"class":140},[117,1263,426],{"class":123},[117,1265,296],{"class":140},[117,1267,426],{"class":123},[117,1269,247],{"class":127},[117,1271,296],{"class":140},[117,1273,426],{"class":123},[117,1275,296],{"class":140},[117,1277,1278],{"class":123},",                      ",[117,1280,1281],{"class":167},"\u002F\u002F outer square\n",[117,1283,1284,1287,1289,1291,1293,1295,1297,1299,1301,1303,1305,1307,1309,1311,1313,1315,1317,1319,1321,1323,1326],{"class":119,"line":533},[117,1285,1286],{"class":127},"        -",[117,1288,473],{"class":140},[117,1290,426],{"class":123},[117,1292,247],{"class":127},[117,1294,473],{"class":140},[117,1296,426],{"class":123},[117,1298,473],{"class":140},[117,1300,426],{"class":123},[117,1302,247],{"class":127},[117,1304,473],{"class":140},[117,1306,426],{"class":123},[117,1308,473],{"class":140},[117,1310,426],{"class":123},[117,1312,473],{"class":140},[117,1314,426],{"class":123},[117,1316,247],{"class":127},[117,1318,473],{"class":140},[117,1320,426],{"class":123},[117,1322,473],{"class":140},[117,1324,1325],{"class":123},"]);    ",[117,1327,1328],{"class":167},"\u002F\u002F inner square\n",[117,1330,1331,1333,1335,1338,1340,1342,1345,1347,1349,1352,1355,1358,1360,1363,1365,1368,1370,1373,1375,1378,1381,1384],{"class":119,"line":573},[117,1332,536],{"class":127},[117,1334,539],{"class":123},[117,1336,1337],{"class":127},"let",[117,1339,545],{"class":123},[117,1341,128],{"class":127},[117,1343,1344],{"class":140}," 16",[117,1346,552],{"class":123},[117,1348,555],{"class":127},[117,1350,1351],{"class":123}," xy.",[117,1353,1354],{"class":140},"length",[117,1356,1357],{"class":123},"; i",[117,1359,564],{"class":127},[117,1361,1362],{"class":123},") xy[i] ",[117,1364,128],{"class":127},[117,1366,1367],{"class":123}," Math.",[117,1369,591],{"class":401},[117,1371,1372],{"class":123},"() ",[117,1374,302],{"class":127},[117,1376,1377],{"class":140}," 2",[117,1379,1380],{"class":127}," -",[117,1382,1383],{"class":140}," 1",[117,1385,1175],{"class":123},[117,1387,1388,1390,1393,1395,1398,1401,1404,1406,1408,1411,1413],{"class":119,"line":612},[117,1389,1184],{"class":127},[117,1391,1392],{"class":140}," points",[117,1394,1190],{"class":127},[117,1396,1397],{"class":123}," tf.",[117,1399,1400],{"class":401},"ndarray",[117,1402,1403],{"class":123},"(xy, [",[117,1405,1220],{"class":140},[117,1407,1223],{"class":127},[117,1409,1410],{"class":123}," n, ",[117,1412,679],{"class":140},[117,1414,1415],{"class":123},"]);\n",[117,1417,1418],{"class":119,"line":643},[117,1419,396],{"emptyLinePlaceholder":395},[117,1421,1422,1424,1427,1429,1431,1433,1435,1438,1441],{"class":119,"line":649},[117,1423,1184],{"class":127},[117,1425,1426],{"class":140}," edges",[117,1428,1190],{"class":127},[117,1430,1397],{"class":123},[117,1432,1400],{"class":401},[117,1434,582],{"class":123},[117,1436,1437],{"class":127},"new",[117,1439,1440],{"class":401}," Int32Array",[117,1442,1443],{"class":123},"([\n",[117,1445,1446,1449,1451,1453,1455,1457,1459,1461,1463,1465,1467,1469,1471,1473,1475,1477,1480],{"class":119,"line":704},[117,1447,1448],{"class":140},"  0",[117,1450,426],{"class":123},[117,1452,296],{"class":140},[117,1454,426],{"class":123},[117,1456,296],{"class":140},[117,1458,426],{"class":123},[117,1460,679],{"class":140},[117,1462,426],{"class":123},[117,1464,679],{"class":140},[117,1466,426],{"class":123},[117,1468,688],{"class":140},[117,1470,426],{"class":123},[117,1472,688],{"class":140},[117,1474,426],{"class":123},[117,1476,182],{"class":140},[117,1478,1479],{"class":123},",   ",[117,1481,1281],{"class":167},[117,1483,1484,1487,1489,1491,1493,1495,1497,1499,1501,1503,1505,1507,1509,1511,1513,1515,1517],{"class":119,"line":747},[117,1485,1486],{"class":140},"  4",[117,1488,426],{"class":123},[117,1490,712],{"class":140},[117,1492,426],{"class":123},[117,1494,712],{"class":140},[117,1496,426],{"class":123},[117,1498,721],{"class":140},[117,1500,426],{"class":123},[117,1502,721],{"class":140},[117,1504,426],{"class":123},[117,1506,730],{"class":140},[117,1508,426],{"class":123},[117,1510,730],{"class":140},[117,1512,426],{"class":123},[117,1514,739],{"class":140},[117,1516,1479],{"class":123},[117,1518,1519],{"class":167},"\u002F\u002F inner square: the hole\n",[117,1521,1522,1525,1527,1529,1531],{"class":119,"line":752},[117,1523,1524],{"class":123},"]), [",[117,1526,1220],{"class":140},[117,1528,426],{"class":123},[117,1530,679],{"class":140},[117,1532,1415],{"class":123},[117,1534,1535],{"class":119,"line":778},[117,1536,396],{"emptyLinePlaceholder":395},[117,1538,1539,1541,1544,1547,1549,1552,1555,1558,1561,1563,1565,1568],{"class":119,"line":807},[117,1540,1184],{"class":127},[117,1542,1543],{"class":123}," { ",[117,1545,1546],{"class":140},"faces",[117,1548,426],{"class":123},[117,1550,1551],{"class":134},"points",[117,1553,1554],{"class":123},": ",[117,1556,1557],{"class":140},"outPoints",[117,1559,1560],{"class":123}," } ",[117,1562,128],{"class":127},[117,1564,1397],{"class":123},[117,1566,1567],{"class":401},"cdt",[117,1569,1570],{"class":123},"(points, { edges });\n",[11,1572,1573,1574,1577],{},"The faces index the output points, not your input array, and the output order can differ. Float64 input gives float64 output on the 64-bit grid. In JavaScript, ",[114,1575,1576],{},"tf.async.cdt"," runs the same call off the main thread.",[32,1579,1581],{"id":1580},"further","Further",[11,1583,1584,1587,1588,426,1591,1594,1595,1598],{},[79,1585,1586],{},"Tutorials."," ",[15,1589,378],{"href":1590},"\u002Ftutorials\u002Fconstrained-delaunay-triangulation-in-cpp",[15,1592,828],{"href":1593},"\u002Ftutorials\u002Fconstrained-delaunay-triangulation-in-python"," and ",[15,1596,1151],{"href":1597},"\u002Ftutorials\u002Fconstrained-delaunay-triangulation-in-javascript",": plain Delaunay, outlines with holes, crossings and feature lines, region labels and index maps.",[11,1600,1601,1604,1605,426,1610,1594,1614,1619,1620,25],{},[79,1602,1603],{},"Documentation."," Constrained Delaunay triangulation in the topology module, in ",[15,1606,378],{"href":1607,"rel":1608},"https:\u002F\u002Ftrueform.polydera.com\u002Fcpp\u002Fmodules\u002Ftopology",[1609],"nofollow",[15,1611,828],{"href":1612,"rel":1613},"https:\u002F\u002Ftrueform.polydera.com\u002Fpy\u002Fmodules\u002Ftopology#constrained-delaunay-triangulation",[1609],[15,1615,1618],{"href":1616,"rel":1617},"https:\u002F\u002Ftrueform.polydera.com\u002Fts\u002Fmodules\u002Ftopology",[1609],"TypeScript",". The C++ page also documents ",[114,1621,213],{},[11,1623,1624,1627,1628,1632,1633,426,1635,1594,1639,25],{},[79,1625,1626],{},"Related."," Outlines from 3D meshes: ",[15,1629,1631],{"href":1630},"\u002Ftutorials\u002Fmesh-slicing-and-cross-sections-in-python","mesh slicing and cross-sections",". The same CDT inside other operations: ",[15,1634,59],{"href":58},[15,1636,1638],{"href":1637},"\u002Falgorithms\u002Fcsg-for-engineering","CSG for engineering",[15,1640,1641],{"href":63},"isocontours",[11,1643,1644,1645,1647],{},"Triangulation is one part of ",[15,1646,18],{"href":17},". The same library does booleans, spatial queries, curvature and remeshing, in C++, Python and the browser.",[1649,1650],"article-cta",{":buttons":1651},"[{\"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\"},{\"label\":\"Try it live\",\"to\":\"\u002Ftrueform\u002Flive\u002Fcdt\",\"icon\":\"i-lucide-play\"}]",[1653,1654],"cite-as",{"author":1655,"title":5},"Sajovic, {\\v{Z}}iga",[1657,1658,1659],"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 .s9osk, html code.shiki .s9osk{--shiki-default:#FFAB70}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 .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 .svObZ, html code.shiki .svObZ{--shiki-default:#B392F0}",{"title":23,"searchDepth":147,"depth":147,"links":1661},[1662,1663,1664,1665,1666,1667,1668,1669],{"id":34,"depth":147,"text":35},{"id":71,"depth":147,"text":72},{"id":186,"depth":147,"text":187},{"id":199,"depth":147,"text":200},{"id":326,"depth":147,"text":327},{"id":340,"depth":147,"text":341},{"id":366,"depth":147,"text":367},{"id":1580,"depth":147,"text":1581},"2026-11-08","Triangulate polygons with holes, feature lines and constraints that cross, with exact predicates. What happens to messy input and how trueform compares with Triangle, CGAL, Geogram and CDT.","md",[1674,1675,1676,1677,1678,1679,1680,1681,1682,1683],"constrained delaunay triangulation","CDT","polygon triangulation with holes","triangulate polygon with holes","intersecting constraints","PSLG triangulation","exact predicates","Triangle alternative","CGAL CDT alternative","delaunator constraints",{},"\u002Fimg\u002Fog\u002Falgorithms\u002Fconstrained-delaunay-triangulation.png","\u002Falgorithms\u002Fconstrained-delaunay-triangulation",{"title":1688,"description":1689},"Constrained Delaunay Triangulation with Holes","Exact constrained Delaunay triangulation of polygons with holes and crossing constraints in C++, Python and JavaScript, measured against Triangle and CGAL.","algorithms\u002Fconstrained-delaunay-triangulation",[1692,1693,1694],"delaunay","triangulation","overview","2026-11-12T12:06:28.000Z","KPjNh8CfXriea-I7q_hwWNG6mfEsB5uSU-8N1JLh1tw",1794486339503]