{"id":51541869,"url":"https://github.com/countertype/libtess-ts","last_synced_at":"2026-07-09T14:30:25.922Z","repository":{"id":369012603,"uuid":"1156000290","full_name":"countertype/libtess-ts","owner":"countertype","description":"Optimized TypeScript fork of libtess.js, itself of a port of the GLU tessellator","archived":false,"fork":false,"pushed_at":"2026-07-03T06:15:49.000Z","size":294,"stargazers_count":0,"open_issues_count":1,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-07-03T08:15:18.850Z","etag":null,"topics":["glu","glu-tesselator","libtess","tesselation","tesselator","tessellation","tessellator","typescript"],"latest_commit_sha":null,"homepage":"","language":"JavaScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"other","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/countertype.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-02-12T06:24:09.000Z","updated_at":"2026-02-14T02:26:28.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/countertype/libtess-ts","commit_stats":null,"previous_names":["countertype/libtess-ts"],"tags_count":null,"template":false,"template_full_name":null,"purl":"pkg:github/countertype/libtess-ts","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/countertype%2Flibtess-ts","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/countertype%2Flibtess-ts/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/countertype%2Flibtess-ts/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/countertype%2Flibtess-ts/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/countertype","download_url":"https://codeload.github.com/countertype/libtess-ts/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/countertype%2Flibtess-ts/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":35303290,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-07-09T02:00:07.329Z","response_time":57,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["glu","glu-tesselator","libtess","tesselation","tesselator","tessellation","tessellator","typescript"],"created_at":"2026-07-09T14:30:25.206Z","updated_at":"2026-07-09T14:30:25.906Z","avatar_url":"https://github.com/countertype.png","language":"JavaScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# libtess-ts\n\nOptimized TypeScript port of [libtess.js](https://github.com/brendankenny/libtess.js) by Brendan Kenny, itself a port of the SGI GLU tessellator by Eric Veach\n\n[![npm](https://img.shields.io/npm/v/libtess-ts)](https://www.npmjs.com/package/libtess-ts)\n[![License](https://img.shields.io/badge/license-SGI--B--2.0-blue)](./LICENSE)\n\n## Performance vs libtess.js\n\nThis library builds on libtess.js, which faithfully ported the GLU tessellator to JavaScript. The core sweep-line tessellation algorithm is unchanged here; the performance difference (typically 15-30% faster, sometimes more on large inputs) comes from implementation-level changes:\n\n- Direct monotone rendering: triangulates monotone faces inline during the sweep, avoiding a separate mesh-modification pass\n- 2D fast path: when you call `gluTessNormal(0, 0, 1)`, vertex projection is folded into `gluTessVertex` instead of requiring a separate loop over all vertices\n- Reduced allocation: scratch buffers, priority queue storage, and temporary objects are reused across calls rather than re-allocated each time\n- Typed arrays in the priority queue heap\n\nSee [changes from libtess.js](#changes-from-libtessjs) for the full list of API differences\n\n### vs tess2.js\n\n[libtess2](https://github.com/memononen/libtess2) by Mikko Mononen is a performance oriented adaptation of the original SGI GLU tessellator with a simpler API. It reports a 15-50x speedup in C. [tess2.js](https://github.com/memononen/tess2.js) is its JavaScript port\n\nIn JavaScript, though, the performance picture is different: libtess-ts is typically 40-90% faster than tess2.js on the same inputs. libtess2's key optimization is a bucketed memory allocator that replaces many small `malloc` calls; a significant win in C, but in JavaScript there is no `malloc` to avoid as the GC manages the heap, so the optimization doesn't carry over\n\nSee [benchmarks](#benchmarks) for numbers\n\n## Installation\n\n```bash\nnpm install libtess-ts\n```\n\n## Quick start\n\n```typescript\nimport { GluTesselator, GLU_TESS } from 'libtess-ts'\n\nconst tess = new GluTesselator()\n\n// Collect triangle indices\nconst indices: number[] = []\ntess.gluTessCallback(GLU_TESS.VERTEX_DATA, (data: number) =\u003e {\n  indices.push(data)\n})\n\n// If your input is 2D (and it probably is), set this\n// Without it the tessellator auto-detects the projection\n// plane, which costs an extra pass over the vertices\ntess.gluTessNormal(0, 0, 1)\n\ntess.gluTessBeginPolygon()\n\ntess.gluTessBeginContour()\ntess.gluTessVertex([0, 0], 0)\ntess.gluTessVertex([10, 0], 1)\ntess.gluTessVertex([10, 10], 2)\ntess.gluTessVertex([0, 10], 3)\ntess.gluTessEndContour()\n\n// Holes are additional contours with opposite winding\ntess.gluTessBeginContour()\ntess.gluTessVertex([2, 2], 4)\ntess.gluTessVertex([2, 8], 5)\ntess.gluTessVertex([8, 8], 6)\ntess.gluTessVertex([8, 2], 7)\ntess.gluTessEndContour()\n\ntess.gluTessEndPolygon()\n\n// indices now contains triangle vertex indices\n```\n\n## API\n\n### Core methods\n\n| Method | Description |\n|---|---|\n| `gluTessBeginPolygon(data?)` | Start a new polygon. Optional data is passed to `_DATA` callbacks |\n| `gluTessBeginContour()` | Start a new contour (outer boundary or hole) |\n| `gluTessVertex(coords, data?)` | Add a vertex. `coords` is `[x, y]` or `[x, y, z]` |\n| `gluTessEndContour()` | Close the current contour |\n| `gluTessEndPolygon()` | Tessellate and emit results via callbacks |\n| `gluTessNormal(x, y, z)` | Set the projection plane normal. Call `(0, 0, 1)` for 2D input, it's faster |\n| `gluTessProperty(which, value)` | Set a tessellation property (winding rule, boundary-only mode) |\n\n### Properties via `gluTessProperty`\n\n| Enum | Value | Description |\n|---|---|---|\n| `GLU_TESS.WINDING_RULE` | 0=ODD, 1=NONZERO, 2=POSITIVE, 3=NEGATIVE, 4=ABS_GEQ_TWO | Winding rule for interior detection |\n| `GLU_TESS.BOUNDARY_ONLY` | 0 or 1 | If 1, emit boundary contours instead of triangles |\n| `GLU_TESS.TOLERANCE` | number | Accepted but ignored (for compatibility) |\n\n### Callbacks\n\nRegister via `gluTessCallback(type, fn)`:\n\n| Type | Callback signature | Description |\n|---|---|---|\n| `GLU_TESS.BEGIN` | `(type: number) =\u003e void` | Start of a primitive (`GL_TRIANGLES`) |\n| `GLU_TESS.VERTEX` | `(data: unknown) =\u003e void` | Vertex data (the value passed to `gluTessVertex`) |\n| `GLU_TESS.END` | `() =\u003e void` | End of a primitive |\n| `GLU_TESS.COMBINE` | `(coords, data, weights) =\u003e unknown` | Vertex interpolation at intersections (see below) |\n| `GLU_TESS.EDGE_FLAG` | `(flag: boolean) =\u003e void` | Edge boundary flag. Registering this forces individual triangle output |\n| `GLU_TESS.ERROR` | `(errorNumber: number) =\u003e void` | Tessellation error (e.g. missing combine callback) |\n| `GLU_TESS.BEGIN_DATA` | `(type: number, polygonData: unknown) =\u003e void` | Like `BEGIN`, with polygon data from `gluTessBeginPolygon` |\n| `GLU_TESS.VERTEX_DATA` | `(data: unknown, polygonData: unknown) =\u003e void` | Like `VERTEX`, with polygon data |\n| `GLU_TESS.END_DATA` | `(polygonData: unknown) =\u003e void` | Like `END`, with polygon data |\n| `GLU_TESS.COMBINE_DATA` | `(coords, data, weights, polygonData) =\u003e unknown` | Like `COMBINE`, with polygon data |\n| `GLU_TESS.EDGE_FLAG_DATA` | `(flag: boolean, polygonData: unknown) =\u003e void` | Like `EDGE_FLAG`, with polygon data |\n| `GLU_TESS.ERROR_DATA` | `(errorNumber: number, polygonData: unknown) =\u003e void` | Like `ERROR`, with polygon data |\n\n### Separate sweep and render\n\nIf you need both triangles and boundary contours from the same input, use `compute()` to run the sweep once, then call the render methods separately:\n\n```typescript\nimport { GluTesselator, WINDING } from 'libtess-ts'\n\nconst tess = new GluTesselator()\n\ntess.gluTessBeginPolygon()\n// ... add contours ...\n// Don't call gluTessEndPolygon(), use compute() instead\n\ntess.compute(WINDING.NONZERO)\n\n// Extract triangles first\ntess.renderTriangles()\n\n// Then extract boundary contours (destructive: call after renderTriangles)\ntess.renderBoundary()\n```\n\n### Combine callback\n\nWhen contours self-intersect, the tessellator needs to create new vertices at the intersection points. Supply a combine callback to interpolate vertex attributes:\n\n```typescript\ntess.gluTessCallback(GLU_TESS.COMBINE, (coords, data, weights) =\u003e {\n  // coords: [x, y, z] of the new vertex\n  // data: [v0, v1, v2, v3] -- up to 4 neighboring vertex data values\n  // weights: [w0, w1, w2, w3] -- interpolation weights (sum to 1.0)\n  return interpolatedData\n})\n```\n\n### Changes from libtess.js\n\nThe core algorithm and `gluTess*` method signatures are identical. If you have working libtess.js code, migration is mostly import changes. Here's what's different:\n\n**Imports and enums:** ESM only, shorter enum names:\n\n```javascript\n// libtess.js\nconst libtess = require('libtess');\nconst tess = new libtess.GluTesselator();\ntess.gluTessCallback(libtess.gluEnum.GLU_TESS_VERTEX, fn);\n\n// libtess-ts\nimport { GluTesselator, GLU_TESS } from 'libtess-ts';\nconst tess = new GluTesselator();\ntess.gluTessCallback(GLU_TESS.VERTEX, fn);\n```\n\n**New exports:**\n\n| Export | Purpose |\n|---|---|\n| `WINDING` | Type-safe winding rule values (`WINDING.ODD`, `WINDING.NONZERO`, etc.) |\n| `GLU_TESS_ERROR` | Error codes passed to the `ERROR` callback (`NEED_COMBINE_CALLBACK`, `COORD_TOO_LARGE`, etc.) |\n| `GL_TRIANGLES`, `GL_LINE_LOOP` | Primitive type constants passed to the `BEGIN` callback |\n\n**New capabilities:**\n\n- `gluTessVertex` accepts `[x, y]` in addition to `[x, y, z]` (z defaults to 0)\n- `compute()`, `renderTriangles()`, and `renderBoundary()` let you run the sweep once and extract multiple output formats (see [separate sweep and render](#separate-sweep-and-render))\n\n## Benchmarks\n\nAll measurements use paired testing with 200 warmup iterations, 500 samples, and Welch's t-test for significance. All runners accumulate output arrays (vertices + elements) for a fair comparison. Run `node benchmarks/benchmark.mjs` to reproduce\n\n### Cold path (new instance per call)\n\n| Workload | libtess.js | libtess-ts | tess2.js | JS vs TS | JS vs T2 |\n|---|---|---|---|---|---|\n| Glyph, 60v | 24 us | 23 us | 30 us | -5% | +23% |\n| Self-intersecting glyph, 224v | 86 us | 78 us | 114 us | -9% | +34% |\n| Star, 1K vertices | 412 us | 399 us | 625 us | -3% | +52% |\n| Star, 7K vertices | 3451 us | 3028 us | 5350 us | -12% | +55% |\n| poly2tri dude, 104v | 44 us | 38 us | 49 us | -14% | +13% |\n| OSM building, 22v | 16 us | 10 us | 15 us | -36% | -3% |\n| OSM NYC z14, 475v | 341 us | 289 us | 550 us | -15% | +61% |\n| Dense intersections, 40v | 1070 us | 919 us | 1287 us | -14% | +20% |\n\n### Warm path (reused instance, libtess.js vs libtess-ts)\n\n| Workload | libtess.js | libtess-ts | Diff | Significance |\n|---|---|---|---|---|\n| Glyph, 60v | 28 us | 19 us | -33% | p \u003c 0.0001 |\n| Self-intersecting glyph, 224v | 92 us | 82 us | -10% | p \u003c 0.0001 |\n| Star, 1K vertices | 434 us | 391 us | -10% | p \u003c 0.0001 |\n| Star, 7K vertices | 4651 us | 3960 us | -15% | p \u003c 0.0001 |\n| poly2tri dude, 104v | 44 us | 33 us | -25% | p \u003c 0.0001 |\n| OSM NYC z14, 475v | 218 us | 187 us | -15% | p \u003c 0.0001 |\n| Dense intersections, 40v | 831 us | 706 us | -15% | p \u003c 0.0001 |\n\nNegative percentages mean the second library is faster\n\n## License\n\n[SGI Free Software License B (Version 2.0)](./LICENSE)\n\nSome test data is derived from [poly2tri](https://github.com/jhasse/poly2tri) (BSD-3-Clause). See [LICENSE_THIRD_PARTY](./LICENSE_THIRD_PARTY)\n\nMaintained by [@jpt](https://github.com/jpt)","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fcountertype%2Flibtess-ts","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fcountertype%2Flibtess-ts","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fcountertype%2Flibtess-ts/lists"}