angular-threejs / angular-three

Angular Renderer for THREE.js
MIT License
201 stars 27 forks source link

not able to use EdgesGeometry #11

Closed comfortme closed 1 year ago

comfortme commented 1 year ago

Hi, I'm trying to replicate below three-js code with angular-three but I couldn't get it working. Am I missing something here?

Three Js Code

const geometry = new THREE.BoxGeometry();
const material = new THREE.MeshBasicMaterial({
  color: new THREE.Color("white")
});

const mesh = new THREE.Mesh(geometry, material);
scene.add(mesh);

const boxGeo = new THREE.BoxGeometry(1, 1, 1),
edgeGeo = new THREE.EdgesGeometry(boxGeo),
line = new THREE.LineSegments(
        edgeGeo,
        new THREE.LineBasicMaterial({
            color: new THREE.Color('black')
        }));
scene.add(line);

My Angular Three version:

<ngt-group>
  <ngt-mesh >
    <ngt-mesh-basic-material [color]="'lightblue'"/>
    <ngt-box-geometry />
  </ngt-mesh>

<ngt-line-segments >

  <ngt-edges-geometry >
    <ngt-box-geometry />
  </ngt-edges-geometry>

  <ngt-line-basic-material [color]="'black'"></ngt-line-basic-material>
</ngt-line-segments>

</ngt-group>

I expect to see the edges of the geometry but I only see the basic material...

image

image

nartc commented 1 year ago

Hi, thank you for the issue. I can help out here

const geometry = new THREE.BoxGeometry();
const material = new THREE.MeshBasicMaterial({
  color: new THREE.Color("white")
});

const mesh = new THREE.Mesh(geometry, material);
scene.add(mesh);

const boxGeo = new THREE.BoxGeometry(1, 1, 1),
edgeGeo = new THREE.EdgesGeometry(boxGeo),
line = new THREE.LineSegments(
        edgeGeo,
        new THREE.LineBasicMaterial({
            color: new THREE.Color('black')
        }));
scene.add(line);

This THREE.js code can be converted over to Angular Three like following

<ngt-mesh>
  <ngt-box-geometry />
  <ngt-mesh-basic-material color="white" />
</ngt-mesh>

<ngt-box-geometry #boxGeo />
<ngt-line-segments>
  <ngt-edges-geometry *args="[boxGeo]" />
  <ngt-line-basic-material color="black" />
</ngt-line-segments>

https://stackblitz.com/edit/angular-three-demo-template-kb2rzd?file=src%2Fapp%2Fscene.component.ts

comfortme commented 1 year ago

Thank you @nartc But I'm a bit confused now. Mesh and Line Segments pick up their children as arguments from the template but why do we need to feed a referance to Edges Geometry as argument manually? How can I know when I need to referance manually in the future with other components? Also if I try the same thing and feed a referance of Edges Geometry to Line Segments as an experiment, I get "Property 'eGeo' does not exist on type 'Scene'."

Doesn't work, Edges Geometry referance not registered.

  <ngt-box-geometry #boxGeo />
    <ngt-edges-geometry #eGeo *args="[boxGeo]" />
    <ngt-line-segments *args="[eGeo]">
      <ngt-line-basic-material color="black" />
    </ngt-line-segments>
nartc commented 1 year ago

This is a quirk of Angular Template and Structural Directive.

Parent / Children relationship is for things that can be attached.

For example:

<ngt-mesh>
    <!-- this is short for -->
    <!-- <ngt-box-geometry attach="geometry" /> -->
    <!-- same thing applies for material . Only Geometries and Materials get this "automatic" attach attribute -->
    <ngt-box-geometry />
    <ngt-mesh-basic-material />
</ngt-mesh>

<!-- another example without automatic attach -->
<ngt-spot-light [castShadow]="true">
    <ngt-vector2 *args="[512, 512]" attach="shadow.mapSize" />
</ngt-spot-light>

is treated as:

const mesh = new Mesh();
const geometry = new BoxGeometry();
mesh.geometry = geometry;
const material = new MeshBasicMaterial();
mesh.material = material;

const spotLight = new SpotLight();
spotLight.castShadow = true;

const mapSize = new Vector2(512, 512);
spotLight.shadow.mapSize.clone(mapSize); // clone, set, setScalar ... will be called appropriately by Angular Three renderer

Parent / Children relationship never sets things as Constructor Arguments.

comfortme commented 1 year ago

Ahh ok, I get it now.

But in your solution, if I try to change the size of the geometry like below, I get "Property 'boxGeo' does not exist on type 'Scene'." because of this quirk. So how can I change the size of the geometry?

<ngt-box-geometry #boxGeo *args="[width, height, depth]" />

nartc commented 1 year ago

Yeah we need another approach for that. I’ll get back as soon as I’m on my laptop

nartc commented 1 year ago

Alright, so I'll try to explain why the following doesn't work and what options we have.

<ngt-box-geometry #boxGeo />
<ngt-edges-geometry #eGeo *args="[boxGeo]" />
<ngt-line-segments *args="[eGeo]">
    <ngt-line-basic-material color="black" />
</ngt-line-segments>

The above template is expanded into

<ngt-box-geometry #boxGeo />
<ng-template [args]="[boxGeo]">
    <ngt-edges-geometry #eGeo />
</ng-template>
<ng-template [args]="[eGeo]">
    <ngt-line-segments>
        <ngt-line-basic-material color="black" />
    </ngt-line-segments>
</ng-template>

This is how Structural Directive works in Angular. They need an ng-template, the * syntax is syntactic sugar for ng-template. With that info, we can see that eGeo variable isn't in the same scope as <ng-template [args]="[eGeo]">

We have two options:

  1. Declare a BoxGeometry in our Typescript class if we do not necessarily need it to be on the template
@Component({/* ... */})
export class SomeComponent {
    boxGeom = new THREE.BoxGeometry(2, 2, 2);
}
<ngt-line-segments>
    <ngt-edges-geometry *args="[boxGeom]" />
    <ngt-line-basic-material color="black" />
</ngt-line-segments>
  1. We can use injectNgtRef() to create a reference so we can pass that reference around. This approach allows you to keep ngt-box-geometry declaratively on the template
@Component({ /* ... */ })
export class SomeComponent {
    readonly boxRef = injectNgtRef<THREE.BoxGeometry>();
    readonly edgesRef = injectNgtRef<THREE.EdgesGeometry>();
}
<!--                                  👇 assign boxRef to [ref] on ngt-box-geometry -->
<!-- This tells Angular Three: "hey, after you create BoxGeometry with [2, 2, 2], assign that instance to boxRef -->
<ngt-box-geometry *args="[2, 2, 2]" [ref]="boxRef" />
<!--                          👇 use boxRef.nativeElement -->
<ngt-edges-geometry *args="[boxRef.nativeElement]" [ref]="edgesRef" />
<!--                          👇 use edgesRef.nativeElement -->
<ngt-line-segments *args="[edgesRef.nativeElement]">
    <ngt-line-basic-material color="black" />
</ngt-line-segments>

Updated Stackblitz: https://stackblitz.com/edit/angular-three-demo-template-kb2rzd?file=src%2Fapp%2Fscene.component.ts

comfortme commented 1 year ago

Ahh! I got so close figuring it out. I tried viewChild method to create a reference but failed somehow :) Anyway thank you for great explanation and solution!

comfortme commented 2 months ago

Hi again Chau, How do we grab references to the objects with *args now since injectNgtRef is not available anymore?

nartc commented 2 months ago

@comfortme with viewChild

<ngt-box-geometry *args="[2, 2, 2]" #box />

<ngt-edges-geometry *args="[boxRef()?.nativeElement ?? null]" #edges />

<ngt-line-segments *args="[edgesRef()?.nativeElement ?? null]">
    <ngt-line-basic-material color="black" />
</ngt-line-segments>
export class MyCmp {
  boxRef = viewChild<ElementRef<BoxGeometry>>('box');
  edgesRef = viewChild<ElementRef<EdgesGeometry>>('edges');
}