[{"data":1,"prerenderedAt":1618},["ShallowReactive",2],{"tutorial-principal-curvatures-of-a-mesh-in-javascript":3},{"id":4,"title":5,"author":6,"body":7,"date":1593,"description":1594,"extension":1595,"keywords":1596,"meta":1607,"navigation":159,"ogImage":1608,"path":1609,"published":159,"seo":1610,"stem":1611,"tags":1612,"__hash__":1617},"tutorials\u002Ftutorials\u002Fprincipal-curvatures-of-a-mesh-in-javascript.md","Fast and Robust Principal Curvatures of a Mesh in JavaScript","Žiga Sajovic",{"type":8,"value":9,"toc":1581},"minimark",[10,28,48,52,59,64,89,102,106,193,207,210,247,251,294,300,303,329,332,381,384,388,391,434,440,453,534,537,541,548,596,602,664,667,853,856,860,868,901,907,910,913,1217,1221,1224,1271,1275,1286,1294,1325,1328,1396,1399,1477,1485,1489,1519,1523,1527,1547,1555,1561,1573,1577],[11,12,13],"note",{},[14,15,16,17,22,23,27],"p",{},"Also available in ",[18,19,21],"a",{"href":20},"\u002Ftutorials\u002Fprincipal-curvatures-of-a-mesh-in-python","Python"," and ",[18,24,26],{"href":25},"\u002Ftutorials\u002Fprincipal-curvatures-of-a-mesh-in-cpp","C++",".",[14,29,30,31,35,36,39,40,43,44,47],{},"Principal curvatures tell you how a surface bends at each point. ",[32,33,34],"code",{},"k0"," is the largest bend, ",[32,37,38],{},"k1"," the smallest. From them you get Gaussian curvature, mean curvature, and the shape index. ",[32,41,42],{},"trueform"," computes all of it per vertex. One ",[32,45,46],{},"npm install",". It runs as WebAssembly in Node.js and the browser, with TypeScript types. This tutorial covers principal curvatures, principal directions, the shape index, and how to read the signs.",[49,50],"article-cta",{":buttons":51},"[{\"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\u002Fgeometry#curvature-analysis\",\"variant\":\"soft\",\"color\":\"neutral\"},{\"label\":\"Try it live\",\"to\":\"https:\u002F\u002Ftrueform.polydera.com\u002Flive-examples\u002Fshape-histogram\",\"icon\":\"i-lucide-play\"}]",[14,53,54],{},[55,56],"img",{"alt":57,"src":58},"Mean curvature on the Stanford dragon: creases orange, flat areas teal","\u002Fimg\u002Ftutorials\u002Fprincipal-curvatures.png",[60,61,63],"h2",{"id":62},"install","Install",[65,66,71],"pre",{"className":67,"code":68,"language":69,"meta":70,"style":70},"language-bash shiki shiki-themes github-dark","npm install @polydera\u002Ftrueform\n","bash","",[32,72,73],{"__ignoreMap":70},[74,75,78,82,86],"span",{"class":76,"line":77},"line",1,[74,79,81],{"class":80},"svObZ","npm",[74,83,85],{"class":84},"sU2Wk"," install",[74,87,88],{"class":84}," @polydera\u002Ftrueform\n",[14,90,91,92,95,96,27],{},"The WebAssembly binary loads on first use. In the browser, multithreading needs ",[32,93,94],{},"SharedArrayBuffer"," headers. See the ",[18,97,101],{"href":98,"rel":99},"https:\u002F\u002Ftrueform.polydera.com\u002Fts\u002Fgetting-started\u002Finstallation",[100],"nofollow","installation guide",[60,103,105],{"id":104},"loading-the-mesh","Loading the mesh",[65,107,111],{"className":108,"code":109,"language":110,"meta":70,"style":70},"language-typescript shiki shiki-themes github-dark","import * as tf from \"@polydera\u002Ftrueform\";\nimport { readFileSync } from \"node:fs\";\n\nconst mesh = tf.readStl(readFileSync(\"part.stl\"));\n","typescript",[32,112,113,139,154,161],{"__ignoreMap":70},[74,114,115,119,123,126,130,133,136],{"class":76,"line":77},[74,116,118],{"class":117},"snl16","import",[74,120,122],{"class":121},"sDLfK"," *",[74,124,125],{"class":117}," as",[74,127,129],{"class":128},"s95oV"," tf ",[74,131,132],{"class":117},"from",[74,134,135],{"class":84}," \"@polydera\u002Ftrueform\"",[74,137,138],{"class":128},";\n",[74,140,142,144,147,149,152],{"class":76,"line":141},2,[74,143,118],{"class":117},[74,145,146],{"class":128}," { readFileSync } ",[74,148,132],{"class":117},[74,150,151],{"class":84}," \"node:fs\"",[74,153,138],{"class":128},[74,155,157],{"class":76,"line":156},3,[74,158,160],{"emptyLinePlaceholder":159},true,"\n",[74,162,164,167,170,173,176,179,182,185,187,190],{"class":76,"line":163},4,[74,165,166],{"class":117},"const",[74,168,169],{"class":121}," mesh",[74,171,172],{"class":117}," =",[74,174,175],{"class":128}," tf.",[74,177,178],{"class":80},"readStl",[74,180,181],{"class":128},"(",[74,183,184],{"class":80},"readFileSync",[74,186,181],{"class":128},[74,188,189],{"class":84},"\"part.stl\"",[74,191,192],{"class":128},"));\n",[14,194,195,196,199,200,203,204,27],{},"In the browser, pass the bytes from ",[32,197,198],{},"fetch"," instead. ",[32,201,202],{},"tf.readObj"," works the same way. If you already have typed arrays, pass them to ",[32,205,206],{},"tf.mesh(faces, points)",[14,208,209],{},"To follow along without a file, make a sphere of radius 2. Its curvature is 1\u002F2 everywhere, so you can check the numbers:",[65,211,213],{"className":108,"code":212,"language":110,"meta":70,"style":70},"const mesh = tf.sphereMesh(2.0, 100, 200);\n",[32,214,215],{"__ignoreMap":70},[74,216,217,219,221,223,225,228,230,233,236,239,241,244],{"class":76,"line":77},[74,218,166],{"class":117},[74,220,169],{"class":121},[74,222,172],{"class":117},[74,224,175],{"class":128},[74,226,227],{"class":80},"sphereMesh",[74,229,181],{"class":128},[74,231,232],{"class":121},"2.0",[74,234,235],{"class":128},", ",[74,237,238],{"class":121},"100",[74,240,235],{"class":128},[74,242,243],{"class":121},"200",[74,245,246],{"class":128},");\n",[60,248,250],{"id":249},"principal-curvatures","Principal curvatures",[65,252,254],{"className":108,"code":253,"language":110,"meta":70,"style":70},"const { k0, k1 } = tf.principalCurvatures(mesh);\n\u002F\u002F k0, k1: NDArray, shape [numberOfPoints], the mesh's dtype\n\u002F\u002F k0 >= k1 at every point\n",[32,255,256,283,289],{"__ignoreMap":70},[74,257,258,260,263,265,267,269,272,275,277,280],{"class":76,"line":77},[74,259,166],{"class":117},[74,261,262],{"class":128}," { ",[74,264,34],{"class":121},[74,266,235],{"class":128},[74,268,38],{"class":121},[74,270,271],{"class":128}," } ",[74,273,274],{"class":117},"=",[74,276,175],{"class":128},[74,278,279],{"class":80},"principalCurvatures",[74,281,282],{"class":128},"(mesh);\n",[74,284,285],{"class":76,"line":141},[74,286,288],{"class":287},"sAwPA","\u002F\u002F k0, k1: NDArray, shape [numberOfPoints], the mesh's dtype\n",[74,290,291],{"class":76,"line":156},[74,292,293],{"class":287},"\u002F\u002F k0 >= k1 at every point\n",[14,295,296],{},[55,297],{"alt":298,"src":299},"Principal curvatures on a carved pot from Thingi10K: Gaussian curvature k0 times k1, saddles in orange, domes and dents in teal","\u002Fimg\u002Ftutorials\u002Fsteps\u002Fprincipal-curvatures-of-a-mesh-1-gaussian.png",[14,301,302],{},"On the sphere both are close to 0.5. Each vertex fits a quadric to its neighborhood. The neighborhood is the k-ring, two rings by default. A larger ring smooths out noise:",[65,304,306],{"className":108,"code":305,"language":110,"meta":70,"style":70},"const smooth = tf.principalCurvatures(mesh, 3);\n",[32,307,308],{"__ignoreMap":70},[74,309,310,312,315,317,319,321,324,327],{"class":76,"line":77},[74,311,166],{"class":117},[74,313,314],{"class":121}," smooth",[74,316,172],{"class":117},[74,318,175],{"class":128},[74,320,279],{"class":80},[74,322,323],{"class":128},"(mesh, ",[74,325,326],{"class":121},"3",[74,328,246],{"class":128},[14,330,331],{},"Gaussian and mean curvature follow directly:",[65,333,335],{"className":108,"code":334,"language":110,"meta":70,"style":70},"const gaussian = k0.mul(k1);\nconst mean = k0.add(k1).mul(0.5);\n",[32,336,337,355],{"__ignoreMap":70},[74,338,339,341,344,346,349,352],{"class":76,"line":77},[74,340,166],{"class":117},[74,342,343],{"class":121}," gaussian",[74,345,172],{"class":117},[74,347,348],{"class":128}," k0.",[74,350,351],{"class":80},"mul",[74,353,354],{"class":128},"(k1);\n",[74,356,357,359,362,364,366,369,372,374,376,379],{"class":76,"line":141},[74,358,166],{"class":117},[74,360,361],{"class":121}," mean",[74,363,172],{"class":117},[74,365,348],{"class":128},[74,367,368],{"class":80},"add",[74,370,371],{"class":128},"(k1).",[74,373,351],{"class":80},[74,375,181],{"class":128},[74,377,378],{"class":121},"0.5",[74,380,246],{"class":128},[14,382,383],{},"On the sphere, Gaussian curvature is close to 0.25 and mean curvature close to 0.5.",[60,385,387],{"id":386},"principal-directions","Principal directions",[14,389,390],{},"Ask for the directions too:",[65,392,394],{"className":108,"code":393,"language":110,"meta":70,"style":70},"const { k0, k1, d0, d1 } = tf.principalDirections(mesh);\n\u002F\u002F d0, d1: NDArray, shape [numberOfPoints, 3]\n",[32,395,396,429],{"__ignoreMap":70},[74,397,398,400,402,404,406,408,410,413,415,418,420,422,424,427],{"class":76,"line":77},[74,399,166],{"class":117},[74,401,262],{"class":128},[74,403,34],{"class":121},[74,405,235],{"class":128},[74,407,38],{"class":121},[74,409,235],{"class":128},[74,411,412],{"class":121},"d0",[74,414,235],{"class":128},[74,416,417],{"class":121},"d1",[74,419,271],{"class":128},[74,421,274],{"class":117},[74,423,175],{"class":128},[74,425,426],{"class":80},"principalDirections",[74,428,282],{"class":128},[74,430,431],{"class":76,"line":141},[74,432,433],{"class":287},"\u002F\u002F d0, d1: NDArray, shape [numberOfPoints, 3]\n",[14,435,436],{},[55,437],{"alt":438,"src":439},"Principal directions on the carved pot's relief: the direction of k0 in pink and of k1 in orange at each vertex, following the ridges and grooves","\u002Fimg\u002Ftutorials\u002Fsteps\u002Fprincipal-curvatures-of-a-mesh-2-directions.png",[14,441,442,444,445,235,447,449,450,452],{},[32,443,412],{}," is the direction of ",[32,446,34],{},[32,448,417],{}," the direction of ",[32,451,38],{},". Both are unit vectors, perpendicular to each other and to the vertex normal. You can check:",[65,454,456],{"className":108,"code":455,"language":110,"meta":70,"style":70},"const n = mesh.pointNormals;\ntf.abs(d0.mul(d1).sum(1)).max();  \u002F\u002F close to 0\ntf.abs(d0.mul(n).sum(1)).max();   \u002F\u002F close to 0\n",[32,457,458,470,506],{"__ignoreMap":70},[74,459,460,462,465,467],{"class":76,"line":77},[74,461,166],{"class":117},[74,463,464],{"class":121}," n",[74,466,172],{"class":117},[74,468,469],{"class":128}," mesh.pointNormals;\n",[74,471,472,475,478,481,483,486,489,491,494,497,500,503],{"class":76,"line":141},[74,473,474],{"class":128},"tf.",[74,476,477],{"class":80},"abs",[74,479,480],{"class":128},"(d0.",[74,482,351],{"class":80},[74,484,485],{"class":128},"(d1).",[74,487,488],{"class":80},"sum",[74,490,181],{"class":128},[74,492,493],{"class":121},"1",[74,495,496],{"class":128},")).",[74,498,499],{"class":80},"max",[74,501,502],{"class":128},"();  ",[74,504,505],{"class":287},"\u002F\u002F close to 0\n",[74,507,508,510,512,514,516,519,521,523,525,527,529,532],{"class":76,"line":156},[74,509,474],{"class":128},[74,511,477],{"class":80},[74,513,480],{"class":128},[74,515,351],{"class":80},[74,517,518],{"class":128},"(n).",[74,520,488],{"class":80},[74,522,181],{"class":128},[74,524,493],{"class":121},[74,526,496],{"class":128},[74,528,499],{"class":80},[74,530,531],{"class":128},"();   ",[74,533,505],{"class":287},[14,535,536],{},"Directions are useful for drawing hatching along a surface, for aligning tool paths, and for finding ridges and valleys.",[60,538,540],{"id":539},"shape-index","Shape index",[14,542,543,544,547],{},"The shape index folds the two curvatures into one number in ",[74,545,546],{},"-1, 1",". It describes the local shape, not its size. A small ball and a large ball have the same shape index.",[65,549,551],{"className":108,"code":550,"language":110,"meta":70,"style":70},"const si = tf.shapeIndex(mesh);      \u002F\u002F shape [numberOfPoints]\nconst si3 = tf.shapeIndex(mesh, 3);  \u002F\u002F larger neighborhood\n",[32,552,553,573],{"__ignoreMap":70},[74,554,555,557,560,562,564,567,570],{"class":76,"line":77},[74,556,166],{"class":117},[74,558,559],{"class":121}," si",[74,561,172],{"class":117},[74,563,175],{"class":128},[74,565,566],{"class":80},"shapeIndex",[74,568,569],{"class":128},"(mesh);      ",[74,571,572],{"class":287},"\u002F\u002F shape [numberOfPoints]\n",[74,574,575,577,580,582,584,586,588,590,593],{"class":76,"line":141},[74,576,166],{"class":117},[74,578,579],{"class":121}," si3",[74,581,172],{"class":117},[74,583,175],{"class":128},[74,585,566],{"class":80},[74,587,323],{"class":128},[74,589,326],{"class":121},[74,591,592],{"class":128},");  ",[74,594,595],{"class":287},"\u002F\u002F larger neighborhood\n",[14,597,598],{},[55,599],{"alt":600,"src":601},"Shape index on the carved pot: each vertex classed as cup, trough, saddle, ridge or cap, from dark orange through sand to pale teal","\u002Fimg\u002Ftutorials\u002Fsteps\u002Fprincipal-curvatures-of-a-mesh-3-shape-index.png",[603,604,605,618],"table",{},[606,607,608],"thead",{},[609,610,611,615],"tr",{},[612,613,614],"th",{},"Range",[612,616,617],{},"Shape",[619,620,621,630,638,646,654],"tbody",{},[609,622,623,627],{},[624,625,626],"td",{},"[-1, -5\u002F8)",[624,628,629],{},"Cup",[609,631,632,635],{},[624,633,634],{},"[-5\u002F8, -3\u002F8)",[624,636,637],{},"Trough",[609,639,640,643],{},[624,641,642],{},"[-3\u002F8, 3\u002F8)",[624,644,645],{},"Saddle",[609,647,648,651],{},[624,649,650],{},"[3\u002F8, 5\u002F8)",[624,652,653],{},"Ridge",[609,655,656,661],{},[624,657,658],{},[74,659,660],{},"5\u002F8, 1",[624,662,663],{},"Cap",[14,665,666],{},"Count the vertices of each shape:",[65,668,670],{"className":108,"code":669,"language":110,"meta":70,"style":70},"const edges = [-1, -5\u002F8, -3\u002F8, 3\u002F8, 5\u002F8, 1.0001];\nconst names = [\"cup\", \"trough\", \"saddle\", \"ridge\", \"cap\"];\n\nconst counts = Object.fromEntries(names.map((name, i) =>\n  [name, si.gte(edges[i]).and(si.lt(edges[i + 1])).sum()]));\n",[32,671,672,736,772,776,815],{"__ignoreMap":70},[74,673,674,676,679,681,684,687,689,691,693,696,699,702,704,706,708,710,712,714,716,718,720,722,724,726,728,730,733],{"class":76,"line":77},[74,675,166],{"class":117},[74,677,678],{"class":121}," edges",[74,680,172],{"class":117},[74,682,683],{"class":128}," [",[74,685,686],{"class":117},"-",[74,688,493],{"class":121},[74,690,235],{"class":128},[74,692,686],{"class":117},[74,694,695],{"class":121},"5",[74,697,698],{"class":117},"\u002F",[74,700,701],{"class":121},"8",[74,703,235],{"class":128},[74,705,686],{"class":117},[74,707,326],{"class":121},[74,709,698],{"class":117},[74,711,701],{"class":121},[74,713,235],{"class":128},[74,715,326],{"class":121},[74,717,698],{"class":117},[74,719,701],{"class":121},[74,721,235],{"class":128},[74,723,695],{"class":121},[74,725,698],{"class":117},[74,727,701],{"class":121},[74,729,235],{"class":128},[74,731,732],{"class":121},"1.0001",[74,734,735],{"class":128},"];\n",[74,737,738,740,743,745,747,750,752,755,757,760,762,765,767,770],{"class":76,"line":141},[74,739,166],{"class":117},[74,741,742],{"class":121}," names",[74,744,172],{"class":117},[74,746,683],{"class":128},[74,748,749],{"class":84},"\"cup\"",[74,751,235],{"class":128},[74,753,754],{"class":84},"\"trough\"",[74,756,235],{"class":128},[74,758,759],{"class":84},"\"saddle\"",[74,761,235],{"class":128},[74,763,764],{"class":84},"\"ridge\"",[74,766,235],{"class":128},[74,768,769],{"class":84},"\"cap\"",[74,771,735],{"class":128},[74,773,774],{"class":76,"line":156},[74,775,160],{"emptyLinePlaceholder":159},[74,777,778,780,783,785,788,791,794,797,800,804,806,809,812],{"class":76,"line":163},[74,779,166],{"class":117},[74,781,782],{"class":121}," counts",[74,784,172],{"class":117},[74,786,787],{"class":128}," Object.",[74,789,790],{"class":80},"fromEntries",[74,792,793],{"class":128},"(names.",[74,795,796],{"class":80},"map",[74,798,799],{"class":128},"((",[74,801,803],{"class":802},"s9osk","name",[74,805,235],{"class":128},[74,807,808],{"class":802},"i",[74,810,811],{"class":128},") ",[74,813,814],{"class":117},"=>\n",[74,816,818,821,824,827,830,833,836,839,842,845,848,850],{"class":76,"line":817},5,[74,819,820],{"class":128},"  [name, si.",[74,822,823],{"class":80},"gte",[74,825,826],{"class":128},"(edges[i]).",[74,828,829],{"class":80},"and",[74,831,832],{"class":128},"(si.",[74,834,835],{"class":80},"lt",[74,837,838],{"class":128},"(edges[i ",[74,840,841],{"class":117},"+",[74,843,844],{"class":121}," 1",[74,846,847],{"class":128},"])).",[74,849,488],{"class":80},[74,851,852],{"class":128},"()]));\n",[14,854,855],{},"On the sphere every vertex is a cap.",[60,857,859],{"id":858},"reading-the-signs","Reading the signs",[14,861,862,863,22,865,867],{},"Signs follow the face orientation. With outward-facing normals, a convex surface has positive curvature. On the sphere ",[32,864,34],{},[32,866,38],{}," are positive and the shape index is near 1. Flip the faces and the signs flip with them:",[65,869,871],{"className":108,"code":870,"language":110,"meta":70,"style":70},"const flipped = tf.reverseWinding(mesh);\ntf.shapeIndex(flipped);  \u002F\u002F near -1 on the same sphere\n",[32,872,873,889],{"__ignoreMap":70},[74,874,875,877,880,882,884,887],{"class":76,"line":77},[74,876,166],{"class":117},[74,878,879],{"class":121}," flipped",[74,881,172],{"class":117},[74,883,175],{"class":128},[74,885,886],{"class":80},"reverseWinding",[74,888,282],{"class":128},[74,890,891,893,895,898],{"class":76,"line":141},[74,892,474],{"class":128},[74,894,566],{"class":80},[74,896,897],{"class":128},"(flipped);  ",[74,899,900],{"class":287},"\u002F\u002F near -1 on the same sphere\n",[14,902,903],{},[55,904],{"alt":905,"src":906},"Reading the signs on a cat figurine from Thingi10K: shape-index classes with outward normals on the left and with the faces flipped on the right, where caps turn into cups and ridges into troughs","\u002Fimg\u002Ftutorials\u002Fsteps\u002Fprincipal-curvatures-of-a-mesh-4-signs.png",[14,908,909],{},"If your results look inverted, check the winding first.",[14,911,912],{},"A height field shows all the shapes. Take a flat grid with +z normals and bend it:",[65,914,916],{"className":108,"code":915,"language":110,"meta":70,"style":70},"const plane = tf.planeMesh(2.0, 2.0, 100, 100);\n\nconst bend = (f) => {\n  const pts = plane.points.clone();\n  const q = pts.data;\n  for (let i = 0; i \u003C q.length; i += 3) q[i + 2] = f(q[i], q[i + 1]);\n  return tf.mesh(plane.faces, pts);\n};\n\nconst saddle = bend((x, y) => x * x - y * y);  \u002F\u002F shape index 0 at the center\nconst ridge = bend((x, y) => -(x * x));        \u002F\u002F bends away from the normal: +0.5\nconst trough = bend((x, y) => x * x);          \u002F\u002F bends toward the normal: -0.5\n",[32,917,918,950,954,977,996,1008,1073,1087,1093,1098,1145,1183],{"__ignoreMap":70},[74,919,920,922,925,927,929,932,934,936,938,940,942,944,946,948],{"class":76,"line":77},[74,921,166],{"class":117},[74,923,924],{"class":121}," plane",[74,926,172],{"class":117},[74,928,175],{"class":128},[74,930,931],{"class":80},"planeMesh",[74,933,181],{"class":128},[74,935,232],{"class":121},[74,937,235],{"class":128},[74,939,232],{"class":121},[74,941,235],{"class":128},[74,943,238],{"class":121},[74,945,235],{"class":128},[74,947,238],{"class":121},[74,949,246],{"class":128},[74,951,952],{"class":76,"line":141},[74,953,160],{"emptyLinePlaceholder":159},[74,955,956,958,961,963,966,969,971,974],{"class":76,"line":156},[74,957,166],{"class":117},[74,959,960],{"class":80}," bend",[74,962,172],{"class":117},[74,964,965],{"class":128}," (",[74,967,968],{"class":802},"f",[74,970,811],{"class":128},[74,972,973],{"class":117},"=>",[74,975,976],{"class":128}," {\n",[74,978,979,982,985,987,990,993],{"class":76,"line":163},[74,980,981],{"class":117},"  const",[74,983,984],{"class":121}," pts",[74,986,172],{"class":117},[74,988,989],{"class":128}," plane.points.",[74,991,992],{"class":80},"clone",[74,994,995],{"class":128},"();\n",[74,997,998,1000,1003,1005],{"class":76,"line":817},[74,999,981],{"class":117},[74,1001,1002],{"class":121}," q",[74,1004,172],{"class":117},[74,1006,1007],{"class":128}," pts.data;\n",[74,1009,1011,1014,1016,1019,1022,1024,1027,1030,1033,1036,1039,1041,1044,1047,1050,1052,1055,1058,1060,1063,1066,1068,1070],{"class":76,"line":1010},6,[74,1012,1013],{"class":117},"  for",[74,1015,965],{"class":128},[74,1017,1018],{"class":117},"let",[74,1020,1021],{"class":128}," i ",[74,1023,274],{"class":117},[74,1025,1026],{"class":121}," 0",[74,1028,1029],{"class":128},"; i ",[74,1031,1032],{"class":117},"\u003C",[74,1034,1035],{"class":128}," q.",[74,1037,1038],{"class":121},"length",[74,1040,1029],{"class":128},[74,1042,1043],{"class":117},"+=",[74,1045,1046],{"class":121}," 3",[74,1048,1049],{"class":128},") q[i ",[74,1051,841],{"class":117},[74,1053,1054],{"class":121}," 2",[74,1056,1057],{"class":128},"] ",[74,1059,274],{"class":117},[74,1061,1062],{"class":80}," f",[74,1064,1065],{"class":128},"(q[i], q[i ",[74,1067,841],{"class":117},[74,1069,844],{"class":121},[74,1071,1072],{"class":128},"]);\n",[74,1074,1076,1079,1081,1084],{"class":76,"line":1075},7,[74,1077,1078],{"class":117},"  return",[74,1080,175],{"class":128},[74,1082,1083],{"class":80},"mesh",[74,1085,1086],{"class":128},"(plane.faces, pts);\n",[74,1088,1090],{"class":76,"line":1089},8,[74,1091,1092],{"class":128},"};\n",[74,1094,1096],{"class":76,"line":1095},9,[74,1097,160],{"emptyLinePlaceholder":159},[74,1099,1101,1103,1106,1108,1110,1112,1115,1117,1120,1122,1124,1127,1130,1132,1134,1137,1139,1142],{"class":76,"line":1100},10,[74,1102,166],{"class":117},[74,1104,1105],{"class":121}," saddle",[74,1107,172],{"class":117},[74,1109,960],{"class":80},[74,1111,799],{"class":128},[74,1113,1114],{"class":802},"x",[74,1116,235],{"class":128},[74,1118,1119],{"class":802},"y",[74,1121,811],{"class":128},[74,1123,973],{"class":117},[74,1125,1126],{"class":128}," x ",[74,1128,1129],{"class":117},"*",[74,1131,1126],{"class":128},[74,1133,686],{"class":117},[74,1135,1136],{"class":128}," y ",[74,1138,1129],{"class":117},[74,1140,1141],{"class":128}," y);  ",[74,1143,1144],{"class":287},"\u002F\u002F shape index 0 at the center\n",[74,1146,1148,1150,1153,1155,1157,1159,1161,1163,1165,1167,1169,1172,1175,1177,1180],{"class":76,"line":1147},11,[74,1149,166],{"class":117},[74,1151,1152],{"class":121}," ridge",[74,1154,172],{"class":117},[74,1156,960],{"class":80},[74,1158,799],{"class":128},[74,1160,1114],{"class":802},[74,1162,235],{"class":128},[74,1164,1119],{"class":802},[74,1166,811],{"class":128},[74,1168,973],{"class":117},[74,1170,1171],{"class":117}," -",[74,1173,1174],{"class":128},"(x ",[74,1176,1129],{"class":117},[74,1178,1179],{"class":128}," x));        ",[74,1181,1182],{"class":287},"\u002F\u002F bends away from the normal: +0.5\n",[74,1184,1186,1188,1191,1193,1195,1197,1199,1201,1203,1205,1207,1209,1211,1214],{"class":76,"line":1185},12,[74,1187,166],{"class":117},[74,1189,1190],{"class":121}," trough",[74,1192,172],{"class":117},[74,1194,960],{"class":80},[74,1196,799],{"class":128},[74,1198,1114],{"class":802},[74,1200,235],{"class":128},[74,1202,1119],{"class":802},[74,1204,811],{"class":128},[74,1206,973],{"class":117},[74,1208,1126],{"class":128},[74,1210,1129],{"class":117},[74,1212,1213],{"class":128}," x);          ",[74,1215,1216],{"class":287},"\u002F\u002F bends toward the normal: -0.5\n",[60,1218,1220],{"id":1219},"off-the-main-thread","Off the main thread",[14,1222,1223],{},"Every curvature function has an async variant. It runs off the main thread and keeps the UI responsive:",[65,1225,1227],{"className":108,"code":1226,"language":110,"meta":70,"style":70},"const { k0, k1 } = await tf.async.principalCurvatures(mesh);\nconst si = await tf.async.shapeIndex(mesh);\n",[32,1228,1229,1255],{"__ignoreMap":70},[74,1230,1231,1233,1235,1237,1239,1241,1243,1245,1248,1251,1253],{"class":76,"line":77},[74,1232,166],{"class":117},[74,1234,262],{"class":128},[74,1236,34],{"class":121},[74,1238,235],{"class":128},[74,1240,38],{"class":121},[74,1242,271],{"class":128},[74,1244,274],{"class":117},[74,1246,1247],{"class":117}," await",[74,1249,1250],{"class":128}," tf.async.",[74,1252,279],{"class":80},[74,1254,282],{"class":128},[74,1256,1257,1259,1261,1263,1265,1267,1269],{"class":76,"line":141},[74,1258,166],{"class":117},[74,1260,559],{"class":121},[74,1262,172],{"class":117},[74,1264,1247],{"class":117},[74,1266,1250],{"class":128},[74,1268,566],{"class":80},[74,1270,282],{"class":128},[60,1272,1274],{"id":1273},"working-with-results","Working with results",[14,1276,1277,1278,1281,1282,1285],{},"All results are ",[32,1279,1280],{},"NDArray","s with one value per point, in the mesh's dtype. ",[32,1283,1284],{},".data"," gives the typed array. Upload it as a vertex attribute to color the mesh, or use it as a weight or a field for other tools.",[14,1287,1288,1289,1293],{},"Draw the lines where the surface changes from saddle to ridge with ",[18,1290,1292],{"href":1291},"\u002Ftutorials\u002Fisocontours-on-a-triangle-mesh-in-javascript","isocontours",":",[65,1295,1297],{"className":108,"code":1296,"language":110,"meta":70,"style":70},"const lines = tf.isocontours(mesh, si, 3 \u002F 8);\n",[32,1298,1299],{"__ignoreMap":70},[74,1300,1301,1303,1306,1308,1310,1312,1315,1317,1320,1323],{"class":76,"line":77},[74,1302,166],{"class":117},[74,1304,1305],{"class":121}," lines",[74,1307,172],{"class":117},[74,1309,175],{"class":128},[74,1311,1292],{"class":80},[74,1313,1314],{"class":128},"(mesh, si, ",[74,1316,326],{"class":121},[74,1318,1319],{"class":117}," \u002F",[74,1321,1322],{"class":121}," 8",[74,1324,246],{"class":128},[14,1326,1327],{},"Cut the mesh into regions by shape with isobands:",[65,1329,1331],{"className":108,"code":1330,"language":110,"meta":70,"style":70},"const { mesh: regions, labels, faceLabels } =\n  tf.isobands(mesh, si, [-3 \u002F 8, 3 \u002F 8]);\n\u002F\u002F labels: 0 concave, 1 saddle, 2 convex\n",[32,1332,1333,1362,1391],{"__ignoreMap":70},[74,1334,1335,1337,1339,1341,1344,1347,1349,1352,1354,1357,1359],{"class":76,"line":77},[74,1336,166],{"class":117},[74,1338,262],{"class":128},[74,1340,1083],{"class":802},[74,1342,1343],{"class":128},": ",[74,1345,1346],{"class":121},"regions",[74,1348,235],{"class":128},[74,1350,1351],{"class":121},"labels",[74,1353,235],{"class":128},[74,1355,1356],{"class":121},"faceLabels",[74,1358,271],{"class":128},[74,1360,1361],{"class":117},"=\n",[74,1363,1364,1367,1370,1373,1375,1377,1379,1381,1383,1385,1387,1389],{"class":76,"line":141},[74,1365,1366],{"class":128},"  tf.",[74,1368,1369],{"class":80},"isobands",[74,1371,1372],{"class":128},"(mesh, si, [",[74,1374,686],{"class":117},[74,1376,326],{"class":121},[74,1378,1319],{"class":117},[74,1380,1322],{"class":121},[74,1382,235],{"class":128},[74,1384,326],{"class":121},[74,1386,1319],{"class":117},[74,1388,1322],{"class":121},[74,1390,1072],{"class":128},[74,1392,1393],{"class":76,"line":156},[74,1394,1395],{"class":287},"\u002F\u002F labels: 0 concave, 1 saddle, 2 convex\n",[14,1397,1398],{},"Flag sharp spots for inspection:",[65,1400,1402],{"className":108,"code":1401,"language":110,"meta":70,"style":70},"const isSharp = tf.abs(k0).gt(10.0).or(tf.abs(k1).gt(10.0));\nconst sharp = tf.arange(\"int32\", mesh.numberOfPoints).booleanIndex(isSharp);\n",[32,1403,1404,1449],{"__ignoreMap":70},[74,1405,1406,1408,1411,1413,1415,1417,1420,1423,1425,1428,1431,1434,1437,1439,1441,1443,1445,1447],{"class":76,"line":77},[74,1407,166],{"class":117},[74,1409,1410],{"class":121}," isSharp",[74,1412,172],{"class":117},[74,1414,175],{"class":128},[74,1416,477],{"class":80},[74,1418,1419],{"class":128},"(k0).",[74,1421,1422],{"class":80},"gt",[74,1424,181],{"class":128},[74,1426,1427],{"class":121},"10.0",[74,1429,1430],{"class":128},").",[74,1432,1433],{"class":80},"or",[74,1435,1436],{"class":128},"(tf.",[74,1438,477],{"class":80},[74,1440,371],{"class":128},[74,1442,1422],{"class":80},[74,1444,181],{"class":128},[74,1446,1427],{"class":121},[74,1448,192],{"class":128},[74,1450,1451,1453,1456,1458,1460,1463,1465,1468,1471,1474],{"class":76,"line":141},[74,1452,166],{"class":117},[74,1454,1455],{"class":121}," sharp",[74,1457,172],{"class":117},[74,1459,175],{"class":128},[74,1461,1462],{"class":80},"arange",[74,1464,181],{"class":128},[74,1466,1467],{"class":84},"\"int32\"",[74,1469,1470],{"class":128},", mesh.numberOfPoints).",[74,1472,1473],{"class":80},"booleanIndex",[74,1475,1476],{"class":128},"(isSharp);\n",[14,1478,1479,1480,1484],{},"Combine curvature with ",[18,1481,1483],{"href":1482},"\u002Ftutorials\u002Ffast-geodesic-distances-in-javascript","geodesic distances"," to look only at the neighborhood of a point.",[60,1486,1488],{"id":1487},"related-tutorials","Related tutorials",[1490,1491,1492,1499,1505,1512],"ul",{},[1493,1494,1495,1498],"li",{},[18,1496,1497],{"href":1482},"Fast and Accurate Geodesic Distances in JavaScript"," — distance along the surface from one seed or many.",[1493,1500,1501,1504],{},[18,1502,1503],{"href":1291},"Fast and Robust Isocontours on a Triangle Mesh in JavaScript"," — curves and bands of equal value on a scalar field.",[1493,1506,1507,1511],{},[18,1508,1510],{"href":1509},"\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.",[1493,1513,1514,1518],{},[18,1515,1517],{"href":1516},"\u002Ftutorials\u002Ffast-stl-and-obj-reading-in-javascript","Fast STL and OBJ Reading in JavaScript"," — load and write meshes, the first step of every pipeline.",[60,1520,1522],{"id":1521},"performance","Performance",[1524,1525],"headline-numbers",{":items":1526},"[{\"value\":\"60.6ms\",\"label\":\"Principal curvatures\",\"detail\":\"1M faces, JavaScript\"}]",[14,1528,1529,1534,1535,1534,1539,1534,1543],{},[74,1530,1533],{"className":1531},[1532],"tag-pill","Apple M4 Max"," ",[74,1536,1538],{"className":1537},[1532],"16 threads",[74,1540,1542],{"className":1541},[1532],"Node.js",[74,1544,1546],{"className":1545},[1532],"WebAssembly",[14,1548,1549,1550,22,1552,1554],{},"Principal curvatures on a 1M-face mesh take 60.6 ms in Node.js. The JavaScript benchmark has no competitor, so there is no comparison here. The same core is measured against libigl on the ",[18,1551,21],{"href":20},[18,1553,26],{"href":25}," pages.",[14,1556,1557],{},[55,1558],{"alt":1559,"src":1560},"Benchmark chart: trueform against other TypeScript libraries, principal curvatures","\u002Fimg\u002Falgorithms\u002Fbenchmark-principal-curvatures-typescript-2026.png",[14,1562,1563,1567,1568],{},[18,1564,1566],{"href":1565},"\u002Ftrueform\u002Fbenchmarks","Full benchmarks"," · ",[18,1569,1572],{"href":1570,"rel":1571},"https:\u002F\u002Ftrueform.polydera.com\u002Fts\u002Fmodules\u002Fgeometry#curvature-analysis",[100],"Documentation",[1574,1575],"cite-as",{"author":1576,"title":5},"Sajovic, {\\v{Z}}iga",[1578,1579,1580],"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 .sAwPA, html code.shiki .sAwPA{--shiki-default:#6A737D}html pre.shiki code .s9osk, html code.shiki .s9osk{--shiki-default:#FFAB70}",{"title":70,"searchDepth":141,"depth":141,"links":1582},[1583,1584,1585,1586,1587,1588,1589,1590,1591,1592],{"id":62,"depth":141,"text":63},{"id":104,"depth":141,"text":105},{"id":249,"depth":141,"text":250},{"id":386,"depth":141,"text":387},{"id":539,"depth":141,"text":540},{"id":858,"depth":141,"text":859},{"id":1219,"depth":141,"text":1220},{"id":1273,"depth":141,"text":1274},{"id":1487,"depth":141,"text":1488},{"id":1521,"depth":141,"text":1522},"2026-10-31","Compute principal curvatures, principal directions, Gaussian and mean curvature, and the shape index of a triangle mesh in JavaScript and TypeScript. WebAssembly, runs in Node.js and the browser. 60.6 ms on a 1M-face mesh.","md",[1597,1598,1599,1600,1601,1602,1603,1604,1605,1606],"principal curvature JavaScript","mesh curvature TypeScript","Gaussian curvature mesh JavaScript","mean curvature browser","shape index mesh","curvature WebAssembly","three.js mesh curvature","npm mesh curvature","libigl principal curvature alternative","vertex curvature JavaScript",{},"\u002Fimg\u002Fog\u002Ftutorials\u002Fprincipal-curvatures-of-a-mesh-in-javascript.png","\u002Ftutorials\u002Fprincipal-curvatures-of-a-mesh-in-javascript",{"title":5,"description":1594},"tutorials\u002Fprincipal-curvatures-of-a-mesh-in-javascript",[1613,110,1614,1615,1616],"javascript","curvature","tutorial","webassembly","qqtA6MzAtF2myH4ozqvDrwrUH5d6_miAcG4nK5PBJx4",1793517812008]