[
  {
    "path": ".editorconfig",
    "content": "root = true\n\n[*]\nend_of_line = lf\ncharset = utf-8\ntrim_trailing_whitespace = true\ninsert_final_newline = true\nindent_style = tab\ninsert_final_newline = true\n"
  },
  {
    "path": ".github/FUNDING.yml",
    "content": "# These are supported funding model platforms\n\ngithub: gkjohnson\npatreon: # Replace with a single Patreon username\nopen_collective: # Replace with a single Open Collective username\nko_fi: # Replace with a single Ko-fi username\ntidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel\ncommunity_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry\nliberapay: # Replace with a single Liberapay username\nissuehunt: # Replace with a single IssueHunt username\notechie: # Replace with a single Otechie username\ncustom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']\n"
  },
  {
    "path": ".github/workflows/examples-build.yml",
    "content": "name: Deploy Examples to GitHub Pages\n\non:\n  push:\n    branches: [ main ]\n\npermissions:\n  contents: read\n  pages: write\n  id-token: write\n\nconcurrency:\n  group: \"pages\"\n  cancel-in-progress: true\n\njobs:\n  deploy:\n    environment:\n      name: github-pages\n      url: ${{ steps.deployment.outputs.page_url }}\n    runs-on: ubuntu-latest\n    steps:\n    - uses: actions/checkout@v4\n\n    - name: Setup Node.js\n      uses: actions/setup-node@v4\n      with:\n        node-version: 22.x\n        cache: 'npm'\n    - run: npm ci\n    - run: npm run build-examples\n\n    - name: Upload artifact\n      uses: actions/upload-pages-artifact@v3\n      with:\n        path: ./example/dist\n\n    - name: Deploy to GitHub Pages\n      id: deployment\n      uses: actions/deploy-pages@v4\n"
  },
  {
    "path": ".github/workflows/node.js.yml",
    "content": "# This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node\n# For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions\n\nname: Node.js CI\n\non:\n  push:\n    branches: [ \"main\" ]\n  pull_request:\n    branches: [ \"*\" ]\n\njobs:\n  build:\n\n    runs-on: ubuntu-latest\n\n    strategy:\n      matrix:\n        node-version: [24.x]\n\n    steps:\n    - uses: actions/checkout@v2\n    - name: Use Node.js ${{ matrix.node-version }}\n      uses: actions/setup-node@v4\n      with:\n        node-version: ${{ matrix.node-version }}\n        cache: 'npm'\n    - run: npm ci\n    - run: npm run lint\n    - run: npm test\n"
  },
  {
    "path": ".gitignore",
    "content": "# Logs\nlogs\n*.log\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\nlerna-debug.log*\n.pnpm-debug.log*\n\n# Diagnostic reports (https://nodejs.org/api/report.html)\nreport.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json\n\n# Runtime data\npids\n*.pid\n*.seed\n*.pid.lock\n\n# Directory for instrumented libs generated by jscoverage/JSCover\nlib-cov\n\n# Coverage directory used by tools like istanbul\ncoverage\n*.lcov\n\n# nyc test coverage\n.nyc_output\n\n# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)\n.grunt\n\n# Bower dependency directory (https://bower.io/)\nbower_components\n\n# node-waf configuration\n.lock-wscript\n\n# Compiled binary addons (https://nodejs.org/api/addons.html)\nbuild/Release\n\n# Dependency directories\nnode_modules/\njspm_packages/\n\n# Snowpack dependency directory (https://snowpack.dev/)\nweb_modules/\n\n# TypeScript cache\n*.tsbuildinfo\n\n# Optional npm cache directory\n.npm\n\n# Optional eslint cache\n.eslintcache\n\n# Optional stylelint cache\n.stylelintcache\n\n# Microbundle cache\n.rpt2_cache/\n.rts2_cache_cjs/\n.rts2_cache_es/\n.rts2_cache_umd/\n\n# Optional REPL history\n.node_repl_history\n\n# Output of 'npm pack'\n*.tgz\n\n# Yarn Integrity file\n.yarn-integrity\n\n# dotenv environment variable files\n.env\n.env.development.local\n.env.test.local\n.env.production.local\n.env.local\n\n# parcel-bundler cache (https://parceljs.org/)\n.cache\n.parcel-cache\n\n# Next.js build output\n.next\nout\n\n# Nuxt.js build / generate output\n.nuxt\n/dist\n\n# Gatsby files\n.cache/\n# Comment in the public line in if your project uses Gatsby and not Next.js\n# https://nextjs.org/blog/next-9-1#public-directory-support\n# public\n\n# vuepress build output\n.vuepress/dist\n\n# vuepress v2.x temp and cache directory\n.temp\n.cache\n\n# Docusaurus cache and generated files\n.docusaurus\n\n# Serverless directories\n.serverless/\n\n# FuseBox cache\n.fusebox/\n\n# DynamoDB Local files\n.dynamodb/\n\n# TernJS port file\n.tern-port\n\n# Stores VSCode versions used for testing VSCode extensions\n.vscode-test\n\n# yarn v2\n.yarn/cache\n.yarn/unplugged\n.yarn/build-state.yml\n.yarn/install-state.gz\n.pnp.*\n"
  },
  {
    "path": "API.md",
    "content": "<!-- This file is generated automatically. Do not edit it directly. -->\n# three-edge-projection\n\n## Constants\n\n### OUTPUT_MESH\n\n```js\nOUTPUT_MESH: number\n```\n\n### OUTPUT_LINE_SEGMENTS\n\n```js\nOUTPUT_LINE_SEGMENTS: number\n```\n\n### OUTPUT_BOTH\n\n```js\nOUTPUT_BOTH: number\n```\n\n## EdgeSet\n\nSet of projected edges produced by ProjectionGenerator.\n\n\n### .getLineGeometry\n\n```js\ngetLineGeometry( meshes = null: Array<Mesh> | null ): BufferGeometry\n```\n\nReturns a new BufferGeometry representing the edges.\n\nPass a list of meshes in to extract edges from a specific subset of meshes in the given\norder. Returns all edges if null.\n\n\n### .getRangeForMesh\n\n```js\ngetRangeForMesh( mesh: Mesh ): Object | null\n```\n\nReturns the range of vertices associated with the given mesh in the geometry returned from\ngetLineGeometry. The `start` value is only relevant if lines are generated with the default\norder and set of meshes.\n\nCan be used to add extra vertex attributes in a geometry associated with a specific subrange\nof the geometry.\n\n\n## MeshVisibilityCuller\n\nUtility for determining visible geometry from a top down orthographic perspective. This can\nbe run before performing projection generation to reduce the complexity of the operation at\nthe cost of potentially missing small details.\n\nConstructor for the visibility culler that takes the renderer to use for culling.\n\n\n### .pixelsPerMeter\n\n```js\npixelsPerMeter: number\n```\n\nThe size of a pixel on a single dimension. If this results in a texture larger than what\nthe graphics context can provide then the rendering is tiled.\n\n\n### .constructor\n\n```js\nconstructor(\n\trenderer: WebGLRenderer,\n\t{\n\t\tpixelsPerMeter = 0.1: number,\n\t}\n)\n```\n\n### .cull\n\n```js\nasync cull( object: Object3D | Array<Object3D> ): Promise<Array<Object3D>>\n```\n\nReturns the set of meshes that are visible within the given object.\n\n\n## PlanarIntersectionGenerator\n\nUtility for generating the line segments produced by a planar intersection with geometry.\n\n\n### .plane\n\n```js\nplane: Plane\n```\n\nPlane that defaults to y up plane at the origin.\n\n\n### .generate\n\n```js\ngenerate( geometry: MeshBVH | BufferGeometry ): BufferGeometry\n```\n\nGenerates a geometry of the resulting line segments from the planar intersection.\n\n\n## ProjectionGenerator\n\nUtility for generating 2D projections of 3D geometry.\n\n\n### .iterationTime\n\n```js\niterationTime: number\n```\n\nHow long to spend trimming edges before yielding.\n\n\n### .angleThreshold\n\n```js\nangleThreshold: number\n```\n\nThe threshold angle in degrees at which edges are generated.\n\n\n### .includeIntersectionEdges\n\n```js\nincludeIntersectionEdges: boolean\n```\n\nWhether to generate edges representing the intersections between triangles.\n\n\n### .generateAsync\n\n```js\nasync generateAsync(\n\tgeometry: Object3D | BufferGeometry | Array<Object3D>,\n\t{\n\t\tonProgress?: (\n\t\t\tpercent: number,\n\t\t\tmessage: string\n\t\t) => void,\n\t\tsignal?: AbortSignal,\n\t}\n): ProjectionResult\n```\n\nGenerate the geometry with a promise-style API.\n\n\n### .generate\n\n```js\ngenerate(\n\tscene: Object3D | BufferGeometry | Array<Object3D>,\n\t{\n\t\tonProgress?: (\n\t\t\tpercent: number,\n\t\t\tmessage: string\n\t\t) => void,\n\t}\n): ProjectionResult\n```\n\nGenerate the edge geometry result using a generator function.\n\n\n## ProjectionResult\n\nResult object returned by ProjectionGenerator containing visible and hidden edge sets.\n\n\n### .visibleEdges\n\n```js\nvisibleEdges: EdgeSet\n```\n\n\n### .hiddenEdges\n\n```js\nhiddenEdges: EdgeSet\n```\n\n\n## SilhouetteGenerator\n\nUsed for generating a projected silhouette of a geometry using the clipper2-js project. Performing\nthese operations can be extremely slow with more complex geometry and not always yield a stable result.\n\n\n### .iterationTime\n\n```js\niterationTime: number\n```\n\nHow long to spend trimming edges before yielding.\n\n\n### .doubleSided\n\n```js\ndoubleSided: boolean\n```\n\nIf `false` then only the triangles facing upwards are included in the silhouette.\n\n\n### .sortTriangles\n\n```js\nsortTriangles: boolean\n```\n\nWhether to sort triangles and project them large-to-small. In some cases this can cause\nthe performance to drop since the union operation is best performed with smooth, simple\nedge shapes.\n\n\n### .output\n\n```js\noutput: number\n```\n\nWhether to output mesh geometry, line segments geometry, or both in an array\n( `[ mesh, line segments ]` ).\n\n\n### .generateAsync\n\n```js\nasync generateAsync(\n\tgeometry: BufferGeometry,\n\t{\n\t\tonProgress?: (\n\t\t\tpercent: number\n\t\t) => void,\n\t\tsignal?: AbortSignal,\n\t}\n): BufferGeometry | Array<BufferGeometry>\n```\n\nGenerate the silhouette geometry with a promise-style API.\n\n\n### .generate\n\n```js\ngenerate(\n\tgeometry: BufferGeometry,\n\t{\n\t\tonProgress?: (\n\t\t\tpercent: number\n\t\t) => void,\n\t}\n): BufferGeometry | Array<BufferGeometry>\n```\n\nGenerate the geometry using a generator function.\n\n\n# WebGPU API\n\n## MeshVisibilityCuller\n\nUtility for determining visible geometry from a top down orthographic perspective. This can\nbe run before performing projection generation to reduce the complexity of the operation at\nthe cost of potentially missing small details.\n\nTakes the WebGPURenderer instance used to render.\n\n\n### .pixelsPerMeter\n\n```js\npixelsPerMeter: number\n```\n\nThe size of a pixel on a single dimension. If this results in a texture larger than what\nthe graphics context can provide then the rendering is tiled.\n\n\n### .constructor\n\n```js\nconstructor(\n\trenderer: WebGPURenderer,\n\t{\n\t\tpixelsPerMeter = 0.1: number,\n\t}\n)\n```\n\n### .cull\n\n```js\nasync cull( object: Object3D | Array<Object3D> ): Promise<Array<Object3D>>\n```\n\nReturns the set of meshes that are visible within the given object.\n\n\n## ProjectionGenerator\n\nTakes the WebGPURenderer instance used to run compute kernels.\n\n\n### .angleThreshold\n\n```js\nangleThreshold: number\n```\n\nThe threshold angle in degrees at which edges are generated.\n\n\n### .batchSize\n\n```js\nbatchSize: number\n```\n\nThe number of edges to process in one compute kernel pass. Larger values can process\nfaster but may cause internal buffers to overflow, resulting in extra kernel executions,\ntaking more time.\n\n\n### .includeIntersectionEdges\n\n```js\nincludeIntersectionEdges: boolean\n```\n\nWhether to generate edges representing the intersections between triangles.\n\n\n### .iterationTime\n\n```js\niterationTime: number\n```\n\nHow long to spend generating edges.\n\n\n### .parallelJobs\n\n```js\nparallelJobs: number\n```\n\nHow many compute jobs to perform in parallel.\n\n\n### .constructor\n\n```js\nconstructor( renderer: WebGPURenderer )\n```\n\n### .generate\n\n```js\nasync generate(\n\tscene: Object3D | BufferGeometry | Array<Object3D>,\n\t{\n\t\tonProgress?: (\n\t\t\tpercent: number,\n\t\t\tmessage: string\n\t\t) => void,\n\t\tsignal?: AbortSignal,\n\t}\n): Promise<ProjectionResult>\n```\n\nAsynchronously generate the edge geometry result.\n\n"
  },
  {
    "path": "CHANGELOG.md",
    "content": "# Changelog\nAll notable changes to this project will be documented in this file.\n\nThe format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)\nand this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).\n\n## [0.0.9] - 2026.04.18\n### Added\n- Use of \"ReadbackBuffer\" from three.js r184 to improve performance, memory management.\n\n## [0.0.8] - 2026.04.03\n### Added\n- Support for projection matrix transformations.\n\n### Changed\n- Increased time spent per frame on edge generation.\n\n\n## [0.0.7] - 2026.03.31\n### Fixed\n- ProjectionGenerator: Fixed intersection edges not being generated correctly.\n\n## [0.0.6] - 2026.03.31\n### Fixed\n- MeshVisibilityCuller: fix case where the id buffer could be corrupted with separate renders.\n- MeshVisibilityCuller: fix incorrect tiling resulting in incorrect results.\n\n### Changed\n- ProjectionGenerator: Adjust the \"onProgress\" option callback to always take the \"progress\" number as the first argument.\n\n### Added\n- Add a \"three-edge-projection/webgpu\" export including a WebGPURenderer-compatible MeshVsibilityCuller, ProjectionGenerator.\n\n## [0.0.5] - 2025.01.29\n### Fixed\n- Accidental variable conflict.\n- Add support for passing arrays of objects to MeshVisibilityCuller & ProjectionGenerator.\n\n## [0.0.4] - 2025.01.29\n### Changed\n- ProjectionGenerator now returns an object with functions for extracting edges.\n\n### Added\n- Ability to extract hidden edges in addition to visible edges.\n- Optimizations to increase generation speed.\n- Remove requirement to merge geometry ahead of time.\n- A \"MeshVisibilityCuller\" class that can be run to help reduce the number of meshes that need to be processed.\n\n### Removed\n- ProjectionGeneratorWorker\n\n## [0.0.3] - 2025.04.04\n### Added\n- PlanarIntersectionGenerator for generating model cross sections.\n\n## [0.0.2] - 2023.09.30\n### Added\n- SilhouetteGenerator: performance improvements by skipping unnecessary triangles that are determined to already be in the shape.\n- SilhouetteGenerator: Perform simplification of edges.\n- SilhouetteGenerator: Add ability to see outline and mesh edges.\n- ProjectionGenerator: `includeIntersectionEdges` option defaults to true.\n\n## [0.0.1] - 2023.09.18\n### Fixed\n- Some missing edges in projection\n\n### Changed\n- Largely simplified code\n- Migrated logic from three-mesh-bvh\n\n### Added\n- ProjectionGenerator class for generating flattened, projected edges\n- SilhouetteGenerator class for generating flattened, projected silhouette geometry (slow and sometimes unstable)\n- Ability to generate intersection edges for projection with `ProjectionGenerator.includeIntersectionEdges`\n\n"
  },
  {
    "path": "LICENSE",
    "content": "MIT License\n\nCopyright (c) 2023 Garrett Johnson\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "README.md",
    "content": "# three-edge-projection\n\n\n[![build](https://img.shields.io/github/actions/workflow/status/gkjohnson/three-edge-projection/node.js.yml?style=flat-square&label=build&branch=main)](https://github.com/gkjohnson/three-edge-projection/actions)\n[![github](https://flat.badgen.net/badge/icon/github?icon=github&label)](https://github.com/gkjohnson/three-edge-projection/)\n[![twitter](https://flat.badgen.net/badge/twitter/@garrettkjohnson/?icon&label)](https://twitter.com/garrettkjohnson)\n[![sponsors](https://img.shields.io/github/sponsors/gkjohnson?style=flat-square&color=1da1f2)](https://github.com/sponsors/gkjohnson/)\n\n![](./docs/banner.png)\n\nEdge projection based on [three-mesh-bvh](https://github.com/gkjohnson/three-mesh-bvh/) to extract visible projected lines along the y-axis into flattened line segments for scalable 2d rendering. Additonally includes a silhouette mesh generator based on [clipper2-js](https://www.npmjs.com/package/clipper2-js) to merge flattened triangles.\n\n# Examples\n\n[Rover edge projection](https://gkjohnson.github.io/three-edge-projection/edgeProjection.html)\n\n[Lego edge projection](https://gkjohnson.github.io/three-edge-projection/edgeProjection.html#lego)\n\n[Silhouette projection](https://gkjohnson.github.io/three-edge-projection/silhouetteProjection.html)\n\n[Floor plan projection](https://gkjohnson.github.io/three-edge-projection/floorProjection.html)\n\n[Planar intersection](https://gkjohnson.github.io/three-edge-projection/planarIntersection.html)\n\n### WebGPU\n\n[Rover edge projection](https://gkjohnson.github.io/three-edge-projection/edgeProjectionWebGPU.html)\n\n# Installation\n\n```\nnpm install github:@gkjohnson/three-edge-projection\n```\n\n# API\n\nSee [API.md](./API.md) for full API documentation.\n\n# Use\n\n**Generator**\n\nMore granular API with control over when edge trimming work happens.\n\n```js\nconst generator = new ProjectionGenerator();\ngenerator.generate( scene );\n\nlet result = task.next();\nwhile ( ! result.done ) {\n\n\tresult = task.next();\n\n}\n\nconst lines = new LineSegments( result.value.getVisibleLineGeometry(), material );\nscene.add( lines );\n```\n\n**Promise**\n\nSimpler API with less control over when the work happens.\n\n```js\nconst generator = new ProjectionGenerator();\nconst result = await generator.generateAsync( scene );\nconst mesh = new Mesh( result.getVisibleLineGeometry(), material );\nscene.add( mesh );\n```\n\n**Visibility Culling**\n\nTo visibility cull a scene before generation you can use MeshVisibilityCuller before running the projection step.\n\n```js\nconst input = new MeshVisibilityCuller( renderer ).cull( scene );\nconst result = await generator.generateAsync( scene );\nconst mesh = new Mesh( result.getVisibleLineGeometry(), material );\nscene.add( mesh );\n```\n"
  },
  {
    "path": "eslint.config.js",
    "content": "import js from '@eslint/js';\nimport globals from 'globals';\nimport mdcs from 'eslint-config-mdcs';\nimport tseslint from 'typescript-eslint';\nimport vitest from '@vitest/eslint-plugin';\nimport jsdoc from 'eslint-plugin-jsdoc';\n\nexport default [\n\t// files to ignore\n\t{\n\t\tname: 'files to ignore',\n\t\tignores: [\n\t\t\t'**/node_modules/**',\n\t\t\t'**/build/**',\n\t\t\t'**/dist/**',\n\t\t],\n\t},\n\n\t// recommended\n\tjs.configs.recommended,\n\t...tseslint.configs.recommended.map( config => ( {\n\t\t...config,\n\t\tfiles: [ '**/*.ts' ],\n\t} ) ),\n\n\t// base rules\n\t{\n\t\tname: 'base rules',\n\t\tfiles: [ '**/*.js' ],\n\t\tlanguageOptions: {\n\t\t\tecmaVersion: 2022,\n\t\t\tsourceType: 'module',\n\t\t\tglobals: {\n\t\t\t\t...globals.browser,\n\t\t\t\t...globals.node,\n\t\t\t},\n\t\t},\n\t\trules: {\n\t\t\t...mdcs.rules,\n\t\t\t'no-unused-vars': [ 'warn', {\n\t\t\t\tvars: 'all',\n\t\t\t\targs: 'none',\n\t\t\t} ],\n\t\t},\n\t},\n\n\t// ts rule overrides\n\t{\n\t\tname: 'ts rule overrides',\n\t\tfiles: [ '**/*.ts' ],\n\t\trules: {\n\t\t\t'no-undef': 'off',\n\t\t\t'no-unused-vars': 'off',\n\t\t\t'@typescript-eslint/no-unused-vars': [ 'error', { args: 'none' } ],\n\t\t\tindent: [ 'error', 2 ],\n\t\t},\n\t},\n\n\t// jsdoc\n\t{\n\t\tname: 'jsdoc rules',\n\t\tfiles: [ '**/*.js' ],\n\t\tplugins: {\n\t\t\tjsdoc,\n\t\t},\n\t\tsettings: {\n\t\t\tjsdoc: {\n\t\t\t\tpreferredTypes: {\n\t\t\t\t\tAny: 'any',\n\t\t\t\t\tBoolean: 'boolean',\n\t\t\t\t\tNumber: 'number',\n\t\t\t\t\tobject: 'Object',\n\t\t\t\t\tString: 'string',\n\t\t\t\t},\n\t\t\t\ttagNamePreference: {\n\t\t\t\t\treturn: 'returns',\n\t\t\t\t\taugments: 'extends',\n\t\t\t\t\tclassdesc: false,\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\trules: {\n\t\t\t'jsdoc/check-tag-names': [ 'error', { definedTags: [ 'warn', 'note', 'section' ] } ],\n\t\t\t'jsdoc/check-types': 'error',\n\t\t\t'jsdoc/no-undefined-types': 'error',\n\t\t\t'jsdoc/require-param-type': 'error',\n\t\t\t'jsdoc/require-returns-type': 'error',\n\t\t\t'jsdoc/require-returns': 'off',\n\t\t\t'jsdoc/require-param-description': 'off',\n\t\t\t'jsdoc/require-returns-description': 'off',\n\t\t},\n\t},\n\n\t// vitest\n\t{\n\t\tname: 'vitest rules',\n\t\tfiles: [ '**/*.test.js', '**/*.test.ts', '**/*.spec.js', '**/*.spec.ts' ],\n\t\tplugins: {\n\t\t\tvitest,\n\t\t},\n\t\tlanguageOptions: {\n\t\t\tglobals: {\n\t\t\t\t...vitest.environments.env.globals,\n\t\t\t},\n\t\t},\n\t\trules: {\n\t\t\t...vitest.configs.recommended.rules,\n\t\t},\n\t},\n];\n"
  },
  {
    "path": "example/bimProjection.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n\n<head>\n  <meta charset=\"UTF-8\">\n  <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n  <title>Document</title>\n  <style>\n    body {\n      margin: 0;\n      padding: 0;\n      font-family: \"Plus Jakarta Sans\", sans-serif;\n      overflow: hidden;\n    }\n\n    .full-screen {\n      width: 100vw;\n      height: 100vh;\n      position: relative;\n      overflow: hidden;\n    }\n\n    #output {\n      position: fixed;\n      bottom: 0;\n      left: 0;\n      padding: 10px;\n      font-size: 12px;\n      color: #c9c9c9;\n      z-index: 1000;\n    }\n  </style>\n</head>\n\n<body>\n\t<div id=\"output\"></div>\n  <div id=\"container\" class=\"full-screen\"></div>\n  <script type=\"module\" src=\"bimProjection.js\"></script>\n</body>\n\n</html>\n"
  },
  {
    "path": "example/bimProjection.js",
    "content": "import {\n\tQuaternion,\n\tAxesHelper,\n\tGroup,\n\tMeshLambertMaterial,\n\tBufferGeometry,\n\tFloat32BufferAttribute,\n\tMesh,\n\tBox3,\n\tVector3,\n\tPlaneGeometry,\n\tMeshBasicMaterial,\n\tLineBasicMaterial,\n\tLineSegments,\n\tLineDashedMaterial,\n\tMatrix4,\n\tBoxGeometry,\n} from 'three';\nimport { GUI } from 'three/examples/jsm/libs/lil-gui.module.min.js';\nimport { MeshBVH, SAH } from 'three-mesh-bvh';\nimport * as OBC from '@thatopen/components';\nimport * as WEBIFC from 'web-ifc';\nimport { GeometryEngine } from '@thatopen/fragments';\nimport { PlanarIntersectionGenerator } from 'three-edge-projection';\nimport { WebGPURenderer } from 'three/webgpu';\nimport { ProjectionGenerator, MeshVisibilityCuller } from 'three-edge-projection/webgpu';\n\nconst params = {\n\tdisplayModel: true,\n\tdisplayDrawThroughProjection: false,\n\tincludeIntersectionEdges: false,\n\tenableClipping: false,\n\tdisplayClippingEdges: true,\n\trotate: () => {\n\n\t\tconst randomQuaternion = new Quaternion();\n\t\trandomQuaternion.random();\n\n\t\tallMeshes.quaternion.copy( randomQuaternion );\n\t\tallMeshes.position.set( 0, 0, 0 );\n\t\tallMeshes.updateMatrixWorld( true );\n\n\t},\n\tregenerate: () => {\n\n\t\tupdateEdges();\n\n\t},\n};\n\nconst ANGLE_THRESHOLD = 50;\nlet gui;\nlet projection, drawThroughProjection;\nlet outputContainer;\n\n// Initialize a WebGPU renderer for compute (separate from OBC's WebGL display renderer)\nconst gpuRenderer = new WebGPURenderer();\nawait gpuRenderer.init();\n\nconst components = new OBC.Components();\ncomponents.init();\n\nconst container = document.getElementById( 'container' );\nconst world = components.get( OBC.Worlds ).create();\nworld.scene = new OBC.SimpleScene( components );\nworld.renderer = new OBC.SimpleRenderer( components, container );\nworld.camera = new OBC.OrthoPerspectiveCamera( components );\nworld.scene.setup();\nworld.scene.three.add( new AxesHelper() );\n\noutputContainer = document.getElementById( 'output' );\n\n// Initialize GeometryEngine for boolean operations\nconst ifcApi = new WEBIFC.IfcAPI();\nifcApi.SetWasmPath( 'https://unpkg.com/web-ifc@0.0.75/', false );\nawait ifcApi.Init();\nconst geometryEngine = new GeometryEngine( ifcApi );\n\n// init fragments worker\nconst githubUrl = 'https://thatopen.github.io/engine_fragment/resources/worker.mjs';\nconst req = await fetch( githubUrl );\nconst blob = await req.blob();\nconst workerFile = new File( [ blob ], 'worker.mjs', { type: 'text/javascript' } );\nconst workerUrl = URL.createObjectURL( workerFile );\nconst fragments = components.get( OBC.FragmentsManager );\nfragments.init( workerUrl );\n\nworld.camera.controls.addEventListener( 'control', () => {\n\n\tfragments.core.update( true );\n\n} );\n\n// Remove z fighting\nfragments.core.models.materials.list.onItemSet.add( ( { value: material } ) => {\n\n\tif ( ! ( 'isLodMaterial' in material && material.isLodMaterial ) ) {\n\n\t\tmaterial.polygonOffset = true;\n\t\tmaterial.polygonOffsetUnits = 1;\n\t\tmaterial.polygonOffsetFactor = Math.random();\n\n\t}\n\n} );\n\n\nconst model = await loadModel( '/frags/m3d.frag' );\nconst allMeshes = new Group();\n// world.scene.three.add(allMeshes);\n\n// Separate group for clipped results\nconst clippedMeshes = new Group();\n// world.scene.three.add(clippedMeshes);\n\nconst material = new MeshLambertMaterial();\n\n// Add picking meshes (deduplicating geometries to save memory)\nconst idsWithGeometry = await model.getItemsIdsWithGeometry();\nconst allMeshesData = await model.getItemsGeometry( idsWithGeometry );\nconst geometries = new Map();\n\nfor ( const itemId in allMeshesData ) {\n\n\tconst meshData = allMeshesData[ itemId ];\n\tfor ( const geomData of meshData ) {\n\n\t\tif (\n\t\t\t! geomData.positions ||\n\t\t\t! geomData.indices ||\n\t\t\t! geomData.transform ||\n\t\t\t! geomData.representationId\n\t\t) {\n\n\t\t\tcontinue;\n\n\t\t}\n\n\t\tconst representationId = geomData.representationId;\n\t\tif ( ! geometries.has( representationId ) ) {\n\n\t\t\tconst geometry = new BufferGeometry();\n\t\t\tgeometry.setAttribute( 'position', new Float32BufferAttribute( geomData.positions, 3 ) );\n\t\t\tgeometry.setAttribute( 'normal', new Float32BufferAttribute( geomData.normals, 3 ) );\n\t\t\tgeometry.setIndex( Array.from( geomData.indices ) );\n\t\t\tgeometries.set( representationId, geometry );\n\n\t\t}\n\n\t\tconst geometry = geometries.get( representationId );\n\t\tconst mesh = new Mesh( geometry, material );\n\t\tmesh.applyMatrix4( geomData.transform );\n\t\tmesh.applyMatrix4( model.object.matrixWorld );\n\t\tmesh.updateWorldMatrix( true, true );\n\t\tallMeshes.add( mesh );\n\n\t}\n\n}\n\n// initialize BVHs\nallMeshes.traverse( c => {\n\n\tif ( c.geometry && ! c.geometry.boundsTree ) {\n\n\t\tconst elCount = c.geometry.index ? c.geometry.index.count : c.geometry.attributes.position.count;\n\t\tc.geometry.groups.forEach( group => {\n\n\t\t\tif ( group.count === Infinity ) {\n\n\t\t\t\tgroup.count = elCount - group.start;\n\n\t\t\t}\n\n\t\t} );\n\n\t\tc.geometry.boundsTree = new MeshBVH( c.geometry );\n\n\t}\n\n} );\n\n// Compute bounding box of allMeshes\nallMeshes.updateWorldMatrix( true, true );\n\nconst box = new Box3();\nallMeshes.traverse( ( child ) => {\n\n\tif ( child.isMesh && child.geometry ) {\n\n\t\tchild.updateWorldMatrix( false, false );\n\t\tbox.expandByObject( child, true );\n\n\t}\n\n} );\n\nconst size = box.getSize( new Vector3() );\nconst center = box.getCenter( new Vector3() );\n\nconsole.log( 'Model bounds:', box.min.toArray(), box.max.toArray() );\nconsole.log( 'Model size:', size.toArray(), 'center:', center.toArray() );\n\n// Create white ground plane on top of the bounding box (plus 3m offset)\nconst planeHeight = box.max.y + 3;\nconst planeSize = Math.max( size.x, size.z ) * 1.5;\nconst planeGeometry = new PlaneGeometry( planeSize, planeSize );\nconst planeMaterial = new MeshBasicMaterial( {\n\tcolor: 0xffffff,\n\ttransparent: true,\n\topacity: 0.95,\n} );\nconst groundPlane = new Mesh( planeGeometry, planeMaterial );\ngroundPlane.rotation.x = - Math.PI / 2; // Rotate to be horizontal\ngroundPlane.position.set( center.x, planeHeight, center.z );\nworld.scene.three.add( groundPlane );\n\nconst clipper = components.get( OBC.Clipper );\n// const clipNormal = new Vector3(0, 1, 0).applyEuler(new Euler(Math.PI / 2, 0, 0)).applyEuler(new Euler(Math.PI / 4, Math.PI / 4, 0));\n// const planeId = clipper.createFromNormalAndCoplanarPoint(world, new Vector3(0, 1, 0), new Vector3(-50, 50, 0))\n// const plane = clipper.list.get(planeId);\n\n// --- Clipping edge projection ---\nconst clippingEdgeMaterial = new LineBasicMaterial( { color: 0xff0000 } );\nconst clippingEdgesGroup = new Group();\nclippingEdgesGroup.position.y = planeHeight + 0.02;\nworld.scene.three.add( clippingEdgesGroup );\n\nconst intersectingMeshes = new Set();\n\n// create projection display mesh\nconst projectionMaterial = new LineBasicMaterial( { color: 0x888888 } );\nprojection = new LineSegments( new BufferGeometry(), projectionMaterial );\nprojection.position.y = planeHeight + 0.01;\n\ndrawThroughProjection = new LineSegments( new BufferGeometry(), new LineDashedMaterial( { color: 0x444444, dashSize: 0.03, gapSize: 0.03, transparent: true } ) );\ndrawThroughProjection.position.y = planeHeight + 0.01;\ndrawThroughProjection.renderOrder = - 1;\nworld.scene.three.add( projection, drawThroughProjection );\n\ngui = new GUI();\ngui.add( params, 'includeIntersectionEdges' );\ngui.add( params, 'displayDrawThroughProjection' );\ngui.add( params, 'enableClipping' );\ngui.add( params, 'displayClippingEdges' );\ngui.add( params, 'rotate' );\ngui.add( params, 'regenerate' );\n\nworld.renderer.onBeforeUpdate.add( () => {\n\n\tdrawThroughProjection.visible = params.displayDrawThroughProjection;\n\tclippingEdgesGroup.visible = params.displayClippingEdges;\n\n} );\n\nupdateEdges();\n\nasync function loadModel( url ) {\n\n\tconst fetched = await fetch( url );\n\tconst buffer = await fetched.arrayBuffer();\n\n\tconst model = await fragments.core.load( buffer, {\n\t\tmodelId: url,\n\t\tcamera: world.camera.three,\n\t\traw: false,\n\t} );\n\n\tworld.scene.three.add( model.object );\n\t// model.object.rotation.x = Math.PI / 4;\n\t// model.object.rotation.y = Math.PI / 4;\n\tconst now = performance.now();\n\tawait fragments.core.update( true );\n\tconst then = performance.now();\n\tconsole.log( `Time taken: ${ then - now }ms` );\n\n\treturn model;\n\n}\n\nfunction generateClippingEdges() {\n\n\t// Clear previous clipping edges\n\tfor ( const child of [ ...clippingEdgesGroup.children ] ) {\n\n\t\tclippingEdgesGroup.remove( child );\n\t\tif ( child.geometry ) child.geometry.dispose();\n\n\t}\n\n\tconst clipPlane = plane.three;\n\tconst generator = new PlanarIntersectionGenerator();\n\tconst invMatrix = new Matrix4();\n\tconst v = new Vector3();\n\n\t// Ensure world matrices are up to date\n\tallMeshes.updateWorldMatrix( true, true );\n\n\tlet totalSegments = 0;\n\tintersectingMeshes.clear();\n\n\tfor ( const child of allMeshes.children ) {\n\n\t\tif ( ! child.isMesh || ! child.geometry ) continue;\n\n\t\t// Transform clip plane to mesh's local space\n\t\tinvMatrix.copy( child.matrixWorld ).invert();\n\t\tconst localPlane = clipPlane.clone().applyMatrix4( invMatrix );\n\n\t\tgenerator.plane.copy( localPlane );\n\n\t\tconst bvh = child.geometry.boundsTree || child.geometry;\n\t\tconst edgeGeom = generator.generate( bvh );\n\n\t\tconst posAttr = edgeGeom.getAttribute( 'position' );\n\t\tif ( ! posAttr || posAttr.count === 0 ) continue;\n\n\t\t// This mesh actually has triangles crossing the plane\n\t\tintersectingMeshes.add( child );\n\n\t\t// Transform positions back to world space and project (flatten Y)\n\t\tconst positions = posAttr.array;\n\t\tfor ( let i = 0; i < positions.length; i += 3 ) {\n\n\t\t\tv.set( positions[ i ], positions[ i + 1 ], positions[ i + 2 ] );\n\t\t\tv.applyMatrix4( child.matrixWorld );\n\t\t\tpositions[ i ] = v.x;\n\t\t\tpositions[ i + 1 ] = 0;\n\t\t\tpositions[ i + 2 ] = v.z;\n\n\t\t}\n\n\t\tposAttr.needsUpdate = true;\n\n\t\tconst line = new LineSegments( edgeGeom, clippingEdgeMaterial );\n\t\tclippingEdgesGroup.add( line );\n\t\ttotalSegments += posAttr.count / 2;\n\n\t}\n\n\tconsole.log( `Clipping edges: ${totalSegments} line segments from ${clippingEdgesGroup.children.length} meshes (${intersectingMeshes.size} intersecting)` );\n\n}\n\n// --- Boolean clipping ---\nfunction applyClipping() {\n\n\t// Clear previous clipped meshes\n\tconst previous = [ ...clippedMeshes.children ];\n\tfor ( const child of previous ) {\n\n\t\tclippedMeshes.remove( child );\n\t\tif ( child.geometry ) child.geometry.dispose();\n\n\t}\n\n\tconst clipPlane = plane.three;\n\tconst n = clipPlane.normal;\n\n\t// Create clipping box: a large box on the clipped side (above the plane)\n\tconst boxSize = Math.max( size.x, size.y, size.z ) * 4;\n\tconst clipBoxGeom = new BoxGeometry( boxSize, boxSize, boxSize );\n\tconst clipBoxMesh = new Mesh( clipBoxGeom, material );\n\t// Orient and position the box on the negative side of the clip plane\n\tclipBoxMesh.quaternion.setFromUnitVectors( new Vector3( 0, 1, 0 ), n );\n\tconst coplanarPoint = new Vector3();\n\tclipPlane.coplanarPoint( coplanarPoint );\n\tclipBoxMesh.position.copy( coplanarPoint ).addScaledVector( n, - boxSize / 2 );\n\tclipBoxMesh.updateMatrixWorld( true );\n\n\tlet clipped = 0, skipped = 0, errors = 0, kept = 0;\n\n\tfor ( const child of allMeshes.children ) {\n\n\t\tif ( ! child.isMesh || ! child.geometry ) continue;\n\n\t\t// Use the precise intersection test from generateClippingEdges()\n\t\tif ( ! intersectingMeshes.has( child ) ) {\n\n\t\t\t// No triangles cross the plane — check which side mesh center is on\n\t\t\tconst meshCenter = new Box3().setFromObject( child, true ).getCenter( new Vector3() );\n\t\t\tconst dist = clipPlane.distanceToPoint( meshCenter );\n\n\t\t\tif ( dist >= 0 ) {\n\n\t\t\t\t// Positive side — keep as-is\n\t\t\t\tconst keepMesh = new Mesh( child.geometry.clone(), material );\n\t\t\t\tkeepMesh.applyMatrix4( child.matrixWorld );\n\t\t\t\tkeepMesh.updateMatrixWorld( true );\n\t\t\t\tclippedMeshes.add( keepMesh );\n\t\t\t\tkept ++;\n\n\t\t\t} else {\n\n\t\t\t\tskipped ++;\n\n\t\t\t}\n\n\t\t\tcontinue;\n\n\t\t}\n\n\t\t// Actually straddles the clip plane — boolean DIFFERENCE\n\t\ttry {\n\n\t\t\tchild.updateMatrixWorld( true );\n\n\t\t\tconst booleanData = {\n\t\t\t\ttype: 'DIFFERENCE',\n\t\t\t\ttarget: child,\n\t\t\t\toperands: [ clipBoxMesh ],\n\t\t\t};\n\n\t\t\tconst resultGeom = new BufferGeometry();\n\t\t\tgeometryEngine.getBooleanOperation( resultGeom, booleanData );\n\n\t\t\t// Check if result has vertices\n\t\t\tconst posAttr = resultGeom.getAttribute( 'position' );\n\t\t\tif ( ! posAttr || posAttr.count === 0 ) {\n\n\t\t\t\tskipped ++;\n\t\t\t\tcontinue;\n\n\t\t\t}\n\n\t\t\t// Result is in world space, so create mesh with identity transform\n\t\t\tconst resultMesh = new Mesh( resultGeom, material );\n\t\t\tresultMesh.updateMatrixWorld( true );\n\t\t\tclippedMeshes.add( resultMesh );\n\t\t\tclipped ++;\n\n\t\t} catch ( e ) {\n\n\t\t\tconsole.warn( 'Boolean op error:', e );\n\t\t\t// On error, keep the original mesh\n\t\t\tconst fallbackMesh = new Mesh( child.geometry.clone(), material );\n\t\t\tfallbackMesh.applyMatrix4( child.matrixWorld );\n\t\t\tfallbackMesh.updateMatrixWorld( true );\n\t\t\tclippedMeshes.add( fallbackMesh );\n\t\t\terrors ++;\n\n\t\t}\n\n\t}\n\n\tconsole.log( `Boolean clipping: clipped=${clipped}, kept=${kept}, skipped=${skipped}, errors=${errors}, total=${allMeshes.children.length}` );\n\n\t// Build BVHs for clipped meshes\n\tclippedMeshes.traverse( c => {\n\n\t\tif ( c.geometry && ! c.geometry.boundsTree ) {\n\n\t\t\tconst elCount = c.geometry.index ? c.geometry.index.count : c.geometry.attributes.position.count;\n\t\t\tc.geometry.groups.forEach( group => {\n\n\t\t\t\tif ( group.count === Infinity ) {\n\n\t\t\t\t\tgroup.count = elCount - group.start;\n\n\t\t\t\t}\n\n\t\t\t} );\n\t\t\tc.geometry.boundsTree = new MeshBVH( c.geometry, { maxLeafSize: 1, strategy: SAH } );\n\n\t\t}\n\n\t} );\n\n\t// Hide original meshes, show clipped\n\t// allMeshes.visible = false;\n\t// model.object.visible = false;\n\t// clippedMeshes.visible = true;\n\n}\n\nasync function updateEdges() {\n\n\toutputContainer.innerText = 'Generating...';\n\n\t// dispose the geometry\n\tprojection.geometry.dispose();\n\tdrawThroughProjection.geometry.dispose();\n\n\t// initialize an empty geometry\n\tprojection.geometry = new BufferGeometry();\n\tdrawThroughProjection.geometry = new BufferGeometry();\n\n\tconst timeStart = window.performance.now();\n\n\tif ( params.enableClipping ) {\n\n\t\tgenerateClippingEdges();\n\t\tapplyClipping();\n\n\t}\n\n\tconst generator = new ProjectionGenerator( gpuRenderer );\n\tgenerator.angleThreshold = ANGLE_THRESHOLD;\n\tgenerator.includeIntersectionEdges = params.includeIntersectionEdges;\n\n\t// Use clippedMeshes if clipping is enabled, otherwise allMeshes\n\tconst meshSource = params.enableClipping ? clippedMeshes : allMeshes;\n\tlet input = await new MeshVisibilityCuller( gpuRenderer, { pixelsPerMeter: 0.01 } ).cull( meshSource );\n\n\tconst result = await generator.generate( input, {\n\t\tonProgress: p => {\n\n\t\t\toutputContainer.innerText = `Generating... ${ ( p * 100 ).toFixed( 1 ) }%`;\n\n\t\t},\n\t} );\n\n\tdrawThroughProjection.geometry.dispose();\n\tdrawThroughProjection.geometry = result.hiddenEdges.getLineGeometry();\n\tdrawThroughProjection.computeLineDistances();\n\n\tprojection.geometry.dispose();\n\tprojection.geometry = result.visibleEdges.getLineGeometry();\n\tconst trimTime = window.performance.now() - timeStart;\n\n\toutputContainer.innerText = `Generation time: ${trimTime.toFixed( 2 )}ms`;\n\n}\n"
  },
  {
    "path": "example/edgeProjection.html",
    "content": "<!DOCTYPE html>\n<html>\n<head>\n    <title>three-edge-projection - Projected Edge Generation</title>\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no\">\n\n    <style type=\"text/css\">\n        html, body {\n            padding: 0;\n            margin: 0;\n            overflow: hidden;\n\t\t\tfont-family: monospace;\n        }\n\n        canvas {\n            width: 100%;\n            height: 100%;\n        }\n\n        #output {\n            color: #333;\n            position: absolute;\n            left: 10px;\n            bottom: 10px;\n            white-space: pre;\n        }\n\n        #info {\n            position: absolute;\n            top: 0;\n            width: 100%;\n            color: #333;\n            font-family: monospace;\n            text-align: center;\n            padding: 5px 0;\n        }\n    </style>\n</head>\n<body>\n\t<div id=\"info\">\n\t\tAccelerated geometry edge projection and clipping onto<br/>the XZ plane for orthographic vector views.\n\t</div>\n\t<div id=\"output\"></div>\n    <script type=\"module\" src=\"./edgeProjection.js\"></script>\n</body>\n</html>\n"
  },
  {
    "path": "example/edgeProjection.js",
    "content": "import {\n\tBox3,\n\tWebGLRenderer,\n\tScene,\n\tDirectionalLight,\n\tAmbientLight,\n\tGroup,\n\tBufferGeometry,\n\tLineSegments,\n\tLineBasicMaterial,\n\tPerspectiveCamera,\n} from 'three';\nimport { GUI } from 'three/examples/jsm/libs/lil-gui.module.min.js';\nimport { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js';\nimport { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js';\nimport { LDrawLoader } from 'three/examples/jsm/loaders/LDrawLoader.js';\nimport { LDrawConditionalLineMaterial } from 'three/examples/jsm/materials/LDrawConditionalLineMaterial.js';\nimport { MeshoptDecoder } from 'three/examples/jsm/libs/meshopt_decoder.module.js';\nimport { ProjectionGenerator, MeshVisibilityCuller } from '..';\nimport { MeshBVH, SAH } from 'three-mesh-bvh';\n\nconst params = {\n\tdisplayModel: true,\n\tdisplayDrawThroughProjection: false,\n\tincludeIntersectionEdges: true,\n\tvisibilityCullMeshes: false,\n\trotate: () => {\n\n\t\tgroup.quaternion.random();\n\t\tgroup.position.set( 0, 0, 0 );\n\t\tgroup.updateMatrixWorld( true );\n\n\t\tconst box = new Box3();\n\t\tbox.setFromObject( model, true );\n\t\tbox.getCenter( group.position ).multiplyScalar( - 1 );\n\t\tgroup.position.y = Math.max( 0, - box.min.y ) + 1;\n\t\tgroup.updateMatrixWorld( true );\n\n\t\tneedsRender = true;\n\n\t\ttask = updateEdges();\n\n\t},\n\tregenerate: () => {\n\n\t\ttask = updateEdges();\n\n\t},\n};\n\nconst ANGLE_THRESHOLD = 50;\nlet needsRender = false;\nlet renderer, camera, scene, gui, controls;\nlet model, projection, drawThroughProjection, group;\nlet outputContainer;\nlet task = null;\n\ninit();\n\nasync function init() {\n\n\toutputContainer = document.getElementById( 'output' );\n\n\tconst bgColor = 0xeeeeee;\n\n\t// renderer setup\n\trenderer = new WebGLRenderer( { antialias: true } );\n\trenderer.setPixelRatio( window.devicePixelRatio );\n\trenderer.setSize( window.innerWidth, window.innerHeight );\n\trenderer.setClearColor( bgColor, 1 );\n\tdocument.body.appendChild( renderer.domElement );\n\n\t// scene setup\n\tscene = new Scene();\n\n\t// lights\n\tconst light = new DirectionalLight( 0xffffff, 3.5 );\n\tlight.position.set( 1, 2, 3 );\n\tscene.add( light );\n\n\tconst ambientLight = new AmbientLight( 0xb0bec5, 0.5 );\n\tscene.add( ambientLight );\n\n\t// load model\n\tgroup = new Group();\n\tscene.add( group );\n\n\twindow.ROOT = group;\n\n\tif ( window.location.hash === '#lego' ) {\n\n\t\t// init loader\n\t\tconst loader = new LDrawLoader();\n\t\tloader.setConditionalLineMaterial( LDrawConditionalLineMaterial );\n\t\tawait loader.preloadMaterials( 'https://raw.githubusercontent.com/gkjohnson/ldraw-parts-library/master/colors/ldcfgalt.ldr' );\n\n\t\t// load model\n\t\tmodel = await loader\n\t\t\t.setPartsLibraryPath( 'https://raw.githubusercontent.com/gkjohnson/ldraw-parts-library/master/complete/ldraw/' )\n\t\t\t.loadAsync( 'https://raw.githubusercontent.com/mrdoob/three.js/dev/examples/models/ldraw/officialLibrary/models/1621-1-LunarMPVVehicle.mpd_Packed.mpd' );\n\n\t\t// adjust model transforms\n\t\tmodel.scale.setScalar( 0.01 );\n\t\tmodel.rotation.x = Math.PI;\n\n\t\t// remove lines\n\t\tconst toRemove = [];\n\t\tmodel.traverse( c => {\n\n\t\t\tif ( c.isLine ) {\n\n\t\t\t\ttoRemove.push( c );\n\n\t\t\t}\n\n\t\t} );\n\n\t\ttoRemove.forEach( c => {\n\n\t\t\tc.removeFromParent();\n\n\t\t} );\n\n\t} else {\n\n\t\tconst gltf = await new GLTFLoader()\n\t\t\t.setMeshoptDecoder( MeshoptDecoder )\n\t\t\t.loadAsync( 'https://raw.githubusercontent.com/gkjohnson/3d-demo-data/main/models/nasa-m2020/Perseverance.glb' );\n\t\tmodel = gltf.scene;\n\n\t}\n\n\t// initialize BVHs\n\tmodel.traverse( c => {\n\n\t\tif ( c.geometry && ! c.geometry.boundsTree ) {\n\n\t\t\tconst elCount = c.geometry.index ? c.geometry.index.count : c.geometry.attributes.position.count;\n\t\t\tc.geometry.groups.forEach( group => {\n\n\t\t\t\tif ( group.count === Infinity ) {\n\n\t\t\t\t\tgroup.count = elCount - group.start;\n\n\t\t\t\t}\n\n\t\t\t} );\n\n\t\t\tc.geometry.boundsTree = new MeshBVH( c.geometry, { maxLeafSize: 1, strategy: SAH } );\n\n\t\t}\n\n\t} );\n\n\t// center model\n\tconst box = new Box3();\n\tbox.setFromObject( model, true );\n\tbox.getCenter( group.position ).multiplyScalar( - 1 );\n\tgroup.position.y = Math.max( 0, - box.min.y ) + 1;\n\tgroup.add( model );\n\tgroup.updateMatrixWorld( true );\n\n\t// create projection display mesh\n\tprojection = new LineSegments( new BufferGeometry(), new LineBasicMaterial( { color: 0x030303, depthWrite: false } ) );\n\tdrawThroughProjection = new LineSegments( new BufferGeometry(), new LineBasicMaterial( { color: 0xcacaca, depthWrite: false } ) );\n\tdrawThroughProjection.renderOrder = - 1;\n\tscene.add( projection, drawThroughProjection );\n\n\t// camera setup\n\tcamera = new PerspectiveCamera( 75, window.innerWidth / window.innerHeight, 0.01, 1e3 );\n\tcamera.position.setScalar( 3.5 );\n\tcamera.updateProjectionMatrix();\n\n\tneedsRender = true;\n\n\t// controls\n\tcontrols = new OrbitControls( camera, renderer.domElement );\n\tcontrols.addEventListener( 'change', () => {\n\n\t\tneedsRender = true;\n\n\t} );\n\n\tgui = new GUI();\n\tgui.add( params, 'displayModel' ).onChange( () => needsRender = true );\n\tgui.add( params, 'displayDrawThroughProjection' ).onChange( () => needsRender = true );\n\tgui.add( params, 'includeIntersectionEdges' );\n\tgui.add( params, 'visibilityCullMeshes' );\n\tgui.add( params, 'rotate' );\n\tgui.add( params, 'regenerate' ).onChange( () => needsRender = true );\n\n\trender();\n\n\ttask = updateEdges();\n\n\twindow.addEventListener( 'resize', function () {\n\n\t\tcamera.aspect = window.innerWidth / window.innerHeight;\n\t\tcamera.updateProjectionMatrix();\n\n\t\trenderer.setSize( window.innerWidth, window.innerHeight );\n\n\t\tneedsRender = true;\n\n\t}, false );\n\n}\n\nasync function* updateEdges( runTime = 30 ) {\n\n\toutputContainer.innerText = 'Generating...';\n\n\t// dispose the geometry\n\tprojection.geometry.dispose();\n\tdrawThroughProjection.geometry.dispose();\n\n\t// initialize an empty geometry\n\tprojection.geometry = new BufferGeometry();\n\tdrawThroughProjection.geometry = new BufferGeometry();\n\n\tconst timeStart = window.performance.now();\n\tconst generator = new ProjectionGenerator();\n\tgenerator.iterationTime = runTime;\n\tgenerator.angleThreshold = ANGLE_THRESHOLD;\n\tgenerator.includeIntersectionEdges = params.includeIntersectionEdges;\n\n\tlet input = [ model ];\n\tif ( params.visibilityCullMeshes ) {\n\n\t\tinput = await new MeshVisibilityCuller( renderer, { pixelsPerMeter: 0.01 } ).cull( input );\n\n\t}\n\n\tconst collection = yield* generator.generate( input, {\n\t\tonProgress: ( tot, msg, edges ) => {\n\n\t\t\toutputContainer.innerText = msg;\n\t\t\tif ( tot ) outputContainer.innerText += ' ' + ( 100 * tot ).toFixed( 1 ) + '%';\n\n\t\t\tif ( edges ) {\n\n\t\t\t\tprojection.geometry.dispose();\n\t\t\t\tprojection.geometry = edges.visibleEdges.getLineGeometry();\n\t\t\t\tneedsRender = true;\n\n\t\t\t}\n\n\t\t},\n\t} );\n\tdrawThroughProjection.geometry.dispose();\n\tdrawThroughProjection.geometry = collection.hiddenEdges.getLineGeometry();\n\n\tprojection.geometry.dispose();\n\tprojection.geometry = collection.visibleEdges.getLineGeometry();\n\tconst geometry = projection.geometry;\n\tconst trimTime = window.performance.now() - timeStart;\n\n\tprojection.geometry.dispose();\n\tprojection.geometry = geometry;\n\toutputContainer.innerText = `Generation time: ${ trimTime.toFixed( 2 ) }ms`;\n\n\tneedsRender = true;\n\n}\n\n\nfunction render() {\n\n\trequestAnimationFrame( render );\n\n\tif ( task ) {\n\n\t\tconst res = task.next();\n\t\tif ( res.done ) {\n\n\t\t\ttask = null;\n\n\t\t}\n\n\t}\n\n\tmodel.visible = params.displayModel;\n\tdrawThroughProjection.visible = params.displayDrawThroughProjection;\n\n\tif ( needsRender ) {\n\n\t\trenderer.render( scene, camera );\n\t\tneedsRender = false;\n\n\t}\n\n}\n"
  },
  {
    "path": "example/edgeProjectionWebGPU.html",
    "content": "<!DOCTYPE html>\n<html>\n<head>\n    <title>three-edge-projection - WebGPU Projected Edge Generation</title>\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no\">\n\n    <style type=\"text/css\">\n        html, body {\n            padding: 0;\n            margin: 0;\n            overflow: hidden;\n\t\t\tfont-family: monospace;\n        }\n\n        canvas {\n            width: 100%;\n            height: 100%;\n        }\n\n        #output {\n            color: #333;\n            position: absolute;\n            left: 10px;\n            bottom: 10px;\n            white-space: pre;\n        }\n\n        #info {\n            position: absolute;\n            top: 0;\n            width: 100%;\n            color: #333;\n            font-family: monospace;\n            text-align: center;\n            padding: 5px 0;\n        }\n    </style>\n</head>\n<body>\n\t<div id=\"info\">\n\t\tWebGPU accelerated geometry edge projection and clipping onto<br/>the XZ plane for orthographic vector views.\n\t</div>\n\t<div id=\"output\"></div>\n    <script type=\"module\" src=\"./edgeProjectionWebGPU.js\"></script>\n</body>\n</html>\n"
  },
  {
    "path": "example/edgeProjectionWebGPU.js",
    "content": "import {\n\tBox3,\n\tScene,\n\tDirectionalLight,\n\tAmbientLight,\n\tGroup,\n\tBufferGeometry,\n\tBufferAttribute,\n\tLineSegments,\n\tLineBasicMaterial,\n\tPerspectiveCamera,\n\tWebGPURenderer,\n} from 'three/webgpu';\nimport { GUI } from 'three/examples/jsm/libs/lil-gui.module.min.js';\nimport { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js';\nimport { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js';\nimport { MeshoptDecoder } from 'three/examples/jsm/libs/meshopt_decoder.module.js';\nimport { ProjectionGenerator, MeshVisibilityCuller } from 'three-edge-projection/webgpu';\nimport { Color } from 'three';\n\nconst params = {\n\tdisplayModel: true,\n\tdisplayDrawThroughProjection: false,\n\tincludeIntersectionEdges: false,\n\tvisibilityCullMeshes: false,\n\tperObjectColors: false,\n\tregenerate: () => {\n\n\t\tupdateEdges();\n\n\t},\n\trotate: () => {\n\n\t\tgroup.quaternion.random();\n\t\tgroup.position.set( 0, 0, 0 );\n\t\tgroup.updateMatrixWorld( true );\n\n\t\tconst box = new Box3();\n\t\tbox.setFromObject( model, true );\n\t\tbox.getCenter( group.position ).multiplyScalar( - 1 );\n\t\tgroup.position.y = Math.max( 0, - box.min.y ) + 1;\n\t\tgroup.updateMatrixWorld( true );\n\n\t\tneedsRender = true;\n\n\t},\n};\n\nlet needsRender = false;\nlet renderer, camera, scene, gui, controls;\nlet model, projection, drawThroughProjection, group;\nlet outputContainer;\nlet abortController;\n\ninit();\n\nasync function init() {\n\n\toutputContainer = document.getElementById( 'output' );\n\n\tconst bgColor = 0xeeeeee;\n\n\t// renderer setup\n\trenderer = new WebGPURenderer( { antialias: true } );\n\trenderer.setPixelRatio( window.devicePixelRatio );\n\trenderer.setSize( window.innerWidth, window.innerHeight );\n\trenderer.setClearColor( bgColor, 1 );\n\tawait renderer.init();\n\tdocument.body.appendChild( renderer.domElement );\n\n\t// scene setup\n\tscene = new Scene();\n\n\t// lights\n\tconst light = new DirectionalLight( 0xffffff, 3.5 );\n\tlight.position.set( 1, 2, 3 );\n\tscene.add( light );\n\n\tconst ambientLight = new AmbientLight( 0xb0bec5, 0.5 );\n\tscene.add( ambientLight );\n\n\t// load model\n\tgroup = new Group();\n\tscene.add( group );\n\n\tconst gltf = await new GLTFLoader()\n\t\t.setMeshoptDecoder( MeshoptDecoder )\n\t\t.loadAsync( 'https://raw.githubusercontent.com/gkjohnson/3d-demo-data/main/models/nasa-m2020/Perseverance.glb' );\n\tmodel = gltf.scene;\n\n\tconst box = new Box3();\n\tbox.setFromObject( model, true );\n\tbox.getCenter( group.position ).multiplyScalar( - 1 );\n\tgroup.position.y = Math.max( 0, - box.min.y ) + 1;\n\tgroup.add( model );\n\tgroup.updateMatrixWorld( true );\n\n\t// create projection display meshes\n\tprojection = new LineSegments( new BufferGeometry(), new LineBasicMaterial( { depthWrite: false } ) );\n\tdrawThroughProjection = new LineSegments( new BufferGeometry(), new LineBasicMaterial( { depthWrite: false } ) );\n\tdrawThroughProjection.renderOrder = - 1;\n\tscene.add( projection, drawThroughProjection );\n\n\t// camera setup\n\tcamera = new PerspectiveCamera( 75, window.innerWidth / window.innerHeight, 0.01, 1e6 );\n\tcamera.position.setScalar( 3.5 );\n\tcamera.updateProjectionMatrix();\n\n\tneedsRender = true;\n\n\t// controls\n\tcontrols = new OrbitControls( camera, renderer.domElement );\n\tcontrols.addEventListener( 'change', () => {\n\n\t\tneedsRender = true;\n\n\t} );\n\n\tgui = new GUI();\n\tconst displayFolder = gui.addFolder( 'Display' );\n\tdisplayFolder.add( params, 'displayModel' ).onChange( () => needsRender = true );\n\tdisplayFolder.add( params, 'displayDrawThroughProjection' ).onChange( () => needsRender = true );\n\n\tconst projectionFolder = gui.addFolder( 'Projection' );\n\tprojectionFolder.add( params, 'includeIntersectionEdges' );\n\tprojectionFolder.add( params, 'visibilityCullMeshes' );\n\tprojectionFolder.add( params, 'perObjectColors' );\n\tprojectionFolder.add( params, 'rotate' );\n\tprojectionFolder.add( params, 'regenerate' );\n\n\trender();\n\n\tupdateEdges();\n\n\twindow.addEventListener( 'resize', function () {\n\n\t\tcamera.aspect = window.innerWidth / window.innerHeight;\n\t\tcamera.updateProjectionMatrix();\n\n\t\trenderer.setSize( window.innerWidth, window.innerHeight );\n\n\t\tneedsRender = true;\n\n\t}, false );\n\n}\n\nasync function updateEdges() {\n\n\tif ( abortController ) {\n\n\t\tabortController.abort();\n\n\t}\n\n\tabortController = new AbortController();\n\n\tprojection.geometry.dispose();\n\tprojection.material.dispose();\n\tprojection.geometry = new BufferGeometry();\n\n\tdrawThroughProjection.geometry.dispose();\n\tdrawThroughProjection.material.dispose();\n\tdrawThroughProjection.geometry = new BufferGeometry();\n\n\tneedsRender = true;\n\n\tconst timeStart = window.performance.now();\n\tconst generator = new ProjectionGenerator( renderer );\n\tgenerator.includeIntersectionEdges = params.includeIntersectionEdges;\n\n\tmodel.visible = true;\n\tlet input = [ model ];\n\tif ( params.visibilityCullMeshes ) {\n\n\t\tinput = await new MeshVisibilityCuller( renderer, { pixelsPerMeter: 0.1 } ).cull( input );\n\n\t}\n\n\tlet result;\n\ttry {\n\n\t\tresult = await generator.generate( input, {\n\t\t\tsignal: abortController.signal,\n\t\t\tonProgress: ( p, msg ) => {\n\n\t\t\t\toutputContainer.innerText = `${ msg }... ${ ( p * 100 ).toFixed( 2 ) }%`;\n\n\t\t\t},\n\t\t} );\n\n\t} catch {\n\n\t\t// cancelled\n\t\treturn;\n\n\t}\n\n\tconst visGeom = result.visibleEdges.getLineGeometry();\n\tconst hidGeom = result.hiddenEdges.getLineGeometry();\n\tif ( params.perObjectColors ) {\n\n\t\tapplyPerObjectColors( result.visibleEdges, visGeom );\n\t\tapplyPerObjectColors( result.hiddenEdges, hidGeom, 0.8 );\n\n\t}\n\n\tprojection.geometry.dispose();\n\tprojection.material.dispose();\n\tprojection.geometry = visGeom;\n\tprojection.material.vertexColors = params.perObjectColors;\n\tprojection.material.color.set( params.perObjectColors ? 0xffffff : 0x030303 );\n\n\tdrawThroughProjection.geometry.dispose();\n\tdrawThroughProjection.material.dispose();\n\tdrawThroughProjection.geometry = hidGeom;\n\tdrawThroughProjection.material.vertexColors = params.perObjectColors;\n\tdrawThroughProjection.material.color.set( params.perObjectColors ? 0xffffff : 0xcacaca );\n\n\tconst elapsed = window.performance.now() - timeStart;\n\toutputContainer.innerText = `Generation time: ${ elapsed.toFixed( 2 ) }ms`;\n\n\tneedsRender = true;\n\n}\n\nfunction applyPerObjectColors( edgeSet, geometry, lightness = 0.5 ) {\n\n\tconst totalVertices = geometry.attributes.position.count;\n\tconst colorArray = new Float32Array( totalVertices * 3 );\n\tconst color = new Color();\n\n\tfor ( const mesh of edgeSet.meshToSegments.keys() ) {\n\n\t\tconst range = edgeSet.getRangeForMesh( mesh );\n\t\tif ( ! range ) continue;\n\n\t\tcolor.setHSL( Math.random(), 0.75, lightness );\n\n\n\t\tfor ( let i = range.start; i < range.start + range.count; i ++ ) {\n\n\t\t\tcolorArray[ i * 3 + 0 ] = color.r;\n\t\t\tcolorArray[ i * 3 + 1 ] = color.g;\n\t\t\tcolorArray[ i * 3 + 2 ] = color.b;\n\n\t\t}\n\n\t}\n\n\tgeometry.setAttribute( 'color', new BufferAttribute( colorArray, 3 ) );\n\n}\n\nfunction render() {\n\n\trequestAnimationFrame( render );\n\n\tmodel.visible = params.displayModel;\n\tdrawThroughProjection.visible = params.displayDrawThroughProjection;\n\n\tif ( needsRender ) {\n\n\t\trenderer.render( scene, camera );\n\t\tneedsRender = false;\n\n\t}\n\n}\n"
  },
  {
    "path": "example/floorProjection.html",
    "content": "<!DOCTYPE html>\n<html>\n<head>\n    <title>three-edge-projection - Projected Edge Generation</title>\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no\">\n\n    <style type=\"text/css\">\n        html, body {\n            padding: 0;\n            margin: 0;\n            overflow: hidden;\n\t\t\tfont-family: monospace;\n\t\t\tbackground-color: #111;\n        }\n\n        canvas {\n            width: 100%;\n            height: 100%;\n        }\n\n        #output {\n            color: white;\n            position: absolute;\n            left: 10px;\n            bottom: 10px;\n            white-space: pre;\n        }\n\n        #info {\n            position: absolute;\n            top: 0;\n            width: 100%;\n            color: white;\n            font-family: monospace;\n            text-align: center;\n            padding: 5px 0;\n        }\n\n\t\ta {\n\t\t\tcolor: white;\n\t\t}\n    </style>\n</head>\n<body>\n\t<div id=\"info\">\n\t\tFloor plan silhouette and edge projection from <a href=\"https://sketchfab.com/3d-models/3d-floor-plan-of-home-apartment-office-layout-506ed4379b5940a793e652c1b7c0d836\">Sketchfab model</a>.\n\t</div>\n\t<div id=\"output\">loading...</div>\n    <script type=\"module\" src=\"./floorProjection.js\"></script>\n</body>\n</html>\n"
  },
  {
    "path": "example/floorProjection.js",
    "content": "import {\n\tBox3,\n\tWebGLRenderer,\n\tScene,\n\tDirectionalLight,\n\tAmbientLight,\n\tGroup,\n\tBufferGeometry,\n\tLineSegments,\n\tLineBasicMaterial,\n\tPerspectiveCamera,\n\tMeshBasicMaterial,\n\tMesh,\n\tDoubleSide,\n} from 'three';\nimport { MapControls } from 'three/examples/jsm/controls/MapControls.js';\nimport { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js';\nimport { mergeGeometries } from 'three/examples/jsm/utils/BufferGeometryUtils.js';\nimport { MeshoptDecoder } from 'three/examples/jsm/libs/meshopt_decoder.module.js';\nimport { ProjectionGenerator, SilhouetteGenerator } from '../src';\nimport { GUI } from 'three/examples/jsm/libs/lil-gui.module.min.js';\n\nconst ANGLE_THRESHOLD = 50;\nlet renderer, camera, scene, gui, controls;\nlet model, outlines, group, silhouette;\nlet outputContainer;\nlet task = null;\n\nconst params = {\n\tdisplayModel: false,\n\tregenerate: () => {\n\n\t\ttask = updateProjection();\n\n\t},\n};\n\ninit();\n\nasync function init() {\n\n\toutputContainer = document.getElementById( 'output' );\n\n\tconst bgColor = 0x111111;\n\n\t// renderer setup\n\trenderer = new WebGLRenderer( { antialias: true } );\n\trenderer.setPixelRatio( window.devicePixelRatio );\n\trenderer.setSize( window.innerWidth, window.innerHeight );\n\trenderer.setClearColor( bgColor, 1 );\n\tdocument.body.appendChild( renderer.domElement );\n\n\t// scene setup\n\tscene = new Scene();\n\n\t// lights\n\tconst light = new DirectionalLight( 0xffffff, 3.5 );\n\tlight.position.set( 1, 2, 3 );\n\tscene.add( light );\n\n\tconst ambientLight = new AmbientLight( 0xb0bec5, 0.5 );\n\tscene.add( ambientLight );\n\n\t// load model\n\tgroup = new Group();\n\tscene.add( group );\n\n\tconst gltf = await new GLTFLoader()\n\t\t.setMeshoptDecoder( MeshoptDecoder )\n\t\t.loadAsync( 'https://raw.githubusercontent.com/gkjohnson/3d-demo-data/main/models/3d-home-layout/scene.glb' );\n\tmodel = gltf.scene;\n\tgroup.updateMatrixWorld( true );\n\n\t// center model\n\tconst box = new Box3();\n\tbox.setFromObject( model, true );\n\tbox.getCenter( group.position ).multiplyScalar( - 1 );\n\tgroup.position.y = Math.max( 0, - box.min.y ) + 1;\n\tgroup.add( model );\n\tmodel.visible = false;\n\n\t// create projection display mesh\n\tsilhouette = new Mesh( new BufferGeometry(), new MeshBasicMaterial( {\n\t\tcolor: '#eee',\n\t\tpolygonOffset: true,\n\t\tpolygonOffsetFactor: 3,\n\t\tpolygonOffsetUnits: 3,\n\t\tside: DoubleSide,\n\t} ) );\n\toutlines = new LineSegments( new BufferGeometry(), new LineBasicMaterial( { color: 0x030303 } ) );\n\tscene.add( outlines, silhouette );\n\n\t// camera setup\n\tcamera = new PerspectiveCamera( 75, window.innerWidth / window.innerHeight, 0.01, 50 );\n\tcamera.position.setScalar( 5.5 );\n\tcamera.updateProjectionMatrix();\n\n\t// controls\n\tcontrols = new MapControls( camera, renderer.domElement );\n\tcontrols.zoomToCursor = true;\n\tcontrols.maxPolarAngle = Math.PI / 3;\n\n\ttask = updateProjection();\n\n\tgui = new GUI();\n\tgui.add( params, 'displayModel' );\n\tgui.add( params, 'regenerate' );\n\n\trender();\n\n\twindow.addEventListener( 'resize', function () {\n\n\t\tcamera.aspect = window.innerWidth / window.innerHeight;\n\t\tcamera.updateProjectionMatrix();\n\n\t\trenderer.setSize( window.innerWidth, window.innerHeight );\n\n\t}, false );\n\n}\n\nfunction* updateProjection() {\n\n\toutputContainer.innerText = 'processing: --';\n\tsilhouette.visible = false;\n\toutlines.visible = false;\n\n\t// transform and merge geometries to project into a single model\n\tconst geometries = [];\n\tmodel.updateWorldMatrix( true, true );\n\tmodel.traverse( c => {\n\n\t\tif ( c.geometry ) {\n\n\t\t\tconst clone = c.geometry.clone();\n\t\t\tclone.applyMatrix4( c.matrixWorld );\n\t\t\tfor ( const key in clone.attributes ) {\n\n\t\t\t\tif ( key !== 'position' ) {\n\n\t\t\t\t\tclone.deleteAttribute( key );\n\n\t\t\t\t}\n\n\t\t\t}\n\n\t\t\tgeometries.push( clone );\n\n\t\t}\n\n\t} );\n\tconst mergedGeometry = mergeGeometries( geometries, false );\n\n\tyield;\n\n\t// generate the silhouette\n\tlet task, result, generator;\n\tgenerator = new SilhouetteGenerator();\n\tgenerator.sortTriangles = true;\n\ttask = generator.generate( mergedGeometry, {\n\n\t\tonProgress: ( p, data ) => {\n\n\t\t\toutputContainer.innerText = `processing: ${ parseFloat( ( p * 100 ).toFixed( 2 ) ) }%`;\n\t\t\tsilhouette.geometry.dispose();\n\t\t\tsilhouette.geometry = data.getGeometry();\n\t\t\tsilhouette.visible = true;\n\n\t\t},\n\n\t} );\n\n\tresult = task.next();\n\twhile ( ! result.done ) {\n\n\t\tresult = task.next();\n\t\tyield;\n\n\t}\n\n\tsilhouette.geometry.dispose();\n\tsilhouette.geometry = result.value;\n\tsilhouette.visible = true;\n\toutputContainer.innerText = 'generating intersection edges...';\n\n\t// generate the edges\n\tgenerator = new ProjectionGenerator();\n\tgenerator.angleThreshold = ANGLE_THRESHOLD;\n\ttask = generator.generate( mergedGeometry, {\n\n\t\tonProgress: ( p, data ) => {\n\n\t\t\toutputContainer.innerText = `processing: ${ parseFloat( ( p * 100 ).toFixed( 2 ) ) }%`;\n\t\t\toutlines.geometry.dispose();\n\t\t\toutlines.geometry = data.visibleEdges.getLineGeometry();\n\t\t\toutlines.visible = true;\n\n\t\t},\n\n\t} );\n\n\tresult = task.next();\n\twhile ( ! result.done ) {\n\n\t\tresult = task.next();\n\t\tyield;\n\n\t}\n\n\toutlines.geometry.dispose();\n\toutlines.geometry = result.value;\n\toutlines.visible = true;\n\toutputContainer.innerText = '';\n\n}\n\n\nfunction render() {\n\n\trequestAnimationFrame( render );\n\n\tif ( task ) {\n\n\t\tconst res = task.next();\n\t\tif ( res.done ) {\n\n\t\t\ttask = null;\n\n\t\t}\n\n\t}\n\n\tmodel.visible = params.displayModel;\n\trenderer.render( scene, camera );\n\n}\n"
  },
  {
    "path": "example/perspectiveProjection.html",
    "content": "<!DOCTYPE html>\n<html>\n<head>\n    <title>three-edge-projection - Perspective Camera Projection</title>\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no\">\n\n    <style type=\"text/css\">\n        html, body {\n            padding: 0;\n            margin: 0;\n            overflow: hidden;\n\t\t\tfont-family: monospace;\n        }\n\n        canvas {\n            width: 100%;\n            height: 100%;\n        }\n\n        #output {\n            color: #333;\n            position: absolute;\n            left: 10px;\n            bottom: 10px;\n            white-space: pre;\n        }\n\n        #info {\n            position: absolute;\n            top: 0;\n            width: 100%;\n            color: #333;\n            font-family: monospace;\n            text-align: center;\n            padding: 5px 0;\n        }\n    </style>\n</head>\n<body>\n\t<div id=\"info\">\n\t\tPerspective camera edge projection - position the camera then click Generate.\n\t\t<br/>\n\t\tNote that compute shader-based generation can result in artifacts due to floating point precision.\n\t</div>\n\t<div id=\"output\"></div>\n    <script type=\"module\" src=\"./perspectiveProjection.js\"></script>\n</body>\n</html>\n"
  },
  {
    "path": "example/perspectiveProjection.js",
    "content": "import {\n\tBox3,\n\tScene,\n\tDirectionalLight,\n\tAmbientLight,\n\tGroup,\n\tBufferGeometry,\n\tLineSegments,\n\tLineBasicMaterial,\n\tPerspectiveCamera,\n\tWebGPURenderer,\n\tVector3,\n} from 'three/webgpu';\nimport { GUI } from 'three/examples/jsm/libs/lil-gui.module.min.js';\nimport { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js';\nimport { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js';\nimport { MeshoptDecoder } from 'three/examples/jsm/libs/meshopt_decoder.module.js';\nimport { ProjectionGenerator as ProjectionGeneratorCompute } from 'three-edge-projection/webgpu';\nimport { ProjectionGenerator } from 'three-edge-projection';\n\nconst params = {\n\tdisplayModel: true,\n\twebGPU: false,\n\tdisplayDrawThroughProjection: false,\n\tincludeIntersectionEdges: false,\n\tregenerate: () => {\n\n\t\tupdateEdges();\n\n\t},\n};\n\nlet needsRender = false;\nlet renderer, camera, scene, gui, controls;\nlet model, projection, drawThroughProjection, group, projectionGroup;\nlet outputContainer;\nlet abortController;\n\ninit();\n\nasync function init() {\n\n\toutputContainer = document.getElementById( 'output' );\n\n\tconst bgColor = 0xeeeeee;\n\n\t// renderer setup\n\trenderer = new WebGPURenderer( { antialias: true } );\n\trenderer.setPixelRatio( window.devicePixelRatio );\n\trenderer.setSize( window.innerWidth, window.innerHeight );\n\trenderer.setClearColor( bgColor, 1 );\n\tawait renderer.init();\n\tdocument.body.appendChild( renderer.domElement );\n\n\t// scene setup\n\tscene = new Scene();\n\n\t// lights\n\tconst light = new DirectionalLight( 0xffffff, 3.5 );\n\tlight.position.set( 1, 2, 3 );\n\tscene.add( light );\n\n\tconst ambientLight = new AmbientLight( 0xb0bec5, 0.5 );\n\tscene.add( ambientLight );\n\n\t// load model\n\tgroup = new Group();\n\tscene.add( group );\n\n\tconst gltf = await new GLTFLoader()\n\t\t.setMeshoptDecoder( MeshoptDecoder )\n\t\t.loadAsync( 'https://raw.githubusercontent.com/gkjohnson/3d-demo-data/main/models/nasa-m2020/Perseverance.glb' );\n\tmodel = gltf.scene;\n\n\tconst box = new Box3();\n\tbox.setFromObject( model, true );\n\tbox.getCenter( group.position ).multiplyScalar( - 1 );\n\tgroup.position.y = Math.max( 0, - box.min.y );\n\tgroup.add( model );\n\tgroup.updateMatrixWorld( true );\n\n\t// create projection display meshes\n\tprojection = new LineSegments( new BufferGeometry(), new LineBasicMaterial( { depthWrite: false } ) );\n\tdrawThroughProjection = new LineSegments( new BufferGeometry(), new LineBasicMaterial( { depthWrite: false } ) );\n\tdrawThroughProjection.renderOrder = - 1;\n\tprojectionGroup = new Group();\n\tprojectionGroup.add( projection, drawThroughProjection );\n\tscene.add( projectionGroup );\n\n\t// camera setup\n\tcamera = new PerspectiveCamera( 75, window.innerWidth / window.innerHeight, 0.01, 100 );\n\tcamera.position.setScalar( 3.5 );\n\tcamera.updateProjectionMatrix();\n\n\tneedsRender = true;\n\n\t// controls\n\tcontrols = new OrbitControls( camera, renderer.domElement );\n\tcontrols.addEventListener( 'change', () => {\n\n\t\tneedsRender = true;\n\n\t} );\n\n\tgui = new GUI();\n\tconst displayFolder = gui.addFolder( 'Display' );\n\tdisplayFolder.add( params, 'displayModel' ).onChange( () => needsRender = true ).listen();\n\tdisplayFolder.add( params, 'displayDrawThroughProjection' ).onChange( () => needsRender = true );\n\n\tconst generationFolder = gui.addFolder( 'Generation' );\n\tgenerationFolder.add( params, 'webGPU' );\n\tgenerationFolder.add( params, 'includeIntersectionEdges' );\n\tgenerationFolder.add( params, 'regenerate' );\n\n\trender();\n\n\tupdateEdges();\n\n\twindow.addEventListener( 'resize', function () {\n\n\t\tcamera.aspect = window.innerWidth / window.innerHeight;\n\t\tcamera.updateProjectionMatrix();\n\n\t\trenderer.setSize( window.innerWidth, window.innerHeight );\n\n\t\tneedsRender = true;\n\n\t}, false );\n\n}\n\nasync function updateEdges() {\n\n\tif ( abortController ) {\n\n\t\tabortController.abort();\n\n\t}\n\n\tabortController = new AbortController();\n\n\tprojection.geometry.dispose();\n\tprojection.material.dispose();\n\tprojection.geometry = new BufferGeometry();\n\n\tdrawThroughProjection.geometry.dispose();\n\tdrawThroughProjection.material.dispose();\n\tdrawThroughProjection.geometry = new BufferGeometry();\n\n\tneedsRender = true;\n\n\tconst timeStart = window.performance.now();\n\n\t// position the projectionGroup to map NDC output back to a camera-facing plane\n\tconst FWD = new Vector3( 0, 0, - 1 ).transformDirection( camera.matrixWorld );\n\tconst distToCenter = - FWD.dot( camera.position ) + 1.75;\n\tconst _v = new Vector3( 1, 1, 1 ).applyMatrix4( camera.projectionMatrixInverse );\n\t_v.multiplyScalar( distToCenter / _v.z );\n\tprojectionGroup.rotation.copy( camera.rotation ).reorder( 'ZYX' );\n\tprojectionGroup.rotation.x += Math.PI / 2;\n\tprojectionGroup.scale.set( _v.x, 1, _v.y );\n\tprojectionGroup.position.copy( camera.position ).addScaledVector( FWD, distToCenter );\n\n\t// construct the generation group — encodes camera VP matrix so the generator\n\t// projects along the camera's view direction instead of world Y\n\tconst scaleGroup = new Group();\n\tconst perspectiveGroup = new Group();\n\tperspectiveGroup.matrixAutoUpdate = false;\n\tscaleGroup.add( perspectiveGroup );\n\n\tmodel.visible = true;\n\tconst clone = group.clone();\n\tperspectiveGroup.add( clone );\n\n\tclone.matrix\n\t\t.multiplyMatrices( camera.matrixWorldInverse, group.matrixWorld )\n\t\t.decompose( clone.position, clone.quaternion, clone.scale );\n\n\tperspectiveGroup.matrix.copy( camera.projectionMatrix );\n\n\tscaleGroup.scale.x = - 1;\n\tscaleGroup.rotation.x = Math.PI / 2;\n\tscaleGroup.updateMatrixWorld( true );\n\n\t// normalize scale so geometry is in a workable range for the GPU\n\tconst box = new Box3();\n\tbox.setFromObject( perspectiveGroup );\n\tscaleGroup.scale.z = 5 / ( box.max.y - box.min.y );\n\tscaleGroup.position.y = - box.min.y * scaleGroup.scale.z - 0.5;\n\tscaleGroup.updateMatrixWorld( true );\n\n\tconst input = [ clone ];\n\n\tlet result;\n\ttry {\n\n\t\tconst onProgress = ( p, msg ) => {\n\n\t\t\toutputContainer.innerText = `${ msg }... ${ ( p * 100 ).toFixed( 2 ) }%`;\n\n\t\t};\n\n\t\tconst options = {\n\t\t\tsignal: abortController.signal,\n\t\t\tonProgress,\n\t\t};\n\n\t\tif ( params.webGPU ) {\n\n\t\t\tconst generator = new ProjectionGeneratorCompute( renderer );\n\t\t\tgenerator.includeIntersectionEdges = params.includeIntersectionEdges;\n\t\t\tresult = await generator.generate( input, options );\n\n\t\t} else {\n\n\t\t\tconst generator = new ProjectionGenerator();\n\t\t\tgenerator.includeIntersectionEdges = params.includeIntersectionEdges;\n\t\t\tresult = await generator.generateAsync( input, options );\n\n\t\t}\n\n\t} catch {\n\n\t\t// cancelled\n\t\treturn;\n\n\t}\n\n\tconst visGeom = result.visibleEdges.getLineGeometry();\n\tconst hidGeom = result.hiddenEdges.getLineGeometry();\n\n\tprojection.geometry.dispose();\n\tprojection.material.dispose();\n\tprojection.geometry = visGeom;\n\tprojection.material = new LineBasicMaterial( { color: 0x030303, depthWrite: false } );\n\n\tdrawThroughProjection.geometry.dispose();\n\tdrawThroughProjection.material.dispose();\n\tdrawThroughProjection.geometry = hidGeom;\n\tdrawThroughProjection.material = new LineBasicMaterial( { color: 0xcacaca, depthWrite: false } );\n\n\tconst elapsed = window.performance.now() - timeStart;\n\toutputContainer.innerText = `Generation time: ${ elapsed.toFixed( 2 ) }ms`;\n\n\tneedsRender = true;\n\n}\n\nfunction render() {\n\n\trequestAnimationFrame( render );\n\n\tmodel.visible = params.displayModel;\n\tdrawThroughProjection.visible = params.displayDrawThroughProjection;\n\n\tif ( needsRender ) {\n\n\t\trenderer.render( scene, camera );\n\t\tneedsRender = false;\n\n\t}\n\n}\n"
  },
  {
    "path": "example/planarIntersection.html",
    "content": "<!DOCTYPE html>\n<html>\n<head>\n    <title>three-edge-projection - Projected Edge Generation</title>\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no\">\n\n    <style type=\"text/css\">\n        html, body {\n            padding: 0;\n            margin: 0;\n            overflow: hidden;\n\t\t\tfont-family: monospace;\n        }\n\n        canvas {\n            width: 100%;\n            height: 100%;\n        }\n\n        #output {\n            color: #eee;\n            position: absolute;\n            left: 10px;\n            bottom: 10px;\n            white-space: pre;\n        }\n\n        #info {\n            position: absolute;\n            top: 0;\n            width: 100%;\n            color: #eee;\n            font-family: monospace;\n            text-align: center;\n            padding: 5px 0;\n        }\n    </style>\n</head>\n<body>\n\t<div id=\"info\">\n\t\tAccelerated planar edge intersection.\n\t</div>\n\t<div id=\"output\"></div>\n    <script type=\"module\" src=\"./planarIntersection.js\"></script>\n</body>\n</html>\n"
  },
  {
    "path": "example/planarIntersection.js",
    "content": "import {\n\tBox3,\n\tWebGLRenderer,\n\tScene,\n\tDirectionalLight,\n\tAmbientLight,\n\tGroup,\n\tMeshBasicMaterial,\n\tBufferGeometry,\n\tLineSegments,\n\tLineBasicMaterial,\n\tPerspectiveCamera,\n\tMesh,\n\tPlaneGeometry,\n\tDoubleSide,\n} from 'three';\nimport { GUI } from 'three/examples/jsm/libs/lil-gui.module.min.js';\nimport { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js';\nimport { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js';\nimport { mergeGeometries } from 'three/examples/jsm/utils/BufferGeometryUtils.js';\nimport { MeshoptDecoder } from 'three/examples/jsm/libs/meshopt_decoder.module.js';\nimport { PlanarIntersectionGenerator } from '..';\nimport { MeshBVH } from 'three-mesh-bvh';\n\nconst params = {\n\tdisplayModel: true,\n\tplanePosition: 1,\n};\n\nlet renderer, camera, scene, gui, controls, bvh;\nlet model, projection, group, plane;\nlet outputContainer;\n\ninit();\n\nasync function init() {\n\n\toutputContainer = document.getElementById( 'output' );\n\n\tconst bgColor = 0x111111;\n\n\t// renderer setup\n\trenderer = new WebGLRenderer( { antialias: true } );\n\trenderer.setPixelRatio( window.devicePixelRatio );\n\trenderer.setSize( window.innerWidth, window.innerHeight );\n\trenderer.setClearColor( bgColor, 1 );\n\trenderer.clear();\n\tdocument.body.appendChild( renderer.domElement );\n\n\t// scene setup\n\tscene = new Scene();\n\n\t// lights\n\tconst light = new DirectionalLight( 0xffffff, 3.5 );\n\tlight.position.set( 1, 2, 3 );\n\tscene.add( light );\n\n\tconst ambientLight = new AmbientLight( 0xb0bec5, 0.5 );\n\tscene.add( ambientLight );\n\n\t// load model\n\tgroup = new Group();\n\tscene.add( group );\n\n\tconst gltf = await new GLTFLoader()\n\t\t.setMeshoptDecoder( MeshoptDecoder )\n\t\t.loadAsync( 'https://raw.githubusercontent.com/gkjohnson/3d-demo-data/main/models/nasa-m2020/Perseverance.glb' );\n\tmodel = gltf.scene;\n\n\t// generate the merged geometry\n\tconst geometries = [];\n\tmodel.updateWorldMatrix( true, true );\n\tmodel.traverse( c => {\n\n\t\tif ( c.geometry ) {\n\n\t\t\tconst clone = c.geometry.clone();\n\t\t\tclone.applyMatrix4( c.matrixWorld );\n\t\t\tfor ( const key in clone.attributes ) {\n\n\t\t\t\tif ( key !== 'position' ) {\n\n\t\t\t\t\tclone.deleteAttribute( key );\n\n\t\t\t\t}\n\n\t\t\t}\n\n\t\t\tgeometries.push( clone );\n\n\t\t}\n\n\t} );\n\tconst mergedGeometry = mergeGeometries( geometries, false );\n\tbvh = new MeshBVH( mergedGeometry, { maxLeafSize: 1 } );\n\n\t// center model\n\tconst box = new Box3();\n\tbox.setFromObject( model, true );\n\tbox.getCenter( group.position ).multiplyScalar( - 1 );\n\tgroup.position.y = Math.max( 0, - box.min.y ) + 1;\n\tgroup.add( model );\n\n\t// create plane to display the cut location\n\tplane = new Mesh( new PlaneGeometry( 5, 5 ), new MeshBasicMaterial( { color: 0x333333, transparent: true, opacity: 0.5, side: DoubleSide } ) );\n\tplane.rotation.x = - Math.PI / 2;\n\tgroup.add( plane );\n\n\t// create projection display mesh\n\tprojection = new LineSegments( new BufferGeometry(), new LineBasicMaterial( { color: 0xeeeeee } ) );\n\tprojection.scale.y = 0;\n\tscene.add( projection );\n\n\t// camera setup\n\tcamera = new PerspectiveCamera( 75, window.innerWidth / window.innerHeight, 0.01, 50 );\n\tcamera.position.setScalar( 3.5 );\n\tcamera.updateProjectionMatrix();\n\n\t// controls\n\tcontrols = new OrbitControls( camera, renderer.domElement );\n\n\tupdateLines();\n\n\tgui = new GUI();\n\tgui.add( params, 'displayModel' );\n\tgui.add( params, 'planePosition', 0, 2.5 ).onChange( () => updateLines() );\n\trender();\n\n\twindow.addEventListener( 'resize', function () {\n\n\t\tcamera.aspect = window.innerWidth / window.innerHeight;\n\t\tcamera.updateProjectionMatrix();\n\n\t\trenderer.setSize( window.innerWidth, window.innerHeight );\n\n\t}, false );\n\n}\n\nfunction updateLines() {\n\n\tprojection.geometry.dispose();\n\n\tconst generator = new PlanarIntersectionGenerator();\n\tgenerator.plane.constant = - params.planePosition;\n\n\tlet start, delta;\n\tstart = performance.now();\n\tprojection.geometry = generator.generate( bvh );\n\tdelta = performance.now() - start;\n\n\toutputContainer.innerText = `${ delta.toFixed( 2 ) }ms`;\n\n}\n\nfunction render() {\n\n\trequestAnimationFrame( render );\n\n\tgroup.visible = params.displayModel;\n\tprojection.visible = params.displayProjection;\n\tplane.position.y = params.planePosition;\n\trenderer.render( scene, camera );\n\n}\n"
  },
  {
    "path": "example/silhouetteProjection.html",
    "content": "<!DOCTYPE html>\n<html>\n<head>\n    <title>three-edge-projection - Projected Edge Generation</title>\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no\">\n\n    <style type=\"text/css\">\n        html, body {\n            padding: 0;\n            margin: 0;\n            overflow: hidden;\n\t\t\tfont-family: monospace;\n        }\n\n        canvas {\n            width: 100%;\n            height: 100%;\n        }\n\n        #output {\n            color: #333;\n            position: absolute;\n            left: 10px;\n            bottom: 10px;\n            white-space: pre;\n        }\n\n        #info {\n            position: absolute;\n            top: 0;\n            width: 100%;\n            color: #333;\n            font-family: monospace;\n            text-align: center;\n            padding: 5px 0;\n        }\n    </style>\n</head>\n<body>\n\t<div id=\"info\">\n\t\tProjected silhouette generation using the \"clipper2-js\" package.\n\t</div>\n\t<div id=\"output\"></div>\n    <script type=\"module\" src=\"./silhouetteProjection.js\"></script>\n</body>\n</html>\n"
  },
  {
    "path": "example/silhouetteProjection.js",
    "content": "import {\n\tBox3,\n\tWebGLRenderer,\n\tScene,\n\tDirectionalLight,\n\tAmbientLight,\n\tGroup,\n\tMeshStandardMaterial,\n\tMeshBasicMaterial,\n\tPerspectiveCamera,\n\tMesh,\n\tTorusKnotGeometry,\n\tDoubleSide,\n\tLineSegments,\n\tLineBasicMaterial,\n} from 'three';\nimport { GUI } from 'three/examples/jsm/libs/lil-gui.module.min.js';\nimport { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js';\nimport { mergeGeometries } from 'three/examples/jsm/utils/BufferGeometryUtils.js';\nimport { OUTPUT_BOTH, SilhouetteGenerator } from '../src';\nimport { SilhouetteGeneratorWorker } from '../src/worker/SilhouetteGeneratorWorker.js';\n\nconst params = {\n\tdisplaySilhouette: true,\n\tdisplayWireframe: false,\n\tdisplayOutline: false,\n\tdisplayModel: true,\n\tuseWorker: false,\n\trotate: () => {\n\n\t\tgroup.quaternion.random();\n\t\tgroup.position.set( 0, 0, 0 );\n\t\tgroup.updateMatrixWorld( true );\n\n\t\tconst box = new Box3();\n\t\tbox.setFromObject( model, true );\n\t\tbox.getCenter( group.position ).multiplyScalar( - 1 );\n\t\tgroup.position.y = Math.max( 0, - box.min.y ) + 1;\n\n\t},\n\tregenerate: () => {\n\n\t\ttask = updateEdges();\n\n\t},\n};\n\nlet renderer, camera, scene, gui, controls;\nlet model, projection, projectionWireframe, group, edges;\nlet outputContainer;\nlet worker;\nlet task = null;\n\ninit();\n\nasync function init() {\n\n\toutputContainer = document.getElementById( 'output' );\n\n\tconst bgColor = 0xeeeeee;\n\n\t// renderer setup\n\trenderer = new WebGLRenderer( { antialias: true } );\n\trenderer.setPixelRatio( window.devicePixelRatio );\n\trenderer.setSize( window.innerWidth, window.innerHeight );\n\trenderer.setClearColor( bgColor, 1 );\n\tdocument.body.appendChild( renderer.domElement );\n\n\t// scene setup\n\tscene = new Scene();\n\n\t// lights\n\tconst light = new DirectionalLight( 0xffffff, 3.5 );\n\tlight.position.set( 1, 2, 3 );\n\tscene.add( light );\n\n\tconst ambientLight = new AmbientLight( 0xb0bec5, 0.5 );\n\tscene.add( ambientLight );\n\n\t// load model\n\tgroup = new Group();\n\tgroup.position.y = 2;\n\tscene.add( group );\n\n\tmodel = new Mesh( new TorusKnotGeometry( 1, 0.4, 120, 30 ), new MeshStandardMaterial( {\n\t\tpolygonOffset: true,\n\t\tpolygonOffsetFactor: 1,\n\t\tpolygonOffsetUnits: 1,\n\t} ) );\n\tmodel.rotation.set( Math.PI / 4, 0, Math.PI / 8 );\n\tgroup.add( model );\n\n\t// create projection display mesh\n\tprojection = new Mesh( undefined, new MeshBasicMaterial( {\n\t\tcolor: 0xf06292,\n\t\tside: DoubleSide,\n\t\tpolygonOffset: true,\n\t\tpolygonOffsetFactor: 1,\n\t\tpolygonOffsetUnits: 1,\n\t} ) );\n\tprojection.position.y = - 2;\n\tscene.add( projection );\n\n\tedges = new LineSegments( undefined, new LineBasicMaterial( { color: 0 } ) );\n\tedges.position.y = - 2;\n\tscene.add( edges );\n\n\tprojectionWireframe = new Mesh( undefined, new MeshBasicMaterial( { color: 0xc2185b, wireframe: true } ) );\n\tprojectionWireframe.position.y = - 2;\n\tscene.add( projectionWireframe );\n\n\t// camera setup\n\tcamera = new PerspectiveCamera( 75, window.innerWidth / window.innerHeight, 0.01, 50 );\n\tcamera.position.setScalar( 4.5 );\n\tcamera.updateProjectionMatrix();\n\n\t// controls\n\tcontrols = new OrbitControls( camera, renderer.domElement );\n\n\tgui = new GUI();\n\tgui.add( params, 'displayModel' );\n\tgui.add( params, 'displaySilhouette' );\n\tgui.add( params, 'displayOutline' );\n\tgui.add( params, 'displayWireframe' );\n\tgui.add( params, 'useWorker' );\n\tgui.add( params, 'rotate' );\n\tgui.add( params, 'regenerate' );\n\n\tworker = new SilhouetteGeneratorWorker();\n\n\ttask = updateEdges();\n\n\trender();\n\n\twindow.addEventListener( 'resize', function () {\n\n\t\tcamera.aspect = window.innerWidth / window.innerHeight;\n\t\tcamera.updateProjectionMatrix();\n\n\t\trenderer.setSize( window.innerWidth, window.innerHeight );\n\n\t}, false );\n\n}\n\nfunction* updateEdges( runTime = 30 ) {\n\n\toutputContainer.innerText = 'processing: --';\n\n\t// transform and merge geometries to project into a single model\n\tlet timeStart = window.performance.now();\n\tconst geometries = [];\n\tmodel.updateWorldMatrix( true, true );\n\tmodel.traverse( c => {\n\n\t\tif ( c.geometry ) {\n\n\t\t\tconst clone = c.geometry.clone();\n\t\t\tclone.applyMatrix4( c.matrixWorld );\n\t\t\tfor ( const key in clone.attributes ) {\n\n\t\t\t\tif ( key !== 'position' ) {\n\n\t\t\t\t\tclone.deleteAttribute( key );\n\n\t\t\t\t}\n\n\t\t\t}\n\n\t\t\tgeometries.push( clone );\n\n\t\t}\n\n\t} );\n\tconst mergedGeometry = mergeGeometries( geometries, false );\n\tconst mergeTime = window.performance.now() - timeStart;\n\n\tyield;\n\n\t// generate the candidate edges\n\ttimeStart = window.performance.now();\n\n\tlet result = null;\n\tif ( ! params.useWorker ) {\n\n\t\tconst generator = new SilhouetteGenerator();\n\t\tgenerator.iterationTime = runTime;\n\t\tgenerator.output = OUTPUT_BOTH;\n\t\tconst task = generator.generate( mergedGeometry, {\n\n\t\t\tonProgress: ( p, info ) => {\n\n\t\t\t\toutputContainer.innerText = `processing: ${ parseFloat( ( p * 100 ).toFixed( 2 ) ) }%`;\n\n\t\t\t\tconst result = info.getGeometry();\n\t\t\t\tprojection.geometry.dispose();\n\t\t\t\tprojection.geometry = result[ 0 ];\n\t\t\t\tprojectionWireframe.geometry = result[ 0 ];\n\n\t\t\t\tedges.geometry.dispose();\n\t\t\t\tedges.geometry = result[ 1 ];\n\n\t\t\t\tif ( params.displaySilhouette || params.displayWireframe || params.displayOutline ) {\n\n\t\t\t\t\tprojection.geometry.dispose();\n\t\t\t\t\tprojection.geometry = result[ 0 ];\n\t\t\t\t\tprojectionWireframe.geometry = result[ 0 ];\n\n\t\t\t\t\tedges.geometry.dispose();\n\t\t\t\t\tedges.geometry = result[ 1 ];\n\n\t\t\t\t}\n\n\t\t\t},\n\n\t\t} );\n\n\t\tlet res = task.next();\n\t\twhile ( ! res.done ) {\n\n\t\t\tres = task.next();\n\t\t\tyield;\n\n\t\t}\n\n\t\tresult = res.value;\n\n\t} else {\n\n\t\tworker\n\t\t\t.generate( mergedGeometry, {\n\t\t\t\toutput: OUTPUT_BOTH,\n\t\t\t\tonProgress: p => {\n\n\t\t\t\t\toutputContainer.innerText = `processing: ${ parseFloat( ( p * 100 ).toFixed( 2 ) ) }%`;\n\n\t\t\t\t},\n\t\t\t} )\n\t\t\t.then( res => {\n\n\t\t\t\tresult = res;\n\n\t\t\t} );\n\n\t\twhile ( result === null ) {\n\n\t\t\tyield;\n\n\t\t}\n\n\t}\n\n\tconst trimTime = window.performance.now() - timeStart;\n\tprojection.geometry.dispose();\n\tprojection.geometry = result[ 0 ];\n\tprojectionWireframe.geometry = result[ 0 ];\n\n\tedges.geometry.dispose();\n\tedges.geometry = result[ 1 ];\n\n\toutputContainer.innerText =\n\t\t`merge geometry  : ${ mergeTime.toFixed( 2 ) }ms\\n` +\n\t\t`edge trimming   : ${ trimTime.toFixed( 2 ) }ms\\n` +\n\t\t`triangles       : ${ projection.geometry.index.count / 3 } tris`;\n\n}\n\nfunction render() {\n\n\trequestAnimationFrame( render );\n\n\tif ( task ) {\n\n\t\tconst res = task.next();\n\t\tif ( res.done ) {\n\n\t\t\ttask = null;\n\n\t\t}\n\n\t}\n\n\tmodel.visible = params.displayModel;\n\tprojection.visible = params.displaySilhouette;\n\tprojectionWireframe.visible = params.displayWireframe;\n\tedges.visible = params.displayOutline;\n\trenderer.render( scene, camera );\n\n}\n"
  },
  {
    "path": "package.json",
    "content": "{\n  \"name\": \"three-edge-projection\",\n  \"version\": \"0.0.9\",\n  \"description\": \"\",\n  \"type\": \"module\",\n  \"main\": \"src/index.js\",\n  \"exports\": {\n    \".\": {\n      \"import\": \"./src/index.js\"\n    },\n    \"./worker\": \"./src/worker/index.js\",\n    \"./webgpu\": \"./src/webgpu/index.js\",\n    \"./src/*\": \"./src/*\"\n  },\n  \"scripts\": {\n    \"start\": \"vite --config ./vite.config.js\",\n    \"build-examples\": \"vite build --config ./vite.config.js\",\n    \"docs:build\": \"node utils/docs/build.js\",\n    \"lint\": \"eslint .\",\n    \"test\": \"vitest run\"\n  },\n  \"files\": [\n    \"src/*\"\n  ],\n  \"keywords\": [\n    \"graphics\",\n    \"tree\",\n    \"bounds\",\n    \"threejs\",\n    \"three-js\",\n    \"bounds-hierarchy\",\n    \"performance\",\n    \"geometry\",\n    \"mesh\",\n    \"acceleration\",\n    \"projection\",\n    \"edges\"\n  ],\n  \"author\": \"Garrett Johnson <garrett.kjohnson@gmail.com>\",\n  \"license\": \"MIT\",\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"git+https://github.com/gkjohnson/three-edge-projection.git\"\n  },\n  \"bugs\": {\n    \"url\": \"https://github.com/gkjohnson/three-edge-projection/issues\"\n  },\n  \"homepage\": \"https://github.com/gkjohnson/three-edge-projection#readme\",\n  \"peerDependencies\": {\n    \"clipper2-js\": \"^0.9.0\",\n    \"three\": \"^0.155.0\",\n    \"three-mesh-bvh\": \"^0.6.0\"\n  },\n  \"devDependencies\": {\n    \"@eslint/js\": \"^9.0.0\",\n    \"@thatopen/components\": \"^3.3.3\",\n    \"@thatopen/fragments\": \"^3.3.7\",\n    \"@vitest/eslint-plugin\": \"^1.1.22\",\n    \"eslint\": \"^9.0.0\",\n    \"eslint-config-mdcs\": \"^5.0.0\",\n    \"eslint-plugin-jsdoc\": \"^62.8.1\",\n    \"globals\": \"^16.5.0\",\n    \"jsdoc\": \"^4.0.5\",\n    \"three\": \">=0.184.0\",\n    \"three-mesh-bvh\": \">=0.9.9\",\n    \"typescript-eslint\": \"^8.48.1\",\n    \"vite\": \"^6.2.2\",\n    \"vitest\": \"^3.0.0\",\n    \"web-ifc\": \"^0.0.77\"\n  }\n}\n"
  },
  {
    "path": "src/EdgeGenerator.js",
    "content": "import { Vector3, Matrix4 } from 'three';\nimport { MeshBVH } from 'three-mesh-bvh';\nimport { generateEdges } from './utils/generateEdges.js';\nimport { generateIntersectionEdges } from './utils/generateIntersectionEdges.js';\nimport { getAllMeshes } from './utils/getAllMeshes.js';\nimport { nextFrame } from './utils/nextFrame.js';\n\nconst _BtoA = /* @__PURE__ */ new Matrix4();\n\n// Class for generating edges for use with the projection generator. Functions take geometries or\n// Object3D instances. If an Object3D is passed then lines for all child meshes will be generated\n// in world space\n// TODO:\n// - add support for progress functions\nexport class EdgeGenerator {\n\n\tconstructor() {\n\n\t\tthis.projectionDirection = new Vector3( 0, 1, 0 );\n\t\tthis.thresholdAngle = 50;\n\t\tthis.iterationTime = 30;\n\t\tthis.yOffset = 1e-6;\n\n\t}\n\n\t// Functions for generating the \"hard\" and silhouette edges of the geometry along the projection direction\n\tgetEdges( ...args ) {\n\n\t\tconst currIterationTime = this.iterationTime;\n\t\tthis.iterationTime = Infinity;\n\n\t\tconst result = this.getEdgesGenerator( ...args ).next().value;\n\t\tthis.iterationTime = currIterationTime;\n\n\t\treturn result;\n\n\t}\n\n\tasync getEdgesAsync( ...args ) {\n\n\t\tconst task = this.getEdgesGenerator( ...args );\n\t\tlet res;\n\t\twhile ( ! res || ! res.done ) {\n\n\t\t\tres = task.next();\n\t\t\tawait nextFrame();\n\n\t\t}\n\n\t\treturn res.value;\n\n\t}\n\n\t*getEdgesGenerator( geometry, resultEdges = [] ) {\n\n\t\t// handle arrays\n\t\tif ( Array.isArray( geometry ) ) {\n\n\t\t\tfor ( let i = 0, l = geometry.length; i < l; i ++ ) {\n\n\t\t\t\tyield* this.getEdgesGenerator( geometry[ i ], resultEdges );\n\n\t\t\t}\n\n\t\t\treturn resultEdges;\n\n\t\t}\n\n\t\tconst { projectionDirection, thresholdAngle, iterationTime, yOffset } = this;\n\t\tif ( geometry.isObject3D ) {\n\n\t\t\tconst meshes = getAllMeshes( geometry );\n\t\t\tlet time = performance.now();\n\t\t\tfor ( let i = 0; i < meshes.length; i ++ ) {\n\n\t\t\t\tif ( performance.now() - time > iterationTime ) {\n\n\t\t\t\t\tyield;\n\n\t\t\t\t}\n\n\t\t\t\tconst mesh = meshes[ i ];\n\t\t\t\tconst results = yield* generateEdges( mesh.geometry, [], {\n\t\t\t\t\tmatrix: mesh.matrixWorld,\n\t\t\t\t\tthresholdAngle: thresholdAngle,\n\t\t\t\t\titerationTime: iterationTime,\n\t\t\t\t} );\n\n\t\t\t\ttransformEdges( results, mesh.matrixWorld, yOffset );\n\n\t\t\t\t// push the edges individually to avoid stack overflow\n\t\t\t\tfor ( let i = 0; i < results.length; i ++ ) {\n\n\t\t\t\t\tresults[ i ].mesh = mesh;\n\t\t\t\t\tresultEdges.push( results[ i ] );\n\n\t\t\t\t}\n\n\t\t\t}\n\n\t\t\treturn resultEdges;\n\n\t\t} else {\n\n\t\t\treturn yield* generateEdges( geometry, resultEdges, {\n\t\t\t\tprojectionDirection: projectionDirection,\n\t\t\t\tthresholdAngle: thresholdAngle,\n\t\t\t\titerationTime: iterationTime,\n\t\t\t} );\n\n\t\t}\n\n\t}\n\n\t// Functions for generating a set of \"intersection\" edges within an existing geometry\n\t// TODO: these needs to support generating \"intersection edges\" within a set of other geometries, as well\n\tgetIntersectionEdges( ...args ) {\n\n\t\tconst currIterationTime = this.iterationTime;\n\t\tthis.iterationTime = Infinity;\n\n\t\tconst result = this.getIntersectionEdgesGenerator( ...args ).next().value;\n\t\tthis.iterationTime = currIterationTime;\n\n\t\treturn result;\n\n\t}\n\n\tasync getIntersectionEdgesAsync( ...args ) {\n\n\t\tconst task = this.getIntersectionEdgesGenerator( ...args );\n\t\tlet res;\n\t\twhile ( ! res || ! res.done ) {\n\n\t\t\tres = task.next();\n\t\t\tawait nextFrame();\n\n\t\t}\n\n\t\treturn res.value;\n\n\t}\n\n\t*getIntersectionEdgesGenerator( geometry, resultEdges = [] ) {\n\n\t\t// handle arrays\n\t\tif ( Array.isArray( geometry ) ) {\n\n\t\t\tfor ( let i = 0, l = geometry.length; i < l; i ++ ) {\n\n\t\t\t\tyield* this.getIntersectionEdgesGenerator( geometry[ i ], resultEdges );\n\n\t\t\t}\n\n\t\t\treturn resultEdges;\n\n\t\t}\n\n\t\tconst { iterationTime, yOffset } = this;\n\t\tif ( geometry.isObject3D ) {\n\n\t\t\t// get the bounds trees from all geometry\n\t\t\tconst meshes = getAllMeshes( geometry );\n\t\t\tconst bvhs = new Map();\n\t\t\tlet time = performance.now();\n\t\t\tfor ( let i = 0; i < meshes.length; i ++ ) {\n\n\t\t\t\tif ( performance.now() - time > iterationTime ) {\n\n\t\t\t\t\tyield;\n\t\t\t\t\ttime = performance.now();\n\n\t\t\t\t}\n\n\t\t\t\tconst mesh = meshes[ i ];\n\t\t\t\tconst geometry = mesh.geometry;\n\t\t\t\tif ( ! bvhs.has( geometry ) ) {\n\n\t\t\t\t\tconst bvh = geometry.boundsTree || new MeshBVH( geometry, { maxLeafSize: 1 } );\n\t\t\t\t\tbvhs.set( geometry, bvh );\n\n\t\t\t\t}\n\n\t\t\t}\n\n\t\t\t// check each mesh against all others\n\t\t\ttime = performance.now();\n\t\t\tfor ( let i = 0; i < meshes.length; i ++ ) {\n\n\t\t\t\tfor ( let j = i; j < meshes.length; j ++ ) {\n\n\t\t\t\t\tif ( performance.now() - time > iterationTime ) {\n\n\t\t\t\t\t\tyield;\n\t\t\t\t\t\ttime = performance.now();\n\n\t\t\t\t\t}\n\n\t\t\t\t\tconst meshA = meshes[ i ];\n\t\t\t\t\tconst meshB = meshes[ j ];\n\t\t\t\t\tconst bvhA = bvhs.get( meshA.geometry );\n\t\t\t\t\tconst bvhB = bvhs.get( meshB.geometry );\n\n\t\t\t\t\t// A-1 * B * v\n\t\t\t\t\t_BtoA\n\t\t\t\t\t\t.copy( meshA.matrixWorld )\n\t\t\t\t\t\t.invert()\n\t\t\t\t\t\t.multiply( meshB.matrixWorld );\n\n\t\t\t\t\tconst results = generateIntersectionEdges( bvhA, bvhB, _BtoA, [], { iterationTime } );\n\t\t\t\t\ttransformEdges( results, meshA.matrixWorld, yOffset );\n\n\t\t\t\t\t// push the edges individually to avoid stack overflow\n\t\t\t\t\tfor ( let i = 0; i < results.length; i ++ ) {\n\n\t\t\t\t\t\tresults[ i ].mesh = meshA;\n\t\t\t\t\t\tresultEdges.push( results[ i ] );\n\n\t\t\t\t\t}\n\n\n\t\t\t\t}\n\n\t\t\t}\n\n\t\t\treturn resultEdges;\n\n\t\t} else {\n\n\t\t\tlet bvh;\n\t\t\tif ( geometry.isBufferGeometry ) {\n\n\t\t\t\tbvh = geometry.boundsTree || new MeshBVH( geometry, { maxLeafSize: 1 } );\n\n\t\t\t} else {\n\n\t\t\t\tbvh = geometry;\n\t\t\t\tgeometry = bvh.geometry;\n\n\t\t\t}\n\n\t\t\t_BtoA.identity();\n\t\t\treturn generateIntersectionEdges( bvh, bvh, _BtoA, resultEdges, { iterationTime } );\n\n\t\t}\n\n\t}\n\n}\n\n// add an offset to avoid precision errors when detecting intersections and clipping\nfunction transformEdges( list, matrix, offset = 0 ) {\n\n\tfor ( let i = 0; i < list.length; i ++ ) {\n\n\t\tconst line = list[ i ];\n\t\tline.applyMatrix4( matrix );\n\t\tline.start.y += offset;\n\t\tline.end.y += offset;\n\n\t}\n\n}\n"
  },
  {
    "path": "src/MeshVisibilityCuller.js",
    "content": "/** @import { WebGLRenderer, Object3D } from 'three' */\nimport {\n\tShaderMaterial,\n\tGLSL3,\n\tWebGLRenderTarget,\n\tBox3,\n\tVector3,\n\tVector4,\n\tOrthographicCamera,\n\tColor,\n\tMesh,\n\tNoBlending,\n} from 'three';\nimport { getAllMeshes } from './utils/getAllMeshes.js';\n\n// RGBA8 ID encoding - supports up to 16,777,215 objects (2^24 - 1)\n// ID 0 is valid, background is indicated by alpha = 0\nfunction encodeId( id, target ) {\n\n\ttarget.x = ( id & 0xFF ) / 255;\n\ttarget.y = ( ( id >> 8 ) & 0xFF ) / 255;\n\ttarget.z = ( ( id >> 16 ) & 0xFF ) / 255;\n\ttarget.w = 1;\n\n}\n\nfunction decodeId( buffer, index ) {\n\n\treturn buffer[ index ] | ( buffer[ index + 1 ] << 8 ) | ( buffer[ index + 2 ] << 16 );\n\n}\n\n// TODO: WebGPU or occlusion queries would let us accelerate this. Ideally would we \"contract\" the depth buffer by one pixel by\n// taking the lowest value from all surrounding pixels in order to avoid mesh misses.\n/**\n * Utility for determining visible geometry from a top down orthographic perspective. This can\n * be run before performing projection generation to reduce the complexity of the operation at\n * the cost of potentially missing small details.\n *\n * Constructor for the visibility culler that takes the renderer to use for culling.\n * @param {WebGLRenderer} renderer\n * @param {Object} [options]\n * @param {number} [options.pixelsPerMeter=0.1]\n */\nexport class MeshVisibilityCuller {\n\n\tconstructor( renderer, options = {} ) {\n\n\t\tconst { pixelsPerMeter = 0.1 } = options;\n\n\t\t/**\n\t\t * The size of a pixel on a single dimension. If this results in a texture larger than what\n\t\t * the graphics context can provide then the rendering is tiled.\n\t\t * @type {number}\n\t\t */\n\t\tthis.pixelsPerMeter = pixelsPerMeter;\n\t\tthis.renderer = renderer;\n\n\t}\n\n\t/**\n\t * Returns the set of meshes that are visible within the given object.\n\t * @param {Object3D|Array<Object3D>} object\n\t * @returns {Promise<Array<Object3D>>}\n\t */\n\tasync cull( objects ) {\n\n\t\tobjects = getAllMeshes( objects );\n\n\t\tconst { renderer, pixelsPerMeter } = this;\n\t\tconst size = new Vector3();\n\t\tconst camera = new OrthographicCamera();\n\t\tconst box = new Box3();\n\t\tconst idMesh = new Mesh( undefined, new IDMaterial() );\n\t\tidMesh.matrixAutoUpdate = false;\n\t\tidMesh.matrixWorldAutoUpdate = false;\n\n\t\tconst target = new WebGLRenderTarget( 1, 1 );\n\n\t\t// get the bounds of the image\n\t\tbox.makeEmpty();\n\t\tobjects.forEach( o => {\n\n\t\t\tbox.expandByObject( o );\n\n\t\t} );\n\n\t\t// get the bounds dimensions\n\t\tbox.getSize( size );\n\n\t\t// calculate the tile and target size\n\t\tconst maxTextureSize = Math.min( renderer.capabilities.maxTextureSize, 2 ** 13 );\n\t\tconst pixelWidth = Math.ceil( size.x / pixelsPerMeter );\n\t\tconst pixelHeight = Math.ceil( size.z / pixelsPerMeter );\n\t\tconst tilesX = Math.ceil( pixelWidth / maxTextureSize );\n\t\tconst tilesY = Math.ceil( pixelHeight / maxTextureSize );\n\n\t\ttarget.setSize( Math.ceil( pixelWidth / tilesX ), Math.ceil( pixelHeight / tilesY ) );\n\n\t\t// set the camera bounds\n\t\tcamera.rotation.x = - Math.PI / 2;\n\t\tcamera.far = ( box.max.y - box.min.y ) + camera.near;\n\t\tcamera.position.y = box.max.y + camera.near;\n\n\t\t// save render state\n\t\tconst color = renderer.getClearColor( new Color() );\n\t\tconst alpha = renderer.getClearAlpha();\n\t\tconst renderTarget = renderer.getRenderTarget();\n\t\tconst autoClear = renderer.autoClear;\n\n\t\t// render ids\n\t\tconst readBuffer = new Uint8Array( target.width * target.height * 4 );\n\t\tconst visibleSet = new Set();\n\t\tconst stepX = size.x / tilesX;\n\t\tconst stepZ = size.z / tilesY;\n\t\tfor ( let x = 0; x < tilesX; x ++ ) {\n\n\t\t\tfor ( let y = 0; y < tilesY; y ++ ) {\n\n\t\t\t\t// update camera\n\t\t\t\tcamera.left = box.min.x + stepX * x;\n\t\t\t\tcamera.top = - ( box.min.z + stepZ * y );\n\n\t\t\t\tcamera.right = camera.left + stepX;\n\t\t\t\tcamera.bottom = camera.top - stepZ;\n\n\t\t\t\tcamera.updateProjectionMatrix();\n\n\t\t\t\t// clear the camera\n\t\t\t\trenderer.autoClear = false;\n\t\t\t\trenderer.setClearColor( 0, 0 );\n\t\t\t\trenderer.setRenderTarget( target );\n\t\t\t\trenderer.clear();\n\n\t\t\t\tfor ( let i = 0; i < objects.length; i ++ ) {\n\n\t\t\t\t\tconst object = objects[ i ];\n\t\t\t\t\tidMesh.matrixWorld.copy( object.matrixWorld );\n\t\t\t\t\tidMesh.geometry = object.geometry;\n\n\t\t\t\t\tidMesh.material.objectId = i;\n\t\t\t\t\trenderer.render( idMesh, camera );\n\n\t\t\t\t}\n\n\t\t\t\t// reset render state before async operation to avoid corruption\n\t\t\t\trenderer.setClearColor( color, alpha );\n\t\t\t\trenderer.setRenderTarget( renderTarget );\n\t\t\t\trenderer.autoClear = autoClear;\n\n\t\t\t\tconst buffer = await renderer.readRenderTargetPixelsAsync( target, 0, 0, target.width, target.height, readBuffer );\n\n\t\t\t\t// find all visible objects - decode RGBA to ID\n\t\t\t\tfor ( let i = 0, l = buffer.length; i < l; i += 4 ) {\n\n\t\t\t\t\t// alpha = 0 indicates background (no object)\n\t\t\t\t\tif ( buffer[ i + 3 ] === 0 ) continue;\n\n\t\t\t\t\tconst id = decodeId( buffer, i );\n\t\t\t\t\tvisibleSet.add( objects[ id ] );\n\n\t\t\t\t}\n\n\t\t\t}\n\n\t\t}\n\n\t\t// dispose of intermediate values\n\t\tidMesh.material.dispose();\n\t\ttarget.dispose();\n\n\t\treturn Array.from( visibleSet );\n\n\t}\n\n}\n\n\nclass IDMaterial extends ShaderMaterial {\n\n\tset objectId( v ) {\n\n\t\tencodeId( v, this.uniforms.objectId.value );\n\n\t}\n\n\tconstructor( params ) {\n\n\t\tsuper( {\n\n\t\t\tglslVersion: GLSL3,\n\t\t\tblending: NoBlending,\n\n\t\t\tuniforms: {\n\t\t\t\tobjectId: { value: new Vector4() },\n\t\t\t},\n\n\t\t\tvertexShader: /* glsl */`\n\t\t\t\tvoid main() {\n\n\t\t\t\t\tgl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );\n\n\t\t\t\t}\n\t\t\t`,\n\n\t\t\tfragmentShader: /* glsl */`\n\t\t\t\tlayout(location = 0) out vec4 out_id;\n\t\t\t\tuniform vec4 objectId;\n\n\t\t\t\tvoid main() {\n\n\t\t\t\t\tout_id = objectId;\n\n\t\t\t\t}\n\t\t\t`,\n\n\t\t} );\n\n\t\tthis.setValues( params );\n\n\t}\n\n}\n"
  },
  {
    "path": "src/PlanarIntersectionGenerator.js",
    "content": "import { BufferAttribute, BufferGeometry, Line3, Plane, Vector3 } from 'three';\nimport { MeshBVH } from 'three-mesh-bvh';\n\nconst _line = new Line3();\nconst _target = new Line3();\nconst _vec = new Vector3();\nconst EPS = 1e-16;\n\n/**\n * Utility for generating the line segments produced by a planar intersection with geometry.\n */\nexport class PlanarIntersectionGenerator {\n\n\tconstructor() {\n\n\t\t/**\n\t\t * Plane that defaults to y up plane at the origin.\n\t\t * @type {Plane}\n\t\t */\n\t\tthis.plane = new Plane( new Vector3( 0, 1, 0 ), 0 );\n\n\t}\n\n\t/**\n\t * Generates a geometry of the resulting line segments from the planar intersection.\n\t * @param {MeshBVH|BufferGeometry} geometry\n\t * @returns {BufferGeometry}\n\t */\n\tgenerate( bvh ) {\n\n\t\tconst { plane } = this;\n\t\tif ( bvh instanceof BufferGeometry ) {\n\n\t\t\tbvh = new MeshBVH( bvh, { maxLeafSize: 1 } );\n\n\t\t}\n\n\t\tconst edgesArray = [];\n\t\tbvh.shapecast( {\n\n\t\t\tintersectsBounds: box => {\n\n\t\t\t\treturn plane.intersectsBox( box );\n\n\t\t\t},\n\n\t\t\tintersectsTriangle: tri => {\n\n\t\t\t\tconst { points } = tri;\n\t\t\t\tlet foundPoints = 0;\n\t\t\t\tfor ( let i = 0; i < 3; i ++ ) {\n\n\t\t\t\t\tconst ni = ( i + 1 ) % 3;\n\t\t\t\t\t_line.start.copy( points[ i ] );\n\t\t\t\t\t_line.end.copy( points[ ni ] );\n\n\t\t\t\t\tif ( plane.intersectLine( _line, _vec ) ) {\n\n\t\t\t\t\t\tif ( foundPoints === 1 ) {\n\n\t\t\t\t\t\t\tif ( _vec.distanceTo( _target.start ) > EPS ) {\n\n\t\t\t\t\t\t\t\t_target.end.copy( _vec );\n\t\t\t\t\t\t\t\tfoundPoints ++;\n\t\t\t\t\t\t\t\tbreak;\n\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t} else {\n\n\t\t\t\t\t\t\t_target.start.copy( _vec );\n\t\t\t\t\t\t\tfoundPoints ++;\n\n\t\t\t\t\t\t}\n\n\t\t\t\t\t}\n\n\t\t\t\t}\n\n\t\t\t\tif ( foundPoints === 2 ) {\n\n\t\t\t\t\tedgesArray.push( ..._target.start, ..._target.end );\n\n\t\t\t\t}\n\n\t\t\t},\n\n\t\t} );\n\n\t\t// generate and return line geometry\n\t\tconst edgeGeom = new BufferGeometry();\n\t\tconst edgeBuffer = new BufferAttribute( new Float32Array( edgesArray ), 3, true );\n\t\tedgeGeom.setAttribute( 'position', edgeBuffer );\n\t\treturn edgeGeom;\n\n\t}\n\n}\n"
  },
  {
    "path": "src/ProjectionGenerator.js",
    "content": "/** @import { Object3D } from 'three' */\nimport {\n\tBufferGeometry,\n\tVector3,\n\tBufferAttribute,\n\tMesh,\n} from 'three';\nimport { MeshBVH, SAH } from 'three-mesh-bvh';\nimport { isYProjectedLineDegenerate } from './utils/triangleLineUtils.js';\nimport { overlapsToLines } from './utils/overlapUtils.js';\nimport { EdgeGenerator } from './EdgeGenerator.js';\nimport { LineObjectsBVH } from './utils/LineObjectsBVH.js';\nimport { bvhcastEdges } from './utils/bvhcastEdges.js';\nimport { getAllMeshes } from './utils/getAllMeshes.js';\nimport { nextFrame } from './utils/nextFrame.js';\n\nconst UP_VECTOR = /* @__PURE__ */ new Vector3( 0, 1, 0 );\n\nfunction toLineGeometry( edges, ranges = null ) {\n\n\t// if no ranges provided, treat the whole array as one range\n\tconst activeRanges = ranges ?? [ { start: 0, count: edges.length } ];\n\n\tlet totalCount = 0;\n\tfor ( let i = 0; i < activeRanges.length; i ++ ) {\n\n\t\ttotalCount += activeRanges[ i ].count;\n\n\t}\n\n\tconst edgeArray = new Float32Array( totalCount * 6 );\n\tlet c = 0;\n\tfor ( let r = 0; r < activeRanges.length; r ++ ) {\n\n\t\tconst { start, count } = activeRanges[ r ];\n\t\tfor ( let i = start, l = start + count; i < l; i ++ ) {\n\n\t\t\tconst line = edges[ i ];\n\t\t\tedgeArray[ c ++ ] = line[ 0 ];\n\t\t\tedgeArray[ c ++ ] = 0;\n\t\t\tedgeArray[ c ++ ] = line[ 2 ];\n\t\t\tedgeArray[ c ++ ] = line[ 3 ];\n\t\t\tedgeArray[ c ++ ] = 0;\n\t\t\tedgeArray[ c ++ ] = line[ 5 ];\n\n\t\t}\n\n\t}\n\n\tconst edgeGeom = new BufferGeometry();\n\tconst edgeBuffer = new BufferAttribute( edgeArray, 3, false );\n\tedgeGeom.setAttribute( 'position', edgeBuffer );\n\treturn edgeGeom;\n\n}\n\n/**\n * Set of projected edges produced by ProjectionGenerator.\n */\nexport class EdgeSet {\n\n\tconstructor() {\n\n\t\tthis.meshToSegments = new Map();\n\t\tthis._rangeCache = null;\n\n\t}\n\n\t/**\n\t * Returns a new BufferGeometry representing the edges.\n\t *\n\t * Pass a list of meshes in to extract edges from a specific subset of meshes in the given\n\t * order. Returns all edges if null.\n\t * @param {Array<Mesh>|null} [meshes=null]\n\t * @returns {BufferGeometry}\n\t */\n\tgetLineGeometry( meshes = null ) {\n\n\t\tconst activeMeshes = meshes !== null ? meshes : Array.from( this.meshToSegments.keys() );\n\t\tconst segments = [];\n\t\tfor ( let i = 0; i < activeMeshes.length; i ++ ) {\n\n\t\t\tconst segs = this.meshToSegments.get( activeMeshes[ i ] );\n\t\t\tif ( segs ) {\n\n\t\t\t\tfor ( let j = 0; j < segs.length; j ++ ) segments.push( segs[ j ] );\n\n\t\t\t}\n\n\t\t}\n\n\t\treturn toLineGeometry( segments );\n\n\t}\n\n\t/**\n\t * Returns the range of vertices associated with the given mesh in the geometry returned from\n\t * getLineGeometry. The `start` value is only relevant if lines are generated with the default\n\t * order and set of meshes.\n\t *\n\t * Can be used to add extra vertex attributes in a geometry associated with a specific subrange\n\t * of the geometry.\n\t * @param {Mesh} mesh\n\t * @returns {{ start: number, count: number }|null}\n\t */\n\tgetRangeForMesh( mesh ) {\n\n\t\tif ( ! this._rangeCache ) {\n\n\t\t\tthis._rangeCache = new Map();\n\t\t\tlet start = 0;\n\t\t\tfor ( const [ m, segs ] of this.meshToSegments ) {\n\n\t\t\t\tthis._rangeCache.set( m, { start: start * 2, count: segs.length * 2 } );\n\t\t\t\tstart += segs.length;\n\n\t\t\t}\n\n\t\t}\n\n\t\treturn this._rangeCache.get( mesh ) ?? null;\n\n\t}\n\n}\n\n/**\n * Result object returned by ProjectionGenerator containing visible and hidden edge sets.\n */\nexport class ProjectionResult {\n\n\tconstructor() {\n\n\t\t/** @type {EdgeSet} */\n\t\tthis.visibleEdges = new EdgeSet();\n\n\t\t/** @type {EdgeSet} */\n\t\tthis.hiddenEdges = new EdgeSet();\n\n\t}\n\n}\n\nclass ProjectedEdgeCollector {\n\n\tconstructor( scene ) {\n\n\t\tthis.meshes = getAllMeshes( scene );\n\t\tthis.bvhs = new Map();\n\t\tthis.result = new ProjectionResult();\n\t\tthis.iterationTime = 30;\n\n\t}\n\n\taddEdges( ...args ) {\n\n\t\tconst currIterationTime = this.iterationTime;\n\t\tthis.iterationTime = Infinity;\n\n\t\tconst result = this.addEdgesGenerator( ...args ).next().value;\n\t\tthis.iterationTime = currIterationTime;\n\n\t\treturn result;\n\n\t}\n\n\t// all edges are expected to be in world coordinates\n\t*addEdgesGenerator( edges, options = {} ) {\n\n\t\tconst { meshes, bvhs, iterationTime } = this;\n\t\tlet time = performance.now();\n\t\tfor ( let i = 0; i < meshes.length; i ++ ) {\n\n\t\t\tif ( performance.now() - time > iterationTime ) {\n\n\t\t\t\tyield;\n\t\t\t\ttime = performance.now();\n\n\t\t\t}\n\n\t\t\tconst mesh = meshes[ i ];\n\t\t\tconst geometry = mesh.geometry;\n\t\t\tif ( ! bvhs.has( geometry ) ) {\n\n\t\t\t\tconst bvh = geometry.boundsTree || new MeshBVH( geometry );\n\t\t\t\tbvhs.set( geometry, bvh );\n\n\t\t\t}\n\n\t\t}\n\n\t\t// initialize hidden line object\n\t\tconst hiddenOverlapMap = {};\n\t\tfor ( let i = 0; i < edges.length; i ++ ) {\n\n\t\t\thiddenOverlapMap[ i ] = [];\n\n\t\t}\n\n\t\t// construct bvh\n\t\tconst edgesBvh = new LineObjectsBVH( edges, { maxLeafSize: 2, strategy: SAH } );\n\n\t\ttime = performance.now();\n\t\tfor ( let m = 0; m < meshes.length; m ++ ) {\n\n\t\t\tif ( performance.now() - time > iterationTime ) {\n\n\t\t\t\tif ( options.onProgress ) {\n\n\t\t\t\t\toptions.onProgress( m, meshes.length );\n\n\t\t\t\t}\n\n\t\t\t\tyield;\n\t\t\t\ttime = performance.now();\n\n\t\t\t}\n\n\t\t\t// use bvhcast to compare all edges against all meshes\n\t\t\tconst mesh = meshes[ m ];\n\t\t\tbvhcastEdges( edgesBvh, bvhs.get( mesh.geometry ), mesh, hiddenOverlapMap );\n\n\t\t}\n\n\t\t// construct the projections\n\t\tconst { result } = this;\n\t\tfor ( let i = 0; i < edges.length; i ++ ) {\n\n\t\t\tif ( performance.now() - time > iterationTime ) {\n\n\t\t\t\tyield;\n\t\t\t\ttime = performance.now();\n\n\t\t\t}\n\n\t\t\t// convert the overlap points to proper lines\n\t\t\tconst line = edges[ i ];\n\t\t\tconst mesh = line.mesh;\n\t\t\tconst hiddenOverlaps = hiddenOverlapMap[ i ];\n\n\t\t\tif ( ! result.visibleEdges.meshToSegments.has( mesh ) ) {\n\n\t\t\t\tresult.visibleEdges.meshToSegments.set( mesh, [] );\n\t\t\t\tresult.hiddenEdges.meshToSegments.set( mesh, [] );\n\n\t\t\t}\n\n\t\t\toverlapsToLines( line, hiddenOverlaps, false, result.visibleEdges.meshToSegments.get( mesh ) );\n\t\t\toverlapsToLines( line, hiddenOverlaps, true, result.hiddenEdges.meshToSegments.get( mesh ) );\n\n\t\t}\n\n\t}\n\n}\n\n/**\n * @callback ProjectionProgressCallback\n * @param {number} percent\n * @param {string} message\n */\n\n/**\n * Utility for generating 2D projections of 3D geometry.\n */\nexport class ProjectionGenerator {\n\n\tconstructor() {\n\n\t\t/**\n\t\t * How long to spend trimming edges before yielding.\n\t\t * @type {number}\n\t\t */\n\t\tthis.iterationTime = 30;\n\n\t\t/**\n\t\t * The threshold angle in degrees at which edges are generated.\n\t\t * @type {number}\n\t\t */\n\t\tthis.angleThreshold = 50;\n\n\t\t/**\n\t\t * Whether to generate edges representing the intersections between triangles.\n\t\t * @type {boolean}\n\t\t */\n\t\tthis.includeIntersectionEdges = true;\n\n\t}\n\n\t/**\n\t * Generate the geometry with a promise-style API.\n\t * @async\n\t * @param {Object3D|BufferGeometry|Array<Object3D>} geometry\n\t * @param {Object} [options]\n\t * @param {ProjectionProgressCallback} [options.onProgress]\n\t * @param {AbortSignal} [options.signal]\n\t * @returns {ProjectionResult}\n\t */\n\tasync generateAsync( geometry, options = {} ) {\n\n\t\tconst { signal } = options;\n\t\tconst task = this.generate( geometry, options );\n\t\tlet res;\n\t\twhile ( ! res || ! res.done ) {\n\n\t\t\tres = task.next();\n\t\t\tawait nextFrame();\n\n\t\t\tsignal.throwIfAborted();\n\n\t\t}\n\n\t\treturn res.value;\n\n\t}\n\n\t/**\n\t * Generate the edge geometry result using a generator function.\n\t * @param {Object3D|BufferGeometry|Array<Object3D>} scene\n\t * @param {Object} [options]\n\t * @param {ProjectionProgressCallback} [options.onProgress]\n\t * @yields {void}\n\t * @returns {ProjectionResult}\n\t */\n\t*generate( scene, options = {} ) {\n\n\t\tconst { iterationTime, angleThreshold, includeIntersectionEdges } = this;\n\t\tconst { onProgress = () => {} } = options;\n\n\t\tif ( scene.isBufferGeometry ) {\n\n\t\t\tscene = new Mesh( scene );\n\n\t\t}\n\n\t\tconst edgeGenerator = new EdgeGenerator();\n\t\tedgeGenerator.iterationTime = iterationTime;\n\t\tedgeGenerator.thresholdAngle = angleThreshold;\n\t\tedgeGenerator.projectionDirection.copy( UP_VECTOR );\n\n\t\tonProgress( 0, 'Extracting edges' );\n\t\tlet edges = [];\n\t\tyield* edgeGenerator.getEdgesGenerator( scene, edges );\n\t\tif ( includeIntersectionEdges ) {\n\n\t\t\tonProgress( 0, 'Extracting self-intersecting edges' );\n\t\t\tyield* edgeGenerator.getIntersectionEdgesGenerator( scene, edges );\n\n\t\t}\n\n\t\t// filter out any degenerate projected edges\n\t\tonProgress( 0, 'Filtering edges' );\n\t\tedges = edges.filter( e => ! isYProjectedLineDegenerate( e ) );\n\n\t\tedges.sort( ( a, b ) => {\n\n\t\t\tconst uuidA = a.mesh.uuid;\n\t\t\tconst uuidB = b.mesh.uuid;\n\t\t\tif ( uuidA === uuidB ) {\n\n\t\t\t\treturn 0;\n\n\t\t\t} else {\n\n\t\t\t\treturn uuidA < uuidB ? - 1 : 1;\n\n\t\t\t}\n\n\t\t} );\n\n\t\tyield;\n\n\t\tconst collector = new ProjectedEdgeCollector( scene );\n\t\tcollector.iterationTime = iterationTime;\n\n\t\tonProgress( 0, 'Clipping edges' );\n\t\tyield* collector.addEdgesGenerator( edges, {\n\t\t\tonProgress: ! onProgress ? null : ( prog, tot ) => {\n\n\t\t\t\tonProgress( prog / tot, 'Clipping edges', collector.result );\n\n\t\t\t},\n\t\t} );\n\n\t\treturn collector.result;\n\n\t}\n\n}\n\n"
  },
  {
    "path": "src/SilhouetteGenerator.js",
    "content": "import { Path64, Clipper, FillRule } from 'clipper2-js';\nimport { ShapeGeometry, Vector3, Shape, Vector2, Triangle, ShapeUtils, BufferGeometry } from 'three';\nimport { compressPoints } from './utils/compressPoints.js';\nimport { triangleIsInsidePaths } from './utils/triangleIsInsidePaths.js';\nimport { getSizeSortedTriList } from './utils/getSizeSortedTriList.js';\nimport { getTriCount } from './utils/geometryUtils.js';\n\nconst AREA_EPSILON = 1e-8;\nconst UP_VECTOR = /* @__PURE__ */ new Vector3( 0, 1, 0 );\nconst _tri = /* @__PURE__ */ new Triangle();\nconst _normal = /* @__PURE__ */ new Vector3();\nconst _center = /* @__PURE__ */ new Vector3();\nconst _vec = /* @__PURE__ */ new Vector3();\n\nfunction convertPathToGeometry( path, scale ) {\n\n\tconst vector2s = path.map( points => {\n\n\t\treturn points.flatMap( v => new Vector2( v.x / scale, v.y / scale ) );\n\n\t} );\n\n\tconst holesShapes = vector2s\n\t\t.filter( p => ShapeUtils.isClockWise( p ) )\n\t\t.map( p => new Shape( p ) );\n\n\tconst solidShapes = vector2s\n\t\t.filter( p => ! ShapeUtils.isClockWise( p ) )\n\t\t.map( p => {\n\n\t\t\tconst shape = new Shape( p );\n\t\t\tshape.holes = holesShapes;\n\t\t\treturn shape;\n\n\t\t} );\n\n\t// flip the triangles so they're facing in the right direction\n\tconst result = new ShapeGeometry( solidShapes ).rotateX( Math.PI / 2 );\n\tresult.index.array.reverse();\n\treturn result;\n\n}\n\nfunction convertPathToLineSegments( path, scale ) {\n\n\tconst arr = [];\n\tpath.forEach( points => {\n\n\t\tfor ( let i = 0, l = points.length; i < l; i ++ ) {\n\n\t\t\tconst i1 = ( i + 1 ) % points.length;\n\t\t\tconst p0 = points[ i ];\n\t\t\tconst p1 = points[ i1 ];\n\t\t\tarr.push(\n\t\t\t\tnew Vector3( p0.x / scale, 0, p0.y / scale ),\n\t\t\t\tnew Vector3( p1.x / scale, 0, p1.y / scale )\n\t\t\t);\n\n\t\t}\n\n\t} );\n\n\tconst result = new BufferGeometry();\n\tresult.setFromPoints( arr );\n\treturn result;\n\n}\n\n/** @type {number} */\nexport const OUTPUT_MESH = 0;\n\n/** @type {number} */\nexport const OUTPUT_LINE_SEGMENTS = 1;\n\n/** @type {number} */\nexport const OUTPUT_BOTH = 2;\n\n/**\n * @callback SilhouetteProgressCallback\n * @param {number} percent\n */\n\n/**\n * Used for generating a projected silhouette of a geometry using the clipper2-js project. Performing\n * these operations can be extremely slow with more complex geometry and not always yield a stable result.\n */\nexport class SilhouetteGenerator {\n\n\tconstructor() {\n\n\t\t/**\n\t\t * How long to spend trimming edges before yielding.\n\t\t * @type {number}\n\t\t */\n\t\tthis.iterationTime = 30;\n\n\t\tthis.intScalar = 1e9;\n\n\t\t/**\n\t\t * If `false` then only the triangles facing upwards are included in the silhouette.\n\t\t * @type {boolean}\n\t\t */\n\t\tthis.doubleSided = false;\n\n\t\t/**\n\t\t * Whether to sort triangles and project them large-to-small. In some cases this can cause\n\t\t * the performance to drop since the union operation is best performed with smooth, simple\n\t\t * edge shapes.\n\t\t * @type {boolean}\n\t\t */\n\t\tthis.sortTriangles = false;\n\n\t\t/**\n\t\t * Whether to output mesh geometry, line segments geometry, or both in an array\n\t\t * ( `[ mesh, line segments ]` ).\n\t\t * @type {number}\n\t\t */\n\t\tthis.output = OUTPUT_MESH;\n\n\t}\n\n\t/**\n\t * Generate the silhouette geometry with a promise-style API.\n\t * @async\n\t * @param {BufferGeometry} geometry\n\t * @param {Object} [options]\n\t * @param {SilhouetteProgressCallback} [options.onProgress]\n\t * @param {AbortSignal} [options.signal]\n\t * @returns {BufferGeometry|Array<BufferGeometry>}\n\t */\n\tgenerateAsync( geometry, options = {} ) {\n\n\t\treturn new Promise( ( resolve, reject ) => {\n\n\t\t\tconst { signal } = options;\n\t\t\tconst task = this.generate( geometry, options );\n\t\t\trun();\n\n\t\t\tfunction run() {\n\n\t\t\t\tif ( signal && signal.aborted ) {\n\n\t\t\t\t\treject( new Error( 'SilhouetteGenerator: Process aborted via AbortSignal.' ) );\n\t\t\t\t\treturn;\n\n\t\t\t\t}\n\n\t\t\t\tconst result = task.next();\n\t\t\t\tif ( result.done ) {\n\n\t\t\t\t\tresolve( result.value );\n\n\t\t\t\t} else {\n\n\t\t\t\t\trequestAnimationFrame( run );\n\n\t\t\t\t}\n\n\t\t\t}\n\n\n\t\t} );\n\n\t}\n\n\t/**\n\t * Generate the geometry using a generator function.\n\t * @param {BufferGeometry} geometry\n\t * @param {Object} [options]\n\t * @param {SilhouetteProgressCallback} [options.onProgress]\n\t * @yields {void}\n\t * @returns {BufferGeometry|Array<BufferGeometry>}\n\t */\n\t*generate( geometry, options = {} ) {\n\n\t\tconst { iterationTime, intScalar, doubleSided, output, sortTriangles } = this;\n\t\tconst { onProgress } = options;\n\t\tconst power = Math.log10( intScalar );\n\t\tconst extendMultiplier = Math.pow( 10, - ( power - 2 ) );\n\n\t\tconst index = geometry.index;\n\t\tconst posAttr = geometry.attributes.position;\n\t\tconst triCount = getTriCount( geometry );\n\t\tlet overallPath = null;\n\n\t\tconst triList = sortTriangles ?\n\t\t\tgetSizeSortedTriList( geometry ) :\n\t\t\tnew Array( triCount ).fill().map( ( v, i ) => i );\n\n\t\tconst handle = {\n\n\t\t\tgetGeometry() {\n\n\t\t\t\tif ( output === OUTPUT_MESH ) {\n\n\t\t\t\t\treturn convertPathToGeometry( overallPath, intScalar );\n\n\t\t\t\t} else if ( output === OUTPUT_LINE_SEGMENTS ) {\n\n\t\t\t\t\treturn convertPathToLineSegments( overallPath, intScalar );\n\n\t\t\t\t} else {\n\n\t\t\t\t\treturn [\n\t\t\t\t\t\tconvertPathToGeometry( overallPath, intScalar ),\n\t\t\t\t\t\tconvertPathToLineSegments( overallPath, intScalar ),\n\t\t\t\t\t];\n\n\t\t\t\t}\n\n\t\t\t}\n\n\t\t};\n\n\t\tlet time = performance.now();\n\t\tfor ( let ti = 0; ti < triCount; ti ++ ) {\n\n\t\t\tconst i = triList[ ti ] * 3;\n\t\t\tlet i0 = i + 0;\n\t\t\tlet i1 = i + 1;\n\t\t\tlet i2 = i + 2;\n\t\t\tif ( index ) {\n\n\t\t\t\ti0 = index.getX( i0 );\n\t\t\t\ti1 = index.getX( i1 );\n\t\t\t\ti2 = index.getX( i2 );\n\n\t\t\t}\n\n\t\t\t// get the triangle\n\t\t\tconst { a, b, c } = _tri;\n\t\t\ta.fromBufferAttribute( posAttr, i0 );\n\t\t\tb.fromBufferAttribute( posAttr, i1 );\n\t\t\tc.fromBufferAttribute( posAttr, i2 );\n\t\t\tif ( ! doubleSided ) {\n\n\t\t\t\t_tri.getNormal( _normal );\n\t\t\t\tif ( _normal.dot( UP_VECTOR ) < 0 ) {\n\n\t\t\t\t\tcontinue;\n\n\t\t\t\t}\n\n\t\t\t}\n\n\t\t\t// flatten the triangle\n\t\t\ta.y = 0;\n\t\t\tb.y = 0;\n\t\t\tc.y = 0;\n\n\t\t\tif ( _tri.getArea() < AREA_EPSILON ) {\n\n\t\t\t\tcontinue;\n\n\t\t\t}\n\n\t\t\t// expand the triangle by a small degree to ensure overlap\n\t\t\t_center\n\t\t\t\t.copy( a )\n\t\t\t\t.add( b )\n\t\t\t\t.add( c )\n\t\t\t\t.multiplyScalar( 1 / 3 );\n\n\t\t\t_vec.subVectors( a, _center ).normalize();\n\t\t\ta.addScaledVector( _vec, extendMultiplier );\n\n\t\t\t_vec.subVectors( b, _center ).normalize();\n\t\t\tb.addScaledVector( _vec, extendMultiplier );\n\n\t\t\t_vec.subVectors( c, _center ).normalize();\n\t\t\tc.addScaledVector( _vec, extendMultiplier );\n\n\t\t\t// create the path\n\t\t\tconst path = new Path64();\n\t\t\tpath.push( Clipper.makePath( [\n\t\t\t\ta.x * intScalar, a.z * intScalar,\n\t\t\t\tb.x * intScalar, b.z * intScalar,\n\t\t\t\tc.x * intScalar, c.z * intScalar,\n\t\t\t] ) );\n\n\t\t\ta.multiplyScalar( intScalar );\n\t\t\tb.multiplyScalar( intScalar );\n\t\t\tc.multiplyScalar( intScalar );\n\t\t\tif ( overallPath && triangleIsInsidePaths( _tri, overallPath ) ) {\n\n\t\t\t\tcontinue;\n\n\t\t\t}\n\n\t\t\t// perform union\n\t\t\tif ( overallPath === null ) {\n\n\t\t\t\toverallPath = path;\n\n\t\t\t} else {\n\n\t\t\t\toverallPath = Clipper.Union( overallPath, path, FillRule.NonZero );\n\t\t\t\toverallPath.forEach( path => compressPoints( path ) );\n\n\t\t\t}\n\n\t\t\tconst delta = performance.now() - time;\n\t\t\tif ( delta > iterationTime ) {\n\n\t\t\t\tif ( onProgress ) {\n\n\t\t\t\t\tconst progress = ti / triCount;\n\t\t\t\t\tonProgress( progress, handle );\n\n\t\t\t\t}\n\n\t\t\t\tyield;\n\t\t\t\ttime = performance.now();\n\n\t\t\t}\n\n\t\t}\n\n\t\treturn handle.getGeometry();\n\n\t}\n\n}\n"
  },
  {
    "path": "src/index.js",
    "content": "export * from './MeshVisibilityCuller.js';\nexport * from './ProjectionGenerator.js';\nexport * from './SilhouetteGenerator.js';\nexport * from './PlanarIntersectionGenerator.js';\n"
  },
  {
    "path": "src/utils/LineObjectsBVH.js",
    "content": "import { BVH } from 'three-mesh-bvh';\nexport class LineObjectsBVH extends BVH {\n\n\tget lines() {\n\n\t\treturn this.primitiveBuffer;\n\n\t}\n\n\tconstructor( lines, options ) {\n\n\t\tsuper( options );\n\n\t\tthis.primitiveBuffer = lines;\n\t\tthis.primitiveBufferStride = 1;\n\n\t\tthis.heightOffset = options.heightOffset ?? 1e3;\n\t\tthis.init( options );\n\n\t}\n\n\twritePrimitiveBounds( i, targetBuffer, writeOffset ) {\n\n\t\tconst { primitiveBuffer, heightOffset } = this;\n\t\tconst { start, end } = primitiveBuffer[ i ];\n\n\t\ttargetBuffer[ writeOffset + 0 ] = Math.min( start.x, end.x );\n\t\ttargetBuffer[ writeOffset + 1 ] = Math.min( start.y, end.y );\n\t\ttargetBuffer[ writeOffset + 2 ] = Math.min( start.z, end.z );\n\n\t\ttargetBuffer[ writeOffset + 3 ] = Math.max( start.x, end.x );\n\t\ttargetBuffer[ writeOffset + 4 ] = Math.max( start.y, end.y ) + heightOffset;\n\t\ttargetBuffer[ writeOffset + 5 ] = Math.max( start.z, end.z );\n\n\t}\n\n\tgetRootRanges() {\n\n\t\treturn [ { offset: 0, count: this.primitiveBuffer.length } ];\n\n\t}\n\n}\n"
  },
  {
    "path": "src/utils/ProjectionEdge.js",
    "content": "import { Line3 } from 'three';\n\nexport class ProjectionEdge extends Line3 {\n\n\tconstructor( start, end ) {\n\n\t\tsuper( start, end );\n\t\tthis.mesh = null;\n\n\t}\n\n\tcopy( source ) {\n\n\t\tsuper.copy( source );\n\t\tthis.mesh = source.mesh || null;\n\t\treturn this;\n\n\t}\n\n}\n"
  },
  {
    "path": "src/utils/bvhcastEdges.js",
    "content": "import { isLineTriangleEdge } from './triangleLineUtils.js';\nimport { trimToBeneathTriPlane } from './trimToBeneathTriPlane.js';\nimport { getProjectedLineOverlap } from './getProjectedLineOverlap.js';\nimport { appendOverlapRange } from './getProjectedOverlaps.js';\nimport { BackSide, DoubleSide, Line3, Vector3 } from 'three';\nimport { ExtendedTriangle } from 'three-mesh-bvh';\n\nconst UP_VECTOR = new Vector3( 0, 1, 0 );\nconst DIST_THRESHOLD = 1e-10;\nconst _beneathLine = /* @__PURE__ */ new Line3();\nconst _overlapLine = /* @__PURE__ */ new Line3();\nconst _tri = /* @__PURE__ */ new ExtendedTriangle();\n_tri.update = () => {\n\n\t// override the \"update\" function so we only calculate the piece we need\n\t_tri.plane.setFromCoplanarPoints( ..._tri.points );\n\n};\n\nexport function bvhcastEdges( edgesBvh, bvh, mesh, hiddenOverlapMap ) {\n\n\tconst { geometry, matrixWorld, material } = mesh;\n\tconst side = material.side;\n\tconst inverted = matrixWorld.determinant() < 0;\n\tconst edges = edgesBvh.lines;\n\n\tedgesBvh.bvhcast( bvh, matrixWorld, {\n\n\t\tintersectsRanges: ( edgeOffset, edgeCount, meshOffset, meshCount ) => {\n\n\t\t\tfor ( let i = meshOffset, l = meshCount + meshOffset; i < l; i ++ ) {\n\n\t\t\t\tlet i0 = 3 * i + 0;\n\t\t\t\tlet i1 = 3 * i + 1;\n\t\t\t\tlet i2 = 3 * i + 2;\n\t\t\t\tif ( geometry.index ) {\n\n\t\t\t\t\ti0 = geometry.index.getX( i0 );\n\t\t\t\t\ti1 = geometry.index.getX( i1 );\n\t\t\t\t\ti2 = geometry.index.getX( i2 );\n\n\t\t\t\t}\n\n\t\t\t\t// Transform mesh triangle to world space\n\t\t\t\tconst { a, b, c } = _tri;\n\t\t\t\ta.fromBufferAttribute( geometry.attributes.position, i0 ).applyMatrix4( matrixWorld );\n\t\t\t\tb.fromBufferAttribute( geometry.attributes.position, i1 ).applyMatrix4( matrixWorld );\n\t\t\t\tc.fromBufferAttribute( geometry.attributes.position, i2 ).applyMatrix4( matrixWorld );\n\t\t\t\t_tri.needsUpdate = true;\n\t\t\t\t_tri.update();\n\n\t\t\t\t// back face culling\n\t\t\t\tif ( side !== DoubleSide ) {\n\n\t\t\t\t\tconst faceUp = _tri.plane.normal.dot( UP_VECTOR ) !== inverted;\n\t\t\t\t\tif ( faceUp === ( side === BackSide ) ) {\n\n\t\t\t\t\t\tcontinue;\n\n\t\t\t\t\t}\n\n\t\t\t\t}\n\n\t\t\t\tconst triMaxY = Math.max( a.y, b.y, c.y );\n\t\t\t\tconst triMinY = Math.min( a.y, b.y, c.y );\n\t\t\t\tfor ( let e = edgeOffset, le = edgeCount + edgeOffset; e < le; e ++ ) {\n\n\t\t\t\t\tconst _line = edges[ e ];\n\n\t\t\t\t\t// Calculate edge and triangle bounds\n\t\t\t\t\tconst lineMinY = Math.min( _line.start.y, _line.end.y );\n\t\t\t\t\tconst lineMaxY = Math.max( _line.start.y, _line.end.y );\n\n\t\t\t\t\t// Skip if triangle is completely below the line\n\t\t\t\t\tif ( triMaxY <= lineMinY ) {\n\n\t\t\t\t\t\tcontinue;\n\n\t\t\t\t\t}\n\n\t\t\t\t\t// Skip if this line lies on a triangle edge\n\t\t\t\t\tif ( isLineTriangleEdge( _tri, _line ) ) {\n\n\t\t\t\t\t\tcontinue;\n\n\t\t\t\t\t}\n\n\t\t\t\t\t// Retrieve the portion of line that is below the triangle plane\n\t\t\t\t\tif ( lineMaxY < triMinY ) {\n\n\t\t\t\t\t\t_beneathLine.copy( _line );\n\n\t\t\t\t\t} else if ( ! trimToBeneathTriPlane( _tri, _line, _beneathLine ) ) {\n\n\t\t\t\t\t\tcontinue;\n\n\t\t\t\t\t}\n\n\t\t\t\t\t// Cull overly small edges\n\t\t\t\t\tif ( _beneathLine.distance() < DIST_THRESHOLD ) {\n\n\t\t\t\t\t\tcontinue;\n\n\t\t\t\t\t}\n\n\t\t\t\t\t// Calculate projected overlap and store in hiddenOverlapMap\n\t\t\t\t\tif ( getProjectedLineOverlap( _beneathLine, _tri, _overlapLine ) ) {\n\n\t\t\t\t\t\tappendOverlapRange( _line, _overlapLine, hiddenOverlapMap[ e ] );\n\n\t\t\t\t\t}\n\n\t\t\t\t}\n\n\t\t\t}\n\n\t\t},\n\n\t} );\n\n}\n"
  },
  {
    "path": "src/utils/compressPoints.js",
    "content": "const DIRECTION_EPSILON = 1e-3;\nconst DIST_EPSILON = 1e2;\n\nfunction sameDirection( p0, p1, p2 ) {\n\n\tconst dx1 = p1.x - p0.x;\n\tconst dy1 = p1.y - p0.y;\n\n\tconst dx2 = p2.x - p1.x;\n\tconst dy2 = p2.y - p1.y;\n\n\tconst s1 = dx1 / dy1;\n\tconst s2 = dx2 / dy2;\n\n\treturn Math.abs( s1 - s2 ) < DIRECTION_EPSILON;\n\n}\n\nfunction areClose( p0, p1 ) {\n\n\tconst dx = p1.x - p0.x;\n\tconst dy = p1.y - p0.y;\n\treturn Math.sqrt( dx * dx + dy * dy ) < DIST_EPSILON;\n\n}\n\nfunction areEqual( p0, p1 ) {\n\n\treturn p0.x === p1.x && p0.y === p1.y;\n\n}\n\nexport function compressPoints( points ) {\n\n\tfor ( let k = 0; k < points.length; k ++ ) {\n\n\t\t// remove points that are equal or very close to each other\n\t\tconst v = points[ k ];\n\t\twhile ( true ) {\n\n\t\t\tconst k1 = k + 1;\n\t\t\tif (\n\t\t\t\tpoints.length > k1 &&\n\t\t\t\t(\n\t\t\t\t\tareEqual( v, points[ k1 ] ) ||\n\t\t\t\t\tareClose( v, points[ k1 ] )\n\t\t\t\t)\n\t\t\t) {\n\n\t\t\t\tpoints.splice( k1, 1 );\n\n\t\t\t} else {\n\n\t\t\t\tbreak;\n\n\t\t\t}\n\n\n\t\t}\n\n\t\t// join lines that are almost the same direction\n\t\twhile ( true ) {\n\n\t\t\tconst k1 = k + 1;\n\t\t\tconst k2 = k + 2;\n\t\t\tif (\n\t\t\t\tpoints.length > k2 &&\n\t\t\t\tsameDirection( v, points[ k1 ], points[ k2 ] )\n\t\t\t) {\n\n\t\t\t\tpoints.splice( k + 1, 1 );\n\n\t\t\t} else {\n\n\t\t\t\tbreak;\n\n\t\t\t}\n\n\t\t}\n\n\t}\n\n}\n"
  },
  {
    "path": "src/utils/generateEdges.js",
    "content": "import { Vector3, Triangle, MathUtils, Matrix4 } from 'three';\nimport { ProjectionEdge } from './ProjectionEdge.js';\n\n// Modified version of js EdgesGeometry logic to handle silhouette edges\nconst EPSILON = 1e-10;\nconst UP_VECTOR = /* @__PURE__ */ new Vector3( 0, 1, 0 );\nconst _v0 = /* @__PURE__ */ new Vector3();\nconst _v1 = /* @__PURE__ */ new Vector3();\nconst _normal = /* @__PURE__ */ new Vector3();\nconst _triangle = /* @__PURE__ */ new Triangle();\nconst _triangleLocal = /* @__PURE__ */ new Triangle();\nconst _localProjection = /* @__PURE__ */ new Vector3();\nconst _invMat = /* @__PURE__ */ new Matrix4();\n\nexport function* generateEdges( geometry, target = [], options = {} ) {\n\n\tconst {\n\t\tmatrix = null,\n\t\tthresholdAngle = 1,\n\t\titerationTime = 30,\n\t} = options;\n\n\t_localProjection.copy( UP_VECTOR );\n\n\tlet isAffine = true;\n\tif ( matrix ) {\n\n\t\tisAffine =\n\t\t\tmatrix.elements[ 3 ] === 0 &&\n\t\t\tmatrix.elements[ 7 ] === 0 &&\n\t\t\tmatrix.elements[ 11 ] === 0 &&\n\t\t\tmatrix.elements[ 15 ] === 1;\n\n\t\tif ( isAffine ) {\n\n\t\t\t_invMat.copy( matrix ).invert();\n\t\t\t_localProjection.transformDirection( _invMat );\n\n\t\t}\n\n\t}\n\n\tconst precisionPoints = 4;\n\tconst precision = Math.pow( 10, precisionPoints );\n\tconst thresholdDot = Math.cos( MathUtils.DEG2RAD * thresholdAngle );\n\n\tconst indexAttr = geometry.getIndex();\n\tconst positionAttr = geometry.getAttribute( 'position' );\n\tconst indexCount = indexAttr ? indexAttr.count : positionAttr.count;\n\n\tconst indexArr = [ 0, 0, 0 ];\n\tconst vertKeys = [ 'a', 'b', 'c' ];\n\tconst hashes = new Array( 3 );\n\n\tconst edgeData = {};\n\tlet time = performance.now();\n\tfor ( let i = 0; i < indexCount; i += 3 ) {\n\n\t\tif ( performance.now() - time > iterationTime ) {\n\n\t\t\tyield;\n\t\t\ttime = performance.now();\n\n\t\t}\n\n\t\tif ( indexAttr ) {\n\n\t\t\tindexArr[ 0 ] = indexAttr.getX( i );\n\t\t\tindexArr[ 1 ] = indexAttr.getX( i + 1 );\n\t\t\tindexArr[ 2 ] = indexAttr.getX( i + 2 );\n\n\t\t} else {\n\n\t\t\tindexArr[ 0 ] = i;\n\t\t\tindexArr[ 1 ] = i + 1;\n\t\t\tindexArr[ 2 ] = i + 2;\n\n\t\t}\n\n\t\tconst { a, b, c } = _triangleLocal;\n\t\t_triangleLocal.a.fromBufferAttribute( positionAttr, indexArr[ 0 ] );\n\t\t_triangleLocal.b.fromBufferAttribute( positionAttr, indexArr[ 1 ] );\n\t\t_triangleLocal.c.fromBufferAttribute( positionAttr, indexArr[ 2 ] );\n\n\t\t// create hashes for the edge from the vertices\n\t\thashes[ 0 ] = `${ Math.round( a.x * precision ) },${ Math.round( a.y * precision ) },${ Math.round( a.z * precision ) }`;\n\t\thashes[ 1 ] = `${ Math.round( b.x * precision ) },${ Math.round( b.y * precision ) },${ Math.round( b.z * precision ) }`;\n\t\thashes[ 2 ] = `${ Math.round( c.x * precision ) },${ Math.round( c.y * precision ) },${ Math.round( c.z * precision ) }`;\n\n\t\t// skip degenerate triangles\n\t\tif ( hashes[ 0 ] === hashes[ 1 ] || hashes[ 1 ] === hashes[ 2 ] || hashes[ 2 ] === hashes[ 0 ] ) {\n\n\t\t\tcontinue;\n\n\t\t}\n\n\t\t// compute normal — fast path uses local-space normal with pre-transformed\n\t\t// projection direction; slow path transforms vertices for world-space normal\n\t\tif ( matrix && ! isAffine ) {\n\n\t\t\t_triangle.copy( _triangleLocal );\n\t\t\t_triangle.a.applyMatrix4( matrix );\n\t\t\t_triangle.b.applyMatrix4( matrix );\n\t\t\t_triangle.c.applyMatrix4( matrix );\n\t\t\t_triangle.getNormal( _normal );\n\n\t\t} else {\n\n\t\t\t_triangleLocal.getNormal( _normal );\n\n\t\t}\n\n\t\t// iterate over every edge\n\t\tfor ( let j = 0; j < 3; j ++ ) {\n\n\t\t\t// get the first and next vertex making up the edge\n\t\t\tconst jNext = ( j + 1 ) % 3;\n\t\t\tconst vecHash0 = hashes[ j ];\n\t\t\tconst vecHash1 = hashes[ jNext ];\n\t\t\tconst v0 = _triangleLocal[ vertKeys[ j ] ];\n\t\t\tconst v1 = _triangleLocal[ vertKeys[ jNext ] ];\n\n\t\t\tconst hash = `${ vecHash0 }_${ vecHash1 }`;\n\t\t\tconst reverseHash = `${ vecHash1 }_${ vecHash0 }`;\n\n\t\t\tif ( reverseHash in edgeData && edgeData[ reverseHash ] ) {\n\n\t\t\t\t// if we found a sibling edge add it into the vertex array if\n\t\t\t\t// it meets the angle threshold and delete the edge from the map.\n\t\t\t\tconst otherNormal = edgeData[ reverseHash ].normal;\n\t\t\t\tconst meetsThreshold = _normal.dot( otherNormal ) <= thresholdDot;\n\n\t\t\t\t// get the dot product relative to the projection angle and\n\t\t\t\t// add an epsilon for nearly vertical triangles\n\t\t\t\tconst _projDir = _localProjection;\n\t\t\t\tlet normDot = _projDir.dot( _normal );\n\t\t\t\tnormDot = Math.abs( normDot ) < EPSILON ? 0 : normDot;\n\n\t\t\t\tlet otherDot = _projDir.dot( otherNormal );\n\t\t\t\totherDot = Math.abs( otherDot ) < EPSILON ? 0 : otherDot;\n\n\t\t\t\tconst projectionThreshold = Math.sign( normDot ) !== Math.sign( otherDot );\n\n\t\t\t\tif ( meetsThreshold || projectionThreshold ) {\n\n\t\t\t\t\tconst line = new ProjectionEdge();\n\t\t\t\t\tline.start.copy( v0 );\n\t\t\t\t\tline.end.copy( v1 );\n\t\t\t\t\ttarget.push( line );\n\n\t\t\t\t}\n\n\t\t\t\tedgeData[ reverseHash ] = null;\n\n\t\t\t} else if ( ! ( hash in edgeData ) ) {\n\n\t\t\t\t// if we've already got an edge here then skip adding a new one\n\t\t\t\tedgeData[ hash ] = {\n\n\t\t\t\t\tindex0: indexArr[ j ],\n\t\t\t\t\tindex1: indexArr[ jNext ],\n\t\t\t\t\tnormal: _normal.clone(),\n\n\t\t\t\t};\n\n\t\t\t}\n\n\t\t}\n\n\t}\n\n\t// iterate over all remaining, unmatched edges and add them to the vertex array\n\tfor ( const key in edgeData ) {\n\n\t\tif ( edgeData[ key ] ) {\n\n\t\t\tconst { index0, index1 } = edgeData[ key ];\n\t\t\t_v0.fromBufferAttribute( positionAttr, index0 );\n\t\t\t_v1.fromBufferAttribute( positionAttr, index1 );\n\n\t\t\tconst line = new ProjectionEdge();\n\t\t\tline.start.copy( _v0 );\n\t\t\tline.end.copy( _v1 );\n\t\t\ttarget.push( line );\n\n\t\t}\n\n\t}\n\n\treturn target;\n\n}\n"
  },
  {
    "path": "src/utils/generateIntersectionEdges.js",
    "content": "import { Line3 } from 'three';\nimport { isLineTriangleEdge } from './triangleLineUtils.js';\nimport { ProjectionEdge } from './ProjectionEdge.js';\n\n// TODO: How can we add support for \"iterationTime\"?\n\nconst _line = /* @__PURE__ */ new Line3();\nexport function generateIntersectionEdges( bvhA, bvhB, matrixBToA, target = [] ) {\n\n\tbvhA.bvhcast( bvhB, matrixBToA, {\n\t\tintersectsTriangles: ( tri1, tri2 ) => {\n\n\t\t\tif ( areTrianglesOnEdge( tri1, tri2 ) ) {\n\n\t\t\t\treturn false;\n\n\t\t\t}\n\n\t\t\tif ( tri1.needsUpdate ) {\n\n\t\t\t\ttri1.update();\n\n\t\t\t}\n\n\t\t\tif ( tri2.needsUpdate ) {\n\n\t\t\t\ttri2.update();\n\n\t\t\t}\n\n\t\t\tif ( Math.abs( tri1.plane.normal.dot( tri2.plane.normal ) ) > 1 - 1e-6 ) {\n\n\t\t\t\treturn false;\n\n\t\t\t}\n\n\t\t\tif (\n\t\t\t\ttri1.intersectsTriangle( tri2, _line, true ) &&\n\t\t\t\t! isLineTriangleEdge( tri1, _line ) &&\n\t\t\t\t! isLineTriangleEdge( tri2, _line )\n\t\t\t) {\n\n\t\t\t\ttarget.push( new ProjectionEdge().copy( _line ) );\n\n\t\t\t}\n\n\t\t}\n\n\t} );\n\n\treturn target;\n\n}\n\nfunction areVectorsEqual( a, b ) {\n\n\treturn a.distanceTo( b ) < 1e-10;\n\n}\n\nfunction areTrianglesOnEdge( t1, t2 ) {\n\n\tconst indices = [ 'a', 'b', 'c' ];\n\tlet tot = 0;\n\tfor ( let i = 0; i < 3; i ++ ) {\n\n\t\tfor ( let j = 0; j < 3; j ++ ) {\n\n\t\t\tconst v0 = t1[ indices[ i ] ];\n\t\t\tconst v1 = t2[ indices[ j ] ];\n\t\t\tif ( areVectorsEqual( v0, v1 ) ) {\n\n\t\t\t\ttot ++;\n\n\t\t\t}\n\n\t\t}\n\n\t}\n\n\treturn tot >= 2;\n\n}\n"
  },
  {
    "path": "src/utils/geometryUtils.js",
    "content": "export function getTriCount( geometry ) {\n\n\tconst { index } = geometry;\n\tconst posAttr = geometry.attributes.position;\n\treturn index ? index.count / 3 : posAttr.count / 3;\n\n}\n"
  },
  {
    "path": "src/utils/getAllMeshes.js",
    "content": "export function getAllMeshes( scene ) {\n\n\tlet arr;\n\tif ( Array.isArray( scene ) ) {\n\n\t\tarr = scene;\n\n\t} else {\n\n\t\tarr = [ scene ];\n\n\t}\n\n\tconst result = new Set();\n\tfor ( let i = 0, l = arr.length; i < l; i ++ ) {\n\n\t\tarr[ i ].traverse( c => {\n\n\t\t\tif ( c.geometry && c.visible ) {\n\n\t\t\t\tresult.add( c );\n\n\t\t\t}\n\n\t\t} );\n\n\t}\n\n\treturn Array.from( result );\n\n}\n"
  },
  {
    "path": "src/utils/getProjectedLineOverlap.js",
    "content": "import { Vector3, Line3, Plane } from 'three';\nimport { ExtendedTriangle } from 'three-mesh-bvh';\n\nconst AREA_EPSILON = 1e-16;\nconst DIST_EPSILON = 1e-16;\nconst _orthoPlane = /* @__PURE__ */ new Plane();\nconst _edgeLine = /* @__PURE__ */ new Line3();\nconst _point = /* @__PURE__ */ new Vector3();\nconst _vec = /* @__PURE__ */ new Vector3();\nconst _tri = /* @__PURE__ */ new ExtendedTriangle();\nconst _line = /* @__PURE__ */ new Line3();\nconst _triLine = /* @__PURE__ */ new Line3();\nconst _dir = /* @__PURE__ */ new Vector3();\nconst _ortho = /* @__PURE__ */ new Vector3();\nconst _triDir = /* @__PURE__ */ new Vector3();\n\n// Returns the portion of the line that is overlapping the triangle when projected\n// TODO: rename this, remove need for tri update, plane\nexport function getProjectedLineOverlap( line, triangle, lineTarget = new Line3() ) {\n\n\t// flatten the shapes\n\t_tri.copy( triangle );\n\t_tri.a.y = 0;\n\t_tri.b.y = 0;\n\t_tri.c.y = 0;\n\t_tri.update();\n\n\t_line.copy( line );\n\t_line.start.y = 0;\n\t_line.end.y = 0;\n\n\t// if the triangle is degenerate then return no overlap\n\tif ( _tri.getArea() <= AREA_EPSILON ) {\n\n\t\treturn null;\n\n\t}\n\n\tconst lineDistance = _line.distance();\n\t_line.delta( _dir ).divideScalar( lineDistance );\n\t_ortho.copy( _dir ).cross( _tri.plane.normal ).normalize();\n\t_orthoPlane.setFromNormalAndCoplanarPoint( _ortho, _line.start );\n\n\t// find the line of intersection of the triangle along the plane if it exists\n\tlet intersectCount = 0;\n\tconst { points } = _tri;\n\tfor ( let i = 0; i < 3; i ++ ) {\n\n\t\tconst p1 = points[ i ];\n\t\tconst p2 = points[ ( i + 1 ) % 3 ];\n\n\t\tconst distToStart = _orthoPlane.distanceToPoint( p1 );\n\t\tconst distToEnd = _orthoPlane.distanceToPoint( p2 );\n\n\t\tconst startIntersects = Math.abs( distToStart ) < DIST_EPSILON;\n\t\tconst endIntersects = Math.abs( distToEnd ) < DIST_EPSILON;\n\n\t\tif ( startIntersects && endIntersects ) {\n\n\t\t\tcontinue;\n\n\t\t} else if ( startIntersects ) {\n\n\t\t\t_point.copy( p1 );\n\n\t\t} else if ( endIntersects ) {\n\n\t\t\tcontinue;\n\n\t\t} else if ( ( distToStart < 0.0 ) == ( distToEnd < 0.0 ) ) {\n\n\t\t\tcontinue;\n\n\t\t} else {\n\n\t\t\t// manual edge-plane intersection (faster than Plane.intersectLine)\n\t\t\tconst t = distToStart / ( distToStart - distToEnd );\n\t\t\t_point.lerpVectors( p1, p2, t );\n\n\t\t}\n\n\t\tif ( intersectCount == 0 ) {\n\n\t\t\t_triLine.start.copy( _point );\n\n\t\t} else if ( intersectCount == 1 ) {\n\n\t\t\t_triLine.end.copy( _point );\n\n\t\t}\n\n\t\tintersectCount ++;\n\t\tif ( intersectCount === 2 ) {\n\n\t\t\tbreak;\n\n\t\t}\n\n\t}\n\n\tif ( intersectCount === 2 ) {\n\n\t\t// find the intersect line if any\n\t\t_triLine.delta( _triDir ).normalize();\n\n\t\t// swap edges so they're facing in the same direction\n\t\tif ( _dir.dot( _triDir ) < 0 ) {\n\n\t\t\tconst tmp = _triLine.start;\n\t\t\t_triLine.start = _triLine.end;\n\t\t\t_triLine.end = tmp;\n\n\t\t}\n\n\t\t// check if the edges are overlapping\n\t\tconst s1 = 0;\n\t\tconst e1 = _vec.subVectors( _line.end, _line.start ).dot( _dir );\n\t\tconst s2 = _vec.subVectors( _triLine.start, _line.start ).dot( _dir );\n\t\tconst e2 = _vec.subVectors( _triLine.end, _line.start ).dot( _dir );\n\t\tconst separated1 = e1 <= s2;\n\t\tconst separated2 = e2 <= s1;\n\n\t\tif ( separated1 || separated2 ) {\n\n\t\t\treturn null;\n\n\t\t}\n\n\t\tline.at(\n\t\t\tMath.max( s1, s2 ) / lineDistance,\n\t\t\tlineTarget.start,\n\t\t);\n\n\t\tline.at(\n\t\t\tMath.min( e1, e2 ) / lineDistance,\n\t\t\tlineTarget.end,\n\t\t);\n\n\t\treturn lineTarget;\n\n\t}\n\n\treturn null;\n\n}\n"
  },
  {
    "path": "src/utils/getProjectedOverlaps.js",
    "content": "import { Vector3 } from 'three';\nconst DIST_EPSILON = 1e-16;\nconst _dir = /* @__PURE__ */ new Vector3();\nconst _v0 = /* @__PURE__ */ new Vector3();\nconst _v1 = /* @__PURE__ */ new Vector3();\nexport function appendOverlapRange( line, overlapLine, overlapsTarget ) {\n\n\tconst result = getOverlapRange( line, overlapLine );\n\tif ( result ) {\n\n\t\tinsertOverlap( result, overlapsTarget );\n\t\treturn true;\n\n\t}\n\n\treturn false;\n\n}\n\n// Returns the overlap range without pushing to array (for binary insertion)\nexport function getOverlapRange( line, overlapLine ) {\n\n\tline.delta( _dir );\n\t_v0.subVectors( overlapLine.start, line.start );\n\t_v1.subVectors( overlapLine.end, line.start );\n\n\tconst length = _dir.length();\n\tlet t0 = _v0.length() / length;\n\tlet t1 = _v1.length() / length;\n\n\tt0 = Math.min( Math.max( t0, 0 ), 1 );\n\tt1 = Math.min( Math.max( t1, 0 ), 1 );\n\n\tif ( Math.abs( t0 - t1 ) <= DIST_EPSILON ) {\n\n\t\treturn null;\n\n\t}\n\n\treturn [ t0, t1 ];\n\n}\n\nexport function insertOverlap( result, overlapsTarget ) {\n\n\tlet [ start, end ] = result;\n\n\t// binary search to find where the for loop should begin iteration\n\tlet left = 0;\n\tlet right = overlapsTarget.length;\n\twhile ( left < right ) {\n\n\t\tconst mid = ( left + right ) >>> 1;\n\t\tif ( overlapsTarget[ mid ][ 0 ] <= start ) {\n\n\t\t\tleft = mid + 1;\n\n\t\t} else {\n\n\t\t\tright = mid;\n\n\t\t}\n\n\t}\n\n\t// start iteration from one position before (in case previous overlap\n\t// extends into ours)\n\tlet insertPoint = Math.max( 0, left - 1 );\n\tlet deleteCount = 0;\n\tfor ( let i = insertPoint, l = overlapsTarget.length; i < l; i ++ ) {\n\n\t\tconst [ otherStart, otherEnd ] = overlapsTarget[ i ];\n\t\tif ( start <= otherEnd && end >= otherStart ) {\n\n\t\t\t// check if there's overlap\n\t\t\tstart = Math.min( otherStart, start );\n\t\t\tend = Math.max( otherEnd, end );\n\t\t\tdeleteCount ++;\n\n\t\t} else if ( start >= otherStart ) {\n\n\t\t\t// otherwise move the insertion point forward\n\t\t\tinsertPoint = i + 1;\n\n\t\t} else {\n\n\t\t\tbreak;\n\n\t\t}\n\n\t}\n\n\toverlapsTarget.splice( insertPoint, deleteCount, [ start, end ] );\n\n}\n"
  },
  {
    "path": "src/utils/getSizeSortedTriList.js",
    "content": "import { Triangle } from 'three';\nimport { getTriCount } from './geometryUtils.js';\n\nconst _tri = new Triangle();\nexport function getSizeSortedTriList( geometry ) {\n\n\tconst index = geometry.index;\n\tconst posAttr = geometry.attributes.position;\n\tconst triCount = getTriCount( geometry );\n\n\treturn new Array( triCount )\n\t\t.fill()\n\t\t.map( ( v, i ) => {\n\n\t\t\tlet i0 = i * 3 + 0;\n\t\t\tlet i1 = i * 3 + 1;\n\t\t\tlet i2 = i * 3 + 2;\n\t\t\tif ( index ) {\n\n\t\t\t\ti0 = index.getX( i0 );\n\t\t\t\ti1 = index.getX( i1 );\n\t\t\t\ti2 = index.getX( i2 );\n\n\t\t\t}\n\n\t\t\t_tri.a.fromBufferAttribute( posAttr, i0 );\n\t\t\t_tri.b.fromBufferAttribute( posAttr, i1 );\n\t\t\t_tri.c.fromBufferAttribute( posAttr, i2 );\n\n\t\t\t_tri.a.y = 0;\n\t\t\t_tri.b.y = 0;\n\t\t\t_tri.c.y = 0;\n\n\t\t\t// get the projected area of the triangle to sort largest triangles first\n\t\t\treturn {\n\t\t\t\tarea: _tri.getArea(),\n\t\t\t\tindex: i,\n\t\t\t};\n\n\t\t} )\n\t\t.sort( ( a, b ) => {\n\n\t\t\t// sort the triangles largest to smallest\n\t\t\treturn b.area - a.area;\n\n\t\t} )\n\t\t.map( o => {\n\n\t\t\t// map to the triangle index\n\t\t\treturn o.index;\n\n\t\t} );\n\n}\n"
  },
  {
    "path": "src/utils/nextFrame.js",
    "content": "export const nextFrame = () => new Promise( resolve => {\n\n\tlet rafHandle;\n\tlet timeoutHandle;\n\tconst cb = () => {\n\n\t\tcancelAnimationFrame( rafHandle );\n\t\tclearTimeout( timeoutHandle );\n\t\tresolve();\n\n\t};\n\n\trafHandle = requestAnimationFrame( cb );\n\ttimeoutHandle = setTimeout( cb, 16 );\n\n} );\n"
  },
  {
    "path": "src/utils/overlapUtils.js",
    "content": "import { Line3 } from 'three';\n\nconst _line = /* @__PURE__ */ new Line3();\n\n// Converts the given array of overlaps into line segments\nexport function overlapsToLines( line, overlaps, invert = false, target = [] ) {\n\n\t// Function assumes the line overlaps are already compressed\n\tlet invOverlaps = [[ 0, 1 ]];\n\tfor ( let i = 0, l = overlaps.length; i < l; i ++ ) {\n\n\t\tconst invOverlap = invOverlaps[ i ];\n\t\tconst overlap = overlaps[ i ];\n\t\tinvOverlap[ 1 ] = overlap[ 0 ];\n\t\tinvOverlaps.push( [ overlap[ 1 ], 1 ] );\n\n\t}\n\n\tif ( invert ) {\n\n\t\t[ overlaps, invOverlaps ] = [ invOverlaps, overlaps ];\n\n\t}\n\n\tfor ( let i = 0, l = invOverlaps.length; i < l; i ++ ) {\n\n\t\tconst { start, end } = line;\n\t\t_line.start.lerpVectors( start, end, invOverlaps[ i ][ 0 ] );\n\t\t_line.end.lerpVectors( start, end, invOverlaps[ i ][ 1 ] );\n\n\t\ttarget.push( new Float32Array( [\n\t\t\t_line.start.x,\n\t\t\t_line.start.y,\n\t\t\t_line.start.z,\n\n\t\t\t_line.end.x,\n\t\t\t_line.end.y,\n\t\t\t_line.end.z,\n\t\t] ) );\n\n\t}\n\n\treturn invOverlaps.length;\n\n}\n"
  },
  {
    "path": "src/utils/planeUtils.js",
    "content": "import { Vector3, Line3 } from \"three\";\n\nconst _line = /* @__PURE__ */ new Line3();\nconst _v0 = /* @__PURE__ */ new Vector3();\nconst _v1 = /* @__PURE__ */ new Vector3();\n\n// returns the the y value on the plane at the given point x, z\nexport function getPlaneYAtPoint( plane, point, target = null ) {\n\n\t_line.start.copy( point );\n\t_line.end.copy( point );\n\n\t_line.start.y += 1e5;\n\t_line.end.y -= 1e5;\n\n\tplane.intersectLine( _line, target );\n\n}\n\n// returns whether the given line is above the given triangle plane\nexport function isLineAbovePlane( plane, line ) {\n\n\tconst linePoint = _v0;\n\tconst planePoint = _v1;\n\tlinePoint.lerpVectors( line.start, line.end, 0.5 );\n\tgetPlaneYAtPoint( plane, linePoint, planePoint );\n\n\treturn planePoint.y < linePoint.y;\n\n}\n"
  },
  {
    "path": "src/utils/triangleIsInsidePaths.js",
    "content": "import { Line3, Ray } from 'three';\n\nfunction xzToXzCopy( v, target ) {\n\n\ttarget.x = v.x;\n\ttarget.y = v.z;\n\n}\n\nfunction epsEquals( a, b ) {\n\n\treturn Math.abs( a - b ) <= 500;\n\n}\n\nfunction vectorEpsEquals( v0, v1 ) {\n\n\treturn epsEquals( v0.x, v1.x ) &&\n\t\tepsEquals( v0.y, v1.y ) &&\n\t\tepsEquals( v0.z, v1.z );\n\n}\n\nexport function triangleIsInsidePaths( tri, paths ) {\n\n\tconst indices = [ 'a', 'b', 'c' ];\n\tconst edges = [ new Line3(), new Line3(), new Line3() ];\n\tconst line = new Line3();\n\tconst ray = new Line3();\n\tray.start\n\t\t.set( 0, 0, 0 )\n\t\t.addScaledVector( tri.a, 1 / 3 )\n\t\t.addScaledVector( tri.b, 1 / 3 )\n\t\t.addScaledVector( tri.c, 1 / 3 );\n\n\txzToXzCopy( ray.start, ray.start );\n\tray.end.copy( ray.start );\n\tray.end.y += 1e10;\n\n\t// get all triangle edges\n\tfor ( let i = 0; i < 3; i ++ ) {\n\n\t\tconst i1 = ( i + 1 ) % 3;\n\t\tconst p0 = tri[ indices[ i ] ];\n\t\tconst p1 = tri[ indices[ i1 ] ];\n\n\t\tconst edge = edges[ i ];\n\t\txzToXzCopy( p0, edge.start );\n\t\txzToXzCopy( p1, edge.end );\n\n\t}\n\n\tlet crossCount = 0;\n\tfor ( let p = 0, pl = paths.length; p < pl; p ++ ) {\n\n\t\tconst points = paths[ p ];\n\t\tfor ( let i = 0, l = points.length; i < l; i ++ ) {\n\n\t\t\tconst i1 = ( i + 1 ) % l;\n\t\t\tline.start.copy( points[ i ] );\n\t\t\tline.start.z = 0;\n\n\t\t\tline.end.copy( points[ i1 ] );\n\t\t\tline.end.z = 0;\n\n\t\t\tif ( lineCrossesLine( ray, line ) ) {\n\n\t\t\t\tcrossCount ++;\n\n\t\t\t}\n\n\t\t\tfor ( let e = 0; e < 3; e ++ ) {\n\n\t\t\t\tconst edge = edges[ e ];\n\t\t\t\tif (\n\t\t\t\t\tlineCrossesLine( edge, line ) ||\n\t\t\t\t\tvectorEpsEquals( edge.start, line.start ) ||\n\t\t\t\t\tvectorEpsEquals( edge.end, line.end ) ||\n\t\t\t\t\tvectorEpsEquals( edge.end, line.start ) ||\n\t\t\t\t\tvectorEpsEquals( edge.start, line.end )\n\t\t\t\t) {\n\n\t\t\t\t\treturn false;\n\n\t\t\t\t}\n\n\t\t\t}\n\n\t\t}\n\n\t}\n\n\treturn crossCount % 2 === 1;\n\n}\n\n// https://stackoverflow.com/questions/3838329/how-can-i-check-if-two-segments-intersect\nfunction lineCrossesLine( l1, l2 ) {\n\n\tfunction ccw( A, B, C ) {\n\n\t\treturn ( C.y - A.y ) * ( B.x - A.x ) > ( B.y - A.y ) * ( C.x - A.x );\n\n\t}\n\n\tconst A = l1.start;\n\tconst B = l1.end;\n\n\tconst C = l2.start;\n\tconst D = l2.end;\n\n\treturn ccw( A, C, D ) !== ccw( B, C, D ) && ccw( A, B, C ) !== ccw( A, B, D );\n\n}\n"
  },
  {
    "path": "src/utils/triangleLineUtils.js",
    "content": "import { Vector3 } from 'three';\n\nconst EPSILON = 1e-16;\nconst UP_VECTOR = /* @__PURE__ */ new Vector3( 0, 1, 0 );\nconst _dir = new Vector3();\n\nexport function isYProjectedLineDegenerate( line ) {\n\n\tline.delta( _dir ).normalize();\n\treturn Math.abs( _dir.dot( UP_VECTOR ) ) >= 1.0 - EPSILON;\n\n}\n\n// checks whether the y-projected triangle will be degenerate\nexport function isYProjectedTriangleDegenerate( tri ) {\n\n\tif ( tri.needsUpdate ) {\n\n\t\ttri.update();\n\n\t}\n\n\treturn Math.abs( tri.plane.normal.dot( UP_VECTOR ) ) <= EPSILON;\n\n}\n\n// Is the provided line exactly an edge on the triangle\nexport function isLineTriangleEdge( tri, line ) {\n\n\t// if this is the same line as on the triangle\n\tconst { start, end } = line;\n\tconst triPoints = tri.points;\n\tlet startMatches = false;\n\tlet endMatches = false;\n\tfor ( let i = 0; i < 3; i ++ ) {\n\n\t\tconst tp = triPoints[ i ];\n\t\tif ( ! startMatches && start.distanceToSquared( tp ) <= EPSILON ) {\n\n\t\t\tstartMatches = true;\n\n\t\t}\n\n\t\tif ( ! endMatches && end.distanceToSquared( tp ) <= EPSILON ) {\n\n\t\t\tendMatches = true;\n\n\t\t}\n\n\t\tif ( startMatches && endMatches ) {\n\n\t\t\treturn true;\n\n\t\t}\n\n\t}\n\n\treturn startMatches && endMatches;\n\n}\n"
  },
  {
    "path": "src/utils/trimToBeneathTriPlane.js",
    "content": "import { Plane, Vector3, MathUtils } from 'three';\n\nconst EPSILON = 1e-16;\nconst UP_VECTOR = /* @__PURE__ */ new Vector3( 0, 1, 0 );\nconst _plane = /* @__PURE__ */ new Plane();\nconst _planeHit = /* @__PURE__ */ new Vector3();\nconst _lineDirection = /* @__PURE__ */ new Vector3();\nexport function trimToBeneathTriPlane( tri, line, lineTarget ) {\n\n\t// update triangle if needed\n\tif ( tri.needsUpdate ) {\n\n\t\ttri.update();\n\n\t}\n\n\t// if the plane is not facing up then flip the direction\n\t_plane.copy( tri.plane );\n\tif ( _plane.normal.dot( UP_VECTOR ) < 0 ) {\n\n\t\t_plane.normal.multiplyScalar( - 1 );\n\t\t_plane.constant *= - 1;\n\n\t}\n\n\tconst startDist = _plane.distanceToPoint( line.start );\n\tconst endDist = _plane.distanceToPoint( line.end );\n\tconst isStartOnPlane = Math.abs( startDist ) < EPSILON;\n\tconst isEndOnPlane = Math.abs( endDist ) < EPSILON;\n\tconst isStartBelow = startDist < 0;\n\tconst isEndBelow = endDist < 0;\n\n\t// if the line and plane are coplanar then return that we can't trim\n\tline.delta( _lineDirection ).normalize();\n\tif ( Math.abs( _plane.normal.dot( _lineDirection ) ) < EPSILON ) {\n\n\t\t// if the line is definitely above or on the plane then skip it\n\t\tif ( isStartOnPlane || ! isStartBelow ) {\n\n\t\t\treturn false;\n\n\t\t} else {\n\n\t\t\tlineTarget.copy( line );\n\t\t\treturn true;\n\n\t\t}\n\n\t}\n\n\t// find the point that's below the plane. If both points are below the plane\n\t// then we assume we're dealing with floating point error\n\tif ( isStartBelow && isEndBelow ) {\n\n\t\t// if the whole line is below then just copy that\n\t\tlineTarget.copy( line );\n\t\treturn true;\n\n\t} else if ( ! isStartBelow && ! isEndBelow ) {\n\n\t\t// if it's wholly above then skip it\n\t\treturn false;\n\n\t} else {\n\n\t\tconst t = MathUtils.mapLinear( 0, startDist, endDist, 0, 1 );\n\t\tline.at( t, _planeHit );\n\n\t\tif ( isStartBelow ) {\n\n\t\t\tlineTarget.start.copy( line.start );\n\t\t\tlineTarget.end.copy( _planeHit );\n\t\t\treturn true;\n\n\t\t} else if ( isEndBelow ) {\n\n\t\t\tlineTarget.end.copy( line.end );\n\t\t\tlineTarget.start.copy( _planeHit );\n\t\t\treturn true;\n\n\t\t}\n\n\t}\n\n\treturn false;\n\n}\n"
  },
  {
    "path": "src/webgpu/MeshVisibilityCuller.js",
    "content": "/** @import { Object3D } from 'three' */\n/** @import { WebGPURenderer } from 'three/webgpu' */\nimport {\n\tBox3,\n\tVector3,\n\tVector4,\n\tOrthographicCamera,\n\tColor,\n\tMesh,\n} from 'three';\nimport { RenderTarget, MeshBasicNodeMaterial } from 'three/webgpu';\nimport { uniform } from 'three/tsl';\nimport { getAllMeshes } from '../utils/getAllMeshes.js';\n\n// RGBA8 ID encoding - supports up to 16,777,215 objects (2^24 - 1)\n// ID 0 is valid, background is indicated by alpha = 0\nfunction encodeId( id, target ) {\n\n\ttarget.x = ( id & 0xFF ) / 255;\n\ttarget.y = ( ( id >> 8 ) & 0xFF ) / 255;\n\ttarget.z = ( ( id >> 16 ) & 0xFF ) / 255;\n\ttarget.w = 1;\n\n}\n\nfunction decodeId( buffer, index ) {\n\n\treturn buffer[ index ] | ( buffer[ index + 1 ] << 8 ) | ( buffer[ index + 2 ] << 16 );\n\n}\n\n/**\n * Utility for determining visible geometry from a top down orthographic perspective. This can\n * be run before performing projection generation to reduce the complexity of the operation at\n * the cost of potentially missing small details.\n *\n * Takes the WebGPURenderer instance used to render.\n * @param {WebGPURenderer} renderer\n * @param {Object} [options]\n * @param {number} [options.pixelsPerMeter=0.1]\n */\nexport class MeshVisibilityCuller {\n\n\tconstructor( renderer, options = {} ) {\n\n\t\tconst { pixelsPerMeter = 0.1 } = options;\n\n\t\t/**\n\t\t * The size of a pixel on a single dimension. If this results in a texture larger than what\n\t\t * the graphics context can provide then the rendering is tiled.\n\t\t * @type {number}\n\t\t */\n\t\tthis.pixelsPerMeter = pixelsPerMeter;\n\t\tthis.renderer = renderer;\n\n\t}\n\n\t/**\n\t * Returns the set of meshes that are visible within the given object.\n\t * @param {Object3D|Array<Object3D>} object\n\t * @returns {Promise<Array<Object3D>>}\n\t */\n\tasync cull( objects ) {\n\n\t\tobjects = getAllMeshes( objects );\n\n\t\tconst { renderer, pixelsPerMeter } = this;\n\t\tconst size = new Vector3();\n\t\tconst camera = new OrthographicCamera();\n\t\tconst box = new Box3();\n\n\t\tconst idValue = new Vector4();\n\t\tconst idUniform = uniform( idValue );\n\t\tconst idMaterial = new MeshBasicNodeMaterial();\n\t\tidMaterial.colorNode = idUniform;\n\n\t\tconst idMesh = new Mesh( undefined, idMaterial );\n\t\tidMesh.matrixAutoUpdate = false;\n\t\tidMesh.matrixWorldAutoUpdate = false;\n\n\t\t// get the bounds of the image\n\t\tbox.makeEmpty();\n\t\tobjects.forEach( o => {\n\n\t\t\tbox.expandByObject( o );\n\n\t\t} );\n\n\t\t// get the bounds dimensions\n\t\tbox.getSize( size );\n\n\t\t// calculate the tile and target size\n\t\tconst maxTextureSize = Math.min( renderer.backend.device.limits.maxTextureDimension2D, 2 ** 13 );\n\t\tconst pixelWidth = Math.ceil( size.x / pixelsPerMeter );\n\t\tconst pixelHeight = Math.ceil( size.z / pixelsPerMeter );\n\t\tconst tilesX = Math.ceil( pixelWidth / maxTextureSize );\n\t\tconst tilesY = Math.ceil( pixelHeight / maxTextureSize );\n\n\t\tconst target = new RenderTarget( Math.ceil( pixelWidth / tilesX ), Math.ceil( pixelHeight / tilesY ) );\n\n\t\t// set the camera bounds\n\t\tcamera.rotation.x = - Math.PI / 2;\n\t\tcamera.far = ( box.max.y - box.min.y ) + camera.near;\n\t\tcamera.position.y = box.max.y + camera.near;\n\n\t\t// save render state\n\t\tconst color = renderer.getClearColor( new Color() );\n\t\tconst alpha = renderer.getClearAlpha();\n\t\tconst renderTarget = renderer.getRenderTarget();\n\t\tconst autoClear = renderer.autoClear;\n\n\t\t// render ids\n\t\tconst visibleSet = new Set();\n\t\tconst stepX = size.x / tilesX;\n\t\tconst stepZ = size.z / tilesY;\n\t\tfor ( let x = 0; x < tilesX; x ++ ) {\n\n\t\t\tfor ( let y = 0; y < tilesY; y ++ ) {\n\n\t\t\t\t// update camera\n\t\t\t\tcamera.left = box.min.x + stepX * x;\n\t\t\t\tcamera.top = - ( box.min.z + stepZ * y );\n\n\t\t\t\tcamera.right = camera.left + stepX;\n\t\t\t\tcamera.bottom = camera.top - stepZ;\n\n\t\t\t\tcamera.updateProjectionMatrix();\n\n\t\t\t\t// clear the render target\n\t\t\t\trenderer.autoClear = false;\n\t\t\t\trenderer.setClearColor( 0, 0 );\n\t\t\t\trenderer.setRenderTarget( target );\n\t\t\t\trenderer.clear();\n\n\t\t\t\tfor ( let i = 0; i < objects.length; i ++ ) {\n\n\t\t\t\t\tconst object = objects[ i ];\n\t\t\t\t\tidMesh.matrixWorld.copy( object.matrixWorld );\n\t\t\t\t\tidMesh.geometry = object.geometry;\n\n\t\t\t\t\tencodeId( i, idValue );\n\t\t\t\t\trenderer.render( idMesh, camera );\n\n\t\t\t\t}\n\n\t\t\t\t// reset render state before async operation to avoid corruption\n\t\t\t\trenderer.setClearColor( color, alpha );\n\t\t\t\trenderer.setRenderTarget( renderTarget );\n\t\t\t\trenderer.autoClear = autoClear;\n\n\t\t\t\tconst buffer = new Uint8Array( await renderer.readRenderTargetPixelsAsync( target, 0, 0, target.width, target.height ) );\n\n\t\t\t\t// find all visible objects - decode RGBA to ID\n\t\t\t\tfor ( let i = 0, l = buffer.length; i < l; i += 4 ) {\n\n\t\t\t\t\t// alpha = 0 indicates background (no object)\n\t\t\t\t\tif ( buffer[ i + 3 ] === 0 ) continue;\n\n\t\t\t\t\tconst id = decodeId( buffer, i );\n\t\t\t\t\tvisibleSet.add( objects[ id ] );\n\n\t\t\t\t}\n\n\t\t\t}\n\n\t\t}\n\n\t\t// dispose of intermediate values\n\t\tidMaterial.dispose();\n\t\ttarget.dispose();\n\n\t\treturn Array.from( visibleSet );\n\n\t}\n\n}\n"
  },
  {
    "path": "src/webgpu/ProjectionGenerator.js",
    "content": "/** @import { Object3D, BufferGeometry } from 'three' */\n/** @import { WebGPURenderer } from 'three/webgpu' */\nimport { IndirectStorageBufferAttribute, ReadbackBuffer, StorageBufferAttribute } from 'three/webgpu';\nimport { storage } from 'three/tsl';\nimport { getAllMeshes } from '../utils/getAllMeshes.js';\nimport { EdgeGenerator } from '../EdgeGenerator.js';\nimport { isYProjectedLineDegenerate } from '../utils/triangleLineUtils.js';\nimport { ProjectionGeneratorBVHComputeData } from './ProjectionGeneratorBVHComputeData.js';\nimport { edgeStruct, overlapRecordStruct } from './nodes/structs.wgsl.js';\nimport { EdgeOverlapsKernel } from './kernels/EdgeOverlapsKernel.js';\nimport { overlapsToLines } from '../utils/overlapUtils.js';\nimport { insertOverlap } from '../utils/getProjectedOverlaps.js';\nimport { ProjectionResult } from '../ProjectionGenerator.js';\nimport { ZeroOutBufferKernel } from './kernels/ZeroOutBufferKernel.js';\nimport { nextFrame } from '../utils/nextFrame.js';\n\n// TODO: Consider storing the ranges with multiple edges clipped per thread to reduce the array size needed\n\nconst MAX_BUFFER_SIZE = 134217728;\n\nconst MAX_OVERLAPS_COUNT = Math.floor( MAX_BUFFER_SIZE / ( overlapRecordStruct.getLength() * 4 ) );\n\n/**\n * @callback ProjectionProgressCallback\n * @param {number} percent\n * @param {string} message\n */\n\n/**\n * Takes the WebGPURenderer instance used to run compute kernels.\n * @param {WebGPURenderer} renderer\n */\nexport class ProjectionGenerator {\n\n\tconstructor( renderer ) {\n\n\t\tthis.renderer = renderer;\n\n\t\t/**\n\t\t * The threshold angle in degrees at which edges are generated.\n\t\t * @type {number}\n\t\t * @default 50\n\t\t */\n\t\tthis.angleThreshold = 50;\n\n\t\t/**\n\t\t * The number of edges to process in one compute kernel pass. Larger values can process\n\t\t * faster but may cause internal buffers to overflow, resulting in extra kernel executions,\n\t\t * taking more time.\n\t\t * @type {number}\n\t\t * @default 100000\n\t\t */\n\t\tthis.batchSize = 100000;\n\n\t\t/**\n\t\t * Whether to generate edges representing the intersections between triangles.\n\t\t * @type {boolean}\n\t\t * @default true\n\t\t */\n\t\tthis.includeIntersectionEdges = true;\n\n\t\t/**\n\t\t * How long to spend generating edges.\n\t\t * @type {number}\n\t\t * @default 300\n\t\t */\n\t\tthis.iterationTime = 300;\n\n\t\t/**\n\t\t * How many compute jobs to perform in parallel.\n\t\t * @type {number}\n\t\t * @default 3\n\t\t */\n\t\tthis.parallelJobs = 3;\n\n\t}\n\n\t/**\n\t * Asynchronously generate the edge geometry result.\n\t * @param {Object3D|BufferGeometry|Array<Object3D>} scene\n\t * @param {Object} [options]\n\t * @param {ProjectionProgressCallback} [options.onProgress]\n\t * @param {AbortSignal} [options.signal]\n\t * @returns {Promise<ProjectionResult>}\n\t */\n\tasync generate( scene, options = {} ) {\n\n\t\tconst { renderer, angleThreshold, includeIntersectionEdges, batchSize, iterationTime, parallelJobs } = this;\n\t\tconst { onProgress = null, signal = null } = options;\n\n\t\t// collect meshes\n\t\tconst meshes = getAllMeshes( scene );\n\n\t\t// generate edges\n\t\tconst edgeGenerator = new EdgeGenerator();\n\t\tedgeGenerator.thresholdAngle = angleThreshold;\n\t\tedgeGenerator.iterationTime = iterationTime;\n\n\t\t// adjust the offset to account for floating point error in the edge processing and intersections.\n\t\t// NOTE: Ideally we should be applying this relative to the scale of the values being used rather that\n\t\t// using a fixed offset.\n\t\tedgeGenerator.yOffset = 5 * 1e-5;\n\n\t\tif ( onProgress ) {\n\n\t\t\tonProgress( 0, 'Generating Edges' );\n\n\t\t}\n\n\t\tlet edges = [];\n\t\tawait edgeGenerator.getEdgesAsync( scene, edges );\n\t\tsignal?.throwIfAborted();\n\n\t\tif ( includeIntersectionEdges ) {\n\n\t\t\tif ( onProgress ) {\n\n\t\t\t\tonProgress( 0, 'Generating Intersection Edges' );\n\n\t\t\t}\n\n\t\t\tawait edgeGenerator.getIntersectionEdgesAsync( scene, edges );\n\t\t\tsignal?.throwIfAborted();\n\n\t\t}\n\n\t\tedges = edges.filter( e => ! isYProjectedLineDegenerate( e ) );\n\n\t\tif ( edges.length === 0 ) {\n\n\t\t\treturn new ProjectionResult();\n\n\t\t}\n\n\t\tonProgress( 0, 'Projecting Edges' );\n\n\t\t//\n\n\t\t// allocate a buffer of edges for at most the requested capacity\n\t\tconst batchCapacity = Math.min( batchSize, edges.length );\n\t\tconst edgeBufferData = new Float32Array( batchCapacity * edgeStruct.getLength() );\n\t\tconst edgeBufferDataU32 = new Uint32Array( edgeBufferData.buffer );\n\t\tconst edgeBufferAttribute = new StorageBufferAttribute( edgeBufferData, edgeStruct.getLength() );\n\n\t\t// overlap output buffer and atomic counter\n\t\tconst overlapsAttribute = new IndirectStorageBufferAttribute( MAX_OVERLAPS_COUNT, overlapRecordStruct.getLength(), Uint32Array );\n\t\tconst bufferPointersAttribute = new IndirectStorageBufferAttribute( 1, 1 );\n\t\tconst overflowFlagAttribute = new IndirectStorageBufferAttribute( 1, 1 );\n\n\t\tconst overlapsStorage = storage( overlapsAttribute, overlapRecordStruct ).setName( 'overlaps' );\n\t\tconst bufferPointersStorage = storage( bufferPointersAttribute, 'uint' ).toAtomic();\n\t\tconst overflowFlagStorage = storage( overflowFlagAttribute, 'uint' ).setName( 'overflowFlag' ).toAtomic();\n\n\t\t//\n\n\t\t// set up scene data\n\t\tconst bvhComputeData = new ProjectionGeneratorBVHComputeData( meshes );\n\t\tbvhComputeData.update();\n\t\tbvhComputeData.fns.collectEdgeOverlaps = bvhComputeData.getCollectEdgeOverlapsFn( {\n\t\t\toverlapsStorage: overlapsStorage,\n\t\t\tbufferPointersStorage: bufferPointersStorage,\n\t\t\toverflowFlagStorage: overflowFlagStorage,\n\t\t} );\n\n\t\t// initialize kernels\n\t\tconst edgeOverlapsKernel = new EdgeOverlapsKernel();\n\t\tedgeOverlapsKernel.setWorkgroupSize( 64, 1, 1 );\n\t\tedgeOverlapsKernel.edges = edgeBufferAttribute;\n\t\tedgeOverlapsKernel.bvhData = bvhComputeData;\n\n\t\tconst zeroOutKernel = new ZeroOutBufferKernel();\n\t\tzeroOutKernel.setWorkgroupSize( 1, 1, 1 );\n\n\t\t//\n\t\tconst intervalsByEdge = new Map();\n\t\tlet progress = 0;\n\t\tconst promises = [];\n\t\tconst edgeStructStride = edgeStruct.getLength();\n\n\t\t// register abort callback\n\t\tconst onAbort = () => jobQueue.cancelAll();\n\t\tsignal?.addEventListener( 'abort', onAbort );\n\n\t\t// job queue and readback buffers to save memory, improve performance\n\t\tconst readbackBufferPool = [];\n\t\tconst jobQueue = new JobQueue();\n\t\tjobQueue.maxJobs = parallelJobs;\n\n\t\tconst runJob = async ( start, count ) => {\n\n\t\t\tif ( signal?.aborted ) {\n\n\t\t\t\treturn;\n\n\t\t\t}\n\n\t\t\t// fill out the edges array\n\t\t\tfor ( let i = 0; i < count; i ++ ) {\n\n\t\t\t\tconst edge = edges[ start + i ];\n\t\t\t\tconst offset = i * edgeStructStride;\n\t\t\t\tedge.start.toArray( edgeBufferData, offset );\n\t\t\t\tedge.end.toArray( edgeBufferData, offset + 3 );\n\t\t\t\tedgeBufferDataU32[ offset + 6 ] = i;\n\n\t\t\t}\n\n\t\t\tedgeBufferAttribute.needsUpdate = true;\n\n\t\t\t// clear the overlaps counter and overflow flag\n\t\t\tzeroOutKernel.target = bufferPointersAttribute;\n\t\t\trenderer.compute( zeroOutKernel.kernel, [ 1, 1, 1 ] );\n\n\t\t\tzeroOutKernel.target = overflowFlagAttribute;\n\t\t\trenderer.compute( zeroOutKernel.kernel, [ 1, 1, 1 ] );\n\n\t\t\t// traverse BVH and write overlaps directly\n\t\t\tedgeOverlapsKernel.edgesToProcess = count;\n\t\t\trenderer.compute( edgeOverlapsKernel.kernel, edgeOverlapsKernel.getDispatchSize( count ) );\n\n\t\t\tlet readbackBuffer;\n\t\t\tif ( readbackBufferPool.length !== 0 ) {\n\n\t\t\t\treadbackBuffer = readbackBufferPool.pop();\n\n\t\t\t} else {\n\n\t\t\t\treadbackBuffer = new ReadbackBuffer( MAX_BUFFER_SIZE );\n\n\t\t\t}\n\n\t\t\tconst [ overlaps, bufferPointers, overflowBuffer ] = await Promise.all( [\n\t\t\t\trenderer.getArrayBufferAsync( overlapsAttribute, readbackBuffer ),\n\t\t\t\trenderer.getArrayBufferAsync( bufferPointersAttribute ),\n\t\t\t\trenderer.getArrayBufferAsync( overflowFlagAttribute ),\n\t\t\t] );\n\n\t\t\t// add the readback buffer back to the pool if we've aborted this run\n\t\t\tif ( signal?.aborted ) {\n\n\t\t\t\treadbackBuffer.release();\n\t\t\t\treadbackBufferPool.push( readbackBuffer );\n\t\t\t\treturn;\n\n\t\t\t}\n\n\t\t\tconst overflow = new Uint32Array( overflowBuffer )[ 0 ];\n\t\t\tif ( overflow > 0 ) {\n\n\t\t\t\tif ( count === 1 ) {\n\n\t\t\t\t\tconsole.error( `ProjectionGenerator: Overlaps buffer insufficient size to store all segments. Please report to three-edge-projection.` );\n\n\t\t\t\t} else {\n\n\t\t\t\t\t// split the job in half and re-queue both halves\n\t\t\t\t\tconst half = Math.ceil( count / 2 );\n\t\t\t\t\tpromises.push( jobQueue.add( runJob, [ start, half ] ) );\n\t\t\t\t\tpromises.push( jobQueue.add( runJob, [ start + half, count - half ] ) );\n\t\t\t\t\treadbackBuffer.release();\n\t\t\t\t\treadbackBufferPool.push( readbackBuffer );\n\t\t\t\t\treturn;\n\n\t\t\t\t}\n\n\t\t\t}\n\n\t\t\t// read buffers\n\t\t\tconst overlapsF32 = new Float32Array( overlaps.buffer );\n\t\t\tconst overlapsU32 = new Uint32Array( overlaps.buffer );\n\t\t\tconst bufferPointersU32 = new Uint32Array( bufferPointers );\n\t\t\tconst stride = overlapRecordStruct.getLength();\n\n\t\t\t// push the overlaps\n\t\t\tfor ( let oi = 0, ol = bufferPointersU32[ 0 ]; oi < ol; oi ++ ) {\n\n\t\t\t\tconst index = oi * stride;\n\t\t\t\tconst ei = start + overlapsU32[ index + 0 ];\n\t\t\t\tconst t0 = overlapsF32[ index + 1 ];\n\t\t\t\tconst t1 = overlapsF32[ index + 2 ];\n\n\t\t\t\tif ( ! intervalsByEdge.has( ei ) ) {\n\n\t\t\t\t\tintervalsByEdge.set( ei, [] );\n\n\t\t\t\t}\n\n\t\t\t\tinsertOverlap( [ t0, t1 ], intervalsByEdge.get( ei ) );\n\n\t\t\t}\n\n\t\t\tprogress += count;\n\n\t\t\t// fire progress\n\t\t\tif ( onProgress ) {\n\n\t\t\t\tonProgress( progress / edges.length, 'Projecting Edges' );\n\n\t\t\t}\n\n\t\t\t// release the buffer to the pool\n\t\t\treadbackBuffer.release();\n\t\t\treadbackBufferPool.push( readbackBuffer );\n\n\t\t};\n\n\t\t// enqueue initial jobs\n\t\tfor ( let e = 0; e < edges.length; e += batchCapacity ) {\n\n\t\t\tpromises.push( jobQueue.add( runJob, [ e, Math.min( batchCapacity, edges.length - e ) ] ) );\n\n\t\t}\n\n\t\t// drain — sequential iteration naturally picks up overflow sub-jobs added to promises\n\t\ttry {\n\n\t\t\tfor ( let i = 0; i < promises.length; i ++ ) {\n\n\t\t\t\tawait promises[ i ];\n\n\t\t\t}\n\n\t\t} finally {\n\n\t\t\tsignal?.removeEventListener( 'abort', onAbort );\n\t\t\t// overlapsAttribute.dispose();\n\t\t\t// bufferPointersAttribute.dispose();\n\t\t\t// overflowFlagAttribute.dispose();\n\t\t\t// edgeBufferAttribute.dispose();\n\n\t\t\t// dispose of all the readback buffers\n\t\t\treadbackBufferPool.forEach( rb => rb.dispose() );\n\n\t\t}\n\n\t\tsignal?.throwIfAborted();\n\n\t\t// push all edges to the \"results\" object\n\t\tconst collector = new ProjectionResult();\n\t\tfor ( let i = 0; i < edges.length; i ++ ) {\n\n\t\t\tconst mesh = edges[ i ].mesh;\n\t\t\tif ( ! collector.visibleEdges.meshToSegments.has( mesh ) ) {\n\n\t\t\t\tcollector.visibleEdges.meshToSegments.set( mesh, [] );\n\t\t\t\tcollector.hiddenEdges.meshToSegments.set( mesh, [] );\n\n\t\t\t}\n\n\t\t\tconst intervals = intervalsByEdge.get( i ) || [];\n\t\t\toverlapsToLines( edges[ i ], intervals, false, collector.visibleEdges.meshToSegments.get( mesh ) );\n\t\t\toverlapsToLines( edges[ i ], intervals, true, collector.hiddenEdges.meshToSegments.get( mesh ) );\n\n\t\t}\n\n\t\treturn collector;\n\n\t}\n\n}\n\nclass JobQueue {\n\n\tconstructor() {\n\n\t\tthis.queue = [];\n\t\tthis.maxJobs = 3;\n\t\tthis.currJobs = 0;\n\t\tthis._scheduled = false;\n\n\t}\n\n\tadd( cb, args ) {\n\n\t\treturn new Promise( ( resolve, reject ) => {\n\n\t\t\tthis.queue.push( {\n\t\t\t\trun: () => {\n\n\t\t\t\t\tconst res = cb( ...args );\n\t\t\t\t\tres\n\t\t\t\t\t\t.then( resolve )\n\t\t\t\t\t\t.catch( reject );\n\n\t\t\t\t\treturn res;\n\n\t\t\t\t},\n\t\t\t\treject,\n\t\t\t} );\n\t\t\tthis.scheduleRun();\n\n\t\t} );\n\n\t}\n\n\tcancelAll() {\n\n\t\tconst { queue } = this;\n\t\twhile ( queue.length > 0 ) {\n\n\t\t\tconst entry = queue.shift();\n\t\t\tentry.reject( new Error( 'JobQueue: cancelled' ) );\n\n\t\t}\n\n\t}\n\n\tasync runJobs() {\n\n\t\tconst { queue } = this;\n\t\twhile ( this.currJobs < this.maxJobs ) {\n\n\t\t\tif ( queue.length === 0 ) {\n\n\t\t\t\treturn;\n\n\t\t\t}\n\n\t\t\tthis.currJobs ++;\n\n\t\t\tawait nextFrame();\n\n\t\t\tconst entry = queue.shift();\n\t\t\tentry.run()\n\t\t\t\t.finally( () => {\n\n\t\t\t\t\tthis.currJobs --;\n\t\t\t\t\tthis.scheduleRun();\n\n\t\t\t\t} );\n\n\t\t}\n\n\t}\n\n\tscheduleRun() {\n\n\t\tif ( this._scheduled ) {\n\n\t\t\treturn;\n\n\t\t}\n\n\t\tthis._scheduled = true;\n\t\trequestAnimationFrame( async () => {\n\n\t\t\tawait this.runJobs();\n\t\t\tthis._scheduled = false;\n\n\t\t} );\n\n\t}\n\n}\n"
  },
  {
    "path": "src/webgpu/ProjectionGeneratorBVHComputeData.js",
    "content": "import { BackSide, DoubleSide, FrontSide } from 'three';\nimport { StructTypeNode } from 'three/webgpu';\nimport { BVHComputeData } from './lib/BVHComputeData.js';\nimport { wgslTagFn } from './lib/nodes/WGSLTagFnNode.js';\nimport { bvhNodeBoundsStruct } from './lib/wgsl/structs.wgsl.js';\nimport { transformBVHBounds } from './nodes/utils.wgsl.js';\nimport { constants as overlapConstants } from './nodes/common.wgsl.js';\nimport { getProjectedOverlapRange, isLineTriangleEdge, trimToBeneathTriPlane } from './nodes/overlapFunctions.wgsl.js';\nimport { LineWGSL, TriWGSL } from './nodes/primitives.js';\n\n// Shape struct carrying world-space line endpoints plus the object-to-world\n// matrix (set by transformShapeFn; identity at top level so world-space\n// bounds pass through unchanged) and the transform buffer index.\nconst edgeLineShapeStruct = new StructTypeNode( {\n\tworldStart: 'vec3f',\n\tworldEnd: 'vec3f',\n\tmatrixWorld: 'mat4x4f',\n\tobjectIndex: 'uint',\n\tedgeIndex: 'uint',\n}, 'EdgeLineShape' );\n\n// Extended transform struct that adds a per-object \"side\" field for back-face\n// culling. Values: 0 = DoubleSide (no cull), 1 = FrontSide, -1 = BackSide.\nconst projectionTransformStruct = new StructTypeNode( {\n\tmatrixWorld: 'mat4x4f',\n\tinverseMatrixWorld: 'mat4x4f',\n\tnodeOffset: 'uint',\n\tvisible: 'uint',\n\tside: 'int',\n\t_alignment0: 'uint',\n}, 'ProjectionTransformStruct' );\n\n// Projection-generator-specific BVHComputeData that only requires position\n// attributes and auto-generates missing BVHs.\nexport class ProjectionGeneratorBVHComputeData extends BVHComputeData {\n\n\tconstructor( bvh, options = {} ) {\n\n\t\tsuper( bvh, {\n\t\t\tattributes: { position: 'vec4f' },\n\t\t\t...options,\n\t\t} );\n\n\t\tthis.bvhMap = new Map();\n\t\tthis.structs.transform = projectionTransformStruct;\n\t\tthis._sharedFns = null;\n\t\tthis._fns = null;\n\n\t}\n\n\twriteTransformData( info, premultiplyMatrix, writeOffset, targetBuffer ) {\n\n\t\tsuper.writeTransformData( info, premultiplyMatrix, writeOffset, targetBuffer );\n\n\t\tconst { object, root } = info;\n\t\tlet material = object.material;\n\t\tif ( Array.isArray( material ) ) {\n\n\t\t\tmaterial = material[ object.geometry.groups[ root ].materialIndex ];\n\n\t\t}\n\n\t\tlet sideValue;\n\t\tswitch ( material.side ) {\n\n\t\t\tcase DoubleSide:\n\t\t\t\tsideValue = 0;\n\t\t\t\tbreak;\n\t\t\tcase FrontSide:\n\t\t\t\tsideValue = 1;\n\t\t\t\tbreak;\n\t\t\tcase BackSide:\n\t\t\t\tsideValue = - 1;\n\t\t\t\tbreak;\n\n\t\t}\n\n\t\tconst transformBufferU32 = new Uint32Array( targetBuffer );\n\t\ttransformBufferU32[ writeOffset * projectionTransformStruct.getLength() + 34 ] = sideValue;\n\n\t}\n\n\tupdate() {\n\n\t\tsuper.update();\n\t\tthis.bvhMap.clear();\n\t\tthis._sharedFns = null;\n\t\tthis._fns = null;\n\n\t}\n\n\t// Returns a WGSL function — fn traverse( edgeIndex, lineStart, lineEnd ) -> void —\n\t// that traverses the BVH for one edge and writes qualifying { edgeIndex, objectIndex, triIndex }\n\t// records to the pairs buffer using atomic slot claiming.\n\t//\n\t// pairsCountsStorage is a 2-element array<atomic<u32>>:\n\t//   [0] write offset — claimed unconditionally via atomicAdd\n\t//   [1] dispatch count — incremented only when the claimed slot is within capacity; equals\n\t//       the number of valid pair records written and is used as K3's dispatch bound\n\t//\n\t// overflowFlagStorage is a 1-element array<atomic<u32>> that accumulates the number of\n\t// pairs that could not be written due to buffer overflow.\n\t//\n\t// NOTE: pairsCountsStorage must be bound as array<atomic<u32>> (read_write storage).\n\tgetCollectEdgeOverlapsFn( { overlapsStorage, bufferPointersStorage, overflowFlagStorage } ) {\n\n\t\tconst { storage } = this;\n\t\tconst { DOUBLE_SIDE, BACK_SIDE, DIST_THRESHOLD } = overlapConstants;\n\n\t\tconst intersectsBoundsFn = wgslTagFn/* wgsl */`\n\t\t\tfn intersectsBounds( shape: ${ edgeLineShapeStruct }, bounds: ${ bvhNodeBoundsStruct } ) -> u32 {\n\n\t\t\t\t// TODO: a proper 3D Line / AABB check with the bottom of the bounds extended downward\n\t\t\t\t// would be best here since we are getting some false positives.\n\n\t\t\t\t// Transform bounds to world space. At the top level the shape matrix\n\t\t\t\t// is identity, so world-space bounds pass through unchanged.\n\t\t\t\tlet aabb = ${ transformBVHBounds }( bounds, shape.matrixWorld );\n\t\t\t\tlet aabbMin = vec3( aabb.min[ 0 ], aabb.min[ 1 ], aabb.min[ 2 ] );\n\t\t\t\tlet aabbMax = vec3( aabb.max[ 0 ], aabb.max[ 1 ], aabb.max[ 2 ] );\n\n\t\t\t\t// Y-cull: bounds entirely below the line\n\t\t\t\tif ( aabbMax.y <= min( shape.worldStart.y, shape.worldEnd.y ) ) {\n\n\t\t\t\t\treturn 0u;\n\n\t\t\t\t}\n\n\t\t\t\t// AABB vs AABB test\n\t\t\t\tlet lineMinX = min( shape.worldStart.x, shape.worldEnd.x );\n\t\t\t\tlet lineMaxX = max( shape.worldStart.x, shape.worldEnd.x );\n\t\t\t\tlet lineMinZ = min( shape.worldStart.z, shape.worldEnd.z );\n\t\t\t\tlet lineMaxZ = max( shape.worldStart.z, shape.worldEnd.z );\n\t\t\t\tif (\n\t\t\t\t\taabbMax.x < lineMinX || aabbMin.x > lineMaxX ||\n\t\t\t\t\taabbMax.z < lineMinZ || aabbMin.z > lineMaxZ\n\t\t\t\t) {\n\n\t\t\t\t\treturn 0u;\n\n\t\t\t\t}\n\n\t\t\t\t// edge SAT axis\n\t\t\t\tlet segDelta = shape.worldEnd.xz - shape.worldStart.xz;\n\t\t\t\tlet segNormal = vec2f( - segDelta.y, segDelta.x );\n\t\t\t\tlet segProj = dot( segNormal, vec2f( shape.worldStart.x, shape.worldStart.z ) );\n\n\t\t\t\tlet aabbCenter = ( aabbMin.xz + aabbMax.xz ) * 0.5;\n\t\t\t\tlet aabbHalf = ( aabbMax.xz - aabbMin.xz ) * 0.5;\n\n\t\t\t\tlet aabbCenterProj = dot( segNormal, aabbCenter );\n\t\t\t\tlet aabbHalfProj = dot( abs( segNormal ), aabbHalf );\n\n\t\t\t\tif ( abs( aabbCenterProj - segProj ) > aabbHalfProj ) {\n\n\t\t\t\t\treturn 0u;\n\n\t\t\t\t}\n\n\t\t\t\treturn 1u;\n\n\t\t\t}\n\t\t`;\n\n\t\tconst transformShapeFn = wgslTagFn/* wgsl */`\n\t\t\tfn transformShape( localShape: ptr<function, ${ edgeLineShapeStruct }>, objectIndex: u32 ) -> void {\n\n\t\t\t\tlocalShape.matrixWorld = ${ storage.transforms }[ objectIndex ].matrixWorld;\n\t\t\t\tlocalShape.objectIndex = objectIndex;\n\n\t\t\t}\n\t\t`;\n\n\t\tconst intersectRangeFn = wgslTagFn/* wgsl */`\n\t\t\tfn traverseRange( shape: ${ edgeLineShapeStruct }, offset: u32, count: u32 ) -> bool {\n\n\t\t\t\tvar tri: ${ TriWGSL.struct };\n\t\t\t\tvar line: ${ LineWGSL.struct };\n\t\t\t\tline.start = shape.worldStart;\n\t\t\t\tline.end = shape.worldEnd;\n\n\t\t\t\tlet lineMinY = min( line.start.y, line.end.y );\n\t\t\t\tlet lineMaxY = max( line.start.y, line.end.y );\n\n\t\t\t\tlet matrixWorld = shape.matrixWorld;\n\t\t\t\tlet side = ${ storage.transforms }[ shape.objectIndex ].side;\n\t\t\t\tlet inverted = determinant( matrixWorld ) < 0.0;\n\n\t\t\t\tfor ( var ti = offset; ti < offset + count; ti = ti + 1u ) {\n\n\t\t\t\t\tlet i0 = ${ storage.index }[ ti * 3u + 0u ];\n\t\t\t\t\tlet i1 = ${ storage.index }[ ti * 3u + 1u ];\n\t\t\t\t\tlet i2 = ${ storage.index }[ ti * 3u + 2u ];\n\n\t\t\t\t\tlet ta = matrixWorld * vec4f( ${ storage.attributes }[ i0 ].position.xyz, 1.0 );\n\t\t\t\t\tlet tb = matrixWorld * vec4f( ${ storage.attributes }[ i1 ].position.xyz, 1.0 );\n\t\t\t\t\tlet tc = matrixWorld * vec4f( ${ storage.attributes }[ i2 ].position.xyz, 1.0 );\n\t\t\t\t\ttri.a = ta.xyz / ta.w;\n\t\t\t\t\ttri.b = tb.xyz / tb.w;\n\t\t\t\t\ttri.c = tc.xyz / tc.w;\n\n\t\t\t\t\t// back-face cull\n\t\t\t\t\tif ( side != ${ DOUBLE_SIDE } ) {\n\n\t\t\t\t\t\tlet triNormal = ${ TriWGSL.getNormal }( tri );\n\t\t\t\t\t\tlet faceUp = ( triNormal.y > 0.0 ) != inverted;\n\t\t\t\t\t\tif ( faceUp == ( side == ${ BACK_SIDE } ) ) {\n\n\t\t\t\t\t\t\tcontinue;\n\n\t\t\t\t\t\t}\n\n\t\t\t\t\t}\n\n\t\t\t\t\tlet triMaxY = max( max( tri.a.y, tri.b.y ), tri.c.y );\n\t\t\t\t\tlet triMinY = min( min( tri.a.y, tri.b.y ), tri.c.y );\n\n\t\t\t\t\t// skip triangles entirely below the edge\n\t\t\t\t\tif ( triMaxY <= lineMinY ) {\n\n\t\t\t\t\t\tcontinue;\n\n\t\t\t\t\t}\n\n\t\t\t\t\t// skip if the edge lies on this triangle\n\t\t\t\t\tif ( ${ isLineTriangleEdge }( tri, line ) ) {\n\n\t\t\t\t\t\tcontinue;\n\n\t\t\t\t\t}\n\n\t\t\t\t\t// trim edge to the portion below the triangle plane; if the\n\t\t\t\t\t// entire line is already below the triangle, use the full line\n\t\t\t\t\tvar beneathLine: ${ LineWGSL.struct };\n\t\t\t\t\tif ( lineMaxY < triMinY ) {\n\n\t\t\t\t\t\tbeneathLine = line;\n\n\t\t\t\t\t} else if ( ! ${ trimToBeneathTriPlane }( tri, line, &beneathLine ) ) {\n\n\t\t\t\t\t\tcontinue;\n\n\t\t\t\t\t}\n\n\t\t\t\t\t// skip degenerate trimmed segments\n\t\t\t\t\t// TODO: add a \"distant\" utility function\n\t\t\t\t\tif ( length( beneathLine.end - beneathLine.start ) < ${ DIST_THRESHOLD } ) {\n\n\t\t\t\t\t\tcontinue;\n\n\t\t\t\t\t}\n\n\t\t\t\t\tvar overlapLine: ${ LineWGSL.struct };\n\t\t\t\t\tif ( ! ${ getProjectedOverlapRange }( beneathLine, tri, &overlapLine ) ) {\n\n\t\t\t\t\t\tcontinue;\n\n\t\t\t\t\t}\n\n\t\t\t\t\t// compute t0/t1 parametric positions along the original edge\n\t\t\t\t\tlet lineDir = line.end - line.start;\n\t\t\t\t\tlet lineLen = length( lineDir );\n\t\t\t\t\tvar t0 = length( overlapLine.start - line.start ) / lineLen;\n\t\t\t\t\tvar t1 = length( overlapLine.end - line.start ) / lineLen;\n\t\t\t\t\tt0 = clamp( t0, 0.0, 1.0 );\n\t\t\t\t\tt1 = clamp( t1, 0.0, 1.0 );\n\n\t\t\t\t\tif ( abs( t0 - t1 ) <= ${ DIST_THRESHOLD } ) {\n\n\t\t\t\t\t\tcontinue;\n\n\t\t\t\t\t}\n\n\t\t\t\t\t// claim a slot and write the overlap record directly\n\t\t\t\t\tlet slot = atomicAdd( &${ bufferPointersStorage }[ 0 ], 1u );\n\t\t\t\t\tif ( slot < arrayLength( &${ overlapsStorage } ) ) {\n\n\t\t\t\t\t\t${ overlapsStorage }[ slot ].edgeIndex = shape.edgeIndex;\n\t\t\t\t\t\t${ overlapsStorage }[ slot ].t0 = t0;\n\t\t\t\t\t\t${ overlapsStorage }[ slot ].t1 = t1;\n\n\t\t\t\t\t} else {\n\n\t\t\t\t\t\tatomicAdd( &${ overflowFlagStorage }[ 0 ], 1u );\n\n\t\t\t\t\t}\n\n\t\t\t\t}\n\n\t\t\t\treturn false;\n\n\t\t\t}\n\t\t`;\n\n\t\tconst traversalFn = this.getShapecastFn( {\n\t\t\tname: 'collectEdgeOverlaps',\n\t\t\tshapeStruct: edgeLineShapeStruct,\n\t\t\tintersectsBoundsFn,\n\t\t\tintersectRangeFn,\n\t\t\ttransformShapeFn,\n\t\t} );\n\n\t\treturn wgslTagFn/* wgsl */`\n\t\t\tfn traverse( edgeIndex: u32, lineStart: vec3f, lineEnd: vec3f ) -> void {\n\n\t\t\t\tvar shape: ${ edgeLineShapeStruct };\n\t\t\t\tshape.worldStart = lineStart;\n\t\t\t\tshape.worldEnd = lineEnd;\n\t\t\t\tshape.matrixWorld = mat4x4f(\n\t\t\t\t\t1.0, 0.0, 0.0, 0.0,\n\t\t\t\t\t0.0, 1.0, 0.0, 0.0,\n\t\t\t\t\t0.0, 0.0, 1.0, 0.0,\n\t\t\t\t\t0.0, 0.0, 0.0, 1.0\n\t\t\t\t);\n\t\t\t\tshape.objectIndex = 0u;\n\t\t\t\tshape.edgeIndex = edgeIndex;\n\n\t\t\t\t${ traversalFn }( shape );\n\n\t\t\t}\n\t\t`;\n\n\t}\n\n}\n"
  },
  {
    "path": "src/webgpu/index.js",
    "content": "export * from './ProjectionGenerator.js';\nexport * from './MeshVisibilityCuller.js';\n"
  },
  {
    "path": "src/webgpu/kernels/EdgeOverlapsKernel.js",
    "content": "import { globalId, storage, uniform } from 'three/tsl';\nimport { wgslTagFn } from '../lib/nodes/WGSLTagFnNode.js';\nimport { ComputeKernel } from '../utils/ComputeKernel.js';\nimport { proxyFn } from '../lib/nodes/NodeProxy.js';\nimport { StorageBufferAttribute } from 'three/webgpu';\nimport { edgeStruct } from '../nodes/structs.wgsl.js';\n\n// One thread per edge — traverses the BVH and writes overlap intervals directly\n// to the overlaps buffer via atomic slot claiming.\nexport class EdgeOverlapsKernel extends ComputeKernel {\n\n\tconstructor() {\n\n\t\tconst params = {\n\t\t\tbvhData: { value: null },\n\t\t\tglobalId: globalId,\n\t\t\tedgesToProcess: uniform( 1, 'uint' ),\n\t\t\tedges: storage( new StorageBufferAttribute( 1, 1, Uint32Array ), edgeStruct ).toReadOnly().setName( 'edges' ),\n\t\t};\n\n\t\tconst edges = params.edges;\n\t\tconst traversalFn = proxyFn( 'bvhData.value.fns.collectEdgeOverlaps', params );\n\t\tconst shader = wgslTagFn/* wgsl */`\n\t\t\tfn compute( globalId: vec3u, edgesToProcess: u32 ) -> void {\n\n\t\t\t\tlet edgeIndex = globalId.x;\n\t\t\t\tlet edgeListLength = arrayLength( &${ edges } );\n\t\t\t\tif ( edgeIndex >= edgeListLength || edgeIndex >= edgesToProcess ) {\n\n\t\t\t\t\treturn;\n\n\t\t\t\t}\n\n\t\t\t\tlet edgeStart = vec3f(\n\t\t\t\t\t${ edges }[ edgeIndex ].start[ 0 ],\n\t\t\t\t\t${ edges }[ edgeIndex ].start[ 1 ],\n\t\t\t\t\t${ edges }[ edgeIndex ].start[ 2 ]\n\t\t\t\t);\n\t\t\t\tlet edgeEnd = vec3f(\n\t\t\t\t\t${ edges }[ edgeIndex ].end[ 0 ],\n\t\t\t\t\t${ edges }[ edgeIndex ].end[ 1 ],\n\t\t\t\t\t${ edges }[ edgeIndex ].end[ 2 ]\n\t\t\t\t);\n\n\t\t\t\t${ traversalFn }( edgeIndex, edgeStart, edgeEnd );\n\n\t\t\t}\n\t\t`;\n\n\t\tsuper( shader( params ) );\n\t\tthis.defineUniformAccessors( params );\n\n\t}\n\n}\n"
  },
  {
    "path": "src/webgpu/kernels/ZeroOutBufferKernel.js",
    "content": "import { IndirectStorageBufferAttribute } from 'three/webgpu';\nimport { storage, globalId } from 'three/tsl';\nimport { wgslTagFn } from '../lib/nodes/WGSLTagFnNode.js';\nimport { ComputeKernel } from '../utils/ComputeKernel.js';\n\nexport class ZeroOutBufferKernel extends ComputeKernel {\n\n\tconstructor( options = {} ) {\n\n\t\tconst {\n\t\t\ttype = 'u32',\n\t\t} = options;\n\n\t\tconst params = {\n\t\t\tglobalId: globalId,\n\t\t\toutputTarget: storage( new IndirectStorageBufferAttribute( 1, 1 ), type ),\n\t\t};\n\n\t\tconst fn = wgslTagFn/* wgsl */`\n\t\t\tfn compute( globalId: vec3u ) -> void {\n\n\t\t\t\t${ params.outputTarget }[ globalId.x ] = 0;\n\n\t\t\t}\n\t\t`;\n\n\t\tsuper( fn( params ) );\n\n\t\tthis.defineUniformAccessors( {\n\t\t\ttarget: params.outputTarget,\n\t\t} );\n\n\t}\n\n}\n"
  },
  {
    "path": "src/webgpu/lib/BVHComputeData.js",
    "content": "import { Matrix4, SkinnedMesh, Vector4 } from 'three';\nimport { Mesh, StorageBufferAttribute, StructTypeNode } from 'three/webgpu';\nimport { storage, wgsl } from 'three/tsl';\nimport { constants } from './wgsl/common.wgsl.js';\nimport { rayStruct, bvhNodeStruct, bvhNodeBoundsStruct } from './wgsl/structs.wgsl.js';\nimport { wgslTagCode, wgslTagFn } from './nodes/WGSLTagFnNode.js';\nimport { MeshBVH, SkinnedMeshBVH, GeometryBVH, ObjectBVH, SAH } from 'three-mesh-bvh';\n\n// TODO: add ability to easily update a single matrix / scene rearrangement (partial update)\n// TODO: add material support w/ function to easily update material\n// \t\t- add a callback for writing a property for a geometry to a range\n// TODO: Add support for other geometry types (tris, lines, custom BVHs etc)\n\n// temporary shim so StructTypeNodes can be passed to storage functions until\n// this is fixed in three.js\nObject.defineProperty( StructTypeNode.prototype, 'layout', {\n\n\tget() {\n\n\t\treturn this;\n\n\t}\n\n} );\nStructTypeNode.prototype.isStruct = true;\n\n//\n\nconst isVisible = object => {\n\n\tlet curr = object;\n\twhile ( curr ) {\n\n\t\tif ( curr.visible === false ) {\n\n\t\t\treturn false;\n\n\t\t}\n\n\t\tcurr = curr.parent;\n\n\t}\n\n\treturn true;\n\n};\n\nconst applyBoneTransform = ( () => {\n\n\t// a vec4-compatible version of SkinnedMesh.applyBoneTransform to support directions, positions\n\tconst _base = new Vector4();\n\tconst _skinIndex = new Vector4();\n\tconst _skinWeight = new Vector4();\n\tconst _matrix4 = new Matrix4();\n\tconst _vector4 = new Vector4();\n\treturn function applyBoneTransform( mesh, index, target ) {\n\n\t\tconst skeleton = mesh.skeleton;\n\t\tconst geometry = mesh.geometry;\n\n\t\t_skinIndex.fromBufferAttribute( geometry.attributes.skinIndex, index );\n\t\t_skinWeight.fromBufferAttribute( geometry.attributes.skinWeight, index );\n\n\t\tif ( target.isVector4 ) {\n\n\t\t\t_base.copy( target );\n\t\t\ttarget.set( 0, 0, 0, 0 );\n\n\t\t} else {\n\n\t\t\t_base.set( ...target, 1 );\n\t\t\ttarget.set( 0, 0, 0 );\n\n\t\t}\n\n\t\t_base.applyMatrix4( mesh.bindMatrix );\n\n\t\tfor ( let i = 0; i < 4; i ++ ) {\n\n\t\t\tconst weight = _skinWeight.getComponent( i );\n\n\t\t\tif ( weight !== 0 ) {\n\n\t\t\t\tconst boneIndex = _skinIndex.getComponent( i );\n\n\t\t\t\t_matrix4.multiplyMatrices( skeleton.bones[ boneIndex ].matrixWorld, skeleton.boneInverses[ boneIndex ] );\n\n\t\t\t\ttarget.addScaledVector( _vector4.copy( _base ).applyMatrix4( _matrix4 ), weight );\n\n\t\t\t}\n\n\t\t}\n\n\t\tif ( target.isVector4 ) {\n\n\t\t\ttarget.w = _base.w;\n\n\t\t}\n\n\t\treturn target.applyMatrix4( mesh.bindMatrixInverse );\n\n\t};\n\n} )();\n\n\n//\n\n// structs\nconst transformStruct = new StructTypeNode( {\n\tmatrixWorld: 'mat4x4f',\n\tinverseMatrixWorld: 'mat4x4f',\n\tnodeOffset: 'uint',\n\tvisible: 'uint',\n\t_alignment0: 'uint',\n\t_alignment1: 'uint',\n}, 'TransformStruct' );\n\nconst intersectionResultStruct = new StructTypeNode( {\n\tindices: 'vec4u',\n\tnormal: 'vec3f',\n\tdidHit: 'bool',\n\tbarycoord: 'vec3f',\n\tobjectIndex: 'uint',\n\tside: 'float',\n\tdist: 'float',\n}, 'IntersectionResult' );\n\n//\n\n// node constants\nconst BYTES_PER_NODE = 6 * 4 + 4 + 4;\nconst UINT32_PER_NODE = BYTES_PER_NODE / 4;\nconst IS_LEAFNODE_FLAG = 0xFFFF;\n\n// scratch\nconst _def = /* @__PURE__ */ new Vector4();\nconst _vec = /* @__PURE__ */ new Vector4();\nconst _matrix = /* @__PURE__ */ new Matrix4();\nconst _inverseMatrix = /* @__PURE__ */ new Matrix4();\n\n// functions\nfunction dereferenceIndex( indexAttr, indirectBuffer ) {\n\n\tconst indexArray = indexAttr ? indexAttr.array : null;\n\tconst result = new Uint32Array( indirectBuffer.length * 3 );\n\tfor ( let i = 0, l = indirectBuffer.length; i < l; i ++ ) {\n\n\t\tconst i3 = 3 * i;\n\t\tconst v3 = 3 * indirectBuffer[ i ];\n\t\tfor ( let c = 0; c < 3; c ++ ) {\n\n\t\t\tresult[ i3 + c ] = indexArray ? indexArray[ v3 + c ] : v3 + c;\n\n\t\t}\n\n\t}\n\n\treturn result;\n\n}\n\nfunction getTotalBVHByteLength( bvh ) {\n\n\treturn bvh._roots.reduce( ( v, root ) => v + root.byteLength, 0 );\n\n}\n\nconst intersectsTriangle = wgslTagFn/* wgsl */ `\n\t// fn\n\tfn intersectsTriangle( ray: ${ rayStruct }, a: vec3f, b: vec3f, c: vec3f ) -> ${ intersectionResultStruct } {\n\n\t\t// TODO: see if we can remove the \"DIST\" epsilon and account for it on ray origin bounce positioning\n\t\tconst DET_EPSILON = 1e-15;\n\t\tconst DIST_EPSILON = 1e-5;\n\n\t\tvar result: ${ intersectionResultStruct };\n\t\tresult.didHit = false;\n\n\t\tlet edge1 = b - a;\n\t\tlet edge2 = c - a;\n\t\tlet n = cross( edge1, edge2 );\n\n\t\tlet det = - dot( ray.direction, n );\n\t\tif ( abs( det ) < DET_EPSILON ) {\n\n\t\t\treturn result;\n\n\t\t}\n\n\t\tlet invdet = 1.0 / det;\n\n\t\tlet AO = ray.origin - a;\n\t\tlet DAO = cross( AO, ray.direction );\n\n\t\tlet u = dot( edge2, DAO ) * invdet;\n\t\tif ( u < 0.0 || u > 1.0 ) {\n\n\t\t\treturn result;\n\n\t\t}\n\n\t\tlet v = - dot( edge1, DAO ) * invdet;\n\t\tif ( v < 0.0 || u + v > 1.0 ) {\n\n\t\t\treturn result;\n\n\t\t}\n\n\t\tlet t = dot( AO, n ) * invdet;\n\t\tlet w = 1.0 - u - v;\n\t\tif ( t < DIST_EPSILON ) {\n\n\t\t\treturn result;\n\n\t\t}\n\n\t\tresult.didHit = true;\n\t\tresult.barycoord = vec3f( w, u, v );\n\t\tresult.dist = t;\n\t\tresult.side = sign( det );\n\t\tresult.normal = result.side * normalize( n );\n\n\t\treturn result;\n\n\t}\n`;\n\nexport class BVHComputeData {\n\n\tconstructor( bvh, options = {} ) {\n\n\t\t// convert the bvh argument to an ObjectBVH. Supports the following as arguments\n\t\t// - Object3D\n\t\t// - BufferGeometry\n\t\t// - GeometryBVH\n\t\t// - Array of the above\n\t\tif ( ! ( bvh instanceof ObjectBVH ) ) {\n\n\t\t\tif ( ! Array.isArray( bvh ) ) {\n\n\t\t\t\tbvh = [ bvh ];\n\n\t\t\t}\n\n\t\t\tconst objects = bvh.map( item => {\n\n\t\t\t\tif ( item.isObject3D ) {\n\n\t\t\t\t\treturn item;\n\n\t\t\t\t} else if ( item.isBufferGeometry ) {\n\n\t\t\t\t\treturn new Mesh( item );\n\n\t\t\t\t} else if ( item instanceof GeometryBVH ) {\n\n\t\t\t\t\tconst dummy = new Mesh();\n\t\t\t\t\tdummy.geometry.boundsTree = item;\n\t\t\t\t\treturn dummy;\n\n\t\t\t\t}\n\n\t\t\t} );\n\n\t\t\tbvh = new ObjectBVH( objects, { strategy: SAH, maxLeafSize: 1 } );\n\n\t\t}\n\n\t\tconst {\n\t\t\tattributes = { position: 'vec4f' },\n\t\t\tautogenerateBvh = true,\n\t\t} = options;\n\n\t\tthis._bvhCache = new Map();\n\n\t\tthis.autogenerateBvh = autogenerateBvh;\n\t\tthis.attributes = attributes;\n\t\tthis.bvh = bvh;\n\n\t\tthis.storage = {\n\t\t\tindex: null,\n\t\t\tattributes: null,\n\t\t\tnodes: null,\n\t\t\ttransforms: null,\n\t\t};\n\n\t\tthis.structs = {\n\t\t\ttransform: transformStruct,\n\t\t\tattributes: null,\n\t\t};\n\n\t\tthis.fns = {\n\t\t\traycastFirstHit: null,\n\t\t};\n\n\t}\n\n\tgetShapecastFn( options ) {\n\n\t\tconst {\n\t\t\tname = `bvh_${ Math.random().toString( 36 ).substring( 2, 7 ) }`,\n\t\t\tshapeStruct,\n\t\t\tresultStruct = null,\n\n\t\t\tboundsOrderFn = null,\n\t\t\tintersectsBoundsFn,\n\t\t\tintersectRangeFn,\n\t\t\ttransformShapeFn = null,\n\t\t\ttransformResultFn = null,\n\t\t} = options;\n\n\t\tconst { storage } = this;\n\t\tconst { BVH_STACK_DEPTH } = constants;\n\n\t\t// handle optional functions\n\t\tlet transformResultSnippet = '';\n\t\tif ( transformResultFn ) {\n\n\t\t\ttransformResultSnippet = wgslTagCode/* wgsl */`${ transformResultFn }( result, i );`;\n\n\t\t}\n\n\t\tlet transformShapeSnippet = '';\n\t\tif ( transformShapeFn ) {\n\n\t\t\ttransformShapeSnippet = wgslTagCode/* wgsl */`${ transformShapeFn }( &localShape, i );`;\n\n\t\t}\n\n\t\tlet leftToRightSnippet = '';\n\t\tif ( boundsOrderFn ) {\n\n\t\t\tleftToRightSnippet = wgslTagCode/* wgsl */`\n\t\t\t\tlet leftToRight = ${ boundsOrderFn }( shape, splitAxis, node );\n\t\t\t\tc1 = select( rightIndex, leftIndex, leftToRight );\n\t\t\t\tc2 = select( leftIndex, rightIndex, leftToRight );\n\t\t\t`;\n\n\t\t}\n\n\t\tconst resultPtrSnippet = resultStruct ? wgslTagCode/* wgsl */`result: ptr<function, ${ resultStruct }>` : '';\n\t\tconst resultArg = resultStruct ? 'result' : '';\n\n\t\tconst getFnBody = leafSnippet => {\n\n\t\t\t// returns a function with a snippet inserted for the leaf intersection test\n\t\t\treturn wgslTagCode/* wgsl */`\n\n\t\t\t\tvar pointer: i32 = 0;\n\t\t\t\tvar stack: array<u32, ${ BVH_STACK_DEPTH }>;\n\t\t\t\tstack[ 0 ] = rootNodeIndex;\n\n\t\t\t\tloop {\n\n\t\t\t\t\tif ( pointer < 0 || pointer >= i32( ${ BVH_STACK_DEPTH } ) ) {\n\n\t\t\t\t\t\tbreak;\n\n\t\t\t\t\t}\n\n\t\t\t\t\tlet nodeIndex = stack[ pointer ];\n\t\t\t\t\tlet node = ${ storage.nodes }[ nodeIndex ];\n\t\t\t\t\tpointer = pointer - 1;\n\n\t\t\t\t\tif ( ${ intersectsBoundsFn }( shape, node.bounds, ${ resultArg } ) == 0u ) {\n\n\t\t\t\t\t\tcontinue;\n\n\t\t\t\t\t}\n\n\t\t\t\t\tlet infoX = node.splitAxisOrTriangleCount;\n\t\t\t\t\tlet infoY = node.rightChildOrTriangleOffset;\n\t\t\t\t\tlet isLeaf = ( infoX & 0xffff0000u ) != 0u;\n\n\t\t\t\t\tif ( isLeaf ) {\n\n\t\t\t\t\t\tlet count = infoX & 0x0000ffffu;\n\t\t\t\t\t\tlet offset = infoY;\n\t\t\t\t\t\t${ leafSnippet }\n\n\t\t\t\t\t} else {\n\n\t\t\t\t\t\tlet leftIndex = nodeIndex + 1u;\n\t\t\t\t\t\tlet splitAxis = infoX & 0x0000ffffu;\n\t\t\t\t\t\tlet rightIndex = nodeIndex + infoY;\n\n\t\t\t\t\t\tvar c1 = rightIndex;\n\t\t\t\t\t\tvar c2 = leftIndex;\n\t\t\t\t\t\t${ leftToRightSnippet }\n\n\t\t\t\t\t\tpointer = pointer + 1;\n\t\t\t\t\t\tstack[ pointer ] = c2;\n\n\t\t\t\t\t\tpointer = pointer + 1;\n\t\t\t\t\t\tstack[ pointer ] = c1;\n\n\t\t\t\t\t}\n\n\t\t\t\t}\n\n\t\t\t`;\n\n\t\t};\n\n\t\tconst blasFn = wgslTagFn/* wgsl */`\n\t\t\t// fn\n\t\t\tfn ${ name }_blas( shape: ${ shapeStruct }, rootNodeIndex: u32, ${ resultPtrSnippet } ) -> bool {\n\n\t\t\t\tvar didHit = false;\n\t\t\t\t${ getFnBody( wgslTagCode/* wgsl */`\n\n\t\t\t\t\tdidHit = ${ intersectRangeFn }( shape, offset, count, ${ resultArg } ) || didHit;\n\n\t\t\t\t` ) }\n\n\t\t\t\treturn didHit;\n\n\t\t\t}\n\t\t`;\n\n\t\tconst tlasFn = wgslTagFn/* wgsl */`\n\t\t\t// fn\n\t\t\tfn ${ name }( shape: ${ shapeStruct }, ${ resultPtrSnippet } ) -> bool {\n\n\t\t\t\tconst rootNodeIndex = 0u;\n\t\t\t\tvar didHit = false;\n\t\t\t\t${ getFnBody( wgslTagCode/* wgsl */`\n\n\t\t\t\t\tfor ( var i = offset; i < offset + count; i ++ ) {\n\n\t\t\t\t\t\tlet transform = ${ storage.transforms }[ i ];\n\t\t\t\t\t\tif ( transform.visible == 0u ) {\n\n\t\t\t\t\t\t\tcontinue;\n\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\t// Transform shape into object local space\n\t\t\t\t\t\tvar localShape = shape;\n\t\t\t\t\t\t${ transformShapeSnippet }\n\n\t\t\t\t\t\tif ( ${ blasFn }( localShape, transform.nodeOffset, ${ resultArg } ) ) {\n\n\t\t\t\t\t\t\t${ transformResultSnippet }\n\t\t\t\t\t\t\tdidHit = true;\n\n\t\t\t\t\t\t}\n\n\t\t\t\t\t}\n\n\t\t\t\t` ) }\n\n\t\t\t\treturn didHit;\n\n\t\t\t}\n\t\t`;\n\n\t\ttlasFn.outputType = resultStruct;\n\t\ttlasFn.functionName = name;\n\n\t\treturn tlasFn;\n\n\t}\n\n\tupdate() {\n\n\t\tconst self = this;\n\t\tconst { attributes, structs, bvh } = this;\n\n\t\t// collect the BVHs\n\t\tconst bvhInfo = [];\n\t\tconst transformInfo = [];\n\n\t\t// accumulate the sizes of the bvh nodes buffer, number of objects, and geometry buffers\n\t\tlet bvhNodesBufferLength = getTotalBVHByteLength( bvh );\n\t\tlet indexBufferLength = 0;\n\t\tlet attributesBufferLength = 0;\n\t\tbvh.primitiveBuffer.forEach( compositeId => {\n\n\t\t\tconst object = bvh.getObjectFromId( compositeId );\n\t\t\tconst instanceId = bvh.getInstanceFromId( compositeId );\n\t\t\tconst range = { start: 0, count: 0, vertexStart: 0, vertexCount: 0 };\n\t\t\tconst primBvh = this.getBVH( object, instanceId, range );\n\n\t\t\tif ( ! primBvh ) {\n\n\t\t\t\tthrow new Error( 'BVHComputeData: BVH not found.' );\n\n\t\t\t}\n\n\t\t\t// if we haven't added this bvh, yet\n\t\t\tif ( ! bvhInfo.find( info => info.bvh === primBvh ) ) {\n\n\t\t\t\t// save the geometry info to write later and increment the buffer sizes\n\t\t\t\tconst info = {\n\t\t\t\t\tindex: bvhInfo.length,\n\t\t\t\t\tbvh: primBvh,\n\t\t\t\t\trange: range,\n\n\t\t\t\t\tbvhNodeOffsets: null,\n\t\t\t\t\tindexBufferOffset: null,\n\n\t\t\t\t};\n\n\t\t\t\t// increase the buffer sizes for bvh and geometry\n\t\t\t\tbvhNodesBufferLength += getTotalBVHByteLength( primBvh );\n\t\t\t\tindexBufferLength += info.range.count;\n\t\t\t\tattributesBufferLength += info.range.vertexCount;\n\t\t\t\tbvhInfo.push( info );\n\n\t\t\t}\n\n\t\t\t// save the index of the bvh associated with this transform\n\t\t\tconst data = bvhInfo.find( info => primBvh === info.bvh );\n\t\t\tprimBvh._roots.forEach( ( root, i ) => {\n\n\t\t\t\ttransformInfo.push( {\n\t\t\t\t\tdata,\n\t\t\t\t\troot: i,\n\t\t\t\t\tobject,\n\t\t\t\t\tinstanceId,\n\t\t\t\t\tcompositeId,\n\t\t\t\t} );\n\n\t\t\t} );\n\n\t\t} );\n\n\t\t//\n\n\t\t// NOTE: These buffer lengths are increased to a minimum size of 2 to avoid the TSL of converting storage buffers\n\t\t// with length 1 being converted to a scalar value.\n\t\t// TODO: remove this when fixed in three\n\t\tconst transformBufferLength = Math.max( transformInfo.length, 2 );\n\t\tindexBufferLength = Math.max( indexBufferLength, 2 );\n\t\tattributesBufferLength = Math.max( attributesBufferLength, 2 );\n\n\t\t// construct the attribute struct\n\t\tconst attributeStruct = new StructTypeNode( attributes, 'bvh_GeometryStruct' );\n\n\t\t// write the geometry buffer attributes & bvh data\n\t\tlet attributesOffset = 0;\n\t\tlet indexOffset = 0;\n\t\tlet nodeWriteOffset = 0;\n\t\tconst indexBuffer = new Uint32Array( indexBufferLength );\n\t\tconst attributesBuffer = new ArrayBuffer( attributesBufferLength * attributeStruct.getLength() * 4 );\n\t\tconst bvhNodesBuffer = new ArrayBuffer( bvhNodesBufferLength );\n\n\t\t// append TLAS data\n\t\tappendBVHData( bvh, 0, transformInfo, 0, bvhNodesBuffer, true );\n\t\tnodeWriteOffset += getTotalBVHByteLength( bvh ) / BYTES_PER_NODE;\n\t\tbvhInfo.forEach( info => {\n\n\t\t\t// append bvh data\n\t\t\tconst bvhNodeOffsets = appendBVHData( info.bvh, indexOffset / 3, transformInfo, nodeWriteOffset, bvhNodesBuffer, false );\n\t\t\tinfo.bvhNodeOffsets = bvhNodeOffsets;\n\n\t\t\t// append geometry data\n\t\t\tappendIndexData( info.bvh, info.range, attributesOffset, indexOffset, indexBuffer );\n\t\t\tappendGeometryData( info.bvh, info.range, attributesOffset, attributesBuffer );\n\t\t\tinfo.indexBufferOffset = indexOffset;\n\n\t\t\t// step the write offsets forward\n\t\t\tindexOffset += info.range.count;\n\t\t\tattributesOffset += info.range.vertexCount;\n\t\t\tnodeWriteOffset += getTotalBVHByteLength( info.bvh ) / BYTES_PER_NODE;\n\n\t\t} );\n\n\t\t//\n\n\t\t// write the transforms\n\t\tconst transformArrayBuffer = new ArrayBuffer( structs.transform.getLength() * transformBufferLength * 4 );\n\t\ttransformInfo.forEach( ( info, i ) => {\n\n\t\t\t_inverseMatrix.copy( bvh.matrixWorld ).invert();\n\t\t\tthis.writeTransformData( info, _inverseMatrix, i, transformArrayBuffer );\n\n\t\t} );\n\n\t\t//\n\n\t\t// set up the storage buffers\n\t\t// if itemSize for StorageBufferAttribute == arraySize,\n\t\t// then buffer is treated not as array of structs, but as a single struct\n\t\t// And that breaks code. For now itemSize = 1 does not seem to break anything\n\t\tconst bvhNodesStorage = storage( new StorageBufferAttribute( new Uint32Array( bvhNodesBuffer ), 1 ), bvhNodeStruct ).toReadOnly().setName( 'bvh_nodes' );\n\t\tconst transformsBuffer = new StorageBufferAttribute( new Uint32Array( transformArrayBuffer ), 1 );\n\t\tconst transformsStorage = storage( transformsBuffer, structs.transform ).toReadOnly().setName( 'bvh_transforms' );\n\t\tconst indexStorage = storage( new StorageBufferAttribute( indexBuffer, 1 ), 'uint' ).toReadOnly().setName( 'bvh_index' );\n\t\tconst attributesStorage = storage( new StorageBufferAttribute( new Uint32Array( attributesBuffer ), attributeStruct.getLength() ), attributeStruct ).toReadOnly().setName( 'bvh_attributes' );\n\n\t\tthis.storage.transforms = transformsStorage;\n\t\tthis.storage.nodes = bvhNodesStorage;\n\t\tthis.storage.index = indexStorage;\n\t\tthis.storage.attributes = attributesStorage;\n\t\tthis.structs.attributes = attributeStruct;\n\n\t\tthis._initFns();\n\t\tthis._bvhCache.clear();\n\n\t\tfunction appendBVHData( bvh, geometryOffset, transformInfo, nodeWriteOffset, target, tlas = false ) {\n\n\t\t\tconst targetU16 = new Uint16Array( target );\n\t\t\tconst targetU32 = new Uint32Array( target );\n\t\t\tconst targetF32 = new Float32Array( target );\n\n\t\t\tconst result = [];\n\t\t\tlet tlasOffset = 0;\n\t\t\tbvh._roots.forEach( root => {\n\n\t\t\t\tconst rootBuffer16 = new Uint16Array( root );\n\t\t\t\tconst rootBuffer32 = new Uint32Array( root );\n\t\t\t\tresult.push( nodeWriteOffset );\n\t\t\t\tfor ( let i = 0, l = root.byteLength / BYTES_PER_NODE; i < l; i ++ ) {\n\n\t\t\t\t\tconst r32 = i * UINT32_PER_NODE;\n\t\t\t\t\tconst r16 = r32 * 2;\n\t\t\t\t\tconst n32 = nodeWriteOffset * UINT32_PER_NODE;\n\t\t\t\t\tconst n16 = n32 * 2;\n\n\t\t\t\t\t// write bounds\n\t\t\t\t\tconst view = new Float32Array( root, i * BYTES_PER_NODE, 6 );\n\t\t\t\t\tif ( i === 0 ) {\n\n\t\t\t\t\t\t// if we're copying the root then check for cases where there are no primitives and therefore\n\t\t\t\t\t\t// be a bounds of [ Infinity, - Infinity ]. Convert this to [ 1, - 1 ] for reliable GPU behavior.\n\t\t\t\t\t\tfor ( let i = 0; i < 3; i ++ ) {\n\n\t\t\t\t\t\t\tconst vMin = view[ i + 0 ];\n\t\t\t\t\t\t\tconst vMax = view[ i + 3 ];\n\t\t\t\t\t\t\tif ( vMin > vMax ) {\n\n\t\t\t\t\t\t\t\ttargetF32[ n32 + i + 0 ] = 1;\n\t\t\t\t\t\t\t\ttargetF32[ n32 + i + 3 ] = - 1;\n\n\t\t\t\t\t\t\t} else {\n\n\t\t\t\t\t\t\t\ttargetF32[ n32 + i + 0 ] = vMin;\n\t\t\t\t\t\t\t\ttargetF32[ n32 + i + 3 ] = vMax;\n\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t}\n\n\t\t\t\t\t} else {\n\n\t\t\t\t\t\ttargetF32.set( view, n32 );\n\n\t\t\t\t\t}\n\n\t\t\t\t\tconst isLeaf = IS_LEAFNODE_FLAG === rootBuffer16[ r16 + 15 ];\n\t\t\t\t\tif ( isLeaf ) {\n\n\t\t\t\t\t\tif ( tlas ) {\n\n\t\t\t\t\t\t\t// 0xFFFF == mesh leaf, 0xFF00 == TLAS leaf\n\t\t\t\t\t\t\ttargetU32[ n32 + 6 ] = tlasOffset;\n\t\t\t\t\t\t\ttargetU16[ n16 + 15 ] = 0xFF00;\n\n\t\t\t\t\t\t\tconst count = rootBuffer16[ r16 + 14 ];\n\t\t\t\t\t\t\t// const offset = rootBuffer32[ r32 + 6 ];\n\n\t\t\t\t\t\t\t// each root is expanded into a separate transform so we need to expand\n\t\t\t\t\t\t\t// the embedded offsets and counts.\n\t\t\t\t\t\t\tlet rootsCount = 0;\n\t\t\t\t\t\t\tfor ( let o = 0; o < count; o ++ ) {\n\n\t\t\t\t\t\t\t\tconst roots = transformInfo[ tlasOffset ].data.bvh._roots.length;\n\t\t\t\t\t\t\t\ttlasOffset += roots;\n\t\t\t\t\t\t\t\trootsCount += roots;\n\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\ttargetU16[ n16 + 14 ] = rootsCount;\n\n\t\t\t\t\t\t} else {\n\n\t\t\t\t\t\t\ttargetU32[ n32 + 6 ] = rootBuffer32[ r32 + 6 ] + geometryOffset;\n\t\t\t\t\t\t\ttargetU16[ n16 + 14 ] = rootBuffer16[ r16 + 14 ];\n\t\t\t\t\t\t\ttargetU16[ n16 + 15 ] = IS_LEAFNODE_FLAG;\n\n\t\t\t\t\t\t}\n\n\t\t\t\t\t} else {\n\n\t\t\t\t\t\ttargetU32[ n32 + 6 ] = rootBuffer32[ r32 + 6 ];\n\t\t\t\t\t\ttargetU32[ n32 + 7 ] = rootBuffer32[ r32 + 7 ];\n\n\t\t\t\t\t}\n\n\t\t\t\t\tnodeWriteOffset ++;\n\n\t\t\t\t}\n\n\t\t\t} );\n\n\t\t\treturn result;\n\n\t\t}\n\n\t\tfunction appendIndexData( bvh, range, valueOffset, writeOffset, target ) {\n\n\t\t\tconst { geometry } = bvh;\n\t\t\tconst { start, count, vertexStart } = range;\n\t\t\tif ( bvh.indirect ) {\n\n\t\t\t\tconst dereferencedIndex = dereferenceIndex( geometry.index, bvh._indirectBuffer );\n\t\t\t\tfor ( let i = 0; i < dereferencedIndex.length; i ++ ) {\n\n\t\t\t\t\ttarget[ i + writeOffset ] = dereferencedIndex[ i ] - vertexStart + valueOffset;\n\n\t\t\t\t}\n\n\t\t\t} else if ( geometry.index ) {\n\n\t\t\t\tfor ( let i = 0; i < count; i ++ ) {\n\n\t\t\t\t\ttarget[ i + writeOffset ] = geometry.index.getX( i + start ) - vertexStart + valueOffset;\n\n\t\t\t\t}\n\n\t\t\t} else {\n\n\t\t\t\tfor ( let i = 0; i < count; i ++ ) {\n\n\t\t\t\t\ttarget[ i + writeOffset ] = i + start + valueOffset;\n\n\t\t\t\t}\n\n\t\t\t}\n\n\t\t}\n\n\t\tfunction appendGeometryData( bvh, range, writeOffset, target ) {\n\n\t\t\t// if \"mesh\" is present then it is assumed to be a SkinnedMeshBVH\n\t\t\tconst { geometry, mesh = null } = bvh;\n\t\t\tconst { vertexStart, vertexCount } = range;\n\t\t\tconst attributesBufferF32 = new Float32Array( target );\n\t\t\tconst attrStructLength = attributeStruct.getLength();\n\t\t\tattributeStruct.membersLayout.forEach( ( { name }, interleavedOffset ) => {\n\n\t\t\t\t// TODO: we should be able to have access to memory layout offsets here via the struct\n\t\t\t\t// API but it's not currently available.\n\t\t\t\tconst attr = geometry.attributes[ name ];\n\t\t\t\tself.getDefaultAttributeValue( name, _def );\n\n\t\t\t\tfor ( let i = 0; i < vertexCount; i ++ ) {\n\n\t\t\t\t\tif ( attr ) {\n\n\t\t\t\t\t\t_vec.fromBufferAttribute( attr, i + vertexStart );\n\n\t\t\t\t\t\tswitch ( attr.itemSize ) {\n\n\t\t\t\t\t\t\tcase 1:\n\t\t\t\t\t\t\t\t_vec.y = _def.y;\n\t\t\t\t\t\t\t\t_vec.z = _def.z;\n\t\t\t\t\t\t\t\t_vec.w = _def.w;\n\t\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\t\tcase 2:\n\t\t\t\t\t\t\t\t_vec.z = _def.z;\n\t\t\t\t\t\t\t\t_vec.w = _def.w;\n\t\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\t\tcase 3:\n\t\t\t\t\t\t\t\t_vec.w = _def.w;\n\t\t\t\t\t\t\t\tbreak;\n\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tif ( mesh && ( name === 'position' || name === 'normal' || name === 'tangent' ) ) {\n\n\t\t\t\t\t\t\tapplyBoneTransform( mesh, i + vertexStart, _vec );\n\n\t\t\t\t\t\t}\n\n\t\t\t\t\t} else {\n\n\t\t\t\t\t\t_vec.copy( _def );\n\n\t\t\t\t\t}\n\n\t\t\t\t\t_vec.toArray( attributesBufferF32, ( writeOffset + i ) * attrStructLength + interleavedOffset * 4 );\n\n\t\t\t\t}\n\n\t\t\t} );\n\n\t\t}\n\n\t}\n\n\t_initFns() {\n\n\t\tconst { storage, structs, fns } = this;\n\n\t\t// raycast first hit\n\t\tconst scratchRayScalar = wgsl( /* wgsl */`\n\t\t\tvar<private> bvh_rayScalar = 1.0;\n\t\t` );\n\t\tfns.raycastFirstHit = this.getShapecastFn( {\n\t\t\tname: 'bvh_RaycastFirstHit',\n\t\t\tshapeStruct: rayStruct,\n\t\t\tresultStruct: intersectionResultStruct,\n\n\t\t\tboundsOrderFn: wgslTagFn/* wgsl */`\n\t\t\t\tfn getBoundsOrder( ray: ${ rayStruct }, splitAxis: u32, node: ${ bvhNodeStruct } ) -> bool {\n\n\t\t\t\t\treturn ray.direction[ splitAxis ] >= 0.0;\n\n\t\t\t\t}\n\t\t\t`,\n\t\t\tintersectsBoundsFn: wgslTagFn/* wgsl */`\n\t\t\t\t${ [ scratchRayScalar ] }\n\n\t\t\t\tfn rayIntersectsBounds( ray: ${ rayStruct }, bounds: ${ bvhNodeBoundsStruct }, result: ptr<function, ${ intersectionResultStruct }> ) -> u32 {\n\n\t\t\t\t\tlet boundsMin = vec3( bounds.min[0], bounds.min[1], bounds.min[2] );\n\t\t\t\t\tlet boundsMax = vec3( bounds.max[0], bounds.max[1], bounds.max[2] );\n\n\t\t\t\t\tlet invDir = 1.0 / ray.direction;\n\t\t\t\t\tlet tMinPlane = ( boundsMin - ray.origin ) * invDir;\n\t\t\t\t\tlet tMaxPlane = ( boundsMax - ray.origin ) * invDir;\n\n\t\t\t\t\tlet tMinHit = vec3f(\n\t\t\t\t\t\tmin( tMinPlane.x, tMaxPlane.x ),\n\t\t\t\t\t\tmin( tMinPlane.y, tMaxPlane.y ),\n\t\t\t\t\t\tmin( tMinPlane.z, tMaxPlane.z )\n\t\t\t\t\t);\n\n\t\t\t\t\tlet tMaxHit = vec3f(\n\t\t\t\t\t\tmax( tMinPlane.x, tMaxPlane.x ),\n\t\t\t\t\t\tmax( tMinPlane.y, tMaxPlane.y ),\n\t\t\t\t\t\tmax( tMinPlane.z, tMaxPlane.z )\n\t\t\t\t\t);\n\n\t\t\t\t\tlet t0 = max( max( tMinHit.x, tMinHit.y ), tMinHit.z );\n\t\t\t\t\tlet t1 = min( min( tMaxHit.x, tMaxHit.y ), tMaxHit.z );\n\n\t\t\t\t\tlet dist = max( t0, 0.0 );\n\t\t\t\t\tif ( t1 < dist ) {\n\n\t\t\t\t\t\treturn 0u;\n\n\t\t\t\t\t} else if ( result.didHit && dist * bvh_rayScalar >= result.dist ) {\n\n\t\t\t\t\t\treturn 0u;\n\n\t\t\t\t\t} else {\n\n\t\t\t\t\t\treturn 1u;\n\n\t\t\t\t\t}\n\n\t\t\t\t}\n\n\t\t\t`,\n\t\t\tintersectRangeFn: wgslTagFn/* wgsl */`\n\t\t\t\t${ [ scratchRayScalar ] }\n\n\t\t\t\tfn intersectRange( ray: ${ rayStruct }, offset: u32, count: u32, result: ptr<function, ${ intersectionResultStruct }> ) -> bool {\n\n\t\t\t\t\tvar didHit = false;\n\t\t\t\t\tfor ( var ti = offset; ti < offset + count; ti = ti + 1u ) {\n\n\t\t\t\t\t\tlet i0 = ${ storage.index }[ ti * 3u ];\n\t\t\t\t\t\tlet i1 = ${ storage.index }[ ti * 3u + 1u ];\n\t\t\t\t\t\tlet i2 = ${ storage.index }[ ti * 3u + 2u ];\n\n\t\t\t\t\t\tlet a = ${ storage.attributes }[ i0 ].position.xyz;\n\t\t\t\t\t\tlet b = ${ storage.attributes }[ i1 ].position.xyz;\n\t\t\t\t\t\tlet c = ${ storage.attributes }[ i2 ].position.xyz;\n\n\t\t\t\t\t\tvar triResult = ${ intersectsTriangle }( ray, a, b, c );\n\t\t\t\t\t\ttriResult.dist *= bvh_rayScalar;\n\t\t\t\t\t\tif ( triResult.didHit && ( ! result.didHit || triResult.dist < result.dist ) ) {\n\n\t\t\t\t\t\t\tresult.didHit = true;\n\t\t\t\t\t\t\tresult.dist = triResult.dist;\n\t\t\t\t\t\t\tresult.normal = triResult.normal;\n\t\t\t\t\t\t\tresult.side = triResult.side;\n\t\t\t\t\t\t\tresult.barycoord = triResult.barycoord;\n\t\t\t\t\t\t\tresult.indices = vec4u( i0, i1, i2, ti );\n\n\t\t\t\t\t\t\tdidHit = true;\n\n\t\t\t\t\t\t}\n\n\t\t\t\t\t}\n\n\t\t\t\t\treturn didHit;\n\n\t\t\t\t}\n\t\t\t`,\n\t\t\ttransformShapeFn: wgslTagFn/* wgsl */`\n\t\t\t\t${ [ scratchRayScalar ] }\n\n\t\t\t\tfn transformRay( ray: ptr<function, ${ rayStruct }>, objectIndex: u32 ) -> void {\n\n\t\t\t\t\tlet toLocal = ${ storage.transforms }[ objectIndex ].inverseMatrixWorld;\n\t\t\t\t\tray.origin = ( toLocal * vec4f( ray.origin, 1.0 ) ).xyz;\n\t\t\t\t\tray.direction = ( toLocal * vec4f( ray.direction, 0.0 ) ).xyz;\n\n\t\t\t\t\tlet len = length( ray.direction );\n\t\t\t\t\tray.direction /= len;\n\t\t\t\t\tbvh_rayScalar = 1.0 / len;\n\n\t\t\t\t}\n\t\t\t`,\n\t\t\ttransformResultFn: wgslTagFn/* wgsl */`\n\t\t\t\tfn transformResult( hit: ptr<function, ${ intersectionResultStruct }>, objectIndex: u32 ) -> void {\n\n\t\t\t\t\tlet toLocal = ${ storage.transforms }[ objectIndex ].inverseMatrixWorld;\n\t\t\t\t\thit.normal = normalize( ( transpose( toLocal ) * vec4f( hit.normal, 0.0 ) ).xyz );\n\t\t\t\t\thit.objectIndex = objectIndex;\n\n\t\t\t\t}\n\t\t\t`,\n\t\t} );\n\n\t\tconst interpolateBody = structs\n\t\t\t.attributes\n\t\t\t.membersLayout\n\t\t\t.map( ( { name } ) => {\n\n\t\t\t\treturn `result.${ name } = a0.${ name } * barycoord.x + a1.${ name } * barycoord.y + a2.${ name } * barycoord.z;`;\n\n\t\t\t} ).join( '\\n' );\n\t\tfns.sampleTrianglePoint = wgslTagFn/* wgsl */`\n\t\t\t// fn\n\t\t\tfn bvh_sampleTrianglePoint( barycoord: vec3f, indices: vec3u ) -> ${ structs.attributes } {\n\n\t\t\t\tvar result: ${ structs.attributes };\n\t\t\t\tvar a0 = ${ storage.attributes }[ indices.x ];\n\t\t\t\tvar a1 = ${ storage.attributes }[ indices.y ];\n\t\t\t\tvar a2 = ${ storage.attributes }[ indices.z ];\n\t\t\t\t${ interpolateBody }\n\t\t\t\treturn result;\n\n\t\t\t}\n\t\t`;\n\n\t}\n\n\twriteTransformData( info, premultiplyMatrix, writeOffset, targetBuffer ) {\n\n\t\tconst { structs } = this;\n\t\tconst transformBufferF32 = new Float32Array( targetBuffer );\n\t\tconst transformBufferU32 = new Uint32Array( targetBuffer );\n\n\t\tconst { object, instanceId, root, data } = info;\n\t\tconst { bvhNodeOffsets } = data;\n\t\tif ( object.isInstancedMesh || object.isBatchedMesh ) {\n\n\t\t\tobject.getMatrixAt( instanceId, _matrix );\n\t\t\t_matrix.premultiply( object.matrixWorld );\n\n\t\t} else {\n\n\t\t\t_matrix.copy( object.matrixWorld );\n\n\t\t}\n\n\t\t// write transform\n\t\t_matrix.premultiply( premultiplyMatrix );\n\t\t_matrix.toArray( transformBufferF32, writeOffset * structs.transform.getLength() );\n\n\t\t// write inverse transform\n\t\t_matrix.invert();\n\t\t_matrix.toArray( transformBufferF32, writeOffset * structs.transform.getLength() + 16 );\n\n\t\t// write node offset\n\t\ttransformBufferU32[ writeOffset * structs.transform.getLength() + 32 ] = bvhNodeOffsets[ root ];\n\n\t\tlet visible = isVisible( object );\n\t\tif ( object.isBatchedMesh ) {\n\n\t\t\tvisible = visible && object.getVisibleAt( instanceId );\n\n\t\t}\n\n\t\ttransformBufferU32[ writeOffset * structs.transform.getLength() + 33 ] = visible ? 1 : 0;\n\n\t}\n\n\tgetBVH( object, instanceId, rangeTarget ) {\n\n\t\tconst { autogenerateBvh, _bvhCache } = this;\n\n\t\tlet bvh = null;\n\t\tif ( object.boundsTree || object.isSkinnedMesh ) {\n\n\t\t\t// this is a case where a mesh has morph targets and skinned meshes\n\t\t\tconst geometry = object.geometry;\n\t\t\trangeTarget.count = geometry.index ? geometry.index.count : geometry.attributes.position.count;\n\t\t\trangeTarget.vertexCount = geometry.attributes.position.count;\n\t\t\tbvh = object.boundsTree || null;\n\n\t\t\tif ( bvh === null && autogenerateBvh ) {\n\n\t\t\t\tconst id = object.uuid;\n\t\t\t\tbvh = _bvhCache.get( id ) || new SkinnedMeshBVH( object );\n\t\t\t\t_bvhCache.set( id, bvh );\n\n\t\t\t}\n\n\t\t} else if ( object.isBatchedMesh ) {\n\n\t\t\tconst geometryId = object.getGeometryIdAt( instanceId );\n\t\t\tconst range = object.getGeometryRangeAt( geometryId );\n\t\t\tObject.assign( rangeTarget, range );\n\t\t\tbvh = object.boundsTrees[ geometryId ] || null;\n\n\t\t\tif ( bvh === null && autogenerateBvh ) {\n\n\t\t\t\tconst id = `batched_${ object.geometry.uuid }_${ range.start }_${ range.count }`;\n\t\t\t\tbvh = _bvhCache.get( id ) || new MeshBVH( object.geometry, { range: { ...rangeTarget } } );\n\t\t\t\t_bvhCache.set( id, bvh );\n\n\t\t\t}\n\n\t\t} else {\n\n\t\t\tconst geometry = object.geometry;\n\t\t\trangeTarget.count = geometry.index ? geometry.index.count : geometry.attributes.position.count;\n\t\t\trangeTarget.vertexCount = geometry.attributes.position.count;\n\t\t\tbvh = object.geometry.boundsTree || null;\n\n\t\t\tif ( bvh === null && autogenerateBvh ) {\n\n\t\t\t\tconst id = geometry.uuid;\n\t\t\t\tbvh = _bvhCache.get( id ) || new MeshBVH( geometry );\n\t\t\t\t_bvhCache.set( id, bvh );\n\n\t\t\t}\n\n\t\t}\n\n\t\treturn bvh;\n\n\t}\n\n\tgetDefaultAttributeValue( key, target ) {\n\n\t\tswitch ( key ) {\n\n\t\t\tcase 'position':\n\t\t\tcase 'color':\n\t\t\t\ttarget.set( 1, 1, 1, 1 );\n\t\t\t\tbreak;\n\n\t\t\tdefault:\n\t\t\t\ttarget.set( 0, 0, 0, 0 );\n\n\t\t}\n\n\t\treturn target;\n\n\t}\n\n\tdispose() {\n\n\t\t// TODO: dispose buffers\n\n\t}\n\n}\n"
  },
  {
    "path": "src/webgpu/lib/nodes/NodeProxy.js",
    "content": "import { Node } from 'three/webgpu';\n\nclass ProxyCallNode extends Node {\n\n\tstatic get type() {\n\n\t\treturn 'ProxyCallNode';\n\n\t}\n\n\tconstructor( proxyNode, params ) {\n\n\t\tsuper();\n\t\tthis.proxyNode = proxyNode;\n\t\tthis.params = params;\n\n\t}\n\n\tsetup() {\n\n\t\treturn this.proxyNode.proxyNode.call( ...this.params );\n\n\t}\n\n}\n\nexport class NodeProxy {\n\n\tget isNode() {\n\n\t\treturn true;\n\n\t}\n\n\t// getter for the node being proxied to\n\tget proxyNode() {\n\n\t\tconst { proxyObject, proxyProperty } = this;\n\t\tconst properties = proxyProperty.split( '.' );\n\t\tlet value = proxyObject;\n\t\tfor ( let i = 0, l = properties.length; i < l; i ++ ) {\n\n\t\t\tvalue = value[ properties[ i ] ];\n\n\t\t}\n\n\t\tif ( 'functionNode' in value ) {\n\n\t\t\treturn value.functionNode;\n\n\t\t} else {\n\n\t\t\treturn value;\n\n\t\t}\n\n\t}\n\n\tconstructor( property, object = null ) {\n\n\t\t// store the proxy property and objects so they can be changed later\n\t\tthis.proxyObject = object;\n\t\tthis.proxyProperty = property;\n\n\t\t// set up a proxy to redirect all calls to the proxied node in order to avoid replicating\n\t\t// expected members for all node types.\n\t\treturn new Proxy( this, {\n\n\t\t\tget( target, property ) {\n\n\t\t\t\tif ( property in target ) {\n\n\t\t\t\t\treturn Reflect.get( target, property );\n\n\t\t\t\t} else {\n\n\t\t\t\t\tconst value = Reflect.get( target.proxyNode, property );\n\t\t\t\t\tif ( typeof value === 'function' ) {\n\n\t\t\t\t\t\treturn value.bind( target.proxyNode );\n\n\t\t\t\t\t} else {\n\n\t\t\t\t\t\treturn value;\n\n\t\t\t\t\t}\n\n\t\t\t\t}\n\n\t\t\t},\n\n\t\t\tset( target, property, value ) {\n\n\t\t\t\tif ( property in target ) {\n\n\t\t\t\t\treturn Reflect.set( target, property, value );\n\n\t\t\t\t} else {\n\n\t\t\t\t\tthrow new Error( 'NodeProxy: Cannot set members of proxied nodes.' );\n\n\t\t\t\t}\n\n\t\t\t},\n\n\t\t} );\n\n\t}\n\n}\n\nexport const proxy = ( ...args ) => {\n\n\treturn new NodeProxy( ...args );\n\n};\n\nexport const proxyFn = ( ...args ) => {\n\n\tconst nodeProxy = new NodeProxy( ...args );\n\tconst fn = ( ...params ) => new ProxyCallNode( nodeProxy, params );\n\tfn.functionNode = nodeProxy;\n\treturn fn;\n\n};\n"
  },
  {
    "path": "src/webgpu/lib/nodes/WGSLTagFnNode.js",
    "content": "import { CodeNode, FunctionNode, Node } from 'three/webgpu';\n\n// minimal node that outputs a raw WGSL expression verbatim when built\nclass LiteralExpression extends Node {\n\n\tconstructor( literal ) {\n\n\t\tsuper();\n\t\tthis.literal = literal;\n\n\t}\n\n\tbuild() {\n\n\t\treturn this.literal;\n\n\t}\n\n}\n\n// wraps a FunctionNode so that build() returns just the function name\nclass PropertyRefNode extends Node {\n\n\tconstructor( node, output = 'property' ) {\n\n\t\tsuper();\n\t\tthis.node = node;\n\t\tthis.output = output;\n\n\t}\n\n\tbuild( builder ) {\n\n\t\treturn this.node.build( builder, this.output );\n\n\t}\n\n}\n\n// wraps a FunctionCallNode so that build() returns the inline call expression,\n// bypassing TempNode's variable wrapping\nclass InlineCallNode extends Node {\n\n\tconstructor( node ) {\n\n\t\tsuper();\n\t\tthis.node = node;\n\n\t}\n\n\tbuild( builder ) {\n\n\t\treturn this.node.generate( builder );\n\n\t}\n\n}\n\n// returns the node that should be registered as an include for the given arg\nfunction getIncludeNode( arg ) {\n\n\tif ( typeof arg === 'function' ) {\n\n\t\tif ( arg.functionNode ) return arg.functionNode;\n\t\tif ( arg.isStruct ) return arg.layout;\n\t\telse return null;\n\n\t} else if ( arg.isNode ) {\n\n\t\treturn new PropertyRefNode( arg );\n\n\t} else {\n\n\t\treturn null;\n\n\t}\n\n}\n\n// extract dependency nodes from template args for include registration\nfunction extractIncludes( args ) {\n\n\tconst includes = [];\n\tfor ( const arg of args ) {\n\n\t\tif ( Array.isArray( arg ) ) {\n\n\t\t\tfor ( const element of arg ) {\n\n\t\t\t\tconst node = getIncludeNode( element );\n\t\t\t\tif ( node ) includes.push( node );\n\n\t\t\t}\n\n\t\t} else {\n\n\t\t\t// WGSLTagCodeNodes should be inlined if found in a template so skip it here\n\t\t\tif ( ! ( arg instanceof WGSLTagCodeNode ) ) {\n\n\t\t\t\tconst node = getIncludeNode( arg );\n\t\t\t\tif ( node ) includes.push( node );\n\n\t\t\t}\n\n\t\t}\n\n\t}\n\n\treturn includes;\n\n}\n\n// normalize args so generate can resolve them uniformly with build():\n// - callable wrappers > PropertyRefNode (emits just the function name)\n// - struct callables > StructTypeNode (emits the type name via build)\n// - FunctionCallNodes > InlineCallNode (emits inline call)\nfunction normalizeArgs( args ) {\n\n\treturn args.map( arg => {\n\n\t\tif ( typeof arg === 'function' && arg.functionNode ) return new PropertyRefNode( arg.functionNode );\n\t\tif ( typeof arg === 'function' && arg.isStruct ) return arg.layout;\n\t\tif ( arg && arg.isNode && arg.functionNode ) return new InlineCallNode( arg );\n\t\tif ( arg && arg.isNode ) {\n\n\t\t\tif ( arg instanceof WGSLTagCodeNode ) {\n\n\t\t\t\t// use a custom flag for this node to inline the output\n\t\t\t\treturn new PropertyRefNode( arg, 'inline' );\n\n\t\t\t} else {\n\n\t\t\t\treturn new PropertyRefNode( arg );\n\n\t\t\t}\n\n\t\t}\n\n\t\treturn arg;\n\n\t} );\n\n}\n\n// interleave static tokens with resolved arg values\nfunction assembleTemplate( tokens, args, builder ) {\n\n\tlet code = '';\n\tfor ( let i = 0, l = tokens.length; i < l; i ++ ) {\n\n\t\tcode += tokens[ i ];\n\t\tif ( i < args.length ) {\n\n\t\t\tconst arg = args[ i ];\n\t\t\tif ( Array.isArray( arg ) ) {\n\n\t\t\t\t// include array — no text output\n\n\t\t\t} else if ( typeof arg === 'string' || typeof arg === 'number' ) {\n\n\t\t\t\tcode += String( arg );\n\n\t\t\t} else {\n\n\t\t\t\tcode += arg.build( builder );\n\n\t\t\t}\n\n\t\t}\n\n\t}\n\n\treturn code;\n\n}\n\nexport class WGSLTagFnNode extends FunctionNode {\n\n\tstatic get type() {\n\n\t\treturn 'WGSLTagFnNode';\n\n\t}\n\n\tconstructor( tokens, args, lang = 'wgsl' ) {\n\n\t\tsuper( '', extractIncludes( args ), lang );\n\n\t\tthis.tokens = tokens;\n\t\tthis.args = args;\n\n\t}\n\n\t// assemble the signature from tokens and arg names then parse\n\tgetNodeFunction( builder ) {\n\n\t\tconst { tokens } = this;\n\t\tconst args = normalizeArgs( this.args );\n\n\t\tconst nodeData = builder.getDataFromNode( this );\n\t\tlet nodeFunction = nodeData.nodeFunction;\n\t\tif ( nodeFunction === undefined ) {\n\n\t\t\t// reconstruct the full code with known names for struct args\n\t\t\t// and dummy identifiers for everything else\n\t\t\tlet fullCode = '';\n\t\t\tfor ( let i = 0, l = tokens.length; i < l; i ++ ) {\n\n\t\t\t\tfullCode += tokens[ i ];\n\n\t\t\t\tif ( i < args.length ) {\n\n\t\t\t\t\tconst arg = args[ i ];\n\t\t\t\t\tif ( Array.isArray( arg ) ) {\n\n\t\t\t\t\t\t// include array — no text output\n\n\t\t\t\t\t} else if ( typeof arg === 'string' || typeof arg === 'number' ) {\n\n\t\t\t\t\t\t// literals\n\t\t\t\t\t\tfullCode += String( arg );\n\n\t\t\t\t\t} else if ( arg.isStructLayoutNode ) {\n\n\t\t\t\t\t\t// struct type node\n\t\t\t\t\t\tfullCode += arg.getNodeType( builder );\n\n\t\t\t\t\t} else if ( arg.isStruct ) {\n\n\t\t\t\t\t\t// struct\n\t\t\t\t\t\tfullCode += arg.layout.getNodeType( builder );\n\n\t\t\t\t\t} else {\n\n\t\t\t\t\t\tfullCode += '_arg' + i;\n\n\t\t\t\t\t}\n\n\t\t\t\t}\n\n\t\t\t}\n\n\t\t\t// remove comments\n\t\t\tfullCode = fullCode.replace( /\\/\\/.+[\\n\\r]/g, '' );\n\n\t\t\t// parse it so we have the signature defined - we will define the body content after\n\t\t\tnodeFunction = builder.parser.parseFunction( fullCode );\n\t\t\tnodeData.nodeFunction = nodeFunction;\n\n\t\t}\n\n\t\treturn nodeFunction;\n\n\t}\n\n\t// get the code for the function\n\tgenerate( builder, output ) {\n\n\t\tconst result = super.generate( builder, output );\n\t\tconst fullCode = assembleTemplate( this.tokens, normalizeArgs( this.args ), builder );\n\n\t\tconst { type } = this.getNodeFunction( builder );\n\t\tconst nodeCode = builder.getCodeFromNode( this, type );\n\n\t\tnodeCode.code = fullCode.replace( /\\/\\/.+[\\n\\r]/g, '' ).replace( /->\\s*void/, '' ).trim();\n\t\treturn result;\n\n\t}\n\n}\n\nexport class WGSLTagCodeNode extends CodeNode {\n\n\tstatic get type() {\n\n\t\treturn 'WGSLTagCodeNode';\n\n\t}\n\n\tconstructor( tokens, args, lang = 'wgsl' ) {\n\n\t\tsuper( '', extractIncludes( args ), lang );\n\n\t\tthis.tokens = tokens;\n\t\tthis.args = args;\n\n\t}\n\n\tbuild( builder, output ) {\n\n\t\tif ( output === 'inline' ) {\n\n\t\t\treturn assembleTemplate( this.tokens, normalizeArgs( this.args ), builder );\n\n\t\t} else {\n\n\t\t\treturn super.build( builder, output );\n\n\t\t}\n\n\t}\n\n\tgenerate( builder ) {\n\n\t\tsuper.generate( builder );\n\n\t\tconst nodeCode = builder.getCodeFromNode( this, this.getNodeType( builder ) );\n\t\tnodeCode.code = assembleTemplate( this.tokens, normalizeArgs( this.args ), builder );\n\t\treturn nodeCode.code;\n\n\t}\n\n}\n\nconst getFn = functionNode => {\n\n\tconst fn = ( ...params ) => {\n\n\t\t// wrap string parameter values as raw WGSL expressions so they\n\t\t// output verbatim as identifiers like local variable names\n\t\tif ( params.length === 1 && params[ 0 ] && typeof params[ 0 ] === 'object' && ! params[ 0 ].isNode ) {\n\n\t\t\tconst obj = params[ 0 ];\n\t\t\tfor ( const key in obj ) {\n\n\t\t\t\tif ( typeof obj[ key ] === 'string' ) {\n\n\t\t\t\t\tobj[ key ] = new LiteralExpression( obj[ key ] );\n\n\t\t\t\t}\n\n\t\t\t}\n\n\t\t}\n\n\t\treturn functionNode.call( ...params );\n\n\t};\n\n\tfn.functionNode = functionNode;\n\treturn fn;\n\n};\n\n// template tag literal function version of \"wgslFn\" & \"wgsl\" to generate\n// functions & code snippets respectively\nexport const wgslTagFn = ( tokens, ...args ) => getFn( new WGSLTagFnNode( tokens, args ) );\nexport const wgslTagCode = ( tokens, ...args ) => new WGSLTagCodeNode( tokens, args );\n\n// glsl versions\nexport const glslTagFn = ( tokens, ...args ) => getFn( new WGSLTagFnNode( tokens, args, 'glsl' ) );\nexport const glslTagCode = ( tokens, ...args ) => new WGSLTagCodeNode( tokens, args, 'glsl' );\n"
  },
  {
    "path": "src/webgpu/lib/wgsl/common.wgsl.js",
    "content": "import { wgslFn, uint, float } from 'three/tsl';\nimport { rayStruct } from './structs.wgsl.js';\n\nexport const constants = {\n\tBVH_STACK_DEPTH: uint( 60 ),\n\tINFINITY: float( 1e20 ),\n};\n\nexport const ndcToCameraRay = wgslFn( /* wgsl*/`\n\n\tfn ndcToCameraRay( ndc: vec2f, inverseModelViewProjection: mat4x4f ) -> Ray {\n\n\t\t// Calculate the ray by picking the points at the near and far plane and deriving the ray\n\t\t// direction from the two points. This approach works for both orthographic and perspective\n\t\t// camera projection matrices.\n\t\t// The returned ray direction is not normalized and extends to the camera far plane.\n\t\tvar homogeneous = vec4f();\n\t\tvar ray = Ray();\n\n\t\thomogeneous = inverseModelViewProjection * vec4f( ndc, 0.0, 1.0 );\n\t\tray.origin = homogeneous.xyz / homogeneous.w;\n\n\t\thomogeneous = inverseModelViewProjection * vec4f( ndc, 1.0, 1.0 );\n\t\tray.direction = ( homogeneous.xyz / homogeneous.w ) - ray.origin;\n\n\t\treturn ray;\n\n\t}\n`, [ rayStruct ] );\n"
  },
  {
    "path": "src/webgpu/lib/wgsl/structs.wgsl.js",
    "content": "import { StructTypeNode } from 'three/webgpu';\n\nexport const rayStruct = new StructTypeNode( {\n\torigin: 'vec3f',\n\tdirection: 'vec3f',\n}, 'Ray' );\n\nexport const bvhNodeBoundsStruct = new StructTypeNode( {\n\tmin: 'array<f32, 3>',\n\tmax: 'array<f32, 3>',\n}, 'BVHBoundingBox' );\nbvhNodeBoundsStruct.getLength = () => 6;\n\nexport const bvhNodeStruct = new StructTypeNode( {\n\tbounds: 'BVHBoundingBox',\n\trightChildOrTriangleOffset: 'uint',\n\tsplitAxisOrTriangleCount: 'uint',\n}, 'BVHNode' );\nbvhNodeStruct.getLength = () => bvhNodeBoundsStruct.getLength() + 2;\n\nexport const intersectionResultStruct = new StructTypeNode( {\n\tdidHit: 'bool',\n\tindices: 'vec4u',\n\tnormal: 'vec3f',\n\tbarycoord: 'vec3f',\n\tside: 'float',\n\tdist: 'float',\n}, 'IntersectionResult' );\n"
  },
  {
    "path": "src/webgpu/nodes/common.wgsl.js",
    "content": "import { float, int } from 'three/tsl';\n\nexport const constants = {\n\tPARALLEL_EPSILON: float( 1e-10 ),\n\tAREA_EPSILON: float( 1e-10 ),\n\tDIST_THRESHOLD: float( 1e-10 ),\n\tVERTEX_EPSILON: float( 1e-10 ),\n\n\tDOUBLE_SIDE: int( 0 ),\n\tBACK_SIDE: int( - 1 ),\n\tFRONT_SIDE: int( 1 ),\n};\n"
  },
  {
    "path": "src/webgpu/nodes/overlapFunctions.wgsl.js",
    "content": "import { wgslTagFn } from '../lib/nodes/WGSLTagFnNode.js';\nimport { constants } from './common.wgsl.js';\nimport { TriWGSL, LineWGSL, PlaneWGSL } from './primitives.js';\nimport { clipResultStruct } from './structs.wgsl.js';\n\nconst { PARALLEL_EPSILON, AREA_EPSILON, DIST_THRESHOLD, VERTEX_EPSILON } = constants;\n\n// Clips triangle (a, b, c) against a plane (plane.xyz = normal, plane.w = constant,\n// equation: dot(normal, p) + constant >= 0 is the kept side).\n// Returns 0, 1, or 2 sub-triangles covering the kept portion.\nexport const clipTriangleToPlane = wgslTagFn/* wgsl */`\n\tfn clipTriangleToPlane( a: vec3f, b: vec3f, c: vec3f, plane: vec4f ) -> ${ clipResultStruct } {\n\n\t\tvar result: ${ clipResultStruct };\n\n\t\tlet da = dot( plane.xyz, a ) + plane.w;\n\t\tlet db = dot( plane.xyz, b ) + plane.w;\n\t\tlet dc = dot( plane.xyz, c ) + plane.w;\n\n\t\tlet aKept = da >= 0.0;\n\t\tlet bKept = db >= 0.0;\n\t\tlet cKept = dc >= 0.0;\n\t\tlet keptCount = u32( aKept ) + u32( bKept ) + u32( cKept );\n\n\t\t// all kept - return the original triangle\n\t\tif ( keptCount == 3u ) {\n\n\t\t\tresult.count = 1u;\n\t\t\tresult.a0 = a;\n\t\t\tresult.b0 = b;\n\t\t\tresult.c0 = c;\n\t\t\treturn result;\n\n\t\t}\n\n\t\t// all discarded\n\t\tif ( keptCount == 0u ) {\n\n\t\t\treturn result;\n\n\t\t}\n\n\t\t// vertex positions and plane distances packed into arrays for index-based access\n\t\tlet pts   = array<vec3f, 3>( a, b, c );\n\t\tlet dists = array<f32, 3>( da, db, dc );\n\n\t\tif ( keptCount == 1u ) {\n\n\t\t\t// apex is the lone kept vertex; the other two are clipped away\n\t\t\tvar apexIdx = 0u;\n\t\t\tif ( bKept ) {\n\n\t\t\t\tapexIdx = 1u;\n\n\t\t\t} else if ( cKept ) {\n\n\t\t\t\tapexIdx = 2u;\n\n\t\t\t}\n\n\t\t\tlet apex = pts[ apexIdx ];\n\t\t\tlet clipped0 = pts[ ( apexIdx + 1u ) % 3u ];\n\t\t\tlet clipped1 = pts[ ( apexIdx + 2u ) % 3u ];\n\n\t\t\tlet apexDist = dists[ apexIdx ];\n\t\t\tlet clipped0Dist = dists[ ( apexIdx + 1u ) % 3u ];\n\t\t\tlet clipped1Dist = dists[ ( apexIdx + 2u ) % 3u ];\n\n\t\t\t// parametric intersection along apex->clipped0 and apex->clipped1\n\t\t\tlet t0 = apexDist / ( apexDist - clipped0Dist );\n\t\t\tlet t1 = apexDist / ( apexDist - clipped1Dist );\n\n\t\t\tresult.count = 1u;\n\t\t\tresult.a0 = apex;\n\t\t\tresult.b0 = mix( apex, clipped0, t0 );\n\t\t\tresult.c0 = mix( apex, clipped1, t1 );\n\t\t\treturn result;\n\n\t\t}\n\n\t\t// the lone discarded vertex is cut off, leaving a quad that we split into two triangles\n\t\tvar discardedIdx = 2u;\n\t\tif ( ! aKept ) {\n\n\t\t\tdiscardedIdx = 0u;\n\n\t\t} else if ( ! bKept ) {\n\n\t\t\tdiscardedIdx = 1u;\n\n\t\t}\n\n\t\t// kept0 and kept1 are the two vertices on the kept side; discarded is the one being cut off\n\t\tlet kept0 = pts[ ( discardedIdx + 1u ) % 3u ];\n\t\tlet kept1 = pts[ ( discardedIdx + 2u ) % 3u ];\n\t\tlet discarded = pts[ discardedIdx ];\n\n\t\tlet kept0Dist = dists[ ( discardedIdx + 1u ) % 3u ];\n\t\tlet kept1Dist = dists[ ( discardedIdx + 2u ) % 3u ];\n\t\tlet discardedDist = dists[ discardedIdx ];\n\n\t\t// parametric intersections along kept0->discarded and kept1->discarded\n\t\tlet t0 = kept0Dist / ( kept0Dist - discardedDist );\n\t\tlet t1 = kept1Dist / ( kept1Dist - discardedDist );\n\n\t\tlet edge0Cut = mix( kept0, discarded, t0 );\n\t\tlet edge1Cut = mix( kept1, discarded, t1 );\n\n\t\t// quad (kept0, kept1, edge1Cut, edge0Cut) split into two triangles\n\t\tresult.count = 2u;\n\t\tresult.a0 = kept0;\n\t\tresult.b0 = kept1;\n\t\tresult.c0 = edge1Cut;\n\t\tresult.a1 = kept0;\n\t\tresult.b1 = edge1Cut;\n\t\tresult.c1 = edge0Cut;\n\t\treturn result;\n\n\t}\n`;\n\n// Clips the edge (lineStart -> lineEnd) to the portion lying at or below the\n// plane of triangle (a, b, c). The plane is always treated as up-facing.\n// Returns TrimResult.valid = false if the entire edge is above the plane.\nexport const trimToBeneathTriPlane = wgslTagFn/* wgsl */`\n\tfn trimToBeneathTriPlane( tri: ${ TriWGSL.struct }, line: ${ LineWGSL.struct }, output: ptr<function, ${ LineWGSL.struct }> ) -> bool {\n\n\t\t// compute the triangle plane, ensuring the normal faces up\n\t\tlet triNormal = ${ TriWGSL.getNormal }( tri );\n\t\tvar plane = ${ PlaneWGSL.fromNormalAndCoplanarPoint }( triNormal, tri.a );\n\t\tif ( plane.normal.y < 0.0 ) {\n\n\t\t\tplane.normal *= - 1.0;\n\t\t\tplane.constant *= - 1.0;\n\n\t\t}\n\n\t\tlet startDist = ${ PlaneWGSL.distanceToPoint }( plane, line.start );\n\t\tlet endDist = ${ PlaneWGSL.distanceToPoint }( plane, line.end );\n\n\t\tlet isStartOnPlane = abs( startDist ) < ${ PARALLEL_EPSILON };\n\t\tlet isEndOnPlane = abs( endDist ) < ${ PARALLEL_EPSILON };\n\n\t\tlet isStartBelow = ! isStartOnPlane && startDist < 0.0;\n\t\tlet isEndBelow = ! isEndOnPlane && endDist < 0.0;\n\n\t\t// coplanar/parallel - only valid if the line is below the plane\n\t\tlet lineDir = normalize( line.end - line.start );\n\t\tif ( abs( dot( plane.normal, lineDir ) ) < ${ PARALLEL_EPSILON } ) {\n\n\t\t\t// if the line is definitely above or on the plane then skip it\n\t\t\tif ( isStartOnPlane || ! isStartBelow ) {\n\n\t\t\t\treturn false;\n\n\t\t\t} else {\n\n\t\t\t\toutput.start = line.start;\n\t\t\t\toutput.end = line.end;\n\t\t\t\treturn true;\n\n\t\t\t}\n\n\t\t}\n\n\t\tif ( isStartBelow && isEndBelow ) {\n\n\t\t\t// both below - keep the full edge\n\t\t\toutput.start = line.start;\n\t\t\toutput.end = line.end;\n\t\t\treturn true;\n\n\t\t} else if ( ! isStartBelow && ! isEndBelow ) {\n\n\t\t\t// both above - discard\n\t\t\treturn false;\n\n\t\t} else {\n\n\t\t\t// straddling - clip at the plane intersection\n\t\t\tlet t = - startDist / ( endDist - startDist );\n\t\t\tlet planeHit = mix( line.start, line.end, t );\n\n\t\t\tif ( isStartBelow ) {\n\n\t\t\t\toutput.start = line.start;\n\t\t\t\toutput.end = planeHit;\n\t\t\t\treturn true;\n\n\t\t\t} else if ( isEndBelow ) {\n\n\t\t\t\toutput.end = line.end;\n\t\t\t\toutput.start = planeHit;\n\t\t\t\treturn true;\n\n\t\t\t}\n\n\t\t}\n\n\t\treturn false;\n\n\t}\n`;\n\n// Returns the parametric overlap [t0, t1] of the edge (lineStart -> lineEnd)\n// against triangle (a, b, c) projected onto the XZ plane.\n// t0 and t1 are in [0, 1] along the original edge. valid = false if no overlap.\nexport const getProjectedOverlapRange = wgslTagFn/* wgsl */`\n\tfn getProjectedOverlapRange( line: ${ LineWGSL.struct }, tri: ${ TriWGSL.struct }, output: ptr<function, ${ LineWGSL.struct }> ) -> bool {\n\n\t\t// project everything to XZ\n\t\tvar _tri = tri;\n\t\t_tri.a.y = 0.0;\n\t\t_tri.b.y = 0.0;\n\t\t_tri.c.y = 0.0;\n\n\t\tvar _line = line;\n\t\t_line.start.y = 0.0;\n\t\t_line.end.y = 0.0;\n\n\t\t// skip degenerate projected triangles\n\t\tif ( ${ TriWGSL.getArea }( _tri ) <= ${ AREA_EPSILON } ) {\n\n\t\t\treturn false;\n\n\t\t}\n\n\t\tvar dir = _line.end - _line.start;\n\t\tlet lineDistance = length( dir );\n\t\tdir = dir / lineDistance;\n\n\t\t// cutting plane: orthogonal to the edge direction in XZ, passing through ls\n\t\tlet normal = ${ TriWGSL.getNormal }( _tri );\n\t\tlet orthoNormal = normalize( cross( dir, normal ) );\n\t\tlet orthoPlane = ${ PlaneWGSL.fromNormalAndCoplanarPoint }( orthoNormal, _line.start );\n\n\t\t// find the two intersections of triangle edges with the cutting plane\n\t\tvar intersectCount = 0u;\n\t\tvar triLineStart = vec3f( 0.0 );\n\t\tvar triLineEnd = vec3f( 0.0 );\n\n\t\tlet triPts = array<vec3f, 3>( _tri.a, _tri.b, _tri.c );\n\t\tfor ( var i = 0u; i < 3u; i ++ ) {\n\n\t\t\tlet p1 = triPts[ i ];\n\t\t\tlet p2 = triPts[ ( i + 1u ) % 3u ];\n\n\t\t\tlet distToStart = ${ PlaneWGSL.distanceToPoint }( orthoPlane, p1 );\n\t\t\tlet distToEnd = ${ PlaneWGSL.distanceToPoint }( orthoPlane, p2 );\n\n\t\t\tlet startIntersects = abs( distToStart ) < ${ DIST_THRESHOLD };\n\t\t\tlet endIntersects = abs( distToEnd ) < ${ DIST_THRESHOLD };\n\n\t\t\t// check of the edge intersects\n\t\t\tvar point = vec3f( 0.0 );\n\t\t\tif ( startIntersects && endIntersects ) {\n\n\t\t\t\tcontinue;\n\n\t\t\t} else if ( startIntersects ) {\n\n\t\t\t\tpoint = p1;\n\n\t\t\t} else if ( endIntersects ) {\n\n\t\t\t\tcontinue;\n\n\t\t\t} else if ( ( distToStart < 0.0 ) == ( distToEnd < 0.0 ) ) {\n\n\t\t\t\tcontinue;\n\n\t\t\t} else {\n\n\t\t\t\tlet t = distToStart / ( distToStart - distToEnd );\n\t\t\t\tpoint = mix( p1, p2, t );\n\n\t\t\t}\n\n\t\t\tif ( intersectCount == 0u ) {\n\n\t\t\t\ttriLineStart = point;\n\n\t\t\t} else if ( intersectCount == 1u ) {\n\n\t\t\t\ttriLineEnd = point;\n\n\t\t\t}\n\n\t\t\tintersectCount ++;\n\t\t\tif ( intersectCount == 2u ) {\n\n\t\t\t\tbreak;\n\n\t\t\t}\n\n\t\t}\n\n\t\tif ( intersectCount == 2u ) {\n\n\t\t\tlet triDir = normalize( triLineEnd - triLineStart );\n\t\t\tif ( dot( dir, triDir ) < 0.0 ) {\n\n\t\t\t\tlet tmp = triLineStart;\n\t\t\t\ttriLineStart = triLineEnd;\n\t\t\t\ttriLineEnd = tmp;\n\n\t\t\t}\n\n\t\t\t// project both segments onto dir and compute the overlap\n\t\t\tlet s1 = 0.0;\n\t\t\tlet e1 = dot( _line.end - _line.start, dir );\n\t\t\tlet s2 = dot( triLineStart - _line.start, dir );\n\t\t\tlet e2 = dot( triLineEnd - _line.start, dir );\n\t\t\tlet separated1 = e1 <= s2;\n\t\t\tlet separated2 = e2 <= s1;\n\n\t\t\tif ( separated1 || separated2 ) {\n\n\t\t\t\treturn false;\n\n\t\t\t}\n\n\t\t\toutput.start = mix( line.start, line.end, max( s1, s2 ) / lineDistance );\n\t\t\toutput.end = mix( line.start, line.end, min( e1, e2 ) / lineDistance );\n\n\t\t\treturn true;\n\n\t\t}\n\n\t\treturn false;\n\n\t}\n`;\n\n\n// Returns true if the edge (lineStart -> lineEnd) lies entirely along the Y axis\n// when projected to XZ — i.e. the line direction is nearly (0, ±1, 0).\nexport const isYProjectedLineDegenerate = wgslTagFn/* wgsl */`\n\tfn isYProjectedLineDegenerate( lineStart: vec3f, lineEnd: vec3f ) -> bool {\n\n\t\tlet dir = normalize( lineEnd - lineStart );\n\t\treturn abs( dir.y ) >= 1.0 - ${ VERTEX_EPSILON };\n\n\t}\n`;\n\n// Returns true if both endpoints of the edge (lineStart -> lineEnd) coincide\n// with two vertices of triangle (a, b, c) — i.e. the edge is a triangle edge.\nexport const isLineTriangleEdge = wgslTagFn/* wgsl */`\n\tfn isLineTriangleEdge( tri: ${ TriWGSL.struct }, line: ${ LineWGSL.struct } ) -> bool {\n\n\t\tlet triPts = array<vec3f, 3>( tri.a, tri.b, tri.c );\n\t\tvar startMatches = false;\n\t\tvar endMatches = false;\n\n\t\tlet start = line.start;\n\t\tlet end = line.end;\n\t\tfor ( var i = 0u; i < 3u; i ++ ) {\n\n\t\t\t// dot is sq length\n\t\t\tlet tp = triPts[ i ];\n\t\t\tlet ds = start - tp;\n\t\t\tlet de = end - tp;\n\t\t\tif ( ! startMatches && dot( ds, ds ) <= ${ VERTEX_EPSILON } ) {\n\n\t\t\t\tstartMatches = true;\n\n\t\t\t}\n\n\t\t\tif ( ! endMatches && dot( de, de ) <= ${ VERTEX_EPSILON } ) {\n\n\t\t\t\tendMatches = true;\n\n\t\t\t}\n\n\t\t\tif ( startMatches && endMatches ) {\n\n\t\t\t\treturn true;\n\n\t\t\t}\n\n\t\t}\n\n\t\treturn startMatches && endMatches;\n\n\t}\n`;\n"
  },
  {
    "path": "src/webgpu/nodes/primitives.js",
    "content": "import { StructTypeNode } from 'three/webgpu';\nimport { wgslTagFn } from '../lib/nodes/WGSLTagFnNode.js';\n\nconst lineStruct = new StructTypeNode( {\n\tstart: 'vec3',\n\tend: 'vec3',\n} );\n\nconst triStruct = new StructTypeNode( {\n\ta: 'vec3',\n\tb: 'vec3',\n\tc: 'vec3',\n} );\n\nconst planeStruct = new StructTypeNode( {\n\tnormal: 'vec3',\n\tconstant: 'float',\n} );\n\nexport const LineWGSL = {\n\tstruct: lineStruct,\n};\n\nexport const TriWGSL = {\n\tstruct: triStruct,\n\tgetNormal: wgslTagFn/* wgsl */`\n\t\tfn tri_getNormal( tri: ${ triStruct } ) -> vec3f {\n\n\t\t\tlet n = cross( tri.c - tri.b, tri.a - tri.b );\n\t\t\tlet lenSq = dot( n, n );\n\t\t\tif ( lenSq < 1e-12 ) {\n\n\t\t\t\treturn vec3( 0.0 );\n\n\t\t\t}\n\n\t\t\treturn n * inverseSqrt( lenSq );\n\n\t\t}\n\t`,\n\tgetArea: wgslTagFn/* wgsl */`\n\t\tfn tri_getArea( tri: ${ triStruct } ) -> f32 {\n\n\t\t\tlet n = cross( tri.c - tri.b, tri.a - tri.b );\n\t\t\tlet lenSq = dot( n, n );\n\t\t\treturn sqrt( lenSq ) * 0.5;\n\n\t\t}\n\t`\n};\n\nexport const PlaneWGSL = {\n\tstruct: planeStruct,\n\tfromNormalAndCoplanarPoint: wgslTagFn/* wgsl */`\n\t\tfn plane_fromNormalAndCoplanarPoint( norm: vec3f, point: vec3f ) -> ${ planeStruct } {\n\n\t\t\tvar plane: ${ planeStruct };\n\t\t\tplane.normal = norm;\n\t\t\tplane.constant = - dot( point, norm );\n\t\t\treturn plane;\n\n\t\t}\n\t`,\n\tdistanceToPoint: wgslTagFn/* wgsl */`\n\t\tfn plane_distanceToPoint( plane: ${ planeStruct }, point: vec3f ) -> f32 {\n\n\t\t\treturn dot( plane.normal, point ) + plane.constant;\n\n\t\t}\n\t`,\n};\n"
  },
  {
    "path": "src/webgpu/nodes/structs.wgsl.js",
    "content": "import { StructTypeNode } from 'three/webgpu';\n\nexport const edgeStruct = new StructTypeNode( {\n\tstart: 'array<f32, 3>',\n\tend: 'array<f32, 3>',\n\tindex: 'uint',\n}, 'Edge' );\nedgeStruct.getLength = () => 7;\n\nexport const clipResultStruct = new StructTypeNode( {\n\tcount: 'uint',\n\ta0: 'vec3f',\n\tb0: 'vec3f',\n\tc0: 'vec3f',\n\ta1: 'vec3f',\n\tb1: 'vec3f',\n\tc1: 'vec3f',\n}, 'ClipResult' );\n\n// One entry per qualifying (edge, triangle) pair recorded during kernel 2.\nexport const triEdgePairStruct = new StructTypeNode( {\n\tedgeIndex: 'uint',\n\tobjectIndex: 'uint',\n\ttriIndex: 'uint',\n\t_alignment0: 'uint',\n}, 'TriEdgePair' );\n\n// One entry per visible overlap interval recorded during kernel 3.\nexport const overlapRecordStruct = new StructTypeNode( {\n\tedgeIndex: 'uint',\n\tt0: 'float',\n\tt1: 'float',\n\t_alignment0: 'uint',\n}, 'OverlapRecord' );\n"
  },
  {
    "path": "src/webgpu/nodes/utils.wgsl.js",
    "content": "import { wgslTagFn } from '../lib/nodes/WGSLTagFnNode.js';\nimport { bvhNodeBoundsStruct } from '../lib/wgsl/structs.wgsl.js';\n\n// Transform all 8 corners of a BVH bounding box by the given matrix and\n// return the world-space AABB that encloses the result.\nexport const transformBVHBounds = wgslTagFn/* wgsl */`\n\tfn transformBVHBounds( bounds: ${ bvhNodeBoundsStruct }, matrix: mat4x4f ) -> ${ bvhNodeBoundsStruct } {\n\n\t\tlet bMin = bounds.min;\n\t\tlet bMax = bounds.max;\n\t\tvar wMin = vec3f( 3e38, 3e38, 3e38 );\n\t\tvar wMax = vec3f( - 3e38, - 3e38, - 3e38 );\n\t\tfor ( var ci = 0u; ci < 8u; ci = ci + 1u ) {\n\n\t\t\tlet corner = vec3f(\n\t\t\t\tselect( bMin[ 0 ], bMax[ 0 ], ( ci & 1u ) != 0u ),\n\t\t\t\tselect( bMin[ 1 ], bMax[ 1 ], ( ci & 2u ) != 0u ),\n\t\t\t\tselect( bMin[ 2 ], bMax[ 2 ], ( ci & 4u ) != 0u )\n\t\t\t);\n\t\t\tvar wc = matrix * vec4f( corner, 1.0 );\n\t\t\twc = wc / wc.w;\n\t\t\twMin = min( wMin, wc.xyz );\n\t\t\twMax = max( wMax, wc.xyz );\n\n\t\t}\n\n\t\tvar result: ${ bvhNodeBoundsStruct };\n\t\tresult.min[ 0 ] = wMin.x;\n\t\tresult.min[ 1 ] = wMin.y;\n\t\tresult.min[ 2 ] = wMin.z;\n\n\t\tresult.max[ 0 ] = wMax.x;\n\t\tresult.max[ 1 ] = wMax.y;\n\t\tresult.max[ 2 ] = wMax.z;\n\t\treturn result;\n\n\t}\n`;\n"
  },
  {
    "path": "src/webgpu/utils/ComputeKernel.js",
    "content": "export class ComputeKernel {\n\n\tget computeNode() {\n\n\t\treturn this.kernel.computeNode;\n\n\t}\n\n\tget workgroupSize() {\n\n\t\treturn this.kernel.workgroupSize;\n\n\t}\n\n\tset needsUpdate( v ) {\n\n\t\t// TODO: hack to force the kernel to rebuild since \"needsUpdate\" is not respected\n\t\tthis.setWorkgroupSize( ...this.workgroupSize );\n\n\t}\n\n\tconstructor( fn, options = {} ) {\n\n\t\tconst {\n\t\t\tworkgroupSize = [ 64 ],\n\t\t} = options;\n\n\t\t// this.workgroupSize = [ ...workgroupSize ];\n\t\tthis._fn = fn;\n\t\tthis.kernel = null;\n\n\t\tthis.setWorkgroupSize( ...workgroupSize );\n\n\t}\n\n\tdefineUniformAccessors( parameters ) {\n\n\t\tfor ( const key in parameters ) {\n\n\t\t\tif ( key in this ) {\n\n\t\t\t\tthrow new Error( `ComputeNode: Uniform name ${ key } is already defined.` );\n\n\t\t\t}\n\n\t\t\tconst node = parameters[ key ];\n\t\t\tif ( 'value' in node ) {\n\n\t\t\t\tObject.defineProperty( this, key, {\n\t\t\t\t\tget() {\n\n\t\t\t\t\t\treturn parameters[ key ].value;\n\n\t\t\t\t\t},\n\t\t\t\t\tset( v ) {\n\n\t\t\t\t\t\tparameters[ key ].value = v;\n\n\t\t\t\t\t},\n\t\t\t\t} );\n\n\t\t\t}\n\n\t\t}\n\n\t}\n\n\tsetWorkgroupSize( x = 64, y = 1, z = 1 ) {\n\n\t\tthis.kernel = this._fn.computeKernel( [ x, y, z ] );\n\t\treturn this;\n\n\t}\n\n\tgetDispatchSize( tx = 1, ty = 1, tz = 1, target = [] ) {\n\n\t\tconst [ wgx, wgy, wgz ] = this.workgroupSize;\n\t\ttarget.length = 3;\n\t\ttarget[ 0 ] = Math.ceil( tx / wgx );\n\t\ttarget[ 1 ] = Math.ceil( ty / wgy );\n\t\ttarget[ 2 ] = Math.ceil( tz / wgz );\n\t\treturn target;\n\n\t}\n\n}\n"
  },
  {
    "path": "src/worker/SilhouetteGeneratorWorker.js",
    "content": "import { BufferAttribute, BufferGeometry } from 'three';\nimport { OUTPUT_BOTH } from '../SilhouetteGenerator';\n\nconst NAME = 'SilhouetteGeneratorWorker';\nexport class SilhouetteGeneratorWorker {\n\n\tconstructor() {\n\n\t\tthis.running = false;\n\t\tthis.worker = new Worker( new URL( './silhouetteAsync.worker.js', import.meta.url ), { type: 'module' } );\n\t\tthis.worker.onerror = e => {\n\n\t\t\tif ( e.message ) {\n\n\t\t\t\tthrow new Error( `${ NAME }: Could not create Web Worker with error \"${ e.message }\"` );\n\n\t\t\t} else {\n\n\t\t\t\tthrow new Error( `${ NAME }: Could not create Web Worker.` );\n\n\t\t\t}\n\n\t\t};\n\n\t}\n\n\tgenerate( geometry, options = {} ) {\n\n\t\tif ( this.running ) {\n\n\t\t\tthrow new Error( `${ NAME }: Already running job.` );\n\n\t\t}\n\n\t\tif ( this.worker === null ) {\n\n\t\t\tthrow new Error( `${ NAME }: Worker has been disposed.` );\n\n\t\t}\n\n\t\tconst { worker } = this;\n\t\tthis.running = true;\n\n\t\treturn new Promise( ( resolve, reject ) => {\n\n\t\t\tworker.onerror = e => {\n\n\t\t\t\treject( new Error( `${ NAME }: ${ e.message }` ) );\n\t\t\t\tthis.running = false;\n\n\t\t\t};\n\n\t\t\tworker.onmessage = e => {\n\n\t\t\t\tthis.running = false;\n\t\t\t\tconst { data } = e;\n\n\t\t\t\tif ( data.error ) {\n\n\t\t\t\t\treject( new Error( data.error ) );\n\t\t\t\t\tworker.onmessage = null;\n\n\t\t\t\t} else if ( data.result ) {\n\n\t\t\t\t\tif ( options.output === OUTPUT_BOTH ) {\n\n\t\t\t\t\t\tconst result = data.result.map( info => {\n\n\t\t\t\t\t\t\tconst geometry = new BufferGeometry();\n\t\t\t\t\t\t\tgeometry.setAttribute( 'position', new BufferAttribute( info.position, 3, false ) );\n\t\t\t\t\t\t\tif ( info.index ) {\n\n\t\t\t\t\t\t\t\tgeometry.setIndex( new BufferAttribute( info.index, 1, false ) );\n\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\treturn geometry;\n\n\t\t\t\t\t\t} );\n\n\t\t\t\t\t\tresolve( result );\n\n\t\t\t\t\t} else {\n\n\t\t\t\t\t\tconst geometry = new BufferGeometry();\n\t\t\t\t\t\tgeometry.setAttribute( 'position', new BufferAttribute( data.result.position, 3, false ) );\n\t\t\t\t\t\tgeometry.setIndex( new BufferAttribute( data.result.index, 1, false ) );\n\t\t\t\t\t\tresolve( geometry );\n\n\t\t\t\t\t}\n\n\t\t\t\t\tworker.onmessage = null;\n\n\t\t\t\t} else if ( options.onProgress ) {\n\n\t\t\t\t\toptions.onProgress( data.progress );\n\n\t\t\t\t}\n\n\t\t\t};\n\n\t\t\tconst index = geometry.index ? geometry.index.array.slice() : null;\n\t\t\tconst position = geometry.attributes.position.array.slice();\n\t\t\tconst transfer = [ position.buffer ];\n\t\t\tif ( index ) {\n\n\t\t\t\ttransfer.push( index.buffer );\n\n\t\t\t}\n\n\t\t\tworker.postMessage( {\n\t\t\t\tindex,\n\t\t\t\tposition,\n\t\t\t\toptions: {\n\t\t\t\t\t...options,\n\t\t\t\t\tonProgress: null,\n\t\t\t\t\tincludedProgressCallback: Boolean( options.onProgress ),\n\t\t\t\t},\n\t\t\t}, transfer );\n\n\t\t} );\n\n\t}\n\n\tdispose() {\n\n\t\tthis.worker.terminate();\n\t\tthis.worker = null;\n\n\t}\n\n}\n"
  },
  {
    "path": "src/worker/silhouetteAsync.worker.js",
    "content": "import { BufferAttribute, BufferGeometry } from 'three';\nimport { OUTPUT_BOTH, SilhouetteGenerator } from '../SilhouetteGenerator.js';\n\nonmessage = function ( { data } ) {\n\n\tlet prevTime = performance.now();\n\tfunction onProgressCallback( progress ) {\n\n\t\tconst currTime = performance.now();\n\t\tif ( currTime - prevTime >= 10 || progress === 1.0 ) {\n\n\t\t\tpostMessage( {\n\n\t\t\t\terror: null,\n\t\t\t\tprogress,\n\n\t\t\t} );\n\t\t\tprevTime = currTime;\n\n\t\t}\n\n\t}\n\n\ttry {\n\n\t\tconst { index, position, options } = data;\n\t\tconst geometry = new BufferGeometry();\n\t\tgeometry.setIndex( new BufferAttribute( index, 1, false ) );\n\t\tgeometry.setAttribute( 'position', new BufferAttribute( position, 3, false ) );\n\n\t\tconst generator = new SilhouetteGenerator();\n\t\tgenerator.doubleSided = options.doubleSided ?? generator.doubleSided;\n\t\tgenerator.output = options.output ?? generator.output;\n\t\tgenerator.intScalar = options.intScalar ?? generator.intScalar;\n\t\tgenerator.sortTriangles = options.sortTriangles ?? generator.sortTriangles;\n\t\tconst task = generator.generate( geometry, {\n\t\t\tonProgress: onProgressCallback,\n\t\t} );\n\n\t\tlet result = task.next();\n\t\twhile ( ! result.done ) {\n\n\t\t\tresult = task.next();\n\n\t\t}\n\n\t\tlet buffers, output;\n\t\tif ( generator.output === OUTPUT_BOTH ) {\n\n\t\t\tbuffers = [];\n\t\t\toutput = [];\n\t\t\tresult.value.forEach( g => {\n\n\t\t\t\tconsole.log( g );\n\t\t\t\tconst posArr = g.attributes.position.array;\n\t\t\t\tconst indexArr = g.index?.array || null;\n\t\t\t\toutput.push( {\n\t\t\t\t\tposition: posArr,\n\t\t\t\t\tindex: indexArr,\n\t\t\t\t} );\n\t\t\t\tbuffers.push(\n\t\t\t\t\tposArr.buffer,\n\t\t\t\t\tindexArr?.buffer,\n\t\t\t\t);\n\n\t\t\t} );\n\n\t\t} else {\n\n\t\t\tconst posArr = result.value.attributes.position.array;\n\t\t\tconst indexArr = result.value.index.array;\n\t\t\toutput = {\n\t\t\t\tposition: posArr,\n\t\t\t\tindex: indexArr,\n\t\t\t};\n\t\t\tbuffers = [ posArr.buffer, indexArr.buffer ];\n\n\t\t}\n\n\t\tpostMessage( {\n\n\t\t\tresult: output,\n\t\t\terror: null,\n\t\t\tprogress: 1,\n\n\t\t}, buffers.filter( b => ! ! b ) );\n\n\t} catch ( error ) {\n\n\t\tpostMessage( {\n\n\t\t\terror,\n\t\t\tprogress: 1,\n\n\t\t} );\n\n\t}\n\n};\n"
  },
  {
    "path": "test/Utils.getProjectedLineOverlap.test.js",
    "content": "import { getProjectedLineOverlap } from '../src/utils/getProjectedLineOverlap.js';\nimport { ExtendedTriangle } from 'three-mesh-bvh';\nimport { Vector3, Line3 } from 'three';\n\ndescribe( 'getProjectedLineOverlap', () => {\n\n\tit( 'should return portion of the line that overlaps on projection.', () => {\n\n\t\tconst triangle = new ExtendedTriangle(\n\t\t\tnew Vector3( 1, 0, 1 ),\n\t\t\tnew Vector3( - 1, 1, 0 ),\n\t\t\tnew Vector3( 1, 0, - 1 ),\n\t\t);\n\n\t\tlet line, target;\n\t\ttarget = new Line3();\n\t\tline = new Line3( new Vector3( 2, - 1, 0 ), new Vector3( - 2, 1, 0 ) );\n\n\t\texpect( getProjectedLineOverlap( line, triangle, target ) ).toBeTruthy();\n\t\texpect( [ ...target.start ] ).toEqual( [ 1, - 0.5, 0 ] );\n\t\texpect( [ ...target.end ] ).toEqual( [ - 1, 0.5, 0 ] );\n\n\t} );\n\n\tit( 'should return null if there is no overlap.', () => {\n\n\t\tconst triangle = new ExtendedTriangle(\n\t\t\tnew Vector3( 1, 0, 1 ),\n\t\t\tnew Vector3( - 1, 1, 0 ),\n\t\t\tnew Vector3( 1, 0, - 1 ),\n\t\t);\n\n\t\tlet line, target;\n\t\ttarget = new Line3();\n\t\tline = new Line3( new Vector3( 3, - 1, 0 ), new Vector3( 1, 1, 0 ) );\n\n\t\texpect( getProjectedLineOverlap( line, triangle, target ) ).toBeFalsy();\n\n\t} );\n\n} );\n"
  },
  {
    "path": "test/Utils.triangleLineUtils.test.js",
    "content": "import { Line3, Vector3 } from 'three';\nimport { ExtendedTriangle } from 'three-mesh-bvh';\nimport { isYProjectedTriangleDegenerate, isLineTriangleEdge } from '../src/utils/triangleLineUtils.js';\n\ndescribe( 'isYProjectedTriangleDegenerate', () => {\n\n\tit( 'should return that a vertical triangle is degenerate.', () => {\n\n\t\tconst triangle = new ExtendedTriangle(\n\t\t\tnew Vector3( 1, 1, 0 ),\n\t\t\tnew Vector3( 0, 0, 0 ),\n\t\t\tnew Vector3( 1, - 1, 0 ),\n\t\t);\n\t\texpect( isYProjectedTriangleDegenerate( triangle ) ).toBe( true );\n\n\t} );\n\n\tit( 'should return that an almost vertical triangle is degenerate.', () => {\n\n\t\tconst triangle = new ExtendedTriangle(\n\t\t\tnew Vector3( 1, 1, 1e-16 ),\n\t\t\tnew Vector3( 0, 0, 0 ),\n\t\t\tnew Vector3( 1, - 1, - 1e-16 ),\n\t\t);\n\t\texpect( isYProjectedTriangleDegenerate( triangle ) ).toBe( true );\n\n\t} );\n\n\tit( 'should return that a non vertical triangle is not degenerate.', () => {\n\n\t\tconst triangle = new ExtendedTriangle(\n\t\t\tnew Vector3( 1, 1, 1 ),\n\t\t\tnew Vector3( 0, 0, 0 ),\n\t\t\tnew Vector3( 1, - 1, - 1 ),\n\t\t);\n\t\texpect( isYProjectedTriangleDegenerate( triangle ) ).toBe( false );\n\n\t} );\n\n} );\n\ndescribe( 'isLineTriangleEdge', () => {\n\n\tit( 'should return true if the line is on the triangle edge.', () => {\n\n\t\tconst triangle = new ExtendedTriangle(\n\t\t\tnew Vector3( 1, 1, 1 ),\n\t\t\tnew Vector3( 0, 0, 0 ),\n\t\t\tnew Vector3( 1, - 1, - 1 ),\n\t\t);\n\n\t\tlet l1, l2, l3;\n\t\tl1 = new Line3( triangle.a, triangle.b );\n\t\tl2 = new Line3( triangle.b, triangle.c );\n\t\tl3 = new Line3( triangle.c, triangle.a );\n\t\texpect( isLineTriangleEdge( triangle, l1 ) ).toBe( true );\n\t\texpect( isLineTriangleEdge( triangle, l2 ) ).toBe( true );\n\t\texpect( isLineTriangleEdge( triangle, l3 ) ).toBe( true );\n\n\t\tl1 = new Line3( triangle.b, triangle.a );\n\t\tl2 = new Line3( triangle.c, triangle.b );\n\t\tl3 = new Line3( triangle.a, triangle.c );\n\t\texpect( isLineTriangleEdge( triangle, l1 ) ).toBe( true );\n\t\texpect( isLineTriangleEdge( triangle, l2 ) ).toBe( true );\n\t\texpect( isLineTriangleEdge( triangle, l3 ) ).toBe( true );\n\n\t} );\n\n\tit( 'should return false if the line is not on the triangle edge.', () => {\n\n\t\tconst triangle = new ExtendedTriangle(\n\t\t\tnew Vector3( 1, 1, 1 ),\n\t\t\tnew Vector3( 0, 0, 0 ),\n\t\t\tnew Vector3( 1, - 1, - 1 ),\n\t\t);\n\n\t\tconst line = new Line3( new Vector3( 0, 0, 1 ), new Vector3( 0, 0, - 1 ) );\n\t\texpect( isLineTriangleEdge( triangle, line ) ).toBe( false );\n\n\t} );\n\n} );\n"
  },
  {
    "path": "test/Utils.trimToBeneathTriPlane.test.js",
    "content": "import { ExtendedTriangle } from 'three-mesh-bvh';\nimport { trimToBeneathTriPlane } from '../src/utils/trimToBeneathTriPlane.js';\nimport { Vector3, Line3 } from 'three';\n\ndescribe( 'trimToBeneathTriPlane', () => {\n\n\tit( 'should trim a line to beneath the triangle.', () => {\n\n\t\tconst triangle = new ExtendedTriangle(\n\t\t\tnew Vector3( 1, 0, 0 ),\n\t\t\tnew Vector3( 0, 0, 1 ),\n\t\t\tnew Vector3( - 1, 0, 0 ),\n\t\t);\n\n\t\tlet line, target, vec;\n\t\tvec = new Vector3();\n\t\ttarget = new Line3();\n\t\tline = new Line3( new Vector3( 0, - 1, 0.5 ), new Vector3( 0, 1, 0.5 ) );\n\n\t\texpect( trimToBeneathTriPlane( triangle, line, target ) ).toBe( true );\n\t\texpect( target.distance() ).toBe( 1 );\n\t\texpect( target.at( 0.5, vec ).y ).toBeLessThan( 0 );\n\t\texpect( triangle.getNormal( vec ).y ).toBe( - 1 );\n\n\t} );\n\n\tit( 'should trim a line to beneath the triangle if flipped.', () => {\n\n\t\tconst triangle = new ExtendedTriangle(\n\t\t\tnew Vector3( - 1, 0, 0 ),\n\t\t\tnew Vector3( 0, 0, 1 ),\n\t\t\tnew Vector3( 1, 0, 0 ),\n\t\t);\n\n\t\tlet line, target, vec;\n\t\tvec = new Vector3();\n\t\ttarget = new Line3();\n\t\tline = new Line3( new Vector3( 0, - 1, 0.5 ), new Vector3( 0, 1, 0.5 ) );\n\n\t\texpect( trimToBeneathTriPlane( triangle, line, target ) ).toBe( true );\n\t\texpect( target.distance() ).toBe( 1 );\n\t\texpect( target.at( 0.5, vec ).y ).toBeLessThan( 0 );\n\t\texpect( triangle.getNormal( vec ).y ).toBe( 1 );\n\n\t} );\n\n\tit( 'should return the whole line is completely beneath the triangle.', () => {\n\n\t\tconst triangle = new ExtendedTriangle(\n\t\t\tnew Vector3( - 1, 0, 0 ),\n\t\t\tnew Vector3( 0, 0, 1 ),\n\t\t\tnew Vector3( 1, 0, 0 ),\n\t\t);\n\n\t\tlet line, target;\n\t\ttarget = new Line3();\n\t\tline = new Line3( new Vector3( 0, - 2, 0.5 ), new Vector3( 0, - 0.5, 0.5 ) );\n\n\t\texpect( trimToBeneathTriPlane( triangle, line, target ) ).toBe( true );\n\t\texpect( target.distance() ).toBe( 1.5 );\n\t\texpect( target ).toEqual( line );\n\n\t} );\n\n\n\tit( 'should not return anything if the whole line is completely above the triangle.', () => {\n\n\t\tconst triangle = new ExtendedTriangle(\n\t\t\tnew Vector3( - 1, 0, 0 ),\n\t\t\tnew Vector3( 0, 0, 1 ),\n\t\t\tnew Vector3( 1, 0, 0 ),\n\t\t);\n\n\t\tlet line, target;\n\t\ttarget = new Line3();\n\t\tline = new Line3( new Vector3( 0, 2, 0.5 ), new Vector3( 0, 0.5, 0.5 ) );\n\n\t\texpect( trimToBeneathTriPlane( triangle, line, target ) ).toBe( false );\n\n\t} );\n\n} );\n"
  },
  {
    "path": "utils/CommandUtils.js",
    "content": "import { existsSync } from 'fs';\nimport { dirname, join } from 'path';\nimport { fileURLToPath } from 'url';\n\n/**\n * Walks up the directory hierarchy from the given path or file URL until a package.json is found.\n * Throws if no package.json is found before reaching the filesystem root.\n * @param {string} urlOrPath - Directory path or file URL (e.g. import.meta.url) to start searching from\n * @returns {string}\n */\nexport function findRootDir( urlOrPath = import.meta.url ) {\n\n\tconst dir = urlOrPath.startsWith( 'file://' ) ? dirname( fileURLToPath( urlOrPath ) ) : urlOrPath;\n\tif ( existsSync( join( dir, 'package.json' ) ) ) return dir;\n\n\tconst parent = dirname( dir );\n\tif ( parent === dir ) throw new Error( 'Could not find package.json' );\n\n\treturn findRootDir( parent );\n\n}\n"
  },
  {
    "path": "utils/docs/RenderDocsUtils.js",
    "content": "// Converts {@link url text} inline tags in a string to Markdown [text](url) links.\nexport function resolveLinks( str ) {\n\n\tif ( ! str ) return str;\n\treturn str.replace( /\\{@link\\s+(\\S+?)(?:\\s+([^}]*?))?\\}/g, ( _, url, text ) => {\n\n\t\treturn text ? `[${ text }](${ url })` : `[${ url }](${ url })`;\n\n\t} );\n\n}\n\n// Renders any @warn / @note custom tags from a doclet as GFM alert blocks.\nfunction renderAlertTags( doc ) {\n\n\tconst lines = [];\n\tfor ( const tag of ( doc.tags || [] ) ) {\n\n\t\tif ( tag.title === 'warn' || tag.title === 'note' ) {\n\n\t\t\tconst type = tag.title === 'warn' ? 'WARNING' : 'NOTE';\n\t\t\tlines.push( `> [!${ type }]` );\n\t\t\tfor ( const line of tag.value.split( '\\n' ) ) {\n\n\t\t\t\tlines.push( `> ${ line }` );\n\n\t\t\t}\n\n\t\t\tlines.push( '' );\n\n\t\t}\n\n\t}\n\n\treturn lines.join( '\\n' );\n\n}\n\n// Converts a heading name to its GitHub Markdown anchor id.\nexport function toAnchor( name ) {\n\n\treturn name.toLowerCase().replace( /[^a-z0-9]+/g, '' );\n\n}\n\n// Formats a callback typedef into an inline arrow-function type string.\n// e.g. \"( a: any, b: any ) => number\"\nfunction formatCallbackType( callbackDoc, callbackMap ) {\n\n\tconst params = ( callbackDoc.params || [] ).map( p => {\n\n\t\tconst type = formatType( p.type, callbackMap );\n\t\treturn `${ p.name }: ${ type }`;\n\n\t} );\n\n\tconst ret = ( callbackDoc.returns && callbackDoc.returns[ 0 ] )\n\t\t? formatType( callbackDoc.returns[ 0 ].type, callbackMap )\n\t\t: 'void';\n\n\tconst sig = params.length > 0 ? ` ${ params.join( ', ' ) } ` : '';\n\treturn `(${ sig }) => ${ ret }`;\n\n}\n\n// Formats a JSDoc type object into a type string, e.g. \"string | Object | null\".\n// Strips JSDoc's dot-generic syntax: Promise.<void> -> Promise<void>\n// Substitutes @callback typedef names with their inline arrow-function signature.\nexport function formatType( typeObj, callbackMap = {} ) {\n\n\tif ( ! typeObj || ! typeObj.names || typeObj.names.length === 0 ) return '';\n\treturn typeObj.names\n\t\t.map( t => {\n\n\t\t\tif ( callbackMap[ t ] ) return formatCallbackType( callbackMap[ t ], callbackMap );\n\t\t\treturn t.replace( /\\.</g, '<' );\n\n\t\t} )\n\t\t.join( ' | ' );\n\n}\n\n// Formats a single param into the inline signature style: \"name = default: Type\"\nexport function formatParam( param, callbackMap = {} ) {\n\n\tconst type = formatType( param.type, callbackMap );\n\n\tif ( param.defaultvalue !== undefined ) {\n\n\t\treturn `${ param.name } = ${ param.defaultvalue }: ${ type }`;\n\n\t}\n\n\treturn `${ param.name }: ${ type }`;\n\n}\n\n// Renders a parameter list into an array of lines. Expands any top-level params that have\n// dotted sub-params as inline destructured objects, with callback types expanded multi-line.\nfunction renderParamLines( allParams, callbackMap ) {\n\n\tconst topLevel = allParams.filter( p => ! p.name.includes( '.' ) );\n\n\tconst nestedMap = {};\n\tfor ( const p of allParams ) {\n\n\t\tif ( p.name.includes( '.' ) ) {\n\n\t\t\tconst topName = p.name.split( '.' )[ 0 ];\n\t\t\tif ( ! nestedMap[ topName ] ) nestedMap[ topName ] = [];\n\t\t\tnestedMap[ topName ].push( p );\n\n\t\t}\n\n\t}\n\n\tconst hasAnyNested = topLevel.some( p => nestedMap[ p.name ] );\n\tif ( ! hasAnyNested ) return null; // caller should use simple inline form\n\n\tconst lines = [];\n\ttopLevel.forEach( ( p, i ) => {\n\n\t\tconst nested = nestedMap[ p.name ];\n\t\tconst comma = i < topLevel.length - 1 ? ',' : '';\n\n\t\tif ( nested ) {\n\n\t\t\tlines.push( '\\t{' );\n\t\t\tfor ( const opt of nested ) {\n\n\t\t\t\tconst name = opt.name.split( '.' ).pop();\n\t\t\t\tconst defStr = opt.defaultvalue !== undefined ? ` = ${ opt.defaultvalue }` : '';\n\t\t\t\tconst optional = opt.optional && opt.defaultvalue === undefined ? '?' : '';\n\t\t\t\tconst typeName = opt.type && opt.type.names && opt.type.names[ 0 ];\n\t\t\t\tconst callbackDoc = typeName && callbackMap[ typeName ];\n\n\t\t\t\tif ( callbackDoc ) {\n\n\t\t\t\t\tconst cbParams = callbackDoc.params || [];\n\t\t\t\t\tconst cbRet = ( callbackDoc.returns && callbackDoc.returns[ 0 ] )\n\t\t\t\t\t\t? formatType( callbackDoc.returns[ 0 ].type, callbackMap )\n\t\t\t\t\t\t: 'void';\n\n\t\t\t\t\tlines.push( `\\t\\t${ name }${ defStr }${ optional }: (` );\n\t\t\t\t\tcbParams.forEach( ( cp, ci ) => {\n\n\t\t\t\t\t\tconst cpType = formatType( cp.type, callbackMap );\n\t\t\t\t\t\tconst cpComma = ci < cbParams.length - 1 ? ',' : '';\n\t\t\t\t\t\tlines.push( `\\t\\t\\t${ cp.name }: ${ cpType }${ cpComma }` );\n\n\t\t\t\t\t} );\n\t\t\t\t\tlines.push( `\\t\\t) => ${ cbRet },` );\n\n\t\t\t\t} else {\n\n\t\t\t\t\tconst type = formatType( opt.type, callbackMap );\n\t\t\t\t\tlines.push( `\\t\\t${ name }${ defStr }${ optional }: ${ type },` );\n\n\t\t\t\t}\n\n\t\t\t}\n\n\t\t\tlines.push( `\\t}${ comma }` );\n\n\t\t} else {\n\n\t\t\tlines.push( `\\t${ formatParam( p, callbackMap ) }${ comma }` );\n\n\t\t}\n\n\t} );\n\n\treturn lines;\n\n}\n\nexport function renderConstructor( classDoc, callbackMap = {} ) {\n\n\tconst lines = [];\n\n\tlines.push( '### .constructor' );\n\tlines.push( '' );\n\tlines.push( '```js' );\n\n\tconst paramLines = renderParamLines( classDoc.params || [], callbackMap );\n\tif ( paramLines ) {\n\n\t\tlines.push( 'constructor(' );\n\t\tlines.push( ...paramLines );\n\t\tlines.push( ')' );\n\n\t} else {\n\n\t\tconst sig = ( classDoc.params || [] )\n\t\t\t.filter( p => ! p.name.includes( '.' ) )\n\t\t\t.map( p => formatParam( p, callbackMap ) )\n\t\t\t.join( ', ' );\n\t\tlines.push( `constructor( ${ sig } )` );\n\n\t}\n\n\tlines.push( '```' );\n\tlines.push( '' );\n\n\t// Constructor description (JSDoc puts it in `description`, not `classdesc`)\n\tif ( classDoc.description ) {\n\n\t\tlines.push( classDoc.description );\n\t\tlines.push( '' );\n\n\t}\n\n\treturn lines.join( '\\n' );\n\n}\n\nexport function renderMember( doc, callbackMap = {} ) {\n\n\tconst lines = [];\n\n\tlines.push( `### .${ doc.name }` );\n\tlines.push( '' );\n\tlines.push( '```js' );\n\n\tconst type = formatType( doc.type, callbackMap );\n\tconst readonly = doc.readonly ? 'readonly ' : '';\n\tlines.push( `${ readonly }${ doc.name }: ${ type }` );\n\n\tlines.push( '```' );\n\tlines.push( '' );\n\n\tif ( doc.description ) {\n\n\t\tlines.push( doc.description );\n\t\tlines.push( '' );\n\n\t}\n\n\tlines.push( renderAlertTags( doc ) );\n\n\treturn lines.join( '\\n' );\n\n}\n\nfunction renderCallable( doc, heading, sigPrefix, callbackMap ) {\n\n\tconst lines = [];\n\n\tlines.push( heading );\n\tlines.push( '' );\n\tlines.push( '```js' );\n\n\tconst allParams = doc.params || [];\n\tconst topLevel = allParams.filter( p => ! p.name.includes( '.' ) );\n\n\tconst ret = ( doc.returns && doc.returns[ 0 ] )\n\t\t? formatType( doc.returns[ 0 ].type, callbackMap )\n\t\t: 'void';\n\n\tconst paramLines = renderParamLines( allParams, callbackMap );\n\tif ( paramLines ) {\n\n\t\tlines.push( `${ sigPrefix }${ doc.name }(` );\n\t\tlines.push( ...paramLines );\n\t\tlines.push( `): ${ ret }` );\n\n\t} else {\n\n\t\tconst params = topLevel.map( p => formatParam( p, callbackMap ) );\n\t\tconst singleLine = params.length\n\t\t\t? `${ sigPrefix }${ doc.name }( ${ params.join( ', ' ) } ): ${ ret }`\n\t\t\t: `${ sigPrefix }${ doc.name }(): ${ ret }`;\n\n\t\tif ( singleLine.length > 80 ) {\n\n\t\t\tlines.push( `${ sigPrefix }${ doc.name }(` );\n\t\t\tparams.forEach( ( p, i ) => {\n\n\t\t\t\tconst comma = i < params.length - 1 ? ',' : '';\n\t\t\t\tlines.push( `\\t${ p }${ comma }` );\n\n\t\t\t} );\n\t\t\tlines.push( `): ${ ret }` );\n\n\t\t} else {\n\n\t\t\tlines.push( singleLine );\n\n\t\t}\n\n\t}\n\n\tlines.push( '```' );\n\tlines.push( '' );\n\n\tif ( doc.description ) {\n\n\t\tlines.push( doc.description );\n\t\tlines.push( '' );\n\n\t}\n\n\tlines.push( renderAlertTags( doc ) );\n\n\treturn lines.join( '\\n' );\n\n}\n\nexport function renderMethod( doc, callbackMap = {} ) {\n\n\tconst isStatic = doc.scope === 'static';\n\tconst headingPrefix = isStatic ? 'static ' : '';\n\tconst sigPrefix = isStatic ? `static ${ doc.async ? 'async ' : '' }` : ( doc.async ? 'async ' : '' );\n\treturn renderCallable( doc, `### ${ headingPrefix }.${ doc.name }`, sigPrefix, callbackMap );\n\n}\n\nexport function renderFunction( doc, callbackMap = {} ) {\n\n\treturn renderCallable( doc, `### ${ doc.name }`, '', callbackMap );\n\n}\n\nexport function renderFunctions( funcs, title = 'Functions', callbackMap = {}, typedefs = [], typedefCallbackMap = {}, resolveLink = null ) {\n\n\tif ( funcs.length === 0 && typedefs.length === 0 ) return '';\n\n\tconst lines = [];\n\n\tlines.push( `## ${ title }` );\n\tlines.push( '' );\n\n\tfor ( const td of typedefs ) {\n\n\t\tlines.push( renderTypedef( td, typedefCallbackMap, resolveLink, 3 ) );\n\n\t}\n\n\tfor ( const fn of funcs ) {\n\n\t\tlines.push( renderFunction( fn, callbackMap ) );\n\n\t}\n\n\treturn lines.join( '\\n' );\n\n}\n\nexport function renderConstants( constants, title = 'Constants', callbackMap = {} ) {\n\n\tif ( constants.length === 0 ) return '';\n\n\tconst lines = [];\n\n\tlines.push( `## ${ title }` );\n\tlines.push( '' );\n\n\tfor ( const c of constants ) {\n\n\t\tconst type = formatType( c.type, callbackMap ) || 'number';\n\t\tlines.push( `### ${ c.name }` );\n\t\tlines.push( '' );\n\t\tlines.push( '```js' );\n\t\tlines.push( `${ c.name }: ${ type }` );\n\t\tlines.push( '```' );\n\t\tlines.push( '' );\n\n\t\tif ( c.description ) {\n\n\t\t\tlines.push( c.description );\n\t\t\tlines.push( '' );\n\n\t\t}\n\n\t}\n\n\treturn lines.join( '\\n' );\n\n}\n\nexport function renderTypedef( typeDoc, callbackMap = {}, resolveLink = null, headingLevel = 2 ) {\n\n\tconst h = '#'.repeat( headingLevel );\n\tconst hSub = '#'.repeat( headingLevel + 1 );\n\tconst lines = [];\n\n\tlines.push( `${ h } ${ typeDoc.name }` );\n\tlines.push( '' );\n\n\t// If the typedef's base type is not plain Object, treat it as an extension\n\tconst baseType = typeDoc.type.names[ 0 ];\n\tif ( baseType && baseType !== 'Object' ) {\n\n\t\tconst link = resolveLink && resolveLink( baseType );\n\t\tconst ref = link ? `[\\`${ baseType }\\`](${ link })` : `\\`${ baseType }\\``;\n\t\tlines.push( `_extends ${ ref }_` );\n\t\tlines.push( '' );\n\n\t}\n\n\tif ( typeDoc.description ) {\n\n\t\tlines.push( typeDoc.description );\n\t\tlines.push( '' );\n\n\t}\n\n\tlines.push( renderAlertTags( typeDoc ) );\n\n\tfor ( const prop of ( typeDoc.properties || [] ) ) {\n\n\t\tconst type = formatType( prop.type, callbackMap );\n\t\tconst optional = prop.optional ? '?' : '';\n\t\tlines.push( `${ hSub } .${ prop.name }` );\n\t\tlines.push( '' );\n\t\tlines.push( '```js' );\n\t\tlines.push( `${ prop.name }${ optional }: ${ type }` );\n\t\tlines.push( '```' );\n\t\tlines.push( '' );\n\n\t\tif ( prop.description ) {\n\n\t\t\tlines.push( prop.description );\n\t\t\tlines.push( '' );\n\n\t\t}\n\n\t}\n\n\treturn lines.join( '\\n' );\n\n}\n\nexport function renderEvents( events, callbackMap = {} ) {\n\n\tconst lines = [];\n\n\tlines.push( '### events' );\n\tlines.push( '' );\n\tlines.push( '```js' );\n\n\tfor ( let i = 0; i < events.length; i ++ ) {\n\n\t\tconst event = events[ i ];\n\n\t\tif ( event.description ) {\n\n\t\t\tfor ( const descLine of event.description.split( '\\n' ) ) {\n\n\t\t\t\tlines.push( `// ${ descLine }` );\n\n\t\t\t}\n\n\t\t}\n\n\t\tconst props = event.properties || [];\n\t\tconst propStr = props.map( p => {\n\n\t\t\tconst type = formatType( p.type, callbackMap );\n\t\t\tconst optional = p.optional ? '?' : '';\n\t\t\treturn `${ p.name }${ optional }: ${ type }`;\n\n\t\t} ).join( ', ' );\n\n\t\tif ( propStr ) {\n\n\t\t\tlines.push( `{ type: '${ event.name }', ${ propStr } }` );\n\n\t\t} else {\n\n\t\t\tlines.push( `{ type: '${ event.name }' }` );\n\n\t\t}\n\n\t\tif ( i < events.length - 1 ) lines.push( '' );\n\n\t}\n\n\tlines.push( '```' );\n\tlines.push( '' );\n\n\treturn lines.join( '\\n' );\n\n}\n\nexport function renderComponent( doc, callbackMap = {} ) {\n\n\tconst lines = [];\n\n\tlines.push( `## ${ doc.name }` );\n\tlines.push( '' );\n\n\tif ( doc.description ) {\n\n\t\tlines.push( doc.description );\n\t\tlines.push( '' );\n\n\t}\n\n\tconst props = ( doc.params || [] ).filter( p => p.name.includes( '.' ) );\n\n\tif ( props.length > 0 ) {\n\n\t\tlines.push( '### Props' );\n\t\tlines.push( '' );\n\t\tlines.push( '```jsx' );\n\t\tlines.push( `<${ doc.name }` );\n\n\t\tfor ( const prop of props ) {\n\n\t\t\tconst name = prop.name.split( '.' ).pop();\n\t\t\tconst type = formatType( prop.type, callbackMap );\n\t\t\tconst optional = prop.optional ? '?' : '';\n\t\t\tconst defStr = prop.defaultvalue !== undefined ? ` = ${ prop.defaultvalue }` : '';\n\t\t\tlines.push( `\\t${ name }${ optional }: ${ type }${ defStr }` );\n\n\t\t}\n\n\t\tlines.push( '/>' );\n\t\tlines.push( '```' );\n\t\tlines.push( '' );\n\n\t\tfor ( const prop of props ) {\n\n\t\t\tconst name = prop.name.split( '.' ).pop();\n\t\t\tconst type = formatType( prop.type, callbackMap );\n\t\t\tconst optional = prop.optional ? '?' : '';\n\t\t\tconst defStr = prop.defaultvalue !== undefined ? ` = ${ prop.defaultvalue }` : '';\n\t\t\tlines.push( `### .${ name }` );\n\t\t\tlines.push( '' );\n\t\t\tlines.push( '```jsx' );\n\t\t\tlines.push( `${ name }${ optional }: ${ type }${ defStr }` );\n\t\t\tlines.push( '```' );\n\t\t\tlines.push( '' );\n\n\t\t\tif ( prop.description ) {\n\n\t\t\t\tlines.push( prop.description );\n\t\t\t\tlines.push( '' );\n\n\t\t\t}\n\n\t\t}\n\n\t}\n\n\treturn lines.join( '\\n' );\n\n}\n\nexport function renderClass( classDoc, members, callbackMap = {}, resolveLink = null ) {\n\n\tconst lines = [];\n\n\tlines.push( `## ${ classDoc.name }` );\n\tlines.push( '' );\n\n\tif ( classDoc.augments && classDoc.augments.length > 0 ) {\n\n\t\tconst base = classDoc.augments[ 0 ];\n\t\tconst link = resolveLink && resolveLink( base );\n\t\tconst ref = link ? `[\\`${ base }\\`](${ link })` : `\\`${ base }\\``;\n\t\tlines.push( `_extends ${ ref }_` );\n\t\tlines.push( '' );\n\n\t}\n\n\tconst classDesc = classDoc.classdesc || classDoc.description;\n\tif ( classDesc ) {\n\n\t\tlines.push( classDesc );\n\t\tlines.push( '' );\n\n\t}\n\n\tlines.push( renderAlertTags( classDoc ) );\n\n\tconst visible = members.filter( m => m.access !== 'private' );\n\t// Treat function doclets that carry an explicit @type tag as properties\n\t// (e.g. arrow-function assignments like `this.schedulingCallback = func => ...`)\n\tconst isProperty = m => m.kind === 'member' || ( m.kind === 'function' && m.type );\n\tconst properties = visible\n\t\t.filter( isProperty )\n\t\t.sort( ( a, b ) => a.meta.lineno - b.meta.lineno );\n\tconst allMethods = visible\n\t\t.filter( m => m.kind === 'function' && ! m.type )\n\t\t.sort( ( a, b ) => a.meta.lineno - b.meta.lineno );\n\tconst staticMethods = allMethods.filter( m => m.scope === 'static' );\n\tconst instanceMethods = allMethods.filter( m => m.scope !== 'static' );\n\tconst events = visible\n\t\t.filter( m => m.kind === 'event' )\n\t\t.sort( ( a, b ) => a.meta.lineno - b.meta.lineno );\n\n\tif ( events.length > 0 ) {\n\n\t\tlines.push( renderEvents( events, callbackMap ) );\n\n\t}\n\n\t// Static methods appear first\n\tfor ( const method of staticMethods ) {\n\n\t\tlines.push( renderMethod( method, callbackMap ) );\n\n\t}\n\n\tfor ( const member of properties ) {\n\n\t\tlines.push( renderMember( member, callbackMap ) );\n\n\t}\n\n\t// Constructor before instance methods\n\tif ( classDoc.params && classDoc.params.length > 0 ) {\n\n\t\tlines.push( renderConstructor( classDoc, callbackMap ) );\n\n\t}\n\n\tfor ( const method of instanceMethods ) {\n\n\t\tlines.push( renderMethod( method, callbackMap ) );\n\n\t}\n\n\treturn lines.join( '\\n' );\n\n}\n"
  },
  {
    "path": "utils/docs/build.js",
    "content": "import { execSync } from 'child_process';\nimport fs from 'fs';\nimport path from 'path';\nimport { renderClass, renderComponent, renderTypedef, renderConstants, renderFunctions, toAnchor, resolveLinks } from './RenderDocsUtils.js';\nimport { findRootDir } from '../CommandUtils.js';\n\nconst ROOT_DIR = findRootDir();\n\nconst ENTRY_POINTS = [\n\t{\n\t\toutput: 'API.md',\n\t\ttitle: 'three-edge-projection',\n\t\tsource: 'src',\n\t\texclude: 'src/webgpu',\n\t},\n\t{\n\t\toutput: 'API.md',\n\t\ttitle: 'WebGPU API',\n\t\tsource: 'src/webgpu',\n\t},\n];\n\n// Run JSDoc for all entry points and build a global type registry for cross-file links\nconst results = ENTRY_POINTS.map( entry => {\n\n\tlet jsdoc = filterDocumented( runJsDoc( path.resolve( ROOT_DIR, entry.source ) ) );\n\tif ( entry.exclude ) {\n\n\t\tconst excludePath = path.resolve( ROOT_DIR, entry.exclude );\n\t\tjsdoc = jsdoc.filter( d => ! d.meta || ! d.meta.path || ! d.meta.path.startsWith( excludePath ) );\n\n\t}\n\n\treturn { entry, jsdoc };\n\n} );\n\n// Doclet type predicates\nconst isClass = d => d.kind === 'class';\nconst isObjectTypedef = d => d.kind === 'typedef' && d.type.names[ 0 ] !== 'function';\nconst isCallbackTypedef = d => d.kind === 'typedef' && d.type.names[ 0 ] === 'function';\nconst isReactComponent = d => ( d.kind === 'function' || d.kind === 'constant' ) && d.tags && d.tags.some( t => t.title === 'component' );\nconst isConstant = d => d.kind === 'constant' && ! d.memberof && ! isReactComponent( d );\nconst isFunction = d => d.kind === 'function' && ! d.memberof && ! isReactComponent( d );\n\n// Only classes, non-callback typedefs, and React components get sections (and therefore anchors) in the output.\nconst typeRegistry = {}; // name -> output path\nfor ( const { entry, jsdoc } of results ) {\n\n\tfor ( const d of jsdoc ) {\n\n\t\tif ( isClass( d ) || isObjectTypedef( d ) || isReactComponent( d ) ) {\n\n\t\t\ttypeRegistry[ d.name ] = entry.output;\n\n\t\t}\n\n\t}\n\n}\n\n// Pass 2: render each entry point and accumulate sections per output file.\nconst outputSections = {}; // output file -> accumulated sections array\nfor ( const { entry, jsdoc } of results ) {\n\n\tconst resolveLink = name => {\n\n\t\t// no link\n\t\tconst targetFile = typeRegistry[ name ];\n\t\tif ( ! targetFile ) {\n\n\t\t\treturn null;\n\n\t\t}\n\n\t\tconst anchor = `#${ toAnchor( name ) }`;\n\t\tif ( targetFile === entry.output ) {\n\n\t\t\t// anchor is in the same file\n\t\t\treturn anchor;\n\n\t\t}\n\n\t\t// relative path + anchor for a different file\n\t\tconst fromDir = path.dirname( path.join( ROOT_DIR, entry.output ) );\n\t\tconst toFile = path.join( ROOT_DIR, targetFile );\n\t\tconst relativePath = path.relative( fromDir, toFile ).replace( /\\\\/g, '/' );\n\t\treturn relativePath + anchor;\n\n\t};\n\n\t// Sort classes topologically so every parent appears before its subclasses.\n\t// Within the same \"depth level\" classes are sorted alphabetically.\n\tconst classes = topologicalSortClasses( jsdoc.filter( d => isClass( d ) ) );\n\n\t// collect @callback typedefs into a map for inline substitution\n\tconst callbackMap = {};\n\tfor ( const d of jsdoc ) {\n\n\t\tif ( isCallbackTypedef( d ) ) {\n\n\t\t\tcallbackMap[ d.name ] = d;\n\n\t\t}\n\n\t}\n\n\t// Sort typedefs so plain-object bases appear before derived types; exclude @callback entries\n\tconst allTypedefs = jsdoc\n\t\t.filter( d => isObjectTypedef( d ) )\n\t\t.sort( ( a, b ) => {\n\n\t\t\tconst aIsBase = a.type.names[ 0 ] === 'Object';\n\t\t\tconst bIsBase = b.type.names[ 0 ] === 'Object';\n\t\t\tif ( aIsBase && ! bIsBase ) return - 1;\n\t\t\tif ( ! aIsBase && bIsBase ) return 1;\n\t\t\treturn a.name.localeCompare( b.name );\n\n\t\t} );\n\n\t// Typedefs tagged with @section are injected before their matching function group\n\tconst typedefsBySection = {};\n\tconst typedefs = [];\n\tfor ( const d of allTypedefs ) {\n\n\t\tconst sectionTag = d.tags && d.tags.find( t => t.title === 'section' );\n\t\tif ( sectionTag ) {\n\n\t\t\tconst key = sectionTag.value;\n\t\t\tif ( ! typedefsBySection[ key ] ) typedefsBySection[ key ] = [];\n\t\t\ttypedefsBySection[ key ].push( d );\n\n\t\t} else {\n\n\t\t\ttypedefs.push( d );\n\n\t\t}\n\n\t}\n\n\t// sort components by source line order\n\tconst components = jsdoc\n\t\t.filter( d => isReactComponent( d ) )\n\t\t.sort( ( a, b ) => a.meta.lineno - b.meta.lineno );\n\n\tconst constsByGroup = groupByTag( jsdoc, isConstant, 'Constants' );\n\tconst funcsByGroup = groupByTag( jsdoc, isFunction, 'Functions' );\n\n\t// cache all fields by associated class name\n\tconst classMembers = {};\n\tfor ( const doc of jsdoc ) {\n\n\t\tif ( doc.memberof && doc.kind !== 'class' ) {\n\n\t\t\tif ( ! classMembers[ doc.memberof ] ) {\n\n\t\t\t\tclassMembers[ doc.memberof ] = [];\n\n\t\t\t}\n\n\t\t\tclassMembers[ doc.memberof ].push( doc );\n\n\t\t}\n\n\t}\n\n\t// construct sections for this entry point\n\tconst sections = [ `# ${ entry.title }`, '' ];\n\n\tfor ( const [ groupName, consts ] of Object.entries( constsByGroup ) ) {\n\n\t\tsections.push( renderConstants( consts, groupName, callbackMap ) );\n\n\t}\n\n\tfor ( const component of components ) {\n\n\t\tsections.push( renderComponent( component, callbackMap ) );\n\n\t}\n\n\tfor ( const cls of classes ) {\n\n\t\tsections.push( renderClass( cls, classMembers[ cls.name ] || [], callbackMap, resolveLink ) );\n\n\t}\n\n\tfor ( const typedef of typedefs ) {\n\n\t\tsections.push( renderTypedef( typedef, callbackMap, resolveLink ) );\n\n\t}\n\n\tfor ( const [ groupName, funcs ] of Object.entries( funcsByGroup ) ) {\n\n\t\tconst sectionTypedefs = typedefsBySection[ groupName ] || [];\n\t\tsections.push( renderFunctions( funcs, groupName, callbackMap, sectionTypedefs, callbackMap, resolveLink ) );\n\n\t}\n\n\tif ( ! outputSections[ entry.output ] ) outputSections[ entry.output ] = [];\n\toutputSections[ entry.output ].push( ...sections );\n\n}\n\n// Write each output file once, after all entry points have been processed.\nconst header = '<!-- This file is generated automatically. Do not edit it directly. -->\\n';\nfor ( const [ outputFile, sections ] of Object.entries( outputSections ) ) {\n\n\tconst output = header + resolveLinks( sections.join( '\\n' ) );\n\tfs.writeFileSync( path.join( ROOT_DIR, outputFile ), output );\n\tconsole.log( `Written: ${ outputFile }` );\n\n}\n\n//\n\nfunction groupByTag( docs, predicate, defaultGroup ) {\n\n\tconst groups = {};\n\tfor ( const d of docs.filter( predicate ).sort( ( a, b ) => a.meta.lineno - b.meta.lineno ) ) {\n\n\t\tconst groupTag = d.tags && d.tags.find( t => t.title === 'section' );\n\t\tconst groupName = groupTag ? groupTag.value : defaultGroup;\n\t\tif ( ! groups[ groupName ] ) groups[ groupName ] = [];\n\t\tgroups[ groupName ].push( d );\n\n\t}\n\n\treturn groups;\n\n}\n\nfunction runJsDoc( source ) {\n\n\t// Default maxBuffer is 1 MB; large source directories can exceed that, so raise it to 32 MB.\n\tconst result = execSync( `npx jsdoc -X -r \"${ source }\"`, { maxBuffer: 32 * 1024 * 1024 } ).toString();\n\treturn JSON.parse( result );\n\n}\n\n// Topological sort: every parent class appears before its subclasses.\n// Siblings (subclasses sharing the same parent) are kept together and ordered alphabetically.\nfunction topologicalSortClasses( classes ) {\n\n\tconst byName = Object.fromEntries( classes.map( c => [ c.name, c ] ) );\n\tconst result = [];\n\tconst visited = new Set();\n\n\t// Build parent -> children map so siblings can be visited eagerly\n\tconst childrenMap = {};\n\tfor ( const cls of classes ) {\n\n\t\tfor ( const parent of ( cls.augments || [] ) ) {\n\n\t\t\tif ( ! childrenMap[ parent ] ) childrenMap[ parent ] = [];\n\t\t\tchildrenMap[ parent ].push( cls );\n\n\t\t}\n\n\t}\n\n\tfunction visit( cls ) {\n\n\t\tif ( visited.has( cls.name ) ) return;\n\t\tvisited.add( cls.name );\n\n\t\t// Visit parent(s) first\n\t\tfor ( const parent of ( cls.augments || [] ) ) {\n\n\t\t\tif ( byName[ parent ] ) visit( byName[ parent ] );\n\n\t\t}\n\n\t\tresult.push( cls );\n\n\t\t// Eagerly visit children alphabetically so all siblings stay grouped together\n\t\tconst children = ( childrenMap[ cls.name ] || [] )\n\t\t\t.slice()\n\t\t\t.sort( ( a, b ) => a.name.localeCompare( b.name ) );\n\t\tfor ( const child of children ) {\n\n\t\t\tvisit( child );\n\n\t\t}\n\n\t}\n\n\t// Alphabetical pre-sort for deterministic output within the same generation\n\t[ ...classes ]\n\t\t.sort( ( a, b ) => a.name.localeCompare( b.name ) )\n\t\t.forEach( visit );\n\n\treturn result;\n\n}\n\nfunction filterDocumented( json ) {\n\n\treturn json.filter( d =>\n\t\td.undocumented !== true &&\n\t\td.ignore !== true &&\n\t\td.kind !== 'package' &&\n\t\td.access !== 'private' &&\n\t\td.inherited !== true &&\n\t\t! d.deprecated\n\t);\n\n}\n"
  },
  {
    "path": "vite.config.js",
    "content": "import { searchForWorkspaceRoot } from 'vite';\nimport fs from 'fs';\n\nexport default {\n\n\troot: './example/',\n\tbase: '',\n\tbuild: {\n\t\ttarget: 'es2022',\n\t\tsourcemap: true,\n\t\toutDir: './dist/',\n\t\tminify: false,\n\t\tterserOptions: {\n\t\t\tcompress: false,\n\t\t\tmangle: false,\n\t\t},\n\t\trollupOptions: {\n\t\t\tinput: fs\n\t\t\t\t.readdirSync( './example/' )\n\t\t\t\t.filter( p => /\\.html$/.test( p ) )\n\t\t\t\t.map( p => `./example/${ p }` ),\n\t\t},\n\t},\n\tserver: {\n\t\tfs: {\n\t\t\tallow: [\n\t\t\t\t// search up for workspace root\n\t\t\t\tsearchForWorkspaceRoot( process.cwd() ),\n\t\t\t],\n\t\t},\n\t}\n\n};\n"
  },
  {
    "path": "vitest.config.js",
    "content": "import { defineConfig } from 'vitest/config';\n\nexport default defineConfig( {\n\ttest: {\n\t\tglobals: true,\n\t\tenvironment: 'node',\n\t\tinclude: [ 'test/**/*.test.js', 'test/**/*.spec.js' ],\n\t},\n} );\n"
  }
]