[{"data":1,"prerenderedAt":1761},["ShallowReactive",2],{"tutorial-constrained-delaunay-triangulation-in-javascript":3},{"id":4,"title":5,"author":6,"body":7,"date":1733,"description":1734,"extension":1735,"keywords":1736,"meta":1749,"navigation":153,"ogImage":1750,"path":1751,"published":153,"seo":1752,"stem":1753,"tags":1754,"__hash__":1760},"tutorials\u002Ftutorials\u002Fconstrained-delaunay-triangulation-in-javascript.md","Fast and Exact Constrained Delaunay Triangulation in JavaScript","Žiga Sajovic",{"type":8,"value":9,"toc":1723},"minimark",[10,28,44,48,55,60,85,98,102,115,295,309,313,316,368,374,384,388,391,788,794,797,801,804,952,958,971,1158,1165,1213,1217,1236,1239,1366,1382,1389,1587,1590,1614,1621,1625,1655,1659,1662,1666,1686,1689,1694,1697,1703,1715,1719],[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-python","Python"," and ",[18,24,26],{"href":25},"\u002Ftutorials\u002Fconstrained-delaunay-triangulation-in-cpp","C++",".",[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. It works in JavaScript and TypeScript with one ",[32,41,42],{},"npm install",", WebAssembly-powered, in Node.js and the browser with the same API. Typed arrays in, typed arrays out. This tutorial covers plain Delaunay, outlines with holes, crossing constraints and index maps, then shows measured times against Delaunator and cdt2d.",[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\u002Fts\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","npm install @polydera\u002Ftrueform\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","npm",[70,79,81],{"class":80},"sU2Wk"," install",[70,83,84],{"class":80}," @polydera\u002Ftrueform\n",[14,86,87,88,91,92,27],{},"The package ships TypeScript types. The WebAssembly binary loads on first use. In the browser, multithreading needs ",[32,89,90],{},"SharedArrayBuffer"," headers; see the ",[18,93,97],{"href":94,"rel":95},"https:\u002F\u002Ftrueform.polydera.com\u002Fts\u002Fgetting-started\u002Finstallation",[96],"nofollow","installation guide",[56,99,101],{"id":100},"loading-the-points","Loading the points",[14,103,104,106,107,110,111,114],{},[32,105,38],{}," takes an ",[32,108,109],{},"[N, 2]"," point array and, optionally, an ",[32,112,113],{},"[E, 2]"," int32 array of constraint edges that index into it. No mesh object is needed.",[61,116,120],{"className":117,"code":118,"language":119,"meta":66,"style":66},"language-typescript shiki shiki-themes github-dark","import * as tf from \"@polydera\u002Ftrueform\";\n\nconst n = 1000;\nconst raw = new Float32Array(2 * n);\nfor (let i = 0; i \u003C raw.length; i++) raw[i] = Math.random() * 2 - 1;\n\nconst points = tf.ndarray(raw, [n, 2]);\n","typescript",[32,121,122,148,155,172,199,266,271],{"__ignoreMap":66},[70,123,124,128,132,135,139,142,145],{"class":72,"line":73},[70,125,127],{"class":126},"snl16","import",[70,129,131],{"class":130},"sDLfK"," *",[70,133,134],{"class":126}," as",[70,136,138],{"class":137},"s95oV"," tf ",[70,140,141],{"class":126},"from",[70,143,144],{"class":80}," \"@polydera\u002Ftrueform\"",[70,146,147],{"class":137},";\n",[70,149,151],{"class":72,"line":150},2,[70,152,154],{"emptyLinePlaceholder":153},true,"\n",[70,156,158,161,164,167,170],{"class":72,"line":157},3,[70,159,160],{"class":126},"const",[70,162,163],{"class":130}," n",[70,165,166],{"class":126}," =",[70,168,169],{"class":130}," 1000",[70,171,147],{"class":137},[70,173,175,177,180,182,185,188,191,194,196],{"class":72,"line":174},4,[70,176,160],{"class":126},[70,178,179],{"class":130}," raw",[70,181,166],{"class":126},[70,183,184],{"class":126}," new",[70,186,187],{"class":76}," Float32Array",[70,189,190],{"class":137},"(",[70,192,193],{"class":130},"2",[70,195,131],{"class":126},[70,197,198],{"class":137}," n);\n",[70,200,202,205,208,211,214,217,220,223,226,229,232,235,238,241,243,246,249,252,255,258,261,264],{"class":72,"line":201},5,[70,203,204],{"class":126},"for",[70,206,207],{"class":137}," (",[70,209,210],{"class":126},"let",[70,212,213],{"class":137}," i ",[70,215,216],{"class":126},"=",[70,218,219],{"class":130}," 0",[70,221,222],{"class":137},"; i ",[70,224,225],{"class":126},"\u003C",[70,227,228],{"class":137}," raw.",[70,230,231],{"class":130},"length",[70,233,234],{"class":137},"; i",[70,236,237],{"class":126},"++",[70,239,240],{"class":137},") raw[i] ",[70,242,216],{"class":126},[70,244,245],{"class":137}," Math.",[70,247,248],{"class":76},"random",[70,250,251],{"class":137},"() ",[70,253,254],{"class":126},"*",[70,256,257],{"class":130}," 2",[70,259,260],{"class":126}," -",[70,262,263],{"class":130}," 1",[70,265,147],{"class":137},[70,267,269],{"class":72,"line":268},6,[70,270,154],{"emptyLinePlaceholder":153},[70,272,274,276,279,281,284,287,290,292],{"class":72,"line":273},7,[70,275,160],{"class":126},[70,277,278],{"class":130}," points",[70,280,166],{"class":126},[70,282,283],{"class":137}," tf.",[70,285,286],{"class":76},"ndarray",[70,288,289],{"class":137},"(raw, [n, ",[70,291,193],{"class":130},[70,293,294],{"class":137},"]);\n",[14,296,297,298,308],{},"If your points come from a 3D file, read it with ",[18,299,301,304,305],{"href":300},"\u002Ftutorials\u002Ffast-stl-and-obj-reading-in-javascript",[32,302,303],{},"tf.readStl"," or ",[32,306,307],{},"tf.readObj"," and project to two coordinates first.",[56,310,312],{"id":311},"delaunay-triangulation","Delaunay triangulation",[14,314,315],{},"Without edges you get the Delaunay triangulation of the convex hull:",[61,317,319],{"className":117,"code":318,"language":119,"meta":66,"style":66},"const { faces, points: outPoints } = tf.cdt(points);\n\u002F\u002F faces:     NDArrayInt32   [K, 3]\n\u002F\u002F outPoints: NDArrayFloat32 [1000, 2]\n",[32,320,321,357,363],{"__ignoreMap":66},[70,322,323,325,328,331,334,338,341,344,347,349,351,354],{"class":72,"line":73},[70,324,160],{"class":126},[70,326,327],{"class":137}," { ",[70,329,330],{"class":130},"faces",[70,332,333],{"class":137},", ",[70,335,337],{"class":336},"s9osk","points",[70,339,340],{"class":137},": ",[70,342,343],{"class":130},"outPoints",[70,345,346],{"class":137}," } ",[70,348,216],{"class":126},[70,350,283],{"class":137},[70,352,353],{"class":76},"cdt",[70,355,356],{"class":137},"(points);\n",[70,358,359],{"class":72,"line":150},[70,360,362],{"class":361},"sAwPA","\u002F\u002F faces:     NDArrayInt32   [K, 3]\n",[70,364,365],{"class":72,"line":157},[70,366,367],{"class":361},"\u002F\u002F outPoints: NDArrayFloat32 [1000, 2]\n",[14,369,370],{},[51,371],{"alt":372,"src":373},"Delaunay triangulation: 1,000 random points triangulated over their convex hull","\u002Fimg\u002Ftutorials\u002Fsteps\u002Fconstrained-delaunay-triangulation-1-delaunay.png",[14,375,376,377,379,380,383],{},"The faces index ",[32,378,343],{},", not your input array. The output order can differ from the input order. To map between them, use ",[32,381,382],{},"returnIndexMap: true"," (below).",[56,385,387],{"id":386},"an-outline-with-a-hole","An outline with a hole",[14,389,390],{},"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,392,394],{"className":117,"code":393,"language":119,"meta":66,"style":66},"const nSteiner = 500;\nconst ring = new Float32Array(2 * (8 + nSteiner));\nring.set([\n  -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\n]);\nfor (let i = 16; i \u003C ring.length; i++) ring[i] = Math.random() * 2 - 1;\nconst ringPoints = tf.ndarray(ring, [8 + nSteiner, 2]);\n\nconst ringEdges = 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(ringPoints, { edges: ringEdges });\n",[32,395,396,410,440,451,501,547,551,600,628,633,657,698,741,755,760],{"__ignoreMap":66},[70,397,398,400,403,405,408],{"class":72,"line":73},[70,399,160],{"class":126},[70,401,402],{"class":130}," nSteiner",[70,404,166],{"class":126},[70,406,407],{"class":130}," 500",[70,409,147],{"class":137},[70,411,412,414,417,419,421,423,425,427,429,431,434,437],{"class":72,"line":150},[70,413,160],{"class":126},[70,415,416],{"class":130}," ring",[70,418,166],{"class":126},[70,420,184],{"class":126},[70,422,187],{"class":76},[70,424,190],{"class":137},[70,426,193],{"class":130},[70,428,131],{"class":126},[70,430,207],{"class":137},[70,432,433],{"class":130},"8",[70,435,436],{"class":126}," +",[70,438,439],{"class":137}," nSteiner));\n",[70,441,442,445,448],{"class":72,"line":157},[70,443,444],{"class":137},"ring.",[70,446,447],{"class":76},"set",[70,449,450],{"class":137},"([\n",[70,452,453,456,459,461,464,466,469,471,473,475,477,479,481,483,485,487,489,491,493,495,498],{"class":72,"line":174},[70,454,455],{"class":126},"  -",[70,457,458],{"class":130},"1",[70,460,333],{"class":137},[70,462,463],{"class":126},"-",[70,465,458],{"class":130},[70,467,468],{"class":137},",  ",[70,470,458],{"class":130},[70,472,333],{"class":137},[70,474,463],{"class":126},[70,476,458],{"class":130},[70,478,468],{"class":137},[70,480,458],{"class":130},[70,482,333],{"class":137},[70,484,458],{"class":130},[70,486,468],{"class":137},[70,488,463],{"class":126},[70,490,458],{"class":130},[70,492,333],{"class":137},[70,494,458],{"class":130},[70,496,497],{"class":137},",              ",[70,499,500],{"class":361},"\u002F\u002F outer square\n",[70,502,503,505,508,510,512,514,516,518,520,522,524,526,528,530,532,534,536,538,540,542,544],{"class":72,"line":201},[70,504,455],{"class":126},[70,506,507],{"class":130},"0.4",[70,509,333],{"class":137},[70,511,463],{"class":126},[70,513,507],{"class":130},[70,515,468],{"class":137},[70,517,507],{"class":130},[70,519,333],{"class":137},[70,521,463],{"class":126},[70,523,507],{"class":130},[70,525,468],{"class":137},[70,527,507],{"class":130},[70,529,333],{"class":137},[70,531,507],{"class":130},[70,533,468],{"class":137},[70,535,463],{"class":126},[70,537,507],{"class":130},[70,539,333],{"class":137},[70,541,507],{"class":130},[70,543,333],{"class":137},[70,545,546],{"class":361},"\u002F\u002F inner square\n",[70,548,549],{"class":72,"line":268},[70,550,294],{"class":137},[70,552,553,555,557,559,561,563,566,568,570,573,575,577,579,582,584,586,588,590,592,594,596,598],{"class":72,"line":273},[70,554,204],{"class":126},[70,556,207],{"class":137},[70,558,210],{"class":126},[70,560,213],{"class":137},[70,562,216],{"class":126},[70,564,565],{"class":130}," 16",[70,567,222],{"class":137},[70,569,225],{"class":126},[70,571,572],{"class":137}," ring.",[70,574,231],{"class":130},[70,576,234],{"class":137},[70,578,237],{"class":126},[70,580,581],{"class":137},") ring[i] ",[70,583,216],{"class":126},[70,585,245],{"class":137},[70,587,248],{"class":76},[70,589,251],{"class":137},[70,591,254],{"class":126},[70,593,257],{"class":130},[70,595,260],{"class":126},[70,597,263],{"class":130},[70,599,147],{"class":137},[70,601,603,605,608,610,612,614,617,619,621,624,626],{"class":72,"line":602},8,[70,604,160],{"class":126},[70,606,607],{"class":130}," ringPoints",[70,609,166],{"class":126},[70,611,283],{"class":137},[70,613,286],{"class":76},[70,615,616],{"class":137},"(ring, [",[70,618,433],{"class":130},[70,620,436],{"class":126},[70,622,623],{"class":137}," nSteiner, ",[70,625,193],{"class":130},[70,627,294],{"class":137},[70,629,631],{"class":72,"line":630},9,[70,632,154],{"emptyLinePlaceholder":153},[70,634,636,638,641,643,645,647,649,652,655],{"class":72,"line":635},10,[70,637,160],{"class":126},[70,639,640],{"class":130}," ringEdges",[70,642,166],{"class":126},[70,644,283],{"class":137},[70,646,286],{"class":76},[70,648,190],{"class":137},[70,650,651],{"class":126},"new",[70,653,654],{"class":76}," Int32Array",[70,656,450],{"class":137},[70,658,660,663,665,667,669,671,673,675,677,679,681,684,686,688,690,693,696],{"class":72,"line":659},11,[70,661,662],{"class":130},"  0",[70,664,333],{"class":137},[70,666,458],{"class":130},[70,668,468],{"class":137},[70,670,458],{"class":130},[70,672,333],{"class":137},[70,674,193],{"class":130},[70,676,468],{"class":137},[70,678,193],{"class":130},[70,680,333],{"class":137},[70,682,683],{"class":130},"3",[70,685,468],{"class":137},[70,687,683],{"class":130},[70,689,333],{"class":137},[70,691,692],{"class":130},"0",[70,694,695],{"class":137},",   ",[70,697,500],{"class":361},[70,699,701,704,706,709,711,713,715,718,720,722,724,727,729,731,733,736,738],{"class":72,"line":700},12,[70,702,703],{"class":130},"  4",[70,705,333],{"class":137},[70,707,708],{"class":130},"5",[70,710,468],{"class":137},[70,712,708],{"class":130},[70,714,333],{"class":137},[70,716,717],{"class":130},"6",[70,719,468],{"class":137},[70,721,717],{"class":130},[70,723,333],{"class":137},[70,725,726],{"class":130},"7",[70,728,468],{"class":137},[70,730,726],{"class":130},[70,732,333],{"class":137},[70,734,735],{"class":130},"4",[70,737,695],{"class":137},[70,739,740],{"class":361},"\u002F\u002F inner square: the hole\n",[70,742,744,747,749,751,753],{"class":72,"line":743},13,[70,745,746],{"class":137},"]), [",[70,748,433],{"class":130},[70,750,333],{"class":137},[70,752,193],{"class":130},[70,754,294],{"class":137},[70,756,758],{"class":72,"line":757},14,[70,759,154],{"emptyLinePlaceholder":153},[70,761,763,765,767,769,771,773,775,777,779,781,783,785],{"class":72,"line":762},15,[70,764,160],{"class":126},[70,766,327],{"class":137},[70,768,330],{"class":130},[70,770,333],{"class":137},[70,772,337],{"class":336},[70,774,340],{"class":137},[70,776,343],{"class":130},[70,778,346],{"class":137},[70,780,216],{"class":126},[70,782,283],{"class":137},[70,784,353],{"class":76},[70,786,787],{"class":137},"(ringPoints, { edges: ringEdges });\n",[14,789,790],{},[51,791],{"alt":792,"src":793},"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,795,796],{},"The interior points inside the hole are dropped. The triangles cover 3.36, the square's 4 minus the hole's 0.64.",[56,798,800],{"id":799},"crossing-constraints-and-feature-lines","Crossing constraints and feature lines",[14,802,803],{},"Constraints may cross. The crossing point is added to the output:",[61,805,807],{"className":117,"code":806,"language":119,"meta":66,"style":66},"const outer = tf.ndarray(new Float32Array([-1, -1,  1, -1,  1, 1,  -1, 1]), [4, 2]);\nconst diagonals = tf.ndarray(new Int32Array([0, 2,  1, 3]), [2, 2]);\n\nconst crossed = tf.cdt(outer, { edges: diagonals });\ncrossed.points.shape;   \u002F\u002F [5, 2]: the four corners and the crossing at (0, 0)\n",[32,808,809,879,924,928,944],{"__ignoreMap":66},[70,810,811,813,816,818,820,822,824,826,828,831,833,835,837,839,841,843,845,847,849,851,853,855,857,859,861,863,865,867,869,871,873,875,877],{"class":72,"line":73},[70,812,160],{"class":126},[70,814,815],{"class":130}," outer",[70,817,166],{"class":126},[70,819,283],{"class":137},[70,821,286],{"class":76},[70,823,190],{"class":137},[70,825,651],{"class":126},[70,827,187],{"class":76},[70,829,830],{"class":137},"([",[70,832,463],{"class":126},[70,834,458],{"class":130},[70,836,333],{"class":137},[70,838,463],{"class":126},[70,840,458],{"class":130},[70,842,468],{"class":137},[70,844,458],{"class":130},[70,846,333],{"class":137},[70,848,463],{"class":126},[70,850,458],{"class":130},[70,852,468],{"class":137},[70,854,458],{"class":130},[70,856,333],{"class":137},[70,858,458],{"class":130},[70,860,468],{"class":137},[70,862,463],{"class":126},[70,864,458],{"class":130},[70,866,333],{"class":137},[70,868,458],{"class":130},[70,870,746],{"class":137},[70,872,735],{"class":130},[70,874,333],{"class":137},[70,876,193],{"class":130},[70,878,294],{"class":137},[70,880,881,883,886,888,890,892,894,896,898,900,902,904,906,908,910,912,914,916,918,920,922],{"class":72,"line":150},[70,882,160],{"class":126},[70,884,885],{"class":130}," diagonals",[70,887,166],{"class":126},[70,889,283],{"class":137},[70,891,286],{"class":76},[70,893,190],{"class":137},[70,895,651],{"class":126},[70,897,654],{"class":76},[70,899,830],{"class":137},[70,901,692],{"class":130},[70,903,333],{"class":137},[70,905,193],{"class":130},[70,907,468],{"class":137},[70,909,458],{"class":130},[70,911,333],{"class":137},[70,913,683],{"class":130},[70,915,746],{"class":137},[70,917,193],{"class":130},[70,919,333],{"class":137},[70,921,193],{"class":130},[70,923,294],{"class":137},[70,925,926],{"class":72,"line":157},[70,927,154],{"emptyLinePlaceholder":153},[70,929,930,932,935,937,939,941],{"class":72,"line":174},[70,931,160],{"class":126},[70,933,934],{"class":130}," crossed",[70,936,166],{"class":126},[70,938,283],{"class":137},[70,940,353],{"class":76},[70,942,943],{"class":137},"(outer, { edges: diagonals });\n",[70,945,946,949],{"class":72,"line":201},[70,947,948],{"class":137},"crossed.points.shape;   ",[70,950,951],{"class":361},"\u002F\u002F [5, 2]: the four corners and the crossing at (0, 0)\n",[14,953,954],{},[51,955],{"alt":956,"src":957},"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,959,960,961,964,965,967,968,970],{},"To keep a constraint as an edge without making it a wall, pass an ",[32,962,963],{},"edgeMask",". ",[32,966,458],{}," marks a wall, ",[32,969,692],{}," a feature line that splits nothing:",[61,972,974],{"className":117,"code":973,"language":119,"meta":66,"style":66},"const edges = tf.ndarray(new Int32Array([\n  0, 1,  1, 2,  2, 3,  3, 0,   \u002F\u002F outline: walls\n  0, 2,  1, 3,                 \u002F\u002F diagonals: feature lines\n]), [6, 2]);\nconst edgeMask = tf.ndarray(new Int8Array([1, 1, 1, 1, 0, 0]), [6]).as(\"bool\");\n\nconst quarters = tf.cdt(outer, { edges, edgeMask });\nquarters.faces.shape;   \u002F\u002F [4, 3]: four quarter triangles, the square stays solid\n",[32,975,976,997,1034,1056,1068,1130,1134,1150],{"__ignoreMap":66},[70,977,978,980,983,985,987,989,991,993,995],{"class":72,"line":73},[70,979,160],{"class":126},[70,981,982],{"class":130}," edges",[70,984,166],{"class":126},[70,986,283],{"class":137},[70,988,286],{"class":76},[70,990,190],{"class":137},[70,992,651],{"class":126},[70,994,654],{"class":76},[70,996,450],{"class":137},[70,998,999,1001,1003,1005,1007,1009,1011,1013,1015,1017,1019,1021,1023,1025,1027,1029,1031],{"class":72,"line":150},[70,1000,662],{"class":130},[70,1002,333],{"class":137},[70,1004,458],{"class":130},[70,1006,468],{"class":137},[70,1008,458],{"class":130},[70,1010,333],{"class":137},[70,1012,193],{"class":130},[70,1014,468],{"class":137},[70,1016,193],{"class":130},[70,1018,333],{"class":137},[70,1020,683],{"class":130},[70,1022,468],{"class":137},[70,1024,683],{"class":130},[70,1026,333],{"class":137},[70,1028,692],{"class":130},[70,1030,695],{"class":137},[70,1032,1033],{"class":361},"\u002F\u002F outline: walls\n",[70,1035,1036,1038,1040,1042,1044,1046,1048,1050,1053],{"class":72,"line":157},[70,1037,662],{"class":130},[70,1039,333],{"class":137},[70,1041,193],{"class":130},[70,1043,468],{"class":137},[70,1045,458],{"class":130},[70,1047,333],{"class":137},[70,1049,683],{"class":130},[70,1051,1052],{"class":137},",                 ",[70,1054,1055],{"class":361},"\u002F\u002F diagonals: feature lines\n",[70,1057,1058,1060,1062,1064,1066],{"class":72,"line":174},[70,1059,746],{"class":137},[70,1061,717],{"class":130},[70,1063,333],{"class":137},[70,1065,193],{"class":130},[70,1067,294],{"class":137},[70,1069,1070,1072,1075,1077,1079,1081,1083,1085,1088,1090,1092,1094,1096,1098,1100,1102,1104,1106,1108,1110,1112,1114,1116,1119,1122,1124,1127],{"class":72,"line":201},[70,1071,160],{"class":126},[70,1073,1074],{"class":130}," edgeMask",[70,1076,166],{"class":126},[70,1078,283],{"class":137},[70,1080,286],{"class":76},[70,1082,190],{"class":137},[70,1084,651],{"class":126},[70,1086,1087],{"class":76}," Int8Array",[70,1089,830],{"class":137},[70,1091,458],{"class":130},[70,1093,333],{"class":137},[70,1095,458],{"class":130},[70,1097,333],{"class":137},[70,1099,458],{"class":130},[70,1101,333],{"class":137},[70,1103,458],{"class":130},[70,1105,333],{"class":137},[70,1107,692],{"class":130},[70,1109,333],{"class":137},[70,1111,692],{"class":130},[70,1113,746],{"class":137},[70,1115,717],{"class":130},[70,1117,1118],{"class":137},"]).",[70,1120,1121],{"class":76},"as",[70,1123,190],{"class":137},[70,1125,1126],{"class":80},"\"bool\"",[70,1128,1129],{"class":137},");\n",[70,1131,1132],{"class":72,"line":268},[70,1133,154],{"emptyLinePlaceholder":153},[70,1135,1136,1138,1141,1143,1145,1147],{"class":72,"line":273},[70,1137,160],{"class":126},[70,1139,1140],{"class":130}," quarters",[70,1142,166],{"class":126},[70,1144,283],{"class":137},[70,1146,353],{"class":76},[70,1148,1149],{"class":137},"(outer, { edges, edgeMask });\n",[70,1151,1152,1155],{"class":72,"line":602},[70,1153,1154],{"class":137},"quarters.faces.shape;   ",[70,1156,1157],{"class":361},"\u002F\u002F [4, 3]: four quarter triangles, the square stays solid\n",[14,1159,1160,1161,1164],{},"If crossings are not allowed in your data, pass ",[32,1162,1163],{},"splitConstraints: false",". The call then returns an empty result on the first crossing. The empty result is the answer to \"do these constraints cross\", not an error.",[61,1166,1168],{"className":117,"code":1167,"language":119,"meta":66,"style":66},"const verbatim = tf.cdt(outer, { edges: diagonals, splitConstraints: false });\nverbatim.faces.shape[0] > 0;   \u002F\u002F false: the diagonals cross\n",[32,1169,1170,1192],{"__ignoreMap":66},[70,1171,1172,1174,1177,1179,1181,1183,1186,1189],{"class":72,"line":73},[70,1173,160],{"class":126},[70,1175,1176],{"class":130}," verbatim",[70,1178,166],{"class":126},[70,1180,283],{"class":137},[70,1182,353],{"class":76},[70,1184,1185],{"class":137},"(outer, { edges: diagonals, splitConstraints: ",[70,1187,1188],{"class":130},"false",[70,1190,1191],{"class":137}," });\n",[70,1193,1194,1197,1199,1202,1205,1207,1210],{"class":72,"line":150},[70,1195,1196],{"class":137},"verbatim.faces.shape[",[70,1198,692],{"class":130},[70,1200,1201],{"class":137},"] ",[70,1203,1204],{"class":126},">",[70,1206,219],{"class":130},[70,1208,1209],{"class":137},";   ",[70,1211,1212],{"class":361},"\u002F\u002F false: the diagonals cross\n",[56,1214,1216],{"id":1215},"working-with-results","Working with results",[14,1218,1219,1221,1222,1225,1226,1221,1228,1231,1232,1235],{},[32,1220,330],{}," is ",[32,1223,1224],{},"[K, 3]"," int32 and ",[32,1227,337],{},[32,1229,1230],{},"[M, 2]"," in the input dtype. Pass ",[32,1233,1234],{},"Float64Array"," points for float64 output.",[14,1237,1238],{},"To trace input points through the triangulation, ask for the index map:",[61,1240,1242],{"className":117,"code":1241,"language":119,"meta":66,"style":66},"const { faces, points: outPoints, indexMap } =\n  tf.cdt(ringPoints, { edges: ringEdges, returnIndexMap: true });\n\nconst { f, keptIds } = indexMap;\nconst sentinel = f.length;\nconst dropped = f.eq(sentinel);         \u002F\u002F input points that fell outside the kept region\nconst created = keptIds.eq(sentinel);   \u002F\u002F output points made at constraint crossings\n",[32,1243,1244,1270,1285,1289,1310,1326,1346],{"__ignoreMap":66},[70,1245,1246,1248,1250,1252,1254,1256,1258,1260,1262,1265,1267],{"class":72,"line":73},[70,1247,160],{"class":126},[70,1249,327],{"class":137},[70,1251,330],{"class":130},[70,1253,333],{"class":137},[70,1255,337],{"class":336},[70,1257,340],{"class":137},[70,1259,343],{"class":130},[70,1261,333],{"class":137},[70,1263,1264],{"class":130},"indexMap",[70,1266,346],{"class":137},[70,1268,1269],{"class":126},"=\n",[70,1271,1272,1275,1277,1280,1283],{"class":72,"line":150},[70,1273,1274],{"class":137},"  tf.",[70,1276,353],{"class":76},[70,1278,1279],{"class":137},"(ringPoints, { edges: ringEdges, returnIndexMap: ",[70,1281,1282],{"class":130},"true",[70,1284,1191],{"class":137},[70,1286,1287],{"class":72,"line":157},[70,1288,154],{"emptyLinePlaceholder":153},[70,1290,1291,1293,1295,1298,1300,1303,1305,1307],{"class":72,"line":174},[70,1292,160],{"class":126},[70,1294,327],{"class":137},[70,1296,1297],{"class":130},"f",[70,1299,333],{"class":137},[70,1301,1302],{"class":130},"keptIds",[70,1304,346],{"class":137},[70,1306,216],{"class":126},[70,1308,1309],{"class":137}," indexMap;\n",[70,1311,1312,1314,1317,1319,1322,1324],{"class":72,"line":201},[70,1313,160],{"class":126},[70,1315,1316],{"class":130}," sentinel",[70,1318,166],{"class":126},[70,1320,1321],{"class":137}," f.",[70,1323,231],{"class":130},[70,1325,147],{"class":137},[70,1327,1328,1330,1333,1335,1337,1340,1343],{"class":72,"line":268},[70,1329,160],{"class":126},[70,1331,1332],{"class":130}," dropped",[70,1334,166],{"class":126},[70,1336,1321],{"class":137},[70,1338,1339],{"class":76},"eq",[70,1341,1342],{"class":137},"(sentinel);         ",[70,1344,1345],{"class":361},"\u002F\u002F input points that fell outside the kept region\n",[70,1347,1348,1350,1353,1355,1358,1360,1363],{"class":72,"line":273},[70,1349,160],{"class":126},[70,1351,1352],{"class":130}," created",[70,1354,166],{"class":126},[70,1356,1357],{"class":137}," keptIds.",[70,1359,1339],{"class":76},[70,1361,1362],{"class":137},"(sentinel);   ",[70,1364,1365],{"class":361},"\u002F\u002F output points made at constraint crossings\n",[14,1367,1368,1371,1372,964,1375,1378,1379,27],{},[32,1369,1370],{},"f[i]"," is the output index of input point ",[32,1373,1374],{},"i",[32,1376,1377],{},"keptIds[k]"," is the input index of output point ",[32,1380,1381],{},"k",[14,1383,1384,1385,1388],{},"To lift a 2D triangulation into 3D, add a third coordinate and wrap it as a mesh. The triangles come out clockwise in the plane, so their normals point down ",[32,1386,1387],{},"-z",". Reverse the winding if you want them up:",[61,1390,1392],{"className":117,"code":1391,"language":119,"meta":66,"style":66},"const m = outPoints.shape[0], src = outPoints.data;\nconst p3 = new Float32Array(3 * m);\nfor (let i = 0; i \u003C m; i++) {\n  p3[3 * i] = src[2 * i];\n  p3[3 * i + 1] = src[2 * i + 1];\n}\n\nconst mesh = tf.reverseWinding(tf.mesh(faces, tf.ndarray(p3, [m, 3])));\nconst objBytes = tf.writeObj(mesh).data;   \u002F\u002F write to disk or wrap in a Blob\n",[32,1393,1394,1419,1441,1467,1491,1525,1530,1534,1567],{"__ignoreMap":66},[70,1395,1396,1398,1401,1403,1406,1408,1411,1414,1416],{"class":72,"line":73},[70,1397,160],{"class":126},[70,1399,1400],{"class":130}," m",[70,1402,166],{"class":126},[70,1404,1405],{"class":137}," outPoints.shape[",[70,1407,692],{"class":130},[70,1409,1410],{"class":137},"], ",[70,1412,1413],{"class":130},"src",[70,1415,166],{"class":126},[70,1417,1418],{"class":137}," outPoints.data;\n",[70,1420,1421,1423,1426,1428,1430,1432,1434,1436,1438],{"class":72,"line":150},[70,1422,160],{"class":126},[70,1424,1425],{"class":130}," p3",[70,1427,166],{"class":126},[70,1429,184],{"class":126},[70,1431,187],{"class":76},[70,1433,190],{"class":137},[70,1435,683],{"class":130},[70,1437,131],{"class":126},[70,1439,1440],{"class":137}," m);\n",[70,1442,1443,1445,1447,1449,1451,1453,1455,1457,1459,1462,1464],{"class":72,"line":157},[70,1444,204],{"class":126},[70,1446,207],{"class":137},[70,1448,210],{"class":126},[70,1450,213],{"class":137},[70,1452,216],{"class":126},[70,1454,219],{"class":130},[70,1456,222],{"class":137},[70,1458,225],{"class":126},[70,1460,1461],{"class":137}," m; i",[70,1463,237],{"class":126},[70,1465,1466],{"class":137},") {\n",[70,1468,1469,1472,1474,1476,1479,1481,1484,1486,1488],{"class":72,"line":174},[70,1470,1471],{"class":137},"  p3[",[70,1473,683],{"class":130},[70,1475,131],{"class":126},[70,1477,1478],{"class":137}," i] ",[70,1480,216],{"class":126},[70,1482,1483],{"class":137}," src[",[70,1485,193],{"class":130},[70,1487,131],{"class":126},[70,1489,1490],{"class":137}," i];\n",[70,1492,1493,1495,1497,1499,1501,1504,1506,1508,1510,1512,1514,1516,1518,1520,1522],{"class":72,"line":201},[70,1494,1471],{"class":137},[70,1496,683],{"class":130},[70,1498,131],{"class":126},[70,1500,213],{"class":137},[70,1502,1503],{"class":126},"+",[70,1505,263],{"class":130},[70,1507,1201],{"class":137},[70,1509,216],{"class":126},[70,1511,1483],{"class":137},[70,1513,193],{"class":130},[70,1515,131],{"class":126},[70,1517,213],{"class":137},[70,1519,1503],{"class":126},[70,1521,263],{"class":130},[70,1523,1524],{"class":137},"];\n",[70,1526,1527],{"class":72,"line":268},[70,1528,1529],{"class":137},"}\n",[70,1531,1532],{"class":72,"line":273},[70,1533,154],{"emptyLinePlaceholder":153},[70,1535,1536,1538,1541,1543,1545,1548,1551,1554,1557,1559,1562,1564],{"class":72,"line":602},[70,1537,160],{"class":126},[70,1539,1540],{"class":130}," mesh",[70,1542,166],{"class":126},[70,1544,283],{"class":137},[70,1546,1547],{"class":76},"reverseWinding",[70,1549,1550],{"class":137},"(tf.",[70,1552,1553],{"class":76},"mesh",[70,1555,1556],{"class":137},"(faces, tf.",[70,1558,286],{"class":76},[70,1560,1561],{"class":137},"(p3, [m, ",[70,1563,683],{"class":130},[70,1565,1566],{"class":137},"])));\n",[70,1568,1569,1571,1574,1576,1578,1581,1584],{"class":72,"line":630},[70,1570,160],{"class":126},[70,1572,1573],{"class":130}," objBytes",[70,1575,166],{"class":126},[70,1577,283],{"class":137},[70,1579,1580],{"class":76},"writeObj",[70,1582,1583],{"class":137},"(mesh).data;   ",[70,1585,1586],{"class":361},"\u002F\u002F write to disk or wrap in a Blob\n",[14,1588,1589],{},"For large inputs, run it off the main thread:",[61,1591,1593],{"className":117,"code":1592,"language":119,"meta":66,"style":66},"const result = await tf.async.cdt(ringPoints, { edges: ringEdges });\n",[32,1594,1595],{"__ignoreMap":66},[70,1596,1597,1599,1602,1604,1607,1610,1612],{"class":72,"line":73},[70,1598,160],{"class":126},[70,1600,1601],{"class":130}," result",[70,1603,166],{"class":126},[70,1605,1606],{"class":126}," await",[70,1608,1609],{"class":137}," tf.async.",[70,1611,353],{"class":76},[70,1613,787],{"class":137},[14,1615,1616,1617,27],{},"The same CDT triangulates the cut surfaces in ",[18,1618,1620],{"href":1619},"\u002Ftutorials\u002Ffast-mesh-booleans-in-javascript","mesh booleans",[56,1622,1624],{"id":1623},"related-tutorials","Related tutorials",[1626,1627,1628,1636,1642,1649],"ul",{},[1629,1630,1631,1635],"li",{},[18,1632,1634],{"href":1633},"\u002Ftutorials\u002Fmesh-slicing-and-cross-sections-in-javascript","Fast and Robust Mesh Slicing and Cross-Sections in JavaScript"," — plane sections, layers and slabs of a mesh.",[1629,1637,1638,1641],{},[18,1639,1640],{"href":1619},"Fast Mesh Booleans in JavaScript"," — exact union, intersection and difference of two meshes.",[1629,1643,1644,1648],{},[18,1645,1647],{"href":1646},"\u002Ftutorials\u002Fisocontours-on-a-triangle-mesh-in-javascript","Fast and Robust Isocontours on a Triangle Mesh in JavaScript"," — curves and bands of equal value on a scalar field.",[1629,1650,1651,1654],{},[18,1652,1653],{"href":300},"Fast STL and OBJ Reading in JavaScript"," — load and write meshes, the first step of every pipeline.",[56,1656,1658],{"id":1657},"performance","Performance",[14,1660,1661],{},"Unconstrained Delaunay triangulation of 1M random points. Delaunator computes unconstrained Delaunay only.",[1663,1664],"headline-numbers",{":items":1665},"[{\"value\":\"36.3ms\",\"label\":\"trueform\",\"detail\":\"Unconstrained Delaunay, 1M points\"},{\"value\":\"456.1ms\",\"label\":\"Delaunator\",\"detail\":\"Same points\"},{\"value\":\"12.6×\",\"label\":\"Faster than Delaunator\",\"detail\":\"Median wall time\"}]",[14,1667,1668,1673,1674,1673,1678,1673,1682],{},[70,1669,1672],{"className":1670},[1671],"tag-pill","Apple M4 Max"," ",[70,1675,1677],{"className":1676},[1671],"16 threads",[70,1679,1681],{"className":1680},[1671],"Node.js",[70,1683,1685],{"className":1684},[1671],"WebAssembly",[14,1687,1688],{},"For constrained input, the benchmark triangulates a wiggly closed contour. trueform takes 45.8 ms on a 100,000-vertex contour. cdt2d, the constrained triangulator we compared against, was measured only up to 10,000 vertices.",[1690,1691],"data-table",{":headers":1692,":highlight":692,":rows":1693},"[\"Input\",\"trueform\",\"Delaunator\",\"cdt2d\"]","[[\"Unconstrained, 1M points\",\"36.3 ms\",\"456.1 ms\",\"not measured\"],[\"Constrained contour, 10,000 vertices\",\"4.4 ms\",\"unconstrained only\",\"5,784.5 ms\"],[\"Constrained contour, 100,000 vertices\",\"45.8 ms\",\"unconstrained only\",\"not measured\"]]",[14,1695,1696],{},"Median of three fresh-process medians. Benchmark run with trueform npm 0.10.2, Delaunator 5.1.0 and cdt2d 1.0.0.",[14,1698,1699],{},[51,1700],{"alt":1701,"src":1702},"Benchmark chart: trueform against other TypeScript libraries, cdt","\u002Fimg\u002Falgorithms\u002Fbenchmark-cdt-typescript-2026.png",[14,1704,1705,1709,1710],{},[18,1706,1708],{"href":1707},"\u002Ftrueform\u002Fbenchmarks","Full benchmarks"," · ",[18,1711,1714],{"href":1712,"rel":1713},"https:\u002F\u002Ftrueform.polydera.com\u002Fts\u002Fmodules\u002Ftopology#constrained-delaunay-triangulation",[96],"Documentation",[1716,1717],"cite-as",{"author":1718,"title":5},"Sajovic, {\\v{Z}}iga",[1720,1721,1722],"style",{},"html pre.shiki code .svObZ, html code.shiki .svObZ{--shiki-default:#B392F0}html pre.shiki code .sU2Wk, html code.shiki .sU2Wk{--shiki-default:#9ECBFF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html pre.shiki code .snl16, html code.shiki .snl16{--shiki-default:#F97583}html pre.shiki code .sDLfK, html code.shiki .sDLfK{--shiki-default:#79B8FF}html pre.shiki code .s95oV, html code.shiki .s95oV{--shiki-default:#E1E4E8}html pre.shiki code .s9osk, html code.shiki .s9osk{--shiki-default:#FFAB70}html pre.shiki code .sAwPA, html code.shiki .sAwPA{--shiki-default:#6A737D}",{"title":66,"searchDepth":150,"depth":150,"links":1724},[1725,1726,1727,1728,1729,1730,1731,1732],{"id":58,"depth":150,"text":59},{"id":100,"depth":150,"text":101},{"id":311,"depth":150,"text":312},{"id":386,"depth":150,"text":387},{"id":799,"depth":150,"text":800},{"id":1215,"depth":150,"text":1216},{"id":1623,"depth":150,"text":1624},{"id":1657,"depth":150,"text":1658},"2026-10-31","Triangulate 2D points with constraint edges in JavaScript and TypeScript. Keep outlines and holes, resolve crossing constraints, track input points. Delaunay of 1M points in 36.3 ms, measured against Delaunator and cdt2d. WebAssembly-powered, runs in Node.js and the browser.","md",[1737,1738,1739,1740,1741,1742,1743,1744,1745,1746,1747,1748],"constrained Delaunay triangulation JavaScript","CDT JavaScript","CDT TypeScript","Delaunay triangulation JavaScript","triangulate polygon with holes JavaScript","2D triangulation browser","WebAssembly Delaunay","Delaunator alternative","cdt2d alternative","earcut alternative","poly2tri alternative","npm constrained Delaunay",{},"\u002Fimg\u002Fog\u002Ftutorials\u002Fconstrained-delaunay-triangulation-in-javascript.png","\u002Ftutorials\u002Fconstrained-delaunay-triangulation-in-javascript",{"title":5,"description":1734},"tutorials\u002Fconstrained-delaunay-triangulation-in-javascript",[1755,119,1756,1757,1758,1759],"javascript","delaunay","triangulation","tutorial","webassembly","hiW9arr1FgDbhZgr5U_s4radrTacWLt2n_zGwTNOWmw",1793517811885]