diff --git a/.editorconfig b/.editorconfig index 6e87a003..9b735217 100644 --- a/.editorconfig +++ b/.editorconfig @@ -4,7 +4,7 @@ root = true [*] charset = utf-8 indent_style = space -indent_size = 2 +indent_size = 4 insert_final_newline = true trim_trailing_whitespace = true diff --git a/.gitignore b/.gitignore index 9bc7b66c..6cb7b7e4 100644 --- a/.gitignore +++ b/.gitignore @@ -45,4 +45,5 @@ Thumbs.db .nx/workspace-data vite.config.*.timestamp* -vitest.config.*.timestamp* \ No newline at end of file +vitest.config.*.timestamp* +.aider* diff --git a/.prettierrc b/.prettierrc index e8c9c5a4..c79dd6fa 100644 --- a/.prettierrc +++ b/.prettierrc @@ -1,7 +1,7 @@ { "singleQuote": true, "useTabs": true, - "tabWidth": 2, + "tabWidth": 4, "printWidth": 120, "plugins": ["prettier-plugin-organize-imports"], "htmlWhitespaceSensitivity": "ignore", diff --git a/CHANGELOG.md b/CHANGELOG.md index fad34ee7..06270c50 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,1234 @@ +## 4.0.0-next.106 (2025-05-18) + +### 💅 Refactors + +- **core:** perf improvement to events ([a0330e22](https://github.com/angular-threejs/angular-three/commit/a0330e22)) + +### ❤️ Thank You + +- Chau @nartc + +## 4.0.0-next.105 (2025-05-17) + +### 🚀 Features + +- **core:** configurable max notification skip count ([2554a59e](https://github.com/angular-threejs/angular-three/commit/2554a59e)) + +### 💅 Refactors + +- **core:** some perf improvements ([154e9b97](https://github.com/angular-threejs/angular-three/commit/154e9b97)) + +### ❤️ Thank You + +- Chau @nartc + +## 4.0.0-next.104 (2025-05-17) + +### 🚀 Features + +- **core:** bump three version ([e7d2f1d0](https://github.com/angular-threejs/angular-three/commit/e7d2f1d0)) + +### 🩹 Fixes + +- **core:** bump threejs 177 ([b2151d77](https://github.com/angular-threejs/angular-three/commit/b2151d77)) +- **plugin:** bump nx 21 ([459feceb](https://github.com/angular-threejs/angular-three/commit/459feceb)) +- **rapier:** debug geometry works with webgpu ([9ef1a1f5](https://github.com/angular-threejs/angular-three/commit/9ef1a1f5)) +- **rapier:** bump dimforge ([02bb4189](https://github.com/angular-threejs/angular-three/commit/02bb4189)) + +### 💅 Refactors + +- **core:** clean up ([e42b7273](https://github.com/angular-threejs/angular-three/commit/e42b7273)) + +### ❤️ Thank You + +- Chau @nartc + +## 4.0.0-next.103 (2025-04-07) + +### 🩹 Fixes + +- **plugin/gltf:** use new symbols for gltf generator ([16492ac5](https://github.com/angular-threejs/angular-three/commit/16492ac5)) + +### ❤️ Thank You + +- Chau + +## 4.0.0-next.102 (2025-04-07) + +### 🩹 Fixes + +- **core:** set check when resolve pierced props ([5d89ae1c](https://github.com/angular-threejs/angular-three/commit/5d89ae1c)) +- **soba/materials:** granularity with blur signals ([f9726701](https://github.com/angular-threejs/angular-three/commit/f9726701)) + +### ❤️ Thank You + +- Chau + +## 4.0.0-next.101 (2025-04-02) + +### 🚀 Features + +- **core/dom:** expose HTMLCanvasElement and host element through template context ([c56de317](https://github.com/angular-threejs/angular-three/commit/c56de317)) + +### ❤️ Thank You + +- Chau + +## 4.0.0-next.100 (2025-04-02) + +### 💅 Refactors + +- **core:** remove unnecessary portal injectors handling ([79970722](https://github.com/angular-threejs/angular-three/commit/79970722)) +- **core:** clean up ([e60aa62c](https://github.com/angular-threejs/angular-three/commit/e60aa62c)) + +### ❤️ Thank You + +- Chau + +## 4.0.0-next.99 (2025-03-25) + +### 🚀 Features + +- **core:** fill catalogue with symbols from THREE automatically ([3ce752cf](https://github.com/angular-threejs/angular-three/commit/3ce752cf)) +- **core:** add fn to remove keys from catalogue; extend returns a clean up fn ([651cfe2b](https://github.com/angular-threejs/angular-three/commit/651cfe2b)) + +### 🩹 Fixes + +- **core/testing:** remove extend(THREE) from testing ([de0e0db0](https://github.com/angular-threejs/angular-three/commit/de0e0db0)) +- **soba/abstractions:** gradienttexture extends canvas-texture instead of pure texture ([1641396a](https://github.com/angular-threejs/angular-three/commit/1641396a)) + +### 💅 Refactors + +- **soba/misc:** use onCleanup to clean up fbo embedded view ([771d55f2](https://github.com/angular-threejs/angular-three/commit/771d55f2)) +- **soba/misc:** use computed for animations API instead of effect ([3d937816](https://github.com/angular-threejs/angular-three/commit/3d937816)) +- **soba/misc:** use deeply nested signal on htmlContent ([d200e3eb](https://github.com/angular-threejs/angular-three/commit/d200e3eb)) + +### ❤️ Thank You + +- nartc + +## 4.0.0-next.98 (2025-03-22) + +### 🚀 Features + +- **core:** rename injectObjectEvents to objectEvents ([5a707562](https://github.com/angular-threejs/angular-three/commit/5a707562)) +- **core:** rename injectBeforeRender to beforeRender ([5ab9a3e7](https://github.com/angular-threejs/angular-three/commit/5ab9a3e7)) +- **core,soba/loaders,soba/staging:** implement resources ([79184dce](https://github.com/angular-threejs/angular-three/commit/79184dce)) +- **rapier:** rename joints to remove inject prefix ([739041a2](https://github.com/angular-threejs/angular-three/commit/739041a2)) +- **soba/abstractions:** rename injectHelper to helper ([bcfefe0a](https://github.com/angular-threejs/angular-three/commit/bcfefe0a)) +- **soba/cameras:** rename injectCubeCamera t cubeCamera ([e14e1dd1](https://github.com/angular-threejs/angular-three/commit/e14e1dd1)) +- **soba/loaders:** rename injectProgress to progress ([e2b05920](https://github.com/angular-threejs/angular-three/commit/e2b05920)) +- **soba/misc:** rename inject* to just function name ([450a8c7b](https://github.com/angular-threejs/angular-three/commit/450a8c7b)) +- **soba/misc:** rename injectIntersect to intersect ([d18fa11d](https://github.com/angular-threejs/angular-three/commit/d18fa11d)) +- **soba/staging:** rename injectMatcapTexture to matcapTextureResource; and injectNormalTexture ([5e4b6f67](https://github.com/angular-threejs/angular-three/commit/5e4b6f67)) + +### 🩹 Fixes + +- **soba/staging:** soba build ([920f7566](https://github.com/angular-threejs/angular-three/commit/920f7566)) + +### 💅 Refactors + +- **soba/loaders:** use fontResource in injectFont ([48ba0c7a](https://github.com/angular-threejs/angular-three/commit/48ba0c7a)) +- **soba/loaders:** use fbxResource in injectFbx ([5a0ddd41](https://github.com/angular-threejs/angular-three/commit/5a0ddd41)) +- **soba/staging:** rename injectSpotLightCommon to spotLightCommon ([905dd333](https://github.com/angular-threejs/angular-three/commit/905dd333)) +- **soba/staging:** use environmentResource in injectEnvironment ([eadbf535](https://github.com/angular-threejs/angular-three/commit/eadbf535)) + +### ❤️ Thank You + +- nartc + +## 4.0.0-next.97 (2025-03-17) + +### 🚀 Features + +- **theatre:** change sheet to a Directive ([99625b16](https://github.com/angular-threejs/angular-three/commit/99625b16)) + +### 💅 Refactors + +- **soba:** clean up ([13815128](https://github.com/angular-threejs/angular-three/commit/13815128)) +- **soba/abstractions:** use pierced props for extensions derivatives on grid ([14a89040](https://github.com/angular-threejs/angular-three/commit/14a89040)) +- **soba/cameras:** bring makeDefault effect on top for orthographic ([52a24031](https://github.com/angular-threejs/angular-three/commit/52a24031)) +- **soba/gizmos:** use injectObjectEvents for all events for optimizing CDR (for now) ([7dc8b027](https://github.com/angular-threejs/angular-three/commit/7dc8b027)) +- **soba/staging:** use pierced props for sky ([c2ad7a69](https://github.com/angular-threejs/angular-three/commit/c2ad7a69)) +- **soba/staging:** use pierced props for spot light volumetric mesh ([d03e5cdd](https://github.com/angular-threejs/angular-three/commit/d03e5cdd)) +- **theatre:** use injectObjectEvents for transform events ([ebcb6f7c](https://github.com/angular-threejs/angular-three/commit/ebcb6f7c)) + +### ❤️ Thank You + +- nartc + +## 4.0.0-next.96 (2025-03-16) + +### 🩹 Fixes + +- **theatre:** exportAs; expose sheetObject on transform and sync; ([ac14210e](https://github.com/angular-threejs/angular-three/commit/ac14210e)) + +### 💅 Refactors + +- **soba/cameras:** adjust perspective camera code ([41b94287](https://github.com/angular-threejs/angular-three/commit/41b94287)) + +### ❤️ Thank You + +- nartc + +## 4.0.0-next.95 (2025-03-15) + +### 🩹 Fixes + +- **theatre:** fix studio import ([2cb857bf](https://github.com/angular-threejs/angular-three/commit/2cb857bf)) + +### ❤️ Thank You + +- nartc + +## 4.0.0-next.94 (2025-03-15) + +### 🩹 Fixes + +- **theatre:** use studio to initialize ([1b517a30](https://github.com/angular-threejs/angular-three/commit/1b517a30)) + +### ❤️ Thank You + +- nartc + +## 4.0.0-next.93 (2025-03-15) + +### 🚀 Features + +- **core:** remove unused code, expose resolveInstanceKey in applyProps ([0bdd9a7f](https://github.com/angular-threejs/angular-three/commit/0bdd9a7f)) +- **plugin:** add tweakpane to migration group ([3dee9634](https://github.com/angular-threejs/angular-three/commit/3dee9634)) +- **plugin:** add theatre to update groups ([1b5c0559](https://github.com/angular-threejs/angular-three/commit/1b5c0559)) +- **theatre:** add theatrejs integration ([87be9501](https://github.com/angular-threejs/angular-three/commit/87be9501)) +- **tweakpane:** rename tweakpane ([54926538](https://github.com/angular-threejs/angular-three/commit/54926538)) + +### 🩹 Fixes + +- **core:** ignore keys.indexOf in loader (need to revisit why) ([f560747a](https://github.com/angular-threejs/angular-three/commit/f560747a)) + +### ❤️ Thank You + +- nartc + +## 4.0.0-next.92 (2025-03-13) + +### 🚀 Features + +- **soba/controls:** remove unused events input from camera controls ([459fd1b0](https://github.com/angular-threejs/angular-three/commit/459fd1b0)) + +### 🩹 Fixes + +- **soba/performances:** hide bvh group when retrying setting up geometries ([bc28579d](https://github.com/angular-threejs/angular-three/commit/bc28579d)) + +### ❤️ Thank You + +- nartc + +## 4.0.0-next.91 (2025-03-11) + +### 🚀 Features + +- **rapier:** update peer deps on rapier3d-compat ([b42e76c6](https://github.com/angular-threejs/angular-three/commit/b42e76c6)) + +### ❤️ Thank You + +- nartc + +## 4.0.0-next.90 (2025-03-11) + +### 💅 Refactors + +- **tweakpane:** rename startChangeEffect to sync ([659ff189](https://github.com/angular-threejs/angular-three/commit/659ff189)) + +### ❤️ Thank You + +- nartc + +## 4.0.0-next.89 (2025-03-11) + +### 💅 Refactors + +- **tweakpane:** use computed for a lot of bindings stuff instead of set signal ([5d77340b](https://github.com/angular-threejs/angular-three/commit/5d77340b)) + +### ❤️ Thank You + +- nartc + +## 4.0.0-next.88 (2025-03-11) + +### 🩹 Fixes + +- **soba/abstractions:** add ngts-text-3D as selector for text3d ([c7d7b5c2](https://github.com/angular-threejs/angular-three/commit/c7d7b5c2)) +- **soba/cameras:** expose fbo.texture as camera content context instead of fbo ([5552d810](https://github.com/angular-threejs/angular-three/commit/5552d810)) + +### 💅 Refactors + +- **soba/staging:** use protected ([efaba5b2](https://github.com/angular-threejs/angular-three/commit/efaba5b2)) + +### ❤️ Thank You + +- nartc + +## 4.0.0-next.87 (2025-03-09) + +This was a version bump only, there were no code changes. + +## 4.0.0-next.86 (2025-03-09) + +This was a version bump only, there were no code changes. + +## 4.0.0-next.85 (2025-03-09) + +### 🩹 Fixes + +- **core:** forgot to update generation ([eb09c7c5](https://github.com/angular-threejs/angular-three/commit/eb09c7c5)) + +### ❤️ Thank You + +- nartc + +## 4.0.0-next.84 (2025-03-09) + +### 🩹 Fixes + +- **core:** add missing skinned-mesh and bone to three types ([e04fc3d5](https://github.com/angular-threejs/angular-three/commit/e04fc3d5)) + +### ❤️ Thank You + +- nartc + +## 4.0.0-next.83 (2025-03-08) + +### 🩹 Fixes + +- **core:** check for handlers before trying to remove event from it ([a5dfa497](https://github.com/angular-threejs/angular-three/commit/a5dfa497)) + +### ❤️ Thank You + +- nartc + +## 4.0.0-next.82 (2025-03-08) + +### 🩹 Fixes + +- **soba/abstractions:** use viewChild on Text to set NgtObjectEvents ([d8780af3](https://github.com/angular-threejs/angular-three/commit/d8780af3)) + +### ❤️ Thank You + +- nartc + +## 4.0.0-next.81 (2025-03-08) + +### 🩹 Fixes + +- **core:** check against ngt-primitive type to prepare ngt-primitive ([0bfaab68](https://github.com/angular-threejs/angular-three/commit/0bfaab68)) +- **soba/abstractions:** set ngtObjectEvents for Text in effect ([55f2a2de](https://github.com/angular-threejs/angular-three/commit/55f2a2de)) + +### 💅 Refactors + +- **core:** rename ([2045bd31](https://github.com/angular-threejs/angular-three/commit/2045bd31)) + +### ❤️ Thank You + +- nartc + +## 4.0.0-next.80 (2025-03-08) + +### 🚀 Features + +- **soba/abstractions:** expose NgtObjectEvents on Text ([ed877f20](https://github.com/angular-threejs/angular-three/commit/ed877f20)) + +### ❤️ Thank You + +- nartc + +## 4.0.0-next.79 (2025-03-08) + +### 🚀 Features + +- **soba/controls:** add TrackballControls ([30ebfb70](https://github.com/angular-threejs/angular-three/commit/30ebfb70)) + +### 🩹 Fixes + +- **core:** make __ngt_args__ optional ([73bd8cbb](https://github.com/angular-threejs/angular-three/commit/73bd8cbb)) +- **soba/controls:** adjust orbit controls ([4bf89340](https://github.com/angular-threejs/angular-three/commit/4bf89340)) + +### ❤️ Thank You + +- nartc + +## 4.0.0-next.78 (2025-03-06) + +### 🚀 Features + +- ⚠️ **soba/cameras:** update `injectFBO` usage ([efb60cd4](https://github.com/angular-threejs/angular-three/commit/efb60cd4)) +- ⚠️ **soba/misc:** injectFBO no longer recreates the RenderTarget in a computed ([12262c78](https://github.com/angular-threejs/angular-three/commit/12262c78)) +- ⚠️ **soba/misc:** `injectDepthBuffer` returns `DepthTexture` instead of Signal ([53f515dd](https://github.com/angular-threejs/angular-three/commit/53f515dd)) + +### 🩹 Fixes + +- **soba/materials:** update `injectFBO` usages ([c972993e](https://github.com/angular-threejs/angular-three/commit/c972993e)) +- **soba/staging:** update `injectFBO` usages ([6aa59172](https://github.com/angular-threejs/angular-three/commit/6aa59172)) + +### ⚠️ Breaking Changes + +- **soba/cameras:** `NgtsCameraContent` `let-texture` template variable is +- **soba/misc:** `injectDepthBuffer` returns `DepthTexture | null` +- **soba/misc:** `injectFBO` now returns a `WebGLRenderTarget` instead + +### ❤️ Thank You + +- nartc + +## 4.0.0-next.77 (2025-03-05) + +### 🩹 Fixes + +- **tweakpane:** add tweakpane/core to peer dep ([0411bcfc](https://github.com/angular-threejs/angular-three/commit/0411bcfc)) + +### ❤️ Thank You + +- nartc + +## 4.0.0-next.76 (2025-03-05) + +### 🩹 Fixes + +- **tweakpane:** adjust how custom container works ([85a767ff](https://github.com/angular-threejs/angular-three/commit/85a767ff)) + +### ❤️ Thank You + +- nartc + +## 4.0.0-next.75 (2025-03-05) + +### 🚀 Features + +- **core/dom:** expose host element from NgtCanvasContent ([fc0a6a74](https://github.com/angular-threejs/angular-three/commit/fc0a6a74)) +- **tweakpane:** accept optional container ([2422bfc7](https://github.com/angular-threejs/angular-three/commit/2422bfc7)) + +### ❤️ Thank You + +- nartc + +## 4.0.0-next.74 (2025-03-05) + +### 🩹 Fixes + +- **soba/misc:** remove deprecated TextureEncoding ([cb9b676d](https://github.com/angular-threejs/angular-three/commit/cb9b676d)) +- **soba/staging:** remove encoding option in injectEnvironment ([f3205dd6](https://github.com/angular-threejs/angular-three/commit/f3205dd6)) + +### ❤️ Thank You + +- nartc + +## 4.0.0-next.73 (2025-03-05) + +### 🚀 Features + +- **core:** allow provideNgtRenderer to accept options ([2bca496b](https://github.com/angular-threejs/angular-three/commit/2bca496b)) + +### ❤️ Thank You + +- nartc + +## 4.0.0-next.72 (2025-03-05) + +### 🩹 Fixes + +- **soba/abstractions:** edges should be segmented ([064944c7](https://github.com/angular-threejs/angular-three/commit/064944c7)) + +### ❤️ Thank You + +- nartc + +## 4.0.0-next.71 (2025-03-03) + +### 🩹 Fixes + +- **tweakpane:** use any for binding value instead ([5a09ecab](https://github.com/angular-threejs/angular-three/commit/5a09ecab)) + +### ❤️ Thank You + +- nartc + +## 4.0.0-next.70 (2025-03-03) + +### 🚀 Features + +- **soba/abstractions:** add ngts-text-3D as a selector to text3d ([f048731d](https://github.com/angular-threejs/angular-three/commit/f048731d)) + +### 🩹 Fixes + +- **soba/materials:** add null to meshtransmissionmaterial background option ([d377bc80](https://github.com/angular-threejs/angular-three/commit/d377bc80)) +- **soba/staging:** when temporal is false, set a timeout for PLM update in accumulative shadows ([e7b47551](https://github.com/angular-threejs/angular-three/commit/e7b47551)) + +### ❤️ Thank You + +- nartc + +## 4.0.0-next.69 (2025-03-02) + +### 🩹 Fixes + +- **rapier:** convexHull collider should set shape as convexHull ([9a82fcc9](https://github.com/angular-threejs/angular-three/commit/9a82fcc9)) + +### ❤️ Thank You + +- nartc + +## 4.0.0-next.68 (2025-03-02) + +### 🚀 Features + +- **core:** expose meshes on loaded object graph ([9a0ee1db](https://github.com/angular-threejs/angular-three/commit/9a0ee1db)) + +### 🩹 Fixes + +- **core:** generate rawValue binding ([b22e0d70](https://github.com/angular-threejs/angular-three/commit/b22e0d70)) +- **core:** make sure NgtThreeElements contain correct types for non-mutable props ([1948b34b](https://github.com/angular-threejs/angular-three/commit/1948b34b)) +- **soba/cameras:** adjust NgtsCubeCameraOptions type ([3e0899d9](https://github.com/angular-threejs/angular-three/commit/3e0899d9)) +- **soba/materials:** adjust MeshTransmissionMaterialOptions type ([8073c453](https://github.com/angular-threejs/angular-three/commit/8073c453)) +- **soba/staging:** adjust lightSource type for caustic ([efe1e8ab](https://github.com/angular-threejs/angular-three/commit/efe1e8ab)) + +### ❤️ Thank You + +- nartc + +## 4.0.0-next.67 (2025-03-01) + +### 🚀 Features + +- bump three 0.174 ([605816e1](https://github.com/angular-threejs/angular-three/commit/605816e1)) + +### ❤️ Thank You + +- nartc + +## 4.0.0-next.66 (2025-02-28) + +### 🚀 Features + +- **rapier:** add interaction groups and physics step support ([8ba12fe6](https://github.com/angular-threejs/angular-three/commit/8ba12fe6)) +- **rapier/addons:** add attractor in addons secondary entry point ([c7e6e451](https://github.com/angular-threejs/angular-three/commit/c7e6e451)) + +### 🩹 Fixes + +- **core:** add missing geometry three elements ([0f3eb989](https://github.com/angular-threejs/angular-three/commit/0f3eb989)) + +### ❤️ Thank You + +- nartc + +## 4.0.0-next.65 (2025-02-27) + +### 🩹 Fixes + +- **rapier:** make sure args and shape colliders are in sync ([0bedb235](https://github.com/angular-threejs/angular-three/commit/0bedb235)) + +### ❤️ Thank You + +- nartc + +## 4.0.0-next.64 (2025-02-27) + +### 🩹 Fixes + +- **rapier:** more inputs adjustments ([757fa4c4](https://github.com/angular-threejs/angular-three/commit/757fa4c4)) + +### ❤️ Thank You + +- nartc + +## 4.0.0-next.63 (2025-02-27) + +### 🚀 Features + +- ⚠️ **rapier:** make the colliders' args the colliders name themselves ([f6149ca8](https://github.com/angular-threejs/angular-three/commit/f6149ca8)) + +### ⚠️ Breaking Changes + +- **rapier:** this moves the `[args]` binding to colliders name + +### ❤️ Thank You + +- nartc + +## 4.0.0-next.62 (2025-02-27) + +### 🩹 Fixes + +- **rapier:** adjust rigidbody and collider input types ([35c25040](https://github.com/angular-threejs/angular-three/commit/35c25040)) + +### ❤️ Thank You + +- nartc + +## 4.0.0-next.61 (2025-02-26) + +### 🚀 Features + +- **rapier:** allow data to be a function ([0dc863fe](https://github.com/angular-threejs/angular-three/commit/0dc863fe)) + +### 🩹 Fixes + +- **core:** add ngt-scene to three elements collection ([4bdd2801](https://github.com/angular-threejs/angular-three/commit/4bdd2801)) + +### ❤️ Thank You + +- nartc + +## 4.0.0-next.60 (2025-02-24) + +### 🩹 Fixes + +- **core:** adjust NgtRendererLike to also have an optional dispose ([cdd34ed5](https://github.com/angular-threejs/angular-three/commit/cdd34ed5)) +- **core:** add `created` event ([b9cd35aa](https://github.com/angular-threejs/angular-three/commit/b9cd35aa)) +- **soba/staging:** dispose texture on destroy ([050aee53](https://github.com/angular-threejs/angular-three/commit/050aee53)) +- **tweakpane:** use default tweakpane parent element ([2d7120f8](https://github.com/angular-threejs/angular-three/commit/2d7120f8)) + +### ❤️ Thank You + +- nartc + +## 4.0.0-next.59 (2025-02-23) + +This was a version bump only, there were no code changes. + +## 4.0.0-next.58 (2025-02-22) + +### 🚀 Features + +- **soba/performances:** add bvh ([89c1e5ab](https://github.com/angular-threejs/angular-three/commit/89c1e5ab)) + +### 🩹 Fixes + +- **soba/performances:** adjust tracking logic in bvh effect ([883e0f27](https://github.com/angular-threejs/angular-three/commit/883e0f27)) + +### ❤️ Thank You + +- nartc + +## 4.0.0-next.57 (2025-02-20) + +### 🩹 Fixes + +- **core:** rename selection apis ([52fe3404](https://github.com/angular-threejs/angular-three/commit/52fe3404)) +- **postprocessing:** use renamed selection api ([281931d2](https://github.com/angular-threejs/angular-three/commit/281931d2)) + +### ❤️ Thank You + +- nartc + +## 4.0.0-next.56 (2025-02-18) + +This was a version bump only, there were no code changes. + +## 4.0.0-next.55 (2025-02-18) + +### 🩹 Fixes + +- **core:** adjust applyProps to match r3f ([0ea7a285](https://github.com/angular-threejs/angular-three/commit/0ea7a285)) + +### ❤️ Thank You + +- nartc + +## 4.0.0-next.54 (2025-02-18) + +### 🩹 Fixes + +- **soba/cameras:** assign manual on camera instance ([898e9f60](https://github.com/angular-threejs/angular-three/commit/898e9f60)) + +### ❤️ Thank You + +- nartc + +## 4.0.0-next.53 (2025-02-18) + +### 🩹 Fixes + +- **soba/cameras:** clean up cameras ([2f034b73](https://github.com/angular-threejs/angular-three/commit/2f034b73)) + +### ❤️ Thank You + +- nartc + +## 4.0.0-next.52 (2025-02-16) + +### 🩹 Fixes + +- **soba:** use elapsedTime instead of getElapsedTime() ([08c8491c](https://github.com/angular-threejs/angular-three/commit/08c8491c)) + +### ❤️ Thank You + +- nartc + +## 4.0.0-next.51 (2025-02-16) + +### 🩹 Fixes + +- **core:** track the three child on platform parent before passing off to the ancestor ([18094f29](https://github.com/angular-threejs/angular-three/commit/18094f29)) + +### ❤️ Thank You + +- nartc + +## 4.0.0-next.50 (2025-02-14) + +### 🩹 Fixes + +- **tweakpane:** append pane as canvas child ([4220b907](https://github.com/angular-threejs/angular-three/commit/4220b907)) + +### ❤️ Thank You + +- nartc + +## 4.0.0-next.49 (2025-02-14) + +### 🩹 Fixes + +- **rapier:** use boolean coercion ([6308326a](https://github.com/angular-threejs/angular-three/commit/6308326a)) +- **soba/abstractions:** use boolean coercion ([a8c842c5](https://github.com/angular-threejs/angular-three/commit/a8c842c5)) +- **soba/gizmos:** use boolean coercion ([b91128e0](https://github.com/angular-threejs/angular-three/commit/b91128e0)) +- **soba/staging:** use boolean coercions ([852b7761](https://github.com/angular-threejs/angular-three/commit/852b7761)) + +### ❤️ Thank You + +- nartc + +## 4.0.0-next.48 (2025-02-14) + +### 🚀 Features + +- **plugin:** add tweakpane as an aux generator option ([78a04ea2](https://github.com/angular-threejs/angular-three/commit/78a04ea2)) +- **tweakpane:** tweakpane v1 ([54f2a3ef](https://github.com/angular-threejs/angular-three/commit/54f2a3ef)) + +### 🩹 Fixes + +- **tweakpane:** expose hostDirectives as public API ([c0aac2fa](https://github.com/angular-threejs/angular-three/commit/c0aac2fa)) + +### ❤️ Thank You + +- nartc + +## 4.0.0-next.47 (2025-02-13) + +### 🚀 Features + +- **plugin:** add aux generator ([384f95e7](https://github.com/angular-threejs/angular-three/commit/384f95e7)) + +### 🩹 Fixes + +- **plugin:** adjust migration group ([32992318](https://github.com/angular-threejs/angular-three/commit/32992318)) + +### 💅 Refactors + +- **plugin:** move generate-util outside ([a378788e](https://github.com/angular-threejs/angular-three/commit/a378788e)) + +### ❤️ Thank You + +- nartc + +## 4.0.0-next.46 (2025-02-13) + +### 🩹 Fixes + +- **plugin:** adjust draco schema again ([ed44f610](https://github.com/angular-threejs/angular-three/commit/ed44f610)) + +### ❤️ Thank You + +- nartc + +## 4.0.0-next.45 (2025-02-13) + +### 🩹 Fixes + +- **plugin:** use string for draco ([d008d2dd](https://github.com/angular-threejs/angular-three/commit/d008d2dd)) + +### ❤️ Thank You + +- nartc + +## 4.0.0-next.44 (2025-02-13) + +### 🩹 Fixes + +- **plugin:** default draco to null? ([a345eee1](https://github.com/angular-threejs/angular-three/commit/a345eee1)) + +### ❤️ Thank You + +- nartc + +## 4.0.0-next.43 (2025-02-13) + +### 🩹 Fixes + +- **plugin:** draco is optional instead of default to false ([f9063a82](https://github.com/angular-threejs/angular-three/commit/f9063a82)) + +### ❤️ Thank You + +- nartc + +## 4.0.0-next.42 (2025-02-13) + +### 🩹 Fixes + +- **plugin:** load draco based on transform ([863d7db7](https://github.com/angular-threejs/angular-three/commit/863d7db7)) + +### ❤️ Thank You + +- nartc + +## 4.0.0-next.41 (2025-02-13) + +### 🩹 Fixes + +- **plugin:** handle draco; clean up ([801a8619](https://github.com/angular-threejs/angular-three/commit/801a8619)) + +### ❤️ Thank You + +- nartc + +## 4.0.0-next.40 (2025-02-12) + +### 🩹 Fixes + +- **plugin:** more gltfPath ([f9cbb3e2](https://github.com/angular-threejs/angular-three/commit/f9cbb3e2)) + +### ❤️ Thank You + +- nartc + +## 4.0.0-next.39 (2025-02-12) + +### 🩹 Fixes + +- **plugin:** gltfPath ([9617006c](https://github.com/angular-threejs/angular-three/commit/9617006c)) + +### ❤️ Thank You + +- nartc + +## 4.0.0-next.38 (2025-02-12) + +### 🩹 Fixes + +- **plugin:** adjust one more time ([9fde6590](https://github.com/angular-threejs/angular-three/commit/9fde6590)) + +### ❤️ Thank You + +- nartc + +## 4.0.0-next.37 (2025-02-12) + +### 🩹 Fixes + +- **plugin:** adjust ([c7025c51](https://github.com/angular-threejs/angular-three/commit/c7025c51)) + +### ❤️ Thank You + +- nartc + +## 4.0.0-next.36 (2025-02-12) + +### 🩹 Fixes + +- **plugin:** let's see if it works ([6c84dc2f](https://github.com/angular-threejs/angular-three/commit/6c84dc2f)) + +### ❤️ Thank You + +- nartc + +## 4.0.0-next.35 (2025-02-12) + +### 🩹 Fixes + +- **plugin:** add keep names and keep groups options ([485657eb](https://github.com/angular-threejs/angular-three/commit/485657eb)) + +### ❤️ Thank You + +- nartc + +## 4.0.0-next.34 (2025-02-12) + +### 🩹 Fixes + +- **plugin:** adjust generate ([8a42b403](https://github.com/angular-threejs/angular-three/commit/8a42b403)) + +### ❤️ Thank You + +- nartc + +## 4.0.0-next.33 (2025-02-12) + +### 🩹 Fixes + +- **plugin:** use let for nodes/materials; call generate; adjust node name ([78af9872](https://github.com/angular-threejs/angular-three/commit/78af9872)) + +### ❤️ Thank You + +- nartc + +## 4.0.0-next.32 (2025-02-12) + +### 🩹 Fixes + +- **plugin:** use options modelPath ([399fa40e](https://github.com/angular-threejs/angular-three/commit/399fa40e)) + +### ❤️ Thank You + +- nartc + +## 4.0.0-next.31 (2025-02-12) + +This was a version bump only, there were no code changes. + +## 4.0.0-next.30 (2025-02-11) + +### 🚀 Features + +- **plugin:** prep ([8e750b19](https://github.com/angular-threejs/angular-three/commit/8e750b19)) + +### ❤️ Thank You + +- nartc + +## 4.0.0-next.29 (2025-02-11) + +### 🚀 Features + +- **plugin:** prep gltf generator ([8e79050a](https://github.com/angular-threejs/angular-three/commit/8e79050a)) + +### 🩹 Fixes + +- **plugin:** expose initGenerator ([25865c20](https://github.com/angular-threejs/angular-three/commit/25865c20)) + +### ❤️ Thank You + +- nartc + +## 4.0.0-next.28 (2025-02-10) + +### 🩹 Fixes + +- **plugin:** get literal value of templateUrl if available ([b5bd817c](https://github.com/angular-threejs/angular-three/commit/b5bd817c)) + +### ❤️ Thank You + +- nartc + +## 4.0.0-next.27 (2025-02-10) + +### 🩹 Fixes + +- **plugin:** use descendant again instead of child ([a14ac5ab](https://github.com/angular-threejs/angular-three/commit/a14ac5ab)) + +### ❤️ Thank You + +- nartc + +## 4.0.0-next.26 (2025-02-10) + +### 🩹 Fixes + +- **plugin:** maintain format of template literal ([3f3e5fa8](https://github.com/angular-threejs/angular-three/commit/3f3e5fa8)) + +### ❤️ Thank You + +- nartc + +## 4.0.0-next.25 (2025-02-10) + +### 🩹 Fixes + +- **plugin:** write template properly ([329bde6f](https://github.com/angular-threejs/angular-three/commit/329bde6f)) + +### ❤️ Thank You + +- nartc + +## 4.0.0-next.24 (2025-02-10) + +### 🩹 Fixes + +- **plugin:** get template metadata via descendant ([44de9464](https://github.com/angular-threejs/angular-three/commit/44de9464)) + +### ❤️ Thank You + +- nartc + +## 4.0.0-next.23 (2025-02-10) + +### 🩹 Fixes + +- **plugin:** look for decorators as descendants ([dfd893bd](https://github.com/angular-threejs/angular-three/commit/dfd893bd)) + +### ❤️ Thank You + +- nartc + +## 4.0.0-next.22 (2025-02-10) + +### 🩹 Fixes + +- **plugin:** use tree.write instead because save() ain't enough ([17c56f73](https://github.com/angular-threejs/angular-three/commit/17c56f73)) + +### ❤️ Thank You + +- nartc + +## 4.0.0-next.21 (2025-02-10) + +### 🩹 Fixes + +- **plugin:** use next version for ngt ([f33aa1b2](https://github.com/angular-threejs/angular-three/commit/f33aa1b2)) + +### ❤️ Thank You + +- nartc + +## 4.0.0-next.20 (2025-02-10) + +### 🚀 Features + +- **plugin:** add angular-three-plugin ([394b95c5](https://github.com/angular-threejs/angular-three/commit/394b95c5)) + +### ❤️ Thank You + +- nartc + +## 4.0.0-next.19 (2025-02-10) + +### 🩹 Fixes + +- use as const array instead of module ([7cbcd9b3](https://github.com/angular-threejs/angular-three/commit/7cbcd9b3)) + +### ❤️ Thank You + +- nartc + +## 4.0.0-next.18 (2025-02-09) + +### 🚀 Features + +- **core:** expose NgtPortal as module with portal and portalContent grouped ([919a1a8f](https://github.com/angular-threejs/angular-three/commit/919a1a8f)) +- **core/dom:** expose NgtCanvas as a module with canvas and canvasContent grouped ([bb0a9491](https://github.com/angular-threejs/angular-three/commit/bb0a9491)) +- **soba/gizmos:** expose NgtsGizmoHelper as module ([a85cf9a9](https://github.com/angular-threejs/angular-three/commit/a85cf9a9)) +- **soba/misc:** expose NgtsHTML as module ([60df7770](https://github.com/angular-threejs/angular-three/commit/60df7770)) +- **soba/staging:** expose NgtsRenderTexture as module ([c76f4f7f](https://github.com/angular-threejs/angular-three/commit/c76f4f7f)) + +### ❤️ Thank You + +- nartc + +## 4.0.0-next.17 (2025-02-09) + +### 🩹 Fixes + +- **soba:** adjust orbit controls options ([60b7d645](https://github.com/angular-threejs/angular-three/commit/60b7d645)) + +### ❤️ Thank You + +- nartc + +## 4.0.0-next.16 (2025-02-09) + +### 🩹 Fixes + +- **core:** pass through defaultGLOptions instead of just canvas for NgtGLOptions ([d67e2a75](https://github.com/angular-threejs/angular-three/commit/d67e2a75)) +- **core:** bubble up events from primitive children ([fb1f73ab](https://github.com/angular-threejs/angular-three/commit/fb1f73ab)) + +### ❤️ Thank You + +- nartc + +## 4.0.0-next.15 (2025-02-08) + +### 🩹 Fixes + +- **core:** use resolveRef in NgtParent ([b2fc55bd](https://github.com/angular-threejs/angular-three/commit/b2fc55bd)) + +### ❤️ Thank You + +- nartc + +## 4.0.0-next.14 (2025-02-08) + +### 🚀 Features + +- **core:** allow injectBeforeRender to accept priority as signal ([5aad08bc](https://github.com/angular-threejs/angular-three/commit/5aad08bc)) + +### 🩹 Fixes + +- **postprocessing:** use injectBeforeRender with priority signal ([16307f30](https://github.com/angular-threejs/angular-three/commit/16307f30)) +- **soba:** use injectBeforeRender with priority signal ([c5d6e3dc](https://github.com/angular-threejs/angular-three/commit/c5d6e3dc)) + +### ❤️ Thank You + +- nartc + +## 4.0.0-next.13 (2025-02-07) + +### 🚀 Features + +- **core:** add change as THREE native events ([e44c2b8a](https://github.com/angular-threejs/angular-three/commit/e44c2b8a)) + +### ❤️ Thank You + +- nartc + +## 4.0.0-next.12 (2025-02-07) + +### 🚀 Features + +- **core:** support pierced property ([e5a7285b](https://github.com/angular-threejs/angular-three/commit/e5a7285b)) + +### ❤️ Thank You + +- nartc + +## 4.0.0-next.11 (2025-02-07) + +### 🩹 Fixes + +- **soba:** use booleanAttribute for adaptive dpr pixelated input ([dfb76f8b](https://github.com/angular-threejs/angular-three/commit/dfb76f8b)) + +### ❤️ Thank You + +- nartc + +## 4.0.0-next.10 (2025-02-05) + +### 🩹 Fixes + +- **core:** run invalidate on canvasOtions changes ([e64eccfc](https://github.com/angular-threejs/angular-three/commit/e64eccfc)) +- **core:** adjust after attach type ([c0197de1](https://github.com/angular-threejs/angular-three/commit/c0197de1)) +- **postprocessing:** expose effectComposer ([726dfdbe](https://github.com/angular-threejs/angular-three/commit/726dfdbe)) + +### ❤️ Thank You + +- nartc + +## 4.0.0-next.9 (2025-02-02) + +### 🚀 Features + +- **postprocessing:** selective bloom effect ([dc6b7d1a](https://github.com/angular-threejs/angular-three/commit/dc6b7d1a)) + +### 🩹 Fixes + +- **core:** applyProps assign geometry directly instead of copying ([68da7aa8](https://github.com/angular-threejs/angular-three/commit/68da7aa8)) +- **soba:** fix billboard rotation ([66eb44f9](https://github.com/angular-threejs/angular-three/commit/66eb44f9)) +- **soba:** gizmo rotation should respect camera up ([9e92d423](https://github.com/angular-threejs/angular-three/commit/9e92d423)) + +### ❤️ Thank You + +- nartc + +## 4.0.0-next.8 (2025-02-01) + +This was a version bump only, there were no code changes. + +## 4.0.0-next.7 (2025-01-28) + +### 🩹 Fixes + +- **core:** injectObjectEvents check for NgtInstanceNode ([d92164e1](https://github.com/angular-threejs/angular-three/commit/d92164e1)) + +### ❤️ Thank You + +- nartc + +## 4.0.0-next.6 (2025-01-28) + +### 🩹 Fixes + +- **soba:** update peer deps ([ae568890](https://github.com/angular-threejs/angular-three/commit/ae568890)) + +### ❤️ Thank You + +- nartc + +## 4.0.0-next.5 (2025-01-28) + +### 🩹 Fixes + +- **core:** assign attach directly on instance state ([554dc48b](https://github.com/angular-threejs/angular-three/commit/554dc48b)) + +### ❤️ Thank You + +- nartc + +## 4.0.0-next.4 (2025-01-26) + +### 🩹 Fixes + +- **soba:** null check for onBeforeRender in intersect ([d8aa85df](https://github.com/angular-threejs/angular-three/commit/d8aa85df)) + +### ❤️ Thank You + +- nartc + +## 4.0.0-next.3 (2025-01-26) + +This was a version bump only, there were no code changes. + +## 4.0.0-next.2 (2025-01-26) + +### 🩹 Fixes + +- **rapier:** compute rotation and quaternion properly ([cfd3ea56](https://github.com/angular-threejs/angular-three/commit/cfd3ea56)) + +### ❤️ Thank You + +- nartc + +## 4.0.0-next.1 (2025-01-26) + +### 🚀 Features + +- **core:** new renderer ([c7bec7eb](https://github.com/angular-threejs/angular-three/commit/c7bec7eb)) + +### 🩹 Fixes + +- **core:** adjust portals with new renderer ([f0243b93](https://github.com/angular-threejs/angular-three/commit/f0243b93)) +- **core:** adjust portals with new renderer (2) ([b62b21d3](https://github.com/angular-threejs/angular-three/commit/b62b21d3)) +- **core:** html now works ([6db540b9](https://github.com/angular-threejs/angular-three/commit/6db540b9)) +- **core:** adjust a lot in renderer ([6869f226](https://github.com/angular-threejs/angular-three/commit/6869f226)) +- **core:** pivot controls work ([43c431e6](https://github.com/angular-threejs/angular-three/commit/43c431e6)) +- **core:** update testing lib ([2110ec66](https://github.com/angular-threejs/angular-three/commit/2110ec66)) +- **core:** adjust tests in soba ([5a18452c](https://github.com/angular-threejs/angular-three/commit/5a18452c)) +- **soba:** adjust html with new renderer ([86ff1a7d](https://github.com/angular-threejs/angular-three/commit/86ff1a7d)) +- **soba,core:** controls now work ([96892ad1](https://github.com/angular-threejs/angular-three/commit/96892ad1)) + +### 💅 Refactors + +- **cannon:** update cannon with new renderer ([b73211db](https://github.com/angular-threejs/angular-three/commit/b73211db)) +- **core:** clean up renderer ([04b39d4c](https://github.com/angular-threejs/angular-three/commit/04b39d4c)) +- **core:** clean up renderer ([8c1ae6c2](https://github.com/angular-threejs/angular-three/commit/8c1ae6c2)) +- **core:** make renderer not aware of store internally ([18cafe1e](https://github.com/angular-threejs/angular-three/commit/18cafe1e)) +- **core:** refactor more with no-store renderer ([c56b03e9](https://github.com/angular-threejs/angular-three/commit/c56b03e9)) +- **core:** update object events ([6cf56c30](https://github.com/angular-threejs/angular-three/commit/6cf56c30)) +- **core:** clean up more renderer code ([698e2752](https://github.com/angular-threejs/angular-three/commit/698e2752)) +- **examples:** rename ([371eb49b](https://github.com/angular-threejs/angular-three/commit/371eb49b)) +- **postprocessing:** update postprocessing with new renderer ([bfb3dbb5](https://github.com/angular-threejs/angular-three/commit/bfb3dbb5)) +- **rapier:** update rapier with new renderer ([25f8ac31](https://github.com/angular-threejs/angular-three/commit/25f8ac31)) +- **soba:** clean up abstractions ([5f40d894](https://github.com/angular-threejs/angular-three/commit/5f40d894)) +- **soba:** update cameras with new renderer ([cb9d2069](https://github.com/angular-threejs/angular-three/commit/cb9d2069)) +- **soba:** update abstractions with new renderer ([441a5f0b](https://github.com/angular-threejs/angular-three/commit/441a5f0b)) +- **soba:** update controls with new renderer ([b16d477e](https://github.com/angular-threejs/angular-three/commit/b16d477e)) +- **soba:** update gizmos with new renderer ([b41f1d64](https://github.com/angular-threejs/angular-three/commit/b41f1d64)) +- **soba:** adjust html with new renderer ([155a68d2](https://github.com/angular-threejs/angular-three/commit/155a68d2)) +- **soba:** adjust performances with new renderer ([a83ba07c](https://github.com/angular-threejs/angular-three/commit/a83ba07c)) +- **soba:** adjust shaders with new renderer ([d9bcca60](https://github.com/angular-threejs/angular-three/commit/d9bcca60)) +- **soba:** adjust staging with new renderer ([d43bd2c7](https://github.com/angular-threejs/angular-three/commit/d43bd2c7)) +- **soba:** adjust stats and vanilla exports with new renderer ([41ca5f5b](https://github.com/angular-threejs/angular-three/commit/41ca5f5b)) +- **soba:** adjust misc with new renderer ([df00fb76](https://github.com/angular-threejs/angular-three/commit/df00fb76)) + +### ❤️ Thank You + +- nartc + ## 3.6.0 (2025-01-11) ### 🚀 Features diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index e3ba2892..229363d8 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -35,7 +35,7 @@ If you would like to _implement_ a new feature, please consider the size of the - For a **Major Feature**, first open an issue and outline your proposal so that it can be discussed. This process allows us to better coordinate our efforts, prevent duplication of work, and help you to craft the change so that it is successfully accepted into the project. - **Note**: Adding a new topic to the documentation, or significantly re-writing a topic, counts as a major feature. + **Note**: Adding a new topic to the documentation, or significantly re-writing a topic, counts as a major feature. - **Small Features** can be crafted and directly [submitted as a Pull Request](#submit-pr). @@ -71,9 +71,9 @@ Before you submit your Pull Request (PR) consider the following guidelines: 4. In your forked repository, make your changes in a new git branch: - ```shell - git checkout -b my-fix-branch main - ``` + ```shell + git checkout -b my-fix-branch main + ``` 5. Create your patch, **including appropriate test cases**. @@ -82,17 +82,17 @@ Before you submit your Pull Request (PR) consider the following guidelines: 7. Commit your changes using a descriptive commit message that follows our [commit message conventions](#commit). Adherence to these conventions is necessary because release notes are automatically generated from these messages. - ```shell - git commit --all - ``` + ```shell + git commit --all + ``` - Note: the optional commit `--all` command line option will automatically "add" and "rm" edited files. + Note: the optional commit `--all` command line option will automatically "add" and "rm" edited files. 8. Push your branch to GitHub: - ```shell - git push origin my-fix-branch - ``` + ```shell + git push origin my-fix-branch + ``` 9. In GitHub, send a pull request to `angular-three:main`. @@ -104,12 +104,12 @@ If we ask for changes via code reviews then: 2. Create a fixup commit and push to your GitHub repository (this will update your Pull Request): - ```shell - git commit --all --fixup HEAD - git push - ``` + ```shell + git commit --all --fixup HEAD + git push + ``` - For more info on working with fixup commits see [here](./contributing-docs/using-fixup-commits.md). + For more info on working with fixup commits see [here](./contributing-docs/using-fixup-commits.md). That's it! Thank you for your contribution! @@ -120,21 +120,21 @@ In order to update the commit message of the last commit on your branch: 1. Check out your branch: - ```shell - git checkout my-fix-branch - ``` + ```shell + git checkout my-fix-branch + ``` 2. Amend the last commit and modify the commit message: - ```shell - git commit --amend - ``` + ```shell + git commit --amend + ``` 3. Push to your GitHub repository: - ```shell - git push --force-with-lease - ``` + ```shell + git push --force-with-lease + ``` > NOTE:
> If you need to update the commit message of an earlier commit, you can use `git rebase` in interactive mode. @@ -146,27 +146,27 @@ After your pull request is merged, you can safely delete your branch and pull th - Delete the remote branch on GitHub either through the GitHub web UI or your local shell as follows: - ```shell - git push origin --delete my-fix-branch - ``` + ```shell + git push origin --delete my-fix-branch + ``` - Check out the main branch: - ```shell - git checkout main -f - ``` + ```shell + git checkout main -f + ``` - Delete the local branch: - ```shell - git branch -D my-fix-branch - ``` + ```shell + git branch -D my-fix-branch + ``` - Update your local `main` with the latest upstream version: - ```shell - git pull --ff upstream main - ``` + ```shell + git pull --ff upstream main + ``` ## Coding Rules diff --git a/apps/astro-docs/.gitignore b/apps/astro-docs/.gitignore deleted file mode 100644 index 6240da8b..00000000 --- a/apps/astro-docs/.gitignore +++ /dev/null @@ -1,21 +0,0 @@ -# build output -dist/ -# generated types -.astro/ - -# dependencies -node_modules/ - -# logs -npm-debug.log* -yarn-debug.log* -yarn-error.log* -pnpm-debug.log* - - -# environment variables -.env -.env.production - -# macOS-specific files -.DS_Store diff --git a/apps/astro-docs/.vscode/extensions.json b/apps/astro-docs/.vscode/extensions.json deleted file mode 100644 index 3a1c9ecd..00000000 --- a/apps/astro-docs/.vscode/extensions.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "recommendations": ["astro-build.astro-vscode"], - "unwantedRecommendations": [] -} diff --git a/apps/astro-docs/.vscode/launch.json b/apps/astro-docs/.vscode/launch.json deleted file mode 100644 index 230708db..00000000 --- a/apps/astro-docs/.vscode/launch.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "version": "0.2.0", - "configurations": [ - { - "command": "./node_modules/.bin/astro dev", - "name": "Development server", - "request": "launch", - "type": "node-terminal" - } - ] -} diff --git a/apps/astro-docs/README.md b/apps/astro-docs/README.md deleted file mode 100644 index e09bf55f..00000000 --- a/apps/astro-docs/README.md +++ /dev/null @@ -1,55 +0,0 @@ -# Starlight Starter Kit: Basics - -[![Built with Starlight](https://astro.badg.es/v2/built-with-starlight/tiny.svg)](https://starlight.astro.build) - -``` -npm create astro@latest -- --template starlight -``` - -[![Open in StackBlitz](https://developer.stackblitz.com/img/open_in_stackblitz.svg)](https://stackblitz.com/github/withastro/starlight/tree/main/examples/basics) -[![Open with CodeSandbox](https://assets.codesandbox.io/github/button-edit-lime.svg)](https://codesandbox.io/p/sandbox/github/withastro/starlight/tree/main/examples/basics) -[![Deploy to Netlify](https://www.netlify.com/img/deploy/button.svg)](https://app.netlify.com/start/deploy?repository=https://github.com/withastro/starlight&create_from_path=examples/basics) -[![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Fwithastro%2Fstarlight%2Ftree%2Fmain%2Fexamples%2Fbasics&project-name=my-starlight-docs&repository-name=my-starlight-docs) - -> 🧑‍🚀 **Seasoned astronaut?** Delete this file. Have fun! - -## 🚀 Project Structure - -Inside of your Astro + Starlight project, you'll see the following folders and files: - -``` -. -├── public/ -├── src/ -│ ├── assets/ -│ ├── content/ -│ │ ├── docs/ -│ │ └── config.ts -│ └── env.d.ts -├── astro.config.mjs -├── package.json -└── tsconfig.json -``` - -Starlight looks for `.md` or `.mdx` files in the `src/content/docs/` directory. Each file is exposed as a route based on its file name. - -Images can be added to `src/assets/` and embedded in Markdown with a relative link. - -Static assets, like favicons, can be placed in the `public/` directory. - -## 🧞 Commands - -All commands are run from the root of the project, from a terminal: - -| Command | Action | -| :------------------------ | :----------------------------------------------- | -| `npm install` | Installs dependencies | -| `npm run dev` | Starts local dev server at `localhost:4321` | -| `npm run build` | Build your production site to `./dist/` | -| `npm run preview` | Preview your build locally, before deploying | -| `npm run astro ...` | Run CLI commands like `astro add`, `astro check` | -| `npm run astro -- --help` | Get help using the Astro CLI | - -## 👀 Want to learn more? - -Check out [Starlight’s docs](https://starlight.astro.build/), read [the Astro documentation](https://docs.astro.build), or jump into the [Astro Discord server](https://astro.build/chat). diff --git a/apps/astro-docs/astro.config.mjs b/apps/astro-docs/astro.config.mjs deleted file mode 100644 index af688caf..00000000 --- a/apps/astro-docs/astro.config.mjs +++ /dev/null @@ -1,198 +0,0 @@ -import analogjsangular from '@analogjs/astro-angular'; -import starlight from '@astrojs/starlight'; -import tailwind from '@astrojs/tailwind'; -import { defineConfig } from 'astro/config'; -import { readFileSync } from 'node:fs'; -import starlightBlog from 'starlight-blog'; - -function includeContentPlugin() { - const map = new Map(); - - return [ - { - name: 'pre-include-content', - enforce: 'pre', - transform(_, id) { - if (!id.includes('?includeContent') || id.includes('astro-entry')) return; - - const [filePath] = id.split('?'); - const fileContent = readFileSync(filePath, 'utf-8'); - - if (map.has(filePath)) return; - map.set(filePath, fileContent.replace(/\t/g, ' ')); - }, - }, - { - name: 'post-include-content', - enforce: 'post', - transform(code, id) { - if (!id.includes('?includeContent') || id.includes('astro-entry')) return; - const [filePath] = id.split('?'); - const fileContent = map.get(filePath); - - return { - code: ` - ${code} - export const content = ${JSON.stringify(fileContent)}; - `, - }; - }, - }, - ]; -} - -// https://astro.build/config -export default defineConfig({ - vite: { - ssr: { - noExternal: [ - 'angular-three', - 'angular-three-soba/**', - 'angular-three-cannon/**', - 'angular-three-rapier/**', - '@angular/common', - '@angular/core', - '@angular/core/rxjs-interop', - 'ngxtension/**', - '@pmndrs/vanilla', - '@pmndrs/cannon-worker-api', - 'three-custom-shader-material', - ], - }, - esbuild: { - jsxDev: true, - }, - plugins: [includeContentPlugin()], - }, - integrations: [ - analogjsangular({ - vite: { - experimental: { - supportAnalogFormat: true, - }, - }, - }), - starlight({ - title: 'Angular Three', - plugins: [ - starlightBlog({ - authors: { - chau: { - name: 'Chau Tran', - url: 'https://nartc.me', - picture: 'https://avatars.githubusercontent.com/u/25516557?v=4', - }, - }, - }), - ], - favicon: './src/assets/angular-three-dark.svg', - tableOfContents: { - minHeadingLevel: 2, - maxHeadingLevel: 4, - }, - logo: { - light: './src/assets/angular-three.svg', - dark: './src/assets/angular-three-dark.svg', - }, - social: { - github: 'https://github.com/angular-threejs/angular-three', - }, - credits: true, - customCss: ['./src/tailwind.css'], - sidebar: [ - { label: 'Introduction', slug: '' }, - { - label: 'Core', - collapsed: true, - items: [ - { - label: 'Getting Started', - collapsed: true, - items: [ - { label: 'Installation', slug: 'core/getting-started/installation' }, - { label: 'Editor Setup', slug: 'core/getting-started/editor-setup' }, - { label: 'First Scene', slug: 'core/getting-started/first-scene' }, - ], - }, - { label: 'Migrate to v2', slug: 'core/migrate-v2' }, - { - label: 'API', - collapsed: true, - items: [ - { label: 'NgtCanvas', slug: 'core/api/canvas' }, - { label: 'Custom Renderer', slug: 'core/api/custom-renderer' }, - { label: 'NgtArgs', slug: 'core/api/args' }, - { label: 'Primitive', slug: 'core/api/primitive' }, - { label: 'Raw Value', slug: 'core/api/raw-value' }, - { label: 'Store', slug: 'core/api/store' }, - ], - }, - { - label: 'Testing', - collapsed: true, - items: [ - { label: 'Introduction', slug: 'core/testing/introduction' }, - { label: 'NgtTestBed', slug: 'core/testing/test-bed' }, - { label: 'fireEvent', slug: 'core/testing/fire-event' }, - { label: 'advance', slug: 'core/testing/advance' }, - { label: 'toGraph', slug: 'core/testing/to-graph' }, - ], - }, - { - label: 'Utilities', - collapsed: true, - items: [ - { label: 'injectBeforeRender', slug: 'core/utilities/before-render' }, - { label: 'injectLoader', slug: 'core/utilities/loader' }, - ], - }, - { - label: 'Advanced', - collapsed: true, - items: [ - { label: 'Directives', slug: 'core/advanced/directives' }, - { label: 'Portals', slug: 'core/advanced/portals' }, - { - label: 'Routed Scene', - slug: 'core/advanced/routed-scene', - }, - { label: 'Performance', slug: 'core/advanced/performance' }, - ], - }, - ], - }, - { - label: 'Soba', - collapsed: true, - items: [{ label: 'Introduction', slug: 'soba/introduction' }], - }, - { - label: 'Cannon', - collapsed: true, - items: [ - { label: 'Introduction', slug: 'cannon/introduction' }, - { label: 'How it works', slug: 'cannon/how-it-works' }, - { label: 'Debug', slug: 'cannon/debug' }, - { label: 'Physics', slug: 'cannon/physics' }, - { label: 'Body Functions', slug: 'cannon/bodies' }, - { label: 'Constraint Functions', slug: 'cannon/constraints' }, - ], - }, - { - label: 'Postprocessing', - collapsed: true, - items: [ - { label: 'Introduction', slug: 'postprocessing/introduction' }, - { label: 'How it works', slug: 'postprocessing/how-it-works' }, - { label: 'EffectComposer', slug: 'postprocessing/effect-composer' }, - ], - }, - { - label: 'Demo', - link: 'https://demo.angularthree.org', - }, - ], - }), - tailwind({ applyBaseStyles: false }), - ], -}); diff --git a/apps/astro-docs/ec.config.mjs b/apps/astro-docs/ec.config.mjs deleted file mode 100644 index 9a8bdaca..00000000 --- a/apps/astro-docs/ec.config.mjs +++ /dev/null @@ -1,6 +0,0 @@ -import { pluginLineNumbers } from '@expressive-code/plugin-line-numbers'; - -export default { - themes: ['dark-plus', 'light-plus'], - plugins: [pluginLineNumbers()], -}; diff --git a/apps/astro-docs/package.json b/apps/astro-docs/package.json deleted file mode 100644 index 016c62cf..00000000 --- a/apps/astro-docs/package.json +++ /dev/null @@ -1,36 +0,0 @@ -{ - "name": "docs", - "type": "module", - "version": "0.0.1", - "scripts": { - "dev": "astro dev", - "start": "astro dev", - "build": "astro build", - "preview": "astro preview", - "astro": "astro" - }, - "dependencies": { - "@analogjs/astro-angular": "1.9.4", - "@astrojs/check": "^0.9.4", - "@astrojs/mdx": "^4.0.3", - "@astrojs/starlight": "^0.30.3", - "@astrojs/tailwind": "^5.1.4", - "angular-three": "^3.0.0", - "angular-three-cannon": "^3.0.0", - "angular-three-postprocessing": "^3.0.0", - "angular-three-soba": "^3.0.0", - "astro": "^5.1.1", - "sharp": "^0.33.5", - "starlight-blog": "^0.15.0", - "tailwindcss": "^3.4.15" - }, - "nx": {}, - "devDependencies": { - "@astrojs/starlight-tailwind": "^3.0.0", - "@expressive-code/plugin-line-numbers": "^0.38.3" - }, - "web-types": [ - "../../node_modules/angular-three/web-types.json", - "../../node_modules/angular-three-soba/web-types.json" - ] -} diff --git a/apps/astro-docs/public/bump.jpg b/apps/astro-docs/public/bump.jpg deleted file mode 100644 index ab860318..00000000 Binary files a/apps/astro-docs/public/bump.jpg and /dev/null differ diff --git a/apps/astro-docs/public/cube/nx.png b/apps/astro-docs/public/cube/nx.png deleted file mode 100644 index 0140c687..00000000 Binary files a/apps/astro-docs/public/cube/nx.png and /dev/null differ diff --git a/apps/astro-docs/public/cube/ny.png b/apps/astro-docs/public/cube/ny.png deleted file mode 100644 index ae1e43cc..00000000 Binary files a/apps/astro-docs/public/cube/ny.png and /dev/null differ diff --git a/apps/astro-docs/public/cube/nz.png b/apps/astro-docs/public/cube/nz.png deleted file mode 100644 index bda3c9b7..00000000 Binary files a/apps/astro-docs/public/cube/nz.png and /dev/null differ diff --git a/apps/astro-docs/public/cube/px.png b/apps/astro-docs/public/cube/px.png deleted file mode 100644 index ffe4fab5..00000000 Binary files a/apps/astro-docs/public/cube/px.png and /dev/null differ diff --git a/apps/astro-docs/public/cube/py.png b/apps/astro-docs/public/cube/py.png deleted file mode 100644 index 50f2c697..00000000 Binary files a/apps/astro-docs/public/cube/py.png and /dev/null differ diff --git a/apps/astro-docs/public/cube/pz.png b/apps/astro-docs/public/cube/pz.png deleted file mode 100644 index 970a0eaa..00000000 Binary files a/apps/astro-docs/public/cube/pz.png and /dev/null differ diff --git a/apps/astro-docs/public/favicon.svg b/apps/astro-docs/public/favicon.svg deleted file mode 100644 index f476d966..00000000 --- a/apps/astro-docs/public/favicon.svg +++ /dev/null @@ -1,34 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/apps/astro-docs/src/assets/angular-three-dark.svg b/apps/astro-docs/src/assets/angular-three-dark.svg deleted file mode 100644 index 16abdbd8..00000000 --- a/apps/astro-docs/src/assets/angular-three-dark.svg +++ /dev/null @@ -1,34 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/apps/astro-docs/src/assets/angular-three.svg b/apps/astro-docs/src/assets/angular-three.svg deleted file mode 100644 index f476d966..00000000 --- a/apps/astro-docs/src/assets/angular-three.svg +++ /dev/null @@ -1,34 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/apps/astro-docs/src/assets/houston.webp b/apps/astro-docs/src/assets/houston.webp deleted file mode 100644 index 930c1649..00000000 Binary files a/apps/astro-docs/src/assets/houston.webp and /dev/null differ diff --git a/apps/astro-docs/src/components/cannon/sample-debug.ts b/apps/astro-docs/src/components/cannon/sample-debug.ts deleted file mode 100644 index ce076d9f..00000000 --- a/apps/astro-docs/src/components/cannon/sample-debug.ts +++ /dev/null @@ -1,124 +0,0 @@ -import { - ChangeDetectionStrategy, - Component, - CUSTOM_ELEMENTS_SCHEMA, - effect, - type ElementRef, - input, - viewChild, - viewChildren, -} from '@angular/core'; -import type { Triplet } from '@pmndrs/cannon-worker-api'; -import { extend, injectStore, NgtArgs, NgtCanvas, type NgtVector3 } from 'angular-three'; -import { NgtcPhysics } from 'angular-three-cannon'; -import { injectBox, injectPlane } from 'angular-three-cannon/body'; -import { NgtcDebug } from 'angular-three-cannon/debug'; -import type { Mesh } from 'three'; -import * as THREE from 'three'; - -extend(THREE); - -@Component({ - selector: 'app-plane', - template: ` - - - - - `, - schemas: [CUSTOM_ELEMENTS_SCHEMA], - changeDetection: ChangeDetectionStrategy.OnPush, - imports: [NgtArgs], -}) -export class Plane { - meshRef = viewChild.required>('mesh'); - constructor() { - injectPlane(() => ({ rotation: [-Math.PI / 2, 0, 0], position: [0, -2.5, 0] }), this.meshRef); - } -} - -@Component({ - selector: 'app-cube', - template: ` - - - - - `, - schemas: [CUSTOM_ELEMENTS_SCHEMA], - changeDetection: ChangeDetectionStrategy.OnPush, -}) -export class Cube { - position = input([0, 5, 0]); - - meshRef = viewChild.required>('mesh'); - - boxApi = injectBox( - () => ({ mass: 1, position: this.position() as Triplet, rotation: [0.4, 0.2, 0.5], args: [1, 1, 1] }), - this.meshRef, - ); -} - -@Component({ - template: ` - - - - - - - - @for (position of cubePositions; track $index) { - - } - - `, - imports: [Plane, Cube, NgtArgs, NgtcPhysics, NgtcDebug], - schemas: [CUSTOM_ELEMENTS_SCHEMA], - changeDetection: ChangeDetectionStrategy.OnPush, -}) -export class SceneGraph { - cubePositions: Triplet[] = [ - [0.1, 5, 0], - [0, 10, -1], - [0, 20, -2], - ]; - - cubes = viewChildren(Cube); - - constructor() { - const store = injectStore(); - - effect((onCleanup) => { - const cubes = this.cubes(); - if (!cubes.length) return; - - const sub = store.snapshot.pointerMissed$.subscribe(() => { - cubes.forEach((cube, index) => { - cube.boxApi()?.position.set(...this.cubePositions[index]); - cube.boxApi()?.rotation.set(0.4, 0.2, 0.5); - }); - }); - onCleanup(() => sub.unsubscribe()); - }); - } -} - -@Component({ - template: ` - - * click to reset the cubes - `, - changeDetection: ChangeDetectionStrategy.OnPush, - imports: [NgtCanvas], - host: { class: 'cannon-sample relative inline' }, -}) -export default class CannonSampleDebug { - sceneGraph = SceneGraph; -} diff --git a/apps/astro-docs/src/components/cannon/sample.ts b/apps/astro-docs/src/components/cannon/sample.ts deleted file mode 100644 index e29b2844..00000000 --- a/apps/astro-docs/src/components/cannon/sample.ts +++ /dev/null @@ -1,122 +0,0 @@ -import { - ChangeDetectionStrategy, - Component, - CUSTOM_ELEMENTS_SCHEMA, - effect, - type ElementRef, - input, - viewChild, - viewChildren, -} from '@angular/core'; -import type { Triplet } from '@pmndrs/cannon-worker-api'; -import { extend, injectStore, NgtArgs, NgtCanvas, type NgtVector3 } from 'angular-three'; -import { NgtcPhysics } from 'angular-three-cannon'; -import { injectBox, injectPlane } from 'angular-three-cannon/body'; -import type { Mesh } from 'three'; -import * as THREE from 'three'; - -extend(THREE); - -@Component({ - selector: 'app-plane', - template: ` - - - - - `, - schemas: [CUSTOM_ELEMENTS_SCHEMA], - changeDetection: ChangeDetectionStrategy.OnPush, - imports: [NgtArgs], -}) -export class Plane { - meshRef = viewChild.required>('mesh'); - constructor() { - injectPlane(() => ({ rotation: [-Math.PI / 2, 0, 0], position: [0, -2.5, 0] }), this.meshRef); - } -} - -@Component({ - selector: 'app-cube', - template: ` - - - - - `, - schemas: [CUSTOM_ELEMENTS_SCHEMA], - changeDetection: ChangeDetectionStrategy.OnPush, -}) -export class Cube { - position = input([0, 5, 0]); - - meshRef = viewChild.required>('mesh'); - - boxApi = injectBox( - () => ({ mass: 1, position: this.position() as Triplet, rotation: [0.4, 0.2, 0.5], args: [1, 1, 1] }), - this.meshRef, - ); -} - -@Component({ - template: ` - - - - - - - - @for (position of cubePositions; track $index) { - - } - - `, - imports: [Plane, Cube, NgtArgs, NgtcPhysics], - schemas: [CUSTOM_ELEMENTS_SCHEMA], - changeDetection: ChangeDetectionStrategy.OnPush, -}) -export class SceneGraph { - cubePositions: Triplet[] = [ - [0.1, 5, 0], - [0, 10, -1], - [0, 20, -2], - ]; - - cubes = viewChildren(Cube); - - constructor() { - const store = injectStore(); - - effect((onCleanup) => { - const cubes = this.cubes(); - if (!cubes.length) return; - - const sub = store.snapshot.pointerMissed$.subscribe(() => { - cubes.forEach((cube, index) => { - cube.boxApi()?.position.set(...this.cubePositions[index]); - cube.boxApi()?.rotation.set(0.4, 0.2, 0.5); - }); - }); - onCleanup(() => sub.unsubscribe()); - }); - } -} - -@Component({ - template: ` - - `, - changeDetection: ChangeDetectionStrategy.OnPush, - imports: [NgtCanvas], - host: { class: 'cannon-sample' }, -}) -export default class CannonSample { - sceneGraph = SceneGraph; -} diff --git a/apps/astro-docs/src/components/cursor/cursor.ts b/apps/astro-docs/src/components/cursor/cursor.ts deleted file mode 100644 index 962ee31c..00000000 --- a/apps/astro-docs/src/components/cursor/cursor.ts +++ /dev/null @@ -1,83 +0,0 @@ -import { DOCUMENT } from '@angular/common'; -import { - ChangeDetectionStrategy, - Component, - CUSTOM_ELEMENTS_SCHEMA, - Directive, - ElementRef, - inject, - signal, - viewChild, -} from '@angular/core'; -import { extend, getLocalState, injectBeforeRender, injectObjectEvents, NgtCanvas } from 'angular-three'; -import { NgtsEnvironment } from 'angular-three-soba/staging'; -import * as THREE from 'three'; -import { type Mesh, Object3D } from 'three'; - -extend(THREE); - -@Directive({ selector: '[cursor]' }) -export class Cursor { - constructor() { - const elementRef = inject>(ElementRef); - const nativeElement = elementRef.nativeElement; - - if (!nativeElement.isObject3D) return; - - const localState = getLocalState(nativeElement); - if (!localState) return; - - const document = inject(DOCUMENT); - - injectObjectEvents(() => nativeElement, { - pointerover: () => { - document.body.style.cursor = 'pointer'; - }, - pointerout: () => { - document.body.style.cursor = 'default'; - }, - }); - } -} - -@Component({ - template: ` - - - - - - - - `, - schemas: [CUSTOM_ELEMENTS_SCHEMA], - changeDetection: ChangeDetectionStrategy.OnPush, - imports: [Cursor, NgtsEnvironment], -}) -export class SceneGraph { - hovered = signal(false); - - meshRef = viewChild.required>('mesh'); - - constructor() { - injectBeforeRender(() => { - const mesh = this.meshRef().nativeElement; - mesh.rotation.x += 0.01; - mesh.rotation.y += 0.01; - }); - } - - protected readonly Math = Math; -} - -@Component({ - template: ` - - `, - imports: [NgtCanvas], - changeDetection: ChangeDetectionStrategy.OnPush, - host: { class: 'cursor-scene' }, -}) -export default class CursorScene { - sceneGraph = SceneGraph; -} diff --git a/apps/astro-docs/src/components/first-scene/first-scene.ts b/apps/astro-docs/src/components/first-scene/first-scene.ts deleted file mode 100644 index 7a473ffe..00000000 --- a/apps/astro-docs/src/components/first-scene/first-scene.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { ChangeDetectionStrategy, Component, computed, input } from '@angular/core'; -import { extend, NgtCanvas } from 'angular-three'; -import * as THREE from 'three'; -import { scenes } from './scenes'; - -extend(THREE); - -@Component({ - template: ` - - `, - imports: [NgtCanvas], - changeDetection: ChangeDetectionStrategy.OnPush, - host: { class: 'first-scene' }, -}) -export default class FirstScene { - step = input.required(); - sceneGraph = computed(() => scenes[this.step()]); -} diff --git a/apps/astro-docs/src/components/first-scene/scene-graph-step-five.ts b/apps/astro-docs/src/components/first-scene/scene-graph-step-five.ts deleted file mode 100644 index b8f32634..00000000 --- a/apps/astro-docs/src/components/first-scene/scene-graph-step-five.ts +++ /dev/null @@ -1,63 +0,0 @@ -import { - ChangeDetectionStrategy, - Component, - CUSTOM_ELEMENTS_SCHEMA, - type ElementRef, - input, - signal, - viewChild, -} from '@angular/core'; -import { injectBeforeRender, type NgtVector3 } from 'angular-three'; -import type { Mesh } from 'three'; - -@Component({ - selector: 'app-cube', - template: ` - - - - - `, - schemas: [CUSTOM_ELEMENTS_SCHEMA], - changeDetection: ChangeDetectionStrategy.OnPush, -}) -class Cube { - position = input([0, 0, 0]); - - meshRef = viewChild.required>('mesh'); - - hovered = signal(false); - clicked = signal(false); - - constructor() { - injectBeforeRender(({ delta }) => { - const mesh = this.meshRef().nativeElement; - mesh.rotation.x += delta; - mesh.rotation.y += delta; - }); - } -} - -@Component({ - template: ` - - - - - - - `, - schemas: [CUSTOM_ELEMENTS_SCHEMA], - changeDetection: ChangeDetectionStrategy.OnPush, - imports: [Cube], -}) -export class SceneGraphStepFive { - protected readonly Math = Math; -} diff --git a/apps/astro-docs/src/components/first-scene/scene-graph-step-four.ts b/apps/astro-docs/src/components/first-scene/scene-graph-step-four.ts deleted file mode 100644 index cafe3e54..00000000 --- a/apps/astro-docs/src/components/first-scene/scene-graph-step-four.ts +++ /dev/null @@ -1,57 +0,0 @@ -import { - ChangeDetectionStrategy, - Component, - CUSTOM_ELEMENTS_SCHEMA, - type ElementRef, - input, - signal, - viewChild, -} from '@angular/core'; -import { injectBeforeRender, type NgtVector3 } from 'angular-three'; -import type { Mesh } from 'three'; - -@Component({ - selector: 'app-cube', - template: ` - - - - - `, - schemas: [CUSTOM_ELEMENTS_SCHEMA], - changeDetection: ChangeDetectionStrategy.OnPush, -}) -class Cube { - position = input([0, 0, 0]); - - meshRef = viewChild.required>('mesh'); - - hovered = signal(false); - clicked = signal(false); - - constructor() { - injectBeforeRender(({ delta }) => { - const mesh = this.meshRef().nativeElement; - mesh.rotation.x += delta; - mesh.rotation.y += delta; - }); - } -} - -@Component({ - template: ` - - - `, - schemas: [CUSTOM_ELEMENTS_SCHEMA], - changeDetection: ChangeDetectionStrategy.OnPush, - imports: [Cube], -}) -export class SceneGraphStepFour {} diff --git a/apps/astro-docs/src/components/first-scene/scene-graph-step-one.ts b/apps/astro-docs/src/components/first-scene/scene-graph-step-one.ts deleted file mode 100644 index 7b5a9cbf..00000000 --- a/apps/astro-docs/src/components/first-scene/scene-graph-step-one.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { ChangeDetectionStrategy, Component, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'; - -@Component({ - template: ` - - - - `, - schemas: [CUSTOM_ELEMENTS_SCHEMA], - changeDetection: ChangeDetectionStrategy.OnPush, -}) -export class SceneGraphStepOne {} diff --git a/apps/astro-docs/src/components/first-scene/scene-graph-step-six.ts b/apps/astro-docs/src/components/first-scene/scene-graph-step-six.ts deleted file mode 100644 index a6f4ceee..00000000 --- a/apps/astro-docs/src/components/first-scene/scene-graph-step-six.ts +++ /dev/null @@ -1,73 +0,0 @@ -import { - ChangeDetectionStrategy, - Component, - CUSTOM_ELEMENTS_SCHEMA, - type ElementRef, - input, - signal, - viewChild, -} from '@angular/core'; -import { extend, injectBeforeRender, injectStore, NgtArgs, type NgtVector3 } from 'angular-three'; -import type { Mesh } from 'three'; -import { OrbitControls } from 'three-stdlib'; - -extend({ OrbitControls }); - -@Component({ - selector: 'app-cube', - template: ` - - - - - `, - schemas: [CUSTOM_ELEMENTS_SCHEMA], - changeDetection: ChangeDetectionStrategy.OnPush, -}) -class Cube { - position = input([0, 0, 0]); - - meshRef = viewChild.required>('mesh'); - - hovered = signal(false); - clicked = signal(false); - - constructor() { - injectBeforeRender(({ delta }) => { - const mesh = this.meshRef().nativeElement; - mesh.rotation.x += delta; - mesh.rotation.y += delta; - }); - } -} - -@Component({ - template: ` - - - - - - - - - - `, - schemas: [CUSTOM_ELEMENTS_SCHEMA], - changeDetection: ChangeDetectionStrategy.OnPush, - imports: [Cube, NgtArgs], -}) -export class SceneGraphStepSix { - protected readonly Math = Math; - - private store = injectStore(); - protected camera = this.store.select('camera'); - protected glDomElement = this.store.select('gl', 'domElement'); -} diff --git a/apps/astro-docs/src/components/first-scene/scene-graph-step-three.ts b/apps/astro-docs/src/components/first-scene/scene-graph-step-three.ts deleted file mode 100644 index 9ddc9dd5..00000000 --- a/apps/astro-docs/src/components/first-scene/scene-graph-step-three.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { - ChangeDetectionStrategy, - Component, - CUSTOM_ELEMENTS_SCHEMA, - type ElementRef, - signal, - viewChild, -} from '@angular/core'; -import { injectBeforeRender } from 'angular-three'; -import type { Mesh } from 'three'; - -@Component({ - template: ` - - - - - `, - schemas: [CUSTOM_ELEMENTS_SCHEMA], - changeDetection: ChangeDetectionStrategy.OnPush, -}) -export class SceneGraphStepThree { - meshRef = viewChild.required>('mesh'); - - hovered = signal(false); - clicked = signal(false); - - constructor() { - injectBeforeRender(({ delta }) => { - const mesh = this.meshRef().nativeElement; - mesh.rotation.x += delta; - mesh.rotation.y += delta; - }); - } -} diff --git a/apps/astro-docs/src/components/first-scene/scene-graph-step-two.ts b/apps/astro-docs/src/components/first-scene/scene-graph-step-two.ts deleted file mode 100644 index 601c7b20..00000000 --- a/apps/astro-docs/src/components/first-scene/scene-graph-step-two.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { ChangeDetectionStrategy, Component, CUSTOM_ELEMENTS_SCHEMA, type ElementRef, viewChild } from '@angular/core'; -import { injectBeforeRender } from 'angular-three'; -import type { Mesh } from 'three'; - -@Component({ - template: ` - - - - `, - schemas: [CUSTOM_ELEMENTS_SCHEMA], - changeDetection: ChangeDetectionStrategy.OnPush, -}) -export class SceneGraphStepTwo { - meshRef = viewChild.required>('mesh'); - - constructor() { - injectBeforeRender(({ delta }) => { - const mesh = this.meshRef().nativeElement; - mesh.rotation.x += delta; - mesh.rotation.y += delta; - }); - } -} diff --git a/apps/astro-docs/src/components/first-scene/scenes.ts b/apps/astro-docs/src/components/first-scene/scenes.ts deleted file mode 100644 index 7f5dbdec..00000000 --- a/apps/astro-docs/src/components/first-scene/scenes.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { SceneGraphStepFive } from './scene-graph-step-five'; -import { SceneGraphStepFour } from './scene-graph-step-four'; -import { SceneGraphStepOne } from './scene-graph-step-one'; -import { SceneGraphStepSix } from './scene-graph-step-six'; -import { SceneGraphStepThree } from './scene-graph-step-three'; -import { SceneGraphStepTwo } from './scene-graph-step-two'; - -export const scenes = { - stepOne: SceneGraphStepOne, - stepTwo: SceneGraphStepTwo, - stepThree: SceneGraphStepThree, - stepFour: SceneGraphStepFour, - stepFive: SceneGraphStepFive, - stepSix: SceneGraphStepSix, -}; diff --git a/apps/astro-docs/src/components/hud/hud.ts b/apps/astro-docs/src/components/hud/hud.ts deleted file mode 100644 index 36943c16..00000000 --- a/apps/astro-docs/src/components/hud/hud.ts +++ /dev/null @@ -1,191 +0,0 @@ -import { - CUSTOM_ELEMENTS_SCHEMA, - ChangeDetectionStrategy, - Component, - ElementRef, - computed, - inject, - input, - signal, - viewChild, -} from '@angular/core'; -import { - NgtArgs, - NgtCanvas, - NgtPortal, - NgtPortalContent, - extend, - injectBeforeRender, - injectStore, -} from 'angular-three'; -import { NgtsText } from 'angular-three-soba/abstractions'; -import { NgtsOrthographicCamera, NgtsPerspectiveCamera } from 'angular-three-soba/cameras'; -import { NgtsOrbitControls } from 'angular-three-soba/controls'; -import { NgtsEnvironment, NgtsRenderTexture, NgtsRenderTextureContent } from 'angular-three-soba/staging'; -import * as THREE from 'three'; -import { Matrix4, Mesh, Scene } from 'three'; - -extend(THREE); - -@Component({ - selector: 'app-torus', - template: ` - - - - - `, - imports: [NgtArgs], - schemas: [CUSTOM_ELEMENTS_SCHEMA], - changeDetection: ChangeDetectionStrategy.OnPush, -}) -export class Torus { - scale = input(1); - hovered = signal(false); - color = computed(() => (this.hovered() ? 'hotpink' : 'orange')); -} - -@Component({ - selector: 'app-face-material', - template: ` - - - - - - - - - - `, - imports: [NgtsText, NgtsRenderTexture, NgtsOrthographicCamera, NgtArgs, NgtsRenderTextureContent], - schemas: [CUSTOM_ELEMENTS_SCHEMA], - changeDetection: ChangeDetectionStrategy.OnPush, -}) -export class FaceMaterial { - index = input.required(); - text = input.required(); - - box = inject(Box); - - color = computed(() => (this.box.hovered() === this.index() ? 'hotpink' : 'orange')); -} - -@Component({ - selector: 'app-box', - template: ` - - - @for (face of faces; track face) { - - } - - `, - imports: [FaceMaterial], - schemas: [CUSTOM_ELEMENTS_SCHEMA], - changeDetection: ChangeDetectionStrategy.OnPush, -}) -export class Box { - position = input([0, 0, 0]); - - mesh = viewChild.required>('mesh'); - - hovered = signal(-1); - clicked = signal(false); - - scale = computed(() => (this.clicked() ? 1.5 : 1)); - - faces = ['front', 'back', 'top', 'bottom', 'left', 'right']; -} - -@Component({ - selector: 'app-view-cube', - template: ` - - - - - - - - - - - - `, - imports: [Box, NgtPortal, NgtPortalContent, NgtsPerspectiveCamera], - schemas: [CUSTOM_ELEMENTS_SCHEMA], - changeDetection: ChangeDetectionStrategy.OnPush, -}) -export class ViewCube { - protected readonly Math = Math; - - private store = injectStore(); - private camera = this.store.select('camera'); - private viewport = this.store.select('viewport'); - - box = viewChild(Box); - - boxPosition = computed(() => [this.viewport().width / 2 - 1, this.viewport().height / 2 - 1, 0]); - - scene = computed(() => { - const scene = new Scene(); - scene.name = 'hud-view-cube-virtual-scene'; - return scene; - }); - - constructor() { - const matrix = new Matrix4(); - injectBeforeRender(() => { - const box = this.box()?.mesh().nativeElement; - if (box) { - matrix.copy(this.camera().matrix).invert(); - box.quaternion.setFromRotationMatrix(matrix); - } - }); - } -} - -@Component({ - template: ` - - - - - - - - - `, - imports: [NgtsOrbitControls, NgtsEnvironment, Torus, ViewCube, NgtArgs], - schemas: [CUSTOM_ELEMENTS_SCHEMA], - changeDetection: ChangeDetectionStrategy.OnPush, - host: { class: 'hud-experience' }, -}) -export class Experience { - protected readonly Math = Math; -} - -@Component({ - template: ` - - `, - imports: [NgtCanvas], - changeDetection: ChangeDetectionStrategy.OnPush, - host: { class: 'hud-docs' }, -}) -export default class HudScene { - scene = Experience; -} diff --git a/apps/astro-docs/src/components/lightformer/lightformer.ts b/apps/astro-docs/src/components/lightformer/lightformer.ts deleted file mode 100644 index 2744729a..00000000 --- a/apps/astro-docs/src/components/lightformer/lightformer.ts +++ /dev/null @@ -1,67 +0,0 @@ -import { ChangeDetectionStrategy, Component, CUSTOM_ELEMENTS_SCHEMA, ElementRef, viewChild } from '@angular/core'; -import { extend, injectBeforeRender, NgtArgs, NgtCanvas } from 'angular-three'; -import { NgtsOrbitControls } from 'angular-three-soba/controls'; -import { NgtsContactShadows, NgtsEnvironment, NgtsLightformer } from 'angular-three-soba/staging'; -import * as THREE from 'three'; -import { Mesh } from 'three'; - -extend(THREE); - -@Component({ - template: ` - - - - - - - - - - - - - - - - - - - - - - - - - - `, - imports: [NgtsEnvironment, NgtsLightformer, NgtsContactShadows, NgtArgs, NgtsOrbitControls], - changeDetection: ChangeDetectionStrategy.OnPush, - schemas: [CUSTOM_ELEMENTS_SCHEMA], -}) -export class Experience { - protected readonly Math = Math; - - private cube = viewChild.required>('cube'); - - constructor() { - injectBeforeRender(({ delta }) => { - const cube = this.cube().nativeElement; - cube.rotation.y += delta * 0.2; - }); - } -} - -@Component({ - template: ` - - `, - schemas: [CUSTOM_ELEMENTS_SCHEMA], - changeDetection: ChangeDetectionStrategy.OnPush, - imports: [NgtCanvas], -}) -export default class LightformerScene { - scene = Experience; -} diff --git a/apps/astro-docs/src/components/postprocessing/sample.ts b/apps/astro-docs/src/components/postprocessing/sample.ts deleted file mode 100644 index 669f3558..00000000 --- a/apps/astro-docs/src/components/postprocessing/sample.ts +++ /dev/null @@ -1,146 +0,0 @@ -/* credit: https://codesandbox.io/p/sandbox/react-postprocessing-dof-blob-forked-7hj8w3?file=/src/App.js:29,15 */ - -import { - ChangeDetectionStrategy, - Component, - computed, - CUSTOM_ELEMENTS_SCHEMA, - type ElementRef, - input, - viewChild, - viewChildren, -} from '@angular/core'; -import { extend, injectBeforeRender, injectLoader, NgtArgs, NgtCanvas } from 'angular-three'; -import { NgtpBloom, NgtpDepthOfField, NgtpEffectComposer, NgtpVignette } from 'angular-three-postprocessing'; -import { injectTexture, NgtsLoader } from 'angular-three-soba/loaders'; -import { NgtsMeshDistortMaterial } from 'angular-three-soba/materials'; -import * as THREE from 'three'; -import { CubeTextureLoader, Material, MathUtils, type Mesh } from 'three'; - -extend(THREE); - -@Component({ - selector: 'app-main-sphere', - template: ` - - - - `, - schemas: [CUSTOM_ELEMENTS_SCHEMA], - changeDetection: ChangeDetectionStrategy.OnPush, - imports: [NgtArgs], -}) -export class MainSphere { - material = input.required(); - - meshRef = viewChild.required>('mesh'); - - constructor() { - injectBeforeRender(({ clock, pointer }) => { - const mesh = this.meshRef().nativeElement; - mesh.rotation.z = clock.getElapsedTime(); - mesh.rotation.y = MathUtils.lerp(mesh.rotation.y, pointer.x * Math.PI, 0.1); - mesh.rotation.x = MathUtils.lerp(mesh.rotation.x, pointer.y * Math.PI, 0.1); - }); - } -} - -@Component({ - selector: 'app-sphere-instances', - template: ` - - - - - @for (position of initialPositions; track $index) { - - - - } - `, - schemas: [CUSTOM_ELEMENTS_SCHEMA], - changeDetection: ChangeDetectionStrategy.OnPush, - imports: [MainSphere, NgtArgs, NgtsMeshDistortMaterial], -}) -export class SphereInstances { - private envMap = injectLoader( - // @ts-expect-error - CubeTextureLoader is ok - () => CubeTextureLoader, - () => [['px.png', 'nx.png', 'py.png', 'ny.png', 'pz.png', 'nz.png']], - { extensions: (loader) => loader.setPath('/cube/') }, - ); - private bumpMap = injectTexture(() => '/bump.jpg'); - - materialOptions = computed(() => ({ - envMap: this.envMap()?.[0], - bumpMap: this.bumpMap(), - emissive: '#010101', - emissiveIntensity: 2, - roughness: 0.1, - metalness: 1, - bumpScale: 0.005, - clearcoat: 1, - clearcoatRoughness: 1, - radius: 1, - distort: 0.4, - toneMapped: false, - })); - - initialPositions = [ - [-4, 20, -12], - [-10, 12, -4], - [-11, -12, -23], - [-16, -6, -10], - [12, -2, -3], - [13, 4, -12], - [14, -2, -23], - [8, 10, -20], - ]; - - private meshesRef = viewChildren>('mesh'); - - constructor() { - injectBeforeRender(() => { - const meshes = this.meshesRef(); - meshes.forEach(({ nativeElement: mesh }) => { - mesh.position.y += 0.02; - if (mesh.position.y > 19) mesh.position.y = -18; - mesh.rotation.x += 0.06; - mesh.rotation.y += 0.06; - mesh.rotation.z += 0.02; - }); - }); - } -} - -@Component({ - template: ` - - - - - - - - - - - `, - schemas: [CUSTOM_ELEMENTS_SCHEMA], - changeDetection: ChangeDetectionStrategy.OnPush, - imports: [SphereInstances, NgtArgs, NgtpEffectComposer, NgtpDepthOfField, NgtpBloom, NgtpVignette], -}) -export class SceneGraph {} - -@Component({ - template: ` - - - `, - changeDetection: ChangeDetectionStrategy.OnPush, - host: { class: 'postprocessing-sample' }, - imports: [NgtCanvas, NgtsLoader], -}) -export default class PostprocessingSample { - sceneGraph = SceneGraph; -} diff --git a/apps/astro-docs/src/components/soba/abstractions/gradient-texture.ts b/apps/astro-docs/src/components/soba/abstractions/gradient-texture.ts deleted file mode 100644 index 8216f950..00000000 --- a/apps/astro-docs/src/components/soba/abstractions/gradient-texture.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { ChangeDetectionStrategy, Component, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'; -import { NgtArgs } from 'angular-three'; -import { NgtsGradientTexture } from 'angular-three-soba/abstractions'; -import { NgtsMeshWobbleMaterial } from 'angular-three-soba/materials'; -import { NgtsFloat } from 'angular-three-soba/staging'; -import { DoubleSide } from 'three'; - -@Component({ - selector: 'gradient-texture-scene', - template: ` - - - - - - - - - `, - schemas: [CUSTOM_ELEMENTS_SCHEMA], - changeDetection: ChangeDetectionStrategy.OnPush, - imports: [NgtsFloat, NgtsFloat, NgtsMeshWobbleMaterial, NgtsGradientTexture, NgtArgs], -}) -export default class GradientTextureScene { - protected readonly DoubleSide = DoubleSide; -} diff --git a/apps/astro-docs/src/components/soba/abstractions/grid.ts b/apps/astro-docs/src/components/soba/abstractions/grid.ts deleted file mode 100644 index a5cac105..00000000 --- a/apps/astro-docs/src/components/soba/abstractions/grid.ts +++ /dev/null @@ -1,99 +0,0 @@ -import { ChangeDetectionStrategy, Component, CUSTOM_ELEMENTS_SCHEMA, input, type Signal } from '@angular/core'; -import { NgtArgs } from 'angular-three'; -import { NgtsGrid } from 'angular-three-soba/abstractions'; -import { NgtsOrbitControls } from 'angular-three-soba/controls'; -import { injectGLTF } from 'angular-three-soba/loaders'; -import { NgtsAccumulativeShadows, NgtsCenter, NgtsEnvironment, NgtsRandomizedLights } from 'angular-three-soba/staging'; - -@Component({ - selector: 'grid-suzi', - template: ` - @if (gltf(); as gltf) { - - - - } - `, - schemas: [CUSTOM_ELEMENTS_SCHEMA], - changeDetection: ChangeDetectionStrategy.OnPush, -}) -export class Suzi { - gltf = injectGLTF( - () => 'https://vazxmixjsiawhamofees.supabase.co/storage/v1/object/public/models/suzanne-high-poly/model.gltf', - ) as Signal; - - rotation = input([0, 0, 0]); - scale = input(1); -} - -@Component({ - selector: 'grid-shadows', - template: ` - - - - `, - imports: [NgtsAccumulativeShadows, NgtsRandomizedLights], - schemas: [CUSTOM_ELEMENTS_SCHEMA], - changeDetection: ChangeDetectionStrategy.OnPush, -}) -export class Shadows {} - -@Component({ - template: ` - - - - - - - - - - - - - - - - - - - - - - - - - - `, - imports: [Suzi, Shadows, NgtsOrbitControls, NgtsEnvironment, NgtsCenter, NgtArgs, NgtsGrid], - schemas: [CUSTOM_ELEMENTS_SCHEMA], - changeDetection: ChangeDetectionStrategy.OnPush, -}) -export default class GridScene { - Math = Math; -} diff --git a/apps/astro-docs/src/components/soba/canvas-options.ts b/apps/astro-docs/src/components/soba/canvas-options.ts deleted file mode 100644 index 4849a1cc..00000000 --- a/apps/astro-docs/src/components/soba/canvas-options.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { InjectionToken } from '@angular/core'; -import { type NgtPerformance, type NgtSignalStore, signalStore } from 'angular-three'; - -export interface OrthographicCameraOptions { - orthographic: true; - camera: { position?: [number, number, number]; zoom?: number }; -} - -export interface PerspectiveCameraOptions { - orthographic: false; - camera: { position?: [number, number, number]; fov?: number }; -} - -export type SetupCanvasOptions = { - performance: Partial>; - background: string; - controls: boolean | { makeDefault?: boolean }; - lights: boolean; -} & (OrthographicCameraOptions | PerspectiveCameraOptions); - -export const defaultCanvasOptions: SetupCanvasOptions = { - camera: { position: [-5, 5, 5], fov: 75 }, - orthographic: false, - performance: { current: 1, min: 0.5, max: 1, debounce: 200 }, - background: 'black', - controls: true, - lights: true, -}; - -export const CANVAS_OPTIONS = new InjectionToken>('canvas options'); - -export function provideCanvasOptions() { - return { provide: CANVAS_OPTIONS, useFactory: () => signalStore(defaultCanvasOptions) }; -} diff --git a/apps/astro-docs/src/components/soba/scene-graph.ts b/apps/astro-docs/src/components/soba/scene-graph.ts deleted file mode 100644 index eba0245d..00000000 --- a/apps/astro-docs/src/components/soba/scene-graph.ts +++ /dev/null @@ -1,60 +0,0 @@ -import { - afterNextRender, - ChangeDetectionStrategy, - Component, - ComponentRef, - CUSTOM_ELEMENTS_SCHEMA, - DestroyRef, - inject, - viewChild, - ViewContainerRef, -} from '@angular/core'; -import { NgtArgs } from 'angular-three'; -import { NgtsOrbitControls } from 'angular-three-soba/controls'; -import { CANVAS_OPTIONS } from './canvas-options'; -import { SOBA_CONTENT } from './soba-content'; - -@Component({ - template: ` - - - - - @if (lights()) { - - - } - - @if (controls(); as controls) { - - } - `, - schemas: [CUSTOM_ELEMENTS_SCHEMA], - changeDetection: ChangeDetectionStrategy.OnPush, - imports: [NgtArgs, NgtsOrbitControls, NgtArgs, NgtsOrbitControls], -}) -export class SceneGraph { - protected readonly Math = Math; - - private canvasOptionsStore = inject(CANVAS_OPTIONS); - background = this.canvasOptionsStore.select('background'); - lights = this.canvasOptionsStore.select('lights'); - controls = this.canvasOptionsStore.select('controls'); - - private sobaContent = inject(SOBA_CONTENT); - - anchor = viewChild.required('anchor', { read: ViewContainerRef }); - - constructor() { - let ref: ComponentRef; - - afterNextRender(() => { - ref = this.anchor().createComponent(this.sobaContent()); - ref.changeDetectorRef.detectChanges(); - }); - - inject(DestroyRef).onDestroy(() => { - ref?.destroy(); - }); - } -} diff --git a/apps/astro-docs/src/components/soba/scenes.ts b/apps/astro-docs/src/components/soba/scenes.ts deleted file mode 100644 index 7fcb5c93..00000000 --- a/apps/astro-docs/src/components/soba/scenes.ts +++ /dev/null @@ -1,47 +0,0 @@ -import type { Type } from '@angular/core'; -import type { SetupCanvasOptions } from './canvas-options'; - -import GradientTextureScene, { - content as gradientTextureContent, -} from './abstractions/gradient-texture?includeContent'; -import GridScene, { content as gridContent } from './abstractions/grid.ts?includeContent'; - -export type EntryPoints = 'abstractions'; -export type EntryPointScene = { - abstractions: 'gradientTexture' | 'grid'; -}; - -export type SceneKeys = { - [TEntry in EntryPoints]: `${TEntry}.${EntryPointScene[TEntry]}`; -}[EntryPoints]; - -export type SceneOptions = { - scene: Type; - text: string; - canvasOptions?: Partial; -}; - -export type Scenes = { - [TEntry in EntryPoints]: { - [TScene in EntryPointScene[TEntry]]: SceneOptions; - }; -}; - -export const scenes: Scenes = { - abstractions: { - gradientTexture: { - scene: GradientTextureScene, - text: gradientTextureContent, - }, - grid: { - scene: GridScene, - text: gridContent, - canvasOptions: { - camera: { position: [10, 12, 12], fov: 25 }, - lights: false, - controls: false, - background: '#303035', - }, - }, - }, -}; diff --git a/apps/astro-docs/src/components/soba/soba-content.ts b/apps/astro-docs/src/components/soba/soba-content.ts deleted file mode 100644 index 0c4533f5..00000000 --- a/apps/astro-docs/src/components/soba/soba-content.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { InjectionToken, signal, type Type, type WritableSignal } from '@angular/core'; - -export const SOBA_CONTENT = new InjectionToken | null>>('SobaContent'); - -const sobaContent = signal | null>(null); - -export function provideSobaContent() { - return { provide: SOBA_CONTENT, useValue: sobaContent }; -} diff --git a/apps/astro-docs/src/components/soba/wrapper.astro b/apps/astro-docs/src/components/soba/wrapper.astro deleted file mode 100644 index 64f83f23..00000000 --- a/apps/astro-docs/src/components/soba/wrapper.astro +++ /dev/null @@ -1,22 +0,0 @@ ---- -import { Tabs, TabItem, Code } from '@astrojs/starlight/components'; -import { type SceneKeys, type SceneOptions, scenes } from './scenes'; -import { SobaWrapper } from './wrapper'; - -interface Props { - scene: SceneKeys; -} - -const { scene } = Astro.props; - -const sceneOptions = (scene.split('.').reduce((acc, cur) => acc[cur], scenes)) as SceneOptions; ---- - - - - - - - - - diff --git a/apps/astro-docs/src/components/soba/wrapper.ts b/apps/astro-docs/src/components/soba/wrapper.ts deleted file mode 100644 index fb01b17f..00000000 --- a/apps/astro-docs/src/components/soba/wrapper.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { ChangeDetectionStrategy, Component, effect, inject, input } from '@angular/core'; -import { extend, NgtCanvas } from 'angular-three'; -import * as THREE from 'three'; -import { CANVAS_OPTIONS, provideCanvasOptions } from './canvas-options'; -import { SceneGraph } from './scene-graph'; -import { type SceneKeys, type SceneOptions, scenes } from './scenes'; -import { provideSobaContent, SOBA_CONTENT } from './soba-content'; - -extend(THREE); - -@Component({ - selector: 'soba-wrapper', - template: ` - - `, - host: { class: 'soba-wrapper block h-96 w-full border border-dashed border-accent-500 rounded' }, - providers: [provideCanvasOptions(), provideSobaContent()], - changeDetection: ChangeDetectionStrategy.OnPush, - imports: [NgtCanvas], -}) -export class SobaWrapper { - sceneGraph = SceneGraph; - - scene = input.required(); - - private sobaContent = inject(SOBA_CONTENT); - private canvasOptionsStore = inject(CANVAS_OPTIONS); - camera = this.canvasOptionsStore.select('camera'); - orthographic = this.canvasOptionsStore.select('orthographic'); - performance = this.canvasOptionsStore.select('performance'); - - constructor() { - effect(() => { - const sceneKey = this.scene(); - - const { scene, canvasOptions } = sceneKey.split('.').reduce((acc, cur) => { - return acc[cur]; - }, scenes) as SceneOptions; - - this.sobaContent.set(scene); - if (canvasOptions) this.canvasOptionsStore.update(canvasOptions); - }); - } -} diff --git a/apps/astro-docs/src/components/template-outlet/template-outlet.ts b/apps/astro-docs/src/components/template-outlet/template-outlet.ts deleted file mode 100644 index 7da937d1..00000000 --- a/apps/astro-docs/src/components/template-outlet/template-outlet.ts +++ /dev/null @@ -1,90 +0,0 @@ -import { NgTemplateOutlet } from '@angular/common'; -import { ChangeDetectionStrategy, Component, CUSTOM_ELEMENTS_SCHEMA, ElementRef, viewChild } from '@angular/core'; -import { extend, injectBeforeRender, NgtCanvas } from 'angular-three'; -import { NgtsGrid } from 'angular-three-soba/abstractions'; -import { NgtsPerspectiveCamera } from 'angular-three-soba/cameras'; -import { NgtsOrbitControls } from 'angular-three-soba/controls'; -import * as THREE from 'three'; -import { DEG2RAD } from 'three/src/math/MathUtils.js'; - -extend(THREE); - -@Component({ - template: ` - - - - - - - - - - - - - - - - - - - - - - - - - `, - schemas: [CUSTOM_ELEMENTS_SCHEMA], - changeDetection: ChangeDetectionStrategy.OnPush, - imports: [NgtsOrbitControls, NgtsPerspectiveCamera, NgtsGrid, NgTemplateOutlet], - host: { class: 'template-outlet-experience' }, -}) -export class Experience { - protected readonly DEG2RAD = DEG2RAD; - - private trailRef = viewChild.required>('trail'); - - constructor() { - injectBeforeRender(() => { - const obj = this.trailRef().nativeElement; - obj.position.x = Math.sin(Date.now() / 1000) * 4; - }); - } -} - -@Component({ - template: ` - - `, - imports: [NgtCanvas], - changeDetection: ChangeDetectionStrategy.OnPush, - host: { class: 'template-outlet-docs' }, -}) -export default class TemplateOutletScene { - scene = Experience; -} diff --git a/apps/astro-docs/src/components/testing/test-bed.ts b/apps/astro-docs/src/components/testing/test-bed.ts deleted file mode 100644 index 71302f1a..00000000 --- a/apps/astro-docs/src/components/testing/test-bed.ts +++ /dev/null @@ -1,55 +0,0 @@ -import { - ChangeDetectionStrategy, - Component, - CUSTOM_ELEMENTS_SCHEMA, - type ElementRef, - signal, - viewChild, -} from '@angular/core'; -import { extend, injectBeforeRender, NgtCanvas } from 'angular-three'; -import type { Mesh } from 'three'; -import * as THREE from 'three'; - -extend(THREE); - -@Component({ - template: ` - - - - - `, - schemas: [CUSTOM_ELEMENTS_SCHEMA], - changeDetection: ChangeDetectionStrategy.OnPush, -}) -class SceneGraph { - hovered = signal(false); - clicked = signal(false); - - meshRef = viewChild.required>('mesh'); - - constructor() { - injectBeforeRender(() => { - const mesh = this.meshRef().nativeElement; - mesh.rotation.x += 0.01; - }); - } -} - -@Component({ - template: ` - - `, - imports: [NgtCanvas], - changeDetection: ChangeDetectionStrategy.OnPush, - host: { class: 'test-bed' }, -}) -export default class TestBed { - sceneGraph = SceneGraph; -} diff --git a/apps/astro-docs/src/content/config.ts b/apps/astro-docs/src/content/config.ts deleted file mode 100644 index 176fe874..00000000 --- a/apps/astro-docs/src/content/config.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { docsSchema } from '@astrojs/starlight/schema'; -import { defineCollection } from 'astro:content'; -import { blogSchema } from 'starlight-blog/schema'; - -export const collections = { - docs: defineCollection({ - schema: docsSchema({ extend: (context) => blogSchema(context) }), - }), -}; diff --git a/apps/astro-docs/src/content/docs/blog/v2.mdx b/apps/astro-docs/src/content/docs/blog/v2.mdx deleted file mode 100644 index e097efd4..00000000 --- a/apps/astro-docs/src/content/docs/blog/v2.mdx +++ /dev/null @@ -1,483 +0,0 @@ ---- -title: Angular Three v2 is here! ❤️ -excerpt: Angular Three v2 is here! ❤️ -date: 2024-09-02 -authors: - - chau -tags: - - angular - - three - - release -featured: true ---- - -import { Tabs, TabItem, Code } from '@astrojs/starlight/components'; -import HudScene, { content as hudContent } from '../../../components/hud/hud?includeContent'; -import LightformerScene, { - content as lightformerContent, -} from '../../../components/lightformer/lightformer?includeContent'; - -After almost a year of development, we're thrilled to announce the release of Angular Three v2! 🎉 Through countless examples and tutorials from other [THREE.js](https://threejs.org) ecosystems, we've identified the shortcomings of the previous version of Angular Three. Since then, we've been working tirelessly to enhance the library, making it more stable, performant, and predictable when working with THREE.js scene graphs. - -Following over 100 beta releases, we're confident that Angular Three v2 is ready for production and represents a significant improvement for the Angular THREE.js ecosystem as a whole. - -## Foreword - -Angular Three v2 is a major release with a substantial time gap since the previous version. It aims to address the limitations of the first version, resulting in numerous breaking changes. Some of these changes are subtle and may not be immediately apparent. - -Consequently, even though the surface-level APIs of v1 and v2 are similar, as both use a custom renderer, we're not providing an upgrade path from v1 to v2. Additionally, Angular Three v2 requires a minimum of Angular v18. Therefore, Angular Three v2 is better suited for new projects rather than upgrading existing ones. - -## What's new in Angular Three v2 - -- Angular Signals 🚦 -- Improved performance and stability 📈 -- Better composability 🧩 -- Better `Portal` powered components 🪞 -- Improved documentation 📖 -- Testing (Experimental) 🧪 - -While this list might seem modest at first glance, the core improvements in Angular Three v2 unlock a wealth of potential that has been incorporated into other packages like `angular-three-soba`, `angular-three-cannon`, and `angular-three-postprocessing`. - -### Angular Signals 🚦 - -The most significant change in Angular Three v2 is the adoption of [Angular Signals](https://angular.dev/guide/signals). Angular Signals is a powerful and flexible way to manage state in modern Angular applications. Angular Signals also provides an entire set of new tools: Signal Inputs, Signal Outputs, and Signal Queries; all of which are designed to work seamlessly together. - -Angular Three v2's core is built on Angular Signals. This means that most, if not all, of Angular Three v2's APIs are Signals-based: they can accept Signals or Functions as arguments and return Signals as results. - -Let's examine some examples from across the Angular Three ecosystem. - -#### `injectStore` - -`injectStore` is a [Custom Inject Function (CIF)](https://nartc.me/blog/inject-function-the-right-way/) that allows the consumers to interact with the Angular Three Store. The store contains THREE.js building blocks such as the root `Scene`, the default `Camera`, and the `Renderer` itself etc... - -```angular-ts {'1. Get the NgtStore instance': 14-15} {'2. select() to access the Signals': 17-19} {'3. Use the signals in the template': 7-8} -@Component({ - template: ` - - - - - - - `, - imports: [NgtArgs] -}) -export class MyCmp { - - - private store = injectStore(); - - - protected camera = this.store.select('camera'); // Signal - protected domElement = this.store.select('gl', 'domElement'); // Signal -} -``` - -#### `injectLoader` - -`injectLoader` is a [Custom Inject Function (CIF)](https://nartc.me/blog/inject-function-the-right-way/) that allows the consumers to interact with THREE.js loaders. - -```angular-ts {'Signal Input in action': 12-13} {'Integrating seamlessly with Signals': 15-16} -@Component({ - template: ` - @if (gltf(); as gltf) { - - } - `, - schemas: [CUSTOM_ELEMENTS_SCHEMA], - imports: [NgtArgs] -}) -export class MyModel { - - - path = input.required(); - - - gltf = injectLoader(() => GLTFLoader, this.path); -} -``` - -#### `injectBody` (from `angular-three-cannon`) - -`injectBody` is a [Custom Inject Function (CIF)](https://nartc.me/blog/inject-function-the-right-way/) that allows the consumers to interact with Cannon.js bodies. - -```angular-ts {'Signal Queries in action': 14-15} {'Integrating seamlessly with Signals': 17-21} {'Using Signals result to control the physics body': 29-31} -@Component({ - template: ` - - - - - `, - schemas: [CUSTOM_ELEMENTS_SCHEMA], - changeDetection: ChangeDetectionStrategy.OnPush, - imports: [NgtArgs], -}) -export class Box { - - - meshRef = viewChild.required>('mesh'); - - - boxApi = injectBox( - () => ({ args: [4, 4, 4], mass: 1, type: 'Kinematic' }), - this.meshRef - ); - - constructor() { - injectBeforeRender(({ clock }) => { - const api = this.boxApi(); - if (!api) return; - const t = clock.getElapsedTime(); - - - api.position.set(Math.sin(t * 2) * 5, Math.cos(t * 2) * 5, 3); - api.rotation.set(Math.sin(t * 6), Math.cos(t * 6), 0); - }); - } -} -``` - -These examples demonstrate just a few of the Angular Signals integrations. By leveraging Angular Signals, Angular Three v2 has eliminated much of its internal artificial timing and complexity in coordinating different 3D entities. This, in turn, makes Angular Three v2 more performant, stable, and predictable. - -### Improved performance and stability 📈 - -While Angular Three has always been performant, thanks to Angular, the introduction of Angular Signals has allowed Angular Three v2 to significantly improve its stability story, with performance benefiting as well. - -#### Elimination of custom `NgtRef` - -Before Angular Signals, Angular Three used a custom `NgtRef` to track the life-cycle of THREE.js entities on the template via the `[ref]` custom property binding. - -```angular-ts -@Component({ - template: ` - - ` -}) -export class MyCmp { - meshRef = injectNgtRef(); -} -``` - -The primary purpose of NgtRef was to provide reactivity to `ElementRef`. With Signal Queries, this is no longer necessary. Instead, Angular Three v2 embraces Angular's approach using its query APIs like `viewChild`, `viewChildren`, `contentChild`, and `contentChildren`. - -```diff lang="angular-ts" -@Component({ - template: ` -- -+ - ` -}) -export class MyCmp { -- meshRef = injectNgtRef(); -+ meshRef = viewChild.required>('mesh'); // Signal> -} -``` - -This change reduces the amount of Angular Three internals needed to handle the custom `NgtRef`. The timing of when Signal Queries are resolved is controlled by Angular, and it should be more streamlined and predictable to end users. - -#### Change Detection and Events - -Previously, Angular Three relied on `ChangeDetectorRef` to trigger change detections on events like `pointerover`, `click`, etc. This was necessary because Angular Three has always run outside Angular Zone, and to provide a better experience for end users, we had to make the event system work as seamlessly as possible. Passing around the `ChangeDetectorRef` and ensuring that the correct Component instance was a significant challenge and was never entirely reliable. - -With Angular v18, Signals and `OnPush` change detection strategy have been designed to work well together. This means that end users can use Signal to drive the state of their components, and Angular Three events will be automatically handled by Angular internal change detection mechanism. - -```angular-ts -@Component({ - template: ` - - - - - `, - changeDetection: ChangeDetectionStrategy.OnPush, -}) -export class MyCmp { - protected hovered = signal(false); - protected active = signal(false); - - protected scale = computed(() => this.active() ? 1.5 : 1); - protected color = computed(() => (this.hovered() ? 'hotpink' : 'orange')); -} -``` - -:::tip[Did you know?] - -If you are feeling good about Zoneless, you can even enable it with `withExperimentalZonelessChangeDetection` and everything will still work as expected as long as you use Signals to drive the state of your applications. - -```ts -bootstrapApplication(AppComponent, { - providers: [provideExperimentalZonelessChangeDetection()], -}); -``` - -::: - -These examples are just a few of the many stability improvements that Angular Three v2 has introduced. There are numerous additional "under the hood" enhancements that unlock many more features and possibilities, which we'll explore in the following sections. - -### Better composability 🧩 - -Angular Three v2 overhauls the `NgtRenderer` to make it more composable. `NgtRenderer` now embraces Angular's default content projection -mechanism to provide a more flexible, predictable, and performant way to render content. In turns, this makes composing different THREE.js entities much easier and more straightforward. - -This improvement was inspired by a new addition to Angular Content Projection: Default content. This new feature allows Angular Three to provide default contents for some abstractions like `NgtsLightformer` while still allowing consumers to override it. - -Consider the following example: - -```angular-ts -@Component({ - template: ` - - - - - - - - ` -}) -export class Box {} -``` - -In this snippet, we have a `Box` component that renders a `Mesh` with `BoxGeometry` and a **default** `MeshNormalMaterial`. The `Box` component can be used as follows: - -```angular-html - - - - - - - - - - - -``` - -#### Object Inputs - -Another win for composability is unlocked by Signal Inputs. With Signal Inputs, Angular Three v2 makes uses of [Object Inputs](https://nartc.me/blog/angular-object-inputs/) to provide a more composable way to pass inputs into custom components via `[parameters]` custom property binding. - -Let's revisit the Box component and allow consumers to pass in everything they can pass into Box to control the Mesh and the geometry. - -```angular-ts -import { NgtMesh } from 'angular-three'; - -@Component({ - template: ` - - - - - - - - `, - imports: [NgtArgs] -}) -export class Box { - boxArgs = input>([1, 1, 1]); - options = input>({}); -} -``` - -Now the consumers can use `Box` component as follows: - -```angular-html - - - - - - -``` - -All `angular-three-soba` components are built on top of Object Inputs concept, which allows for a much better composability story without the need to implement custom `ngtCompound` construct in the renderer internals. - -Thanks to this new improvement, abstractions in `angular-three-soba` are significantly easier to use and reason about. Here's another example of `Text3D` component with `Center` and `Float`. - -```angular-html {'center things': 3-4 } {'float things': 6-7 } {'text 3d': 9-10 } { 'content projection for text material': 16-17 } - - - - - - - - - - - - - - - - -``` - -You just use them, nest them, and compose them. The possibilities are endless. - -### Better `Portal` powered components 🪞 - -`NgtPortal` has also received a major upgrade. It is now truly a portal with a separate layered `NgtStore` on top of the default root `NgtStore`. This means that the content of `NgtPortal` is rendered into an off-screen buffer, with access to the state of both the root and the layered `NgtStore`. This allows consumers to have better predictability and control over the components rendered inside an `ngt-portal` component. - -#### Heads-up display example - -For instance, we can use `NgtPortal` and `NgtsRenderTexture` (which also relies on `NgtPortal`) to create a heads-up display (HUD) sample. - -The main scene contains the torus (donut). The view cube (HUD) is rendered in a portal with its own `PerspectiveCamera`. Then each face of the view cube is yet rendered into a separate portal with its own `OrthographicCamera` and a `Text` component to render the face name. - -You can check out the code here: [HudScene](https://github.com/angular-threejs/angular-three/blob/main/apps/kitchen-sink/src/app/soba/hud/experience.ts) - - - -
- -
-
- - - -
- -#### Environment with Lightformers - -`NgtsEnvironment` with content projection has never worked correctly. Now with v2 `NgtPortal`, we can finally have proper `NgtsEnvironment` content with `NgtsLightformer` to control the lighting of the environment. - - - -
- -
-
- - - -
- -### Improved documentation 📖 - -Angular Three v2 documentation is powered by [Starlight](https://starlight.astro.build/) and [AnalogJS](https://analogjs.org) to provide an enhanced experience for Angular users. - -- Improved syntax highlighting powered by [Expressive Code](https://expressive-code.com/) and [Shiki](https://shiki.style) -- Embedded Angular components powered by [AnalogJS](https://analogjs.org) - -This very release blog post is powered by the same stack, making it a delight to work with. With this, we aim to provide a superior documentation experience for Angular Three users. - -### Testing (Experimental) 🧪 - -Angular Three v2 introduces an experimental testing module available through the `angular-three/testing` entry point. This module provides utilities to help you write unit tests for your Angular Three components and scene graphs. - -:::caution - -The testing API is currently in Developer Preview and may be subject to changes without following semantic versioning. - -::: - -#### Key Features - -1. **NgtTestBed**: A utility that extends Angular's TestBed, specifically tailored for Angular Three components. -2. **Mocked Rendering**: Tests run without actual 3D rendering, focusing on scene graph state assertions. -3. **Event Simulation**: Ability to simulate Three.js-specific events like `click`, `pointerover`, etc. -4. **Animation Frame Control**: Methods to advance animation frames for testing time-based behaviors. - -#### Basic Usage - -Here's a simple example of how you might use the testing utilities: - -```angular-ts -import { NgtTestBed } from 'angular-three/testing'; - -@Component({ - template: ` - - - - - `, - schemas: [CUSTOM_ELEMENTS_SCHEMA], - changeDetection: ChangeDetectionStrategy.OnPush, -}) -class MyThreeComponent { - clicked = signal(false); - color = computed(() => (this.clicked() ? 'hotpink' : 'orange')); - - meshRef = viewChild.required>('mesh'); - - constructor() { - injectBeforeRender(({ delta }) => { - const mesh = this.meshRef().nativeElement; - mesh.rotation.x += delta; - mesh.rotation.y += delta; - }); - } -} - -describe('MyThreeComponent', () => { - it('should render a mesh', async () => { - const { scene, fireEvent, advance } = NgtTestBed.create(MyThreeComponent); - - expect(scene.children.length).toBe(1); - const mesh = scene.children[0] as THREE.Mesh; - expect(mesh.isMesh).toBe(true); - - await fireEvent(mesh, 'click'); - // Assert changes after click - - await advance(1); // Advance one frame - // Assert changes after animation - }); -}); -``` - -## Getting Started - -To get started with Angular Three v2, check out the [documentation](https://angularthree.org/). - -Github: [https://github.com/angular-threejs/angular-three](https://github.com/angular-threejs/angular-three) - -There is also a [template repository](https://github.com/angular-threejs/template) that you can use to start a new project with Angular Three v2. - -## Roadmap - -As we celebrate the release of Angular Three v2, our focus now shifts to promoting its adoption and ensuring its stability. Here's a glimpse of our immediate plans: - -- **Promotion and Education**: We're committed to creating a wealth of resources to help developers get the most out of Angular Three v2. This includes: - - - Writing in-depth articles and blog posts about various features and use cases - - Developing comprehensive tutorials covering both basic and advanced topics - - Creating video content to demonstrate real-world applications of Angular Three v2 - -- **Enhancing Test Coverage**: To maintain the reliability and stability of Angular Three, we're prioritizing the expansion of our unit test suite. This will help us catch potential issues early and ensure a smooth experience for all users. - -- **Community Engagement**: We plan to actively engage with the community through workshops, webinars, and conference talks to showcase the power and flexibility of Angular Three v2. - -We're excited about these next steps and look forward to seeing what amazing projects our community will create with Angular Three v2! - -## Acknowledgements - -The journey to Angular Three v2 has been a collaborative effort, and we'd like to express our heartfelt gratitude to several key contributors: - -- **The PMNDRS Ecosystem**: We owe a great deal to the PMNDRS (Poimandres) community and their various `@pmndrs` packages. Their innovative work in the 3D web space has been a constant source of inspiration and has significantly influenced the direction of Angular Three. - -- **The Angular Team**: We extend our sincere thanks to the Angular team for their continuous improvements to the framework. Many of the enhancements in Angular Three v2, particularly those leveraging Signals, were made possible by the Angular team's forward-thinking approach to reactive programming. - -- **The Wider Angular Community**: Last but not least, we're grateful to the entire Angular community for your support, enthusiasm, and patience throughout this development process. Your passion for pushing the boundaries of what's possible with Angular continues to drive us forward. - -We're proud to be part of such a vibrant and supportive ecosystem, and we look forward to continuing this journey with all of you as we explore the exciting possibilities that Angular Three v2 brings to the world of 3D web development. - -## Conclusion - -The development of Angular Three v2 has been a long journey, but we're excited to see what you can create with it. We hope you enjoy the improvements and new features that Angular Three v2 brings to the table. If you have any feedback or suggestions, please don't hesitate to reach out to us on [GitHub](https://github.com/angular-threejs/angular-three/issues). - -Thank you for reading this blog post. We hope you found it informative and learned something new. Happy coding! diff --git a/apps/astro-docs/src/content/docs/blog/v3.mdx b/apps/astro-docs/src/content/docs/blog/v3.mdx deleted file mode 100644 index c8e1a173..00000000 --- a/apps/astro-docs/src/content/docs/blog/v3.mdx +++ /dev/null @@ -1,75 +0,0 @@ ---- -title: Angular Three v3 -excerpt: Angular Three v3 -date: 2025-01-04 -authors: - - chau -tags: - - angular - - three - - release -featured: true ---- - -Angular Three v3 is here with Angular 19 support! 🎉 - -## What's in Angular Three v3 - -- Angular 19 support 🚀 -- Stable Testing API 🧪 -- Routed scenes support is back 🧭 -- Deprecation removals 🗑️ - -### Angular 19 support - -Angular Three v3 drops support for Angular 18 and below. This is because Angular 19 comes with a huge change -to the Effect API (Learn more [here](https://riegler.fr/blog/2024-10-15-effect-context/)). While nothing really changes much to consumers, -the way Angular Three utilizes the Effect API changes. From the library perspective, this update also allows for Angular Three components to go `standalone` by default. - -That said, there are several benefits of Angular 19 to Angular Three, and its consumers: - -- Lighter effects with less microtasks; One of the changes to the new Effect API is that a certain type of effects is scheduled in a smarter way instead of _microtask-ing_ everything. -- Faster execution in some areas; Another change to the new Effect API is that setting signals inside of effects is now allowed. Hence, `untracked` trick or `allowSignalWrites` is no longer needed. -- Decreased library size; With default `standalone: true` and static analysis on `imports` array, Angular Three v3 is able to shed some of its bundle size. - -### Stable Testing API - -Angular Three Testing API (`angular-three/testing`) has been graduated from Developer Preview. Nothing changes to the API itself. The Testing API -has been used to test a lot of Angular Three usages since its experimental phase and we are happy to announce that it is now stable. - -### Routed scenes support - -Due to [a fix in Angular Router](https://github.com/angular/angular/commit/3839cfbb18fcc70cae5a6ba4ba7676b1c4acf7a0), Angular Three has been _soft deprecating_ the Routed Scene use-case. -However, the support is back in v3 with a solution. - -Angular Three implements a custom `RouterOutlet`, `NgtRouterOutlet`, for routed scenes. All it does is to make sure the outlet can provide -the `NgtRendererFactory` when it activates the routed components. This _might_ seem like a workaround to some folks but we believe that it should work correctly -for most use-cases. If you run into any problem at all, please file an issue on [Github](https://github.com/angular-threejs/angular-three). - -Check out the [Routed Scene](/core/advanced/routed-scene) documentation for more information. - -### Deprecation removals - -#### `NgtObjectEventsHostDirective` - -Short-hand host directive for `NgtObjectEvents` has been removed. - -```ts -// instead of -hostDirectives: [NgtObjectEventsHostDirective], - -// use -hostDirectives: [{directive: NgtObjectEvents, inputs: [/* explicit inputs */], outputs: [/* explicit outputs */]}], -``` - -#### `NgtsPivotControls` from `angular-three-soba/controls` - -The export `NgtsPivotControls` has been removed from `angular-three-soba/controls`. Update your import to `angular-three-soba/gizmos` - -#### `erp` input in `NgtrPhysics` - -`erp` input in `NgtrPhysics` has been removed as it's been a no-op for a while. - -## Conclusion - -This is a small major release to make sure Angular Three is not holding you back from Angular 19. Thank you. Happy coding! diff --git a/apps/astro-docs/src/content/docs/cannon/bodies.mdx b/apps/astro-docs/src/content/docs/cannon/bodies.mdx deleted file mode 100644 index 8a2faec4..00000000 --- a/apps/astro-docs/src/content/docs/cannon/bodies.mdx +++ /dev/null @@ -1,133 +0,0 @@ ---- -title: Body Functions -description: Detailed explanation of the Body functions in Angular Three Cannon ---- - -Angular Three Cannon provides various body functions to create different types of physics bodies. These functions are used to add physical properties to your 3D objects. - -## Available Body Functions - -All body functions are available from `angular-three-cannon/body`: - -```angular-ts -import { - injectBox, - injectSphere, - injectPlane, - injectCylinder, - injectHeightfield, - injectParticle, - injectConvexPolyhedron, - injectTrimesh, - injectCompound -} from 'angular-three-cannon/body'; -``` - -## Usage - -The general pattern for using these functions is: - -```angular-ts -import { Component, ElementRef, viewChild } from '@angular/core'; -import { injectBox } from 'angular-three-cannon/body'; -import { NgtMesh } from 'angular-three'; - -@Component({ - selector: 'app-physics-box', - template: ` - - - - - `, -}) -export class PhysicsBox { - mesh = viewChild.required>('mesh'); - - boxBody = injectBox( - () => ({ - mass: 1, - position: [0, 5, 0], - args: [1, 1, 1], - }), - this.mesh - ); -} -``` - -## Body Functions - -| Function | Description | Specific Options | -| ------------------------ | ------------------------------- | ------------------------------------------------------ | -| `injectBox` | Creates a box-shaped body | `args: [width, height, depth]` | -| `injectSphere` | Creates a spherical body | `args: [radius]` | -| `injectPlane` | Creates an infinite plane | No specific options | -| `injectCylinder` | Creates a cylindrical body | `args: [radiusTop, radiusBottom, height, numSegments]` | -| `injectHeightfield` | Creates a heightfield body | `args: [data, options]` | -| `injectParticle` | Creates a particle (point mass) | No specific options | -| `injectConvexPolyhedron` | Creates a convex polyhedron | `args: [vertices, faces]` | -| `injectTrimesh` | Creates a triangular mesh body | `args: [vertices, indices]` | -| `injectCompound` | Creates a compound body | `shapes: Array<{ type, args, position?, rotation? }>` | - -## Common Options - -All body functions accept a set of common options: - -| Option | Type | Description | -| ---------------------- | --------------------------------- | ------------------------------------------------------------- | -| `mass` | number | The mass of the body (0 for static bodies) | -| `position` | [x: number, y: number, z: number] | Initial position of the body | -| `rotation` | [x: number, y: number, z: number] | Initial rotation of the body (in radians) | -| `velocity` | [x: number, y: number, z: number] | Initial velocity of the body | -| `angularVelocity` | [x: number, y: number, z: number] | Initial angular velocity of the body | -| `linearDamping` | number | Linear damping of the body (0 = no damping, 1 = full damping) | -| `angularDamping` | number | Angular damping of the body | -| `fixedRotation` | boolean | If true, body will not rotate | -| `collisionFilterGroup` | number | The collision group the body belongs to | -| `collisionFilterMask` | number | Which groups this body can collide with | -| `trigger` | boolean | If true, body acts as a trigger (no collision response) | -| `onCollide` | function | Callback function when collision occurs | -| `onCollideBegin` | function | Callback function when collision begins | -| `onCollideEnd` | function | Callback function when collision ends | - -## Advanced Usage - -You can dynamically update body properties using the returned API: - -```angular-ts -import { Component, ElementRef, viewChild, signal } from '@angular/core'; -import { injectBox } from 'angular-three-cannon/body'; -import { NgtMesh } from 'angular-three'; - -@Component({ - selector: 'app-physics-box', - template: ` - - - - - - `, -}) -export class PhysicsBox { - mesh = viewChild.required>('mesh'); - - boxBody = injectBox( - () => ({ - mass: 1, - position: [0, 5, 0], - args: [1, 1, 1], - }), - this.mesh - ); - - jump() { - const api = this.boxBody(); - if (api) { - api.applyImpulse([0, 5, 0], [0, 0, 0]); - } - } -} -``` - -This example shows how to apply an impulse to make the box "jump" when a button is clicked. diff --git a/apps/astro-docs/src/content/docs/cannon/constraints.mdx b/apps/astro-docs/src/content/docs/cannon/constraints.mdx deleted file mode 100644 index f1c75ab6..00000000 --- a/apps/astro-docs/src/content/docs/cannon/constraints.mdx +++ /dev/null @@ -1,158 +0,0 @@ ---- -title: Constraint Functions -description: Detailed explanation of the Constraint functions in Angular Three Cannon ---- - -Angular Three Cannon provides various constraint functions to create different types of physical constraints between bodies. These functions are used to limit and control the relative movement of physics bodies. - -## Available Constraint Functions - -All constraint functions are available from `angular-three-cannon/constraint`: - -```angular-ts -import { - injectPointToPoint, - injectConeTwist, - injectDistance, - injectLock, - injectHinge -} from 'angular-three-cannon/constraint'; -``` - -## Usage - -The general pattern for using these functions is: - -```angular-ts -import { Component, ElementRef, viewChild } from '@angular/core'; -import { injectHinge } from 'angular-three-cannon/constraint'; -import { NgtMesh } from 'angular-three'; - -@Component({ - selector: 'app-hinge-constraint', - template: ` - - - - - - - `, -}) -export class HingeConstraint { - bodyA = viewChild.required>('bodyA'); - bodyB = viewChild.required>('bodyB'); - - hingeConstraint = injectHinge( - this.bodyA, - this.bodyB, - { - pivotA: [1, 0, 0], - pivotB: [-1, 0, 0], - axisA: [0, 1, 0], - axisB: [0, 1, 0], - } - ); -} -``` - -## Constraint Functions - -| Function | Description | Specific Options | -| -------------------- | ----------------------------------- | ------------------------------------ | -| `injectPointToPoint` | Creates a point-to-point constraint | `pivotA`, `pivotB` | -| `injectConeTwist` | Creates a cone twist constraint | `pivotA`, `pivotB`, `axisA`, `axisB` | -| `injectDistance` | Creates a distance constraint | `distance` | -| `injectLock` | Creates a lock constraint | `maxForce` | -| `injectHinge` | Creates a hinge constraint | `pivotA`, `pivotB`, `axisA`, `axisB` | - -## Common Options - -All constraint functions accept two bodies as the first two arguments, followed by an options object. Common options include: - -| Option | Type | Description | -| ---------- | --------------------------------- | ---------------------------------------------------------------- | -| `pivotA` | [x: number, y: number, z: number] | The pivot point for body A in local space | -| `pivotB` | [x: number, y: number, z: number] | The pivot point for body B in local space | -| `axisA` | [x: number, y: number, z: number] | The axis for body A (for certain constraints) | -| `axisB` | [x: number, y: number, z: number] | The axis for body B (for certain constraints) | -| `maxForce` | number | The maximum force that can be applied to maintain the constraint | - -## Specific Options - -### PointToPoint Constraint - -- No additional specific options - -### ConeTwist Constraint - -- `angle`: number - The maximum cone angle in radians -- `twistAngle`: number - The maximum twist angle in radians - -### Distance Constraint - -- `distance`: number - The fixed distance between the bodies - -### Lock Constraint - -- No additional specific options - -### Hinge Constraint - -- `collideConnected`: boolean - Whether the connected bodies should collide with each other - -## Advanced Usage - -You can dynamically control constraints using the returned API: - -```angular-ts -import { Component, ElementRef, viewChild, signal } from '@angular/core'; -import { injectHinge } from 'angular-three-cannon/constraint'; -import { NgtMesh } from 'angular-three'; - -@Component({ - selector: 'app-hinge-constraint', - template: ` - - - - - - - - `, -}) -export class HingeConstraint { - bodyA = viewChild.required>('bodyA'); - bodyB = viewChild.required>('bodyB'); - - motorEnabled = signal(false); - - hingeConstraint = injectHinge( - this.bodyA, - this.bodyB, - { - pivotA: [1, 0, 0], - pivotB: [-1, 0, 0], - axisA: [0, 1, 0], - axisB: [0, 1, 0], - } - ); - - toggleMotor() { - const api = this.hingeConstraint(); - if (api) { - if (this.motorEnabled()) { - api.disableMotor(); - } else { - api.enableMotor(); - api.setMotorSpeed(1); - api.setMotorMaxForce(10); - } - this.motorEnabled.update(value => !value); - } - } -} -``` - -This example demonstrates how to toggle a motor on a hinge constraint, showing the advanced control you have over constraints during runtime. diff --git a/apps/astro-docs/src/content/docs/cannon/debug.mdx b/apps/astro-docs/src/content/docs/cannon/debug.mdx deleted file mode 100644 index 6a505c6b..00000000 --- a/apps/astro-docs/src/content/docs/cannon/debug.mdx +++ /dev/null @@ -1,85 +0,0 @@ ---- -title: Debug -description: Details about the Angular Three Cannon Debug ---- - -import { Tabs, TabItem, Code } from '@astrojs/starlight/components'; -import CannonSampleDebug, { content } from '../../../components/cannon/sample-debug?includeContent'; - -`angular-three-cannon/debug` provides a debug experience for Cannon.js physics engine using `cannon-es-debugger`. This -allows us to visualize how Cannon _sees_ the 3D scene graph. - -### Installation - -```shell -npm install cannon-es-debugger -# yarn add cannon-es-debugger -# pnpm add cannon-es-debugger -``` - -### Import `NgtcDebug` from `angular-three-cannon/debug` - -```angular-ts -import { NgtcDebug } from 'angular-three-cannon/debug'; -``` - -### Attach `[debug]` to the `ngtc-physics` component - -```angular-ts -@Component({ - imports: [NgtcDebug, NgtcPhysics], - template: ` - - - - ` -}) -export class SceneGraph {} -``` - -### Passing options to `NgtcDebug` - -We can pass options to `NgtcDebug` by passing in an object to the `debug` input - -```angular-ts -@Component({ - imports: [NgtcDebug, NgtcPhysics], - template: ` - - - - ` -}) -export class SceneGraph {} -``` - -:::note - -If the _debug_ wireframes are not visible when `NgtcDebug` is enabled, make sure to set the `args` for the Body Shape. - -```diff lang="angular-ts" -injectBox( - () => ({ - mass: 1, -+ args: [1, 1, 1], - }), - this.meshRef -); -``` - -::: - -### Example - - - -
- -
-
- - - -
diff --git a/apps/astro-docs/src/content/docs/cannon/how-it-works.mdx b/apps/astro-docs/src/content/docs/cannon/how-it-works.mdx deleted file mode 100644 index a73831a3..00000000 --- a/apps/astro-docs/src/content/docs/cannon/how-it-works.mdx +++ /dev/null @@ -1,97 +0,0 @@ ---- -title: How it works -description: How Angular Three Cannon works ---- - -import { Tabs, TabItem, Code } from '@astrojs/starlight/components'; -import CannonSample, { content } from '../../../components/cannon/sample?includeContent'; - -### Import `NgtcPhysics` from `angular-three-cannon` - -```angular-ts -import { NgtcPhysics } from 'angular-three-cannon'; -``` - -### Render the `NgtcPhysics` component in your scene graph - -```angular-ts -@Component({ - imports: [NgtcPhysics], - template: ` - - - - `, - schemas: [CUSTOM_ELEMENTS_SCHEMA], -}) -export class SceneGraph {} -``` - -### Pick a Body shape - -`angular-three-cannon/body` provides a variety of body shapes in form of Custom Inject Function that can be used to create physics bodies. - -```angular-ts -import { injectBox } from 'angular-three-cannon/body'; - -@Component({ - selector: 'app-box', - template: ` - - - - `, - imports: [NgtArgs], - schemas: [CUSTOM_ELEMENTS_SCHEMA], - changeDetection: ChangeDetectionStrategy.OnPush, -}) -export class Box { - mesh = viewChild.required>('mesh'); - box = injectBox(() => ({ mass: 1 }), this.mesh); -} -``` - -`injectBox` accepts some physics properties as an argument and an `() => ElementRef` as the second argument to tie the body to the object. - -The returned value is a `Signal` that can be used to control the body, set position, rotation, and subscribe to changes. - -```angular-ts {19-23} -import { injectBox } from 'angular-three-cannon/body'; - -@Component({ - selector: 'app-box', - template: ` - - - - `, - imports: [NgtArgs], - schemas: [CUSTOM_ELEMENTS_SCHEMA], - changeDetection: ChangeDetectionStrategy.OnPush, -}) -export class Box { - mesh = viewChild.required>('mesh'); - box = injectBox(() => ({ mass: 1 }), this.mesh); - - constructor() { - injectBeforeRender(({ clock }) => { - const api = this.box(); - if (!api) return; - api.position.set(Math.sin(clock.getElapsedTime()) * 5, 0, 0); - }) - } -} -``` - -### Example - - - -
- -
-
- - - -
diff --git a/apps/astro-docs/src/content/docs/cannon/introduction.mdx b/apps/astro-docs/src/content/docs/cannon/introduction.mdx deleted file mode 100644 index 9388bdeb..00000000 --- a/apps/astro-docs/src/content/docs/cannon/introduction.mdx +++ /dev/null @@ -1,27 +0,0 @@ ---- -title: Introduction -description: Introduction to the Angular Three Cannon package ---- - -Angular Three Cannon is an integration of the [Cannon.js](https://github.com/pmndrs/cannon-es) physics engine for use with Angular Three. - -This implementation is based on the [@react-three/cannon](https://github.com/pmndrs/use-cannon) library. - -- [x] Doesn't block the main thread, runs in a web worker -- [x] Supports all the features of cannon-es - -Examples are available at [angular-three-cannon](https://demo.angularthree.org/cannon) - -## Installation - -```sh -npm install angular-three-cannon cannon-es @pmndrs/cannon-worker-api -# yarn add angular-three-cannon cannon-es @pmndrs/cannon-worker-api -# pnpm add angular-three-cannon cannon-es @pmndrs/cannon-worker-api -``` - -## Compatibility Matrix - -| Angular Three Cannon Version | cannon-es version | @pmndrs/cannon-worker-api version | -| ---------------------------- | ----------------- | --------------------------------- | -| 2.0.0 | ^0.20.0 | ^1.0.0 | diff --git a/apps/astro-docs/src/content/docs/cannon/physics.mdx b/apps/astro-docs/src/content/docs/cannon/physics.mdx deleted file mode 100644 index 30ae5259..00000000 --- a/apps/astro-docs/src/content/docs/cannon/physics.mdx +++ /dev/null @@ -1,71 +0,0 @@ ---- -title: NgtcPhysics -description: Detailed explanation of the NgtcPhysics component and its options ---- - -The `NgtcPhysics` component is the core of Angular Three Cannon. It sets up the physics world and manages the simulation. All physics-enabled objects should be children of this component. - -## Usage - -```angular-ts -import { NgtcPhysics } from 'angular-three-cannon'; -import { Component, signal } from '@angular/core'; - -@Component({ - imports: [NgtcPhysics], - template: ` - - - - `, -}) -export class PhysicsScene {} -``` - -## Options - -The `NgtcPhysics` component accepts an `options` input with the following properties: - -| Option | Type | Default | Description | -| ------------------------ | ---------------- | ----------------------------------- | ------------------------------------------------------------ | -| `allowSleep` | boolean | `false` | If true, allows bodies to fall asleep for better performance | -| `axisIndex` | number | `0` | Axis index for broadphase optimization | -| `broadphase` | string | `'Naive'` | Broadphase algorithm to use. Options: 'Naive', 'SAP' | -| `defaultContactMaterial` | object | `{ contactEquationStiffness: 1e6 }` | Default contact material properties | -| `frictionGravity` | number[] \| null | `null` | Gravity to use for friction calculations | -| `gravity` | number[] | `[0, -9.81, 0]` | Gravity force applied to all bodies | -| `iterations` | number | `5` | Number of solver iterations per step | -| `quatNormalizeFast` | boolean | `false` | If true, uses fast quaternion normalization | -| `quatNormalizeSkip` | number | `0` | Number of steps to skip before normalizing quaternions | -| `size` | number | `1000` | Maximum number of physics bodies | -| `solver` | string | `'GS'` | Constraint solver to use. Options: 'GS' (Gauss-Seidel) | -| `tolerance` | number | `0.001` | Solver tolerance | -| `isPaused` | boolean | `false` | If true, pauses the physics simulation | -| `maxSubSteps` | number | `10` | Maximum number of sub-steps per frame | -| `shouldInvalidate` | boolean | `true` | If true, forces a re-render after each physics step | -| `stepSize` | number | `1/60` | Fixed time step size | - -## Advanced Usage - -You can dynamically update physics options using Angular Signals: - -```angular-ts -import { Component, signal } from '@angular/core'; -import { NgtcPhysics } from 'angular-three-cannon'; - -@Component({ - imports: [NgtcPhysics], - template: ` - - - - `, -}) -export class PhysicsScene { - gravity = signal([0, -9.81, 0]); - - toggleGravity() { - this.gravity.update((current) => [0, current[1] * -1, 0]); - } -} -``` diff --git a/apps/astro-docs/src/content/docs/core/advanced/directives.mdx b/apps/astro-docs/src/content/docs/core/advanced/directives.mdx deleted file mode 100644 index 1624afd9..00000000 --- a/apps/astro-docs/src/content/docs/core/advanced/directives.mdx +++ /dev/null @@ -1,65 +0,0 @@ ---- -title: Extending Functionality with Directives -description: Details about extending functionality with Directives ---- - -import CursorScene from '../../../../components/cursor/cursor'; - -Angular Three elements are like regular DOM elements; they are just rendered to the Canvas instead of the DOM. -With that in mind, we can extend the functionality of Angular Three elements by using Directives like we do with regular DOM elements. - -## Attribute Directives in Angular - -When we attach an **Attribute Directive** to an element, we have access to the element's host instance via `ElementRef` token. Angular Three elements -return the actual THREE.js entity instance as the host instance so that we can access the THREE.js APIs to extend the functionality of the element. - -## Build a `cursor` Directive - -Let's build a `cursor` directive that will change the cursor to a `pointer` when the element is hovered. - -```angular-ts {'inject ElementRef': 4-5} {'Get localState': 9-10} {'Attach pointer events to host element': 14-15} -@Directive({selector: '[cursor]'}) -export class Cursor { - constructor() { - - const elementRef = inject>(ElementRef); - const nativeElement = elementRef.nativeElement; - - if (!nativeElement.isObject3D) return; - - const localState = getLocalState(nativeElement); - if (!localState) return; - - const document = inject(DOCUMENT); - - injectObjectEvents(() => nativeElement, { - pointerover: () => { - document.body.style.cursor = 'pointer'; - }, - pointerout: () => { - document.body.style.cursor = 'default'; - }, - }); - } -} -``` - -Now, we can use the `cursor` directive on any element to change the cursor to a `pointer` when the element is hovered. - -:::note - -We do not constraint the type of the element that the `cursor` directive can be attached to but it only works for elements that -are subjected to the events system like `Mesh` etc... - -::: - -```angular-html 'cursor' - - - - -``` - -
- -
diff --git a/apps/astro-docs/src/content/docs/core/advanced/performance.mdx b/apps/astro-docs/src/content/docs/core/advanced/performance.mdx deleted file mode 100644 index 4747cb7c..00000000 --- a/apps/astro-docs/src/content/docs/core/advanced/performance.mdx +++ /dev/null @@ -1,87 +0,0 @@ ---- -title: Performance -description: Details about the Angular Three Performance ---- - -## Reuse Geometries and Materials - -Each Geometry and Material consumes the GPU's resources. If we know certain Geometries and/or Materials will repeat, we can reuse them - -### Imperative - -We can have static geometries and materials as Component's properties - -```angular-ts -@Component({ - template: ` - - - `, -}) -export class SceneGraph { - readonly sphere = new THREE.SphereGeometry(1, 32, 32); - readonly redMaterial = new THREE.MeshBasicMaterial({ color: 'red' }); -} -``` - -We can also store these static objects in a Service to reuse across the application or turn them into Signals. - -### Declarative - -We can put the Geometries and Materials declaratively on the template so they can react to Input changes; and still can reuse them - -```angular-html - - - - - -``` - -:::tip[Did you know?] - -Setting `attach="none"` on the Material and Geometry components will prevent them from being attached to the parent, which is a `Scene` in this case. - -::: - -## On-demand Rendering - -> Credit: [React Three Fiber](https://docs.pmnd.rs/react-three-fiber/advanced/scaling-performance#on-demand-rendering) - -The SceneGraph is usually rendered at 60 frames per second. This makes sense if the SceneGraph contains constantly moving parts (eg: game). Consequently, this drains the device's resources. - -If the SceneGraph has static entities, or entities that are allowed to come to a rest, constantly rendering at 60fps would be wasteful. In those cases, we can opt into on-demand rendering, which will only render when necessary. All we have to do is to set `frameloop="demand"` on the `` - -```angular-html - -``` - -When using `frameloop="demand"`, Angular Three will render: when properties of the entities change, when the camera moves via custom controls (from `angular-three-soba`) etc... -To render, `invalidate()` function needs to be called and this is done automatically by Angular Three when necessary. - -The consumers can also call `invalidate()` manually to render _on demand_. - -```angular-ts -@Component() -export class MyCmp { - private store = injectStore(); - - protected invalidate = this.store.select('invalidate'); - - constructor() { - effect(() => { - const invalidate = this.invalidate(); - - // do something - - // ready to render - invalidate(); - }) - } - - onSomething() { - // or use the invalidate() from the Store snapshot - this.store.snapshot.invalidate(); - } -} -``` diff --git a/apps/astro-docs/src/content/docs/core/advanced/portals.mdx b/apps/astro-docs/src/content/docs/core/advanced/portals.mdx deleted file mode 100644 index 26bc2e4f..00000000 --- a/apps/astro-docs/src/content/docs/core/advanced/portals.mdx +++ /dev/null @@ -1,141 +0,0 @@ ---- -title: Portals -description: Details about the Angular Three ways of handling "portals" ---- - -import { Tabs, TabItem, Code } from '@astrojs/starlight/components'; -import TemplateOutletScene, { - content as templateOutletContent, -} from '../../../../components/template-outlet/template-outlet?includeContent'; - -Portals might have different meaning depending on the use-cases. In general, -it means that we want to render something as children of something else without following -the hierarchy of the template. Pseudo-code looks something like this: - -```html - - - - - - -``` - -## `NgTemplateOutlet` - -For many cases, we can use `NgTemplateOutlet` if we just want to _portal_ objects around with (or without) different context data. In other words, we can -use this technique to _reuse_ templates. - - - -
- -
-
- - - -
- -What we're seeing here is: - -- An `Object3D` that is being moved back and forth -- A `Mesh` as a child of the `Object3D` -- A `Group` -- Another `Mesh` as a child of the `Group` - -The main takeaway here is that this `Mesh` is being _reused_ and has different color based on where it's rendered. - -```angular-html - - - - - - - - - - - - - - - - - -``` - -## `NgtParent` - -This technique is useful for when you _cannot_ control the template for, well, **ng-template**. -For example, routed components via [`ngt-router-outlet`](./routed-scene#custom-routed-scene) - -`NgtParent` is a structural directive and it takes an input `parent`. `parent` accepts - -- A `string`: which will be used to look up the object with `getObjectByName()` -- An `Object3D` -- An `ElementRef` -- or a `Signal` of all of these above - -Attaching `*parent` on an element will _portal_ that element as a child to the `parent` input. - -:::tip[Did you know?] - -Check out the [Routed Rocks example](https://demo.angularthree.org/routed-rocks) - -::: - -## `NgtPortal` - -In THREE.js, there is a construct called `WebGLRenderTarget`. It is used to render the scene into a texture and then -render the texture into the canvas. This is useful for things like post-processing effects, or HUD-like visuals. - -:::tip[Did you know?] - -Recommended read: [Beautiful and Mind-bending Effects with WebGLRenderTarget](https://blog.maximeheckel.com/posts/beautiful-and-mind-bending-effects-with-webgl-render-targets/) - -::: - -In Angular Three, we can use `NgtPortal` component to create an off-screen buffer that can be used to render secondary scenes. - -`NgtPortal` provides a _layered_ `NgtSignalStore` that its children can inject. This makes sure that children of `NgtPortal` -access the state of the `NgtPortal` and not the root `NgtSignalStore`. - -```angular-ts -@Component({ - template: ` - - - - - - - - - - - - - `, - imports: [NgtPortal, NgtPortalContent], -}) -export class HUD { - secondaryScene = new Scene(); -} -``` - -The portal can have its own scene, camera, and children. - -:::note - -The `NgtsPerspectiveCamera` in the example above is an abstraction -over `THREE.PerspectiveCamera` that has the ability to make itself the default camera for the closest `NgtSignalStore`. - -::: - -:::tip[Did you know?] - -Check out [HUD example](/blog/v2#heads-up-display-example) - -::: diff --git a/apps/astro-docs/src/content/docs/core/advanced/routed-scene.mdx b/apps/astro-docs/src/content/docs/core/advanced/routed-scene.mdx deleted file mode 100644 index 233e278b..00000000 --- a/apps/astro-docs/src/content/docs/core/advanced/routed-scene.mdx +++ /dev/null @@ -1,136 +0,0 @@ ---- -title: Routed Scene -description: Details about the Angular Three `NgtRoutedScene` ---- - -import { Tabs, TabItem } from '@astrojs/starlight/components'; - -:::caution - -Due to [a fix in Angular Router](https://github.com/angular/angular/commit/3839cfbb18fcc70cae5a6ba4ba7676b1c4acf7a0), Angular Three -implements a custom `RouterOutlet` to enable this feature. Use with caution. File an issue if you encounter any problems. - -::: - -Angular Three supports routed scenes. This is useful for when you want to have different scene graphs for different routes but keep the -root `NgtCanvas` component the same. - -To enable routed scenes, pass `'routed'` to the `sceneGraph` input of the `NgtCanvas` component. - -```angular-html - -``` - -## Example - -To start, we can create two Scene components: `RedScene` and `BlueScene` - - - - ```angular-ts - import { Component, viewChild, ChangeDetectionStrategy, CUSTOM_ELEMENTS_SCHEMA, ElementRef } from '@angular/core'; - import { injectBeforeRender } from 'angular-three'; - - @Component({ - template: ` - - - - - `, - schemas: [CUSTOM_ELEMENTS_SCHEMA], - changeDetection: ChangeDetectionStrategy.OnPush, - }) - export default class RedScene { - cubeRef = viewChild.required>('cube'); - - constructor() { - injectBeforeRender(({ clock }) => { - this.cube().nativeElement.rotation.x = clock.elapsedTime; - this.cube().nativeElement.rotation.y = clock.elapsedTime; - }); - } - } - ``` - - - - - ```angular-ts - import { Component, viewChild, ChangeDetectionStrategy, CUSTOM_ELEMENTS_SCHEMA, ElementRef } from '@angular/core'; - import { injectBeforeRender } from 'angular-three'; - - @Component({ - template: ` - - - - - `, - schemas: [CUSTOM_ELEMENTS_SCHEMA], - changeDetection: ChangeDetectionStrategy.OnPush, - }) - export default class BlueScene { - cubeRef = viewChild.required>('cube'); - - constructor() { - injectBeforeRender(({ clock }) => { - this.cube().nativeElement.rotation.x = clock.elapsedTime; - this.cube().nativeElement.rotation.y = clock.elapsedTime; - }); - } - } - ``` - - - - -Next, we'll use `RedScene` and `BlueScene` in our routing configuration. - -```ts -import { bootstrapApplication } from '@angular/platform-browser'; -import { provideRouter } from '@angular/router'; -import { AppComponent } from './app/app.component'; - -bootstrapApplication(AppComponent, { - providers: [ - provideRouter([ - { - path: '', - loadComponent: () => import('./app/red-scene.component'), - }, - { - path: 'blue', - loadComponent: () => import('./app/blue-scene.component'), - }, - ]), - ], -}).catch((err) => console.error(err)); -``` - -## Custom routed scene - -Using `'routed'` will use the default `NgtRoutedScene` component provided by Angular Three. It is also possible to have your own routed scene component. - -```html - -``` - -```angular-ts -@Component({ - template: ` - - - - `, - imports: [ - NgtRouterOutlet, // a custom router-outlet - NgtsCameraControls, - ], -}) -export class CustomRoutedScene { - static [ROUTED_SCENE] = true; // flag for `NgtRenderer` -} -``` - -Check out the [Routed Scene demo](https://demo.angularthree.org/routed) for a better example. diff --git a/apps/astro-docs/src/content/docs/core/api/args.mdx b/apps/astro-docs/src/content/docs/core/api/args.mdx deleted file mode 100644 index 1061e609..00000000 --- a/apps/astro-docs/src/content/docs/core/api/args.mdx +++ /dev/null @@ -1,47 +0,0 @@ ---- -title: NgtArgs -description: Details about the Angular Three NgtArgs structural directive ---- - -There are entities in THREE.js ecosystem that requires **Constructor Arguments** to be passed in like `OrbitControls`. -Or there are entities that require reconstructing when **Constructor Arguments** changed like the Geometries. - -```ts -let geometry = new BoxGeometry(); // [1, 1, 1] box -mesh.geometry = geometry; - -// later when we want a bigger box -mesh.geometry.dispose(); // dispose old box -// construct new box -geometry = new BoxGeometry(2, 2, 2); // [2, 2, 2] box -mesh.geometry = geometry; -``` - -To achieve this, Angular Three provides the `NgtArgs` structural directive. - -```angular-html - - - -``` - -When `boxArgs` changes, `NgtArgs` will destroy the current `BoxGeometry` instance and reconstruct a new one. - -`NgtArgs` accepts an array of **Constructor Arguments** that the entity accepts. - -```angular-html - - - - - - - - - - - - -``` - -Please consult [THREE.js documentation](https://threejs.org/) for details about the entities and their arguments. diff --git a/apps/astro-docs/src/content/docs/core/api/canvas.mdx b/apps/astro-docs/src/content/docs/core/api/canvas.mdx deleted file mode 100644 index 8becf324..00000000 --- a/apps/astro-docs/src/content/docs/core/api/canvas.mdx +++ /dev/null @@ -1,72 +0,0 @@ ---- -title: NgtCanvas -description: Angular Three Canvas API ---- - -Everything in Angular Three starts with the `NgtCanvas` component. - -```angular-ts -@Component({ - template: ` - - `, - imports: [NgtCanvas], - changeDetection: ChangeDetectionStrategy.OnPush, -}) -export class Experience { - SceneGraph = SceneGraph; -} -``` - -## Inputs - -| Property | Description | Default Value | -| -------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------- | -| **sceneGraph** | (required) A Component class that is the root of the Scene Graph. It can also accept `'routed'` and `NgtCanvas` will use `NgtRoutedScene` by default to enable routed Scene Graph. | - | -| orthographic | A `boolean` flag to determine whether to create an `OrthographicCamera` instead of a `PerspectiveCamera`. | false | -| camera | Options for the default camera. Pass in the options accordingly depending on whether the camera is `Orthographic` or `Perspective` | - | -| lookAt | A `Vector3` or `Partial` that sets the default lookAt position. | - | -| frameloop | A `string` that sets the frameloop for the canvas. It can also accept `'always'`, `'demand'`, or `'never'` to set the frameloop type. | 'always' | -| gl | A `WebGLRenderer` instance or properties that go into the default renderer. | - | -| scene | A `Scene` instance or properties that go into the default scene. | - | -| raycaster | A `Partial` object that allows you to customize the raycaster. | - | -| legacy | A `boolean` flag to determine whether to use legacy lights. | false | -| linear | A `boolean` flag to determine whether to use linear color space. | false | -| flat | A `boolean` flag to determine whether to use flat shading. | false | -| shadows | A `boolean` flag to determine whether to enable shadows `PCFSoftShadowMap`. It can also accept a `string` from `'basic', 'percentage', 'soft', or 'variance'` to set the shadow map type. It can also accept a `Partial` | false | -| events | A function that allows you to customize the event handler for pointer events. It takes an `EventManager` as an argument and returns an `EventManager`. You can use this to register custom event handlers for pointer events. | - | -| eventSource | The element that the events will be attached to. If not provided, the events will be attached to the host element of `NgtCanvas` component | - | -| eventPrefix | A `string` that sets the event prefix for the canvas. | 'offset' | -| size | Dimensions of the canvas. It can also accept a `Partial` to set the width and height. | - | -| performance | A `Partial` object that allows you to customize the performance of the canvas. | - | -| dpr | A `number` or `[min, max]` that sets the device pixel ratio. | - | - -## Outputs - -| Property | Description | -| ------------- | ------------------------------------------------------- | -| created | Emit when the canvas is created but before rendering | -| pointerMissed | Emit when a pointer event is not captured by any object | - -## Canvas Defaults - -If you have both the scene graphs in Vanilla THREE.js and Angular Three, and you see that the one in Angular Three _looks different_, it might be because of the defaults that Angular Three sets for the underlying THREE.js building blocks. - -`NgtCanvas` sets up a translucent `THREE.WebGLRenderer` with the following constructor arguments: - -- antialias = true -- alpha = true -- powerReference = 'high-performance' - -and the following properties: - -- outputEncoding = THREE.sRGBEncoding -- toneMapping = THREE.ACESFilmicToneMapping - -A `window:resize` listener that will update the `THREE.Renderer` and `THREE.Camera` when the container is resized. - -From THREE.js 0.139+, `THREE.ColorManagement.legacyMode` is set to `false` to enable automatic conversion of colors based on the Renderer's configured color space. For more on this topic, check [THREE.js Color Management](https://threejs.org/docs/#manual/en/introduction/Color-management). diff --git a/apps/astro-docs/src/content/docs/core/api/custom-renderer.mdx b/apps/astro-docs/src/content/docs/core/api/custom-renderer.mdx deleted file mode 100644 index a98ade93..00000000 --- a/apps/astro-docs/src/content/docs/core/api/custom-renderer.mdx +++ /dev/null @@ -1,408 +0,0 @@ ---- -title: Custom Renderer -description: Details about the Angular Three Custom Renderer capabilities ---- - -## Catalogue - -Angular Three Custom Renderer maintains a **single catalogue** of entities to render. By default, the catalogue is empty. - -### `extend` - -In order to populate the catalogue, call the `extend` function and pass in a `Record` of entities. Angular Three then maps the catalogue to Custom Elements tags with the following naming convention: - -```ts -extend({ - Mesh, // makes ngt-mesh available - BoxGeometry, // makes ngt-box-geometry available - /* ... */, - MyMesh: Mesh, // makes ngt-my-mesh available -}) -``` - -:::tip[Did you know?] - -- `extend` should be called outside of the Component/Directive or in the `constructor`. This is before rendering takes place. -- `extend` can be called multiple times and will merge the `catalogue` with the new entities. - - ```ts - extend({ Mesh, MeshBasicMaterial }); - extend({ OrbitControls }); - ``` - -::: - -### `CUSTOM_ELEMENTS_SCHEMA` - -The `CUSTOM_ELEMENTS_SCHEMA` is required to use Angular Three elements as Angular does not support custom schemas at the moment. - -```angular-ts -@Component({ - template: ``, - schemas: [CUSTOM_ELEMENTS_SCHEMA], -}) -export class Experience {} -``` - -:::tip[Did you know?] - -Only components that use custom NGT elements need to have `CUSTOM_ELEMENTS_SCHEMA`. This flag only affects the compiler, not the runtime. - -::: - -## Property Bindings - -We can use property bindings to pass data to any of the NGT elements' properties. - -```angular-html {2-4,8} - - - - -``` - -:::note - -Angular Three tries its best to provide some sort of intellisense for the elements but it's not always possible. -Please consult THREE.js documentation for more information about the elements' properties. - -::: - -### Shortcuts - -Some THREE.js entities have different methods to update their values due to performance and how WebGL works. For example, `THREE.Vector3` has a `set` method that accepts a tuple of `[x, y, z]` instead. -On the other hand, if we pass in a `THREE.Vector3` instance, the current `THREE.Vector3` will call `copy` and pass in the new instance instead. - -This characteristic is baked into the Angular Three Custom Renderer so it is more intuitive to pass values to these properties. - -#### `NgtVector*` - -`NgtVector2`, `NgtVector3`, and `NgtVector4` are shortcuts types for `THREE.Vector2`, `THREE.Vector3`, and `THREE.Vector4` respectively. - -```angular-html {"accepts a tuple": 2-3} {"accepts a scalar": 4-5} {"accepts an instance": 6-7} - -``` - -#### `NgtEuler` and `NgtQuaternion` - -Similar to `NgtVector*`, these types are shortcuts for `THREE.Euler` and `THREE.Quaternion` respectively. - -#### `ColorRepresentation` - -Similar to `NgtVector*`, any elements that accept a `color` property can accept a `ColorRepresentation` type. This is -a THREE.js type and Angular Three Custom Renderer will apply the value properly. - -```angular-html {"accept a css color": 2-3} {"accept a hex color": 5-6} {"accept rgb/hsl": 8-9} {"accept an instance": 11-12} {"accept a hexadecimal number": 14-15} - - - - - - - - - - - - - - - -``` - -### NGT Properties - -Aside from the elements' own properties, there are a few properties that are specific to the Angular Three Custom Renderer. - -#### `parameters` - -All custom elements accept a `parameters` property that accepts an object of properties to pass to the underlying entity. - -```angular-html - -``` - -#### `attach` - -This property is used to specify a property on the parent that this element should be **attached** to. Attaching takes into account -the life-cycle of the elements and will automatically detach when the elements are destroyed. - -##### Static Value - -If the property on the parent is a static value, use Attribute Binding to bind a static `string` to the `attach` property. - -```angular-html - - - -``` - -This is equivalent to: - -```ts -const mesh = new THREE.Mesh(); -const material = new THREE.MeshBasicMaterial(); - -mesh.material = material; -``` - -:::tip[Did you know?] - -- All Geometries have `attach="geometry"` by default. -- All Materials have `attach="material"` by default. - -```angular-html - - - - - - -``` - -::: - -##### Nested Path - -We can attach to a nested property on the parent by using a dot-separated path. - -```angular-html - - - -``` - -This is equivalent to: - -```ts -const spotLight = new THREE.SpotLight(); -spotLight.castShadow = true; - -const vector2 = new THREE.Vector2(); -// shortcut is still applied automatically -spotLight.shadow.mapSize.copy(vector2); -``` - -##### Dynamic Value - -We can pass a dynamic value to `attach` property by using Property Binding syntax `[attach]`. When this is the case, `attach` -accepts `Array` as well as `string` - -```angular-ts -@Component({ - template: ` - - - @for (color of colors; track $index) { - - } - - ` -}) -export class MyCube { - colors = [ - 'red', - 'green', - 'blue', - 'yellow', - 'orange', - 'purple', - ]; // cube has 6 faces -} -``` - -This is equivalent to: - -```ts -const mesh = new THREE.Mesh(); -const geometry = new THREE.BoxGeometry(); - -mesh.geometry = geometry; -mesh.material = []; - -const colors = ['red', 'green', 'blue', 'yellow', 'orange', 'purple']; - -for (let i = 0; i < colors.length; i++) { - const material = new THREE.MeshLambertMaterial(); - material.color.set(colors[i]); - mesh.material[i] = material; -} -``` - -##### `NgtAttachFunction` - -Optionally, we can pass a `NgtAttachFunction` to `attach` property. We are responsible for attaching and detaching the elements. - -```angular-ts -import { createAttachFunction } from 'angular-three'; - -@Component({ - template: ` - - - - ` -}) -export class Experience { - attachFn = createAttachFunction(({ parent, child }) => { - const oldMaterial = parent.material; - parent.material = child; - - // return a clean-up function that will be called when `ngt-mesh-basic-material` is destroyed - return () => { - parent.material = oldMaterial; - } - }) -} -``` - -#### `priority` - -See [Render Priority](#render-priority) for more information. - -## Event Bindings - -### Pointer Events - -:::note - -The events system in NGT is completely ported from R3F. For more information, please check [React Three Fiber Events](https://docs.pmnd.rs/react-three-fiber/api/events) - -::: - -| Event Name | Description | -| ------------- | ------------------------------------------------------------------- | -| click | If observed, emits when the object is clicked. | -| contextmenu | If observed, emits when the object is right-clicked. | -| dblclick | If observed, emits when the object is double clicked. | -| pointerup | If observed, emits when the pointer moves up while on the object. | -| pointerdown | If observed, emits when the pointer moves down while on the object. | -| pointerover | If observed, emits when the pointer is over the object. | -| pointerout | If observed, emits when the pointer gets on then out of the object. | -| pointerenter | If observed, emits when the pointer gets on the object. | -| pointerleave | If observed, emits when the pointer gets on then out of the object. | -| pointermove | If observed, emits when the pointer moves while on the object. | -| pointermissed | If observed, emits when the pointer misses the object. | -| pointercancel | If observed, emits when the current pointer event gets cancelled. | -| wheel | If observed, emits when the wheel is acted on when on the object. | - -### `beforeRender` - -:::caution - -This is **discouraged** at the moment due to how Angular Zoneless Change Detection works. -`(beforeRender)` handler will trigger change detection on every frame if used with `provideExperimentalZonelessChangeDetection()`. - -In the meantime, [`injectBeforeRender`](/core/utilities/before-render) is a better alternative. - -::: - -To register a callback in the animation loop, listen to the `beforeRender` event on a NGT element. - -```angular-ts -@Component({ - template: ` - - ` -}) -export class Experience { - onBeforeRender(event: NgtBeforeRenderEvent) { - const { object, state } = event; - // runs on every frame - } -} -``` - -When the element is destroyed, the callback will be removed automatically. - -#### Render Priority - -By default, NGT renders the scene on every frame. -If we need to control this process, we can pass `priority` as **Attribute Binding** with number-string values -to any object whose `(beforeRender)` is being listened to. -When a `priority` is set, we are responsible to render our scene. - -```angular-ts -@Component({ - template: ` - - - `, -}) -export class SceneGraph { - onBeforeRender(event: NgtBeforeRenderEvent) { - const { gl, scene, camera } = event.state; - // do something - gl.render(scene, camera); - // do something else - } - - onOtherBeforeRender(event: NgtBeforeRenderEvent) { - // this runs after the above beforeRender - } -} -``` - -### `attached` - -This event is emitted when the element is **attached** or **added** to the parent. - -```angular-ts -@Component({ - template: ` - - - - ` -}) -export class Experience { - onAttached(event: NgtAfterAttach) { - const { parent, node } = event; - // ^? Mesh ^? MeshBasicMaterial - } -} -``` - -### `updated` - -This event is emitted when the element is **updated**. - -```angular-ts -@Component({ - template: ` - - ` -}) -export class Experience { - onUpdated(event: Mesh) { } -} -``` diff --git a/apps/astro-docs/src/content/docs/core/api/primitive.mdx b/apps/astro-docs/src/content/docs/core/api/primitive.mdx deleted file mode 100644 index d19c8004..00000000 --- a/apps/astro-docs/src/content/docs/core/api/primitive.mdx +++ /dev/null @@ -1,41 +0,0 @@ ---- -title: Primitive -description: Details about the Angular Three `ngt-primitive` ---- - -There are occasions where we have a pre-existing entities that we want to include in our Angular Three Scene Graph, -we can use the `ngt-primitive` element to do this. - -```angular-html - -``` - -:::note - -Learn more about [`NgtArgs`](/core/api/args) - -::: - -- `ngt-primitive` always requires `*args` with the entity to render. -- `ngt-primitive` accepts `parameters` which are passed to the entity. -- Attaching works the same for `ngt-primitive` -- Differ from other Custom Elements, Angular Three Custom Renderer does not destroy the entity for `ngt-primitive` - -A more realistic example would be to use `ngt-primitive` to render a GLTF model. - -```angular-ts -@Component({ - template: ` - - `, - imports: [NgtArgs] -}) -export class Model { - private gltf = injectGLTF(() => 'path/to/model.glb'); - protected model = computed(() => { - const gltf = this.gltf(); - if (!gltf) return null; - return gltf.scene; - }); -} -``` diff --git a/apps/astro-docs/src/content/docs/core/api/raw-value.mdx b/apps/astro-docs/src/content/docs/core/api/raw-value.mdx deleted file mode 100644 index d1d50c6f..00000000 --- a/apps/astro-docs/src/content/docs/core/api/raw-value.mdx +++ /dev/null @@ -1,26 +0,0 @@ ---- -title: Raw Value -description: Details about the Angular Three `ngt-value` ---- - -There are occasions where we want to **declaratively** set a value on a property of the parent element, we can use `ngt-value` -element to do this. - -```angular-html - - - - - -``` - -:::note - -We can achieve the same result by using `parameters` property on the `ngt-point-light` element if we don't have to worry about -the life-cycle of the `ngt-value` or using control-flow with `ngt-value` - -```angular-html - -``` - -::: diff --git a/apps/astro-docs/src/content/docs/core/api/store.mdx b/apps/astro-docs/src/content/docs/core/api/store.mdx deleted file mode 100644 index fa031133..00000000 --- a/apps/astro-docs/src/content/docs/core/api/store.mdx +++ /dev/null @@ -1,98 +0,0 @@ ---- -title: Store -description: Details about the Angular Three Store ---- - -Angular Three `NgtCanvas` starts with a default `NgtSignalStore` that is used to keep track of -the state of the canvas like the Renderer, Camera, Scene, Controls, events, size, etc... - -We can access this store via the `injectStore` function - -:::caution - -`injectStore` **must be used** inside of `NgtCanvas` component - -::: - -## Reactive State - -The store exposes its properties as `Signal` via the `select()` function. `select()` creates a `computed` Signal -under the hood and caches the created Signal. - -```angular-ts -export class Experience { - private store = injectStore(); - camera = this.store.select('camera'); // Signal - gl = this.store.select('gl'); // Signal -} -``` - -We can access nested properties of the store by passing more arguments to `select()` - -```angular-ts -export class Experience { - private store = injectStore(); - domElement = this.store.select('gl', 'domElement'); // Signal - width = this.store.select('size', 'width'); // Signal - height = this.store.select('size', 'height'); // Signal -} -``` - -:::note - -`select()` supports up to 4 arguments to access up to 4 levels of the store's property. -Any more than that will probably be better off using `computed()` - -::: - -To access the entire store, we can use `select()` without any arguments or use `state` property. - -```angular-ts -export class Experience { - private store = injectStore(); - state = this.store.state; // Signal; -} -``` - -## Snapshot State - -We can access the **latest** state of the store via the `snapshot` property. `snapshot` always returns the latest state -upon accessing. - -```angular-ts -export class Experience { - private store = injectStore(); - - constructor() { - afterNextRender(() => { - const { gl, camera, size, /* ... */ } = this.store.snapshot; - }) - } -} -``` - -This is useful when we want to access the latest state **imperatively** without reactivity. Most of the time, this is used in -the animation loop. - -### `get()` - -Alternatively, we can access the snapshot properties as values via `get()`. - -```angular-ts -export class Experience { - private store = injectStore(); - camera = this.store.get('camera'); // NgtCamera - gl = this.store.get('gl'); // WebGLRenderer -} -``` - -We can access nested properties of the store by passing more arguments to `get()` - -```angular-ts -export class Experience { - private store = injectStore(); - domElement = this.store.get('gl', 'domElement'); // HTMLCanvasElement - width = this.store.get('size', 'width'); // number - height = this.store.get('size', 'height'); // number -} -``` diff --git a/apps/astro-docs/src/content/docs/core/getting-started/editor-setup.mdx b/apps/astro-docs/src/content/docs/core/getting-started/editor-setup.mdx deleted file mode 100644 index 76791398..00000000 --- a/apps/astro-docs/src/content/docs/core/getting-started/editor-setup.mdx +++ /dev/null @@ -1,28 +0,0 @@ ---- -title: Code Editor Setup -description: Set up your code editor for Angular Three ---- - -import { Tabs, TabItem } from '@astrojs/starlight/components'; - -Angular Three uses a Custom Renderer and works with THREE.js via a collection of Custom Elements. Hence, -we need to set up our code editor to recognize these Custom Elements. - - - - Angular Three provides and sets up a `web-types.json` file that can be used with Jetbrains IDEs out of the box. - - In the case where manual setup is required, `web-types.json` can be found in the `node_modules/angular-three/web-types.json` - - - - Angular Three provides a `metadata.json` file that can be used with VSCode `html.customData` setting. - Please refer to the [VSCode documentation](https://code.visualstudio.com/docs/languages/html#_html-custom-data) for more information. - - The `metadata.json` file can be found in the `node_modules/angular-three/metadata.json` - - - - TBD - - diff --git a/apps/astro-docs/src/content/docs/core/getting-started/first-scene.mdx b/apps/astro-docs/src/content/docs/core/getting-started/first-scene.mdx deleted file mode 100644 index 052e4655..00000000 --- a/apps/astro-docs/src/content/docs/core/getting-started/first-scene.mdx +++ /dev/null @@ -1,571 +0,0 @@ ---- -title: Our First 3D Scene -description: Create our first 3D scene with Angular Three. ---- - -import { Tabs, TabItem } from '@astrojs/starlight/components'; -import FirstScene from '../../../../components/first-scene/first-scene'; - -Before diving into the API details of Angular Three, let's create a simple scene together to get a feel for what it's like to use Angular Three. - -:::note - -- This guide assumes that you have knowledge of [Angular](https://angular.io/) and [THREE.js](https://threejs.org/). -- Start a new project using the [Angular CLI](https://angular.io/cli) or [Nx](https://nx.dev/) then follow the [Installation](/core/getting-started/installation) guide. - -::: - -## Create the `SceneGraph` component - -This component will be the root of our scene graph. - -```angular-ts title="src/app/scene-graph.component.ts" -import { Component, CUSTOM_ELEMENTS_SCHEMA, ChangeDetectionStrategy } from '@angular/core'; - -@Component({ - template: ` - - `, - schemas: [CUSTOM_ELEMENTS_SCHEMA], - changeDetection: ChangeDetectionStrategy.OnPush, -}) -export class SceneGraph {} -``` - -- `CUSTOM_ELEMENTS_SCHEMA` is required to use Angular Three components. -- `selector` is left empty because we're not rendering `SceneGraph` as a regular Angular component. - -## Set up the Canvas - -The `SceneGraph` component will be rendered by the `NgtCanvas` component. - -```angular-ts title="src/app/app.component.ts" {8,10,13} -import { Component } from '@angular/core'; -import { NgtCanvas } from 'angular-three'; -import { SceneGraph } from './scene-graph.component'; - -@Component({ - selector: 'app-root', - template: ` - - `, - imports: [NgtCanvas], -}) -export class AppComponent { - protected sceneGraph = SceneGraph; -} -``` - -`NgtCanvas` uses the `sceneGraph` input to render the `SceneGraph` component with the Custom Renderer as well as sets up the following THREE.js building blocks: - -- A `WebGLRenderer` with anti-aliasing enabled and transparent background. -- A default `PerspectiveCamera` with a default position of `[x:0, y:0, z:5]`. -- A default `Scene` -- A render loop that renders the scene on every frame -- A `window:resize` event listener that automatically updates the Renderer and Camera when the viewport is resized - -### Set the dimensions of the canvas - -We'll set up some basic styles in `styles.css` - -```css title="src/styles.css" -html, -body { - height: 100%; - width: 100%; - margin: 0; -} -``` - -Next, let's set some styles for `:host` in `src/app/app.component.ts` - -```diff lang="angular-ts" -import { Component } from '@angular/core'; -import { NgtCanvas } from 'angular-three'; -import { SceneGraph } from './scene-graph.component'; - -@Component({ - selector: 'app-root', - template: ` - - `, - imports: [NgtCanvas], -+ styles: [` -+ :host { -+ display: block; -+ height: 100dvh; -+ } -+ `], -}) -export class AppComponent { - protected sceneGraph = SceneGraph; -} -``` - -:::tip[Did you know?] - -`NgtCanvas` is designed to fill the parent container so we can control the dimensions of the canvas using the parent container's dimensions. - -```html -
- -
-``` - -::: - -## Extending Angular Three Catalogue - -As a custom renderer, Angular Three maintains a **single catalogue** of entities to render. By default, the catalogue is empty. We can extend the catalogue by calling the `extend` function and pass in a `Record` of entities. Angular Three then maps the catalogue to Custom Elements tags with the following naming convention: - -``` - -``` - -For example: - -```ts -import { extend } from 'angular-three'; -import { Mesh, BoxGeometry } from 'three'; - -extend({ - Mesh, // makes ngt-mesh available - BoxGeometry // makes ngt-box-geometry available, - MyMesh: Mesh, // makes ngt-my-mesh available -}); -``` - -:::tip[Did you know?] - -The catalogue isn't limited to THREE.js core entities. We can extend the catalogue with any custom THREE.js entities the same way. - -```ts -import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls'; - -extend({ - OrbitControls, // makes ngt-orbit-controls available -}); -``` - -::: - -:::caution - -We can also extend the entire THREE.js namespace by passing in `THREE` as the only argument to `extend`. However, this is not recommended as it brings in all of THREE.js and bloats the bundle size. - -```ts -import { extend } from 'angular-three'; -import * as THREE from 'three'; - -extend(THREE); // everything in THREE is now available -``` - -::: - -For the purpose of this guide, we'll extend THREE.js namespace so we do not have to go back and forth to extend more entities as we go. - -```diff lang="angular-ts" title="src/app/app.component.ts" -- import { NgtCanvas } from 'angular-three'; -+ import { NgtCanvas, extend } from 'angular-three'; -+ import * as THREE from 'three'; - -+ extend(THREE); - -/* the rest of the code remains the same */ -``` - -## Render a THREE.js Entity - -Now that we have extended the THREE.js namespace, we can render a THREE.js entity. Let's render a cube with a `Mesh` and `BoxGeometry` from THREE.js. - -```diff lang="angular-ts" title="src/app/scene-graph.component.ts" -import { Component, CUSTOM_ELEMENTS_SCHEMA, ChangeDetectionStrategy } from '@angular/core'; - -@Component({ - template: ` -+ -+ -+ - `, - schemas: [CUSTOM_ELEMENTS_SCHEMA], - changeDetection: ChangeDetectionStrategy.OnPush, -}) -export class SceneGraph {} -``` - -And here's the result: - -
- -
- -## Animation - -The best way to animate a THREE.js entity is to participate in the animation loop with `injectBeforeRender`. Let's animate the cube by rotating it on the X and Y axes. - -```angular-ts title="src/app/scene-graph.component.ts" {"Import injectBeforeRender": 2-3} {"Use template variable": 8-9} {"Get the reference with viewChild": 17-18} {"Animate the cube": 21-26} -import { Component, CUSTOM_ELEMENTS_SCHEMA, ChangeDetectionStrategy, ElementRef, viewChild } from '@angular/core'; - -import { injectBeforeRender } from 'angular-three'; -import { Mesh } from 'three'; - -@Component({ - template: ` - - - - - `, - schemas: [CUSTOM_ELEMENTS_SCHEMA], - changeDetection: ChangeDetectionStrategy.OnPush, -}) -export class SceneGraph { - - meshRef = viewChild.required>('mesh'); - - constructor() { - - injectBeforeRender(() => { - const mesh = this.meshRef().nativeElement; - mesh.rotation.x += 0.01; - mesh.rotation.y += 0.01; - }); - } -} -``` - -
- -
- -## Make a Component - -Using Angular means we can make components out of our template. Let's do that for our cube - - - - ```angular-ts - import { Component, CUSTOM_ELEMENTS_SCHEMA, ElementRef, viewChild } from '@angular/core'; - import { injectBeforeRender } from 'angular-three'; - import { Mesh } from 'three'; - - @Component({ - selector: 'app-cube', - template: ` - - - - `, - schemas: [CUSTOM_ELEMENTS_SCHEMA] - }) - export class Cube { - meshRef = viewChild.required>('mesh'); - - constructor() { - injectBeforeRender(({ delta }) => { - const mesh = this.meshRef().nativeElement; - mesh.rotation.x += delta; - mesh.rotation.y += delta; - }); - } - } - ``` - - - - - ```diff lang="angular-ts" - import { Component, CUSTOM_ELEMENTS_SCHEMA, ChangeDetectionStrategy } from '@angular/core'; - +import { Cube } from './cube.component'; - - @Component({ - template: ` - - - - - - - + - `, - + imports: [Cube], - schemas: [CUSTOM_ELEMENTS_SCHEMA], - changeDetection: ChangeDetectionStrategy.OnPush, - }) - export class SceneGraph {} - ``` - - - - -Everything is the same as before, except we now have a `Cube` component that -can have its own state and logic. - -We will add 2 states `hovered` and `clicked` to the cube component: - -- When the cube is hovered, we'll change its color -- When the cube is clicked, we'll change its scale - -```diff lang="angular-ts" title="src/app/cube.component.ts" -- import { Component, CUSTOM_ELEMENTS_SCHEMA, ElementRef, viewChild } from '@angular/core'; -+ import { Component, CUSTOM_ELEMENTS_SCHEMA, ElementRef, viewChild, signal } from '@angular/core'; -import { injectBeforeRender } from 'angular-three'; -import { Mesh } from 'three'; - -@Component({ - selector: 'app-cube', - template: ` - - -+ - - `, - schemas: [CUSTOM_ELEMENTS_SCHEMA] -}) -export class Cube { - meshRef = viewChild.required>('mesh'); - -+ hovered = signal(false); -+ clicked = signal(false); - - constructor() { - injectBeforeRender(() => { - const mesh = this.meshRef().nativeElement; - mesh.rotation.x += 0.01; - mesh.rotation.y += 0.01; - }); - } -} -``` - -Our cube is now _interactive_! - -
- -
- -### Render another Cube - -Just like any other Angular component, we can render another cube by adding another `` tag to the template. -However, we need to render the cube in different positions so we can see them both on the scene. - -Let's do that by adding a `position` input to the Cube component - -```diff lang="angular-ts" title="src/app/cube.component.ts" -- import { Component, CUSTOM_ELEMENTS_SCHEMA, ElementRef, viewChild, signal } from '@angular/core'; -+ import { Component, CUSTOM_ELEMENTS_SCHEMA, ElementRef, viewChild, signal, input } from '@angular/core'; - -- import { injectBeforeRender } from 'angular-three'; -+ import { injectBeforeRender, NgtVector3 } from 'angular-three'; -import { Mesh } from 'three'; - -@Component({ - selector: 'app-cube', - template: ` - - - - - `, - schemas: [CUSTOM_ELEMENTS_SCHEMA] -}) -export class Cube { -+ position = input([0, 0, 0]); - - meshRef = viewChild.required>('mesh'); - - hovered = signal(false); - clicked = signal(false); - - constructor() { - injectBeforeRender(() => { - const mesh = this.meshRef().nativeElement; - mesh.rotation.x += 0.01; - mesh.rotation.y += 0.01; - }); - } -} -``` - -`position` input is a `NgtVector3` which is an _expanded_ version of `THREE.Vector3`. It can accept: - -- A `THREE.Vector3` instance -- A tuple of `[x, y, z]` -- A scalar value that will be used for all axes - -```diff lang="angular-ts" title="src/app/scene-graph.component.ts" -import { Component, CUSTOM_ELEMENTS_SCHEMA, ChangeDetectionStrategy } from '@angular/core'; -import { Cube } from './cube.component'; - -@Component({ - template: ` -- -+ -+ - `, - imports: [Cube], - schemas: [CUSTOM_ELEMENTS_SCHEMA], - changeDetection: ChangeDetectionStrategy.OnPush, -}) -export class SceneGraph {} -``` - -and now we have 2 cubes that have their own states, and react to events independently. - -
- -
- -## Lighting - -Let's add some lights to our scene to make the cubes look more dynamic as they look bland at the moment. - -```diff lang="angular-ts" title="src/app/scene-graph.component.ts" -import { Component, CUSTOM_ELEMENTS_SCHEMA, ChangeDetectionStrategy } from '@angular/core'; -import { Cube } from './cube.component'; - -@Component({ - template: ` -+ -+ -+ - - - - `, - imports: [Cube], - schemas: [CUSTOM_ELEMENTS_SCHEMA], - changeDetection: ChangeDetectionStrategy.OnPush, -}) -export class SceneGraph { -+ protected readonly Math = Math; -} -``` - -Next, we will want to change the `Material` of the cube to `MeshStandardMaterial` so that it can be lit by the lights. - -```diff lang="angular-ts" title="src/app/cube.component.ts" -import { Component, CUSTOM_ELEMENTS_SCHEMA, ElementRef, viewChild, signal, input } from '@angular/core'; -import { injectBeforeRender, NgtVector3 } from 'angular-three'; -import { Mesh } from 'three'; - -@Component({ - selector: 'app-cube', - template: ` - - -- -+ - - `, - schemas: [CUSTOM_ELEMENTS_SCHEMA], -}) -export class Cube { - position = input([0, 0, 0]); - - meshRef = viewChild.required>('mesh'); - - hovered = signal(false); - clicked = signal(false); - - constructor() { - injectBeforeRender(() => { - const mesh = this.meshRef().nativeElement; - mesh.rotation.x += 0.01; - mesh.rotation.y += 0.01; - }); - } -} -``` - -Our cubes look better now, with dimensionality, showing that they are 3D objects. - -
- -
- -## Bonus: Take control of the camera - -Who hasn't tried to _grab_ the scene and move it around? Let's take control of the camera and make it move around with `OrbitControls`. - -```sh -npm install three-stdlib -# yarn add three-stdlib -# pnpm add three-stdlib -``` - -`three-stdlib` provides a better API to work with THREE.js extra modules like `OrbitControls`. - -```diff lang="angular-ts" title="src/app/scene-graph.component.ts" -import { Component, CUSTOM_ELEMENTS_SCHEMA, ChangeDetectionStrategy } from '@angular/core'; -+import { injectStore, extend, NgtArgs } from 'angular-three'; -+import { OrbitControls } from 'three-stdlib'; -import { Cube } from './cube.component'; - -+extend({ OrbitControls }); // makes ngt-orbit-controls available - -@Component({ - template: ` - - - - - - - -+ -+ - `, -- imports: [Cube], -+ imports: [Cube, NgtArgs], - schemas: [CUSTOM_ELEMENTS_SCHEMA], - changeDetection: ChangeDetectionStrategy.OnPush, -}) -export class SceneGraph { - protected readonly Math = Math; - -+ private store = injectStore(); -+ protected camera = this.store.select('camera'); -+ protected glDomElement = this.store.select('gl', 'domElement'); -} -``` - -If we were to use `OrbitControls` in a vanilla THREE.js application, we would need to -instantiate it with the `camera` and `WebGLRenderer#domElement`. - -With Angular Three, we use `NgtArgs` structural directive to pass _Constructor Arguments_ to the underlying element. - -To access the `camera` and `glDomElement`, we use `injectStore` to access the state of the canvas. - -
- -
- -And that concludes our guide. We have learned how to create a basic scene, add some lights, and make our cubes interactive. -We also learned how to use `NgtArgs` to pass arguments to the underlying THREE.js elements. Finally, we learned how to -use `injectStore` to access the state of the canvas. - -## What's next? - -Now that we have a basic understanding of how to create a scene, we can start building more complex scenes. - -- Try different geometries and materials -- Try different lights -- Immerse yourself in the world of THREE.js diff --git a/apps/astro-docs/src/content/docs/core/getting-started/installation.mdx b/apps/astro-docs/src/content/docs/core/getting-started/installation.mdx deleted file mode 100644 index 2176e997..00000000 --- a/apps/astro-docs/src/content/docs/core/getting-started/installation.mdx +++ /dev/null @@ -1,81 +0,0 @@ ---- -title: Installation -description: Install Angular Three and its dependencies. ---- - -import { Tabs, TabItem } from '@astrojs/starlight/components'; - - - - The easiest way to get started with Angular Three is to use the [template repository](https://github.com/angular-threejs/template) - - The template repository is kept up to date with the latest version of Angular Three. - - :::tip[Did you know?] - - If you have the [Github CLI](https://cli.github.com/) installed, you can use the `gh repo create` command to clone the template repository directly from the command line. - - ```shell - gh repo create / - --template angular-threejs/template - --public - --clone - ``` - - ::: - - - - Angular Three provides an `ng-add` schematic to set up `angular-three` and its dependencies. - - ```shell - ng add angular-three - ``` - - - - 1. Install `angular-three` as a dependency - - ```shell - npm install angular-three - # yarn add angular-three - # pnpm add angular-three - ``` - - 2. Run the `init` generator to set up `angular-three` and its dependencies. - - ```shell - nx generate angular-three:init - ``` - - - - 1. Install `angular-three` and its dependencies. - - ```shell - npm install angular-three three ngxtension - # yarn add angular-three three ngxtension - # pnpm add angular-three three ngxtension - ``` - - 2. Install `@types/three` as a development dependency. - - ```shell - npm install --save-dev @types/three - # yarn add --dev @types/three - # pnpm add --dev @types/three - ``` - - 3. Turn on `skipLibCheck` in `tsconfig.json` file (if haven't already) - - ```json {3} - { - "compilerOptions": { - "skipLibCheck": true - } - } - ``` - - - - diff --git a/apps/astro-docs/src/content/docs/core/migrate-v2.mdx b/apps/astro-docs/src/content/docs/core/migrate-v2.mdx deleted file mode 100644 index 53c67b56..00000000 --- a/apps/astro-docs/src/content/docs/core/migrate-v2.mdx +++ /dev/null @@ -1,151 +0,0 @@ ---- -title: Migrate to V2 -description: Details about Angular Three v2 migration ---- - -As mentioned in the [v2 release blog post](https://angularthree.org/blog/v2), Angular Three v2 is a major release with a substantial change in the migration strategy. It aims to address the limitations of the previous version, resulting in numerous breaking changes. Some of these changes are subtle and may not be immediately apparent. - -While it is mentioned that migration path cannot be provided in details, I will try my best to provide a non-exhaustive overview of the changes and how to migrate. - -## Custom Renderer - -While many have noticed the custom renderer usage from the previous version of the documentation, it has been brought to my attention that some folks are still using `angular-three` before the custom renderer. - -Here's a quick summary: - -- Use `NgtCanvas` with `sceneGraph` input instead of putting your scene graph as content child of `ngt-canvas` - -```diff lang="angular-html" -- -- -- -- -- -- - -+ -``` - -- `sceneGraph` is a reference to a `Component` that renders your entire scene graph. - -```angular-ts -@Component({ - standalone: true, - template: ` - - - - - `, - schemas: [CUSTOM_ELEMENTS_SCHEMA], -}) -export class SceneGraph {} - -@Component({ - template: ` - - `, - imports: [NgtCanvas], -}) -export class App { - protected sceneGraph = SceneGraph; -} -``` - -- `CUSTOM_ELEMENTS_SCHEMA` is required. Please refer to the [Custom Renderer](/core/api/custom-renderer) documentation for more information. -- All `ngt-*` elements do not have an importable symbol (they're not `Directive` nor `Component`). - -## Scene Graph Inputs - -Since the `sceneGraph` reference is passed in as an input and the `NgtCanvas` will render the scene graph with the custom renderer, there is no straightforward way to pass Inputs to the scene graph component itself. - -Use a shared service or a shared `Signal` to pass data from outside to the Scene Graph component. - -```angular-ts -const count = signal(0); - -@Component({ - template: ``, -}) -export class SceneGraph { - onClick() { - count.update((prev) => prev + 1); - } -} - -@Component({ - template: ` - -

Current count: {{ count() }}

- `, -}) -export class App { - protected sceneGraph = SceneGraph; - protected count = count; -} -``` - -## Custom Inject Functions - -All of `injectNgt*` functions have been renamed to just `inject*` instead. - -```diff -- injectNgtLoader(); -+ injectLoader(); - -- injectNgtStore(); -+ injectStore(); -``` - -## `NgtSignalStore` - -While not common, consumers can use `signalStore()` to create a `NgtSignalStore`. The purpose of `NgtSignalStore` is a minimal store implementation that `angular-three` uses internally. - -### Methods - -1. `select(...keys, options?)`: Retrieves a Signal of a part of the state. - - - Can select nested properties using multiple arguments - - Returns `Signal` where T is the type of the selected state - -2. `get(...keys)`: Retrieves the current value of a part of the state. - - - Can access nested properties using multiple arguments - - Returns the value directly, not wrapped in a Signal - -3. `update(stateOrUpdater)`: Updates the state. - - - Accepts a partial state object or an updater function - - Updater function receives the previous state and returns a partial state - -4. `state`: A Signal representing the entire state. - - - Equivalent to `select()` without arguments - -5. `snapshot`: A getter that returns the current state value. - - Equivalent to `get()` without arguments - -### Creation - -Created using the `signalStore` function: - -```typescript -signalStore( - initialState?: Partial | ((api: Pick, 'get' | 'update' | 'select'>) => Partial), - options?: CreateSignalOptions -): NgtSignalStore -``` - -## Signals everywhere - -All public APIs return `Signal` instead of `Observable`. Consumers can try to use `toObservable()` to convert it to an `Observable` if needed to minimize the changes. - -## `NgtRef` - -`NgtRef` has been removed. Use Signal Query API instead: `viewChild`, `viewChildren`, `contentChild`, and `contentChildren`. - -## Missing components from `angular-three-soba` - -During the work on v2, most components are rewritten from scratch to accommodate the Signal APIs. Hence, there are some components that are missing from previous version of `angular-three-soba`. - -If you find missing components, please open an issue on Github. diff --git a/apps/astro-docs/src/content/docs/core/testing/advance.mdx b/apps/astro-docs/src/content/docs/core/testing/advance.mdx deleted file mode 100644 index 5fd2cd8c..00000000 --- a/apps/astro-docs/src/content/docs/core/testing/advance.mdx +++ /dev/null @@ -1,55 +0,0 @@ ---- -title: advance -description: Details about the Angular Three Testing `advance` method ---- - -`advance` is a method on `NgtTestBed` that allows us to advance frame to run animations in the scene graph. - -### `advance(frames, delta)` - -`advance` accepts two arguments: - -- `frames` is the number of frames to advance -- `delta` is the delta time to use for the animations. It can be a number or an array of numbers. - -```ts -const { fixture, advance } = NgtTestBed.create(SceneGraph); - -await advance(1); -// assert the scene graph state after 1 frame -``` - -## Example Scenario - -For this example, we will use `advance` to test the animations. - -```diff lang="ts" -import { NgtTestBed } from 'angular-three/testing'; - -describe('SceneGraph', () => { - it('should render', async () => { - const { scene, fireEvent, advance } = NgtTestBed.create(SceneGraph); - - expect(scene.children.length).toEqual(1); - const mesh = scene.children[0] as Mesh; - expect(mesh.isMesh).toEqual(true); - - expect(material.color.getHexString()).toEqual('ffa500'); - - await fireEvent(mesh, 'pointerover'); - expect(material.color.getHexString()).toEqual('ff69b4'); - - await fireEvent(mesh, 'pointerout'); - expect(material.color.getHexString()).toEqual('ffa500'); - - await fireEvent(mesh, 'click'); - expect(mesh.scale.toArray()).toEqual([1.5, 1.5, 1.5]); - -+ expect(mesh.rotation.x).toEqual(0); -+ await advance(1); - - // the cube should have rotated by 0.01 on the X axis since we advanced 1 frame -+ expect(mesh.rotation.x).toEqual(0.01); - }); -}); -``` diff --git a/apps/astro-docs/src/content/docs/core/testing/fire-event.mdx b/apps/astro-docs/src/content/docs/core/testing/fire-event.mdx deleted file mode 100644 index d8eb7c58..00000000 --- a/apps/astro-docs/src/content/docs/core/testing/fire-event.mdx +++ /dev/null @@ -1,76 +0,0 @@ ---- -title: fireEvent -description: Details about the Angular Three Testing `fireEvent` method ---- - -`fireEvent` is a method on `NgtTestBed` that allows us to fire events on any element in the scene graph. - -### `fireEvent(element, eventName, eventData)` - -`fireEvent` accepts three arguments: - -- `element` is the element to fire the event on -- `eventName` is the name of the event to fire. Must be events that are supported by Angular Three events system. -- `eventData` is an optional object that contains the event data - -```ts -const { fireEvent } = NgtTestBed.create(SceneGraph); - -await fireEvent(mesh, 'click'); -await fireEvent(mesh, 'pointerover'); -``` - -#### `fireEvent.setAutoDetectChanges(auto: boolean)` - -After firing an event, a Change Detection is needed with `fixture.detectChanges()` to flush any changes that may have occurred (e.g: signal state changes). - -`fireEvent` does this automatically, but we can disable it by calling `fireEvent.setAutoDetectChanges(false)`. - -```ts -const { fixture, fireEvent } = NgtTestBed.create(SceneGraph); -fireEvent.setAutoDetectChanges(false); - -await fireEvent(mesh, 'click'); -fixture.detectChanges(); - -await fireEvent(mesh, 'pointerover'); -fixture.detectChanges(); -``` - -## Example Scenario - -For this example, we will use `fireEvent` to fire `pointerover`, `pointerout`, and `click` events on the cube -and assert the cube's state after each event. - -```diff lang="ts" -import { NgtTestBed } from 'angular-three/testing'; - -describe('SceneGraph', () => { - it('should render', async () => { - const { scene, fireEvent, advance } = NgtTestBed.create(SceneGraph); - - expect(scene.children.length).toEqual(1); - const mesh = scene.children[0] as Mesh; - expect(mesh.isMesh).toEqual(true); - -+ expect(material.color.getHexString()).toEqual('ffa500'); - -+ await fireEvent(mesh, 'pointerover'); -+ expect(material.color.getHexString()).toEqual('ff69b4'); - -+ await fireEvent(mesh, 'pointerout'); -+ expect(material.color.getHexString()).toEqual('ffa500'); - -+ await fireEvent(mesh, 'click'); -+ expect(mesh.scale.toArray()).toEqual([1.5, 1.5, 1.5]); - }); -}); -``` - -:::note - -We use `color.getHexString()` to compare the color because `material.color` is a `Color` instance, even though we use CSS Color string in the template. - -::: - -Last but not least, we will use `advance` to test the animations. diff --git a/apps/astro-docs/src/content/docs/core/testing/introduction.mdx b/apps/astro-docs/src/content/docs/core/testing/introduction.mdx deleted file mode 100644 index 9c8c40b3..00000000 --- a/apps/astro-docs/src/content/docs/core/testing/introduction.mdx +++ /dev/null @@ -1,62 +0,0 @@ ---- -title: Introduction -description: Introduction to the Angular Three Testing API ---- - -import TestBed from '../../../../components/testing/test-bed'; - -Angular Three Testing provides a set of utilities to help us write unit tests for the scene graphs built with Angular Three. - -In test environment, we do not **actually** render the scene graph. Instead, we assert the state of the scene graph against the expected state -to ensure that the Angular Three renderer works as expected. - -## Example Scenario - -Assuming we have the following `SceneGraph` - -```angular-ts -@Component({ - template: ` - - - - - `, - schemas: [CUSTOM_ELEMENTS_SCHEMA], - changeDetection: ChangeDetectionStrategy.OnPush, -}) -class SceneGraph { - hovered = signal(false); - clicked = signal(false); - - meshRef = viewChild.required>('mesh') - - constructor() { - injectBeforeRender(() => { - const mesh = this.meshRef().nativeElement; - mesh.rotation.x += 0.01; - }) - } -} -``` - -The _rendered_ result of the above scene graph is as follows: - -
- -
- -Our goal is to test the `SceneGraph` component and assert: - -- The mesh is rendered -- The material color changes when the mesh is hovered -- The mesh scales when the mesh is clicked -- The mesh rotates by 0.01 radians per frame - -First, let's look at `NgtTestBed` diff --git a/apps/astro-docs/src/content/docs/core/testing/test-bed.mdx b/apps/astro-docs/src/content/docs/core/testing/test-bed.mdx deleted file mode 100644 index b1ad51f9..00000000 --- a/apps/astro-docs/src/content/docs/core/testing/test-bed.mdx +++ /dev/null @@ -1,83 +0,0 @@ ---- -title: NgtTestBed -description: Details about the Angular Three `NgtTestBed` utility ---- - -`NgtTestBed` is a utility from `angular-three/testing` that abstracts `TestBed` to make it easier to test our scene graph components. - -## `create()` - -`NgtTestBed` exposes a single static method `create` that accepts a Component class and returns an object with the following properties: - -```ts -const ngtTestBed = NgtTestBed.create(SceneGraph); -ngtTestBed.fixture; // ComponentFixture -ngtTestBed.store; // NgtSignalStore -ngtTestBed.scene; // root THREE.Scene -ngtTestBed.sceneInstanceNode; // root Scene local state -ngtTestBed.canvas; // the mocked HTMLCanvasElement -ngtTestBed.destroy; // method to destroy the fixture -ngtTestBed.fireEvent; // method to fire events on an element in the scene graph -ngtTestBed.advance; // method to advance the animation loop manually per frame -ngtTestBed.toGraph; // method to convert the scene graph to a simple object -``` - -### Options - -`create` accepts an optional second argument that allows us to pass in options to customize the `TestBed`. - -```ts -const ngtTestBed = NgtTestBed.create(SceneGraph, { - mockCanvasOptions: { width: 1280, height: 720 }, - canvasConfiguration: { - camera: { position: [0, 0, 5] }, - shadows: true, - }, - ...TestBedOptions, -}); -``` - -- `canvasConfiguration` is an object whose role is similar to `NgtCanvas` inputs. - :::note - - Options that determine the Color Space like `linear`, `flat`, `legacy` **can** affect the assertions in the test for THREE.js entities that are affected by different color spaces (e.g: `Color`) - - ::: - -- `mockCanvasOptions` is an object that allows us to customize the mocked canvas element. It accepts `width` and `height` as well as `beforeReturn` which is a function that allows us to return a mocked `HTMLCanvasElement` before the `TestBed` is created. - -## Example Scenario - -For this example, we will use `scene`, `fireEvent`, and `advance` to test the `SceneGraph` component. - -- `scene` allows us to assert the state of the scene graph -- `fireEvent` allows us to fire events on the cube -- `advance` allows us to advance the animation loop manually per frame - -```ts -import { NgtTestBed } from 'angular-three/testing'; - -describe('SceneGraph', () => { - it('should render', async () => { - const { scene, fireEvent, advance } = NgtTestBed.create(SceneGraph); - }); -}); -``` - -With `scene`, we can assert the state of the scene graph. We can assert -however we want to. To keep things simple, we will just assert that the root Scene has a child which is a `Mesh` - -```diff lang="ts" -import { NgtTestBed } from 'angular-three/testing'; - -describe('SceneGraph', () => { - it('should render', async () => { - const { scene, fireEvent, advance } = NgtTestBed.create(SceneGraph); -+ expect(scene.children.length).toEqual(1); -+ const mesh = scene.children[0] as Mesh; -+ expect(mesh.isMesh).toEqual(true); - }); -}); -``` - -Next, we will test the `Mesh` events with `fireEvent` diff --git a/apps/astro-docs/src/content/docs/core/testing/to-graph.mdx b/apps/astro-docs/src/content/docs/core/testing/to-graph.mdx deleted file mode 100644 index 0b9f1f84..00000000 --- a/apps/astro-docs/src/content/docs/core/testing/to-graph.mdx +++ /dev/null @@ -1,36 +0,0 @@ ---- -title: toGraph -description: Details about the Angular Three Testing `toGraph` function ---- - -`toGraph` is a function that allows you to convert a THREE.js object into a simple `NgtTestGraphObject`. - -This is useful for snapshot testing as it _at least_ provides the scene graph structure. - -:::caution - -In many cases, we can use `toJSON` on the THREE.js object to better structure. However, this is not always possible - -- The `uuid` property will always be different for each test run making it difficult to do snapshot testing -- Some THREE.objects do not have `toJSON` method and will throw an error when we try to call `scene.toJSON()` from the root - -::: - -```ts -const { fixture, toGraph } = NgtTestBed.create(SceneGraph); -fixture.detectChanges(); - -expect(toGraph()).toMatchSnapshot(); - -/** - * For a scene with a single mesh - * - * [ - * { - * "type": "Mesh", - * "name": "", - * "children": [] - * } - * ] - */ -``` diff --git a/apps/astro-docs/src/content/docs/core/utilities/before-render.mdx b/apps/astro-docs/src/content/docs/core/utilities/before-render.mdx deleted file mode 100644 index fa1f7f40..00000000 --- a/apps/astro-docs/src/content/docs/core/utilities/before-render.mdx +++ /dev/null @@ -1,35 +0,0 @@ ---- -title: injectBeforeRender -description: Details about the Angular Three `injectBeforeRender` function ---- - -`injectBeforeRender` is a function that allows the consumers to register a callback to be run in the animation loop. - -It takes a callback that has access to the state of the canvas and the delta time since the last frame. - -`injectBeforeRender` cleans up the callback when the component is destroyed. Alternatively, `injectBeforeRender` returns a clean up function that can be called manually. - -```angular-ts -import { Component, ElementRef, viewChild } from '@angular/core'; -import { injectBeforeRender } from 'angular-three'; -import { Mesh } from 'three'; - -@Component({ - template: ` - - - - ` -}) -export class Experience { - meshRef = viewChild.required>('mesh'); - - constructor() { - injectBeforeRender(({ delta }) => { - const mesh = this.meshRef().nativeElement; - mesh.rotation.x += delta; - mesh.rotation.y += delta; - }); - } -} -``` diff --git a/apps/astro-docs/src/content/docs/core/utilities/loader.mdx b/apps/astro-docs/src/content/docs/core/utilities/loader.mdx deleted file mode 100644 index 2586a9a2..00000000 --- a/apps/astro-docs/src/content/docs/core/utilities/loader.mdx +++ /dev/null @@ -1,31 +0,0 @@ ---- -title: injectLoader -description: A utility to inject a loader into your Angular component. ---- - -`injectLoader` is a utility function that abstracts the process of loading 3D assets. - -It accepts: - -- A function that returns a `THREE.Loader` constructor -- A function that returns a `string` or `string[]` -- An option object that accepts the following properties: - - `extensions`: A function that accepts a `THREE.Loader` and returns nothing. It is used to extend the loader with additional functionality. - - `onLoad`: A function that accepts the loaded data and returns nothing. It is called after the loader has finished loading the data. - - `onProgress`: A function that accepts a `ProgressEvent` and returns nothing. It is called when the loader emits a progress event. - - `injector`: An `Injector` instance. It is used to set the Injection Context for the loader - -```angular-ts -import { injectLoader } from 'angular-three'; - -@Component({ - template: ` - @if (gltfResult(); as gltf) { - - } - `, -}) -export class Experience { - gltfResult = injectLoader(() => GLTFLoader, () => 'path/to/model.glb'); -} -``` diff --git a/apps/astro-docs/src/content/docs/index.mdx b/apps/astro-docs/src/content/docs/index.mdx deleted file mode 100644 index 99067697..00000000 --- a/apps/astro-docs/src/content/docs/index.mdx +++ /dev/null @@ -1,60 +0,0 @@ ---- -title: Introduction -description: Get started building your scene graph with Angular Three. -slug: '' ---- - -Angular Three is a custom Angular Renderer that uses [THREE.js](https://threejs.org/) to render 3D scenes. - -## Key Features - -##### Declarative Scene Graph - -Angular Three allows the users to declaratively build a scene graph using the familiar Angular template syntax. This approach enables Angular developers to leverage familiar skills and tools of Angular template to build 3D scenes. - -##### Performance - -Angular Three is a custom renderer that _renders_ THREE.js entities directly to the Canvas. This means that there is no overhead of instantiating custom Components or Directives to turn them into THREE.js objects. - -##### Compatibility - -_"Anything that can be done in THREE.js, can be done in Angular Three."_ This is the motto that Angular Three stands for. - -##### Signals - -Angular Three fully embraces [Angular Signals](https://angular.dev/guide/signals) to drive the state of the scene graph. Most, if not all, of Angular Three's APIs are based on `Signal` and return `Signal`. - -## Versioning - -Angular Three follows the [Semantic Versioning](https://semver.org/) standard. - -- **Patch**: Bug fixes and small improvements. -- **Minor**: New features and improvements. -- **Major**: Breaking changes. - -Angular Three consists of the following packages: - -- `angular-three`: The main package that provides a custom renderer for Angular. -- `angular-three-soba`: A collection of utilities and abstractions for building 3D applications with Angular Three. -- `angular-three-postprocessing`: A collection of post-processing effects for Angular Three. -- `angular-three-rapier`: Rapier.js integration for Angular Three. -- `angular-three-cannon`: Cannon.js integration for Angular Three. - -#### Compatibility Matrix - -| Angular Three Version | THREE.js Version | Angular Version | ngxtension Version | -| --------------------- | ---------------- | ---------------------------- | ------------------ | -| ^3.0.0 | 0.148+ | 19+ | 3+ | -| ^2.0.0 | 0.148+ | 18+, 19+ (_not recommended_) | 3+ | - -## Acknowledgements - -Angular Three would not have been possible without the following open source projects: - -#### [THREE.js](https://threejs.org/) - -The foundation of Angular Three is THREE.js. It is a powerful and flexible 3D library that provides a wide range of tools for creating 3D scenes. - -#### [React Three Fiber (R3F)](https://github.com/pmndrs/react-three-fiber) - -Fundamentally, NGT and R3F are both custom renderers for THREE.js. Technically, there are differences between Angular Renderer (NGT) and React Reconciler (R3F). However, the core concepts are the same and R3F has been a major inspiration for NGT. diff --git a/apps/astro-docs/src/content/docs/postprocessing/effect-composer.mdx b/apps/astro-docs/src/content/docs/postprocessing/effect-composer.mdx deleted file mode 100644 index 6002fb9f..00000000 --- a/apps/astro-docs/src/content/docs/postprocessing/effect-composer.mdx +++ /dev/null @@ -1,76 +0,0 @@ ---- -title: EffectComposer -description: Details about the Angular Three EffectComposer ---- - -# EffectComposer - -The EffectComposer is a crucial component in Angular Three's postprocessing pipeline. It allows you to combine multiple postprocessing effects and apply them to your 3D scene. The EffectComposer manages the rendering process, applying effects in the order they are defined. - -## Usage - -To use the EffectComposer, wrap your effects inside the `` component: - -```html - - - - - -``` - -## Options - -The EffectComposer accepts the following options: - -| Option | Type | Default | Description | -| --------------- | --------------- | ------------- | ---------------------------------------------------------- | -| enabled | boolean | true | Enables or disables the EffectComposer | -| depthBuffer | boolean | undefined | Enables depth buffer | -| stencilBuffer | boolean | undefined | Enables stencil buffer | -| autoClear | boolean | true | Automatically clears the render target | -| resolutionScale | number | undefined | Scales the render resolution | -| multisampling | number | 8 | Sets the number of samples for multisampling anti-aliasing | -| frameBufferType | TextureDataType | HalfFloatType | Sets the frame buffer type | -| renderPriority | number | 1 | Sets the render priority | -| camera | Camera | undefined | Custom camera for rendering | -| scene | Scene | undefined | Custom scene for rendering | - -## Examples - -1. Basic usage with Bloom and Vignette effects: - -```html - - - - -``` - -2. Using EffectComposer with custom options: - -```html - - - - -``` - -3. Combining EffectComposer with other scene elements: - -```html - - - - - - - - - - - - - - -``` diff --git a/apps/astro-docs/src/content/docs/postprocessing/how-it-works.mdx b/apps/astro-docs/src/content/docs/postprocessing/how-it-works.mdx deleted file mode 100644 index ced364a7..00000000 --- a/apps/astro-docs/src/content/docs/postprocessing/how-it-works.mdx +++ /dev/null @@ -1,60 +0,0 @@ ---- -title: How it works -description: How Angular Three Postprocessing works ---- - -import { Tabs, TabItem, Code } from '@astrojs/starlight/components'; -import PostprocessingSample, { content } from '../../../components/postprocessing/sample?includeContent'; - -### Import `NgtpEffectComposer` from `angular-three-postprocessing` - -```angular-ts -import { NgtpEffectComposer } from 'angular-three-postprocessing'; -``` - -### Render the `NgtpEffectComposer` component in your scene graph - -```angular-ts -@Component({ - imports: [NgtpEffectComposer], - template: ` - - - - `, - schemas: [CUSTOM_ELEMENTS_SCHEMA], -}) -export class SceneGraph {} -``` - -### Pick an effect - -```diff lang="angular-ts" --import { NgtpEffectComposer } from 'angular-three-postprocessing'; -+import { NgtpEffectComposer, NgtpBloom, NgtpNoise } from 'angular-three-postprocessing'; - -@Component({ - imports: [NgtpEffectComposer, NgtpBloom, NgtpNoise], - template: ` - -+ -+ - - `, - schemas: [CUSTOM_ELEMENTS_SCHEMA], -}) -export class SceneGraph {} -``` - -### Example - - - -
- -
-
- - - -
diff --git a/apps/astro-docs/src/content/docs/postprocessing/introduction.mdx b/apps/astro-docs/src/content/docs/postprocessing/introduction.mdx deleted file mode 100644 index 819c6204..00000000 --- a/apps/astro-docs/src/content/docs/postprocessing/introduction.mdx +++ /dev/null @@ -1,28 +0,0 @@ ---- -title: Introduction -description: Introduction to the Angular Three Postprocessing package ---- - -Angular Three Postprocessing is an integration of the [Postprocessing](https://github.com/vanruesc/postprocessing) library for use with Angular Three. - -This implementation is based on the [@react-three/postprocessing](https://github.com/pmndrs/react-postprocessing) library. - -:::tip[Did you know?] - -Check out [Postprocessing](https://github.com/vanruesc/postprocessing) documentation for more information on actual usages of the library. - -::: - -## Installation - -```shell -npm install angular-three-postprocessing postprocessing maath three-stdlib -# yarn add angular-three-postprocessing postprocessing maath three-stdlib -# pnpm add angular-three-postprocessing postprocessing maath three-stdlib -``` - -## Compatibility Matrix - -| Angular Three Postprocessing Version | postprocessing version | maath version | three-stdlib version | -| ------------------------------------ | ---------------------- | ------------- | -------------------- | -| 2.0.0 | ^6.0.0 | ^0.10.0 | ^2.0.0 | diff --git a/apps/astro-docs/src/content/docs/soba/introduction.mdx b/apps/astro-docs/src/content/docs/soba/introduction.mdx deleted file mode 100644 index 1ae9fc16..00000000 --- a/apps/astro-docs/src/content/docs/soba/introduction.mdx +++ /dev/null @@ -1,42 +0,0 @@ ---- -title: Introduction -description: Introduction to the Angular Three Soba package ---- - -Angular Three Soba is currently a port from [React Three Drei](https://github.com/pmndrs/drei) to provide a set of utilities and abstractions for building 3D applications with Angular Three. -As the ecosystem grows, we will continue to add more utilities and abstractions to Soba. - -## Installation - -```sh -npm install angular-three-soba three-stdlib -# yarn add angular-three-soba three-stdlib -# pnpm add angular-three-soba three-stdlib -``` - -## Entry Points and Peer Dependencies - -Angular Three Soba is broken up into several entry points, each entry point requires a set of different peer dependencies. - -| Angular Three Soba Version | `three-stdlib` version | -| -------------------------- | ---------------------- | -| 2.0.0 | >=2.0.0 | - -Here's a table that shows the peer dependencies for each entry point: - -| Entry Point | Peer Dependencies | Notes | -| --------------- | ------------------------------------------------------------------- | ----------------------------------------------------------- | -| abstractions | `troika-three-text` | | -| cameras | - | | -| controls | `camera-controls`, `maath` | | -| loaders | - | | -| materials | `@pmndrs/vanilla`, `three-custom-shader-material`, `three-mesh-bvh` | | -| misc | - | | -| performances | - | | -| shaders | `three-mesh-bvh`, `@pmndrs/vanilla` | Typically, you won't need to use this entry point directly. | -| staging | `@monogrid/gainmap-js`, `@pmndrs/vanilla`, `three-mesh-bvh` | | -| vanilla-exports | `@pmndrs/vanilla` | Typically, you won't need to use this entry point directly. | - -## Documentation - -Angular Three Soba contains a variety of complex examples that demonstrate the capabilities of the library. These examples (and documentations) are available in the [Soba Storybook](https://angularthreesoba.netlify.app). diff --git a/apps/astro-docs/src/env.d.ts b/apps/astro-docs/src/env.d.ts deleted file mode 100644 index 5b33cacb..00000000 --- a/apps/astro-docs/src/env.d.ts +++ /dev/null @@ -1,15 +0,0 @@ -/// -/// - -declare module '*.analog' { - import type { Type } from '@angular/core'; - const component: Type; - export default component; -} - -declare module '*?includeContent' { - import type { Type } from '@angular/core'; - const component: Type; - export const content: string; - export default component; -} diff --git a/apps/astro-docs/src/tailwind.css b/apps/astro-docs/src/tailwind.css deleted file mode 100644 index b5c61c95..00000000 --- a/apps/astro-docs/src/tailwind.css +++ /dev/null @@ -1,3 +0,0 @@ -@tailwind base; -@tailwind components; -@tailwind utilities; diff --git a/apps/astro-docs/tailwind.config.mjs b/apps/astro-docs/tailwind.config.mjs deleted file mode 100644 index 346300f3..00000000 --- a/apps/astro-docs/tailwind.config.mjs +++ /dev/null @@ -1,18 +0,0 @@ -import starlightPlugin from '@astrojs/starlight-tailwind'; -import colors from 'tailwindcss/colors'; - -/** @type {import('tailwindcss').Config} */ -export default { - content: ['./src/**/*.{astro,html,js,jsx,md,mdx,svelte,ts,tsx,vue}'], - theme: { - extend: { - colors: { - // Your preferred accent color. Indigo is closest to Starlight’s defaults. - accent: colors.slate, - // Your preferred gray scale. Zinc is closest to Starlight’s defaults. - gray: colors.zinc, - }, - }, - }, - plugins: [starlightPlugin()], -}; diff --git a/apps/astro-docs/tsconfig.app.json b/apps/astro-docs/tsconfig.app.json deleted file mode 100644 index e82de6fa..00000000 --- a/apps/astro-docs/tsconfig.app.json +++ /dev/null @@ -1,34 +0,0 @@ -{ - "extends": "./tsconfig.json", - "compileOnSave": false, - "compilerOptions": { - "baseUrl": "./", - "outDir": "./dist/out-tsc", - "forceConsistentCasingInFileNames": true, - "strict": true, - "noImplicitOverride": true, - "noPropertyAccessFromIndexSignature": true, - "noImplicitReturns": true, - "noFallthroughCasesInSwitch": true, - "sourceMap": true, - "declaration": false, - "downlevelIteration": true, - "experimentalDecorators": true, - "moduleResolution": "node", - "importHelpers": true, - "noEmit": false, - "target": "es2020", - "module": "es2020", - "lib": ["es2020", "dom"], - "skipLibCheck": true - }, - "angularCompilerOptions": { - "enableI18nLegacyMessageIdFormat": false, - "strictInjectionParameters": true, - "strictInputAccessModifiers": true, - "strictTemplates": true, - "allowJs": false - }, - "files": [], - "include": ["src/**/*.ts"] -} diff --git a/apps/astro-docs/tsconfig.json b/apps/astro-docs/tsconfig.json deleted file mode 100644 index 53789299..00000000 --- a/apps/astro-docs/tsconfig.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "extends": "astro/tsconfigs/base" -} diff --git a/apps/kitchen-sink/eslint.config.js b/apps/examples/eslint.config.js similarity index 100% rename from apps/kitchen-sink/eslint.config.js rename to apps/examples/eslint.config.js diff --git a/apps/kitchen-sink/project.json b/apps/examples/project.json similarity index 68% rename from apps/kitchen-sink/project.json rename to apps/examples/project.json index a75a791d..bf88f38f 100644 --- a/apps/kitchen-sink/project.json +++ b/apps/examples/project.json @@ -1,30 +1,31 @@ { - "name": "kitchen-sink", + "name": "examples", "$schema": "../../node_modules/nx/schemas/project-schema.json", "projectType": "application", "prefix": "app", - "sourceRoot": "apps/kitchen-sink/src", + "sourceRoot": "apps/examples/src", "tags": [], "targets": { "build": { "executor": "@angular/build:application", "outputs": ["{options.outputPath}"], "options": { - "outputPath": "dist/apps/kitchen-sink", - "index": "apps/kitchen-sink/src/index.html", - "browser": "apps/kitchen-sink/src/main.ts", - "tsConfig": "apps/kitchen-sink/tsconfig.app.json", + "outputPath": "dist/apps/examples", + "index": "apps/examples/src/index.html", + "browser": "apps/examples/src/main.ts", + "tsConfig": "apps/examples/tsconfig.app.json", "assets": [ { "glob": "**/*", - "input": "apps/kitchen-sink/public" + "input": "apps/examples/public" } ], - "styles": ["apps/kitchen-sink/src/styles.css"], + "styles": ["apps/examples/src/styles.css"], "scripts": [], "loader": { ".blob": "file", - ".glb": "file" + ".glb": "file", + ".gltf": "file" } }, "configurations": { @@ -32,8 +33,8 @@ "budgets": [ { "type": "initial", - "maximumWarning": "500kb", - "maximumError": "1mb" + "maximumWarning": "1mb", + "maximumError": "4mb" }, { "type": "anyComponentStyle", @@ -59,10 +60,10 @@ "executor": "@angular/build:dev-server", "configurations": { "production": { - "buildTarget": "kitchen-sink:build:production" + "buildTarget": "examples:build:production" }, "development": { - "buildTarget": "kitchen-sink:build:development" + "buildTarget": "examples:build:development" } }, "defaultConfiguration": "development" @@ -70,7 +71,7 @@ "extract-i18n": { "executor": "@angular-devkit/build-angular:extract-i18n", "options": { - "buildTarget": "kitchen-sink:build" + "buildTarget": "examples:build" } }, "lint": { diff --git a/apps/kitchen-sink/public/1200px-Starbucks_Logo_ab_2011.svg.png b/apps/examples/public/1200px-Starbucks_Logo_ab_2011.svg.png similarity index 100% rename from apps/kitchen-sink/public/1200px-Starbucks_Logo_ab_2011.svg.png rename to apps/examples/public/1200px-Starbucks_Logo_ab_2011.svg.png diff --git a/apps/kitchen-sink/public/911-transformed.glb b/apps/examples/public/911-transformed.glb similarity index 100% rename from apps/kitchen-sink/public/911-transformed.glb rename to apps/examples/public/911-transformed.glb diff --git a/apps/kitchen-sink/public/Astronaut-transformed.glb b/apps/examples/public/Astronaut-transformed.glb similarity index 100% rename from apps/kitchen-sink/public/Astronaut-transformed.glb rename to apps/examples/public/Astronaut-transformed.glb diff --git a/apps/kitchen-sink/public/Beetle.glb b/apps/examples/public/Beetle.glb similarity index 100% rename from apps/kitchen-sink/public/Beetle.glb rename to apps/examples/public/Beetle.glb diff --git a/apps/kitchen-sink/public/_redirects b/apps/examples/public/_redirects similarity index 100% rename from apps/kitchen-sink/public/_redirects rename to apps/examples/public/_redirects diff --git a/apps/kitchen-sink/public/angular.glb b/apps/examples/public/angular.glb similarity index 100% rename from apps/kitchen-sink/public/angular.glb rename to apps/examples/public/angular.glb diff --git a/apps/kitchen-sink/public/bendy.glb b/apps/examples/public/bendy.glb similarity index 100% rename from apps/kitchen-sink/public/bendy.glb rename to apps/examples/public/bendy.glb diff --git a/apps/kitchen-sink/public/blender-threejs-journey-20k-hat-transformed.glb b/apps/examples/public/blender-threejs-journey-20k-hat-transformed.glb similarity index 100% rename from apps/kitchen-sink/public/blender-threejs-journey-20k-hat-transformed.glb rename to apps/examples/public/blender-threejs-journey-20k-hat-transformed.glb diff --git a/apps/kitchen-sink/public/blender-threejs-journey-20k-transformed.glb b/apps/examples/public/blender-threejs-journey-20k-transformed.glb similarity index 100% rename from apps/kitchen-sink/public/blender-threejs-journey-20k-transformed.glb rename to apps/examples/public/blender-threejs-journey-20k-transformed.glb diff --git a/apps/kitchen-sink/public/bomb-gp.glb b/apps/examples/public/bomb-gp.glb similarity index 100% rename from apps/kitchen-sink/public/bomb-gp.glb rename to apps/examples/public/bomb-gp.glb diff --git a/apps/kitchen-sink/public/bowl.glb b/apps/examples/public/bowl.glb similarity index 100% rename from apps/kitchen-sink/public/bowl.glb rename to apps/examples/public/bowl.glb diff --git a/apps/kitchen-sink/public/bunny-transformed.glb b/apps/examples/public/bunny-transformed.glb similarity index 100% rename from apps/kitchen-sink/public/bunny-transformed.glb rename to apps/examples/public/bunny-transformed.glb diff --git a/apps/kitchen-sink/public/bust-1-d.glb b/apps/examples/public/bust-1-d.glb similarity index 100% rename from apps/kitchen-sink/public/bust-1-d.glb rename to apps/examples/public/bust-1-d.glb diff --git a/apps/kitchen-sink/public/bust-2-d.glb b/apps/examples/public/bust-2-d.glb similarity index 100% rename from apps/kitchen-sink/public/bust-2-d.glb rename to apps/examples/public/bust-2-d.glb diff --git a/apps/kitchen-sink/public/bust-3-d.glb b/apps/examples/public/bust-3-d.glb similarity index 100% rename from apps/kitchen-sink/public/bust-3-d.glb rename to apps/examples/public/bust-3-d.glb diff --git a/apps/kitchen-sink/public/bust-4-d.glb b/apps/examples/public/bust-4-d.glb similarity index 100% rename from apps/kitchen-sink/public/bust-4-d.glb rename to apps/examples/public/bust-4-d.glb diff --git a/apps/kitchen-sink/public/bust-5-d.glb b/apps/examples/public/bust-5-d.glb similarity index 100% rename from apps/kitchen-sink/public/bust-5-d.glb rename to apps/examples/public/bust-5-d.glb diff --git a/apps/kitchen-sink/public/coffee-transformed.glb b/apps/examples/public/coffee-transformed.glb similarity index 100% rename from apps/kitchen-sink/public/coffee-transformed.glb rename to apps/examples/public/coffee-transformed.glb diff --git a/apps/kitchen-sink/public/cup.glb b/apps/examples/public/cup.glb similarity index 100% rename from apps/kitchen-sink/public/cup.glb rename to apps/examples/public/cup.glb diff --git a/apps/kitchen-sink/public/diamond.glb b/apps/examples/public/diamond.glb old mode 100755 new mode 100644 similarity index 100% rename from apps/kitchen-sink/public/diamond.glb rename to apps/examples/public/diamond.glb diff --git a/apps/kitchen-sink/public/dragon_chestplate.glb b/apps/examples/public/dragon_chestplate.glb similarity index 100% rename from apps/kitchen-sink/public/dragon_chestplate.glb rename to apps/examples/public/dragon_chestplate.glb diff --git a/apps/kitchen-sink/public/drums-final.mp3 b/apps/examples/public/drums-final.mp3 similarity index 100% rename from apps/kitchen-sink/public/drums-final.mp3 rename to apps/examples/public/drums-final.mp3 diff --git a/apps/kitchen-sink/public/earth.gltf b/apps/examples/public/earth.gltf similarity index 99% rename from apps/kitchen-sink/public/earth.gltf rename to apps/examples/public/earth.gltf index 17cca099..a0941bb4 100644 --- a/apps/kitchen-sink/public/earth.gltf +++ b/apps/examples/public/earth.gltf @@ -255,8 +255,9 @@ { "children": [5, 6, 7], "matrix": [ - 99.86371360033668, -6.938893903907228e-16, -5.2190713685417345, 0, -5.219071368541729, -0.000004371139006309477, - -99.86371360033658, 0, -2.281328639885949e-7, 99.9999999999999, -0.000004365181749399483, 0, 0, 0, 0, 1 + 99.86371360033668, -6.938893903907228e-16, -5.2190713685417345, 0, -5.219071368541729, + -0.000004371139006309477, -99.86371360033658, 0, -2.281328639885949e-7, 99.9999999999999, + -0.000004365181749399483, 0, 0, 0, 0, 1 ], "name": "URF-Height" }, diff --git a/apps/kitchen-sink/public/env/skydiving/nx.png b/apps/examples/public/env/skydiving/nx.png similarity index 100% rename from apps/kitchen-sink/public/env/skydiving/nx.png rename to apps/examples/public/env/skydiving/nx.png diff --git a/apps/kitchen-sink/public/env/skydiving/ny.png b/apps/examples/public/env/skydiving/ny.png similarity index 100% rename from apps/kitchen-sink/public/env/skydiving/ny.png rename to apps/examples/public/env/skydiving/ny.png diff --git a/apps/kitchen-sink/public/env/skydiving/nz.png b/apps/examples/public/env/skydiving/nz.png similarity index 100% rename from apps/kitchen-sink/public/env/skydiving/nz.png rename to apps/examples/public/env/skydiving/nz.png diff --git a/apps/kitchen-sink/public/env/skydiving/px.png b/apps/examples/public/env/skydiving/px.png similarity index 100% rename from apps/kitchen-sink/public/env/skydiving/px.png rename to apps/examples/public/env/skydiving/px.png diff --git a/apps/kitchen-sink/public/env/skydiving/py.png b/apps/examples/public/env/skydiving/py.png similarity index 100% rename from apps/kitchen-sink/public/env/skydiving/py.png rename to apps/examples/public/env/skydiving/py.png diff --git a/apps/kitchen-sink/public/env/skydiving/pz.png b/apps/examples/public/env/skydiving/pz.png similarity index 100% rename from apps/kitchen-sink/public/env/skydiving/pz.png rename to apps/examples/public/env/skydiving/pz.png diff --git a/apps/ionic-app/public/favicon.ico b/apps/examples/public/favicon.ico similarity index 100% rename from apps/ionic-app/public/favicon.ico rename to apps/examples/public/favicon.ico diff --git a/apps/kitchen-sink/public/helvetiker_regular.typeface.json b/apps/examples/public/helvetiker_regular.typeface.json similarity index 100% rename from apps/kitchen-sink/public/helvetiker_regular.typeface.json rename to apps/examples/public/helvetiker_regular.typeface.json diff --git a/apps/kitchen-sink/public/magnet.glb b/apps/examples/public/magnet.glb similarity index 100% rename from apps/kitchen-sink/public/magnet.glb rename to apps/examples/public/magnet.glb diff --git a/apps/kitchen-sink/public/nx-cloud.glb b/apps/examples/public/nx-cloud.glb similarity index 100% rename from apps/kitchen-sink/public/nx-cloud.glb rename to apps/examples/public/nx-cloud.glb diff --git a/apps/kitchen-sink/public/nx.glb b/apps/examples/public/nx.glb similarity index 100% rename from apps/kitchen-sink/public/nx.glb rename to apps/examples/public/nx.glb diff --git a/apps/kitchen-sink/public/pingpong.glb b/apps/examples/public/pingpong.glb similarity index 100% rename from apps/kitchen-sink/public/pingpong.glb rename to apps/examples/public/pingpong.glb diff --git a/apps/kitchen-sink/public/pink-d.glb b/apps/examples/public/pink-d.glb similarity index 100% rename from apps/kitchen-sink/public/pink-d.glb rename to apps/examples/public/pink-d.glb diff --git a/apps/kitchen-sink/public/rock2/scene.bin b/apps/examples/public/rock2/scene.bin similarity index 100% rename from apps/kitchen-sink/public/rock2/scene.bin rename to apps/examples/public/rock2/scene.bin diff --git a/apps/examples/public/rock2/scene.gltf b/apps/examples/public/rock2/scene.gltf new file mode 100644 index 00000000..523b8484 --- /dev/null +++ b/apps/examples/public/rock2/scene.gltf @@ -0,0 +1,209 @@ +{ + "accessors": [ + { + "bufferView": 2, + "componentType": 5126, + "count": 780, + "max": [0.97853600978851318, 0.90372800827026367, 1], + "min": [-0.97853600978851318, -0.90372800827026367, -1], + "type": "VEC3" + }, + { + "bufferView": 2, + "byteOffset": 9360, + "componentType": 5126, + "count": 780, + "max": [0.99373799562454224, 0.99997526407241821, 0.99129462242126465], + "min": [-0.99643802642822266, -0.99973654747009277, -0.99750721454620361], + "type": "VEC3" + }, + { + "bufferView": 3, + "componentType": 5126, + "count": 780, + "max": [0.99999797344207764, 0.97842699289321899, 0.99881500005722046, 1], + "min": [-0.9980810284614563, -0.81174397468566895, -0.999642014503479, -1], + "type": "VEC4" + }, + { + "bufferView": 1, + "componentType": 5126, + "count": 780, + "max": [0.99800002574920654, 0.91248899698257446], + "min": [0.0020000000949949026, 0.0020000000949949026], + "type": "VEC2" + }, + { + "bufferView": 0, + "componentType": 5125, + "count": 4014, + "max": [779], + "min": [0], + "type": "SCALAR" + } + ], + "asset": { + "extras": { + "author": "irs1182 (https://sketchfab.com/irs1182)", + "license": "CC-BY-4.0 (http://creativecommons.org/licenses/by/4.0/)", + "source": "https://sketchfab.com/models/17c3ff6a8f3145d68fbf653ebf650ee9", + "title": "Rock" + }, + "generator": "Sketchfab-3.19.3", + "version": "2.0" + }, + "bufferViews": [ + { + "buffer": 0, + "byteLength": 16056, + "byteOffset": 0, + "name": "floatBufferViews", + "target": 34963 + }, + { + "buffer": 0, + "byteLength": 6240, + "byteOffset": 16056, + "byteStride": 8, + "name": "floatBufferViews", + "target": 34962 + }, + { + "buffer": 0, + "byteLength": 18720, + "byteOffset": 22296, + "byteStride": 12, + "name": "floatBufferViews", + "target": 34962 + }, + { + "buffer": 0, + "byteLength": 12480, + "byteOffset": 41016, + "byteStride": 16, + "name": "floatBufferViews", + "target": 34962 + } + ], + "buffers": [ + { + "byteLength": 53496, + "uri": "scene.bin" + } + ], + "images": [ + { + "uri": "textures/08___Default_metallicRoughness.jpg" + }, + { + "uri": "textures/08___Default_normal.jpg" + }, + { + "uri": "textures/08___Default_baseColor.jpeg" + } + ], + "materials": [ + { + "doubleSided": true, + "emissiveFactor": [0, 0, 0], + "name": "08___Default", + "normalTexture": { + "index": 1, + "scale": 1, + "texCoord": 0 + }, + "occlusionTexture": { + "index": 0, + "strength": 1, + "texCoord": 0 + }, + "pbrMetallicRoughness": { + "baseColorFactor": [1, 1, 1, 1], + "baseColorTexture": { + "index": 2, + "texCoord": 0 + }, + "metallicFactor": 0.2943978659, + "metallicRoughnessTexture": { + "index": 0, + "texCoord": 0 + }, + "roughnessFactor": 1 + } + } + ], + "meshes": [ + { + "name": "defaultMaterial", + "primitives": [ + { + "attributes": { + "NORMAL": 1, + "POSITION": 0, + "TANGENT": 2, + "TEXCOORD_0": 3 + }, + "indices": 4, + "material": 0, + "mode": 4 + } + ] + } + ], + "nodes": [ + { + "children": [1], + "name": "RootNode (gltf orientation matrix)", + "rotation": [-0.70710678118654746, -0, -0, 0.70710678118654757] + }, + { + "children": [2], + "name": "RootNode (model correction matrix)" + }, + { + "children": [3], + "matrix": [ + 1, 0, 0, 0, 0, -4.3711390063094768e-8, 0.999999999999999, 0, 0, -0.999999999999999, + -4.3711390063094768e-8, 0, 0, 0, 0, 1 + ], + "name": "Collada visual scene group" + }, + { + "children": [4], + "name": "RetopoGroup002" + }, + { + "mesh": 0, + "name": "defaultMaterial" + } + ], + "samplers": [ + { + "magFilter": 9729, + "minFilter": 9987, + "wrapS": 10497, + "wrapT": 10497 + } + ], + "scene": 0, + "scenes": [ + { + "name": "OSG_Scene", + "nodes": [0] + } + ], + "textures": [ + { + "sampler": 0, + "source": 0 + }, + { + "sampler": 0, + "source": 1 + }, + { + "sampler": 0, + "source": 2 + } + ] +} diff --git a/apps/kitchen-sink/public/rock2/textures/08___Default_baseColor.jpeg b/apps/examples/public/rock2/textures/08___Default_baseColor.jpeg similarity index 100% rename from apps/kitchen-sink/public/rock2/textures/08___Default_baseColor.jpeg rename to apps/examples/public/rock2/textures/08___Default_baseColor.jpeg diff --git a/apps/kitchen-sink/public/rock2/textures/08___Default_metallicRoughness.jpg b/apps/examples/public/rock2/textures/08___Default_metallicRoughness.jpg similarity index 100% rename from apps/kitchen-sink/public/rock2/textures/08___Default_metallicRoughness.jpg rename to apps/examples/public/rock2/textures/08___Default_metallicRoughness.jpg diff --git a/apps/kitchen-sink/public/rock2/textures/08___Default_normal.jpg b/apps/examples/public/rock2/textures/08___Default_normal.jpg similarity index 100% rename from apps/kitchen-sink/public/rock2/textures/08___Default_normal.jpg rename to apps/examples/public/rock2/textures/08___Default_normal.jpg diff --git a/apps/kitchen-sink/public/sky-texture.jpg b/apps/examples/public/sky-texture.jpg similarity index 100% rename from apps/kitchen-sink/public/sky-texture.jpg rename to apps/examples/public/sky-texture.jpg diff --git a/apps/kitchen-sink/public/skydiver.glb b/apps/examples/public/skydiver.glb similarity index 100% rename from apps/kitchen-sink/public/skydiver.glb rename to apps/examples/public/skydiver.glb diff --git a/apps/kitchen-sink/public/snare-final.mp3 b/apps/examples/public/snare-final.mp3 similarity index 100% rename from apps/kitchen-sink/public/snare-final.mp3 rename to apps/examples/public/snare-final.mp3 diff --git a/apps/kitchen-sink/public/spaceship-transformed.glb b/apps/examples/public/spaceship-transformed.glb similarity index 100% rename from apps/kitchen-sink/public/spaceship-transformed.glb rename to apps/examples/public/spaceship-transformed.glb diff --git a/apps/examples/public/spaceship.gltf b/apps/examples/public/spaceship.gltf new file mode 100644 index 00000000..363740d1 --- /dev/null +++ b/apps/examples/public/spaceship.gltf @@ -0,0 +1,234 @@ +{ + "asset": { "generator": "Khronos glTF Blender I/O v1.5.17", "version": "2.0" }, + "scene": 0, + "scenes": [{ "name": "Scene", "nodes": [0] }], + "nodes": [{ "mesh": 0, "name": "Cube.003" }], + "materials": [ + { + "doubleSided": true, + "name": "Mat0", + "pbrMetallicRoughness": { + "baseColorFactor": [0.799103319644928, 0.005181401502341032, 0.027320900931954384, 1], + "metallicFactor": 0, + "roughnessFactor": 0.47058823704719543 + }, + "emissiveFactor": [0, 0, 0], + "alphaMode": "OPAQUE" + }, + { + "doubleSided": true, + "name": "Mat1", + "pbrMetallicRoughness": { + "baseColorFactor": [0.7991032004356384, 0.1980692595243454, 0.035601358860731125, 1], + "metallicFactor": 0, + "roughnessFactor": 0.5 + }, + "emissiveFactor": [0, 0, 0], + "alphaMode": "OPAQUE" + }, + { + "doubleSided": true, + "name": "Mat2", + "pbrMetallicRoughness": { + "baseColorFactor": [0.13286834955215454, 0.13286836445331573, 0.13286836445331573, 1], + "metallicFactor": 0, + "roughnessFactor": 0.5 + }, + "emissiveFactor": [0, 0, 0], + "alphaMode": "OPAQUE" + }, + { + "doubleSided": true, + "name": "Window_Frame", + "pbrMetallicRoughness": { + "baseColorFactor": [0.0331047847867012, 0.033104777336120605, 0.0331047847867012, 1], + "metallicFactor": 0, + "roughnessFactor": 0.5 + }, + "emissiveFactor": [0, 0, 0], + "alphaMode": "OPAQUE" + }, + { + "doubleSided": true, + "name": "Mat4", + "pbrMetallicRoughness": { "baseColorFactor": [0, 0, 0, 1], "metallicFactor": 0, "roughnessFactor": 0.5 }, + "emissiveFactor": [0, 0, 0], + "alphaMode": "OPAQUE" + }, + { + "doubleSided": true, + "name": "Mat3", + "pbrMetallicRoughness": { + "baseColorFactor": [0.6724433898925781, 0.6724433302879333, 0.6724433898925781, 1], + "metallicFactor": 0, + "roughnessFactor": 0.5 + }, + "emissiveFactor": [0, 0, 0], + "alphaMode": "OPAQUE" + }, + { + "doubleSided": true, + "name": "Window", + "pbrMetallicRoughness": { + "baseColorFactor": [0.4851498007774353, 0.8631572723388672, 1, 1], + "roughnessFactor": 0, + "metallicFactor": 1 + }, + "emissiveFactor": [0, 0, 0], + "alphaMode": "OPAQUE" + } + ], + "meshes": [ + { + "name": "Cube.005", + "primitives": [ + { + "attributes": { "POSITION": 1, "NORMAL": 2 }, + "indices": 0, + "material": 0, + "mode": 4, + "extensions": { + "KHR_draco_mesh_compression": { "bufferView": 0, "attributes": { "POSITION": 0, "NORMAL": 1 } } + } + }, + { + "attributes": { "POSITION": 4, "NORMAL": 5 }, + "indices": 3, + "material": 1, + "mode": 4, + "extensions": { + "KHR_draco_mesh_compression": { "bufferView": 1, "attributes": { "POSITION": 0, "NORMAL": 1 } } + } + }, + { + "attributes": { "POSITION": 7, "NORMAL": 8 }, + "indices": 6, + "material": 2, + "mode": 4, + "extensions": { + "KHR_draco_mesh_compression": { "bufferView": 2, "attributes": { "POSITION": 0, "NORMAL": 1 } } + } + }, + { + "attributes": { "POSITION": 10, "NORMAL": 11 }, + "indices": 9, + "material": 3, + "mode": 4, + "extensions": { + "KHR_draco_mesh_compression": { "bufferView": 3, "attributes": { "POSITION": 0, "NORMAL": 1 } } + } + }, + { + "attributes": { "POSITION": 13, "NORMAL": 14 }, + "indices": 12, + "material": 4, + "mode": 4, + "extensions": { + "KHR_draco_mesh_compression": { "bufferView": 4, "attributes": { "POSITION": 0, "NORMAL": 1 } } + } + }, + { + "attributes": { "POSITION": 16, "NORMAL": 17 }, + "indices": 15, + "material": 5, + "mode": 4, + "extensions": { + "KHR_draco_mesh_compression": { "bufferView": 5, "attributes": { "POSITION": 0, "NORMAL": 1 } } + } + }, + { + "attributes": { "POSITION": 19, "NORMAL": 20 }, + "indices": 18, + "material": 6, + "mode": 4, + "extensions": { + "KHR_draco_mesh_compression": { "bufferView": 6, "attributes": { "POSITION": 0, "NORMAL": 1 } } + } + } + ] + } + ], + "accessors": [ + { "componentType": 5123, "count": 60, "type": "SCALAR" }, + { + "componentType": 5126, + "count": 24, + "max": [0.5108072090626462, -0.4213953877753261, -1.0054540918347425], + "min": [-0.4825829792500241, -0.541630022573541, -1.0078781772137386], + "type": "VEC3" + }, + { "componentType": 5126, "count": 24, "type": "VEC3" }, + { "componentType": 5123, "count": 13326, "type": "SCALAR" }, + { + "componentType": 5126, + "count": 4398, + "max": [1.8477268242288578, 0.2553964191142324, 1.9506381091456557], + "min": [-1.7905872401576188, -1.1053578194956926, -2.2802957829814106], + "type": "VEC3" + }, + { "componentType": 5126, "count": 4398, "type": "VEC3" }, + { "componentType": 5123, "count": 44301, "type": "SCALAR" }, + { + "componentType": 5126, + "count": 13357, + "max": [1.7818311868671333, 0.2560550233592158, 1.9506021351947052], + "min": [-1.7322296710147123, -1.0099807591571073, -2.206621131433413], + "type": "VEC3" + }, + { "componentType": 5126, "count": 13357, "type": "VEC3" }, + { "componentType": 5123, "count": 636, "type": "SCALAR" }, + { + "componentType": 5126, + "count": 226, + "max": [0.6599146386851436, -0.020084407309759023, 0.4952644285598964], + "min": [-0.6617880043426723, -0.7533936676422328, -1.2934863743225307], + "type": "VEC3" + }, + { "componentType": 5126, "count": 226, "type": "VEC3" }, + { "componentType": 5123, "count": 1422, "type": "SCALAR" }, + { + "componentType": 5126, + "count": 480, + "max": [1.4881080126960393, -0.4207165063621361, 1.57790809738735], + "min": [-1.4427120423514959, -0.8441048598487493, -0.912359211464655], + "type": "VEC3" + }, + { "componentType": 5126, "count": 480, "type": "VEC3" }, + { "componentType": 5123, "count": 879, "type": "SCALAR" }, + { + "componentType": 5126, + "count": 299, + "max": [0.3329383138918446, -0.5012074743734667, 2.468574396971091], + "min": [-0.3308996860289143, -0.6171928350233601, 1.8112160261369183], + "type": "VEC3" + }, + { "componentType": 5126, "count": 299, "type": "VEC3" }, + { "componentType": 5123, "count": 84, "type": "SCALAR" }, + { + "componentType": 5126, + "count": 30, + "max": [0.6592579995007648, -0.02583246521968402, 0.3846669450263949], + "min": [-0.6615596328587665, -0.27207567437602415, 0.17581144825504882], + "type": "VEC3" + }, + { "componentType": 5126, "count": 30, "type": "VEC3" } + ], + "bufferViews": [ + { "buffer": 0, "byteOffset": 0, "byteLength": 238 }, + { "buffer": 0, "byteOffset": 240, "byteLength": 12128 }, + { "buffer": 0, "byteOffset": 12368, "byteLength": 32109 }, + { "buffer": 0, "byteOffset": 44480, "byteLength": 884 }, + { "buffer": 0, "byteOffset": 45364, "byteLength": 1371 }, + { "buffer": 0, "byteOffset": 46736, "byteLength": 1000 }, + { "buffer": 0, "byteOffset": 47736, "byteLength": 273 } + ], + "buffers": [ + { + "name": "buffer", + "byteLength": 48012, + "uri": "data:application/octet-stream;base64,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" + } + ], + "extensionsRequired": ["KHR_draco_mesh_compression"], + "extensionsUsed": ["KHR_draco_mesh_compression"] +} diff --git a/apps/kitchen-sink/public/suzanne.glb b/apps/examples/public/suzanne.glb similarity index 100% rename from apps/kitchen-sink/public/suzanne.glb rename to apps/examples/public/suzanne.glb diff --git a/apps/kitchen-sink/public/synth-final.mp3 b/apps/examples/public/synth-final.mp3 similarity index 100% rename from apps/kitchen-sink/public/synth-final.mp3 rename to apps/examples/public/synth-final.mp3 diff --git a/apps/kitchen-sink/public/texture/skydiver_BaseColor.webp b/apps/examples/public/texture/skydiver_BaseColor.webp similarity index 100% rename from apps/kitchen-sink/public/texture/skydiver_BaseColor.webp rename to apps/examples/public/texture/skydiver_BaseColor.webp diff --git a/apps/kitchen-sink/public/texture/skydiver_Clothes.webp b/apps/examples/public/texture/skydiver_Clothes.webp similarity index 100% rename from apps/kitchen-sink/public/texture/skydiver_Clothes.webp rename to apps/examples/public/texture/skydiver_Clothes.webp diff --git a/apps/kitchen-sink/public/texture/skydiver_Metallic.webp b/apps/examples/public/texture/skydiver_Metallic.webp similarity index 100% rename from apps/kitchen-sink/public/texture/skydiver_Metallic.webp rename to apps/examples/public/texture/skydiver_Metallic.webp diff --git a/apps/kitchen-sink/public/texture/skydiver_Normal.webp b/apps/examples/public/texture/skydiver_Normal.webp similarity index 100% rename from apps/kitchen-sink/public/texture/skydiver_Normal.webp rename to apps/examples/public/texture/skydiver_Normal.webp diff --git a/apps/kitchen-sink/public/texture/skydiver_Roughness.webp b/apps/examples/public/texture/skydiver_Roughness.webp similarity index 100% rename from apps/kitchen-sink/public/texture/skydiver_Roughness.webp rename to apps/examples/public/texture/skydiver_Roughness.webp diff --git a/apps/kitchen-sink/public/three.png b/apps/examples/public/three.png similarity index 100% rename from apps/kitchen-sink/public/three.png rename to apps/examples/public/three.png diff --git a/apps/kitchen-sink/public/view.svg b/apps/examples/public/view.svg similarity index 100% rename from apps/kitchen-sink/public/view.svg rename to apps/examples/public/view.svg diff --git a/apps/kitchen-sink/public/wheel.glb b/apps/examples/public/wheel.glb similarity index 100% rename from apps/kitchen-sink/public/wheel.glb rename to apps/examples/public/wheel.glb diff --git a/apps/kitchen-sink/public/ybot.glb b/apps/examples/public/ybot.glb similarity index 100% rename from apps/kitchen-sink/public/ybot.glb rename to apps/examples/public/ybot.glb diff --git a/apps/kitchen-sink/src/app/app.component.ts b/apps/examples/src/app/app.component.ts similarity index 90% rename from apps/kitchen-sink/src/app/app.component.ts rename to apps/examples/src/app/app.component.ts index 82fc0da5..7c66e3df 100644 --- a/apps/kitchen-sink/src/app/app.component.ts +++ b/apps/examples/src/app/app.component.ts @@ -14,6 +14,7 @@ import { filter } from 'rxjs'; + @@ -54,21 +55,21 @@ export class AppComponent { private navigationEnd = toSignal(this.router.events.pipe(filter((event) => event instanceof NavigationEnd))); private activationEnd = toSignal(this.router.events.pipe(filter((event) => event instanceof ActivationEnd))); - currentRoute = computed(() => { + protected currentRoute = computed(() => { const navigationEnd = this.navigationEnd(); if (!navigationEnd) return 'soba'; const [segment] = navigationEnd.urlAfterRedirects.split('/').filter(Boolean); return segment; }); - currentSourcePath = computed(() => { + protected currentSourcePath = computed(() => { const navigationEnd = this.navigationEnd(); if (!navigationEnd) return ''; const paths = navigationEnd.urlAfterRedirects.split('/').filter(Boolean); - return `https://github.com/angular-threejs/angular-three/tree/main/apps/kitchen-sink/src/app/${paths.join('/')}`; + return `https://github.com/angular-threejs/angular-three/tree/v4-next/apps/examples/src/app/${paths.join('/')}`; }); - currentActivatedRouteCredits = computed(() => { + protected currentActivatedRouteCredits = computed(() => { const activationEnd = this.activationEnd(); if (!activationEnd) return null; @@ -82,7 +83,7 @@ export class AppComponent { return deepestChild.data['credits'] as { title: string; link: string; class: string }; }); - onChange(event: Event) { + protected onChange(event: Event) { const target = event.target as HTMLSelectElement; void this.router.navigate([target.value]); } diff --git a/apps/kitchen-sink/src/app/app.config.ts b/apps/examples/src/app/app.config.ts similarity index 82% rename from apps/kitchen-sink/src/app/app.config.ts rename to apps/examples/src/app/app.config.ts index f906bfe9..a92e8013 100644 --- a/apps/kitchen-sink/src/app/app.config.ts +++ b/apps/examples/src/app/app.config.ts @@ -1,11 +1,12 @@ import { ApplicationConfig, provideExperimentalZonelessChangeDetection } from '@angular/core'; import { provideRouter, withComponentInputBinding } from '@angular/router'; +import { provideNgtRenderer } from 'angular-three/dom'; import { appRoutes } from './app.routes'; export const appConfig: ApplicationConfig = { providers: [ provideExperimentalZonelessChangeDetection(), - // provideZoneChangeDetection({ eventCoalescing: true, runCoalescing: true }), provideRouter(appRoutes, withComponentInputBinding()), + provideNgtRenderer(), ], }; diff --git a/apps/kitchen-sink/src/app/app.routes.ts b/apps/examples/src/app/app.routes.ts similarity index 88% rename from apps/kitchen-sink/src/app/app.routes.ts rename to apps/examples/src/app/app.routes.ts index c5d7294a..9601bcf6 100644 --- a/apps/kitchen-sink/src/app/app.routes.ts +++ b/apps/examples/src/app/app.routes.ts @@ -13,6 +13,12 @@ export const appRoutes: Route[] = [ loadChildren: () => import('./postprocessing/postprocessing.routes'), title: 'Postprocessing - Angular Three Demo', }, + { + path: 'theatre', + loadComponent: () => import('./theatre/theatre'), + loadChildren: () => import('./theatre/theatre.routes'), + title: 'TheatreJS - Angular Three Demo', + }, { path: 'soba', loadComponent: () => import('./soba/soba'), @@ -45,8 +51,6 @@ export const appRoutes: Route[] = [ }, { path: '', - // redirectTo: 'cannon', - // redirectTo: 'postprocessing', redirectTo: 'soba', pathMatch: 'full', }, diff --git a/apps/kitchen-sink/src/app/cannon/basic/basic.ts b/apps/examples/src/app/cannon/basic/basic.ts similarity index 72% rename from apps/kitchen-sink/src/app/cannon/basic/basic.ts rename to apps/examples/src/app/cannon/basic/basic.ts index f1855cfb..51182d65 100644 --- a/apps/kitchen-sink/src/app/cannon/basic/basic.ts +++ b/apps/examples/src/app/cannon/basic/basic.ts @@ -1,23 +1,24 @@ import { ChangeDetectionStrategy, Component, inject } from '@angular/core'; -import { NgtCanvas } from 'angular-three'; -import { Experience } from './experience'; +import { NgtCanvas } from 'angular-three/dom'; +import { SceneGraph } from './scene'; import { State } from './state'; @Component({ template: ` - + + +
|
`, - imports: [NgtCanvas], + imports: [NgtCanvas, SceneGraph], changeDetection: ChangeDetectionStrategy.OnPush, host: { class: 'basic-cannon ' }, providers: [State], }) export default class Basic { - protected scene = Experience; protected state = inject(State); } diff --git a/apps/kitchen-sink/src/app/cannon/basic/experience.ts b/apps/examples/src/app/cannon/basic/scene.ts similarity index 86% rename from apps/kitchen-sink/src/app/cannon/basic/experience.ts rename to apps/examples/src/app/cannon/basic/scene.ts index 184e571a..73154772 100644 --- a/apps/kitchen-sink/src/app/cannon/basic/experience.ts +++ b/apps/examples/src/app/cannon/basic/scene.ts @@ -12,7 +12,7 @@ import { import { Triplet } from '@pmndrs/cannon-worker-api'; import { NgtArgs } from 'angular-three'; import { NgtcPhysics } from 'angular-three-cannon'; -import { injectBox, injectPlane } from 'angular-three-cannon/body'; +import { box, plane } from 'angular-three-cannon/body'; import { NgtcDebug } from 'angular-three-cannon/debug'; import { Mesh } from 'three'; import { State } from './state'; @@ -20,7 +20,7 @@ import { State } from './state'; @Component({ selector: 'app-plane', template: ` - + @@ -35,14 +35,14 @@ export class Plane { private mesh = viewChild.required>('mesh'); constructor() { - injectPlane(() => ({ mass: 0, position: this.position(), args: this.args }), this.mesh); + plane(() => ({ mass: 0, position: this.position(), args: this.args }), this.mesh); } } @Component({ selector: 'app-box', template: ` - + @@ -57,11 +57,12 @@ export class Box { private mesh = viewChild.required>('mesh'); constructor() { - injectBox(() => ({ mass: 10000, position: this.position(), args: this.args }), this.mesh); + box(() => ({ mass: 10000, position: this.position(), args: this.args }), this.mesh); } } @Component({ + selector: 'app-scene-graph', template: ` diff --git a/apps/examples/src/app/cannon/chain/chain.ts b/apps/examples/src/app/cannon/chain/chain.ts new file mode 100644 index 00000000..7f39d8df --- /dev/null +++ b/apps/examples/src/app/cannon/chain/chain.ts @@ -0,0 +1,16 @@ +import { ChangeDetectionStrategy, Component } from '@angular/core'; +import { NgtCanvas } from 'angular-three/dom'; +import { SceneGraph } from './scene'; + +@Component({ + template: ` + + + +
* Click to reset
+ `, + imports: [NgtCanvas, SceneGraph], + changeDetection: ChangeDetectionStrategy.OnPush, + host: { class: 'chain-cannon' }, +}) +export default class Chain {} diff --git a/apps/kitchen-sink/src/app/cannon/chain/experience.ts b/apps/examples/src/app/cannon/chain/scene.ts similarity index 86% rename from apps/kitchen-sink/src/app/cannon/chain/experience.ts rename to apps/examples/src/app/cannon/chain/scene.ts index 73fef9a5..3a6624f5 100644 --- a/apps/kitchen-sink/src/app/cannon/chain/experience.ts +++ b/apps/examples/src/app/cannon/chain/scene.ts @@ -16,10 +16,10 @@ import { } from '@angular/core'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { CylinderArgs, Triplet } from '@pmndrs/cannon-worker-api'; -import { NgtArgs, injectBeforeRender, injectStore } from 'angular-three'; +import { NgtArgs, beforeRender, injectStore } from 'angular-three'; import { NgtcPhysics } from 'angular-three-cannon'; -import { injectBox, injectCylinder, injectSphere } from 'angular-three-cannon/body'; -import { injectConeTwist } from 'angular-three-cannon/constraint'; +import { box, cylinder, sphere } from 'angular-three-cannon/body'; +import { coneTwist } from 'angular-three-cannon/constraint'; import { Color, ColorRepresentation, Mesh, Object3D } from 'three'; interface Handle { @@ -63,12 +63,12 @@ export class ChainLink { protected mesh = viewChild.required>('mesh'); constructor() { - injectCylinder(() => ({ mass: 1, args: this.args(), linearDamping: 0.8, position: this.position() }), this.mesh); + cylinder(() => ({ mass: 1, args: this.args(), linearDamping: 0.8, position: this.position() }), this.mesh); const injector = inject(Injector); // NOTE: we want to run this in afterNextRender because we want the input to resolve afterNextRender(() => { - injectConeTwist(this.parent.ref, this.mesh, { + coneTwist(this.parent.ref, this.mesh, { injector, options: { angle: Math.PI / 8, @@ -140,9 +140,9 @@ export class PointerHandle { protected mesh = viewChild.required>('mesh'); constructor() { - const boxApi = injectBox(() => ({ args: this.args, position: this.position, type: 'Kinematic' }), this.mesh); + const boxApi = box(() => ({ args: this.args, position: this.position, type: 'Kinematic' }), this.mesh); - injectBeforeRender(({ pointer: { x, y }, viewport: { width, height } }) => { + beforeRender(({ pointer: { x, y }, viewport: { width, height } }) => { boxApi()?.position.set((x * width) / 2, (y * height) / 2, 0); }); } @@ -170,11 +170,12 @@ export class StaticHandle { protected mesh = viewChild.required>('mesh'); constructor() { - injectSphere(() => ({ args: [1.5], position: this.position(), type: 'Static' }), this.mesh); + sphere(() => ({ args: [1.5], position: this.position(), type: 'Static' }), this.mesh); } } @Component({ + selector: 'app-scene-graph', template: ` @@ -185,7 +186,7 @@ export class StaticHandle { [penumbra]="1" [intensity]="Math.PI" [decay]="0" - [castShadow]="true" + castShadow /> @@ -205,7 +206,7 @@ export class StaticHandle { schemas: [CUSTOM_ELEMENTS_SCHEMA], host: { class: 'chain-experience' }, }) -export class Experience { +export class SceneGraph { protected Math = Math; private store = injectStore(); @@ -220,11 +221,8 @@ export class Experience { ); constructor() { - this.store - .get('pointerMissed$') - .pipe(takeUntilDestroyed()) - .subscribe(() => { - this.resetCount.update((prev) => prev + 1); - }); + this.store.snapshot.pointerMissed$.pipe(takeUntilDestroyed()).subscribe(() => { + this.resetCount.update((prev) => prev + 1); + }); } } diff --git a/apps/examples/src/app/cannon/compound/compound.ts b/apps/examples/src/app/cannon/compound/compound.ts new file mode 100644 index 00000000..bb2bf2da --- /dev/null +++ b/apps/examples/src/app/cannon/compound/compound.ts @@ -0,0 +1,15 @@ +import { ChangeDetectionStrategy, Component } from '@angular/core'; +import { NgtCanvas } from 'angular-three/dom'; +import { SceneGraph } from './scene'; + +@Component({ + template: ` + + + + `, + imports: [NgtCanvas, SceneGraph], + changeDetection: ChangeDetectionStrategy.OnPush, + host: { class: 'compound-cannon' }, +}) +export default class Compound {} diff --git a/apps/kitchen-sink/src/app/cannon/compound/experience.ts b/apps/examples/src/app/cannon/compound/scene.ts similarity index 87% rename from apps/kitchen-sink/src/app/cannon/compound/experience.ts rename to apps/examples/src/app/cannon/compound/scene.ts index dfc0addc..c475360f 100644 --- a/apps/kitchen-sink/src/app/cannon/compound/experience.ts +++ b/apps/examples/src/app/cannon/compound/scene.ts @@ -4,6 +4,7 @@ import { Component, ElementRef, afterNextRender, + booleanAttribute, effect, input, output, @@ -13,7 +14,7 @@ import { import { Triplet } from '@pmndrs/cannon-worker-api'; import { NgtArgs } from 'angular-three'; import { NgtcPhysics } from 'angular-three-cannon'; -import { injectCompound, injectPlane } from 'angular-three-cannon/body'; +import { compound, plane } from 'angular-three-cannon/body'; import { NgtcDebug } from 'angular-three-cannon/debug'; import { Group } from 'three'; @@ -25,7 +26,7 @@ import { Group } from 'three';
- + @@ -40,7 +41,7 @@ export class Plane { private group = viewChild.required>('group'); constructor() { - injectPlane(() => ({ type: 'Static', rotation: this.rotation() }), this.group); + plane(() => ({ type: 'Static', rotation: this.rotation() }), this.group); } } @@ -48,11 +49,11 @@ export class Plane { selector: 'app-compound-body', template: ` - + - + @@ -68,7 +69,7 @@ export class CompoundBody { position = input([0, 0, 0]); rotation = input([0, 0, 0]); - isTrigger = input(false); + isTrigger = input(false, { transform: booleanAttribute }); mass = input(12); positionChanged = output(); @@ -77,7 +78,7 @@ export class CompoundBody { private group = viewChild.required>('group'); constructor() { - const compoundApi = injectCompound( + const compoundApi = compound( () => ({ isTrigger: this.isTrigger(), mass: this.mass(), @@ -107,10 +108,11 @@ export class CompoundBody { } @Component({ + selector: 'app-scene-graph', template: ` - + @@ -129,7 +131,7 @@ export class CompoundBody { } @if (copy()) { - + } `, @@ -138,7 +140,7 @@ export class CompoundBody { changeDetection: ChangeDetectionStrategy.OnPush, host: { class: 'compound-experience' }, }) -export class Experience { +export class SceneGraph { protected Math = Math; protected ready = signal(false); diff --git a/apps/examples/src/app/cannon/convexpolyhedron/convexpolyhedron.ts b/apps/examples/src/app/cannon/convexpolyhedron/convexpolyhedron.ts new file mode 100644 index 00000000..a629f1c3 --- /dev/null +++ b/apps/examples/src/app/cannon/convexpolyhedron/convexpolyhedron.ts @@ -0,0 +1,16 @@ +import { ChangeDetectionStrategy, Component } from '@angular/core'; +import { NgtCanvas } from 'angular-three/dom'; +import { SceneGraph } from './scene'; + +@Component({ + template: ` + + + +
* Click to invert gravity
+ `, + imports: [NgtCanvas, SceneGraph], + changeDetection: ChangeDetectionStrategy.OnPush, + host: { class: 'convex-cannon' }, +}) +export default class ConvexPolyhedron {} diff --git a/apps/kitchen-sink/src/app/cannon/convexpolyhedron/experience.ts b/apps/examples/src/app/cannon/convexpolyhedron/scene.ts similarity index 91% rename from apps/kitchen-sink/src/app/cannon/convexpolyhedron/experience.ts rename to apps/examples/src/app/cannon/convexpolyhedron/scene.ts index 87ce33bc..689aff38 100644 --- a/apps/kitchen-sink/src/app/cannon/convexpolyhedron/experience.ts +++ b/apps/examples/src/app/cannon/convexpolyhedron/scene.ts @@ -12,9 +12,9 @@ import { import { Triplet } from '@pmndrs/cannon-worker-api'; import { NgtArgs } from 'angular-three'; import { NgtcPhysics } from 'angular-three-cannon'; -import { injectConvexPolyhedron } from 'angular-three-cannon/body'; +import { convexPolyhedron } from 'angular-three-cannon/body'; import { NgtcDebug } from 'angular-three-cannon/debug'; -import { injectGLTF } from 'angular-three-soba/loaders'; +import { gltfResource } from 'angular-three-soba/loaders'; import { BoxGeometry, BufferGeometry, ConeGeometry, Mesh } from 'three'; import { GLTF, Geometry } from 'three-stdlib'; import { UiPlane } from '../ui/plane'; @@ -34,7 +34,7 @@ function toConvexProps(bufferGeometry: BufferGeometry): [vertices: Triplet[], fa template: ` >('mesh'); constructor() { - injectConvexPolyhedron( + convexPolyhedron( () => ({ args: this.args(), mass: 100, @@ -75,8 +75,8 @@ export class Cone { template: ` >('mesh'); constructor() { - injectConvexPolyhedron( + convexPolyhedron( () => ({ args: this.args(), mass: 100, @@ -121,12 +121,12 @@ type DiamondGLTF = GLTF & { @if (geometry(); as geometry) { - + } `, @@ -137,9 +137,9 @@ type DiamondGLTF = GLTF & { export class Diamond { protected positionRotationInputs = inject(PositionRotationInput, { host: true }); - private gltf = injectGLTF(() => './diamond.glb'); + private gltf = gltfResource(() => './diamond.glb'); protected geometry = computed(() => { - const gltf = this.gltf(); + const gltf = this.gltf.value(); if (!gltf) return null; return gltf.nodes.Cylinder.geometry; }); @@ -152,7 +152,7 @@ export class Diamond { private mesh = viewChild>('mesh'); constructor() { - injectConvexPolyhedron( + convexPolyhedron( () => ({ args: this.args(), mass: 100, @@ -165,11 +165,12 @@ export class Diamond { } @Component({ + selector: 'app-scene-graph', template: ` + + + `, - imports: [NgtCanvas], + imports: [NgtCanvas, SceneGraph], changeDetection: ChangeDetectionStrategy.OnPush, host: { class: 'cube-heap-cannon' }, }) export default class CubeHeap { - protected scene = Experience; + protected scene = SceneGraph; onPointerMissed() { shape.update((prev) => (prev === 'box' ? 'sphere' : 'box')); diff --git a/apps/kitchen-sink/src/app/cannon/cube-heap/experience.ts b/apps/examples/src/app/cannon/cube-heap/scene.ts similarity index 80% rename from apps/kitchen-sink/src/app/cannon/cube-heap/experience.ts rename to apps/examples/src/app/cannon/cube-heap/scene.ts index 7a20d667..313570ca 100644 --- a/apps/kitchen-sink/src/app/cannon/cube-heap/experience.ts +++ b/apps/examples/src/app/cannon/cube-heap/scene.ts @@ -11,9 +11,9 @@ import { viewChild, } from '@angular/core'; import { Triplet } from '@pmndrs/cannon-worker-api'; -import { NgtArgs, injectBeforeRender } from 'angular-three'; +import { NgtArgs, beforeRender } from 'angular-three'; import { NgtcPhysics } from 'angular-three-cannon'; -import { NgtcBodyPublicApi, injectBox, injectPlane, injectSphere } from 'angular-three-cannon/body'; +import { NgtcBodyPublicApi, box, plane, sphere } from 'angular-three-cannon/body'; import { Color, InstancedMesh, Mesh } from 'three'; import niceColors from '../../colors'; import { shape } from './state'; @@ -21,7 +21,7 @@ import { shape } from './state'; @Component({ selector: 'app-plane', template: ` - + @@ -36,7 +36,7 @@ export class Plane { private mesh = viewChild.required>('mesh'); constructor() { - injectPlane(() => ({ rotation: this.rotation() }), this.mesh); + plane(() => ({ rotation: this.rotation() }), this.mesh); } } @@ -49,7 +49,7 @@ export abstract class InstancesInput { abstract bodyApi: Signal; constructor() { - injectBeforeRender(() => { + beforeRender(() => { this.bodyApi() ?.at(Math.floor(Math.random() * this.count())) .position.set(0, Math.random() * 2, 0); @@ -60,11 +60,11 @@ export abstract class InstancesInput { @Component({ selector: 'app-boxes', template: ` - + - + `, imports: [NgtArgs], @@ -75,7 +75,7 @@ export class Boxes extends InstancesInput { protected args = computed(() => [this.size(), this.size(), this.size()]); private mesh = viewChild>('mesh'); - bodyApi = injectBox( + bodyApi = box( () => ({ args: this.args(), mass: 1, position: [Math.random() - 0.5, Math.random() * 2, Math.random() - 0.5] }), this.mesh, ); @@ -84,11 +84,11 @@ export class Boxes extends InstancesInput { @Component({ selector: 'app-spheres', template: ` - + - + `, imports: [NgtArgs], @@ -99,19 +99,24 @@ export class Spheres extends InstancesInput { protected args = computed(() => [this.size(), this.size(), this.size()]); private mesh = viewChild>('mesh'); - bodyApi = injectSphere( - () => ({ args: [this.size()], mass: 1, position: [Math.random() - 0.5, Math.random() * 2, Math.random() - 0.5] }), + bodyApi = sphere( + () => ({ + args: [this.size()], + mass: 1, + position: [Math.random() - 0.5, Math.random() * 2, Math.random() - 0.5], + }), this.mesh, ); } @Component({ + selector: 'app-scene-graph', template: ` + +
+ `, + changeDetection: ChangeDetectionStrategy.OnPush, + imports: [NgtCanvas, SceneGraph], + host: { class: 'kinematic-cannon' }, +}) +export default class KinematicCube {} diff --git a/apps/kitchen-sink/src/app/cannon/kinematic-cube/experience.ts b/apps/examples/src/app/cannon/kinematic-cube/scene.ts similarity index 82% rename from apps/kitchen-sink/src/app/cannon/kinematic-cube/experience.ts rename to apps/examples/src/app/cannon/kinematic-cube/scene.ts index 75987d2a..bb1d47a6 100644 --- a/apps/kitchen-sink/src/app/cannon/kinematic-cube/experience.ts +++ b/apps/examples/src/app/cannon/kinematic-cube/scene.ts @@ -8,16 +8,16 @@ import { viewChild, } from '@angular/core'; import { Triplet } from '@pmndrs/cannon-worker-api'; -import { NgtArgs, injectBeforeRender } from 'angular-three'; +import { NgtArgs, beforeRender } from 'angular-three'; import { NgtcPhysics } from 'angular-three-cannon'; -import { injectBox, injectPlane, injectSphere } from 'angular-three-cannon/body'; +import { box, plane, sphere } from 'angular-three-cannon/body'; import { Color, InstancedMesh, Mesh } from 'three'; import niceColors from '../../colors'; @Component({ selector: 'app-plane', template: ` - + @@ -34,14 +34,14 @@ export class Plane { private mesh = viewChild.required>('mesh'); constructor() { - injectPlane(() => ({ position: this.position(), rotation: this.rotation() }), this.mesh); + plane(() => ({ position: this.position(), rotation: this.rotation() }), this.mesh); } } @Component({ selector: 'app-box', template: ` - + @@ -56,12 +56,12 @@ export class Box { private mesh = viewChild.required>('mesh'); constructor() { - const boxApi = injectBox(() => ({ args: this.args, mass: 1, type: 'Kinematic' }), this.mesh); + const boxApi = box(() => ({ args: this.args, mass: 1, type: 'Kinematic' }), this.mesh); - injectBeforeRender(({ clock }) => { + beforeRender(({ clock }) => { const api = boxApi(); if (!api) return; - const t = clock.getElapsedTime(); + const t = clock.elapsedTime; api.position.set(Math.sin(t * 2) * 5, Math.cos(t * 2) * 5, 3); api.rotation.set(Math.sin(t * 6), Math.cos(t * 6), 0); }); @@ -71,16 +71,11 @@ export class Box { @Component({ selector: 'app-instanced-spheres', template: ` - + - + `, imports: [NgtArgs], @@ -105,7 +100,7 @@ export class InstancedSpheres { }); constructor() { - injectSphere( + sphere( (index) => ({ args: [1], mass: 1, @@ -117,11 +112,12 @@ export class InstancedSpheres { } @Component({ + selector: 'app-scene-graph', template: ` + +
+ `, + imports: [NgtCanvas, SceneGraph], + changeDetection: ChangeDetectionStrategy.OnPush, + host: { class: 'monday-morning-cannon cursor-none' }, +}) +export default class MondayMorning {} diff --git a/apps/kitchen-sink/src/app/cannon/monday-morning/experience.ts b/apps/examples/src/app/cannon/monday-morning/scene.ts similarity index 85% rename from apps/kitchen-sink/src/app/cannon/monday-morning/experience.ts rename to apps/examples/src/app/cannon/monday-morning/scene.ts index 014afa13..dcf62673 100644 --- a/apps/kitchen-sink/src/app/cannon/monday-morning/experience.ts +++ b/apps/examples/src/app/cannon/monday-morning/scene.ts @@ -6,6 +6,7 @@ import { Injector, Signal, afterNextRender, + booleanAttribute, computed, inject, input, @@ -13,26 +14,26 @@ import { viewChild, } from '@angular/core'; import { ConeTwistConstraintOpts, Triplet } from '@pmndrs/cannon-worker-api'; -import { NgtArgs, NgtThreeEvent, NgtVector3, injectBeforeRender, injectObjectEvents } from 'angular-three'; +import { NgtArgs, NgtThreeEvent, NgtVector3, beforeRender, objectEvents } from 'angular-three'; import { NgtcPhysics } from 'angular-three-cannon'; -import { injectBox, injectCompound, injectCylinder, injectSphere } from 'angular-three-cannon/body'; -import { NgtcConstraintApi, injectConeTwist, injectPointToPoint } from 'angular-three-cannon/constraint'; +import { box, compound, cylinder, sphere } from 'angular-three-cannon/body'; +import { NgtcConstraintApi, coneTwist, pointToPoint } from 'angular-three-cannon/constraint'; import { NgtcDebug } from 'angular-three-cannon/debug'; import { NgtsRoundedBox } from 'angular-three-soba/abstractions'; -import { injectGLTF } from 'angular-three-soba/loaders'; +import { gltfResource } from 'angular-three-soba/loaders'; import { NgtsSpotLight } from 'angular-three-soba/staging'; import { Group, Material, Mesh, Object3D } from 'three'; import { GLTF } from 'three-stdlib'; import { UiPlane } from '../ui/plane'; import { createRagdoll } from './config'; -function injectDragConstraint(ref: Signal | undefined>) { +function dragConstraint(ref: Signal | undefined>) { const cursorRef = inject(Cursor); const injector = inject(Injector); - let pointToPoint: Signal; + let pointToPointApi: Signal; afterNextRender(() => { - pointToPoint = injectPointToPoint(cursorRef.mesh, ref, { + pointToPointApi = pointToPoint(cursorRef.mesh, ref, { injector, options: { pivotA: [0, 0, 0], pivotB: [0, 0, 0] }, disableOnStart: true, @@ -43,11 +44,11 @@ function injectDragConstraint(ref: Signal | undefined>) { event.stopPropagation(); //@ts-expect-error Investigate proper types here. event.target.setPointerCapture(event.pointerId); - pointToPoint()?.enable(); + pointToPointApi()?.enable(); } function onPointerUp() { - pointToPoint()?.disable(); + pointToPointApi()?.disable(); } return { onPointerUp, onPointerDown }; @@ -81,7 +82,7 @@ export class Box { depth = input(1); color = input('white'); opacity = input(1); - transparent = input(false); + transparent = input(false, { transform: booleanAttribute }); args = input([1, 1, 1]); position = input([0, 0, 0]); scale = input([1, 1, 1]); @@ -93,7 +94,7 @@ export class Box { meshRef = computed(() => this.roundedBoxRef().meshRef()); constructor() { - injectObjectEvents(this.meshRef, { + objectEvents(this.meshRef, { pointerdown: (event) => { this.pointerdown.emit(event as NgtThreeEvent); }, @@ -141,13 +142,13 @@ export class BodyPart { private box = viewChild.required(Box); private body = computed(() => this.box().meshRef()); - protected dragConstraint = injectDragConstraint(this.body); + protected dragConstraint = dragConstraint(this.body); constructor() { const injector = inject(Injector); const parent = inject(BodyPart, { skipSelf: true, optional: true }); - injectBox( + box( () => ({ args: [...this.shapeConfig().args], linearDamping: 0.99, @@ -159,7 +160,7 @@ export class BodyPart { afterNextRender(() => { if (parent && this.constraintOpts()) { - injectConeTwist(this.body, parent.body, { + coneTwist(this.body, parent.body, { injector, options: this.constraintOpts(), }); @@ -183,7 +184,7 @@ export class BodyPart { [args]="[0.3, 0.01, 0.1]" [opacity]="0.8" [position]="[-0.3, 0.1, 0.5]" - [transparent]="true" + transparent />
@@ -237,11 +238,11 @@ export class RagDoll { private mouth = viewChild('mouth', { read: Box }); constructor() { - injectBeforeRender(({ clock }) => { + beforeRender(({ clock }) => { const [eyes, mouth] = [this.eyes(), this.mouth()]; if (eyes && mouth) { - eyes.nativeElement.position.y = Math.sin(clock.getElapsedTime() * 1) * 0.06; - mouth.meshRef().nativeElement.scale.y = (1 + Math.sin(clock.getElapsedTime())) * 0.6; + eyes.nativeElement.position.y = Math.sin(clock.elapsedTime * 1) * 0.06; + mouth.meshRef().nativeElement.scale.y = (1 + Math.sin(clock.elapsedTime)) * 0.6; } }); } @@ -269,10 +270,10 @@ export class RagDoll { }) export class Chair { private group = viewChild.required>('group'); - protected dragConstraint = injectDragConstraint(this.group); + protected dragConstraint = dragConstraint(this.group); constructor() { - injectCompound( + compound( () => ({ mass: 1, position: [-6, 0, 0], @@ -306,18 +307,18 @@ interface CupGLTF extends GLTF { [dispose]="null" > - @if (gltf(); as gltf) { + @if (gltf.value(); as gltf) { } @@ -327,13 +328,13 @@ interface CupGLTF extends GLTF { changeDetection: ChangeDetectionStrategy.OnPush, }) export class Mug { - protected gltf = injectGLTF(() => './cup.glb'); + protected gltf = gltfResource(() => './cup.glb'); private group = viewChild.required>('group'); - protected dragConstraint = injectDragConstraint(this.group); + protected dragConstraint = dragConstraint(this.group); constructor() { - injectCylinder( + cylinder( () => ({ args: [0.6, 0.6, 1, 16], mass: 1, position: [9, 0, 0], rotation: [Math.PI / 2, 0, 0] }), this.group, ); @@ -362,23 +363,23 @@ export class Table { private leg4Ref = viewChild('leg4', { read: Box }); constructor() { - injectBox( + box( () => ({ args: [2.5, 0.25, 2.5], position: [9, -0.8, 0], type: 'Static' }), computed(() => this.seatRef()?.meshRef()), ); - injectBox( + box( () => ({ args: [0.25, 2, 0.25], position: [7.2, -3, 1.8], type: 'Static' }), computed(() => this.leg1Ref()?.meshRef()), ); - injectBox( + box( () => ({ args: [0.25, 2, 0.25], position: [10.8, -3, 1.8], type: 'Static' }), computed(() => this.leg2Ref()?.meshRef()), ); - injectBox( + box( () => ({ args: [0.25, 2, 0.25], position: [7.2, -3, -1.8], type: 'Static' }), computed(() => this.leg3Ref()?.meshRef()), ); - injectBox( + box( () => ({ args: [0.25, 2, 0.25], position: [10.8, -3, -1.8], type: 'Static' }), computed(() => this.leg4Ref()?.meshRef()), ); @@ -390,7 +391,7 @@ export class Table { template: ` - + `, @@ -402,8 +403,8 @@ export class Cursor { mesh = viewChild.required>('mesh'); constructor() { - const sphereApi = injectSphere(() => ({ args: [0.5], position: [0, 0, 10000], type: 'Static' }), this.mesh); - injectBeforeRender(({ pointer, viewport: { width, height } }) => { + const sphereApi = sphere(() => ({ args: [0.5], position: [0, 0, 10000], type: 'Static' }), this.mesh); + beforeRender(({ pointer, viewport: { width, height } }) => { const x = pointer.x * width; const y = (pointer.y * height) / 1.9 + -x / 3.5; sphereApi()?.position.set(x / 1.4, y, 0); @@ -422,7 +423,7 @@ export class Cursor { - + @@ -494,6 +496,6 @@ export class Lamp { schemas: [CUSTOM_ELEMENTS_SCHEMA], host: { class: 'monday-morning-experience' }, }) -export class Experience { +export class SceneGraph { protected Math = Math; } diff --git a/apps/kitchen-sink/src/app/cannon/ui/plane.ts b/apps/examples/src/app/cannon/ui/plane.ts similarity index 91% rename from apps/kitchen-sink/src/app/cannon/ui/plane.ts rename to apps/examples/src/app/cannon/ui/plane.ts index cd2411b3..4fd0dacb 100644 --- a/apps/kitchen-sink/src/app/cannon/ui/plane.ts +++ b/apps/examples/src/app/cannon/ui/plane.ts @@ -8,14 +8,14 @@ import { viewChild, } from '@angular/core'; import { NgtArgs } from 'angular-three'; -import { injectPlane } from 'angular-three-cannon/body'; +import { plane } from 'angular-three-cannon/body'; import { Mesh } from 'three'; import { PositionRotationInput } from './position-rotation-input'; @Component({ selector: 'app-ui-plane', template: ` - + @if (useShadowMaterial()) { @@ -39,7 +39,7 @@ export class UiPlane { private mesh = viewChild.required>('mesh'); constructor() { - injectPlane( + plane( () => ({ type: 'Static', rotation: this.positionRotationInput.rotation(), diff --git a/apps/kitchen-sink/src/app/cannon/ui/position-rotation-input.ts b/apps/examples/src/app/cannon/ui/position-rotation-input.ts similarity index 100% rename from apps/kitchen-sink/src/app/cannon/ui/position-rotation-input.ts rename to apps/examples/src/app/cannon/ui/position-rotation-input.ts diff --git a/apps/kitchen-sink/src/app/colors.ts b/apps/examples/src/app/colors.ts similarity index 100% rename from apps/kitchen-sink/src/app/colors.ts rename to apps/examples/src/app/colors.ts diff --git a/apps/examples/src/app/misc/basic/basic.ts b/apps/examples/src/app/misc/basic/basic.ts new file mode 100644 index 00000000..45637416 --- /dev/null +++ b/apps/examples/src/app/misc/basic/basic.ts @@ -0,0 +1,14 @@ +import { ChangeDetectionStrategy, Component } from '@angular/core'; +import { NgtCanvas } from 'angular-three/dom'; +import { Scene } from './scene'; + +@Component({ + template: ` + + + + `, + imports: [NgtCanvas, Scene], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export default class Basic {} diff --git a/apps/examples/src/app/misc/basic/scene.ts b/apps/examples/src/app/misc/basic/scene.ts new file mode 100644 index 00000000..cf8b709d --- /dev/null +++ b/apps/examples/src/app/misc/basic/scene.ts @@ -0,0 +1,232 @@ +import { + ChangeDetectionStrategy, + Component, + CUSTOM_ELEMENTS_SCHEMA, + ElementRef, + HostAttributeToken, + inject, + input, + signal, + viewChild, +} from '@angular/core'; +import { beforeRender, injectStore, NgtArgs, NgtPortal, NgtVector3 } from 'angular-three'; +import * as THREE from 'three'; + +@Component({ + selector: 'app-condition-box', + template: ` + @if (true) { + + + + + + + + } + `, + imports: [NgtArgs], + schemas: [CUSTOM_ELEMENTS_SCHEMA], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class ConditionBox { + position = input(0); + + onAttach(event: any) { + console.log('in condition box', event); + } +} + +@Component({ + selector: 'app-box', + template: ` + + + + + + + + + + `, + imports: [NgtArgs], + schemas: [CUSTOM_ELEMENTS_SCHEMA], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class Box { + position = input(0); + color = input('turquoise'); + + constructor() { + const store = injectStore(); + const context = inject(new HostAttributeToken('context'), { optional: true }); + console.log({ context, store: store.snapshot.id, previous: store.snapshot.previousRoot }); + } + + onAttach(event: any) { + console.log('in box', event); + } +} + +@Component({ + selector: 'app-nested-box', + template: ` + + + + + + + + + + + `, + imports: [NgtArgs, Box], + schemas: [CUSTOM_ELEMENTS_SCHEMA], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class NestedBox { + position = input(0); + color = input('turquoise'); + + onAttach(event: any) { + console.log('in nested box', event); + } +} + +@Component({ + selector: 'app-scene', + template: ` + + + + + + + + + + + + + + + + + + @if (show()) { + + + + + } + + + + + + + + + + + + + + + + + @if (show()) { + + @if (show()) { + + } + + } + + + + + + + + + + + + + + + @if (true) { + + } + + @if (show()) { + + } + + + + + + + @if (show()) { + + } + + + + + + + + + @if (show()) { + + } + + + `, + imports: [NgtArgs, Box, ConditionBox, NgtPortal, NestedBox], + changeDetection: ChangeDetectionStrategy.OnPush, + schemas: [CUSTOM_ELEMENTS_SCHEMA], + host: { + '(document:click)': 'onDocumentClick($event)', + }, +}) +export class Scene { + protected readonly Math = Math; + + protected show = signal(true); + protected color = signal('hotpink'); + protected sphereArgs = signal([0.5, 32, 32]); + + protected virtualScene = new THREE.Scene(); + + private groupRef = viewChild.required>('group'); + + protected mesh = new THREE.Mesh(new THREE.IcosahedronGeometry(), new THREE.MeshNormalMaterial()); + + constructor() { + setInterval(() => { + this.show.update((v) => !v); + this.sphereArgs.update((v) => [v[0] === 0.5 ? 1 : 0.5, v[1], v[2]]); + }, 2500); + + beforeRender(({ delta }) => { + const group = this.groupRef().nativeElement; + group.rotation.x += delta; + group.rotation.y += delta; + }); + } + + onDocumentClick(event: MouseEvent) { + console.log('document', event); + } + + onAttach(event: any) { + console.log('in scene', event); + } +} diff --git a/apps/examples/src/app/misc/misc.routes.ts b/apps/examples/src/app/misc/misc.routes.ts new file mode 100644 index 00000000..82acb039 --- /dev/null +++ b/apps/examples/src/app/misc/misc.routes.ts @@ -0,0 +1,68 @@ +import { Routes } from '@angular/router'; + +const routes: Routes = [ + { + path: 'basic', + loadComponent: () => import('./basic/basic'), + }, + { + path: 'svg-renderer', + loadComponent: () => import('./svg-renderer/svg-renderer'), + data: { + credits: { + title: 'SVG Renderer w/ Lines', + link: 'https://threejs.org/examples/#svg_lines', + class: 'text-white', + }, + }, + }, + { + path: 'pointer-events', + loadComponent: () => import('./pointer-events/pointer-events'), + data: { + credits: { + title: 'Pointer Events', + link: 'https://docs.tresjs.org/api/events.html', + class: 'text-white', + }, + }, + }, + { + path: 'webgpu-renderer', + loadComponent: () => import('./webgpu-renderer/webgpu-renderer'), + data: { + credits: { + title: "Threlte's WebGPU Renderer example", + link: 'https://threlte.xyz/docs/learn/advanced/webgpu', + }, + }, + }, + { + path: 'webgpu-tsl', + loadComponent: () => import('./webgpu-tsl/webgpu-tsl'), + data: { + credits: { + title: 'THREE.js TSL Angular Slicing', + link: 'https://threejs.org/examples/?q=tsl#webgpu_tsl_angular_slicing', + }, + }, + }, + { + path: 'particle-maxime', + loadComponent: () => import('./particle-maxime/particle-maxime'), + data: { + credits: { + title: 'The magical world of particles with React Three Fiber and shaders', + link: 'https://blog.maximeheckel.com/posts/the-magical-world-of-particles-with-react-three-fiber-and-shaders/', + class: 'text-white', + }, + }, + }, + { + path: '', + redirectTo: 'basic', + pathMatch: 'full', + }, +]; + +export default routes; diff --git a/apps/kitchen-sink/src/app/misc/misc.ts b/apps/examples/src/app/misc/misc.ts similarity index 89% rename from apps/kitchen-sink/src/app/misc/misc.ts rename to apps/examples/src/app/misc/misc.ts index 68015859..526706c7 100644 --- a/apps/kitchen-sink/src/app/misc/misc.ts +++ b/apps/examples/src/app/misc/misc.ts @@ -1,11 +1,7 @@ import { ChangeDetectionStrategy, Component } from '@angular/core'; import { RouterLink, RouterLinkActive, RouterOutlet } from '@angular/router'; -import { extend } from 'angular-three'; -import * as THREE from 'three'; import routes from './misc.routes'; -extend(THREE); - @Component({ template: `
@@ -32,6 +28,6 @@ extend(THREE); changeDetection: ChangeDetectionStrategy.OnPush, host: { class: 'misc' }, }) -export default class Misc { +export default class Soba { protected examples = routes.filter((route) => !!route.path).map((route) => route.path); } diff --git a/apps/examples/src/app/misc/particle-maxime/fragment.glsl b/apps/examples/src/app/misc/particle-maxime/fragment.glsl new file mode 100644 index 00000000..050628fc --- /dev/null +++ b/apps/examples/src/app/misc/particle-maxime/fragment.glsl @@ -0,0 +1,25 @@ +varying vec3 vPosition; + +const vec3 COLOR_1 = vec3(0.894, 0.0, 0.208); // #E40035 +const vec3 COLOR_2 = vec3(0.965, 0.039, 0.282); // #F60A48 +const vec3 COLOR_3 = vec3(0.949, 0.027, 0.333); // #F20755 +const vec3 COLOR_4 = vec3(0.863, 0.031, 0.490); // #DC087D +const vec3 COLOR_5 = vec3(0.592, 0.090, 0.906); // #9717E7 +const vec3 COLOR_6 = vec3(0.424, 0.0, 0.961); // #6C00F5 + +const float STEP_1 = 0.24; +const float STEP_2 = 0.352; +const float STEP_3 = 0.494; +const float STEP_4 = 0.745; + +void main() { + float gradientPos = (vPosition.y + 1.0) * 0.5; + + vec3 color = gradientPos < STEP_1 ? mix(COLOR_1, COLOR_2, gradientPos / STEP_1) : + gradientPos < STEP_2 ? mix(COLOR_2, COLOR_3, (gradientPos - STEP_1) / (STEP_2 - STEP_1)) : + gradientPos < STEP_3 ? mix(COLOR_3, COLOR_4, (gradientPos - STEP_2) / (STEP_3 - STEP_2)) : + gradientPos < STEP_4 ? mix(COLOR_4, COLOR_5, (gradientPos - STEP_3) / (STEP_4 - STEP_3)) : + mix(COLOR_5, COLOR_6, (gradientPos - STEP_4) / (1.0 - STEP_4)); + + gl_FragColor = vec4(color, 1.0); +} diff --git a/apps/examples/src/app/misc/particle-maxime/particle-maxime.ts b/apps/examples/src/app/misc/particle-maxime/particle-maxime.ts new file mode 100644 index 00000000..4bf38345 --- /dev/null +++ b/apps/examples/src/app/misc/particle-maxime/particle-maxime.ts @@ -0,0 +1,15 @@ +import { ChangeDetectionStrategy, Component } from '@angular/core'; +import { NgtCanvas } from 'angular-three/dom'; +import { SceneGraph } from './scene'; + +@Component({ + template: ` + + + + `, + imports: [NgtCanvas, SceneGraph], + changeDetection: ChangeDetectionStrategy.OnPush, + host: { class: 'particle-maxime' }, +}) +export default class ParticleMaxime {} diff --git a/apps/examples/src/app/misc/particle-maxime/scene.ts b/apps/examples/src/app/misc/particle-maxime/scene.ts new file mode 100644 index 00000000..652c380d --- /dev/null +++ b/apps/examples/src/app/misc/particle-maxime/scene.ts @@ -0,0 +1,184 @@ +import { + ChangeDetectionStrategy, + Component, + CUSTOM_ELEMENTS_SCHEMA, + ElementRef, + input, + signal, + viewChild, +} from '@angular/core'; +import { beforeRender, extend, NgtArgs, NgtPortal } from 'angular-three'; +import { NgtpBloom, NgtpEffectComposer } from 'angular-three-postprocessing'; +import { NgtsPerspectiveCamera } from 'angular-three-soba/cameras'; +import { NgtsOrbitControls } from 'angular-three-soba/controls'; +import { fbo } from 'angular-three-soba/misc'; +import * as THREE from 'three'; + +import { SimulationMaterial } from './simulation-material'; + +import { TweakpaneNumber, TweakpanePane } from 'angular-three-tweakpane'; +import fragmentShader from './fragment.glsl' with { loader: 'text' }; +import vertexShader from './vertex.glsl' with { loader: 'text' }; + +extend({ SimulationMaterial }); + +@Component({ + selector: 'app-fbo-particles', + template: ` + + + + + + + + + + + + + + + + + + + `, + imports: [NgtArgs, NgtPortal], + schemas: [CUSTOM_ELEMENTS_SCHEMA], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class FBOParticles { + protected readonly Math = Math; + protected readonly vertexShader = vertexShader; + protected readonly fragmentShader = fragmentShader; + protected readonly AdditiveBlending = THREE.AdditiveBlending; + + frequency = input.required(); + timeScale = input.required(); + + protected size = 128; + protected virtualScene = new THREE.Scene(); + protected virtualCamera = new THREE.OrthographicCamera(-1, 1, 1, -1, 1 / Math.pow(2, 53), 1); + protected positions = new Float32Array([-1, -1, 0, 1, -1, 0, 1, 1, 0, -1, -1, 0, 1, 1, 0, -1, 1, 0]); + protected uvs = new Float32Array([ + 0, + 0, // bottom-left + 1, + 0, // bottom-right + 1, + 1, // top-right + 0, + 0, // bottom-left + 1, + 1, // top-right + 0, + 1, // top-left + ]); + private renderTarget = fbo(() => ({ + width: this.size, + height: this.size, + settings: { + minFilter: THREE.NearestFilter, + magFilter: THREE.NearestFilter, + format: THREE.RGBAFormat, + stencilBuffer: false, + type: THREE.FloatType, + }, + })); + + protected particlePositions = (() => { + const length = this.size * this.size; + const particles = new Float32Array(length * 3); + for (let i = 0; i < length; i++) { + let i3 = i * 3; + particles[i3 + 0] = (i % this.size) / this.size; + particles[i3 + 1] = i / this.size / this.size; + } + return particles; + })(); + + protected uniforms: Record = { + uPositions: { value: null }, + }; + + private simulationMaterialRef = viewChild>('simulationMaterial'); + + constructor() { + beforeRender(({ gl, clock }) => { + gl.setRenderTarget(this.renderTarget); + gl.clear(); + gl.render(this.virtualScene, this.virtualCamera); + gl.setRenderTarget(null); + + const simulationMaterial = this.simulationMaterialRef()?.nativeElement; + if (!simulationMaterial) return; + + this.uniforms['uPositions'].value = this.renderTarget.texture; + simulationMaterial.uniforms['uFrequency'].value = this.frequency(); + simulationMaterial.uniforms['uTime'].value = clock.elapsedTime * this.timeScale(); + }); + } +} + +@Component({ + selector: 'app-scene-graph', + template: ` + + + + + + + + + + + + + + + + + + `, + imports: [ + NgtsPerspectiveCamera, + NgtsOrbitControls, + NgtArgs, + FBOParticles, + NgtpEffectComposer, + NgtpBloom, + TweakpanePane, + TweakpaneNumber, + ], + changeDetection: ChangeDetectionStrategy.OnPush, + schemas: [CUSTOM_ELEMENTS_SCHEMA], +}) +export class SceneGraph { + protected readonly Math = Math; + + protected frequency = signal(0.5); + protected timeScale = signal(1); +} diff --git a/apps/examples/src/app/misc/particle-maxime/shapes.json b/apps/examples/src/app/misc/particle-maxime/shapes.json new file mode 100644 index 00000000..e6b23e65 --- /dev/null +++ b/apps/examples/src/app/misc/particle-maxime/shapes.json @@ -0,0 +1,253 @@ +{ + "curve": { + "metadata": { + "version": 4.6, + "type": "BufferGeometry", + "generator": "BufferGeometry.toJSON" + }, + "uuid": "53489895-da59-4d5d-9a40-eee5e2e229c9", + "type": "BufferGeometry", + "data": { + "attributes": { + "position": { + "itemSize": 3, + "type": "Float32Array", + "array": [ + 0.22693952918052673, 0.029999999329447746, -0.09325665235519409, 0.15878146886825562, + 0.029999999329447746, -0.23989956080913544, 0.23402345180511475, 0.029999999329447746, + -0.20510819554328918, 0.22693952918052673, -0.029999999329447746, -0.09325665235519409, + 0.15878146886825562, -0.029999999329447746, -0.23989956080913544, 0.23402345180511475, + -0.029999999329447746, -0.20510819554328918, 0.22693952918052673, -0.029999999329447746, + -0.09325665235519409, 0.22693952918052673, -0.029999999329447746, -0.09325665235519409, + 0.22693952918052673, 0.029999999329447746, -0.09325665235519409, 0.22693952918052673, + 0.029999999329447746, -0.09325665235519409, 0.15878146886825562, -0.029999999329447746, + -0.23989956080913544, 0.15878146886825562, 0.029999999329447746, -0.23989956080913544, + 0.23402345180511475, -0.029999999329447746, -0.20510819554328918, 0.23402345180511475, + 0.029999999329447746, -0.20510819554328918 + ], + "normalized": false + }, + "normal": { + "itemSize": 3, + "type": "Float32Array", + "array": [ + 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, -1, 0, 0, -1, 0, 0, -1, 0, 0.18479567766189575, 0, + 0.9827770590782166, 0.18479567766189575, 0, 0.9827770590782166, 0.18479567766189575, 0, + 0.9827770590782166, 0.18479567766189575, 0, 0.9827770590782166, -0.707806408405304, 0, + -0.7064064145088196, -0.707806408405304, 0, -0.7064064145088196, 0.8591474294662476, 0, + -0.5117282271385193, 0.8591474294662476, 0, -0.5117282271385193 + ], + "normalized": false + }, + "uv": { + "itemSize": 2, + "type": "Float32Array", + "array": [ + 0, 1, 0.5, 1, 1, 1, 0, 1, 0.5, 1, 1, 1, 0, 1, 1, 1, 0, 0, 1, 0, 0.3333333432674408, 1, + 0.3333333432674408, 0, 0.6666666865348816, 1, 0.6666666865348816, 0 + ], + "normalized": false + } + }, + "index": { + "type": "Uint16Array", + "array": [0, 2, 1, 3, 4, 5, 8, 11, 10, 8, 10, 6, 11, 13, 12, 11, 12, 10, 13, 9, 7, 13, 7, 12] + }, + "boundingSphere": { + "center": [0.19640246033668518, 0, -0.16657810658216476], + "radius": 0.08770048237491211 + } + } + }, + "curveTwo": { + "metadata": { + "version": 4.6, + "type": "BufferGeometry", + "generator": "BufferGeometry.toJSON" + }, + "uuid": "2ba02139-accd-4e26-bbd0-b52206ce8207", + "type": "BufferGeometry", + "data": { + "attributes": { + "position": { + "itemSize": 3, + "type": "Float32Array", + "array": [ + 0.13535654544830322, 0.029999999329447746, -0.031094953417778015, 0.08387815952301025, + 0.029999999329447746, -0.060440465807914734, 0.09434881806373596, 0.029999999329447746, + -0.08580739796161652, 0.17636427283287048, 0.029999999329447746, -0.08580739796161652, + 0.1868349313735962, 0.029999999329447746, -0.060440465807914734, 0.13535654544830322, + -0.029999999329447746, -0.031094953417778015, 0.08387815952301025, -0.029999999329447746, + -0.060440465807914734, 0.09434881806373596, -0.029999999329447746, -0.08580739796161652, + 0.17636427283287048, -0.029999999329447746, -0.08580739796161652, 0.1868349313735962, + -0.029999999329447746, -0.060440465807914734, 0.13535654544830322, -0.029999999329447746, + -0.031094953417778015, 0.13535654544830322, -0.029999999329447746, -0.031094953417778015, + 0.13535654544830322, 0.029999999329447746, -0.031094953417778015, 0.13535654544830322, + 0.029999999329447746, -0.031094953417778015, 0.08387815952301025, -0.029999999329447746, + -0.060440465807914734, 0.08387815952301025, 0.029999999329447746, -0.060440465807914734, + 0.09434881806373596, -0.029999999329447746, -0.08580739796161652, 0.09434881806373596, + 0.029999999329447746, -0.08580739796161652, 0.17636427283287048, -0.029999999329447746, + -0.08580739796161652, 0.17636427283287048, 0.029999999329447746, -0.08580739796161652, + 0.1868349313735962, -0.029999999329447746, -0.060440465807914734, 0.1868349313735962, + 0.029999999329447746, -0.060440465807914734 + ], + "normalized": false + }, + "normal": { + "itemSize": 3, + "type": "Float32Array", + "array": [ + 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, -1, 0, 0, -1, 0, 0, -1, 0, 0, -1, 0, 0, -1, 0, + 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, -0.9458460211753845, 0, 0.32461580634117126, + -0.9458460211753845, 0, 0.32461580634117126, -0.5561071634292603, 0, -0.8311106562614441, + -0.5561071634292603, 0, -0.8311106562614441, 0.5561071634292603, 0, -0.8311106562614441, + 0.5561071634292603, 0, -0.8311106562614441, 0.9458460211753845, 0, 0.32461580634117126, + 0.9458460211753845, 0, 0.32461580634117126 + ], + "normalized": false + }, + "uv": { + "itemSize": 2, + "type": "Float32Array", + "array": [ + 0, 1, 0.25, 1, 0.5, 1, 0.75, 1, 1, 1, 0, 1, 0.25, 1, 0.5, 1, 0.75, 1, 1, 1, 0, 1, 1, 1, 0, 0, 1, + 0, 0.20000000298023224, 1, 0.20000000298023224, 0, 0.4000000059604645, 1, 0.4000000059604645, 0, + 0.6000000238418579, 1, 0.6000000238418579, 0, 0.800000011920929, 1, 0.800000011920929, 0 + ], + "normalized": false + } + }, + "index": { + "type": "Uint16Array", + "array": [ + 1, 3, 2, 1, 4, 3, 0, 4, 1, 6, 7, 8, 6, 8, 9, 5, 6, 9, 12, 15, 14, 12, 14, 10, 15, 17, 16, 15, 16, + 14, 17, 19, 18, 17, 18, 16, 19, 21, 20, 19, 20, 18, 21, 13, 11, 21, 11, 20 + ] + }, + "boundingSphere": { + "center": [0.13535654544830322, 0, -0.058451175689697266], + "radius": 0.06556208564582824 + } + } + }, + "curveThree": { + "metadata": { + "version": 4.6, + "type": "BufferGeometry", + "generator": "BufferGeometry.toJSON" + }, + "uuid": "080fe7a1-c564-47c5-ab29-a7b2e43ad705", + "type": "BufferGeometry", + "data": { + "attributes": { + "position": { + "itemSize": 3, + "type": "Float32Array", + "array": [ + 0.1623375415802002, 0.029999999329447746, -0.11870823800563812, 0.1084037721157074, + 0.029999999329447746, -0.11870823800563812, 0.13535654544830322, 0.029999999329447746, + -0.1842559576034546, 0.1623375415802002, -0.029999999329447746, -0.11870823800563812, + 0.1084037721157074, -0.029999999329447746, -0.11870823800563812, 0.13535654544830322, + -0.029999999329447746, -0.1842559576034546, 0.1623375415802002, -0.029999999329447746, + -0.11870823800563812, 0.1623375415802002, -0.029999999329447746, -0.11870823800563812, + 0.1623375415802002, 0.029999999329447746, -0.11870823800563812, 0.1623375415802002, + 0.029999999329447746, -0.11870823800563812, 0.1084037721157074, -0.029999999329447746, + -0.11870823800563812, 0.1084037721157074, 0.029999999329447746, -0.11870823800563812, + 0.13535654544830322, -0.029999999329447746, -0.1842559576034546, 0.13535654544830322, + 0.029999999329447746, -0.1842559576034546 + ], + "normalized": false + }, + "normal": { + "itemSize": 3, + "type": "Float32Array", + "array": [ + 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, -1, 0, 0, -1, 0, 0, -1, 0, 0.8308638334274292, 0, + 0.556475818157196, 0.8308638334274292, 0, 0.556475818157196, 0.8308638334274292, 0, + 0.556475818157196, 0.8308638334274292, 0, 0.556475818157196, -0.8307866454124451, 0, + 0.5565910339355469, -0.8307866454124451, 0, 0.5565910339355469, -0.00019999999494757503, 0, -1, + -0.00019999999494757503, 0, -1 + ], + "normalized": false + }, + "uv": { + "itemSize": 2, + "type": "Float32Array", + "array": [ + 0, 1, 0.5, 1, 1, 1, 0, 1, 0.5, 1, 1, 1, 0, 1, 1, 1, 0, 0, 1, 0, 0.3333333432674408, 1, + 0.3333333432674408, 0, 0.6666666865348816, 1, 0.6666666865348816, 0 + ], + "normalized": false + } + }, + "index": { + "type": "Uint16Array", + "array": [1, 0, 2, 4, 5, 3, 8, 11, 10, 8, 10, 6, 11, 13, 12, 11, 12, 10, 13, 9, 7, 13, 7, 12] + }, + "boundingSphere": { + "center": [0.1353706568479538, 0, -0.15148209780454636], + "radius": 0.051974404451110574 + } + } + }, + "curveFour": { + "metadata": { + "version": 4.6, + "type": "BufferGeometry", + "generator": "BufferGeometry.toJSON" + }, + "uuid": "b12f976f-1e75-47e5-bf28-50eab4321455", + "type": "BufferGeometry", + "data": { + "attributes": { + "position": { + "itemSize": 3, + "type": "Float32Array", + "array": [ + 0.0366896390914917, 0.029999999329447746, -0.20510819554328918, 0.11193162202835083, + 0.029999999329447746, -0.23989956080913544, 0.04377356171607971, 0.029999999329447746, + -0.09325665235519409, 0.0366896390914917, -0.029999999329447746, -0.20510819554328918, + 0.11193162202835083, -0.029999999329447746, -0.23989956080913544, 0.04377356171607971, + -0.029999999329447746, -0.09325665235519409, 0.0366896390914917, -0.029999999329447746, + -0.20510819554328918, 0.0366896390914917, -0.029999999329447746, -0.20510819554328918, + 0.0366896390914917, 0.029999999329447746, -0.20510819554328918, 0.0366896390914917, + 0.029999999329447746, -0.20510819554328918, 0.11193162202835083, -0.029999999329447746, + -0.23989956080913544, 0.11193162202835083, 0.029999999329447746, -0.23989956080913544, + 0.04377356171607971, -0.029999999329447746, -0.09325665235519409, 0.04377356171607971, + 0.029999999329447746, -0.09325665235519409 + ], + "normalized": false + }, + "normal": { + "itemSize": 3, + "type": "Float32Array", + "array": [ + 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, -1, 0, 0, -1, 0, 0, -1, 0, -0.8591474294662476, 0, + -0.5117282271385193, -0.8591474294662476, 0, -0.5117282271385193, -0.8591474294662476, 0, + -0.5117282271385193, -0.8591474294662476, 0, -0.5117282271385193, 0.707806408405304, 0, + -0.7064064145088196, 0.707806408405304, 0, -0.7064064145088196, -0.18479567766189575, 0, + 0.9827770590782166, -0.18479567766189575, 0, 0.9827770590782166 + ], + "normalized": false + }, + "uv": { + "itemSize": 2, + "type": "Float32Array", + "array": [ + 0, 1, 0.5, 1, 1, 1, 0, 1, 0.5, 1, 1, 1, 0, 1, 1, 1, 0, 0, 1, 0, 0.3333333432674408, 1, + 0.3333333432674408, 0, 0.6666666865348816, 1, 0.6666666865348816, 0 + ], + "normalized": false + } + }, + "index": { + "type": "Uint16Array", + "array": [0, 2, 1, 3, 4, 5, 8, 11, 10, 8, 10, 6, 11, 13, 12, 11, 12, 10, 13, 9, 7, 13, 7, 12] + }, + "boundingSphere": { + "center": [0.07431063055992126, 0, -0.16657810658216476], + "radius": 0.08770048237491211 + } + } + } +} diff --git a/apps/examples/src/app/misc/particle-maxime/simulation-fragment.glsl b/apps/examples/src/app/misc/particle-maxime/simulation-fragment.glsl new file mode 100644 index 00000000..6db5bffe --- /dev/null +++ b/apps/examples/src/app/misc/particle-maxime/simulation-fragment.glsl @@ -0,0 +1,160 @@ +uniform sampler2D positions; +uniform float uTime; +uniform float uFrequency; + +varying vec2 vUv; + +// Source: https://github.com/drcmda/glsl-curl-noise2 +// and: https://github.com/guoweish/glsl-noise-simplex/blob/master/3d.glsl + +// +// Description : Array and textureless GLSL 2D/3D/4D simplex +// noise functions. +// Author : Ian McEwan, Ashima Arts. +// Maintainer : ijm +// Lastmod : 20110822 (ijm) +// License : Copyright (C) 2011 Ashima Arts. All rights reserved. +// Distributed under the MIT License. See LICENSE file. +// https://github.com/ashima/webgl-noise +// + +vec3 mod289(vec3 x) { + return x - floor(x * (1.0 / 289.0)) * 289.0; +} + +vec4 mod289(vec4 x) { + return x - floor(x * (1.0 / 289.0)) * 289.0; +} + +vec4 permute(vec4 x) { + return mod289(((x * 34.0) + 1.0) * x); +} + +vec4 taylorInvSqrt(vec4 r) +{ + return 1.79284291400159 - 0.85373472095314 * r; +} + +float snoise(vec3 v) +{ + const vec2 C = vec2(1.0 / 6.0, 1.0 / 3.0); + const vec4 D = vec4(0.0, 0.5, 1.0, 2.0); + + // First corner + vec3 i = floor(v + dot(v, C.yyy)); + vec3 x0 = v - i + dot(i, C.xxx); + + // Other corners + vec3 g = step(x0.yzx, x0.xyz); + vec3 l = 1.0 - g; + vec3 i1 = min(g.xyz, l.zxy); + vec3 i2 = max(g.xyz, l.zxy); + + // x0 = x0 - 0.0 + 0.0 * C.xxx; + // x1 = x0 - i1 + 1.0 * C.xxx; + // x2 = x0 - i2 + 2.0 * C.xxx; + // x3 = x0 - 1.0 + 3.0 * C.xxx; + vec3 x1 = x0 - i1 + C.xxx; + vec3 x2 = x0 - i2 + C.yyy; // 2.0*C.x = 1/3 = C.y + vec3 x3 = x0 - D.yyy; // -1.0+3.0*C.x = -0.5 = -D.y + + // Permutations + i = mod289(i); + vec4 p = permute(permute(permute( + i.z + vec4(0.0, i1.z, i2.z, 1.0)) + + i.y + vec4(0.0, i1.y, i2.y, 1.0)) + + i.x + vec4(0.0, i1.x, i2.x, 1.0)); + + // Gradients: 7x7 points over a square, mapped onto an octahedron. + // The ring size 17*17 = 289 is close to a multiple of 49 (49*6 = 294) + float n_ = 0.142857142857; // 1.0/7.0 + vec3 ns = n_ * D.wyz - D.xzx; + + vec4 j = p - 49.0 * floor(p * ns.z * ns.z); // mod(p,7*7) + + vec4 x_ = floor(j * ns.z); + vec4 y_ = floor(j - 7.0 * x_); // mod(j,N) + + vec4 x = x_ * ns.x + ns.yyyy; + vec4 y = y_ * ns.x + ns.yyyy; + vec4 h = 1.0 - abs(x) - abs(y); + + vec4 b0 = vec4(x.xy, y.xy); + vec4 b1 = vec4(x.zw, y.zw); + + //vec4 s0 = vec4(lessThan(b0,0.0))*2.0 - 1.0; + //vec4 s1 = vec4(lessThan(b1,0.0))*2.0 - 1.0; + vec4 s0 = floor(b0) * 2.0 + 1.0; + vec4 s1 = floor(b1) * 2.0 + 1.0; + vec4 sh = -step(h, vec4(0.0)); + + vec4 a0 = b0.xzyw + s0.xzyw * sh.xxyy; + vec4 a1 = b1.xzyw + s1.xzyw * sh.zzww; + + vec3 p0 = vec3(a0.xy, h.x); + vec3 p1 = vec3(a0.zw, h.y); + vec3 p2 = vec3(a1.xy, h.z); + vec3 p3 = vec3(a1.zw, h.w); + + //Normalise gradients + vec4 norm = taylorInvSqrt(vec4(dot(p0, p0), dot(p1, p1), dot(p2, p2), dot(p3, p3))); + p0 *= norm.x; + p1 *= norm.y; + p2 *= norm.z; + p3 *= norm.w; + + // Mix final noise value + vec4 m = max(0.6 - vec4(dot(x0, x0), dot(x1, x1), dot(x2, x2), dot(x3, x3)), 0.0); + m = m * m; + return 42.0 * dot(m * m, vec4(dot(p0, x0), dot(p1, x1), + dot(p2, x2), dot(p3, x3))); +} + +vec3 snoiseVec3(vec3 x) { + float s = snoise(vec3(x)); + float s1 = snoise(vec3(x.y - 19.1, x.z + 33.4, x.x + 47.2)); + float s2 = snoise(vec3(x.z + 74.2, x.x - 124.5, x.y + 99.4)); + vec3 c = vec3(s, s1, s2); + return c; +} + +vec3 curlNoise(vec3 p) { + const float e = .1; + vec3 dx = vec3(e, 0.0, 0.0); + vec3 dy = vec3(0.0, e, 0.0); + vec3 dz = vec3(0.0, 0.0, e); + + vec3 p_x0 = snoiseVec3(p - dx); + vec3 p_x1 = snoiseVec3(p + dx); + vec3 p_y0 = snoiseVec3(p - dy); + vec3 p_y1 = snoiseVec3(p + dy); + vec3 p_z0 = snoiseVec3(p - dz); + vec3 p_z1 = snoiseVec3(p + dz); + + float x = p_y1.z - p_y0.z - p_z1.y + p_z0.y; + float y = p_z1.x - p_z0.x - p_x1.z + p_x0.z; + float z = p_x1.y - p_x0.y - p_y1.x + p_y0.x; + + const float divisor = 1.0 / (2.0 * e); + return normalize(vec3(x, y, z) * divisor); +} + +void main() { + vec3 pos = texture2D(positions, vUv).rgb; + vec3 curlPos = texture2D(positions, vUv).rgb; + + vec3 originalPos = pos; + vec3 noise = curlNoise(pos * uFrequency + uTime * 0.1); + + // Calculate pulsing mix factor (0 to 1) + float pulseSpeed = 0.5; + float mixFactor = (sin(uTime * pulseSpeed) + 1.0) * 0.5; + + // Mix between original position and noise with pulsing effect + pos = mix(originalPos, originalPos + noise * 0.8, mixFactor); + + // Add secondary motion that also pulses + pos += curlNoise(pos * uFrequency * 3.0) * 0.2 * mixFactor; + + gl_FragColor = vec4(pos, 1.0); +} diff --git a/apps/examples/src/app/misc/particle-maxime/simulation-material.ts b/apps/examples/src/app/misc/particle-maxime/simulation-material.ts new file mode 100644 index 00000000..e218bc86 --- /dev/null +++ b/apps/examples/src/app/misc/particle-maxime/simulation-material.ts @@ -0,0 +1,91 @@ +import simulationFragmentShader from './simulation-fragment.glsl' with { loader: 'text' }; +import simulationVertexShader from './simulation-vertex.glsl' with { loader: 'text' }; + +import shapes from './shapes.json'; + +import * as THREE from 'three'; + +function generateRandomPointInTriangle(v1: THREE.Vector3, v2: THREE.Vector3, v3: THREE.Vector3) { + const r1 = Math.random(); + const r2 = Math.random(); + const r3 = Math.random(); + const sum = r1 + r2 + r3; + + return new THREE.Vector3() + .addScaledVector(v1, r1 / sum) + .addScaledVector(v2, r2 / sum) + .addScaledVector(v3, r3 / sum); +} + +function createGeometryFromCurve(curveData: (typeof shapes)['curve']) { + const geometry = new THREE.BufferGeometry(); + geometry.setAttribute('position', new THREE.Float32BufferAttribute(curveData.data.attributes.position.array, 3)); + geometry.setAttribute('normal', new THREE.Float32BufferAttribute(curveData.data.attributes.normal.array, 3)); + geometry.setAttribute('uv', new THREE.Float32BufferAttribute(curveData.data.attributes.uv.array, 2)); + geometry.setIndex(new THREE.Uint16BufferAttribute(curveData.data.index.array, 1)); + return geometry; +} + +function getRandomData(width: number, height: number) { + const length = width * height * 4; + const data = new Float32Array(length); + const curves = [shapes.curve, shapes.curveTwo, shapes.curveThree, shapes.curveFour]; + + const scale = 8; + const baseRotation = new THREE.Euler(Math.PI / 2, 0, 0); + const positions = Array(4).fill(new THREE.Vector3(0, 0, 0)); + + for (let i = 0; i < length; i += 4) { + const shapeIndex = Math.floor((i / length) * curves.length); + const curveData = curves[shapeIndex]; + + const matrix = new THREE.Matrix4() + .makeRotationFromEuler(baseRotation) + .setPosition(positions[shapeIndex]) + .scale(new THREE.Vector3(scale, scale, scale)); + + const geometry = createGeometryFromCurve(curveData); + const positionsArr = geometry.attributes['position'].array; + const indices = curveData.data.index.array; + + const triangleIndex = Math.floor(Math.random() * (indices.length / 3)) * 3; + + const vertices = [0, 1, 2].map((offset) => { + const idx = indices[triangleIndex + offset] * 3; + return new THREE.Vector3(positionsArr[idx], positionsArr[idx + 1], positionsArr[idx + 2]); + }) as [THREE.Vector3, THREE.Vector3, THREE.Vector3]; + + const pos = generateRandomPointInTriangle(...vertices); + pos.applyMatrix4(matrix); + + data[i] = pos.x; + data[i + 1] = pos.y; + data[i + 2] = pos.z; + data[i + 3] = 1.0; + } + + return data; +} + +export class SimulationMaterial extends THREE.ShaderMaterial { + constructor(size: number) { + const positionsTexture = new THREE.DataTexture( + getRandomData(size, size), + size, + size, + THREE.RGBAFormat, + THREE.FloatType, + ); + positionsTexture.needsUpdate = true; + + super({ + uniforms: { + positions: { value: positionsTexture }, + uFrequency: { value: 0.25 }, + uTime: { value: 0 }, + }, + vertexShader: simulationVertexShader, + fragmentShader: simulationFragmentShader, + }); + } +} diff --git a/apps/examples/src/app/misc/particle-maxime/simulation-vertex.glsl b/apps/examples/src/app/misc/particle-maxime/simulation-vertex.glsl new file mode 100644 index 00000000..940a3e96 --- /dev/null +++ b/apps/examples/src/app/misc/particle-maxime/simulation-vertex.glsl @@ -0,0 +1,11 @@ +varying vec2 vUv; + +void main() { + vUv = uv; + + vec4 modelPosition = modelMatrix * vec4(position, 1.0); + vec4 viewPosition = viewMatrix * modelPosition; + vec4 projectedPosition = projectionMatrix * viewPosition; + + gl_Position = projectedPosition; +} diff --git a/apps/examples/src/app/misc/particle-maxime/vertex.glsl b/apps/examples/src/app/misc/particle-maxime/vertex.glsl new file mode 100644 index 00000000..e848eb91 --- /dev/null +++ b/apps/examples/src/app/misc/particle-maxime/vertex.glsl @@ -0,0 +1,17 @@ +uniform sampler2D uPositions; +varying vec3 vPosition; + +void main() { + vec3 pos = texture2D(uPositions, position.xy).xyz; + vPosition = pos; + + vec4 modelPosition = modelMatrix * vec4(pos, 1.0); + vec4 viewPosition = viewMatrix * modelPosition; + vec4 projectedPosition = projectionMatrix * viewPosition; + + gl_Position = projectedPosition; + + gl_PointSize = 3.0; + // Size attenuation; + gl_PointSize *= step(1.0 - (1.0 / 64.0), position.x) + 0.5; +} diff --git a/apps/examples/src/app/misc/pointer-events/pointer-events.ts b/apps/examples/src/app/misc/pointer-events/pointer-events.ts new file mode 100644 index 00000000..7b3d6c69 --- /dev/null +++ b/apps/examples/src/app/misc/pointer-events/pointer-events.ts @@ -0,0 +1,15 @@ +import { ChangeDetectionStrategy, Component } from '@angular/core'; +import { NgtCanvas } from 'angular-three/dom'; +import { SceneGraph } from './scene'; + +@Component({ + template: ` + + + + `, + changeDetection: ChangeDetectionStrategy.OnPush, + host: { class: 'pointer-events' }, + imports: [NgtCanvas, SceneGraph], +}) +export default class PointerEvents {} diff --git a/apps/examples/src/app/misc/pointer-events/scene.ts b/apps/examples/src/app/misc/pointer-events/scene.ts new file mode 100644 index 00000000..91bacc0f --- /dev/null +++ b/apps/examples/src/app/misc/pointer-events/scene.ts @@ -0,0 +1,57 @@ +import { ChangeDetectionStrategy, Component, CUSTOM_ELEMENTS_SCHEMA, ElementRef, viewChild } from '@angular/core'; +import { beforeRender, NgtArgs, NgtThreeElements } from 'angular-three'; +import { NgtsOrbitControls } from 'angular-three-soba/controls'; +import * as THREE from 'three'; + +@Component({ + selector: 'app-scene-graph', + template: ` + + + + + + + @for (x of positions; track $index) { + @for (y of positions; track $index) { + @for (z of positions; track $index) { + + + + + } + } + } + + + + `, + imports: [NgtsOrbitControls, NgtArgs], + schemas: [CUSTOM_ELEMENTS_SCHEMA], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class SceneGraph { + protected readonly Math = Math; + + private groupRef = viewChild.required>('group'); + + protected readonly positions = [-2.5, 0, 2.5]; + + constructor() { + beforeRender(({ delta }) => { + this.groupRef().nativeElement.rotation.y += delta * 0.25; + }); + } + + onPointerEnter(material: NgtThreeElements['ngt-mesh-standard-material']) { + (material as THREE.MeshStandardMaterial).color.set('mediumpurple'); + } + + onPointerLeave(material: NgtThreeElements['ngt-mesh-standard-material']) { + (material as THREE.MeshStandardMaterial).color.set('#efefef'); + } +} diff --git a/apps/kitchen-sink/src/app/misc/svg-renderer/experience.ts b/apps/examples/src/app/misc/svg-renderer/scene.ts similarity index 90% rename from apps/kitchen-sink/src/app/misc/svg-renderer/experience.ts rename to apps/examples/src/app/misc/svg-renderer/scene.ts index 01446859..7408cc2f 100644 --- a/apps/kitchen-sink/src/app/misc/svg-renderer/experience.ts +++ b/apps/examples/src/app/misc/svg-renderer/scene.ts @@ -1,8 +1,9 @@ import { ChangeDetectionStrategy, Component, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'; -import { injectBeforeRender, NgtArgs } from 'angular-three'; +import { beforeRender, NgtArgs } from 'angular-three'; import { BufferGeometry, Float32BufferAttribute } from 'three'; @Component({ + selector: 'app-scene-graph', template: ` @@ -20,7 +21,7 @@ import { BufferGeometry, Float32BufferAttribute } from 'three'; changeDetection: ChangeDetectionStrategy.OnPush, imports: [NgtArgs], }) -export class Experience { +export class SceneGraph { protected readonly Math = Math; protected whiteHex = 0xffffff; @@ -44,7 +45,7 @@ export class Experience { })(); constructor() { - injectBeforeRender(({ scene }) => { + beforeRender(({ scene }) => { let count = 0; const time = performance.now() / 1000; scene.traverse((child) => { diff --git a/apps/kitchen-sink/src/app/misc/svg-renderer/svg-renderer.ts b/apps/examples/src/app/misc/svg-renderer/svg-renderer.ts similarity index 53% rename from apps/kitchen-sink/src/app/misc/svg-renderer/svg-renderer.ts rename to apps/examples/src/app/misc/svg-renderer/svg-renderer.ts index 64576c87..eada2824 100644 --- a/apps/kitchen-sink/src/app/misc/svg-renderer/svg-renderer.ts +++ b/apps/examples/src/app/misc/svg-renderer/svg-renderer.ts @@ -1,23 +1,20 @@ import { ChangeDetectionStrategy, Component } from '@angular/core'; -import { NgtCanvas, NgtCanvasElement } from 'angular-three'; +import { NgtGLOptions } from 'angular-three'; +import { NgtCanvas } from 'angular-three/dom'; import { SVGRenderer } from 'three-stdlib'; -import { Experience } from './experience'; +import { SceneGraph } from './scene'; @Component({ template: ` - + + + `, - imports: [NgtCanvas], + imports: [NgtCanvas, SceneGraph], changeDetection: ChangeDetectionStrategy.OnPush, }) export default class SVGRendererExample { - sceneGraph = Experience; - - svgRendererFactory = (canvas: NgtCanvasElement) => { + svgRendererFactory: NgtGLOptions = ({ canvas }) => { const renderer = new SVGRenderer(); if (canvas instanceof HTMLCanvasElement) { diff --git a/apps/examples/src/app/misc/webgpu-renderer/scene.ts b/apps/examples/src/app/misc/webgpu-renderer/scene.ts new file mode 100644 index 00000000..3688cba4 --- /dev/null +++ b/apps/examples/src/app/misc/webgpu-renderer/scene.ts @@ -0,0 +1,125 @@ +import { + ChangeDetectionStrategy, + Component, + CUSTOM_ELEMENTS_SCHEMA, + DestroyRef, + ElementRef, + inject, + viewChild, +} from '@angular/core'; +import { beforeRender, extend, NgtAfterAttach, NgtArgs } from 'angular-three'; +import { NgtsOrbitControls } from 'angular-three-soba/controls'; +import * as THREE from 'three/webgpu'; + +@Component({ + selector: 'app-scene-graph', + template: ` + + + + @for (obj of objects; track $index) { + + + + } + + + + + + `, + imports: [NgtArgs, NgtsOrbitControls], + changeDetection: ChangeDetectionStrategy.OnPush, + schemas: [CUSTOM_ELEMENTS_SCHEMA], +}) +export class SceneGraph { + protected readonly Math = Math; + protected readonly DoubleSide = THREE.DoubleSide; + + protected readonly geometries = [ + new THREE.ConeGeometry(1.0, 2.0, 3, 1), + new THREE.BoxGeometry(2.0, 2.0, 2.0), + new THREE.PlaneGeometry(2.0, 2, 1, 1), + new THREE.CapsuleGeometry(), + new THREE.CircleGeometry(1.0, 3), + new THREE.CylinderGeometry(1.0, 1.0, 2.0, 3, 1), + new THREE.DodecahedronGeometry(1.0, 0), + new THREE.IcosahedronGeometry(1.0, 0), + new THREE.OctahedronGeometry(1.0, 0), + new THREE.PolyhedronGeometry([0, 0, 0], [0, 0, 0], 1, 0), + new THREE.RingGeometry(1.0, 1.5, 3), + new THREE.SphereGeometry(1.0, 3, 2), + new THREE.TetrahedronGeometry(1.0, 0), + new THREE.TorusGeometry(1.0, 0.5, 3, 3), + new THREE.TorusKnotGeometry(1.0, 0.5, 20, 3, 1, 1), + ]; + + protected readonly objects = Array.from({ length: 3000 }, (_, i) => { + const color = Math.random() * 0xffffff; + const geometry = this.geometries[i % this.geometries.length]; + const rotationSpeed = this.randomizeRotationSpeed(new THREE.Euler()); + + return { + color, + geometry, + userData: { rotationSpeed }, + }; + }); + + private groupRef = viewChild.required>('group'); + + private position = new THREE.Vector3(); + private rotation = new THREE.Euler(); + private quaternion = new THREE.Quaternion(); + private scale = new THREE.Vector3(); + + constructor() { + const remove = extend(THREE); + + beforeRender(() => { + const group = this.groupRef().nativeElement; + for (const child of group.children) { + const { rotationSpeed } = child.userData; + child.rotation.set( + child.rotation.x + rotationSpeed.x, + child.rotation.y + rotationSpeed.y, + child.rotation.z + rotationSpeed.z, + ); + } + }); + + inject(DestroyRef).onDestroy(() => { + remove(); + }); + } + + protected onAttached(event: NgtAfterAttach) { + this.randomizeMatrix(event.node.matrix); + event.node.matrix.decompose(event.node.position, event.node.quaternion, event.node.scale); + } + + private randomizeMatrix(matrix: THREE.Matrix4) { + this.position.x = Math.random() * 80 - 40; + this.position.y = Math.random() * 80 - 40; + this.position.z = Math.random() * 80 - 40; + this.rotation.x = Math.random() * 2 * Math.PI; + this.rotation.y = Math.random() * 2 * Math.PI; + this.rotation.z = Math.random() * 2 * Math.PI; + this.quaternion.setFromEuler(this.rotation); + const factorScale = 1; + this.scale.x = this.scale.y = this.scale.z = 0.35 * factorScale + Math.random() * 0.5 * factorScale; + return matrix.compose(this.position, this.quaternion, this.scale); + } + + private randomizeRotationSpeed(rotation: THREE.Euler) { + rotation.x = Math.random() * 0.05; + rotation.y = Math.random() * 0.05; + rotation.z = Math.random() * 0.05; + return rotation; + } +} diff --git a/apps/examples/src/app/misc/webgpu-renderer/webgpu-renderer.ts b/apps/examples/src/app/misc/webgpu-renderer/webgpu-renderer.ts new file mode 100644 index 00000000..1a0101b2 --- /dev/null +++ b/apps/examples/src/app/misc/webgpu-renderer/webgpu-renderer.ts @@ -0,0 +1,33 @@ +import { ChangeDetectionStrategy, Component, signal } from '@angular/core'; +import { NgtFrameloop, NgtGLOptions } from 'angular-three'; +import { NgtCanvas } from 'angular-three/dom'; +import * as THREE from 'three/webgpu'; +import { SceneGraph } from './scene'; + +@Component({ + template: ` + + + + `, + changeDetection: ChangeDetectionStrategy.OnPush, + imports: [NgtCanvas, SceneGraph], + host: { class: 'webgpu-renderer' }, +}) +export default class WebGPURenderer { + protected frameloop = signal('never'); + protected glFactory: NgtGLOptions = (defaultOptions) => { + const renderer = new THREE.WebGPURenderer({ + canvas: defaultOptions.canvas as HTMLCanvasElement, + powerPreference: 'high-performance', + antialias: true, + forceWebGL: false, + }); + + renderer.init().then(() => { + this.frameloop.set('always'); + }); + + return renderer; + }; +} diff --git a/apps/examples/src/app/misc/webgpu-tsl/gears.glb b/apps/examples/src/app/misc/webgpu-tsl/gears.glb new file mode 100644 index 00000000..9b6d8cae Binary files /dev/null and b/apps/examples/src/app/misc/webgpu-tsl/gears.glb differ diff --git a/apps/examples/src/app/misc/webgpu-tsl/royal_esplanade_1k.hdr b/apps/examples/src/app/misc/webgpu-tsl/royal_esplanade_1k.hdr new file mode 100644 index 00000000..6d986c70 Binary files /dev/null and b/apps/examples/src/app/misc/webgpu-tsl/royal_esplanade_1k.hdr differ diff --git a/apps/examples/src/app/misc/webgpu-tsl/scene.ts b/apps/examples/src/app/misc/webgpu-tsl/scene.ts new file mode 100644 index 00000000..45dc17b5 --- /dev/null +++ b/apps/examples/src/app/misc/webgpu-tsl/scene.ts @@ -0,0 +1,190 @@ +import { NgTemplateOutlet } from '@angular/common'; +import { + ChangeDetectionStrategy, + Component, + computed, + CUSTOM_ELEMENTS_SCHEMA, + DestroyRef, + effect, + ElementRef, + inject, + signal, + viewChild, +} from '@angular/core'; +import { beforeRender, extend, injectStore, loaderResource } from 'angular-three'; +import { NgtsPerspectiveCamera } from 'angular-three-soba/cameras'; +import { NgtsOrbitControls } from 'angular-three-soba/controls'; +import { gltfResource } from 'angular-three-soba/loaders'; +import { TweakpaneCheckbox, TweakpaneColor, TweakpaneNumber, TweakpanePane } from 'angular-three-tweakpane'; +import { GLTF, RGBELoader } from 'three-stdlib'; +import * as THREE from 'three/webgpu'; +import { DirectionalLight, MeshPhysicalNodeMaterial } from 'three/webgpu'; +import { SliceMaterial } from './slice-material'; + +import gearsGLB from './gears.glb' with { loader: 'file' }; +import royalHDR from './royal_esplanade_1k.hdr' with { loader: 'file' }; + +gltfResource.preload(gearsGLB); + +interface GearsGLB extends GLTF { + nodes: { axle: THREE.Mesh; gears: THREE.Mesh; outerHull: THREE.Mesh }; +} + +@Component({ + selector: 'app-scene-graph', + template: ` + + + + + + + + + + + + @if (gltf.value(); as gltf) { + @let gears = gltf.nodes.gears; + @let axle = gltf.nodes.axle; + @let outerHull = gltf.nodes.outerHull; + + + + + + + + } + + + + + + + + + + + + + + + + `, + imports: [ + NgtsPerspectiveCamera, + NgtsOrbitControls, + NgTemplateOutlet, + SliceMaterial, + TweakpanePane, + TweakpaneColor, + TweakpaneCheckbox, + TweakpaneNumber, + ], + changeDetection: ChangeDetectionStrategy.OnPush, + schemas: [CUSTOM_ELEMENTS_SCHEMA], +}) +export class SceneGraph { + private gearsRef = viewChild.required>('gears'); + + protected environmentMap = loaderResource( + () => RGBELoader, + () => royalHDR, + ); + protected gltf = gltfResource(() => gearsGLB); + + private store = injectStore(); + + protected metalness = 0.5; + protected roughness = 0.25; + protected envMapIntensity = 0.5; + protected color = '#858080'; + + protected rotate = signal(true); + protected sliceColor = signal('#9370DB'); + protected startAngleDegrees = signal(60); + protected arcAngleDegrees = signal(90); + + protected arcAngle = computed(() => THREE.MathUtils.DEG2RAD * this.arcAngleDegrees()); + protected startAngle = computed(() => THREE.MathUtils.DEG2RAD * this.startAngleDegrees()); + + constructor() { + const remove = extend({ MeshPhysicalNodeMaterial, DirectionalLight }); + + beforeRender(({ delta }) => { + if (!this.rotate()) return; + this.gearsRef().nativeElement.rotation.y += 0.1 * delta; + }); + + effect((onCleanup) => { + const environmentMap = this.environmentMap.value(); + if (!environmentMap) return; + + const scene = this.store.scene(); + + const oldBackground = scene.background; + const oldEnvironment = scene.environment; + const blurriness = scene.backgroundBlurriness; + + environmentMap.mapping = THREE.EquirectangularReflectionMapping; + + scene.background = environmentMap; + scene.backgroundBlurriness = 0.5; + scene.environment = environmentMap; + + onCleanup(() => { + scene.background = oldBackground; + scene.environment = oldEnvironment; + scene.backgroundBlurriness = blurriness; + }); + }); + + inject(DestroyRef).onDestroy(() => { + remove(); + }); + } +} diff --git a/apps/examples/src/app/misc/webgpu-tsl/slice-material.ts b/apps/examples/src/app/misc/webgpu-tsl/slice-material.ts new file mode 100644 index 00000000..b8b8637b --- /dev/null +++ b/apps/examples/src/app/misc/webgpu-tsl/slice-material.ts @@ -0,0 +1,37 @@ +import { Directive, effect, ElementRef, inject, input } from '@angular/core'; +import { color, uniform } from 'three/tsl'; +import * as THREE from 'three/webgpu'; +import { outputNodeFn, shadowNodeFn } from './tsl'; + +@Directive({ selector: 'ngt-mesh-physical-node-material[slice]' }) +export class SliceMaterial { + arcAngle = input(0.5 * Math.PI); + startAngle = input(0); + sliceColor = input('black'); + + private material = inject>(ElementRef); + + private uArcAngle = uniform(0.5 * Math.PI); + private uStartAngle = uniform(0); + private uColor = uniform(color('black')); + + constructor() { + this.material.nativeElement.outputNode = outputNodeFn({ + startAngle: this.uStartAngle, + arcAngle: this.uArcAngle, + color: this.uColor, + }); + this.material.nativeElement.castShadowNode = shadowNodeFn({ + startAngle: this.uStartAngle, + arcAngle: this.uArcAngle, + }); + this.material.nativeElement.side = THREE.DoubleSide; + + effect(() => { + const [arcAngle, startAngle, sliceColor] = [this.arcAngle(), this.startAngle(), this.sliceColor()]; + this.uStartAngle.value = startAngle; + this.uArcAngle.value = arcAngle; + this.uColor.value.set(sliceColor); + }); + } +} diff --git a/apps/examples/src/app/misc/webgpu-tsl/tsl.ts b/apps/examples/src/app/misc/webgpu-tsl/tsl.ts new file mode 100644 index 00000000..f2a66781 --- /dev/null +++ b/apps/examples/src/app/misc/webgpu-tsl/tsl.ts @@ -0,0 +1,26 @@ +import type { NodeRepresentation, ShaderNodeObject } from 'three/tsl'; +import { Fn, If, PI2, atan, frontFacing, output, positionLocal, vec4 } from 'three/tsl'; +import type { Node } from 'three/webgpu'; + +type AngleInputs = { startAngle: NodeRepresentation; arcAngle: NodeRepresentation }; + +const inAngle = Fn( + ([position, startAngle, endAngle]: [ShaderNodeObject, NodeRepresentation, NodeRepresentation]) => { + const angle = atan(position.y, position.x).sub(startAngle).mod(PI2).toVar(); + return angle.greaterThan(0).and(angle.lessThan(endAngle)); + }, +); + +export const outputNodeFn = Fn(({ startAngle, arcAngle, color }: AngleInputs & { color: NodeRepresentation }) => { + inAngle(positionLocal.xy, startAngle, arcAngle).discard(); + const finalOutput = output; + If(frontFacing.not(), () => { + finalOutput.assign(vec4(color, 1)); + }); + return finalOutput; +}); + +export const shadowNodeFn = Fn(({ startAngle, arcAngle }: AngleInputs) => { + inAngle(positionLocal.xy, startAngle, arcAngle).discard(); + return vec4(0, 0, 0, 1); +}); diff --git a/apps/examples/src/app/misc/webgpu-tsl/webgpu-tsl.ts b/apps/examples/src/app/misc/webgpu-tsl/webgpu-tsl.ts new file mode 100644 index 00000000..8b3d0cf2 --- /dev/null +++ b/apps/examples/src/app/misc/webgpu-tsl/webgpu-tsl.ts @@ -0,0 +1,46 @@ +import { ChangeDetectionStrategy, Component, signal } from '@angular/core'; +import { NgtFrameloop, NgtGLOptions } from 'angular-three'; +import { NgtCanvas } from 'angular-three/dom'; +import * as THREE from 'three/webgpu'; +import { SceneGraph } from './scene'; + +@Component({ + template: ` + + + + + + * There seems to be an issue with WebGPURenderer and dispose process. Environment map will not load properly + on navigating away and back. + + `, + changeDetection: ChangeDetectionStrategy.OnPush, + imports: [NgtCanvas, SceneGraph], + host: { class: 'webgpu-tsl' }, +}) +export default class WebGPUTSL { + protected frameloop = signal('never'); + protected glFactory: NgtGLOptions = (defaultOptions) => { + const renderer = new THREE.WebGPURenderer({ + canvas: defaultOptions.canvas as HTMLCanvasElement, + antialias: true, + forceWebGL: false, + }); + + renderer.init().then(() => { + this.frameloop.set('always'); + }); + + const dispose = renderer.dispose.bind(renderer); + Object.assign(renderer, { + dispose: () => { + renderer._renderLists?.dispose(); + renderer._renderContexts?.dispose(); + dispose(); + }, + }); + + return renderer; + }; +} diff --git a/apps/examples/src/app/postprocessing/basic/basic.ts b/apps/examples/src/app/postprocessing/basic/basic.ts new file mode 100644 index 00000000..ce4ce072 --- /dev/null +++ b/apps/examples/src/app/postprocessing/basic/basic.ts @@ -0,0 +1,15 @@ +import { ChangeDetectionStrategy, Component } from '@angular/core'; +import { NgtCanvas } from 'angular-three/dom'; +import { SceneGraph } from './scene'; + +@Component({ + template: ` + + + + `, + changeDetection: ChangeDetectionStrategy.OnPush, + imports: [NgtCanvas, SceneGraph], + host: { class: 'basic-postprocessing' }, +}) +export default class Basic {} diff --git a/apps/kitchen-sink/src/app/postprocessing/basic/experience.ts b/apps/examples/src/app/postprocessing/basic/scene.ts similarity index 89% rename from apps/kitchen-sink/src/app/postprocessing/basic/experience.ts rename to apps/examples/src/app/postprocessing/basic/scene.ts index ee064bc7..bec2af90 100644 --- a/apps/kitchen-sink/src/app/postprocessing/basic/experience.ts +++ b/apps/examples/src/app/postprocessing/basic/scene.ts @@ -6,7 +6,7 @@ import { input, viewChild, } from '@angular/core'; -import { NgtArgs, injectBeforeRender } from 'angular-three'; +import { NgtArgs, beforeRender } from 'angular-three'; import { NgtpEffectComposer, NgtpGodRays } from 'angular-three-postprocessing'; import { Group, Mesh } from 'three'; @@ -30,6 +30,7 @@ export class Sun { } @Component({ + selector: 'app-scene-graph', template: ` @@ -53,9 +54,7 @@ export class Sun { - @if (sun.sunRef().nativeElement; as sun) { - - } + `, schemas: [CUSTOM_ELEMENTS_SCHEMA], @@ -63,11 +62,11 @@ export class Sun { changeDetection: ChangeDetectionStrategy.OnPush, host: { class: 'experience-basic-postprocessing' }, }) -export class Experience { +export class SceneGraph { private group = viewChild.required>('group'); constructor() { - injectBeforeRender(() => { + beforeRender(() => { const { nativeElement } = this.group(); nativeElement.rotation.x += 0.005; nativeElement.rotation.y += 0.005; diff --git a/apps/examples/src/app/postprocessing/outline/outline.ts b/apps/examples/src/app/postprocessing/outline/outline.ts new file mode 100644 index 00000000..857cf485 --- /dev/null +++ b/apps/examples/src/app/postprocessing/outline/outline.ts @@ -0,0 +1,15 @@ +import { ChangeDetectionStrategy, Component } from '@angular/core'; +import { NgtCanvas } from 'angular-three/dom'; +import { SceneGraph } from './scene'; + +@Component({ + template: ` + + + + `, + changeDetection: ChangeDetectionStrategy.OnPush, + host: { class: 'postprocessing-outline' }, + imports: [NgtCanvas, SceneGraph], +}) +export default class PostprocessingOutline {} diff --git a/apps/kitchen-sink/src/app/postprocessing/outline/experience.ts b/apps/examples/src/app/postprocessing/outline/scene.ts similarity index 69% rename from apps/kitchen-sink/src/app/postprocessing/outline/experience.ts rename to apps/examples/src/app/postprocessing/outline/scene.ts index 4380b739..5461f64d 100644 --- a/apps/kitchen-sink/src/app/postprocessing/outline/experience.ts +++ b/apps/examples/src/app/postprocessing/outline/scene.ts @@ -1,5 +1,5 @@ import { ChangeDetectionStrategy, Component, CUSTOM_ELEMENTS_SCHEMA, signal } from '@angular/core'; -import { NgtArgs, NgtSelect, NgtSelection } from 'angular-three'; +import { NgtArgs, NgtSelection, NgtSelectionApi } from 'angular-three'; import { NgtpEffectComposer, NgtpOutline, NgtpSMAA } from 'angular-three-postprocessing'; import { NgtsOrbitControls } from 'angular-three-soba/controls'; import { KernelSize } from 'postprocessing'; @@ -7,15 +7,16 @@ import { KernelSize } from 'postprocessing'; /** * There are multiple ways to use the Outline effect. * - * 1. Via NgtSelection and NgtSelect + * 1. Via NgtSelectionApi and NgtSelect * This is the recommended way to use the Outline effect. * - * 1a. We can use NgtSelection as hostDirective (as shown) to enable Selection on the entire scene. + * 1a. We can use NgtSelectionApi as hostDirective (as shown) to enable Selection on the entire scene. * NgtpOutline will automatically be aware of the NgtSelection context and will use it for the selected objects. * - * 1b. We can wrap `` around the objects we want to select AS WELL AS the Outline effect. + * 1b. We can wrap `` around the objects we want to select AS WELL AS the Outline effect. + * When using this approach, you can use `NgtSelection` in the imports array instead of [NgtSelectionApi, NgtSelect]. * - * ngtSelect can be used on ngt-group or ngt-mesh. ngt-group will select all children, ngt-mesh will only select itself. + * select can be used on ngt-group or ngt-mesh. ngt-group will select all children, ngt-mesh will only select itself. * * 2. Via selection input on NgtpOutline * If we want to control the selection ourselves, we can pass in the selection input an Array of Object3D or ElementRef @@ -26,6 +27,7 @@ import { KernelSize } from 'postprocessing'; */ @Component({ + selector: 'app-scene-graph', template: ` @@ -35,7 +37,7 @@ import { KernelSize } from 'postprocessing'; - + @@ -51,7 +53,6 @@ import { KernelSize } from 'postprocessing'; - @@ -59,10 +60,10 @@ import { KernelSize } from 'postprocessing'; schemas: [CUSTOM_ELEMENTS_SCHEMA], changeDetection: ChangeDetectionStrategy.OnPush, host: { class: 'postprocessing-sample' }, - hostDirectives: [NgtSelection], - imports: [NgtsOrbitControls, NgtSelect, NgtpEffectComposer, NgtpOutline, NgtArgs, NgtpSMAA], + hostDirectives: [NgtSelectionApi], + imports: [NgtsOrbitControls, NgtSelection, NgtpEffectComposer, NgtpOutline, NgtArgs, NgtpSMAA], }) -export class Experience { +export class SceneGraph { protected KernelSize = KernelSize; protected hovered = signal(false); } diff --git a/apps/kitchen-sink/src/app/postprocessing/postprocessing.routes.ts b/apps/examples/src/app/postprocessing/postprocessing.routes.ts similarity index 100% rename from apps/kitchen-sink/src/app/postprocessing/postprocessing.routes.ts rename to apps/examples/src/app/postprocessing/postprocessing.routes.ts diff --git a/apps/kitchen-sink/src/app/postprocessing/postprocessing.ts b/apps/examples/src/app/postprocessing/postprocessing.ts similarity index 92% rename from apps/kitchen-sink/src/app/postprocessing/postprocessing.ts rename to apps/examples/src/app/postprocessing/postprocessing.ts index 535da0dc..cf20acc6 100644 --- a/apps/kitchen-sink/src/app/postprocessing/postprocessing.ts +++ b/apps/examples/src/app/postprocessing/postprocessing.ts @@ -1,11 +1,7 @@ import { ChangeDetectionStrategy, Component } from '@angular/core'; import { RouterLink, RouterLinkActive, RouterOutlet } from '@angular/router'; -import { extend } from 'angular-three'; -import * as THREE from 'three'; import routes from './postprocessing.routes'; -extend(THREE); - @Component({ template: `
diff --git a/apps/examples/src/app/rapier/active-collision-types/active-collision-types.ts b/apps/examples/src/app/rapier/active-collision-types/active-collision-types.ts new file mode 100644 index 00000000..8f165c2d --- /dev/null +++ b/apps/examples/src/app/rapier/active-collision-types/active-collision-types.ts @@ -0,0 +1,72 @@ +import { ChangeDetectionStrategy, Component, CUSTOM_ELEMENTS_SCHEMA, signal, viewChild } from '@angular/core'; +import { ActiveCollisionTypes } from '@dimforge/rapier3d-compat'; +import { beforeRender, NgtArgs } from 'angular-three'; +import { NgtrRigidBody } from 'angular-three-rapier'; +import { ResetOrbitControls } from '../reset-orbit-controls'; + +@Component({ + selector: 'app-ball', + template: ` + + + + + + + `, + changeDetection: ChangeDetectionStrategy.OnPush, + schemas: [CUSTOM_ELEMENTS_SCHEMA], + imports: [NgtrRigidBody], +}) +export class Ball { + protected readonly activeCollisionTypes = ActiveCollisionTypes.DEFAULT | ActiveCollisionTypes.KINEMATIC_FIXED; + + private rigidBodyRef = viewChild.required(NgtrRigidBody); + + protected color = signal('blue'); + + constructor() { + beforeRender(({ clock }) => { + const rb = this.rigidBodyRef().rigidBody(); + if (!rb) return; + + rb.setTranslation({ x: Math.sin(clock.elapsedTime) * 3, y: 0, z: 0 }, true); + }); + } +} + +@Component({ + selector: 'app-wall', + template: ` + + + + + + + `, + changeDetection: ChangeDetectionStrategy.OnPush, + schemas: [CUSTOM_ELEMENTS_SCHEMA], + imports: [NgtrRigidBody, NgtArgs], +}) +export class Wall {} + +@Component({ + selector: 'app-active-collision-types-rapier', + template: ` + + + + + `, + hostDirectives: [ResetOrbitControls], + changeDetection: ChangeDetectionStrategy.OnPush, + schemas: [CUSTOM_ELEMENTS_SCHEMA], + imports: [Ball, Wall], +}) +export default class ActiveCollisionTypesExample {} diff --git a/apps/examples/src/app/rapier/all-colliders/all-colliders.ts b/apps/examples/src/app/rapier/all-colliders/all-colliders.ts new file mode 100644 index 00000000..578f0920 --- /dev/null +++ b/apps/examples/src/app/rapier/all-colliders/all-colliders.ts @@ -0,0 +1,251 @@ +import { ChangeDetectionStrategy, Component, computed, CUSTOM_ELEMENTS_SCHEMA, input } from '@angular/core'; +import { NgtArgs, NgtThreeElements, NgtVector3 } from 'angular-three'; +import { + NgtrBallCollider, + NgtrCapsuleCollider, + NgtrConeCollider, + NgtrCuboidCollider, + NgtrCylinderCollider, + NgtrHeightfieldCollider, + NgtrRigidBody, + NgtrRoundConeCollider, + NgtrRoundCuboidCollider, + NgtrRoundCylinderCollider, +} from 'angular-three-rapier'; +import { NgtsHTML } from 'angular-three-soba/misc'; +import * as THREE from 'three'; +import { RoundedBoxGeometry } from 'three-stdlib'; +import { ResetOrbitControls } from '../reset-orbit-controls'; +import { suzanneResource } from '../suzanne'; + +@Component({ + selector: 'app-cute-box', + template: ` + + + + + `, + changeDetection: ChangeDetectionStrategy.OnPush, + schemas: [CUSTOM_ELEMENTS_SCHEMA], +}) +export class CuteBox { + options = input({} as Partial); +} + +@Component({ + selector: 'app-rigid-body-box', + template: ` + + + + + + + `, + changeDetection: ChangeDetectionStrategy.OnPush, + schemas: [CUSTOM_ELEMENTS_SCHEMA], + imports: [NgtrRigidBody], +}) +export class RigidBodyBox { + position = input([0, 0, 0]); +} + +@Component({ + selector: 'app-suzanne', + template: ` + + `, + imports: [NgtArgs], + changeDetection: ChangeDetectionStrategy.OnPush, + schemas: [CUSTOM_ELEMENTS_SCHEMA], +}) +export class Suzanne { + visible = input(true); + + private suzanne = suzanneResource(); + protected scene = computed(() => { + const suzanne = this.suzanne.value(); + if (!suzanne) return null; + return suzanne.nodes.Suzanne.clone(); + }); +} + +@Component({ + selector: 'app-all-colliders-rapier', + template: ` + + + + + +
CuboidCollider
+
+
+ + + + + + + +
RoundCuboidCollider
+
+
+ + + + + + + + +
BallCollider
+
+
+ + + + + + + + +
CapsuleCollider
+
+
+ + + + + + + + +
CylinderCollider
+
+
+ + + + +
TrimeshCollider
+
+
+ + + + +
HullCollider
+
+
+ + + + + + + +
Invisible Collider
+
+
+ + + + + + + + +
ConeCollider
+
+
+ + + + + + + + +
RoundConeCollider
+
+
+ + + + + + + + +
RoundCylinderCollider
+
+
+ + + + + + + +
HeightfieldCollider
+
+
+ + +
+ `, + imports: [ + NgtrRigidBody, + RigidBodyBox, + CuteBox, + Suzanne, + NgtrCuboidCollider, + NgtrRoundCuboidCollider, + NgtrBallCollider, + NgtrCapsuleCollider, + NgtrCylinderCollider, + NgtrConeCollider, + NgtrRoundConeCollider, + NgtrRoundCylinderCollider, + NgtrHeightfieldCollider, + NgtsHTML, + NgtArgs, + ], + hostDirectives: [ResetOrbitControls], + changeDetection: ChangeDetectionStrategy.OnPush, + schemas: [CUSTOM_ELEMENTS_SCHEMA], +}) +export default class AllCollidersExample { + protected readonly DoubleSide = THREE.DoubleSide; + + protected heightFieldHeight = 10; + protected heightFieldWidth = 10; + protected heightField = Array.from({ length: this.heightFieldHeight * this.heightFieldWidth }, () => Math.random()); + protected heightFieldGeometry = new THREE.PlaneGeometry( + this.heightFieldWidth, + this.heightFieldHeight, + this.heightFieldWidth - 1, + this.heightFieldHeight - 1, + ); + + protected roundBoxGeometry = new RoundedBoxGeometry(1.4, 1.4, 1.4, 8, 0.2); + + constructor() { + this.heightField.forEach((v, index) => { + this.heightFieldGeometry.attributes['position'].array[index * 3 + 2] = v; + }); + this.heightFieldGeometry.scale(1, -1, 1); + this.heightFieldGeometry.rotateX(-Math.PI / 2); + this.heightFieldGeometry.rotateY(-Math.PI / 2); + this.heightFieldGeometry.computeVertexNormals(); + } +} diff --git a/apps/examples/src/app/rapier/all-shapes/all-shapes.ts b/apps/examples/src/app/rapier/all-shapes/all-shapes.ts new file mode 100644 index 00000000..35a3993e --- /dev/null +++ b/apps/examples/src/app/rapier/all-shapes/all-shapes.ts @@ -0,0 +1,177 @@ +import { ChangeDetectionStrategy, Component, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'; +import { gltfResource } from 'angular-three-soba/loaders'; +import { suzanneResource } from '../suzanne'; + +import { + NgtrBallCollider, + NgtrConeCollider, + NgtrConvexHullCollider, + NgtrCuboidCollider, + NgtrRigidBody, + NgtrTrimeshCollider, +} from 'angular-three-rapier'; +import { NgtsHTML } from 'angular-three-soba/misc'; +import { ResetOrbitControls } from '../reset-orbit-controls'; +import offsetTorusGLB from './offset-torus.glb' with { loader: 'file' }; + +gltfResource.preload(offsetTorusGLB); + +@Component({ + selector: 'app-offset-torus', + template: ` + @if (gltf.value(); as gltf) { + + } + `, + schemas: [CUSTOM_ELEMENTS_SCHEMA], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class OffsetTorus { + protected gltf = gltfResource(() => offsetTorusGLB); +} + +@Component({ + selector: 'app-suzanne', + template: ` + @if (gltf.value(); as gltf) { + + } + `, + schemas: [CUSTOM_ELEMENTS_SCHEMA], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class Suzanne { + protected gltf = suzanneResource(); +} + +@Component({ + selector: 'app-all-shapes-rapier-example', + template: ` + + + + +
Auto Cuboid
+
+
+ + + + +
Auto Ball
+
+
+ + + + +
Auto Hull
+
+
+ + + + +
Auto Trimesh
+
+
+ + + + + +
Custom Cuboid
+
+
+ + + + + +
Custom Ball
+
+
+ + + + + +
Custom Cone
+
+
+ + @if (gltf.value(); as gltf) { + @let geometry = gltf.nodes.Suzanne.geometry; + + + + + +
Custom Trimesh
+
+
+ + + + + +
Custom Convex Hull
+
+
+ } + + + + + + +
Custom Combound shape
+
+
+ + + + + + +
Auto and Custom Combound shape
+
+
+ + + + + + + +
Mesh with offset geometry
+
+
+
+
+ `, + hostDirectives: [ResetOrbitControls], + schemas: [CUSTOM_ELEMENTS_SCHEMA], + changeDetection: ChangeDetectionStrategy.OnPush, + imports: [ + NgtrRigidBody, + Suzanne, + NgtsHTML, + NgtrCuboidCollider, + NgtrBallCollider, + NgtrConeCollider, + NgtrTrimeshCollider, + NgtrConvexHullCollider, + OffsetTorus, + ], +}) +export default class AllShapesExample { + protected gltf = suzanneResource(); +} diff --git a/apps/examples/src/app/rapier/all-shapes/offset-torus.glb b/apps/examples/src/app/rapier/all-shapes/offset-torus.glb new file mode 100644 index 00000000..8566998c Binary files /dev/null and b/apps/examples/src/app/rapier/all-shapes/offset-torus.glb differ diff --git a/apps/examples/src/app/rapier/attractors/attractors.ts b/apps/examples/src/app/rapier/attractors/attractors.ts new file mode 100644 index 00000000..9663acdd --- /dev/null +++ b/apps/examples/src/app/rapier/attractors/attractors.ts @@ -0,0 +1,59 @@ +import { ChangeDetectionStrategy, Component, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'; +import { NgtArgs, NgtVector3 } from 'angular-three'; +import { NgtrInstancedRigidBodies, NgtrInteractionGroups, NgtrRigidBody } from 'angular-three-rapier'; +import { NgtrAttractor } from 'angular-three-rapier/addons'; +import { NgtsHTML } from 'angular-three-soba/misc'; +import { ResetOrbitControls } from '../reset-orbit-controls'; + +@Component({ + selector: 'app-attractors-rapier', + template: ` + + + + + + + + + + + + + +
Nested Attractor
+
+ + + + + + +
Repeller
+
+
+ + + + +
Attractor
+
+
+
+ `, + hostDirectives: [ResetOrbitControls], + changeDetection: ChangeDetectionStrategy.OnPush, + schemas: [CUSTOM_ELEMENTS_SCHEMA], + imports: [NgtrInstancedRigidBodies, NgtArgs, NgtrRigidBody, NgtrInteractionGroups, NgtsHTML, NgtrAttractor], +}) +export default class AttractorsExample { + protected instances = Array.from({ length: 100 }, (_, index) => ({ + key: index, + position: [Math.floor(Math.random() * 30), Math.random() * 30 * 0.5, 0] as NgtVector3, + })); +} diff --git a/apps/examples/src/app/rapier/basic/basic.ts b/apps/examples/src/app/rapier/basic/basic.ts new file mode 100644 index 00000000..4b403e77 --- /dev/null +++ b/apps/examples/src/app/rapier/basic/basic.ts @@ -0,0 +1,52 @@ +import { ChangeDetectionStrategy, Component, CUSTOM_ELEMENTS_SCHEMA, signal } from '@angular/core'; +import { beforeRender } from 'angular-three'; +import { NgtrCuboidCollider, NgtrRigidBody } from 'angular-three-rapier'; + +@Component({ + selector: 'app-rapier-basic', + template: ` + + + + + + + @if (currentCollider() === 1) { + + } @else if (currentCollider() === 2) { + + } @else if (currentCollider() === 3) { + + } @else { + + } + `, + schemas: [CUSTOM_ELEMENTS_SCHEMA], + changeDetection: ChangeDetectionStrategy.OnPush, + host: { class: 'experience-basic-rapier' }, + imports: [NgtrRigidBody, NgtrCuboidCollider], +}) +export default class Basic { + protected currentCollider = signal(1); + + constructor() { + beforeRender(({ camera }) => { + const currentCollider = this.currentCollider(); + if (currentCollider === 2) { + camera.position.lerp({ x: 10, y: 10, z: 10 }, 0.1); + } else if (currentCollider === 3) { + camera.position.lerp({ x: 15, y: 15, z: 15 }, 0.1); + } else if (currentCollider === 4) { + camera.position.lerp({ x: 20, y: 40, z: 40 }, 0.1); + } + }); + } +} diff --git a/apps/examples/src/app/rapier/car/car.ts b/apps/examples/src/app/rapier/car/car.ts new file mode 100644 index 00000000..b84a9613 --- /dev/null +++ b/apps/examples/src/app/rapier/car/car.ts @@ -0,0 +1,74 @@ +import { + ChangeDetectionStrategy, + Component, + computed, + CUSTOM_ELEMENTS_SCHEMA, + Directive, + inject, + input, +} from '@angular/core'; +import { beforeRender, NgtArgs, NgtVector3 } from 'angular-three'; +import { NgtrRigidBody, revoluteJoint } from 'angular-three-rapier'; +import { ResetOrbitControls } from '../reset-orbit-controls'; + +@Directive({ selector: '[rigidBody][wheel]' }) +export class WheelJoint { + body = input.required({ alias: 'wheel' }); + bodyAnchor = input.required({ alias: 'position' }); + + private wheel = inject(NgtrRigidBody, { host: true }); + private rigidBody = computed(() => this.body().rigidBody()); + + constructor() { + const revoluteJointApi = revoluteJoint(this.rigidBody, this.wheel.rigidBody, { + data: () => ({ body1Anchor: this.bodyAnchor(), body2Anchor: [0, 0, 0], axis: [0, 0, 1] }), + }); + + beforeRender(() => { + const jointApi = revoluteJointApi(); + if (!jointApi) return; + jointApi.configureMotorVelocity(20, 10); + }); + } +} + +@Component({ + selector: 'app-car-rapier', + template: ` + + + + + + + + + @for (position of wheelPositions; track $index) { + + + + + + + } + + `, + hostDirectives: [ResetOrbitControls], + changeDetection: ChangeDetectionStrategy.OnPush, + schemas: [CUSTOM_ELEMENTS_SCHEMA], + imports: [NgtrRigidBody, WheelJoint, NgtArgs], +}) +export default class CarExample { + protected wheelPositions: NgtVector3[] = [ + [-3, 0, 2], + [-3, 0, -2], + [3, 0, 2], + [3, 0, -2], + ]; + protected readonly Math = Math; +} diff --git a/apps/kitchen-sink/src/app/rapier/cluster/cluster.ts b/apps/examples/src/app/rapier/cluster/cluster.ts similarity index 75% rename from apps/kitchen-sink/src/app/rapier/cluster/cluster.ts rename to apps/examples/src/app/rapier/cluster/cluster.ts index 9fb954b0..c0dd379f 100644 --- a/apps/kitchen-sink/src/app/rapier/cluster/cluster.ts +++ b/apps/examples/src/app/rapier/cluster/cluster.ts @@ -7,38 +7,37 @@ import { inject, viewChild, } from '@angular/core'; -import { injectBeforeRender, NgtArgs, NON_ROOT } from 'angular-three'; +import { beforeRender, checkUpdate, NgtArgs } from 'angular-three'; import { NgtrInstancedRigidBodies, NgtrPhysics } from 'angular-three-rapier'; import { Color, InstancedMesh, Vector3 } from 'three'; +import { ResetOrbitControls } from '../reset-orbit-controls'; const BALLS = 1000; @Component({ + selector: 'app-cluster-rapier', template: ` - - + + `, + hostDirectives: [ResetOrbitControls], schemas: [CUSTOM_ELEMENTS_SCHEMA], changeDetection: ChangeDetectionStrategy.OnPush, host: { class: 'cluster-rapier' }, imports: [NgtrInstancedRigidBodies, NgtArgs], }) -export class ClusterExample { - static [NON_ROOT] = true; - +export default class ClusterExample { protected readonly BALLS = BALLS; - protected bodies = Array.from({ length: BALLS }, (_, index) => { - return { - key: index, - position: [Math.floor(index / 30), (index % 30) * 0.5, 0] as [number, number, number], - }; - }); + protected bodies = Array.from({ length: BALLS }, (_, index) => ({ + key: index, + position: [Math.floor(index / 30), (index % 30) * 0.5, 0] as [number, number, number], + })); private rigidBodiesRef = viewChild.required(NgtrInstancedRigidBodies); private instancedMeshRef = viewChild>('instancedMesh'); @@ -46,7 +45,7 @@ export class ClusterExample { private physics = inject(NgtrPhysics); constructor() { - injectBeforeRender(() => { + beforeRender(() => { const paused = this.physics.paused(); if (paused) return; @@ -69,7 +68,7 @@ export class ClusterExample { for (let i = 0; i < BALLS; i++) { instancedMesh.setColorAt(i, new Color(Math.random() * 0xffffff)); } - if (instancedMesh.instanceColor) instancedMesh.instanceColor.needsUpdate = true; + checkUpdate(instancedMesh.instanceColor); }); } } diff --git a/apps/examples/src/app/rapier/contact-force-events/contact-force-events.ts b/apps/examples/src/app/rapier/contact-force-events/contact-force-events.ts new file mode 100644 index 00000000..78994ba7 --- /dev/null +++ b/apps/examples/src/app/rapier/contact-force-events/contact-force-events.ts @@ -0,0 +1,107 @@ +import { + ChangeDetectionStrategy, + Component, + CUSTOM_ELEMENTS_SCHEMA, + DestroyRef, + inject, + output, + signal, + viewChild, +} from '@angular/core'; +import { NgtArgs } from 'angular-three'; +import { NgtrContactForcePayload, NgtrRigidBody } from 'angular-three-rapier'; +import * as THREE from 'three'; +import { ResetOrbitControls } from '../reset-orbit-controls'; + +// magic number: this is the start force for where the ball drops from +// and is used to calculate the color change +const startForce = 6500; +const startColor = new THREE.Color(0xffffff); +const floorColor = signal(startColor.clone().multiplyScalar(1).getHex()); + +@Component({ + selector: 'app-ball', + template: ` + + + + + + + `, + imports: [NgtrRigidBody], + changeDetection: ChangeDetectionStrategy.OnPush, + schemas: [CUSTOM_ELEMENTS_SCHEMA], +}) +export class Ball { + contactForce = output(); + + private rigidBodyRef = viewChild.required(NgtrRigidBody); + + protected onContactForce(event: NgtrContactForcePayload) { + const rigidBody = this.rigidBodyRef().rigidBody(); + + const { totalForceMagnitude } = event; + + if (totalForceMagnitude < 300) { + rigidBody?.applyImpulse({ x: 0, y: 65, z: 0 }, true); + } + + this.contactForce.emit(totalForceMagnitude); + console.log('contact force', event); + } + + protected onCollisionEnter() { + console.log('collision enter'); + } +} + +@Component({ + selector: 'app-floor', + template: ` + + + + + + + `, + imports: [NgtrRigidBody, NgtArgs], + changeDetection: ChangeDetectionStrategy.OnPush, + schemas: [CUSTOM_ELEMENTS_SCHEMA], +}) +export class Floor { + protected readonly floorColor = floorColor; +} + +@Component({ + selector: 'app-rapier-contact-force-events', + template: ` + + + + + `, + hostDirectives: [ResetOrbitControls], + imports: [Ball, Floor], + changeDetection: ChangeDetectionStrategy.OnPush, + schemas: [CUSTOM_ELEMENTS_SCHEMA], +}) +export default class ContactForceEventsExample { + constructor() { + inject(DestroyRef).onDestroy(() => { + floorColor.set(startColor.clone().getHex()); + }); + } + + protected onContactForce(totalForceMagnitude: number) { + const color = startColor.clone().multiplyScalar(1 - totalForceMagnitude / startForce); + floorColor.set(color.getHex()); + } +} diff --git a/apps/examples/src/app/rapier/cradle/cradle.ts b/apps/examples/src/app/rapier/cradle/cradle.ts new file mode 100644 index 00000000..4e7aeffc --- /dev/null +++ b/apps/examples/src/app/rapier/cradle/cradle.ts @@ -0,0 +1,70 @@ +import { ChangeDetectionStrategy, Component, computed, CUSTOM_ELEMENTS_SCHEMA, input, viewChild } from '@angular/core'; +import { NgtEuler, NgtVector3 } from 'angular-three'; +import { NgtrBallCollider, NgtrCylinderCollider, NgtrRigidBody, sphericalJoint } from 'angular-three-rapier'; +import { ResetOrbitControls } from '../reset-orbit-controls'; + +@Component({ + selector: 'app-rod', + template: ` + + + + + + + + + + + + + + + + + + `, + schemas: [CUSTOM_ELEMENTS_SCHEMA], + changeDetection: ChangeDetectionStrategy.OnPush, + imports: [NgtrRigidBody, NgtrCylinderCollider, NgtrBallCollider], +}) +export class Rod { + position = input([0, 0, 0]); + rotation = input([0, 0, 0]); + + private anchorRef = viewChild.required('anchor', { read: NgtrRigidBody }); + private rodRef = viewChild.required('rod', { read: NgtrRigidBody }); + + constructor() { + const anchor = computed(() => this.anchorRef().rigidBody()); + const rod = computed(() => this.rodRef().rigidBody()); + + sphericalJoint(anchor, rod, { + data: { body1Anchor: [0, 0, 0], body2Anchor: [0, 0, 0] }, + }); + } +} + +@Component({ + selector: 'app-cradle-rapier', + template: ` + + + + + + + + `, + schemas: [CUSTOM_ELEMENTS_SCHEMA], + hostDirectives: [ResetOrbitControls], + changeDetection: ChangeDetectionStrategy.OnPush, + imports: [Rod], +}) +export default class CradleExample {} diff --git a/apps/kitchen-sink/src/app/rapier/instanced-mesh/instanced-mesh.ts b/apps/examples/src/app/rapier/instanced-mesh/instanced-mesh.ts similarity index 80% rename from apps/kitchen-sink/src/app/rapier/instanced-mesh/instanced-mesh.ts rename to apps/examples/src/app/rapier/instanced-mesh/instanced-mesh.ts index fc8c248e..2d8541d8 100644 --- a/apps/kitchen-sink/src/app/rapier/instanced-mesh/instanced-mesh.ts +++ b/apps/examples/src/app/rapier/instanced-mesh/instanced-mesh.ts @@ -7,28 +7,30 @@ import { signal, viewChild, } from '@angular/core'; -import { injectStore, NgtArgs, NgtThreeEvent, NON_ROOT } from 'angular-three'; +import { checkUpdate, injectStore, NgtArgs, NgtThreeEvent } from 'angular-three'; import { NgtrInstancedRigidBodies, NgtrInstancedRigidBodyOptions } from 'angular-three-rapier'; import { Color, InstancedMesh } from 'three'; -import { injectSuzanne } from '../suzanne'; +import { ResetOrbitControls } from '../reset-orbit-controls'; +import { suzanneResource } from '../suzanne'; const MAX_COUNT = 2000; @Component({ + selector: 'app-rapier-instanced-mesh', template: ` - @if (gltf(); as gltf) { + @if (gltf.value(); as gltf) { @@ -36,19 +38,18 @@ const MAX_COUNT = 2000; } `, + hostDirectives: [ResetOrbitControls], schemas: [CUSTOM_ELEMENTS_SCHEMA], changeDetection: ChangeDetectionStrategy.OnPush, host: { class: 'instanced-mesh-rapier' }, imports: [NgtrInstancedRigidBodies, NgtArgs], }) -export class InstancedMeshExample { - static [NON_ROOT] = true; - +export default class InstancedMeshExample { protected readonly MAX_COUNT = MAX_COUNT; private instancedMeshRef = viewChild>('instancedMesh'); - protected gltf = injectSuzanne(); + protected gltf = suzanneResource(); private store = injectStore(); protected bodies = signal(Array.from({ length: 100 }, () => this.createBody())); @@ -61,9 +62,7 @@ export class InstancedMeshExample { for (let i = 0; i < MAX_COUNT; i++) { instancedMesh.setColorAt(i, new Color(Math.random() * 0xffffff)); } - if (instancedMesh.instanceColor) { - instancedMesh.instanceColor.needsUpdate = true; - } + checkUpdate(instancedMesh.instanceColor); }); effect((onCleanup) => { diff --git a/apps/kitchen-sink/src/app/rapier/joints/joints.ts b/apps/examples/src/app/rapier/joints/joints.ts similarity index 83% rename from apps/kitchen-sink/src/app/rapier/joints/joints.ts rename to apps/examples/src/app/rapier/joints/joints.ts index 3e46460e..2bc43574 100644 --- a/apps/kitchen-sink/src/app/rapier/joints/joints.ts +++ b/apps/examples/src/app/rapier/joints/joints.ts @@ -8,15 +8,16 @@ import { viewChild, viewChildren, } from '@angular/core'; -import { injectBeforeRender, NgtVector3, NON_ROOT } from 'angular-three'; -import { injectPrismaticJoint, injectSphericalJoint, NgtrRigidBody, NgtrRigidBodyType } from 'angular-three-rapier'; +import { beforeRender, NgtVector3 } from 'angular-three'; +import { NgtrRigidBody, NgtrRigidBodyType, prismaticJoint, sphericalJoint } from 'angular-three-rapier'; import { Quaternion, Vector3 } from 'three'; +import { ResetOrbitControls } from '../reset-orbit-controls'; @Component({ selector: 'app-rope-segment', imports: [NgtrRigidBody], template: ` - + `, @@ -38,7 +39,7 @@ export class RopeJoint { constructor() { const bodyA = computed(() => this.bodyA().rigidBody()); const bodyB = computed(() => this.bodyB().rigidBody()); - injectSphericalJoint(bodyA, bodyB, { data: { body1Anchor: [-0.5, 0, 0], body2Anchor: [0.5, 0, 0] } }); + sphericalJoint(bodyA, bodyB, { data: { body1Anchor: [-0.5, 0, 0], body2Anchor: [0.5, 0, 0] } }); } } @@ -48,7 +49,7 @@ export class RopeJoint { @for (i of count(); track $index) { - + @@ -80,7 +81,7 @@ export class Rope { const q = new Quaternion(); const v = new Vector3(); - injectBeforeRender(() => { + beforeRender(() => { const now = performance.now(); const ropeSegments = this.ropeSegments(); const firstRope = ropeSegments[0]?.rigidBodyRef()?.rigidBody(); @@ -100,12 +101,12 @@ export class Rope { selector: 'app-prismatic', template: ` - + - + @@ -123,24 +124,24 @@ export class Prismatic { constructor() { const bodyA = computed(() => this.bodyA().rigidBody()); const bodyB = computed(() => this.bodyB().rigidBody()); - injectPrismaticJoint(bodyA, bodyB, { + prismaticJoint(bodyA, bodyB, { data: { body1Anchor: [-4, 0, 0], body2Anchor: [0, 4, 0], axis: [1, 0, 0], limits: [-2, 2] }, }); } } @Component({ + selector: 'app-rapier-joints', template: ` `, + hostDirectives: [ResetOrbitControls], schemas: [CUSTOM_ELEMENTS_SCHEMA], changeDetection: ChangeDetectionStrategy.OnPush, host: { class: 'joints-rapier' }, imports: [Rope, Prismatic], }) -export class JointsExample { - static [NON_ROOT] = true; -} +export default class JointsExample {} diff --git a/apps/examples/src/app/rapier/kinematics/kinematics.ts b/apps/examples/src/app/rapier/kinematics/kinematics.ts new file mode 100644 index 00000000..d60e2dad --- /dev/null +++ b/apps/examples/src/app/rapier/kinematics/kinematics.ts @@ -0,0 +1,100 @@ +import { ChangeDetectionStrategy, Component, CUSTOM_ELEMENTS_SCHEMA, viewChild } from '@angular/core'; +import { beforeRender, NgtArgs } from 'angular-three'; +import { NgtrRigidBody } from 'angular-three-rapier'; +import * as THREE from 'three'; +import { ResetOrbitControls } from '../reset-orbit-controls'; + +@Component({ + selector: 'app-ball', + template: ` + + + + + + + `, + changeDetection: ChangeDetectionStrategy.OnPush, + schemas: [CUSTOM_ELEMENTS_SCHEMA], + imports: [NgtrRigidBody], +}) +export class Ball { + private rigidBodyRef = viewChild.required(NgtrRigidBody); + + constructor() { + beforeRender(() => { + const rigidBody = this.rigidBodyRef().rigidBody(); + if (!rigidBody) return; + if (rigidBody.translation().y < -10) { + rigidBody.setTranslation({ x: Math.random() * 2, y: 20, z: 0 }, true); + rigidBody.setLinvel({ x: 0, y: 0, z: 0 }, true); + } + }); + } +} + +@Component({ + selector: 'app-kinematics-rapier', + template: ` + + + + + + + + + + + + + + + + + + + + + + `, + hostDirectives: [ResetOrbitControls], + changeDetection: ChangeDetectionStrategy.OnPush, + schemas: [CUSTOM_ELEMENTS_SCHEMA], + imports: [Ball, NgtrRigidBody, NgtArgs], +}) +export default class KinematicsExample { + private torusRef = viewChild.required('torus', { read: NgtrRigidBody }); + private platformRef = viewChild.required('platform', { read: NgtrRigidBody }); + + constructor() { + beforeRender(() => { + const now = performance.now(); + + const torus = this.torusRef().rigidBody(); + if (torus) { + const euler = new THREE.Euler(now / 1000, 0, 0); + torus.setNextKinematicRotation(new THREE.Quaternion().setFromEuler(euler)); + } + + const platform = this.platformRef().rigidBody(); + if (platform) { + platform.setNextKinematicTranslation({ + x: Math.sin(now / 100), + y: -8 + Math.sin(now / 50) * 0.5, + z: 0, + }); + } + }); + } +} diff --git a/apps/examples/src/app/rapier/locked-transforms/locked-transforms.ts b/apps/examples/src/app/rapier/locked-transforms/locked-transforms.ts new file mode 100644 index 00000000..e1c7cb71 --- /dev/null +++ b/apps/examples/src/app/rapier/locked-transforms/locked-transforms.ts @@ -0,0 +1,72 @@ +import { ChangeDetectionStrategy, Component, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'; +import { NgtrRigidBody } from 'angular-three-rapier'; +import { NgtsHTML } from 'angular-three-soba/misc'; +import { ResetOrbitControls } from '../reset-orbit-controls'; + +@Component({ + selector: 'app-rapier-locked-transforms', + template: ` + + + + + + +
Locked Rotations
+
+
+
+ + + + + + +
Locked Translations
+
+
+
+ + + + + + +
Enabled Rotations [true, false, false]
+
+
+
+ + + + + + +
Enabled Translations [true, false, false]
+
+
+
+
+ `, + schemas: [CUSTOM_ELEMENTS_SCHEMA], + hostDirectives: [ResetOrbitControls], + changeDetection: ChangeDetectionStrategy.OnPush, + imports: [NgtrRigidBody, NgtsHTML], +}) +export default class LockedTransformsExample {} diff --git a/apps/examples/src/app/rapier/map.glb b/apps/examples/src/app/rapier/map.glb new file mode 100644 index 00000000..9241f26e Binary files /dev/null and b/apps/examples/src/app/rapier/map.glb differ diff --git a/apps/examples/src/app/rapier/objects.glb b/apps/examples/src/app/rapier/objects.glb new file mode 100644 index 00000000..6efbc326 Binary files /dev/null and b/apps/examples/src/app/rapier/objects.glb differ diff --git a/apps/kitchen-sink/src/app/rapier/performance/performance.ts b/apps/examples/src/app/rapier/performance/performance.ts similarity index 70% rename from apps/kitchen-sink/src/app/rapier/performance/performance.ts rename to apps/examples/src/app/rapier/performance/performance.ts index 4f52033c..d0bacaf3 100644 --- a/apps/kitchen-sink/src/app/rapier/performance/performance.ts +++ b/apps/examples/src/app/rapier/performance/performance.ts @@ -8,20 +8,27 @@ import { signal, viewChild, } from '@angular/core'; -import { injectBeforeRender, NgtVector3, NON_ROOT } from 'angular-three'; +import { beforeRender, NgtVector3 } from 'angular-three'; import { NgtrRigidBody } from 'angular-three-rapier'; -import { injectGLTF } from 'angular-three-soba/loaders'; +import { gltfResource } from 'angular-three-soba/loaders'; import { Mesh, Vector3Like } from 'three'; import { GLTF } from 'three-stdlib'; -import { injectSuzanne } from '../suzanne'; +import { ResetOrbitControls } from '../reset-orbit-controls'; +import { suzanneResource } from '../suzanne'; @Component({ selector: 'app-monkey', template: ` - @if (gltf(); as gltf) { - - - + @if (gltf.value(); as gltf) { + + + } @@ -31,15 +38,15 @@ import { injectSuzanne } from '../suzanne'; imports: [NgtrRigidBody], }) export class Monkey { - position = input([0, 0, 0]); + position = input([0, 0, 0]); dead = output(); - protected gltf = injectSuzanne(); + protected gltf = suzanneResource(); private rigidBody = viewChild(NgtrRigidBody); constructor() { - injectBeforeRender(() => { + beforeRender(() => { const rigidBody = this.rigidBody()?.rigidBody(); if (!rigidBody) return; if (rigidBody.translation().y < -10) { @@ -72,7 +79,11 @@ export class MonkeySwarm { ...prev, { key: Math.random() + Date.now(), - position: [Math.random() * 10 - 5, Math.random(), Math.random() * 10 - 5] as [number, number, number], + position: [Math.random() * 10 - 5, Math.random(), Math.random() * 10 - 5] as [ + number, + number, + number, + ], }, ]); }, 50); @@ -95,9 +106,9 @@ type BendyGLTF = GLTF & { selector: 'app-bendy', template: ` - @if (gltf(); as gltf) { - - + @if (gltf.value(); as gltf) { + + @@ -112,10 +123,11 @@ export class Bendy { position = input([0, 0, 0]); scale = input([1, 1, 1]); - protected gltf = injectGLTF(() => './bendy.glb'); + protected gltf = gltfResource(() => './bendy.glb'); } @Component({ + selector: 'app-rapier-performance', template: ` @@ -125,11 +137,10 @@ export class Bendy { `, + hostDirectives: [ResetOrbitControls], schemas: [CUSTOM_ELEMENTS_SCHEMA], changeDetection: ChangeDetectionStrategy.OnPush, host: { class: 'performance-rapier' }, imports: [Bendy, MonkeySwarm], }) -export class PerformanceExample { - static [NON_ROOT] = true; -} +export default class PerformanceExample {} diff --git a/apps/examples/src/app/rapier/plinko.glb b/apps/examples/src/app/rapier/plinko.glb new file mode 100644 index 00000000..9b286cd5 Binary files /dev/null and b/apps/examples/src/app/rapier/plinko.glb differ diff --git a/apps/examples/src/app/rapier/rapier.routes.ts b/apps/examples/src/app/rapier/rapier.routes.ts new file mode 100644 index 00000000..85f18288 --- /dev/null +++ b/apps/examples/src/app/rapier/rapier.routes.ts @@ -0,0 +1,105 @@ +import { Routes } from '@angular/router'; +import { provideResetOrbitControls } from './reset-orbit-controls'; + +const routes: Routes = [ + { + path: '', + loadComponent: () => import('./wrapper'), + children: [ + { + path: 'basic', + loadComponent: () => import('./basic/basic'), + }, + { + path: 'rope-joint', + providers: [provideResetOrbitControls(50, [0, 0.25, 0.75])], + loadComponent: () => import('./rope-joint/rope-joint'), + }, + { + path: 'spring', + providers: [provideResetOrbitControls(8)], + loadComponent: () => import('./spring/spring'), + }, + { + path: 'cluster', + loadComponent: () => import('./cluster/cluster'), + }, + { + path: 'instanced-mesh', + providers: [provideResetOrbitControls(30)], + loadComponent: () => import('./instanced-mesh/instanced-mesh'), + }, + { + path: 'joints', + loadComponent: () => import('./joints/joints'), + }, + { + path: 'performance', + providers: [provideResetOrbitControls(15)], + loadComponent: () => import('./performance/performance'), + }, + { + path: 'all-colliders', + providers: [provideResetOrbitControls(30)], + loadComponent: () => import('./all-colliders/all-colliders'), + }, + { + path: 'sensors', + providers: [provideResetOrbitControls(30)], + loadComponent: () => import('./sensors/sensors'), + }, + { + path: 'contact-force-events', + providers: [provideResetOrbitControls(10)], + loadComponent: () => import('./contact-force-events/contact-force-events'), + }, + { + path: 'active-collision-types', + providers: [provideResetOrbitControls(10)], + loadComponent: () => import('./active-collision-types/active-collision-types'), + }, + { + path: 'attractors', + providers: [provideResetOrbitControls(40)], + loadComponent: () => import('./attractors/attractors'), + }, + { + path: 'kinematics', + providers: [provideResetOrbitControls(30)], + loadComponent: () => import('./kinematics/kinematics'), + }, + { + path: 'car', + providers: [provideResetOrbitControls(30)], + loadComponent: () => import('./car/car'), + }, + { + path: 'locked-transforms', + providers: [provideResetOrbitControls(30)], + loadComponent: () => import('./locked-transforms/locked-transforms'), + }, + { + path: 'cradle', + loadComponent: () => import('./cradle/cradle'), + }, + { + path: 'all-shapes', + loadComponent: () => import('./all-shapes/all-shapes'), + }, + { + path: '', + redirectTo: 'basic', + pathMatch: 'full', + }, + ], + data: { + credits: { + title: 'React Three Rapier', + link: 'https://react-three-rapier.pmnd.rs', + class: 'left-2', + }, + }, + }, +]; + +export default routes; diff --git a/apps/kitchen-sink/src/app/rapier/rapier.ts b/apps/examples/src/app/rapier/rapier.ts similarity index 83% rename from apps/kitchen-sink/src/app/rapier/rapier.ts rename to apps/examples/src/app/rapier/rapier.ts index 99e3be4b..2edd9889 100644 --- a/apps/kitchen-sink/src/app/rapier/rapier.ts +++ b/apps/examples/src/app/rapier/rapier.ts @@ -1,11 +1,6 @@ import { ChangeDetectionStrategy, Component } from '@angular/core'; import { RouterLink, RouterLinkActive, RouterOutlet } from '@angular/router'; -import { extend } from 'angular-three'; -import * as THREE from 'three'; - -import { SCENES_MAP } from './constants'; - -extend(THREE); +import routes from './rapier.routes'; @Component({ template: ` @@ -34,5 +29,5 @@ extend(THREE); host: { class: 'rapier' }, }) export default class Rapier { - protected examples = Object.keys(SCENES_MAP); + protected examples = routes[0].children?.filter((route) => !!route.path).map((route) => route.path) || []; } diff --git a/apps/examples/src/app/rapier/reset-orbit-controls.ts b/apps/examples/src/app/rapier/reset-orbit-controls.ts new file mode 100644 index 00000000..d3319a63 --- /dev/null +++ b/apps/examples/src/app/rapier/reset-orbit-controls.ts @@ -0,0 +1,52 @@ +import { Directive, effect, inject, InjectionToken, Provider } from '@angular/core'; +import { injectStore } from 'angular-three'; +import * as THREE from 'three'; +import { OrbitControls } from 'three-stdlib'; + +const directionVector = new THREE.Vector3(); +const RESET_ORBIT_CONTROLS_DISTANCE = new InjectionToken('distance'); +const RESET_ORBIT_CONTROLS_DIR = new InjectionToken<[number, number, number]>('dir'); + +export function provideResetOrbitControls(distance: number, dir?: [number, number, number]) { + const providers: Provider = [{ provide: RESET_ORBIT_CONTROLS_DISTANCE, useValue: distance }]; + + if (dir) { + providers.push({ provide: RESET_ORBIT_CONTROLS_DIR, useValue: dir }); + } + + return providers; +} + +function injectResetOrbitControls() { + const distance = inject(RESET_ORBIT_CONTROLS_DISTANCE, { optional: true }) || 20; + const dir = inject(RESET_ORBIT_CONTROLS_DIR, { optional: true }) || [0, 0, 1]; + return { distance, dir }; +} + +@Directive() +export class ResetOrbitControls { + constructor() { + const { distance, dir } = injectResetOrbitControls(); + const store = injectStore(); + + effect(() => { + const controls = store.controls() as OrbitControls; + if (!controls) return; + + const camera = controls.object; // This is the camera that OrbitControls is controlling + + // Get the current look-at target + const target = controls.target; + target.set(0, 0, 0); + + // Calculate the direction vector from target to camera + directionVector.fromArray(dir).normalize(); + + // Set the new camera position + camera.position.copy(target).add(directionVector.multiplyScalar(distance)); + + // Update the controls + controls.update(); + }); + } +} diff --git a/apps/kitchen-sink/src/app/rapier/rope-joint/rope-joint.ts b/apps/examples/src/app/rapier/rope-joint/rope-joint.ts similarity index 65% rename from apps/kitchen-sink/src/app/rapier/rope-joint/rope-joint.ts rename to apps/examples/src/app/rapier/rope-joint/rope-joint.ts index 8721a9de..bd6371d3 100644 --- a/apps/kitchen-sink/src/app/rapier/rope-joint/rope-joint.ts +++ b/apps/examples/src/app/rapier/rope-joint/rope-joint.ts @@ -1,24 +1,15 @@ -import { - afterNextRender, - ChangeDetectionStrategy, - Component, - computed, - CUSTOM_ELEMENTS_SCHEMA, - inject, - Injector, - input, - viewChild, -} from '@angular/core'; -import { NgtArgs, NgtVector3, NON_ROOT } from 'angular-three'; -import { injectRopeJoint, NgtrBallCollider, NgtrRigidBody } from 'angular-three-rapier'; +import { ChangeDetectionStrategy, Component, computed, CUSTOM_ELEMENTS_SCHEMA, input, viewChild } from '@angular/core'; +import { NgtArgs, NgtVector3 } from 'angular-three'; +import { NgtrBallCollider, NgtrRigidBody, ropeJoint } from 'angular-three-rapier'; +import { ResetOrbitControls } from '../reset-orbit-controls'; const WALL_COLORS = ['#50514F', '#CBD4C2', '#FFFCFF', '#247BA0', '#C3B299']; @Component({ selector: 'app-floor', template: ` - - + + @@ -33,11 +24,11 @@ export class Floor {} @Component({ selector: 'app-box-wall', template: ` - + @for (row of rows(); track row) { @for (column of columns(); track column) { - - + + @@ -65,21 +56,21 @@ export class BoxWall { template: ` - + - + - + `, @@ -96,21 +87,17 @@ export class RopeJoint { private ballBody = viewChild.required('ball', { read: NgtrRigidBody }); constructor() { - const injector = inject(Injector); + const anchorBody = computed(() => this.anchorBody().rigidBody()); + const ballBody = computed(() => this.ballBody().rigidBody()); - afterNextRender(() => { - const anchorBody = computed(() => this.anchorBody().rigidBody()); - const ballBody = computed(() => this.ballBody().rigidBody()); - - injectRopeJoint(anchorBody, ballBody, { - injector, - data: { body1Anchor: [0, 0, 0], body2Anchor: [0, 0, 0], length: this.length() }, - }); + ropeJoint(anchorBody, ballBody, { + data: () => ({ body1Anchor: [0, 0, 0], body2Anchor: [0, 0, 0], length: this.length() }), }); } } @Component({ + selector: 'app-rapier-rope-joint', template: ` @@ -118,11 +105,10 @@ export class RopeJoint { `, + hostDirectives: [ResetOrbitControls], schemas: [CUSTOM_ELEMENTS_SCHEMA], changeDetection: ChangeDetectionStrategy.OnPush, host: { class: 'rope-joint-rapier' }, imports: [Floor, BoxWall, RopeJoint], }) -export class RopeJointExample { - static [NON_ROOT] = true; -} +export default class RopeJointExample {} diff --git a/apps/examples/src/app/rapier/sensors/sensors.ts b/apps/examples/src/app/rapier/sensors/sensors.ts new file mode 100644 index 00000000..21807a4c --- /dev/null +++ b/apps/examples/src/app/rapier/sensors/sensors.ts @@ -0,0 +1,105 @@ +import { ChangeDetectionStrategy, Component, CUSTOM_ELEMENTS_SCHEMA, effect, signal, viewChild } from '@angular/core'; +import { RigidBody } from '@dimforge/rapier3d-compat'; +import { beforeRender } from 'angular-three'; +import { NgtrCuboidCollider, NgtrRigidBody } from 'angular-three-rapier'; +import { NgtsText } from 'angular-three-soba/abstractions'; +import * as THREE from 'three'; +import { ResetOrbitControls } from '../reset-orbit-controls'; + +const material = new THREE.MeshPhysicalMaterial(); + +@Component({ + selector: 'app-goal', + template: ` + + @for (goal of goalTransforms; track $index) { + + + + } + + @if (intersecting()) { + + } + + + + `, + imports: [NgtrRigidBody, NgtsText, NgtrCuboidCollider], + changeDetection: ChangeDetectionStrategy.OnPush, + schemas: [CUSTOM_ELEMENTS_SCHEMA], +}) +export class Goal { + protected readonly material = material; + + protected goalTransforms = [ + { scale: [11, 1, 1], position: [0, 3, 0] }, + { scale: [1, 6, 1], position: [-5, 0, 0] }, + { scale: [1, 6, 1], position: [5, 0, 0] }, + { scale: [1, 1, 3], position: [-5, -3, 0] }, + { scale: [1, 1, 3], position: [5, -3, 0] }, + ]; + + protected intersecting = signal(false); +} + +@Component({ + selector: 'app-ball', + template: ` + + + + + + `, + imports: [NgtrRigidBody], + changeDetection: ChangeDetectionStrategy.OnPush, + schemas: [CUSTOM_ELEMENTS_SCHEMA], +}) +export class Ball { + protected readonly material = material; + + private rigidBody = viewChild.required(NgtrRigidBody); + + constructor() { + effect(() => { + const rigidBody = this.rigidBody().rigidBody(); + if (!rigidBody) return; + this.restart(rigidBody); + }); + + beforeRender(() => { + const rigidBody = this.rigidBody().rigidBody(); + if (!rigidBody) return; + if (rigidBody.translation().z > 10) { + this.restart(rigidBody); + } + }); + } + + private restart(rigidBody: RigidBody) { + rigidBody.setTranslation({ x: 0, y: -7, z: -24 }, true); + rigidBody.setLinvel({ x: 0, y: 0, z: 7 }, true); + } +} + +@Component({ + selector: 'app-rapier-sensors', + template: ` + + + + + `, + hostDirectives: [ResetOrbitControls], + imports: [Goal, Ball], + changeDetection: ChangeDetectionStrategy.OnPush, + schemas: [CUSTOM_ELEMENTS_SCHEMA], +}) +export default class SensorsExample {} diff --git a/apps/kitchen-sink/src/app/rapier/spring/spring.ts b/apps/examples/src/app/rapier/spring/spring.ts similarity index 55% rename from apps/kitchen-sink/src/app/rapier/spring/spring.ts rename to apps/examples/src/app/rapier/spring/spring.ts index 7758ba05..43c057fb 100644 --- a/apps/kitchen-sink/src/app/rapier/spring/spring.ts +++ b/apps/examples/src/app/rapier/spring/spring.ts @@ -1,23 +1,14 @@ -import { - afterNextRender, - ChangeDetectionStrategy, - Component, - computed, - CUSTOM_ELEMENTS_SCHEMA, - inject, - Injector, - input, - viewChild, -} from '@angular/core'; -import { NgtArgs, NgtVector3, NON_ROOT, vector3 } from 'angular-three'; -import { injectSpringJoint, NgtrBallCollider, NgtrRigidBody } from 'angular-three-rapier'; +import { ChangeDetectionStrategy, Component, computed, CUSTOM_ELEMENTS_SCHEMA, input, viewChild } from '@angular/core'; +import { NgtArgs, NgtVector3, vector3 } from 'angular-three'; +import { NgtrBallCollider, NgtrRigidBody, springJoint } from 'angular-three-rapier'; import { ColorRepresentation } from 'three'; +import { ResetOrbitControls } from '../reset-orbit-controls'; @Component({ selector: 'app-box', template: ` - - + + @@ -36,17 +27,17 @@ export class Box { selector: 'app-ball-spring', template: ` - + - + `, schemas: [CUSTOM_ELEMENTS_SCHEMA], @@ -64,34 +55,30 @@ export class BallSpring { private stiffness = 1.0e3; constructor() { - const injector = inject(Injector); + const floorBody = computed(() => this.floorRigidBody().rigidBody()); + const ballBody = computed(() => this.ballBody().rigidBody()); - afterNextRender(() => { - const floorBody = computed(() => this.floorRigidBody().rigidBody()); - const ballBody = computed(() => this.ballBody().rigidBody()); + const criticalDamping = computed(() => 2 * Math.sqrt(this.stiffness * this.mass())); + const dampingRatio = computed(() => this.jointNum() / (this.total() / 2)); + const damping = computed(() => dampingRatio() * criticalDamping()); + const positionVector = vector3(this.position); - const criticalDamping = computed(() => 2 * Math.sqrt(this.stiffness * this.mass())); - const dampingRatio = computed(() => this.jointNum() / (this.total() / 2)); - const damping = computed(() => dampingRatio() * criticalDamping()); - const positionVector = vector3(this.position); - - injectSpringJoint(ballBody, floorBody, { - injector, - data: { - body1Anchor: [0, 0, 0], - body2Anchor: [positionVector().x, positionVector().y - 3, positionVector().z], - restLength: 0, - stiffness: this.stiffness, - damping: damping(), - }, - }); + springJoint(ballBody, floorBody, { + data: () => ({ + body1Anchor: [0, 0, 0], + body2Anchor: [positionVector().x, positionVector().y - 3, positionVector().z], + restLength: 0, + stiffness: this.stiffness, + damping: damping(), + }), }); } } @Component({ + selector: 'app-rapier-spring', template: ` - + @for (ballPosition of balls; track $index) { @@ -109,14 +96,13 @@ export class BallSpring { } `, + hostDirectives: [ResetOrbitControls], schemas: [CUSTOM_ELEMENTS_SCHEMA], changeDetection: ChangeDetectionStrategy.OnPush, host: { class: 'spring-rapier' }, imports: [NgtrRigidBody, BallSpring, Box], }) -export class SpringExample { - static [NON_ROOT] = true; - +export default class SpringExample { protected readonly COLORS_ARR = ['#335C67', '#FFF3B0', '#E09F3E', '#9E2A2B', '#540B0E']; - protected balls = Array.from({ length: 30 }, (_, i) => [-20 + 1.5 * (i + 1), 7.5, -30] as const); + protected balls = Array.from({ length: 30 }, (_, i) => [-20 + 1.5 * (i + 1), 7.5, -30] as [number, number, number]); } diff --git a/apps/examples/src/app/rapier/susanne.glb b/apps/examples/src/app/rapier/susanne.glb new file mode 100644 index 00000000..9ba08a5c Binary files /dev/null and b/apps/examples/src/app/rapier/susanne.glb differ diff --git a/apps/examples/src/app/rapier/suzanne.ts b/apps/examples/src/app/rapier/suzanne.ts new file mode 100644 index 00000000..82c5676e --- /dev/null +++ b/apps/examples/src/app/rapier/suzanne.ts @@ -0,0 +1,13 @@ +import { gltfResource } from 'angular-three-soba/loaders'; +import { Mesh } from 'three'; +import { GLTF } from 'three-stdlib'; + +type SuzanneGLTF = GLTF & { + nodes: { Suzanne: Mesh }; +}; + +gltfResource.preload('./suzanne.glb'); + +export function suzanneResource() { + return gltfResource(() => './suzanne.glb'); +} diff --git a/apps/examples/src/app/rapier/wrapper-default.ts b/apps/examples/src/app/rapier/wrapper-default.ts new file mode 100644 index 00000000..1f70a318 --- /dev/null +++ b/apps/examples/src/app/rapier/wrapper-default.ts @@ -0,0 +1,66 @@ +import { ChangeDetectionStrategy, Component, CUSTOM_ELEMENTS_SCHEMA, input } from '@angular/core'; +import { RouterOutlet } from '@angular/router'; +import { NgtrPhysics, NgtrRigidBody } from 'angular-three-rapier'; +import { NgtsOrbitControls } from 'angular-three-soba/controls'; +import { NgtsEnvironment } from 'angular-three-soba/staging'; + +@Component({ + selector: 'app-floor', + template: ` + + + + + + + `, + schemas: [CUSTOM_ELEMENTS_SCHEMA], + changeDetection: ChangeDetectionStrategy.OnPush, + imports: [NgtrRigidBody], +}) +export class Floor {} + +@Component({ + selector: 'app-rapier-wrapper-default', + template: ` + + + + + + + + + + @if (o.activatedRoute.snapshot.url[0].path !== 'basic') { + + } + + + `, + schemas: [CUSTOM_ELEMENTS_SCHEMA], + changeDetection: ChangeDetectionStrategy.OnPush, + imports: [NgtrPhysics, NgtsEnvironment, NgtsOrbitControls, Floor, RouterOutlet], + host: { class: 'rapier-wrapper-default' }, +}) +export class RapierWrapperDefault { + debug = input(true); + interpolate = input(false); + paused = input(false); +} diff --git a/apps/examples/src/app/rapier/wrapper.ts b/apps/examples/src/app/rapier/wrapper.ts new file mode 100644 index 00000000..41f3f71d --- /dev/null +++ b/apps/examples/src/app/rapier/wrapper.ts @@ -0,0 +1,26 @@ +import { ChangeDetectionStrategy, Component, signal } from '@angular/core'; +import { TweakpaneCheckbox, TweakpanePane } from 'angular-three-tweakpane'; +import { NgtCanvas } from 'angular-three/dom'; +import { RapierWrapperDefault } from './wrapper-default'; + +@Component({ + template: ` + + + + + + + + + + + `, + changeDetection: ChangeDetectionStrategy.OnPush, + imports: [NgtCanvas, RapierWrapperDefault, TweakpanePane, TweakpaneCheckbox], +}) +export default class RapierWrapper { + protected debug = signal(true); + protected interpolate = signal(false); + protected paused = signal(false); +} diff --git a/apps/kitchen-sink/src/app/routed-rocks/colored-rock.ts b/apps/examples/src/app/routed-rocks/colored-rock.ts similarity index 97% rename from apps/kitchen-sink/src/app/routed-rocks/colored-rock.ts rename to apps/examples/src/app/routed-rocks/colored-rock.ts index 0c939d2f..22efb100 100644 --- a/apps/kitchen-sink/src/app/routed-rocks/colored-rock.ts +++ b/apps/examples/src/app/routed-rocks/colored-rock.ts @@ -9,8 +9,8 @@ import { RockStore } from './store'; @if (selectedRock(); as rock) { ({ + id: index + 1, + slug: color.slug, + label: color.label, + name: `rock-${color.slug}`, + path: `/routed-rocks/rocks/${color.slug}`, + color: color.color, + angle: ((360 / colors.length) * index * Math.PI) / 180, +})); diff --git a/apps/kitchen-sink/src/app/routed-rocks/cursor.ts b/apps/examples/src/app/routed-rocks/cursor.ts similarity index 75% rename from apps/kitchen-sink/src/app/routed-rocks/cursor.ts rename to apps/examples/src/app/routed-rocks/cursor.ts index 7be8da70..81fe82d6 100644 --- a/apps/kitchen-sink/src/app/routed-rocks/cursor.ts +++ b/apps/examples/src/app/routed-rocks/cursor.ts @@ -1,6 +1,6 @@ import { DOCUMENT } from '@angular/common'; import { Directive, ElementRef, inject } from '@angular/core'; -import { getLocalState, injectObjectEvents } from 'angular-three'; +import { getInstanceState, objectEvents } from 'angular-three'; import { Object3D } from 'three'; @Directive({ selector: '[cursor]' }) @@ -11,12 +11,12 @@ export class Cursor { if (!nativeElement.isObject3D) return; - const localState = getLocalState(nativeElement); - if (!localState) return; + const instanceState = getInstanceState(nativeElement); + if (!instanceState) return; const document = inject(DOCUMENT); - injectObjectEvents(() => nativeElement, { + objectEvents(() => nativeElement, { pointerover: () => { document.body.style.cursor = 'pointer'; }, diff --git a/apps/kitchen-sink/src/app/routed-rocks/rocks.routes.ts b/apps/examples/src/app/routed-rocks/rocks.routes.ts similarity index 100% rename from apps/kitchen-sink/src/app/routed-rocks/rocks.routes.ts rename to apps/examples/src/app/routed-rocks/rocks.routes.ts diff --git a/apps/kitchen-sink/src/app/routed-rocks/rocks.ts b/apps/examples/src/app/routed-rocks/rocks.ts similarity index 75% rename from apps/kitchen-sink/src/app/routed-rocks/rocks.ts rename to apps/examples/src/app/routed-rocks/rocks.ts index 2ad5a999..4bfe449b 100644 --- a/apps/kitchen-sink/src/app/routed-rocks/rocks.ts +++ b/apps/examples/src/app/routed-rocks/rocks.ts @@ -1,8 +1,8 @@ -import { ChangeDetectionStrategy, Component, CUSTOM_ELEMENTS_SCHEMA, effect, inject, Signal } from '@angular/core'; -import { Router } from '@angular/router'; -import { injectStore, NgtArgs, NgtRouterOutlet } from 'angular-three'; +import { ChangeDetectionStrategy, Component, CUSTOM_ELEMENTS_SCHEMA, effect, inject } from '@angular/core'; +import { Router, RouterOutlet } from '@angular/router'; +import { injectStore, NgtArgs } from 'angular-three'; import { NgtsCameraControls } from 'angular-three-soba/controls'; -import { injectGLTF } from 'angular-three-soba/loaders'; +import { gltfResource } from 'angular-three-soba/loaders'; import CameraControls from 'camera-controls'; import { DoubleSide, FrontSide, Mesh, MeshStandardMaterial } from 'three'; import { GLTF } from 'three-stdlib'; @@ -21,27 +21,27 @@ interface RockGLTF extends GLTF { - + - + - @if (gltf(); as gltf) { + @if (gltf.value(); as gltf) { @@ -60,9 +66,9 @@ interface RockGLTF extends GLTF { @@ -79,9 +85,9 @@ interface RockGLTF extends GLTF { } - + `, - imports: [NgtRouterOutlet, NgtArgs, NgtsCameraControls, Cursor], + imports: [RouterOutlet, NgtArgs, NgtsCameraControls, Cursor], schemas: [CUSTOM_ELEMENTS_SCHEMA], changeDetection: ChangeDetectionStrategy.OnPush, host: { class: 'rocks' }, @@ -97,20 +103,17 @@ export default class Rocks { private rockStore = inject(RockStore); private store = injectStore(); - private scene = this.store.select('scene'); - private controls = this.store.select('controls') as Signal; - - protected gltf = injectGLTF(() => './rock2/scene.gltf'); + protected gltf = gltfResource(() => './rock2/scene.gltf'); constructor() { effect(() => { - const controls = this.controls(); + const controls = this.store.controls() as CameraControls; if (!controls) return; - const gltf = this.gltf(); + const gltf = this.gltf.value(); if (!gltf) return; - const scene = this.scene(); + const scene = this.store.scene(); const rock = this.rockStore.selectedRock(); const obj = rock ? scene.getObjectByName(rock.name) : gltf.scene; diff --git a/apps/kitchen-sink/src/app/routed-rocks/routed-rocks.routes.ts b/apps/examples/src/app/routed-rocks/routed-rocks.routes.ts similarity index 100% rename from apps/kitchen-sink/src/app/routed-rocks/routed-rocks.routes.ts rename to apps/examples/src/app/routed-rocks/routed-rocks.routes.ts diff --git a/apps/kitchen-sink/src/app/routed-rocks/routed-rocks.ts b/apps/examples/src/app/routed-rocks/routed-rocks.ts similarity index 53% rename from apps/kitchen-sink/src/app/routed-rocks/routed-rocks.ts rename to apps/examples/src/app/routed-rocks/routed-rocks.ts index 4e052d41..040cd1bb 100644 --- a/apps/kitchen-sink/src/app/routed-rocks/routed-rocks.ts +++ b/apps/examples/src/app/routed-rocks/routed-rocks.ts @@ -1,15 +1,15 @@ import { ChangeDetectionStrategy, Component } from '@angular/core'; -import { extend, NgtCanvas } from 'angular-three'; -import * as THREE from 'three'; +import { NgtRoutedScene } from 'angular-three'; +import { NgtCanvas } from 'angular-three/dom'; import { RockStore } from './store'; -extend(THREE); - @Component({ template: ` - + + + `, - imports: [NgtCanvas], + imports: [NgtCanvas, NgtRoutedScene], providers: [RockStore], changeDetection: ChangeDetectionStrategy.OnPush, host: { class: 'routed-rocks block h-svh' }, diff --git a/apps/kitchen-sink/src/app/routed-rocks/store.ts b/apps/examples/src/app/routed-rocks/store.ts similarity index 100% rename from apps/kitchen-sink/src/app/routed-rocks/store.ts rename to apps/examples/src/app/routed-rocks/store.ts diff --git a/apps/kitchen-sink/src/app/routed/bomb-gp.glb b/apps/examples/src/app/routed/bomb-gp.glb similarity index 100% rename from apps/kitchen-sink/src/app/routed/bomb-gp.glb rename to apps/examples/src/app/routed/bomb-gp.glb diff --git a/apps/kitchen-sink/src/app/routed/bomb.ts b/apps/examples/src/app/routed/bomb.ts similarity index 79% rename from apps/kitchen-sink/src/app/routed/bomb.ts rename to apps/examples/src/app/routed/bomb.ts index eeb0a095..e0fbff4d 100644 --- a/apps/kitchen-sink/src/app/routed/bomb.ts +++ b/apps/examples/src/app/routed/bomb.ts @@ -1,10 +1,10 @@ import { ChangeDetectionStrategy, Component, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'; -import { injectGLTF } from 'angular-three-soba/loaders'; +import { gltfResource } from 'angular-three-soba/loaders'; import { NgtsMeshTransmissionMaterial } from 'angular-three-soba/materials'; import { Mesh } from 'three'; import { GLTF } from 'three-stdlib'; -import bombUrl from './bomb-gp.glb'; +import bombUrl from './bomb-gp.glb' with { loader: 'file' }; interface BombGLTF extends GLTF { nodes: { @@ -15,7 +15,7 @@ interface BombGLTF extends GLTF { @Component({ selector: 'app-bomb', template: ` - @if (gltf(); as gltf) { + @if (gltf.value(); as gltf) { (() => bombUrl); + protected gltf = gltfResource(() => bombUrl); } diff --git a/apps/kitchen-sink/src/app/routed/current-route.ts b/apps/examples/src/app/routed/current.ts similarity index 94% rename from apps/kitchen-sink/src/app/routed/current-route.ts rename to apps/examples/src/app/routed/current.ts index 449d94da..481594a8 100644 --- a/apps/kitchen-sink/src/app/routed/current-route.ts +++ b/apps/examples/src/app/routed/current.ts @@ -3,7 +3,7 @@ import { NgtVector3 } from 'angular-three'; import { NgtsText } from 'angular-three-soba/abstractions'; @Component({ - selector: 'app-current-route', + selector: 'app-current', template: ` - + - + @@ -41,8 +42,8 @@ import { CurrentRoute } from './current-route'; `, schemas: [CUSTOM_ELEMENTS_SCHEMA], + changeDetection: ChangeDetectionStrategy.OnPush, imports: [ - NgtRouterOutlet, NgtArgs, NgtsFloat, CurrentRoute, @@ -52,11 +53,10 @@ import { CurrentRoute } from './current-route'; NgtpEffectComposer, NgtpN8AO, NgtpTiltShift2, + RouterOutlet, ], }) -export class CustomRoutedScene { - static [ROUTED_SCENE] = true; - +export class RoutedScene { protected readonly Math = Math; private router = inject(Router); @@ -70,7 +70,7 @@ export class CustomRoutedScene { ); constructor() { - injectBeforeRender(({ camera, pointer, delta }) => { + beforeRender(({ camera, pointer, delta }) => { easing.damp3( camera.position, [Math.sin(-pointer.x) * 5, pointer.y * 3.5, 15 + Math.cos(pointer.x) * 10], diff --git a/apps/kitchen-sink/src/app/routed/routed.routes.ts b/apps/examples/src/app/routed/routed.routes.ts similarity index 82% rename from apps/kitchen-sink/src/app/routed/routed.routes.ts rename to apps/examples/src/app/routed/routed.routes.ts index b15fffe3..92a7f176 100644 --- a/apps/kitchen-sink/src/app/routed/routed.routes.ts +++ b/apps/examples/src/app/routed/routed.routes.ts @@ -13,11 +13,7 @@ const routes: Routes = [ path: 'bomb', loadComponent: () => import('./bomb'), }, - { - path: '', - redirectTo: 'knot', - pathMatch: 'full', - }, + { path: '', redirectTo: 'knot', pathMatch: 'full' }, ]; export default routes; diff --git a/apps/kitchen-sink/src/app/routed/routed.ts b/apps/examples/src/app/routed/routed.ts similarity index 64% rename from apps/kitchen-sink/src/app/routed/routed.ts rename to apps/examples/src/app/routed/routed.ts index ab89f698..a880bd34 100644 --- a/apps/kitchen-sink/src/app/routed/routed.ts +++ b/apps/examples/src/app/routed/routed.ts @@ -1,18 +1,17 @@ import { ChangeDetectionStrategy, Component } from '@angular/core'; import { RouterLink, RouterLinkActive } from '@angular/router'; -import { extend, NgtCanvas } from 'angular-three'; -import * as THREE from 'three'; -import { CustomRoutedScene } from './custom-routed-scene'; - -extend(THREE); +import { NgtCanvas } from 'angular-three/dom'; +import { RoutedScene } from './routed-scene'; @Component({ template: `
- + + +
-