[{"data":1,"prerenderedAt":1266},["ShallowReactive",2],{"tutorial-fast-stl-and-obj-reading-in-javascript":3},{"id":4,"title":5,"author":6,"body":7,"date":1237,"description":1238,"extension":1239,"keywords":1240,"meta":1253,"navigation":174,"ogImage":1254,"path":1255,"published":174,"seo":1256,"stem":1257,"tags":1258,"__hash__":1265},"tutorials\u002Ftutorials\u002Ffast-stl-and-obj-reading-in-javascript.md","Fast STL and OBJ Reading in JavaScript","Žiga Sajovic",{"type":8,"value":9,"toc":1226},"minimark",[10,28,48,52,57,82,95,99,121,226,233,240,353,368,372,401,412,455,458,462,469,602,608,615,702,713,717,730,775,789,859,862,866,869,944,948,982,985,1069,1072,1113,1117,1153,1157,1168,1172,1192,1197,1200,1206,1218,1222],[11,12,13],"note",{},[14,15,16,17,22,23,27],"p",{},"Also available in ",[18,19,21],"a",{"href":20},"\u002Ftutorials\u002Ffast-stl-and-obj-reading-in-python","Python"," and ",[18,24,26],{"href":25},"\u002Ftutorials\u002Ffast-stl-and-obj-reading-in-cpp","C++",".",[14,29,30,31,35,36,39,40,43,44,47],{},"Most mesh work starts with reading a file, and on large meshes the reader can take longer than the work. ",[32,33,34],"code",{},"trueform"," reads STL and OBJ from bytes into a mesh: an ",[32,37,38],{},"[N, 3]"," face array and an ",[32,41,42],{},"[M, 3]"," point array. STL triangles are welded into shared vertices as they load. It works in JavaScript and TypeScript with one ",[32,45,46],{},"npm install",", WebAssembly-powered, in Node.js and the browser with the same API. This tutorial covers reading and writing both formats, triangles, quads and mixed polygons, double precision, writing with a transformation, and measured read times against the Three.js loaders.",[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\u002Fio\",\"variant\":\"soft\",\"color\":\"neutral\"}]",[53,54,56],"h2",{"id":55},"install","Install",[58,59,64],"pre",{"className":60,"code":61,"language":62,"meta":63,"style":63},"language-bash shiki shiki-themes github-dark","npm install @polydera\u002Ftrueform\n","bash","",[32,65,66],{"__ignoreMap":63},[67,68,71,75,79],"span",{"class":69,"line":70},"line",1,[67,72,74],{"class":73},"svObZ","npm",[67,76,78],{"class":77},"sU2Wk"," install",[67,80,81],{"class":77}," @polydera\u002Ftrueform\n",[14,83,84,85,88,89,27],{},"The package ships TypeScript types. The WebAssembly binary loads on first use. In the browser, multithreading needs ",[32,86,87],{},"SharedArrayBuffer"," headers; see the ",[18,90,94],{"href":91,"rel":92},"https:\u002F\u002Ftrueform.polydera.com\u002Fts\u002Fgetting-started\u002Finstallation",[93],"nofollow","installation guide",[53,96,98],{"id":97},"reading-stl","Reading STL",[14,100,101,102,105,106,109,110,113,114,117,118,120],{},"All readers take the file's bytes, as a ",[32,103,104],{},"Uint8Array"," or an ",[32,107,108],{},"ArrayBuffer",". In Node.js, ",[32,111,112],{},"readFileSync"," returns a ",[32,115,116],{},"Buffer",", which is a ",[32,119,104],{},":",[58,122,126],{"className":123,"code":124,"language":125,"meta":63,"style":63},"language-typescript shiki shiki-themes github-dark","import * as tf from \"@polydera\u002Ftrueform\";\nimport { readFileSync } from \"node:fs\";\n\nconst mesh = tf.readStl(readFileSync(\"model.stl\"));\nmesh.faces;     \u002F\u002F NDArrayInt32   [N, 3]\nmesh.points;    \u002F\u002F NDArrayFloat32 [M, 3]\n","typescript",[32,127,128,154,169,176,207,217],{"__ignoreMap":63},[67,129,130,134,138,141,145,148,151],{"class":69,"line":70},[67,131,133],{"class":132},"snl16","import",[67,135,137],{"class":136},"sDLfK"," *",[67,139,140],{"class":132}," as",[67,142,144],{"class":143},"s95oV"," tf ",[67,146,147],{"class":132},"from",[67,149,150],{"class":77}," \"@polydera\u002Ftrueform\"",[67,152,153],{"class":143},";\n",[67,155,157,159,162,164,167],{"class":69,"line":156},2,[67,158,133],{"class":132},[67,160,161],{"class":143}," { readFileSync } ",[67,163,147],{"class":132},[67,165,166],{"class":77}," \"node:fs\"",[67,168,153],{"class":143},[67,170,172],{"class":69,"line":171},3,[67,173,175],{"emptyLinePlaceholder":174},true,"\n",[67,177,179,182,185,188,191,194,197,199,201,204],{"class":69,"line":178},4,[67,180,181],{"class":132},"const",[67,183,184],{"class":136}," mesh",[67,186,187],{"class":132}," =",[67,189,190],{"class":143}," tf.",[67,192,193],{"class":73},"readStl",[67,195,196],{"class":143},"(",[67,198,112],{"class":73},[67,200,196],{"class":143},[67,202,203],{"class":77},"\"model.stl\"",[67,205,206],{"class":143},"));\n",[67,208,210,213],{"class":69,"line":209},5,[67,211,212],{"class":143},"mesh.faces;     ",[67,214,216],{"class":215},"sAwPA","\u002F\u002F NDArrayInt32   [N, 3]\n",[67,218,220,223],{"class":69,"line":219},6,[67,221,222],{"class":143},"mesh.points;    ",[67,224,225],{"class":215},"\u002F\u002F NDArrayFloat32 [M, 3]\n",[14,227,228],{},[229,230],"img",{"alt":231,"src":232},"Reading STL: the triangles as an STL file stores them, each with its own three corners, next to the welded mesh read_stl returns, one connected smooth surface","\u002Fimg\u002Ftutorials\u002Fsteps\u002Ffast-stl-and-obj-reading-1-weld.png",[14,234,235,236,239],{},"In the browser, from ",[32,237,238],{},"fetch"," or a file input:",[58,241,243],{"className":123,"code":242,"language":125,"meta":63,"style":63},"const response = await fetch(\"model.stl\");\nconst mesh = tf.readStl(new Uint8Array(await response.arrayBuffer()));\n\nconst file = input.files[0];\nconst mesh2 = tf.readStl(new Uint8Array(await file.arrayBuffer()));\n",[32,244,245,267,301,305,323],{"__ignoreMap":63},[67,246,247,249,252,254,257,260,262,264],{"class":69,"line":70},[67,248,181],{"class":132},[67,250,251],{"class":136}," response",[67,253,187],{"class":132},[67,255,256],{"class":132}," await",[67,258,259],{"class":73}," fetch",[67,261,196],{"class":143},[67,263,203],{"class":77},[67,265,266],{"class":143},");\n",[67,268,269,271,273,275,277,279,281,284,287,289,292,295,298],{"class":69,"line":156},[67,270,181],{"class":132},[67,272,184],{"class":136},[67,274,187],{"class":132},[67,276,190],{"class":143},[67,278,193],{"class":73},[67,280,196],{"class":143},[67,282,283],{"class":132},"new",[67,285,286],{"class":73}," Uint8Array",[67,288,196],{"class":143},[67,290,291],{"class":132},"await",[67,293,294],{"class":143}," response.",[67,296,297],{"class":73},"arrayBuffer",[67,299,300],{"class":143},"()));\n",[67,302,303],{"class":69,"line":171},[67,304,175],{"emptyLinePlaceholder":174},[67,306,307,309,312,314,317,320],{"class":69,"line":178},[67,308,181],{"class":132},[67,310,311],{"class":136}," file",[67,313,187],{"class":132},[67,315,316],{"class":143}," input.files[",[67,318,319],{"class":136},"0",[67,321,322],{"class":143},"];\n",[67,324,325,327,330,332,334,336,338,340,342,344,346,349,351],{"class":69,"line":209},[67,326,181],{"class":132},[67,328,329],{"class":136}," mesh2",[67,331,187],{"class":132},[67,333,190],{"class":143},[67,335,193],{"class":73},[67,337,196],{"class":143},[67,339,283],{"class":132},[67,341,286],{"class":73},[67,343,196],{"class":143},[67,345,291],{"class":132},[67,347,348],{"class":143}," file.",[67,350,297],{"class":73},[67,352,300],{"class":143},[14,354,355,356,359,360,363,364,367],{},"An STL file stores three corners per triangle with no sharing. ",[32,357,358],{},"tf.readStl"," merges corners at the same position, so ",[32,361,362],{},"points"," holds each vertex once and ",[32,365,366],{},"faces"," indexes into it. The result is a connected mesh, ready for topology queries. Binary and ASCII STL both work. STL stores float32 by spec, so the mesh is always float32.",[53,369,371],{"id":370},"reading-obj","Reading OBJ",[58,373,375],{"className":123,"code":374,"language":125,"meta":63,"style":63},"const mesh = tf.readObj(readFileSync(\"model.obj\"));\n",[32,376,377],{"__ignoreMap":63},[67,378,379,381,383,385,387,390,392,394,396,399],{"class":69,"line":70},[67,380,181],{"class":132},[67,382,184],{"class":136},[67,384,187],{"class":132},[67,386,190],{"class":143},[67,388,389],{"class":73},"readObj",[67,391,196],{"class":143},[67,393,112],{"class":73},[67,395,196],{"class":143},[67,397,398],{"class":77},"\"model.obj\"",[67,400,206],{"class":143},[14,402,403,404,407,408,411],{},"By default ",[32,405,406],{},"tf.readObj"," expects triangles. Pass ",[32,409,410],{},"{ dtype: \"float64\" }"," for double-precision points:",[58,413,415],{"className":123,"code":414,"language":125,"meta":63,"style":63},"const mesh64 = tf.readObj(readFileSync(\"model.obj\"), { dtype: \"float64\" });\nmesh64.dtype;   \u002F\u002F \"float64\"\n",[32,416,417,447],{"__ignoreMap":63},[67,418,419,421,424,426,428,430,432,434,436,438,441,444],{"class":69,"line":70},[67,420,181],{"class":132},[67,422,423],{"class":136}," mesh64",[67,425,187],{"class":132},[67,427,190],{"class":143},[67,429,389],{"class":73},[67,431,196],{"class":143},[67,433,112],{"class":73},[67,435,196],{"class":143},[67,437,398],{"class":77},[67,439,440],{"class":143},"), { dtype: ",[67,442,443],{"class":77},"\"float64\"",[67,445,446],{"class":143}," });\n",[67,448,449,452],{"class":69,"line":156},[67,450,451],{"class":143},"mesh64.dtype;   ",[67,453,454],{"class":215},"\u002F\u002F \"float64\"\n",[14,456,457],{},"Only positions and faces are read. Normals, texture coordinates, materials and groups are skipped.",[53,459,461],{"id":460},"quads-and-mixed-polygons","Quads and mixed polygons",[14,463,464,465,468],{},"OBJ files may mix triangles, quads and larger polygons. Pass ",[32,466,467],{},"{ dynamic: true }"," to read every polygon and triangulate it on load:",[58,470,472],{"className":123,"code":471,"language":125,"meta":63,"style":63},"import { writeFileSync } from \"node:fs\";\n\nwriteFileSync(\"quads.obj\",\n  \"v 0 0 0\\nv 1 0 0\\nv 1 1 0\\nv 0 1 0\\nv 2 0 0\\nv 2 1 0\\n\" +\n  \"f 1 2 3 4\\nf 2 5 6 3\\n\");\n\nconst quads = tf.readObj(readFileSync(\"quads.obj\"), { dynamic: true });\nquads.numberOfFaces;   \u002F\u002F 4 triangles from 2 quads\n",[32,473,474,487,491,504,543,559,563,593],{"__ignoreMap":63},[67,475,476,478,481,483,485],{"class":69,"line":70},[67,477,133],{"class":132},[67,479,480],{"class":143}," { writeFileSync } ",[67,482,147],{"class":132},[67,484,166],{"class":77},[67,486,153],{"class":143},[67,488,489],{"class":69,"line":156},[67,490,175],{"emptyLinePlaceholder":174},[67,492,493,496,498,501],{"class":69,"line":171},[67,494,495],{"class":73},"writeFileSync",[67,497,196],{"class":143},[67,499,500],{"class":77},"\"quads.obj\"",[67,502,503],{"class":143},",\n",[67,505,506,509,512,515,517,520,522,525,527,530,532,535,537,540],{"class":69,"line":178},[67,507,508],{"class":77},"  \"v 0 0 0",[67,510,511],{"class":136},"\\n",[67,513,514],{"class":77},"v 1 0 0",[67,516,511],{"class":136},[67,518,519],{"class":77},"v 1 1 0",[67,521,511],{"class":136},[67,523,524],{"class":77},"v 0 1 0",[67,526,511],{"class":136},[67,528,529],{"class":77},"v 2 0 0",[67,531,511],{"class":136},[67,533,534],{"class":77},"v 2 1 0",[67,536,511],{"class":136},[67,538,539],{"class":77},"\"",[67,541,542],{"class":132}," +\n",[67,544,545,548,550,553,555,557],{"class":69,"line":209},[67,546,547],{"class":77},"  \"f 1 2 3 4",[67,549,511],{"class":136},[67,551,552],{"class":77},"f 2 5 6 3",[67,554,511],{"class":136},[67,556,539],{"class":77},[67,558,266],{"class":143},[67,560,561],{"class":69,"line":219},[67,562,175],{"emptyLinePlaceholder":174},[67,564,566,568,571,573,575,577,579,581,583,585,588,591],{"class":69,"line":565},7,[67,567,181],{"class":132},[67,569,570],{"class":136}," quads",[67,572,187],{"class":132},[67,574,190],{"class":143},[67,576,389],{"class":73},[67,578,196],{"class":143},[67,580,112],{"class":73},[67,582,196],{"class":143},[67,584,500],{"class":77},[67,586,587],{"class":143},"), { dynamic: ",[67,589,590],{"class":136},"true",[67,592,446],{"class":143},[67,594,596,599],{"class":69,"line":595},8,[67,597,598],{"class":143},"quads.numberOfFaces;   ",[67,600,601],{"class":215},"\u002F\u002F 4 triangles from 2 quads\n",[14,603,604,605,607],{},"Without ",[32,606,467],{},", a file of quads reads as a mesh with 0 faces. If a mesh comes back empty, this is the first thing to check.",[14,609,610,611,614],{},"To see the polygons before triangulating, read the raw data. Faces come back as an ",[32,612,613],{},"OffsetBlockedBuffer",", one block per polygon:",[58,616,618],{"className":123,"code":617,"language":125,"meta":63,"style":63},"const raw = tf.readObjData(readFileSync(\"quads.obj\"), { dynamic: true });\nraw.faces.length;          \u002F\u002F 2 polygons\nraw.faces.get(0).data;     \u002F\u002F Int32Array [0, 1, 2, 3]\n\nconst mesh = tf.triangulate(raw);   \u002F\u002F 4 triangles\n",[32,619,620,648,662,679,683],{"__ignoreMap":63},[67,621,622,624,627,629,631,634,636,638,640,642,644,646],{"class":69,"line":70},[67,623,181],{"class":132},[67,625,626],{"class":136}," raw",[67,628,187],{"class":132},[67,630,190],{"class":143},[67,632,633],{"class":73},"readObjData",[67,635,196],{"class":143},[67,637,112],{"class":73},[67,639,196],{"class":143},[67,641,500],{"class":77},[67,643,587],{"class":143},[67,645,590],{"class":136},[67,647,446],{"class":143},[67,649,650,653,656,659],{"class":69,"line":156},[67,651,652],{"class":143},"raw.faces.",[67,654,655],{"class":136},"length",[67,657,658],{"class":143},";          ",[67,660,661],{"class":215},"\u002F\u002F 2 polygons\n",[67,663,664,666,669,671,673,676],{"class":69,"line":171},[67,665,652],{"class":143},[67,667,668],{"class":73},"get",[67,670,196],{"class":143},[67,672,319],{"class":136},[67,674,675],{"class":143},").data;     ",[67,677,678],{"class":215},"\u002F\u002F Int32Array [0, 1, 2, 3]\n",[67,680,681],{"class":69,"line":178},[67,682,175],{"emptyLinePlaceholder":174},[67,684,685,687,689,691,693,696,699],{"class":69,"line":209},[67,686,181],{"class":132},[67,688,184],{"class":136},[67,690,187],{"class":132},[67,692,190],{"class":143},[67,694,695],{"class":73},"triangulate",[67,697,698],{"class":143},"(raw);   ",[67,700,701],{"class":215},"\u002F\u002F 4 triangles\n",[14,703,704,705,708,709,712],{},"OBJ indices start at 1; the arrays start at 0. ",[32,706,707],{},"tf.readStlData"," gives the same raw ",[32,710,711],{},"{ faces, points }"," view for STL.",[53,714,716],{"id":715},"writing-stl-and-obj","Writing STL and OBJ",[14,718,719,22,722,725,726,729],{},[32,720,721],{},"tf.writeStl",[32,723,724],{},"tf.writeObj"," return the file as bytes. ",[32,727,728],{},".data"," is the typed array:",[58,731,733],{"className":123,"code":732,"language":125,"meta":63,"style":63},"writeFileSync(\"out.stl\", tf.writeStl(mesh).data);   \u002F\u002F binary STL\nwriteFileSync(\"out.obj\", tf.writeObj(mesh).data);   \u002F\u002F ASCII OBJ\n",[32,734,735,756],{"__ignoreMap":63},[67,736,737,739,741,744,747,750,753],{"class":69,"line":70},[67,738,495],{"class":73},[67,740,196],{"class":143},[67,742,743],{"class":77},"\"out.stl\"",[67,745,746],{"class":143},", tf.",[67,748,749],{"class":73},"writeStl",[67,751,752],{"class":143},"(mesh).data);   ",[67,754,755],{"class":215},"\u002F\u002F binary STL\n",[67,757,758,760,762,765,767,770,772],{"class":69,"line":156},[67,759,495],{"class":73},[67,761,196],{"class":143},[67,763,764],{"class":77},"\"out.obj\"",[67,766,746],{"class":143},[67,768,769],{"class":73},"writeObj",[67,771,752],{"class":143},[67,773,774],{"class":215},"\u002F\u002F ASCII OBJ\n",[14,776,777,778,781,782,784,785,788],{},"In the browser, wrap the bytes in a ",[32,779,780],{},"Blob"," to download or upload them. The bytes live in WebAssembly memory, which is shared when threads are on, and browsers refuse a ",[32,783,780],{}," built over shared memory. ",[32,786,787],{},".slice()"," copies them into a plain buffer first:",[58,790,792],{"className":123,"code":791,"language":125,"meta":63,"style":63},"const bytes = tf.writeStl(mesh).data.slice();\nconst blob = new Blob([bytes], { type: \"model\u002Fstl\" });\nconst url = URL.createObjectURL(blob);\n",[32,793,794,816,839],{"__ignoreMap":63},[67,795,796,798,801,803,805,807,810,813],{"class":69,"line":70},[67,797,181],{"class":132},[67,799,800],{"class":136}," bytes",[67,802,187],{"class":132},[67,804,190],{"class":143},[67,806,749],{"class":73},[67,808,809],{"class":143},"(mesh).data.",[67,811,812],{"class":73},"slice",[67,814,815],{"class":143},"();\n",[67,817,818,820,823,825,828,831,834,837],{"class":69,"line":156},[67,819,181],{"class":132},[67,821,822],{"class":136}," blob",[67,824,187],{"class":132},[67,826,827],{"class":132}," new",[67,829,830],{"class":73}," Blob",[67,832,833],{"class":143},"([bytes], { type: ",[67,835,836],{"class":77},"\"model\u002Fstl\"",[67,838,446],{"class":143},[67,840,841,843,846,848,851,853,856],{"class":69,"line":171},[67,842,181],{"class":132},[67,844,845],{"class":136}," url",[67,847,187],{"class":132},[67,849,850],{"class":136}," URL",[67,852,27],{"class":143},[67,854,855],{"class":73},"createObjectURL",[67,857,858],{"class":143},"(blob);\n",[14,860,861],{},"OBJ output matches the mesh dtype: float32 points are written with 9 significant digits, float64 with 17, enough for an exact round trip.",[53,863,865],{"id":864},"writing-with-a-transformation","Writing with a transformation",[14,867,868],{},"A transformation set on the mesh is applied as it is written. The points in memory do not change. Use a shallow copy to write a moved version without touching the original:",[58,870,872],{"className":123,"code":871,"language":125,"meta":63,"style":63},"const moved = mesh.shallowCopy();\nmoved.transformation = tf.makeTranslation(0, 0, 5);   \u002F\u002F move up by 5\n\nwriteFileSync(\"moved.stl\", tf.writeStl(moved).data);\n",[32,873,874,891,924,928],{"__ignoreMap":63},[67,875,876,878,881,883,886,889],{"class":69,"line":70},[67,877,181],{"class":132},[67,879,880],{"class":136}," moved",[67,882,187],{"class":132},[67,884,885],{"class":143}," mesh.",[67,887,888],{"class":73},"shallowCopy",[67,890,815],{"class":143},[67,892,893,896,899,901,904,906,908,911,913,915,918,921],{"class":69,"line":156},[67,894,895],{"class":143},"moved.transformation ",[67,897,898],{"class":132},"=",[67,900,190],{"class":143},[67,902,903],{"class":73},"makeTranslation",[67,905,196],{"class":143},[67,907,319],{"class":136},[67,909,910],{"class":143},", ",[67,912,319],{"class":136},[67,914,910],{"class":143},[67,916,917],{"class":136},"5",[67,919,920],{"class":143},");   ",[67,922,923],{"class":215},"\u002F\u002F move up by 5\n",[67,925,926],{"class":69,"line":171},[67,927,175],{"emptyLinePlaceholder":174},[67,929,930,932,934,937,939,941],{"class":69,"line":178},[67,931,495],{"class":73},[67,933,196],{"class":143},[67,935,936],{"class":77},"\"moved.stl\"",[67,938,746],{"class":143},[67,940,749],{"class":73},[67,942,943],{"class":143},"(moved).data);\n",[53,945,947],{"id":946},"working-with-results","Working with results",[14,949,950,951,953,954,910,958,910,963,910,967,971,972,22,975,978,979,27],{},"The mesh goes straight into the rest of ",[32,952,34],{},": ",[18,955,957],{"href":956},"\u002Ftutorials\u002Ffast-mesh-booleans-in-javascript","booleans",[18,959,962],{"href":960,"rel":961},"https:\u002F\u002Ftrueform.polydera.com\u002Fts\u002Fmodules\u002Fremesh",[93],"decimation",[18,964,966],{"href":965},"\u002Ftutorials\u002Ffast-closest-point-on-a-mesh-in-javascript","closest points",[18,968,970],{"href":969},"\u002Ftutorials\u002Ffast-connected-components-in-javascript","connected components",". For Three.js, ",[32,973,974],{},"faces.data",[32,976,977],{},"points.data"," become the index and position attributes of a ",[32,980,981],{},"BufferGeometry",[14,983,984],{},"A round trip keeps the data:",[58,986,988],{"className":123,"code":987,"language":125,"meta":63,"style":63},"const sphere = tf.sphereMesh(1.0, 100, 200);\nwriteFileSync(\"sphere.obj\", tf.writeObj(sphere).data);\n\nconst back = tf.readObj(readFileSync(\"sphere.obj\"));\n\u002F\u002F back.faces and back.points equal sphere.faces and sphere.points\n",[32,989,990,1021,1037,1041,1064],{"__ignoreMap":63},[67,991,992,994,997,999,1001,1004,1006,1009,1011,1014,1016,1019],{"class":69,"line":70},[67,993,181],{"class":132},[67,995,996],{"class":136}," sphere",[67,998,187],{"class":132},[67,1000,190],{"class":143},[67,1002,1003],{"class":73},"sphereMesh",[67,1005,196],{"class":143},[67,1007,1008],{"class":136},"1.0",[67,1010,910],{"class":143},[67,1012,1013],{"class":136},"100",[67,1015,910],{"class":143},[67,1017,1018],{"class":136},"200",[67,1020,266],{"class":143},[67,1022,1023,1025,1027,1030,1032,1034],{"class":69,"line":156},[67,1024,495],{"class":73},[67,1026,196],{"class":143},[67,1028,1029],{"class":77},"\"sphere.obj\"",[67,1031,746],{"class":143},[67,1033,769],{"class":73},[67,1035,1036],{"class":143},"(sphere).data);\n",[67,1038,1039],{"class":69,"line":171},[67,1040,175],{"emptyLinePlaceholder":174},[67,1042,1043,1045,1048,1050,1052,1054,1056,1058,1060,1062],{"class":69,"line":178},[67,1044,181],{"class":132},[67,1046,1047],{"class":136}," back",[67,1049,187],{"class":132},[67,1051,190],{"class":143},[67,1053,389],{"class":73},[67,1055,196],{"class":143},[67,1057,112],{"class":73},[67,1059,196],{"class":143},[67,1061,1029],{"class":77},[67,1063,206],{"class":143},[67,1065,1066],{"class":69,"line":209},[67,1067,1068],{"class":215},"\u002F\u002F back.faces and back.points equal sphere.faces and sphere.points\n",[14,1070,1071],{},"To parse off the main thread, use the async twins:",[58,1073,1075],{"className":123,"code":1074,"language":125,"meta":63,"style":63},"const mesh = await tf.async.readStl(bytes);\nconst stl = await tf.async.writeStl(mesh);\n",[32,1076,1077,1095],{"__ignoreMap":63},[67,1078,1079,1081,1083,1085,1087,1090,1092],{"class":69,"line":70},[67,1080,181],{"class":132},[67,1082,184],{"class":136},[67,1084,187],{"class":132},[67,1086,256],{"class":132},[67,1088,1089],{"class":143}," tf.async.",[67,1091,193],{"class":73},[67,1093,1094],{"class":143},"(bytes);\n",[67,1096,1097,1099,1102,1104,1106,1108,1110],{"class":69,"line":156},[67,1098,181],{"class":132},[67,1100,1101],{"class":136}," stl",[67,1103,187],{"class":132},[67,1105,256],{"class":132},[67,1107,1089],{"class":143},[67,1109,749],{"class":73},[67,1111,1112],{"class":143},"(mesh);\n",[53,1114,1116],{"id":1115},"related-tutorials","Related tutorials",[1118,1119,1120,1127,1133,1139,1146],"ul",{},[1121,1122,1123,1126],"li",{},[18,1124,1125],{"href":956},"Fast Mesh Booleans in JavaScript"," — exact union, intersection and difference of two meshes.",[1121,1128,1129,1132],{},[18,1130,1131],{"href":965},"Fast and Robust Closest Point on a Mesh in JavaScript"," — nearest surface point for a batch of queries.",[1121,1134,1135,1138],{},[18,1136,1137],{"href":969},"Fast Connected Components in JavaScript"," — split a mesh into parts and find its boundary loops.",[1121,1140,1141,1145],{},[18,1142,1144],{"href":1143},"\u002Ftutorials\u002Fprincipal-curvatures-of-a-mesh-in-javascript","Fast and Robust Principal Curvatures of a Mesh in JavaScript"," — curvature values and directions at every vertex.",[1121,1147,1148,1152],{},[18,1149,1151],{"href":1150},"\u002Ftutorials\u002Fconstrained-delaunay-triangulation-in-javascript","Fast and Exact Constrained Delaunay Triangulation in JavaScript"," — triangulate points and outlines, with holes and constraints.",[53,1154,1156],{"id":1155},"performance","Performance",[14,1158,1159,1160,1163,1164,1167],{},"Read times for the Stanford Dragon at 1,025,766 faces. For STL the comparison is Three.js ",[32,1161,1162],{},"STLLoader"," followed by an exact weld, so both sides return a shared-vertex mesh. For OBJ it is the Three.js ",[32,1165,1166],{},"OBJLoader"," parser alone.",[1169,1170],"headline-numbers",{":items":1171},"[{\"value\":\"11.0ms\",\"label\":\"OBJ read\",\"detail\":\"1,025,766 faces\"},{\"value\":\"36.6ms\",\"label\":\"STL read and weld\",\"detail\":\"1,025,766 faces\"},{\"value\":\"64×\",\"label\":\"Faster than Three.js OBJLoader\",\"detail\":\"Median wall time\"}]",[14,1173,1174,1179,1180,1179,1184,1179,1188],{},[67,1175,1178],{"className":1176},[1177],"tag-pill","Apple M4 Max"," ",[67,1181,1183],{"className":1182},[1177],"16 threads",[67,1185,1187],{"className":1186},[1177],"Node.js",[67,1189,1191],{"className":1190},[1177],"WebAssembly",[1193,1194],"data-table",{":headers":1195,":highlight":319,":rows":1196},"[\"Format\",\"trueform\",\"Three.js\",\"vs trueform\"]","[[\"OBJ, 1,025,766 faces\",\"11.0 ms\",\"703.8 ms (parser only)\",\"64×\"],[\"STL, 1,025,766 faces\",\"36.6 ms\",\"848.6 ms (with exact weld)\",\"23×\"]]",[14,1198,1199],{},"Median of three fresh-process medians. Benchmark run with trueform npm 0.10.2 and Three.js 0.185.1.",[14,1201,1202],{},[229,1203],{"alt":1204,"src":1205},"Benchmark chart: trueform against other TypeScript libraries, io","\u002Fimg\u002Falgorithms\u002Fbenchmark-io-typescript-2026.png",[14,1207,1208,1212,1213],{},[18,1209,1211],{"href":1210},"\u002Ftrueform\u002Fbenchmarks","Full benchmarks"," · ",[18,1214,1217],{"href":1215,"rel":1216},"https:\u002F\u002Ftrueform.polydera.com\u002Fts\u002Fmodules\u002Fio",[93],"Documentation",[1219,1220],"cite-as",{"author":1221,"title":5},"Sajovic, {\\v{Z}}iga",[1223,1224,1225],"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}",{"title":63,"searchDepth":156,"depth":156,"links":1227},[1228,1229,1230,1231,1232,1233,1234,1235,1236],{"id":55,"depth":156,"text":56},{"id":97,"depth":156,"text":98},{"id":370,"depth":156,"text":371},{"id":460,"depth":156,"text":461},{"id":715,"depth":156,"text":716},{"id":864,"depth":156,"text":865},{"id":946,"depth":156,"text":947},{"id":1115,"depth":156,"text":1116},{"id":1155,"depth":156,"text":1156},"2026-10-31","Read and write STL and OBJ mesh files in JavaScript and TypeScript, in Node.js and the browser. A 1M-triangle OBJ in 11.0 ms and a welded binary STL in 36.6 ms, measured against Three.js loaders. WebAssembly-powered, one npm install.","md",[1241,1242,1243,1244,1245,1246,1247,1248,1249,1250,1251,1252],"read STL JavaScript","read OBJ JavaScript","load STL browser","parse OBJ TypeScript","fast STL loader JavaScript","fast OBJ loader JavaScript","write STL JavaScript","three.js STLLoader alternative","three.js OBJLoader alternative","WebAssembly STL parser","npm STL reader","Node.js read STL",{},"\u002Fimg\u002Fog\u002Ftutorials\u002Ffast-stl-and-obj-reading-in-javascript.png","\u002Ftutorials\u002Ffast-stl-and-obj-reading-in-javascript",{"title":5,"description":1238},"tutorials\u002Ffast-stl-and-obj-reading-in-javascript",[1259,125,1260,1261,1262,1263,1264],"javascript","io","stl","obj","tutorial","webassembly","DqDUl3DQMGUTdOs8rIuTFvVtcGxEjy9GISYCJdVfFsI",1793517812008]