├── .changeset ├── README.md └── config.json ├── .editorconfig ├── .github ├── dependabot.yml └── workflows │ ├── preview.yml │ └── release.yml ├── .gitignore ├── .nvmrc ├── .prettierrc.json ├── .yarn └── releases │ └── yarn-4.1.1.cjs ├── .yarnrc.yml ├── DEVELOPMENT.md ├── LICENSE ├── README.md ├── apps ├── docs │ ├── .gitignore │ ├── .prettierrc │ ├── README.md │ ├── build.sh │ ├── docs-style.css │ ├── package.json │ └── typedoc.json └── navmesh-website │ ├── .gitignore │ ├── .prettierrc.json │ ├── index.html │ ├── package.json │ ├── src │ ├── app.tsx │ ├── assets │ │ └── dungeon.gltf │ ├── features │ │ ├── controls │ │ │ ├── controls.ts │ │ │ ├── index.ts │ │ │ └── leva-text.tsx │ │ ├── download │ │ │ ├── download.ts │ │ │ └── index.ts │ │ ├── error-handling │ │ │ ├── error-boundary.tsx │ │ │ ├── error-message.ts │ │ │ └── index.ts │ │ ├── recast │ │ │ ├── crowd │ │ │ │ └── recast-agent.tsx │ │ │ ├── export │ │ │ │ └── nav-mesh-to-gltf.ts │ │ │ ├── helpers │ │ │ │ ├── nav-mesh-debug-drawer.tsx │ │ │ │ └── nav-mesh-generator-input-helper.tsx │ │ │ └── index.ts │ │ ├── ui │ │ │ ├── index.ts │ │ │ └── loading-spinner.tsx │ │ └── upload │ │ │ ├── index.ts │ │ │ ├── loaders.ts │ │ │ ├── model-drop-zone.tsx │ │ │ └── read-file.ts │ ├── google-analytics.d.ts │ ├── index.css │ ├── main.tsx │ ├── pages │ │ ├── editor-page.tsx │ │ ├── index.ts │ │ └── upload-page.tsx │ ├── state │ │ └── editor-state.ts │ ├── tunnels.tsx │ ├── types.d.ts │ └── vite-env.d.ts │ ├── tsconfig.json │ ├── tsconfig.node.json │ ├── vercel.json │ └── vite.config.ts ├── ci ├── build-all.sh ├── build-packages.sh └── install-emsdk.sh ├── examples ├── playcanvas-vite-example │ ├── .gitignore │ ├── .prettierrc.json │ ├── index.html │ ├── package.json │ ├── src │ │ ├── index.css │ │ └── index.ts │ ├── tsconfig.json │ ├── tsconfig.node.json │ └── vite.config.ts ├── three-node-cjs-example │ ├── index.cjs │ └── package.json ├── three-node-esm-example │ ├── index.mjs │ └── package.json ├── three-parcel-example │ ├── index.html │ ├── index.js │ ├── package.json │ └── styles.css ├── three-vanilla-example │ └── index.html ├── three-vite-example │ ├── .gitignore │ ├── .prettierrc.json │ ├── index.html │ ├── package.json │ ├── src │ │ ├── index.css │ │ ├── index.ts │ │ └── vite-env.d.ts │ ├── tsconfig.json │ ├── tsconfig.node.json │ └── vite.config.ts └── three-vite-worker-example │ ├── .gitignore │ ├── .prettierrc.json │ ├── index.html │ ├── package.json │ ├── src │ ├── index.css │ ├── index.ts │ ├── navmesh-worker.ts │ └── vite-env.d.ts │ ├── tsconfig.json │ ├── tsconfig.node.json │ └── vite.config.ts ├── package.json ├── packages ├── recast-navigation-core │ ├── .gitignore │ ├── .prettierrc.json │ ├── CHANGELOG.md │ ├── LICENSE │ ├── README.md │ ├── eslint.config.mjs │ ├── package.json │ ├── rollup.config.js │ ├── src │ │ ├── arrays.ts │ │ ├── crowd.ts │ │ ├── debug-drawer-utils.ts │ │ ├── detour.ts │ │ ├── index.ts │ │ ├── nav-mesh-query.ts │ │ ├── nav-mesh.ts │ │ ├── random.ts │ │ ├── raw-module.ts │ │ ├── raw.ts │ │ ├── recast.ts │ │ ├── serdes │ │ │ ├── export.ts │ │ │ ├── import.ts │ │ │ └── index.ts │ │ ├── tile-cache.ts │ │ ├── types.ts │ │ └── utils.ts │ └── tsconfig.json ├── recast-navigation-generators │ ├── .gitignore │ ├── .prettierrc.json │ ├── CHANGELOG.md │ ├── LICENSE │ ├── README.md │ ├── eslint.config.mjs │ ├── package.json │ ├── rollup.config.js │ ├── src │ │ ├── generators │ │ │ ├── common.ts │ │ │ ├── generate-solo-nav-mesh.ts │ │ │ ├── generate-tile-cache.ts │ │ │ ├── generate-tiled-nav-mesh.ts │ │ │ ├── index.ts │ │ │ └── merge-positions-and-indices.ts │ │ ├── index.ts │ │ └── types.ts │ └── tsconfig.json ├── recast-navigation-playcanvas │ ├── .gitignore │ ├── .prettierrc.json │ ├── CHANGELOG.md │ ├── LICENSE │ ├── README.md │ ├── eslint.config.mjs │ ├── package.json │ ├── rollup.config.js │ ├── src │ │ ├── debug │ │ │ ├── debug-drawer.ts │ │ │ └── index.ts │ │ ├── helpers │ │ │ ├── crowd-helper.ts │ │ │ ├── index.ts │ │ │ ├── nav-mesh-helper.ts │ │ │ └── tile-cache-helper.ts │ │ ├── index.ts │ │ └── utils │ │ │ ├── generators.ts │ │ │ ├── get-positions-and-indices.ts │ │ │ └── index.ts │ └── tsconfig.json ├── recast-navigation-three │ ├── .gitignore │ ├── .prettierrc.json │ ├── CHANGELOG.md │ ├── LICENSE │ ├── README.md │ ├── eslint.config.mjs │ ├── package.json │ ├── rollup.config.js │ ├── src │ │ ├── debug │ │ │ ├── debug-drawer.ts │ │ │ └── index.ts │ │ ├── helpers │ │ │ ├── crowd-helper.ts │ │ │ ├── index.ts │ │ │ ├── nav-mesh-helper.ts │ │ │ └── tile-cache-helper.ts │ │ ├── index.ts │ │ └── utils │ │ │ ├── generators.ts │ │ │ ├── get-positions-and-indices.ts │ │ │ └── index.ts │ └── tsconfig.json ├── recast-navigation-wasm │ ├── .gitignore │ ├── CHANGELOG.md │ ├── CMakeLists.txt │ ├── LICENSE │ ├── README.md │ ├── build.sh │ ├── example.html │ ├── front-matter.js │ ├── package.json │ ├── recast-navigation.idl │ └── src │ │ ├── Arrays.h │ │ ├── ChunkyTriMesh.cpp │ │ ├── ChunkyTriMesh.h │ │ ├── Crowd.cpp │ │ ├── Crowd.h │ │ ├── DebugDraw │ │ ├── DebugDraw.cpp │ │ ├── DebugDraw.h │ │ ├── DetourDebugDraw.cpp │ │ ├── DetourDebugDraw.h │ │ ├── RecastDebugDraw.cpp │ │ └── RecastDebugDraw.h │ │ ├── Detour.cpp │ │ ├── Detour.h │ │ ├── NavMesh.cpp │ │ ├── NavMesh.h │ │ ├── NavMeshQuery.cpp │ │ ├── NavMeshQuery.h │ │ ├── NavMeshSerdes.cpp │ │ ├── NavMeshSerdes.h │ │ ├── Recast.h │ │ ├── Refs.h │ │ ├── TileCache.cpp │ │ ├── TileCache.h │ │ ├── Vec.h │ │ └── recast-navigation.h └── recast-navigation │ ├── .gitignore │ ├── .prettierrc.json │ ├── .storybook │ ├── common │ │ ├── agent-path.tsx │ │ ├── debug.tsx │ │ ├── dungeon-environment.tsx │ │ ├── nav-test-environment.tsx │ │ └── use-nav-mesh-config.ts │ ├── decorators.tsx │ ├── main.ts │ ├── manager.ts │ ├── parameters.ts │ ├── preview.ts │ ├── public │ │ ├── dungeon.gltf │ │ └── nav_test.glb │ ├── stories │ │ ├── advanced │ │ │ ├── custom-areas-generator.ts │ │ │ ├── custom-areas.stories.tsx │ │ │ └── flood-fill-pruning.stories.tsx │ │ ├── crowd │ │ │ ├── fixed-stepping-with-interpolation.stories.tsx │ │ │ ├── multiple-agents.stories.tsx │ │ │ └── single-agent.stories.tsx │ │ ├── debug │ │ │ └── debug-drawer.stories.tsx │ │ ├── external-use │ │ │ └── three-pathfinding.stories.tsx │ │ ├── importing-and-exporting.stories.tsx │ │ ├── nav-mesh-query │ │ │ ├── compute-path.stories.tsx │ │ │ ├── compute-smooth-path.stories.tsx │ │ │ ├── find-random-point.stories.tsx │ │ │ ├── move-along-surface.stories.tsx │ │ │ ├── nearby-polygons.stories.tsx │ │ │ └── raycast.stories.tsx │ │ ├── nav-mesh │ │ │ ├── custom-bounds.stories.tsx │ │ │ ├── models.stories.tsx │ │ │ ├── non-indexed-geometry.stories.tsx │ │ │ └── walkable-slope-angle.stories.tsx │ │ ├── obstacles │ │ │ ├── many-obstacles.stories.tsx │ │ │ ├── obstacles-crowd.stories.tsx │ │ │ └── obstacles-path.stories.tsx │ │ ├── off-mesh-connections │ │ │ └── off-mesh-connections.stories.tsx │ │ └── vite-env.d.ts │ └── styles.css │ ├── CHANGELOG.md │ ├── LICENSE │ ├── README.md │ ├── cover.png │ ├── eslint.config.mjs │ ├── package.json │ ├── rollup.config.js │ ├── src │ ├── generators.ts │ ├── index.ts │ └── types.d.ts │ ├── tsconfig.json │ ├── tst │ ├── crowd.spec.ts │ ├── nav-mesh-query.spec.ts │ └── utils.ts │ ├── vite.config.mts │ └── vitest.config.mts └── yarn.lock /.changeset/README.md: -------------------------------------------------------------------------------- 1 | # Changesets 2 | 3 | Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works 4 | with multi-package repos, or single-package repos to help you version and publish your code. You can 5 | find the full documentation for it [in our repository](https://github.com/changesets/changesets) 6 | 7 | We have a quick list of common questions to get you started engaging with this project in 8 | [our documentation](https://github.com/changesets/changesets/blob/main/docs/common-questions.md) 9 | -------------------------------------------------------------------------------- /.changeset/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://unpkg.com/@changesets/config@2.0.0/schema.json", 3 | "changelog": "@changesets/cli/changelog", 4 | "commit": false, 5 | "fixed": [["recast-navigation", "@recast-navigation/*"]], 6 | "linked": [], 7 | "access": "public", 8 | "baseBranch": "main", 9 | "updateInternalDependencies": "patch", 10 | "ignore": [ 11 | "navmesh-website", 12 | "three-node-cjs-example", 13 | "three-node-esm-example", 14 | "three-vite-example", 15 | "three-vite-worker-example", 16 | "playcanvas-vite-example" 17 | ], 18 | "___experimentalUnsafeOptions_WILL_CHANGE_IN_PATCH": { 19 | "onlyUpdatePeerDependentsWhenOutOfRange": true 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | end_of_line = lf 5 | insert_final_newline = true 6 | 7 | [*.{js,json,yml}] 8 | charset = utf-8 9 | indent_style = space 10 | indent_size = 2 11 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: 'npm' # See documentation for possible values 9 | directory: '/' # Location of package manifests 10 | schedule: 11 | interval: 'weekly' 12 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .yarn/* 2 | !.yarn/patches 3 | !.yarn/plugins 4 | !.yarn/releases 5 | !.yarn/sdks 6 | !.yarn/versions 7 | .pnp.* 8 | node_modules 9 | .emsdk-cache 10 | build 11 | dist 12 | .vscode 13 | .vercel 14 | emsdk 15 | .parcel-cache 16 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | v22.0.0 2 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "es5", 3 | "tabWidth": 2, 4 | "semi": true, 5 | "singleQuote": true 6 | } 7 | -------------------------------------------------------------------------------- /.yarnrc.yml: -------------------------------------------------------------------------------- 1 | compressionLevel: mixed 2 | 3 | enableGlobalCache: false 4 | 5 | nodeLinker: node-modules 6 | 7 | yarnPath: .yarn/releases/yarn-4.1.1.cjs 8 | 9 | npmRegistryServer: "https://registry.npmjs.org" 10 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | Copyright © 2025 Isaac Mason 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | packages/recast-navigation/README.md -------------------------------------------------------------------------------- /apps/docs/.gitignore: -------------------------------------------------------------------------------- 1 | tmp 2 | -------------------------------------------------------------------------------- /apps/docs/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "es5", 3 | "tabWidth": 2, 4 | "semi": true, 5 | "singleQuote": true 6 | } 7 | -------------------------------------------------------------------------------- /apps/docs/README.md: -------------------------------------------------------------------------------- 1 | # recast-navigation-docs 2 | 3 | Documentation for recast-navigation-js. 4 | 5 | ## Building 6 | 7 | As a prerequisite, you will need to have `node` and `yarn` installed. 8 | 9 | ```bash 10 | # install dependencies 11 | yarn 12 | 13 | # build the docs 14 | yarn build 15 | 16 | # (optional) serve the docs locally 17 | npx http-server ./dist 18 | ``` 19 | -------------------------------------------------------------------------------- /apps/docs/build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | mkdir -p ./tmp 4 | 5 | remove_start_marker="" 6 | remove_end_marker="" 7 | sed "/$remove_start_marker/,/$remove_end_marker/d" "./../../packages/recast-navigation/README.md" > "./tmp/README.md" 8 | 9 | typedoc --tsconfig ../../packages/recast-navigation/tsconfig.json 10 | 11 | cp ../../packages/recast-navigation/cover.png ./dist/cover.png -------------------------------------------------------------------------------- /apps/docs/docs-style.css: -------------------------------------------------------------------------------- 1 | @import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;600;800&display=swap'); 2 | 3 | body { 4 | font-family: 'Inter', sans-serif; 5 | } 6 | 7 | /* Hide unnecessary member visibility filter */ 8 | #tsd-filter-visibility { 9 | display: none !important; 10 | } 11 | 12 | /* Easier to read line-height */ 13 | .tsd-typography p { 14 | line-height: 1.7; 15 | } 16 | 17 | /* GitHub styles for
 blocks */
18 | :root {
19 |   --light-hl-0: #d73a49; /* declarations */
20 |   --light-hl-1: #000000;
21 |   --light-hl-2: #0070c1;
22 |   --light-hl-3: #6442c1; /* names */
23 |   --light-hl-4: #24292e; /* variable names */
24 |   --light-hl-5: #098658;
25 |   --light-hl-6: #6a737d; /* comments */
26 |   --light-hl-7: #af00db;
27 |   --light-hl-8: #267f99;
28 |   --light-hl-9: #a31515;
29 |   --light-hl-10: #000000ff;
30 | 
31 |   --light-code-background: #f6f8fa;
32 | 
33 |   --light-color-link: #0366d6;
34 | }
35 | 


--------------------------------------------------------------------------------
/apps/docs/package.json:
--------------------------------------------------------------------------------
 1 | {
 2 |     "name": "docs",
 3 |     "packageManager": "yarn@3.5.0",
 4 |     "version": "0.0.0",
 5 |     "private": true,
 6 |     "scripts": {
 7 |         "build": "sh build.sh"
 8 |     },
 9 |     "devDependencies": {
10 |         "typedoc": "^0.25.7",
11 |         "typescript": "^5.4.3"
12 |     }
13 | }
14 | 


--------------------------------------------------------------------------------
/apps/docs/typedoc.json:
--------------------------------------------------------------------------------
 1 | {
 2 |   "name": "recast-navigation-js",
 3 |   "entryPoints": [
 4 |     "../../packages/recast-navigation/src/index.ts",
 5 |     "../../packages/recast-navigation/src/generators.ts"
 6 |   ],
 7 |   "entryPointStrategy": "expand",
 8 |   "out": "dist/",
 9 |   "readme": "./tmp/README.md",
10 |   "customCss": "./docs-style.css",
11 |   "cleanOutputDir": true,
12 |   "excludePrivate": true,
13 |   "excludeProtected": true,
14 |   "excludeExternals": false,
15 |   "excludeInternal": true,
16 |   "hideGenerator": true
17 | }
18 | 


--------------------------------------------------------------------------------
/apps/navmesh-website/.gitignore:
--------------------------------------------------------------------------------
 1 | # Logs
 2 | logs
 3 | *.log
 4 | npm-debug.log*
 5 | yarn-debug.log*
 6 | yarn-error.log*
 7 | pnpm-debug.log*
 8 | lerna-debug.log*
 9 | 
10 | node_modules
11 | dist
12 | dist-ssr
13 | *.local
14 | 
15 | # Editor directories and files
16 | .vscode/*
17 | !.vscode/extensions.json
18 | .idea
19 | .DS_Store
20 | *.suo
21 | *.ntvs*
22 | *.njsproj
23 | *.sln
24 | *.sw?
25 | 


--------------------------------------------------------------------------------
/apps/navmesh-website/.prettierrc.json:
--------------------------------------------------------------------------------
1 | {
2 |   "trailingComma": "es5",
3 |   "tabWidth": 2,
4 |   "semi": true,
5 |   "singleQuote": true
6 | }
7 | 


--------------------------------------------------------------------------------
/apps/navmesh-website/index.html:
--------------------------------------------------------------------------------
 1 | 
 2 | 
 3 |   
 4 |     
 5 |     
 6 |     
 7 |     NavMesh Generator - recast-navigation
 8 |     
12 |     <%- analyticsScript %>
13 |   
14 |   
15 |     
16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /apps/navmesh-website/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "navmesh-website", 3 | "private": true, 4 | "type": "module", 5 | "scripts": { 6 | "dev": "vite", 7 | "build": "tsc && vite build", 8 | "preview": "vite preview" 9 | }, 10 | "dependencies": { 11 | "@pmndrs/assets": "^1.7.0", 12 | "@react-three/drei": "^9.114.3", 13 | "@react-three/fiber": "^8.17.10", 14 | "leva": "^0.9.35", 15 | "react": "^18.3.1", 16 | "react-dom": "^18.2.0", 17 | "react-dropzone": "^14.2.9", 18 | "react-router-dom": "^6.27.0", 19 | "recast-navigation": "workspace:*", 20 | "styled-components": "^6.1.13", 21 | "suspend-react": "^0.1.3", 22 | "three": "^0.172.0", 23 | "tunnel-rat": "^0.1.2", 24 | "zustand": "^5.0.0" 25 | }, 26 | "devDependencies": { 27 | "@types/react": "^18.3.11", 28 | "@types/react-dom": "^18.3.1", 29 | "@types/three": "^0.172.0", 30 | "@vitejs/plugin-react": "^4.3.2", 31 | "typescript": "^5.6.3", 32 | "vite": "^5.4.9", 33 | "vite-plugin-html": "^3.2.2" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /apps/navmesh-website/src/app.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | RouterProvider, 3 | createBrowserRouter, 4 | redirect, 5 | } from 'react-router-dom'; 6 | import { EditorPage, UploadPage } from './pages'; 7 | 8 | export const RouterPaths = { 9 | upload: '/', 10 | editor: '/editor', 11 | }; 12 | 13 | const router = createBrowserRouter([ 14 | { 15 | path: RouterPaths.upload, 16 | Component: UploadPage, 17 | }, 18 | { 19 | path: RouterPaths.editor, 20 | Component: EditorPage, 21 | }, 22 | { 23 | path: '*', 24 | loader: () => { 25 | return redirect(RouterPaths.upload); 26 | }, 27 | }, 28 | ]); 29 | 30 | export default () => ; 31 | -------------------------------------------------------------------------------- /apps/navmesh-website/src/features/controls/index.ts: -------------------------------------------------------------------------------- 1 | export * from './controls'; 2 | export * from './leva-text'; 3 | -------------------------------------------------------------------------------- /apps/navmesh-website/src/features/controls/leva-text.tsx: -------------------------------------------------------------------------------- 1 | import { createPlugin, useInputContext, styled } from 'leva/plugin'; 2 | 3 | const Text = styled('div', { 4 | color: '#ccc', 5 | lineHeight: '1.5em', 6 | paddingTop: '5px', 7 | whiteSpace: 'pre-line', 8 | }); 9 | 10 | export const textPlugin = createPlugin({ 11 | component: () => { 12 | const { label } = useInputContext(); 13 | 14 | return {label}; 15 | }, 16 | }); 17 | 18 | export const levaText = (text: string) => textPlugin({ label: text }); 19 | -------------------------------------------------------------------------------- /apps/navmesh-website/src/features/download/download.ts: -------------------------------------------------------------------------------- 1 | export const download = (data: BlobPart, type: string, filename: string) => { 2 | const blob = new Blob([data], { type }); 3 | 4 | const url = URL.createObjectURL(blob); 5 | 6 | const link = document.createElement('a'); 7 | link.href = url; 8 | link.download = filename; 9 | 10 | link.click(); 11 | }; 12 | -------------------------------------------------------------------------------- /apps/navmesh-website/src/features/download/index.ts: -------------------------------------------------------------------------------- 1 | export * from './download'; 2 | -------------------------------------------------------------------------------- /apps/navmesh-website/src/features/error-handling/error-boundary.tsx: -------------------------------------------------------------------------------- 1 | import { Component } from 'react'; 2 | import { ErrorMessage } from './error-message'; 3 | 4 | export type ErrorBoundaryState = { 5 | hasError: boolean; 6 | errorMessage?: string; 7 | }; 8 | 9 | export type ErrorBoundaryProps = { 10 | children: React.ReactNode; 11 | }; 12 | 13 | export class ErrorBoundary extends Component< 14 | ErrorBoundaryProps, 15 | ErrorBoundaryState 16 | > { 17 | constructor(props: ErrorBoundaryProps) { 18 | super(props); 19 | this.state = { hasError: false, errorMessage: undefined }; 20 | } 21 | 22 | static getDerivedStateFromError(error: Error) { 23 | return { hasError: true, errorMessage: error.message }; 24 | } 25 | 26 | render() { 27 | if (this.state.hasError) { 28 | return Uncaught exception: {this.state.errorMessage}; 29 | } 30 | 31 | return this.props.children; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /apps/navmesh-website/src/features/error-handling/error-message.ts: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | 3 | export const ErrorMessage = styled.div` 4 | position: absolute; 5 | bottom: 0px; 6 | left: 50% - 140px; 7 | width: 280px; 8 | z-index: 1; 9 | 10 | margin: 0.5em; 11 | padding: 0.5em; 12 | 13 | background-color: #222; 14 | color: #fae864; 15 | 16 | border: 1px solid #fae864; 17 | border-radius: 0.2em; 18 | 19 | font-size: 1em; 20 | font-weight: 400; 21 | `; 22 | -------------------------------------------------------------------------------- /apps/navmesh-website/src/features/error-handling/index.ts: -------------------------------------------------------------------------------- 1 | export * from './error-boundary'; 2 | export * from './error-message'; 3 | -------------------------------------------------------------------------------- /apps/navmesh-website/src/features/recast/export/nav-mesh-to-gltf.ts: -------------------------------------------------------------------------------- 1 | import { NavMesh } from 'recast-navigation'; 2 | import { 3 | BufferAttribute, 4 | BufferGeometry, 5 | Mesh, 6 | MeshStandardMaterial, 7 | Scene, 8 | } from 'three'; 9 | import { GLTFExporter } from 'three/addons'; 10 | 11 | const navMeshToPositionsAndIndices = (navMesh: NavMesh) => { 12 | const positions: number[] = []; 13 | const indices: number[] = []; 14 | let tri = 0; 15 | 16 | const maxTiles = navMesh.getMaxTiles(); 17 | 18 | for (let tileIndex = 0; tileIndex < maxTiles; tileIndex++) { 19 | const tile = navMesh.getTile(tileIndex); 20 | const tileHeader = tile.header(); 21 | 22 | if (!tileHeader) continue; 23 | 24 | const tilePolyCount = tileHeader.polyCount(); 25 | 26 | for ( 27 | let tilePolyIndex = 0; 28 | tilePolyIndex < tilePolyCount; 29 | ++tilePolyIndex 30 | ) { 31 | const poly = tile.polys(tilePolyIndex); 32 | 33 | if (poly.getType() === 1) continue; 34 | 35 | const polyVertCount = poly.vertCount(); 36 | const polyDetail = tile.detailMeshes(tilePolyIndex); 37 | const polyDetailTriBase = polyDetail.triBase(); 38 | const polyDetailTriCount = polyDetail.triCount(); 39 | 40 | for ( 41 | let polyDetailTriIndex = 0; 42 | polyDetailTriIndex < polyDetailTriCount; 43 | ++polyDetailTriIndex 44 | ) { 45 | const detailTrisBaseIndex = 46 | (polyDetailTriBase + polyDetailTriIndex) * 4; 47 | 48 | for (let trianglePoint = 0; trianglePoint < 3; ++trianglePoint) { 49 | if ( 50 | tile.detailTris(detailTrisBaseIndex + trianglePoint) < polyVertCount 51 | ) { 52 | const tileVertsBaseIndex = 53 | poly.verts(tile.detailTris(detailTrisBaseIndex + trianglePoint)) * 54 | 3; 55 | 56 | positions.push( 57 | tile.verts(tileVertsBaseIndex), 58 | tile.verts(tileVertsBaseIndex + 1), 59 | tile.verts(tileVertsBaseIndex + 2) 60 | ); 61 | } else { 62 | const tileVertsBaseIndex = 63 | (polyDetail.vertBase() + 64 | tile.detailTris(detailTrisBaseIndex + trianglePoint) - 65 | poly.vertCount()) * 66 | 3; 67 | 68 | positions.push( 69 | tile.detailVerts(tileVertsBaseIndex), 70 | tile.detailVerts(tileVertsBaseIndex + 1), 71 | tile.detailVerts(tileVertsBaseIndex + 2) 72 | ); 73 | } 74 | 75 | indices.push(tri++); 76 | } 77 | } 78 | } 79 | } 80 | 81 | return [positions, indices]; 82 | }; 83 | 84 | type GLTFExport = 85 | | ArrayBuffer 86 | | { 87 | [key: string]: any; 88 | }; 89 | 90 | export const navMeshToGLTF = (navMesh: NavMesh): Promise => { 91 | const exporter = new GLTFExporter(); 92 | 93 | const [positions, indices] = navMeshToPositionsAndIndices(navMesh); 94 | 95 | const geometry = new BufferGeometry(); 96 | 97 | geometry.setAttribute( 98 | 'position', 99 | new BufferAttribute(new Float32Array(positions), 3) 100 | ); 101 | geometry.setIndex(new BufferAttribute(new Uint16Array(indices), 1)); 102 | 103 | const material = new MeshStandardMaterial({ color: 0xffffff }); 104 | 105 | const mesh = new Mesh(geometry, material); 106 | const scene = new Scene(); 107 | scene.add(mesh); 108 | 109 | return new Promise((resolve, reject) => { 110 | exporter.parse(scene, resolve, reject); 111 | }); 112 | }; 113 | -------------------------------------------------------------------------------- /apps/navmesh-website/src/features/recast/helpers/nav-mesh-generator-input-helper.tsx: -------------------------------------------------------------------------------- 1 | import { useMemo } from 'react'; 2 | import { 3 | BufferAttribute, 4 | BufferGeometry, 5 | Mesh, 6 | MeshBasicMaterial, 7 | } from 'three'; 8 | 9 | export const NavMeshGeneratorInputHelper = ({ 10 | enabled, 11 | indexedTriangleMesh, 12 | navMeshGeneratorInputDebugColor, 13 | navMeshGeneratorInputWireframe, 14 | navMeshGeneratorInputOpacity, 15 | }: { 16 | enabled: boolean; 17 | indexedTriangleMesh: 18 | | { 19 | positions: Float32Array; 20 | indices: Uint32Array; 21 | } 22 | | undefined; 23 | navMeshGeneratorInputDebugColor: string; 24 | navMeshGeneratorInputWireframe: boolean; 25 | navMeshGeneratorInputOpacity: number; 26 | }) => { 27 | const helper = useMemo(() => { 28 | if (!indexedTriangleMesh) return undefined; 29 | 30 | const geometry = new BufferGeometry(); 31 | 32 | geometry.setAttribute( 33 | 'position', 34 | new BufferAttribute(indexedTriangleMesh.positions, 3) 35 | ); 36 | geometry.setIndex(new BufferAttribute(indexedTriangleMesh.indices, 1)); 37 | geometry.computeBoundingBox(); 38 | geometry.computeBoundingSphere(); 39 | 40 | const mesh = new Mesh( 41 | geometry, 42 | new MeshBasicMaterial({ 43 | transparent: true, 44 | color: Number(navMeshGeneratorInputDebugColor.replace('#', '0x')), 45 | wireframe: navMeshGeneratorInputWireframe, 46 | opacity: navMeshGeneratorInputOpacity, 47 | depthWrite: false, 48 | }) 49 | ); 50 | 51 | return mesh; 52 | }, [ 53 | indexedTriangleMesh, 54 | navMeshGeneratorInputDebugColor, 55 | navMeshGeneratorInputWireframe, 56 | navMeshGeneratorInputOpacity, 57 | ]); 58 | 59 | return enabled && helper && ; 60 | }; 61 | -------------------------------------------------------------------------------- /apps/navmesh-website/src/features/recast/index.ts: -------------------------------------------------------------------------------- 1 | export * from './helpers/nav-mesh-generator-input-helper'; 2 | export * from './helpers/nav-mesh-debug-drawer'; 3 | export * from './export/nav-mesh-to-gltf'; 4 | export * from './crowd/recast-agent'; 5 | -------------------------------------------------------------------------------- /apps/navmesh-website/src/features/ui/index.ts: -------------------------------------------------------------------------------- 1 | export * from './loading-spinner'; 2 | -------------------------------------------------------------------------------- /apps/navmesh-website/src/features/ui/loading-spinner.tsx: -------------------------------------------------------------------------------- 1 | import styled, { keyframes } from 'styled-components'; 2 | 3 | const Centered = styled.div` 4 | position: absolute; 5 | top: 0; 6 | left: 0; 7 | 8 | display: flex; 9 | align-items: center; 10 | justify-content: center; 11 | flex-direction: column; 12 | 13 | box-sizing: border-box; 14 | width: 100%; 15 | height: 100dvh; 16 | padding: 2em; 17 | `; 18 | 19 | const SpinnerKeyframes = keyframes` 20 | from { 21 | transform: rotate(0deg); 22 | } 23 | to { 24 | transform: rotate(360deg); 25 | } 26 | `; 27 | 28 | export const Spinner = styled.div` 29 | width: 50px; 30 | height: 50px; 31 | border: 3px solid rgba(0, 0, 0, 0); 32 | border-top: 3px solid #fff; 33 | border-radius: 50%; 34 | animation: ${SpinnerKeyframes} 1s ease infinite; 35 | `; 36 | 37 | export const LoadingSpinner = () => ( 38 | 39 | 40 | 41 | ); 42 | -------------------------------------------------------------------------------- /apps/navmesh-website/src/features/upload/index.ts: -------------------------------------------------------------------------------- 1 | export * from './loaders'; 2 | export * from './model-drop-zone'; 3 | export * from './read-file'; 4 | -------------------------------------------------------------------------------- /apps/navmesh-website/src/features/upload/loaders.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Group, 3 | Mesh, 4 | MeshStandardMaterial, 5 | REVISION, 6 | WebGLRenderer, 7 | } from 'three'; 8 | import { DRACOLoader, FBXLoader, GLTF, GLTFLoader, KTX2Loader, OBJLoader } from 'three/addons'; 9 | import { MeshoptDecoder } from 'three/examples/jsm/libs/meshopt_decoder.module.js'; 10 | 11 | const THREE_PATH = `https://unpkg.com/three@0.${REVISION}.x`; 12 | 13 | const dracoloader = new DRACOLoader().setDecoderPath( 14 | `${THREE_PATH}/examples/jsm/libs/draco/gltf/` 15 | ); 16 | 17 | const ktx2Loader = new KTX2Loader().setTranscoderPath( 18 | `${THREE_PATH}/examples/jsm/libs/basis/` 19 | ); 20 | 21 | export const gltfLoader = new GLTFLoader() 22 | .setCrossOrigin('anonymous') 23 | .setDRACOLoader(dracoloader) 24 | .setKTX2Loader(ktx2Loader.detectSupport(new WebGLRenderer())) 25 | .setMeshoptDecoder(MeshoptDecoder); 26 | 27 | const loadGltf = async (buffer: ArrayBuffer) => { 28 | const { scene } = await new Promise((resolve, reject) => 29 | gltfLoader.parse(buffer, '', resolve, reject) 30 | ); 31 | return scene; 32 | }; 33 | 34 | const objLoader = new OBJLoader().setCrossOrigin('anonymous'); 35 | 36 | const loadObj = (buffer: ArrayBuffer) => { 37 | const group = objLoader.parse(new TextDecoder('utf-8').decode(buffer)); 38 | 39 | const material = new MeshStandardMaterial({ 40 | color: '#ccc', 41 | }); 42 | 43 | group.traverse((child) => { 44 | if (child instanceof Mesh) { 45 | child.material = material; 46 | } 47 | }); 48 | 49 | return group; 50 | }; 51 | 52 | const fbxLoader = new FBXLoader().setCrossOrigin('anonymous'); 53 | 54 | const loadFbx = (buffer: ArrayBuffer) => { 55 | return fbxLoader.parse(buffer, ''); 56 | }; 57 | 58 | export const loadModel = async ( 59 | buffer: ArrayBuffer, 60 | file: File 61 | ): Promise => { 62 | if (file.name.endsWith('.glb') || file.name.endsWith('.gltf')) { 63 | return loadGltf(buffer); 64 | } else if (file.name.endsWith('.obj')) { 65 | return loadObj(buffer); 66 | } else if (file.name.endsWith('.fbx')) { 67 | return loadFbx(buffer); 68 | } 69 | 70 | throw new Error(`Unsupported file type: ${file.name}`); 71 | }; 72 | -------------------------------------------------------------------------------- /apps/navmesh-website/src/features/upload/model-drop-zone.tsx: -------------------------------------------------------------------------------- 1 | import { useDropzone } from 'react-dropzone'; 2 | import styled from 'styled-components'; 3 | 4 | const ModelDropZoneWrapper = styled.div` 5 | display: flex; 6 | flex-direction: column; 7 | align-items: center; 8 | justify-content: center; 9 | 10 | width: 100%; 11 | height: 100%; 12 | 13 | font-size: 2em; 14 | 15 | font-weight: 600; 16 | line-height: 1.3; 17 | text-align: center; 18 | 19 | color: #fff; 20 | 21 | .example { 22 | padding: 0; 23 | border: none; 24 | background: none; 25 | 26 | color: #0094ff; 27 | 28 | font-size: inherit; 29 | font-weight: inherit; 30 | text-decoration: underline; 31 | 32 | cursor: pointer; 33 | } 34 | `; 35 | 36 | export type ModelDropZoneProps = { 37 | onDrop: (acceptedFiles: File[]) => void; 38 | selectExample: () => void; 39 | }; 40 | 41 | export const ModelDropZone = ({ 42 | onDrop, 43 | selectExample, 44 | }: ModelDropZoneProps) => { 45 | const { getRootProps, getInputProps, isDragActive } = useDropzone({ 46 | onDrop, 47 | maxFiles: 1, 48 | }); 49 | 50 | return ( 51 | 52 | 53 | 54 | {isDragActive ? ( 55 |

Drop your model here ...

56 | ) : ( 57 | <> 58 |

59 | Drag 'n' drop your model here or{' '} 60 | 69 |

70 | 71 | )} 72 |
73 | ); 74 | }; 75 | -------------------------------------------------------------------------------- /apps/navmesh-website/src/features/upload/read-file.ts: -------------------------------------------------------------------------------- 1 | export const readFile = ( 2 | file: File 3 | ): Promise<{ 4 | buffer: ArrayBuffer; 5 | }> => { 6 | return new Promise((resolve, reject) => { 7 | const reader = new FileReader(); 8 | 9 | reader.onabort = () => reject('file reading was aborted'); 10 | reader.onerror = () => reject('file reading has failed'); 11 | 12 | reader.onload = async () => { 13 | const buffer = reader.result as ArrayBuffer; 14 | resolve({ buffer }); 15 | }; 16 | 17 | reader.readAsArrayBuffer(file); 18 | }); 19 | }; 20 | -------------------------------------------------------------------------------- /apps/navmesh-website/src/google-analytics.d.ts: -------------------------------------------------------------------------------- 1 | declare function gtag(...args: unknown[]): void; 2 | -------------------------------------------------------------------------------- /apps/navmesh-website/src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | padding: 0; 4 | font-family: sans-serif; 5 | background: #222; 6 | } 7 | 8 | body, 9 | #root, 10 | canvas { 11 | width: 100%; 12 | height: 100vh; 13 | } 14 | -------------------------------------------------------------------------------- /apps/navmesh-website/src/main.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom/client'; 3 | import App from './app'; 4 | import './index.css'; 5 | 6 | ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render( 7 | 8 | 9 | 10 | ); 11 | -------------------------------------------------------------------------------- /apps/navmesh-website/src/pages/index.ts: -------------------------------------------------------------------------------- 1 | export * from './editor-page'; 2 | export * from './upload-page'; 3 | -------------------------------------------------------------------------------- /apps/navmesh-website/src/state/editor-state.ts: -------------------------------------------------------------------------------- 1 | import { NavMesh } from 'recast-navigation'; 2 | import { 3 | SoloNavMeshGeneratorIntermediates, 4 | TiledNavMeshGeneratorIntermediates, 5 | } from 'recast-navigation/generators'; 6 | import { Group } from 'three'; 7 | import { create } from 'zustand'; 8 | 9 | export type EditorState = { 10 | loading: boolean; 11 | error?: string; 12 | 13 | model?: Group; 14 | 15 | indexedTriangleMesh?: { 16 | positions: Float32Array; 17 | indices: Uint32Array; 18 | }; 19 | 20 | generatorIntermediates?: 21 | | SoloNavMeshGeneratorIntermediates 22 | | TiledNavMeshGeneratorIntermediates; 23 | 24 | navMesh?: NavMesh; 25 | }; 26 | 27 | export const useEditorState = create< 28 | EditorState & { setEditorState: (partial: Partial) => void } 29 | >((set) => ({ 30 | loading: false, 31 | error: undefined, 32 | model: undefined, 33 | indexedTriangleMesh: undefined, 34 | generatorIntermediates: undefined, 35 | navMesh: undefined, 36 | setEditorState: (partial) => set(partial), 37 | })); 38 | -------------------------------------------------------------------------------- /apps/navmesh-website/src/tunnels.tsx: -------------------------------------------------------------------------------- 1 | import tunnel from 'tunnel-rat'; 2 | 3 | export const HtmlTunnel = tunnel(); 4 | -------------------------------------------------------------------------------- /apps/navmesh-website/src/types.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.exr' { 2 | const src: string; 3 | export default src; 4 | } 5 | -------------------------------------------------------------------------------- /apps/navmesh-website/src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /apps/navmesh-website/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2022", 4 | "useDefineForClassFields": true, 5 | "lib": ["DOM", "DOM.Iterable", "ES2022"], 6 | "allowJs": false, 7 | "skipLibCheck": true, 8 | "esModuleInterop": false, 9 | "allowSyntheticDefaultImports": true, 10 | "strict": true, 11 | "forceConsistentCasingInFileNames": true, 12 | "module": "ES2022", 13 | "moduleResolution": "Bundler", 14 | "resolveJsonModule": true, 15 | "isolatedModules": true, 16 | "noEmit": true, 17 | "jsx": "react-jsx" 18 | }, 19 | "include": ["src"], 20 | "references": [{ "path": "./tsconfig.node.json" }] 21 | } 22 | -------------------------------------------------------------------------------- /apps/navmesh-website/tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "module": "ES2022", 5 | "moduleResolution": "Node", 6 | "allowSyntheticDefaultImports": true 7 | }, 8 | "include": ["vite.config.ts"] 9 | } 10 | -------------------------------------------------------------------------------- /apps/navmesh-website/vercel.json: -------------------------------------------------------------------------------- 1 | { 2 | "rewrites": [{ "source": "/(.*)", "destination": "/" }] 3 | } 4 | -------------------------------------------------------------------------------- /apps/navmesh-website/vite.config.ts: -------------------------------------------------------------------------------- 1 | import react from '@vitejs/plugin-react'; 2 | import { defineConfig } from 'vite'; 3 | import { createHtmlPlugin } from 'vite-plugin-html'; 4 | 5 | // https://vitejs.dev/config/ 6 | export default defineConfig(() => { 7 | const gtagId = process.env.VITE_GTAG_ID; 8 | 9 | const analyticsScript = gtagId 10 | ? ` 11 | 12 | 21 | ` 22 | : ` 23 | 27 | `; 28 | 29 | return { 30 | plugins: [ 31 | react(), 32 | createHtmlPlugin({ 33 | minify: true, 34 | inject: { 35 | data: { 36 | analyticsScript, 37 | }, 38 | }, 39 | }), 40 | ], 41 | }; 42 | }); 43 | -------------------------------------------------------------------------------- /ci/build-all.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | yarn 4 | 5 | bash ./ci/install-emsdk.sh 6 | 7 | source ./emsdk/emsdk_env.sh 8 | 9 | yarn build 10 | -------------------------------------------------------------------------------- /ci/build-packages.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | yarn 4 | 5 | bash ./ci/install-emsdk.sh 6 | 7 | source ./emsdk/emsdk_env.sh 8 | 9 | yarn build:packages 10 | -------------------------------------------------------------------------------- /ci/install-emsdk.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | git clone https://github.com/emscripten-core/emsdk.git 4 | 5 | cd emsdk 6 | 7 | ./emsdk install 3.1.61 8 | ./emsdk activate 3.1.61 9 | -------------------------------------------------------------------------------- /examples/playcanvas-vite-example/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | -------------------------------------------------------------------------------- /examples/playcanvas-vite-example/.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "es5", 3 | "tabWidth": 2, 4 | "semi": true, 5 | "singleQuote": true 6 | } 7 | -------------------------------------------------------------------------------- /examples/playcanvas-vite-example/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Vite + React + TS 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /examples/playcanvas-vite-example/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "playcanvas-vite-example", 3 | "private": true, 4 | "type": "module", 5 | "scripts": { 6 | "dev": "vite", 7 | "build": "tsc && vite build", 8 | "preview": "vite preview" 9 | }, 10 | "dependencies": { 11 | "@recast-navigation/playcanvas": "workspace:^", 12 | "playcanvas": "^2.0.0", 13 | "recast-navigation": "workspace:^" 14 | }, 15 | "devDependencies": { 16 | "typescript": "^5.4.3", 17 | "vite": "^5.4.8" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /examples/playcanvas-vite-example/src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | padding: 0; 4 | } 5 | 6 | body, 7 | canvas { 8 | width: 100%; 9 | height: 100vh; 10 | } 11 | -------------------------------------------------------------------------------- /examples/playcanvas-vite-example/src/index.ts: -------------------------------------------------------------------------------- 1 | import { NavMeshHelper, pcToSoloNavMesh } from '@recast-navigation/playcanvas'; 2 | import * as pc from 'playcanvas'; 3 | import { init as initRecast } from 'recast-navigation'; 4 | 5 | const init = async () => { 6 | await initRecast(); 7 | 8 | const canvas = document.querySelector('#app') as HTMLCanvasElement; 9 | 10 | const device = await pc.createGraphicsDevice(canvas, { 11 | deviceTypes: [pc.DEVICETYPE_WEBGL2], 12 | }); 13 | device.maxPixelRatio = Math.min(window.devicePixelRatio, 2); 14 | 15 | const app = new pc.Application(canvas); 16 | app.setCanvasFillMode(pc.FILLMODE_FILL_WINDOW); 17 | app.setCanvasResolution(pc.RESOLUTION_AUTO); 18 | app.start(); 19 | 20 | const resize = () => app.resizeCanvas(); 21 | window.addEventListener('resize', resize); 22 | 23 | const ground = new pc.Entity('cube'); 24 | ground.addComponent('render'); 25 | ground.render!.meshInstances = [ 26 | new pc.MeshInstance( 27 | pc.Mesh.fromGeometry( 28 | app.graphicsDevice, 29 | new pc.BoxGeometry({ halfExtents: new pc.Vec3(5, 0.2, 5) }) 30 | ), 31 | new pc.StandardMaterial() 32 | ), 33 | ]; 34 | app.root.addChild(ground); 35 | 36 | const obstacle = new pc.Entity('obstacle'); 37 | obstacle.addComponent('render', { 38 | type: 'box', 39 | material: new pc.StandardMaterial(), 40 | }); 41 | obstacle.setLocalScale(1, 1, 1); 42 | obstacle.setPosition(0, 0.5, 0); 43 | app.root.addChild(obstacle); 44 | 45 | const camera = new pc.Entity('camera'); 46 | camera.addComponent('camera', { 47 | clearColor: new pc.Color(0.5, 0.6, 0.9), 48 | }); 49 | app.root.addChild(camera); 50 | camera.setPosition(5, 5, 5); 51 | camera.lookAt(0, 0, 0); 52 | 53 | const light = new pc.Entity('light'); 54 | light.addComponent('light'); 55 | app.root.addChild(light); 56 | light.setEulerAngles(45, 0, 0); 57 | 58 | const recastConfig = { 59 | cs: 0.05, 60 | ch: 0.2, 61 | }; 62 | 63 | const navMeshMeshInstances = [ 64 | ...ground.render!.meshInstances, 65 | ...obstacle.render!.meshInstances, 66 | ]; 67 | 68 | const { success, navMesh } = pcToSoloNavMesh( 69 | navMeshMeshInstances, 70 | recastConfig 71 | ); 72 | 73 | if (success) { 74 | const navMeshHelper = new NavMeshHelper(navMesh, app.graphicsDevice); 75 | 76 | app.root.addChild(navMeshHelper); 77 | } 78 | }; 79 | 80 | init(); 81 | -------------------------------------------------------------------------------- /examples/playcanvas-vite-example/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2022", 4 | "useDefineForClassFields": true, 5 | "lib": ["DOM", "DOM.Iterable", "ES2022"], 6 | "allowJs": false, 7 | "skipLibCheck": true, 8 | "esModuleInterop": false, 9 | "allowSyntheticDefaultImports": true, 10 | "strict": true, 11 | "forceConsistentCasingInFileNames": true, 12 | "module": "ES2022", 13 | "moduleResolution": "Bundler", 14 | "resolveJsonModule": true, 15 | "isolatedModules": true, 16 | "noEmit": true, 17 | "jsx": "react-jsx" 18 | }, 19 | "include": ["src"], 20 | "references": [{ "path": "./tsconfig.node.json" }] 21 | } 22 | -------------------------------------------------------------------------------- /examples/playcanvas-vite-example/tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "module": "ES2022", 5 | "moduleResolution": "Node", 6 | "allowSyntheticDefaultImports": true 7 | }, 8 | "include": ["vite.config.ts"] 9 | } 10 | -------------------------------------------------------------------------------- /examples/playcanvas-vite-example/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite' 2 | import react from '@vitejs/plugin-react' 3 | 4 | // https://vitejs.dev/config/ 5 | export default defineConfig({ 6 | plugins: [react()], 7 | }) 8 | -------------------------------------------------------------------------------- /examples/three-node-cjs-example/index.cjs: -------------------------------------------------------------------------------- 1 | const THREE = require('three'); 2 | const Recast = require('recast-navigation'); 3 | const RecastGenerators = require('recast-navigation/generators'); 4 | const RecastThree = require('@recast-navigation/three'); 5 | 6 | Recast.init().then(() => { 7 | console.log(RecastThree); 8 | 9 | const groundMesh = new THREE.Mesh(new THREE.BoxGeometry(5, 0.2, 5)); 10 | 11 | /** 12 | * @type {import('recast-navigation/generators').SoloNavMeshGeneratorConfig} 13 | */ 14 | const config = { 15 | borderSize: 0, 16 | cs: 0.2, 17 | ch: 0.2, 18 | walkableSlopeAngle: 35, 19 | walkableHeight: 1, 20 | walkableClimb: 1, 21 | walkableRadius: 1, 22 | maxEdgeLen: 12, 23 | maxSimplificationError: 1.3, 24 | minRegionArea: 8, 25 | mergeRegionArea: 20, 26 | maxVertsPerPoly: 6, 27 | detailSampleDist: 6, 28 | detailSampleMaxError: 1, 29 | }; 30 | 31 | const positions = groundMesh.geometry.attributes.position.array; 32 | const indices = groundMesh.geometry.index.array; 33 | 34 | const { navMesh } = RecastGenerators.generateSoloNavMesh(positions, indices, config); 35 | 36 | const navMeshQuery = new Recast.NavMeshQuery(navMesh); 37 | 38 | const { point: closestPoint } = navMeshQuery.findClosestPoint({ x: 2, y: 1, z: 2 }); 39 | 40 | console.log(closestPoint.x, closestPoint.y, closestPoint.z); 41 | }); 42 | -------------------------------------------------------------------------------- /examples/three-node-cjs-example/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "three-node-cjs-example", 3 | "private": true, 4 | "version": "1.0.0", 5 | "description": "example of using recast-navigation in node", 6 | "scripts": { 7 | "start": "node --experimental-require-module index.cjs" 8 | }, 9 | "author": "", 10 | "license": "ISC", 11 | "dependencies": { 12 | "recast-navigation": "0.39.0", 13 | "three": "^0.172.0" 14 | }, 15 | "devDependencies": { 16 | "@types/three": "^0.172.0" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /examples/three-node-esm-example/index.mjs: -------------------------------------------------------------------------------- 1 | import { NavMeshQuery, init } from 'recast-navigation'; 2 | import { generateSoloNavMesh } from 'recast-navigation/generators'; 3 | import * as RecastThree from '@recast-navigation/three'; 4 | import { BoxGeometry, Mesh } from 'three'; 5 | 6 | await init(); 7 | 8 | console.log(RecastThree); 9 | 10 | const groundMesh = new Mesh(new BoxGeometry(5, 0.2, 5)); 11 | 12 | /** 13 | * @type {import('recast-navigation/generators').SoloNavMeshGeneratorConfig} 14 | */ 15 | const config = { 16 | borderSize: 0, 17 | cs: 0.2, 18 | ch: 0.2, 19 | walkableSlopeAngle: 35, 20 | walkableHeight: 1, 21 | walkableClimb: 1, 22 | walkableRadius: 1, 23 | maxEdgeLen: 12, 24 | maxSimplificationError: 1.3, 25 | minRegionArea: 8, 26 | mergeRegionArea: 20, 27 | maxVertsPerPoly: 6, 28 | detailSampleDist: 6, 29 | detailSampleMaxError: 1, 30 | }; 31 | 32 | const positions = groundMesh.geometry.attributes.position.array; 33 | const indices = groundMesh.geometry.index.array; 34 | 35 | const { navMesh } = generateSoloNavMesh(positions, indices, config); 36 | 37 | const navMeshQuery = new NavMeshQuery(navMesh); 38 | 39 | const { point: closestPoint } = navMeshQuery.findClosestPoint({ x: 2, y: 1, z: 2 }); 40 | 41 | console.log(closestPoint.x, closestPoint.y, closestPoint.z); 42 | -------------------------------------------------------------------------------- /examples/three-node-esm-example/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "three-node-esm-example", 3 | "private": true, 4 | "type": "module", 5 | "version": "1.0.0", 6 | "description": "example of using recast-navigation in node", 7 | "scripts": { 8 | "start": "node index.mjs" 9 | }, 10 | "author": "", 11 | "license": "ISC", 12 | "dependencies": { 13 | "recast-navigation": "0.39.0", 14 | "three": "^0.172.0" 15 | }, 16 | "devDependencies": { 17 | "@types/three": "^0.172.0" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /examples/three-parcel-example/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 |
6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /examples/three-parcel-example/index.js: -------------------------------------------------------------------------------- 1 | import { NavMeshHelper, threeToSoloNavMesh } from '@recast-navigation/three'; 2 | import { init as initRecast } from 'recast-navigation'; 3 | import * as THREE from 'three'; 4 | import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js'; 5 | 6 | import './styles.css'; 7 | 8 | const init = async () => { 9 | await initRecast(); 10 | 11 | const renderer = new THREE.WebGLRenderer({ 12 | antialias: true, 13 | }); 14 | 15 | const rootDomElement = document.body.querySelector('#root'); 16 | rootDomElement.appendChild(renderer.domElement); 17 | 18 | const camera = new THREE.PerspectiveCamera( 19 | 75, 20 | window.innerWidth / window.innerHeight, 21 | 0.1, 22 | 1000 23 | ); 24 | camera.position.set(10, 10, 10); 25 | 26 | const scene = new THREE.Scene(); 27 | 28 | const onResize = () => { 29 | renderer.setSize(window.innerWidth, window.innerHeight); 30 | camera.aspect = window.innerWidth / window.innerHeight; 31 | camera.updateProjectionMatrix(); 32 | }; 33 | 34 | onResize(); 35 | 36 | window.addEventListener('resize', onResize); 37 | 38 | const geometry = new THREE.BoxGeometry(10, 0.2, 10); 39 | const material = new THREE.MeshBasicMaterial({ color: 0x333333 }); 40 | 41 | const mesh = new THREE.Mesh(geometry, material); 42 | scene.add(mesh); 43 | 44 | const ambientLight = new THREE.AmbientLight(0x404040); 45 | scene.add(ambientLight); 46 | 47 | const pointLight = new THREE.PointLight(0xffffff, 1, 100); 48 | scene.add(pointLight); 49 | 50 | new OrbitControls(camera, renderer.domElement); 51 | 52 | const { success, navMesh } = threeToSoloNavMesh([mesh]); 53 | 54 | if (success) { 55 | const navMeshHelper = new NavMeshHelper(navMesh); 56 | 57 | scene.add(navMeshHelper); 58 | } 59 | 60 | const loop = () => { 61 | renderer.render(scene, camera); 62 | requestAnimationFrame(loop); 63 | }; 64 | 65 | loop(); 66 | }; 67 | 68 | init(); 69 | -------------------------------------------------------------------------------- /examples/three-parcel-example/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "three-parcel-example", 3 | "private": true, 4 | "packageManager": "yarn@3.5.0", 5 | "scripts": { 6 | "dev": "parcel index.html", 7 | "build": "parcel build index.html" 8 | }, 9 | "dependencies": { 10 | "react": "18.3.1", 11 | "recast-navigation": "workspace:^", 12 | "three": "^0.172.0" 13 | }, 14 | "devDependencies": { 15 | "parcel": "^2.11.0" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /examples/three-parcel-example/styles.css: -------------------------------------------------------------------------------- 1 | #root, 2 | canvas { 3 | width: 100%; 4 | height: 100vh; 5 | } 6 | 7 | body { 8 | margin: 0; 9 | } 10 | -------------------------------------------------------------------------------- /examples/three-vite-example/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | -------------------------------------------------------------------------------- /examples/three-vite-example/.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "es5", 3 | "tabWidth": 2, 4 | "semi": true, 5 | "singleQuote": true 6 | } 7 | -------------------------------------------------------------------------------- /examples/three-vite-example/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | three-vite-wasm-example 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /examples/three-vite-example/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "three-vite-example", 3 | "private": true, 4 | "type": "module", 5 | "scripts": { 6 | "dev": "vite", 7 | "build": "tsc && vite build", 8 | "preview": "vite preview" 9 | }, 10 | "dependencies": { 11 | "recast-navigation": "0.39.0", 12 | "three": "^0.172.0" 13 | }, 14 | "devDependencies": { 15 | "@types/three": "^0.172.0", 16 | "typescript": "^5.4.3", 17 | "vite": "^5.4.8" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /examples/three-vite-example/src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | padding: 0; 4 | } 5 | 6 | body, 7 | #root, 8 | canvas { 9 | width: 100%; 10 | height: 100vh; 11 | } 12 | -------------------------------------------------------------------------------- /examples/three-vite-example/src/index.ts: -------------------------------------------------------------------------------- 1 | import { NavMeshHelper, threeToSoloNavMesh } from '@recast-navigation/three'; 2 | import { init as initRecast } from 'recast-navigation'; 3 | import * as THREE from 'three'; 4 | import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js'; 5 | 6 | import './index.css'; 7 | 8 | const init = async () => { 9 | await initRecast(); 10 | 11 | const renderer = new THREE.WebGLRenderer({ 12 | antialias: true, 13 | }); 14 | 15 | const rootDomElement = document.body.querySelector('#root')!; 16 | rootDomElement.appendChild(renderer.domElement); 17 | 18 | const camera = new THREE.PerspectiveCamera( 19 | 75, 20 | window.innerWidth / window.innerHeight, 21 | 0.1, 22 | 1000 23 | ); 24 | camera.position.set(10, 10, 10); 25 | 26 | const scene = new THREE.Scene(); 27 | 28 | const onResize = () => { 29 | renderer.setSize(window.innerWidth, window.innerHeight); 30 | camera.aspect = window.innerWidth / window.innerHeight; 31 | camera.updateProjectionMatrix(); 32 | }; 33 | 34 | onResize(); 35 | 36 | window.addEventListener('resize', onResize); 37 | 38 | const geometry = new THREE.BoxGeometry(10, 0.2, 10); 39 | const material = new THREE.MeshBasicMaterial({ color: 0x333333 }); 40 | 41 | const mesh = new THREE.Mesh(geometry, material); 42 | scene.add(mesh); 43 | 44 | const ambientLight = new THREE.AmbientLight(0x404040); 45 | scene.add(ambientLight); 46 | 47 | const pointLight = new THREE.PointLight(0xffffff, 1, 100); 48 | scene.add(pointLight); 49 | 50 | new OrbitControls(camera, renderer.domElement); 51 | 52 | const { success, navMesh } = threeToSoloNavMesh([mesh]); 53 | 54 | if (success) { 55 | const navMeshHelper = new NavMeshHelper(navMesh); 56 | 57 | scene.add(navMeshHelper); 58 | } 59 | 60 | const loop = () => { 61 | renderer.render(scene, camera); 62 | requestAnimationFrame(loop); 63 | }; 64 | 65 | loop(); 66 | }; 67 | 68 | init(); 69 | -------------------------------------------------------------------------------- /examples/three-vite-example/src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /examples/three-vite-example/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2022", 4 | "useDefineForClassFields": true, 5 | "lib": ["DOM", "DOM.Iterable", "ES2022"], 6 | "allowJs": false, 7 | "skipLibCheck": true, 8 | "esModuleInterop": false, 9 | "allowSyntheticDefaultImports": true, 10 | "strict": true, 11 | "forceConsistentCasingInFileNames": true, 12 | "module": "ES2022", 13 | "moduleResolution": "Bundler", 14 | "resolveJsonModule": true, 15 | "isolatedModules": true, 16 | "noEmit": true, 17 | "jsx": "react-jsx" 18 | }, 19 | "include": ["src"], 20 | "references": [{ "path": "./tsconfig.node.json" }] 21 | } 22 | -------------------------------------------------------------------------------- /examples/three-vite-example/tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "module": "ES2022", 5 | "moduleResolution": "Node", 6 | "allowSyntheticDefaultImports": true 7 | }, 8 | "include": ["vite.config.ts"] 9 | } 10 | -------------------------------------------------------------------------------- /examples/three-vite-example/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite'; 2 | 3 | // https://vitejs.dev/config/ 4 | export default defineConfig({ 5 | plugins: [], 6 | }); 7 | -------------------------------------------------------------------------------- /examples/three-vite-worker-example/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | -------------------------------------------------------------------------------- /examples/three-vite-worker-example/.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "es5", 3 | "tabWidth": 2, 4 | "semi": true, 5 | "singleQuote": true 6 | } 7 | -------------------------------------------------------------------------------- /examples/three-vite-worker-example/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Vite + React + TS 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /examples/three-vite-worker-example/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "three-vite-worker-example", 3 | "private": true, 4 | "type": "module", 5 | "scripts": { 6 | "dev": "vite", 7 | "build": "tsc && vite build", 8 | "preview": "vite preview" 9 | }, 10 | "dependencies": { 11 | "recast-navigation": "0.39.0", 12 | "three": "^0.172.0" 13 | }, 14 | "devDependencies": { 15 | "@types/three": "^0.172.0", 16 | "typescript": "^5.4.3", 17 | "vite": "^5.4.8" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /examples/three-vite-worker-example/src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | padding: 0; 4 | } 5 | 6 | body, 7 | #root, 8 | canvas { 9 | width: 100%; 10 | height: 100vh; 11 | } 12 | -------------------------------------------------------------------------------- /examples/three-vite-worker-example/src/index.ts: -------------------------------------------------------------------------------- 1 | import { DebugDrawer, getPositionsAndIndices } from '@recast-navigation/three'; 2 | import { NavMesh, importNavMesh, init as initRecast } from 'recast-navigation'; 3 | import * as THREE from 'three'; 4 | import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js'; 5 | import NavMeshWorker from './navmesh-worker?worker'; 6 | 7 | import './index.css'; 8 | 9 | const init = async () => { 10 | await initRecast(); 11 | 12 | const renderer = new THREE.WebGLRenderer({ 13 | antialias: true, 14 | }); 15 | 16 | const rootDomElement = document.body.querySelector('#root')!; 17 | rootDomElement.appendChild(renderer.domElement); 18 | 19 | const camera = new THREE.PerspectiveCamera( 20 | 75, 21 | window.innerWidth / window.innerHeight, 22 | 0.1, 23 | 1000 24 | ); 25 | camera.position.set(10, 10, 10); 26 | 27 | const scene = new THREE.Scene(); 28 | 29 | const onResize = () => { 30 | renderer.setSize(window.innerWidth, window.innerHeight); 31 | camera.aspect = window.innerWidth / window.innerHeight; 32 | camera.updateProjectionMatrix(); 33 | }; 34 | 35 | onResize(); 36 | 37 | window.addEventListener('resize', onResize); 38 | 39 | const ground = new THREE.Mesh( 40 | new THREE.BoxGeometry(10, 0.2, 10), 41 | new THREE.MeshBasicMaterial({ color: 0x999999 }) 42 | ); 43 | scene.add(ground); 44 | 45 | const obstacle = new THREE.Mesh( 46 | new THREE.BoxGeometry(2, 2, 2), 47 | new THREE.MeshBasicMaterial({ color: 0x333333 }) 48 | ); 49 | obstacle.position.set(0, 1, 0); 50 | scene.add(obstacle); 51 | 52 | const ambientLight = new THREE.AmbientLight(0x404040); 53 | scene.add(ambientLight); 54 | 55 | const pointLight = new THREE.PointLight(0xffffff, 1, 100); 56 | scene.add(pointLight); 57 | 58 | new OrbitControls(camera, renderer.domElement); 59 | 60 | const [positions, indices] = getPositionsAndIndices([ground, obstacle]); 61 | 62 | const navMeshConfig = { 63 | cs: 0.05, 64 | ch: 0.2, 65 | }; 66 | 67 | const debugDrawer = new DebugDrawer(); 68 | scene.add(debugDrawer); 69 | 70 | const worker = new NavMeshWorker(); 71 | 72 | let navMesh: NavMesh | undefined; 73 | 74 | worker.onmessage = (event) => { 75 | const navMeshExport = event.data; 76 | 77 | const result = importNavMesh(navMeshExport); 78 | 79 | navMesh = result.navMesh; 80 | 81 | debugDrawer.clear(); 82 | debugDrawer.drawNavMesh(navMesh); 83 | }; 84 | 85 | worker.postMessage({ positions, indices, config: navMeshConfig }, [ 86 | positions.buffer, 87 | indices.buffer, 88 | ]); 89 | 90 | const loop = () => { 91 | renderer.render(scene, camera); 92 | requestAnimationFrame(loop); 93 | }; 94 | 95 | loop(); 96 | }; 97 | 98 | init(); 99 | -------------------------------------------------------------------------------- /examples/three-vite-worker-example/src/navmesh-worker.ts: -------------------------------------------------------------------------------- 1 | import { RecastConfig, exportNavMesh, init } from '@recast-navigation/core'; 2 | import { generateSoloNavMesh } from '@recast-navigation/generators'; 3 | 4 | self.onmessage = async (event: { 5 | data: { 6 | positions: Float32Array; 7 | indices: Uint32Array; 8 | config: Partial; 9 | }; 10 | }) => { 11 | await init(); 12 | 13 | const { positions, indices, config } = event.data; 14 | 15 | const { success, navMesh } = generateSoloNavMesh(positions, indices, config); 16 | 17 | if (!success) return; 18 | 19 | const navMeshExport = exportNavMesh(navMesh); 20 | 21 | self.postMessage(navMeshExport, { transfer: [navMeshExport.buffer] }); 22 | 23 | navMesh.destroy(); 24 | }; 25 | -------------------------------------------------------------------------------- /examples/three-vite-worker-example/src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /examples/three-vite-worker-example/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2022", 4 | "useDefineForClassFields": true, 5 | "lib": ["DOM", "DOM.Iterable", "ES2022"], 6 | "allowJs": false, 7 | "skipLibCheck": true, 8 | "esModuleInterop": false, 9 | "allowSyntheticDefaultImports": true, 10 | "strict": true, 11 | "forceConsistentCasingInFileNames": true, 12 | "module": "ES2022", 13 | "moduleResolution": "Bundler", 14 | "resolveJsonModule": true, 15 | "isolatedModules": true, 16 | "noEmit": true, 17 | "jsx": "react-jsx" 18 | }, 19 | "include": ["src"], 20 | "references": [{ "path": "./tsconfig.node.json" }] 21 | } 22 | -------------------------------------------------------------------------------- /examples/three-vite-worker-example/tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "module": "ES2022", 5 | "moduleResolution": "Node", 6 | "allowSyntheticDefaultImports": true 7 | }, 8 | "include": ["vite.config.ts"] 9 | } 10 | -------------------------------------------------------------------------------- /examples/three-vite-worker-example/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite'; 2 | 3 | // https://vitejs.dev/config/ 4 | export default defineConfig({ 5 | worker: { 6 | format: 'es', 7 | }, 8 | }); 9 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "recast-navigation-root", 3 | "private": true, 4 | "workspaces": [ 5 | "packages/recast-navigation-wasm", 6 | "packages/recast-navigation-core", 7 | "packages/recast-navigation-generators", 8 | "packages/recast-navigation-three", 9 | "packages/recast-navigation-playcanvas", 10 | "packages/recast-navigation", 11 | "apps/*", 12 | "examples/*" 13 | ], 14 | "scripts": { 15 | "build": "yarn build:packages && yarn build:apps", 16 | "build:packages": "yarn workspaces foreach -t -A --include @recast-navigation/wasm --include @recast-navigation/core --include @recast-navigation/generators --include recast-navigation --include @recast-navigation/three --include @recast-navigation/playcanvas run build", 17 | "build:apps": "yarn workspaces foreach -A -t --include navmesh-website --include docs run build", 18 | "build:examples": "yarn workspaces foreach -A -t --include three-parcel-example --include three-vite-example --include three-vite-worker-example --include playcanvas-vite-example run build", 19 | "test": "yarn test:packages && yarn test:node-smoke-test && yarn lint", 20 | "test:packages": "(cd packages/recast-navigation && yarn test)", 21 | "test:node-smoke-test": "(cd ./examples/three-node-cjs-example && yarn start) && (cd ./examples/three-node-esm-example && yarn start)", 22 | "lint": "yarn workspaces foreach -A -t run lint", 23 | "change": "yarn changeset", 24 | "release": "yarn build && yarn test", 25 | "publish": "changeset publish", 26 | "version": "yarn changeset version && yarn install --mode update-lockfile" 27 | }, 28 | "devDependencies": { 29 | "@changesets/cli": "^2.26.2", 30 | "vercel": "^33.7.0" 31 | }, 32 | "packageManager": "yarn@4.1.1" 33 | } 34 | -------------------------------------------------------------------------------- /packages/recast-navigation-core/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | 26 | # jest coverage 27 | coverage 28 | -------------------------------------------------------------------------------- /packages/recast-navigation-core/.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "es5", 3 | "tabWidth": 2, 4 | "semi": true, 5 | "singleQuote": true 6 | } 7 | -------------------------------------------------------------------------------- /packages/recast-navigation-core/LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | Copyright © 2025 Isaac Mason 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /packages/recast-navigation-core/README.md: -------------------------------------------------------------------------------- 1 | # @recast-navigation/core 2 | 3 | This package contains the the core functionality for `recast-navigation`. Please refer to the [main package's README](https://github.com/isaac-mason/recast-navigation-js/tree/main/packages/recast-navigation/README.md) for usage information. 4 | -------------------------------------------------------------------------------- /packages/recast-navigation-core/eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import config from '@isaac-mason/eslint-config-typescript' 2 | 3 | export default [...config] 4 | -------------------------------------------------------------------------------- /packages/recast-navigation-core/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@recast-navigation/core", 3 | "description": "Recast Navigation for JavaScript!", 4 | "author": { 5 | "name": "Isaac Mason", 6 | "email": "isaac@isaacmason.com", 7 | "url": "https://isaacmason.com/" 8 | }, 9 | "version": "0.39.0", 10 | "license": "MIT", 11 | "homepage": "https://github.com/isaac-mason/recast-navigation-js", 12 | "bugs": { 13 | "url": "https://github.com/isaac-mason/recast-navigation-js/issues" 14 | }, 15 | "type": "module", 16 | "main": "./dist/index.mjs", 17 | "module": "./dist/index.mjs", 18 | "types": "./dist/index.d.ts", 19 | "files": [ 20 | "dist/**", 21 | "README.md", 22 | "LICENSE" 23 | ], 24 | "scripts": { 25 | "build": "rollup --config rollup.config.js --bundleConfigAsCjs", 26 | "test": "tsc", 27 | "lint": "eslint src" 28 | }, 29 | "dependencies": { 30 | "@recast-navigation/wasm": "0.39.0" 31 | }, 32 | "devDependencies": { 33 | "@babel/core": "^7.24.5", 34 | "@babel/preset-env": "^7.24.5", 35 | "@babel/preset-typescript": "^7.24.1", 36 | "@isaac-mason/eslint-config-typescript": "^0.0.9", 37 | "@rollup/plugin-babel": "^6.0.4", 38 | "@rollup/plugin-commonjs": "^25.0.7", 39 | "@rollup/plugin-node-resolve": "^15.0.1", 40 | "@rollup/plugin-typescript": "^11.1.6", 41 | "babel-loader": "^9.1.3", 42 | "eslint": "^9.6.0", 43 | "prettier": "^3.1.0", 44 | "rollup": "^4.14.0", 45 | "rollup-plugin-copy": "^3.4.0", 46 | "rollup-plugin-filesize": "^10.0.0", 47 | "typescript": "^5.4.3" 48 | }, 49 | "packageManager": "yarn@3.2.1" 50 | } 51 | -------------------------------------------------------------------------------- /packages/recast-navigation-core/rollup.config.js: -------------------------------------------------------------------------------- 1 | import babel from '@rollup/plugin-babel'; 2 | import commonjs from '@rollup/plugin-commonjs'; 3 | import resolve from '@rollup/plugin-node-resolve'; 4 | import typescript from '@rollup/plugin-typescript'; 5 | import path from 'path'; 6 | import filesize from 'rollup-plugin-filesize'; 7 | 8 | const babelOptions = { 9 | babelrc: false, 10 | extensions: ['.ts'], 11 | exclude: '**/node_modules/**', 12 | babelHelpers: 'bundled', 13 | presets: [ 14 | [ 15 | '@babel/preset-env', 16 | { 17 | loose: true, 18 | modules: false, 19 | targets: '>1%, not dead, not ie 11, not op_mini all', 20 | }, 21 | ], 22 | '@babel/preset-typescript', 23 | ], 24 | }; 25 | 26 | export default [ 27 | { 28 | input: './src/index.ts', 29 | external: ['@recast-navigation/wasm'], 30 | output: [ 31 | { 32 | file: 'dist/index.mjs', 33 | format: 'es', 34 | }, 35 | ], 36 | plugins: [ 37 | resolve(), 38 | commonjs(), 39 | typescript({ 40 | tsconfig: path.resolve(__dirname, 'tsconfig.json'), 41 | emitDeclarationOnly: true, 42 | }), 43 | babel(babelOptions), 44 | filesize(), 45 | ], 46 | }, 47 | ]; 48 | -------------------------------------------------------------------------------- /packages/recast-navigation-core/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './arrays'; 2 | export * from './crowd'; 3 | export * from './debug-drawer-utils'; 4 | export * from './detour'; 5 | export * from './nav-mesh'; 6 | export * from './nav-mesh-query'; 7 | export * from './random'; 8 | export * from './raw'; 9 | export * from './recast'; 10 | export * from './serdes'; 11 | export * from './tile-cache'; 12 | export * from './utils'; 13 | -------------------------------------------------------------------------------- /packages/recast-navigation-core/src/random.ts: -------------------------------------------------------------------------------- 1 | import { Raw } from './raw'; 2 | 3 | export const getRandomSeed = () => { 4 | return Raw.Module.FastRand.prototype.getSeed(); 5 | }; 6 | 7 | export const setRandomSeed = (seed: number) => { 8 | Raw.Module.FastRand.prototype.setSeed(seed); 9 | }; 10 | -------------------------------------------------------------------------------- /packages/recast-navigation-core/src/raw-module.ts: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/isaac-mason/recast-navigation-js/416da48186b9e95736c7e1555e40858c610d78b1/packages/recast-navigation-core/src/raw-module.ts -------------------------------------------------------------------------------- /packages/recast-navigation-core/src/serdes/export.ts: -------------------------------------------------------------------------------- 1 | import { NavMesh } from '../nav-mesh'; 2 | import { TileCache } from '../tile-cache'; 3 | import { Raw } from '../raw'; 4 | 5 | const exportImpl = (navMesh: NavMesh, tileCache?: TileCache): Uint8Array => { 6 | const navMeshExport = Raw.NavMeshExporter.exportNavMesh( 7 | navMesh.raw, 8 | tileCache?.raw as never 9 | ); 10 | 11 | const arrView = new Uint8Array( 12 | Raw.Module.HEAPU8.buffer, 13 | navMeshExport.dataPointer, 14 | navMeshExport.size 15 | ); 16 | 17 | const data = new Uint8Array(navMeshExport.size); 18 | data.set(arrView); 19 | Raw.NavMeshExporter.freeNavMeshExport(navMeshExport); 20 | 21 | return data; 22 | }; 23 | 24 | export const exportNavMesh = (navMesh: NavMesh): Uint8Array => { 25 | return exportImpl(navMesh); 26 | }; 27 | 28 | export const exportTileCache = ( 29 | navMesh: NavMesh, 30 | tileCache: TileCache 31 | ): Uint8Array => { 32 | return exportImpl(navMesh, tileCache); 33 | }; 34 | -------------------------------------------------------------------------------- /packages/recast-navigation-core/src/serdes/import.ts: -------------------------------------------------------------------------------- 1 | import { NavMesh } from '../nav-mesh'; 2 | import { Raw, type RawModule } from '../raw'; 3 | import { TileCache, TileCacheMeshProcess } from '../tile-cache'; 4 | 5 | const createNavMeshExport = (data: Uint8Array) => { 6 | const nDataBytes = data.length * data.BYTES_PER_ELEMENT; 7 | const dataPtr = Raw.Module._malloc(nDataBytes); 8 | 9 | const dataHeap = new Uint8Array( 10 | Raw.Module.HEAPU8.buffer, 11 | dataPtr, 12 | nDataBytes 13 | ); 14 | dataHeap.set(data); 15 | 16 | const navMeshExport = new Raw.Module.NavMeshExport(); 17 | navMeshExport.dataPointer = dataHeap.byteOffset; 18 | navMeshExport.size = data.length; 19 | 20 | return { navMeshExport, dataHeap }; 21 | }; 22 | 23 | export type ImportNavMeshResult = { 24 | navMesh: NavMesh; 25 | }; 26 | 27 | export const importNavMesh = ( 28 | data: Uint8Array 29 | ): ImportNavMeshResult => { 30 | const { navMeshExport, dataHeap } = createNavMeshExport(data); 31 | 32 | const result = Raw.NavMeshImporter.importNavMesh(navMeshExport, undefined!); 33 | 34 | Raw.Module._free(dataHeap.byteOffset); 35 | 36 | const navMesh = new NavMesh(result.navMesh); 37 | 38 | return { navMesh }; 39 | }; 40 | 41 | export type ImportTileCacheResult = { 42 | navMesh: NavMesh; 43 | tileCache: TileCache; 44 | allocator: RawModule.RecastLinearAllocator; 45 | compressor: RawModule.RecastFastLZCompressor; 46 | }; 47 | 48 | export const importTileCache = ( 49 | data: Uint8Array, 50 | tileCacheMeshProcess: TileCacheMeshProcess 51 | ): ImportTileCacheResult => { 52 | const { navMeshExport, dataHeap } = createNavMeshExport(data); 53 | 54 | const result = Raw.NavMeshImporter.importNavMesh( 55 | navMeshExport, 56 | tileCacheMeshProcess.raw as never 57 | ); 58 | 59 | Raw.Module._free(dataHeap.byteOffset); 60 | 61 | const navMesh = new NavMesh(result.navMesh); 62 | const tileCache = new TileCache(result.tileCache); 63 | 64 | const allocator = result.allocator; 65 | const compressor = result.compressor; 66 | 67 | return { navMesh, tileCache, allocator, compressor }; 68 | }; 69 | -------------------------------------------------------------------------------- /packages/recast-navigation-core/src/serdes/index.ts: -------------------------------------------------------------------------------- 1 | export * from './export'; 2 | export * from './import'; 3 | -------------------------------------------------------------------------------- /packages/recast-navigation-core/src/types.ts: -------------------------------------------------------------------------------- 1 | export type Pretty = T extends unknown ? { [K in keyof T]: T[K] } : never; 2 | -------------------------------------------------------------------------------- /packages/recast-navigation-core/src/utils.ts: -------------------------------------------------------------------------------- 1 | import { Raw, type RawModule } from './raw'; 2 | 3 | export type Vector3 = { x: number; y: number; z: number }; 4 | 5 | export type Vector3Tuple = [number, number, number]; 6 | 7 | export type Vector2 = { x: number; y: number }; 8 | 9 | export type Vector2Tuple = [x: number, y: number]; 10 | 11 | export const vec3 = { 12 | toRaw: ({ x, y, z }: Vector3, existing?: RawModule.Vec3) => { 13 | if (existing) { 14 | existing.x = x; 15 | existing.y = y; 16 | existing.z = z; 17 | return existing; 18 | } 19 | 20 | return new Raw.Module.Vec3(x, y, z); 21 | }, 22 | fromRaw: (vec3: RawModule.Vec3) => { 23 | const { x, y, z } = vec3; 24 | 25 | return { x, y, z }; 26 | }, 27 | fromArray: ([x, y, z]: number[]) => { 28 | return { x, y, z }; 29 | }, 30 | toArray: ({ x, y, z }: Vector3): Vector3Tuple => { 31 | return [x, y, z]; 32 | }, 33 | lerp: ( 34 | a: Vector3, 35 | b: Vector3, 36 | t: number, 37 | out: Vector3 = { x: 0, y: 0, z: 0 } 38 | ) => { 39 | out.x = a.x + (b.x - a.x) * t; 40 | out.y = a.y + (b.y - a.y) * t; 41 | out.z = a.z + (b.z - a.z) * t; 42 | }, 43 | copy: (source: Vector3, out: Vector3 = { x: 0, y: 0, z: 0 }) => { 44 | out.x = source.x; 45 | out.y = source.y; 46 | out.z = source.z; 47 | }, 48 | }; 49 | 50 | export const array = (getter: (index: number) => T, count: number) => { 51 | const array: T[] = []; 52 | 53 | for (let i = 0; i < count; i++) { 54 | array.push(getter(i)); 55 | } 56 | 57 | return array; 58 | }; 59 | -------------------------------------------------------------------------------- /packages/recast-navigation-core/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2022", 4 | "useDefineForClassFields": true, 5 | "module": "ES2022", 6 | "lib": ["ES2022", "DOM"], 7 | "moduleResolution": "Bundler", 8 | "strict": true, 9 | "resolveJsonModule": true, 10 | "isolatedModules": true, 11 | "esModuleInterop": true, 12 | "noEmit": true, 13 | "declaration": true, 14 | "noUnusedLocals": true, 15 | "noUnusedParameters": true, 16 | "noImplicitReturns": true, 17 | "skipLibCheck": true, 18 | "baseUrl": "./", 19 | "rootDir": "./src", 20 | "allowSyntheticDefaultImports": true, 21 | "outDir": "./dist" 22 | }, 23 | "files": ["./src/index.ts"], 24 | } 25 | -------------------------------------------------------------------------------- /packages/recast-navigation-generators/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | 26 | # jest coverage 27 | coverage 28 | -------------------------------------------------------------------------------- /packages/recast-navigation-generators/.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "es5", 3 | "tabWidth": 2, 4 | "semi": true, 5 | "singleQuote": true 6 | } 7 | -------------------------------------------------------------------------------- /packages/recast-navigation-generators/LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | Copyright © 2025 Isaac Mason 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /packages/recast-navigation-generators/README.md: -------------------------------------------------------------------------------- 1 | # @recast-navigation/generators 2 | 3 | This package contains "Preset" NavMesh generators for @recast-navigation/core that make getting started easy. 4 | 5 | These can also be used as a basis for your own generator. 6 | 7 | Please refer to the [main package's README](https://github.com/isaac-mason/recast-navigation-js/tree/main/packages/recast-navigation/README.md) for usage information. 8 | -------------------------------------------------------------------------------- /packages/recast-navigation-generators/eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import config from '@isaac-mason/eslint-config-typescript' 2 | 3 | export default [...config] 4 | -------------------------------------------------------------------------------- /packages/recast-navigation-generators/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@recast-navigation/generators", 3 | "description": "NavMesh generator implementations for @recast-navigation/core", 4 | "author": { 5 | "name": "Isaac Mason", 6 | "email": "isaac@isaacmason.com", 7 | "url": "https://isaacmason.com/" 8 | }, 9 | "version": "0.39.0", 10 | "license": "MIT", 11 | "homepage": "https://github.com/isaac-mason/recast-navigation-js", 12 | "bugs": { 13 | "url": "https://github.com/isaac-mason/recast-navigation-js/issues" 14 | }, 15 | "type": "module", 16 | "main": "./dist/index.mjs", 17 | "module": "./dist/index.mjs", 18 | "types": "./dist/index.d.ts", 19 | "files": [ 20 | "dist/**", 21 | "README.md", 22 | "LICENSE" 23 | ], 24 | "scripts": { 25 | "build": "rollup --config rollup.config.js --bundleConfigAsCjs", 26 | "test": "tsc", 27 | "lint": "eslint src" 28 | }, 29 | "dependencies": { 30 | "@recast-navigation/core": "0.39.0", 31 | "@recast-navigation/wasm": "0.39.0" 32 | }, 33 | "devDependencies": { 34 | "@babel/core": "^7.24.5", 35 | "@babel/preset-env": "^7.24.5", 36 | "@babel/preset-typescript": "^7.24.1", 37 | "@isaac-mason/eslint-config-typescript": "^0.0.9", 38 | "@rollup/plugin-babel": "^6.0.4", 39 | "@rollup/plugin-commonjs": "^25.0.7", 40 | "@rollup/plugin-node-resolve": "^15.0.1", 41 | "@rollup/plugin-typescript": "^11.1.6", 42 | "eslint": "^9.6.0", 43 | "prettier": "^3.1.0", 44 | "rollup": "^4.14.0", 45 | "rollup-plugin-copy": "^3.4.0", 46 | "rollup-plugin-filesize": "^10.0.0", 47 | "typescript": "^5.4.3" 48 | }, 49 | "packageManager": "yarn@3.2.1" 50 | } 51 | -------------------------------------------------------------------------------- /packages/recast-navigation-generators/rollup.config.js: -------------------------------------------------------------------------------- 1 | import babel from '@rollup/plugin-babel'; 2 | import commonjs from '@rollup/plugin-commonjs'; 3 | import resolve from '@rollup/plugin-node-resolve'; 4 | import typescript from '@rollup/plugin-typescript'; 5 | import path from 'path'; 6 | import filesize from 'rollup-plugin-filesize'; 7 | 8 | const babelOptions = { 9 | babelrc: false, 10 | extensions: ['.ts'], 11 | exclude: '**/node_modules/**', 12 | babelHelpers: 'bundled', 13 | presets: [ 14 | [ 15 | '@babel/preset-env', 16 | { 17 | loose: true, 18 | modules: false, 19 | targets: '>1%, not dead, not ie 11, not op_mini all', 20 | }, 21 | ], 22 | '@babel/preset-typescript', 23 | ], 24 | }; 25 | 26 | export default [ 27 | { 28 | input: './src/index.ts', 29 | external: ['@recast-navigation/core'], 30 | output: [ 31 | { 32 | file: 'dist/index.mjs', 33 | format: 'es', 34 | }, 35 | ], 36 | plugins: [ 37 | resolve(), 38 | commonjs(), 39 | typescript({ 40 | tsconfig: path.resolve(__dirname, 'tsconfig.json'), 41 | emitDeclarationOnly: true, 42 | }), 43 | babel(babelOptions), 44 | filesize(), 45 | ], 46 | }, 47 | ]; 48 | -------------------------------------------------------------------------------- /packages/recast-navigation-generators/src/generators/common.ts: -------------------------------------------------------------------------------- 1 | import { OffMeshConnectionParams, vec3 } from '@recast-navigation/core'; 2 | 3 | export const getBoundingBox = ( 4 | positions: ArrayLike, 5 | indices: ArrayLike 6 | ) => { 7 | const bbMin = { x: Infinity, y: Infinity, z: Infinity }; 8 | const bbMax = { x: -Infinity, y: -Infinity, z: -Infinity }; 9 | 10 | for (let i = 0; i < indices.length; i++) { 11 | const ind = indices[i]; 12 | 13 | const x = positions[ind * 3]; 14 | const y = positions[ind * 3 + 1]; 15 | const z = positions[ind * 3 + 2]; 16 | 17 | bbMin.x = Math.min(bbMin.x, x); 18 | bbMin.y = Math.min(bbMin.y, y); 19 | bbMin.z = Math.min(bbMin.z, z); 20 | 21 | bbMax.x = Math.max(bbMax.x, x); 22 | bbMax.y = Math.max(bbMax.y, y); 23 | bbMax.z = Math.max(bbMax.z, z); 24 | } 25 | 26 | return { 27 | bbMin: vec3.toArray(bbMin), 28 | bbMax: vec3.toArray(bbMax), 29 | }; 30 | }; 31 | 32 | export const dtIlog2 = (v: number) => { 33 | let r = 0; 34 | let shift = 0; 35 | 36 | r = Number(v > 0xffff) << 4; 37 | v >>= r; 38 | 39 | shift = Number(v > 0xff) << 3; 40 | v >>= shift; 41 | r |= shift; 42 | 43 | shift = Number(v > 0xf) << 2; 44 | v >>= shift; 45 | r |= shift; 46 | 47 | shift = Number(v > 0x3) << 1; 48 | v >>= shift; 49 | r |= shift; 50 | r |= v >> 1; 51 | 52 | return r; 53 | }; 54 | 55 | export const dtNextPow2 = (v: number) => { 56 | v--; 57 | v |= v >> 1; 58 | v |= v >> 2; 59 | v |= v >> 4; 60 | v |= v >> 8; 61 | v |= v >> 16; 62 | v++; 63 | return v; 64 | }; 65 | 66 | export type OffMeshConnectionGeneratorParams = { 67 | offMeshConnections?: OffMeshConnectionParams[]; 68 | }; 69 | -------------------------------------------------------------------------------- /packages/recast-navigation-generators/src/generators/index.ts: -------------------------------------------------------------------------------- 1 | export * from './common'; 2 | export * from './generate-solo-nav-mesh'; 3 | export * from './generate-tile-cache'; 4 | export * from './generate-tiled-nav-mesh'; 5 | export * from './merge-positions-and-indices'; 6 | -------------------------------------------------------------------------------- /packages/recast-navigation-generators/src/generators/merge-positions-and-indices.ts: -------------------------------------------------------------------------------- 1 | export const mergePositionsAndIndices = ( 2 | meshes: Array<{ 3 | positions: ArrayLike; 4 | indices: ArrayLike; 5 | }> 6 | ): [Float32Array, Uint32Array] => { 7 | const mergedPositions: number[] = []; 8 | const mergedIndices: number[] = []; 9 | 10 | const positionToIndex: { [hash: string]: number } = {}; 11 | let indexCounter = 0; 12 | 13 | for (const { positions, indices } of meshes) { 14 | for (let i = 0; i < indices.length; i++) { 15 | const pt = indices[i] * 3; 16 | 17 | const x = positions[pt]; 18 | const y = positions[pt + 1]; 19 | const z = positions[pt + 2]; 20 | 21 | const key = `${x}_${y}_${z}`; 22 | let idx = positionToIndex[key]; 23 | 24 | if (!idx) { 25 | positionToIndex[key] = idx = indexCounter; 26 | mergedPositions.push(x, y, z); 27 | indexCounter++; 28 | } 29 | 30 | mergedIndices.push(idx); 31 | } 32 | } 33 | 34 | return [Float32Array.from(mergedPositions), Uint32Array.from(mergedIndices)]; 35 | }; 36 | -------------------------------------------------------------------------------- /packages/recast-navigation-generators/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './generators'; 2 | -------------------------------------------------------------------------------- /packages/recast-navigation-generators/src/types.ts: -------------------------------------------------------------------------------- 1 | export type Pretty = T extends unknown ? { [K in keyof T]: T[K] } : never; 2 | -------------------------------------------------------------------------------- /packages/recast-navigation-generators/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2022", 4 | "useDefineForClassFields": true, 5 | "module": "ES2022", 6 | "lib": ["ES2022", "DOM"], 7 | "moduleResolution": "Bundler", 8 | "strict": true, 9 | "resolveJsonModule": true, 10 | "isolatedModules": true, 11 | "esModuleInterop": true, 12 | "noEmit": true, 13 | "declaration": true, 14 | "noUnusedLocals": true, 15 | "noUnusedParameters": true, 16 | "noImplicitReturns": true, 17 | "skipLibCheck": true, 18 | "baseUrl": "./", 19 | "rootDir": "./src", 20 | "allowSyntheticDefaultImports": true, 21 | "outDir": "./dist" 22 | }, 23 | "files": ["./src/index.ts"] 24 | } 25 | -------------------------------------------------------------------------------- /packages/recast-navigation-playcanvas/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | 26 | # jest coverage 27 | coverage 28 | -------------------------------------------------------------------------------- /packages/recast-navigation-playcanvas/.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "es5", 3 | "tabWidth": 2, 4 | "semi": true, 5 | "singleQuote": true 6 | } 7 | -------------------------------------------------------------------------------- /packages/recast-navigation-playcanvas/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # @recast-navigation/playcanvas 2 | 3 | ## 0.39.0 4 | 5 | ### Minor Changes 6 | 7 | - da46c6c: feat: remove OffMeshConnectionsHelper in favour of DebugDrawer off mesh connection debug drawing 8 | 9 | ### Patch Changes 10 | 11 | - Updated dependencies [fe172f7] 12 | - Updated dependencies [fe172f7] 13 | - @recast-navigation/core@0.39.0 14 | - @recast-navigation/generators@0.39.0 15 | 16 | ## 0.38.0 17 | 18 | ### Patch Changes 19 | 20 | - Updated dependencies [b54ae16] 21 | - Updated dependencies [93d383e] 22 | - @recast-navigation/generators@0.38.0 23 | - @recast-navigation/core@0.38.0 24 | 25 | ## 0.37.0 26 | 27 | ### Patch Changes 28 | 29 | - Updated dependencies [f8968fc] 30 | - @recast-navigation/generators@0.37.0 31 | - @recast-navigation/core@0.37.0 32 | 33 | ## 0.36.1 34 | 35 | ### Patch Changes 36 | 37 | - @recast-navigation/core@0.36.1 38 | - @recast-navigation/generators@0.36.1 39 | 40 | ## 0.36.0 41 | 42 | ### Patch Changes 43 | 44 | - Updated dependencies [753e4e2] 45 | - @recast-navigation/core@0.36.0 46 | - @recast-navigation/generators@0.36.0 47 | 48 | ## 0.35.2 49 | 50 | ### Patch Changes 51 | 52 | - @recast-navigation/core@0.35.2 53 | - @recast-navigation/generators@0.35.2 54 | 55 | ## 0.35.1 56 | 57 | ### Patch Changes 58 | 59 | - 4574e65: feat: remove terser, ship human readable javascript to npm 60 | - Updated dependencies [4574e65] 61 | - @recast-navigation/core@0.35.1 62 | - @recast-navigation/generators@0.35.1 63 | 64 | ## 0.35.0 65 | 66 | ### Patch Changes 67 | 68 | - Updated dependencies [4e63338] 69 | - @recast-navigation/generators@0.35.0 70 | - @recast-navigation/core@0.35.0 71 | 72 | ## 1.0.0 73 | 74 | - Initial release! 75 | -------------------------------------------------------------------------------- /packages/recast-navigation-playcanvas/LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | Copyright © 2025 Isaac Mason 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /packages/recast-navigation-playcanvas/README.md: -------------------------------------------------------------------------------- 1 | # @recast-navigation/playcanvas 2 | 3 | PlayCanvas nav mesh generation and visualisation helpers for [`recast-navigation`](https://github.com/isaac-mason/recast-navigation-js/tree/main/packages/recast-navigation). 4 | 5 | ## Installation 6 | 7 | ```bash 8 | > npm install @recast-navigation/playcanvas recast-navigation 9 | ``` 10 | 11 | ## Usage 12 | 13 | ### Importing 14 | 15 | To import the PlayCanvas glue, you can either use the `playcanvas` entrypoint in `recast-navigation`: 16 | 17 | ```ts 18 | import { init } from 'recast-navigation' 19 | import { ... } from 'recast-navigation/playcanvas'; 20 | ``` 21 | 22 | Or you can use the packages directly: 23 | 24 | ```ts 25 | import { init } from '@recast-navigation/core' 26 | import { ... } from '@recast-navigation/playcanvas'; 27 | ``` 28 | 29 | ### Initialization 30 | 31 | Before you can use the library, you must initialize it. This is an asynchronous operation. 32 | 33 | Calling `init()` after the library has already been initialized will return a promise that resolves immediately. 34 | 35 | ```ts 36 | import { init } from 'recast-navigation'; 37 | 38 | await init(); 39 | ``` 40 | 41 | ### Generating a NavMesh 42 | 43 | This package provides convenience functions for generating nav meshes from playcanvas MeshInstance objects. 44 | 45 | ```ts 46 | import { init } from 'recast-navigation'; 47 | import { pcToSoloNavMesh, pcToTiledNavMesh, pcToTileCache } from 'recast-navigation/playcanvas'; 48 | 49 | /* initialize the library */ 50 | await init(); 51 | 52 | /* generate a solo navmesh */ 53 | const { success, navMesh } = pcToSoloNavMesh(meshInstances, { 54 | // ... nav mesh generation config ... 55 | }}); 56 | 57 | /* generate a tiled navmesh */ 58 | const { success, navMesh } = pcToTiledNavMesh(meshInstances, { 59 | tileSize: 16, 60 | // ... nav mesh generation config ... 61 | }}); 62 | 63 | /* generate a tile cache with support for temporary obstacles */ 64 | const { success, navMesh, tileCache } = pcToTileCache(meshInstances, { 65 | tileSize: 16, 66 | // ... nav mesh generation config ... 67 | }}); 68 | ``` 69 | 70 | ### Interacting with a NavMesh 71 | 72 | You can documentation for interacting with the generated navmesh in the core library README: 73 | 74 | https://github.com/isaac-mason/recast-navigation-js 75 | 76 | This library provides helpers that are used in conjunction with the core library. 77 | 78 | ### Helpers 79 | 80 | This package provides helpers for visualizing various recast-navigation objects in playcanvas. 81 | 82 | #### `NavMeshHelper` 83 | 84 | ```ts 85 | import { NavMeshHelper } from '@recast-navigation/playcanvas'; 86 | 87 | const navMeshHelper = new NavMeshHelper(navMesh, graphicsDevice); 88 | 89 | this.entity.add(navMeshHelper); 90 | 91 | // update the helper when the navmesh changes 92 | navMeshHelper.update(); 93 | ``` 94 | 95 | #### `TileCacheHelper` 96 | 97 | Visualises obstacles in a `TileCache`. 98 | 99 | ```ts 100 | import { TileCacheHelper } from '@recast-navigation/playcanvas'; 101 | 102 | const tileCacheHelper = new TileCacheHelper(tileCache); 103 | 104 | this.entity.add(tileCacheHelper); 105 | 106 | // update the helper after adding or removing obstacles 107 | tileCacheHelper.update(); 108 | ``` 109 | 110 | #### `CrowdHelper` 111 | 112 | Visualises agents in a `Crowd`. 113 | 114 | ```ts 115 | import { CrowdHelper } from '@recast-navigation/playcanvas'; 116 | 117 | const crowdHelper = new CrowdHelper(crowd, graphicsDevice); 118 | 119 | this.entity.add(crowdHelper); 120 | 121 | // update the helper after updating the crowd 122 | crowdHelper.update(); 123 | ``` 124 | -------------------------------------------------------------------------------- /packages/recast-navigation-playcanvas/eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import config from '@isaac-mason/eslint-config-typescript' 2 | 3 | export default [...config] 4 | -------------------------------------------------------------------------------- /packages/recast-navigation-playcanvas/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@recast-navigation/playcanvas", 3 | "description": "Utilities for using recast-navigation with PlayCanvas", 4 | "version": "0.39.0", 5 | "license": "MIT", 6 | "homepage": "https://github.com/isaac-mason/recast-navigation-js", 7 | "bugs": { 8 | "url": "https://github.com/isaac-mason/recast-navigation-js/issues" 9 | }, 10 | "type": "module", 11 | "main": "./dist/index.mjs", 12 | "module": "./dist/index.mjs", 13 | "types": "./dist/index.d.ts", 14 | "files": [ 15 | "dist/**", 16 | "README.md", 17 | "LICENSE" 18 | ], 19 | "scripts": { 20 | "build": "rollup --config rollup.config.js --bundleConfigAsCjs", 21 | "test": "tsc", 22 | "lint": "eslint src" 23 | }, 24 | "dependencies": { 25 | "@recast-navigation/core": "0.39.0", 26 | "@recast-navigation/generators": "0.39.0" 27 | }, 28 | "devDependencies": { 29 | "@babel/core": "^7.24.5", 30 | "@babel/preset-env": "^7.24.5", 31 | "@babel/preset-typescript": "^7.24.1", 32 | "@isaac-mason/eslint-config-typescript": "^0.0.9", 33 | "@rollup/plugin-babel": "^6.0.4", 34 | "@rollup/plugin-commonjs": "^25.0.7", 35 | "@rollup/plugin-node-resolve": "^15.0.1", 36 | "@rollup/plugin-typescript": "^11.1.6", 37 | "eslint": "^9.6.0", 38 | "playcanvas": "^2.0.0", 39 | "prettier": "^3.1.0", 40 | "rollup": "^4.14.0", 41 | "rollup-plugin-copy": "^3.4.0", 42 | "rollup-plugin-filesize": "^10.0.0", 43 | "typescript": "^5.4.3" 44 | }, 45 | "packageManager": "yarn@3.2.1", 46 | "peerDependencies": { 47 | "playcanvas": "^2.0.0" 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /packages/recast-navigation-playcanvas/rollup.config.js: -------------------------------------------------------------------------------- 1 | import babel from '@rollup/plugin-babel'; 2 | import commonjs from '@rollup/plugin-commonjs'; 3 | import resolve from '@rollup/plugin-node-resolve'; 4 | import typescript from '@rollup/plugin-typescript'; 5 | import path from 'path'; 6 | import filesize from 'rollup-plugin-filesize'; 7 | 8 | const babelOptions = { 9 | babelrc: false, 10 | extensions: ['.ts'], 11 | exclude: '**/node_modules/**', 12 | babelHelpers: 'bundled', 13 | presets: [ 14 | [ 15 | '@babel/preset-env', 16 | { 17 | loose: true, 18 | modules: false, 19 | targets: '>1%, not dead, not ie 11, not op_mini all', 20 | }, 21 | ], 22 | '@babel/preset-typescript', 23 | ], 24 | }; 25 | 26 | export default [ 27 | { 28 | input: './src/index.ts', 29 | external: [ 30 | '@recast-navigation/core', 31 | '@recast-navigation/generators', 32 | 'playcanvas', 33 | ], 34 | output: [ 35 | { 36 | file: 'dist/index.mjs', 37 | format: 'es', 38 | }, 39 | ], 40 | plugins: [ 41 | resolve(), 42 | commonjs(), 43 | typescript({ 44 | tsconfig: path.resolve(__dirname, 'tsconfig.json'), 45 | emitDeclarationOnly: true, 46 | }), 47 | babel(babelOptions), 48 | filesize(), 49 | ], 50 | }, 51 | ]; 52 | -------------------------------------------------------------------------------- /packages/recast-navigation-playcanvas/src/debug/index.ts: -------------------------------------------------------------------------------- 1 | export * from './debug-drawer'; 2 | -------------------------------------------------------------------------------- /packages/recast-navigation-playcanvas/src/helpers/index.ts: -------------------------------------------------------------------------------- 1 | export * from './crowd-helper'; 2 | export * from './nav-mesh-helper'; 3 | export * from './tile-cache-helper'; 4 | -------------------------------------------------------------------------------- /packages/recast-navigation-playcanvas/src/helpers/nav-mesh-helper.ts: -------------------------------------------------------------------------------- 1 | import { NavMesh, getNavMeshPositionsAndIndices } from '@recast-navigation/core'; 2 | import { 3 | BLEND_NORMAL, 4 | Entity, 5 | GraphicsDevice, 6 | Material, 7 | Mesh, 8 | MeshInstance, 9 | StandardMaterial, 10 | calculateNormals, 11 | } from 'playcanvas'; 12 | 13 | export type NavMeshHelperParams = { 14 | navMeshMaterial?: Material; 15 | }; 16 | 17 | /** 18 | * NavMeshHelper class for visualizing the nav mesh in PlayCanvas 19 | */ 20 | export class NavMeshHelper extends Entity { 21 | navMesh: NavMesh; 22 | 23 | navMeshMaterial: Material; 24 | 25 | mesh!: Mesh; 26 | 27 | constructor( 28 | navMesh: NavMesh, 29 | graphicsDevice: GraphicsDevice, 30 | params?: NavMeshHelperParams 31 | ) { 32 | super(); 33 | 34 | this.navMesh = navMesh; 35 | 36 | // Create material if not provided 37 | this.navMeshMaterial = 38 | params?.navMeshMaterial || this.createDefaultMaterial(); 39 | 40 | // Update the mesh 41 | this.updateMesh(graphicsDevice); 42 | 43 | // Create a MeshInstance with the mesh and material 44 | const meshInstance: MeshInstance = new MeshInstance( 45 | this.mesh, 46 | this.navMeshMaterial 47 | ); 48 | 49 | // Add a render component and assign the mesh instance 50 | this.addComponent('render', { 51 | meshInstances: [meshInstance], 52 | }); 53 | } 54 | 55 | /** 56 | * Creates a default material for the nav mesh visualization 57 | * @private 58 | */ 59 | createDefaultMaterial() { 60 | const material: StandardMaterial = new StandardMaterial(); 61 | material.diffuse.set(1, 0.65, 0); 62 | material.opacity = 0.7; 63 | material.blendType = BLEND_NORMAL; 64 | material.depthWrite = false; 65 | material.update(); 66 | return material; 67 | } 68 | 69 | /** 70 | * Updates the mesh using the nav mesh data 71 | * @private 72 | */ 73 | updateMesh(graphicsDevice: GraphicsDevice) { 74 | const [positions, indices] = getNavMeshPositionsAndIndices(this.navMesh); 75 | const normals = calculateNormals(positions, indices); 76 | 77 | // Create the mesh 78 | this.mesh = new Mesh(graphicsDevice); 79 | this.mesh.setPositions(positions); 80 | this.mesh.setIndices(indices); 81 | this.mesh.setNormals(normals); 82 | this.mesh.update(); 83 | } 84 | 85 | /** 86 | * Update the PlayCanvas nav mesh visualization. 87 | * 88 | * This should be called after updating the nav mesh. 89 | */ 90 | update() { 91 | // Re-create the mesh 92 | const graphicsDevice: GraphicsDevice = this.mesh.vertexBuffer.device; 93 | this.updateMesh(graphicsDevice); 94 | 95 | // Update the mesh instance with the new mesh 96 | this.render!.meshInstances[0].mesh = this.mesh; 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /packages/recast-navigation-playcanvas/src/helpers/tile-cache-helper.ts: -------------------------------------------------------------------------------- 1 | import { Obstacle, TileCache } from '@recast-navigation/core'; 2 | import { 3 | Color, 4 | Entity, 5 | Material, 6 | RENDERSTYLE_WIREFRAME, 7 | StandardMaterial, 8 | Vec3, 9 | } from 'playcanvas'; 10 | 11 | /** 12 | * Parameters for creating TileCacheHelper. 13 | */ 14 | export type TileCacheHelperParams = { 15 | obstacleMaterial?: Material; 16 | }; 17 | 18 | /** 19 | * Represents a helper class to visualize tile cache obstacles in PlayCanvas. 20 | */ 21 | export class TileCacheHelper extends Entity { 22 | tileCache: TileCache; 23 | obstacleMeshes = new Map(); 24 | obstacleMaterial: Material; 25 | 26 | constructor(tileCache: TileCache, params?: TileCacheHelperParams) { 27 | super(); 28 | 29 | this.tileCache = tileCache; 30 | 31 | // Initialize obstacleMaterial 32 | if (params?.obstacleMaterial) { 33 | this.obstacleMaterial = params.obstacleMaterial; 34 | } else { 35 | const material = new StandardMaterial(); 36 | material.diffuse = new Color(1, 0, 0); // Red color 37 | material.update(); 38 | this.obstacleMaterial = material; 39 | } 40 | 41 | this.updateHelper(); 42 | } 43 | 44 | /** 45 | * Update the obstacle meshes. 46 | * 47 | * This should be called after adding or removing obstacles. 48 | */ 49 | updateHelper() { 50 | const unseen = new Set(this.obstacleMeshes.keys()); 51 | 52 | for (const obstacle of this.tileCache.obstacles.values()) { 53 | let obstacleEntity = this.obstacleMeshes.get(obstacle); 54 | 55 | unseen.delete(obstacle); 56 | 57 | if (!obstacleEntity) { 58 | const { position } = obstacle; 59 | 60 | obstacleEntity = new Entity(); 61 | obstacleEntity.setPosition( 62 | new Vec3(position.x, position.y, position.z) 63 | ); 64 | 65 | if (obstacle.type === 'box') { 66 | const { halfExtents, angle } = obstacle; 67 | 68 | obstacleEntity.addComponent('render', { 69 | type: 'box', 70 | material: this.obstacleMaterial, 71 | }); 72 | 73 | obstacleEntity.setLocalScale( 74 | halfExtents.x * 2, 75 | halfExtents.y * 2, 76 | halfExtents.z * 2 77 | ); 78 | 79 | obstacleEntity.setEulerAngles(0, angle * (180 / Math.PI), 0); 80 | } else if (obstacle.type === 'cylinder') { 81 | const { radius, height } = obstacle; 82 | 83 | obstacleEntity.addComponent('render', { 84 | type: 'cylinder', 85 | material: this.obstacleMaterial, 86 | }); 87 | 88 | obstacleEntity.setLocalScale(radius * 2, height, radius * 2); 89 | obstacleEntity.translateLocal(0, height / 2, 0); 90 | } else { 91 | throw new Error( 92 | `Unknown obstacle type: ${(obstacle as Obstacle).type}` 93 | ); 94 | } 95 | 96 | // Set render style to wireframe 97 | if (obstacleEntity.render) { 98 | obstacleEntity.render.meshInstances.forEach((meshInstance) => { 99 | meshInstance.renderStyle = RENDERSTYLE_WIREFRAME; 100 | }); 101 | } 102 | 103 | this.addChild(obstacleEntity); 104 | this.obstacleMeshes.set(obstacle, obstacleEntity); 105 | } 106 | } 107 | 108 | for (const obstacle of unseen) { 109 | const obstacleEntity = this.obstacleMeshes.get(obstacle); 110 | 111 | if (obstacleEntity) { 112 | obstacleEntity.destroy(); 113 | this.obstacleMeshes.delete(obstacle); 114 | } 115 | } 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /packages/recast-navigation-playcanvas/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './debug'; 2 | export * from './helpers'; 3 | export * from './utils'; 4 | -------------------------------------------------------------------------------- /packages/recast-navigation-playcanvas/src/utils/generators.ts: -------------------------------------------------------------------------------- 1 | import { 2 | SoloNavMeshGeneratorConfig, 3 | TileCacheGeneratorConfig, 4 | TiledNavMeshGeneratorConfig, 5 | generateSoloNavMesh, 6 | generateTileCache, 7 | generateTiledNavMesh, 8 | } from '@recast-navigation/generators'; 9 | import { MeshInstance } from 'playcanvas'; 10 | import { getPositionsAndIndices } from './get-positions-and-indices'; 11 | 12 | export const pcToSoloNavMesh = ( 13 | meshes: MeshInstance[], 14 | navMeshGeneratorConfig: Partial = {}, 15 | keepIntermediates = false 16 | ) => { 17 | const [positions, indices] = getPositionsAndIndices(meshes); 18 | 19 | return generateSoloNavMesh( 20 | positions, 21 | indices, 22 | navMeshGeneratorConfig, 23 | keepIntermediates 24 | ); 25 | }; 26 | 27 | export const pcToTiledNavMesh = ( 28 | meshes: MeshInstance[], 29 | navMeshGeneratorConfig: Partial = {}, 30 | keepIntermediates = false 31 | ) => { 32 | const [positions, indices] = getPositionsAndIndices(meshes); 33 | 34 | return generateTiledNavMesh( 35 | positions, 36 | indices, 37 | navMeshGeneratorConfig, 38 | keepIntermediates 39 | ); 40 | }; 41 | 42 | export const threeToTileCache = ( 43 | meshes: MeshInstance[], 44 | navMeshGeneratorConfig: Partial = {}, 45 | keepIntermediates = false 46 | ) => { 47 | const [positions, indices] = getPositionsAndIndices(meshes); 48 | 49 | return generateTileCache( 50 | positions, 51 | indices, 52 | navMeshGeneratorConfig, 53 | keepIntermediates 54 | ); 55 | }; 56 | -------------------------------------------------------------------------------- /packages/recast-navigation-playcanvas/src/utils/get-positions-and-indices.ts: -------------------------------------------------------------------------------- 1 | import { mergePositionsAndIndices } from '@recast-navigation/generators'; 2 | import { 3 | INDEXFORMAT_UINT32, 4 | Mat4, 5 | Mesh, 6 | MeshInstance, 7 | SEMANTIC_POSITION, 8 | Vec3, 9 | VertexBuffer, 10 | VertexFormat, 11 | VertexIterator, 12 | } from 'playcanvas'; 13 | 14 | const _vector3: Vec3 = new Vec3(); 15 | 16 | export const getPositionsAndIndices = ( 17 | meshInstances: MeshInstance[] 18 | ): [Float32Array, Uint32Array] => { 19 | const toMerge: { 20 | positions: ArrayLike; 21 | indices: ArrayLike; 22 | }[] = []; 23 | 24 | for (const meshInstance of meshInstances) { 25 | const mesh: Mesh = meshInstance.mesh; 26 | 27 | const vertexBuffer: VertexBuffer = mesh.vertexBuffer; 28 | if (!vertexBuffer) { 29 | continue; 30 | } 31 | 32 | const vertexFormat: VertexFormat = vertexBuffer.getFormat(); 33 | const positionElement = vertexFormat.elements.find( 34 | (element) => element.name === SEMANTIC_POSITION 35 | ); 36 | 37 | if (!positionElement || positionElement.numComponents !== 3) { 38 | continue; 39 | } 40 | 41 | const worldMatrix: Mat4 = meshInstance.node.getWorldTransform(); 42 | 43 | const positions = new Float32Array(vertexBuffer.getNumVertices() * 3); 44 | const iterator = new VertexIterator(vertexBuffer); 45 | iterator.readData(SEMANTIC_POSITION, positions); 46 | 47 | for (let i: number = 0; i < positions.length; i += 3) { 48 | _vector3.set(positions[i], positions[i + 1], positions[i + 2]); 49 | 50 | worldMatrix.transformPoint(_vector3, _vector3); 51 | 52 | positions[i] = _vector3.x; 53 | positions[i + 1] = _vector3.y; 54 | positions[i + 2] = _vector3.z; 55 | } 56 | 57 | let indices: ArrayLike | undefined; 58 | 59 | if (mesh.indexBuffer[0]) { 60 | const indexBuffer = mesh.indexBuffer[0]; 61 | const indexFormat = 62 | indexBuffer.getFormat() === INDEXFORMAT_UINT32 63 | ? Uint32Array 64 | : Uint16Array; 65 | const indicesCopy = new indexFormat(indexBuffer.getNumIndices()); 66 | const originalIndices = new indexFormat(indexBuffer.storage); 67 | indicesCopy.set(originalIndices); 68 | indices = indicesCopy; 69 | } else { 70 | // this will become indexed when merging with other meshes 71 | const ascendingIndex: number[] = []; 72 | for (let i = 0; i < vertexBuffer.getNumVertices(); i++) { 73 | ascendingIndex.push(i); 74 | } 75 | indices = ascendingIndex; 76 | } 77 | 78 | toMerge.push({ 79 | positions, 80 | indices, 81 | }); 82 | } 83 | 84 | return mergePositionsAndIndices(toMerge); 85 | }; 86 | -------------------------------------------------------------------------------- /packages/recast-navigation-playcanvas/src/utils/index.ts: -------------------------------------------------------------------------------- 1 | export * from './generators'; 2 | export * from './get-positions-and-indices'; 3 | -------------------------------------------------------------------------------- /packages/recast-navigation-playcanvas/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2022", 4 | "useDefineForClassFields": true, 5 | "module": "ES2022", 6 | "lib": ["ES2022", "DOM"], 7 | "moduleResolution": "Bundler", 8 | "strict": true, 9 | "resolveJsonModule": true, 10 | "isolatedModules": true, 11 | "esModuleInterop": true, 12 | "noEmit": true, 13 | "declaration": true, 14 | "noUnusedLocals": true, 15 | "noUnusedParameters": true, 16 | "noImplicitReturns": true, 17 | "skipLibCheck": true, 18 | "baseUrl": "./", 19 | "rootDir": "./src", 20 | "allowSyntheticDefaultImports": true, 21 | "outDir": "./dist" 22 | }, 23 | "files": ["./src/index.ts"] 24 | } 25 | -------------------------------------------------------------------------------- /packages/recast-navigation-three/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | 26 | # jest coverage 27 | coverage 28 | -------------------------------------------------------------------------------- /packages/recast-navigation-three/.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "es5", 3 | "tabWidth": 2, 4 | "semi": true, 5 | "singleQuote": true 6 | } 7 | -------------------------------------------------------------------------------- /packages/recast-navigation-three/LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | Copyright © 2025 Isaac Mason 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /packages/recast-navigation-three/eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import config from '@isaac-mason/eslint-config-typescript' 2 | 3 | export default [...config] 4 | -------------------------------------------------------------------------------- /packages/recast-navigation-three/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@recast-navigation/three", 3 | "description": "Utilities for using recast-navigation with Three.js", 4 | "author": { 5 | "name": "Isaac Mason", 6 | "email": "isaac@isaacmason.com", 7 | "url": "https://isaacmason.com/" 8 | }, 9 | "version": "0.39.0", 10 | "license": "MIT", 11 | "homepage": "https://github.com/isaac-mason/recast-navigation-js", 12 | "bugs": { 13 | "url": "https://github.com/isaac-mason/recast-navigation-js/issues" 14 | }, 15 | "type": "module", 16 | "main": "./dist/index.mjs", 17 | "module": "./dist/index.mjs", 18 | "types": "./dist/index.d.ts", 19 | "files": [ 20 | "dist/**", 21 | "README.md", 22 | "LICENSE" 23 | ], 24 | "scripts": { 25 | "build": "rollup --config rollup.config.js --bundleConfigAsCjs", 26 | "test": "tsc", 27 | "lint": "eslint src" 28 | }, 29 | "dependencies": { 30 | "@recast-navigation/core": "0.39.0", 31 | "@recast-navigation/generators": "0.39.0" 32 | }, 33 | "peerDependencies": { 34 | "@types/three": "0.x.x", 35 | "three": "0.x.x" 36 | }, 37 | "devDependencies": { 38 | "@babel/core": "^7.24.5", 39 | "@babel/preset-env": "^7.24.5", 40 | "@babel/preset-typescript": "^7.24.1", 41 | "@isaac-mason/eslint-config-typescript": "^0.0.9", 42 | "@rollup/plugin-babel": "^6.0.4", 43 | "@rollup/plugin-commonjs": "^25.0.7", 44 | "@rollup/plugin-node-resolve": "^15.0.1", 45 | "@rollup/plugin-typescript": "^11.1.6", 46 | "@types/three": "^0.172.0", 47 | "eslint": "^9.6.0", 48 | "prettier": "^3.1.0", 49 | "rollup": "^4.14.0", 50 | "rollup-plugin-copy": "^3.4.0", 51 | "rollup-plugin-filesize": "^10.0.0", 52 | "three": "^0.172.0", 53 | "typescript": "^5.4.3" 54 | }, 55 | "packageManager": "yarn@3.2.1" 56 | } 57 | -------------------------------------------------------------------------------- /packages/recast-navigation-three/rollup.config.js: -------------------------------------------------------------------------------- 1 | import babel from '@rollup/plugin-babel'; 2 | import commonjs from '@rollup/plugin-commonjs'; 3 | import resolve from '@rollup/plugin-node-resolve'; 4 | import typescript from '@rollup/plugin-typescript'; 5 | import path from 'path'; 6 | import filesize from 'rollup-plugin-filesize'; 7 | 8 | const babelOptions = { 9 | babelrc: false, 10 | extensions: ['.ts'], 11 | exclude: '**/node_modules/**', 12 | babelHelpers: 'bundled', 13 | presets: [ 14 | [ 15 | '@babel/preset-env', 16 | { 17 | loose: true, 18 | modules: false, 19 | targets: '>1%, not dead, not ie 11, not op_mini all', 20 | }, 21 | ], 22 | '@babel/preset-typescript', 23 | ], 24 | }; 25 | 26 | export default [ 27 | { 28 | input: `./src/index.ts`, 29 | external: [ 30 | '@recast-navigation/core', 31 | '@recast-navigation/generators', 32 | 'three', 33 | /three\/addons\/.*/ 34 | 35 | ], 36 | output: [ 37 | { 38 | file: 'dist/index.mjs', 39 | format: 'es', 40 | }, 41 | ], 42 | plugins: [ 43 | resolve(), 44 | commonjs(), 45 | typescript({ 46 | tsconfig: path.resolve(__dirname, 'tsconfig.json'), 47 | emitDeclarationOnly: true, 48 | }), 49 | babel(babelOptions), 50 | filesize(), 51 | ], 52 | onwarn(warning, warn) { 53 | // suppress eval warnings 54 | if (warning.code === 'EVAL') return; 55 | warn(warning); 56 | }, 57 | }, 58 | ]; 59 | -------------------------------------------------------------------------------- /packages/recast-navigation-three/src/debug/index.ts: -------------------------------------------------------------------------------- 1 | export * from './debug-drawer'; 2 | -------------------------------------------------------------------------------- /packages/recast-navigation-three/src/helpers/crowd-helper.ts: -------------------------------------------------------------------------------- 1 | import { Crowd, CrowdAgent } from '@recast-navigation/core'; 2 | import { 3 | CylinderGeometry, 4 | Material, 5 | Mesh, 6 | MeshBasicMaterial, 7 | Object3D, 8 | Vector3, 9 | } from 'three'; 10 | 11 | export type CrowdHelperParams = { 12 | agentMaterial?: Material; 13 | }; 14 | 15 | export class CrowdHelper extends Object3D { 16 | agentMeshes: Map = new Map(); 17 | 18 | recastCrowd: Crowd; 19 | 20 | agentMaterial: Material; 21 | 22 | constructor(crowd: Crowd, params?: CrowdHelperParams) { 23 | super(); 24 | 25 | this.recastCrowd = crowd; 26 | 27 | this.agentMaterial = 28 | params?.agentMaterial ?? new MeshBasicMaterial({ color: 'red' }); 29 | 30 | this.update(); 31 | } 32 | 33 | /** 34 | * Update the three debug view of the crowd agents. 35 | * 36 | * This should be called after updating the crowd. 37 | */ 38 | update() { 39 | const agents = this.recastCrowd.getAgents(); 40 | 41 | const unseen = new Set(this.agentMeshes.keys()); 42 | 43 | for (const agent of agents) { 44 | unseen.delete(agent.agentIndex); 45 | 46 | const position = agent.position(); 47 | const velocity = agent.velocity(); 48 | 49 | let agentMesh: Mesh | undefined = this.agentMeshes.get(agent.agentIndex); 50 | 51 | if (agentMesh === undefined) { 52 | agentMesh = this.createAgentMesh(agent); 53 | 54 | this.add(agentMesh); 55 | this.agentMeshes.set(agent.agentIndex, agentMesh); 56 | } else { 57 | this.updateAgentGeometry(agentMesh, agent); 58 | } 59 | 60 | agentMesh.position.set( 61 | position.x, 62 | position.y + agent.height / 2, 63 | position.z 64 | ); 65 | 66 | agentMesh.lookAt( 67 | new Vector3().copy(agentMesh.position).add(velocity as Vector3) 68 | ); 69 | } 70 | 71 | for (const agentId of unseen) { 72 | const agentMesh = this.agentMeshes.get(agentId); 73 | 74 | if (agentMesh) { 75 | this.remove(agentMesh); 76 | this.agentMeshes.delete(agentId); 77 | } 78 | } 79 | } 80 | 81 | private createAgentMesh(agent: CrowdAgent): Mesh { 82 | const mesh = new Mesh(); 83 | 84 | mesh.material = this.agentMaterial; 85 | 86 | this.updateAgentGeometry(mesh, agent); 87 | 88 | mesh.userData = { 89 | radius: agent.radius, 90 | height: agent.height, 91 | }; 92 | 93 | return mesh; 94 | } 95 | 96 | private updateAgentGeometry(agentMesh: Mesh, agentParams: CrowdAgent) { 97 | if ( 98 | agentMesh.userData.radius !== agentParams.radius || 99 | agentMesh.userData.height !== agentParams.height 100 | ) { 101 | const geometry = new CylinderGeometry( 102 | agentParams.radius, 103 | agentParams.radius, 104 | agentParams.height 105 | ); 106 | 107 | agentMesh.geometry.dispose(); 108 | agentMesh.geometry = geometry; 109 | 110 | agentMesh.userData.radius = agentParams.radius; 111 | agentMesh.userData.height = agentParams.height; 112 | } 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /packages/recast-navigation-three/src/helpers/index.ts: -------------------------------------------------------------------------------- 1 | export * from './crowd-helper'; 2 | export * from './nav-mesh-helper'; 3 | export * from './tile-cache-helper'; 4 | -------------------------------------------------------------------------------- /packages/recast-navigation-three/src/helpers/nav-mesh-helper.ts: -------------------------------------------------------------------------------- 1 | import { getNavMeshPositionsAndIndices, NavMesh } from '@recast-navigation/core'; 2 | import { 3 | BufferAttribute, 4 | BufferGeometry, 5 | Material, 6 | Mesh, 7 | MeshBasicMaterial, 8 | Object3D, 9 | } from 'three'; 10 | 11 | export type NavMeshHelperParams = { 12 | navMeshMaterial?: Material; 13 | }; 14 | 15 | export class NavMeshHelper extends Object3D { 16 | navMesh: NavMesh; 17 | 18 | mesh: Mesh; 19 | 20 | navMeshMaterial: Material; 21 | 22 | navMeshGeometry: BufferGeometry; 23 | 24 | constructor(navMesh: NavMesh, params?: NavMeshHelperParams) { 25 | super(); 26 | 27 | this.navMesh = navMesh; 28 | 29 | this.navMeshGeometry = new BufferGeometry(); 30 | 31 | this.navMeshMaterial = params?.navMeshMaterial 32 | ? params.navMeshMaterial 33 | : new MeshBasicMaterial({ 34 | color: 'orange', 35 | transparent: true, 36 | opacity: 0.7, 37 | depthWrite: false, 38 | }); 39 | 40 | this.update(); 41 | 42 | this.mesh = new Mesh(this.navMeshGeometry, this.navMeshMaterial); 43 | 44 | this.add(this.mesh); 45 | } 46 | 47 | /** 48 | * Update the three debug nav mesh. 49 | * 50 | * This should be called after updating the nav mesh. 51 | */ 52 | update() { 53 | const [positions, indices] = getNavMeshPositionsAndIndices(this.navMesh); 54 | 55 | this.navMeshGeometry.setAttribute( 56 | 'position', 57 | new BufferAttribute(Float32Array.from(positions), 3) 58 | ); 59 | this.navMeshGeometry.setIndex( 60 | new BufferAttribute(Uint32Array.from(indices), 1) 61 | ); 62 | this.navMeshGeometry.computeVertexNormals(); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /packages/recast-navigation-three/src/helpers/tile-cache-helper.ts: -------------------------------------------------------------------------------- 1 | import { Obstacle, TileCache } from '@recast-navigation/core'; 2 | import { 3 | BoxGeometry, 4 | CylinderGeometry, 5 | Material, 6 | Mesh, 7 | MeshBasicMaterial, 8 | Object3D, 9 | Vector3, 10 | } from 'three'; 11 | 12 | export type TileCacheHelperParams = { 13 | obstacleMaterial?: Material; 14 | }; 15 | 16 | export class TileCacheHelper extends Object3D { 17 | tileCache: TileCache; 18 | 19 | obstacleMeshes: Map = new Map(); 20 | 21 | obstacleMaterial: Material; 22 | 23 | constructor(tileCache: TileCache, params?: TileCacheHelperParams) { 24 | super(); 25 | 26 | this.tileCache = tileCache; 27 | 28 | this.obstacleMaterial = params?.obstacleMaterial 29 | ? params.obstacleMaterial 30 | : new MeshBasicMaterial({ 31 | color: 'red', 32 | wireframe: true, 33 | wireframeLinewidth: 2, 34 | }); 35 | 36 | this.update(); 37 | } 38 | 39 | /** 40 | * Update the obstacle meshes. 41 | * 42 | * This should be called after adding or removing obstacles. 43 | */ 44 | update() { 45 | const unseen = new Set(this.obstacleMeshes.keys()); 46 | 47 | for (const [, obstacle] of this.tileCache.obstacles) { 48 | const obstacleMesh = this.obstacleMeshes.get(obstacle); 49 | 50 | unseen.delete(obstacle); 51 | 52 | if (!obstacleMesh) { 53 | const { position } = obstacle; 54 | 55 | const mesh = new Mesh(undefined, this.obstacleMaterial); 56 | 57 | mesh.position.copy(position as Vector3); 58 | 59 | if (obstacle.type === 'box') { 60 | const { halfExtents, angle } = obstacle; 61 | 62 | mesh.geometry = new BoxGeometry( 63 | halfExtents.x * 2, 64 | halfExtents.y * 2, 65 | halfExtents.z * 2 66 | ); 67 | 68 | mesh.rotation.y = angle; 69 | } else if (obstacle.type === 'cylinder') { 70 | const { radius, height } = obstacle; 71 | 72 | mesh.geometry = new CylinderGeometry(radius, radius, height, 16); 73 | 74 | mesh.position.y += height / 2; 75 | } else { 76 | throw new Error(`Unknown obstacle type: ${obstacle}`); 77 | } 78 | 79 | this.add(mesh); 80 | this.obstacleMeshes.set(obstacle, mesh); 81 | } 82 | } 83 | 84 | for (const obstacle of unseen) { 85 | const obstacleMesh = this.obstacleMeshes.get(obstacle); 86 | 87 | if (obstacleMesh) { 88 | this.remove(obstacleMesh); 89 | this.obstacleMeshes.delete(obstacle); 90 | } 91 | } 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /packages/recast-navigation-three/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './debug'; 2 | export * from './helpers'; 3 | export * from './utils'; 4 | -------------------------------------------------------------------------------- /packages/recast-navigation-three/src/utils/generators.ts: -------------------------------------------------------------------------------- 1 | import { 2 | SoloNavMeshGeneratorConfig, 3 | TileCacheGeneratorConfig, 4 | TiledNavMeshGeneratorConfig, 5 | generateSoloNavMesh, 6 | generateTileCache, 7 | generateTiledNavMesh 8 | } from '@recast-navigation/generators'; 9 | import { Mesh } from 'three'; 10 | import { getPositionsAndIndices } from './get-positions-and-indices'; 11 | 12 | export const threeToSoloNavMesh = ( 13 | meshes: Mesh[], 14 | navMeshGeneratorConfig: Partial = {}, 15 | keepIntermediates = false 16 | ) => { 17 | const [positions, indices] = getPositionsAndIndices(meshes); 18 | 19 | return generateSoloNavMesh( 20 | positions, 21 | indices, 22 | navMeshGeneratorConfig, 23 | keepIntermediates 24 | ); 25 | }; 26 | 27 | export const threeToTiledNavMesh = ( 28 | meshes: Mesh[], 29 | navMeshGeneratorConfig: Partial = {}, 30 | keepIntermediates = false 31 | ) => { 32 | const [positions, indices] = getPositionsAndIndices(meshes); 33 | 34 | return generateTiledNavMesh( 35 | positions, 36 | indices, 37 | navMeshGeneratorConfig, 38 | keepIntermediates 39 | ); 40 | }; 41 | 42 | export const threeToTileCache = ( 43 | meshes: Mesh[], 44 | navMeshGeneratorConfig: Partial = {}, 45 | keepIntermediates = false 46 | ) => { 47 | const [positions, indices] = getPositionsAndIndices(meshes); 48 | 49 | return generateTileCache( 50 | positions, 51 | indices, 52 | navMeshGeneratorConfig, 53 | keepIntermediates 54 | ); 55 | }; 56 | -------------------------------------------------------------------------------- /packages/recast-navigation-three/src/utils/get-positions-and-indices.ts: -------------------------------------------------------------------------------- 1 | import { mergePositionsAndIndices } from '@recast-navigation/generators'; 2 | import { BufferAttribute, Mesh, Vector3 } from 'three'; 3 | 4 | const _position = new Vector3(); 5 | 6 | export const getPositionsAndIndices = ( 7 | meshes: Mesh[] 8 | ): [positions: Float32Array, indices: Uint32Array] => { 9 | const toMerge: { 10 | positions: ArrayLike; 11 | indices: ArrayLike; 12 | }[] = []; 13 | 14 | for (const mesh of meshes) { 15 | const positionAttribute = mesh.geometry.attributes 16 | .position as BufferAttribute; 17 | 18 | if (!positionAttribute || positionAttribute.itemSize !== 3) { 19 | continue; 20 | } 21 | 22 | mesh.updateMatrixWorld(); 23 | 24 | const positions = new Float32Array(positionAttribute.array); 25 | 26 | for (let i = 0; i < positions.length; i += 3) { 27 | const pos = _position.set( 28 | positions[i], 29 | positions[i + 1], 30 | positions[i + 2] 31 | ); 32 | mesh.localToWorld(pos); 33 | positions[i] = pos.x; 34 | positions[i + 1] = pos.y; 35 | positions[i + 2] = pos.z; 36 | } 37 | 38 | let indices: ArrayLike | undefined = 39 | mesh.geometry.getIndex()?.array; 40 | 41 | if (indices === undefined) { 42 | // this will become indexed when merging with other meshes 43 | const ascendingIndex: number[] = []; 44 | for (let i = 0; i < positionAttribute.count; i++) { 45 | ascendingIndex.push(i); 46 | } 47 | indices = ascendingIndex; 48 | } 49 | 50 | toMerge.push({ 51 | positions, 52 | indices, 53 | }); 54 | } 55 | 56 | return mergePositionsAndIndices(toMerge); 57 | }; 58 | -------------------------------------------------------------------------------- /packages/recast-navigation-three/src/utils/index.ts: -------------------------------------------------------------------------------- 1 | export * from './generators'; 2 | export * from './get-positions-and-indices'; 3 | -------------------------------------------------------------------------------- /packages/recast-navigation-three/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2022", 4 | "useDefineForClassFields": true, 5 | "module": "ES2022", 6 | "lib": ["ES2022", "DOM"], 7 | "moduleResolution": "Bundler", 8 | "strict": true, 9 | "resolveJsonModule": true, 10 | "isolatedModules": true, 11 | "esModuleInterop": true, 12 | "noEmit": true, 13 | "declaration": true, 14 | "noUnusedLocals": true, 15 | "noUnusedParameters": true, 16 | "noImplicitReturns": true, 17 | "skipLibCheck": true, 18 | "baseUrl": "./", 19 | "rootDir": "./src", 20 | "allowSyntheticDefaultImports": true, 21 | "outDir": "./dist" 22 | }, 23 | "files": ["./src/index.ts"] 24 | } 25 | -------------------------------------------------------------------------------- /packages/recast-navigation-wasm/.gitignore: -------------------------------------------------------------------------------- 1 | recastnavigation 2 | -------------------------------------------------------------------------------- /packages/recast-navigation-wasm/LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | Copyright © 2025 Isaac Mason 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /packages/recast-navigation-wasm/README.md: -------------------------------------------------------------------------------- 1 | # @recast-navigation/wasm 2 | 3 | WASM build of Recast Navigation. 4 | 5 | This library isn't intended for direct use - [`recast-navigation`](https://github.com/isaac-mason/recast-navigation-js/tree/main/packages/recast-navigation) is probably what you're looking for. It uses `@recast-navigation/wasm` internally, and provides a more friendly API. 6 | 7 | This WASM build is based on the [Babylon.js Recast Navigation Extension](https://github.com/BabylonJS/Extensions/tree/master/recastjs). 8 | -------------------------------------------------------------------------------- /packages/recast-navigation-wasm/build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # sh build.sh [release|debug] 4 | 5 | if [ -z $1 ] 6 | then 7 | BUILD_TYPE=release 8 | else 9 | BUILD_TYPE=$1 10 | shift 11 | fi 12 | 13 | # create directories 14 | mkdir -p ./build 15 | mkdir -p dist 16 | 17 | # clone recast navigation library 18 | [ ! -d "recastnavigation" ] && git clone https://github.com/isaac-mason/recastnavigation.git 19 | (cd recastnavigation && git checkout '599fd0f023181c0a484df2a18cf1d75a3553852e') 20 | 21 | # emscripten builds 22 | emcmake cmake -B build -DCMAKE_BUILD_TYPE=$BUILD_TYPE 23 | cmake --build build 24 | 25 | # generate typescript definitions 26 | yarn run webidl-dts-gen -e -d -i recast-navigation.idl -o ./dist/recast-navigation.d.ts -n Recast 27 | 28 | # copy files to dist 29 | cp ./build/recast-navigation.* ./dist/ 30 | -------------------------------------------------------------------------------- /packages/recast-navigation-wasm/example.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /packages/recast-navigation-wasm/front-matter.js: -------------------------------------------------------------------------------- 1 | // @recast-navigation/wasm, a port of recast navigation to JavaScript. 2 | // https://github.com/isaac-mason/recast-navigation-js 3 | 4 | // recastnavigation 5 | // https://github.com/recastnavigation/recastnavigation 6 | 7 | // Recast & Detour 8 | // Copyright (c) 2009 Mikko Mononen memon@inside.org 9 | // 10 | // This software is provided 'as-is', without any express or implied 11 | // warranty. In no event will the authors be held liable for any damages 12 | // arising from the use of this software. 13 | // 14 | // Permission is granted to anyone to use this software for any purpose, 15 | // including commercial applications, and to alter it and redistribute it 16 | // freely, subject to the following restrictions: 17 | // 18 | // 1. The origin of this software must not be misrepresented; you must not 19 | // claim that you wrote the original software. If you use this software 20 | // in a product, an acknowledgment in the product documentation would be 21 | // appreciated but is not required. 22 | // 2. Altered source versions must be plainly marked as such, and must not be 23 | // misrepresented as being the original software. 24 | // 3. This notice may not be removed or altered from any source distribution. 25 | -------------------------------------------------------------------------------- /packages/recast-navigation-wasm/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@recast-navigation/wasm", 3 | "description": "A webassembly port of Recast Navigation", 4 | "author": { 5 | "name": "Isaac Mason", 6 | "email": "isaac@isaacmason.com", 7 | "url": "https://isaacmason.com/" 8 | }, 9 | "version": "0.39.0", 10 | "license": "MIT", 11 | "homepage": "https://github.com/isaac-mason/recast-navigation-js", 12 | "bugs": { 13 | "url": "https://github.com/isaac-mason/recast-navigation-js/issues" 14 | }, 15 | "type": "module", 16 | "main": "./dist/recast-navigation.wasm-compat.js", 17 | "types": "./dist/recast-navigation.d.ts", 18 | "exports": { 19 | ".": { 20 | "types": "./dist/recast-navigation.d.ts", 21 | "import": "./dist/recast-navigation.wasm-compat.js", 22 | "default": "./dist/recast-navigation.wasm-compat.js" 23 | }, 24 | "./wasm": { 25 | "types": "./dist/recast-navigation.d.ts", 26 | "import": "./dist/recast-navigation.wasm.js", 27 | "default": "./dist/recast-navigation.wasm.js" 28 | }, 29 | "./wasm-compat": { 30 | "types": "./dist/recast-navigation.d.ts", 31 | "import": "./dist/recast-navigation.wasm-compat.js", 32 | "default": "./dist/recast-navigation.wasm-compat.js" 33 | } 34 | }, 35 | "files": [ 36 | "dist/recast-navigation.d.ts", 37 | "dist/recast-navigation.wasm-compat.js", 38 | "dist/recast-navigation.wasm.js", 39 | "dist/recast-navigation.wasm.wasm", 40 | "README.md", 41 | "LICENSE" 42 | ], 43 | "scripts": { 44 | "build": "sh build.sh", 45 | "build:debug": "sh build.sh debug" 46 | }, 47 | "devDependencies": { 48 | "webidl-dts-gen": "^1.11.0" 49 | }, 50 | "packageManager": "yarn@3.2.2" 51 | } 52 | -------------------------------------------------------------------------------- /packages/recast-navigation-wasm/src/Arrays.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | 6 | template 7 | class ArrayWrapperTemplate 8 | { 9 | public: 10 | T *data; 11 | int size; 12 | bool isView; 13 | 14 | ArrayWrapperTemplate() 15 | { 16 | data = nullptr; 17 | size = 0; 18 | isView = false; 19 | } 20 | 21 | ~ArrayWrapperTemplate() 22 | { 23 | free(); 24 | } 25 | 26 | void free() 27 | { 28 | if (!isView) 29 | { 30 | delete[] data; 31 | } 32 | 33 | size = 0; 34 | this->data = nullptr; 35 | } 36 | 37 | void copy(const T *data, int size) 38 | { 39 | free(); 40 | this->data = new T[size]; 41 | memcpy(this->data, data, size * sizeof(T)); 42 | this->size = size; 43 | this->isView = false; 44 | } 45 | 46 | void view(T *data) 47 | { 48 | this->data = data; 49 | this->size = 0; 50 | this->isView = true; 51 | } 52 | 53 | void resize(int size) 54 | { 55 | free(); 56 | data = new T[size]; 57 | memset(data, 0, size * sizeof(T)); 58 | this->size = size; 59 | this->isView = false; 60 | } 61 | 62 | T get(int index) 63 | { 64 | return data[index]; 65 | } 66 | 67 | void set(int index, T value) 68 | { 69 | data[index] = value; 70 | } 71 | 72 | T* getDataPointer() const 73 | { 74 | return data; 75 | } 76 | }; 77 | 78 | class IntArray : public ArrayWrapperTemplate 79 | { 80 | }; 81 | 82 | class UnsignedIntArray : public ArrayWrapperTemplate 83 | { 84 | }; 85 | 86 | class UnsignedCharArray : public ArrayWrapperTemplate 87 | { 88 | }; 89 | 90 | class UnsignedShortArray : public ArrayWrapperTemplate 91 | { 92 | }; 93 | 94 | class FloatArray : public ArrayWrapperTemplate 95 | { 96 | }; 97 | -------------------------------------------------------------------------------- /packages/recast-navigation-wasm/src/ChunkyTriMesh.cpp: -------------------------------------------------------------------------------- 1 | #include "./ChunkyTriMesh.h" 2 | 3 | bool ChunkyTriMeshUtils::createChunkyTriMesh(const FloatArray *verts, const IntArray *tris, int ntris, int trisPerChunk, rcChunkyTriMesh *chunkyTriMesh) 4 | { 5 | return rcCreateChunkyTriMesh(verts->data, tris->data, ntris, trisPerChunk, chunkyTriMesh); 6 | } 7 | 8 | int ChunkyTriMeshUtils::getChunksOverlappingRect(rcChunkyTriMesh *chunkyTriMesh, float *tbmin, float *tbmax, IntArray *ids, const int maxIds) 9 | { 10 | return rcGetChunksOverlappingRect(chunkyTriMesh, tbmin, tbmax, ids->data, maxIds); 11 | } 12 | 13 | IntArray *ChunkyTriMeshUtils::getChunkyTriMeshNodeTris(rcChunkyTriMesh *chunkyTriMesh, int nodeIndex) 14 | { 15 | rcChunkyTriMeshNode &node = chunkyTriMesh->nodes[nodeIndex]; 16 | int *tris = &chunkyTriMesh->tris[node.i * 3]; 17 | 18 | IntArray *result = new IntArray; 19 | result->view(tris); 20 | 21 | return result; 22 | } 23 | -------------------------------------------------------------------------------- /packages/recast-navigation-wasm/src/ChunkyTriMesh.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "../recastnavigation/RecastDemo/Include/ChunkyTriMesh.h" 4 | #include "./Arrays.h" 5 | 6 | class ChunkyTriMeshUtils 7 | { 8 | public: 9 | bool createChunkyTriMesh(const FloatArray *verts, const IntArray *tris, int ntris, int trisPerChunk, rcChunkyTriMesh *chunkyTriMesh); 10 | 11 | int getChunksOverlappingRect(rcChunkyTriMesh *chunkyTriMesh, float *tbmin, float *tbmax, IntArray *ids, const int maxIds); 12 | 13 | IntArray *getChunkyTriMeshNodeTris(rcChunkyTriMesh *chunkyTriMesh, int nodeIndex); 14 | }; 15 | -------------------------------------------------------------------------------- /packages/recast-navigation-wasm/src/Crowd.cpp: -------------------------------------------------------------------------------- 1 | #include "./Crowd.h" 2 | 3 | int CrowdUtils::getActiveAgentCount(dtCrowd *crowd) 4 | { 5 | return crowd->getActiveAgents(NULL, crowd->getAgentCount()); 6 | } 7 | 8 | bool CrowdUtils::overOffMeshConnection(dtCrowd *crowd, int idx) 9 | { 10 | const dtCrowdAgent *agent = crowd->getAgent(idx); 11 | const float triggerRadius = agent->params.radius * 2.25f; 12 | if (!agent->ncorners) 13 | return false; 14 | const bool offMeshConnection = (agent->cornerFlags[agent->ncorners - 1] & DT_STRAIGHTPATH_OFFMESH_CONNECTION) ? true : false; 15 | if (offMeshConnection) 16 | { 17 | const float distSq = dtVdist2DSqr(agent->npos, &agent->cornerVerts[(agent->ncorners - 1) * 3]); 18 | if (distSq < triggerRadius * triggerRadius) 19 | { 20 | return true; 21 | } 22 | } 23 | return false; 24 | } 25 | 26 | void CrowdUtils::agentTeleport(dtCrowd *crowd, int idx, const float *destination, const float *halfExtents, dtQueryFilter *filter) 27 | { 28 | if (idx < 0 || idx > crowd->getAgentCount()) 29 | { 30 | return; 31 | } 32 | 33 | dtPolyRef polyRef = 0; 34 | 35 | crowd->getNavMeshQuery()->findNearestPoly(destination, halfExtents, filter, &polyRef, 0); 36 | 37 | dtCrowdAgent *ag = crowd->getEditableAgent(idx); 38 | 39 | float nearest[3]; 40 | dtVcopy(nearest, destination); 41 | 42 | ag->corridor.reset(polyRef, nearest); 43 | ag->boundary.reset(); 44 | ag->partial = false; 45 | 46 | ag->topologyOptTime = 0; 47 | ag->targetReplanTime = 0; 48 | ag->nneis = 0; 49 | 50 | dtVset(ag->dvel, 0, 0, 0); 51 | dtVset(ag->nvel, 0, 0, 0); 52 | dtVset(ag->vel, 0, 0, 0); 53 | dtVcopy(ag->npos, nearest); 54 | 55 | ag->desiredSpeed = 0; 56 | 57 | if (polyRef) 58 | { 59 | ag->state = DT_CROWDAGENT_STATE_WALKING; 60 | } 61 | else 62 | { 63 | ag->state = DT_CROWDAGENT_STATE_INVALID; 64 | } 65 | 66 | ag->targetState = DT_CROWDAGENT_TARGET_NONE; 67 | } 68 | -------------------------------------------------------------------------------- /packages/recast-navigation-wasm/src/Crowd.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "../recastnavigation/Detour/Include/DetourCommon.h" 4 | #include "../recastnavigation/DetourCrowd/Include/DetourCrowd.h" 5 | 6 | class CrowdUtils 7 | { 8 | public: 9 | int getActiveAgentCount(dtCrowd *crowd); 10 | 11 | bool overOffMeshConnection(dtCrowd *crowd, int idx); 12 | 13 | void agentTeleport(dtCrowd *crowd, int idx, const float *destination, const float *halfExtents, dtQueryFilter *filter); 14 | }; 15 | -------------------------------------------------------------------------------- /packages/recast-navigation-wasm/src/DebugDraw/DebugDraw.cpp: -------------------------------------------------------------------------------- 1 | #include "DebugDraw.h" 2 | 3 | void DebugDraw::depthMask(bool state) 4 | { 5 | handleDepthMask(state); 6 | } 7 | 8 | void DebugDraw::texture(bool state) 9 | { 10 | handleTexture(state); 11 | } 12 | 13 | void DebugDraw::begin(duDebugDrawPrimitives prim, float size) 14 | { 15 | handleBegin(prim, size); 16 | } 17 | 18 | void DebugDraw::vertex(const float *pos, unsigned int color) 19 | { 20 | handleVertexWithColor(pos[0], pos[1], pos[2], color); 21 | } 22 | 23 | void DebugDraw::vertex(const float x, const float y, const float z, unsigned int color) 24 | { 25 | handleVertexWithColor(x, y, z, color); 26 | } 27 | 28 | void DebugDraw::vertex(const float *pos, unsigned int color, const float *uv) 29 | { 30 | handleVertexWithColorAndUV(pos[0], pos[1], pos[2], color, uv[0], uv[1]); 31 | } 32 | 33 | void DebugDraw::vertex(const float x, const float y, const float z, unsigned int color, const float u, const float v) 34 | { 35 | handleVertexWithColorAndUV(x, y, z, color, u, v); 36 | } 37 | 38 | void DebugDraw::end() 39 | { 40 | handleEnd(); 41 | } 42 | -------------------------------------------------------------------------------- /packages/recast-navigation-wasm/src/DebugDraw/DebugDraw.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "../../recastnavigation/DebugUtils/Include/DebugDraw.h" 4 | #include "../../recastnavigation/DebugUtils/Include/RecastDebugDraw.h" 5 | #include "../../recastnavigation/DebugUtils/Include/DetourDebugDraw.h" 6 | 7 | class DebugDraw : public duDebugDraw 8 | { 9 | public: 10 | virtual void depthMask(bool state); 11 | virtual void texture(bool state); 12 | virtual void begin(duDebugDrawPrimitives prim, float size = 1.0f); 13 | virtual void vertex(const float *pos, unsigned int color); 14 | virtual void vertex(const float x, const float y, const float z, unsigned int color); 15 | virtual void vertex(const float *pos, unsigned int color, const float *uv); 16 | virtual void vertex(const float x, const float y, const float z, unsigned int color, const float u, const float v); 17 | virtual void end(); 18 | 19 | virtual void handleDepthMask(bool state) = 0; 20 | virtual void handleTexture(bool state) = 0; 21 | virtual void handleBegin(duDebugDrawPrimitives prim, float size = 1.0f) = 0; 22 | virtual void handleVertexWithColor(const float x, const float y, const float z, unsigned int color) = 0; 23 | virtual void handleVertexWithColorAndUV(const float x, const float y, const float z, unsigned int color, const float u, const float v) = 0; 24 | virtual void handleEnd() = 0; 25 | }; 26 | -------------------------------------------------------------------------------- /packages/recast-navigation-wasm/src/DebugDraw/DetourDebugDraw.cpp: -------------------------------------------------------------------------------- 1 | #include "DetourDebugDraw.h" 2 | 3 | void DetourDebugDraw::debugDrawNavMesh(duDebugDraw *dd, const dtNavMesh &mesh, const int flags) 4 | { 5 | duDebugDrawNavMesh(dd, mesh, flags); 6 | } 7 | 8 | void DetourDebugDraw::debugDrawNavMeshWithClosedList(struct duDebugDraw *dd, const dtNavMesh &mesh, const dtNavMeshQuery &query, unsigned char flags) 9 | { 10 | duDebugDrawNavMeshWithClosedList(dd, mesh, query, flags); 11 | } 12 | 13 | void DetourDebugDraw::debugDrawNavMeshNodes(struct duDebugDraw *dd, const dtNavMeshQuery &query) 14 | { 15 | duDebugDrawNavMeshNodes(dd, query); 16 | } 17 | 18 | void DetourDebugDraw::debugDrawNavMeshBVTree(struct duDebugDraw *dd, const dtNavMesh &mesh) 19 | { 20 | duDebugDrawNavMeshBVTree(dd, mesh); 21 | } 22 | 23 | void DetourDebugDraw::debugDrawNavMeshPortals(struct duDebugDraw *dd, const dtNavMesh &mesh) 24 | { 25 | duDebugDrawNavMeshPortals(dd, mesh); 26 | } 27 | 28 | void DetourDebugDraw::debugDrawNavMeshPolysWithFlags(struct duDebugDraw *dd, const dtNavMesh &mesh, const unsigned short polyFlags, const unsigned int col) 29 | { 30 | duDebugDrawNavMeshPolysWithFlags(dd, mesh, polyFlags, col); 31 | } 32 | 33 | void DetourDebugDraw::debugDrawNavMeshPoly(struct duDebugDraw *dd, const dtNavMesh &mesh, dtPolyRef ref, const unsigned int col) 34 | { 35 | duDebugDrawNavMeshPoly(dd, mesh, ref, col); 36 | } 37 | 38 | void DetourDebugDraw::debugDrawTileCacheLayerAreas(struct duDebugDraw *dd, const dtTileCacheLayer &layer, const float cs, const float ch) 39 | { 40 | duDebugDrawTileCacheLayerAreas(dd, layer, cs, ch); 41 | } 42 | 43 | void DetourDebugDraw::debugDrawTileCacheLayerRegions(struct duDebugDraw *dd, const dtTileCacheLayer &layer, const float cs, const float ch) 44 | { 45 | duDebugDrawTileCacheLayerRegions(dd, layer, cs, ch); 46 | } 47 | 48 | void DetourDebugDraw::debugDrawTileCacheContours(duDebugDraw *dd, const struct dtTileCacheContourSet &lcset, const float *orig, const float cs, const float ch) 49 | { 50 | duDebugDrawTileCacheContours(dd, lcset, orig, cs, ch); 51 | } 52 | 53 | void DetourDebugDraw::debugDrawTileCachePolyMesh(duDebugDraw *dd, const struct dtTileCachePolyMesh &lmesh, const float *orig, const float cs, const float ch) 54 | { 55 | duDebugDrawTileCachePolyMesh(dd, lmesh, orig, cs, ch); 56 | } 57 | -------------------------------------------------------------------------------- /packages/recast-navigation-wasm/src/DebugDraw/DetourDebugDraw.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "../../recastnavigation/DebugUtils/Include/DebugDraw.h" 4 | #include "../../recastnavigation/DebugUtils/Include/DetourDebugDraw.h" 5 | 6 | class DetourDebugDraw 7 | { 8 | public: 9 | void debugDrawNavMesh(duDebugDraw *dd, const dtNavMesh &mesh, const int flags); 10 | void debugDrawNavMeshWithClosedList(struct duDebugDraw *dd, const dtNavMesh &mesh, const dtNavMeshQuery &query, unsigned char flags); 11 | void debugDrawNavMeshNodes(struct duDebugDraw *dd, const dtNavMeshQuery &query); 12 | void debugDrawNavMeshBVTree(struct duDebugDraw *dd, const dtNavMesh &mesh); 13 | void debugDrawNavMeshPortals(struct duDebugDraw *dd, const dtNavMesh &mesh); 14 | void debugDrawNavMeshPolysWithFlags(struct duDebugDraw *dd, const dtNavMesh &mesh, const unsigned short polyFlags, const unsigned int col); 15 | void debugDrawNavMeshPoly(struct duDebugDraw *dd, const dtNavMesh &mesh, dtPolyRef ref, const unsigned int col); 16 | void debugDrawTileCacheLayerAreas(struct duDebugDraw *dd, const dtTileCacheLayer &layer, const float cs, const float ch); 17 | void debugDrawTileCacheLayerRegions(struct duDebugDraw *dd, const dtTileCacheLayer &layer, const float cs, const float ch); 18 | void debugDrawTileCacheContours(duDebugDraw *dd, const struct dtTileCacheContourSet &lcset, const float *orig, const float cs, const float ch); 19 | void debugDrawTileCachePolyMesh(duDebugDraw *dd, const struct dtTileCachePolyMesh &lmesh, const float *orig, const float cs, const float ch); 20 | }; 21 | -------------------------------------------------------------------------------- /packages/recast-navigation-wasm/src/DebugDraw/RecastDebugDraw.cpp: -------------------------------------------------------------------------------- 1 | #include "RecastDebugDraw.h" 2 | 3 | void RecastDebugDraw::debugDrawHeightfieldSolid(duDebugDraw *dd, const rcHeightfield &hf) 4 | { 5 | duDebugDrawHeightfieldSolid(dd, hf); 6 | } 7 | 8 | void RecastDebugDraw::debugDrawHeightfieldWalkable(duDebugDraw *dd, const rcHeightfield &hf) 9 | { 10 | duDebugDrawHeightfieldWalkable(dd, hf); 11 | } 12 | 13 | void RecastDebugDraw::debugDrawCompactHeightfieldSolid(duDebugDraw *dd, const rcCompactHeightfield &chf) 14 | { 15 | duDebugDrawCompactHeightfieldSolid(dd, chf); 16 | } 17 | 18 | void RecastDebugDraw::debugDrawCompactHeightfieldRegions(duDebugDraw *dd, const rcCompactHeightfield &chf) 19 | { 20 | duDebugDrawCompactHeightfieldRegions(dd, chf); 21 | } 22 | 23 | void RecastDebugDraw::debugDrawCompactHeightfieldDistance(duDebugDraw *dd, const rcCompactHeightfield &chf) 24 | { 25 | duDebugDrawCompactHeightfieldDistance(dd, chf); 26 | } 27 | 28 | void RecastDebugDraw::debugDrawHeightfieldLayer(duDebugDraw *dd, const rcHeightfieldLayer &layer, const int idx) 29 | { 30 | duDebugDrawHeightfieldLayer(dd, layer, idx); 31 | } 32 | 33 | void RecastDebugDraw::debugDrawHeightfieldLayers(duDebugDraw *dd, const rcHeightfieldLayerSet &lset) 34 | { 35 | duDebugDrawHeightfieldLayers(dd, lset); 36 | } 37 | 38 | void RecastDebugDraw::debugDrawRegionConnections(duDebugDraw *dd, const rcContourSet &cset, const float alpha = 1.0f) 39 | { 40 | duDebugDrawRegionConnections(dd, cset, alpha); 41 | } 42 | 43 | void RecastDebugDraw::debugDrawRawContours(duDebugDraw *dd, const rcContourSet &cset, const float alpha = 1.0f) 44 | { 45 | duDebugDrawRawContours(dd, cset, alpha); 46 | } 47 | 48 | void RecastDebugDraw::debugDrawContours(duDebugDraw *dd, const rcContourSet &cset, const float alpha = 1.0f) 49 | { 50 | duDebugDrawContours(dd, cset, alpha); 51 | } 52 | 53 | void RecastDebugDraw::debugDrawPolyMesh(duDebugDraw *dd, const rcPolyMesh &mesh) 54 | { 55 | duDebugDrawPolyMesh(dd, mesh); 56 | } 57 | 58 | void RecastDebugDraw::debugDrawPolyMeshDetail(duDebugDraw *dd, const rcPolyMeshDetail &dmesh) 59 | { 60 | duDebugDrawPolyMeshDetail(dd, dmesh); 61 | } 62 | -------------------------------------------------------------------------------- /packages/recast-navigation-wasm/src/DebugDraw/RecastDebugDraw.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "../../recastnavigation/DebugUtils/Include/DebugDraw.h" 4 | #include "../../recastnavigation/DebugUtils/Include/RecastDebugDraw.h" 5 | 6 | class RecastDebugDraw 7 | { 8 | public: 9 | void debugDrawHeightfieldSolid(duDebugDraw *dd, const rcHeightfield &hf); 10 | void debugDrawHeightfieldWalkable(duDebugDraw *dd, const rcHeightfield &hf); 11 | void debugDrawCompactHeightfieldSolid(duDebugDraw *dd, const rcCompactHeightfield &chf); 12 | void debugDrawCompactHeightfieldRegions(duDebugDraw *dd, const rcCompactHeightfield &chf); 13 | void debugDrawCompactHeightfieldDistance(duDebugDraw *dd, const rcCompactHeightfield &chf); 14 | void debugDrawHeightfieldLayer(duDebugDraw *dd, const rcHeightfieldLayer &layer, const int idx); 15 | void debugDrawHeightfieldLayers(duDebugDraw *dd, const rcHeightfieldLayerSet &lset); 16 | void debugDrawRegionConnections(duDebugDraw *dd, const rcContourSet &cset, const float alpha); 17 | void debugDrawRawContours(duDebugDraw *dd, const rcContourSet &cset, const float alpha); 18 | void debugDrawContours(duDebugDraw *dd, const rcContourSet &cset, const float alpha); 19 | void debugDrawPolyMesh(duDebugDraw *dd, const rcPolyMesh &mesh); 20 | void debugDrawPolyMeshDetail(duDebugDraw *dd, const rcPolyMeshDetail &dmesh); 21 | }; 22 | -------------------------------------------------------------------------------- /packages/recast-navigation-wasm/src/Detour.cpp: -------------------------------------------------------------------------------- 1 | #include "./Detour.h" 2 | 3 | void DetourNavMeshBuilder::setPolyMeshCreateParams(dtNavMeshCreateParams *navMeshCreateParams, rcPolyMesh *polyMesh) 4 | { 5 | navMeshCreateParams->verts = polyMesh->verts; 6 | navMeshCreateParams->vertCount = polyMesh->nverts; 7 | navMeshCreateParams->polys = polyMesh->polys; 8 | navMeshCreateParams->polyAreas = polyMesh->areas; 9 | navMeshCreateParams->polyFlags = polyMesh->flags; 10 | navMeshCreateParams->polyCount = polyMesh->npolys; 11 | navMeshCreateParams->nvp = polyMesh->nvp; 12 | 13 | rcVcopy(navMeshCreateParams->bmin, polyMesh->bmin); 14 | rcVcopy(navMeshCreateParams->bmax, polyMesh->bmax); 15 | } 16 | 17 | void DetourNavMeshBuilder::setPolyMeshDetailCreateParams(dtNavMeshCreateParams *navMeshCreateParams, rcPolyMeshDetail *polyMeshDetail) 18 | { 19 | navMeshCreateParams->detailMeshes = polyMeshDetail->meshes; 20 | navMeshCreateParams->detailVerts = polyMeshDetail->verts; 21 | navMeshCreateParams->detailVertsCount = polyMeshDetail->nverts; 22 | navMeshCreateParams->detailTris = polyMeshDetail->tris; 23 | navMeshCreateParams->detailTriCount = polyMeshDetail->ntris; 24 | } 25 | 26 | void DetourNavMeshBuilder::setOffMeshConnections(dtNavMeshCreateParams *navMeshCreateParams, int offMeshConCount, float *offMeshConVerts, float *offMeshConRad, unsigned char *offMeshConDirs, unsigned char *offMeshConAreas, unsigned short *offMeshConFlags, unsigned int *offMeshConUserId) 27 | { 28 | int n = offMeshConCount; 29 | 30 | float *verts = new float[n * 3 * 2]; 31 | memcpy(verts, offMeshConVerts, sizeof(float) * offMeshConCount * 3 * 2); 32 | 33 | float *rads = new float[n]; 34 | memcpy(rads, offMeshConRad, sizeof(float) * n); 35 | 36 | unsigned char *dirs = new unsigned char[n]; 37 | memcpy(dirs, offMeshConDirs, sizeof(unsigned char) * n); 38 | 39 | unsigned char *areas = new unsigned char[n]; 40 | memcpy(areas, offMeshConAreas, sizeof(unsigned char) * n); 41 | 42 | unsigned short *flags = new unsigned short[n]; 43 | memcpy(flags, offMeshConFlags, sizeof(unsigned short) * n); 44 | 45 | unsigned int *userIds = new unsigned int[n]; 46 | memcpy(userIds, offMeshConUserId, sizeof(unsigned int) * n); 47 | 48 | navMeshCreateParams->offMeshConCount = offMeshConCount; 49 | navMeshCreateParams->offMeshConVerts = verts; 50 | navMeshCreateParams->offMeshConRad = rads; 51 | navMeshCreateParams->offMeshConDir = dirs; 52 | navMeshCreateParams->offMeshConAreas = areas; 53 | navMeshCreateParams->offMeshConFlags = flags; 54 | navMeshCreateParams->offMeshConUserID = userIds; 55 | } 56 | 57 | CreateNavMeshDataResult *DetourNavMeshBuilder::createNavMeshData(dtNavMeshCreateParams ¶ms) 58 | { 59 | CreateNavMeshDataResult *result = new CreateNavMeshDataResult; 60 | 61 | UnsignedCharArray *navMeshData = new UnsignedCharArray; 62 | result->navMeshData = navMeshData; 63 | 64 | if (!dtCreateNavMeshData(¶ms, &navMeshData->data, &navMeshData->size)) 65 | { 66 | result->success = false; 67 | delete navMeshData; 68 | result->navMeshData = nullptr; 69 | } 70 | else 71 | { 72 | result->success = true; 73 | } 74 | 75 | return result; 76 | } 77 | 78 | int DetourTileCacheBuilder::buildTileCacheLayer( 79 | dtTileCacheCompressor *comp, 80 | dtTileCacheLayerHeader *header, 81 | const UnsignedCharArray *heights, 82 | const UnsignedCharArray *areas, 83 | const UnsignedCharArray *cons, 84 | UnsignedCharArray *tileCacheData) 85 | { 86 | return dtBuildTileCacheLayer(comp, header, heights->data, areas->data, cons->data, &tileCacheData->data, &tileCacheData->size); 87 | } 88 | -------------------------------------------------------------------------------- /packages/recast-navigation-wasm/src/Detour.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "../recastnavigation/Recast/Include/Recast.h" 4 | #include "../recastnavigation/Detour/Include/DetourStatus.h" 5 | #include "../recastnavigation/Detour/Include/DetourNavMeshBuilder.h" 6 | #include "../recastnavigation/DetourCrowd/Include/DetourCrowd.h" 7 | #include "../recastnavigation/DetourTileCache/Include/DetourTileCacheBuilder.h" 8 | #include "./Arrays.h" 9 | 10 | class Detour 11 | { 12 | public: 13 | int FAILURE = DT_FAILURE; 14 | int SUCCESS = DT_SUCCESS; 15 | int IN_PROGRESS = DT_IN_PROGRESS; 16 | int STATUS_DETAIL_MASK = DT_STATUS_DETAIL_MASK; 17 | int WRONG_MAGIC = DT_WRONG_MAGIC; 18 | int WRONG_VERSION = DT_WRONG_VERSION; 19 | int OUT_OF_MEMORY = DT_OUT_OF_MEMORY; 20 | int INVALID_PARAM = DT_INVALID_PARAM; 21 | int BUFFER_TOO_SMALL = DT_BUFFER_TOO_SMALL; 22 | int OUT_OF_NODES = DT_OUT_OF_NODES; 23 | int PARTIAL_RESULT = DT_PARTIAL_RESULT; 24 | int ALREADY_OCCUPIED = DT_ALREADY_OCCUPIED; 25 | 26 | int VERTS_PER_POLYGON = DT_VERTS_PER_POLYGON; 27 | int NAVMESH_MAGIC = DT_NAVMESH_MAGIC; 28 | int NAVMESH_VERSION = DT_NAVMESH_VERSION; 29 | int NAVMESH_STATE_MAGIC = DT_NAVMESH_STATE_MAGIC; 30 | int NAVMESH_STATE_VERSION = DT_NAVMESH_STATE_VERSION; 31 | 32 | int TILECACHE_MAGIC = DT_TILECACHE_MAGIC; 33 | int TILECACHE_VERSION = DT_TILECACHE_VERSION; 34 | unsigned char TILECACHE_NULL_AREA = DT_TILECACHE_NULL_AREA; 35 | unsigned char TILECACHE_WALKABLE_AREA = DT_TILECACHE_WALKABLE_AREA; 36 | unsigned short TILECACHE_NULL_IDX = DT_TILECACHE_NULL_IDX; 37 | 38 | unsigned int NULL_LINK = DT_NULL_LINK; 39 | unsigned short EXT_LINK = DT_EXT_LINK; 40 | unsigned int OFFMESH_CON_BIDIR = DT_OFFMESH_CON_BIDIR; 41 | 42 | bool statusSucceed(dtStatus status) 43 | { 44 | return dtStatusSucceed(status); 45 | } 46 | 47 | bool statusFailed(dtStatus status) 48 | { 49 | return dtStatusFailed(status); 50 | } 51 | 52 | bool statusInProgress(dtStatus status) 53 | { 54 | return dtStatusInProgress(status); 55 | } 56 | 57 | bool statusDetail(dtStatus status, unsigned int detail) 58 | { 59 | return dtStatusDetail(status, detail); 60 | } 61 | 62 | dtCrowd *allocCrowd() 63 | { 64 | return dtAllocCrowd(); 65 | } 66 | 67 | void freeCrowd(dtCrowd *crowd) 68 | { 69 | dtFreeCrowd(crowd); 70 | } 71 | }; 72 | 73 | struct CreateNavMeshDataResult 74 | { 75 | bool success; 76 | UnsignedCharArray *navMeshData; 77 | }; 78 | 79 | class DetourNavMeshBuilder 80 | { 81 | public: 82 | void setPolyMeshCreateParams(dtNavMeshCreateParams *navMeshCreateParams, rcPolyMesh *polyMesh); 83 | 84 | void setPolyMeshDetailCreateParams(dtNavMeshCreateParams *navMeshCreateParams, rcPolyMeshDetail *polyMeshDetail); 85 | 86 | void setOffMeshConnections(dtNavMeshCreateParams *navMeshCreateParams, int offMeshConCount, float *offMeshConVerts, float *offMeshConRad, unsigned char *offMeshConDirs, unsigned char *offMeshConAreas, unsigned short *offMeshConFlags, unsigned int *offMeshConUserId); 87 | 88 | CreateNavMeshDataResult *createNavMeshData(dtNavMeshCreateParams ¶ms); 89 | }; 90 | 91 | class DetourTileCacheBuilder 92 | { 93 | public: 94 | int buildTileCacheLayer( 95 | dtTileCacheCompressor *comp, 96 | dtTileCacheLayerHeader *header, 97 | const UnsignedCharArray *heights, 98 | const UnsignedCharArray *areas, 99 | const UnsignedCharArray *cons, 100 | UnsignedCharArray *tileCacheData); 101 | }; 102 | -------------------------------------------------------------------------------- /packages/recast-navigation-wasm/src/NavMesh.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "../recastnavigation/Recast/Include/Recast.h" 4 | #include "../recastnavigation/Detour/Include/DetourStatus.h" 5 | #include "../recastnavigation/Detour/Include/DetourNavMesh.h" 6 | #include "../recastnavigation/Detour/Include/DetourCommon.h" 7 | #include "./Refs.h" 8 | #include "./Arrays.h" 9 | #include "./Vec.h" 10 | 11 | struct NavMeshRemoveTileResult 12 | { 13 | unsigned int status; 14 | unsigned char *data; 15 | int dataSize; 16 | }; 17 | 18 | struct NavMeshCalcTileLocResult 19 | { 20 | int tileX; 21 | int tileY; 22 | }; 23 | 24 | struct NavMeshGetTilesAtResult 25 | { 26 | const dtMeshTile *tiles; 27 | int tileCount; 28 | }; 29 | 30 | struct NavMeshGetTileAndPolyByRefResult 31 | { 32 | dtStatus status; 33 | const dtMeshTile *tile; 34 | const dtPoly *poly; 35 | }; 36 | 37 | struct NavMeshStoreTileStateResult 38 | { 39 | dtStatus status; 40 | unsigned char *data; 41 | int dataSize; 42 | }; 43 | 44 | class NavMesh 45 | { 46 | public: 47 | dtNavMesh *m_navMesh; 48 | 49 | NavMesh() 50 | { 51 | m_navMesh = dtAllocNavMesh(); 52 | } 53 | 54 | NavMesh(dtNavMesh *navMesh) 55 | { 56 | m_navMesh = navMesh; 57 | } 58 | 59 | bool initSolo(UnsignedCharArray *navMeshData); 60 | 61 | bool initTiled(const dtNavMeshParams *params); 62 | 63 | dtStatus addTile(UnsignedCharArray *navMeshData, int flags, dtTileRef lastRef, UnsignedIntRef *tileRef); 64 | 65 | NavMeshRemoveTileResult removeTile(dtTileRef ref); 66 | 67 | dtNavMesh *getNavMesh() 68 | { 69 | return m_navMesh; 70 | } 71 | 72 | NavMeshCalcTileLocResult calcTileLoc(const float *pos) const; 73 | 74 | void decodePolyId(dtPolyRef ref, UnsignedIntRef *salt, UnsignedIntRef *it, UnsignedIntRef *ip); 75 | 76 | dtPolyRef encodePolyId(unsigned int salt, unsigned int it, unsigned ip); 77 | 78 | const dtMeshTile *getTileAt(const int tx, const int ty, const int tlayer) const; 79 | 80 | NavMeshGetTilesAtResult getTilesAt(const int x, const int y, const int maxTiles) const; 81 | 82 | dtTileRef getTileRefAt(int x, int y, int layer) const; 83 | 84 | dtTileRef getTileRef(const dtMeshTile *tile) const; 85 | 86 | const dtMeshTile *getTileByRef(dtTileRef ref) const; 87 | 88 | int getMaxTiles() const; 89 | 90 | const dtMeshTile *getTile(int i) const; 91 | 92 | NavMeshGetTileAndPolyByRefResult getTileAndPolyByRef(const dtPolyRef ref) const; 93 | 94 | NavMeshGetTileAndPolyByRefResult getTileAndPolyByRefUnsafe(const dtPolyRef ref) const; 95 | 96 | bool isValidPolyRef(dtPolyRef ref) const; 97 | 98 | dtPolyRef getPolyRefBase(const dtMeshTile *tile) const; 99 | 100 | dtStatus getOffMeshConnectionPolyEndPoints(dtPolyRef prevRef, dtPolyRef polyRef, Vec3 *startPos, Vec3 *endPos) const; 101 | 102 | const dtOffMeshConnection *getOffMeshConnectionByRef(dtPolyRef ref) const; 103 | 104 | dtStatus setPolyFlags(dtPolyRef ref, unsigned short flags); 105 | 106 | dtStatus getPolyFlags(dtPolyRef ref, UnsignedShortRef *flags) const; 107 | 108 | dtStatus setPolyArea(dtPolyRef ref, unsigned char area); 109 | 110 | dtStatus getPolyArea(dtPolyRef ref, UnsignedCharRef *area) const; 111 | 112 | int getTileStateSize(const dtMeshTile *tile) const; 113 | 114 | NavMeshStoreTileStateResult storeTileState(const dtMeshTile *tile, const int maxDataSize) const; 115 | 116 | dtStatus restoreTileState(dtMeshTile *tile, const unsigned char *data, const int maxDataSize); 117 | 118 | void destroy(); 119 | }; 120 | -------------------------------------------------------------------------------- /packages/recast-navigation-wasm/src/NavMeshQuery.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "../recastnavigation/Recast/Include/Recast.h" 4 | #include "../recastnavigation/Detour/Include/DetourStatus.h" 5 | #include "../recastnavigation/Detour/Include/DetourCommon.h" 6 | #include "../recastnavigation/Detour/Include/DetourNavMeshQuery.h" 7 | #include "./Refs.h" 8 | #include "./Arrays.h" 9 | #include "./Vec.h" 10 | #include "./NavMesh.h" 11 | 12 | class FastRand 13 | { 14 | public: 15 | static inline int seed = 1337; 16 | 17 | static inline int fastrand() 18 | { 19 | seed = (214013 * seed + 2531011); 20 | return (seed >> 16) & 0x7FFF; 21 | } 22 | 23 | static inline float r01() 24 | { 25 | return fastrand() / 32767.0f; 26 | } 27 | 28 | static int getSeed() 29 | { 30 | return seed; 31 | } 32 | 33 | static void setSeed(int newSeed) 34 | { 35 | seed = newSeed; 36 | } 37 | }; 38 | 39 | class NavMeshQuery 40 | { 41 | public: 42 | dtNavMeshQuery *m_navQuery; 43 | 44 | NavMeshQuery(); 45 | 46 | NavMeshQuery(dtNavMeshQuery *navMeshQuery); 47 | 48 | dtStatus init(NavMesh *navMesh, const int maxNodes); 49 | 50 | dtStatus findPath(dtPolyRef startRef, dtPolyRef endRef, const float *startPos, const float *endPos, const dtQueryFilter *filter, UnsignedIntArray *path, int maxPath); 51 | 52 | dtStatus closestPointOnPoly(dtPolyRef ref, const float *pos, Vec3 *closest, BoolRef *posOverPoly); 53 | 54 | dtStatus findClosestPoint(const float *position, const float *halfExtents, const dtQueryFilter *filter, UnsignedIntRef *resultPolyRef, Vec3 *resultPoint, BoolRef *resultPosOverPoly); 55 | 56 | dtStatus findStraightPath( 57 | const float *startPos, 58 | const float *endPos, 59 | UnsignedIntArray *path, 60 | FloatArray *straightPath, 61 | UnsignedCharArray *straightPathFlags, 62 | UnsignedIntArray *straightPathRefs, 63 | IntRef *straightPathCount, 64 | const int maxStraightPath, 65 | const int options); 66 | 67 | dtStatus findNearestPoly(const float *center, const float *halfExtents, const dtQueryFilter *filter, UnsignedIntRef *nearestRef, Vec3 *nearestPt, BoolRef *isOverPoly); 68 | 69 | dtStatus findPolysAroundCircle(dtPolyRef startRef, const float *centerPos, const float radius, const dtQueryFilter *filter, UnsignedIntArray *resultRef, UnsignedIntArray *resultParent, FloatArray *resultCost, IntRef *resultCount, const int maxResult); 70 | 71 | dtStatus queryPolygons(const float *center, const float *halfExtents, const dtQueryFilter *filter, UnsignedIntArray *polys, IntRef *polyCount, const int maxPolys); 72 | 73 | dtStatus raycast(dtPolyRef startRef, const float *startPos, const float *endPos, const dtQueryFilter *filter, const unsigned int options, dtRaycastHit *hit, dtPolyRef prevRef); 74 | 75 | dtStatus findRandomPointAroundCircle(dtPolyRef startRef, const float *centerPos, const float radius, const dtQueryFilter *filter, UnsignedIntRef *resultRandomRef, Vec3 *resultRandomPoint); 76 | 77 | dtStatus moveAlongSurface(dtPolyRef startRef, const float *startPos, const float *endPos, const dtQueryFilter *filter, Vec3 *resultPos, UnsignedIntArray *visited, int maxVisitedSize); 78 | 79 | dtStatus findRandomPoint(const dtQueryFilter *filter, UnsignedIntRef *resultRandomRef, Vec3 *resultRandomPoint); 80 | 81 | dtStatus getPolyHeight(dtPolyRef ref, const float *pos, FloatRef *height); 82 | 83 | void destroy(); 84 | }; 85 | -------------------------------------------------------------------------------- /packages/recast-navigation-wasm/src/NavMeshSerdes.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "../recastnavigation/Recast/Include/Recast.h" 4 | #include "../recastnavigation/Detour/Include/DetourStatus.h" 5 | #include "../recastnavigation/Detour/Include/DetourNavMesh.h" 6 | #include "../recastnavigation/Detour/Include/DetourCommon.h" 7 | #include "../recastnavigation/Detour/Include/DetourNavMeshBuilder.h" 8 | #include "../recastnavigation/DetourTileCache/Include/DetourTileCache.h" 9 | #include "./Refs.h" 10 | #include "./NavMesh.h" 11 | #include "./TileCache.h" 12 | 13 | struct NavMeshExport 14 | { 15 | void *dataPointer; 16 | int size; 17 | }; 18 | 19 | class NavMeshExporter 20 | { 21 | public: 22 | NavMeshExporter() {} 23 | 24 | NavMeshExport exportNavMesh(NavMesh *navMesh, TileCache *tileCache) const; 25 | void freeNavMeshExport(NavMeshExport *navMeshExport); 26 | }; 27 | 28 | struct NavMeshImporterResult 29 | { 30 | bool success; 31 | NavMesh *navMesh; 32 | TileCache *tileCache; 33 | RecastLinearAllocator *allocator; 34 | RecastFastLZCompressor *compressor; 35 | }; 36 | 37 | class NavMeshImporter 38 | { 39 | public: 40 | NavMeshImporter() {} 41 | 42 | NavMeshImporterResult importNavMesh(NavMeshExport *navMeshExport, TileCacheMeshProcessJsImpl &meshProcess); 43 | }; 44 | -------------------------------------------------------------------------------- /packages/recast-navigation-wasm/src/Refs.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | class BoolRef 4 | { 5 | public: 6 | bool value; 7 | 8 | BoolRef() {} 9 | ~BoolRef() {} 10 | }; 11 | 12 | class IntRef 13 | { 14 | public: 15 | int value; 16 | 17 | IntRef() {} 18 | ~IntRef() {} 19 | }; 20 | 21 | class UnsignedIntRef 22 | { 23 | public: 24 | unsigned int value; 25 | 26 | UnsignedIntRef() {} 27 | ~UnsignedIntRef() {} 28 | }; 29 | 30 | class UnsignedCharRef 31 | { 32 | public: 33 | unsigned char value; 34 | 35 | UnsignedCharRef() {} 36 | ~UnsignedCharRef() {} 37 | }; 38 | 39 | class UnsignedShortRef 40 | { 41 | public: 42 | unsigned short value; 43 | 44 | UnsignedShortRef() {} 45 | ~UnsignedShortRef() {} 46 | }; 47 | 48 | struct FloatRef 49 | { 50 | float value; 51 | 52 | FloatRef() {} 53 | ~FloatRef() {} 54 | }; 55 | -------------------------------------------------------------------------------- /packages/recast-navigation-wasm/src/TileCache.cpp: -------------------------------------------------------------------------------- 1 | #include "./TileCache.h" 2 | 3 | bool TileCache::init(const dtTileCacheParams *params, RecastLinearAllocator *allocator, RecastFastLZCompressor *compressor, TileCacheMeshProcessJsImpl &meshProcess) 4 | { 5 | if (!m_tileCache) 6 | { 7 | return false; 8 | } 9 | 10 | TileCacheMeshProcessWrapper *recastMeshProcess = new TileCacheMeshProcessWrapper(meshProcess); 11 | 12 | dtStatus status = m_tileCache->init(params, allocator, compressor, recastMeshProcess); 13 | if (dtStatusFailed(status)) 14 | { 15 | return false; 16 | } 17 | 18 | m_talloc = allocator; 19 | m_tcomp = compressor; 20 | m_tmproc = recastMeshProcess; 21 | 22 | return true; 23 | }; 24 | 25 | TileCacheAddTileResult TileCache::addTile(UnsignedCharArray *tileCacheData, unsigned char flags) 26 | { 27 | TileCacheAddTileResult result; 28 | 29 | result.status = m_tileCache->addTile(tileCacheData->data, tileCacheData->size, flags, &result.tileRef); 30 | 31 | return result; 32 | } 33 | 34 | dtStatus TileCache::buildNavMeshTile(const dtCompressedTileRef *ref, NavMesh *navMesh) 35 | { 36 | return m_tileCache->buildNavMeshTile(*ref, navMesh->getNavMesh()); 37 | }; 38 | 39 | dtStatus TileCache::buildNavMeshTilesAt(const int tx, const int ty, NavMesh *navMesh) 40 | { 41 | return m_tileCache->buildNavMeshTilesAt(tx, ty, navMesh->getNavMesh()); 42 | }; 43 | 44 | TileCacheUpdateResult TileCache::update(NavMesh *navMesh) 45 | { 46 | TileCacheUpdateResult result; 47 | 48 | result.status = m_tileCache->update(0, navMesh->getNavMesh(), &result.upToDate); 49 | 50 | return result; 51 | } 52 | 53 | TileCacheAddObstacleResult TileCache::addCylinderObstacle(const Vec3 &position, float radius, float height) 54 | { 55 | dtObstacleRef ref(-1); 56 | 57 | TileCacheAddObstacleResult result; 58 | 59 | if (!m_tileCache) 60 | { 61 | result.status = DT_FAILURE; 62 | return result; 63 | } 64 | 65 | result.status = m_tileCache->addObstacle(&position.x, radius, height, &ref); 66 | 67 | m_obstacles.push_back(ref); 68 | 69 | result.ref = &m_obstacles.back(); 70 | 71 | return result; 72 | } 73 | 74 | TileCacheAddObstacleResult TileCache::addBoxObstacle(const Vec3 &position, const Vec3 &extent, float angle) 75 | { 76 | dtObstacleRef ref(-1); 77 | 78 | TileCacheAddObstacleResult result; 79 | 80 | if (!m_tileCache) 81 | { 82 | result.status = DT_FAILURE; 83 | return result; 84 | } 85 | 86 | result.status = m_tileCache->addBoxObstacle(&position.x, &extent.x, angle, &ref); 87 | 88 | m_obstacles.push_back(ref); 89 | 90 | result.ref = &m_obstacles.back(); 91 | 92 | return result; 93 | } 94 | 95 | dtStatus TileCache::removeObstacle(dtObstacleRef *obstacle) 96 | { 97 | if (!m_tileCache || !obstacle || *obstacle == -1) 98 | { 99 | return DT_FAILURE; 100 | } 101 | 102 | dtStatus status = m_tileCache->removeObstacle(*obstacle); 103 | 104 | auto iter = std::find(m_obstacles.begin(), m_obstacles.end(), *obstacle); 105 | if (iter != m_obstacles.end()) 106 | { 107 | m_obstacles.erase(iter); 108 | } 109 | 110 | return status; 111 | } 112 | 113 | void TileCache::destroy() 114 | { 115 | if (m_tileCache) 116 | { 117 | dtFreeTileCache(m_tileCache); 118 | } 119 | 120 | m_talloc->reset(); 121 | m_talloc = 0; 122 | m_tcomp = 0; 123 | m_tmproc = 0; 124 | } 125 | -------------------------------------------------------------------------------- /packages/recast-navigation-wasm/src/Vec.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | class Vec3 6 | { 7 | public: 8 | float x, y, z; 9 | 10 | Vec3() {} 11 | Vec3(float v) : x(v), y(v), z(v) {} 12 | Vec3(float x, float y, float z) : x(x), y(y), z(z) {} 13 | }; 14 | -------------------------------------------------------------------------------- /packages/recast-navigation-wasm/src/recast-navigation.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "./Arrays.h" 4 | #include "./Refs.h" 5 | #include "./Vec.h" 6 | #include "./TileCache.h" 7 | #include "./NavMesh.h" 8 | #include "./NavMeshQuery.h" 9 | #include "./Crowd.h" 10 | #include "./NavMeshSerdes.h" 11 | #include "./Recast.h" 12 | #include "./Detour.h" 13 | #include "./ChunkyTriMesh.h" 14 | #include "./DebugDraw/DebugDraw.h" 15 | #include "./DebugDraw/RecastDebugDraw.h" 16 | #include "./DebugDraw/DetourDebugDraw.h" 17 | -------------------------------------------------------------------------------- /packages/recast-navigation/.gitignore: -------------------------------------------------------------------------------- 1 | index.mjs 2 | index.mjs.map 3 | index.d.ts 4 | 5 | generators.mjs 6 | generators.mjs.map 7 | generators.d.ts 8 | 9 | three.mjs 10 | three.mjs.map 11 | three.d.ts 12 | 13 | storybook-static 14 | -------------------------------------------------------------------------------- /packages/recast-navigation/.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "es5", 3 | "tabWidth": 2, 4 | "semi": true, 5 | "singleQuote": true 6 | } 7 | -------------------------------------------------------------------------------- /packages/recast-navigation/.storybook/common/agent-path.tsx: -------------------------------------------------------------------------------- 1 | import { Line } from '@react-three/drei'; 2 | import { CrowdAgent } from '@recast-navigation/core'; 3 | import React, { useEffect, useState } from 'react'; 4 | import * as THREE from 'three'; 5 | 6 | export type AgentPathProps = { 7 | agent?: CrowdAgent; 8 | target?: THREE.Vector3; 9 | }; 10 | 11 | export const AgentPath = ({ agent, target }: AgentPathProps) => { 12 | const [agentPath, setAgentPath] = useState< 13 | THREE.Vector3Tuple[] | undefined 14 | >(); 15 | 16 | useEffect(() => { 17 | if (!agent) return; 18 | 19 | if (!target) { 20 | setAgentPath(undefined); 21 | return; 22 | } 23 | 24 | const interval = setInterval(() => { 25 | const path = [agent.position(), ...agent.corners()]; 26 | 27 | if (path.length) { 28 | setAgentPath(path.map((p) => [p.x, p.y + 0.1, p.z])); 29 | } else { 30 | setAgentPath(undefined); 31 | } 32 | }, 200); 33 | 34 | return () => { 35 | clearInterval(interval); 36 | }; 37 | }, [agent, target]); 38 | 39 | return ( 40 | <> 41 | {agentPath && } 42 | 43 | {target && ( 44 | 45 | 46 | 47 | 48 | 49 | 50 | )} 51 | 52 | ); 53 | }; 54 | -------------------------------------------------------------------------------- /packages/recast-navigation/.storybook/common/debug.tsx: -------------------------------------------------------------------------------- 1 | import { useFrame } from '@react-three/fiber'; 2 | import { Crowd, NavMesh, TileCache } from '@recast-navigation/core'; 3 | import { 4 | CrowdHelper, 5 | DebugDrawer, 6 | TileCacheHelper, 7 | } from '@recast-navigation/three'; 8 | import React, { useEffect, useMemo } from 'react'; 9 | import { Material } from 'three'; 10 | 11 | export type DebugProps = { 12 | autoUpdate?: boolean; 13 | navMesh?: NavMesh; 14 | tileCache?: TileCache; 15 | obstacleMaterial?: Material; 16 | crowd?: Crowd; 17 | agentMaterial?: Material; 18 | }; 19 | 20 | export const Debug = ({ 21 | autoUpdate, 22 | navMesh, 23 | tileCache, 24 | obstacleMaterial, 25 | crowd, 26 | agentMaterial, 27 | }: DebugProps) => { 28 | const debugDrawer = useMemo(() => { 29 | return new DebugDrawer(); 30 | }, []); 31 | 32 | const tileCacheHelper = useMemo(() => { 33 | if (!tileCache) return null; 34 | 35 | return new TileCacheHelper(tileCache, { 36 | obstacleMaterial, 37 | }); 38 | }, [tileCache, obstacleMaterial]); 39 | 40 | const crowdHelper = useMemo(() => { 41 | if (!crowd) return null; 42 | 43 | return new CrowdHelper(crowd, { 44 | agentMaterial, 45 | }); 46 | }, [crowd, agentMaterial]); 47 | 48 | useFrame(() => { 49 | if (crowdHelper) { 50 | crowdHelper.update(); 51 | } 52 | }); 53 | 54 | useEffect(() => { 55 | if (!navMesh) return; 56 | 57 | const update = () => { 58 | debugDrawer.clear(); 59 | debugDrawer.drawNavMeshPolysWithFlags(navMesh, 1, 0x0000ff); 60 | }; 61 | 62 | update(); 63 | 64 | const interval = setInterval(() => { 65 | update(); 66 | }, 1000); 67 | 68 | return () => { 69 | clearInterval(interval); 70 | }; 71 | }, [navMesh]); 72 | 73 | useEffect(() => { 74 | if (!tileCacheHelper || !autoUpdate) return; 75 | 76 | const interval = setInterval(() => { 77 | tileCacheHelper.update(); 78 | }, 100); 79 | 80 | return () => { 81 | clearInterval(interval); 82 | }; 83 | }, [tileCacheHelper]); 84 | 85 | return ( 86 | <> 87 | {debugDrawer && } 88 | 89 | 90 | {tileCacheHelper && } 91 | 92 | 93 | {crowdHelper && } 94 | 95 | ); 96 | }; 97 | -------------------------------------------------------------------------------- /packages/recast-navigation/.storybook/common/dungeon-environment.tsx: -------------------------------------------------------------------------------- 1 | import { useGLTF } from '@react-three/drei'; 2 | import React from 'react'; 3 | import { GLTF } from 'three-stdlib'; 4 | 5 | const assetUrl = 'dungeon.gltf'; 6 | 7 | export const DungeonEnvironment = () => { 8 | const gltf = useGLTF(assetUrl) as GLTF; 9 | 10 | return ; 11 | }; 12 | 13 | useGLTF.preload(assetUrl); 14 | -------------------------------------------------------------------------------- /packages/recast-navigation/.storybook/common/nav-test-environment.tsx: -------------------------------------------------------------------------------- 1 | import { useGLTF } from '@react-three/drei'; 2 | import type { ThreeElements } from '@react-three/fiber'; 3 | import React from 'react'; 4 | import { GLTF } from 'three-stdlib'; 5 | 6 | const assetUrl = 'nav_test.glb'; 7 | 8 | export const NavTestEnvironment = (props: Omit) => { 9 | const gltf = useGLTF(assetUrl) as GLTF; 10 | 11 | return ; 12 | }; 13 | 14 | useGLTF.preload(assetUrl); 15 | -------------------------------------------------------------------------------- /packages/recast-navigation/.storybook/common/use-nav-mesh-config.ts: -------------------------------------------------------------------------------- 1 | import { useControls } from 'leva'; 2 | import { RecastConfig } from 'recast-navigation'; 3 | 4 | export const useNavMeshConfig = ( 5 | prefix?: string, 6 | defaults?: Partial 7 | ): Partial => 8 | useControls(`${prefix ? prefix + '-' : ''}config`, { 9 | borderSize: { 10 | value: defaults?.borderSize ?? 0, 11 | label: 'Border Size', 12 | }, 13 | tileSize: { 14 | value: defaults?.tileSize ?? 0, 15 | label: 'Tile Size', 16 | }, 17 | cs: { 18 | value: defaults?.cs ?? 0.2, 19 | label: 'Cell Size', 20 | }, 21 | ch: { 22 | value: defaults?.ch ?? 0.2, 23 | label: 'Cell Height', 24 | }, 25 | walkableSlopeAngle: { 26 | value: defaults?.walkableSlopeAngle ?? 60, 27 | label: 'Walkable Slope Angle', 28 | }, 29 | walkableHeight: { 30 | value: defaults?.walkableHeight ?? 2, 31 | label: 'Walkable Height', 32 | }, 33 | walkableClimb: { 34 | value: defaults?.walkableClimb ?? 2, 35 | label: 'Walkable Climb', 36 | }, 37 | walkableRadius: { 38 | value: defaults?.walkableRadius ?? 0.2, 39 | label: 'Walkable Radius', 40 | }, 41 | maxEdgeLen: { 42 | value: defaults?.maxEdgeLen ?? 12, 43 | label: 'Max Edge Length', 44 | }, 45 | maxSimplificationError: { 46 | value: defaults?.maxSimplificationError ?? 1.3, 47 | label: 'Max Simplification Error', 48 | }, 49 | minRegionArea: { 50 | value: defaults?.minRegionArea ?? 8, 51 | label: 'Min Region Area', 52 | }, 53 | mergeRegionArea: { 54 | value: defaults?.mergeRegionArea ?? 20, 55 | label: 'Merge Region Area', 56 | }, 57 | maxVertsPerPoly: { 58 | value: defaults?.maxVertsPerPoly ?? 6, 59 | label: 'Max Verts Per Poly', 60 | }, 61 | detailSampleDist: { 62 | value: defaults?.detailSampleDist ?? 6, 63 | label: 'Detail Sample Dist', 64 | }, 65 | detailSampleMaxError: { 66 | value: defaults?.detailSampleMaxError ?? 1, 67 | label: 'Detail Sample Max Error', 68 | }, 69 | }); 70 | -------------------------------------------------------------------------------- /packages/recast-navigation/.storybook/decorators.tsx: -------------------------------------------------------------------------------- 1 | import { Environment, Loader } from '@react-three/drei'; 2 | import { Canvas } from '@react-three/fiber'; 3 | import * as RecastNavigation from '@recast-navigation/core'; 4 | import { Leva } from 'leva'; 5 | import React, { Suspense } from 'react'; 6 | import { suspend } from 'suspend-react'; 7 | import tunnel from 'tunnel-rat' 8 | 9 | export const htmlTunnel = tunnel(); 10 | 11 | const city = import('@pmndrs/assets/hdri/city.exr'); 12 | 13 | type SetupProps = { 14 | children: React.ReactNode; 15 | }; 16 | 17 | const Setup = ({ children }: SetupProps) => { 18 | return ( 19 |
20 | 28 | {children} 29 | 30 | 31 | 32 | 33 | 34 |
35 | ); 36 | }; 37 | 38 | export const decorators = [ 39 | (Story: () => JSX.Element) => { 40 | suspend(async () => { 41 | await RecastNavigation.init() 42 | 43 | ;(window as any).RecastNavigation = RecastNavigation 44 | }, []); 45 | 46 | return ; 47 | }, 48 | (Story: () => JSX.Element) => ( 49 | 50 | 51 | 52 | ), 53 | (Story: () => JSX.Element) => ( 54 | <> 55 | 56 | 63 | 64 | ), 65 | ]; 66 | -------------------------------------------------------------------------------- /packages/recast-navigation/.storybook/main.ts: -------------------------------------------------------------------------------- 1 | import { dirname, join, resolve } from 'path'; 2 | 3 | module.exports = { 4 | stories: ['./stories/**/*.mdx', './stories/**/*.stories.@(js|jsx|ts|tsx)'], 5 | addons: [ 6 | getAbsolutePath('@storybook/addon-links'), 7 | getAbsolutePath('@storybook/addon-essentials'), 8 | ], 9 | framework: { 10 | name: getAbsolutePath('@storybook/react-vite'), 11 | options: {}, 12 | }, 13 | staticDirs: ['./public'], 14 | docs: { 15 | autodocs: false, 16 | }, 17 | }; 18 | 19 | /** 20 | * This function is used to resolve the absolute path of a package. 21 | * It is needed in projects that use Yarn PnP or are set up within a monorepo. 22 | */ 23 | function getAbsolutePath(value: string): any { 24 | return dirname(require.resolve(join(value, 'package.json'))); 25 | } 26 | -------------------------------------------------------------------------------- /packages/recast-navigation/.storybook/manager.ts: -------------------------------------------------------------------------------- 1 | import { addons } from '@storybook/manager-api'; 2 | 3 | addons.setConfig({ 4 | showPanel: false, 5 | }); 6 | -------------------------------------------------------------------------------- /packages/recast-navigation/.storybook/parameters.ts: -------------------------------------------------------------------------------- 1 | export const parameters = { 2 | options: { 3 | showPanel: false, 4 | }, 5 | }; 6 | -------------------------------------------------------------------------------- /packages/recast-navigation/.storybook/preview.ts: -------------------------------------------------------------------------------- 1 | import './styles.css'; 2 | 3 | export const parameters = { 4 | controls: { 5 | matchers: { 6 | color: /(background|color)$/i, 7 | date: /Date$/, 8 | }, 9 | }, 10 | options: { 11 | storySort: { 12 | method: 'alphabetical', 13 | order: [ 14 | 'Crowd', 15 | 'NavMeshQuery', 16 | 'NavMesh', 17 | 'TileCache', 18 | 'Utilities', 19 | 'External Use', 20 | 'Off Mesh Connections', 21 | 'Helpers', 22 | 'Advanced', 23 | 'Debug', 24 | ], 25 | locales: 'en-US', 26 | }, 27 | }, 28 | }; 29 | -------------------------------------------------------------------------------- /packages/recast-navigation/.storybook/public/nav_test.glb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/isaac-mason/recast-navigation-js/416da48186b9e95736c7e1555e40858c610d78b1/packages/recast-navigation/.storybook/public/nav_test.glb -------------------------------------------------------------------------------- /packages/recast-navigation/.storybook/stories/advanced/flood-fill-pruning.stories.tsx: -------------------------------------------------------------------------------- 1 | import { OrbitControls } from '@react-three/drei'; 2 | import { ThreeEvent } from '@react-three/fiber'; 3 | import { floodFillPruneNavMesh, NavMeshQuery } from '@recast-navigation/core'; 4 | import { DebugDrawer, threeToSoloNavMesh } from '@recast-navigation/three'; 5 | import React, { useEffect, useState } from 'react'; 6 | import * as THREE from 'three'; 7 | import { Group, Mesh } from 'three'; 8 | import { NavTestEnvironment } from '../../common/nav-test-environment'; 9 | import { decorators, htmlTunnel } from '../../decorators'; 10 | import { parameters } from '../../parameters'; 11 | 12 | export default { 13 | title: 'Advanced / Flood Fill Pruning', 14 | decorators, 15 | parameters, 16 | }; 17 | 18 | export const FloodFillPruning = () => { 19 | const [group, setGroup] = useState(null); 20 | 21 | const [debug, setDebug] = useState(); 22 | 23 | const [point, setPoint] = useState( 24 | new THREE.Vector3(0.8, 0.26, 3.3) 25 | ); 26 | 27 | useEffect(() => { 28 | if (!group) return; 29 | 30 | const meshes: Mesh[] = []; 31 | 32 | group.traverse((child) => { 33 | if (child instanceof Mesh) { 34 | meshes.push(child); 35 | } 36 | }); 37 | 38 | const agentRadius = 0.1; 39 | const cellSize = 0.05; 40 | 41 | const { success, navMesh } = threeToSoloNavMesh(meshes, { 42 | cs: cellSize, 43 | ch: 0.2, 44 | walkableRadius: Math.ceil(agentRadius / cellSize), 45 | }); 46 | 47 | if (!success) return; 48 | 49 | const navMeshQuery = new NavMeshQuery(navMesh); 50 | 51 | const nearestPolyResult = navMeshQuery.findNearestPoly(point, { 52 | halfExtents: { x: 2, y: 2, z: 2 }, 53 | }); 54 | 55 | if (!nearestPolyResult.success) return; 56 | 57 | floodFillPruneNavMesh(navMesh, [nearestPolyResult.nearestRef]); 58 | 59 | /* debug draw */ 60 | const debug = new DebugDrawer(); 61 | 62 | debug.drawNavMeshPolysWithFlags(navMesh, 1, 0x0000ff); 63 | 64 | setDebug(debug); 65 | 66 | return () => { 67 | setDebug(undefined); 68 | 69 | debug.dispose(); 70 | navMeshQuery.destroy(); 71 | navMesh.destroy(); 72 | }; 73 | }, [group, point]); 74 | 75 | const onPointerDown = ({ point }: ThreeEvent) => { 76 | setPoint(new THREE.Vector3(point.x, point.y, point.z)); 77 | }; 78 | 79 | return ( 80 | <> 81 | 82 | 83 | 84 | 85 | {debug && } 86 | 87 | 88 | 89 | 90 |
102 | click to set flood fill start point 103 |
104 |
105 | 106 | ); 107 | }; 108 | -------------------------------------------------------------------------------- /packages/recast-navigation/.storybook/stories/crowd/multiple-agents.stories.tsx: -------------------------------------------------------------------------------- 1 | import { OrbitControls } from '@react-three/drei'; 2 | import { ThreeEvent, useFrame } from '@react-three/fiber'; 3 | import { Crowd, NavMesh, NavMeshQuery } from '@recast-navigation/core'; 4 | import React, { useEffect, useState } from 'react'; 5 | import { threeToSoloNavMesh } from '@recast-navigation/three'; 6 | import { Group, Mesh, MeshStandardMaterial } from 'three'; 7 | import { Debug } from '../../common/debug'; 8 | import { NavTestEnvironment } from '../../common/nav-test-environment'; 9 | import { decorators } from '../../decorators'; 10 | import { parameters } from '../../parameters'; 11 | 12 | export default { 13 | title: 'Crowd / Crowd With Multiple Agents', 14 | decorators, 15 | parameters, 16 | }; 17 | 18 | const agentMaterial = new MeshStandardMaterial({ 19 | color: 'red', 20 | }); 21 | 22 | export const CrowdWithMultipleAgents = () => { 23 | const [group, setGroup] = useState(null); 24 | 25 | const [navMesh, setNavMesh] = useState(); 26 | const [navMeshQuery, setNavMeshQuery] = useState(); 27 | const [crowd, setCrowd] = useState(); 28 | 29 | useEffect(() => { 30 | if (!group) return; 31 | 32 | const meshes: Mesh[] = []; 33 | 34 | group.traverse((child) => { 35 | if (child instanceof Mesh) { 36 | meshes.push(child); 37 | } 38 | }); 39 | 40 | const maxAgentRadius = 0.15; 41 | const cellSize = 0.05; 42 | 43 | const { success, navMesh } = threeToSoloNavMesh(meshes, { 44 | cs: cellSize, 45 | ch: 0.2, 46 | walkableRadius: Math.ceil(maxAgentRadius / cellSize), 47 | }); 48 | 49 | if (!success) return; 50 | 51 | const navMeshQuery = new NavMeshQuery(navMesh); 52 | 53 | const crowd = new Crowd(navMesh, { 54 | maxAgents: 10, 55 | maxAgentRadius, 56 | }); 57 | 58 | for (let i = 0; i < 10; i++) { 59 | const { randomPoint: position } = 60 | navMeshQuery.findRandomPointAroundCircle({ x: -2, y: 0, z: 3 }, 1); 61 | 62 | crowd.addAgent(position, { 63 | radius: 0.1 + Math.random() * 0.05, 64 | height: 0.5, 65 | maxAcceleration: 4.0, 66 | maxSpeed: 1.0, 67 | separationWeight: 1.0, 68 | }); 69 | } 70 | 71 | setNavMesh(navMesh); 72 | setNavMeshQuery(navMeshQuery); 73 | setCrowd(crowd); 74 | 75 | return () => { 76 | navMeshQuery.destroy(); 77 | crowd.destroy(); 78 | navMesh.destroy(); 79 | 80 | setNavMesh(undefined); 81 | setNavMeshQuery(undefined); 82 | setCrowd(undefined); 83 | }; 84 | }, [group]); 85 | 86 | useFrame((_, delta) => { 87 | if (!crowd) return; 88 | 89 | const clampedDelta = Math.min(delta, 0.1); 90 | 91 | crowd.update(clampedDelta); 92 | }); 93 | 94 | const onClick = (e: ThreeEvent) => { 95 | if (!navMesh || !navMeshQuery || !crowd) return; 96 | 97 | const { point: target } = navMeshQuery.findClosestPoint(e.point); 98 | 99 | for (const agent of crowd.getAgents()) { 100 | agent.requestMoveTarget(target); 101 | } 102 | }; 103 | 104 | return ( 105 | <> 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | ); 115 | }; 116 | -------------------------------------------------------------------------------- /packages/recast-navigation/.storybook/stories/nav-mesh-query/compute-path.stories.tsx: -------------------------------------------------------------------------------- 1 | import { Line, OrbitControls } from '@react-three/drei'; 2 | import { NavMesh, NavMeshQuery } from '@recast-navigation/core'; 3 | import { threeToSoloNavMesh } from '@recast-navigation/three'; 4 | import React, { useEffect, useState } from 'react'; 5 | import { Group, Mesh, MeshBasicMaterial, Vector3Tuple } from 'three'; 6 | import { Debug } from '../../common/debug'; 7 | import { NavTestEnvironment } from '../../common/nav-test-environment'; 8 | import { decorators } from '../../decorators'; 9 | import { parameters } from '../../parameters'; 10 | 11 | export default { 12 | title: 'NavMeshQuery / Compute Path', 13 | decorators, 14 | parameters, 15 | }; 16 | 17 | const navMeshMaterial = new MeshBasicMaterial({ 18 | wireframe: true, 19 | color: 'red', 20 | }); 21 | 22 | export const ComputePath = () => { 23 | const [group, setGroup] = useState(null); 24 | 25 | const [navMesh, setNavMesh] = useState(); 26 | const [path, setPath] = useState(); 27 | 28 | useEffect(() => { 29 | if (!group) return; 30 | 31 | const meshes: Mesh[] = []; 32 | 33 | group.traverse((child) => { 34 | if (child instanceof Mesh) { 35 | meshes.push(child); 36 | } 37 | }); 38 | 39 | const { success, navMesh } = threeToSoloNavMesh(meshes, { 40 | cs: 0.05, 41 | ch: 0.2, 42 | }); 43 | 44 | if (!success) return; 45 | 46 | const navMeshQuery = new NavMeshQuery(navMesh); 47 | 48 | const { point: start } = navMeshQuery.findClosestPoint({ 49 | x: -4.128927083678903, 50 | y: 0.2664172349988201, 51 | z: 4.8521110263641685, 52 | }); 53 | 54 | const { point: end } = navMeshQuery.findClosestPoint({ 55 | x: 2.0756323479723005, 56 | y: 2.38756142461898, 57 | z: -1.9437325288048717, 58 | }); 59 | 60 | const { path } = navMeshQuery.computePath(start, end); 61 | 62 | setNavMesh(navMesh); 63 | setPath(path ? path.map((v) => [v.x, v.y, v.z]) : undefined); 64 | 65 | return () => { 66 | navMesh.destroy(); 67 | navMeshQuery.destroy(); 68 | 69 | setNavMesh(undefined); 70 | setPath(undefined); 71 | }; 72 | }, [group]); 73 | 74 | return ( 75 | <> 76 | 77 | 78 | 79 | 80 | {path && } 81 | 82 | 83 | 84 | 85 | 86 | ); 87 | }; 88 | -------------------------------------------------------------------------------- /packages/recast-navigation/.storybook/stories/nav-mesh-query/find-random-point.stories.tsx: -------------------------------------------------------------------------------- 1 | import { NavMesh, NavMeshQuery } from '@recast-navigation/core'; 2 | import { threeToSoloNavMesh } from '@recast-navigation/three'; 3 | import React, { useEffect, useState } from 'react'; 4 | import { Group, Mesh, MeshBasicMaterial, Vector3Tuple } from 'three'; 5 | import { Debug } from '../../common/debug'; 6 | import { NavTestEnvironment } from '../../common/nav-test-environment'; 7 | import { decorators, htmlTunnel } from '../../decorators'; 8 | import { parameters } from '../../parameters'; 9 | import { OrbitControls } from '@react-three/drei'; 10 | import { setRandomSeed } from '@recast-navigation/core'; 11 | 12 | export default { 13 | title: 'NavMeshQuery / Find Random Point', 14 | decorators, 15 | parameters, 16 | }; 17 | 18 | const navMeshMaterial = new MeshBasicMaterial({ 19 | wireframe: true, 20 | color: 'red', 21 | }); 22 | 23 | export const FindRandomPoint = () => { 24 | const [group, setGroup] = useState(null); 25 | 26 | const [navMesh, setNavMesh] = useState(); 27 | const [navMeshQuery, setNavMeshQuery] = useState(); 28 | const [point, setPoint] = useState([0, 0, 0]); 29 | 30 | const newPoint = async function () { 31 | if (!navMeshQuery) return; 32 | 33 | const result = navMeshQuery.findRandomPoint(); 34 | if (!result) return; 35 | 36 | const point = result.randomPoint; 37 | setPoint([point.x, point.y, point.z]); 38 | }; 39 | 40 | useEffect(() => { 41 | if (!group) return; 42 | 43 | setRandomSeed(1337); 44 | 45 | const meshes: Mesh[] = []; 46 | 47 | group.traverse((child) => { 48 | if (child instanceof Mesh) { 49 | meshes.push(child); 50 | } 51 | }); 52 | 53 | const { success, navMesh } = threeToSoloNavMesh(meshes, { 54 | cs: 0.05, 55 | ch: 0.2, 56 | }); 57 | 58 | if (!success) return; 59 | 60 | const navMeshQuery = new NavMeshQuery(navMesh); 61 | 62 | setNavMesh(navMesh); 63 | setNavMeshQuery(navMeshQuery); 64 | 65 | return () => { 66 | setNavMesh(undefined); 67 | setNavMeshQuery(undefined); 68 | 69 | navMeshQuery.destroy(); 70 | navMesh.destroy(); 71 | }; 72 | }, [group]); 73 | 74 | useEffect(() => { 75 | newPoint(); 76 | }, [navMeshQuery]); 77 | 78 | return ( 79 | <> 80 | 81 | 82 | 83 | 84 | {point && ( 85 | 86 | 87 | 88 | 89 | )} 90 | 91 | 92 | 93 | 94 | 95 | 96 |
104 | 115 |
116 |
117 | 118 | ); 119 | }; 120 | -------------------------------------------------------------------------------- /packages/recast-navigation/.storybook/stories/nav-mesh/custom-bounds.stories.tsx: -------------------------------------------------------------------------------- 1 | import { OrbitControls } from '@react-three/drei'; 2 | import { NavMesh, NavMeshQuery } from '@recast-navigation/core'; 3 | import { threeToSoloNavMesh } from '@recast-navigation/three'; 4 | import React, { useEffect, useState } from 'react'; 5 | import { Box3, Group, Mesh } from 'three'; 6 | import { Debug } from '../../common/debug'; 7 | import { NavTestEnvironment } from '../../common/nav-test-environment'; 8 | import { decorators } from '../../decorators'; 9 | import { parameters } from '../../parameters'; 10 | 11 | export default { 12 | title: 'NavMesh / Custom Bounds', 13 | decorators, 14 | parameters, 15 | }; 16 | 17 | const BOUNDS = new Box3(); 18 | BOUNDS.min.set(-3, -1, -5); 19 | BOUNDS.max.set(9, 5, 3); 20 | 21 | export const CustomBounds = () => { 22 | const [group, setGroup] = useState(null); 23 | 24 | const [navMesh, setNavMesh] = useState(); 25 | const [navMeshQuery, setNavMeshQuery] = useState(); 26 | 27 | useEffect(() => { 28 | if (!group) return; 29 | 30 | const meshes: Mesh[] = []; 31 | 32 | group.traverse((child) => { 33 | if (child instanceof Mesh) { 34 | meshes.push(child); 35 | } 36 | }); 37 | 38 | const walkableRadiusWorld = 0.1; 39 | const cellSize = 0.05; 40 | 41 | const { success, navMesh } = threeToSoloNavMesh(meshes, { 42 | cs: cellSize, 43 | ch: 0.2, 44 | walkableRadius: Math.ceil(walkableRadiusWorld / cellSize), 45 | bounds: [BOUNDS.min.toArray(), BOUNDS.max.toArray()], 46 | }); 47 | 48 | if (!success) { 49 | return; 50 | } 51 | 52 | setNavMesh(navMesh); 53 | setNavMeshQuery(navMeshQuery); 54 | 55 | return () => { 56 | navMesh.destroy(); 57 | 58 | setNavMesh(undefined); 59 | setNavMeshQuery(undefined); 60 | }; 61 | }, [group]); 62 | 63 | return ( 64 | <> 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | ); 76 | }; 77 | -------------------------------------------------------------------------------- /packages/recast-navigation/.storybook/stories/nav-mesh/non-indexed-geometry.stories.tsx: -------------------------------------------------------------------------------- 1 | import { Bounds, OrbitControls } from '@react-three/drei'; 2 | import { NavMesh } from '@recast-navigation/core'; 3 | import { threeToSoloNavMesh } from '@recast-navigation/three'; 4 | import React, { useEffect, useState } from 'react'; 5 | import { BoxGeometry, Group, Mesh } from 'three'; 6 | import { Debug } from '../../common/debug'; 7 | import { decorators } from '../../decorators'; 8 | import { parameters } from '../../parameters'; 9 | 10 | export default { 11 | title: 'NavMesh / Non Indexed Geometry', 12 | decorators, 13 | parameters, 14 | }; 15 | 16 | const boxGeometry = new BoxGeometry(10, 10, 10); 17 | const nonIndexedBoxGeometry = boxGeometry.clone().toNonIndexed(); 18 | 19 | export const NonIndexedGeometry = () => { 20 | const [group, setGroup] = useState(null); 21 | 22 | const [navMesh, setNavMesh] = useState(); 23 | 24 | useEffect(() => { 25 | if (!group) return; 26 | 27 | const meshes: Mesh[] = []; 28 | 29 | group.traverse((child) => { 30 | if (child instanceof Mesh) { 31 | meshes.push(child); 32 | } 33 | }); 34 | 35 | const { navMesh } = threeToSoloNavMesh(meshes, { 36 | cs: 0.15, 37 | ch: 0.2, 38 | walkableRadius: 0.6, 39 | walkableClimb: 2.1, 40 | walkableSlopeAngle: 60, 41 | }); 42 | 43 | setNavMesh(navMesh); 44 | 45 | return () => { 46 | setNavMesh(undefined); 47 | }; 48 | }, [group]); 49 | 50 | return ( 51 | <> 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | ); 71 | }; 72 | -------------------------------------------------------------------------------- /packages/recast-navigation/.storybook/stories/nav-mesh/walkable-slope-angle.stories.tsx: -------------------------------------------------------------------------------- 1 | import { Bounds, OrbitControls } from '@react-three/drei'; 2 | import { NavMesh } from '@recast-navigation/core'; 3 | import { threeToSoloNavMesh } from '@recast-navigation/three'; 4 | import { useControls } from 'leva'; 5 | import React, { useEffect, useState } from 'react'; 6 | import { Group, Mesh } from 'three'; 7 | import { DEG2RAD } from 'three/src/math/MathUtils.js'; 8 | import { Debug } from '../../common/debug'; 9 | import { decorators } from '../../decorators'; 10 | import { parameters } from '../../parameters'; 11 | 12 | export default { 13 | title: 'NavMesh / Walkable Slope', 14 | decorators, 15 | parameters, 16 | }; 17 | 18 | const DEGREES = [0, 5, 10, 15, 20, 25, 30, 35, 40, 45]; 19 | 20 | const ANGLES = DEGREES.map((angle, idx) => ({ 21 | degrees: angle, 22 | rad: angle * DEG2RAD, 23 | position: [(-DEGREES.length * 5) / 2 + idx * 5, 0, 0] as const, 24 | })); 25 | 26 | export const WalkableSlope = () => { 27 | const [group, setGroup] = useState(null); 28 | 29 | const [navMesh, setNavMesh] = useState(); 30 | 31 | const { walkableSlopeAngle } = useControls('walkable-slope-angle', { 32 | walkableSlopeAngle: { 33 | value: 24, 34 | step: 1, 35 | min: 5, 36 | max: 50, 37 | }, 38 | }); 39 | 40 | useEffect(() => { 41 | if (!group) return; 42 | 43 | const meshes: Mesh[] = []; 44 | 45 | group.traverse((child) => { 46 | if (child instanceof Mesh) { 47 | meshes.push(child); 48 | } 49 | }); 50 | 51 | const { navMesh } = threeToSoloNavMesh(meshes, { 52 | cs: 0.05, 53 | ch: 0.05, 54 | walkableSlopeAngle, 55 | }); 56 | 57 | setNavMesh(navMesh); 58 | 59 | return () => { 60 | setNavMesh(undefined); 61 | }; 62 | }, [group, walkableSlopeAngle]); 63 | 64 | return ( 65 | <> 66 | 67 | 68 | {ANGLES.map(({ degrees, rad, position }) => ( 69 | 70 | 71 | 72 | 73 | ))} 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | ); 82 | }; 83 | -------------------------------------------------------------------------------- /packages/recast-navigation/.storybook/stories/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// -------------------------------------------------------------------------------- /packages/recast-navigation/.storybook/styles.css: -------------------------------------------------------------------------------- 1 | body { 2 | padding: 0 !important; /* :) */ 3 | } -------------------------------------------------------------------------------- /packages/recast-navigation/LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | Copyright © 2025 Isaac Mason 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /packages/recast-navigation/cover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/isaac-mason/recast-navigation-js/416da48186b9e95736c7e1555e40858c610d78b1/packages/recast-navigation/cover.png -------------------------------------------------------------------------------- /packages/recast-navigation/eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import config from '@isaac-mason/eslint-config-typescript' 2 | 3 | export default [...config] 4 | -------------------------------------------------------------------------------- /packages/recast-navigation/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "recast-navigation", 3 | "description": "Recast Navigation for JavaScript!", 4 | "keywords": [ 5 | "recastnavigation", 6 | "recast-navigation", 7 | "recast", 8 | "detour", 9 | "navmesh", 10 | "pathfinding", 11 | "navigation", 12 | "crowd-simulation", 13 | "wasm", 14 | "webassembly" 15 | ], 16 | "author": { 17 | "name": "Isaac Mason", 18 | "email": "isaac@isaacmason.com", 19 | "url": "https://isaacmason.com/" 20 | }, 21 | "version": "0.39.0", 22 | "license": "MIT", 23 | "homepage": "https://github.com/isaac-mason/recast-navigation-js", 24 | "bugs": { 25 | "url": "https://github.com/isaac-mason/recast-navigation-js/issues" 26 | }, 27 | "type": "module", 28 | "main": "./index.mjs", 29 | "module": "./index.mjs", 30 | "types": "./index.d.ts", 31 | "exports": { 32 | ".": { 33 | "types": "./index.d.ts", 34 | "import": "./index.mjs", 35 | "default": "./index.mjs" 36 | }, 37 | "./generators": { 38 | "types": "./generators.d.ts", 39 | "import": "./generators.mjs", 40 | "default": "./generators.mjs" 41 | } 42 | }, 43 | "files": [ 44 | "index.mjs", 45 | "index.mjs.map", 46 | "index.d.ts", 47 | "generators.mjs", 48 | "generators.mjs.map", 49 | "generators.d.ts", 50 | "LICENSE", 51 | "README.md" 52 | ], 53 | "scripts": { 54 | "clean": "(rm index.mjs index.d.ts generators.mjs generators.d.ts || true)", 55 | "build": "yarn clean && rollup --config rollup.config.js --bundleConfigAsCjs", 56 | "storybook": "storybook dev -p 6006", 57 | "build-storybook": "storybook build", 58 | "test": "tsc && vitest run", 59 | "lint": "eslint src" 60 | }, 61 | "dependencies": { 62 | "@recast-navigation/core": "0.39.0", 63 | "@recast-navigation/generators": "0.39.0" 64 | }, 65 | "devDependencies": { 66 | "@babel/core": "^7.24.5", 67 | "@babel/preset-env": "^7.24.5", 68 | "@babel/preset-typescript": "^7.24.1", 69 | "@isaac-mason/eslint-config-typescript": "^0.0.9", 70 | "@mdx-js/react": "^2.3.0", 71 | "@pmndrs/assets": "^1.6.0", 72 | "@react-three/drei": "^9.105.4", 73 | "@react-three/fiber": "^8.16.3", 74 | "@rollup/plugin-babel": "^6.0.4", 75 | "@rollup/plugin-commonjs": "^25.0.7", 76 | "@rollup/plugin-node-resolve": "^15.0.1", 77 | "@rollup/plugin-typescript": "^11.1.6", 78 | "@storybook/addon-actions": "^8.0.9", 79 | "@storybook/addon-essentials": "^8.0.9", 80 | "@storybook/addon-interactions": "^8.0.9", 81 | "@storybook/addon-links": "^8.0.9", 82 | "@storybook/addon-mdx-gfm": "^8.0.9", 83 | "@storybook/react": "^8.0.9", 84 | "@storybook/react-vite": "^8.0.9", 85 | "@types/react": "^18.3.1", 86 | "@types/react-dom": "^18.2.8", 87 | "@types/three": "^0.172.0", 88 | "@vitejs/plugin-react": "^4.2.1", 89 | "eslint": "^9.6.0", 90 | "leva": "^0.9.35", 91 | "react": "^18.3.1", 92 | "react-dom": "^18.2.0", 93 | "rollup": "^4.14.0", 94 | "rollup-plugin-copy": "^3.4.0", 95 | "rollup-plugin-filesize": "^10.0.0", 96 | "storybook": "^8.0.9", 97 | "suspend-react": "^0.1.3", 98 | "three": "^0.172.0", 99 | "three-pathfinding": "^1.2.0", 100 | "typescript": "^5.4.3", 101 | "vite": "^5.4.8", 102 | "vitest": "^2.1.2", 103 | "zustand": "^4.5.2" 104 | }, 105 | "packageManager": "yarn@3.2.2" 106 | } 107 | -------------------------------------------------------------------------------- /packages/recast-navigation/rollup.config.js: -------------------------------------------------------------------------------- 1 | import babel from '@rollup/plugin-babel'; 2 | import commonjs from '@rollup/plugin-commonjs'; 3 | import resolve from '@rollup/plugin-node-resolve'; 4 | import typescript from '@rollup/plugin-typescript'; 5 | import path from 'path'; 6 | import filesize from 'rollup-plugin-filesize'; 7 | 8 | const babelOptions = { 9 | babelrc: false, 10 | extensions: ['.ts'], 11 | exclude: '**/node_modules/**', 12 | babelHelpers: 'bundled', 13 | presets: [ 14 | [ 15 | '@babel/preset-env', 16 | { 17 | loose: true, 18 | modules: false, 19 | targets: '>1%, not dead, not ie 11, not op_mini all', 20 | }, 21 | ], 22 | '@babel/preset-typescript', 23 | ], 24 | }; 25 | 26 | const plugins = [ 27 | resolve(), 28 | commonjs(), 29 | typescript({ 30 | tsconfig: path.resolve(__dirname, `tsconfig.json`), 31 | emitDeclarationOnly: true, 32 | }), 33 | babel(babelOptions), 34 | filesize(), 35 | ]; 36 | 37 | const entrypoint = ({ name, external }) => ({ 38 | input: `./src/${name}.ts`, 39 | external, 40 | output: [ 41 | { 42 | file: `./${name}.mjs`, 43 | format: 'es', 44 | }, 45 | ], 46 | plugins, 47 | }); 48 | 49 | export default [ 50 | entrypoint({ name: 'index', external: ['@recast-navigation/core'] }), 51 | entrypoint({ 52 | name: 'generators', 53 | external: ['@recast-navigation/core', '@recast-navigation/generators'], 54 | }), 55 | ]; 56 | -------------------------------------------------------------------------------- /packages/recast-navigation/src/generators.ts: -------------------------------------------------------------------------------- 1 | export * from '@recast-navigation/generators'; 2 | -------------------------------------------------------------------------------- /packages/recast-navigation/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from '@recast-navigation/core'; 2 | -------------------------------------------------------------------------------- /packages/recast-navigation/src/types.d.ts: -------------------------------------------------------------------------------- 1 | import type { Object3DNode } from '@react-three/fiber'; 2 | 3 | declare global { 4 | namespace JSX { 5 | interface IntrinsicElements { 6 | /** `object` assumed to extend `THREE.Object3D` */ 7 | primitive: Object3DNode & { object: THREE.Object3D }; 8 | } 9 | } 10 | } 11 | 12 | export {} 13 | -------------------------------------------------------------------------------- /packages/recast-navigation/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2022", 4 | "useDefineForClassFields": true, 5 | "module": "ES2022", 6 | "lib": ["ES2022", "DOM"], 7 | "moduleResolution": "Bundler", 8 | "strict": true, 9 | "resolveJsonModule": true, 10 | "isolatedModules": true, 11 | "esModuleInterop": true, 12 | "noEmit": true, 13 | "declaration": true, 14 | "noUnusedLocals": true, 15 | "noUnusedParameters": true, 16 | "noImplicitReturns": true, 17 | "skipLibCheck": true, 18 | "baseUrl": "./", 19 | "rootDir": "./src", 20 | "allowSyntheticDefaultImports": true, 21 | "outDir": ".", 22 | }, 23 | "files": ["./src/index.ts", "./src/generators.ts"] 24 | } 25 | -------------------------------------------------------------------------------- /packages/recast-navigation/tst/crowd.spec.ts: -------------------------------------------------------------------------------- 1 | import { Crowd, NavMesh, init } from 'recast-navigation'; 2 | import { generateSoloNavMesh } from 'recast-navigation/generators'; 3 | import { BoxGeometry, BufferAttribute, Mesh } from 'three'; 4 | import { beforeEach, describe, expect, test } from 'vitest'; 5 | import { expectVectorToBeCloseTo } from './utils'; 6 | 7 | describe('Crowd', () => { 8 | let navMesh: NavMesh; 9 | let crowd: Crowd; 10 | 11 | beforeEach(async () => { 12 | await init(); 13 | 14 | const mesh = new Mesh(new BoxGeometry(5, 0.1, 5)); 15 | 16 | const positions = ( 17 | mesh.geometry.getAttribute('position') as BufferAttribute 18 | ).array; 19 | const indices = mesh.geometry.getIndex()!.array; 20 | 21 | const result = generateSoloNavMesh(positions, indices); 22 | 23 | if (!result.success) throw new Error('nav mesh generation failed'); 24 | 25 | navMesh = result.navMesh; 26 | 27 | crowd = new Crowd(navMesh, { 28 | maxAgents: 10, 29 | maxAgentRadius: 0.5, 30 | }); 31 | }); 32 | 33 | test('goto', () => { 34 | const agent = crowd.addAgent( 35 | { x: 0, y: 0, z: 0 }, 36 | { 37 | radius: 0.5, 38 | } 39 | ); 40 | 41 | expectVectorToBeCloseTo(agent.position(), { x: 0, y: 0, z: 0 }, 0.3); 42 | 43 | agent.requestMoveTarget({ x: 2, y: 0, z: 2 }); 44 | 45 | for (let i = 0; i < 120; i++) { 46 | crowd.update(1 / 60); 47 | } 48 | 49 | expectVectorToBeCloseTo(agent.position(), { x: 2, y: 0, z: 2 }, 0.3); 50 | }); 51 | 52 | test('teleport', () => { 53 | const agent = crowd.addAgent( 54 | { x: 0, y: 0, z: 0 }, 55 | { 56 | radius: 0.5, 57 | } 58 | ); 59 | 60 | expectVectorToBeCloseTo(agent.position(), { x: 0, y: 0, z: 0 }, 0.3); 61 | 62 | agent.teleport({ x: 2, y: 0, z: 2 }); 63 | 64 | expectVectorToBeCloseTo(agent.position(), { x: 2, y: 0, z: 2 }, 0.3); 65 | }); 66 | 67 | test('parameter getters and setters', () => { 68 | const agent = crowd.addAgent( 69 | { x: 0, y: 0, z: 0 }, 70 | { 71 | radius: 0.5, 72 | } 73 | ); 74 | 75 | expect(agent.radius).toBeCloseTo(0.5); 76 | 77 | agent.radius = 0.2; 78 | 79 | expect(agent.radius).toBeCloseTo(0.2); 80 | }); 81 | }); 82 | -------------------------------------------------------------------------------- /packages/recast-navigation/tst/nav-mesh-query.spec.ts: -------------------------------------------------------------------------------- 1 | import { NavMesh, NavMeshQuery, init } from 'recast-navigation'; 2 | import { generateSoloNavMesh } from 'recast-navigation/generators'; 3 | import { BoxGeometry, BufferAttribute, Mesh } from 'three'; 4 | import { beforeEach, describe, test, expect } from 'vitest'; 5 | import { expectVectorToBeCloseTo } from './utils'; 6 | 7 | describe('NavMeshQuery', () => { 8 | let navMesh: NavMesh; 9 | let navMeshQuery: NavMeshQuery; 10 | 11 | beforeEach(async () => { 12 | await init(); 13 | 14 | const mesh = new Mesh(new BoxGeometry(5, 0.1, 5)); 15 | 16 | const positions = ( 17 | mesh.geometry.getAttribute('position') as BufferAttribute 18 | ).array; 19 | const indices = mesh.geometry.getIndex()!.array; 20 | 21 | const result = generateSoloNavMesh(positions, indices); 22 | 23 | if (!result.success) throw new Error('nav mesh generation failed'); 24 | 25 | navMesh = result.navMesh; 26 | 27 | navMeshQuery = new NavMeshQuery(navMesh); 28 | }); 29 | 30 | test('findClosestPoint', () => { 31 | const { point: closestPoint } = navMeshQuery.findClosestPoint({ 32 | x: 2, 33 | y: 1, 34 | z: 2, 35 | }); 36 | 37 | expect(closestPoint.x).toBe(2); 38 | expect(closestPoint.y).toBeCloseTo(0.15, 0.01); 39 | expect(closestPoint.z).toBe(2); 40 | }); 41 | 42 | test('computePath', () => { 43 | const { point: start } = navMeshQuery.findClosestPoint({ 44 | x: -2, 45 | y: 0, 46 | z: -2, 47 | }); 48 | 49 | const { point: end } = navMeshQuery.findClosestPoint({ 50 | x: 2, 51 | y: 0, 52 | z: 2, 53 | }); 54 | 55 | const { path } = navMeshQuery.computePath(start, end); 56 | 57 | expect(path.length).toBeGreaterThan(0); 58 | 59 | expectVectorToBeCloseTo(path[0], start, 0.01); 60 | 61 | expectVectorToBeCloseTo(path[path.length - 1], end, 0.01); 62 | }); 63 | }); 64 | -------------------------------------------------------------------------------- /packages/recast-navigation/tst/utils.ts: -------------------------------------------------------------------------------- 1 | import { expect } from 'vitest'; 2 | import { Vector3 } from '@recast-navigation/core/src'; 3 | 4 | export const expectVectorToBeCloseTo = ( 5 | expected: Vector3, 6 | actual: Vector3, 7 | numDigits?: number 8 | ) => { 9 | expect(expected.x).toBeCloseTo(actual.x, numDigits); 10 | expect(expected.y).toBeCloseTo(actual.y, numDigits); 11 | expect(expected.z).toBeCloseTo(actual.z, numDigits); 12 | }; 13 | -------------------------------------------------------------------------------- /packages/recast-navigation/vite.config.mts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "vite"; 2 | import react from "@vitejs/plugin-react"; 3 | 4 | export default defineConfig({ 5 | plugins: [react()], 6 | }); -------------------------------------------------------------------------------- /packages/recast-navigation/vitest.config.mts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vitest/config'; 2 | 3 | export default defineConfig({ 4 | test: { 5 | name: 'recast-navigation', 6 | }, 7 | }); 8 | --------------------------------------------------------------------------------