From 7adf33d3c5f9d1f61d068c7e61371716fe638e02 Mon Sep 17 00:00:00 2001 From: Chau Tran Date: Sun, 28 Nov 2021 00:34:19 -0600 Subject: [PATCH] feat(core): ready to start core generators --- .../box-geometry/box-geometry.directive.ts | 10 +- nx.json | 9 +- package.json | 4 +- packages/core/audios/README.md | 3 + packages/core/audios/ng-package.json | 5 + packages/core/audios/src/index.ts | 1 + .../audio-listener.directive.ts | 83 +++++ packages/core/cameras/README.md | 3 + packages/core/cameras/ng-package.json | 5 + .../src/cube-camera/cube-camera.directive.ts | 51 +++ packages/core/cameras/src/index.ts | 1 + packages/core/geometries/README.md | 3 + packages/core/geometries/ng-package.json | 5 + .../box-geometry/box-geometry.directive.ts | 32 ++ packages/core/geometries/src/index.ts | 1 + packages/core/group/README.md | 3 + packages/core/group/ng-package.json | 5 + packages/core/group/src/index.ts | 1 + .../core/group/src/lib/group.directive.ts | 40 ++ packages/core/materials/README.md | 3 + packages/core/materials/ng-package.json | 5 + packages/core/materials/src/index.ts | 1 + .../mesh-basic-material.directive.ts | 31 ++ packages/core/meshes/README.md | 3 + packages/core/meshes/ng-package.json | 5 + packages/core/meshes/src/index.ts | 3 + .../instanced-mesh.directive.ts | 30 ++ .../core/meshes/src/mesh/mesh.directive.ts | 23 ++ .../skinned-mesh/skinned-mesh.directive.ts | 139 +++++++ packages/core/points/README.md | 3 + packages/core/points/ng-package.json | 5 + packages/core/points/src/index.ts | 1 + .../core/points/src/lib/points.directive.ts | 34 ++ packages/core/project.json | 18 +- packages/core/src/index.ts | 24 ++ packages/core/src/lib/canvas.component.ts | 57 +-- .../animation-subscriber.controller.ts | 78 ++++ .../src/lib/controllers/audio.controller.ts | 60 +++ .../core/src/lib/controllers/controller.ts | 104 ++++++ .../material-geometry.controller.ts | 228 ++++++++++++ .../object-3d-inputs.controller.ts | 175 +++++++++ .../lib/controllers/object-3d.controller.ts | 343 ++++++++++++++++++ packages/core/src/lib/core.module.ts | 14 +- packages/core/src/lib/models/common.ts | 15 +- packages/core/src/lib/models/events.ts | 6 +- .../models/states/animation-frame-state.ts | 5 - .../src/lib/models/states/events-state.ts | 4 +- packages/core/src/lib/models/states/state.ts | 1 + packages/core/src/lib/models/three.ts | 6 +- .../core/src/lib/resize/resize.service.ts | 8 +- .../src/lib/services/destroyed.service.ts | 10 + .../core/src/lib/services/loader.service.ts | 83 +++++ .../core/src/lib/services/loop.service.ts | 18 +- .../src/lib/stores/animation-frame.store.ts | 31 +- .../src/lib/stores/canvas-inputs.store.ts | 3 +- .../lib/stores/enhanced-component-store.ts | 23 +- packages/core/src/lib/stores/events.store.ts | 6 +- .../core/src/lib/stores/instances.store.ts | 2 +- .../core/src/lib/stores/performance.store.ts | 25 +- packages/core/src/lib/stores/store.ts | 18 +- packages/core/src/lib/three/attribute.ts | 91 +++++ packages/core/src/lib/three/audio.ts | 56 +++ packages/core/src/lib/three/camera.ts | 48 +++ packages/core/src/lib/three/curve.ts | 52 +++ packages/core/src/lib/three/geometry.ts | 95 +++++ packages/core/src/lib/three/helper.ts | 56 +++ packages/core/src/lib/three/light.ts | 59 +++ packages/core/src/lib/three/line.ts | 22 ++ packages/core/src/lib/three/material.ts | 92 +++++ packages/core/src/lib/three/mesh.ts | 20 + packages/core/src/lib/three/sprite.ts | 60 +++ packages/core/src/lib/three/texture.ts | 53 +++ packages/core/src/lib/utils/events.ts | 12 +- packages/core/src/lib/utils/make.ts | 76 ++++ packages/core/stats/README.md | 3 + packages/core/stats/ng-package.json | 5 + packages/core/stats/src/index.ts | 1 + .../core/stats/src/lib/stats.directive.ts | 71 ++++ packages/demo/.browserslistrc | 16 + packages/demo/.eslintrc.json | 36 ++ packages/demo/project.json | 86 +++++ packages/demo/src/app/app.component.ts | 24 ++ packages/demo/src/app/app.module.ts | 24 ++ packages/demo/src/assets/.gitkeep | 0 .../demo/src/environments/environment.prod.ts | 3 + packages/demo/src/environments/environment.ts | 16 + packages/demo/src/favicon.ico | Bin 0 -> 15086 bytes packages/demo/src/index.html | 13 + packages/demo/src/main.ts | 13 + packages/demo/src/polyfills.ts | 52 +++ packages/demo/src/styles.scss | 6 + packages/demo/tsconfig.app.json | 10 + packages/demo/tsconfig.editor.json | 7 + packages/demo/tsconfig.json | 24 ++ tsconfig.base.json | 12 +- workspace.json | 3 +- yarn.lock | 82 ++--- 97 files changed, 3033 insertions(+), 182 deletions(-) create mode 100644 packages/core/audios/README.md create mode 100644 packages/core/audios/ng-package.json create mode 100644 packages/core/audios/src/index.ts create mode 100644 packages/core/audios/src/lib/audio-listener/audio-listener.directive.ts create mode 100644 packages/core/cameras/README.md create mode 100644 packages/core/cameras/ng-package.json create mode 100644 packages/core/cameras/src/cube-camera/cube-camera.directive.ts create mode 100644 packages/core/cameras/src/index.ts create mode 100644 packages/core/geometries/README.md create mode 100644 packages/core/geometries/ng-package.json create mode 100644 packages/core/geometries/src/box-geometry/box-geometry.directive.ts create mode 100644 packages/core/geometries/src/index.ts create mode 100644 packages/core/group/README.md create mode 100644 packages/core/group/ng-package.json create mode 100644 packages/core/group/src/index.ts create mode 100644 packages/core/group/src/lib/group.directive.ts create mode 100644 packages/core/materials/README.md create mode 100644 packages/core/materials/ng-package.json create mode 100644 packages/core/materials/src/index.ts create mode 100644 packages/core/materials/src/mesh-basic-material/mesh-basic-material.directive.ts create mode 100644 packages/core/meshes/README.md create mode 100644 packages/core/meshes/ng-package.json create mode 100644 packages/core/meshes/src/index.ts create mode 100644 packages/core/meshes/src/instanced-mesh/instanced-mesh.directive.ts create mode 100644 packages/core/meshes/src/mesh/mesh.directive.ts create mode 100644 packages/core/meshes/src/skinned-mesh/skinned-mesh.directive.ts create mode 100644 packages/core/points/README.md create mode 100644 packages/core/points/ng-package.json create mode 100644 packages/core/points/src/index.ts create mode 100644 packages/core/points/src/lib/points.directive.ts create mode 100644 packages/core/src/lib/controllers/animation-subscriber.controller.ts create mode 100644 packages/core/src/lib/controllers/audio.controller.ts create mode 100644 packages/core/src/lib/controllers/controller.ts create mode 100644 packages/core/src/lib/controllers/material-geometry.controller.ts create mode 100644 packages/core/src/lib/controllers/object-3d-inputs.controller.ts create mode 100644 packages/core/src/lib/controllers/object-3d.controller.ts create mode 100644 packages/core/src/lib/services/destroyed.service.ts create mode 100644 packages/core/src/lib/services/loader.service.ts create mode 100644 packages/core/src/lib/three/attribute.ts create mode 100644 packages/core/src/lib/three/audio.ts create mode 100644 packages/core/src/lib/three/camera.ts create mode 100644 packages/core/src/lib/three/curve.ts create mode 100644 packages/core/src/lib/three/geometry.ts create mode 100644 packages/core/src/lib/three/helper.ts create mode 100644 packages/core/src/lib/three/light.ts create mode 100644 packages/core/src/lib/three/line.ts create mode 100644 packages/core/src/lib/three/material.ts create mode 100644 packages/core/src/lib/three/mesh.ts create mode 100644 packages/core/src/lib/three/sprite.ts create mode 100644 packages/core/src/lib/three/texture.ts create mode 100644 packages/core/src/lib/utils/make.ts create mode 100644 packages/core/stats/README.md create mode 100644 packages/core/stats/ng-package.json create mode 100644 packages/core/stats/src/index.ts create mode 100644 packages/core/stats/src/lib/stats.directive.ts create mode 100644 packages/demo/.browserslistrc create mode 100644 packages/demo/.eslintrc.json create mode 100644 packages/demo/project.json create mode 100644 packages/demo/src/app/app.component.ts create mode 100644 packages/demo/src/app/app.module.ts create mode 100644 packages/demo/src/assets/.gitkeep create mode 100644 packages/demo/src/environments/environment.prod.ts create mode 100644 packages/demo/src/environments/environment.ts create mode 100644 packages/demo/src/favicon.ico create mode 100644 packages/demo/src/index.html create mode 100644 packages/demo/src/main.ts create mode 100644 packages/demo/src/polyfills.ts create mode 100644 packages/demo/src/styles.scss create mode 100644 packages/demo/tsconfig.app.json create mode 100644 packages/demo/tsconfig.editor.json create mode 100644 packages/demo/tsconfig.json diff --git a/legacy/packages/core/geometries/src/lib/box-geometry/box-geometry.directive.ts b/legacy/packages/core/geometries/src/lib/box-geometry/box-geometry.directive.ts index 93c0ec2b6..b590efe92 100644 --- a/legacy/packages/core/geometries/src/lib/box-geometry/box-geometry.directive.ts +++ b/legacy/packages/core/geometries/src/lib/box-geometry/box-geometry.directive.ts @@ -1,6 +1,6 @@ // GENERATED import { NgtGeometry } from '@angular-three/core'; -import { NgModule, Directive, Input } from '@angular/core'; +import { Directive, Input, NgModule } from '@angular/core'; import * as THREE from 'three'; @Directive({ @@ -10,12 +10,13 @@ import * as THREE from 'three'; { provide: NgtGeometry, useExisting: NgtBoxGeometry, - } + }, ], }) export class NgtBoxGeometry extends NgtGeometry { - - static ngAcceptInputType_args: ConstructorParameters | undefined; + static ngAcceptInputType_args: + | ConstructorParameters + | undefined; @Input() set args(v: ConstructorParameters) { this.extraArgs = v; @@ -29,4 +30,3 @@ export class NgtBoxGeometry extends NgtGeometry { exports: [NgtBoxGeometry], }) export class NgtBoxGeometryModule {} - diff --git a/nx.json b/nx.json index 37cad130a..2734e20de 100644 --- a/nx.json +++ b/nx.json @@ -17,14 +17,19 @@ }, "generators": { "@nrwl/angular:application": { + "style": "scss", "linter": "eslint", - "unitTestRunner": "jest" + "unitTestRunner": "jest", + "e2eTestRunner": "none" }, "@nrwl/angular:library": { + "style": "scss", "linter": "eslint", "unitTestRunner": "jest" }, - "@nrwl/angular:component": {} + "@nrwl/angular:component": { + "style": "scss" + } }, "defaultProject": "core" } diff --git a/package.json b/package.json index c394c5a9f..11112e091 100644 --- a/package.json +++ b/package.json @@ -39,8 +39,8 @@ "@types/jest": "27.0.3", "@types/node": "16.11.10", "@types/three": "0.134.0", - "@typescript-eslint/eslint-plugin": "~5.4.0", - "@typescript-eslint/parser": "~5.4.0", + "@typescript-eslint/eslint-plugin": "~5.5.0", + "@typescript-eslint/parser": "~5.5.0", "eslint": "8.3.0", "eslint-config-prettier": "8.3.0", "jest": "27.3.1", diff --git a/packages/core/audios/README.md b/packages/core/audios/README.md new file mode 100644 index 000000000..d8b13279b --- /dev/null +++ b/packages/core/audios/README.md @@ -0,0 +1,3 @@ +# @angular-three/core/audios + +Secondary entry point of `@angular-three/core`. It can be used by importing from `@angular-three/core/audios`. diff --git a/packages/core/audios/ng-package.json b/packages/core/audios/ng-package.json new file mode 100644 index 000000000..c781f0df4 --- /dev/null +++ b/packages/core/audios/ng-package.json @@ -0,0 +1,5 @@ +{ + "lib": { + "entryFile": "src/index.ts" + } +} diff --git a/packages/core/audios/src/index.ts b/packages/core/audios/src/index.ts new file mode 100644 index 000000000..59d806d6b --- /dev/null +++ b/packages/core/audios/src/index.ts @@ -0,0 +1 @@ +export const greeting = 'Hello World!'; diff --git a/packages/core/audios/src/lib/audio-listener/audio-listener.directive.ts b/packages/core/audios/src/lib/audio-listener/audio-listener.directive.ts new file mode 100644 index 000000000..655deb67e --- /dev/null +++ b/packages/core/audios/src/lib/audio-listener/audio-listener.directive.ts @@ -0,0 +1,83 @@ +import { + applyProps, + EnhancedComponentStore, + NgtDestroyedService, + NgtStore, + tapEffect, +} from '@angular-three/core'; +import { + Directive, + EventEmitter, + Input, + NgModule, + NgZone, + OnInit, + Output, +} from '@angular/core'; +import { withLatestFrom } from 'rxjs'; +import * as THREE from 'three'; + +@Directive({ + selector: 'ngt-audio-listener', + exportAs: 'ngtAudioListener', +}) +export class NgtAudioListener extends EnhancedComponentStore implements OnInit { + @Input() filter?: AudioNode; + @Input() timeDelta?: number; + + @Output() ready = new EventEmitter(); + + #listener!: THREE.AudioListener; + get listener() { + return this.#listener; + } + + constructor( + private store: NgtStore, + private destroyed: NgtDestroyedService, + private ngZone: NgZone + ) { + super({}); + } + + ngOnInit() { + this.ngZone.runOutsideAngular(() => { + this.#listener = new THREE.AudioListener(); + const props = { + filter: this.filter, + timeDelta: this.timeDelta, + }; + applyProps(this.listener, props); + this.#ready(this.store.selectors.ready$); + }); + } + + #ready = this.effect((ready$) => + ready$.pipe( + withLatestFrom(this.store.selectors.camera$), + tapEffect(([ready, camera]) => { + this.ngZone.runOutsideAngular(() => { + if (ready && camera) { + camera.add(this.listener); + this.ready.emit(); + } + }); + + return () => { + this.ngZone.runOutsideAngular(() => { + if (ready && camera) { + this.listener.clear(); + camera.remove(this.listener); + } + }); + }; + }) + ) + ); +} + +@NgModule({ + declarations: [NgtAudioListener], + exports: [NgtAudioListener], +}) +export class NgtAudioListenerModule {} diff --git a/packages/core/cameras/README.md b/packages/core/cameras/README.md new file mode 100644 index 000000000..6bcefbebf --- /dev/null +++ b/packages/core/cameras/README.md @@ -0,0 +1,3 @@ +# @angular-three/core/cameras + +Secondary entry point of `@angular-three/core`. It can be used by importing from `@angular-three/core/cameras`. diff --git a/packages/core/cameras/ng-package.json b/packages/core/cameras/ng-package.json new file mode 100644 index 000000000..c781f0df4 --- /dev/null +++ b/packages/core/cameras/ng-package.json @@ -0,0 +1,5 @@ +{ + "lib": { + "entryFile": "src/index.ts" + } +} diff --git a/packages/core/cameras/src/cube-camera/cube-camera.directive.ts b/packages/core/cameras/src/cube-camera/cube-camera.directive.ts new file mode 100644 index 000000000..37a533aef --- /dev/null +++ b/packages/core/cameras/src/cube-camera/cube-camera.directive.ts @@ -0,0 +1,51 @@ +import { + NGT_OBJECT_CONTROLLER_PROVIDER, + NGT_OBJECT_WATCHED_CONTROLLER, + NgtObject3dController, +} from '@angular-three/core'; +import { + Directive, + Inject, + Input, + NgModule, + NgZone, + OnInit, +} from '@angular/core'; +import * as THREE from 'three'; + +@Directive({ + selector: 'ngt-cube-camera', + exportAs: 'ngtCubeCamera', + providers: [NGT_OBJECT_CONTROLLER_PROVIDER], +}) +export class NgtCubeCamera implements OnInit { + static ngAcceptInputType_args: ConstructorParameters; + + #cubeCamera?: THREE.CubeCamera; + get cubeCamera() { + return this.#cubeCamera; + } + + @Input() args!: ConstructorParameters; + + constructor( + @Inject(NGT_OBJECT_WATCHED_CONTROLLER) + private objectController: NgtObject3dController, + private ngZone: NgZone + ) {} + + ngOnInit() { + this.objectController.initFn = () => { + return this.ngZone.runOutsideAngular(() => { + this.#cubeCamera = new THREE.CubeCamera(...this.args); + return this.#cubeCamera; + }); + }; + } +} + +@NgModule({ + declarations: [NgtCubeCamera], + exports: [NgtCubeCamera], +}) +export class NgtCubeCameraModule {} diff --git a/packages/core/cameras/src/index.ts b/packages/core/cameras/src/index.ts new file mode 100644 index 000000000..59d806d6b --- /dev/null +++ b/packages/core/cameras/src/index.ts @@ -0,0 +1 @@ +export const greeting = 'Hello World!'; diff --git a/packages/core/geometries/README.md b/packages/core/geometries/README.md new file mode 100644 index 000000000..f5d876441 --- /dev/null +++ b/packages/core/geometries/README.md @@ -0,0 +1,3 @@ +# @angular-three/core/geometries + +Secondary entry point of `@angular-three/core`. It can be used by importing from `@angular-three/core/geometries`. diff --git a/packages/core/geometries/ng-package.json b/packages/core/geometries/ng-package.json new file mode 100644 index 000000000..c781f0df4 --- /dev/null +++ b/packages/core/geometries/ng-package.json @@ -0,0 +1,5 @@ +{ + "lib": { + "entryFile": "src/index.ts" + } +} diff --git a/packages/core/geometries/src/box-geometry/box-geometry.directive.ts b/packages/core/geometries/src/box-geometry/box-geometry.directive.ts new file mode 100644 index 000000000..109c2d0ef --- /dev/null +++ b/packages/core/geometries/src/box-geometry/box-geometry.directive.ts @@ -0,0 +1,32 @@ +// GENERATED +import { NgtGeometry } from '@angular-three/core'; +import { Directive, Input, NgModule } from '@angular/core'; +import * as THREE from 'three'; + +@Directive({ + selector: 'ngt-box-geometry', + exportAs: 'ngtBoxGeometry', + providers: [ + { + provide: NgtGeometry, + useExisting: NgtBoxGeometry, + }, + ], +}) +export class NgtBoxGeometry extends NgtGeometry { + static ngAcceptInputType_args: + | ConstructorParameters + | undefined; + + @Input() set args(v: ConstructorParameters) { + this.geometryArgs = v; + } + + geometryType = THREE.BoxGeometry; +} + +@NgModule({ + declarations: [NgtBoxGeometry], + exports: [NgtBoxGeometry], +}) +export class NgtBoxGeometryModule {} diff --git a/packages/core/geometries/src/index.ts b/packages/core/geometries/src/index.ts new file mode 100644 index 000000000..b221888fc --- /dev/null +++ b/packages/core/geometries/src/index.ts @@ -0,0 +1 @@ +export * from './box-geometry/box-geometry.directive'; diff --git a/packages/core/group/README.md b/packages/core/group/README.md new file mode 100644 index 000000000..8d1ee0920 --- /dev/null +++ b/packages/core/group/README.md @@ -0,0 +1,3 @@ +# @angular-three/core/group + +Secondary entry point of `@angular-three/core`. It can be used by importing from `@angular-three/core/group`. diff --git a/packages/core/group/ng-package.json b/packages/core/group/ng-package.json new file mode 100644 index 000000000..c781f0df4 --- /dev/null +++ b/packages/core/group/ng-package.json @@ -0,0 +1,5 @@ +{ + "lib": { + "entryFile": "src/index.ts" + } +} diff --git a/packages/core/group/src/index.ts b/packages/core/group/src/index.ts new file mode 100644 index 000000000..8e35dae1b --- /dev/null +++ b/packages/core/group/src/index.ts @@ -0,0 +1 @@ +export * from './lib/group.directive'; diff --git a/packages/core/group/src/lib/group.directive.ts b/packages/core/group/src/lib/group.directive.ts new file mode 100644 index 000000000..b6c7eb3e8 --- /dev/null +++ b/packages/core/group/src/lib/group.directive.ts @@ -0,0 +1,40 @@ +import { + NGT_OBJECT_CONTROLLER_PROVIDER, + NGT_OBJECT_WATCHED_CONTROLLER, + NgtObject3dController, +} from '@angular-three/core'; +import { Directive, Inject, NgModule, NgZone, OnInit } from '@angular/core'; +import * as THREE from 'three'; + +@Directive({ + selector: 'ngt-group', + exportAs: 'ngtGroup', + providers: [NGT_OBJECT_CONTROLLER_PROVIDER], +}) +export class NgtGroup implements OnInit { + #group?: THREE.Group; + get group() { + return this.#group; + } + + constructor( + @Inject(NGT_OBJECT_WATCHED_CONTROLLER) + private objectController: NgtObject3dController, + private ngZone: NgZone + ) {} + + ngOnInit() { + this.objectController.initFn = () => { + return this.ngZone.runOutsideAngular(() => { + this.#group = new THREE.Group(); + return this.#group; + }); + }; + } +} + +@NgModule({ + declarations: [NgtGroup], + exports: [NgtGroup], +}) +export class NgtGroupModule {} diff --git a/packages/core/materials/README.md b/packages/core/materials/README.md new file mode 100644 index 000000000..10248e1e8 --- /dev/null +++ b/packages/core/materials/README.md @@ -0,0 +1,3 @@ +# @angular-three/core/materials + +Secondary entry point of `@angular-three/core`. It can be used by importing from `@angular-three/core/materials`. diff --git a/packages/core/materials/ng-package.json b/packages/core/materials/ng-package.json new file mode 100644 index 000000000..c781f0df4 --- /dev/null +++ b/packages/core/materials/ng-package.json @@ -0,0 +1,5 @@ +{ + "lib": { + "entryFile": "src/index.ts" + } +} diff --git a/packages/core/materials/src/index.ts b/packages/core/materials/src/index.ts new file mode 100644 index 000000000..a51f3bea0 --- /dev/null +++ b/packages/core/materials/src/index.ts @@ -0,0 +1 @@ +export * from './mesh-basic-material/mesh-basic-material.directive'; diff --git a/packages/core/materials/src/mesh-basic-material/mesh-basic-material.directive.ts b/packages/core/materials/src/mesh-basic-material/mesh-basic-material.directive.ts new file mode 100644 index 000000000..f889c79a3 --- /dev/null +++ b/packages/core/materials/src/mesh-basic-material/mesh-basic-material.directive.ts @@ -0,0 +1,31 @@ +// GENERATED +import { NgtMaterial } from '@angular-three/core'; +import { Directive, NgModule } from '@angular/core'; +import * as THREE from 'three'; + +@Directive({ + selector: 'ngt-mesh-basic-material', + exportAs: 'ngtMeshBasicMaterial', + providers: [ + { + provide: NgtMaterial, + useExisting: NgtMeshBasicMaterial, + }, + ], +}) +export class NgtMeshBasicMaterial extends NgtMaterial< + THREE.MeshBasicMaterialParameters, + THREE.MeshBasicMaterial +> { + static ngAcceptInputType_parameters: + | THREE.MeshBasicMaterialParameters + | undefined; + + materialType = THREE.MeshBasicMaterial; +} + +@NgModule({ + declarations: [NgtMeshBasicMaterial], + exports: [NgtMeshBasicMaterial], +}) +export class NgtMeshBasicMaterialModule {} diff --git a/packages/core/meshes/README.md b/packages/core/meshes/README.md new file mode 100644 index 000000000..82b26a942 --- /dev/null +++ b/packages/core/meshes/README.md @@ -0,0 +1,3 @@ +# @angular-three/core/meshes + +Secondary entry point of `@angular-three/core`. It can be used by importing from `@angular-three/core/meshes`. diff --git a/packages/core/meshes/ng-package.json b/packages/core/meshes/ng-package.json new file mode 100644 index 000000000..c781f0df4 --- /dev/null +++ b/packages/core/meshes/ng-package.json @@ -0,0 +1,5 @@ +{ + "lib": { + "entryFile": "src/index.ts" + } +} diff --git a/packages/core/meshes/src/index.ts b/packages/core/meshes/src/index.ts new file mode 100644 index 000000000..4eeb41a41 --- /dev/null +++ b/packages/core/meshes/src/index.ts @@ -0,0 +1,3 @@ +export * from './mesh/mesh.directive'; +export * from './instanced-mesh/instanced-mesh.directive'; +export * from './skinned-mesh/skinned-mesh.directive'; diff --git a/packages/core/meshes/src/instanced-mesh/instanced-mesh.directive.ts b/packages/core/meshes/src/instanced-mesh/instanced-mesh.directive.ts new file mode 100644 index 000000000..a80846f47 --- /dev/null +++ b/packages/core/meshes/src/instanced-mesh/instanced-mesh.directive.ts @@ -0,0 +1,30 @@ +import { + NGT_MATERIAL_GEOMETRY_CONTROLLER_PROVIDER, + NGT_OBJECT_POST_INIT, + NGT_OBJECT_TYPE, + NgtCommonMesh, +} from '@angular-three/core'; +import { Directive, NgModule } from '@angular/core'; +import * as THREE from 'three'; + +@Directive({ + selector: 'ngt-instanced-mesh', + exportAs: 'ngtInstancedMesh', + providers: [ + NGT_MATERIAL_GEOMETRY_CONTROLLER_PROVIDER, + { provide: NGT_OBJECT_TYPE, useValue: THREE.InstancedMesh }, + { + provide: NGT_OBJECT_POST_INIT, + useValue: (object: THREE.InstancedMesh) => { + object.instanceMatrix.setUsage(THREE.DynamicDrawUsage); + }, + }, + ], +}) +export class NgtInstancedMesh extends NgtCommonMesh {} + +@NgModule({ + declarations: [NgtInstancedMesh], + exports: [NgtInstancedMesh], +}) +export class NgtInstancedMeshModule {} diff --git a/packages/core/meshes/src/mesh/mesh.directive.ts b/packages/core/meshes/src/mesh/mesh.directive.ts new file mode 100644 index 000000000..cd3d2a6a4 --- /dev/null +++ b/packages/core/meshes/src/mesh/mesh.directive.ts @@ -0,0 +1,23 @@ +import { + NGT_MATERIAL_GEOMETRY_CONTROLLER_PROVIDER, + NGT_OBJECT_TYPE, + NgtCommonMesh, +} from '@angular-three/core'; +import { Directive, NgModule } from '@angular/core'; +import * as THREE from 'three'; + +@Directive({ + selector: 'ngt-mesh', + exportAs: 'ngtMesh', + providers: [ + NGT_MATERIAL_GEOMETRY_CONTROLLER_PROVIDER, + { provide: NGT_OBJECT_TYPE, useValue: THREE.Mesh }, + ], +}) +export class NgtMesh extends NgtCommonMesh {} + +@NgModule({ + declarations: [NgtMesh], + exports: [NgtMesh], +}) +export class NgtMeshModule {} diff --git a/packages/core/meshes/src/skinned-mesh/skinned-mesh.directive.ts b/packages/core/meshes/src/skinned-mesh/skinned-mesh.directive.ts new file mode 100644 index 000000000..283e9b410 --- /dev/null +++ b/packages/core/meshes/src/skinned-mesh/skinned-mesh.directive.ts @@ -0,0 +1,139 @@ +import { + NGT_MATERIAL_GEOMETRY_CONTROLLER_PROVIDER, + NGT_OBJECT_CONTROLLER_PROVIDER, + NGT_OBJECT_INPUTS_WATCHED_CONTROLLER, + NGT_OBJECT_TYPE, + NGT_OBJECT_WATCHED_CONTROLLER, + NgtCommonMesh, + NgtMatrix4, + NgtObject3dController, + NgtObject3dInputsController, +} from '@angular-three/core'; +import { + ContentChildren, + Directive, + EventEmitter, + Inject, + Input, + NgModule, + NgZone, + OnInit, + Optional, + Output, + QueryList, +} from '@angular/core'; +import * as THREE from 'three'; + +@Directive({ + selector: 'ngt-bone', + exportAs: 'ngtBone', + providers: [NGT_OBJECT_CONTROLLER_PROVIDER], +}) +export class NgtBone implements OnInit { + #bone?: THREE.Bone; + get bone() { + return this.#bone; + } + + constructor( + @Inject(NGT_OBJECT_WATCHED_CONTROLLER) + private objectController: NgtObject3dController, + @Inject(NGT_OBJECT_INPUTS_WATCHED_CONTROLLER) + private objectInputsController: NgtObject3dInputsController, + private ngZone: NgZone, + @Optional() + private parentSkinnedMesh: NgtSkinnedMesh | null + ) { + if (!parentSkinnedMesh) { + throw new Error('ngt-bone must be used within a ngt-skinned-mesh'); + } + objectInputsController.appendTo = parentSkinnedMesh.mesh; + } + + ngOnInit() { + this.objectController.initFn = () => { + return this.ngZone.runOutsideAngular(() => { + this.#bone = new THREE.Bone(); + return this.#bone; + }); + }; + } +} + +@Directive({ + selector: 'ngt-skeleton', + exportAs: 'ngtSkeleton', +}) +export class NgtSkeleton implements OnInit { + @Input() boneInverses?: NgtMatrix4[]; + @Output() ready = new EventEmitter(); + + @ContentChildren(NgtBone) bones?: QueryList; + + #skeleton?: THREE.Skeleton; + get skeleton() { + return this.#skeleton; + } + + constructor( + private ngZone: NgZone, + @Optional() private skinnedMesh: NgtSkinnedMesh | null + ) { + if (!skinnedMesh) { + throw new Error('ngt-skeleton must be used within a ngt-skinned-mesh'); + } + } + + ngOnInit() { + this.ngZone.runOutsideAngular(() => { + if (this.bones) { + const boneInverses: THREE.Matrix4[] | undefined = this.boneInverses + ? this.boneInverses.map((threeMaxtrix) => { + if (threeMaxtrix instanceof THREE.Matrix4) return threeMaxtrix; + return new THREE.Matrix4().set(...threeMaxtrix); + }) + : undefined; + this.#skeleton = new THREE.Skeleton( + this.bones.filter((bone) => !!bone.bone).map((bone) => bone.bone!), + boneInverses + ); + this.ready.emit(); + + if (this.skinnedMesh) { + const bindMatrix: THREE.Matrix4 | undefined = this.skinnedMesh + .bindMatrix + ? this.skinnedMesh.bindMatrix instanceof THREE.Matrix4 + ? this.skinnedMesh.bindMatrix + : new THREE.Matrix4().set(...this.skinnedMesh.bindMatrix) + : undefined; + this.skinnedMesh.mesh.bind(this.skeleton!, bindMatrix); + } + } + }); + } +} + +@Directive({ + selector: 'ngt-skinned-mesh', + exportAs: 'ngtSkinnedMesh', + providers: [ + NGT_MATERIAL_GEOMETRY_CONTROLLER_PROVIDER, + { provide: NGT_OBJECT_TYPE, useValue: THREE.SkinnedMesh }, + ], +}) +export class NgtSkinnedMesh extends NgtCommonMesh { + @Input() set args(v: [boolean]) { + if (this.materialGeometryController) { + this.materialGeometryController.meshArgs = v; + } + } + + @Input() bindMatrix?: NgtMatrix4; + @Input() bindMode?: string; +} + +@NgModule({ + declarations: [NgtSkinnedMesh, NgtBone, NgtSkeleton], + exports: [NgtSkinnedMesh, NgtBone, NgtSkeleton], +}) +export class NgtSkinnedMeshModule {} diff --git a/packages/core/points/README.md b/packages/core/points/README.md new file mode 100644 index 000000000..b29e08fce --- /dev/null +++ b/packages/core/points/README.md @@ -0,0 +1,3 @@ +# @angular-three/core/points + +Secondary entry point of `@angular-three/core`. It can be used by importing from `@angular-three/core/points`. diff --git a/packages/core/points/ng-package.json b/packages/core/points/ng-package.json new file mode 100644 index 000000000..c781f0df4 --- /dev/null +++ b/packages/core/points/ng-package.json @@ -0,0 +1,5 @@ +{ + "lib": { + "entryFile": "src/index.ts" + } +} diff --git a/packages/core/points/src/index.ts b/packages/core/points/src/index.ts new file mode 100644 index 000000000..907095b95 --- /dev/null +++ b/packages/core/points/src/index.ts @@ -0,0 +1 @@ +export * from './lib/points.directive'; diff --git a/packages/core/points/src/lib/points.directive.ts b/packages/core/points/src/lib/points.directive.ts new file mode 100644 index 000000000..dcc9239e6 --- /dev/null +++ b/packages/core/points/src/lib/points.directive.ts @@ -0,0 +1,34 @@ +import { + NGT_MATERIAL_GEOMETRY_CONTROLLER_PROVIDER, + NGT_MATERIAL_GEOMETRY_WATCHED_CONTROLLER, + NGT_OBJECT_TYPE, + NgtMaterialGeometryController, +} from '@angular-three/core'; +import { Directive, Inject, NgModule } from '@angular/core'; +import * as THREE from 'three'; + +@Directive({ + selector: 'ngt-points', + exportAs: 'ngtPoints', + providers: [ + NGT_MATERIAL_GEOMETRY_CONTROLLER_PROVIDER, + { provide: NGT_OBJECT_TYPE, useExisting: THREE.Points }, + ], +}) +export class NgtPoints { + constructor( + @Inject(NGT_MATERIAL_GEOMETRY_WATCHED_CONTROLLER) + private materialGeometryController: NgtMaterialGeometryController + ) {} + + get points() { + return this.materialGeometryController.objectController + .object3d as THREE.Points; + } +} + +@NgModule({ + declarations: [NgtPoints], + exports: [NgtPoints], +}) +export class NgtPointsModule {} diff --git a/packages/core/project.json b/packages/core/project.json index f65bc9938..de147496c 100644 --- a/packages/core/project.json +++ b/packages/core/project.json @@ -33,7 +33,23 @@ "options": { "lintFilePatterns": [ "packages/core/src/**/*.ts", - "packages/core/src/**/*.html" + "packages/core/src/**/*.html", + "packages/core/meshes/**/*.ts", + "packages/core/meshes/**/*.html", + "packages/core/materials/**/*.ts", + "packages/core/materials/**/*.html", + "packages/core/geometries/**/*.ts", + "packages/core/geometries/**/*.html", + "packages/core/stats/**/*.ts", + "packages/core/stats/**/*.html", + "packages/core/points/**/*.ts", + "packages/core/points/**/*.html", + "packages/core/group/**/*.ts", + "packages/core/group/**/*.html", + "packages/core/cameras/**/*.ts", + "packages/core/cameras/**/*.html", + "packages/core/audios/**/*.ts", + "packages/core/audios/**/*.html" ] } } diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index bc1eddee3..a003b2551 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -1,7 +1,28 @@ +export * from './lib/models'; + export * from './lib/core.module'; export * from './lib/canvas.component'; +export * from './lib/controllers/controller'; +export * from './lib/controllers/object-3d.controller'; +export * from './lib/controllers/material-geometry.controller'; +export * from './lib/controllers/object-3d-inputs.controller'; +export * from './lib/controllers/animation-subscriber.controller'; +export * from './lib/controllers/audio.controller'; + +export * from './lib/three/audio'; +export * from './lib/three/line'; +export * from './lib/three/texture'; +export * from './lib/three/geometry'; +export * from './lib/three/light'; +export * from './lib/three/attribute'; +export * from './lib/three/curve'; +export * from './lib/three/helper'; +export * from './lib/three/material'; +export * from './lib/three/mesh'; +export * from './lib/three/sprite'; + export * from './lib/resize/tokens'; export * from './lib/performance/tokens'; @@ -14,8 +35,11 @@ export * from './lib/stores/animation-frame.store'; export * from './lib/stores/instances.store'; export * from './lib/services/loop.service'; +export * from './lib/services/destroyed.service'; +export * from './lib/services/loader.service'; export * from './lib/utils/apply-props'; export * from './lib/utils/array-partition'; export * from './lib/utils/build-graph'; export * from './lib/utils/make-id'; +export * from './lib/utils/make'; diff --git a/packages/core/src/lib/canvas.component.ts b/packages/core/src/lib/canvas.component.ts index ba8f5d74c..893927a8e 100644 --- a/packages/core/src/lib/canvas.component.ts +++ b/packages/core/src/lib/canvas.component.ts @@ -36,7 +36,7 @@ import { NgtStore } from './stores/store'; @Component({ selector: 'ngt-canvas', exportAs: 'ngtCanvas', - template: ` `, + template: ` `, styles: [ ` :host { @@ -126,6 +126,7 @@ export class NgtCanvas extends EnhancedComponentStore implements OnInit { } @Output() created = new EventEmitter(); + @Output() pointermissed = new EventEmitter(); @ViewChild('rendererCanvas', { static: true }) rendererCanvas!: ElementRef; @@ -149,11 +150,22 @@ export class NgtCanvas extends EnhancedComponentStore implements OnInit { this.eventsStore.init(); this.animationFrameStore.init(); - this.ready(this.store.selectors.ready$); + // if there is handler to pointermissed on the canvas + // update pointermissed in events store so that + // events util will handle it + if (this.pointermissed.observed) { + this.eventsStore.updaters.setPointermissed((event) => { + this.ngZone.runOutsideAngular(() => { + this.pointermissed.emit(event); + }); + }); + } + + this.#ready(this.store.selectors.ready$); }); } - readonly ready = this.effect((ready$) => + readonly #ready = this.effect((ready$) => ready$.pipe( tap((ready) => { this.ngZone.runOutsideAngular(() => { @@ -171,42 +183,3 @@ export class NgtCanvas extends EnhancedComponentStore implements OnInit { ) ); } - -/** - * gl?: GLProps - * events?: (store: UseStore) => EventManager - * size?: Size - * mode?: typeof modes[number] - * onCreated?: (state: RootState) => void - */ - -/** - * gl: THREE.WebGLRenderer - * size: Size - * vr?: boolean - * shadows?: boolean | Partial - * linear?: boolean - * flat?: boolean - * orthographic?: boolean - * frameloop?: 'always' | 'demand' | 'never' - * performance?: Partial> - * dpr?: Dpr - * clock?: THREE.Clock - * raycaster?: Partial - * camera?: - * | Camera - * | Partial< - * ReactThreeFiber.Object3DNode & - * ReactThreeFiber.Object3DNode & - * ReactThreeFiber.Object3DNode - * > - * onPointerMissed?: (event: ThreeEvent) => void - */ - -/** - * type GLProps = - * | Renderer - * | ((canvas: HTMLCanvasElement) => Renderer) - * | Partial | THREE.WebGLRendererParameters> - * | undefined - */ diff --git a/packages/core/src/lib/controllers/animation-subscriber.controller.ts b/packages/core/src/lib/controllers/animation-subscriber.controller.ts new file mode 100644 index 000000000..dc015bd51 --- /dev/null +++ b/packages/core/src/lib/controllers/animation-subscriber.controller.ts @@ -0,0 +1,78 @@ +import { + Directive, + EventEmitter, + Input, + NgModule, + NgZone, + OnDestroy, + Output, +} from '@angular/core'; +import { Subscription } from 'rxjs'; +import * as THREE from 'three'; +import { NgtRender } from '../models'; +import { NgtAnimationFrameStore } from '../stores/animation-frame.store'; +import { Controller, createControllerProviderFactory } from './controller'; + +@Directive({ + selector: '[animateReady]', + exportAs: 'ngtAnimationSubscriberController', +}) +export class NgtAnimationSubscriberController + extends Controller + implements OnDestroy +{ + @Input() priority = 0; + @Output() animateReady = new EventEmitter(); + + #animateSubscription?: Subscription; + + constructor( + private animationFrameStore: NgtAnimationFrameStore, + ngZone: NgZone + ) { + super(ngZone); + } + + subscribe(obj: THREE.Object3D) { + this.ngZone.runOutsideAngular(() => { + // only subscribe to animation frame if there's an output handler + if (this.animateReady.observed) { + this.#animateSubscription = this.animationFrameStore.register({ + obj, + callback: this.animateReady.emit.bind(this.animateReady), + priority: this.priority, + }); + } + }); + } + + ngOnDestroy() { + this.ngZone.runOutsideAngular(() => { + if (this.#animateSubscription) { + this.#animateSubscription.unsubscribe(); + } + }); + } + + get controller(): Controller | undefined { + return undefined; + } + + get props(): string[] { + return []; + } +} + +@NgModule({ + declarations: [NgtAnimationSubscriberController], + exports: [NgtAnimationSubscriberController], +}) +export class NgtAnimationSubscriberControllerModule {} + +export const [ + NGT_ANIMATION_SUBSCRIBER_WATCHED_CONTROLLER, + NGT_ANIMATION_SUBSCRIBER_CONTROLLER_PROVIDER, +] = createControllerProviderFactory({ + watchedControllerTokenName: 'Watched AnimationSubscriberController', + controller: NgtAnimationSubscriberController, +}); diff --git a/packages/core/src/lib/controllers/audio.controller.ts b/packages/core/src/lib/controllers/audio.controller.ts new file mode 100644 index 000000000..3dd076838 --- /dev/null +++ b/packages/core/src/lib/controllers/audio.controller.ts @@ -0,0 +1,60 @@ +import { Directive, Input, NgModule } from '@angular/core'; +import { Controller, createControllerProviderFactory } from './controller'; + +@Directive({ + selector: 'ngt-audio, ngt-positional-audio, ngt-soba-positional-audio', + exportAs: 'ngtAudioController', +}) +export class NgtAudioController extends Controller { + @Input() autoplay?: boolean; + @Input() buffer: null | AudioBuffer = null; + @Input() detune = 0; + @Input() loop = false; + @Input() loopStart = 0; + @Input() loopEnd = 0; + @Input() offset = 0; + @Input() duration: number | undefined = undefined; + @Input() playbackRate = 1; + @Input() isPlaying = false; + @Input() hasPlaybackControl = true; + @Input() sourceType = 'empty'; + @Input() source: null | AudioBufferSourceNode = null; + @Input() filters: AudioNode[] = []; + + @Input() audioController?: NgtAudioController; + + get props(): string[] { + return [ + 'autoplay', + 'buffer', + 'detune', + 'loop', + 'loopStart', + 'loopEnd', + 'offset', + 'duration', + 'playbackRate', + 'isPlaying', + 'hasPlaybackControl', + 'sourceType', + 'source', + 'filters', + ]; + } + + get controller(): Controller | undefined { + return this.audioController; + } +} + +@NgModule({ + declarations: [NgtAudioController], + exports: [NgtAudioController], +}) +export class NgtAudioControllerModule {} + +export const [NGT_AUDIO_WATCHED_CONTROLLER, NGT_AUDIO_CONTROLLER_PROVIDER] = + createControllerProviderFactory({ + watchedControllerTokenName: 'Watched AudioController', + controller: NgtAudioController, + }); diff --git a/packages/core/src/lib/controllers/controller.ts b/packages/core/src/lib/controllers/controller.ts new file mode 100644 index 000000000..2c72f05af --- /dev/null +++ b/packages/core/src/lib/controllers/controller.ts @@ -0,0 +1,104 @@ +import { + ChangeDetectorRef, + Directive, + InjectionToken, + NgZone, + OnChanges, + OnInit, + Optional, + Provider, + SimpleChanges, + Type, +} from '@angular/core'; +import { Observable, ReplaySubject, takeUntil } from 'rxjs'; +import { UnknownRecord } from '../models'; +import { NgtDestroyedService } from '../services/destroyed.service'; + +@Directive() +export abstract class Controller implements OnChanges, OnInit { + abstract get props(): string[]; + + abstract get controller(): Controller | undefined; + + readonly change$ = new ReplaySubject(1); + + constructor(protected ngZone: NgZone) {} + + ngOnChanges(changes: SimpleChanges) { + if (this.controller) { + this.controller.ngOnChanges(changes); + this.change$.next(changes); + } else { + this.change$.next(changes); + } + } + + ngOnInit() { + this.ngZone.runOutsideAngular(() => { + if (this.controller) { + this.props.forEach((prop) => { + (this as UnknownRecord)[prop] = ( + this.controller as unknown as UnknownRecord + )[prop]; + }); + } + }); + } +} + +export interface CreateControllerTokenFactoryOptions< + TController extends Controller +> { + watchedControllerTokenName: string; + controller: Type; + newInstanceOnNull?: boolean; +} + +export function controllerFactory( + newInstanceOnNull = false, + controllerType: Type +) { + return ( + controller: TController | null, + changeDetectorRef: ChangeDetectorRef, + destroyed: Observable + ): TController | null => { + if (!controller) { + return newInstanceOnNull ? new controllerType() : null; + } + + controller.change$.pipe(takeUntil(destroyed)).subscribe(() => { + changeDetectorRef.markForCheck(); + }); + + return controller; + }; +} + +export function createControllerProviderFactory< + TController extends Controller +>({ + watchedControllerTokenName, + controller, + newInstanceOnNull = false, +}: CreateControllerTokenFactoryOptions): [ + InjectionToken, + Provider[] +] { + const watchedControllerToken = new InjectionToken(watchedControllerTokenName); + + const controllerProvider: Provider[] = [ + NgtDestroyedService, + { + provide: watchedControllerToken, + deps: [ + [new Optional(), controller], + ChangeDetectorRef, + NgtDestroyedService, + ], + useFactory: controllerFactory(newInstanceOnNull, controller), + }, + ]; + + return [watchedControllerToken, controllerProvider]; +} diff --git a/packages/core/src/lib/controllers/material-geometry.controller.ts b/packages/core/src/lib/controllers/material-geometry.controller.ts new file mode 100644 index 000000000..e3143cc56 --- /dev/null +++ b/packages/core/src/lib/controllers/material-geometry.controller.ts @@ -0,0 +1,228 @@ +import { + AfterContentInit, + ContentChild, + ContentChildren, + Directive, + Inject, + InjectionToken, + Input, + NgModule, + NgZone, + OnInit, + QueryList, +} from '@angular/core'; +import * as THREE from 'three'; +import { BufferGeometry, Material } from 'three'; +import { AnyConstructor, AnyExtenderFunction, UnknownRecord } from '../models'; +import { NgtInstancesStore } from '../stores/instances.store'; +import { NgtGeometry } from '../three/geometry'; +import { NgtMaterial } from '../three/material'; +import { Controller, createControllerProviderFactory } from './controller'; +import { + NGT_OBJECT_CONTROLLER_PROVIDER, + NGT_OBJECT_WATCHED_CONTROLLER, + NgtObject3dController, +} from './object-3d.controller'; + +@Directive({ + selector: 'ngt-mesh', + exportAs: 'ngtMaterialGeometryController', + providers: [NGT_OBJECT_CONTROLLER_PROVIDER], +}) +export class NgtMaterialGeometryController + extends Controller + implements AfterContentInit, OnInit +{ + @ContentChildren(NgtMaterial, { descendants: true }) set materialDirectives( + v: QueryList + ) { + if (this.material == null && v) { + this.material = + v.length === 1 + ? v.first.material + : v.toArray().map((dir) => dir.material); + } + } + + @ContentChild(NgtGeometry) + set bufferGeometryDirective(v: NgtGeometry) { + if (this.geometry == null && v) { + this.geometry = v.geometry; + } + } + + #geometryInput?: string | THREE.BufferGeometry | undefined; + + @Input() set geometry(v: string | THREE.BufferGeometry | undefined) { + this.#geometryInput = v; + this.#geometry = this.#getGeometry(v); + } + + get geometry() { + return this.#geometry; + } + + #geometry: THREE.BufferGeometry | undefined = undefined; + + #materialInput?: + | string + | string[] + | THREE.Material + | THREE.Material[] + | undefined; + + @Input() set material( + v: string | string[] | THREE.Material | THREE.Material[] | undefined + ) { + if (!(Array.isArray(v) && !v.length)) { + this.#materialInput = v; + } + this.#material = this.#getMaterial(v); + } + + get material() { + return this.#material; + } + + #material: THREE.Material | THREE.Material[] | undefined = undefined; + + #meshArgs: unknown[] = []; + set meshArgs(v: unknown | unknown[]) { + this.#meshArgs = Array.isArray(v) ? v : [v]; + } + + @Input() materialGeometryController?: NgtMaterialGeometryController; + + @Input() morphTargetInfluences?: number[]; + @Input() morphTargetDictionary?: { [key: string]: number }; + + constructor( + ngZone: NgZone, + private instancesStore: NgtInstancesStore, + @Inject(NGT_OBJECT_WATCHED_CONTROLLER) + public objectController: NgtObject3dController, + @Inject(NGT_OBJECT_TYPE) + public objectType: AnyConstructor, + @Inject(NGT_OBJECT_POST_INIT) + public objectPostInit: AnyExtenderFunction | undefined + ) { + super(ngZone); + } + + ngOnInit() { + super.ngOnInit(); + this.objectController.initFn = () => { + if (!this.geometry) { + this.#geometry = this.#getGeometry(this.#geometryInput); + } + + if (!this.material) { + this.#material = this.#getMaterial(this.#materialInput); + } + + const object = new this.objectType( + this.geometry, + this.material, + ...this.#meshArgs + ); + + if (this.morphTargetDictionary && 'morphTargetDictionary' in object) { + (object as unknown as UnknownRecord).morphTargetDictionary = + this.morphTargetDictionary; + } + + if (this.morphTargetInfluences && 'morphTargetInfluences' in object) { + (object as unknown as UnknownRecord).morphTargetInfluences = + this.morphTargetInfluences; + } + + if (this.objectPostInit) { + this.objectPostInit(object); + } + + return object; + }; + } + + ngAfterContentInit() { + this.objectController.init(); + } + + #getMaterial( + input: string | string[] | Material | Material[] | undefined + ): Material | Material[] | undefined { + if (input) { + if (Array.isArray(input)) { + if (!input.length) return undefined; + + if (input[0] instanceof THREE.Material) { + return input as THREE.Material[]; + } + + return (input as string[]).map( + (materialId) => + this.instancesStore.getImperativeState().materials[materialId] + ); + } + + if (input instanceof THREE.Material) { + return input; + } + + return this.instancesStore.getImperativeState().materials[input]; + } + + return undefined; + } + + #getGeometry( + input: string | BufferGeometry | undefined + ): BufferGeometry | undefined { + if (input) { + if (input instanceof THREE.BufferGeometry) { + return input; + } + + return this.instancesStore.getImperativeState().geometries[input]; + } + + return undefined; + } + + get controller(): Controller | undefined { + return this.materialGeometryController; + } + + get props(): string[] { + return [ + 'material', + 'geometry', + 'morphTargetInfluences', + 'morphTargetDictionary', + ]; + } +} + +@NgModule({ + declarations: [NgtMaterialGeometryController], + exports: [NgtMaterialGeometryController], +}) +export class NgtMaterialGeometryControllerModule {} + +export const [ + NGT_MATERIAL_GEOMETRY_WATCHED_CONTROLLER, + NGT_MATERIAL_GEOMETRY_CONTROLLER_PROVIDER, +] = createControllerProviderFactory({ + watchedControllerTokenName: 'Watched MaterialGeometryController', + controller: NgtMaterialGeometryController, +}); + +export const NGT_OBJECT_TYPE = new InjectionToken('Object3d Type', { + providedIn: 'root', + factory: () => THREE.Object3D, +}); + +export const NGT_OBJECT_POST_INIT = new InjectionToken('Object3d PostInit', { + providedIn: 'root', + factory: () => undefined, +}); diff --git a/packages/core/src/lib/controllers/object-3d-inputs.controller.ts b/packages/core/src/lib/controllers/object-3d-inputs.controller.ts new file mode 100644 index 000000000..aad0b090d --- /dev/null +++ b/packages/core/src/lib/controllers/object-3d-inputs.controller.ts @@ -0,0 +1,175 @@ +import { + Directive, + EventEmitter, + Input, + NgModule, + NgZone, + Output, +} from '@angular/core'; +import * as THREE from 'three'; +import { + NgtColor, + NgtEuler, + NgtEvent, + NgtQuaternion, + NgtVector3, + UnknownRecord, +} from '../models'; +import { makeColor, makeForSet, makeVector3 } from '../utils/make'; +import { Controller, createControllerProviderFactory } from './controller'; + +@Directive({ + selector: 'ngt-mesh', + exportAs: 'ngtObject3dInputsController', +}) +export class NgtObject3dInputsController extends Controller { + @Input() name?: string; + + @Input() set position(position: NgtVector3 | undefined) { + this.#position = makeVector3(position); + } + + get position() { + return this.#position; + } + + #position?: THREE.Vector3; + + @Input() set rotation(rotation: NgtEuler | undefined) { + this.#rotation = makeForSet(THREE.Euler, rotation); + } + + get rotation() { + return this.#rotation; + } + + #rotation?: THREE.Euler; + + @Input() set quaternion(quaternion: NgtQuaternion | undefined) { + this.#quaternion = makeForSet(THREE.Quaternion, quaternion); + } + + get quaternion() { + return this.#quaternion; + } + + #quaternion?: THREE.Quaternion; + + @Input() set scale(scale: NgtVector3 | undefined) { + this.#scale = makeVector3(scale); + } + + get scale() { + return this.#scale; + } + + #scale?: THREE.Vector3; + + @Input() set color(color: NgtColor | undefined) { + this.#color = makeColor(color); + } + + get color() { + return this.#color; + } + + #color?: THREE.Color; + + @Input() userData?: UnknownRecord; + @Input() castShadow = false; + @Input() receiveShadow = false; + @Input() visible = true; + @Input() matrixAutoUpdate = true; + @Input() dispose?: () => void; + + @Input() appendMode: 'immediate' | 'root' | 'none' = 'immediate'; + @Input() appendTo?: THREE.Object3D; + + @Input() object3dInputsController?: NgtObject3dInputsController; + + // events + @Output() click = new EventEmitter>(); + @Output() contextmenu = new EventEmitter>(); + @Output() dblclick = new EventEmitter>(); + @Output() pointerup = new EventEmitter>(); + @Output() pointerdown = new EventEmitter>(); + @Output() pointerover = new EventEmitter>(); + @Output() pointerout = new EventEmitter>(); + @Output() pointerenter = new EventEmitter>(); + @Output() pointerleave = new EventEmitter>(); + @Output() pointermove = new EventEmitter>(); + @Output() pointermissed = new EventEmitter>(); + @Output() pointercancel = new EventEmitter>(); + @Output() wheel = new EventEmitter>(); + + get props(): string[] { + return [ + 'name', + 'position', + 'rotation', + 'quaternion', + 'scale', + 'color', + 'userData', + 'dispose', + 'castShadow', + 'receiveShadow', + 'visible', + 'matrixAutoUpdate', + 'appendMode', + 'appendTo', + 'click', + 'contextmenu', + 'dblclick', + 'pointerup', + 'pointerdown', + 'pointerover', + 'pointerout', + 'pointerenter', + 'pointerleave', + 'pointermove', + 'pointermissed', + 'pointercancel', + 'wheel', + ]; + } + + get controller(): Controller | undefined { + return this.object3dInputsController; + } + + constructor(ngZone: NgZone) { + super(ngZone); + } + + get object3dProps() { + return { + name: this.name, + position: this.position, + rotation: this.rotation, + quaternion: this.quaternion, + scale: this.scale, + color: this.color, + userData: this.userData, + dispose: this.dispose, + castShadow: this.castShadow, + receiveShadow: this.receiveShadow, + visible: this.visible, + matrixAutoUpdate: this.matrixAutoUpdate, + }; + } +} + +@NgModule({ + declarations: [NgtObject3dInputsController], + exports: [NgtObject3dInputsController], +}) +export class NgtObject3dInputsControllerModule {} + +export const [ + NGT_OBJECT_INPUTS_WATCHED_CONTROLLER, + NGT_OBJECT_INPUTS_CONTROLLER_PROVIDER, +] = createControllerProviderFactory({ + watchedControllerTokenName: 'Watched Object3dInputsController', + controller: NgtObject3dInputsController, +}); diff --git a/packages/core/src/lib/controllers/object-3d.controller.ts b/packages/core/src/lib/controllers/object-3d.controller.ts new file mode 100644 index 000000000..fd4ac7ec1 --- /dev/null +++ b/packages/core/src/lib/controllers/object-3d.controller.ts @@ -0,0 +1,343 @@ +import { + Directive, + EventEmitter, + Inject, + NgModule, + NgZone, + OnDestroy, + Optional, + Output, + SkipSelf, +} from '@angular/core'; +import { Subscription, take } from 'rxjs'; +import * as THREE from 'three'; +import { + NgtEvent, + NgtEventHandlers, + NgtInstance, + NgtInstanceInternal, + UnknownRecord, +} from '../models'; +import { NgtEventsStore } from '../stores/events.store'; +import { NgtInstancesStore } from '../stores/instances.store'; +import { NgtStore } from '../stores/store'; +import { applyProps } from '../utils/apply-props'; +import { + NGT_ANIMATION_SUBSCRIBER_CONTROLLER_PROVIDER, + NGT_ANIMATION_SUBSCRIBER_WATCHED_CONTROLLER, + NgtAnimationSubscriberController, +} from './animation-subscriber.controller'; +import { Controller, createControllerProviderFactory } from './controller'; +import { + NGT_OBJECT_INPUTS_CONTROLLER_PROVIDER, + NGT_OBJECT_INPUTS_WATCHED_CONTROLLER, + NgtObject3dInputsController, +} from './object-3d-inputs.controller'; + +const supportedEvents = [ + 'click', + 'contextmenu', + 'dblclick', + 'pointerup', + 'pointerdown', + 'pointerover', + 'pointerout', + 'pointerenter', + 'pointerleave', + 'pointermove', + 'pointermissed', + 'pointercancel', + 'wheel', +] as const; + +@Directive({ + selector: 'ngt-mesh', + exportAs: 'ngtObject3dController', + providers: [ + NGT_OBJECT_INPUTS_CONTROLLER_PROVIDER, + NGT_ANIMATION_SUBSCRIBER_CONTROLLER_PROVIDER, + ], +}) +export class NgtObject3dController extends Controller implements OnDestroy { + @Output() ready = new EventEmitter(); + + #object3d?: THREE.Object3D; + #inputChangesSubscription?: Subscription; + #object3dInputsController: NgtObject3dInputsController; + + #initFn?: () => THREE.Object3D; + + set initFn(v: () => THREE.Object3D) { + this.#initFn = v; + } + + get initFn() { + if (!this.#initFn) { + throw new Error('initFn needs to be set'); + } + return this.#initFn as () => THREE.Object3D; + } + + constructor( + ngZone: NgZone, + private store: NgtStore, + private eventsStore: NgtEventsStore, + private instancesStore: NgtInstancesStore, + @Inject(NGT_OBJECT_INPUTS_WATCHED_CONTROLLER) + private objectInputsController: NgtObject3dInputsController, + @Inject(NGT_ANIMATION_SUBSCRIBER_WATCHED_CONTROLLER) + private animationSubscriberController: NgtAnimationSubscriberController, + @Optional() @SkipSelf() private parentObject3d: NgtObject3dController | null + ) { + super(ngZone); + + this.#object3dInputsController = + objectInputsController.object3dInputsController ?? objectInputsController; + } + + ngOnInit() { + super.ngOnInit(); + this.ngZone.runOutsideAngular(() => { + this.#inputChangesSubscription = + this.#object3dInputsController.change$.subscribe(() => { + this.ngZone.runOutsideAngular(() => { + if (this.object3d) { + this.#applyCustomProps(); + } + }); + }); + }); + } + + init() { + this.ngZone.runOutsideAngular(() => { + this.#object3d = this.initFn(); + if (this.object3d) { + this.#applyCustomProps(); + + const observedEvents = supportedEvents.reduce( + (result, event) => { + if (this.objectInputsController[event].observed) { + result.handlers[event] = this.#eventNameToHandler(event); + result.eventCount += 1; + } + return result; + }, + { handlers: {}, eventCount: 0 } as { + handlers: NgtEventHandlers; + eventCount: number; + } + ); + + // setup __ngt instance + applyProps(this.object3d, { + __ngt: { + stateGetter: () => this.store.getImperativeState(), + eventsStateGetter: () => this.eventsStore.getImperativeState(), + handlers: observedEvents.handlers, + eventCount: observedEvents.eventCount, + linear: this.store.getImperativeState().linear, + } as NgtInstanceInternal, + }); + + // add as an interaction if there are events observed + if (observedEvents.eventCount > 0) { + this.eventsStore.addInteraction(this.object3d); + } + + this.instancesStore.saveObject(this.object3d as unknown as NgtInstance); + + if (this.objectInputsController.appendMode !== 'none') { + this.#appendToParent(); + } + + this.#objectReady(); + } + }); + } + + #objectReady() { + this.ready.emit(); + if (this.animationSubscriberController) { + this.animationSubscriberController.subscribe(this.object3d); + } + } + + #appendToParent(): void { + // Schedule this in the next loop to allow for all appendTo's to settle + // TODO: find better way + setTimeout(() => { + if (this.objectInputsController.appendTo) { + this.objectInputsController.appendTo.add(this.object3d); + return; + } + + if (this.objectInputsController.appendMode === 'root') { + this.#addToScene(); + return; + } + + if (this.objectInputsController.appendMode === 'immediate') { + this.#addToParent(); + } + }); + } + + #addToScene() { + const { scene } = this.store.getImperativeState(); + if (scene) { + scene.add(this.object3d); + } + } + + #addToParent() { + if (this.parentObject3d) { + this.parentObject3d.object3d.add(this.object3d); + } else { + this.#addToScene(); + } + } + + #remove() { + if (this.objectInputsController.appendTo) { + this.objectInputsController.appendTo.remove(this.object3d); + } else if ( + this.parentObject3d && + this.objectInputsController.appendMode === 'immediate' + ) { + this.parentObject3d.object3d.remove(this.object3d); + } else { + const { scene } = this.store.getImperativeState(); + if (scene) { + scene.remove(this.object3d); + } + } + + this.object3d.clear(); + } + + #eventNameToHandler(eventName: typeof supportedEvents[number]) { + return ( + event: Parameters< + Exclude + >[0] + ) => { + this.ngZone.run(() => { + this.objectInputsController[eventName].emit(event as NgtEvent); + }); + }; + } + + #applyCustomProps() { + this.ngZone.runOutsideAngular(() => { + const customProps = { + castShadow: this.objectInputsController.castShadow, + receiveShadow: this.objectInputsController.receiveShadow, + visible: this.objectInputsController.visible, + matrixAutoUpdate: this.objectInputsController.matrixAutoUpdate, + } as UnknownRecord; + + if (this.objectInputsController.name) { + customProps['name'] = this.objectInputsController.name; + } + + if (this.objectInputsController.position) { + customProps['position'] = this.objectInputsController.position; + } + + if (this.objectInputsController.rotation) { + customProps['rotation'] = this.objectInputsController.rotation; + } else if (this.objectInputsController.quaternion) { + customProps['quaternion'] = this.objectInputsController.quaternion; + } + + if (this.objectInputsController.scale) { + customProps['scale'] = this.objectInputsController.scale; + } + + if (this.objectInputsController.userData) { + customProps['userData'] = this.objectInputsController.userData; + } + + if (this.objectInputsController.color) { + customProps['color'] = this.objectInputsController.color; + if (!this.store.getImperativeState().linear) { + (customProps['color'] as THREE.Color).convertSRGBToLinear(); + } + } + + if (this.objectInputsController.dispose) { + customProps['dispose'] = this.objectInputsController.dispose; + } + + this.objectInputsController.change$.pipe(take(1)).subscribe((changes) => { + if (changes) { + for (const [inputName, inputChange] of Object.entries(changes)) { + if ( + !inputChange.isFirstChange() || + [ + 'name', + 'position', + 'rotation', + 'quaternion', + 'scale', + 'userData', + 'color', + 'dispose', + 'castShadow', + 'receiveShadow', + 'visible', + 'matrixAutoUpdate', + 'object3dController', + ].includes(inputName) // skip 14 common inputs + ) { + continue; + } + customProps[inputName] = inputChange.currentValue; + } + } + }); + + applyProps(this.object3d, customProps); + this.object3d.updateMatrix(); + }); + } + + ngOnDestroy() { + this.ngZone.runOutsideAngular(() => { + if (this.#inputChangesSubscription) { + this.#inputChangesSubscription.unsubscribe(); + } + + if (this.object3d) { + this.#remove(); + this.instancesStore.removeObject(this.object3d.uuid); + this.eventsStore.removeInteraction(this.object3d.uuid); + } + }); + } + + get object3d(): THREE.Object3D { + return this.#object3d as THREE.Object3D; + } + + get controller(): Controller | undefined { + return undefined; + } + + get props(): string[] { + return []; + } +} + +@NgModule({ + declarations: [NgtObject3dController], + exports: [NgtObject3dController], +}) +export class NgtObject3dControllerModule {} + +export const [NGT_OBJECT_WATCHED_CONTROLLER, NGT_OBJECT_CONTROLLER_PROVIDER] = + createControllerProviderFactory({ + watchedControllerTokenName: 'Watched Object3dController', + controller: NgtObject3dController, + }); diff --git a/packages/core/src/lib/core.module.ts b/packages/core/src/lib/core.module.ts index 4089196a7..abb009515 100644 --- a/packages/core/src/lib/core.module.ts +++ b/packages/core/src/lib/core.module.ts @@ -1,8 +1,20 @@ import { NgModule } from '@angular/core'; import { NgtCanvas } from './canvas.component'; +import { NgtAnimationSubscriberControllerModule } from './controllers/animation-subscriber.controller'; +import { NgtAudioControllerModule } from './controllers/audio.controller'; +import { NgtMaterialGeometryControllerModule } from './controllers/material-geometry.controller'; +import { NgtObject3dInputsControllerModule } from './controllers/object-3d-inputs.controller'; +import { NgtObject3dControllerModule } from './controllers/object-3d.controller'; @NgModule({ declarations: [NgtCanvas], - exports: [NgtCanvas], + exports: [ + NgtCanvas, + NgtMaterialGeometryControllerModule, + NgtObject3dControllerModule, + NgtObject3dInputsControllerModule, + NgtAudioControllerModule, + NgtAnimationSubscriberControllerModule, + ], }) export class NgtCoreModule {} diff --git a/packages/core/src/lib/models/common.ts b/packages/core/src/lib/models/common.ts index 26795df7b..07a1935c2 100644 --- a/packages/core/src/lib/models/common.ts +++ b/packages/core/src/lib/models/common.ts @@ -1,5 +1,6 @@ export type UnknownRecord = Record; export type AnyConstructor = new (...args: any[]) => TObject; +export type AnyExtenderFunction = (object: TObject) => void; export type Properties = Pick< T, @@ -17,10 +18,12 @@ export type BranchingReturn< > = ConditionalType; export type LessFirstConstructorParameters< - T, - TReturn = T extends [infer First, ...infer Rest] ? Rest : T -> = TReturn; + T extends AnyConstructor, + TConstructor = ConstructorParameters +> = TConstructor extends [infer First, ...infer Rest] ? Rest : TConstructor; export type LessFirstTwoConstructorParameters< - T, - TReturn = T extends [infer First, infer Second, ...infer Rest] ? Rest : T -> = TReturn; + T extends AnyConstructor, + TConstructor = ConstructorParameters +> = TConstructor extends [infer First, infer Second, ...infer Rest] + ? Rest + : TConstructor; diff --git a/packages/core/src/lib/models/events.ts b/packages/core/src/lib/models/events.ts index 28fca69c7..eaa945f21 100644 --- a/packages/core/src/lib/models/events.ts +++ b/packages/core/src/lib/models/events.ts @@ -1,7 +1,7 @@ import type { NgtIntersectionEvent } from './intersection'; -export type NgtEvent = TEvent & NgtIntersectionEvent; -export type NgtDomEvent = NgtEvent; +export type NgtEvent = NgtIntersectionEvent; +export type NgtDomEvent = PointerEvent | MouseEvent | WheelEvent; export interface NgtEventHandlers { click?: (event: NgtEvent) => void; @@ -14,7 +14,7 @@ export interface NgtEventHandlers { pointerenter?: (event: NgtEvent) => void; pointerleave?: (event: NgtEvent) => void; pointermove?: (event: NgtEvent) => void; - pointermissed?: (event: NgtEvent) => void; + pointermissed?: (event: MouseEvent) => void; pointercancel?: (event: NgtEvent) => void; wheel?: (event: NgtEvent) => void; } diff --git a/packages/core/src/lib/models/states/animation-frame-state.ts b/packages/core/src/lib/models/states/animation-frame-state.ts index 23171eeaf..b95223a47 100644 --- a/packages/core/src/lib/models/states/animation-frame-state.ts +++ b/packages/core/src/lib/models/states/animation-frame-state.ts @@ -1,11 +1,6 @@ import * as THREE from 'three'; import type { NgtRender } from '../render'; -export interface NgtAnimationReady { - animateObject: TObject; - renderState: NgtRender; -} - export type NgtAnimationCallback = ( state: NgtRender, obj: TObject diff --git a/packages/core/src/lib/models/states/events-state.ts b/packages/core/src/lib/models/states/events-state.ts index 652a86e96..b9aacef82 100644 --- a/packages/core/src/lib/models/states/events-state.ts +++ b/packages/core/src/lib/models/states/events-state.ts @@ -17,14 +17,14 @@ export type NgtSupportedEvents = export interface NgtEventsInternal { interaction: THREE.Object3D[]; - hovered: Map; + hovered: Map>; capturedMap: Map>; initialClick: [x: number, y: number]; initialHits: THREE.Object3D[]; } export interface NgtEventsStoreState { - pointermissed?: (event: NgtEvent) => void; + pointermissed?: (event: MouseEvent) => void; connected: false | HTMLElement; internal: NgtEventsInternal; handlers?: Record; diff --git a/packages/core/src/lib/models/states/state.ts b/packages/core/src/lib/models/states/state.ts index f593c6694..5324e30f5 100644 --- a/packages/core/src/lib/models/states/state.ts +++ b/packages/core/src/lib/models/states/state.ts @@ -10,6 +10,7 @@ export interface NgtState { frameloop: 'always' | 'demand' | 'never'; ready: boolean; vr: boolean; + linear: boolean; size: NgtSize; viewport: NgtViewport & { getCurrentViewport: ( diff --git a/packages/core/src/lib/models/three.ts b/packages/core/src/lib/models/three.ts index a6616a1d0..d5df6c4f9 100644 --- a/packages/core/src/lib/models/three.ts +++ b/packages/core/src/lib/models/three.ts @@ -22,11 +22,7 @@ export type NgtVector4 = | THREE.Vector4 | Parameters | Parameters[0]; -export type NgtColor = - | ConstructorParameters - | THREE.Color - | number - | string; // Parameters will not work here because of multiple function signatures in three.js types +export type NgtColor = THREE.ColorRepresentation | NgtTriplet; export type NgtColorArray = typeof THREE.Color | Parameters; export type NgtLayers = THREE.Layers | Parameters[0]; export type NgtQuaternion = diff --git a/packages/core/src/lib/resize/resize.service.ts b/packages/core/src/lib/resize/resize.service.ts index c1f34325c..e9f7a0f71 100644 --- a/packages/core/src/lib/resize/resize.service.ts +++ b/packages/core/src/lib/resize/resize.service.ts @@ -136,11 +136,11 @@ export class NgtResize extends Observable { }) .pipe(debounceAndDestroy(scrollDebounce)) .subscribe(boundEntriesCallback); - - fromEvent(document.defaultView as Window, 'resize') - .pipe(debounceAndDestroy(resizeDebounce)) - .subscribe(boundEntriesCallback); } + + fromEvent(document.defaultView as Window, 'resize') + .pipe(debounceAndDestroy(resizeDebounce)) + .subscribe(boundEntriesCallback); }); return () => { diff --git a/packages/core/src/lib/services/destroyed.service.ts b/packages/core/src/lib/services/destroyed.service.ts new file mode 100644 index 000000000..f99704e1e --- /dev/null +++ b/packages/core/src/lib/services/destroyed.service.ts @@ -0,0 +1,10 @@ +import { Injectable, OnDestroy } from '@angular/core'; +import { Subject } from 'rxjs'; + +@Injectable() +export class NgtDestroyedService extends Subject implements OnDestroy { + ngOnDestroy() { + this.next(); + this.complete(); + } +} diff --git a/packages/core/src/lib/services/loader.service.ts b/packages/core/src/lib/services/loader.service.ts new file mode 100644 index 000000000..4686c835f --- /dev/null +++ b/packages/core/src/lib/services/loader.service.ts @@ -0,0 +1,83 @@ +import { Injectable, OnDestroy } from '@angular/core'; +import { + catchError, + defer, + forkJoin, + map, + Observable, + of, + ReplaySubject, + share, + tap, + throwError, +} from 'rxjs'; +import * as THREE from 'three'; +import type { GLTF } from 'three/examples/jsm/loaders/GLTFLoader'; +import type { + BranchingReturn, + LoaderExtensions, + NgtLoaderResult, + NgtObjectMap, +} from '../models'; +import { buildGraph } from '../utils/build-graph'; + +@Injectable({ + providedIn: 'root', +}) +export class NgtLoaderService implements OnDestroy { + private readonly cached = new Map(); + + use( + loaderConstructor: new () => NgtLoaderResult, + input: TUrl, + extensions?: LoaderExtensions, + onProgress?: (event: ProgressEvent) => void + ): TUrl extends any[] + ? Observable[]> + : Observable> { + const keys = (Array.isArray(input) ? input : [input]) as string[]; + const loader = new loaderConstructor(); + if (extensions) { + extensions(loader); + } + + const results$ = forkJoin( + keys.map((key) => { + if (this.cached.has(key)) { + return of(this.cached.get(key)); + } + + return defer(() => loader.loadAsync(key, onProgress)).pipe( + tap((data) => { + if (data.scene) { + Object.assign(data, buildGraph(data.scene as THREE.Scene)); + } + this.cached.set(key, data); + }), + catchError((err) => { + console.error(`Error loading ${key}: ${err.message}`); + return throwError(err); + }) + ); + }) + ) as Observable[]>; + + return defer(() => + Array.isArray(input) + ? results$ + : results$.pipe(map((results) => results[0])) + ).pipe( + share({ + connector: () => new ReplaySubject(), + resetOnRefCountZero: true, + resetOnError: true, + }) + ) as TUrl extends any[] + ? Observable[]> + : Observable>; + } + + ngOnDestroy() { + this.cached.clear(); + } +} diff --git a/packages/core/src/lib/services/loop.service.ts b/packages/core/src/lib/services/loop.service.ts index 57baca251..842c06993 100644 --- a/packages/core/src/lib/services/loop.service.ts +++ b/packages/core/src/lib/services/loop.service.ts @@ -5,8 +5,8 @@ import { NgtStore } from '../stores/store'; @Injectable() export class NgtLoopService { - private running = false; - private repeat?: number; + #running = false; + #repeat?: number; constructor( private store: NgtStore, @@ -68,24 +68,24 @@ export class NgtLoopService { loop(timestamp: number): number | undefined { return this.ngZone.runOutsideAngular(() => { - this.running = true; - this.repeat = 0; + this.#running = true; + this.#repeat = 0; const state = this.store.getImperativeState(); if ( state.ready && (state.frameloop === 'always' || this.store.frames > 0) ) { - this.repeat += this.render( + this.#repeat += this.render( timestamp, state, this.animationFrameStore.getImperativeState() ); } - if (this.repeat > 0) return requestAnimationFrame(this.loop.bind(this)); + if (this.#repeat > 0) return requestAnimationFrame(this.loop.bind(this)); - this.running = false; + this.#running = false; return; }); } @@ -96,8 +96,8 @@ export class NgtLoopService { // Increase frames, do not go higher than 60 this.store.frames = Math.min(60, this.store.frames + 1); // If the render-loop isn't active, start it - if (!this.running) { - this.running = true; + if (!this.#running) { + this.#running = true; requestAnimationFrame(this.loop.bind(this)); } } diff --git a/packages/core/src/lib/stores/animation-frame.store.ts b/packages/core/src/lib/stores/animation-frame.store.ts index 09b4ee74a..153500c88 100644 --- a/packages/core/src/lib/stores/animation-frame.store.ts +++ b/packages/core/src/lib/stores/animation-frame.store.ts @@ -18,7 +18,7 @@ export class NgtAnimationFrameStore extends EnhancedComponentStore { this.ngZone.runOutsideAngular(() => { - this.updateSubscribers(this.selectors.animations$); + this.#updateSubscribers(this.selectors.animations$); }); return () => { @@ -40,19 +40,32 @@ export class NgtAnimationFrameStore extends EnhancedComponentStore { - this.ngZone.runOutsideAngular(() => { - this.patchState((state) => { - const { [uuid]: _, ...animations } = state.animations; - return { animations }; - }); - }); + return (prevAnimation, isUnsub) => { + if ( + (prevAnimation && prevAnimation.obj !== animation.obj) || + isUnsub + ) { + this.unregister(uuid); + } }; }) ) ); - private readonly updateSubscribers = this.effect< + readonly unregister = this.effect((uuid$) => + uuid$.pipe( + tap((uuid) => { + this.ngZone.runOutsideAngular(() => { + this.patchState((state) => { + const { [uuid]: _, ...animations } = state.animations; + return { animations }; + }); + }); + }) + ) + ); + + readonly #updateSubscribers = this.effect< NgtAnimationFrameStoreState['animations'] >((animations$) => animations$.pipe( diff --git a/packages/core/src/lib/stores/canvas-inputs.store.ts b/packages/core/src/lib/stores/canvas-inputs.store.ts index ad53c0066..1cb86eea9 100644 --- a/packages/core/src/lib/stores/canvas-inputs.store.ts +++ b/packages/core/src/lib/stores/canvas-inputs.store.ts @@ -1,7 +1,6 @@ import { ElementRef, Inject, Injectable } from '@angular/core'; import * as THREE from 'three'; -import { NgtPerformance } from '../models'; -import { NgtCanvasInputsState } from '../models/states/canvas-inputs-state'; +import { NgtCanvasInputsState, NgtPerformance } from '../models'; import { NGT_PERFORMANCE_OPTIONS } from '../performance/tokens'; import { EnhancedComponentStore } from './enhanced-component-store'; diff --git a/packages/core/src/lib/stores/enhanced-component-store.ts b/packages/core/src/lib/stores/enhanced-component-store.ts index f62d0d6e9..2039ecbc5 100644 --- a/packages/core/src/lib/stores/enhanced-component-store.ts +++ b/packages/core/src/lib/stores/enhanced-component-store.ts @@ -102,18 +102,18 @@ export abstract class EnhancedComponentStore< getUpdaters(this) as unknown as StoreUpdaters & AsyncStoreUpdaters; - private readonly $imperative: BehaviorSubject; + readonly #imperative$: BehaviorSubject; protected constructor(state: TState) { super(state); - this.$imperative = new BehaviorSubject(state); - this.watchImperative(this.state$); + this.#imperative$ = new BehaviorSubject(state); + this.#watchImperative(this.state$); } - private readonly watchImperative = this.effect((state$) => + readonly #watchImperative = this.effect((state$) => state$.pipe( tap((state) => { - this.$imperative.next(state); + this.#imperative$.next(state); }) ) ); @@ -197,7 +197,7 @@ export abstract class EnhancedComponentStore< } getImperativeState(): TState { - return this.$imperative.getValue(); + return this.#imperative$.getValue(); } } @@ -228,16 +228,19 @@ export function tapEffect( effectFn: ( value: T, firstRun: boolean - ) => ((previousValue: T | undefined) => void) | void + ) => ((previousValue: T | undefined, isUnsubscribed: boolean) => void) | void ): MonoTypeOperatorFunction { - let cleanupFn: (previousValue: T | undefined) => void = noop; + let cleanupFn: ( + previousValue: T | undefined, + isUnsubscribed: boolean + ) => void = noop; let firstRun = false; let latestValue: T | undefined = undefined; return tap({ next: (value: T) => { if (cleanupFn && firstRun) { - cleanupFn(latestValue); + cleanupFn(latestValue, false); } const cleanUpOrVoid = effectFn(value, firstRun); @@ -253,7 +256,7 @@ export function tapEffect( }, unsubscribe: () => { if (cleanupFn) { - cleanupFn(latestValue); + cleanupFn(latestValue, true); } }, }); diff --git a/packages/core/src/lib/stores/events.store.ts b/packages/core/src/lib/stores/events.store.ts index 2b7d08236..ecb8144d2 100644 --- a/packages/core/src/lib/stores/events.store.ts +++ b/packages/core/src/lib/stores/events.store.ts @@ -1,8 +1,9 @@ import { Injectable, NgZone } from '@angular/core'; -import { tap, withLatestFrom } from 'rxjs'; +import { noop, tap, withLatestFrom } from 'rxjs'; import * as THREE from 'three'; import { NgtDomEvent, + NgtEvent, NgtEventsStoreState, NgtPointerCaptureTarget, } from '../models'; @@ -27,11 +28,12 @@ const names = { export class NgtEventsStore extends EnhancedComponentStore { constructor(private store: NgtStore, private ngZone: NgZone) { super({ + pointermissed: noop, connected: false, handlers: {} as NgtEventsStoreState['handlers'], internal: { interaction: [], - hovered: new Map(), + hovered: new Map>(), capturedMap: new Map< number, Map diff --git a/packages/core/src/lib/stores/instances.store.ts b/packages/core/src/lib/stores/instances.store.ts index 05adc0d9f..01ff23123 100644 --- a/packages/core/src/lib/stores/instances.store.ts +++ b/packages/core/src/lib/stores/instances.store.ts @@ -31,7 +31,7 @@ export class NgtInstancesStore extends EnhancedComponentStore((state, { geometry, id = geometry.uuid }) => ({ ...state, - bufferGeometries: { ...state.geometries, [id]: geometry }, + geometries: { ...state.geometries, [id]: geometry }, })); readonly removeGeometry = this.updater((state, id) => { diff --git a/packages/core/src/lib/stores/performance.store.ts b/packages/core/src/lib/stores/performance.store.ts index 05efc66bb..0f5517e1a 100644 --- a/packages/core/src/lib/stores/performance.store.ts +++ b/packages/core/src/lib/stores/performance.store.ts @@ -19,24 +19,23 @@ export class NgtPerformanceStore extends EnhancedComponentStore $.pipe( tap(() => { this.ngZone.runOutsideAngular(() => { - this.setPerformance(this.canvasInputsStore.selectors.performance$); + this.#setPerformance(this.canvasInputsStore.selectors.performance$); }); }) ) ); - private readonly setPerformance = this.effect( - (performance$) => - performance$.pipe( - tap(({ min, max, debounce, current }) => { - this.ngZone.runOutsideAngular(() => { - this.updaters.setCurrent(current); - this.updaters.setMin(min); - this.updaters.setMax(max); - this.updaters.setDebounce(debounce); - }); - }) - ) + readonly #setPerformance = this.effect((performance$) => + performance$.pipe( + tap(({ min, max, debounce, current }) => { + this.ngZone.runOutsideAngular(() => { + this.updaters.setCurrent(current); + this.updaters.setMin(min); + this.updaters.setMax(max); + this.updaters.setDebounce(debounce); + }); + }) + ) ); readonly regress = this.effect(($) => diff --git a/packages/core/src/lib/stores/store.ts b/packages/core/src/lib/stores/store.ts index c68d6c4bd..3e2e4a66e 100644 --- a/packages/core/src/lib/stores/store.ts +++ b/packages/core/src/lib/stores/store.ts @@ -21,7 +21,7 @@ const defaultTarget = new THREE.Vector3(); @Injectable() export class NgtStore extends EnhancedComponentStore { - private readonly sizeResult$ = this.select( + readonly #sizeResult$ = this.select( this.resizeResult$, ({ width, height }) => ({ width, @@ -30,14 +30,14 @@ export class NgtStore extends EnhancedComponentStore { { debounce: true } ); - private readonly dprResult$ = this.select( + readonly #dprResult$ = this.select( this.resizeResult$, this.selectors.viewport$, ({ dpr }, viewport) => ({ ...viewport, dpr }), { debounce: true } ); - private readonly allReady$ = this.select( + readonly #allReady$ = this.select( this.selectors.scene$, this.selectors.camera$, this.selectors.renderer$, @@ -47,7 +47,7 @@ export class NgtStore extends EnhancedComponentStore { { debounce: true } ); - private readonly dimensions$ = this.select( + readonly #dimensions$ = this.select( this.selectors.size$, this.selectors.viewport$, (size, viewport) => ({ size, viewport }), @@ -69,6 +69,7 @@ export class NgtStore extends EnhancedComponentStore { clock: canvasInputsStore.getImperativeState().clock, frameloop: canvasInputsStore.getImperativeState().frameloop, vr: canvasInputsStore.getImperativeState().vr, + linear: canvasInputsStore.getImperativeState().linear, viewport: { initialDpr: calculateDpr(canvasInputsStore.getImperativeState().dpr), dpr: calculateDpr(canvasInputsStore.getImperativeState().dpr), @@ -120,20 +121,21 @@ export class NgtStore extends EnhancedComponentStore { canvas$.pipe( tapEffect((canvasElement) => { this.ngZone.runOutsideAngular(() => { - this.updaters.setReady(this.allReady$); - this.updaters.setSize(this.sizeResult$); - this.updaters.setViewport(this.dprResult$); + this.updaters.setReady(this.#allReady$); + this.updaters.setSize(this.#sizeResult$); + this.updaters.setViewport(this.#dprResult$); this.updaters.setFrameloop( this.canvasInputsStore.selectors.frameloop$ ); this.updaters.setVr(this.canvasInputsStore.selectors.vr$); + this.updaters.setLinear(this.canvasInputsStore.selectors.linear$); this.initRenderer(canvasElement); this.initScene(); this.initCamera(); this.initRaycaster(); - this.updateDimensions(this.dimensions$); + this.updateDimensions(this.#dimensions$); }); return () => { diff --git a/packages/core/src/lib/three/attribute.ts b/packages/core/src/lib/three/attribute.ts new file mode 100644 index 000000000..9e708a9c9 --- /dev/null +++ b/packages/core/src/lib/three/attribute.ts @@ -0,0 +1,91 @@ +import { + Directive, + Input, + NgZone, + OnChanges, + OnDestroy, + OnInit, + Optional, +} from '@angular/core'; +import * as THREE from 'three'; +import type { AnyConstructor } from '../models'; +import { NgtGeometry } from './geometry'; + +@Directive() +export abstract class NgtAttribute< + TAttribute extends + | THREE.BufferAttribute + | THREE.InterleavedBufferAttribute = THREE.BufferAttribute +> implements OnInit, OnChanges, OnDestroy +{ + @Input() attach?: THREE.BuiltinShaderAttributeName; + + abstract attributeType: AnyConstructor; + + constructor( + protected ngZone: NgZone, + @Optional() protected geometryDirective: NgtGeometry | null + ) {} + + #attributeArgs: unknown[] = []; + + protected set attributeArgs(v: unknown | unknown[]) { + this.#attributeArgs = Array.isArray(v) ? v : [v]; + this.ngZone.runOutsideAngular(() => { + this.init(); + }); + } + + #attribute?: TAttribute; + #defaultValue?: TAttribute; + + ngOnChanges() { + this.ngZone.runOutsideAngular(() => { + if (this.attribute) { + this.attribute.needsUpdate = true; + } + }); + } + + ngOnInit() { + this.ngZone.runOutsideAngular(() => { + if (!this.attribute) { + this.init(); + } + }); + } + + private init() { + if (this.geometryDirective && this.attach) { + this.#attribute = new this.attributeType(...this.#attributeArgs); + if (this.attribute) { + this.#defaultValue = this.geometryDirective.geometry.attributes[ + this.attach + ] as TAttribute; + this.geometryDirective.geometry.setAttribute( + this.attach, + this.attribute + ); + } + } + } + + ngOnDestroy() { + this.ngZone.runOutsideAngular(() => { + if (this.geometryDirective && this.attach) { + if (this.#defaultValue !== undefined) { + this.geometryDirective.geometry.setAttribute( + this.attach, + this.#defaultValue + ); + } else { + this.geometryDirective.geometry.deleteAttribute(this.attach); + } + } + }); + } + + get attribute(): TAttribute | undefined { + return this.#attribute; + } +} diff --git a/packages/core/src/lib/three/audio.ts b/packages/core/src/lib/three/audio.ts new file mode 100644 index 000000000..607d83dd8 --- /dev/null +++ b/packages/core/src/lib/three/audio.ts @@ -0,0 +1,56 @@ +import { Directive, Inject, Input, NgZone, OnInit } from '@angular/core'; +import * as THREE from 'three'; +import { + NGT_OBJECT_WATCHED_CONTROLLER, + NgtObject3dController, +} from '../controllers/object-3d.controller'; +import { AnyConstructor } from '../models'; + +@Directive() +export abstract class NgtCommonAudio< + TAudioNode extends AudioNode = GainNode, + TAudio extends THREE.Audio = THREE.Audio +> implements OnInit +{ + @Input() listener!: THREE.AudioListener; + + #audioArgs: unknown[] = []; + protected set audioArgs(v: unknown | unknown[]) { + this.#audioArgs = Array.isArray(v) ? v : [v]; + this.ngZone.runOutsideAngular(() => { + this.objectController.init(); + }); + } + + constructor( + @Inject(NGT_OBJECT_WATCHED_CONTROLLER) + protected objectController: NgtObject3dController, + protected ngZone: NgZone + ) {} + + abstract audioType: AnyConstructor; + + #audio!: TAudio; + + ngOnInit() { + this.objectController.initFn = () => { + return this.ngZone.runOutsideAngular(() => { + if (!this.listener) { + throw new Error('Cannot initialize Audio without an AudioListener'); + } + + this.#audio = new this.audioType(this.listener, ...this.#audioArgs); + return this.#audio; + }); + }; + this.ngZone.runOutsideAngular(() => { + if (!this.#audio) { + this.objectController.init(); + } + }); + } + + get audio() { + return this.#audio; + } +} diff --git a/packages/core/src/lib/three/camera.ts b/packages/core/src/lib/three/camera.ts new file mode 100644 index 000000000..b6a48e8a9 --- /dev/null +++ b/packages/core/src/lib/three/camera.ts @@ -0,0 +1,48 @@ +import { Directive, Inject, NgZone } from '@angular/core'; +import * as THREE from 'three'; +import { + NGT_OBJECT_WATCHED_CONTROLLER, + NgtObject3dController, +} from '../controllers/object-3d.controller'; +import { AnyConstructor } from '../models'; + +@Directive() +export abstract class NgtCommonCamera< + TCamera extends THREE.Camera = THREE.Camera +> { + abstract cameraType: AnyConstructor; + + constructor( + @Inject(NGT_OBJECT_WATCHED_CONTROLLER) + protected objectController: NgtObject3dController, + protected ngZone: NgZone + ) {} + + #cameraArgs: unknown[] = []; + protected set cameraArgs(v: unknown | unknown[]) { + this.#cameraArgs = Array.isArray(v) ? v : [v]; + this.ngZone.runOutsideAngular(() => { + this.objectController.init(); + }); + } + + ngOnInit() { + this.objectController.initFn = () => { + return this.ngZone.runOutsideAngular(() => { + this.#camera = new this.cameraType(...this.#cameraArgs); + return this.#camera; + }); + }; + + this.ngZone.runOutsideAngular(() => { + if (!this.#camera) { + this.objectController.init(); + } + }); + } + + #camera!: TCamera; + get camera(): TCamera { + return this.#camera; + } +} diff --git a/packages/core/src/lib/three/curve.ts b/packages/core/src/lib/three/curve.ts new file mode 100644 index 000000000..10795f778 --- /dev/null +++ b/packages/core/src/lib/three/curve.ts @@ -0,0 +1,52 @@ +import { Directive, Input, NgZone, OnInit, Optional } from '@angular/core'; +import * as THREE from 'three'; +import type { AnyConstructor } from '../models'; +import { NgtGeometry } from './geometry'; + +@Directive() +export abstract class NgtCurve< + TCurve extends THREE.Curve = THREE.Curve +> implements OnInit +{ + @Input() divisions?: number; + + abstract curveType: AnyConstructor; + + #curveArgs: unknown[] = []; + + protected set curveArgs(v: unknown | unknown[]) { + this.#curveArgs = Array.isArray(v) ? v : [v]; + this.ngZone.runOutsideAngular(() => { + this.init(); + }); + } + + constructor( + protected ngZone: NgZone, + @Optional() protected geometryDirective: NgtGeometry | null + ) {} + + #curve?: TCurve; + + ngOnInit() { + this.ngZone.runOutsideAngular(() => { + if (!this.curve) { + this.init(); + } + }); + } + + private init() { + this.#curve = new this.curveType(...this.#curveArgs); + if (this.curve && this.geometryDirective) { + const points = this.curve.getPoints(this.divisions); + this.geometryDirective.geometry.setFromPoints( + points as unknown as THREE.Vector3[] | THREE.Vector2[] + ); + } + } + + get curve(): TCurve | undefined { + return this.#curve; + } +} diff --git a/packages/core/src/lib/three/geometry.ts b/packages/core/src/lib/three/geometry.ts new file mode 100644 index 000000000..8cfdc9a61 --- /dev/null +++ b/packages/core/src/lib/three/geometry.ts @@ -0,0 +1,95 @@ +import { + Directive, + EventEmitter, + Input, + NgZone, + OnDestroy, + OnInit, + Optional, + Output, +} from '@angular/core'; +import * as THREE from 'three'; +import { NgtObject3dController } from '../controllers/object-3d.controller'; +import type { AnyConstructor, UnknownRecord } from '../models'; +import { NgtInstancesStore } from '../stores/instances.store'; + +@Directive() +export abstract class NgtGeometry< + TGeometry extends THREE.BufferGeometry = THREE.BufferGeometry +> implements OnInit, OnDestroy +{ + @Input() ngtId?: string; + @Output() ready = new EventEmitter(); + + constructor( + protected instancesStore: NgtInstancesStore, + protected ngZone: NgZone, + @Optional() private parentObject: NgtObject3dController | null + ) {} + + abstract geometryType: AnyConstructor; + + #geometryArgs: unknown[] = []; + protected set geometryArgs(v: unknown | unknown[]) { + this.#geometryArgs = Array.isArray(v) ? v : [v]; + this.ngZone.runOutsideAngular(() => { + this.#init(); + }); + } + + ngOnInit() { + this.ngZone.runOutsideAngular(() => { + if (!this.geometry) { + this.#init(); + } + }); + } + + #init() { + // geometry has changed. reconstruct + if (this.geometry) { + // cleanup + this.instancesStore.removeGeometry(this.ngtId || this.geometry.uuid); + if (this.parentObject) { + const object3d = this.parentObject.object3d as unknown as UnknownRecord; + if (object3d.geometry) { + (object3d.geometry as THREE.BufferGeometry).dispose(); + } + } + + // reconstruct + this.#construct(); + if (this.parentObject) { + const object3d = this.parentObject.object3d as unknown as UnknownRecord; + object3d.geometry = this.geometry; + } + } else { + this.#construct(); + } + } + + #construct() { + this.#geometry = new this.geometryType(...this.#geometryArgs); + + this.instancesStore.saveGeometry({ + id: this.ngtId || this.geometry.uuid, + geometry: this.geometry, + }); + + this.ready.emit(); + } + + #geometry!: TGeometry; + get geometry(): TGeometry { + return this.#geometry; + } + + ngOnDestroy() { + this.ngZone.runOutsideAngular(() => { + if (this.geometry) { + this.instancesStore.removeGeometry(this.ngtId || this.geometry.uuid); + this.geometry.dispose(); + } + }); + } +} diff --git a/packages/core/src/lib/three/helper.ts b/packages/core/src/lib/three/helper.ts new file mode 100644 index 000000000..be646ef6e --- /dev/null +++ b/packages/core/src/lib/three/helper.ts @@ -0,0 +1,56 @@ +import { Directive, Inject, NgZone, OnChanges, OnInit } from '@angular/core'; +import * as THREE from 'three'; +import { + NGT_OBJECT_WATCHED_CONTROLLER, + NgtObject3dController, +} from '../controllers/object-3d.controller'; +import { AnyConstructor } from '../models'; + +@Directive() +export abstract class NgtHelper + implements OnInit, OnChanges +{ + abstract helperType: AnyConstructor; + + #helper!: THelper; + #helperArgs: unknown[] = []; + protected set helperArgs(v: unknown | unknown[]) { + this.#helperArgs = Array.isArray(v) ? v : [v]; + this.ngZone.runOutsideAngular(() => { + this.objectController.init(); + }); + } + + constructor( + @Inject(NGT_OBJECT_WATCHED_CONTROLLER) + protected objectController: NgtObject3dController, + protected ngZone: NgZone + ) {} + + ngOnChanges() { + this.ngZone.runOutsideAngular(() => { + if (!this.#helper) { + this.objectController.init(); + } + }); + } + + ngOnInit() { + this.objectController.initFn = () => { + return this.ngZone.runOutsideAngular(() => { + this.#helper = new this.helperType(...this.#helperArgs); + return this.#helper; + }); + }; + + this.ngZone.runOutsideAngular(() => { + if (!this.#helper) { + this.objectController.init(); + } + }); + } + + get helper() { + return this.#helper; + } +} diff --git a/packages/core/src/lib/three/light.ts b/packages/core/src/lib/three/light.ts new file mode 100644 index 000000000..cb7b8ee30 --- /dev/null +++ b/packages/core/src/lib/three/light.ts @@ -0,0 +1,59 @@ +import { Directive, Inject, Input, NgZone, OnInit } from '@angular/core'; +import * as THREE from 'three'; +import { + NGT_OBJECT_WATCHED_CONTROLLER, + NgtObject3dController, +} from '../controllers/object-3d.controller'; +import type { AnyConstructor } from '../models'; +import { applyProps } from '../utils/apply-props'; + +@Directive() +export abstract class NgtLight + implements OnInit +{ + abstract lightType: AnyConstructor; + + @Input() intensity?: number; + @Input() shadow?: Partial; + + constructor( + @Inject(NGT_OBJECT_WATCHED_CONTROLLER) + protected objectController: NgtObject3dController, + protected ngZone: NgZone + ) {} + + #lightArgs: unknown[] = []; + protected set lightArgs(v: unknown | unknown[]) { + this.#lightArgs = Array.isArray(v) ? v : [v]; + this.ngZone.runOutsideAngular(() => { + this.objectController.init(); + }); + } + + #light!: TLight; + + ngOnInit() { + this.objectController.initFn = () => { + return this.ngZone.runOutsideAngular(() => { + this.#light = new this.lightType(...this.#lightArgs); + const props = { + intensity: this.intensity, + shadow: this.shadow, + }; + applyProps(this.#light, props); + + return this.#light; + }); + }; + + this.ngZone.runOutsideAngular(() => { + if (!this.#light) { + this.objectController.init(); + } + }); + } + + get light() { + return this.#light; + } +} diff --git a/packages/core/src/lib/three/line.ts b/packages/core/src/lib/three/line.ts new file mode 100644 index 000000000..641190974 --- /dev/null +++ b/packages/core/src/lib/three/line.ts @@ -0,0 +1,22 @@ +import { Directive, Inject, Optional } from '@angular/core'; +import * as THREE from 'three'; +import { + NGT_MATERIAL_GEOMETRY_WATCHED_CONTROLLER, + NGT_OBJECT_TYPE, + NgtMaterialGeometryController, +} from '../controllers/material-geometry.controller'; + +@Directive({ + providers: [{ provide: NGT_OBJECT_TYPE, useValue: THREE.Line }], +}) +export abstract class NgtCommonLine { + constructor( + @Optional() + @Inject(NGT_MATERIAL_GEOMETRY_WATCHED_CONTROLLER) + protected materialGeometryController: NgtMaterialGeometryController | null + ) {} + + get line() { + return this.materialGeometryController?.objectController.object3d as TLine; + } +} diff --git a/packages/core/src/lib/three/material.ts b/packages/core/src/lib/three/material.ts new file mode 100644 index 000000000..704dc3237 --- /dev/null +++ b/packages/core/src/lib/three/material.ts @@ -0,0 +1,92 @@ +import { + Directive, + EventEmitter, + Input, + NgZone, + OnDestroy, + OnInit, + Output, +} from '@angular/core'; +import * as THREE from 'three'; +import type { AnyConstructor, NgtColor, UnknownRecord } from '../models'; +import { NgtInstancesStore } from '../stores/instances.store'; +import { NgtStore } from '../stores/store'; +import { makeColor } from '../utils/make'; + +@Directive() +export abstract class NgtMaterial< + TMaterialParameters extends THREE.MaterialParameters = THREE.MaterialParameters, + TMaterial extends THREE.Material = THREE.Material +> implements OnInit, OnDestroy +{ + @Input() ngtId?: string; + + @Output() ready = new EventEmitter(); + + @Input() set parameters(v: TMaterialParameters | undefined) { + this.#parameters = v; + if (v && this.material) { + this.ngZone.runOutsideAngular(() => { + this.convertColorToLinear(v); + this.material.setValues(v); + this.material.needsUpdate = true; + }); + } + } + + get parameters(): TMaterialParameters | undefined { + return this.#parameters; + } + + #parameters?: TMaterialParameters; + + constructor( + protected ngZone: NgZone, + protected instancesStore: NgtInstancesStore, + protected store: NgtStore + ) {} + + abstract materialType: AnyConstructor; + + ngOnInit() { + this.ngZone.runOutsideAngular(() => { + if (this.parameters) { + this.convertColorToLinear(this.parameters); + } + this.#material = new this.materialType(this.parameters); + this.instancesStore.saveMaterial({ + id: this.ngtId, + material: this.material, + }); + + this.ready.emit(); + }); + } + + #material!: TMaterial; + get material(): TMaterial { + return this.#material; + } + + private convertColorToLinear(parameters: TMaterialParameters) { + if ('color' in parameters) { + const colorParams = (parameters as UnknownRecord)['color'] as NgtColor; + (parameters as UnknownRecord)['color'] = makeColor(colorParams); + + if (!this.store.getImperativeState().linear) { + ( + (parameters as UnknownRecord)['color'] as THREE.Color + ).convertSRGBToLinear(); + } + } + } + + ngOnDestroy() { + this.ngZone.runOutsideAngular(() => { + if (this.material) { + this.instancesStore.removeMaterial(this.ngtId || this.material.uuid); + this.material.dispose(); + } + }); + } +} diff --git a/packages/core/src/lib/three/mesh.ts b/packages/core/src/lib/three/mesh.ts new file mode 100644 index 000000000..bcd8fb7da --- /dev/null +++ b/packages/core/src/lib/three/mesh.ts @@ -0,0 +1,20 @@ +import { Directive, Inject, NgZone, Optional } from '@angular/core'; +import * as THREE from 'three'; +import { + NGT_MATERIAL_GEOMETRY_WATCHED_CONTROLLER, + NgtMaterialGeometryController, +} from '../controllers/material-geometry.controller'; + +@Directive() +export abstract class NgtCommonMesh { + constructor( + @Optional() + @Inject(NGT_MATERIAL_GEOMETRY_WATCHED_CONTROLLER) + protected materialGeometryController: NgtMaterialGeometryController | null, + protected ngZone: NgZone + ) {} + + get mesh() { + return this.materialGeometryController?.objectController.object3d as TMesh; + } +} diff --git a/packages/core/src/lib/three/sprite.ts b/packages/core/src/lib/three/sprite.ts new file mode 100644 index 000000000..b78603189 --- /dev/null +++ b/packages/core/src/lib/three/sprite.ts @@ -0,0 +1,60 @@ +import { + AfterContentInit, + ContentChild, + Directive, + Inject, + Input, + NgZone, + OnInit, +} from '@angular/core'; +import * as THREE from 'three'; +import { + NGT_OBJECT_CONTROLLER_PROVIDER, + NgtObject3dController, +} from '../controllers/object-3d.controller'; +import type { AnyConstructor } from '../models'; +import { NgtMaterial } from './material'; + +@Directive() +export abstract class NgtCommonSprite< + TSprite extends THREE.Sprite = THREE.Sprite +> implements AfterContentInit, OnInit +{ + @Input() material?: THREE.SpriteMaterial; + + @ContentChild(NgtMaterial) materialDirective?: NgtMaterial; + + abstract spriteType: AnyConstructor; + + #sprite!: TSprite; + + constructor( + @Inject(NGT_OBJECT_CONTROLLER_PROVIDER) + protected objectController: NgtObject3dController, + protected ngZone: NgZone + ) {} + + ngOnInit() { + this.objectController.initFn = () => { + return this.ngZone.runOutsideAngular(() => { + if (this.material) { + this.#sprite = new this.spriteType(this.material); + } else if (this.materialDirective) { + if (this.materialDirective.material instanceof THREE.SpriteMaterial) { + this.#sprite = new this.spriteType(this.materialDirective.material); + } + } + + return this.#sprite; + }); + }; + } + + ngAfterContentInit() { + this.objectController.init(); + } + + get sprite() { + return this.#sprite; + } +} diff --git a/packages/core/src/lib/three/texture.ts b/packages/core/src/lib/three/texture.ts new file mode 100644 index 000000000..4d8702993 --- /dev/null +++ b/packages/core/src/lib/three/texture.ts @@ -0,0 +1,53 @@ +import { + Directive, + EventEmitter, + NgZone, + OnDestroy, + OnInit, + Output, +} from '@angular/core'; +import * as THREE from 'three'; +import type { AnyConstructor } from '../models'; + +@Directive() +export abstract class NgtTexture + implements OnInit, OnDestroy +{ + @Output() ready = new EventEmitter(); + + abstract textureType: AnyConstructor; + + constructor(protected ngZone: NgZone) {} + + #textureArgs: unknown[] = []; + + protected set textureArgs(v: unknown | unknown[]) { + this.#textureArgs = Array.isArray(v) ? v : [v]; + this.ngZone.runOutsideAngular(() => { + this.#texture = new this.textureType(...this.#textureArgs); + }); + } + + #texture?: TTexture; + + ngOnInit() { + this.ngZone.runOutsideAngular(() => { + if (!this.texture) { + this.#texture = new this.textureType(...this.#textureArgs); + this.ready.emit(); + } + }); + } + + get texture(): TTexture | undefined { + return this.#texture; + } + + ngOnDestroy() { + this.ngZone.runOutsideAngular(() => { + if (this.texture) { + this.texture.dispose(); + } + }); + } +} diff --git a/packages/core/src/lib/utils/events.ts b/packages/core/src/lib/utils/events.ts index a1026bec0..32a4b144a 100644 --- a/packages/core/src/lib/utils/events.ts +++ b/packages/core/src/lib/utils/events.ts @@ -213,7 +213,7 @@ export function createEvents( intersections: NgtIntersection[], event: NgtDomEvent, delta: number, - callback: (event: NgtDomEvent) => void + callback: (event: NgtEvent) => void ) { const { raycaster, mouse, camera } = stateGetter(); const { internal } = eventsStateGetter(); @@ -331,7 +331,7 @@ export function createEvents( }; // Call subscribers - callback(raycastEvent as NgtDomEvent); + callback(raycastEvent); // Event bubbling may be interrupted by stopPropagation if (localState.stopped) break; } @@ -384,13 +384,13 @@ export function createEvents( if (isClickEvent && !hits.length) { if (delta <= 2) { pointerMissed(event, internal.interaction); - if (pointermissed) pointermissed(event as NgtEvent); + if (pointermissed) pointermissed(event); } } // Take care of unhover if (isPointerMove) cancelPointer(hits); - handleIntersects(hits, event, delta, (data: NgtDomEvent) => { + handleIntersects(hits, event, delta, (data: NgtEvent) => { const eventObject = data.eventObject; const instance = (eventObject as unknown as NgtInstance).__ngt; const handlers = instance?.handlers; @@ -448,9 +448,7 @@ export function createEvents( function pointerMissed(event: MouseEvent, objects: THREE.Object3D[]) { objects.forEach((object: THREE.Object3D) => - (object as unknown as NgtInstance).__ngt?.handlers?.pointermissed?.( - event as NgtEvent - ) + (object as unknown as NgtInstance).__ngt?.handlers?.pointermissed?.(event) ); } diff --git a/packages/core/src/lib/utils/make.ts b/packages/core/src/lib/utils/make.ts new file mode 100644 index 000000000..7e62d2c8b --- /dev/null +++ b/packages/core/src/lib/utils/make.ts @@ -0,0 +1,76 @@ +import * as THREE from 'three'; +import { + AnyConstructor, + NgtColor, + NgtVector2, + NgtVector3, + NgtVector4, +} from '../models'; + +export function makeVector2(input?: NgtVector2): THREE.Vector2 | undefined { + if (!input) return undefined; + + if (input instanceof THREE.Vector2) { + return input; + } + + if (Array.isArray(input)) { + return new THREE.Vector2(...input); + } + + return new THREE.Vector2(input, input); +} + +export function makeVector3(input?: NgtVector3): THREE.Vector3 | undefined { + if (!input) return undefined; + + if (input instanceof THREE.Vector3) { + return input; + } + + if (Array.isArray(input)) { + return new THREE.Vector3(...input); + } + + return new THREE.Vector3(input, input, input); +} + +export function makeVector4(input?: NgtVector4): THREE.Vector4 | undefined { + if (!input) return undefined; + + if (input instanceof THREE.Vector4) { + return input; + } + + if (Array.isArray(input)) { + return new THREE.Vector4(...input); + } + + return new THREE.Vector4(input, input, input, input); +} + +export function makeForSet>( + type: TType, + input?: InstanceType | Parameters +): InstanceType | undefined { + if (!input) return undefined; + + if (input instanceof type) { + return input as InstanceType; + } + + return new type(...(input as Parameters)); +} + +export function makeColor(color?: NgtColor): THREE.Color | undefined { + if (!color) return undefined; + if (color instanceof THREE.Color) { + return color; + } + + if (Array.isArray(color)) { + return new THREE.Color(...color); + } + + return new THREE.Color(color); +} diff --git a/packages/core/stats/README.md b/packages/core/stats/README.md new file mode 100644 index 000000000..a80b97c1d --- /dev/null +++ b/packages/core/stats/README.md @@ -0,0 +1,3 @@ +# @angular-three/core/stats + +Secondary entry point of `@angular-three/core`. It can be used by importing from `@angular-three/core/stats`. diff --git a/packages/core/stats/ng-package.json b/packages/core/stats/ng-package.json new file mode 100644 index 000000000..c781f0df4 --- /dev/null +++ b/packages/core/stats/ng-package.json @@ -0,0 +1,5 @@ +{ + "lib": { + "entryFile": "src/index.ts" + } +} diff --git a/packages/core/stats/src/index.ts b/packages/core/stats/src/index.ts new file mode 100644 index 000000000..3d410296a --- /dev/null +++ b/packages/core/stats/src/index.ts @@ -0,0 +1 @@ +export * from './lib/stats.directive'; diff --git a/packages/core/stats/src/lib/stats.directive.ts b/packages/core/stats/src/lib/stats.directive.ts new file mode 100644 index 000000000..b6fa96043 --- /dev/null +++ b/packages/core/stats/src/lib/stats.directive.ts @@ -0,0 +1,71 @@ +import { NgtAnimationFrameStore } from '@angular-three/core'; +import { DOCUMENT } from '@angular/common'; +import { + Directive, + Inject, + Input, + NgModule, + NgZone, + OnDestroy, + OnInit, +} from '@angular/core'; +import { Subscription } from 'rxjs'; +// import Stats from minified bundle for minimizing bundle size +// @ts-ignore +import Stats from 'three/examples/js/libs/stats.min'; + +@Directive({ + selector: 'ngt-stats', + exportAs: 'ngtStats', +}) +export class NgtStats implements OnInit, OnDestroy { + @Input() parent?: HTMLElement; + + #node: HTMLElement; + #stats?: Stats; + #animationSubscription?: Subscription; + + constructor( + private animationFrameStore: NgtAnimationFrameStore, + private ngZone: NgZone, + @Inject(DOCUMENT) private document: Document + ) { + this.#node = document.body; + } + + ngOnInit() { + this.ngZone.runOutsideAngular(() => { + if (this.parent) { + this.#node = this.parent; + } + + this.#stats = new Stats(); + this.#node.appendChild(this.#stats.dom); + this.#animationSubscription = this.animationFrameStore.register({ + obj: null, + callback: () => { + this.#stats.update(); + }, + }); + }); + } + + ngOnDestroy() { + this.ngZone.runOutsideAngular(() => { + if (this.#animationSubscription) { + this.#animationSubscription.unsubscribe(); + } + + if (this.#stats) { + this.#stats.end(); + this.#node.removeChild(this.#stats.dom); + } + }); + } +} + +@NgModule({ + declarations: [NgtStats], + exports: [NgtStats], +}) +export class NgtStatsModule {} diff --git a/packages/demo/.browserslistrc b/packages/demo/.browserslistrc new file mode 100644 index 000000000..4f9ac2698 --- /dev/null +++ b/packages/demo/.browserslistrc @@ -0,0 +1,16 @@ +# This file is used by the build system to adjust CSS and JS output to support the specified browsers below. +# For additional information regarding the format and rule options, please see: +# https://github.com/browserslist/browserslist#queries + +# For the full list of supported browsers by the Angular framework, please see: +# https://angular.io/guide/browser-support + +# You can see what browsers were selected by your queries by running: +# npx browserslist + +last 1 Chrome version +last 1 Firefox version +last 2 Edge major versions +last 2 Safari major versions +last 2 iOS major versions +Firefox ESR diff --git a/packages/demo/.eslintrc.json b/packages/demo/.eslintrc.json new file mode 100644 index 000000000..256ba9672 --- /dev/null +++ b/packages/demo/.eslintrc.json @@ -0,0 +1,36 @@ +{ + "extends": ["../../.eslintrc.json"], + "ignorePatterns": ["!**/*"], + "overrides": [ + { + "files": ["*.ts"], + "extends": [ + "plugin:@nrwl/nx/angular", + "plugin:@angular-eslint/template/process-inline-templates" + ], + "rules": { + "@angular-eslint/directive-selector": [ + "error", + { + "type": "attribute", + "prefix": "ngt", + "style": "camelCase" + } + ], + "@angular-eslint/component-selector": [ + "error", + { + "type": "element", + "prefix": "ngt", + "style": "kebab-case" + } + ] + } + }, + { + "files": ["*.html"], + "extends": ["plugin:@nrwl/nx/angular-template"], + "rules": {} + } + ] +} diff --git a/packages/demo/project.json b/packages/demo/project.json new file mode 100644 index 000000000..ebf9062a8 --- /dev/null +++ b/packages/demo/project.json @@ -0,0 +1,86 @@ +{ + "projectType": "application", + "root": "packages/demo", + "sourceRoot": "packages/demo/src", + "prefix": "ngt", + "targets": { + "build": { + "executor": "@angular-devkit/build-angular:browser", + "outputs": ["{options.outputPath}"], + "options": { + "outputPath": "dist/packages/demo", + "index": "packages/demo/src/index.html", + "main": "packages/demo/src/main.ts", + "polyfills": "packages/demo/src/polyfills.ts", + "tsConfig": "packages/demo/tsconfig.app.json", + "inlineStyleLanguage": "scss", + "assets": ["packages/demo/src/favicon.ico", "packages/demo/src/assets"], + "styles": ["packages/demo/src/styles.scss"], + "scripts": [] + }, + "configurations": { + "production": { + "budgets": [ + { + "type": "initial", + "maximumWarning": "500kb", + "maximumError": "1mb" + }, + { + "type": "anyComponentStyle", + "maximumWarning": "2kb", + "maximumError": "4kb" + } + ], + "fileReplacements": [ + { + "replace": "packages/demo/src/environments/environment.ts", + "with": "packages/demo/src/environments/environment.prod.ts" + } + ], + "outputHashing": "all" + }, + "development": { + "buildOptimizer": false, + "optimization": false, + "vendorChunk": true, + "extractLicenses": false, + "sourceMap": true, + "namedChunks": true + } + }, + "defaultConfiguration": "production" + }, + "serve": { + "executor": "@angular-devkit/build-angular:dev-server", + "configurations": { + "production": { + "browserTarget": "demo:build:production" + }, + "development": { + "browserTarget": "demo:build:development" + } + }, + "defaultConfiguration": "development", + "options": { + "port": 4200 + } + }, + "extract-i18n": { + "executor": "@angular-devkit/build-angular:extract-i18n", + "options": { + "browserTarget": "demo:build" + } + }, + "lint": { + "executor": "@nrwl/linter:eslint", + "options": { + "lintFilePatterns": [ + "packages/demo/src/**/*.ts", + "packages/demo/src/**/*.html" + ] + } + } + }, + "tags": ["scope:demo", "type:app"] +} diff --git a/packages/demo/src/app/app.component.ts b/packages/demo/src/app/app.component.ts new file mode 100644 index 000000000..cfe9ec24d --- /dev/null +++ b/packages/demo/src/app/app.component.ts @@ -0,0 +1,24 @@ +import { Component } from '@angular/core'; +import * as THREE from 'three'; + +@Component({ + selector: 'ngt-root', + template: ` + + + + + + + + `, +}) +export class AppComponent { + title = 'demo'; + + onAnimateReady(mesh: THREE.Mesh) { + mesh.rotation.x = mesh.rotation.y += 0.01; + } +} diff --git a/packages/demo/src/app/app.module.ts b/packages/demo/src/app/app.module.ts new file mode 100644 index 000000000..a03dbedcd --- /dev/null +++ b/packages/demo/src/app/app.module.ts @@ -0,0 +1,24 @@ +import { NgtCoreModule } from '@angular-three/core'; +import { NgtBoxGeometryModule } from '@angular-three/core/geometries'; +import { NgtMeshBasicMaterialModule } from '@angular-three/core/materials'; +import { NgtMeshModule } from '@angular-three/core/meshes'; +import { NgtStatsModule } from '@angular-three/core/stats'; +import { NgModule } from '@angular/core'; +import { BrowserModule } from '@angular/platform-browser'; + +import { AppComponent } from './app.component'; + +@NgModule({ + declarations: [AppComponent], + imports: [ + BrowserModule, + NgtCoreModule, + NgtMeshModule, + NgtMeshBasicMaterialModule, + NgtBoxGeometryModule, + NgtStatsModule, + ], + providers: [], + bootstrap: [AppComponent], +}) +export class AppModule {} diff --git a/packages/demo/src/assets/.gitkeep b/packages/demo/src/assets/.gitkeep new file mode 100644 index 000000000..e69de29bb diff --git a/packages/demo/src/environments/environment.prod.ts b/packages/demo/src/environments/environment.prod.ts new file mode 100644 index 000000000..c9669790b --- /dev/null +++ b/packages/demo/src/environments/environment.prod.ts @@ -0,0 +1,3 @@ +export const environment = { + production: true, +}; diff --git a/packages/demo/src/environments/environment.ts b/packages/demo/src/environments/environment.ts new file mode 100644 index 000000000..66998ae9a --- /dev/null +++ b/packages/demo/src/environments/environment.ts @@ -0,0 +1,16 @@ +// This file can be replaced during build by using the `fileReplacements` array. +// `ng build` replaces `environment.ts` with `environment.prod.ts`. +// The list of file replacements can be found in `angular.json`. + +export const environment = { + production: false, +}; + +/* + * For easier debugging in development mode, you can import the following file + * to ignore zone related error stack frames such as `zone.run`, `zoneDelegate.invokeTask`. + * + * This import should be commented out in production mode because it will have a negative impact + * on performance if an error is thrown. + */ +// import 'zone.js/plugins/zone-error'; // Included with Angular CLI. diff --git a/packages/demo/src/favicon.ico b/packages/demo/src/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..317ebcb2336e0833a22dddf0ab287849f26fda57 GIT binary patch literal 15086 zcmeI332;U^%p|z7g|#(P)qFEA@4f!_@qOK2 z_lJl}!lhL!VT_U|uN7%8B2iKH??xhDa;*`g{yjTFWHvXn;2s{4R7kH|pKGdy(7z!K zgftM+Ku7~24TLlh(!g)gz|foI94G^t2^IO$uvX$3(OR0<_5L2sB)lMAMy|+`xodJ{ z_Uh_1m)~h?a;2W{dmhM;u!YGo=)OdmId_B<%^V^{ovI@y`7^g1_V9G}*f# zNzAtvou}I!W1#{M^@ROc(BZ! z+F!!_aR&Px3_reO(EW+TwlW~tv*2zr?iP7(d~a~yA|@*a89IUke+c472NXM0wiX{- zl`UrZC^1XYyf%1u)-Y)jj9;MZ!SLfd2Hl?o|80Su%Z?To_=^g_Jt0oa#CT*tjx>BI z16wec&AOWNK<#i0Qd=1O$fymLRoUR*%;h@*@v7}wApDl^w*h}!sYq%kw+DKDY)@&A z@9$ULEB3qkR#85`lb8#WZw=@})#kQig9oqy^I$dj&k4jU&^2(M3q{n1AKeGUKPFbr z1^<)aH;VsG@J|B&l>UtU#Ejv3GIqERzYgL@UOAWtW<{p#zy`WyJgpCy8$c_e%wYJL zyGHRRx38)HyjU3y{-4z6)pzb>&Q1pR)B&u01F-|&Gx4EZWK$nkUkOI|(D4UHOXg_- zw{OBf!oWQUn)Pe(=f=nt=zkmdjpO^o8ZZ9o_|4tW1ni+Un9iCW47*-ut$KQOww!;u z`0q)$s6IZO!~9$e_P9X!hqLxu`fpcL|2f^I5d4*a@Dq28;@2271v_N+5HqYZ>x;&O z05*7JT)mUe&%S0@UD)@&8SmQrMtsDfZT;fkdA!r(S=}Oz>iP)w=W508=Rc#nNn7ym z1;42c|8($ALY8#a({%1#IXbWn9-Y|0eDY$_L&j{63?{?AH{);EzcqfydD$@-B`Y3<%IIj7S7rK_N}je^=dEk%JQ4c z!tBdTPE3Tse;oYF>cnrapWq*o)m47X1`~6@(!Y29#>-#8zm&LXrXa(3=7Z)ElaQqj z-#0JJy3Fi(C#Rx(`=VXtJ63E2_bZGCz+QRa{W0e2(m3sI?LOcUBx)~^YCqZ{XEPX)C>G>U4tfqeH8L(3|pQR*zbL1 zT9e~4Tb5p9_G}$y4t`i*4t_Mr9QYvL9C&Ah*}t`q*}S+VYh0M6GxTTSXI)hMpMpIq zD1ImYqJLzbj0}~EpE-aH#VCH_udYEW#`P2zYmi&xSPs_{n6tBj=MY|-XrA;SGA_>y zGtU$?HXm$gYj*!N)_nQ59%lQdXtQZS3*#PC-{iB_sm+ytD*7j`D*k(P&IH2GHT}Eh z5697eQECVIGQAUe#eU2I!yI&%0CP#>%6MWV z@zS!p@+Y1i1b^QuuEF*13CuB zu69dve5k7&Wgb+^s|UB08Dr3u`h@yM0NTj4h7MnHo-4@xmyr7(*4$rpPwsCDZ@2be zRz9V^GnV;;?^Lk%ynzq&K(Aix`mWmW`^152Hoy$CTYVehpD-S1-W^#k#{0^L`V6CN+E z!w+xte;2vu4AmVNEFUOBmrBL>6MK@!O2*N|2=d|Y;oN&A&qv=qKn73lDD zI(+oJAdgv>Yr}8(&@ZuAZE%XUXmX(U!N+Z_sjL<1vjy1R+1IeHt`79fnYdOL{$ci7 z%3f0A*;Zt@ED&Gjm|OFTYBDe%bbo*xXAQsFz+Q`fVBH!N2)kaxN8P$c>sp~QXnv>b zwq=W3&Mtmih7xkR$YA)1Yi?avHNR6C99!u6fh=cL|KQ&PwF!n@ud^n(HNIImHD!h87!i*t?G|p0o+eelJ?B@A64_9%SBhNaJ64EvKgD&%LjLCYnNfc; znj?%*p@*?dq#NqcQFmmX($wms@CSAr9#>hUR^=I+=0B)vvGX%T&#h$kmX*s=^M2E!@N9#m?LhMvz}YB+kd zG~mbP|D(;{s_#;hsKK9lbVK&Lo734x7SIFJ9V_}2$@q?zm^7?*XH94w5Qae{7zOMUF z^?%F%)c1Y)Q?Iy?I>knw*8gYW#ok|2gdS=YYZLiD=CW|Nj;n^x!=S#iJ#`~Ld79+xXpVmUK^B(xO_vO!btA9y7w3L3-0j-y4 z?M-V{%z;JI`bk7yFDcP}OcCd*{Q9S5$iGA7*E1@tfkyjAi!;wP^O71cZ^Ep)qrQ)N z#wqw0_HS;T7x3y|`P==i3hEwK%|>fZ)c&@kgKO1~5<5xBSk?iZV?KI6&i72H6S9A* z=U(*e)EqEs?Oc04)V-~K5AUmh|62H4*`UAtItO$O(q5?6jj+K^oD!04r=6#dsxp?~}{`?&sXn#q2 zGuY~7>O2=!u@@Kfu7q=W*4egu@qPMRM>(eyYyaIE<|j%d=iWNdGsx%c!902v#ngNg z@#U-O_4xN$s_9?(`{>{>7~-6FgWpBpqXb`Ydc3OFL#&I}Irse9F_8R@4zSS*Y*o*B zXL?6*Aw!AfkNCgcr#*yj&p3ZDe2y>v$>FUdKIy_2N~}6AbHc7gA3`6$g@1o|dE>vz z4pl(j9;kyMsjaw}lO?(?Xg%4k!5%^t#@5n=WVc&JRa+XT$~#@rldvN3S1rEpU$;XgxVny7mki3 z-Hh|jUCHrUXuLr!)`w>wgO0N%KTB-1di>cj(x3Bav`7v z3G7EIbU$z>`Nad7Rk_&OT-W{;qg)-GXV-aJT#(ozdmnA~Rq3GQ_3mby(>q6Ocb-RgTUhTN)))x>m&eD;$J5Bg zo&DhY36Yg=J=$Z>t}RJ>o|@hAcwWzN#r(WJ52^g$lh^!63@hh+dR$&_dEGu&^CR*< z!oFqSqO@>xZ*nC2oiOd0eS*F^IL~W-rsrO`J`ej{=ou_q^_(<$&-3f^J z&L^MSYWIe{&pYq&9eGaArA~*kA + + + + Demo + + + + + + + + diff --git a/packages/demo/src/main.ts b/packages/demo/src/main.ts new file mode 100644 index 000000000..d9a2e7e4a --- /dev/null +++ b/packages/demo/src/main.ts @@ -0,0 +1,13 @@ +import { enableProdMode } from '@angular/core'; +import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; + +import { AppModule } from './app/app.module'; +import { environment } from './environments/environment'; + +if (environment.production) { + enableProdMode(); +} + +platformBrowserDynamic() + .bootstrapModule(AppModule) + .catch((err) => console.error(err)); diff --git a/packages/demo/src/polyfills.ts b/packages/demo/src/polyfills.ts new file mode 100644 index 000000000..e4555ed11 --- /dev/null +++ b/packages/demo/src/polyfills.ts @@ -0,0 +1,52 @@ +/** + * This file includes polyfills needed by Angular and is loaded before the app. + * You can add your own extra polyfills to this file. + * + * This file is divided into 2 sections: + * 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers. + * 2. Application imports. Files imported after ZoneJS that should be loaded before your main + * file. + * + * The current setup is for so-called "evergreen" browsers; the last versions of browsers that + * automatically update themselves. This includes recent versions of Safari, Chrome (including + * Opera), Edge on the desktop, and iOS and Chrome on mobile. + * + * Learn more in https://angular.io/guide/browser-support + */ + +/*************************************************************************************************** + * BROWSER POLYFILLS + */ + +/** + * By default, zone.js will patch all possible macroTask and DomEvents + * user can disable parts of macroTask/DomEvents patch by setting following flags + * because those flags need to be set before `zone.js` being loaded, and webpack + * will put import in the top of bundle, so user need to create a separate file + * in this directory (for example: zone-flags.ts), and put the following flags + * into that file, and then add the following code before importing zone.js. + * import './zone-flags'; + * + * The flags allowed in zone-flags.ts are listed here. + * + * The following flags will work for all browsers. + * + * (window as any).__Zone_disable_requestAnimationFrame = true; // disable patch requestAnimationFrame + * (window as any).__Zone_disable_on_property = true; // disable patch onProperty such as onclick + * (window as any).__zone_symbol__UNPATCHED_EVENTS = ['scroll', 'mousemove']; // disable patch specified eventNames + * + * in IE/Edge developer tools, the addEventListener will also be wrapped by zone.js + * with the following flag, it will bypass `zone.js` patch for IE/Edge + * + * (window as any).__Zone_enable_cross_context_check = true; + * + */ + +/*************************************************************************************************** + * Zone JS is required by default for Angular itself. + */ +import 'zone.js'; // Included with Angular CLI. + +/*************************************************************************************************** + * APPLICATION IMPORTS + */ diff --git a/packages/demo/src/styles.scss b/packages/demo/src/styles.scss new file mode 100644 index 000000000..303062538 --- /dev/null +++ b/packages/demo/src/styles.scss @@ -0,0 +1,6 @@ +/* You can add global styles to this file, and also import other style files */ +html, body { + height: 100%; + width: 100%; + margin: 0; +} diff --git a/packages/demo/tsconfig.app.json b/packages/demo/tsconfig.app.json new file mode 100644 index 000000000..323b7c411 --- /dev/null +++ b/packages/demo/tsconfig.app.json @@ -0,0 +1,10 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../dist/out-tsc", + "types": [] + }, + "files": ["src/main.ts", "src/polyfills.ts"], + "include": ["src/**/*.d.ts"], + "exclude": ["**/*.test.ts", "**/*.spec.ts"] +} diff --git a/packages/demo/tsconfig.editor.json b/packages/demo/tsconfig.editor.json new file mode 100644 index 000000000..7e1969db5 --- /dev/null +++ b/packages/demo/tsconfig.editor.json @@ -0,0 +1,7 @@ +{ + "extends": "./tsconfig.json", + "include": ["**/*.ts"], + "compilerOptions": { + "types": ["node"] + } +} diff --git a/packages/demo/tsconfig.json b/packages/demo/tsconfig.json new file mode 100644 index 000000000..bcc01b6af --- /dev/null +++ b/packages/demo/tsconfig.json @@ -0,0 +1,24 @@ +{ + "extends": "../../tsconfig.base.json", + "files": [], + "include": [], + "references": [ + { + "path": "./tsconfig.app.json" + }, + { + "path": "./tsconfig.editor.json" + } + ], + "compilerOptions": { + "forceConsistentCasingInFileNames": true, + "strict": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true + }, + "angularCompilerOptions": { + "strictInjectionParameters": true, + "strictInputAccessModifiers": true, + "strictTemplates": true + } +} diff --git a/tsconfig.base.json b/tsconfig.base.json index 0445f4aaa..243de7fbc 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -15,7 +15,17 @@ "skipDefaultLibCheck": true, "baseUrl": ".", "paths": { - "@angular-three/core": ["packages/core/src/index.ts"] + "@angular-three/core": ["packages/core/src/index.ts"], + "@angular-three/core/audios": ["packages/core/audios/src/index.ts"], + "@angular-three/core/cameras": ["packages/core/cameras/src/index.ts"], + "@angular-three/core/geometries": [ + "packages/core/geometries/src/index.ts" + ], + "@angular-three/core/group": ["packages/core/group/src/index.ts"], + "@angular-three/core/materials": ["packages/core/materials/src/index.ts"], + "@angular-three/core/meshes": ["packages/core/meshes/src/index.ts"], + "@angular-three/core/points": ["packages/core/points/src/index.ts"], + "@angular-three/core/stats": ["packages/core/stats/src/index.ts"] } }, "exclude": ["node_modules", "tmp"] diff --git a/workspace.json b/workspace.json index 8b9593d60..510614a6b 100644 --- a/workspace.json +++ b/workspace.json @@ -1,6 +1,7 @@ { "version": 2, "projects": { - "core": "packages/core" + "core": "packages/core", + "demo": "packages/demo" } } diff --git a/yarn.lock b/yarn.lock index 77c2ce4b2..c1a08a54c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2061,13 +2061,13 @@ dependencies: "@types/yargs-parser" "*" -"@typescript-eslint/eslint-plugin@~5.4.0": - version "5.4.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.4.0.tgz#05e711a2e7b68342661fde61bccbd1531c19521a" - integrity sha512-9/yPSBlwzsetCsGEn9j24D8vGQgJkOTr4oMLas/w886ZtzKIs1iyoqFrwsX2fqYEeUwsdBpC21gcjRGo57u0eg== +"@typescript-eslint/eslint-plugin@~5.5.0": + version "5.5.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.5.0.tgz#12d5f47f127af089b985f3a205c0e34a812f8fce" + integrity sha512-4bV6fulqbuaO9UMXU0Ia0o6z6if+kmMRW8rMRyfqXj/eGrZZRGedS4n0adeGNnjr8LKAM495hrQ7Tea52UWmQA== dependencies: - "@typescript-eslint/experimental-utils" "5.4.0" - "@typescript-eslint/scope-manager" "5.4.0" + "@typescript-eslint/experimental-utils" "5.5.0" + "@typescript-eslint/scope-manager" "5.5.0" debug "^4.3.2" functional-red-black-tree "^1.0.1" ignore "^5.1.8" @@ -2087,15 +2087,15 @@ eslint-scope "^5.1.1" eslint-utils "^3.0.0" -"@typescript-eslint/experimental-utils@5.4.0": - version "5.4.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/experimental-utils/-/experimental-utils-5.4.0.tgz#238a7418d2da3b24874ba35385eb21cc61d2a65e" - integrity sha512-Nz2JDIQUdmIGd6p33A+naQmwfkU5KVTLb/5lTk+tLVTDacZKoGQisj8UCxk7onJcrgjIvr8xWqkYI+DbI3TfXg== +"@typescript-eslint/experimental-utils@5.5.0": + version "5.5.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/experimental-utils/-/experimental-utils-5.5.0.tgz#3fe2514dc2f3cd95562206e4058435ea51df609e" + integrity sha512-kjWeeVU+4lQ1SLYErRKV5yDXbWDPkpbzTUUlfAUifPYvpX0qZlrcCZ96/6oWxt3QxtK5WVhXz+KsnwW9cIW+3A== dependencies: "@types/json-schema" "^7.0.9" - "@typescript-eslint/scope-manager" "5.4.0" - "@typescript-eslint/types" "5.4.0" - "@typescript-eslint/typescript-estree" "5.4.0" + "@typescript-eslint/scope-manager" "5.5.0" + "@typescript-eslint/types" "5.5.0" + "@typescript-eslint/typescript-estree" "5.5.0" eslint-scope "^5.1.1" eslint-utils "^3.0.0" @@ -2111,14 +2111,14 @@ eslint-scope "^5.1.1" eslint-utils "^3.0.0" -"@typescript-eslint/parser@~5.4.0": - version "5.4.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-5.4.0.tgz#3aa83ce349d66e39b84151f6d5464928044ca9e3" - integrity sha512-JoB41EmxiYpaEsRwpZEYAJ9XQURPFer8hpkIW9GiaspVLX8oqbqNM8P4EP8HOZg96yaALiLEVWllA2E8vwsIKw== +"@typescript-eslint/parser@~5.5.0": + version "5.5.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-5.5.0.tgz#a38070e225330b771074daa659118238793f7fcd" + integrity sha512-JsXBU+kgQOAgzUn2jPrLA+Rd0Y1dswOlX3hp8MuRO1hQDs6xgHtbCXEiAu7bz5hyVURxbXcA2draasMbNqrhmg== dependencies: - "@typescript-eslint/scope-manager" "5.4.0" - "@typescript-eslint/types" "5.4.0" - "@typescript-eslint/typescript-estree" "5.4.0" + "@typescript-eslint/scope-manager" "5.5.0" + "@typescript-eslint/types" "5.5.0" + "@typescript-eslint/typescript-estree" "5.5.0" debug "^4.3.2" "@typescript-eslint/scope-manager@4.33.0": @@ -2137,13 +2137,13 @@ "@typescript-eslint/types" "5.3.0" "@typescript-eslint/visitor-keys" "5.3.0" -"@typescript-eslint/scope-manager@5.4.0": - version "5.4.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-5.4.0.tgz#aaab08415f4a9cf32b870c7750ae8ba4607126a1" - integrity sha512-pRxFjYwoi8R+n+sibjgF9iUiAELU9ihPBtHzocyW8v8D8G8KeQvXTsW7+CBYIyTYsmhtNk50QPGLE3vrvhM5KA== +"@typescript-eslint/scope-manager@5.5.0": + version "5.5.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-5.5.0.tgz#2b9f3672fa6cddcb4160e7e8b49ef1fd00f83c09" + integrity sha512-0/r656RmRLo7CbN4Mdd+xZyPJ/fPCKhYdU6mnZx+8msAD8nJSP8EyCFkzbd6vNVZzZvWlMYrSNekqGrCBqFQhg== dependencies: - "@typescript-eslint/types" "5.4.0" - "@typescript-eslint/visitor-keys" "5.4.0" + "@typescript-eslint/types" "5.5.0" + "@typescript-eslint/visitor-keys" "5.5.0" "@typescript-eslint/types@4.33.0": version "4.33.0" @@ -2155,10 +2155,10 @@ resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-5.3.0.tgz#af29fd53867c2df0028c57c36a655bd7e9e05416" integrity sha512-fce5pG41/w8O6ahQEhXmMV+xuh4+GayzqEogN24EK+vECA3I6pUwKuLi5QbXO721EMitpQne5VKXofPonYlAQg== -"@typescript-eslint/types@5.4.0": - version "5.4.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-5.4.0.tgz#b1c130f4b381b77bec19696c6e3366f9781ce8f2" - integrity sha512-GjXNpmn+n1LvnttarX+sPD6+S7giO+9LxDIGlRl4wK3a7qMWALOHYuVSZpPTfEIklYjaWuMtfKdeByx0AcaThA== +"@typescript-eslint/types@5.5.0": + version "5.5.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-5.5.0.tgz#fee61ae510e84ed950a53937a2b443e078107003" + integrity sha512-OaYTqkW3GnuHxqsxxJ6KypIKd5Uw7bFiQJZRyNi1jbMJnK3Hc/DR4KwB6KJj6PBRkJJoaNwzMNv9vtTk87JhOg== "@typescript-eslint/typescript-estree@4.33.0": version "4.33.0" @@ -2186,13 +2186,13 @@ semver "^7.3.5" tsutils "^3.21.0" -"@typescript-eslint/typescript-estree@5.4.0": - version "5.4.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-5.4.0.tgz#fe524fb308973c68ebeb7428f3b64499a6ba5fc0" - integrity sha512-nhlNoBdhKuwiLMx6GrybPT3SFILm5Gij2YBdPEPFlYNFAXUJWX6QRgvi/lwVoadaQEFsizohs6aFRMqsXI2ewA== +"@typescript-eslint/typescript-estree@5.5.0": + version "5.5.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-5.5.0.tgz#12f422698c1636bd0206086bbec9844c54625ebc" + integrity sha512-pVn8btYUiYrjonhMAO0yG8lm7RApzy2L4RC7Td/mC/qFkyf6vRbGyZozoA94+w6D2Y2GRqpMoCWcwx/EUOzyoQ== dependencies: - "@typescript-eslint/types" "5.4.0" - "@typescript-eslint/visitor-keys" "5.4.0" + "@typescript-eslint/types" "5.5.0" + "@typescript-eslint/visitor-keys" "5.5.0" debug "^4.3.2" globby "^11.0.4" is-glob "^4.0.3" @@ -2215,12 +2215,12 @@ "@typescript-eslint/types" "5.3.0" eslint-visitor-keys "^3.0.0" -"@typescript-eslint/visitor-keys@5.4.0": - version "5.4.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-5.4.0.tgz#09bc28efd3621f292fe88c86eef3bf4893364c8c" - integrity sha512-PVbax7MeE7tdLfW5SA0fs8NGVVr+buMPrcj+CWYWPXsZCH8qZ1THufDzbXm1xrZ2b2PA1iENJ0sRq5fuUtvsJg== +"@typescript-eslint/visitor-keys@5.5.0": + version "5.5.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-5.5.0.tgz#4787586897b61f26068a3db5c50b3f5d254f9083" + integrity sha512-4GzJ1kRtsWzHhdM40tv0ZKHNSbkDhF0Woi/TDwVJX6UICwJItvP7ZTXbjTkCdrors7ww0sYe0t+cIKDAJwZ7Kw== dependencies: - "@typescript-eslint/types" "5.4.0" + "@typescript-eslint/types" "5.5.0" eslint-visitor-keys "^3.0.0" "@webassemblyjs/ast@1.11.1":